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::atomic::{AtomicU64, Ordering as AtomicOrdering};
30use std::sync::{Arc, Mutex};
31
32use crate::ast::Sexp;
33use crate::error::{LispError, MacroDefHead, Result, TemplateInvariantKind, UnquoteForm};
34
35/// Default ceiling on `Expander::expand`'s recursive re-entry into a
36/// macro-call form. A macro whose expansion contains another call to
37/// itself — the canonical runaway `(defmacro loop (x) `(loop ,x))` —
38/// pre-lift stack-overflowed the process (a below-floor abort with no
39/// `Result` witness). The ceiling turns that abort into a
40/// [`LispError::ExpansionDepthExceeded`] rejection at the expander
41/// boundary. `256` is large enough that no lawful hand-authored
42/// expansion nesting hits it (nested `when` / `let` / template
43/// compositions cap out in single digits in practice) and small
44/// enough that a runaway is rejected in microseconds instead of
45/// consuming a process stack. Consumers that need a tighter or looser
46/// ceiling call [`Expander::set_max_expansion_depth`].
47pub const DEFAULT_MAX_EXPANSION_DEPTH: usize = 256;
48
49/// Default ceiling on the expansion cache's entry count. Peer to
50/// [`DEFAULT_MAX_EXPANSION_DEPTH`] one RESOURCE axis over on the
51/// expander surface — where the depth ceiling bounds RECURSION (a
52/// runaway macro's stack), this ceiling bounds MEMORY (the memoized
53/// `apply(macro, args)` result table). Pre-lift the cache was an
54/// unbounded [`HashMap`] shared across [`Expander`] clones via
55/// [`std::sync::Arc`]; a long-running host (REPL, LSP,
56/// `tatara-check`, MCP server) that expanded many distinct
57/// `(macro, args)` pairs would accrete cache entries without bound —
58/// the same below-floor resource-drift the depth ceiling closed on
59/// the recursion axis. Post-lift the [`Expander::apply`] insert path
60/// consults this ceiling before growing the cache; once the entry
61/// count reaches the ceiling, new expansions still succeed (the
62/// cache remains a pure PERFORMANCE optimization — never a
63/// CORRECTNESS gate) but skip cache insertion until the operator
64/// clears the cache via [`Expander::clear_cache`]. Consumers that
65/// need a tighter ceiling (embedded / test harnesses) call
66/// [`Expander::set_max_cache_entries`] with a lower cap; consumers
67/// that want unbounded caching (batch compilation runs where memory
68/// is not the concern) call it with [`usize::MAX`].
69///
70/// `8192` is large enough that lawful macro authoring — a small
71/// closed set of macros, each expanded against a bounded set of
72/// argument shapes — never brushes the ceiling on a session-length
73/// workload (a typescape with 20 macros × 32 distinct arg-shapes ×
74/// several call sites still sits under 1K entries), and small
75/// enough that a pathological expansion loop (a macro emitting
76/// unique arg-shapes on every call) is rejected before it exhausts
77/// process memory. The default is deliberately generous — the
78/// ceiling exists to prevent unbounded drift, not to force operators
79/// to tune it. Consumers that want a tighter policy configure it
80/// explicitly.
81pub const DEFAULT_MAX_CACHE_ENTRIES: usize = 8192;
82
83/// Default ceiling on a single macro-expansion output's structural
84/// node count. Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`] and
85/// [`DEFAULT_MAX_CACHE_ENTRIES`] one RESOURCE-DIMENSION axis over on
86/// the expander surface — where the depth ceiling bounds RECURSION
87/// LENGTH (a runaway macro's stack) and the cache ceiling bounds
88/// MEMOIZATION WIDTH (the memoized `apply(macro, args)` table's
89/// entry count), this ceiling bounds OUTPUT SIZE (the "expansion
90/// bomb" axis: a well-defined macro whose depth stays small while
91/// the produced tree grows past any reasonable working-set budget).
92/// Pre-lift the canonical
93/// `(defmacro bomb (x) `(list ,x ,x ,x ,x ,x ,x ,x ,x))` composed
94/// under itself a handful of times exhausted the process heap while
95/// the depth counter was still at low single digits and the cache
96/// entry count sat under any reasonable ceiling — a below-floor
97/// resource-drift the depth AND cache ceilings both admitted through.
98/// Post-lift the [`Expander::expand_with_depth`] site consults this
99/// ceiling on every freshly-applied macro output via
100/// [`crate::ast::Sexp::node_count`]; once an `apply` result exceeds
101/// the ceiling the expander returns
102/// [`crate::error::LispError::ExpansionSizeExceeded`] with
103/// `macro_name`, `size`, and `limit` populated. Consumers that need
104/// a tighter or looser ceiling call
105/// [`Expander::set_max_expansion_size`].
106///
107/// `65_536` is large enough that lawful macro-authored output — a
108/// domain-language body composed from a few dozen typed primitives,
109/// each expanded against a bounded set of arg-shapes — never brushes
110/// the ceiling on a session-length workload (a typical `defpoint` /
111/// `defmonitor` expansion sits under 100 nodes, and even a
112/// hand-authored `defcheck` cascade tops out in the low thousands),
113/// and small enough that a pathological "output bomb" (a macro
114/// producing a 2^N-node blob from a small input) is rejected in
115/// milliseconds instead of consuming multiple gigabytes of process
116/// memory in the worst case. The default is deliberately generous —
117/// the ceiling exists to prevent unbounded drift, not to force
118/// operators to tune it. Consumers that want a tighter policy
119/// configure it explicitly.
120pub const DEFAULT_MAX_EXPANSION_SIZE: usize = 65_536;
121
122/// Default ceiling on a registered macro's BODY structural node count.
123/// Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`], [`DEFAULT_MAX_CACHE_ENTRIES`],
124/// and [`DEFAULT_MAX_EXPANSION_SIZE`] one PIPELINE-STAGE axis over on
125/// the expander surface — the three prior guards close the RESOURCE
126/// surface at EXPAND time (recursion length, memoization width,
127/// single-`apply` output size); this ceiling closes it at REGISTER time
128/// (the SIZE the body itself contributes to storage in
129/// [`Expander::macros`], to [`compile_template`]'s pre-expansion walk,
130/// and to every subsequent [`substitute`] walk that recurses through
131/// the body-shaped template). Pre-lift a pathological
132/// `(defmacro huge (x) `<template-of-N-million-nodes>)` would land the
133/// body in the macros table before any `apply` ran and inflate the
134/// per-registration cost proportional to `N` — a below-floor
135/// resource-drift the three expand-time ceilings all admitted through
136/// because none of them fires at the registration boundary. Post-lift
137/// the [`Expander::register_macro_def`] site consults this ceiling
138/// against `def.body.node_count()`; once the observed size exceeds
139/// the ceiling the expander returns
140/// [`crate::error::LispError::MacroBodySizeExceeded`] with
141/// `macro_name`, `size`, and `limit` populated, and BOTH the
142/// [`Expander::macros`] AND [`Expander::templates`] tables stay
143/// exactly as they were — no partial-write window in which the
144/// registration succeeded halfway.
145///
146/// `16_384` is large enough that lawful hand-authored macro bodies
147/// (an outer quasi-quote wrapping a few dozen nested lists with a
148/// handful of substitution slots) never brush the ceiling on any
149/// realistic authoring workload — even a heavily-composed
150/// `defalertpolicy` template with multi-page cascades tops out in the
151/// low thousands — and small enough that a pathological "authoring
152/// bomb" (a code-generator that unrolls a `Vec<TypedEntity>` into a
153/// giant literal body) is rejected at the registration boundary in
154/// microseconds instead of leaking a multi-megabyte template into the
155/// live macros table. The default is deliberately generous — the
156/// ceiling exists to prevent unbounded drift, not to force operators
157/// to tune it. Consumers that want a tighter or looser ceiling call
158/// [`Expander::set_max_macro_body_size`].
159pub const DEFAULT_MAX_MACRO_BODY_SIZE: usize = 16_384;
160
161/// Default ceiling on the [`Expander::macros`] table's entry count.
162/// Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one RESOURCE-DIMENSION axis
163/// over on the REGISTER-time surface — where the body-size ceiling
164/// bounds a single macro's BODY SIZE (the per-registration authoring
165/// bomb), this ceiling bounds TABLE ENTRY COUNT (the cumulative
166/// registration bomb: a code-generator emitting unbounded fresh
167/// `(defmacro N (x) `(list ,x))` heads whose bodies each sit
168/// comfortably under [`DEFAULT_MAX_MACRO_BODY_SIZE`] yet collectively
169/// saturate process memory). Also peer to [`DEFAULT_MAX_CACHE_ENTRIES`]
170/// one PIPELINE-STAGE axis over — where the cache-entries ceiling
171/// bounds EXPAND-time memoization width, this ceiling bounds
172/// REGISTER-time macro-table width. The two entry-count guards close
173/// the two pipeline stages symmetrically. Pre-lift the macros table
174/// was an unbounded [`HashMap`]; a long-running host (REPL, LSP,
175/// `tatara-check`, MCP server) that ingested a source stream of
176/// distinct `(defmacro …)` heads would accrete entries without
177/// bound — a below-floor resource-drift the four prior guards (depth,
178/// cache-entries, expansion-size, body-size) all admitted through
179/// because none of them fires against the CUMULATIVE registration
180/// count.
181///
182/// Overwrites (a re-registration of an already-registered key) do not
183/// grow the table and never trigger this ceiling — operators can
184/// redefine a macro at capacity without hitting the gate. Only FRESH
185/// keys are counted against the ceiling.
186///
187/// `4096` is large enough that lawful hand-authored typescapes never
188/// brush the ceiling on any realistic authoring workload — a
189/// typescape with 20-100 macros × several call sites sits well under
190/// 1K entries, and even a library-heavy project composing hundreds of
191/// domain macros stays under a fraction of the default — and small
192/// enough that a pathological registration bomb (a code-generator
193/// unrolling a `Vec<TypedEntity>` into a giant `(defmacro fresh-N …)`
194/// stream) is rejected at the registration boundary in microseconds
195/// instead of leaking a multi-megabyte macros table into the live
196/// expander. The default is deliberately generous — the ceiling
197/// exists to prevent unbounded drift, not to force operators to tune
198/// it. Consumers that want a tighter or looser ceiling call
199/// [`Expander::set_max_registered_macros`].
200pub const DEFAULT_MAX_REGISTERED_MACROS: usize = 4096;
201
202/// Default ceiling on a registered macro's lambda-list ARITY — the
203/// count of parameter slots (`required` + `optional` + `rest?`) the
204/// [`MacroDef`] declares. Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one
205/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
206/// body-size bounds a per-registration BODY node count (the AST-nodes
207/// of the template a macro rewrites INTO), this ceiling bounds a
208/// per-registration PARAM slot count (the fresh symbols
209/// [`compile_template`]'s `,name`-index resolution AND
210/// [`MacroParams::bind`]'s per-index binder walk have to thread through
211/// on every call). Peer to [`DEFAULT_MAX_REGISTERED_MACROS`] one
212/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
213/// the registered-macros ceiling bounds cumulative registration COUNT
214/// (the table-scoped resource), this ceiling bounds a per-registration
215/// PARAM-LIST WIDTH (the per-entry resource). Together the three
216/// REGISTER-time ceilings close the (per-body SIZE, per-body ARITY,
217/// per-table COUNT) three-corner surface — a `Vec<TypedEntity>`
218/// arity-bomb code-generator emitting a
219/// `(defmacro huge (a-1 a-2 … a-N-million) `,a-1)` head with a
220/// single-node body slips past `max_macro_body_size` and past
221/// `max_registered_macros` (one fresh key), yet each subsequent call
222/// pays O(N) walking the required-run binder — a below-floor
223/// resource-drift the two prior REGISTER-time guards admit through
224/// because neither of them fires against the PER-REGISTRATION
225/// PARAM-LIST WIDTH.
226///
227/// The default is deliberately generous — lawful hand-authored macros
228/// carry a handful of required params + a small optional run + at
229/// most a single `&rest` slot (`observability-stack`'s `defalertpolicy`
230/// carries under 8 slots; even keyword-heavy authoring surfaces sit
231/// well under 32 slots). `128` sits an order of magnitude past any
232/// realistic authoring workload yet an order of magnitude BELOW a
233/// pathological code-generator's arity-bomb output, so the ceiling
234/// gates the drift without brushing lawful traffic. Consumers that
235/// want a tighter or looser ceiling call
236/// [`Expander::set_max_macro_arity`].
237///
238/// Frontier inspiration: Common Lisp's `LAMBDA-PARAMETERS-LIMIT`
239/// standard variable (CLHS §3.4.1) — the runtime-reflectable
240/// "upper exclusive bound on the number of parameters that may
241/// appear in a lambda list" every conforming implementation carries.
242/// SBCL's is `4611686018427387903`, ECL's is `4096`, CLisp's is `4096`.
243/// `DEFAULT_MAX_MACRO_ARITY` is the substrate's typed-Rust peer at
244/// compile time, translated through pleme-io primitives: a
245/// `pub const usize` on the typed macro-expander algebra rather than
246/// a runtime-mutable global, wired into the substrate's typed
247/// [`crate::error::LispError`] Result algebra at
248/// [`Expander::register_macro_def`] rather than raised as an
249/// implementation-defined condition.
250pub const DEFAULT_MAX_MACRO_ARITY: usize = 128;
251
252/// Typed snapshot of the six `Expander` resource ceilings — the
253/// bundled peer of the six [`Expander::max_expansion_depth`] /
254/// [`Expander::max_cache_entries`] / [`Expander::max_expansion_size`] /
255/// [`Expander::max_macro_body_size`] / [`Expander::max_registered_macros`] /
256/// [`Expander::max_macro_arity`] individual getters. The six knobs form
257/// ONE resource surface (the (PIPELINE-STAGE × RESOURCE-DIMENSION)
258/// grid the last six commits closed corner-by-corner) yet the
259/// pre-lift `Expander` surface exposed them only as SIX independent
260/// projections — inspecting the full posture required six calls, and
261/// bulk-configuring the expander (a preset for CI, a preset for the
262/// REPL, a preset for a test harness) required six independent
263/// setter invocations whose ordering the type system did not gate.
264///
265/// Post-lift the typed bundle binds the six knobs at ONE typed value
266/// on the `Expander` surface. A `#[derive(Clone, Copy, Debug,
267/// PartialEq, Eq)]` posture — every ceiling is a `Copy` scalar —
268/// gives callers a cheap snapshot they can serialize, compare, or
269/// pass around; the `Copy` implementation guarantees the bundle
270/// never carries a hidden allocation. Adding a SEVENTH ceiling
271/// extends this struct AND [`DEFAULT_RESOURCE_LIMITS`] in lockstep
272/// via rustc's field-exhaustiveness on the `..Default::default()`-free
273/// literal construction — no independent-field-drift window.
274///
275/// Peer of the six `DEFAULT_MAX_*` module constants one AGGREGATION
276/// axis over: where each constant carries ONE ceiling, this struct
277/// carries the SIX-fold cross-product. [`DEFAULT_RESOURCE_LIMITS`]
278/// pins the (defaults × aggregation) corner so a consumer that wants
279/// the shipped posture in one value does not re-derive the six
280/// individual constants at its call site.
281///
282/// Theory grounding: THEORY.md §II.1 — typed entry / typed exit for
283/// the resource-limit configuration surface. The pre-lift six
284/// independent knobs were a set of typed scalars but their
285/// composition into an "Expander resource posture" was untyped —
286/// callers assembled the posture at their call site with no
287/// compile-time exhaustiveness check that all six were considered.
288/// Frontier inspiration: `tokio::runtime::Builder`'s bundled knob
289/// struct — a runtime's ceiling posture is ONE typed value rather
290/// than a chain of independent method calls; translation through
291/// pleme-io primitives is the plain `Copy` snapshot below, no
292/// builder-pattern indirection.
293#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294pub struct ResourceLimits {
295 /// See [`Expander::max_expansion_depth`].
296 pub max_expansion_depth: usize,
297 /// See [`Expander::max_cache_entries`].
298 pub max_cache_entries: usize,
299 /// See [`Expander::max_expansion_size`].
300 pub max_expansion_size: usize,
301 /// See [`Expander::max_macro_body_size`].
302 pub max_macro_body_size: usize,
303 /// See [`Expander::max_registered_macros`].
304 pub max_registered_macros: usize,
305 /// See [`Expander::max_macro_arity`].
306 pub max_macro_arity: usize,
307}
308
309impl Default for ResourceLimits {
310 /// The shipped posture — every field seeded from its matching
311 /// `DEFAULT_MAX_*` module constant. Pinned equal to
312 /// [`DEFAULT_RESOURCE_LIMITS`] as a compile-time identity so a
313 /// future re-tuning of ONE default lands in ONE place and both
314 /// projections pick it up mechanically.
315 fn default() -> Self {
316 DEFAULT_RESOURCE_LIMITS
317 }
318}
319
320/// The shipped [`ResourceLimits`] posture — every field bound to its
321/// matching `DEFAULT_MAX_*` module constant at ONE typed value. This
322/// is the `const` peer of [`ResourceLimits::default`]; the two are
323/// pinned equal so a call-site that wants the shipped posture as a
324/// compile-time value binds through the `const` and a call-site that
325/// wants it as a runtime `Default::default()` value binds through the
326/// trait impl — both routes reach the SAME six numbers.
327///
328/// Adding a SEVENTH ceiling extends this constant AND the struct AND
329/// [`Expander`]'s constructors in lockstep — rustc's
330/// field-exhaustiveness on this `..Default::default()`-free literal
331/// forces the new field to appear here, and the trait impl above
332/// reuses this constant so no independent-default-drift window opens
333/// between the two projections.
334pub const DEFAULT_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
335 max_expansion_depth: DEFAULT_MAX_EXPANSION_DEPTH,
336 max_cache_entries: DEFAULT_MAX_CACHE_ENTRIES,
337 max_expansion_size: DEFAULT_MAX_EXPANSION_SIZE,
338 max_macro_body_size: DEFAULT_MAX_MACRO_BODY_SIZE,
339 max_registered_macros: DEFAULT_MAX_REGISTERED_MACROS,
340 max_macro_arity: DEFAULT_MAX_MACRO_ARITY,
341};
342
343/// The unbounded [`ResourceLimits`] posture — every ceiling seeded to
344/// [`usize::MAX`] at ONE typed value. This is the CEILING-LIFTED peer of
345/// [`DEFAULT_RESOURCE_LIMITS`] one PRESET-POSTURE axis over on the
346/// named-preset surface: where the default constant binds each field to
347/// the shipped `DEFAULT_MAX_*` module constant, this constant binds
348/// each field to the raw `usize::MAX` sentinel that every
349/// [`Expander::set_max_expansion_depth`] / [`Expander::set_max_cache_entries`] /
350/// [`Expander::set_max_expansion_size`] / [`Expander::set_max_macro_body_size`] /
351/// [`Expander::set_max_registered_macros`] / [`Expander::set_max_macro_arity`]
352/// setter's doc names as "the ceiling is effectively lifted" —
353/// admitting any lawful value the type system supports.
354///
355/// A test-harness fixture that wants "exercise the raw expander with no
356/// ceiling gating anything" pre-lift composed the posture at its call
357/// site as six independent [`usize::MAX`] literals (or six independent
358/// setter invocations post-`Expander::new`) whose exhaustiveness the
359/// type system did NOT gate — a copy-paste that dropped one of the six
360/// lifts left THAT ceiling at its default and gated the fixture
361/// silently. Post-lift the posture is ONE named typed constant every
362/// such fixture routes through, AND rustc's field-exhaustiveness on
363/// the `..Default::default()`-free literal here forces a SEVENTH
364/// ceiling extension to appear at this site in lockstep with
365/// [`DEFAULT_RESOURCE_LIMITS`] and the [`ResourceLimits`] struct
366/// itself — no independent-preset-drift window.
367///
368/// Peer to [`DEFAULT_RESOURCE_LIMITS`] on the (posture × aggregation)
369/// grid: both constants live at the AGGREGATION corner (six ceilings
370/// as ONE typed value); the posture axis distinguishes the SHIPPED
371/// default from the CEILING-LIFTED unbounded preset. Disagrees with
372/// [`DEFAULT_RESOURCE_LIMITS`] on every field (every default is a
373/// concrete non-[`usize::MAX`] positive constant, so the six-way
374/// disagreement is structurally exhaustive) — pinned as a typed
375/// theorem in the [`ResourceLimits`] test cohort.
376///
377/// Frontier inspiration: `tokio::runtime::Builder`'s pattern of
378/// exposing named preset thunks (`multi_thread`, `current_thread`)
379/// alongside the field-level configurators, so a caller that wants a
380/// standard posture binds through ONE named entry rather than
381/// composing the field literals at the call site. Translation
382/// through pleme-io primitives is the plain `const` snapshot below,
383/// no builder-pattern indirection — the `Copy` posture on
384/// [`ResourceLimits`] already gives callers the struct-update
385/// literal (`ResourceLimits { max_expansion_depth: 4,
386/// ..UNBOUNDED_RESOURCE_LIMITS }`) that isolates the ONE ceiling
387/// still gating from the five lifted.
388pub const UNBOUNDED_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
389 max_expansion_depth: usize::MAX,
390 max_cache_entries: usize::MAX,
391 max_expansion_size: usize::MAX,
392 max_macro_body_size: usize::MAX,
393 max_registered_macros: usize::MAX,
394 max_macro_arity: usize::MAX,
395};
396
397/// The zero [`ResourceLimits`] posture — every ceiling seeded to `0` at
398/// ONE typed value. This is the BOTTOM peer of
399/// [`UNBOUNDED_RESOURCE_LIMITS`] one LATTICE-POLE axis over on the
400/// (bottom, top) bounded-lattice preset-pair surface: where the
401/// ceiling-lifted preset carries every gate at [`usize::MAX`]
402/// (admitting every lawful `usize`-indexed value the type system
403/// supports), this preset carries every gate at `0` (rejecting every
404/// non-empty input past the first depth step, cache slot, macro-body
405/// node, macro registration, or arity slot). Together the two close
406/// the (BOTTOM, TOP) preset-posture pair on the bounded-lattice
407/// diagonal, and [`DEFAULT_RESOURCE_LIMITS`] sits strictly between
408/// them (`EMPTY.leq(DEFAULT) && DEFAULT.leq(UNBOUNDED)`).
409///
410/// **Bounded-lattice identity elements**: [`ResourceLimits`] under the
411/// pointwise `min` / `max` operations forms a BOUNDED lattice; each
412/// operation acquires its identity element from the OPPOSITE pole of
413/// the preset-pair diagonal, and its ANNIHILATOR from the SAME pole.
414/// * [`Self::strictest`] (meet, pointwise `min`) —
415/// [`UNBOUNDED_RESOURCE_LIMITS`] is the identity
416/// (`a.strictest(UNBOUNDED) == a` for every posture, since pointwise
417/// `min` against [`usize::MAX`] returns `a`); this constant is the
418/// ANNIHILATOR (`a.strictest(EMPTY) == EMPTY` for every posture,
419/// since pointwise `min` against `0` returns `0`).
420/// * [`Self::most_permissive`] (join, pointwise `max`) — this
421/// constant is the identity (`a.most_permissive(EMPTY) == a` for
422/// every posture, since pointwise `max` against `0` returns `a`);
423/// [`UNBOUNDED_RESOURCE_LIMITS`] is the ANNIHILATOR
424/// (`a.most_permissive(UNBOUNDED) == UNBOUNDED`).
425/// * [`Self::leq`] (partial order) — this constant is the MINIMUM
426/// (`EMPTY.leq(a) == true` for every posture, since `0 <= x` holds
427/// for every `usize x`); [`UNBOUNDED_RESOURCE_LIMITS`] is the
428/// MAXIMUM (`a.leq(UNBOUNDED) == true` for every posture, since
429/// `x <= usize::MAX` holds for every `usize x`).
430///
431/// **Fold-identity use case**: a caller aggregating a slice of
432/// postures through [`Self::most_permissive`] (computing the pointwise
433/// least upper bound across the slice) seeds the fold from this
434/// constant — the join-identity property guarantees the seed does not
435/// distort the fold's result:
436///
437/// ```rust,ignore
438/// let joined = postures.iter().copied()
439/// .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
440/// ```
441///
442/// The dual pattern (folding through [`Self::strictest`] to compute
443/// the pointwise greatest lower bound) seeds from
444/// [`UNBOUNDED_RESOURCE_LIMITS`] — the meet-identity from the OPPOSITE
445/// pole. Pre-lift, an aggregator either (a) picked ONE arbitrary
446/// element of the slice as the seed and folded through the tail — a
447/// non-total fixup that panicked on an empty slice — or (b) seeded
448/// from `Some(first)`-style option wrapper — a runtime `Option` that
449/// carried a type-level admission the aggregate might be absent even
450/// though the algebra defines it exhaustively via its identity. Post-
451/// lift the fold takes ONE typed identity element and handles empty
452/// slices as the algebraic identity value — a boundary case which the
453/// bounded-lattice structure resolves at TYPE level rather than the
454/// caller resolving at CALL SITE with a wrapper type.
455///
456/// Adding a SEVENTH ceiling extends this constant AND the struct AND
457/// [`DEFAULT_RESOURCE_LIMITS`] AND [`UNBOUNDED_RESOURCE_LIMITS`] in
458/// lockstep — rustc's field-exhaustiveness on this
459/// `..Default::default()`-free literal forces the new field to appear
460/// at this site, and the identity-law tests pin `0` as the seventh
461/// field's value at compile time via the const-fn peer of the
462/// existing lattice-law const-fn pins.
463///
464/// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
465/// proofs; the identity element of a lattice operation is itself a
466/// typed named entry whose composition with any other element is the
467/// element (join) or the identity (meet). THEORY.md §V.1 — knowable
468/// platform; the bounded-lattice bottom binds at ONE typed constant
469/// rather than a six-primitive inline literal every consumer that
470/// needs the fold identity carries at its call site. Frontier
471/// inspiration: the algebraic-datatypes tradition of a `Monoid` typeclass
472/// (Haskell's `mempty`, Scala's `Monoid.empty`, Rust's various
473/// `Default::default` for `Sum`/`Product` newtypes) — a caller
474/// aggregating values through a binary combinator binds its identity
475/// element at ONE named entry rather than composing the identity
476/// literal at every fold site. Translation through pleme-io primitives
477/// is the plain `const` snapshot below, no typeclass indirection — the
478/// `Copy` posture on [`ResourceLimits`] already gives callers the
479/// struct-update literal (`ResourceLimits { max_macro_arity: 3,
480/// ..EMPTY_RESOURCE_LIMITS }`) that isolates the ONE ceiling admitting
481/// non-zero input from the five sealed at `0`.
482pub const EMPTY_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
483 max_expansion_depth: 0,
484 max_cache_entries: 0,
485 max_expansion_size: 0,
486 max_macro_body_size: 0,
487 max_registered_macros: 0,
488 max_macro_arity: 0,
489};
490
491/// Pointwise `min` / `max` primitives the [`ResourceLimits`] lattice binds
492/// its meet ([`ResourceLimits::strictest`]) and join
493/// ([`ResourceLimits::most_permissive`]) operations to. Named at module
494/// scope as `const fn` so both operations are themselves `const fn` on
495/// stable — composing two postures at compile time (a caller stitching a
496/// new `pub const` preset from two shipped presets) needs no runtime
497/// evaluation. Ternary form rather than [`usize::min`] / [`usize::max`]
498/// so the two `const fn` remain valid on any tatara MSRV whose stability
499/// on the primitive const-fn methods has not been established.
500const fn min_usize(a: usize, b: usize) -> usize {
501 if a <= b {
502 a
503 } else {
504 b
505 }
506}
507
508const fn max_usize(a: usize, b: usize) -> usize {
509 if a >= b {
510 a
511 } else {
512 b
513 }
514}
515
516impl ResourceLimits {
517 /// Pointwise-`min` across the six ceilings — the STRICTEST posture
518 /// whose admissible input set is the intersection of `self`'s and
519 /// `other`'s admissible input sets. `a.strictest(b)` admits an
520 /// input iff BOTH `a` AND `b` admit it: on every field the
521 /// per-axis ceiling is the smaller of the two, so a value that
522 /// clears the meet clears BOTH postures, and a value that any
523 /// posture rejects the meet also rejects.
524 ///
525 /// The meet of the `ResourceLimits` lattice under the pointwise
526 /// partial order `a ≤ b iff every field of a ≤ every field of b`
527 /// (tighter-first — a POSTURE admits FEWER inputs on that field's
528 /// axis when the ceiling is smaller). Peer of
529 /// [`Self::most_permissive`] one COMBINATOR axis over on the
530 /// lattice-algebra surface: where `most_permissive` is the join
531 /// (pointwise-`max`, LUB), this is the meet (pointwise-`min`, GLB).
532 /// Together the two operations close the (meet, join) lattice
533 /// operator pair on the [`ResourceLimits`] posture algebra.
534 ///
535 /// The concrete identity on the shipped preset pair holds
536 /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.strictest(`
537 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`DEFAULT_RESOURCE_LIMITS`].
538 /// Every `DEFAULT_MAX_*` module constant is a concrete positive
539 /// value strictly less than [`usize::MAX`], so on every axis the
540 /// pointwise-`min` picks the DEFAULT side; the six-axis pin holds
541 /// as a typed theorem in the test cohort.
542 ///
543 /// Idempotent (`a.strictest(a) == a`), commutative
544 /// (`a.strictest(b) == b.strictest(a)`), associative
545 /// (`(a.strictest(b)).strictest(c) == a.strictest(b.strictest(c))`),
546 /// and satisfies the absorption identity with the join
547 /// (`a.strictest(a.most_permissive(b)) == a`) — all pinned as
548 /// typed theorems in the test cohort. Distributive over the join
549 /// (`a.strictest(b.most_permissive(c)) ==
550 /// a.strictest(b).most_permissive(a.strictest(c))`) because
551 /// pointwise-`min` and pointwise-`max` on [`usize`] each
552 /// distribute over the other on every axis.
553 ///
554 /// `const fn` so a caller can compose two named presets into a
555 /// third at compile time (`pub const CI_RESOURCE_LIMITS:
556 /// ResourceLimits = SOME_OPERATOR_PRESET.strictest(
557 /// SOME_CI_PRESET);`) rather than deferring the composition to
558 /// a runtime `Default::default()` chain.
559 ///
560 /// Pre-lift, a caller wanting the tightest common posture across
561 /// two shipped presets composed the intersection at its call site
562 /// as six independent `usize::min(a.max_X, b.max_X)` invocations
563 /// stitched into a fresh `ResourceLimits { ... }` literal — the
564 /// same six-inline-primitive shape the pre-`ResourceLimits`
565 /// bundled `Expander` posture required its callers to carry, and
566 /// the same exhaustiveness gap the `..Default::default()`-free
567 /// literal on the bundled struct exists to close: a copy-paste
568 /// that dropped ONE of the six `min` invocations left THAT
569 /// ceiling at whichever posture the caller happened to seed the
570 /// literal with, and the type system did not gate the drop.
571 /// Post-lift the composition binds at ONE typed method on the
572 /// posture algebra — a caller writes
573 /// `operator_preset.strictest(ci_preset)` and rustc's
574 /// field-exhaustiveness at the const's construction guarantees
575 /// every ceiling is threaded through the `min` primitive.
576 ///
577 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
578 /// preserves proofs; the tightest posture of two preset-carried
579 /// resource proofs is itself a `ResourceLimits` whose proof
580 /// content is the intersection of the two, and this method is
581 /// the typed named entry composing them. THEORY.md §V.1 —
582 /// knowable platform; the pointwise-`min` combinator becomes a
583 /// TYPE-level operation on the posture algebra rather than an
584 /// inline six-primitive cascade at every consumer that composes
585 /// two presets. `tatara-lattice`'s `meet` / `join` operators on
586 /// `Classification` (see `pleme-io/tatara` workspace crate
587 /// docstring: "Lattice algebra over Classification — `meet` /
588 /// `join` / `leq` / `Baseline`") close the SAME shape one layer
589 /// down: an entity's classification is a lattice; a resource
590 /// posture is a lattice; both algebras carry the (meet, join)
591 /// operator pair as their fundamental composition primitives.
592 #[must_use]
593 pub const fn strictest(self, other: Self) -> Self {
594 Self {
595 max_expansion_depth: min_usize(self.max_expansion_depth, other.max_expansion_depth),
596 max_cache_entries: min_usize(self.max_cache_entries, other.max_cache_entries),
597 max_expansion_size: min_usize(self.max_expansion_size, other.max_expansion_size),
598 max_macro_body_size: min_usize(self.max_macro_body_size, other.max_macro_body_size),
599 max_registered_macros: min_usize(
600 self.max_registered_macros,
601 other.max_registered_macros,
602 ),
603 max_macro_arity: min_usize(self.max_macro_arity, other.max_macro_arity),
604 }
605 }
606
607 /// Pointwise-`max` across the six ceilings — the MOST-PERMISSIVE
608 /// posture whose admissible input set is the union of `self`'s
609 /// and `other`'s admissible input sets. `a.most_permissive(b)`
610 /// admits an input iff EITHER `a` OR `b` admits it: on every
611 /// field the per-axis ceiling is the larger of the two, so a
612 /// value that either posture accepts the join accepts, and a
613 /// value the join rejects both postures reject.
614 ///
615 /// The join of the `ResourceLimits` lattice under the pointwise
616 /// partial order `a ≤ b iff every field of a ≤ every field of b`
617 /// (tighter-first). Peer of [`Self::strictest`] one COMBINATOR
618 /// axis over on the lattice-algebra surface: where `strictest` is
619 /// the meet (pointwise-`min`, GLB), this is the join
620 /// (pointwise-`max`, LUB). Together the two operations close the
621 /// (meet, join) lattice operator pair on the [`ResourceLimits`]
622 /// posture algebra.
623 ///
624 /// The concrete identity on the shipped preset pair holds
625 /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.most_permissive(`
626 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`UNBOUNDED_RESOURCE_LIMITS`].
627 /// Every `DEFAULT_MAX_*` module constant is a concrete positive
628 /// value strictly less than [`usize::MAX`], so on every axis the
629 /// pointwise-`max` picks the UNBOUNDED side; the six-axis pin
630 /// holds as a typed theorem in the test cohort.
631 ///
632 /// Idempotent, commutative, associative, and satisfies the
633 /// absorption identity with the meet
634 /// (`a.most_permissive(a.strictest(b)) == a`) — all pinned as
635 /// typed theorems in the test cohort. Distributive over the meet
636 /// (`a.most_permissive(b.strictest(c)) ==
637 /// a.most_permissive(b).strictest(a.most_permissive(c))`).
638 ///
639 /// `const fn` so a caller can compose two named presets into a
640 /// third at compile time rather than deferring the composition
641 /// to a runtime `Default::default()` chain.
642 ///
643 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
644 /// preserves proofs; the most-permissive posture of two preset-
645 /// carried resource proofs is itself a `ResourceLimits` whose
646 /// proof content is the union of the two, and this method is the
647 /// typed named entry composing them. THEORY.md §V.1 — knowable
648 /// platform; the pointwise-`max` combinator becomes a TYPE-level
649 /// operation on the posture algebra rather than an inline
650 /// six-primitive cascade at every consumer that composes two
651 /// presets.
652 #[must_use]
653 pub const fn most_permissive(self, other: Self) -> Self {
654 Self {
655 max_expansion_depth: max_usize(self.max_expansion_depth, other.max_expansion_depth),
656 max_cache_entries: max_usize(self.max_cache_entries, other.max_cache_entries),
657 max_expansion_size: max_usize(self.max_expansion_size, other.max_expansion_size),
658 max_macro_body_size: max_usize(self.max_macro_body_size, other.max_macro_body_size),
659 max_registered_macros: max_usize(
660 self.max_registered_macros,
661 other.max_registered_macros,
662 ),
663 max_macro_arity: max_usize(self.max_macro_arity, other.max_macro_arity),
664 }
665 }
666
667 /// Pointwise partial-order relation across the six ceilings — `self`
668 /// is at least as tight as `other` iff every field of `self` is at
669 /// most the matching field of `other`. `a.leq(b)` holds iff every
670 /// input the `a` posture admits the `b` posture also admits: `a`'s
671 /// per-axis ceilings all sit at or below `b`'s per-axis ceilings, so
672 /// any value that clears `a`'s tighter gates also clears `b`'s
673 /// looser gates.
674 ///
675 /// The partial order underneath the [`ResourceLimits`] lattice.
676 /// Peer of [`Self::strictest`] (meet, GLB) and [`Self::most_permissive`]
677 /// (join, LUB) one PRIMITIVE-KIND axis over on the lattice-algebra
678 /// surface: where those two are the (meet, join) COMBINATORS, this
679 /// is the RELATION they are defined against. Together the three
680 /// close the (meet, join, leq) primitive triple on the
681 /// [`ResourceLimits`] posture algebra — the same triple
682 /// `tatara-lattice`'s [`Lattice`] trait exposes as its three
683 /// required methods for every classification lattice one crate over
684 /// (see `tatara-lattice/src/lib.rs`).
685 ///
686 /// **Meet-agreement** (`a.leq(b) ⇔ a.strictest(b) == a`): if `a` is
687 /// tighter on every axis than `b`, then the pointwise `min` picks
688 /// `a` on every axis, so `a.strictest(b) == a`; conversely, if
689 /// `a.strictest(b) == a`, every field's `min` picked `a`, so every
690 /// field of `a` is at most the matching field of `b`. Pinned as
691 /// `resource_limits_leq_agrees_with_meet` in the test cohort.
692 ///
693 /// **Join-agreement** (`a.leq(b) ⇔ a.most_permissive(b) == b`):
694 /// symmetric statement on the LUB side; pinned as
695 /// `resource_limits_leq_agrees_with_join`. Together the two
696 /// agreements are the canonical lattice cross-check axiom
697 /// `a ≤ b ⇔ a ⊓ b = a ⇔ a ⊔ b = b` documented in
698 /// `tatara-lattice/src/lib.rs`'s preamble.
699 ///
700 /// Reflexive (`a.leq(a) == true`), antisymmetric (`a.leq(b) &&
701 /// b.leq(a) → a == b`), transitive (`a.leq(b) && b.leq(c) →
702 /// a.leq(c)`) — all pinned as typed theorems in the test cohort.
703 /// NOT total: two postures with reversed per-axis orderings (some
704 /// axes where `a` is tighter, some where `b` is tighter) are
705 /// incomparable — `!a.leq(b) && !b.leq(a)` — pinned by
706 /// `resource_limits_leq_is_not_total_on_asymmetric_postures`.
707 ///
708 /// **Bounds pins in terms of leq**:
709 /// * `a.strictest(b).leq(a) && a.strictest(b).leq(b)` (meet is
710 /// the greatest lower bound) — pinned by
711 /// `resource_limits_strictest_is_leq_both_operands`, the typed
712 /// lattice-relation companion to the per-axis
713 /// `resource_limits_strictest_is_dominated_by_both_operands_pointwise`.
714 /// * `a.leq(a.most_permissive(b)) && b.leq(a.most_permissive(b))`
715 /// (join is the least upper bound) — pinned by
716 /// `resource_limits_most_permissive_is_geq_both_operands`, the
717 /// typed lattice-relation companion to the per-axis
718 /// `resource_limits_most_permissive_dominates_both_operands_pointwise`.
719 ///
720 /// **Concrete-preset pin**: [`DEFAULT_RESOURCE_LIMITS`]`.leq(`
721 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == true` (every
722 /// `DEFAULT_MAX_*` module constant is a concrete positive value at
723 /// most [`usize::MAX`]) AND
724 /// `!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS)` (every
725 /// `DEFAULT_MAX_*` is strictly less than [`usize::MAX`], so on
726 /// every axis [`usize::MAX`] exceeds the matching default) — the
727 /// partial order is a STRICT order on the shipped preset pair.
728 /// Pinned by `resource_limits_leq_of_default_and_unbounded_is_a_strict_order`.
729 ///
730 /// `const fn` so a caller can pin a preset-relation identity at
731 /// compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
732 /// .leq(UNBOUNDED_RESOURCE_LIMITS));`) — the typed lattice-relation
733 /// peer of the `const fn` composition ability on
734 /// [`Self::strictest`] / [`Self::most_permissive`].
735 ///
736 /// Pre-lift, a caller wanting to decide whether one posture was
737 /// tighter than another either (a) composed six independent
738 /// per-axis `<=` comparisons at the call site stitched into a
739 /// six-fold `&&` — the same six-inline-primitive shape the
740 /// pre-`ResourceLimits` bundled Expander posture required its
741 /// callers to carry, and the same exhaustiveness gap the
742 /// `..Default::default()`-free literal on the bundled struct exists
743 /// to close: a copy-paste that dropped ONE of the six comparisons
744 /// silently admitted a posture that violated tightness on THAT
745 /// axis — or (b) invoked [`Self::strictest`] and compared the result
746 /// to `self` — a correct but roundabout composition of the partial-
747 /// order primitive against its lattice-companion. Post-lift the
748 /// relation binds at ONE typed method on the posture algebra; a
749 /// caller writes `tighter.leq(looser)` and rustc's exhaustiveness
750 /// on the six-field conjunction below guarantees every ceiling is
751 /// threaded through the `<=` primitive.
752 ///
753 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
754 /// preserves proofs; the partial-order relation on two preset-
755 /// carried resource proofs is itself a typed named entry composing
756 /// their tightness ordering. THEORY.md §V.1 — knowable platform;
757 /// the pointwise `<=` combinator becomes a TYPE-level operation on
758 /// the posture algebra rather than an inline six-primitive
759 /// conjunction at every consumer that compares two presets.
760 /// `tatara-lattice`'s [`Lattice::leq`] method on `Classification`
761 /// closes the SAME shape one layer down — an entity's
762 /// classification is a lattice with a `leq` partial order; a
763 /// resource posture is a lattice with a `leq` partial order; both
764 /// algebras carry the (meet, join, leq) primitive triple as their
765 /// fundamental relation-and-combinator surface.
766 #[must_use]
767 pub const fn leq(self, other: Self) -> bool {
768 self.max_expansion_depth <= other.max_expansion_depth
769 && self.max_cache_entries <= other.max_cache_entries
770 && self.max_expansion_size <= other.max_expansion_size
771 && self.max_macro_body_size <= other.max_macro_body_size
772 && self.max_registered_macros <= other.max_registered_macros
773 && self.max_macro_arity <= other.max_macro_arity
774 }
775
776 /// Pointwise-`min` fold across a slice of postures — the STRICTEST
777 /// posture whose admissible input set is the intersection of every
778 /// operand's admissible input sets. `ResourceLimits::strictest_of(&[a,
779 /// b, c])` admits an input iff EVERY operand admits it: on every
780 /// field the per-axis ceiling is the smallest across the whole slice,
781 /// so a value that clears the aggregated meet clears EVERY operand,
782 /// and a value that ANY operand rejects the aggregated meet also
783 /// rejects.
784 ///
785 /// The N-ary MEET aggregation on the [`ResourceLimits`] lattice —
786 /// the extension of [`Self::strictest`] from a 2-input pairwise
787 /// combinator to an N-input fold across a slice. Seeded from
788 /// [`UNBOUNDED_RESOURCE_LIMITS`] (the meet-identity — its per-axis
789 /// [`usize::MAX`] never wins the pointwise `min` against a concrete
790 /// positive value, so the seed contributes nothing to the fold's
791 /// result), which makes the empty-slice case aggregate to the
792 /// identity element rather than requiring an `Option<Self>` wrapper
793 /// the caller inspects at every consumption site. Peer of
794 /// [`Self::most_permissive_of`] one COMBINATOR axis over on the
795 /// N-ary aggregation surface: where `most_permissive_of` is the
796 /// N-ary JOIN (LUB across the slice, seeded from the join-identity
797 /// [`EMPTY_RESOURCE_LIMITS`]), this is the N-ary MEET (GLB across
798 /// the slice, seeded from the meet-identity). Together the two
799 /// close the (N-ary meet, N-ary join) aggregation pair the pairwise
800 /// combinators [`Self::strictest`] / [`Self::most_permissive`]
801 /// extend from 2 to N.
802 ///
803 /// **Empty-slice identity**: `strictest_of(&[]) ==
804 /// UNBOUNDED_RESOURCE_LIMITS` — the null aggregate binds structurally
805 /// to the meet-identity element. Pre-lift a caller composing "the
806 /// tightest posture across THIS SLICE" from the pairwise `strictest`
807 /// primitive either (a) seeded the fold from a non-empty first
808 /// element and panicked on empty input, or (b) wrapped the aggregate
809 /// in `Option<Self>` — a runtime type-level admission the aggregate
810 /// might be absent even though the algebra's bounded-lattice
811 /// structure defines the null aggregate exhaustively via its
812 /// identity element. Post-lift the null aggregate is
813 /// [`UNBOUNDED_RESOURCE_LIMITS`] at the TYPE level, not a wrapper
814 /// the consumer inspects.
815 ///
816 /// **Single-element identity**: `strictest_of(&[a]) == a` — the
817 /// 1-input aggregate is the input verbatim, since
818 /// `UNBOUNDED.strictest(a) == a` for every posture `a` by the
819 /// meet-identity law.
820 ///
821 /// **Two-element identity**: `strictest_of(&[a, b]) ==
822 /// a.strictest(b)` — the 2-input aggregate reduces to the pairwise
823 /// combinator, since `UNBOUNDED.strictest(a).strictest(b) ==
824 /// a.strictest(b)`.
825 ///
826 /// **Order-independent**: `strictest_of(&[a, b, c]) ==
827 /// strictest_of(&[c, b, a])` — the fold inherits the commutativity
828 /// AND associativity of the pairwise `strictest`, so any permutation
829 /// of the slice yields the same aggregate.
830 ///
831 /// `const fn` so a caller can compose an arbitrary-length preset
832 /// slice into a third preset at compile time (`pub const
833 /// COMBINED_PRESET: ResourceLimits = ResourceLimits::strictest_of(
834 /// &[SOME_OPERATOR_PRESET, SOME_CI_PRESET, SOME_HOSTILE_PRESET]);`)
835 /// rather than deferring the composition to a runtime chain — the
836 /// N-ary const-fn peer of the pairwise const-fn on
837 /// [`Self::strictest`]. `Copy` on [`ResourceLimits`] lets the const-
838 /// fn loop index the slice by value without an explicit `.clone()`
839 /// (which const-fn would not permit anyway).
840 ///
841 /// Pre-lift, the SAME `.iter().copied().fold(UNBOUNDED, strictest)`
842 /// three-primitive cascade appeared verbatim in
843 /// `unbounded_resource_limits_seeds_strictest_fold_over_slice` AND at
844 /// every future consumer that would compose "the tightest across
845 /// this set of presets" — a PRIME DIRECTIVE ≥2 trigger, since the
846 /// fold docstring on [`UNBOUNDED_RESOURCE_LIMITS`] explicitly names
847 /// this pattern as the constant's fold-identity use case. Post-lift
848 /// the N-ary aggregation binds at ONE typed method the algebra
849 /// exposes, and every future consumer routes through ONE name whose
850 /// signature carries the identity-element choice INTO the method
851 /// (rather than the consumer having to remember which seed pairs
852 /// with which combinator at each call site).
853 ///
854 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
855 /// preserves proofs; the N-ary aggregation of N preset-carried
856 /// resource proofs is itself a `ResourceLimits` whose proof content
857 /// is the intersection of every operand's, and this method is the
858 /// typed named entry composing them. THEORY.md §V.1 — knowable
859 /// platform; the N-ary meet becomes a TYPE-level operation on the
860 /// posture algebra rather than an inline fold cascade at every
861 /// consumer that aggregates a slice of presets — with the
862 /// meet-identity seed baked in so the consumer cannot pair the join-
863 /// identity (`EMPTY`) with the meet combinator (a silent
864 /// distortion that would collapse every axis to `0`). Frontier
865 /// inspiration: the algebraic-datatypes tradition of a `Monoid`
866 /// typeclass exposing `mconcat` / `Monoid.combineAll` alongside the
867 /// pairwise `mappend` — the N-ary aggregation of a collection
868 /// through a binary combinator is a first-class named method the
869 /// typeclass carries. Translation through pleme-io primitives is
870 /// the plain `const fn` slice-fold below, no typeclass indirection
871 /// — the meet-identity element already exists as a named `pub const`
872 /// on the algebra so the fold picks it up structurally.
873 #[must_use]
874 pub const fn strictest_of(postures: &[Self]) -> Self {
875 let mut acc = UNBOUNDED_RESOURCE_LIMITS;
876 let mut i = 0;
877 while i < postures.len() {
878 acc = acc.strictest(postures[i]);
879 i += 1;
880 }
881 acc
882 }
883
884 /// Pointwise-`max` fold across a slice of postures — the MOST-
885 /// PERMISSIVE posture whose admissible input set is the union of
886 /// every operand's admissible input sets.
887 /// `ResourceLimits::most_permissive_of(&[a, b, c])` admits an input
888 /// iff ANY operand admits it: on every field the per-axis ceiling
889 /// is the largest across the whole slice.
890 ///
891 /// The N-ary JOIN aggregation on the [`ResourceLimits`] lattice —
892 /// the extension of [`Self::most_permissive`] from a 2-input
893 /// pairwise combinator to an N-input fold across a slice. Seeded
894 /// from [`EMPTY_RESOURCE_LIMITS`] (the join-identity — every field
895 /// at `0`, which never wins the pointwise `max` against any
896 /// non-zero value, so the seed contributes nothing to the fold's
897 /// result), which makes the empty-slice case aggregate to the
898 /// identity element. Peer of [`Self::strictest_of`] one COMBINATOR
899 /// axis over on the N-ary aggregation surface: where `strictest_of`
900 /// is the N-ary MEET (seeded from the meet-identity
901 /// [`UNBOUNDED_RESOURCE_LIMITS`]), this is the N-ary JOIN (seeded
902 /// from the join-identity). Together the two close the (N-ary meet,
903 /// N-ary join) aggregation pair.
904 ///
905 /// **Empty-slice identity**: `most_permissive_of(&[]) ==
906 /// EMPTY_RESOURCE_LIMITS` — the null aggregate binds structurally
907 /// to the join-identity element.
908 ///
909 /// **Single-element identity**: `most_permissive_of(&[a]) == a` —
910 /// the 1-input aggregate is the input verbatim, since
911 /// `EMPTY.most_permissive(a) == a` for every posture `a` by the
912 /// join-identity law.
913 ///
914 /// **Two-element identity**: `most_permissive_of(&[a, b]) ==
915 /// a.most_permissive(b)` — the 2-input aggregate reduces to the
916 /// pairwise combinator.
917 ///
918 /// **Order-independent**: `most_permissive_of(&[a, b, c]) ==
919 /// most_permissive_of(&[c, b, a])` — the fold inherits the
920 /// commutativity AND associativity of the pairwise
921 /// `most_permissive`.
922 ///
923 /// `const fn` so a caller can compose an arbitrary-length preset
924 /// slice at compile time — the N-ary const-fn peer of the pairwise
925 /// const-fn on [`Self::most_permissive`].
926 ///
927 /// Pre-lift, the SAME `.iter().copied().fold(EMPTY,
928 /// most_permissive)` three-primitive cascade appeared verbatim in
929 /// `empty_resource_limits_seeds_most_permissive_fold_over_slice`
930 /// AND at every future consumer that would compose "the loosest
931 /// across this set of presets" — a PRIME DIRECTIVE ≥2 trigger.
932 /// Post-lift the N-ary aggregation binds at ONE typed method with
933 /// the join-identity seed baked in so the consumer cannot pair the
934 /// meet-identity (`UNBOUNDED`) with the join combinator (a silent
935 /// distortion that would inflate every axis to [`usize::MAX`]).
936 ///
937 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
938 /// preserves proofs; the N-ary join of N preset-carried resource
939 /// proofs is itself a `ResourceLimits`. THEORY.md §V.1 — knowable
940 /// platform; the N-ary join becomes a TYPE-level operation on the
941 /// posture algebra rather than an inline fold cascade at every
942 /// consumer that aggregates a slice of presets.
943 #[must_use]
944 pub const fn most_permissive_of(postures: &[Self]) -> Self {
945 let mut acc = EMPTY_RESOURCE_LIMITS;
946 let mut i = 0;
947 while i < postures.len() {
948 acc = acc.most_permissive(postures[i]);
949 i += 1;
950 }
951 acc
952 }
953
954 /// Pointwise projection of `self` into the closed range `[lower, upper]`
955 /// — the bounded-lattice bracket combinator. `a.clamp(lower, upper)`
956 /// returns the posture whose per-axis ceiling is the input's own
957 /// ceiling raised to at least `lower`'s and then lowered to at most
958 /// `upper`'s: on every field the result is `min(max(a, lower), upper)`.
959 ///
960 /// The BRACKET operation on the [`ResourceLimits`] lattice — the
961 /// composition of [`Self::most_permissive`] with `lower` followed by
962 /// [`Self::strictest`] with `upper`. Peer of the (meet, join) pairwise
963 /// combinators one ARITY axis over (2 → 3 inputs) on the lattice-
964 /// algebra surface: where those two combine a `self` with ONE bound,
965 /// this combines a `self` with TWO bounds and closes the (unbounded-
966 /// side, bounded) row on the combinator-arity face. The bounded-lattice
967 /// analog of [`Ord::clamp`] on total orders, and the natural typed
968 /// entry a caller enforces "this posture must sit within the
969 /// `[ORG_MIN, ORG_MAX]` operator bracket" through.
970 ///
971 /// **In-range identity**: if `lower.leq(self) && self.leq(upper)` (the
972 /// input already sits within the bracket), then `a.clamp(lower, upper)
973 /// == a` — the pointwise `max(a, lower)` picks `a` on every axis (since
974 /// `lower ≤ a` per-axis), then pointwise `min(..., upper)` picks `a`
975 /// again (since `a ≤ upper` per-axis). Pinned as
976 /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`.
977 ///
978 /// **Below-lower floor**: if `self.leq(lower)` and `lower.leq(upper)`
979 /// (input tighter than the bracket floor, and floor within ceiling),
980 /// then `a.clamp(lower, upper) == lower` — the pointwise `max` lifts
981 /// every axis to `lower`, then the pointwise `min` keeps `lower` (since
982 /// `lower ≤ upper` per-axis). Pinned as
983 /// `resource_limits_clamp_of_posture_below_lower_returns_lower`.
984 ///
985 /// **Above-upper ceiling**: if `upper.leq(self)` and `lower.leq(upper)`
986 /// (input looser than the bracket ceiling), then `a.clamp(lower, upper)
987 /// == upper` — the pointwise `max` keeps `self` on every axis (since
988 /// `lower ≤ self` per-axis via transitivity through `upper`), then the
989 /// pointwise `min` lowers every axis to `upper`. Pinned as
990 /// `resource_limits_clamp_of_posture_above_upper_returns_upper`.
991 ///
992 /// **Bracket-membership** (`lower.leq(a.clamp(lower, upper)) &&
993 /// a.clamp(lower, upper).leq(upper)` for every `a` when `lower.leq(upper)`):
994 /// the result always sits within the `[lower, upper]` bracket
995 /// regardless of the input's position relative to the bracket. This is
996 /// the DEFINING contract of the clamp primitive — every consumer that
997 /// invokes it does so precisely to guarantee the result sits within
998 /// the bracket, and the type system now carries that guarantee at ONE
999 /// named entry. Pinned as
1000 /// `resource_limits_clamp_result_sits_within_the_bracket`.
1001 ///
1002 /// **Extrema-bracket identity**: `a.clamp(EMPTY_RESOURCE_LIMITS,
1003 /// UNBOUNDED_RESOURCE_LIMITS) == a` — clamping against the bounded-
1004 /// lattice extrema is the identity function, since every posture
1005 /// already sits within the widest possible bracket. This is the
1006 /// bounded-lattice cross-check with the identity elements previously
1007 /// lifted (`EMPTY_RESOURCE_LIMITS` as the leq-minimum and
1008 /// `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum). Pinned as
1009 /// `resource_limits_clamp_with_lattice_extrema_returns_the_input`.
1010 ///
1011 /// **Degenerate-bracket collapse**: `a.clamp(x, x) == x` for every `a,
1012 /// x` — a zero-width bracket collapses every input to the single point
1013 /// `x`. Pinned as
1014 /// `resource_limits_clamp_with_equal_bounds_returns_the_bound`.
1015 ///
1016 /// **Idempotence**: `a.clamp(lower, upper).clamp(lower, upper) ==
1017 /// a.clamp(lower, upper)` — re-applying the same bracket to an
1018 /// already-clamped posture returns the same result, since the clamped
1019 /// value already sits within the bracket. Pinned as
1020 /// `resource_limits_clamp_is_idempotent`.
1021 ///
1022 /// `const fn` so a caller can pre-clamp a shipped preset against an
1023 /// operator-supplied policy bracket at compile time (`pub const
1024 /// POLICY_CLAMPED: ResourceLimits = SOME_PRESET.clamp(
1025 /// ORG_MIN_PRESET, ORG_MAX_PRESET);`) — the typed bracket-combinator
1026 /// peer of the const-fn composition ability on [`Self::strictest`] /
1027 /// [`Self::most_permissive`].
1028 ///
1029 /// Pre-lift, a caller wanting to bracket a posture within a
1030 /// `[lower, upper]` range composed the two-step cascade at its call
1031 /// site as `self.most_permissive(lower).strictest(upper)` — the same
1032 /// PRIME DIRECTIVE ≥2 pattern the pairwise combinators already lifted
1033 /// one arity down from the six-inline-primitive per-axis cascade.
1034 /// The `resource_limits_clamp_agrees_with_direct_two_step_cascade`
1035 /// pin below composes the pre-lift cascade at ONE explicit assertion
1036 /// site (its sole purpose is to pin the method-vs-cascade equivalence)
1037 /// — every future consumer that enforces a
1038 /// policy bracket (an operator-supplied `[ORG_MIN, ORG_MAX]`, a
1039 /// hostile-input `[EMPTY, HOSTILE_MAX]`, a CI-time `[TEST_MIN,
1040 /// TEST_MAX]`) would compose the same cascade at its call site with
1041 /// no exhaustiveness gate on the composition order. Post-lift the
1042 /// bracket binds at ONE typed method whose signature carries the
1043 /// bounds' roles (`lower` first, `upper` second) into the method
1044 /// rather than the consumer having to remember which side pairs with
1045 /// which combinator at each call site — a copy-paste that swapped the
1046 /// two combinators would clip the input to `min(max(a, upper), lower)`
1047 /// (the WRONG bracket order that returns `lower` for every input
1048 /// looser than `lower` regardless of `upper`'s position), a silent
1049 /// distortion the type system did not gate pre-lift and now does
1050 /// through the method's parameter order.
1051 ///
1052 /// **Non-lattice-bracket behaviour** (`!lower.leq(upper)`): the four
1053 /// documented invariants above are stated for the well-formed bracket
1054 /// case where `lower.leq(upper)`. When the two bounds are pointwise-
1055 /// incomparable OR strictly reversed (`upper.leq(lower) && lower !=
1056 /// upper`), the result is pointwise `min(max(a, lower), upper)` which
1057 /// is well-defined but no longer sits within a "bracket" in the
1058 /// intuitive sense — for a reversed bound the pointwise `max` pushes
1059 /// the value up to `lower`, then the pointwise `min` pushes it down to
1060 /// `upper`, producing `upper` at every axis (since `upper < lower`
1061 /// per-axis makes `min(anything ≥ lower, upper) == upper`). This
1062 /// matches [`Ord::clamp`]'s "min ≤ max is a precondition, otherwise
1063 /// behaviour is unspecified but well-defined" contract on total orders;
1064 /// callers routing preset pairs through `clamp` maintain the
1065 /// `lower.leq(upper)` invariant at their bounds' construction site,
1066 /// not at every clamp call site.
1067 ///
1068 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1069 /// proofs; the bracket projection of a preset-carried resource proof
1070 /// against two bound preset-carried resource proofs is itself a
1071 /// `ResourceLimits` whose proof content is the input's proof intersected
1072 /// with the ceiling's and unioned with the floor's, and this method is
1073 /// the typed named entry composing them. THEORY.md §V.1 — knowable
1074 /// platform; the bracket combinator becomes a TYPE-level operation on
1075 /// the posture algebra rather than an inline two-step composition at
1076 /// every consumer that enforces a policy range — with the (lower,
1077 /// upper) parameter order baked in so the consumer cannot swap the
1078 /// two combinators (a silent distortion that would clip the input to
1079 /// the wrong bracket order). Frontier inspiration: [`Ord::clamp`] on
1080 /// total orders — the stdlib exposes a first-class named bracket
1081 /// combinator alongside the pairwise `min` / `max`, and the bounded-
1082 /// lattice extension is the same shape one dimension up (per-axis
1083 /// pointwise on a Cartesian product of total orders). Translation
1084 /// through pleme-io primitives is the plain `const fn` composition
1085 /// below, no trait indirection — the pairwise combinators already
1086 /// exist as `const fn` on the algebra so the bracket picks them up
1087 /// structurally.
1088 #[must_use]
1089 pub const fn clamp(self, lower: Self, upper: Self) -> Self {
1090 self.most_permissive(lower).strictest(upper)
1091 }
1092
1093 /// Pointwise bracket-membership predicate — `self` sits within the closed
1094 /// range `[lower, upper]` iff every axis's ceiling satisfies
1095 /// `lower ≤ self ≤ upper`. `a.within(lower, upper)` holds iff
1096 /// `lower.leq(self) && self.leq(upper)`: `self`'s per-axis ceilings all
1097 /// sit at or above `lower`'s AND at or below `upper`'s per-axis ceilings.
1098 ///
1099 /// The boolean PREDICATE peer of the BRACKET COMBINATOR [`Self::clamp`]
1100 /// one PRIMITIVE-KIND axis over on the bracket-primitive surface: where
1101 /// `clamp` PROJECTS an arbitrary input into the `[lower, upper]` bracket,
1102 /// this DECIDES whether the input already sits within it. Peer of
1103 /// [`Self::leq`] one ARITY axis over on the lattice-relation surface:
1104 /// where `leq` is the pairwise partial-order RELATION between two
1105 /// postures, this is the three-input CONTAINMENT RELATION deciding
1106 /// whether a posture sits between two bounds. Together the (leq, within)
1107 /// pair closes the (2-input, 3-input) row on the relation-arity face of
1108 /// the [`ResourceLimits`] lattice-algebra surface — the boolean twin of
1109 /// the (`strictest`+`most_permissive`, `clamp`) combinator-arity closure.
1110 ///
1111 /// **Clamp fixed-point theorem** (`a.within(lower, upper) ⇔
1112 /// a.clamp(lower, upper) == a`): the input sits within the bracket iff
1113 /// the clamp is the identity on it. The forward direction is the
1114 /// in-range identity arm of [`Self::clamp`] (already pinned as
1115 /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`);
1116 /// the reverse follows from the bracket-membership contract on `clamp`
1117 /// (the clamp result always sits within the bracket, so if the clamp
1118 /// equals the input, the input also sits within the bracket). Pinned as
1119 /// `resource_limits_within_agrees_with_clamp_fixed_point` — the CANONICAL
1120 /// cross-check axiom binding the predicate to the combinator, analogous
1121 /// to the meet-agreement (`a.leq(b) ⇔ a.strictest(b) == a`) and
1122 /// join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`) axioms binding
1123 /// the `leq` relation to its combinator peers.
1124 ///
1125 /// **In-range identity**: if `lower.leq(self) && self.leq(upper)`
1126 /// (the input already sits within the bracket), then `a.within(lower,
1127 /// upper) == true`. Pinned as
1128 /// `resource_limits_within_of_posture_in_range_is_true`.
1129 ///
1130 /// **Below-lower rejection**: if the input is strictly tighter than the
1131 /// bracket floor on any axis (`!lower.leq(self)`), then
1132 /// `a.within(lower, upper) == false` regardless of `self`'s relation to
1133 /// `upper` — the boolean conjunction rejects the input. Pinned as
1134 /// `resource_limits_within_of_posture_below_lower_is_false`.
1135 ///
1136 /// **Above-upper rejection**: if the input is strictly looser than the
1137 /// bracket ceiling on any axis (`!self.leq(upper)`), then
1138 /// `a.within(lower, upper) == false`. Pinned as
1139 /// `resource_limits_within_of_posture_above_upper_is_false`.
1140 ///
1141 /// **Extrema-bracket identity**: `a.within(EMPTY_RESOURCE_LIMITS,
1142 /// UNBOUNDED_RESOURCE_LIMITS) == true` for every posture, since every
1143 /// posture sits within the widest possible bracket — the bounded-lattice
1144 /// cross-check with the identity elements previously lifted. Pinned as
1145 /// `resource_limits_within_with_lattice_extrema_is_true`.
1146 ///
1147 /// **Degenerate-bracket collapse**: `a.within(x, x) ⇔ a == x` — a
1148 /// zero-width bracket admits only the single point `x`. Follows from
1149 /// antisymmetry of `leq`: `x.leq(a) && a.leq(x) → a == x`. Pinned as
1150 /// `resource_limits_within_of_equal_bounds_iff_equal_to_bound`.
1151 ///
1152 /// **Reflexive-bracket identity**: `a.within(a, a) == true` for every
1153 /// posture — every input trivially sits within the zero-width bracket
1154 /// at itself, since `a.leq(a)` holds by reflexivity of `leq`. Pinned as
1155 /// `resource_limits_within_of_self_is_reflexive`.
1156 ///
1157 /// `const fn` so a caller can pin a preset-bracket-membership identity
1158 /// at compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
1159 /// .within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS));`) — the
1160 /// typed bracket-predicate peer of the const-fn evaluability on
1161 /// [`Self::leq`] and [`Self::clamp`].
1162 ///
1163 /// Pre-lift, a caller wanting to decide whether a posture sat within a
1164 /// `[lower, upper]` bracket composed the two-primitive scaffolding at its
1165 /// call site as `lower.leq(a) && a.leq(upper)` — the same PRIME DIRECTIVE
1166 /// ≥2 pattern the pairwise `strictest` / `most_permissive` / `leq`
1167 /// combinators already lifted one arity down from six-inline-primitive
1168 /// per-axis cascades. The clamp test cohort exercises this shape at
1169 /// every prerequisite pin (`FLOOR.leq(MID) && MID.leq(CEILING)`) AND at
1170 /// the bracket-membership contract's core assertion (`FLOOR.leq(clamped)
1171 /// && clamped.leq(CEILING)`); post-lift both bindings route through ONE
1172 /// named method whose signature carries the containment relation into the
1173 /// type system rather than the consumer composing the two-primitive
1174 /// conjunction at every call site — a copy-paste that swapped the
1175 /// bounds would test `upper.leq(a) && a.leq(lower)` (the WRONG
1176 /// containment direction returning `true` only for postures that
1177 /// simultaneously exceed the ceiling AND fall below the floor, i.e.
1178 /// only for the impossible `upper.leq(a) && a.leq(lower)` case when
1179 /// `lower.leq(upper)`), a silent distortion the type system did not gate
1180 /// pre-lift and now does through the method's parameter order.
1181 ///
1182 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1183 /// proofs; the bracket-membership predicate on three preset-carried
1184 /// resource proofs is itself a typed named entry composing the input's
1185 /// containment ordering. THEORY.md §V.1 — knowable platform; the
1186 /// pointwise `≤`-conjunction combinator becomes a TYPE-level operation
1187 /// on the posture algebra rather than an inline two-primitive
1188 /// conjunction at every consumer that decides bracket membership — with
1189 /// the (lower, upper) parameter order baked in so the consumer cannot
1190 /// swap the two bounds. Frontier inspiration: the interval-containment
1191 /// predicate is the natural boolean twin of stdlib's [`Ord::clamp`] —
1192 /// stdlib lacks an `is_in_range(min, max)` method, but the bounded-
1193 /// lattice extension of the pattern is straightforward on the pointwise
1194 /// partial order and closes the (combinator, predicate) row on the
1195 /// bracket-primitive surface. Translation through pleme-io primitives is
1196 /// the plain `const fn` `leq`-conjunction below, no trait indirection —
1197 /// the partial-order relation already exists as a `const fn` on the
1198 /// algebra so the predicate picks it up structurally.
1199 #[must_use]
1200 pub const fn within(self, lower: Self, upper: Self) -> bool {
1201 lower.leq(self) && self.leq(upper)
1202 }
1203
1204 /// Boolean `leq`-conjunction across a slice of postures — `self` sits
1205 /// at-or-below every operand in `postures`. `a.is_lower_bound_of(&[b,
1206 /// c, d])` holds iff `a.leq(b) && a.leq(c) && a.leq(d)`: on every
1207 /// operand `self`'s per-axis ceilings sit at-or-below the operand's,
1208 /// so `self` is a common lower bound for the slice.
1209 ///
1210 /// The N-ary boolean PREDICATE peer of the pairwise partial-order
1211 /// relation [`Self::leq`] one ARITY axis over on the lattice-relation
1212 /// surface: where `leq` decides the two-input partial-order between
1213 /// two postures, this decides the (1 + N)-input containment-from-below
1214 /// between `self` and a slice of postures. Peer of
1215 /// [`Self::strictest_of`] one PRIMITIVE-KIND axis over on the N-ary
1216 /// aggregation surface: where `strictest_of` COMPUTES the meet (the
1217 /// greatest lower bound of the slice), this DECIDES whether `self` is
1218 /// a lower bound of the slice (member of the lower-bound set the meet
1219 /// is the largest element of). Together `strictest_of` and
1220 /// `is_lower_bound_of` close the (combinator, predicate) row on the
1221 /// N-ary-aggregation face, exactly the way (`strictest`,
1222 /// `most_permissive`) ↔ `leq` closes the pairwise row and (`clamp` ↔
1223 /// `within`) closes the bracket row on the (combinator, predicate) ×
1224 /// (pairwise, N-ary, bracket) primitive surface.
1225 ///
1226 /// **Empty-slice vacuous truth**: `a.is_lower_bound_of(&[]) == true`
1227 /// for every posture — the empty conjunction is vacuously true, since
1228 /// every posture is trivially a lower bound of the empty set. Peer of
1229 /// `strictest_of(&[]) == UNBOUNDED_RESOURCE_LIMITS`'s empty-slice
1230 /// identity: the empty slice aggregates to the identity element of
1231 /// the underlying operation (`true` for boolean conjunction,
1232 /// [`UNBOUNDED_RESOURCE_LIMITS`] for pointwise `min`), so the empty
1233 /// aggregate never rejects. Pinned as
1234 /// `resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true`.
1235 ///
1236 /// **Single-element identity**: `a.is_lower_bound_of(&[b]) ==
1237 /// a.leq(b)` — the 1-input predicate reduces to the pairwise
1238 /// relation, the same way `strictest_of(&[a]) == a` reduces the
1239 /// 1-input N-ary combinator to the operand verbatim. Pinned as
1240 /// `resource_limits_is_lower_bound_of_single_element_reduces_to_leq`.
1241 ///
1242 /// **Meet witness**: `strictest_of(&postures).is_lower_bound_of(&
1243 /// postures) == true` for every slice — the N-ary meet is always a
1244 /// lower bound of the slice it aggregates. The definitional link
1245 /// between the N-ary COMBINATOR and the N-ary PREDICATE: the
1246 /// aggregate `strictest_of` produces is a member of the lower-bound
1247 /// set `is_lower_bound_of` characterizes. Pinned as
1248 /// `resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice`.
1249 ///
1250 /// **Universal-bottom witness**: `EMPTY_RESOURCE_LIMITS
1251 /// .is_lower_bound_of(&postures) == true` for every slice — the
1252 /// lattice bottom is a common lower bound of every set, since
1253 /// `EMPTY.leq(a) == true` for every posture by the lattice-bottom
1254 /// axiom (`0 ≤ x` per-axis). Pinned as
1255 /// `resource_limits_empty_is_universal_lower_bound_of_every_slice`.
1256 ///
1257 /// **Any-operand rejection**: if `!self.leq(postures[i])` for any
1258 /// `i`, then `a.is_lower_bound_of(&postures) == false` — the
1259 /// conjunction short-circuits on the first violating operand and
1260 /// rejects. Pinned as
1261 /// `resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self`.
1262 ///
1263 /// **Peer** — [`Self::is_upper_bound_of`] one COMBINATOR-DIRECTION
1264 /// axis over: the two close the (`is_lower_bound_of`,
1265 /// `is_upper_bound_of`) N-ary boolean-conjunction pair the pairwise
1266 /// `leq` extends from 2 to N, itself peer of the (`strictest_of`,
1267 /// `most_permissive_of`) N-ary combinator pair one PRIMITIVE-KIND
1268 /// axis over.
1269 ///
1270 /// `const fn` so a caller can pin an N-ary-bound-membership identity
1271 /// at compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS
1272 /// .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS,
1273 /// UNBOUNDED_RESOURCE_LIMITS]));`) — the N-ary predicate peer of the
1274 /// const-fn evaluability on [`Self::leq`] and [`Self::within`].
1275 /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the
1276 /// slice by value without an explicit `.clone()` (which const-fn
1277 /// would not permit anyway).
1278 ///
1279 /// Pre-lift, a caller wanting to decide whether a posture was a
1280 /// common lower bound for a slice composed the
1281 /// `postures.iter().all(|p| self.leq(*p))` two-primitive scaffolding
1282 /// at every call site — the same PRIME DIRECTIVE ≥2 pattern the
1283 /// pairwise `leq` combinator already lifted one arity down from the
1284 /// six-inline-primitive per-axis cascade, and the same shape the
1285 /// pairwise `within` combinator already lifted one arity down from
1286 /// the two-primitive `lower.leq(a) && a.leq(upper)` conjunction.
1287 /// Post-lift the N-ary predicate binds at ONE typed method whose
1288 /// signature carries the containment direction (`self ≤ every
1289 /// operand`) into the type system rather than the consumer composing
1290 /// the `all()`-conjunction at every call site — a copy-paste that
1291 /// flipped the leq direction would test `postures[i].leq(self)` (the
1292 /// dual `is_upper_bound_of` question returning `true` only when
1293 /// `self` DOMINATES rather than UNDERLIES the slice), a silent
1294 /// distortion the method's parameter direction now gates.
1295 ///
1296 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1297 /// preserves proofs; the N-ary containment-from-below predicate on
1298 /// (1 + N) preset-carried resource proofs is itself a typed named
1299 /// entry composing the pairwise partial-order relation. THEORY.md
1300 /// §V.1 — knowable platform; the N-ary `leq`-conjunction becomes a
1301 /// TYPE-level operation on the posture algebra rather than an inline
1302 /// two-primitive conjunction at every consumer that decides
1303 /// N-ary-bound membership. Frontier inspiration: the algebraic-
1304 /// datatypes tradition of a `Foldable` typeclass exposing `all` /
1305 /// `Data.Foldable.all` alongside the pairwise relation — the N-ary
1306 /// traversal of a collection through a boolean-conjunction seed is a
1307 /// first-class named method the typeclass carries. Translation
1308 /// through pleme-io primitives is the plain `const fn` slice-walk
1309 /// below, no typeclass indirection — the partial-order relation
1310 /// already exists as a `const fn` on the algebra so the predicate
1311 /// picks it up structurally.
1312 #[must_use]
1313 pub const fn is_lower_bound_of(self, postures: &[Self]) -> bool {
1314 let mut i = 0;
1315 while i < postures.len() {
1316 if !self.leq(postures[i]) {
1317 return false;
1318 }
1319 i += 1;
1320 }
1321 true
1322 }
1323
1324 /// Boolean `leq`-conjunction across a slice of postures — every operand
1325 /// in `postures` sits at-or-below `self`. `a.is_upper_bound_of(&[b, c,
1326 /// d])` holds iff `b.leq(a) && c.leq(a) && d.leq(a)`: on every operand
1327 /// the operand's per-axis ceilings sit at-or-below `self`'s, so `self`
1328 /// is a common upper bound for the slice.
1329 ///
1330 /// The DUAL N-ary predicate of [`Self::is_lower_bound_of`] one
1331 /// COMBINATOR-DIRECTION axis over: the two close the (lower, upper)
1332 /// N-ary boolean-conjunction pair the pairwise `leq` extends from 2
1333 /// to N, itself peer of the (`strictest_of`, `most_permissive_of`)
1334 /// N-ary combinator pair one PRIMITIVE-KIND axis over on the N-ary-
1335 /// aggregation face of the (combinator, predicate) × (pairwise,
1336 /// N-ary, bracket) primitive surface. Peer of
1337 /// [`Self::most_permissive_of`] one PRIMITIVE-KIND axis over on the
1338 /// N-ary aggregation surface: where `most_permissive_of` COMPUTES
1339 /// the join (the least upper bound of the slice), this DECIDES
1340 /// whether `self` is an upper bound of the slice (member of the
1341 /// upper-bound set the join is the smallest element of).
1342 ///
1343 /// **Empty-slice vacuous truth**: `a.is_upper_bound_of(&[]) == true`
1344 /// for every posture — the empty conjunction is vacuously true. Peer
1345 /// of `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
1346 /// slice identity: the empty aggregate binds structurally to the
1347 /// identity element of the underlying operation. Pinned as
1348 /// `resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true`.
1349 ///
1350 /// **Single-element identity**: `a.is_upper_bound_of(&[b]) ==
1351 /// b.leq(a)` — the 1-input predicate reduces to the pairwise
1352 /// relation with the direction flipped (since `self` sits ABOVE the
1353 /// operand rather than BELOW it). Pinned as
1354 /// `resource_limits_is_upper_bound_of_single_element_reduces_to_leq`.
1355 ///
1356 /// **Join witness**: `most_permissive_of(&postures).is_upper_bound_of(&
1357 /// postures) == true` for every slice — the N-ary join is always an
1358 /// upper bound of the slice it aggregates. The dual definitional link
1359 /// between N-ary COMBINATOR and N-ary PREDICATE: the aggregate
1360 /// `most_permissive_of` produces is a member of the upper-bound set
1361 /// `is_upper_bound_of` characterizes. Pinned as
1362 /// `resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice`.
1363 ///
1364 /// **Universal-top witness**: `UNBOUNDED_RESOURCE_LIMITS
1365 /// .is_upper_bound_of(&postures) == true` for every slice — the
1366 /// lattice top is a common upper bound of every set, since
1367 /// `a.leq(UNBOUNDED) == true` for every posture by the lattice-top
1368 /// axiom (`x ≤ usize::MAX` per-axis). Pinned as
1369 /// `resource_limits_unbounded_is_universal_upper_bound_of_every_slice`.
1370 ///
1371 /// **Any-operand rejection**: if `!postures[i].leq(self)` for any
1372 /// `i`, then `a.is_upper_bound_of(&postures) == false` — the
1373 /// conjunction short-circuits and rejects. Pinned as
1374 /// `resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self`.
1375 ///
1376 /// `const fn` so a caller can pin an N-ary-bound-membership identity
1377 /// at compile time. See [`Self::is_lower_bound_of`] for the shared
1378 /// pre-lift ≥2 pattern (`postures.iter().all(|p| p.leq(*self))`), the
1379 /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1380 /// `Foldable::all` frontier inspiration — this method carries the
1381 /// dual `postures[i].leq(self)` direction rather than
1382 /// `self.leq(postures[i])`.
1383 #[must_use]
1384 pub const fn is_upper_bound_of(self, postures: &[Self]) -> bool {
1385 let mut i = 0;
1386 while i < postures.len() {
1387 if !postures[i].leq(self) {
1388 return false;
1389 }
1390 i += 1;
1391 }
1392 true
1393 }
1394
1395 /// Strict pointwise partial-order relation across the six ceilings —
1396 /// `self` is STRICTLY tighter than `other` iff `self.leq(other)` AND
1397 /// `self != other`. `a.lt(b)` holds iff every input the `a` posture
1398 /// admits the `b` posture also admits AND there is at least one axis
1399 /// on which `a`'s ceiling is strictly smaller than `b`'s: `a`'s per-
1400 /// axis ceilings all sit at-or-below `b`'s AND at least one field
1401 /// closes the inequality strictly, so `b` strictly extends `a`'s
1402 /// admissible input set on at least one axis.
1403 ///
1404 /// The STRICT peer of [`Self::leq`] one STRICTNESS axis over on the
1405 /// pairwise partial-order surface: where `leq` is the REFLEXIVE
1406 /// non-strict `≤` relation (`a.leq(a) == true`), this is the
1407 /// IRREFLEXIVE strict `<` relation (`a.lt(a) == false`). Together
1408 /// the two close the (non-strict, strict) partial-order pair on the
1409 /// pairwise-relation face of the (combinator, predicate) ×
1410 /// (strictness) primitive surface — itself peer of the
1411 /// (`strictest`, `most_permissive`) meet/join combinator pair one
1412 /// PRIMITIVE-KIND axis over (both bind against the same pointwise
1413 /// `≤` ordering).
1414 ///
1415 /// Encoded as `self.leq(other) && !other.leq(self)` — the two-
1416 /// primitive antisymmetric encoding of strict `<` on any partial
1417 /// order, pointwise or otherwise. Equivalent on the
1418 /// [`ResourceLimits`] pointwise partial order (which is
1419 /// antisymmetric) to `self.leq(other) && self != other`:
1420 /// antisymmetry says `self.leq(other) && other.leq(self) ⇒ self ==
1421 /// other`, so `self.leq(other) && !other.leq(self)` iff
1422 /// `self.leq(other) && self != other`. The antisymmetric-leq
1423 /// encoding is chosen because it does NOT rely on
1424 /// [`PartialEq::eq`] being `const fn` (which the derived impl is
1425 /// not on stable) — both [`Self::leq`] calls are already `const fn`
1426 /// on the algebra.
1427 ///
1428 /// **Irreflexivity**: `a.lt(a) == false` for every posture. A
1429 /// posture is never strictly tighter than itself, since
1430 /// `a.leq(a) && !a.leq(a)` short-circuits `false` on the second
1431 /// conjunct. Pinned as
1432 /// `resource_limits_lt_is_irreflexive`.
1433 ///
1434 /// **Asymmetry**: `a.lt(b) ⇒ !b.lt(a)` for every pair. The two
1435 /// directions of the strict relation cannot both hold, since
1436 /// `a.lt(b)` implies `!b.leq(a)`, and `b.lt(a)` requires
1437 /// `b.leq(a)` — the two are contradictory. Pinned as
1438 /// `resource_limits_lt_is_asymmetric`.
1439 ///
1440 /// **Transitivity**: `a.lt(b) && b.lt(c) ⇒ a.lt(c)` for every
1441 /// triple. Inherits from the transitivity of [`Self::leq`] on both
1442 /// conjuncts, with the strictness carried through by the outer
1443 /// antisymmetric leg (if `c.leq(a)` held, transitivity of `leq`
1444 /// would give `b.leq(a)` contradicting `a.lt(b)`). Pinned as
1445 /// `resource_limits_lt_is_transitive`.
1446 ///
1447 /// **Refines [`Self::leq`]**: `a.lt(b) ⇒ a.leq(b)` for every pair
1448 /// — the strict relation refines the non-strict one. Conversely
1449 /// `a.leq(b) && !a.lt(b) ⇒ a == b` (the reflexive tie is exactly
1450 /// the non-strict-minus-strict gap). Pinned as
1451 /// `resource_limits_lt_refines_leq` AND
1452 /// `resource_limits_lt_agrees_with_leq_minus_equality`.
1453 ///
1454 /// **Bounded-lattice diagonal**:
1455 /// `EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS)` AND
1456 /// `DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` AND
1457 /// `EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` — the
1458 /// strict chain across the (bottom, middle, top) preset triple on
1459 /// the bounded-lattice diagonal. Every `DEFAULT_MAX_*` module
1460 /// constant is a concrete positive value strictly less than
1461 /// [`usize::MAX`] and strictly greater than `0`, so the two
1462 /// endpoints and the interior are strictly ordered on every axis.
1463 /// Pinned as `resource_limits_lt_of_bottom_diagonal_pinned`.
1464 ///
1465 /// **Incomparable rejection**: on the two hand-authored asymmetric
1466 /// postures (three axes smaller in each direction), both
1467 /// directions of the strict relation fail (`!a.lt(b) && !b.lt(a)`)
1468 /// because neither `leq` direction holds. The predicate does NOT
1469 /// promote incomparable pairs to a comparison verdict. Pinned as
1470 /// `resource_limits_lt_rejects_incomparable_postures`.
1471 ///
1472 /// **Antisymmetric-encoding cross-check**: `a.lt(b) == (a.leq(b)
1473 /// && !b.leq(a))` at every pair by definition, AND `a.lt(b) ==
1474 /// (a.leq(b) && a != b)` at every pair by antisymmetry. Both
1475 /// encodings are pinned equal to the shipped body across the
1476 /// (empty, default, unbounded, mid, other) 5×5 preset matrix.
1477 /// Pinned as
1478 /// `resource_limits_lt_agrees_with_direct_antisymmetric_encoding`.
1479 ///
1480 /// `const fn` so a caller can pin a strict-order identity at
1481 /// compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(
1482 /// DEFAULT_RESOURCE_LIMITS));`) rather than deferring the
1483 /// composition to a runtime chain — sibling of the const-fn
1484 /// evaluability pin on [`Self::leq`] one STRICTNESS axis over.
1485 ///
1486 /// Pre-lift, a caller wanting "is this posture STRICTLY tighter
1487 /// than that one?" composed the two-primitive
1488 /// `a.leq(b) && !b.leq(a)` (or equivalently `a.leq(b) && a != b`)
1489 /// conjunction at its call site — a PRIME DIRECTIVE ≥2 pattern
1490 /// once two consumers need it, and one that couples the strictness
1491 /// interpretation to the caller's chosen encoding
1492 /// (antisymmetric-leq vs. leq-and-not-equal, which coincide on
1493 /// pointwise partial orders but diverge on non-antisymmetric
1494 /// preorders). Post-lift the strict relation binds at ONE typed
1495 /// method the algebra exposes, and the encoding is a private
1496 /// implementation detail no caller can drift from.
1497 ///
1498 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1499 /// preserves proofs; the strict-tightening relation between two
1500 /// preset-carried resource proofs is itself a typed named `bool`
1501 /// predicate composing them via the underlying `leq`. THEORY.md
1502 /// §V.1 — knowable platform; the strict `<` relation becomes a
1503 /// TYPE-level primitive on the posture algebra rather than an
1504 /// inline two-primitive conjunction at every consumer that
1505 /// decides "did we strictly tighten?" `tatara-lattice`'s partial
1506 /// order on `Classification` closes the SAME shape one layer
1507 /// down: an entity's classification lattice carries a strict `<`
1508 /// peer of its `leq` for the same reason a resource posture does.
1509 /// Frontier inspiration: [`PartialOrd::lt`] on Rust's partial-
1510 /// order trait; [`Ord::lt`] on the total-order trait — the
1511 /// strict variant of `≤` is a first-class named method the
1512 /// standard library exposes rather than leaving each consumer to
1513 /// compose `partial_cmp(&other) == Some(Less)` at its call site.
1514 /// Translation through pleme-io primitives is the plain `const
1515 /// fn` antisymmetric encoding below, no trait indirection — the
1516 /// non-strict [`Self::leq`] already exists as a `const fn` on the
1517 /// algebra so the strict variant picks it up structurally.
1518 #[must_use]
1519 pub const fn lt(self, other: Self) -> bool {
1520 self.leq(other) && !other.leq(self)
1521 }
1522
1523 /// Strict pointwise partial-order relation dual of [`Self::lt`] —
1524 /// `a.gt(b) == b.lt(a)`. `self` is STRICTLY looser than `other`
1525 /// iff `other.leq(self)` AND `self != other`: on every axis
1526 /// `self`'s ceiling sits at-or-above `other`'s, and at least one
1527 /// axis closes the inequality strictly, so `self` strictly extends
1528 /// `other`'s admissible input set on at least one axis.
1529 ///
1530 /// The DIRECTION peer of [`Self::lt`] one DIRECTION axis over on
1531 /// the strict-pairwise-relation face — where `lt` is the strict
1532 /// "below" relation (`a < b`), this is the strict "above"
1533 /// relation (`a > b`). Together the two close the (below, above)
1534 /// direction pair on the strict pairwise relation, exactly as
1535 /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] close
1536 /// the (below, above) direction pair on the non-strict N-ary
1537 /// relation one ARITY axis over.
1538 ///
1539 /// Same laws as [`Self::lt`] with directions flipped:
1540 /// irreflexive (`a.gt(a) == false`), asymmetric
1541 /// (`a.gt(b) ⇒ !b.gt(a)`), transitive
1542 /// (`a.gt(b) && b.gt(c) ⇒ a.gt(c)`); refines a hypothetical `geq`
1543 /// (the reflexive `≥`) exactly as `lt` refines `leq`. Pinned on
1544 /// the shipped preset triangle: `UNBOUNDED_RESOURCE_LIMITS.gt(
1545 /// DEFAULT_RESOURCE_LIMITS) && DEFAULT_RESOURCE_LIMITS.gt(
1546 /// EMPTY_RESOURCE_LIMITS)`.
1547 ///
1548 /// Encoded as `other.lt(self)` — one primitive delegation to
1549 /// [`Self::lt`] so the strict-relation encoding lives at exactly
1550 /// one implementation site, and a future re-derivation of `lt`
1551 /// (e.g. to a different antisymmetry encoding) propagates to `gt`
1552 /// mechanically rather than requiring a per-method fix-up.
1553 ///
1554 /// `const fn` for the same compile-time-pin reasons as
1555 /// [`Self::lt`]. See [`Self::lt`] for the full docstring, the
1556 /// pre-lift `a.leq(b) && !b.leq(a)` two-primitive PRIME DIRECTIVE
1557 /// ≥2 pattern, the theory anchor (THEORY.md §II.1 invariant 5,
1558 /// §V.1), and the [`Ord::gt`] frontier inspiration.
1559 #[must_use]
1560 pub const fn gt(self, other: Self) -> bool {
1561 other.lt(self)
1562 }
1563
1564 /// Non-strict pointwise partial-order relation dual of
1565 /// [`Self::leq`] — `a.geq(b) == b.leq(a)`. `self` is AT-OR-LOOSER-
1566 /// THAN `other` iff on every axis `self`'s ceiling sits at-or-above
1567 /// `other`'s: every input the `other` posture admits the `self`
1568 /// posture also admits, so `self` dominates `other`'s admissible
1569 /// input set pointwise (possibly with equality on some or every
1570 /// axis).
1571 ///
1572 /// The DIRECTION peer of [`Self::leq`] one DIRECTION axis over on
1573 /// the non-strict pairwise-relation face — where `leq` is the
1574 /// non-strict "below" relation (`a ≤ b`), this is the non-strict
1575 /// "above" relation (`a ≥ b`). Together the two close the
1576 /// (below, above) direction pair on the non-strict pairwise
1577 /// relation, exactly as [`Self::lt`] / [`Self::gt`] close the
1578 /// (below, above) direction pair on the STRICT pairwise relation
1579 /// one STRICTNESS axis over. The STRICTNESS peer of [`Self::gt`]
1580 /// one STRICTNESS axis over on the "above" column of the
1581 /// (direction × strictness) 2×2 partial-order face — where `gt` is
1582 /// the IRREFLEXIVE strict `>` relation (`a.gt(a) == false`), this
1583 /// is the REFLEXIVE non-strict `≥` relation (`a.geq(a) == true`).
1584 /// The four methods `(leq, lt, gt, geq)` EXHAUSTIVELY close the
1585 /// (below, above) × (non-strict, strict) 2×2 pairwise partial-order
1586 /// face on [`ResourceLimits`], with each method the single-axis
1587 /// peer of two others (one DIRECTION axis over AND one STRICTNESS
1588 /// axis over).
1589 ///
1590 /// Same laws as [`Self::leq`] with directions flipped:
1591 /// reflexive (`a.geq(a) == true`), antisymmetric
1592 /// (`a.geq(b) && b.geq(a) ⇒ a == b`), transitive
1593 /// (`a.geq(b) && b.geq(c) ⇒ a.geq(c)`); refines [`Self::gt`]
1594 /// (`a.gt(b) ⇒ a.geq(b)`) exactly as `leq` is refined by `lt`.
1595 /// NOT total on incomparable postures: on the two hand-authored
1596 /// asymmetric postures both directions of `geq` fail — the
1597 /// non-strict relation does not promote incomparable pairs to a
1598 /// comparison verdict any more than the strict one does. Pinned on
1599 /// the shipped preset triangle:
1600 /// `UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS) &&
1601 /// DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS)`.
1602 ///
1603 /// Encoded as `other.leq(self)` — one primitive delegation to
1604 /// [`Self::leq`] so the non-strict-relation encoding lives at
1605 /// exactly one implementation site, and a future re-derivation of
1606 /// `leq` (e.g. to a different pointwise conjunction) propagates to
1607 /// `geq` mechanically rather than requiring a per-method fix-up.
1608 /// Mirrors [`Self::gt`]'s `other.lt(self)` delegation one
1609 /// STRICTNESS axis over.
1610 ///
1611 /// **N-ary-predicate link**: `a.geq(b) == a.is_upper_bound_of(&[b])`
1612 /// — the pairwise "above" relation is exactly the 1-input
1613 /// specialization of the N-ary upper-bound predicate. The
1614 /// (`geq`, `is_upper_bound_of`) pair sits on the same ARITY axis
1615 /// as (`leq`, `is_lower_bound_of`) one DIRECTION axis over — every
1616 /// pairwise partial-order relation extends to an N-ary bound-
1617 /// membership predicate the substrate carries as a named entry.
1618 ///
1619 /// `const fn` for the same compile-time-pin reasons as
1620 /// [`Self::leq`] (`const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
1621 /// .geq(DEFAULT_RESOURCE_LIMITS));`). See [`Self::leq`] for the
1622 /// full docstring, the pre-lift six-inline-`>=`-conjunction pattern
1623 /// callers wrote before the pairwise relation was named, the
1624 /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1625 /// [`PartialOrd::ge`] frontier inspiration.
1626 #[must_use]
1627 pub const fn geq(self, other: Self) -> bool {
1628 other.leq(self)
1629 }
1630
1631 /// Antichain characterization on the [`ResourceLimits`] partial
1632 /// order — `true` iff `self` and `other` are INCOMPARABLE, i.e.,
1633 /// NEITHER `self.leq(other)` NOR `self.geq(other)` holds. The two
1634 /// postures then sit on distinct branches of the lattice: at least
1635 /// one axis has `self` strictly below `other`, and at least one
1636 /// axis has `self` strictly above `other`, so neither pointwise-
1637 /// domination direction closes and neither preset dominates the
1638 /// other on every field.
1639 ///
1640 /// The ANTICHAIN CORNER on the (leq, geq, lt, gt, incomparable)
1641 /// pairwise-relation surface — the corner that the four pairwise
1642 /// partial-order predicates leave open. On a TOTAL order every
1643 /// pair of elements sits at exactly one of `{Less, Equal,
1644 /// Greater}`, so `is_incomparable` would fold to `false`
1645 /// unconditionally; on the [`ResourceLimits`] pointwise partial
1646 /// order the antichain is non-empty (witnessed by
1647 /// `HAND_AUTHORED_MID_POSTURE` + `HAND_AUTHORED_OTHER_POSTURE`,
1648 /// which the just-lifted [`Self::leq`] / [`Self::geq`] / [`Self::lt`]
1649 /// / [`Self::gt`] tests already pin as neither-`leq`-nor-`geq`), so
1650 /// the corner carries a genuine bit distinct from
1651 /// `!self.leq(other) && !self.geq(other)` inlined at the callsite.
1652 /// The (leq, geq, lt, gt) 2×2 pairwise partial-order face the just-
1653 /// lifted [`Self::geq`] EXHAUSTIVELY closes at its (below, above) ×
1654 /// (non-strict, strict) grid pins the COMPARABLE cases; THIS
1655 /// projection pins the DISJOINT antichain complement one COVER
1656 /// axis over — the (comparable, incomparable) two-cell partition of
1657 /// the total (ordered pair × verdict) surface.
1658 ///
1659 /// Symmetric on its argument order: `a.is_incomparable(b) ==
1660 /// b.is_incomparable(a)` — the composition `!self.leq(other) &&
1661 /// !self.geq(other)` folds through `self.leq(other) ==
1662 /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1663 /// conjunction's commutativity, so the projection is
1664 /// order-independent on its two inputs. This SYMMETRY POSTURE
1665 /// distinguishes it from [`Self::leq`] / [`Self::lt`] (both
1666 /// antisymmetric — swapping inputs FLIPS the verdict) and from
1667 /// [`Self::within`] (asymmetric — the first input is the target
1668 /// and the last two are the bracket bounds).
1669 ///
1670 /// Irreflexive on its own argument: `a.is_incomparable(a) ==
1671 /// false` — every posture is comparable to itself via
1672 /// [`Self::leq`]'s reflexivity (`a.leq(a) == true`), so the
1673 /// conjunction's `!self.leq(other)` leg forces the verdict to
1674 /// `false` at every diagonal input. Sibling posture to
1675 /// [`Self::lt`]'s irreflexivity one COVER axis over: `lt` is
1676 /// irreflexive because the strict relation rules out equality;
1677 /// THIS projection is irreflexive because the comparable
1678 /// relation already includes equality.
1679 ///
1680 /// **Bottom-pole absorption**: `a.is_incomparable(
1681 /// EMPTY_RESOURCE_LIMITS) == false` on every posture `a` —
1682 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1683 /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1684 /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1685 /// identity, which falsifies the `!self.geq(other)` leg. The
1686 /// bounded-lattice bottom is comparable to every posture. Pinned
1687 /// on the canonical preset roster.
1688 ///
1689 /// **Top-pole absorption**: `a.is_incomparable(
1690 /// UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a` —
1691 /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1692 /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1693 /// usize::MAX` unconditionally), which falsifies the
1694 /// `!self.leq(other)` leg. The bounded-lattice top is comparable
1695 /// to every posture. Pinned on the canonical preset roster.
1696 ///
1697 /// **Antichain load-bearing arm**:
1698 /// `HAND_AUTHORED_MID_POSTURE.is_incomparable(
1699 /// HAND_AUTHORED_OTHER_POSTURE) == true` — the two hand-authored
1700 /// asymmetric postures sit on distinct branches (each is smaller
1701 /// on three axes and larger on the other three), so neither
1702 /// pointwise-domination direction closes and the antichain
1703 /// verdict is `true`. Pairs with
1704 /// `resource_limits_geq_rejects_incomparable_postures` +
1705 /// `resource_limits_leq_rejects_incomparable_postures` one COVER
1706 /// axis over: those pin the two comparable-direction predicates
1707 /// FALSIFY on this pair; THIS projection pins the antichain
1708 /// verdict HOLDS on the SAME pair — the two are the negation-
1709 /// paired sides of the (comparable, incomparable) two-cell
1710 /// partition.
1711 ///
1712 /// **De Morgan duality**: `a.is_incomparable(b) == !(a.leq(b) ||
1713 /// a.geq(b))` — the antichain corner is the SET-COMPLEMENT of
1714 /// the union of the two pointwise-domination directions. Pinned
1715 /// by `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1716 /// which sweeps every ordered pair in the preset roster and
1717 /// confirms the equivalence at every cell.
1718 ///
1719 /// Encoded as `!self.leq(other) && !self.geq(other)` — one
1720 /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1721 /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1722 /// antichain characterization lives at exactly one implementation
1723 /// site, and a future re-derivation of [`Self::leq`] (e.g. to a
1724 /// different pointwise conjunction) propagates to
1725 /// `is_incomparable` mechanically rather than requiring a per-
1726 /// method fix-up. Mirrors [`Self::gt`]'s `other.lt(self)`
1727 /// delegation and [`Self::geq`]'s `other.leq(self)` delegation one
1728 /// COVER axis over on the pairwise-relation surface.
1729 ///
1730 /// `const fn` for the same compile-time-pin reasons as
1731 /// [`Self::leq`] (`const _: () =
1732 /// assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(
1733 /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin an antichain-
1734 /// disagreement identity at compile time — a build-break rather
1735 /// than a runtime `assert!` on the first execution.
1736 ///
1737 /// Pre-lift, a caller that wanted "these two postures are
1738 /// incomparable on the pointwise partial-order" composed
1739 /// `!a.leq(b) && !b.leq(a)` at the callsite (or the algebraically
1740 /// equivalent `!a.leq(b) && !a.geq(b)` after `geq` landed), a
1741 /// two-primitive composition that appeared verbatim at every
1742 /// prospective antichain-detection site pre-lift — a ≥2 PRIME
1743 /// DIRECTIVE trigger with the (comparable, incomparable) two-cell
1744 /// partition of the ordered pair × verdict surface as its
1745 /// substrate posture. Post-lift the antichain characterization is
1746 /// ONE named primitive on the [`ResourceLimits`] surface,
1747 /// `const fn`-composable into a compile-time bound, and the two-
1748 /// primitive `!leq && !geq` composition lives at ONE
1749 /// implementation site.
1750 ///
1751 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1752 /// preserves proofs; the antichain characterization is the
1753 /// SET-COMPLEMENT of the union of the two comparable-direction
1754 /// predicates, and pinning the projection at ONE typed primitive
1755 /// makes the (comparable, incomparable) two-cell partition of the
1756 /// ordered-pair × verdict surface a substrate-level THEOREM
1757 /// rather than a per-consumer inline `!leq && !geq` sanity check.
1758 /// THEORY.md §V.1 — knowable platform; the antichain-detection
1759 /// corner was an unnamed two-primitive composition recurring at
1760 /// every prospective "these two postures sit on distinct
1761 /// branches?" callsite pre-lift.
1762 ///
1763 /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1764 /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` returning
1765 /// `Nothing` on incomparable inputs — the antichain corner is
1766 /// exactly the `isNothing` projection of that method; Julia's
1767 /// `Base.Order.NoOrder` sentinel on the same abstract; Rust's own
1768 /// `PartialOrd::partial_cmp` returning `Option<Ordering>` whose
1769 /// `None` arm is the antichain corner; Coq's `PreOrder` +
1770 /// classical LEM-derived `~ (leq a b) /\ ~ (leq b a)` proof
1771 /// obligation on the antichain arm. Translation through pleme-io
1772 /// primitives: the antichain characterization on the closed-set-
1773 /// paired [`ResourceLimits`] posture binds through the just-
1774 /// lifted [`Self::leq`] and [`Self::geq`] primitives (both const-
1775 /// evaluable, both delegating to [`Self::leq`]) with a two-arm
1776 /// conjunction on the negated forms — no new dep, no supertrait
1777 /// bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no
1778 /// allocation, `const fn` throughout so the antichain verdict is
1779 /// a compile-time expression at every preset-tuple call site.
1780 #[must_use]
1781 pub const fn is_incomparable(self, other: Self) -> bool {
1782 !self.leq(other) && !self.geq(other)
1783 }
1784
1785 /// Pointwise partial-order COMPARABILITY relation across the six
1786 /// ceilings — `true` iff `self` and `other` sit on the SAME lattice
1787 /// branch, i.e., EITHER `self.leq(other)` OR `self.geq(other)`
1788 /// holds. The two postures then admit a pointwise-domination
1789 /// verdict in at least one direction: every axis has `self` at-or-
1790 /// below `other` (the `leq` branch) or every axis has `self`
1791 /// at-or-above `other` (the `geq` branch), so at least one
1792 /// direction closes and one preset dominates the other on every
1793 /// field.
1794 ///
1795 /// The COMPARABLE CORNER on the (leq, geq, lt, gt, incomparable,
1796 /// comparable) pairwise-relation surface — the DIRECT POSITIVE
1797 /// DUAL of [`Self::is_incomparable`] one COVER-COMPLEMENT axis
1798 /// over on the (comparable, incomparable) two-cell partition of
1799 /// the ordered pair × verdict surface. The just-lifted
1800 /// [`Self::is_incomparable`] projection pins the ANTICHAIN half of
1801 /// that partition (the pairs where NEITHER pointwise-domination
1802 /// direction closes); THIS projection pins the DUAL COMPARABLE
1803 /// half (the pairs where AT LEAST ONE direction closes) —
1804 /// EXHAUSTIVELY CLOSING the (comparable, incomparable) two-cell
1805 /// partition at its FINAL SECOND tile on the pairwise-relation
1806 /// surface. The (leq, geq, lt, gt) 2×2 pairwise partial-order face
1807 /// carries the four DIRECTIONAL comparable predicates one COVER
1808 /// axis over; THIS projection folds them into ONE POSITIVE
1809 /// COMPARABILITY verdict — the SET-UNION of the two pointwise-
1810 /// domination directions.
1811 ///
1812 /// Symmetric on its argument order: `a.is_comparable(b) ==
1813 /// b.is_comparable(a)` — the composition `self.leq(other) ||
1814 /// self.geq(other)` folds through `self.leq(other) ==
1815 /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1816 /// disjunction's commutativity, so the projection is order-
1817 /// independent on its two inputs. This SYMMETRY POSTURE mirrors
1818 /// [`Self::is_incomparable`]'s symmetry one CELL axis over on the
1819 /// (comparable, incomparable) partition: both cells of an
1820 /// EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface must
1821 /// themselves be symmetric — the two verdicts flip in lockstep on
1822 /// argument swap. Distinguishes it from [`Self::leq`] / [`Self::lt`]
1823 /// (both antisymmetric — swapping inputs FLIPS the verdict).
1824 ///
1825 /// Reflexive on its own argument: `a.is_comparable(a) == true` —
1826 /// every posture is comparable to itself via [`Self::leq`]'s
1827 /// reflexivity (`a.leq(a) == true`), so the disjunction's
1828 /// `self.leq(other)` leg holds at every diagonal input. Sibling
1829 /// posture to [`Self::leq`]'s reflexivity one COVER axis over:
1830 /// the diagonal COMPARABLE cell is a substrate-level THEOREM
1831 /// falling out of the partial-order axioms, and THIS projection
1832 /// pins it as ONE positive verdict rather than the caller re-
1833 /// deriving `a.leq(a) || a.geq(a) == true` at every diagonal
1834 /// callsite.
1835 ///
1836 /// **Bottom-pole absorption**: `a.is_comparable(
1837 /// EMPTY_RESOURCE_LIMITS) == true` on every posture `a` —
1838 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1839 /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1840 /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1841 /// identity, which satisfies the `self.geq(other)` leg. The
1842 /// bounded-lattice bottom is comparable to every posture. The
1843 /// COMPLEMENT of [`Self::is_incomparable`]'s bottom-pole
1844 /// absorption pin one CELL axis over — where the antichain
1845 /// verdict FOLDS FALSE at the bottom pole, the comparability
1846 /// verdict FOLDS TRUE. Pinned on the canonical preset roster.
1847 ///
1848 /// **Top-pole absorption**: `a.is_comparable(
1849 /// UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a` —
1850 /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1851 /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1852 /// usize::MAX` unconditionally), which satisfies the
1853 /// `self.leq(other)` leg. The bounded-lattice top is comparable
1854 /// to every posture. The COMPLEMENT of [`Self::is_incomparable`]'s
1855 /// top-pole absorption pin one CELL axis over; pinned on the
1856 /// canonical preset roster.
1857 ///
1858 /// **Antichain load-bearing arm**:
1859 /// `HAND_AUTHORED_MID_POSTURE.is_comparable(
1860 /// HAND_AUTHORED_OTHER_POSTURE) == false` — the two hand-authored
1861 /// asymmetric postures sit on distinct branches (each is smaller
1862 /// on three axes and larger on the other three), so NEITHER
1863 /// pointwise-domination direction closes and the comparability
1864 /// verdict is `false`. The DIRECT FALSIFICATION of
1865 /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin one CELL
1866 /// axis over on the SAME pair — the two are the negation-paired
1867 /// sides of the (comparable, incomparable) two-cell partition,
1868 /// and pinning BOTH cells' verdicts on the SAME load-bearing
1869 /// antichain pair closes the partition as a substrate-level
1870 /// EXCLUSIVITY THEOREM rather than one direction's assertion.
1871 ///
1872 /// **De Morgan duality**: `a.is_comparable(b) == !a.is_incomparable(b)`
1873 /// — the comparable corner is the SET-COMPLEMENT of the antichain
1874 /// corner. Pinned by
1875 /// `resource_limits_is_comparable_is_de_morgan_dual_of_incomparable`
1876 /// which sweeps every ordered pair in the preset roster and
1877 /// confirms the equivalence at every cell. Together with
1878 /// `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1879 /// (which pins the OTHER direction on
1880 /// `a.leq(b) || a.geq(b)` composition), the two projections carry
1881 /// the (comparable, incomparable) two-cell partition as an
1882 /// EXHAUSTIVE-AND-DISJOINT decomposition of the ordered pair ×
1883 /// verdict surface.
1884 ///
1885 /// Encoded as `self.leq(other) || self.geq(other)` — one
1886 /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1887 /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1888 /// comparability characterization lives at exactly one
1889 /// implementation site, and a future re-derivation of [`Self::leq`]
1890 /// (e.g. to a different pointwise conjunction) propagates to
1891 /// `is_comparable` mechanically rather than requiring a per-
1892 /// method fix-up. Mirrors [`Self::is_incomparable`]'s
1893 /// `!self.leq(other) && !self.geq(other)` delegation one COVER
1894 /// axis over on the pairwise-relation surface — the two share
1895 /// the SAME two-primitive substrate and diverge only on the
1896 /// negation posture (`!… && !…` vs `… || …`), which De Morgan's
1897 /// law pins as the SAME projection.
1898 ///
1899 /// `const fn` for the same compile-time-pin reasons as
1900 /// [`Self::is_incomparable`] (`const _: () =
1901 /// assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(
1902 /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin a
1903 /// comparability-agreement identity at compile time — a build-
1904 /// break rather than a runtime `assert!` on the first execution.
1905 ///
1906 /// Pre-lift, a caller that wanted "these two postures are
1907 /// comparable on the pointwise partial-order" composed
1908 /// `a.leq(b) || a.geq(b)` at the callsite — a two-primitive
1909 /// composition that appeared verbatim at every prospective
1910 /// comparability-detection site pre-lift, and the NEGATION of the
1911 /// same two-primitive composition [`Self::is_incomparable`]
1912 /// lifted one COVER axis over — a ≥2 PRIME DIRECTIVE trigger with
1913 /// the (comparable, incomparable) two-cell partition of the
1914 /// ordered pair × verdict surface as its substrate posture. Post-
1915 /// lift the comparability characterization is ONE named primitive
1916 /// on the [`ResourceLimits`] surface, `const fn`-composable into
1917 /// a compile-time bound, and the two-primitive `leq || geq`
1918 /// composition lives at ONE implementation site.
1919 ///
1920 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1921 /// preserves proofs; the comparability characterization is the
1922 /// SET-UNION of the two pointwise-domination directions, and
1923 /// pinning the projection at ONE typed primitive makes the
1924 /// (comparable, incomparable) two-cell partition of the ordered-
1925 /// pair × verdict surface a substrate-level THEOREM proved on
1926 /// BOTH SIDES rather than one side asserted and the other
1927 /// inferred at consumer sites. THEORY.md §V.1 — knowable
1928 /// platform; the comparability-detection corner was an unnamed
1929 /// two-primitive composition recurring at every prospective
1930 /// "these two postures sit on the same branch?" callsite pre-
1931 /// lift.
1932 ///
1933 /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1934 /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` — the
1935 /// comparable corner is exactly the `isJust` projection of that
1936 /// method (the DUAL of `is_incomparable`'s `isNothing` projection
1937 /// on the SAME method); Julia's `Base.Order.lt` + `Base.Order.gt`
1938 /// disjunction on the pairwise-comparability check; Rust's own
1939 /// `PartialOrd::partial_cmp` whose `Some(_)` arm is the
1940 /// comparable corner; Coq's `PreOrder` + classical LEM-derived
1941 /// `leq a b \/ leq b a` proof obligation on the comparable arm.
1942 /// Translation through pleme-io primitives: the comparability
1943 /// characterization on the closed-set-paired [`ResourceLimits`]
1944 /// posture binds through the just-lifted [`Self::leq`] and
1945 /// [`Self::geq`] primitives (both const-evaluable, both
1946 /// delegating to [`Self::leq`]) with a two-arm disjunction on the
1947 /// direct forms — no new dep, no supertrait bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
1948 /// throughout so the comparability verdict is a compile-time
1949 /// expression at every preset-tuple call site.
1950 #[must_use]
1951 pub const fn is_comparable(self, other: Self) -> bool {
1952 self.leq(other) || self.geq(other)
1953 }
1954
1955 /// Pointwise partial-order VERDICT across the six ceilings — the
1956 /// `Option<Ordering>` corner on the pairwise-relation surface. Folds
1957 /// the four possible outcomes ([`Ordering::Less`], [`Ordering::Equal`],
1958 /// [`Ordering::Greater`], INCOMPARABLE) into ONE typed value, so a
1959 /// caller that wants "how do these two postures relate?" gets the
1960 /// FULL verdict in a single call rather than composing
1961 /// [`Self::leq`] / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] /
1962 /// `PartialEq::eq` at its callsite.
1963 ///
1964 /// The `Option<Ordering>` PROJECTION on the pairwise-relation surface —
1965 /// the DIRECT LIFT of the frontier inspiration the just-lifted
1966 /// [`Self::is_incomparable`] + [`Self::is_comparable`] pair explicitly
1967 /// named (Rust's `PartialOrd::partial_cmp`, Haskell's
1968 /// `Data.PartialOrd.compare :: a -> a -> Maybe Ordering`, Julia's
1969 /// `Base.Order`, Coq's `PreOrder` decidability obligation). Those two
1970 /// projections pinned ONE ARM each of the surface — the antichain
1971 /// arm ([`Self::is_incomparable`] → `is_none()`) and the comparability
1972 /// arm ([`Self::is_comparable`] → `is_some()`); THIS lift closes the
1973 /// surface at its FULL VERDICT corner by returning the ordering
1974 /// direction on the comparable arm and `None` on the antichain arm —
1975 /// the (Less, Equal, Greater, None) 4-tile partition of the ordered
1976 /// pair × verdict surface at ONE named primitive rather than a
1977 /// four-way conditional composition at every consumer that wanted
1978 /// the full verdict.
1979 ///
1980 /// **Antichain-arm identity**: `a.partial_cmp(b).is_none() ==
1981 /// a.is_incomparable(b)` — the `None` arm of the return coincides
1982 /// EXACTLY with the antichain-corner projection [`Self::is_incomparable`]
1983 /// lifts. On the antichain the two-arm conjunction `!self.leq(other)
1984 /// && !self.geq(other)` holds; neither branch of THIS method's
1985 /// disjunction closes, so the fall-through arm binds. Pinned as a
1986 /// substrate-level identity in the test cohort.
1987 ///
1988 /// **Comparability-arm identity**: `a.partial_cmp(b).is_some() ==
1989 /// a.is_comparable(b)` — the `Some(_)` arm of the return coincides
1990 /// EXACTLY with the comparability-corner projection [`Self::is_comparable`]
1991 /// lifts. On every comparable pair at least one of the two
1992 /// pointwise-domination legs closes, so the method returns a
1993 /// concrete [`Ordering`] variant. Together with the antichain-arm
1994 /// identity, THIS method is the JOINT LIFT of the (comparable,
1995 /// incomparable) two-cell partition into ONE `Option<Ordering>`
1996 /// value.
1997 ///
1998 /// **Equality on the diagonal**: `a.partial_cmp(a) ==
1999 /// Some(Ordering::Equal)` on every posture — the `leq` reflexivity
2000 /// (`a.leq(a) == true`) AND the `geq` reflexivity (`a.geq(a) ==
2001 /// true`) both hold at the diagonal, so both branches of the
2002 /// conjunction close and the equality arm fires. The diagonal cell
2003 /// of the (Less, Equal, Greater, None) 4-tile partition on the
2004 /// ordered pair × verdict surface pins to the Equal tile.
2005 ///
2006 /// **Ordering-direction agreement**: `a.partial_cmp(b) ==
2007 /// Some(Ordering::Less)` iff `a.lt(b)` (strict less); `a.partial_cmp(b)
2008 /// == Some(Ordering::Greater)` iff `a.gt(b)` (strict greater);
2009 /// `a.partial_cmp(b) == Some(Ordering::Equal)` iff `a == b` (via
2010 /// antisymmetry `a.leq(b) && a.geq(b) → a == b`). The four-arm
2011 /// dispatch below binds each verdict tile to its named companion
2012 /// primitive at exactly one implementation site, so a caller that
2013 /// wants the strict-less verdict binds through either `a.lt(b)` OR
2014 /// `matches!(a.partial_cmp(b), Some(Ordering::Less))` and both route
2015 /// to the same substrate.
2016 ///
2017 /// **Antisymmetry under argument swap**: `a.partial_cmp(b) ==
2018 /// b.partial_cmp(a).map(Ordering::reverse)` — swapping the argument
2019 /// pair FLIPS the ordering-direction verdict (`Less <-> Greater`,
2020 /// `Equal` fixed, `None` fixed). Discriminates [`Self::is_incomparable`] /
2021 /// [`Self::is_comparable`] (both SYMMETRIC on argument swap) one
2022 /// POSTURE axis over: the projections carry no directional content
2023 /// and are argument-order-independent; the FULL verdict carries
2024 /// directional content and inverts under swap. Pinned in the test
2025 /// cohort.
2026 ///
2027 /// **Bottom-pole absorption**: `EMPTY_RESOURCE_LIMITS.partial_cmp(a)`
2028 /// returns `Some(Ordering::Less)` for every posture `a` strictly
2029 /// above [`EMPTY_RESOURCE_LIMITS`] on the pointwise partial order,
2030 /// AND `Some(Ordering::Equal)` at `a == EMPTY_RESOURCE_LIMITS`.
2031 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so on
2032 /// every posture the `EMPTY.leq(a)` leg closes; the antisymmetric
2033 /// leg discriminates the strict-less arm (concrete positive posture)
2034 /// from the equality arm (`a == EMPTY`). The DIRECT REFINEMENT of
2035 /// [`Self::is_comparable`]'s bottom-pole absorption pin one
2036 /// DIRECTION-CONTENT axis over — where the comparability projection
2037 /// folds TRUE at the bottom pole across every posture, THIS method
2038 /// discriminates the equality-vs-strict-less refinement on the
2039 /// SAME preset roster.
2040 ///
2041 /// **Top-pole absorption**: symmetric statement — `a.partial_cmp(
2042 /// UNBOUNDED_RESOURCE_LIMITS)` returns `Some(Ordering::Less)` for
2043 /// every posture `a` strictly below the top and
2044 /// `Some(Ordering::Equal)` at `a == UNBOUNDED`. Pinned on the
2045 /// canonical preset roster.
2046 ///
2047 /// **Antichain load-bearing arm**: `HAND_AUTHORED_MID_POSTURE
2048 /// .partial_cmp(HAND_AUTHORED_OTHER_POSTURE) == None` — the two
2049 /// hand-authored asymmetric postures sit on distinct branches
2050 /// (each is smaller on three axes and larger on the other three),
2051 /// so NEITHER pointwise-domination direction closes and the
2052 /// fall-through `None` arm fires. The DIRECT LIFT of
2053 /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin AND
2054 /// [`Self::is_comparable`]'s antichain-arm FALSIFIES pin one
2055 /// PROJECTION axis over on the SAME hand-authored pair — the two
2056 /// projections pinned each ARM's cell verdict; THIS method pins
2057 /// the FULL `Option<Ordering>` return at the antichain corner
2058 /// (`None`).
2059 ///
2060 /// Encoded as `if self.leq(other) && other.leq(self) { Equal } else
2061 /// if self.leq(other) { Less } else if other.leq(self) { Greater }
2062 /// else { None }` — a four-arm dispatch on the (`self.leq(other)`,
2063 /// `other.leq(self)`) 2×2 boolean truth-table, with each arm binding
2064 /// to its named companion primitive at exactly one implementation
2065 /// site so a future re-derivation of [`Self::leq`] propagates to
2066 /// EVERY ordering-direction verdict mechanically rather than
2067 /// requiring a per-arm fix-up. The `self.leq(other) && other.leq(self)`
2068 /// conjunction is the pointwise antisymmetric-equality
2069 /// characterization — pinned as a substrate-level identity by the
2070 /// `resource_limits_leq_is_antisymmetric` test cohort, so the
2071 /// Equal-arm binding above is a substrate-level THEOREM rather than
2072 /// an ad-hoc composition.
2073 ///
2074 /// `const fn` for the same compile-time-pin reasons as [`Self::leq`]
2075 /// / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] / [`Self::is_incomparable`]
2076 /// / [`Self::is_comparable`] one COVER axis over on the pairwise-
2077 /// relation surface (`const _: () = assert!(matches!(
2078 /// EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
2079 /// Some(Ordering::Less)));`) — a caller can pin a full-verdict
2080 /// identity at compile time as a build-break rather than a runtime
2081 /// `assert!` on the first execution. [`Ordering`] itself is
2082 /// `const`-constructible (the three variants are plain enum
2083 /// constructors), so the return-value cascade below stays inside
2084 /// stable const-fn scope with no runtime allocation, no supertrait
2085 /// bound, and no new dep.
2086 ///
2087 /// Pre-lift, a caller that wanted the FULL pointwise ordering
2088 /// verdict either (a) composed four separate calls
2089 /// (`a.lt(b)` / `a == b` / `a.gt(b)` / `a.is_incomparable(b)`) at
2090 /// its callsite and dispatched on the resulting 4-way conditional —
2091 /// a per-consumer FOUR-primitive composition whose exhaustiveness
2092 /// the type system did NOT gate (a dropped arm silently miscategorized
2093 /// the pair) — or (b) invoked [`Self::is_comparable`] to get the
2094 /// `is_some`/`is_none` distinction and THEN chose one of `lt` / `gt`
2095 /// / equality on the `is_some` arm — a two-stage composition that
2096 /// double-evaluates the `leq` / `geq` primitives on every consumer.
2097 /// Post-lift the full verdict is ONE named primitive returning
2098 /// `Option<Ordering>`, `const fn`-composable into a compile-time
2099 /// bound, and the two-primitive `leq(self, other)` + `leq(other, self)`
2100 /// substrate binds at ONE implementation site — a ≥2 PRIME
2101 /// DIRECTIVE trigger, since the four-way ordering dispatch appeared
2102 /// inline at every prospective full-verdict callsite pre-lift.
2103 ///
2104 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2105 /// preserves proofs; the FULL pointwise partial-order verdict on
2106 /// two preset-carried resource proofs is itself a typed
2107 /// `Option<Ordering>` value whose four arms each carry a distinct
2108 /// substrate-level relation proof (`Less` ⇒ strict-less on every
2109 /// axis; `Equal` ⇒ pointwise equality on every axis; `Greater` ⇒
2110 /// strict-greater on every axis; `None` ⇒ antichain — asymmetric
2111 /// per-axis orderings). Pinning the projection at ONE typed
2112 /// primitive makes the (Less, Equal, Greater, None) 4-tile
2113 /// partition of the ordered pair × verdict surface a substrate-
2114 /// level THEOREM rather than a per-consumer inline four-way
2115 /// dispatch. THEORY.md §V.1 — knowable platform; the full-verdict
2116 /// projection was an unnamed four-primitive composition recurring
2117 /// at every prospective "how do these two postures relate?"
2118 /// callsite pre-lift.
2119 ///
2120 /// Frontier inspiration: Rust's own `PartialOrd::partial_cmp :: fn
2121 /// partial_cmp(&self, &Self) -> Option<Ordering>` — the CANONICAL
2122 /// `Option<Ordering>` corner on any partial-order-carrying type;
2123 /// Haskell's `Data.PartialOrd.compare :: PartialOrd a => a -> a
2124 /// -> Maybe Ordering`; Julia's `Base.Order.NoOrder` sentinel folded
2125 /// into `Option<Ordering>` on the `Base.Order`-compatible partial
2126 /// order; Coq's `PreOrder` + `Decidable` typeclass composition
2127 /// giving `forall a b, {leq a b} + {leq b a} + {~(leq a b) /\
2128 /// ~(leq b a)}` — the three-way decidable sum where the third
2129 /// disjunct is the antichain corner. Translation through pleme-io
2130 /// primitives: the full-verdict projection binds through the just-
2131 /// lifted [`Self::leq`] primitive (invoked twice — once in each
2132 /// argument order — to derive the (`self.leq(other)`,
2133 /// `other.leq(self)`) 2×2 truth-table the four-arm dispatch reads
2134 /// against), no new dep, no supertrait bound (`Sized + Copy +
2135 /// 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
2136 /// throughout. This method does NOT wire an `impl PartialOrd for
2137 /// ResourceLimits` — the derive would give lexicographic ordering
2138 /// on the six fields (WRONG for the pointwise lattice), and manually
2139 /// implementing the trait would expose `<` / `<=` / `>` / `>=`
2140 /// operators that overlap the named `lt` / `leq` / `gt` / `geq`
2141 /// primitives already lifted one COVER axis over. Keeping the
2142 /// `Option<Ordering>` corner at an inherent method preserves the
2143 /// tight name-directed surface without introducing an operator
2144 /// alias for each of the four directional predicates.
2145 #[must_use]
2146 pub const fn partial_cmp(self, other: Self) -> Option<Ordering> {
2147 let self_leq = self.leq(other);
2148 let other_leq = other.leq(self);
2149 if self_leq && other_leq {
2150 Some(Ordering::Equal)
2151 } else if self_leq {
2152 Some(Ordering::Less)
2153 } else if other_leq {
2154 Some(Ordering::Greater)
2155 } else {
2156 None
2157 }
2158 }
2159
2160 /// Boolean `is_comparable`-conjunction across every distinct pair of a
2161 /// slice of postures — `postures` is a CHAIN (a totally-ordered subset of
2162 /// the pointwise partial-order lattice) iff every distinct pair
2163 /// `(postures[i], postures[j])` sits on the same lattice branch
2164 /// (`postures[i].is_comparable(postures[j])`).
2165 ///
2166 /// The SET-LEVEL N-ary peer of [`Self::is_comparable`] one CARDINALITY
2167 /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2168 /// primitive surface. Where [`Self::is_comparable`] decides the pairwise
2169 /// question "do THESE TWO postures sit on the same branch?", THIS
2170 /// projection decides the set-level question "does EVERY PAIR in this
2171 /// slice sit on the same branch?" — the natural lift of the pair-level
2172 /// verdict to the collection level, matching the arity extension the
2173 /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] pair carries
2174 /// one PRIMITIVE-KIND axis over on the (leq, comparable) × (pairwise,
2175 /// N-ary) primitive surface.
2176 ///
2177 /// **Empty-slice vacuous truth**: `ResourceLimits::is_chain(&[]) == true`
2178 /// — the empty conjunction is vacuously true; the empty slice contains
2179 /// no distinct pairs to reject. Peer of
2180 /// `is_lower_bound_of(&[]) == true`'s empty-slice identity one
2181 /// PRIMITIVE-KIND axis over. Pinned as
2182 /// `resource_limits_is_chain_empty_slice_is_vacuously_true`.
2183 ///
2184 /// **Singleton vacuous truth**: `ResourceLimits::is_chain(&[a]) == true`
2185 /// for every posture `a` — a one-element slice contains no distinct
2186 /// pairs, so the outer-and-inner-index conjunction never enters the
2187 /// inner loop and returns `true` vacuously. Pinned as
2188 /// `resource_limits_is_chain_singleton_is_vacuously_true`.
2189 ///
2190 /// **Diagonal-duplicate identity**: `ResourceLimits::is_chain(&[a, a])
2191 /// == true` for every posture `a` — [`Self::is_comparable`] is
2192 /// REFLEXIVE (`a.is_comparable(a) == true`), so the single distinct
2193 /// index pair `(0, 1)` binds `a.is_comparable(a) == true` and the
2194 /// conjunction holds. Distinguishes it from [`Self::is_antichain`] one
2195 /// COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2196 /// `false` (via [`Self::is_incomparable`]'s IRREFLEXIVITY). Pinned as
2197 /// `resource_limits_is_chain_of_diagonal_duplicate_is_true`.
2198 ///
2199 /// **Ordered-chain closure**: `ResourceLimits::is_chain(
2200 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2201 /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple on
2202 /// the bounded-lattice diagonal is an ascending chain (`EMPTY <=
2203 /// DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among the
2204 /// three is comparable. Pinned as
2205 /// `resource_limits_is_chain_holds_on_the_shipped_preset_triple`.
2206 ///
2207 /// **Antichain rejection**: `ResourceLimits::is_chain(
2208 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2209 /// — the two hand-authored asymmetric postures sit on distinct branches
2210 /// (each is smaller on three axes and larger on the other three), so
2211 /// neither pointwise-domination direction closes and the pair is
2212 /// INCOMPARABLE. The chain conjunction rejects at the first
2213 /// incomparable pair. Pinned as
2214 /// `resource_limits_is_chain_rejects_the_hand_authored_antichain_pair`.
2215 ///
2216 /// **Mixed-set rejection**: `ResourceLimits::is_chain(
2217 /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2218 /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the first two
2219 /// AND the first-and-third pairs are comparable in isolation, the
2220 /// second-and-third pair is incomparable, so the whole-set conjunction
2221 /// rejects. The set-level chain verdict is stricter than the union of
2222 /// its pair-level verdicts — one antichain pair anywhere in the slice
2223 /// falsifies the whole projection. Pinned as
2224 /// `resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair`.
2225 ///
2226 /// **`most_permissive_of` closure witness**: on every slice `postures`
2227 /// where `ResourceLimits::is_chain(postures)` holds, the N-ary join
2228 /// `most_permissive_of(postures)` is a MEMBER of the slice (not just a
2229 /// bound above it) — the maximum element of a chain sits inside the
2230 /// chain. The set-level verdict thus underwrites the pointwise-`max`
2231 /// identification of the chain's top element. NOT pinned as a test in
2232 /// this cohort (the general witness needs a slice-membership predicate
2233 /// not yet lifted); recorded here as the future callsite the chain
2234 /// verdict unlocks.
2235 ///
2236 /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2237 /// i+1..n`) with one [`Self::is_comparable`] delegation per distinct
2238 /// pair. The `j = i + 1` inner starting index skips the diagonal AND
2239 /// the mirror pairs (`(j, i)` pairs the outer loop already visited from
2240 /// the `(i, j)` direction) — [`Self::is_comparable`] is SYMMETRIC on
2241 /// argument swap, so the mirror pair carries no additional information
2242 /// and the (n choose 2) unique-pair count is the tight iteration budget
2243 /// for the set-level verdict. `postures[i].is_comparable(postures[j])`
2244 /// (not `.leq(...) || .geq(...)` directly) so the primitive delegation
2245 /// lives at exactly one implementation site — a future re-derivation of
2246 /// [`Self::is_comparable`] (e.g. to a different comparability
2247 /// characterization) propagates to `is_chain` mechanically rather than
2248 /// requiring a per-callsite fix-up.
2249 ///
2250 /// `const fn` for the same compile-time-pin reasons as
2251 /// [`Self::is_comparable`] one CARDINALITY axis over on the (comparable,
2252 /// incomparable) × (pair-level, set-level) primitive surface (`const _:
2253 /// () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS,
2254 /// DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));`), so a
2255 /// caller can pin a chain-membership identity at compile time — a
2256 /// build-break rather than a runtime `assert!` on the first execution.
2257 /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the slice
2258 /// by value without an explicit `.clone()` (which const-fn would not
2259 /// permit anyway).
2260 ///
2261 /// Pre-lift, a caller wanting "is this slice a chain?" composed the
2262 /// doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2263 /// postures[i + 1..].iter().all(|b| a.is_comparable(*b)))` two-primitive
2264 /// scaffolding at every prospective callsite — a PRIME DIRECTIVE ≥2
2265 /// pattern once two consumers need the set-level chain verdict, and one
2266 /// whose exhaustiveness the type system did NOT gate (a slice iterator
2267 /// that stopped at the first `false` still needed the caller to bind
2268 /// the outer-and-inner-index conjunction correctly, and a copy-paste
2269 /// that swapped `postures[i]` with `postures[j]` would test the SAME
2270 /// pair via [`Self::is_comparable`]'s symmetry — silent no-op — but a
2271 /// copy-paste that inverted the primitive delegation to
2272 /// [`Self::is_incomparable`] would test the DUAL question and silently
2273 /// distort the verdict). Post-lift the chain verdict binds at ONE typed
2274 /// method the algebra exposes, and the doubly-indexed all-pairs
2275 /// scaffolding lives at ONE implementation site.
2276 ///
2277 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2278 /// proofs; the set-level chain verdict on N preset-carried resource
2279 /// proofs is itself a typed named `bool` predicate composing them via
2280 /// the underlying pair-level [`Self::is_comparable`] projection.
2281 /// THEORY.md §V.1 — knowable platform; the doubly-indexed all-pairs
2282 /// comparability conjunction becomes a TYPE-level operation on the
2283 /// posture algebra rather than an inline two-primitive scaffolding at
2284 /// every consumer that decides "is this slice a totally-ordered
2285 /// subset?"
2286 ///
2287 /// Frontier inspiration: Haskell's `Data.List.isSorted`
2288 /// (adjacent-pair-comparability variant on `Ord a`); Coq's `Sorted`
2289 /// inductive on lists over a `TotalOrder`; Idris's `Sorted` predicate
2290 /// over `Ord`-carrying lists; the order-theoretic notion of a CHAIN
2291 /// (a totally-ordered subset of a poset) canonicalized in
2292 /// Birkhoff's *Lattice Theory* and every subsequent poset text.
2293 /// Translation through pleme-io primitives: on a `PartialOrd`-derived
2294 /// carrier the frontier `isSorted` variants reduce to adjacent-pair
2295 /// comparability because total-order transitivity closes the chain; on
2296 /// a PARTIAL order (the pointwise lattice) the adjacent-pair check is
2297 /// INSUFFICIENT (three postures `a, b, c` where `a` and `c` are
2298 /// comparable but `b` is incomparable to at least one of them satisfy
2299 /// adjacent-pair comparability without forming a chain), so this
2300 /// projection walks EVERY distinct pair rather than only the adjacent
2301 /// ones — the strict partial-order generalization of the frontier
2302 /// total-order predicate. No new dep, no supertrait bound, `const fn`
2303 /// throughout.
2304 #[must_use]
2305 pub const fn is_chain(postures: &[Self]) -> bool {
2306 let n = postures.len();
2307 let mut i = 0;
2308 while i < n {
2309 let mut j = i + 1;
2310 while j < n {
2311 if !postures[i].is_comparable(postures[j]) {
2312 return false;
2313 }
2314 j += 1;
2315 }
2316 i += 1;
2317 }
2318 true
2319 }
2320
2321 /// Boolean `is_incomparable`-conjunction across every distinct pair of a
2322 /// slice of postures — `postures` is an ANTICHAIN (a pairwise-
2323 /// incomparable subset of the pointwise partial-order lattice) iff every
2324 /// distinct pair `(postures[i], postures[j])` sits on distinct branches
2325 /// (`postures[i].is_incomparable(postures[j])`).
2326 ///
2327 /// The SET-LEVEL N-ary peer of [`Self::is_incomparable`] one CARDINALITY
2328 /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2329 /// primitive surface — the DIRECT SET-LEVEL DUAL of [`Self::is_chain`]
2330 /// one COVER-COMPLEMENT axis over on the SAME set-level cardinality
2331 /// slice. Together the two close the (chain, antichain) two-cell face
2332 /// on the set-level pairwise-relation surface, exactly ONE CARDINALITY
2333 /// axis over from the (comparable, incomparable) two-cell partition of
2334 /// the ordered-pair × verdict surface [`Self::is_comparable`] and
2335 /// [`Self::is_incomparable`] closed one arity down.
2336 ///
2337 /// The (chain, antichain) pair is NOT a partition of the set-level
2338 /// verdict surface: a slice of ≥3 postures whose pair-level verdicts
2339 /// mix comparable and incomparable pairs is neither a chain nor an
2340 /// antichain, so the two set-level projections carry a THREE-cell
2341 /// face (chain, antichain, mixed) at the set level whose ends the two
2342 /// projections lifted here pin. Distinguishes it from the pair-level
2343 /// (comparable, incomparable) partition [`Self::is_comparable`] +
2344 /// [`Self::is_incomparable`] EXHAUSTIVELY closed one arity down: at
2345 /// arity 2 every pair is exactly one of comparable-or-incomparable, and
2346 /// the two projections partition the ordered-pair × verdict surface; at
2347 /// arity ≥3 the mixed-set cell opens up between them and the two
2348 /// set-level projections carry the ends of that three-cell face.
2349 ///
2350 /// **Empty-slice vacuous truth**: `ResourceLimits::is_antichain(&[])
2351 /// == true` — the empty conjunction is vacuously true; the empty slice
2352 /// contains no distinct pairs to reject. Peer of [`Self::is_chain`]'s
2353 /// empty-slice identity one COVER-COMPLEMENT axis over: both cells of
2354 /// the (chain, antichain) set-level pair AGREE at the empty slice on
2355 /// the vacuous-truth verdict — the two cells coincide at the empty
2356 /// face of the set-level verdict surface. Pinned as
2357 /// `resource_limits_is_antichain_empty_slice_is_vacuously_true`.
2358 ///
2359 /// **Singleton vacuous truth**: `ResourceLimits::is_antichain(&[a])
2360 /// == true` for every posture `a` — a one-element slice contains no
2361 /// distinct pairs, so the outer-and-inner-index conjunction never
2362 /// enters the inner loop and returns `true` vacuously. Peer of
2363 /// [`Self::is_chain`]'s singleton-vacuous-truth identity: both cells
2364 /// AGREE at singleton slices too — the (chain, antichain) verdict
2365 /// surface coincides at every slice with strictly fewer than two
2366 /// distinct pairs. Pinned as
2367 /// `resource_limits_is_antichain_singleton_is_vacuously_true`.
2368 ///
2369 /// **Diagonal-duplicate rejection**: `ResourceLimits::is_antichain(
2370 /// &[a, a]) == false` for every posture `a` — [`Self::is_incomparable`]
2371 /// is IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
2372 /// distinct index pair `(0, 1)` binds `a.is_incomparable(a) == false`
2373 /// and the conjunction rejects. Distinguishes it from [`Self::is_chain`]
2374 /// one COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2375 /// `true` (via [`Self::is_comparable`]'s REFLEXIVITY). The reflexivity/
2376 /// irreflexivity divergence between the two pair-level projections
2377 /// PROPAGATES to the set-level projections at any slice with a
2378 /// duplicated element — the two set-level cells DIVERGE at every
2379 /// diagonal-duplicate slice, the mirror of their AGREEMENT at every
2380 /// empty-or-singleton slice one CARDINALITY axis over. Pinned as
2381 /// `resource_limits_is_antichain_of_diagonal_duplicate_is_false`.
2382 ///
2383 /// **Hand-authored antichain closure**: `ResourceLimits::is_antichain(
2384 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2385 /// — the two hand-authored asymmetric postures sit on distinct
2386 /// branches (each is smaller on three axes and larger on the other
2387 /// three), so neither pointwise-domination direction closes and the
2388 /// pair is INCOMPARABLE. The antichain conjunction accepts every
2389 /// distinct pair of the slice. Pinned as
2390 /// `resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair`.
2391 ///
2392 /// **Chain rejection**: `ResourceLimits::is_antichain(
2393 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]) == false` — the
2394 /// shipped-preset pair on the bounded-lattice diagonal is a strict
2395 /// chain (`EMPTY.lt(DEFAULT)`), so the single distinct pair is
2396 /// COMPARABLE and the antichain conjunction rejects. The DIRECT
2397 /// FALSIFICATION of [`Self::is_chain`]'s ordered-chain closure one
2398 /// CELL axis over on the SAME slice — the two set-level cells carry
2399 /// the negation-paired verdict at every strictly-chained slice, the
2400 /// mirror of their AGREEMENT at every empty-or-singleton slice one
2401 /// CARDINALITY axis over. Pinned as
2402 /// `resource_limits_is_antichain_rejects_the_shipped_preset_pair`.
2403 ///
2404 /// **Mixed-set rejection**: `ResourceLimits::is_antichain(
2405 /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2406 /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the
2407 /// second-and-third pair is incomparable, the first pair
2408 /// (`DEFAULT`-and-`MID`) is comparable, so the whole-set conjunction
2409 /// rejects. Pins the three-cell (chain, antichain, mixed) set-level
2410 /// face at the mixed-set cell: the same slice both [`Self::is_chain`]
2411 /// AND [`Self::is_antichain`] reject at, and the third cell of the
2412 /// three-cell face the two set-level projections carry the ends of.
2413 /// Pinned as
2414 /// `resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair`.
2415 ///
2416 /// **De Morgan mirror at arity 2**: on any two-element slice `[a, b]`
2417 /// (`a != b`), `ResourceLimits::is_antichain(&[a, b]) ==
2418 /// !ResourceLimits::is_chain(&[a, b])` — at exactly two DISTINCT
2419 /// elements the set-level (chain, antichain) two-cell face reduces to
2420 /// the pair-level (comparable, incomparable) two-cell partition and
2421 /// the two cells become MUTUALLY EXCLUSIVE (the mixed cell requires
2422 /// ≥3 elements to open). The set-level De Morgan mirror of the
2423 /// pair-level identity `a.is_comparable(b) == !a.is_incomparable(b)`
2424 /// [`Self::is_comparable`] pins one CARDINALITY axis over. Pinned as
2425 /// `resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs`.
2426 ///
2427 /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2428 /// i+1..n`) with one [`Self::is_incomparable`] delegation per distinct
2429 /// pair — the SAME iteration structure as [`Self::is_chain`] one
2430 /// PRIMITIVE-DELEGATION axis over, with only the inner primitive
2431 /// swapped between the two. The two set-level projections share the
2432 /// SAME two-primitive substrate (the doubly-indexed pair walk + one
2433 /// pair-level projection) and diverge only on the pair-level primitive
2434 /// they delegate to (`is_comparable` vs `is_incomparable`), which De
2435 /// Morgan's law pins as the pair-level negation-paired substrate.
2436 ///
2437 /// `const fn` for the same compile-time-pin reasons as
2438 /// [`Self::is_incomparable`] one CARDINALITY axis over on the
2439 /// (comparable, incomparable) × (pair-level, set-level) primitive
2440 /// surface (`const _: () = assert!(ResourceLimits::is_antichain(
2441 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]));`), so
2442 /// a caller can pin an antichain-membership identity at compile time
2443 /// — a build-break rather than a runtime `assert!` on the first
2444 /// execution.
2445 ///
2446 /// Pre-lift, a caller wanting "is this slice an antichain?" composed
2447 /// the doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2448 /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))` two-
2449 /// primitive scaffolding at every prospective callsite — the SAME
2450 /// PRIME DIRECTIVE ≥2 pattern [`Self::is_chain`]'s pre-lift shape
2451 /// carried, with only the pair-level primitive swapped. Post-lift the
2452 /// antichain verdict binds at ONE typed method the algebra exposes,
2453 /// and the doubly-indexed all-pairs scaffolding lives at ONE
2454 /// implementation site — the same site [`Self::is_chain`] delegates
2455 /// to, factored down to a per-projection choice of pair-level
2456 /// primitive.
2457 ///
2458 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2459 /// proofs; the set-level antichain verdict on N preset-carried
2460 /// resource proofs is itself a typed named `bool` predicate composing
2461 /// them via the underlying pair-level [`Self::is_incomparable`]
2462 /// projection. THEORY.md §V.1 — knowable platform; the doubly-indexed
2463 /// all-pairs incomparability conjunction becomes a TYPE-level
2464 /// operation on the posture algebra rather than an inline two-
2465 /// primitive scaffolding at every consumer that decides "is this
2466 /// slice a pairwise-incomparable subset?"
2467 ///
2468 /// Frontier inspiration: the order-theoretic notion of an ANTICHAIN
2469 /// (a pairwise-incomparable subset of a poset) canonicalized in
2470 /// Dilworth's theorem (a poset's width — the maximum antichain size —
2471 /// equals the minimum number of chains covering the poset) and the
2472 /// Robinson–Schensted correspondence's Young-tableau chain/antichain
2473 /// duality; Haskell's `Data.List` idiom of
2474 /// `all (uncurry incomparable) . pairs` for a partial-order carrier;
2475 /// Coq's `Antichain` inductive on lists over a `PartialOrder`.
2476 /// Translation through pleme-io primitives: the doubly-indexed pair
2477 /// walk directly, with [`Self::is_incomparable`] as the pair-level
2478 /// primitive, no new dep, no supertrait bound, `const fn` throughout.
2479 /// The (chain, antichain) set-level pair opens the door to Dilworth's
2480 /// width invariant on the [`ResourceLimits`] lattice at a future
2481 /// callsite — the maximum antichain size is a substrate-level
2482 /// dimension the two set-level projections lift here jointly
2483 /// characterize.
2484 #[must_use]
2485 pub const fn is_antichain(postures: &[Self]) -> bool {
2486 let n = postures.len();
2487 let mut i = 0;
2488 while i < n {
2489 let mut j = i + 1;
2490 while j < n {
2491 if !postures[i].is_incomparable(postures[j]) {
2492 return false;
2493 }
2494 j += 1;
2495 }
2496 i += 1;
2497 }
2498 true
2499 }
2500
2501 /// The MIDDLE-CELL corner of the (chain, antichain, mixed) three-cell
2502 /// face on the set-level pairwise-relation surface — `postures` is a
2503 /// MIXED slice iff it is NEITHER a chain NOR an antichain, computed
2504 /// as the boolean conjunction of the negations of the just-lifted
2505 /// [`Self::is_chain`] and [`Self::is_antichain`] projections.
2506 ///
2507 /// EXHAUSTIVELY CLOSES the (chain, antichain, mixed) three-cell face
2508 /// at its previously-open middle cell — the two set-level ENDS the
2509 /// just-lifted [`Self::is_chain`] / [`Self::is_antichain`] pair
2510 /// pinned already carried the (chain, antichain) two ends; this
2511 /// projection binds the third cell (F, F) as a NAMED typed `bool`.
2512 /// The three cells now sit at (T, F) — pure chain, (F, T) — pure
2513 /// antichain, and (F, F) — mixed. The fourth cell (T, T) is only
2514 /// achievable at the degenerate empty-or-singleton slice where BOTH
2515 /// projections agree on the vacuous-truth verdict — [`Self::is_mixed`]
2516 /// reports `false` there too, aligning with the intuition that a
2517 /// slice with strictly fewer than two distinct pairs is neither
2518 /// chain-mixed nor antichain-mixed.
2519 ///
2520 /// **Empty-slice contract**: `ResourceLimits::is_mixed(&[]) == false`
2521 /// — [`Self::is_chain`] and [`Self::is_antichain`] BOTH return `true`
2522 /// vacuously, so `!true && !true == false`. The empty slice is NOT
2523 /// mixed. Pinned as
2524 /// `resource_limits_is_mixed_empty_slice_is_false`.
2525 ///
2526 /// **Singleton contract**: `ResourceLimits::is_mixed(&[a]) == false`
2527 /// for every posture `a` — a one-element slice contains no distinct
2528 /// pairs; both [`Self::is_chain`] and [`Self::is_antichain`] return
2529 /// `true` vacuously. Pinned as
2530 /// `resource_limits_is_mixed_singleton_is_false`.
2531 ///
2532 /// **Diagonal-duplicate contract**: `ResourceLimits::is_mixed(&[a, a])
2533 /// == false` for every posture `a` — [`Self::is_chain`] returns
2534 /// `true` via [`Self::is_comparable`]'s REFLEXIVITY;
2535 /// [`Self::is_antichain`] returns `false` via
2536 /// [`Self::is_incomparable`]'s IRREFLEXIVITY; the conjunction is
2537 /// `!true && !false == false`. The diagonal-duplicate slice is a
2538 /// CHAIN, not mixed. Pinned as
2539 /// `resource_limits_is_mixed_of_diagonal_duplicate_is_false`.
2540 ///
2541 /// **Chain rejection**: `ResourceLimits::is_mixed(
2542 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2543 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the shipped-preset triple
2544 /// on the bounded-lattice diagonal is an ascending chain,
2545 /// [`Self::is_chain`] returns `true`, and `!true && …` short-
2546 /// circuits to `false`. Pinned as
2547 /// `resource_limits_is_mixed_rejects_the_shipped_preset_triple`.
2548 ///
2549 /// **Antichain rejection**: `ResourceLimits::is_mixed(
2550 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) ==
2551 /// false` — the hand-authored antichain pair binds
2552 /// [`Self::is_antichain`] `true`, and `!… && !true == false`. Pinned
2553 /// as
2554 /// `resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair`.
2555 ///
2556 /// **Mixed-set closure (LOAD-BEARING `true`-arm catch)**:
2557 /// `ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS,
2558 /// HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2559 /// — the (DEFAULT, MID) pair is comparable (falsifying
2560 /// [`Self::is_antichain`]) while the (MID, OTHER) pair is
2561 /// incomparable (falsifying [`Self::is_chain`]); the conjunction
2562 /// `!false && !false == true`. This is the SAME fixture the sibling
2563 /// [`Self::is_chain`] and [`Self::is_antichain`] BOTH reject at —
2564 /// pinning the DISCRIMINATING positive-arm catch for the (mixed)
2565 /// cell of the three-cell face. Pinned as
2566 /// `resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair`.
2567 ///
2568 /// **Trichotomy exhaustiveness at arity ≥3 with distinct
2569 /// elements**: for every slice `postures` with `len() >= 2` and
2570 /// pairwise-distinct elements, EXACTLY ONE of [`Self::is_chain`],
2571 /// [`Self::is_antichain`], [`Self::is_mixed`] returns `true` — the
2572 /// three cells partition the set-level verdict surface at every
2573 /// non-degenerate slice. At arity < 2 (empty and singleton) the
2574 /// verdict surface degenerates to the vacuous-truth (T, T, F)
2575 /// cell; at arity ≥2 with any duplicated element the verdict
2576 /// surface degenerates to the chain-only (T, F, F) cell (chain
2577 /// wins via reflexivity, antichain drops via irreflexivity).
2578 /// Pinned as
2579 /// `resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices`.
2580 ///
2581 /// Encoded as the boolean conjunction of the negations of the two
2582 /// just-lifted set-level projections — one implementation site, ZERO
2583 /// new pair-level primitive delegations, `const fn` throughout via
2584 /// the underlying `const fn` bodies. The doubly-indexed all-pairs
2585 /// walk lives at ONE site (the [`Self::is_chain`] +
2586 /// [`Self::is_antichain`] implementations), and this projection
2587 /// factors THROUGH both without reopening it.
2588 ///
2589 /// Pre-lift, a caller wanting "is this slice a mixed subset (neither
2590 /// chain nor antichain)?" composed the doubly-indexed
2591 /// `!postures.iter().enumerate().all(|(i, a)|
2592 /// postures[i + 1..].iter().all(|b| a.is_comparable(*b))) &&
2593 /// !postures.iter().enumerate().all(|(i, a)|
2594 /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))`
2595 /// two-scaffolding conjunction at every prospective callsite —
2596 /// the SAME PRIME DIRECTIVE ≥2 pattern the pair of just-lifted
2597 /// projections' pre-lift shapes carried, doubled. Post-lift the
2598 /// mixed-set verdict binds at ONE typed method the algebra exposes.
2599 ///
2600 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2601 /// preserves proofs; the set-level mixed-set verdict on N preset-
2602 /// carried resource proofs is itself a typed named `bool` predicate
2603 /// composing them via the underlying set-level [`Self::is_chain`] +
2604 /// [`Self::is_antichain`] projections. THEORY.md §V.1 — knowable
2605 /// platform; the doubly-negated set-level conjunction becomes a
2606 /// TYPE-level operation on the posture algebra rather than an inline
2607 /// negation-composition at every consumer that decides "is this
2608 /// slice a mixed subset?"
2609 ///
2610 /// Frontier inspiration: the order-theoretic notion of a MIXED
2611 /// subset (a subset that is neither a chain nor an antichain) sits
2612 /// at the interior of the (chain, antichain, mixed) trichotomy that
2613 /// canonicalizes the SET-LEVEL verdict surface of a poset — a slice
2614 /// whose Hasse-diagram induced subgraph carries both order-comparable
2615 /// AND order-incomparable edges. Dilworth's width invariant and the
2616 /// dual Mirsky theorem (a poset's height equals the minimum number of
2617 /// antichains covering it) both take mixed sets as their non-trivial
2618 /// inputs — a chain has width 1 and a mixed set of width ≥2 is
2619 /// exactly where the width invariant becomes informative. Haskell's
2620 /// `Data.List` idiom of `not . isChain && not . isAntichain`
2621 /// composition on a partial-order carrier. Translation through
2622 /// pleme-io primitives: the boolean conjunction of the negations of
2623 /// the just-lifted set-level projections, no new dep, no supertrait
2624 /// bound, `const fn` throughout.
2625 #[must_use]
2626 pub const fn is_mixed(postures: &[Self]) -> bool {
2627 !Self::is_chain(postures) && !Self::is_antichain(postures)
2628 }
2629
2630 /// Boolean `leq`-conjunction across every CONSECUTIVE pair of a slice
2631 /// of postures — `postures` is ASCENDING (a leq-monotone sequence on
2632 /// the pointwise partial order) iff every consecutive pair
2633 /// `(postures[i], postures[i + 1])` satisfies
2634 /// `postures[i].leq(postures[i + 1])`.
2635 ///
2636 /// The SEQUENCE-LEVEL N-ary peer of [`Self::leq`] one CARDINALITY axis
2637 /// over on the (pairwise, sequence-level) primitive surface — the
2638 /// DIRECT SEQUENCE-LEVEL LIFT of the pair-level partial-order primitive
2639 /// through a consecutive-index walk. Opens a NEW AXIS one CONSECUTIVE-
2640 /// VS-ALL-PAIRS step over from the just-lifted set-level projections
2641 /// [`Self::is_chain`] / [`Self::is_antichain`] / [`Self::is_mixed`]:
2642 /// where the set-level triple quantifies over EVERY distinct pair
2643 /// `(i, j)` with `j > i`, this projection quantifies over only the
2644 /// `n - 1` CONSECUTIVE pairs `(i, i + 1)`. The two axes lift the same
2645 /// pair-level primitive family through DIFFERENT quantification shapes
2646 /// — the set-level triple carries UNORDERED verdicts about the whole
2647 /// slice's pairwise-relation graph, while this projection carries an
2648 /// ORDER-SENSITIVE verdict about the slice's specific enumeration.
2649 ///
2650 /// **Transitivity theorem — `is_ascending ⇒ is_chain`**: for every
2651 /// slice `postures`, `Self::is_ascending(postures) == true` implies
2652 /// `Self::is_chain(postures) == true`. The pointwise partial order is
2653 /// TRANSITIVE (`a.leq(b) && b.leq(c) ⇒ a.leq(c)` — verified structurally
2654 /// by [`Self::leq`]'s six-axis pointwise `<=` conjunction, since each
2655 /// `usize <=` is transitive), so a consecutive-pair leq-chain closes
2656 /// under transitive composition into the all-pairs leq-chain; every
2657 /// (i, j) pair with j > i decomposes into the composition of
2658 /// consecutive-pair leqs (i, i+1), (i+1, i+2), ..., (j-1, j), which
2659 /// transitivity collapses into `postures[i].leq(postures[j])`, and
2660 /// `leq` implies `is_comparable`. This is a LOAD-BEARING substrate-
2661 /// level theorem: the sequence-level ascending verdict is strictly
2662 /// STRONGER than the set-level chain verdict (a chain admits any
2663 /// permutation of a leq-sorted enumeration; an ascending sequence
2664 /// admits exactly the leq-sorted one). Pinned as
2665 /// `resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice`.
2666 ///
2667 /// **Empty-slice vacuous truth**: `ResourceLimits::is_ascending(&[])
2668 /// == true` — the empty conjunction is vacuously true; the empty
2669 /// slice contains no consecutive pairs to reject. Peer of
2670 /// [`Self::is_chain`]'s empty-slice identity one CONSECUTIVE-VS-ALL-
2671 /// PAIRS axis over: both cells of the (set-level, sequence-level)
2672 /// projection pair AGREE at the empty slice on the vacuous-truth
2673 /// verdict — the two cells coincide at the empty face of the N-ary
2674 /// verdict surface. Pinned as
2675 /// `resource_limits_is_ascending_empty_slice_is_vacuously_true`.
2676 ///
2677 /// **Singleton vacuous truth**: `ResourceLimits::is_ascending(&[a])
2678 /// == true` for every posture `a` — a one-element slice contains no
2679 /// consecutive pairs, so the walk never enters the loop and returns
2680 /// `true` vacuously. Peer of [`Self::is_chain`]'s singleton-vacuous-
2681 /// truth identity: both projections AGREE at singleton slices too.
2682 /// Pinned as
2683 /// `resource_limits_is_ascending_singleton_is_vacuously_true`.
2684 ///
2685 /// **Diagonal-duplicate identity**: `ResourceLimits::is_ascending(
2686 /// &[a, a]) == true` for every posture `a` — [`Self::leq`] is
2687 /// REFLEXIVE (`a.leq(a) == true` via each axis's `<=` reflexivity), so
2688 /// the single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
2689 /// the conjunction holds. AGREES with [`Self::is_chain`]'s diagonal-
2690 /// duplicate verdict at every diagonal-duplicate slice — the two
2691 /// projections diverge only where the ORDER matters, and duplicated
2692 /// elements carry no ordering information to distinguish them.
2693 /// Pinned as `resource_limits_is_ascending_of_diagonal_duplicate_is_true`.
2694 ///
2695 /// **Ascending shipped-preset triple closure**: `ResourceLimits::
2696 /// is_ascending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2697 /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple
2698 /// on the bounded-lattice diagonal is a leq-monotone ascending
2699 /// sequence (`EMPTY.leq(DEFAULT).leq(UNBOUNDED)` pointwise), so every
2700 /// consecutive pair satisfies `leq` and the conjunction closes.
2701 /// Pinned as
2702 /// `resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple`.
2703 ///
2704 /// **Descending shipped-preset triple rejection**: `ResourceLimits::
2705 /// is_ascending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2706 /// EMPTY_RESOURCE_LIMITS]) == false` — the reversed enumeration of the
2707 /// same shipped-preset chain is a leq-monotone DESCENDING sequence, so
2708 /// the first consecutive pair binds `UNBOUNDED.leq(DEFAULT) == false`
2709 /// and the walk rejects at index 0. The DIRECT ORDER-SENSITIVITY
2710 /// FALSIFICATION of the ascending closure one PERMUTATION axis over on
2711 /// the SAME chain — [`Self::is_chain`] accepts BOTH orderings (its
2712 /// verdict is order-INSENSITIVE), while this projection accepts only
2713 /// the leq-sorted one. Pinned as
2714 /// `resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple`.
2715 ///
2716 /// **Non-monotone chain permutation rejection**: `ResourceLimits::
2717 /// is_ascending(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,
2718 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the third permutation of
2719 /// the shipped-preset chain has `DEFAULT.leq(EMPTY) == false` at the
2720 /// first consecutive pair, so the walk rejects, EVEN THOUGH the slice
2721 /// is a chain ([`Self::is_chain`] accepts it). Pins the DISCRIMINATING
2722 /// arm between the sequence-level and set-level projections: a chain
2723 /// permutation that is neither ascending nor descending falsifies BOTH
2724 /// sequence-level projections while both set-level projections accept
2725 /// it. Pinned as
2726 /// `resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation`.
2727 ///
2728 /// **Hand-authored antichain rejection**: `ResourceLimits::is_ascending(
2729 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2730 /// — the two hand-authored asymmetric postures are incomparable
2731 /// (neither `MID.leq(OTHER)` nor `OTHER.leq(MID)`), so the single
2732 /// consecutive pair rejects. Pinned as
2733 /// `resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair`.
2734 ///
2735 /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`) with
2736 /// one [`Self::leq`] delegation per consecutive pair — the SAME walk
2737 /// structure the standard-library `<[T]>::is_sorted_by` idiom carries
2738 /// on a totally-ordered carrier, generalized to the partial-order
2739 /// [`Self::leq`] primitive here. `const fn` throughout via the
2740 /// underlying `const fn` [`Self::leq`] body — a caller can pin an
2741 /// ascending-sequence identity at compile time
2742 /// (`const _: () = assert!(ResourceLimits::is_ascending(&[
2743 /// EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2744 /// UNBOUNDED_RESOURCE_LIMITS]));`) — a build-break rather than a
2745 /// runtime `assert!` on the first execution.
2746 ///
2747 /// Pre-lift, a caller wanting "is this slice a leq-monotone ascending
2748 /// sequence?" composed the singly-indexed
2749 /// `postures.windows(2).all(|w| w[0].leq(w[1]))` two-primitive
2750 /// scaffolding at every prospective callsite — the SAME PRIME
2751 /// DIRECTIVE ≥2 pattern the set-level projections' pre-lift shapes
2752 /// carried, one CARDINALITY axis over. Post-lift the ascending
2753 /// verdict binds at ONE typed method the algebra exposes, and the
2754 /// consecutive-pair walk lives at ONE implementation site — the peer
2755 /// [`Self::is_descending`] shares the SAME walk structure and diverges
2756 /// only on the pair-level primitive it delegates to (`geq` in place of
2757 /// `leq`).
2758 ///
2759 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2760 /// proofs; the sequence-level ascending verdict on N preset-carried
2761 /// resource proofs is itself a typed named `bool` predicate composing
2762 /// them via the underlying pair-level [`Self::leq`] projection.
2763 /// THEORY.md §V.1 — knowable platform; the consecutive-pair leq
2764 /// conjunction becomes a TYPE-level operation on the posture algebra
2765 /// rather than an inline windowed scaffolding at every consumer that
2766 /// decides "is this slice a leq-monotone ascending sequence?"
2767 ///
2768 /// Frontier inspiration: the order-theoretic notion of a MONOTONE
2769 /// SEQUENCE on a poset — the standard-library `Iterator::is_sorted`
2770 /// / `<[T]>::is_sorted_by` idioms carry the totally-ordered case;
2771 /// Haskell's `Data.List` idiom of `and . zipWith leq xs . tail xs`
2772 /// on a partial-order carrier; Coq's `Sorted` inductive on lists over
2773 /// a `Relation`. Translation through pleme-io primitives: the singly-
2774 /// indexed consecutive-pair walk directly, with [`Self::leq`] as the
2775 /// pair-level primitive, no new dep, no supertrait bound, `const fn`
2776 /// throughout. The (ascending, descending) sequence-level pair opens
2777 /// the door to `is_strictly_ascending` / `is_strictly_descending` via
2778 /// [`Self::lt`] / [`Self::gt`] one STRICTNESS axis over, and to
2779 /// `is_monotone` (`is_ascending || is_descending`) one DIRECTION-
2780 /// AGNOSTIC axis over, at future callsites.
2781 #[must_use]
2782 pub const fn is_ascending(postures: &[Self]) -> bool {
2783 let n = postures.len();
2784 let mut i = 0;
2785 while i + 1 < n {
2786 if !postures[i].leq(postures[i + 1]) {
2787 return false;
2788 }
2789 i += 1;
2790 }
2791 true
2792 }
2793
2794 /// Boolean `geq`-conjunction across every CONSECUTIVE pair of a slice
2795 /// of postures — `postures` is DESCENDING (a geq-monotone sequence on
2796 /// the pointwise partial order) iff every consecutive pair
2797 /// `(postures[i], postures[i + 1])` satisfies
2798 /// `postures[i].geq(postures[i + 1])`.
2799 ///
2800 /// The DIRECT SEQUENCE-LEVEL DUAL of [`Self::is_ascending`] one PAIR-
2801 /// LEVEL-PRIMITIVE axis over — where `is_ascending` delegates to
2802 /// [`Self::leq`], this projection delegates to [`Self::geq`]. The
2803 /// two together close the (ascending, descending) two-cell face on
2804 /// the sequence-level pairwise-relation surface, exactly one
2805 /// CONSECUTIVE-VS-ALL-PAIRS axis over from the set-level (chain,
2806 /// antichain) two-cell face [`Self::is_chain`] / [`Self::is_antichain`]
2807 /// pin, and exactly one PAIR-LEVEL-PRIMITIVE axis over from the
2808 /// (leq, geq) pair-level two-cell face [`Self::leq`] / [`Self::geq`]
2809 /// pin one CARDINALITY axis down.
2810 ///
2811 /// The (ascending, descending) pair is NOT a partition of the
2812 /// sequence-level verdict surface: a slice of ≥3 postures whose
2813 /// consecutive-pair verdicts mix leq and geq (or contain any
2814 /// incomparable consecutive pair) is neither ascending nor
2815 /// descending, so the two sequence-level projections carry a THREE-
2816 /// cell face (ascending, descending, non-monotone) at the sequence
2817 /// level whose ends the two projections lifted here pin. The
2818 /// (T, T) corner is achievable at every diagonal-duplicate slice
2819 /// AND at every empty-or-singleton slice — [`Self::leq`] and
2820 /// [`Self::geq`] are BOTH REFLEXIVE, so a repeated element passes
2821 /// both consecutive-pair conjunctions.
2822 ///
2823 /// **Transitivity theorem — `is_descending ⇒ is_chain`**: for every
2824 /// slice `postures`, `Self::is_descending(postures) == true` implies
2825 /// `Self::is_chain(postures) == true`, by the SAME transitive
2826 /// composition [`Self::is_ascending`] carries one PAIR-LEVEL-PRIMITIVE
2827 /// axis over — the pointwise partial order is transitive under `geq`
2828 /// (by [`Self::geq`]'s composition `other.leq(self)` and `leq`'s six-
2829 /// axis transitivity), so a consecutive-pair geq-chain closes under
2830 /// transitive composition into an all-pairs geq-chain, and `geq`
2831 /// implies `is_comparable`. Pinned as
2832 /// `resource_limits_is_descending_implies_is_chain_on_every_shipped_slice`.
2833 ///
2834 /// **Empty-slice vacuous truth**: `ResourceLimits::is_descending(&[])
2835 /// == true` — the empty conjunction is vacuously true. Peer of
2836 /// [`Self::is_ascending`]'s empty-slice identity one PAIR-LEVEL-
2837 /// PRIMITIVE axis over: both cells of the (ascending, descending)
2838 /// sequence-level pair AGREE at the empty slice. Pinned as
2839 /// `resource_limits_is_descending_empty_slice_is_vacuously_true`.
2840 ///
2841 /// **Singleton vacuous truth**: `ResourceLimits::is_descending(&[a])
2842 /// == true` for every posture `a`. Pinned as
2843 /// `resource_limits_is_descending_singleton_is_vacuously_true`.
2844 ///
2845 /// **Diagonal-duplicate identity**: `ResourceLimits::is_descending(
2846 /// &[a, a]) == true` for every posture `a` — [`Self::geq`] is
2847 /// REFLEXIVE, so the single consecutive pair binds `a.geq(a) == true`.
2848 /// AGREES with [`Self::is_ascending`]'s diagonal-duplicate verdict —
2849 /// both leq and geq carry reflexivity, so the sequence-level (T, T)
2850 /// corner opens at every diagonal-duplicate slice, distinct from the
2851 /// set-level (T, F) verdict [`Self::is_chain`] / [`Self::is_antichain`]
2852 /// carry on the same slice. Pinned as
2853 /// `resource_limits_is_descending_of_diagonal_duplicate_is_true`.
2854 ///
2855 /// **Descending shipped-preset triple closure**: `ResourceLimits::
2856 /// is_descending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2857 /// EMPTY_RESOURCE_LIMITS]) == true` — the reversed enumeration of the
2858 /// shipped-preset chain is a geq-monotone descending sequence. The
2859 /// DIRECT MIRROR of [`Self::is_ascending`]'s ascending closure one
2860 /// PERMUTATION axis over — pinning the two closures on the SAME chain
2861 /// in REVERSED orderings closes the (ascending, descending) sequence-
2862 /// level pair on the shipped chain. Pinned as
2863 /// `resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple`.
2864 ///
2865 /// **Ascending shipped-preset triple rejection**: `ResourceLimits::
2866 /// is_descending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2867 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the ascending-sorted
2868 /// enumeration binds `EMPTY.geq(DEFAULT) == false`. The DIRECT
2869 /// FALSIFICATION of [`Self::is_ascending`]'s ascending closure one
2870 /// CELL axis over on the SAME slice. Pinned as
2871 /// `resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple`.
2872 ///
2873 /// **Hand-authored antichain rejection**: `ResourceLimits::is_descending(
2874 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2875 /// — the two hand-authored asymmetric postures are incomparable, so
2876 /// the single consecutive pair rejects. Pinned as
2877 /// `resource_limits_is_descending_rejects_the_hand_authored_antichain_pair`.
2878 ///
2879 /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`)
2880 /// with one [`Self::geq`] delegation per consecutive pair — the SAME
2881 /// iteration structure as [`Self::is_ascending`] one PAIR-LEVEL-
2882 /// PRIMITIVE axis over, with only the inner primitive swapped between
2883 /// the two. `const fn` throughout via the underlying `const fn`
2884 /// [`Self::geq`] body.
2885 ///
2886 /// Pre-lift, a caller wanting "is this slice a geq-monotone descending
2887 /// sequence?" composed the singly-indexed
2888 /// `postures.windows(2).all(|w| w[0].geq(w[1]))` two-primitive
2889 /// scaffolding at every prospective callsite. Post-lift the
2890 /// descending verdict binds at ONE typed method the algebra exposes.
2891 ///
2892 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2893 /// proofs. THEORY.md §V.1 — knowable platform.
2894 ///
2895 /// Frontier inspiration: `Iterator::is_sorted_by` with a reversed
2896 /// comparator on a totally-ordered carrier; Coq's `StronglySorted`
2897 /// on a `<=` relation applied with the argument order flipped.
2898 /// Translation through pleme-io primitives: the singly-indexed
2899 /// consecutive-pair walk directly, with [`Self::geq`] as the pair-
2900 /// level primitive.
2901 #[must_use]
2902 pub const fn is_descending(postures: &[Self]) -> bool {
2903 let n = postures.len();
2904 let mut i = 0;
2905 while i + 1 < n {
2906 if !postures[i].geq(postures[i + 1]) {
2907 return false;
2908 }
2909 i += 1;
2910 }
2911 true
2912 }
2913}
2914
2915/// Cache key: (macro name, SipHash-2-4 of args). We hash `Sexp` directly via
2916/// its manual `Hash` impl — no serde_json round-trip per cache lookup.
2917type CacheKey = (String, u64);
2918
2919/// A registered macro definition.
2920#[derive(Debug, Clone)]
2921pub struct MacroDef {
2922 pub name: String,
2923 pub params: MacroParams,
2924 /// The template body (usually a Quasiquote).
2925 pub body: Sexp,
2926}
2927
2928impl MacroDef {
2929 /// Project the macro body to its substitution-walked form: the inner of
2930 /// the outer `Sexp::Quasiquote` when `(defmacro NAME (PARAMS) `(...))`
2931 /// authored the body through the canonical quasi-quote affordance, OR
2932 /// `&self.body` verbatim when authored without one. The two expansion
2933 /// strategies — bytecode (`compile_template`) and substitute (`apply`'s
2934 /// substitute fallback) — both walk this projection, never the raw
2935 /// `body`, because the outer quasi-quote is the syntactic "you're
2936 /// inside a template" marker and the substitution semantics operate on
2937 /// what's INSIDE it. Naming the projection lifts the inline
2938 /// `match &def.body { Sexp::Quasiquote(inner) => inner.as_ref(),
2939 /// other => other }` peel that appeared verbatim at BOTH sites — well
2940 /// past the ≥2 PRIME-DIRECTIVE trigger — into ONE function the two
2941 /// strategies share, so a regression that drifts ONE strategy's
2942 /// body-projection from the other (e.g. one path peels twice and the
2943 /// other peels once, or one path treats `Sexp::Quote(...)` as a
2944 /// template marker and the other doesn't) becomes structurally
2945 /// impossible: there is exactly one implementation both strategies
2946 /// call.
2947 ///
2948 /// Single-level peel by design: a nested `` ``form `` body unwraps to
2949 /// `` `form `` (the inner quasi-quote stays as-is), matching the v0
2950 /// "no nested quasi-quotes" scope the module preamble declares. A
2951 /// non-quasi-quote body — `(defmacro NAME (PARAMS) BODY)` where BODY
2952 /// is a plain `Sexp::List` / `Sexp::Atom` — returns `&self.body`
2953 /// verbatim, the "other" arm of the legacy match. The borrow is
2954 /// strictly `&'a Sexp` rooted in `&'a self.body` (no clone, no
2955 /// allocation); both `compile_node` (bytecode path) and `substitute`
2956 /// (substitute path) consume the projection immediately and never
2957 /// outlive the borrow.
2958 ///
2959 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
2960 /// inline copies of the body-peel match is the ≥2 trigger, and the
2961 /// substrate names the projection ONCE so authoring surfaces and
2962 /// future expansion strategies (a third interpreter? a JIT? a
2963 /// debugger that wants to render the body without the outer
2964 /// quasi-quote marker?) bind to ONE primitive. THEORY.md §II.1
2965 /// invariant 2 — free middle; the two expansion strategies emit
2966 /// IDENTICAL output for the same (macro, args) pair, and sharing one
2967 /// body-projection makes that per-strategy agreement structural at
2968 /// the entry to the walker, not a two-site discipline the
2969 /// `expansion_layers_agree_on_output_and_cache_wins` benchmark only
2970 /// observes after the fact.
2971 #[must_use]
2972 pub fn template_body(&self) -> &Sexp {
2973 match &self.body {
2974 Sexp::Quasiquote(inner) => inner.as_ref(),
2975 other => other,
2976 }
2977 }
2978}
2979
2980/// A macro's parameter list — structurally "zero or more required
2981/// positional params, then zero or more `&optional` params, then an OPTIONAL
2982/// single `&rest` param." This is the canonical Lisp lambda-list ordering
2983/// (Common Lisp `(req* &optional opt* &rest r)`), made a TYPE.
2984///
2985/// This shape promotes the invariants the reader ([`parse_params`])
2986/// previously upheld only by construction — `&rest` is LAST, there is AT MOST
2987/// ONE of it, and (now) `&optional` params sit strictly between the required
2988/// run and the rest — from *unobserved discipline* to *unrepresentable
2989/// state*. The prior representation `Vec<Param>` admitted `[Rest, Required]`
2990/// (a `&rest` in the middle) and `[Rest, Rest]` (two of them); both are
2991/// nonsense the binder cannot honor, yet the type permitted them. The flat
2992/// param INDEX that the bytecode references (`Subst(idx)` / `Splice(idx)`)
2993/// and the positional binder both walk would silently misalign on such a
2994/// `Vec` — a `Rest` at index 0 of `[Rest, Required]` makes the binder grab
2995/// every arg, then fail to bind the trailing `Required`, mapping the
2996/// template's index-1 substitution onto the wrong value. `MacroParams`
2997/// cannot express either shape: `rest` is exactly one `Option<String>`,
2998/// always conceptually after every `required` then every `optional` name,
2999/// and the three kinds live in distinct fields whose order is fixed by the
3000/// struct, not by a discipline the binder trusts a `Vec` to have upheld.
3001///
3002/// `optional` differs from `required` in the binder, not the index contract:
3003/// a required name with no arg at its position is a `MissingMacroArg`
3004/// rejection; an optional name with no arg binds to its declared default form
3005/// — `Sexp::Nil` when none was given, the parsed default literal when one was.
3006/// Both shapes — `&optional x` and `&optional (x 5)` — are now structural in
3007/// the typed [`OptionalParam`] entry rather than smeared across a flat
3008/// `Vec<String>` the binder would have had to discover the default for
3009/// elsewhere.
3010///
3011/// The flat-index contract the template bytecode depends on is preserved by
3012/// [`MacroParams::names`]: index `0..required.len()` are the required names
3013/// in order, the next `optional.len()` indices are the optional names, and
3014/// the final index (if present) is the rest name — the canonical lambda-list
3015/// order. [`MacroParams::bind`] produces the per-index bound values in that
3016/// same order, so the name-keyed (`bind_args` → `substitute`) and
3017/// index-keyed (`apply_compiled`) expansion strategies share ONE binder and
3018/// can never drift.
3019///
3020/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3021/// unrepresentable"; the lambda-list ordering (required → optional → rest,
3022/// rest-is-last, at-most-one-rest) becomes structural. THEORY.md §VI.1 —
3023/// generation over composition; the positional binding loop (verbatim in
3024/// both `bind_args` and `apply_compiled`, the ≥2 PRIME-DIRECTIVE trigger) is
3025/// lifted to ONE owner, `bind`, which the optional arm extends in one place.
3026#[derive(Debug, Clone, Default, PartialEq)]
3027pub struct MacroParams {
3028 pub required: Vec<String>,
3029 pub optional: Vec<OptionalParam>,
3030 pub rest: Option<String>,
3031}
3032
3033/// One entry in a macro's `&optional` section — a name plus an optional
3034/// default form. The two surface shapes the reader admits collapse into this
3035/// single typed shape:
3036///
3037/// * `&optional x` ⇒ `OptionalParam { name: "x", default: None }`
3038/// * `&optional (x 5)` ⇒ `OptionalParam { name: "x", default: Some(Int(5)) }`
3039///
3040/// The `default: Option<Sexp>` slot makes the per-param default-form a
3041/// FIELD on each optional entry, not a discipline a sibling `Vec<Sexp>` would
3042/// have had to maintain in lock-step with `Vec<String>`. Without this shape
3043/// the binder cannot tell "no arg supplied, no default declared → bind nil"
3044/// from "no arg supplied, default `5` declared → bind `5`": both would
3045/// collapse onto `Sexp::Nil`, the precise silent misalignment the typed
3046/// shape exists to forbid.
3047///
3048/// The default is the LITERAL `Sexp` — there is no evaluator in v0, so a
3049/// `(x (foo 1))` spec parks `(foo 1)` verbatim as the bound value when `x`'s
3050/// arg is absent. This is the no-evaluator floor of CL semantics: any
3051/// arbitrary form is admitted at the gate, what it MEANS is the next layer's
3052/// concern. The default is parsed exactly once at `defmacro`/
3053/// `defpoint-template`/`defcheck` time (inside `parse_params`); every call
3054/// to that macro consumes the same parsed `Sexp` via `Clone`, never re-
3055/// reading the source.
3056///
3057/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3058/// unrepresentable"; the (name, default?) pair is one entry rather than two
3059/// parallel `Vec`s a regression could desynchronize. THEORY.md §VI.1 —
3060/// generation over composition; the binder's optional arm consults
3061/// `param.default` in ONE place, so the substitute and bytecode strategies
3062/// inherit identical default-resolution semantics from the shared `bind`.
3063#[derive(Debug, Clone, PartialEq)]
3064pub struct OptionalParam {
3065 pub name: String,
3066 pub default: Option<Sexp>,
3067}
3068
3069impl OptionalParam {
3070 /// `&optional x` — a bare optional name with no default. An absent
3071 /// argument binds to `Sexp::Nil` (the no-default-form floor).
3072 #[must_use]
3073 pub fn bare(name: impl Into<String>) -> Self {
3074 Self {
3075 name: name.into(),
3076 default: None,
3077 }
3078 }
3079
3080 /// `&optional (x DEFAULT)` — an optional with a default form. An absent
3081 /// argument binds to `default.clone()`.
3082 #[must_use]
3083 pub fn with_default(name: impl Into<String>, default: Sexp) -> Self {
3084 Self {
3085 name: name.into(),
3086 default: Some(default),
3087 }
3088 }
3089
3090 /// The bound value when an absent call leaves this optional slot unfilled:
3091 /// the declared default form (cloned) when one was authored, OR the
3092 /// canonical `Sexp::Nil` floor when none was — the CL `&optional` no-
3093 /// default-form floor. ONE named primitive on the typed [`OptionalParam`]
3094 /// every absent-call binder consults; before this lift the same two-arm
3095 /// fallback `param.default.clone().unwrap_or(Sexp::Nil)` lived inline at
3096 /// [`MacroParams::bind`]'s optional arm, and a future absence-resolver
3097 /// (the kwarg-gate's typed-default fill? a future `&supplied-p` slot's
3098 /// "was this defaulted?" bit?) would have had to re-derive the same
3099 /// two-arm fallback at every site that walks the optional run.
3100 ///
3101 /// The projection IS the structural identity binding the typed
3102 /// `default: Option<Sexp>` slot to its bound-value contract:
3103 /// * `bare(name).resolved_default()` is `Sexp::Nil` (the no-default
3104 /// floor — `default.is_none()`).
3105 /// * `with_default(name, d).resolved_default()` is `d.clone()` (the
3106 /// declared default — `default = Some(d)` projected through Clone).
3107 ///
3108 /// `resolved_default()` is the typed accessor companion to the
3109 /// `bare` / `with_default` constructors: those two constructors define
3110 /// the ONLY admissible shapes of the typed `default` slot, and this
3111 /// accessor names the BOUND-VALUE projection both shapes yield at the
3112 /// binder's absence arm. Together the three close the `OptionalParam`'s
3113 /// self-contained typed surface — every authored shape lands through ONE
3114 /// of two constructors, and every absent-call binder reads through this
3115 /// ONE accessor.
3116 ///
3117 /// Returns an owned `Sexp` (not `&Sexp`) because the binder pushes the
3118 /// resolved default into a fresh `Vec<Sexp>` slot at every absent call;
3119 /// the `default.clone()` projection is the same allocation the pre-lift
3120 /// inline expression performed, just named at the typed boundary. The
3121 /// `Sexp::Nil` floor is a free per-call construction (a unit variant
3122 /// with no payload), so the no-default path is free of allocation
3123 /// beyond the function return slot.
3124 ///
3125 /// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid
3126 /// states unrepresentable"; the "no-default-form floor" structural
3127 /// concept becomes a NAMED projection on [`OptionalParam`] rather than
3128 /// re-derived `param.default.clone().unwrap_or(Sexp::Nil)` arithmetic
3129 /// at every site that walks the bound optional run. Authoring tools
3130 /// (REPL, LSP, `tatara-check`) that want to render "this optional
3131 /// binds to {default-form|nil} when absent" bind to ONE method on the
3132 /// typed param. THEORY.md §VI.1 — generation over composition; the
3133 /// constructor pair `bare` / `with_default` defines the typed shapes
3134 /// and the `resolved_default` accessor names the symmetric
3135 /// bound-value projection — the typed accessor companion. THEORY.md
3136 /// §II.1 invariant 2 — free middle; both expansion strategies route
3137 /// through the SHARED `MacroParams::bind`, so the new accessor is
3138 /// exposed to the bytecode and substitute paths uniformly via that
3139 /// shared binder.
3140 #[must_use]
3141 pub fn resolved_default(&self) -> Sexp {
3142 self.default.clone().unwrap_or(Sexp::Nil)
3143 }
3144}
3145
3146/// The value carrier a macro call binds its arguments in.
3147///
3148/// [`MacroParams::bind_carrier`] is the single positional binder every
3149/// expansion path routes through; this trait is the only thing that varies
3150/// between those paths. It names the two — and only two — places a binder
3151/// must SYNTHESIZE a value the caller never wrote:
3152///
3153/// * [`Self::lift_default`] — an `&optional` slot the call left unfilled
3154/// binds to the declared default form (or the `Sexp::Nil` floor). The
3155/// default is authored as plain `Sexp` at `defmacro` time, so each
3156/// carrier says how a template literal enters it.
3157/// * [`Self::collect_rest`] — the `&rest` slot collects the surplus args
3158/// into ONE value. `Sexp` wraps them in `Sexp::List`; a span-carrying
3159/// carrier additionally has to decide what span the synthesized list
3160/// wears.
3161///
3162/// Everything else in the binder — the required run, the arity rejections,
3163/// the required→optional→rest ordering — is carrier-independent and lives
3164/// exactly once.
3165///
3166/// `Site` is the per-call synthesis context. It is `()` for `Sexp`, whose
3167/// values carry no position; a span-carrying carrier sets it to the macro
3168/// CALL SITE, because a value the call never supplied has no source position
3169/// of its own and the call site is the honest place to point an error at.
3170///
3171/// Theory anchor: THEORY.md §V.1 — knowable platform; "which lambda-list
3172/// shapes does this expansion path accept?" stops being a per-path answer.
3173/// THEORY.md §VI.1 — generation over composition.
3174pub trait MacroArgCarrier: Clone {
3175 /// Per-call synthesis context — `()` when the carrier needs none.
3176 type Site: Copy;
3177
3178 /// Lift a template-authored default form into this carrier, for an
3179 /// `&optional` slot the call left unfilled.
3180 fn lift_default(default: &Sexp, site: Self::Site) -> Self;
3181
3182 /// Collect the `&rest` surplus into one carrier value.
3183 fn collect_rest(items: Vec<Self>, site: Self::Site) -> Self;
3184}
3185
3186impl MacroArgCarrier for Sexp {
3187 /// `Sexp` carries no position, so there is nothing to thread — the unit
3188 /// site is the structural statement that this carrier synthesizes values
3189 /// without context.
3190 type Site = ();
3191
3192 fn lift_default(default: &Sexp, (): ()) -> Self {
3193 default.clone()
3194 }
3195
3196 fn collect_rest(items: Vec<Self>, (): ()) -> Self {
3197 Sexp::List(items)
3198 }
3199}
3200
3201impl MacroParams {
3202 /// Canonical `&rest` marker — the ONE typed `&'static str` on
3203 /// [`MacroParams`] the parser's rest-slot dispatch specialises on.
3204 /// The Common-Lisp lambda-list `&rest` keyword names the position
3205 /// at which the parser stops collecting `required` / `optional`
3206 /// names and binds every subsequent arg into a `Sexp::List` at the
3207 /// [`Self::rest`] slot. Sibling posture to [`Self::OPTIONAL_MARKER`]
3208 /// (`"&optional"`) on the same lambda-list-keyword algebra layer:
3209 /// the two constants are the closed set of Common-Lisp
3210 /// lambda-list-keyword `&'static str` markers the parser's typed
3211 /// dispatch specialises on, sharing the canonical `char` lead byte
3212 /// [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3213 ///
3214 /// Pre-lift the same `"&rest"` bytes lived inline as a single
3215 /// `s == "&rest"` comparison inside [`parse_params`], with the two
3216 /// typed `&'static str` markers the CL lambda-list algebra
3217 /// specialises on smeared across TWO parallel `s == "..."` inline
3218 /// comparisons at the SAME dispatch cascade. Post-lift the
3219 /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3220 /// binds at ONE constant on the typed [`MacroParams`] algebra that
3221 /// both the parser AND any future authoring / rendering surface
3222 /// route through; a refactor that swaps the marker (e.g. a
3223 /// Clojure-compat port to `&` ONLY, an Elisp-compat port that
3224 /// keeps `&rest` as-is, a Scheme R7RS-style port to `.` for the
3225 /// dotted-pair rest slot) touches ONE constant rather than the
3226 /// two inline literals scattered across the parser AND the
3227 /// diagnostic message surfaces.
3228 ///
3229 /// Structural round-trip contract:
3230 /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3231 /// — the projection law binding the `&'static str` marker to its
3232 /// canonical `char` lead byte on the typed algebra, pinned by
3233 /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`.
3234 ///
3235 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3236 /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3237 /// binds at ONE `pub const` on the typed [`MacroParams`] algebra
3238 /// regardless of which parser / authoring surface reaches in.
3239 /// THEORY.md §V.1 — knowable platform; the canonical CL lambda-
3240 /// list `&rest` keyword becomes a TYPE-level constant on the
3241 /// typed macro-params algebra rather than an inline `"&rest"`
3242 /// literal at the parser's rest-slot dispatch arm.
3243 pub const REST_MARKER: &'static str = "&rest";
3244
3245 /// Canonical `&optional` marker — the ONE typed `&'static str` on
3246 /// [`MacroParams`] the parser's optional-section dispatch
3247 /// specialises on. The Common-Lisp lambda-list `&optional` keyword
3248 /// names the position at which the parser switches subsequent
3249 /// bare-symbol names from the `required` bin to the `optional`
3250 /// bin, until either the end of the param list OR the
3251 /// [`Self::REST_MARKER`] terminal is reached. Sibling posture to
3252 /// [`Self::REST_MARKER`] (`"&rest"`) on the same lambda-list-
3253 /// keyword algebra layer: the two constants are the closed set of
3254 /// CL lambda-list-keyword `&'static str` markers the parser's
3255 /// typed dispatch specialises on, sharing the canonical `char`
3256 /// lead byte [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3257 ///
3258 /// Structural round-trip contract:
3259 /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3260 /// — pinned by
3261 /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3262 ///
3263 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3264 /// (`&optional` section-switch marker, canonical `&'static str`)
3265 /// pairing binds at ONE `pub const` on the typed [`MacroParams`]
3266 /// algebra regardless of which parser / authoring surface reaches
3267 /// in. THEORY.md §V.1 — knowable platform; the canonical CL
3268 /// lambda-list `&optional` keyword becomes a TYPE-level constant
3269 /// on the typed macro-params algebra rather than an inline
3270 /// `"&optional"` literal at the parser's optional-section
3271 /// dispatch arm.
3272 pub const OPTIONAL_MARKER: &'static str = "&optional";
3273
3274 /// Canonical `&` LEAD byte shared across [`Self::REST_MARKER`]
3275 /// (`"&rest"`) and [`Self::OPTIONAL_MARKER`] (`"&optional"`) — the
3276 /// ONE canonical `char` on the [`MacroParams`] algebra that the
3277 /// Common-Lisp lambda-list-keyword disjointness contract binds to.
3278 ///
3279 /// Sibling posture to the closed set of `pub const` per-role
3280 /// canonical bytes on the substrate's other closed-set outer
3281 /// algebras: [`crate::ast::Atom::STR_DELIMITER`] (`'"'`),
3282 /// [`crate::ast::Atom::STR_ESCAPE_LEAD`] (`'\\'`),
3283 /// [`crate::ast::Atom::KEYWORD_MARKER_LEAD`] (`':'`),
3284 /// [`crate::ast::Atom::BOOL_LITERAL_LEAD`] (`'#'`),
3285 /// [`crate::ast::Sexp::LIST_OPEN`] (`'('`),
3286 /// [`crate::ast::Sexp::LIST_CLOSE`] (`')'`),
3287 /// [`crate::ast::Sexp::COMMENT_LEAD`] (`';'`),
3288 /// [`crate::ast::Sexp::COMMENT_TERM`] (`'\n'`),
3289 /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`] (`'@'`), and
3290 /// every `crate::ast::QuoteForm::lead_char` projection — every
3291 /// canonical per-role byte the substrate's typed algebras
3292 /// specialise on is now a `pub const` on its owning closed-set
3293 /// algebra. This constant closes the CL lambda-list-keyword
3294 /// family lead byte at the SAME algebra as its two `&'static str`
3295 /// marker projections so a delimiter swap (e.g. a Racket-compat
3296 /// port from CL `&rest` / `&optional` to `#!rest` / `#!optional`
3297 /// keyword-args, a Clojure-compat port to `&` ONLY as the rest
3298 /// marker) lands at ONE constant on the algebra.
3299 ///
3300 /// Structural round-trip contract:
3301 /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3302 /// AND
3303 /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3304 /// — the projection law binding the two `&'static str` markers to
3305 /// their shared canonical `char` lead byte, pinned by
3306 /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
3307 /// and
3308 /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3309 ///
3310 /// Disjointness contract: `LAMBDA_LIST_KEYWORD_LEAD`'s byte MUST
3311 /// differ from every sibling outer-marker `char` the substrate's
3312 /// other closed-set algebras specialise on. A collision would
3313 /// silently break the reader's outer dispatch: an `&`-prefixed
3314 /// bare atom `&rest` / `&optional` would collide with whichever
3315 /// marker it aliased. Pinned structurally at
3316 /// `macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`.
3317 ///
3318 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3319 /// (CL lambda-list-keyword LEAD byte, canonical `'&'`) pairing
3320 /// binds at ONE constant on the typed [`MacroParams`] algebra
3321 /// regardless of which of the two `&'static str` marker consumers
3322 /// reaches in. THEORY.md §V.1 — knowable platform; the canonical
3323 /// CL lambda-list-keyword LEAD byte becomes a TYPE-level constant
3324 /// on the substrate algebra rather than an inline `'&'` glyph at
3325 /// every docstring pinning the family disjointness contract.
3326 pub const LAMBDA_LIST_KEYWORD_LEAD: char = '&';
3327
3328 /// Closed-set forced-arity ALL array over the CL lambda-list-keyword
3329 /// `&'static str` markers the parser's typed dispatch specialises on
3330 /// — the [`Self::REST_MARKER`] (`"&rest"`) rest-slot marker followed
3331 /// by the [`Self::OPTIONAL_MARKER`] (`"&optional"`) optional-section
3332 /// marker, in the canonical CL lambda-list ordering (`&optional`
3333 /// binds AFTER `&required`, `&rest` binds AFTER `&optional`, so the
3334 /// declaration walk fires `REST_MARKER` FIRST as the terminal-arm
3335 /// early return; the ALL array's ordering keys on marker
3336 /// declaration order in this file, not on parse-time ordering).
3337 ///
3338 /// Sibling posture to the closed set of `pub const ALL: [Self; N]`
3339 /// forced-arity arrays on the substrate's other closed-set outer
3340 /// algebras: [`crate::ast::AtomKind::ALL`] (`[Self; 6]`),
3341 /// [`crate::ast::QuoteForm::ALL`] (`[Self; 4]`),
3342 /// [`crate::error::SexpShape::ALL`] (`[Self; 12]`),
3343 /// [`crate::error::UnquoteForm::ALL`] (`[Self; 2]`),
3344 /// [`crate::error::MacroDefHead::ALL`] (`[Self; 3]`) — every
3345 /// closed-set algebra the substrate carries pins its cardinality at
3346 /// the declaration site via a `pub const ALL` array whose forced
3347 /// arity fails compilation if a new variant lands without being
3348 /// added to the set. `LAMBDA_LIST_KEYWORDS` closes the same shape
3349 /// on the CL lambda-list-keyword family: a third marker (`&key`
3350 /// for keyword-args, `&aux` for auxiliaries, `&body` for the
3351 /// docstring-carrying tail of `defmacro` bodies) extends this ONE
3352 /// `pub const` + the matching per-role marker `pub const` above,
3353 /// AND rustc enforces every downstream consumer's closed-set
3354 /// sweep picks up the new marker at the SAME structural site.
3355 ///
3356 /// Pre-lift the two `&'static str` markers were named on the typed
3357 /// algebra but the family they close was NOT — the two structural
3358 /// round-trip pins (`_rest_marker_prefixed_by_lambda_list_keyword_
3359 /// lead` AND `_optional_marker_prefixed_by_lambda_list_keyword_
3360 /// lead`) each named ONE marker inline as a duplicate 3-line
3361 /// `assert!(A.starts_with(LEAD), ...)` shape, and the pairwise-
3362 /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3363 /// named the two markers as a hand-rolled `assert_ne!` pair.
3364 /// Post-lift the ALL array binds the family at the typed algebra so
3365 /// every family-wide contract (LEAD-byte round-trip, pairwise
3366 /// disjointness, membership gate) routes through the ONE array and
3367 /// a future `&key` / `&aux` extension automatically extends the
3368 /// sweep without re-deriving per-marker assertions.
3369 ///
3370 /// Structural round-trip contract: every element `m` of
3371 /// `Self::LAMBDA_LIST_KEYWORDS` satisfies
3372 /// `m.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)` — pinned by
3373 /// `macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead`.
3374 /// Cardinality contract: `Self::LAMBDA_LIST_KEYWORDS.len() == 2`
3375 /// pins the current family size at the type level; a future third
3376 /// marker extends the array's arity AND updates every downstream
3377 /// pin at ONE structural site. Pairwise-disjointness contract:
3378 /// every distinct index pair `(i, j)` in `Self::LAMBDA_LIST_KEYWORDS`
3379 /// satisfies `Self::LAMBDA_LIST_KEYWORDS[i] !=
3380 /// Self::LAMBDA_LIST_KEYWORDS[j]` — pinned by
3381 /// `macro_params_lambda_list_keywords_pairwise_distinct`.
3382 ///
3383 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3384 /// CL lambda-list-keyword family closure binds at ONE `pub const`
3385 /// on the typed [`MacroParams`] algebra regardless of which
3386 /// consumer (parser dispatch, family-wide contract sweep,
3387 /// authoring / rendering surface, `is_lambda_list_keyword`
3388 /// membership gate) reaches in. THEORY.md §V.1 — knowable
3389 /// platform; the family's cardinality becomes a TYPE-level
3390 /// constant on the substrate algebra rather than a per-consumer
3391 /// hand-rolled enumeration of the two markers. THEORY.md §VI.1 —
3392 /// generation over composition; the family-wide contract sweeps
3393 /// (LEAD-byte round-trip, pairwise disjointness, membership gate)
3394 /// emerge from the composition of TWO substrate primitives (this
3395 /// `pub const` array + the per-role `pub const REST_MARKER` /
3396 /// `pub const OPTIONAL_MARKER`) rather than as per-marker inline
3397 /// assertions the current pins duplicate structurally.
3398 ///
3399 /// Frontier inspiration: Common Lisp's `LAMBDA-LIST-KEYWORDS`
3400 /// standard variable (CLHS §3.4.1) — the runtime-reflectable list
3401 /// of every reserved lambda-list keyword the implementation
3402 /// recognises; `LAMBDA_LIST_KEYWORDS` is the substrate's typed-Rust
3403 /// peer at compile time, with the closed-set ALL array standing in
3404 /// for CL's runtime-mutable list. Translation through pleme-io
3405 /// primitives: a `pub const [&'static str; N]` on the typed
3406 /// [`MacroParams`] algebra rather than a runtime-mutable list;
3407 /// forced-arity closure at the type layer with the same
3408 /// family-reflective read surface.
3409 pub const LAMBDA_LIST_KEYWORDS: [&'static str; 2] = [Self::REST_MARKER, Self::OPTIONAL_MARKER];
3410
3411 /// Typed membership gate over the closed set
3412 /// [`Self::LAMBDA_LIST_KEYWORDS`] — `true` iff `s` matches some
3413 /// element of the ALL array byte-for-byte, `false` for every other
3414 /// input.
3415 ///
3416 /// The typed "does this symbol name a CL lambda-list keyword?"
3417 /// projection on the [`MacroParams`] algebra. Sibling posture to the
3418 /// closed set of per-algebra membership / classifier gates the
3419 /// substrate carries:
3420 /// [`crate::ast::QuoteForm::from_lead_char`] (per-char quote-family
3421 /// dispatch — Some/None on a closed 3-char set),
3422 /// [`crate::ast::Sexp::is_bare_atom_boundary`] (six-fold
3423 /// bare-atom-boundary sweep), and
3424 /// [`crate::ast::Atom::decode_str_escape`] (Str-escape decode table
3425 /// on a closed 6-arm set) — every family-membership gate the
3426 /// substrate carries binds at ONE method on the closed-set
3427 /// algebra so a family extension lands at ONE place plus the
3428 /// per-role marker without re-deriving per-consumer inline
3429 /// enumerations.
3430 ///
3431 /// Pre-lift the "is this symbol a CL lambda-list keyword?" concept
3432 /// existed structurally but was NOT named on the algebra — the
3433 /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3434 /// AND the cross-axis disjointness sweep
3435 /// (`_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`)
3436 /// each named the concept implicitly as a hand-rolled per-marker
3437 /// `assert_ne!` sweep. Post-lift the membership gate binds at ONE
3438 /// typed method on the algebra a future consumer (a parser-time
3439 /// "this symbol looks like a lambda-list keyword but isn't
3440 /// recognised" hint, an authoring-surface completion bar that
3441 /// suggests `&rest` / `&optional` at param-list positions, a
3442 /// pretty-printer that colors CL lambda-list keywords distinctly)
3443 /// binds against without re-deriving the per-marker enumeration
3444 /// at every consumer site.
3445 ///
3446 /// Contract:
3447 /// * Every element of [`Self::LAMBDA_LIST_KEYWORDS`] classifies
3448 /// as `true` — pinned by
3449 /// `macro_params_is_lambda_list_keyword_accepts_every_marker`.
3450 /// * The bare CL lambda-list-keyword LEAD byte
3451 /// ([`Self::LAMBDA_LIST_KEYWORD_LEAD`]) as a 1-char string
3452 /// classifies as `false` — pinned by
3453 /// `macro_params_is_lambda_list_keyword_rejects_bare_lead_byte`.
3454 /// * Symbols starting with the LEAD byte but naming an
3455 /// unrecognised keyword (`"&key"`, `"&aux"`, `"&body"`)
3456 /// classify as `false` — pinned by
3457 /// `macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names`.
3458 ///
3459 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3460 /// closed-set membership gate binds at ONE typed method on the
3461 /// [`MacroParams`] algebra regardless of which consumer reaches
3462 /// in. THEORY.md §III — the typescape; the "is this a CL
3463 /// lambda-list keyword?" projection becomes a TYPE projection on
3464 /// the substrate algebra rather than per-consumer inline
3465 /// enumeration of the two markers. THEORY.md §V.1 — knowable
3466 /// platform; a future third marker (`&key`, `&aux`, `&body`)
3467 /// extends [`Self::LAMBDA_LIST_KEYWORDS`] ONCE and every
3468 /// membership-gate consumer picks up the new marker for free.
3469 ///
3470 /// Frontier inspiration: Common Lisp's `(find X LAMBDA-LIST-KEYWORDS)`
3471 /// pattern for testing lambda-list keyword membership at runtime;
3472 /// `is_lambda_list_keyword` is the substrate's typed-Rust peer at
3473 /// compile time. Translation through pleme-io primitives: a linear
3474 /// sweep over the [`Self::LAMBDA_LIST_KEYWORDS`] `pub const` array
3475 /// rather than a runtime `find` over a mutable list; ordinary
3476 /// slice `contains` on the `&'static str` array.
3477 #[must_use]
3478 pub fn is_lambda_list_keyword(s: &str) -> bool {
3479 Self::LAMBDA_LIST_KEYWORDS.contains(&s)
3480 }
3481
3482 /// The flat, ordered param-name list the template bytecode indexes into:
3483 /// every `required` name in order, then every `optional` name in order,
3484 /// then the `rest` name if present. `names()[i]` is the param `Subst(i)`
3485 /// / `Splice(i)` reference.
3486 #[must_use]
3487 pub fn names(&self) -> Vec<&str> {
3488 self.required
3489 .iter()
3490 .map(String::as_str)
3491 .chain(self.optional.iter().map(|p| p.name.as_str()))
3492 .chain(self.rest.as_deref())
3493 .collect()
3494 }
3495
3496 /// The rest-less maximum arity of this param list: `required.len() +
3497 /// optional.len()`. Two equivalent readings collapse into ONE primitive
3498 /// on the typed `MacroParams`:
3499 ///
3500 /// * The **rest-start boundary**: when `self.rest` is `Some`, the
3501 /// `&rest` slot collects `args[fixed_arity()..]` into a
3502 /// `Sexp::List` (the empty slice when the call is exactly
3503 /// saturated). `fixed_arity()` IS that slice's start index.
3504 /// * The **rest-less maximum arity**: when `self.rest` is `None`,
3505 /// `args.len() > fixed_arity()` is the surplus-args rejection
3506 /// boundary [`bind`](Self::bind) checks before raising
3507 /// `LispError::TooManyMacroArgs` (the call-site mirror of
3508 /// `RestParamTrailingTokens`'s definition-site rejection).
3509 ///
3510 /// Both readings ARE the same arithmetic; [`bind`](Self::bind)
3511 /// previously inlined the same `self.required.len() +
3512 /// self.optional.len()` expression THREE times — once inside the
3513 /// `Vec::with_capacity(required + optional + rest?)` slot, once at
3514 /// the `rest_start` site (inside `if let Some(_rest_name) =
3515 /// self.rest`), and once at the `expected` site (inside the
3516 /// rest-less `else`). The latter two live in mutually-exclusive
3517 /// branches yet name ONE structural concept; lifting the arithmetic
3518 /// to a single named primitive makes that concept first-class on
3519 /// the typed param list.
3520 ///
3521 /// `fixed_arity()` IGNORES the `rest` slot by construction — a
3522 /// `&rest` param has no maximum and is not part of the fixed run.
3523 /// `names().len() == fixed_arity() + usize::from(self.rest.is_some())`
3524 /// is the structural identity binding this primitive to
3525 /// [`names`](Self::names) and to the `Vec::with_capacity` hint
3526 /// [`bind`](Self::bind) computes for the bound-values vec.
3527 ///
3528 /// Theory anchor: THEORY.md §V.1 — knowable platform; the structural
3529 /// "rest-start boundary / rest-less max arity" concept becomes a
3530 /// named `&MacroParams` projection rather than re-derived arithmetic
3531 /// at every site that walks the bound run. Authoring tools (REPL,
3532 /// LSP, `tatara-check`) that want to render "this macro takes
3533 /// between `required.len()` and `fixed_arity()` args (or unbounded
3534 /// if `rest.is_some()`)" bind to ONE method on the typed param
3535 /// list. THEORY.md §VI.1 — generation over composition; three
3536 /// inline copies of the same arithmetic in one function is past
3537 /// the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
3538 /// named. THEORY.md §II.1 invariant 2 — free middle; both
3539 /// expansion strategies route through the SHARED `bind`, so the
3540 /// new primitive is exposed to the bytecode and substitute paths
3541 /// uniformly — no per-strategy drift in how the boundary is
3542 /// computed.
3543 #[must_use]
3544 pub fn fixed_arity(&self) -> usize {
3545 self.required.len() + self.optional.len()
3546 }
3547
3548 /// The TOTAL lambda-list arity of this param list — every named
3549 /// slot the reader accepted, in the canonical CL lambda-list
3550 /// ordering: `required.len() + optional.len() + rest.is_some() as
3551 /// usize`. Peer to [`Self::fixed_arity`] one REST-SLOT axis over
3552 /// on the `MacroParams` algebra — where [`Self::fixed_arity`]
3553 /// ignores the rest slot (the rest-less maximum-arity /
3554 /// rest-start-boundary reading, unbounded on the call-time surface
3555 /// when `self.rest.is_some()`), this projection INCLUDES the rest
3556 /// slot as one additional named param (the total-slots reading,
3557 /// bounded on the type-time surface regardless of `rest`).
3558 ///
3559 /// `Self::total_arity() == Self::names().len()` — a `Vec`-free
3560 /// projection of the [`Self::names`] flat-index list's length. Two
3561 /// equivalent readings collapse into ONE named primitive on the
3562 /// typed [`MacroParams`] algebra:
3563 ///
3564 /// * The **`Vec::with_capacity` hint**: [`Self::bind`] previously
3565 /// inlined `self.fixed_arity() + usize::from(self.rest.is_some())`
3566 /// as the bound-vec capacity hint. That expression IS this
3567 /// primitive.
3568 /// * The **REGISTER-time arity ceiling**: on the arity-bomb
3569 /// `(defmacro huge (a-1 a-2 … a-N) `,a-1)` [`Expander::register_macro_def`]
3570 /// compares `def.params.total_arity()` against
3571 /// [`Expander::max_macro_arity`] BEFORE the body-size gate walks
3572 /// the body or any insert lands. `total_arity()` IS that gate's
3573 /// axis.
3574 ///
3575 /// The structural identity
3576 /// `names().len() == fixed_arity() + usize::from(rest.is_some())
3577 /// == total_arity()`
3578 /// binds the primitive to [`Self::names`] and [`Self::fixed_arity`]
3579 /// on the [`MacroParams`] algebra — pinned by
3580 /// `macro_params_total_arity_matches_names_len` AND
3581 /// `macro_params_total_arity_equals_fixed_arity_plus_rest_bit`.
3582 ///
3583 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
3584 /// "how many named slots does this macro declare?" projection
3585 /// becomes a NAMED `&MacroParams` accessor rather than re-derived
3586 /// arithmetic at [`Self::bind`]'s capacity hint AND at the
3587 /// [`Expander::register_macro_def`] arity-ceiling gate.
3588 /// THEORY.md §VI.1 — generation over composition; two inline copies
3589 /// of the same three-arm arithmetic across two consumers is the
3590 /// ≥2 PRIME-DIRECTIVE trigger once the structural shape is named.
3591 /// THEORY.md §II.1 invariant 2 — free middle; both the binder AND
3592 /// the arity-guard consult ONE primitive, so a regression that
3593 /// drifts the guard's arity computation from the binder's capacity
3594 /// hint (e.g. one counts `rest` as one slot, the other counts it
3595 /// as zero) becomes structurally impossible.
3596 #[must_use]
3597 pub fn total_arity(&self) -> usize {
3598 self.fixed_arity() + usize::from(self.rest.is_some())
3599 }
3600
3601 /// Bind call args to params positionally, returning the per-index bound
3602 /// values parallel to [`names`](Self::names): each required name takes
3603 /// the arg at its position (a missing one is
3604 /// [`missing_macro_arg`](self::missing_macro_arg)); each optional name
3605 /// takes the arg at its position, or — when the call ran out of args —
3606 /// its declared default form (`Sexp::Nil` when none was declared,
3607 /// matching CL's `&optional` floor); and a present `rest` collects every
3608 /// arg beyond the required+optional run into a `Sexp::List` (the empty
3609 /// list when none remain). Args beyond a REST-LESS param list have
3610 /// nowhere to bind and are rejected
3611 /// ([`too_many_macro_args`](self::too_many_macro_args)) rather than
3612 /// silently truncated. This is the single binding loop every expansion
3613 /// strategy shares — `apply_compiled` consumes the index vec directly,
3614 /// `bind_args` zips it against `names()` into the name-keyed map — and
3615 /// it is the `Sexp` instantiation of the carrier-generic
3616 /// [`Self::bind_carrier`].
3617 fn bind(&self, macro_name: &str, args: &[Sexp]) -> Result<Vec<Sexp>> {
3618 self.bind_carrier(macro_name, args, ())
3619 }
3620
3621 /// The ONE positional binder, generic over the value carrier a macro call
3622 /// binds its arguments in — [`Sexp`] for the plain `Sexp`→`Sexp`
3623 /// expander, `Spanned` for the span-preserving one, and any future
3624 /// carrier a consumer implements [`MacroArgCarrier`] for.
3625 ///
3626 /// Genericising this loop is what makes ONE lambda-list semantics
3627 /// (required run → `&optional` run with per-param defaults → at-most-one
3628 /// `&rest`, plus the too-few / too-many arity rejections) reachable from
3629 /// every expansion path, instead of each path restating the loop against
3630 /// its own value type. Before this lift the span-preserving expander
3631 /// carried its own `bind_spanned_args` that knew only `Required`/`Rest`
3632 /// — so `(defmacro f (a &optional b) …)` parsed on one path and was
3633 /// rejected on the other, and a surplus argument was a hard error on one
3634 /// path and silently dropped on the other. Both divergences are now
3635 /// structurally impossible: there is exactly one loop.
3636 ///
3637 /// `site` is the carrier's synthesis context — `()` for `Sexp` (a
3638 /// default form and a rest list need no context to construct), the
3639 /// macro CALL SITE span for `Spanned` (a value the CALL never supplied
3640 /// has no source position of its own, so it is stamped at the call).
3641 ///
3642 /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
3643 /// binding loop had reached three copies (plain, spanned, and
3644 /// `tatara-lisp-eval`'s macro-time env binder), well past the ≥2
3645 /// PRIME-DIRECTIVE trigger.
3646 pub fn bind_carrier<V: MacroArgCarrier>(
3647 &self,
3648 macro_name: &str,
3649 args: &[V],
3650 site: V::Site,
3651 ) -> Result<Vec<V>> {
3652 let mut out = Vec::with_capacity(self.total_arity());
3653 for (i, name) in self.required.iter().enumerate() {
3654 let arg = args
3655 .get(i)
3656 .cloned()
3657 .ok_or_else(|| missing_macro_arg(macro_name, name))?;
3658 out.push(arg);
3659 }
3660 let opt_start = self.required.len();
3661 for (j, param) in self.optional.iter().enumerate() {
3662 // Absent optional slot binds to the typed `resolved_default()`
3663 // projection on `OptionalParam`: the declared default form when
3664 // one was authored, OR the `Sexp::Nil` no-default floor when
3665 // none was. The two-arm fallback `param.default.clone().
3666 // unwrap_or(Sexp::Nil)` previously inlined here is now ONE named
3667 // accessor on the typed param both expansion strategies share via
3668 // `MacroParams::bind_carrier`.
3669 let arg = match args.get(opt_start + j) {
3670 Some(supplied) => supplied.clone(),
3671 None => V::lift_default(¶m.resolved_default(), site),
3672 };
3673 out.push(arg);
3674 }
3675 if let Some(_rest_name) = self.rest.as_ref() {
3676 // The `&rest` slot collects args[fixed_arity()..] (the empty
3677 // slice when the call is exactly saturated); the boundary is
3678 // the typed `fixed_arity()` primitive both branches share.
3679 let rest = args.get(self.fixed_arity()..).unwrap_or(&[]).to_vec();
3680 out.push(V::collect_rest(rest, site));
3681 } else {
3682 // No `&rest` slot — the param list has a FIXED maximum arity
3683 // of `fixed_arity()`. Surplus args have nowhere to bind;
3684 // reject rather than silently truncate. Closes the call-site
3685 // mirror of `RestParamTrailingTokens` (the definition-site
3686 // rejection lifted by the prior-run typed-promotion lineage),
3687 // so the typed-entry macro-call-gate is structurally complete
3688 // in both directions: too-few (`MissingMacroArg`) AND too-many
3689 // (`TooManyMacroArgs`).
3690 let expected = self.fixed_arity();
3691 if args.len() > expected {
3692 return Err(too_many_macro_args(macro_name, expected, args.len()));
3693 }
3694 }
3695 Ok(out)
3696 }
3697}
3698
3699/// How many macro applications each expansion layer answered (memo, bytecode,
3700/// substitute), read through [`Expander::expansion_counts`].
3701///
3702/// A count of work, not a timing: which layer ran is a deterministic fact of
3703/// the strategy and the workload, so a test can assert the fast path is wired
3704/// without measuring the machine it runs on.
3705#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3706pub struct ExpansionCounts {
3707 /// Applications answered from the memo table.
3708 pub cache_hits: u64,
3709 /// Applications run through the compiled template bytecode.
3710 pub compiled: u64,
3711 /// Applications run through the name-keyed substitute walker.
3712 pub substituted: u64,
3713}
3714
3715/// The live counters behind [`ExpansionCounts`]. Atomic because `apply`
3716/// takes `&self`; a clone starts from the source's current values.
3717#[derive(Default)]
3718struct ApplyCounters {
3719 cache_hits: AtomicU64,
3720 compiled: AtomicU64,
3721 substituted: AtomicU64,
3722}
3723
3724impl ApplyCounters {
3725 fn snapshot(&self) -> ExpansionCounts {
3726 ExpansionCounts {
3727 cache_hits: self.cache_hits.load(AtomicOrdering::Relaxed),
3728 compiled: self.compiled.load(AtomicOrdering::Relaxed),
3729 substituted: self.substituted.load(AtomicOrdering::Relaxed),
3730 }
3731 }
3732}
3733
3734impl Clone for ApplyCounters {
3735 fn clone(&self) -> Self {
3736 let c = self.snapshot();
3737 Self {
3738 cache_hits: AtomicU64::new(c.cache_hits),
3739 compiled: AtomicU64::new(c.compiled),
3740 substituted: AtomicU64::new(c.substituted),
3741 }
3742 }
3743}
3744
3745/// Macro environment. Collects `defmacro` forms and rewrites callers.
3746///
3747/// Expansion strategy is tunable per-expander:
3748/// - **Compiled (default)** — every registered macro's template is walked once
3749/// and flattened into a linear `CompiledTemplate` (a tiny bytecode: Literal,
3750/// Subst(index), Splice(index), BeginList, EndList). Expansion of a call
3751/// is then a linear pass with no HashMap lookups and no recursion through
3752/// the template Sexp. Purely-literal subtrees compile to a single
3753/// `Literal(Sexp)` op — huge win for macros where most of the body is fixed.
3754/// - **Substitute-only** — runs the name-keyed `substitute` walker. Slower
3755/// but proves equivalence; used in the benchmark test to measure the
3756/// compiled-vs-substituted speedup.
3757#[derive(Clone, Default)]
3758pub struct Expander {
3759 macros: HashMap<String, MacroDef>,
3760 /// Pre-compiled template bytecodes, populated when `compile_templates`.
3761 templates: HashMap<String, CompiledTemplate>,
3762 /// When true, register a CompiledTemplate alongside each macro and dispatch
3763 /// expansion through the bytecode interpreter.
3764 compile_templates: bool,
3765 /// Memoization of `apply(macro, args)` — repeated calls with identical
3766 /// args skip expansion entirely. Shared across clones so realizations of
3767 /// the same `CompilerSpec` benefit across .compile() invocations.
3768 cache: Arc<Mutex<HashMap<CacheKey, Sexp>>>,
3769 /// Toggle caching. Default on — caching is the actual performance win
3770 /// the bytecode layer enables.
3771 cache_enabled: bool,
3772 /// The six resource ceilings, held as ONE typed [`ResourceLimits`] bundle
3773 /// rather than six sibling `max_*: usize` fields. Peer of the six
3774 /// getter/setter pairs one AGGREGATION axis over — where each pair
3775 /// projects ONE ceiling, this field carries the SIX-fold cross-product
3776 /// so [`Self::resource_limits`] returns `self.limits` verbatim and
3777 /// [`Self::set_resource_limits`] assigns through the bundle in ONE move.
3778 ///
3779 /// Adding a SEVENTH ceiling touches [`ResourceLimits`] + the matching
3780 /// `DEFAULT_MAX_*` module constant + the paired individual getter/setter
3781 /// — the [`Expander`] struct, its constructors, AND
3782 /// [`Self::resource_limits`] / [`Self::set_resource_limits`] stay
3783 /// exactly as they are. That's the compounding win of the
3784 /// consolidation: the (Expander, constructors, bulk getter, bulk
3785 /// setter) four-corner face is field-shape-agnostic — it composes with
3786 /// the bundle by value, not by field-by-field literal.
3787 ///
3788 /// Consumers on the read path (`register_macro_def`'s three REGISTER-time
3789 /// gates, `expand_with_depth`'s depth + output-size gates, `apply`'s
3790 /// cache-entries gate) dereference through `self.limits.max_*`; the
3791 /// projection is a `Copy usize` load — same shape as reading a bare
3792 /// field, no `Deref`, no `Arc`, no lock.
3793 limits: ResourceLimits,
3794 /// Per-layer application counts, read through [`Self::expansion_counts`].
3795 counters: ApplyCounters,
3796}
3797
3798impl Expander {
3799 /// Default-posture expander with an operator-supplied resource-ceiling
3800 /// bundle — the AGGREGATION-axis peer of
3801 /// [`Self::set_resource_limits`] one CONSTRUCTOR-STAGE axis over.
3802 /// Where [`Self::set_resource_limits`] applies the bundled six ceilings
3803 /// POST-construction on an already-built expander, this constructor
3804 /// binds them AT construction so a call-site that wants a fresh
3805 /// expander with a non-default posture composes ONE typed call
3806 /// (`Expander::with_limits(limits)`) rather than the two-step
3807 /// (`let mut e = Expander::new(); e.set_resource_limits(limits);`)
3808 /// dance the post-construction sibling still admits.
3809 ///
3810 /// The (compile_templates, cache_enabled) execution-strategy pair
3811 /// carries the [`Self::new`] default posture (`true, true`) — the
3812 /// bytecode-with-cache strategy — since callers that want the
3813 /// substitute-only strategy still route through
3814 /// [`Self::new_substitute_only`]. Post-lift both convenience
3815 /// constructors delegate their `macros` / `templates` / `cache` field
3816 /// literals through THIS primitive at the resource-posture axis: the
3817 /// four-out-of-six field literals (`HashMap::new()` × 2 +
3818 /// `Arc::new(Mutex::new(HashMap::new()))` + `limits`) lived
3819 /// byte-identical across [`Self::new`] and [`Self::new_substitute_only`]
3820 /// pre-lift; the two divergent flags (`compile_templates`,
3821 /// `cache_enabled`) still differ between the two constructors, and the
3822 /// substitute-only variant flips them AFTER threading through
3823 /// `with_limits` — same shape as [`Self::new_bytecode_no_cache`] does
3824 /// on the `cache_enabled` flag.
3825 ///
3826 /// Round-trip identity: `Expander::with_limits(limits).resource_limits()
3827 /// == limits` for any `ResourceLimits` — pinned as a typed theorem in
3828 /// the test cohort. The at-construction assignment is a direct `Copy`
3829 /// of the bundled field (no per-field cascade), so a mid-construction
3830 /// panic window on the six knobs is structurally impossible. Peer of
3831 /// the `Expander::with_limits(DEFAULT_RESOURCE_LIMITS) ==
3832 /// Expander::new()` delegation identity — pinned on the resource-
3833 /// posture projection so a future refactor that drifts the two
3834 /// constructors on the ceiling axis fails loudly.
3835 ///
3836 /// Frontier inspiration: Cranelift's `settings::Flags::new(builder)` —
3837 /// codegen ceilings (opt level, allocator strategy, feature enables)
3838 /// bind at CONSTRUCTION-time on a typed `Flags` bundle rather than
3839 /// composed through post-construction setters on the `Isa`. Same
3840 /// posture as this lift: the (getter, setter, at-construction)
3841 /// three-corner face on the resource-posture surface now binds at ONE
3842 /// typed bundle across ALL three surfaces.
3843 ///
3844 /// Theory grounding: THEORY.md §II.1 invariants 1 + 2 — typed entry +
3845 /// free middle. Pre-lift the "expander with a chosen resource posture"
3846 /// concept was untyped: consumers composed [`Self::new`] +
3847 /// [`Self::set_resource_limits`] at their call site with no
3848 /// compile-time gate that BOTH steps land atomically. Post-lift the
3849 /// concept has ONE named entry point on the `Expander` surface, and a
3850 /// future consumer (`RealizedCompiler` builder that ships a
3851 /// CI-tightened preset, an LSP session that ships a REPL-permissive
3852 /// preset, a test-harness fixture that ships an `UNBOUNDED`-style
3853 /// preset) binds through ONE typed constructor.
3854 pub fn with_limits(limits: ResourceLimits) -> Self {
3855 Self {
3856 macros: HashMap::new(),
3857 templates: HashMap::new(),
3858 compile_templates: true,
3859 cache: Arc::new(Mutex::new(HashMap::new())),
3860 cache_enabled: true,
3861 limits,
3862 counters: ApplyCounters::default(),
3863 }
3864 }
3865
3866 /// Default expander — compiled bytecode + expansion cache enabled.
3867 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3868 /// so the (macros, templates, cache, limits) field-shape agreement
3869 /// with every other constructor on this `Expander` surface lives at
3870 /// ONE literal.
3871 pub fn new() -> Self {
3872 Self::with_limits(DEFAULT_RESOURCE_LIMITS)
3873 }
3874
3875 /// Expander using the legacy substitute path (no template compilation,
3876 /// no cache). Kept for benchmarking + equivalence testing.
3877 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3878 /// and flips the two divergent execution-strategy flags — same
3879 /// composition shape as [`Self::new_bytecode_no_cache`] one flag over.
3880 pub fn new_substitute_only() -> Self {
3881 let mut e = Self::with_limits(DEFAULT_RESOURCE_LIMITS);
3882 e.compile_templates = false;
3883 e.cache_enabled = false;
3884 e
3885 }
3886
3887 /// Expander with bytecode on but expansion cache off — isolates the cache
3888 /// contribution from the bytecode infrastructure. Benchmark baseline.
3889 pub fn new_bytecode_no_cache() -> Self {
3890 let mut e = Self::new();
3891 e.cache_enabled = false;
3892 e
3893 }
3894
3895 /// Toggle the expansion cache at runtime.
3896 pub fn set_cache_enabled(&mut self, enabled: bool) {
3897 self.cache_enabled = enabled;
3898 }
3899
3900 /// How many entries are currently cached.
3901 pub fn cache_size(&self) -> usize {
3902 self.cache.lock().unwrap().len()
3903 }
3904
3905 /// How many macro applications each expansion layer has answered since
3906 /// this expander was built.
3907 pub fn expansion_counts(&self) -> ExpansionCounts {
3908 self.counters.snapshot()
3909 }
3910
3911 /// Clear the expansion cache (e.g., after redefining a macro).
3912 pub fn clear_cache(&self) {
3913 self.cache.lock().unwrap().clear();
3914 }
3915
3916 /// The configured ceiling on `expand`'s recursive re-entry into a
3917 /// macro-call form. Defaults to [`DEFAULT_MAX_EXPANSION_DEPTH`].
3918 #[must_use]
3919 pub fn max_expansion_depth(&self) -> usize {
3920 self.limits.max_expansion_depth
3921 }
3922
3923 /// Set the ceiling on `expand`'s recursive re-entry into a macro-call
3924 /// form. Tests bind low values (e.g. `4`) to pin the runaway-rejection
3925 /// shape without paying for a 256-round walk; consumers that compose
3926 /// deep proven macro cascades can raise it. Zero is admitted and
3927 /// makes every first macro-call return
3928 /// [`LispError::ExpansionDepthExceeded`] immediately — useful for
3929 /// contract-tests that assert "this reader path never expands a
3930 /// macro."
3931 pub fn set_max_expansion_depth(&mut self, depth: usize) {
3932 self.limits.max_expansion_depth = depth;
3933 }
3934
3935 /// The configured ceiling on the expansion cache's entry count.
3936 /// Defaults to [`DEFAULT_MAX_CACHE_ENTRIES`]. Peer to
3937 /// [`Self::max_expansion_depth`] one RESOURCE axis over.
3938 #[must_use]
3939 pub fn max_cache_entries(&self) -> usize {
3940 self.limits.max_cache_entries
3941 }
3942
3943 /// Set the ceiling on the expansion cache's entry count. Tests bind
3944 /// low values (e.g. `2`) to pin the bounded-cache contract without
3945 /// paying for an 8K-entry walk; consumers that expand many distinct
3946 /// argument shapes can raise it. Zero is admitted and effectively
3947 /// disables caching (every fresh `(name, args)` pair skips the
3948 /// insert path) — useful for contract-tests that assert "this
3949 /// expander realizes without ever caching." [`usize::MAX`] is the
3950 /// unbounded-cache mode for batch compilation runs where memory is
3951 /// not the concern.
3952 pub fn set_max_cache_entries(&mut self, cap: usize) {
3953 self.limits.max_cache_entries = cap;
3954 }
3955
3956 /// The configured ceiling on a single macro-`apply` output's
3957 /// [`crate::ast::Sexp::node_count`]. Defaults to
3958 /// [`DEFAULT_MAX_EXPANSION_SIZE`]. Peer to
3959 /// [`Self::max_expansion_depth`] and [`Self::max_cache_entries`]
3960 /// one RESOURCE-DIMENSION axis over.
3961 #[must_use]
3962 pub fn max_expansion_size(&self) -> usize {
3963 self.limits.max_expansion_size
3964 }
3965
3966 /// Set the ceiling on a single macro-`apply` output's node count.
3967 /// Tests bind low values (e.g. `8`) to pin the "expansion bomb"
3968 /// rejection shape without materializing a 64K-node blob;
3969 /// consumers that compose deep proven macro cascades can raise it.
3970 /// [`usize::MAX`] admits any lawful output (the ceiling is
3971 /// effectively lifted); operators that want strict "your macros
3972 /// must fit in N nodes" contract-tests set it exactly.
3973 pub fn set_max_expansion_size(&mut self, size: usize) {
3974 self.limits.max_expansion_size = size;
3975 }
3976
3977 /// The configured ceiling on a registered macro's BODY
3978 /// [`crate::ast::Sexp::node_count`]. Defaults to
3979 /// [`DEFAULT_MAX_MACRO_BODY_SIZE`]. Peer to
3980 /// [`Self::max_expansion_depth`], [`Self::max_cache_entries`],
3981 /// and [`Self::max_expansion_size`] one PIPELINE-STAGE axis over
3982 /// (the three prior guards fire at EXPAND time; this one fires
3983 /// at REGISTER time).
3984 #[must_use]
3985 pub fn max_macro_body_size(&self) -> usize {
3986 self.limits.max_macro_body_size
3987 }
3988
3989 /// Set the ceiling on a registered macro's BODY node count.
3990 /// Tests bind low values (e.g. `4`) to pin the authoring-bomb
3991 /// rejection shape without materializing a 16K-node blob;
3992 /// consumers that author deep proven macro-body templates can
3993 /// raise it. [`usize::MAX`] admits any lawful body (the ceiling
3994 /// is effectively lifted). Zero is admitted and rejects every
3995 /// non-empty body immediately (a `Nil` body is 1 node, so the
3996 /// smallest lawful body is still rejected under a zero
3997 /// ceiling) — useful for contract-tests that assert "this
3998 /// expander refuses every registration."
3999 pub fn set_max_macro_body_size(&mut self, size: usize) {
4000 self.limits.max_macro_body_size = size;
4001 }
4002
4003 /// The configured ceiling on the [`Self::macros`] table's entry
4004 /// count. Defaults to [`DEFAULT_MAX_REGISTERED_MACROS`]. Peer to
4005 /// [`Self::max_macro_body_size`] one RESOURCE-DIMENSION axis over
4006 /// on the REGISTER-time surface, and to [`Self::max_cache_entries`]
4007 /// one PIPELINE-STAGE axis over.
4008 #[must_use]
4009 pub fn max_registered_macros(&self) -> usize {
4010 self.limits.max_registered_macros
4011 }
4012
4013 /// Set the ceiling on the [`Self::macros`] table's entry count.
4014 /// Tests bind low values (e.g. `3`) to pin the registration-bomb
4015 /// rejection shape without materializing a 4K-entry macros table;
4016 /// consumers that compose deep proven typescapes can raise it.
4017 /// [`usize::MAX`] admits any lawful cumulative registration count
4018 /// (the ceiling is effectively lifted). Zero is admitted and
4019 /// rejects every FRESH registration immediately — useful for
4020 /// contract-tests that assert "this expander accepts no new
4021 /// macros" — while still admitting overwrites of any macro that
4022 /// was registered before the ceiling was set (a re-registration
4023 /// of an existing key never grows the table).
4024 pub fn set_max_registered_macros(&mut self, cap: usize) {
4025 self.limits.max_registered_macros = cap;
4026 }
4027
4028 /// The configured ceiling on a registered macro's lambda-list
4029 /// arity. Defaults to [`DEFAULT_MAX_MACRO_ARITY`]. Peer to
4030 /// [`Self::max_macro_body_size`] and [`Self::max_registered_macros`]
4031 /// one RESOURCE-DIMENSION axis over on the REGISTER-time surface.
4032 #[must_use]
4033 pub fn max_macro_arity(&self) -> usize {
4034 self.limits.max_macro_arity
4035 }
4036
4037 /// Set the ceiling on a registered macro's lambda-list arity —
4038 /// the [`MacroParams::total_arity`] projection of `def.params`.
4039 /// Tests bind low values (e.g. `2`) to pin the arity-bomb rejection
4040 /// shape without materializing a 128-slot authoring blob;
4041 /// consumers that author high-arity domain macros (a `defschema`
4042 /// with 40 typed slots) can raise it. [`usize::MAX`] admits any
4043 /// lawful arity (the ceiling is effectively lifted). Zero is
4044 /// admitted and rejects every non-nullary macro registration
4045 /// immediately — a `(defmacro nullary () BODY)` is still admitted
4046 /// (its `total_arity()` is `0`, and only strict overrun rejects) —
4047 /// useful for contract-tests that assert "this expander accepts
4048 /// only nullary macros."
4049 pub fn set_max_macro_arity(&mut self, cap: usize) {
4050 self.limits.max_macro_arity = cap;
4051 }
4052
4053 /// Snapshot the six configured resource ceilings as ONE typed
4054 /// [`ResourceLimits`] value — the bundled peer of the six
4055 /// [`Self::max_expansion_depth`] / [`Self::max_cache_entries`] /
4056 /// [`Self::max_expansion_size`] / [`Self::max_macro_body_size`] /
4057 /// [`Self::max_registered_macros`] / [`Self::max_macro_arity`]
4058 /// individual getters. Consumers that want to inspect the full
4059 /// resource posture (a test that compares a preset expander to a
4060 /// reference posture, a diagnostic that renders "the six ceilings"
4061 /// as one line, a caller that clones + selectively overrides one
4062 /// field via a struct-update literal) bind to this projection at
4063 /// ONE call site rather than composing the six individual getters.
4064 ///
4065 /// Post the [`Self::limits`]-field consolidation the projection is
4066 /// a direct `Copy` of the bundled field — a `usize`-sized fixed
4067 /// return with no per-field literal walk. Adding a SEVENTH ceiling
4068 /// extends [`ResourceLimits`] and the paired individual
4069 /// getter/setter; this bulk getter is field-shape-agnostic and
4070 /// stays exactly as it is.
4071 #[must_use]
4072 pub fn resource_limits(&self) -> ResourceLimits {
4073 self.limits
4074 }
4075
4076 /// Bulk-replace the six configured resource ceilings from ONE typed
4077 /// [`ResourceLimits`] value — the bundled peer of the six
4078 /// [`Self::set_max_expansion_depth`] /
4079 /// [`Self::set_max_cache_entries`] /
4080 /// [`Self::set_max_expansion_size`] /
4081 /// [`Self::set_max_macro_body_size`] /
4082 /// [`Self::set_max_registered_macros`] /
4083 /// [`Self::set_max_macro_arity`] individual setters. Presets
4084 /// (a hardened CI expander, a permissive REPL expander, a
4085 /// throttled test harness) compose the six ceilings ONCE at the
4086 /// preset call site and apply them ATOMICALLY here rather than
4087 /// threading six independent setter invocations whose ordering
4088 /// the type system does not gate.
4089 ///
4090 /// Round-trip identity: `let l = e.resource_limits(); e.set_resource_limits(l);`
4091 /// leaves `e` in an equivalent posture — pinned as a typed theorem
4092 /// in the [`ResourceLimits`] test cohort. Post the [`Self::limits`]-
4093 /// field consolidation the assignment is a direct `Copy` of the
4094 /// bundled field, structurally atomic — no per-field destructure +
4095 /// six-way write cascade in which a mid-cascade panic could leave
4096 /// the six knobs in a half-updated posture. Sibling of
4097 /// [`Self::resource_limits`]; the pair closes the (getter, setter)
4098 /// face on the aggregation axis.
4099 pub fn set_resource_limits(&mut self, limits: ResourceLimits) {
4100 self.limits = limits;
4101 }
4102
4103 pub fn with_macros<I: IntoIterator<Item = MacroDef>>(defs: I) -> Result<Self> {
4104 let mut e = Self::new();
4105 for d in defs {
4106 e.register_macro_def(d)?;
4107 }
4108 Ok(e)
4109 }
4110
4111 /// Register a parsed [`MacroDef`] into this expander's macro tables —
4112 /// the single named primitive on the `Expander` surface every
4113 /// macro-registration consumer routes through.
4114 ///
4115 /// The registration discipline is a two-step composition:
4116 /// 1. When [`Self::compile_templates`](Self::new) is on (the
4117 /// `Self::new` default; flipped off by [`Self::new_substitute_only`]),
4118 /// [`compile_template`] pre-compiles the macro body to a typed
4119 /// [`CompiledTemplate`] bytecode and inserts it into `self.templates`
4120 /// keyed by `def.name`.
4121 /// 2. The `MacroDef` is moved into `self.macros` keyed by `def.name` —
4122 /// always, regardless of `compile_templates`, because the substitute
4123 /// strategy reads `self.macros` exclusively while the bytecode
4124 /// strategy consults `self.templates` first and falls back to
4125 /// `self.macros` for the body and params.
4126 ///
4127 /// The order is structural: `compile_template` borrows `&def` while
4128 /// `self.macros.insert(def.name.clone(), def)` consumes `def` — the
4129 /// template pre-compile MUST precede the move into `self.macros`, and the
4130 /// `def.name.clone()` projection captures the key for the moved insert.
4131 /// `?`-routing through `compile_template` preserves the structural
4132 /// ordering of the rejection chain end-to-end: a template-compile error
4133 /// (`UnboundTemplateVar` for an unbound `,name`, `NonSymbolUnquoteTarget`
4134 /// for `,5` / `,(nested)`, et al.) short-circuits BEFORE `self.macros`
4135 /// is mutated, so a failed registration leaves both tables exactly as
4136 /// they were — no partial-write window in which `self.macros.has(name)`
4137 /// is true but `self.templates.has(name)` is missing (a regression that
4138 /// would silently coerce the bytecode strategy onto the substitute path
4139 /// for that one macro despite `compile_templates: true`).
4140 ///
4141 /// Before this lift the same two-step block —
4142 ///
4143 /// ```ignore
4144 /// if self.compile_templates {
4145 /// self.templates.insert(def.name.clone(), compile_template(&def)?);
4146 /// }
4147 /// self.macros.insert(def.name.clone(), def);
4148 /// ```
4149 ///
4150 /// — lived byte-identical (modulo `self`/`e` and `def`/`d` substitutions)
4151 /// at TWO sites: [`Self::with_macros`] (the constructor that
4152 /// bulk-registers an `IntoIterator<Item = MacroDef>`, e.g. a curated
4153 /// preloaded set the caller assembled out-of-band) and
4154 /// [`Self::expand_program`]'s `(defmacro …)`-head arm (the program-level
4155 /// walker that recognizes a `defmacro` / `defpoint-template` / `defcheck`
4156 /// head via [`macro_def_from`] and registers it as a side-effect of
4157 /// walking the program). After this lift the registration block lives in
4158 /// ONE method on the `Expander`; both consumers and any future
4159 /// macro-registration surface bind to ONE primitive instead of
4160 /// re-deriving the two-step discipline at every call site.
4161 ///
4162 /// `pub` so authoring surfaces (an LSP that incrementally registers a
4163 /// `(defmacro …)` head as the user finishes typing it without a full
4164 /// program re-expand, a REPL `:define-macro` command that registers a
4165 /// pre-parsed `MacroDef` directly, a future "library merge" operation
4166 /// that absorbs another expander's macro set MacroDef-by-MacroDef) can
4167 /// register a typed `MacroDef` without round-tripping through source
4168 /// serialization first. Sibling of [`Self::with_macros`] (the
4169 /// bulk-from-iterator constructor — itself the
4170 /// `defs.into_iter().try_fold((), |_, d| self.register_macro_def(d))`
4171 /// shape on a fresh expander) and [`Self::expand_program`] (the
4172 /// source-level walker that recognizes `(defmacro …)` heads via
4173 /// [`macro_def_from`] — itself the program-level fold-over-defmacro-heads
4174 /// of this method). All three end up at this primitive.
4175 ///
4176 /// Returns `Result<()>` so the consumer's rejection chain composes with
4177 /// `?`-routing — `with_macros` short-circuits its bulk loop on the first
4178 /// `compile_template` failure; `expand_program` short-circuits its
4179 /// program walk at the offending `(defmacro …)` form. Infallibility on
4180 /// the `compile_templates: false` path is preserved (`compile_template`
4181 /// is gated behind the conditional), so a substitute-only expander never
4182 /// emits the `compile_template`-side rejection chain.
4183 ///
4184 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4185 /// byte-identical inline copies of the registration block across
4186 /// `with_macros` and `expand_program` is past the ≥2 PRIME-DIRECTIVE
4187 /// trigger once the structural shape is named. THEORY.md §V.1 — knowable
4188 /// platform; the macro-registration discipline becomes a NAMED primitive
4189 /// on the substrate's `Expander` surface rather than a per-consumer
4190 /// inline duplication that future emitters (an LSP, a REPL, a library-
4191 /// merge operator) would have had to re-derive. THEORY.md §II.1
4192 /// invariant 2 — free middle; both consumers route through the SAME
4193 /// registration primitive, so a regression that drifts ONE consumer's
4194 /// discipline from the other (one path skips the template pre-compile,
4195 /// one path inserts the `self.macros` entry in a different order, a
4196 /// future third side-effect — logging, attestation, metrics — emitted
4197 /// at one site but not the other) cannot reach the substrate's runtime:
4198 /// there is exactly one implementation both consumers route through.
4199 ///
4200 /// Frontier inspiration: Racket's `(define-syntax name template)` at
4201 /// REPL is exactly this — register a typed macro into the live
4202 /// namespace, no source round-trip; the substrate's `register_macro_def`
4203 /// is the Rust-typed peer of that surface, lifted onto the `Expander`'s
4204 /// table-level algebra (`macros: HashMap<String, MacroDef>` +
4205 /// `templates: HashMap<String, CompiledTemplate>`). MLIR's
4206 /// `OpRegistry::registerOp<Op>()` — typed-op registration into a
4207 /// dialect's live table at construction-time AND at JIT-walk-time
4208 /// through ONE registration entry point; `register_macro_def` is the
4209 /// pleme-io peer of that registration entry point on the macro-table
4210 /// algebra.
4211 pub fn register_macro_def(&mut self, def: MacroDef) -> Result<()> {
4212 // Reject a registration-bomb (a code-generator emitting
4213 // unbounded fresh `(defmacro fresh-N (x) `(list ,x))` heads
4214 // whose bodies each sit comfortably under
4215 // `max_macro_body_size` yet collectively saturate process
4216 // memory) at the cheapest gate on the REGISTER-time surface
4217 // — a `HashMap::len` comparison, no body walk. Peer to the
4218 // body-size gate below one RESOURCE-DIMENSION axis over
4219 // (COUNT vs. SIZE) and to the expand-time cache-entries gate
4220 // in `apply` one PIPELINE-STAGE axis over (REGISTER-time
4221 // COUNT vs. EXPAND-time COUNT). Fires BEFORE the body-size
4222 // gate walks `def.body.node_count()` or any insert lands, so
4223 // a failed registration leaves BOTH tables exactly as they
4224 // were. Overwrites (a re-registration of an already-
4225 // registered key) are ADMITTED at any table size — a
4226 // re-`defmacro` never grows the table, so the ceiling cannot
4227 // gate operator redefinitions. Keyed on `>=` (a table
4228 // already at ceiling rejects a fresh key) so
4229 // `max_registered_macros` names the LARGEST admissible
4230 // `self.macros.len()` — same posture the expand-time
4231 // cache-entries gate holds on the memoization cache.
4232 let is_overwrite = self.macros.contains_key(&def.name);
4233 if !is_overwrite && self.macros.len() >= self.limits.max_registered_macros {
4234 return Err(LispError::RegisteredMacrosExceeded {
4235 macro_name: def.name.clone(),
4236 count: self.macros.len(),
4237 limit: self.limits.max_registered_macros,
4238 });
4239 }
4240 // Reject an arity-bomb `(defmacro huge (a-1 a-2 … a-N-million)
4241 // `,a-1)` — a code-generator whose macro body sits at ONE node
4242 // (well under `max_macro_body_size`) yet whose param list
4243 // carries millions of fresh names the per-index binder walks on
4244 // every call. Peer to the body-size gate below one RESOURCE-
4245 // DIMENSION axis over on the REGISTER-time surface (per-body
4246 // ARITY vs. per-body SIZE), AND peer to the table-count gate
4247 // above one RESOURCE-DIMENSION axis over on the REGISTER-time
4248 // surface (per-registration WIDTH vs. cumulative COUNT). Fires
4249 // BEFORE the body-size gate walks `def.body.node_count()` or
4250 // any insert lands, so a failed registration leaves BOTH tables
4251 // exactly as they were. Cheaper than the body-size gate — a
4252 // three-arm addition on `def.params` fields, no AST walk —
4253 // hence positioned BEFORE it on the O(1)-first ordering of the
4254 // REGISTER-time chain. Keyed on `>` (equality is admitted, only
4255 // strict overrun rejects) so `max_macro_arity` names the
4256 // LARGEST admissible `def.params.total_arity()` — same posture
4257 // the body-size gate holds on the body node count.
4258 let arity = def.params.total_arity();
4259 if arity > self.limits.max_macro_arity {
4260 return Err(LispError::MacroArityExceeded {
4261 macro_name: def.name.clone(),
4262 arity,
4263 limit: self.limits.max_macro_arity,
4264 });
4265 }
4266 // Reject an authoring-bomb `(defmacro huge (x) `<template-of-
4267 // N-million-nodes>)` at the REGISTRATION boundary — the
4268 // PIPELINE-STAGE peer of the EXPAND-time output-size gate in
4269 // `expand_with_depth`. Fires BEFORE `compile_template` walks
4270 // the body or `self.macros.insert` lands the entry, so a
4271 // failed registration leaves BOTH tables exactly as they
4272 // were (no partial-write window in which the body-size
4273 // rejection happened but the template pre-compile already
4274 // consumed cycles or the macros table already carries the
4275 // over-ceiling entry). Keyed on `>` rather than `>=` so
4276 // `max_macro_body_size` names the LARGEST admissible body —
4277 // equality is admitted, only strict overrun rejects — same
4278 // posture the expand-time output-size gate holds.
4279 let body_size = def.body.node_count();
4280 if body_size > self.limits.max_macro_body_size {
4281 return Err(LispError::MacroBodySizeExceeded {
4282 macro_name: def.name.clone(),
4283 size: body_size,
4284 limit: self.limits.max_macro_body_size,
4285 });
4286 }
4287 if self.compile_templates {
4288 self.templates
4289 .insert(def.name.clone(), compile_template(&def)?);
4290 }
4291 self.macros.insert(def.name.clone(), def);
4292 Ok(())
4293 }
4294
4295 /// Expand a whole program. Returns the list of top-level forms after
4296 /// `defmacro` definitions are registered and all macro calls expanded.
4297 pub fn expand_program(&mut self, forms: Vec<Sexp>) -> Result<Vec<Sexp>> {
4298 let mut out = Vec::new();
4299 for form in forms {
4300 if let Some(def) = macro_def_from(&form)? {
4301 self.register_macro_def(def)?;
4302 continue;
4303 }
4304 out.push(self.expand(&form)?);
4305 }
4306 Ok(out)
4307 }
4308
4309 /// Read a source string into top-level forms via [`crate::reader::read`],
4310 /// then route the forms through [`expand_program`](Self::expand_program) —
4311 /// the from-source posture of the yield-all-forms-after-expansion primitive,
4312 /// in ONE method on the `Expander` surface.
4313 ///
4314 /// Before this lift the same two-step chain — `let forms = read(src)?;
4315 /// <expander>.expand_program(forms)` — lived inline at two sites in
4316 /// [`crate::compiler_spec`]: [`RealizedCompiler::compile`](crate::compiler_spec::RealizedCompiler::compile)
4317 /// (the public from-source untyped-expansion entry on a realized compiler,
4318 /// returning the expanded `Vec<Sexp>` for untyped consumers like
4319 /// `tatara-check`'s per-form dispatcher) AND [`realize_in_memory`](crate::compiler_spec::realize_in_memory)'s
4320 /// `:macros` library load loop (the per-spec-macro source absorption that
4321 /// builds the preloaded expander's macro library through `expand_program`'s
4322 /// `defmacro` recognition side-effect). After this lift the read-then-expand
4323 /// composition lives in ONE method on the `Expander`; each of the two
4324 /// consumers binds it with the per-site expander posture that fits its call
4325 /// boundary — `self.preloaded.clone()` for `RealizedCompiler::compile`'s
4326 /// per-call clone, `&mut preloaded` for `realize_in_memory`'s shared
4327 /// build-up.
4328 ///
4329 /// Sibling of [`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)
4330 /// — that method stacks the typed-keyword projection on top of the
4331 /// from-source pipeline (`read + expand_program + iter_calls_to(_,
4332 /// keyword) + map + collect`); this method is the bare yield-all-forms
4333 /// from-source primitive (`read + expand_program`) the typed dispatchers
4334 /// stack their keyword projection atop. The two together close the
4335 /// from-source posture of the program-level walk family on the
4336 /// `Expander` surface: bare (this method) vs. typed-keyword-projected
4337 /// (the sibling).
4338 ///
4339 /// Closes the 2×2 program-level walk family on the `Expander` surface:
4340 /// from-forms × yield-all ([`expand_program`](Self::expand_program)),
4341 /// from-forms × keyword-projected ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)),
4342 /// from-source × keyword-projected ([`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)),
4343 /// AND now from-source × yield-all (this method). The four together name
4344 /// the canonical surfaces a dispatcher composes with to extract an
4345 /// expanded program from either a pre-parsed `Vec<Sexp>` (from-forms
4346 /// posture, for callers composing with another `Sexp`-producing surface)
4347 /// or a raw `&str` (from-source posture, for callers consuming
4348 /// authoring-surface source text directly), with or without a
4349 /// typed-keyword filter on the result.
4350 ///
4351 /// `?`-routing through `read` preserves the structural ordering of the
4352 /// rejection chain end-to-end: a reader error (lexer / parser /
4353 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4354 /// `expand_program` runs; an `expand_program` error
4355 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4356 /// `RestParamMissingName`, et al.) short-circuits at the offending form.
4357 /// Each consumer's rejection chain remains exactly what it was pre-lift,
4358 /// now sourced from ONE composition point rather than two.
4359 ///
4360 /// `defmacro`-registration side-effects fire on `&mut self` exactly as
4361 /// they do for the from-forms primitive — `realize_in_memory`'s per-spec
4362 /// build-up depends on every `defmacro` in every `:macros` source
4363 /// landing in `self.macros` (and, when `compile_templates` is on, in
4364 /// `self.templates`); `RealizedCompiler::compile`'s per-call clone
4365 /// posture isolates absorption to the cloned expander, so a
4366 /// `defmacro` in the user's source does NOT leak into the persistent
4367 /// realized compiler. Both postures' absorption semantics are
4368 /// preserved by routing through this primitive instead of inlining
4369 /// the two-step chain.
4370 ///
4371 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4372 /// inline copies of the `let forms = read(src)?; <expander>.expand_program(forms)`
4373 /// two-step chain across `RealizedCompiler::compile` and
4374 /// `realize_in_memory` is past the ≥2 PRIME-DIRECTIVE trigger once the
4375 /// structural shape is named. THEORY.md §V.1 — knowable platform; the
4376 /// read-then-expand composition becomes a NAMED primitive on the
4377 /// substrate's `Expander` surface rather than a re-derived two-step
4378 /// inline pipeline at every consumer. THEORY.md §II.1 invariant 2 —
4379 /// free middle; both consumers route through the SAME composition
4380 /// primitive, so a regression that drifts ONE consumer's pipeline
4381 /// shape from the other cannot reach the substrate's runtime.
4382 ///
4383 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4384 /// namespace — the from-source-string entry to namespace-level
4385 /// program evaluation is the Racket idiom; the substrate's
4386 /// `expand_source_program` is the Rust-typed peer of that, sourced
4387 /// from `&str` and yielding the post-macroexpansion `Vec<Sexp>`
4388 /// without a typed-keyword filter — exactly the shape an untyped
4389 /// consumer (`tatara-check`'s per-form dispatcher, a REPL's
4390 /// "expand this buffer" command, an LSP's "show me the expanded
4391 /// program" handler) binds to.
4392 pub fn expand_source_program(&mut self, src: &str) -> Result<Vec<Sexp>> {
4393 let forms = crate::reader::read(src)?;
4394 self.expand_program(forms)
4395 }
4396
4397 /// Compose the expander's program-level expansion with the substrate's
4398 /// slice-side typed-keyword projection ([`iter_calls_to`]) and a
4399 /// caller-supplied per-form projection — `expand_program(forms)?` followed
4400 /// by `iter_calls_to(&expanded, keyword).map(project).collect()`, in ONE
4401 /// method on the `Expander` surface. Both [`compile_typed`](crate::compile::compile_typed)
4402 /// and [`compile_named_from_forms`](crate::compile::compile_named_from_forms)
4403 /// route through this primitive — they differ only in the per-form
4404 /// projection `F`: `T::compile_from_args` for the bare-kwargs form
4405 /// (`compile_typed`), and the NAME-then-`T::compile_from_args` split
4406 /// (`compile_named_from_forms`).
4407 ///
4408 /// Before this lift each dispatcher opened the same three-step pipeline
4409 /// inline — `let mut exp = Expander::new(); let expanded =
4410 /// exp.expand_program(forms)?; iter_calls_to(&expanded, T::KEYWORD)
4411 /// .map(<per-site>).collect()` — past the ≥2 PRIME-DIRECTIVE trigger
4412 /// once the structural shape is named. After this lift the pipeline
4413 /// lives in ONE method on the `Expander` surface and the two
4414 /// dispatchers thread their per-site projection through `F`; a
4415 /// regression that drifts ONE dispatcher's pipeline from the other (a
4416 /// future emitter that re-derives `expand_program + iter_calls_to`
4417 /// inline rather than composing through this primitive, a future
4418 /// preloaded-expander consumer that wants the same walk but on its
4419 /// own `Expander` rather than a fresh one) is no longer a silent
4420 /// two-site divergence: the method binds the composition once and
4421 /// every consumer threads the per-site projection through `F`.
4422 ///
4423 /// Method on `Expander` (not a free function) so the primitive
4424 /// composes with the broader expander surface: a preloaded expander
4425 /// (e.g., one that has already absorbed a set of `defmacro` forms
4426 /// via `expand_program` or `with_macros`) can call this method on
4427 /// its own state to walk a follow-up program — the
4428 /// `compiler_spec`'s realize path is the natural future consumer for
4429 /// that shape. `compile_typed` and `compile_named_from_forms`
4430 /// instantiate a fresh `Expander::new()` and dispatch through this
4431 /// method on it; a future preloaded consumer dispatches through it
4432 /// on the preloaded expander directly. ONE primitive, two postures
4433 /// (fresh vs. preloaded), no per-posture duplication of the
4434 /// `expand_program + iter_calls_to + map + collect` pipeline.
4435 ///
4436 /// `F` is `FnMut(&[Sexp]) -> Result<R>` — the per-form projection
4437 /// that fits the consumer's call site. The standard-library
4438 /// `Iterator::map` bound is `FnMut`, so a closure that captures
4439 /// mutable state (a future dispatcher that threads a running index
4440 /// or a borrowed accumulator into the projection) composes
4441 /// naturally. The `Result<R>` projection short-circuits on the
4442 /// first error via `Iterator::collect::<Result<Vec<R>, _>>()`, so
4443 /// the per-form rejection chain (`compile_named_from_forms`'s
4444 /// `NamedFormMissingName` for the missing NAME slot,
4445 /// `NamedFormNonSymbolName` for the non-symbol NAME slot,
4446 /// `T::compile_from_args`'s typed-entry kwargs gate, AND
4447 /// `compile_typed`'s bare-kwargs `T::compile_from_args` rejection)
4448 /// fires in the same order under the new shape.
4449 ///
4450 /// `R` is owned by construction — the iterator's `&[Sexp]` items
4451 /// borrow from the local `expanded: Vec<Sexp>` and that borrow
4452 /// ends when the `.collect()` consumes the iterator, so a `R`
4453 /// that borrowed from `expanded` would fail to compile here. The
4454 /// two production consumers (`Vec<T>`, `Vec<NamedDefinition<T>>`)
4455 /// are both owned-`R` shapes, matching the borrow's structural
4456 /// constraint.
4457 ///
4458 /// Sibling of [`expand_program`](Self::expand_program) — that
4459 /// method names the FIRST step of the pipeline (program-level
4460 /// macroexpansion, yielding `Vec<Sexp>`); this method composes it
4461 /// with the SECOND step (slice-side typed-keyword projection +
4462 /// per-form mapper). The split lets a consumer that wants the
4463 /// expanded forms WITHOUT the keyword filter (`tatara-check`'s
4464 /// per-form dispatcher walks every form, not just matched
4465 /// keywords) bind to `expand_program` directly; a consumer that
4466 /// wants both halves composed binds to this method.
4467 ///
4468 /// Closes the substrate's program-level walk family on the
4469 /// `Expander` surface: `expand_program` (yield-all-forms-after-
4470 /// expansion), `expand` (per-form recursive expansion), `apply`
4471 /// (per-call substitution), AND now `expand_and_collect_calls_to`
4472 /// (typed-keyword projection of the expanded forms with a per-form
4473 /// mapper). Together the four name the canonical surfaces a
4474 /// dispatcher composes with to extract a typed program from a
4475 /// post-expansion form set.
4476 ///
4477 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4478 /// two inline copies of the `Expander::new() + expand_program +
4479 /// iter_calls_to(_, T::KEYWORD) + map + collect` pipeline across
4480 /// `compile_typed` and `compile_named_from_forms` is past the ≥2
4481 /// PRIME-DIRECTIVE trigger once the structural shape is named.
4482 /// THEORY.md §V.1 — knowable platform; the program-level walk's
4483 /// typed-keyword-projection composition becomes a NAMED primitive
4484 /// on the substrate's `Expander` surface rather than a re-derived
4485 /// three-step inline pipeline at every consumer. Authoring tools
4486 /// (REPL, LSP, `tatara-check`) that want to walk a program by
4487 /// typed keyword bind to ONE method on the `Expander` instead of
4488 /// re-implementing the composition. THEORY.md §II.1 invariant 1 —
4489 /// typed entry; the typed-keyword filter over an expanded program
4490 /// IS the rust-level typed-entry-batch gate, and naming its
4491 /// single shape lifts the gate from two-site duplication to one
4492 /// rust method the substrate's diagnostic promotions hang off of.
4493 /// THEORY.md §II.1 invariant 2 — free middle; both consumers
4494 /// route through the SAME composition primitive, each binding the
4495 /// per-form projection that fits its call site — a regression
4496 /// that drifts ONE dispatcher's pipeline shape from the other
4497 /// cannot reach the substrate's runtime.
4498 ///
4499 /// Frontier inspiration: MLIR's `Region::walk<Op>(callback)` —
4500 /// every typed rewriter that wants "for every Op of kind K in this
4501 /// region, run callback" binds to ONE typed walker that composes
4502 /// the kind filter with the per-op visitor; the substrate's
4503 /// `expand_and_collect_calls_to` is the unstructured-projection
4504 /// peer of that walker, lifted onto the post-expansion `&[Sexp]`
4505 /// algebra with the per-form projection as the visitor. Racket's
4506 /// `syntax-parse` `~seq (keyword args ...) ...` ellipsis-form —
4507 /// the program-level typed-keyword filter with per-match handler
4508 /// is the closed-form sibling of `~seq`'s repeated-pattern
4509 /// matcher, translated through pleme-io primitives as ONE method
4510 /// on `Expander` composing `expand_program` with `iter_calls_to`
4511 /// and a per-form mapper. GHC Core's `everything :: forall b. (b
4512 /// -> b -> b) -> GenericQ b -> GenericQ b` — the typed-IR rewriter's
4513 /// program-level fold over a typed selector is named ONCE and
4514 /// every consumer threads the per-node projection; the substrate's
4515 /// `expand_and_collect_calls_to` is the keyword-projected sibling
4516 /// of that fold on the `&[Sexp]` algebra.
4517 pub fn expand_and_collect_calls_to<R, F>(
4518 &mut self,
4519 forms: Vec<Sexp>,
4520 keyword: &str,
4521 mut project: F,
4522 ) -> Result<Vec<R>>
4523 where
4524 F: FnMut(&[Sexp]) -> Result<R>,
4525 {
4526 // Routes through the typed-decoded classifier sibling
4527 // [`Self::expand_and_collect_calls_to_any`] with a constant-
4528 // classifier decoder — the same constant-classifier composition
4529 // [`crate::ast::iter_calls_to`] uses to route through
4530 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
4531 // discarded `()` typed witness (`then_some(())`) is consumed by
4532 // the wrapper projection `|(), args| project(args)` so the
4533 // keyword consumer's per-form mapper sees only the args tail,
4534 // matching the pre-lift signature exactly. Both the keyword AND
4535 // classifier expander walks now share ONE pipeline implementation
4536 // (`expand_program + iter_calls_to_any + map + collect`); a
4537 // regression that drifts a future debug-mode logger, span-aware
4538 // borrow walker, or fused-iterator invariant from one expander
4539 // walk to the other becomes structurally impossible.
4540 self.expand_and_collect_calls_to_any(
4541 forms,
4542 |h| (h == keyword).then_some(()),
4543 move |(), args| project(args),
4544 )
4545 }
4546
4547 /// Compose the expander's program-level expansion with the substrate's
4548 /// slice-side typed-decoded classifier projection ([`iter_calls_to_any`])
4549 /// and a caller-supplied per-form projection — `expand_program(forms)?`
4550 /// followed by `iter_calls_to_any(&expanded, decode).map(|(decoded, args)|
4551 /// project(decoded, args)).collect()`, in ONE method on the `Expander`
4552 /// surface. The typed-decoded sibling of
4553 /// [`Self::expand_and_collect_calls_to`] — where that method filters by
4554 /// ONE constant keyword, this method filters AND TYPES by a caller-
4555 /// supplied classifier, yielding the typed witness alongside the per-form
4556 /// projection's args input.
4557 ///
4558 /// Closes the substrate's program-level walk family on the `Expander`
4559 /// surface at the typed-decoded corner the prior runs' slice-side
4560 /// `iter_calls_to_any` lift left open — the (keyword, classifier) 2×2
4561 /// of compose-on-iter projections:
4562 ///
4563 /// | | per-form algebra | slice algebra | expander surface |
4564 /// |----------------|-----------------------------|------------------------------|-------------------------------------|
4565 /// | keyword | [`Sexp::as_call_to`] | [`iter_calls_to`] | [`Self::expand_and_collect_calls_to`] |
4566 /// | classifier `F` | [`Sexp::as_call_to_any`] | [`iter_calls_to_any`] | `expand_and_collect_calls_to_any` (this) |
4567 ///
4568 /// The keyword corner now ROUTES through the classifier corner at every
4569 /// row: [`Sexp::as_call_to`] = constant-classifier projection of
4570 /// [`Sexp::as_call_to_any`]; [`iter_calls_to`] = constant-classifier
4571 /// projection of [`iter_calls_to_any`]; [`Self::expand_and_collect_calls_to`]
4572 /// = constant-classifier projection of THIS method. The substrate's
4573 /// soft-dispatch family is structurally complete at every algebra
4574 /// level — per-form, slice, AND expander — so a regression that drifts
4575 /// a future debug-mode logger, span-aware borrow walker, or
4576 /// fused-iterator invariant from one walk to its sibling at any algebra
4577 /// level becomes structurally impossible.
4578 ///
4579 /// Two plausible future consumer shapes the typed-decoded expander walk
4580 /// admits with no boilerplate:
4581 /// * **Closed-set classifier** — `expand_and_collect_calls_to_any(forms,
4582 /// MacroDefHead::from_keyword, |head, args| dispatch(head, args))`
4583 /// macroexpands a program and walks each `(defmacro …)` /
4584 /// `(defpoint-template …)` / `(defcheck …)` form decoded to the typed
4585 /// `MacroDefHead` enum with its args tail. Future `tatara-check`
4586 /// consumers that want "for every macro-definition form in this
4587 /// buffer, dispatch by typed kind" bind to ONE method on the
4588 /// `Expander` rather than the three-step `expand_program +
4589 /// iter_calls_to_any + map + collect` pipeline at each consumer site.
4590 /// * **Live-registry classifier** — `expand_and_collect_calls_to_any(forms,
4591 /// |h| registry.lookup(h), |handler, args| handler.handle(args))`
4592 /// macroexpands a program and walks each form whose head matches a
4593 /// runtime registry, decoded to a handler reference. Future
4594 /// `tatara-check`-shaped runtime dispatchers (the kind already named
4595 /// by [`iter_calls_to_any`]'s docstring as "the natural future
4596 /// consumer") bind to ONE method on the `Expander` rather than the
4597 /// three-step pipeline.
4598 ///
4599 /// The closed-form composition binding the keyword sibling to this typed-
4600 /// decoded primitive is the structural identity every consumer can pin
4601 /// against:
4602 ///
4603 /// ```ignore
4604 /// expand_and_collect_calls_to(forms, k, p) ==
4605 /// expand_and_collect_calls_to_any(forms,
4606 /// |h| (h == k).then_some(()),
4607 /// |(), args| p(args))
4608 /// ```
4609 ///
4610 /// `D` is `FnMut(&str) -> Option<T>` — the typed-decoded classifier the
4611 /// substrate's per-form algebra already shapes ([`Sexp::as_call_to_any`]
4612 /// uses `FnOnce`, the slice algebra and this method use `FnMut` for the
4613 /// same reason: the walk calls the decoder once per matching form, and a
4614 /// decoder that captures mutable state (a counter, a registry cache, a
4615 /// visited-set) maintains that state across the program-level walk).
4616 /// `F` is `FnMut(T, &[Sexp]) -> Result<R>` — the per-form projection that
4617 /// receives BOTH the typed witness AND the args tail; the keyword sibling
4618 /// discards the (unit) typed witness via `|(), args| project(args)`. The
4619 /// `Result<R>` projection short-circuits on the first error via
4620 /// `Iterator::collect::<Result<Vec<R>, _>>()` so the per-form rejection
4621 /// chain fires in source order at the first failing matched form.
4622 ///
4623 /// `R` is owned by construction — same constraint as the keyword
4624 /// sibling. `T` is owned because the underlying [`iter_calls_to_any`]
4625 /// requires `T: 'a` where `'a` is the post-expansion `Vec<Sexp>` borrow
4626 /// lifetime; consumers projecting to a typed `Copy` enum
4627 /// (`MacroDefHead`, a future `AuthoringDirective`) get the value
4628 /// directly per form, consumers projecting to a borrowed `&'static str`
4629 /// (a closed-set head) project to `&'static str` and inherit the static
4630 /// lifetime through the classifier, consumers projecting to a `&Handler`
4631 /// (a live-registry classifier) bind through the registry's owned
4632 /// borrow with `&self` outliving the walk.
4633 ///
4634 /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
4635 /// keyword sibling [`Self::expand_and_collect_calls_to`] is now a
4636 /// CONSEQUENCE of this typed-decoded primitive + a constant-classifier
4637 /// decoder, parallel to how [`iter_calls_to`] is a consequence of
4638 /// [`iter_calls_to_any`] on the slice algebra. The substrate's
4639 /// program-level walk family is no longer two parallel implementations
4640 /// the type system fails to bind (a future regression that drifts the
4641 /// keyword pipeline's instrumentation from the classifier pipeline's
4642 /// instrumentation cannot reach the runtime — both compose through ONE
4643 /// `expand_program + iter_calls_to_any + map + collect` body, with the
4644 /// keyword filter expressed as a closed-form classifier). THEORY.md
4645 /// §V.1 — knowable platform; the typed-decoded expander walk becomes a
4646 /// NAMED primitive on the substrate's `Expander` surface rather than a
4647 /// future re-derived three-step inline pipeline per consumer.
4648 /// Authoring tools (REPL, LSP, `tatara-check`) that want to walk a
4649 /// program by typed-decoded classifier bind to ONE method on the
4650 /// `Expander` instead of re-implementing the composition. THEORY.md
4651 /// §II.1 invariant 2 — free middle; both expander-surface dispatchers
4652 /// (keyword + classifier) route through the SAME `expand_program +
4653 /// iter_calls_to_any + map + collect` composition primitive.
4654 ///
4655 /// Frontier inspiration: MLIR's `Region::walk<OpInterface,
4656 /// OpInterface2, …>([&](auto op) { … })` — the typed-IR walk over a
4657 /// region yielding ops decoded to their typed interface witness with
4658 /// per-op callback is the MLIR idiom; the substrate's
4659 /// `expand_and_collect_calls_to_any` is the unstructured-Rust peer on
4660 /// the post-expansion `&[Sexp]` algebra, with `decode: FnMut(&str) ->
4661 /// Option<T>` standing in for MLIR's typed-interface dyn-cast bag and
4662 /// `project: FnMut(T, &[Sexp]) -> Result<R>` standing in for the
4663 /// typed-op callback. Racket's `syntax-parse` `~or* (~datum defX)
4664 /// (~datum defY) (head args …)` over an ellipsis-form combined with a
4665 /// `#:with name:id` typed-witness binder — the program-level
4666 /// typed-decoded filter with per-match handler is the closed-form
4667 /// sibling of `syntax-parse`'s typed-choice repeater, translated
4668 /// through pleme-io primitives as ONE method on `Expander`. GHC Core's
4669 /// `everythingBut :: (a -> a -> a) -> GenericQ (a, Bool) -> GenericQ a`
4670 /// — the typed-IR rewriter's program-level fold over a typed
4671 /// kind-with-stop selector is named ONCE and every consumer threads
4672 /// the per-node typed projection; this method is the typed-decoded
4673 /// peer on the `Vec<Sexp>` algebra, with the classifier playing
4674 /// `everythingBut`'s typed-selector role.
4675 pub fn expand_and_collect_calls_to_any<R, F, D, T>(
4676 &mut self,
4677 forms: Vec<Sexp>,
4678 decode: D,
4679 mut project: F,
4680 ) -> Result<Vec<R>>
4681 where
4682 D: FnMut(&str) -> Option<T>,
4683 F: FnMut(T, &[Sexp]) -> Result<R>,
4684 {
4685 let expanded = self.expand_program(forms)?;
4686 crate::ast::iter_calls_to_any(&expanded, decode)
4687 .map(|(decoded, args)| project(decoded, args))
4688 .collect()
4689 }
4690
4691 /// Read a source string into top-level forms via [`crate::reader::read`],
4692 /// then route the forms through
4693 /// [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to) — the
4694 /// from-source posture of the program-level walk, in ONE method on the
4695 /// `Expander` surface.
4696 ///
4697 /// Before this lift the same two-step chain — `let forms = read(src)?;
4698 /// <expander>.expand_and_collect_calls_to(forms, keyword, project)` — lived
4699 /// inline at four sites: the two free-function typed dispatchers
4700 /// ([`compile_typed`](crate::compile::compile_typed) and
4701 /// [`compile_named`](crate::compile::compile_named) via
4702 /// [`compile_named_from_forms`](crate::compile::compile_named_from_forms))
4703 /// AND the two preloaded-expander methods on
4704 /// [`RealizedCompiler`](crate::compiler_spec::RealizedCompiler)
4705 /// (`compile_typed`, `compile_named`). After this lift the read-then-walk
4706 /// composition lives in ONE method on the `Expander`; each of the four
4707 /// dispatchers binds it with the per-site `(expander posture, projection)`
4708 /// pair that fits its call boundary — `Expander::new()` for the
4709 /// fresh-expander dispatchers, `self.preloaded.clone()` for the
4710 /// preloaded-expander dispatchers; `T::compile_from_args` for the
4711 /// bare-kwargs projection, `named_form_projection::<T>` for the
4712 /// NAME-then-kwargs projection.
4713 ///
4714 /// Sibling of [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)
4715 /// — that method takes a pre-parsed `Vec<Sexp>` (the from-forms posture,
4716 /// for callers that have already read or that compose with another
4717 /// `Sexp`-producing surface like a macro-expanded subform); this method
4718 /// takes a `&str` (the from-source posture, for callers consuming
4719 /// authoring-surface source text directly). Both compose with the same
4720 /// `expand_program + iter_calls_to + map + collect` pipeline through ONE
4721 /// from-forms primitive — the from-source posture stacks one read step on
4722 /// top, projecting `crate::reader::read`'s `Result<Vec<Sexp>>` into the
4723 /// from-forms primitive via `?`.
4724 ///
4725 /// `?`-routing through `read` preserves the structural ordering of the
4726 /// rejection chain end-to-end: a reader error (lexer / parser /
4727 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4728 /// `expand_program` runs; an `expand_program` error
4729 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4730 /// `RestParamMissingName`, et al.) short-circuits BEFORE the keyword
4731 /// filter walks anything; a per-form `project` error
4732 /// (`NamedFormMissingName`, `NamedFormNonSymbolName`,
4733 /// `T::compile_from_args`'s typed-entry kwargs gate) short-circuits at the
4734 /// first failing matched form via `Iterator::collect::<Result<Vec<R>, _>>()`.
4735 /// Each dispatcher's rejection chain remains exactly what it was pre-lift,
4736 /// now sourced from ONE composition point rather than four.
4737 ///
4738 /// Closes the program-level walk family on the `Expander` surface across
4739 /// BOTH the from-forms posture
4740 /// ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to))
4741 /// AND the from-source posture (this method) — together with
4742 /// `expand_program` (yield-all-forms-after-expansion), `expand` (per-form
4743 /// recursive expansion), and `apply` (per-call substitution), they name
4744 /// the canonical surfaces a dispatcher composes with to extract a typed
4745 /// program from a post-expansion form set. A future dispatcher that wants
4746 /// "read this source, then walk every call to keyword K and project each
4747 /// to R" — a debug-mode REPL command, an LSP "find all typed-domain
4748 /// definitions in this buffer" handler, a `tatara-check` command that
4749 /// dispatches each typed `(defX …)` form in `checks.lisp` to its
4750 /// registered domain — binds to ONE method on the `Expander` instead of
4751 /// re-deriving the two-step `read + expand_and_collect_calls_to` chain.
4752 ///
4753 /// Theory anchor: THEORY.md §VI.1 — generation over composition; four
4754 /// inline copies of the `read(src)? + <expander>.expand_and_collect_calls_to`
4755 /// chain across the two fresh-expander dispatchers and the two
4756 /// preloaded-expander dispatchers is past the ≥2 PRIME-DIRECTIVE trigger
4757 /// once the structural shape is named. THEORY.md §V.1 — knowable
4758 /// platform; the read-then-walk composition becomes a NAMED primitive on
4759 /// the substrate's `Expander` surface rather than a re-derived two-step
4760 /// inline pipeline at every dispatcher. THEORY.md §II.1 invariant 2 —
4761 /// free middle; all four dispatchers route through the SAME composition
4762 /// primitive, so a regression that drifts ONE dispatcher's read-then-walk
4763 /// pipeline from the others cannot reach the substrate's runtime — the
4764 /// type system binds all consumers to the from-source primitive's single
4765 /// emission shape.
4766 ///
4767 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4768 /// namespace populated with the preloaded compiler's `require`d macros —
4769 /// the from-source-string entry to typed-program evaluation inside a
4770 /// namespace that carries the macro library is the Racket idiom; the
4771 /// substrate's `expand_source_and_collect_calls_to` is the Rust-typed
4772 /// peer of that, with the typed-keyword projection composed in.
4773 ///
4774 /// Post-lift the body routes through the typed-decoded sibling
4775 /// [`Self::expand_source_and_collect_calls_to_any`] with a
4776 /// constant-classifier decoder — the same constant-classifier
4777 /// composition [`Self::expand_and_collect_calls_to`] uses to route
4778 /// through [`Self::expand_and_collect_calls_to_any`] on the
4779 /// from-forms axis, and that [`crate::ast::iter_calls_to`] uses to
4780 /// route through [`crate::ast::iter_calls_to_any`] on the slice
4781 /// algebra. The discarded `()` typed witness (`then_some(())`) is
4782 /// consumed by the wrapper projection `|(), args| project(args)`
4783 /// so the keyword consumer's per-form mapper sees only the args
4784 /// tail. Both the keyword AND classifier from-source walks now
4785 /// share ONE pipeline implementation (`read + expand_program +
4786 /// iter_calls_to_any + map + collect`) — a regression that drifts
4787 /// a future debug-mode logger, span-aware borrow walker, or
4788 /// fused-iterator invariant from one from-source walk to the
4789 /// other becomes structurally impossible.
4790 pub fn expand_source_and_collect_calls_to<R, F>(
4791 &mut self,
4792 src: &str,
4793 keyword: &str,
4794 mut project: F,
4795 ) -> Result<Vec<R>>
4796 where
4797 F: FnMut(&[Sexp]) -> Result<R>,
4798 {
4799 self.expand_source_and_collect_calls_to_any(
4800 src,
4801 |h| (h == keyword).then_some(()),
4802 move |(), args| project(args),
4803 )
4804 }
4805
4806 /// Read a source string into top-level forms via [`crate::reader::read`],
4807 /// then route the forms through
4808 /// [`expand_and_collect_calls_to_any`](Self::expand_and_collect_calls_to_any) —
4809 /// the from-source posture of the typed-decoded classifier walk, in
4810 /// ONE method on the `Expander` surface. The typed-decoded sibling of
4811 /// [`Self::expand_source_and_collect_calls_to`] — where that method
4812 /// filters by ONE constant keyword, this method filters AND TYPES by
4813 /// a caller-supplied classifier, yielding the typed witness alongside
4814 /// the per-form projection's args input. Sourced from `&str` rather
4815 /// than a `Vec<Sexp>`.
4816 ///
4817 /// Closes the substrate's program-level walk family on the `Expander`
4818 /// surface across BOTH input postures × BOTH projection forms — the
4819 /// (from-forms, from-source) × (keyword, classifier) 2×2 of
4820 /// compose-on-iter projections:
4821 ///
4822 /// | | from-forms (`Vec<Sexp>`) | from-source (`&str`) |
4823 /// |----------------|---------------------------------------------------------------|---------------------------------------------------------------------|
4824 /// | keyword | [`Self::expand_and_collect_calls_to`] | [`Self::expand_source_and_collect_calls_to`] |
4825 /// | classifier `F` | [`Self::expand_and_collect_calls_to_any`] | `expand_source_and_collect_calls_to_any` (this) |
4826 ///
4827 /// Each row's keyword corner now ROUTES through the classifier corner
4828 /// at every column: [`Self::expand_source_and_collect_calls_to`]'s
4829 /// body composes this typed-decoded primitive with a
4830 /// `|h| (h == keyword).then_some(())` decoder and the wrapper
4831 /// projection `|(), args| project(args)`, parallel to how
4832 /// [`Self::expand_and_collect_calls_to`] routes through
4833 /// [`Self::expand_and_collect_calls_to_any`] on the from-forms axis
4834 /// and [`crate::ast::iter_calls_to`] routes through
4835 /// [`crate::ast::iter_calls_to_any`] on the slice algebra. The 2×2
4836 /// reduces to ONE pipeline implementation
4837 /// (`read + expand_program + iter_calls_to_any + map + collect`)
4838 /// every consumer threads its `(decoder, project)` pair through.
4839 ///
4840 /// Future consumer shapes the typed-decoded from-source primitive
4841 /// admits:
4842 /// * **Closed-set classifier** — `expand_source_and_collect_calls_to_any(
4843 /// src, MacroDefHead::from_keyword, |head, args| { … })` walks a
4844 /// source buffer dispatching every `(defmacro …)` / `(defpoint-template …)` /
4845 /// `(defcheck …)` form to its typed `MacroDefHead` arm — exactly the
4846 /// shape a future LSP "find every macro-definition form in this
4847 /// buffer, decode each by typed kind" handler reaches for, sourced
4848 /// from authoring text directly rather than a pre-parsed
4849 /// `Vec<Sexp>`.
4850 /// * **Live-registry classifier** — `expand_source_and_collect_calls_to_any(
4851 /// src, |h| registry.get(h), |handler, args| handler.compile(args))`
4852 /// walks a source buffer dispatching every form whose head is
4853 /// registered to its typed handler — exactly the shape
4854 /// `tatara-check`'s "macroexpand checks.lisp and dispatch every
4855 /// `(defX …)` form through the registered domain dispatcher"
4856 /// walker reaches for, sourced from disk-loaded source text
4857 /// rather than the round-tripped `Vec<Sexp>` the typed
4858 /// dispatchers consume.
4859 ///
4860 /// `?`-routing through `read` preserves the structural ordering of
4861 /// the rejection chain end-to-end: a reader error (lexer / parser /
4862 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4863 /// `expand_program` runs; an `expand_program` error short-circuits
4864 /// BEFORE the classifier filter walks anything; a per-form `project`
4865 /// error short-circuits at the first failing matched form via
4866 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
4867 /// rejection chain inherits the from-forms typed-decoded primitive's
4868 /// shape verbatim, now sourced from `&str` via ONE composition point.
4869 ///
4870 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4871 /// the (from-forms, from-source) × (keyword, classifier) 2×2 closes
4872 /// at the from-source classifier corner this method establishes —
4873 /// the prior runs' from-forms classifier sibling
4874 /// (`expand_and_collect_calls_to_any`, run e47d043) AND from-source
4875 /// keyword sibling (`expand_source_and_collect_calls_to`, run
4876 /// b477d81) AND slice-side classifier sibling (`iter_calls_to_any`,
4877 /// run 38625e3) named three of the four corners; this lift names
4878 /// the fourth so the family is structurally complete at every
4879 /// algebra level. THEORY.md §V.1 — knowable platform; the
4880 /// read-then-classifier-walk composition becomes a NAMED primitive
4881 /// on the substrate's `Expander` surface rather than a future
4882 /// re-derived `read(src)? + expand_and_collect_calls_to_any(…)`
4883 /// two-step chain at every consumer that wants to walk source text
4884 /// by typed-decoded classifier. THEORY.md §II.1 invariant 2 — free
4885 /// middle; the from-source keyword filter
4886 /// (`expand_source_and_collect_calls_to`) now routes through the
4887 /// from-source classifier filter via the constant-classifier
4888 /// composition, so a regression that drifts the keyword filter's
4889 /// instrumentation from the classifier filter's instrumentation
4890 /// becomes structurally impossible. The pre-lift inline
4891 /// `read(src)? + expand_and_collect_calls_to(forms, keyword, project)`
4892 /// chain is now a TYPED CONSEQUENCE of the typed-decoded primitive
4893 /// composed with a constant-classifier decoder, not a parallel
4894 /// implementation the type system happens to not catch.
4895 ///
4896 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4897 /// namespace + typed-syntax-class dispatch — the from-source-string
4898 /// entry to typed-program evaluation inside a namespace that carries
4899 /// the macro library, dispatching by typed syntax-class is the
4900 /// Racket idiom; the substrate's
4901 /// `expand_source_and_collect_calls_to_any` is the Rust-typed peer
4902 /// of that, with the typed-decoded classifier composed in. MLIR's
4903 /// `parseSourceFile<Op>(srcFile, ctx)` then `mod.walk<OpInterface>(
4904 /// callback)` — the parse-then-typed-walk over a source buffer
4905 /// dispatching by typed-interface witness is the MLIR idiom; this
4906 /// method is the substrate's typed `&[Sexp]`-algebra peer.
4907 pub fn expand_source_and_collect_calls_to_any<R, F, D, T>(
4908 &mut self,
4909 src: &str,
4910 decode: D,
4911 project: F,
4912 ) -> Result<Vec<R>>
4913 where
4914 D: FnMut(&str) -> Option<T>,
4915 F: FnMut(T, &[Sexp]) -> Result<R>,
4916 {
4917 let forms = crate::reader::read(src)?;
4918 self.expand_and_collect_calls_to_any(forms, decode, project)
4919 }
4920
4921 /// Compose the expander's program-level expansion with the substrate's
4922 /// typed-decoded classifier walk AND the named-form NAME-shape gate
4923 /// ([`crate::compile::split_name_slot`]) — the from-forms posture of
4924 /// the (named NAME-then-kwargs × typed-decoded classifier) cell of
4925 /// the typed-dispatcher matrix on the `Expander` surface, sibling of
4926 /// [`Self::expand_to_named`] (the constant-`T::KEYWORD` × named cell)
4927 /// and of [`Self::expand_and_collect_calls_to_any`] (the from-forms
4928 /// classifier × bare-kwargs cell).
4929 ///
4930 /// Routes through [`Self::expand_and_collect_calls_to_any`] with a
4931 /// wrapper projection that composes [`crate::compile::split_name_slot`]
4932 /// (the named-form arity + NAME-shape gate on the substrate's
4933 /// `&[Sexp]` algebra) with the caller-supplied per-form projection.
4934 /// The decoder yields `Option<(T, &'static str)>` — the typed witness
4935 /// PAIRED with the canonical static keyword the named-form structural
4936 /// rejection variants ([`crate::error::LispError::NamedFormMissingName`],
4937 /// [`crate::error::LispError::NamedFormNonSymbolName`]) carry as
4938 /// `&'static str` slots. Threading the `&'static` constraint through
4939 /// the decoder's return type pins the same compile-time discipline
4940 /// [`crate::compile::split_name_slot`]'s `keyword: &'static str`
4941 /// parameter pins at the gate boundary — a typo in the canonical
4942 /// keyword can never drift into the diagnostic at runtime, same posture
4943 /// as the constant-`T::KEYWORD` cell where `T::KEYWORD` is
4944 /// `&'static str` by trait construction.
4945 ///
4946 /// Closes the (named × typed-decoded classifier) corner of the
4947 /// typed-dispatcher matrix on the `Expander` surface that the prior
4948 /// run's [`crate::compile::split_name_slot`] lift (dd50801) named as
4949 /// the future change the slice-side gate's named lift enables. The
4950 /// 2×2 of compose-on-iter projections on the named-form axis becomes
4951 /// structurally complete on the `Expander` surface:
4952 ///
4953 /// | | constant `T::KEYWORD` | typed-decoded classifier |
4954 /// |----------------|-------------------------------------|----------------------------------------------|
4955 /// | from-forms | [`Self::expand_to_named`] | `expand_and_collect_named_calls_to_any` (this) |
4956 /// | from-source | [`Self::expand_source_to_named`] | [`Self::expand_source_and_collect_named_calls_to_any`] |
4957 ///
4958 /// The constant-`T::KEYWORD` column is the typed CONSEQUENCE of the
4959 /// classifier column: an `expand_to_named::<T>(forms)` call composes
4960 ///
4961 /// ```ignore
4962 /// expand_and_collect_named_calls_to_any(forms,
4963 /// |h| (h == T::KEYWORD).then_some(((), T::KEYWORD)),
4964 /// |(), name, spec_args| {
4965 /// let spec = T::compile_from_args(spec_args)?;
4966 /// Ok(NamedDefinition { name: name.to_string(), spec })
4967 /// })
4968 /// ```
4969 ///
4970 /// Both columns route through ONE composition point on the
4971 /// `Expander` surface (the typed-decoded classifier walk inside
4972 /// [`Self::expand_and_collect_calls_to_any`]) and ONE gate body on
4973 /// the `&[Sexp]` algebra ([`crate::compile::split_name_slot`]). A
4974 /// regression that drifts ONE cell's NAME-slot rejection chain from
4975 /// the others is structurally impossible — every cell binds to the
4976 /// SAME `split_name_slot` body, the SAME `expand_program +
4977 /// iter_calls_to_any + map + collect` pipeline. The
4978 /// pre-lift `NamedFormMissingName` / `NamedFormNonSymbolName`
4979 /// rejection chain inside `compile::named_form_projection` (the
4980 /// `pub(crate)` typed-domain SPECIALIZATION that
4981 /// [`Self::expand_to_named`] routes through) fires identically
4982 /// through this classifier primitive — both consumers compose
4983 /// [`crate::compile::split_name_slot`] with their per-site typed
4984 /// continuation.
4985 ///
4986 /// Future consumer shapes the typed-decoded named-form primitive
4987 /// admits with no boilerplate:
4988 /// * **Closed-set classifier** — a `tatara-check` runner that
4989 /// dispatches every `(defmonitor NAME …)` / `(defnotify NAME …)`
4990 /// / `(defalertpolicy NAME …)` form in `checks.lisp` through ONE
4991 /// classifier (a closed-set enum `Domain::{Monitor, Notify,
4992 /// AlertPolicy}` with `Domain::from_keyword`) decoding each
4993 /// head to its typed kind PAIRED with its canonical static
4994 /// keyword, then dispatches in the per-form projection — binds
4995 /// to ONE method rather than two-step `expand_and_collect_calls_to_any
4996 /// + split_name_slot` composition at each consumer site.
4997 /// * **Live-registry classifier** — a future `tatara-check`-shaped
4998 /// runtime dispatcher: `expand_and_collect_named_calls_to_any(forms,
4999 /// |h| registry.get(h).map(|d| (d, d.keyword())), |dispatcher,
5000 /// name, args| dispatcher.compile(name, args))` walks a program
5001 /// dispatching every named form whose head is in a runtime
5002 /// registry, decoded to a handler reference AND its canonical
5003 /// keyword (sourced from the dispatcher itself, NOT from the
5004 /// matched head text). The `&'static str` discipline on the
5005 /// keyword slot is preserved through the dispatcher's own
5006 /// `keyword() -> &'static str` accessor.
5007 /// * **`compile_named_any` free function family** — the natural
5008 /// fresh-expander free-function + preloaded-`RealizedCompiler`
5009 /// consumer pair, sibling of the existing `compile_typed_any`
5010 /// / `RealizedCompiler::compile_typed_any` pair. A future run
5011 /// lands those as 3-line composes on top of this primitive.
5012 ///
5013 /// `D` is `FnMut(&str) -> Option<(T, &'static str)>` — the typed-
5014 /// decoded classifier yields a typed witness `T` AND its canonical
5015 /// static keyword for the gate. `F` is `FnMut(T, &str, &[Sexp]) ->
5016 /// Result<R>` — the per-form projection receives the typed witness
5017 /// `T` ALONGSIDE the NAME slot's BORROWED `&str` projection (sourced
5018 /// from [`Sexp::as_symbol_or_string`], which accepts BOTH symbol and
5019 /// string NAME-author surfaces) AND the spec args tail. Consumers
5020 /// that need owned ownership of the NAME (`NamedDefinition.name:
5021 /// String`, JSON-serialized payloads) `.to_string()` themselves —
5022 /// pushing the clone to the consumer boundary keeps the primitive
5023 /// allocation-free. The `Result<R>` projection short-circuits on
5024 /// the first error via `Iterator::collect::<Result<Vec<R>, _>>()`
5025 /// inside [`Self::expand_and_collect_calls_to_any`].
5026 ///
5027 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
5028 /// the (named × typed-decoded classifier) cell becomes a NAMED
5029 /// primitive on the `Expander` surface composed FROM TWO substrate
5030 /// primitives ([`Self::expand_and_collect_calls_to_any`] +
5031 /// [`crate::compile::split_name_slot`]) rather than re-derived
5032 /// inline at every named-classifier consumer's call site.
5033 /// THEORY.md §II.1 invariant 1 — typed entry; the typed-decoded
5034 /// classifier-filtered + NAME-shape-gated + caller-projected walk
5035 /// IS the typed-entry-batch gate for named-form dispatch at the
5036 /// `Expander` surface, with the `&'static str` keyword discipline
5037 /// preserved through the decoder's return type. THEORY.md §II.1
5038 /// invariant 2 — free middle; ALL four cells of the named-form
5039 /// dispatcher matrix on the `Expander` surface route through ONE
5040 /// composition point — a regression that drifts ONE cell's
5041 /// instrumentation from the others is structurally impossible.
5042 /// THEORY.md §V.1 — knowable platform; the named-form classifier
5043 /// walk becomes a discoverable primitive that LSP / REPL /
5044 /// `tatara-check` consumers bind to ONE method on the `Expander`
5045 /// instead of re-implementing the two-step `expand_and_collect_calls_to_any
5046 /// + split_name_slot` composition.
5047 ///
5048 /// Frontier inspiration: MLIR's `Region::walk<NamedOpInterface>(
5049 /// [&](auto op) { auto name = op.getName(); … })` — the typed-IR
5050 /// walk over a region yielding ops decoded to their typed interface
5051 /// witness with the named-symbol accessor pre-extracted is the
5052 /// MLIR idiom for named typed dispatch; this method is the
5053 /// substrate's unstructured-Rust peer with the typed-decoded
5054 /// classifier composed in and the NAME slot extracted via the
5055 /// substrate's `split_name_slot` gate. Racket's `syntax-parse`
5056 /// `~or* ((~datum defX) name:id arg ...) ((~datum defY) name:id
5057 /// arg ...) ((~datum defZ) name:id arg ...)` over an ellipsis-form
5058 /// — the typed-choice repeater with named-slot binder PAIRED with
5059 /// per-arm dispatch — is the Racket idiom; this primitive is the
5060 /// substrate's Rust-typed peer with the closed-set classifier
5061 /// playing the `~or*` typed-choice role and the NAME slot extracted
5062 /// by the substrate's slice-side gate.
5063 pub fn expand_and_collect_named_calls_to_any<R, F, D, T>(
5064 &mut self,
5065 forms: Vec<Sexp>,
5066 decode: D,
5067 mut project: F,
5068 ) -> Result<Vec<R>>
5069 where
5070 D: FnMut(&str) -> Option<(T, &'static str)>,
5071 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5072 {
5073 // Routes through the slice-side typed-decoded named projection
5074 // [`crate::ast::iter_named_calls_to_any`] — the same
5075 // `expand_program + iter_*_to_any + map + collect` shape
5076 // [`Self::expand_and_collect_calls_to_any`] uses on the bare-kwargs
5077 // axis. Pre-lift this method routed through the bare expander
5078 // surface and welded [`crate::compile::split_name_slot`] inside
5079 // the projection closure; post-lift the named gate composition
5080 // lives at the slice level (`iter_named_calls_to_any`'s body)
5081 // and the Expander surface inherits it through delegation. Both
5082 // rows of the Expander surface (bare-kwargs, named) now share
5083 // ONE pipeline skeleton on the slice algebra — a regression that
5084 // drifts a future debug-mode logger, span-aware borrow walker,
5085 // or fused-iterator invariant from one row to the other becomes
5086 // structurally impossible at the slice boundary.
5087 let expanded = self.expand_program(forms)?;
5088 crate::ast::iter_named_calls_to_any(&expanded, decode)
5089 .map(|maybe_triple| {
5090 let (decoded, name, spec_args) = maybe_triple?;
5091 project(decoded, name, spec_args)
5092 })
5093 .collect()
5094 }
5095
5096 /// Read a source string into top-level forms via [`crate::reader::read`],
5097 /// then route the forms through
5098 /// [`Self::expand_and_collect_named_calls_to_any`] — the from-source
5099 /// posture of the (named × typed-decoded classifier) cell on the
5100 /// `Expander` surface, sibling of
5101 /// [`Self::expand_source_and_collect_calls_to_any`] (the from-source
5102 /// classifier × bare-kwargs cell).
5103 ///
5104 /// Closes the (from-forms, from-source) × (constant `T::KEYWORD`,
5105 /// typed-decoded classifier) 2×2 on the named-form axis of the
5106 /// `Expander` surface — together with [`Self::expand_to_named`],
5107 /// [`Self::expand_source_to_named`], and
5108 /// [`Self::expand_and_collect_named_calls_to_any`], every cell of the
5109 /// matrix binds to ONE composition point.
5110 ///
5111 /// `?`-routing through `read` preserves the structural ordering of
5112 /// the rejection chain end-to-end: a reader error (lexer / parser /
5113 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5114 /// `expand_program` runs; an `expand_program` error short-circuits
5115 /// BEFORE the classifier filter walks anything; the named-form gate
5116 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5117 /// [`crate::compile::split_name_slot`] fires for the first matched
5118 /// form that violates either condition; a per-form `project` error
5119 /// short-circuits at the first failing matched form via
5120 /// `Iterator::collect::<Result<Vec<R>, _>>()`.
5121 ///
5122 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to_any`].
5123 /// THEORY.md §VI.1 (generation over composition; the from-source
5124 /// posture inherits the named-form gate composition through
5125 /// delegation rather than re-deriving the `read(src)? + walk + gate`
5126 /// chain at every from-source named-classifier consumer's call
5127 /// site), THEORY.md §II.1 invariant 2 (free middle; the from-source
5128 /// posture and the from-forms posture route through the SAME named-
5129 /// form composition).
5130 pub fn expand_source_and_collect_named_calls_to_any<R, F, D, T>(
5131 &mut self,
5132 src: &str,
5133 decode: D,
5134 project: F,
5135 ) -> Result<Vec<R>>
5136 where
5137 D: FnMut(&str) -> Option<(T, &'static str)>,
5138 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5139 {
5140 let forms = crate::reader::read(src)?;
5141 self.expand_and_collect_named_calls_to_any(forms, decode, project)
5142 }
5143
5144 /// Macroexpand a pre-parsed program through `self` and project every
5145 /// `(keyword NAME :k v …)` form into `R` via a caller-supplied
5146 /// `(name, args) -> Result<R>` projection — the constant-keyword
5147 /// sibling of [`Self::expand_and_collect_named_calls_to_any`] on the
5148 /// named-form axis of the `Expander` surface, parallel to how
5149 /// [`Self::expand_and_collect_calls_to`] is the constant-keyword
5150 /// sibling of [`Self::expand_and_collect_calls_to_any`] on the
5151 /// bare-kwargs axis.
5152 ///
5153 /// Routes through the classifier sibling with a constant-classifier
5154 /// decoder that yields a discarded `()` typed witness paired with
5155 /// the `&'static str` keyword — the same `(T, &'static str)` decoder
5156 /// shape [`crate::compile::split_name_slot`] (composed inside the
5157 /// classifier primitive) pins at the slice-side gate boundary for
5158 /// the named-form structural rejection chain
5159 /// (`NamedFormMissingName.keyword`, `NamedFormNonSymbolName.keyword`).
5160 /// `&'static` is the same lifetime discipline the typed-decoded
5161 /// classifier signature enforces; the named gate's verbatim keyword
5162 /// threading inherits it through the constant-classifier composition.
5163 ///
5164 /// Projection signature `FnMut(&str, &[Sexp]) -> Result<R>` receives
5165 /// the BORROWED NAME slot and the BORROWED spec args tail. Consumers
5166 /// that need owned ownership (`NamedDefinition.name: String`) call
5167 /// `.to_string()` themselves — pushing the clone to the consumer
5168 /// boundary keeps the primitive allocation-free, matching the
5169 /// classifier sibling's projection signature on the NAME slot.
5170 ///
5171 /// Closes the (named NAME-then-kwargs × constant-keyword) cell of
5172 /// the `Expander` typed-walk family at the runtime-keyword × untyped
5173 /// `R` corner — pre-lift the cell was reachable ONLY through
5174 /// [`Self::expand_to_named`] with the `T: TataraDomain` type
5175 /// parameter baking `T::KEYWORD` AND the `T::compile_from_args`-
5176 /// based `crate::compile::named_form_projection<T>` projection
5177 /// into the dispatch through `expand_and_collect_calls_to(forms,
5178 /// T::KEYWORD, named_form_projection::<T>)`. Post-lift the cell
5179 /// surfaces as ONE method that takes the keyword and the
5180 /// `(name, args) -> R` projection as caller-supplied parameters,
5181 /// and [`Self::expand_to_named`] routes through it as the typed
5182 /// `T::KEYWORD`-constant specialization. The split lets a consumer
5183 /// that wants "walk every form whose head is a runtime keyword `kw`
5184 /// and project `(NAME, spec_args) -> R` for an arbitrary `R`" (a
5185 /// future REPL `:walk-named <kw>` command, an LSP "find-named-
5186 /// declarations-of-keyword `kw`" handler, a `tatara-check` runner
5187 /// dispatching on a single typed keyword whose projection isn't
5188 /// `T::compile_from_args`) bind to ONE method on the `Expander`
5189 /// surface rather than re-deriving the
5190 /// `expand_and_collect_named_calls_to_any(forms, |h| (h ==
5191 /// kw).then_some(((), kw)), |(), name, args| project(name, args))`
5192 /// two-line composition inline.
5193 ///
5194 /// Sibling of [`Self::expand_source_and_collect_named_calls_to`]
5195 /// — that method stacks a [`crate::reader::read`] step on top of
5196 /// this one (the from-source posture); this method takes a
5197 /// pre-parsed `Vec<Sexp>` (the from-forms posture). Together with
5198 /// [`Self::expand_and_collect_named_calls_to_any`] /
5199 /// [`Self::expand_source_and_collect_named_calls_to_any`], the four
5200 /// cells of the (named × {constant-keyword, classifier} ×
5201 /// {from-forms, from-source}) sub-matrix close on the `Expander`
5202 /// surface — each cell binds to ONE composition point, with the
5203 /// constant-keyword cells routing through the classifier cells via
5204 /// constant-classifier decoding.
5205 ///
5206 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
5207 /// the `split_name_slot` composition lived at TWO sites pre-lift
5208 /// (`crate::compile::named_form_projection` body, which
5209 /// [`Self::expand_to_named`] routes through, AND
5210 /// [`Self::expand_and_collect_named_calls_to_any`] body, which the
5211 /// typed-decoded classifier consumers route through) — past the
5212 /// ≥2 PRIME-DIRECTIVE trigger once the named-form gate is named.
5213 /// THEORY.md §V.1 — knowable platform; the named constant-keyword
5214 /// walk becomes a NAMED primitive on the `Expander` surface
5215 /// composable by any future consumer (REPL, LSP, `tatara-check`)
5216 /// instead of a re-derived two-line `expand_and_collect_named_
5217 /// calls_to_any(forms, constant-decoder, wrapper-projection)`
5218 /// composition. THEORY.md §II.1 invariant 2 — free middle; both
5219 /// the typed (`expand_to_named<T>`) and untyped (this method)
5220 /// constant-keyword named cells route through the SAME named
5221 /// classifier primitive — a regression that drifts ONE cell's
5222 /// NAME-slot rejection chain (`NamedFormMissingName`,
5223 /// `NamedFormNonSymbolName`) from the other becomes structurally
5224 /// impossible.
5225 ///
5226 /// Frontier inspiration: Racket's `syntax-parse` `((~datum kw)
5227 /// name:id arg ...)` arm — the named NAME-slot binder under a
5228 /// constant `~datum` keyword paired with a per-match handler is
5229 /// the Racket idiom; this method is the substrate's Rust-typed
5230 /// peer with the constant-keyword filter composed at the
5231 /// classifier corner. MLIR's `Region::walk<NamedOpKind>([&](auto
5232 /// op) { auto name = op.getName(); … })` against a single
5233 /// operation kind — the typed-IR walk yielding ops decoded to
5234 /// their typed kind with the named-symbol accessor pre-extracted,
5235 /// specialized on a constant kind; this method is the substrate's
5236 /// unstructured-`R` peer with the constant-classifier composition
5237 /// pinned at the call boundary.
5238 pub fn expand_and_collect_named_calls_to<R, F>(
5239 &mut self,
5240 forms: Vec<Sexp>,
5241 keyword: &'static str,
5242 mut project: F,
5243 ) -> Result<Vec<R>>
5244 where
5245 F: FnMut(&str, &[Sexp]) -> Result<R>,
5246 {
5247 // Routes through the typed-decoded named-classifier sibling
5248 // [`Self::expand_and_collect_named_calls_to_any`] with a
5249 // constant-classifier decoder — the same constant-classifier
5250 // composition [`Self::expand_and_collect_calls_to`] uses to
5251 // route through [`Self::expand_and_collect_calls_to_any`] on
5252 // the bare-kwargs axis, and that
5253 // [`crate::ast::iter_calls_to`] uses to route through
5254 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
5255 // discarded `()` typed witness (`then_some(((), keyword))`)
5256 // is consumed by the wrapper projection
5257 // `|(), name, args| project(name, args)` so the keyword
5258 // consumer's per-form mapper sees only `(name, spec_args)`,
5259 // matching the bare projection signature.
5260 self.expand_and_collect_named_calls_to_any(
5261 forms,
5262 |h| (h == keyword).then_some(((), keyword)),
5263 move |(), name, args| project(name, args),
5264 )
5265 }
5266
5267 /// Read a source string into top-level forms via [`crate::reader::read`],
5268 /// then route the forms through
5269 /// [`Self::expand_and_collect_named_calls_to`] — the from-source
5270 /// posture of the (named × constant-keyword) cell on the
5271 /// `Expander` surface.
5272 ///
5273 /// Composes [`crate::reader::read`] with
5274 /// [`Self::expand_and_collect_named_calls_to`] — the
5275 /// `(keyword, project)` binding is bound in ONE place (the
5276 /// from-forms row) and this from-source sibling inherits the
5277 /// binding through delegation, mirroring how
5278 /// [`Self::expand_source_and_collect_calls_to`] composes
5279 /// [`crate::reader::read`] with [`Self::expand_and_collect_calls_to`]
5280 /// on the bare-kwargs axis.
5281 ///
5282 /// `?`-routing through `read` preserves the structural ordering of
5283 /// the rejection chain end-to-end: a reader error (lexer / parser /
5284 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5285 /// `expand_program` runs; an `expand_program` error short-circuits
5286 /// BEFORE the keyword filter walks anything; the named-form gate
5287 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5288 /// [`crate::compile::split_name_slot`] fires for the first matched
5289 /// form that violates either condition; a per-form `project` error
5290 /// short-circuits at the first failing matched form via
5291 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
5292 /// rejection chain inherits the constant-keyword named primitive's
5293 /// shape verbatim, now sourced from `&str` via ONE composition
5294 /// point.
5295 ///
5296 /// Sibling of [`Self::expand_and_collect_named_calls_to`] (the
5297 /// from-forms posture) and of [`Self::expand_source_and_collect_calls_to`]
5298 /// (the from-source × bare-kwargs cell on the constant-keyword
5299 /// row). The four cells of (named × {constant, classifier} ×
5300 /// {from-forms, from-source}) close on the `Expander` surface
5301 /// post-lift, each routing through ONE composition point.
5302 ///
5303 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to`].
5304 /// THEORY.md §VI.1 (generation over composition; the from-source
5305 /// posture inherits the constant-keyword named composition through
5306 /// delegation rather than re-deriving the
5307 /// `read(src)? + expand_and_collect_named_calls_to` chain at every
5308 /// from-source named-keyword consumer's call site),
5309 /// THEORY.md §II.1 invariant 2 (free middle; the from-source
5310 /// posture and the from-forms posture route through the SAME
5311 /// composition).
5312 pub fn expand_source_and_collect_named_calls_to<R, F>(
5313 &mut self,
5314 src: &str,
5315 keyword: &'static str,
5316 mut project: F,
5317 ) -> Result<Vec<R>>
5318 where
5319 F: FnMut(&str, &[Sexp]) -> Result<R>,
5320 {
5321 // From-source = from-forms × constant-classifier specialization
5322 // of the named classifier primitive's from-source sibling. The
5323 // discarded `()` typed witness and the `move |(), name, args|
5324 // project(name, args)` wrapper mirror the from-forms posture's
5325 // composition — both postures route through the SAME named
5326 // classifier primitive on the `Expander` surface, the
5327 // from-source row stacking one `read` step on top.
5328 self.expand_source_and_collect_named_calls_to_any(
5329 src,
5330 |h| (h == keyword).then_some(((), keyword)),
5331 move |(), name, args| project(name, args),
5332 )
5333 }
5334
5335 /// Expand a single form. Top-level macro calls are rewritten; recurses
5336 /// into list children.
5337 ///
5338 /// Routes the macro-call dispatch surface through the substrate's
5339 /// typed-decoded call decomposition: `as_call_to_any(|h|
5340 /// self.macros.get(h))` answers "is this form an invocation of any
5341 /// registered macro, decoded to `(&MacroDef, args)`?" in ONE
5342 /// structural query on the `Sexp` algebra. Pre-lift the same site
5343 /// opened the three-step chain `as_list() + as_call() + self.macros.
5344 /// get(head)` inline — `as_list()` for the children-walk fallthrough,
5345 /// `as_call()` for the (head, args) decomposition (which itself
5346 /// re-derives `as_list()` internally), and `self.macros.get(head)`
5347 /// for the registry lookup; post-lift the call-recognition runs as
5348 /// ONE `as_call_to_any` projection with the HashMap lookup as its
5349 /// classifier, and the `as_list()` fallthrough fires only on the
5350 /// not-a-macro-call path. Sibling consumer to `macro_def_from` — the
5351 /// typed-macro-definition dispatcher that routes through
5352 /// `as_call_to_any(MacroDefHead::from_keyword)` with the closed-set
5353 /// enum classifier. With both in place, BOTH dispatch sites in the
5354 /// macro expander (definition-recognition + call-recognition)
5355 /// project through the SAME family primitive on the `Sexp` algebra,
5356 /// each binding the classifier that fits its candidate set — closed
5357 /// enum for the static head-set, HashMap lookup for the live
5358 /// registry. A regression that drifts ONE site from the other (a
5359 /// future emitter that re-derives `as_list()` + `head.as_symbol()` +
5360 /// `self.macros.get(_)` inline rather than routing through the
5361 /// family) is no longer a silent two-site divergence.
5362 pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
5363 self.expand_with_depth(form, 0)
5364 }
5365
5366 /// The depth-carrying peer of [`Self::expand`]. Each
5367 /// post-apply re-expansion increments `depth`; each tree-child
5368 /// descent passes `depth` through unchanged. The runaway
5369 /// `(defmacro loop (x) `(loop ,x))` accretes depth on the
5370 /// re-expansion path — one unit per round — and hits the ceiling
5371 /// in `max_expansion_depth` rounds; lawful nested macros
5372 /// (`(when1 (when1 x))`) accrete depth in the low single digits
5373 /// because tree traversal doesn't count.
5374 ///
5375 /// The `depth >= limit` check sits INSIDE the macro-call arm
5376 /// (`Some((def, args)) = form.as_call_to_any(…)`) rather than at
5377 /// the top of the function so `def.name.clone()` is available at
5378 /// the rejection site — the operator gets `macro_name` populated
5379 /// from the actual offending call rather than a `<unknown>`
5380 /// fallback. Tree-child descent is not gated on the ceiling
5381 /// because the tree-child arm does not accrete depth; a lawfully
5382 /// deep tree is bounded by the reader's own stack, not by this
5383 /// ceiling.
5384 ///
5385 /// The peer OUTPUT-SIZE gate (`expanded.node_count() >
5386 /// self.limits.max_expansion_size`) sits INSIDE the same macro-call arm,
5387 /// between the `apply` and the re-expansion recursion, so a
5388 /// single macro's `apply` output whose structural size crosses
5389 /// the ceiling is rejected AT the offending `apply` boundary
5390 /// (with `macro_name` populated from `def.name`) before the
5391 /// runaway tree feeds back into further expansion. The gate is
5392 /// keyed on `>` rather than `>=` so `max_expansion_size` names
5393 /// the LARGEST admissible output — the ceiling admits equality,
5394 /// only strict overrun rejects. Together with the depth ceiling
5395 /// (recursion length) and the cache ceiling (memoization width),
5396 /// this closes the expander's RESOURCE surface at three typed
5397 /// dimensions.
5398 fn expand_with_depth(&self, form: &Sexp, depth: usize) -> Result<Sexp> {
5399 if let Some((def, args)) = form.as_call_to_any(|h| self.macros.get(h)) {
5400 if depth >= self.limits.max_expansion_depth {
5401 return Err(LispError::ExpansionDepthExceeded {
5402 macro_name: def.name.clone(),
5403 limit: self.limits.max_expansion_depth,
5404 });
5405 }
5406 let expanded = self.apply(def, args)?;
5407 // Reject a single `apply` output whose structural node
5408 // count crosses the OUTPUT-SIZE ceiling — the RESOURCE
5409 // axis peer of the depth (recursion length) and
5410 // cache-entries (memoization width) ceilings. Catches
5411 // the canonical "expansion bomb" where a well-defined
5412 // macro produces a 2^N-node blob from a small input
5413 // while `depth` stays in the low single digits and the
5414 // cache entry count sits under any reasonable ceiling.
5415 let expanded_size = expanded.node_count();
5416 if expanded_size > self.limits.max_expansion_size {
5417 return Err(LispError::ExpansionSizeExceeded {
5418 macro_name: def.name.clone(),
5419 size: expanded_size,
5420 limit: self.limits.max_expansion_size,
5421 });
5422 }
5423 // Recurse — the expansion itself may contain more macro calls.
5424 return self.expand_with_depth(&expanded, depth + 1);
5425 }
5426 // Not a macro call — expand children if this is a list; otherwise
5427 // (atom / Nil / quote-family wrapper) return the form verbatim.
5428 let Some(list) = form.as_list() else {
5429 return Ok(form.clone());
5430 };
5431 let mut out = Vec::with_capacity(list.len());
5432 for item in list {
5433 out.push(self.expand_with_depth(item, depth)?);
5434 }
5435 Ok(Sexp::List(out))
5436 }
5437
5438 /// Apply a macro to its argument list.
5439 ///
5440 /// Three-layer fast path:
5441 /// 1. If `cache_enabled`, hash `(name, args)` and consult the memo table.
5442 /// 2. If a compiled template exists, run the bytecode interpreter.
5443 /// 3. Otherwise fall back to the name-keyed substitute walker.
5444 fn apply(&self, def: &MacroDef, args: &[Sexp]) -> Result<Sexp> {
5445 // Layer 1: expansion cache.
5446 let cache_key = if self.cache_enabled {
5447 args_cache_key(&def.name, args)
5448 } else {
5449 None
5450 };
5451 if let Some(ref key) = cache_key {
5452 if let Some(cached) = self.cache.lock().unwrap().get(key) {
5453 self.counters
5454 .cache_hits
5455 .fetch_add(1, AtomicOrdering::Relaxed);
5456 return Ok(cached.clone());
5457 }
5458 }
5459
5460 // Layer 2: compiled bytecode.
5461 let result = if let Some(tmpl) = self.templates.get(&def.name) {
5462 self.counters.compiled.fetch_add(1, AtomicOrdering::Relaxed);
5463 apply_compiled(&def.name, &def.params, tmpl, args)?
5464 } else {
5465 self.counters
5466 .substituted
5467 .fetch_add(1, AtomicOrdering::Relaxed);
5468 // Layer 3: substitute fallback. Walk the body's substitution
5469 // projection — the inner of the outer quasi-quote when present,
5470 // the body verbatim otherwise — through the shared
5471 // `MacroDef::template_body` primitive both strategies route on.
5472 let bindings = bind_args(&def.name, &def.params, args)?;
5473 substitute(def.template_body(), &bindings)?
5474 };
5475
5476 // Populate cache on miss — capped by `max_cache_entries` so the
5477 // memoization table cannot drift unboundedly across a long-
5478 // running host session. When the cap is reached the freshly-
5479 // computed result is returned verbatim; correctness is
5480 // unaffected because caching is a pure PERFORMANCE optimization
5481 // (the miss path always recomputes the same value the cache
5482 // would have returned).
5483 if let Some(key) = cache_key {
5484 let mut cache = self.cache.lock().unwrap();
5485 if cache.len() < self.limits.max_cache_entries {
5486 cache.insert(key, result.clone());
5487 }
5488 }
5489 Ok(result)
5490 }
5491
5492 pub fn has(&self, name: &str) -> bool {
5493 self.macros.contains_key(name)
5494 }
5495
5496 pub fn len(&self) -> usize {
5497 self.macros.len()
5498 }
5499
5500 pub fn is_empty(&self) -> bool {
5501 self.macros.is_empty()
5502 }
5503}
5504
5505// ── Compiled template bytecode ───────────────────────────────────────
5506
5507/// One op in the template bytecode. Emitted during compilation; consumed at
5508/// expansion to materialize a form without HashMap lookups or recursion.
5509#[derive(Clone, Debug, PartialEq)]
5510pub enum TemplateOp {
5511 /// Push a literal Sexp. Used for atoms and entirely-literal subtrees.
5512 Literal(Sexp),
5513 /// Push the bound arg at the given param index.
5514 Subst(usize),
5515 /// If the bound arg is a list, append its items to the current list; else
5516 /// push it as a single item.
5517 Splice(usize),
5518 /// Begin a new List — pushes a fresh builder onto the expansion stack.
5519 BeginList,
5520 /// End the current List — pops the builder, wraps as `Sexp::List`.
5521 EndList,
5522}
5523
5524/// Pre-compiled template. Built once per macro, interpreted many times.
5525#[derive(Clone, Debug, Default, PartialEq)]
5526pub struct CompiledTemplate {
5527 pub ops: Vec<TemplateOp>,
5528}
5529
5530/// Walk a macro definition's template body and emit linear bytecode.
5531/// Purely-literal subtrees compile to a single `Literal(clone)` op.
5532///
5533/// Compilation can fail if the template references a name that isn't a
5534/// declared parameter — same semantic as the substitute path.
5535///
5536/// Top-level `,@X` bodies (the splice is the entire body, not nested inside
5537/// a `(... ,@xs ...)` list) are rejected here at compile time so the
5538/// bytecode path agrees with the substitute path's emission-time rejection
5539/// (`splice_outside_list`). Without this gate the bytecode interpreter's
5540/// outermost stack frame silently absorbed the splice's items, and the same
5541/// macro emitted different output across paths — `compiled_template_matches
5542/// _substitute_path` only covered well-positioned splice bodies. After this
5543/// gate every `,@-outside-list` body is rejected at registration time on
5544/// both paths with ONE structural variant (`LispError::SpliceOutsideList`).
5545///
5546/// The gate routes through [`Sexp::as_unquote`] — the typed-marker
5547/// projection that pairs `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at
5548/// ONE structural query — matching `Some((UnquoteForm::Splice, inner))`
5549/// rather than the per-arm `Sexp::UnquoteSplice(inner)` literal that
5550/// recurred at three sites pre-lift (`compile_node` Subst/Splice arms,
5551/// `substitute` top-level + list-inner). Sibling shape to `substitute`'s
5552/// list-inner Splice arm — both use the same `Some((UnquoteForm::Splice,
5553/// inner))` shape; `substitute`'s top-level arm uses the wider
5554/// `Some((kind, inner))` and dispatches inside on `kind`. After this lift
5555/// every production-site recognizer of "is this an `,@X` form" routes
5556/// through ONE typed-marker projection rather than re-deriving the
5557/// (Sexp variant, UnquoteForm variant) pair inline.
5558pub fn compile_template(def: &MacroDef) -> Result<CompiledTemplate> {
5559 // Walk the body's substitution projection — the inner of the outer
5560 // quasi-quote when present, the body verbatim otherwise — through the
5561 // shared `MacroDef::template_body` primitive the substitute path also
5562 // routes on. Same projection, both strategies, by construction.
5563 let body = def.template_body();
5564 if let Some((UnquoteForm::Splice, inner)) = body.as_unquote() {
5565 return Err(splice_outside_list(inner));
5566 }
5567 let names = def.params.names();
5568 let mut ops = Vec::new();
5569 compile_node(body, &names, &mut ops)?;
5570 Ok(CompiledTemplate { ops })
5571}
5572
5573fn compile_node(node: &Sexp, params: &[&str], ops: &mut Vec<TemplateOp>) -> Result<()> {
5574 // Fast-path literal: if the subtree has no Unquote/UnquoteSplice, emit a
5575 // single Literal op. This is the big win for macros where most of the
5576 // template is fixed structure.
5577 if !contains_unquote(node) {
5578 ops.push(TemplateOp::Literal(node.clone()));
5579 return Ok(());
5580 }
5581 // Routes the `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice(inner)` arms
5582 // through [`Sexp::as_unquote`] — the typed-marker projection that
5583 // pairs `Sexp::Unquote ↔ UnquoteForm::Unquote` and
5584 // `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at ONE site. The per-form
5585 // `TemplateOp` emission (`Subst` vs `Splice`) keys on the same typed
5586 // `form` value the gate-1+gate-2 composition `resolve_unquote_in_params`
5587 // threads through. Pre-lift the (Sexp variant, UnquoteForm variant)
5588 // pairing was bound per-arm — a future emitter that matched
5589 // `Sexp::Unquote(_)` but threaded `UnquoteForm::Splice` into
5590 // `resolve_unquote_in_params` (or vice versa) would type-check but
5591 // render a misleading diagnostic at the gate-1 / gate-2 rejection.
5592 // Post-lift the pair is bound at ONE projection function and the
5593 // `match form` mechanically lowers it to the bytecode op.
5594 if let Some((form, inner)) = node.as_unquote() {
5595 let idx = resolve_unquote_in_params(inner, params, form)?;
5596 ops.push(match form {
5597 UnquoteForm::Unquote => TemplateOp::Subst(idx),
5598 UnquoteForm::Splice => TemplateOp::Splice(idx),
5599 });
5600 return Ok(());
5601 }
5602 match node {
5603 Sexp::List(items) => {
5604 ops.push(TemplateOp::BeginList);
5605 for item in items {
5606 compile_node(item, params, ops)?;
5607 }
5608 ops.push(TemplateOp::EndList);
5609 }
5610 _ => ops.push(TemplateOp::Literal(node.clone())),
5611 }
5612 Ok(())
5613}
5614
5615fn contains_unquote(node: &Sexp) -> bool {
5616 // Route every quote-family wrapper recognition — both the
5617 // unquote-only subset (Unquote/UnquoteSplice → short-circuit `true`)
5618 // AND the quote-only subset (Quote/Quasiquote → recurse into inner) —
5619 // through [`Sexp::as_quote_form`]'s typed-marker projection. Pre-lift
5620 // the recognizer split into two arms: `as_unquote().is_some()` (the
5621 // substitution subset gate) + the inline
5622 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm (the remaining
5623 // quote-only subset). Post-lift both arms route through ONE
5624 // projection and the (Sexp variant, QuoteForm variant) pairing binds
5625 // at the closed-set algebra. The `form.as_unquote_form().is_some()`
5626 // gate is the SAME 2-of-4 subset projection [`Sexp::as_unquote`]
5627 // derives from, so recognizing "is this an unquote-family wrapper"
5628 // and "is this a quote-only wrapper" share ONE typed dispatch site;
5629 // rustc enforces that a future `Sexp` wrapper extension carry
5630 // through both `QuoteForm::ALL` AND `QuoteForm::as_unquote_form`'s
5631 // arm. Sibling posture to `Hash for Sexp`'s four-arm
5632 // `hash_discriminator` collapse, `Display for Sexp`'s `prefix`
5633 // collapse, `interop`'s `iac_forge_tag` collapse, and `domain`'s
5634 // `sexp_shape` collapse — every production-site quote-family
5635 // recognizer now routes through ONE projection on the algebra.
5636 if let Some((form, inner)) = node.as_quote_form() {
5637 return form.as_unquote_form().is_some() || contains_unquote(inner);
5638 }
5639 match node {
5640 Sexp::List(items) => items.iter().any(contains_unquote),
5641 _ => false,
5642 }
5643}
5644
5645/// Splice a resolved template value into an in-progress list builder —
5646/// the SHARED coercion both expansion strategies apply once `,@name`'s
5647/// gate-1 (must-be-a-symbol) and gate-2 (must-be-bound-in-scope) have
5648/// resolved the bound value. ONE named primitive the bytecode path
5649/// (`apply_compiled`'s `TemplateOp::Splice` arm) AND the substitute path
5650/// (`substitute`'s list-inner `Sexp::UnquoteSplice` arm) share. Before
5651/// this lift the three-arm coercion —
5652///
5653/// ```ignore
5654/// match value {
5655/// Sexp::List(items) => builder.extend(items.iter().cloned()),
5656/// Sexp::Nil => {}
5657/// other => builder.push(other.clone()),
5658/// }
5659/// ```
5660///
5661/// — was inlined at BOTH sites; the splice RESULT semantics (the last
5662/// inline-duplicated piece of the splice path after the prior runs lifted
5663/// gate-1, gate-2, and their composition) lived in two places that MUST
5664/// agree. After this lift the coercion lives in ONE function, so a
5665/// regression that drifts one strategy's splice posture from the other —
5666/// e.g. changing the `Sexp::Nil` arm to push an empty list at the
5667/// bytecode path but not the substitute path, or coercing a non-list
5668/// scalar differently across the two strategies — becomes structurally
5669/// impossible: there is exactly one implementation both strategies call.
5670///
5671/// The coercion's three arms ARE the no-evaluator template language's
5672/// splice contract: a bound LIST flattens its elements into the builder
5673/// (the canonical splice), a bound NIL contributes nothing (splicing the
5674/// empty list), and any other bound value splices as a single element (a
5675/// scalar `,@x` degrades to `,x` rather than erroring — invariant 2's
5676/// "free middle" lets the macro author rely on this without a
5677/// mid-rewrite type check; the typed-exit gate re-validates the
5678/// assembled form). Naming the contract once gives a future gate-3
5679/// (typed-shape enforcement on bound splice targets) ONE site to wrap
5680/// rather than two inline arms to keep in lockstep.
5681///
5682/// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the two
5683/// expansion strategies MUST produce identical output for the same
5684/// (macro, args) pair, and naming the splice coercion once makes that
5685/// per-strategy agreement structural rather than a two-site discipline
5686/// the `expansion_layers_agree_on_output_and_cache_wins` benchmark only
5687/// observes after the fact. THEORY.md §V.1 — knowable platform; the
5688/// splice RESULT semantics becomes a NAMED primitive authoring tools and
5689/// future runs bind to. THEORY.md §VI.1 — generation over composition;
5690/// the two-site coercion is lifted to ONE function, closing the last
5691/// inline-duplicated piece of the splice path the prior runs' gate lifts
5692/// (02173dc gate-1, 68da647 gate-2, b456f1f composition) left behind.
5693fn splice_value_into(builder: &mut Vec<Sexp>, value: &Sexp) {
5694 match value {
5695 Sexp::List(items) => builder.extend(items.iter().cloned()),
5696 Sexp::Nil => {}
5697 other => builder.push(other.clone()),
5698 }
5699}
5700
5701/// Promote the previously `LispError::Compile`-shaped helper into the
5702/// structural `LispError::TemplateInvariant { macro_name, kind }` variant.
5703/// The four reachable bytecode-runtime invariant violations in
5704/// `apply_compiled` — Subst-bad-index, Splice-bad-index, EndList-empty-
5705/// stack, final-no-value — funnel through ONE emission shape keyed on
5706/// the closed-set `TemplateInvariantKind` enum. The index payload of
5707/// the Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
5708/// / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
5709/// kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
5710/// is structurally unrepresentable — the type system encodes "this gate
5711/// has an index, that gate does not."
5712///
5713/// Display matches the legacy `Compile`-shaped diagnostic byte-for-byte
5714/// across all four kinds (`"compile error in {macro_name}: <invariant>"`)
5715/// via the closed-set `TemplateInvariantKind::message()` projection, so
5716/// authoring-tool substring greps (`tatara-check`, REPL) see no drift
5717/// across the lift.
5718///
5719/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
5720/// of bytecode-invariant failure modes becomes a TYPE rather than a
5721/// free-form `message: String` slot. THEORY.md §VI.1 — generation over
5722/// composition; the typed enum lands the structural-completeness floor
5723/// for the bytecode-runtime surface, parallel to how `CompilerSpecIoStage`
5724/// lands the structural-completeness floor for the disk-persistence
5725/// surface (`compiler_spec.rs`, the immediately prior claude-routine
5726/// lift on a sibling file). THEORY.md §II.1 invariant 5 (composition
5727/// preserves proofs): a well-formed bytecode invariant is the proof
5728/// that drives the interpreter; the structural variant makes the
5729/// proof's REJECTION shape first-class — authoring tools (REPL, LSP,
5730/// `tatara-check`) pattern-match on the `kind` slot and bind to the
5731/// gate identity directly instead of substring-parsing the rendered
5732/// diagnostic.
5733fn template_invariant_violation(macro_name: &str, kind: TemplateInvariantKind) -> LispError {
5734 LispError::TemplateInvariant {
5735 macro_name: macro_name.into(),
5736 kind,
5737 }
5738}
5739
5740/// Look up a bound-arg by its template-bytecode index, or raise the
5741/// structural `LispError::TemplateInvariant` rejection with the
5742/// caller-supplied `kind` constructor applied to the bad index. ONE
5743/// named primitive both bytecode-runtime arms that read a bound arg
5744/// by index — [`TemplateOp::Subst`] (single-value push) AND
5745/// [`TemplateOp::Splice`] (list-splicing) — route through.
5746///
5747/// Before this lift the same `args_by_index.get(*idx).ok_or_else(||
5748/// template_invariant_violation(macro_name, KIND(*idx)))?` projection
5749/// appeared at BOTH arms of [`apply_compiled`], differing only in the
5750/// kind constructor: [`TemplateInvariantKind::SubstBadIndex`] at the
5751/// `Subst` arm, [`TemplateInvariantKind::SpliceBadIndex`] at the
5752/// `Splice` arm. The arms also diverged on what they did with the
5753/// returned `&Sexp` — `Subst` cloned and pushed, `Splice` consumed
5754/// the borrow through [`splice_value_into`] — but the lookup-and-
5755/// reject prelude was byte-identical modulo the kind, well past the
5756/// ≥2 PRIME-DIRECTIVE trigger.
5757///
5758/// After this lift the lookup-and-reject shape lives in ONE function;
5759/// the two arms thread the per-call-site kind constructor through the
5760/// helper and apply their respective post-lookup verbs at the call
5761/// site. The `kind: FnOnce(usize) -> TemplateInvariantKind` parameter
5762/// encodes the closed-set bytecode-runtime "this gate has an index"
5763/// surface at the type level — only the two
5764/// [`TemplateInvariantKind`] variants whose payload IS the bad index
5765/// (`SubstBadIndex(usize)` and `SpliceBadIndex(usize)`) construct
5766/// directly through `FnOnce(usize) -> TemplateInvariantKind`; the
5767/// stack-gate variants ([`TemplateInvariantKind::EndListEmptyStack`]
5768/// and [`TemplateInvariantKind::FinalNoValue`]) carry no payload and
5769/// would not type-check at this boundary, so the invalid combination
5770/// "stack-gate kind reached from an op-index lookup" is structurally
5771/// unrepresentable at the helper's call boundary the same way
5772/// [`TemplateInvariantKind`]'s closed-set shape makes it
5773/// unrepresentable in the variant itself.
5774///
5775/// Sibling of [`template_invariant_violation`]: that helper builds the
5776/// typed [`LispError::TemplateInvariant`] variant from a fully-formed
5777/// `kind`; this helper composes the index-keyed lookup with the
5778/// variant-builder, so the kind constructor doesn't have to be evaluated
5779/// eagerly at the call site (lazy via `FnOnce`, only fires on the bad-
5780/// index path). A future fifth bytecode op that reads a bound arg by
5781/// index (a hypothetical [`TemplateOp::Conditional`] that branches on a
5782/// bound boolean, a [`TemplateOp::Project`] that extracts a sub-field
5783/// of a bound `Sexp::List`) extends the family in ONE call to
5784/// `resolve_bound_arg` with the new kind constructor (`KIND(usize) ->
5785/// TemplateInvariantKind`) — the bytecode-runtime's bound-arg-by-index
5786/// projection becomes ONE structural primitive consumers compose with.
5787///
5788/// The returned `&'a Sexp` borrows from `args_by_index` verbatim —
5789/// `Subst`'s arm consumes it through `.clone()` (the consumer pushes
5790/// an owned value into the builder); `Splice`'s arm consumes it
5791/// through [`splice_value_into`] (the consumer borrows for the
5792/// per-arm coercion). The borrow's lifetime `'a` is the unified
5793/// lifetime of `args_by_index`, matching the call site's borrow
5794/// posture.
5795///
5796/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5797/// inline copies of the index-lookup-and-reject prelude across the
5798/// `apply_compiled` body's `Subst` and `Splice` arms is past the ≥2
5799/// PRIME-DIRECTIVE trigger once the structural shape is named.
5800/// THEORY.md §V.1 — knowable platform / "make invalid states
5801/// unrepresentable"; the bytecode-runtime bound-arg-by-index lookup
5802/// becomes a NAMED primitive on the substrate's `&[Sexp]` algebra
5803/// rather than a re-derived `get + ok_or_else + template_invariant_
5804/// violation` chain at every op-arm that reads by index. A future
5805/// authoring tool (REPL, LSP, `tatara-check`) that wants to surface
5806/// "this bytecode op's bound-arg lookup misfired at idx N" binds to
5807/// ONE function. THEORY.md §II.1 invariant 2 — free middle; both
5808/// expansion strategies route through the SHARED `MacroParams::bind`,
5809/// AND the bytecode strategy's op-arms route through this SHARED
5810/// `resolve_bound_arg` lookup — the bytecode-runtime's
5811/// proof-of-well-formedness is now structurally uniform across the
5812/// two reachable index-lookup ops, so a regression that drifts ONE
5813/// arm's posture (e.g. accepts an out-of-range idx at one arm but
5814/// not the other, or swaps the kind constructor at a single arm) is
5815/// no longer a silent two-site divergence.
5816fn resolve_bound_arg<'a>(
5817 args_by_index: &'a [Sexp],
5818 idx: usize,
5819 macro_name: &str,
5820 kind: impl FnOnce(usize) -> TemplateInvariantKind,
5821) -> Result<&'a Sexp> {
5822 args_by_index
5823 .get(idx)
5824 .ok_or_else(|| template_invariant_violation(macro_name, kind(idx)))
5825}
5826
5827/// Project the bytecode-runtime stack to its in-progress builder frame —
5828/// the `&mut Vec<Sexp>` every value-emitting op writes into. ONE named
5829/// primitive both push-emitting arms (`TemplateOp::Literal` /
5830/// `TemplateOp::Subst` / post-`EndList` parent fold) AND the splice-
5831/// emitting arm (`TemplateOp::Splice`) route through.
5832///
5833/// Before this lift the same `stack.last_mut().unwrap()` projection
5834/// appeared at FOUR sites inside [`apply_compiled`]'s op-loop:
5835///
5836/// * `TemplateOp::Literal` — pushes the literal `Sexp` into the
5837/// current builder.
5838/// * `TemplateOp::Subst` — pushes the cloned bound-arg into the
5839/// current builder.
5840/// * `TemplateOp::Splice` — splices the bound-arg into the current
5841/// builder via [`splice_value_into`].
5842/// * `TemplateOp::EndList` — after popping the just-finished list
5843/// frame, pushes the folded `Sexp::List(items)` into the parent
5844/// builder (the new current frame).
5845///
5846/// Four byte-identical re-derivations of the same projection, well past
5847/// the ≥2 PRIME-DIRECTIVE trigger. After this lift the four sites
5848/// collapse to a single `current_builder_mut(&mut stack).{push|extend}`
5849/// call, and the bytecode-runtime invariant the projection rests on
5850/// — "the op-loop always sees at least one stack frame" — lives in ONE
5851/// expect message rather than four silent `.unwrap()` calls.
5852///
5853/// The expect rationale: [`apply_compiled`] seeds the stack with the
5854/// outermost frame at entry (`vec![Vec::with_capacity(1)]`); every
5855/// `TemplateOp::BeginList` pushes a NEW frame and every
5856/// `TemplateOp::EndList` pops it, so the count stays at OR ABOVE 1
5857/// throughout the op-loop. Stack-depleting failure modes are caught
5858/// upstream by their own structural variants:
5859/// [`TemplateInvariantKind::EndListEmptyStack`] fires inside
5860/// [`apply_compiled`]'s `EndList` arm via [`Vec::pop`]'s `Option`
5861/// gate, BEFORE the parent-fold push runs against
5862/// `current_builder_mut`; [`TemplateInvariantKind::FinalNoValue`]
5863/// fires AFTER the op-loop completes, on the outermost `stack.pop()`
5864/// that returns the assembled result. So a reachable
5865/// `current_builder_mut(&mut stack)` always observes a non-empty
5866/// stack, and the `expect` is a structural-invariant marker, not a
5867/// load-bearing rejection path.
5868///
5869/// Sibling of [`resolve_bound_arg`] (the bytecode-runtime bound-arg
5870/// lookup primitive lifted in the prior claude-routine run on this
5871/// module — 492a235) and [`template_invariant_violation`] (the
5872/// structural-variant error builder for the bytecode-runtime's
5873/// closed-set invariant-violation surface). Together the three primitives
5874/// name the bytecode-runtime's substrate-level operations: lookup-a-
5875/// bound-arg ([`resolve_bound_arg`]), build-the-invariant-rejection
5876/// ([`template_invariant_violation`]), and project-to-the-current-
5877/// builder (this lift). A future bytecode op that emits ONE OR MORE
5878/// values into the current builder — a hypothetical
5879/// `TemplateOp::SpliceMany(indices: Vec<usize>)` that splices a batch,
5880/// a `TemplateOp::PushQuoted(form: Sexp)` that wraps before push, a
5881/// span-annotated emit-with-position op — composes with ONE call to
5882/// [`current_builder_mut`] and the per-op post-projection verb
5883/// (`.push(…)`, `.extend(…)`, `splice_value_into(…, _)`); a future
5884/// instrumentation hook that wants to log every op's emit before
5885/// it lands in the builder wraps ONE call boundary, not four inline
5886/// `stack.last_mut().unwrap()` sites.
5887///
5888/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
5889/// inline copies of the top-of-stack projection in one function is
5890/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5891/// named. THEORY.md §V.1 — knowable platform; the bytecode-runtime's
5892/// current-builder projection becomes a NAMED primitive on the
5893/// substrate's `&mut [Vec<Sexp>]` slice algebra rather than a re-derived
5894/// `last_mut + unwrap` chain at every op-arm that emits into the
5895/// builder. The expect message names the invariant
5896/// ("bytecode-runtime invariant: at least one stack frame during
5897/// op-loop") so a regression that drifts the loop's frame management
5898/// surfaces a NAMED panic, not a silent `unwrap` over `None`.
5899/// THEORY.md §II.1 invariant 2 — free middle; both expansion
5900/// strategies route through the SHARED `MacroParams::bind` upstream
5901/// AND the bytecode strategy's op-arms now route through this SHARED
5902/// `current_builder_mut` projection downstream — the bytecode-runtime's
5903/// substrate-level surface (lookup + emit) is named in two
5904/// composable primitives the op-arms compose with.
5905fn current_builder_mut(stack: &mut [Vec<Sexp>]) -> &mut Vec<Sexp> {
5906 stack
5907 .last_mut()
5908 .expect("bytecode-runtime invariant: at least one stack frame during op-loop")
5909}
5910
5911/// Pop the top stack frame off the bytecode-runtime stack, or raise the
5912/// structural [`LispError::TemplateInvariant`] rejection with the
5913/// supplied [`TemplateInvariantKind`] when the stack is empty — ONE
5914/// named primitive both pop-emitting sites in [`apply_compiled`] route
5915/// through.
5916///
5917/// Before this lift two byte-identical
5918/// `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
5919/// kind))?` chains lived inline in [`apply_compiled`]:
5920///
5921/// * `TemplateOp::EndList` arm — pops the just-finished list frame
5922/// before the parent-fold push, with kind
5923/// [`TemplateInvariantKind::EndListEmptyStack`] guarding the
5924/// unreachable empty-stack failure mode.
5925/// * Post-loop final pop — consumes the outermost frame that
5926/// accumulated the template's result, with kind
5927/// [`TemplateInvariantKind::FinalNoValue`] guarding the
5928/// unreachable seed-frame-already-popped failure mode.
5929///
5930/// Two byte-identical re-derivations of the same projection inside one
5931/// function, past the ≥2 PRIME-DIRECTIVE trigger once the structural
5932/// shape is named. After this lift the two sites collapse to a single
5933/// `pop_builder_frame(&mut stack, macro_name, KIND)?` call, and the
5934/// bytecode-runtime invariant the projection rests on — "an empty-stack
5935/// pop is a structural-variant rejection, not a silent `Option::None`"
5936/// — lives in ONE composition point rather than two.
5937///
5938/// Sibling of [`current_builder_mut`] (the bytecode-runtime stack's
5939/// *project-to-top-frame* primitive — the borrow face, never resizes
5940/// the stack) and [`resolve_bound_arg`] (the bound-arg-by-index
5941/// lookup primitive with per-call-site `TemplateInvariantKind`
5942/// constructor). Where `current_builder_mut` borrows the in-progress
5943/// top frame for emission and never panics on a reachable input
5944/// (callers route past it only when the seed frame is present), this
5945/// primitive *consumes* the top frame off the stack and projects its
5946/// absence into a structural `LispError::TemplateInvariant` rejection
5947/// — the failure-on-empty face of the same `&mut Vec<Vec<Sexp>>`
5948/// algebra. The pair —
5949/// [`current_builder_mut`] + [`pop_builder_frame`] — close the
5950/// substrate's bytecode-runtime stack-frame projection algebra at the
5951/// borrow/consume boundary: borrow-the-top-frame for emission (no
5952/// rejection — the invariant rests on the seed-frame contract),
5953/// consume-the-top-frame for finalization (rejection-routed —
5954/// the absence projects through `TemplateInvariantKind` into a
5955/// structural variant of [`LispError`]). Together with
5956/// [`resolve_bound_arg`] (lookup args by index, with per-call-site
5957/// kind constructor) and [`template_invariant_violation`] (the typed
5958/// rejection emitter the three primitives share), the four
5959/// substrate-level operations name the bytecode-runtime's named
5960/// projection surface: lookup-a-bound-arg, project-to-the-current-
5961/// builder, consume-a-finished-builder-frame, build-the-invariant-
5962/// rejection.
5963///
5964/// `kind: TemplateInvariantKind` is the closed-set typed enum whose
5965/// four variants are EXACTLY the four reachable bytecode-runtime
5966/// invariant-violation modes (`SubstBadIndex(usize)` /
5967/// `SpliceBadIndex(usize)` from the index-lookup sibling primitive,
5968/// `EndListEmptyStack` from the `EndList` arm, `FinalNoValue` from
5969/// the post-loop final pop). The Subst / Splice indexed variants
5970/// thread their `usize` payload INSIDE the variant, so the invalid
5971/// combination "stack-gate kind with an op-index" (a hypothetical
5972/// `EndListEmptyStack(99)` carrying a Subst-style payload) is
5973/// structurally unrepresentable at this helper's boundary — the
5974/// caller cannot misroute an indexed kind through `pop_builder_frame`
5975/// at compile time because the kind's data shape is part of its
5976/// variant identity. Same closed-set guarantee
5977/// [`template_invariant_violation`] gives the four kinds; this
5978/// helper composes that guarantee with the `stack.pop()` projection
5979/// at the two pop-emitting sites.
5980///
5981/// The future-run extensions ride this floor: a future bytecode op
5982/// that consumes one or more finished frames — a hypothetical
5983/// `TemplateOp::EndMany(n: usize)` that pops `n` frames into a
5984/// flattened list, a span-aware `TemplateOp::EndListWithSpan(pos)`
5985/// that pops with a position-annotated rejection — composes with
5986/// ONE call (or a fold over N calls) to [`pop_builder_frame`]
5987/// without re-deriving the stack-pop-and-reject shape. A future
5988/// instrumentation hook (a debug-mode logger that records every
5989/// frame consumption, a span-aware pop that threads `Sexp` positions
5990/// through, a multi-frame fold that pops N frames in one step)
5991/// wraps ONE call boundary rather than keeping two inline chains in
5992/// lockstep at the production op-loop sites.
5993///
5994/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5995/// inline copies of the stack-pop-and-reject projection across the
5996/// `apply_compiled` body's `EndList` arm and post-loop final-pop is
5997/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5998/// named — the same threshold [`resolve_bound_arg`] (the index-lookup
5999/// sibling) crossed in the prior claude-routine run on this file
6000/// (492a235) and [`current_builder_mut`] (the top-frame-borrow
6001/// sibling) crossed two runs ago (c6a5a9d). THEORY.md §V.1 — knowable
6002/// platform / "make invalid states unrepresentable"; the bytecode-
6003/// runtime stack-frame consume operation becomes a NAMED primitive
6004/// on the substrate's `&mut Vec<Vec<Sexp>>` algebra rather than a
6005/// re-derived `pop + ok_or_else + template_invariant_violation`
6006/// chain at every op-arm that consumes a frame. THEORY.md §II.1
6007/// invariant 2 — free middle; both pop-emitting sites route through
6008/// the SHARED `pop_builder_frame` projection, so a regression that
6009/// drifts ONE site's posture (e.g. accepts an empty-stack pop at the
6010/// `EndList` arm but not the final pop, or swaps the kind constructor
6011/// at a single site) is no longer a silent two-site divergence — the
6012/// type system binds both sites to ONE composition point.
6013///
6014/// Frontier inspiration: MLIR's `Block::eraseFromParent()` against a
6015/// region's block list — the structured-IR's block-consumption
6016/// operation is a named typed primitive that yields a typed
6017/// `LogicalResult` rejection rather than a silent `nullptr` projection
6018/// when the parent region is empty; the substrate's
6019/// `pop_builder_frame` is the unstructured-projection peer on the
6020/// substrate's `&mut Vec<Vec<Sexp>>` stack-frame algebra, with
6021/// `TemplateInvariantKind` standing in for MLIR's `LogicalResult`'s
6022/// closed-set rejection identity. GHC Core's `popTickish` /
6023/// `stackPop` family — every Core-IR transform that consumes a stack
6024/// frame off the rewriter's working stack binds to one named pop
6025/// primitive that threads a typed `WantedFailure` rejection when the
6026/// stack is empty; translated through pleme-io primitives as ONE
6027/// `pop_builder_frame(stack, macro_name, kind)` call with
6028/// `TemplateInvariantKind` carrying the closed-set rejection
6029/// identity.
6030fn pop_builder_frame(
6031 stack: &mut Vec<Vec<Sexp>>,
6032 macro_name: &str,
6033 kind: TemplateInvariantKind,
6034) -> Result<Vec<Sexp>> {
6035 stack
6036 .pop()
6037 .ok_or_else(|| template_invariant_violation(macro_name, kind))
6038}
6039
6040/// Execute a pre-compiled template against the macro's argument list.
6041fn apply_compiled(
6042 macro_name: &str,
6043 params: &MacroParams,
6044 tmpl: &CompiledTemplate,
6045 args: &[Sexp],
6046) -> Result<Sexp> {
6047 // Resolve args by param index through the shared positional binder —
6048 // identical semantics to the `bind_args` (substitute) path by construction.
6049 let args_by_index = params.bind(macro_name, args)?;
6050
6051 // Run the bytecode against a stack of in-progress list builders. The
6052 // outermost frame accumulates the single result the template yields.
6053 // Each emit-into-builder arm routes through the shared
6054 // `current_builder_mut` projection — the bytecode-runtime invariant
6055 // "at least one stack frame during the op-loop" lives in ONE expect
6056 // message rather than four silent `.unwrap()` calls.
6057 let mut stack: Vec<Vec<Sexp>> = vec![Vec::with_capacity(1)];
6058 for op in &tmpl.ops {
6059 match op {
6060 TemplateOp::Literal(s) => current_builder_mut(&mut stack).push(s.clone()),
6061 TemplateOp::Subst(idx) => {
6062 // Bound-arg-by-index lookup routes through the shared
6063 // `resolve_bound_arg` projection with `SubstBadIndex` as
6064 // the per-call-site kind constructor; the post-lookup
6065 // verb (clone + push into the current builder) is the
6066 // Subst arm's per-op shape.
6067 let v = resolve_bound_arg(
6068 &args_by_index,
6069 *idx,
6070 macro_name,
6071 TemplateInvariantKind::SubstBadIndex,
6072 )?
6073 .clone();
6074 current_builder_mut(&mut stack).push(v);
6075 }
6076 TemplateOp::Splice(idx) => {
6077 // Sibling lookup through `resolve_bound_arg` with
6078 // `SpliceBadIndex` as the per-call-site kind constructor;
6079 // the post-lookup verb (`splice_value_into` against the
6080 // current builder) consumes the borrow directly without
6081 // an intermediate clone.
6082 let v = resolve_bound_arg(
6083 &args_by_index,
6084 *idx,
6085 macro_name,
6086 TemplateInvariantKind::SpliceBadIndex,
6087 )?;
6088 splice_value_into(current_builder_mut(&mut stack), v);
6089 }
6090 TemplateOp::BeginList => stack.push(Vec::new()),
6091 TemplateOp::EndList => {
6092 // Pop the just-finished list frame through the shared
6093 // `pop_builder_frame` projection with
6094 // `EndListEmptyStack` as the per-call-site kind
6095 // constructor; the post-pop verb (folded
6096 // `Sexp::List(items)` push into the parent frame via
6097 // `current_builder_mut`) is the EndList arm's per-op
6098 // shape — sibling of the Subst/Splice arms' index-
6099 // lookup-then-emit posture, with the index-lookup
6100 // primitive (`resolve_bound_arg`) and the frame-
6101 // consume primitive (`pop_builder_frame`) BOTH routing
6102 // through the same `TemplateInvariantKind` closed-set
6103 // rejection identity.
6104 let items = pop_builder_frame(
6105 &mut stack,
6106 macro_name,
6107 TemplateInvariantKind::EndListEmptyStack,
6108 )?;
6109 current_builder_mut(&mut stack).push(Sexp::List(items));
6110 }
6111 }
6112 }
6113 // Final pop consumes the outermost (seed) frame via the same
6114 // shared projection — `FinalNoValue` as the per-call-site kind
6115 // constructor pins this site's structural-variant identity, the
6116 // post-pop verb (the `top.len() == 1` arity gate below) is the
6117 // post-loop tail's per-site shape.
6118 let mut top = pop_builder_frame(&mut stack, macro_name, TemplateInvariantKind::FinalNoValue)?;
6119 if top.len() == 1 {
6120 Ok(top.remove(0))
6121 } else {
6122 Ok(Sexp::List(top))
6123 }
6124}
6125
6126/// Hash of `(macro_name, args)` for cache keying — hot path, kept lean.
6127/// Uses `DefaultHasher` (SipHash-2-4) — fast enough that the cache hit rate
6128/// needed to net a win is low even for cheap macros.
6129fn args_cache_key(macro_name: &str, args: &[Sexp]) -> Option<CacheKey> {
6130 let mut h = DefaultHasher::new();
6131 args.len().hash(&mut h);
6132 for a in args {
6133 a.hash(&mut h);
6134 }
6135 Some((macro_name.to_string(), h.finish()))
6136}
6137
6138/// `pub(crate)` so the span-preserving expander recognizes a macro
6139/// definition through THIS function rather than a second copy of the
6140/// `defmacro`-head / name / param-list / body decomposition. A `MacroDef`
6141/// retains no spans (macros are keyed by name), so the spanned path lowers
6142/// its form and lands here — one recognizer, one lambda-list parser, one
6143/// error taxonomy.
6144pub(crate) fn macro_def_from(form: &Sexp) -> Result<Option<MacroDef>> {
6145 // Route the typed-macro-definition dispatch surface through the
6146 // substrate's typed-decoded call decomposition: `as_call_to_any`
6147 // performs the `as_list + head_symbol + MacroDefHead::from_keyword`
6148 // three-step chain in ONE structural query on the `Sexp` algebra.
6149 // The legacy diagnostic anchors on `list.len()` (the FULL form arity
6150 // including the head) — preserved here as `args.len() + 1` so
6151 // `LispError::DefmacroArity.arity` carries the same value across the
6152 // lift.
6153 let Some((head, args)) = form.as_call_to_any(MacroDefHead::from_keyword) else {
6154 return Ok(None);
6155 };
6156 if args.len() < 3 {
6157 return Err(defmacro_arity(head, args.len() + 1));
6158 }
6159 let name = args[0]
6160 .as_symbol()
6161 .ok_or_else(|| defmacro_non_symbol_name(head, &args[0]))?
6162 .to_string();
6163 let param_list = args[1]
6164 .as_list()
6165 .ok_or_else(|| defmacro_non_list_params(head, &args[1]))?;
6166 let params = parse_params(param_list)?;
6167 let body = args[2].clone();
6168 Ok(Some(MacroDef { name, params, body }))
6169}
6170
6171fn parse_params(list: &[Sexp]) -> Result<MacroParams> {
6172 let mut required = Vec::new();
6173 let mut optional: Vec<OptionalParam> = Vec::new();
6174 let mut optional_marker: Option<usize> = None;
6175 let mut i = 0;
6176 while i < list.len() {
6177 // In the optional section a `(name default)` LIST form is a valid spec
6178 // alongside a bare-symbol spec. The list form is only meaningful here,
6179 // so the dispatch fires before the `as_symbol()` gate that would
6180 // otherwise reject it as `NonSymbolParam`.
6181 if optional_marker.is_some() {
6182 if let Sexp::List(items) = &list[i] {
6183 optional.push(parse_optional_list_spec(i, &list[i], items)?);
6184 i += 1;
6185 continue;
6186 }
6187 }
6188 let s = list[i]
6189 .as_symbol()
6190 .ok_or_else(|| non_symbol_param(i, &list[i]))?;
6191 if s == MacroParams::REST_MARKER {
6192 let Some(next) = list.get(i + 1) else {
6193 return Err(rest_param_missing_name(i, None));
6194 };
6195 let Some(name) = next.as_symbol() else {
6196 return Err(rest_param_missing_name(i, Some(next)));
6197 };
6198 let trailing = &list[i + 2..];
6199 if !trailing.is_empty() {
6200 return Err(rest_param_trailing_tokens(i, trailing));
6201 }
6202 return Ok(MacroParams {
6203 required,
6204 optional,
6205 rest: Some(name.to_string()),
6206 });
6207 }
6208 if s == MacroParams::OPTIONAL_MARKER {
6209 if let Some(first) = optional_marker {
6210 return Err(optional_marker_repeated(first, i));
6211 }
6212 optional_marker = Some(i);
6213 i += 1;
6214 continue;
6215 }
6216 if optional_marker.is_some() {
6217 optional.push(OptionalParam::bare(s));
6218 } else {
6219 required.push(s.to_string());
6220 }
6221 i += 1;
6222 }
6223 Ok(MacroParams {
6224 required,
6225 optional,
6226 rest: None,
6227 })
6228}
6229
6230/// Project a `Sexp::List` in the `&optional` section to a typed
6231/// [`OptionalParam`]. The only admissible shape is `(NAME DEFAULT)` — a
6232/// list of exactly TWO elements whose first element is a symbol. Every
6233/// other list shape is the structural rejection
6234/// [`LispError::OptionalParamMalformed`], with a typed `reason`
6235/// ([`OptionalParamMalformedReason`]) naming WHICH way the spec is
6236/// malformed — empty, missing-default, extra-elements, or non-symbol name.
6237///
6238/// `position` is the loop index inside `parse_params`, mirroring the
6239/// `position`/`rest_position`/`first_position` slots on the sibling
6240/// `parse_params` rejection variants. `list_form` is the offending
6241/// `Sexp::List` itself, projected through `crate::domain::sexp_witness` so
6242/// the variant carries BOTH `SexpShape::List` AND the rendered form (for
6243/// LSP / REPL / `tatara-check` consumption). `items` is the list body,
6244/// avoiding a re-`as_list()` at the call boundary.
6245fn parse_optional_list_spec(
6246 position: usize,
6247 list_form: &Sexp,
6248 items: &[Sexp],
6249) -> Result<OptionalParam> {
6250 use crate::error::OptionalParamMalformedReason as R;
6251 // Arity dispatch — threads through the typed classifier on the
6252 // closed-set algebra so the accept-arity binding (arity ==
6253 // `R::OPTIONAL_PARAM_SPEC_ARITY`) is derived from ONE typed const
6254 // rather than the bare literal `2`. Post-classification the arity
6255 // is exactly the accept target; the head-symbol gate is the only
6256 // remaining rejection axis.
6257 if let Some(reason) = R::classify_arity(items.len()) {
6258 return Err(optional_param_malformed(position, list_form, reason));
6259 }
6260 let Some(name) = items[0].as_symbol() else {
6261 return Err(optional_param_malformed(
6262 position,
6263 list_form,
6264 R::NonSymbolName,
6265 ));
6266 };
6267 Ok(OptionalParam::with_default(name, items[1].clone()))
6268}
6269
6270fn bind_args(
6271 macro_name: &str,
6272 params: &MacroParams,
6273 args: &[Sexp],
6274) -> Result<HashMap<String, Sexp>> {
6275 // Zip the shared positional binding (parallel to `names()`) into the
6276 // name-keyed map the `substitute` path looks substitutions up in.
6277 let vals = params.bind(macro_name, args)?;
6278 Ok(params
6279 .names()
6280 .into_iter()
6281 .map(String::from)
6282 .zip(vals)
6283 .collect())
6284}
6285
6286/// Substitute `,name` and `,@name` within a template.
6287/// `,@name` only makes sense inside a List — it splices the bound list into
6288/// the containing list.
6289///
6290/// Routes both unquote-family sites — the top-level `,X` / `,@X`
6291/// recognition AND the list-inner per-item splice recognition — through
6292/// the substrate's typed-marker projection [`Sexp::as_unquote`]. Pre-lift
6293/// each site opened its own `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice
6294/// (inner)` arm paired with a `UnquoteForm::Unquote` / `UnquoteForm::
6295/// Splice` literal; post-lift the (Sexp variant, UnquoteForm variant)
6296/// pairing is bound at ONE projection function the type system threads
6297/// through `(UnquoteForm, &Sexp)`, eliminating the silent two-site
6298/// pairing drift the prior shape allowed.
6299fn substitute(form: &Sexp, bindings: &HashMap<String, Sexp>) -> Result<Sexp> {
6300 if let Some((kind, inner)) = form.as_unquote() {
6301 return match kind {
6302 UnquoteForm::Unquote => resolve_unquote_in_bindings(inner, bindings, kind).cloned(),
6303 UnquoteForm::Splice => Err(splice_outside_list(inner)),
6304 };
6305 }
6306 match form {
6307 Sexp::List(items) => {
6308 let mut out: Vec<Sexp> = Vec::with_capacity(items.len());
6309 for item in items {
6310 if let Some((UnquoteForm::Splice, inner)) = item.as_unquote() {
6311 let val = resolve_unquote_in_bindings(inner, bindings, UnquoteForm::Splice)?;
6312 splice_value_into(&mut out, val);
6313 } else {
6314 out.push(substitute(item, bindings)?);
6315 }
6316 }
6317 Ok(Sexp::List(out))
6318 }
6319 _ => Ok(form.clone()),
6320 }
6321}
6322
6323/// Lift the four inline `LispError::Compile { form: format!("{prefix}{name}"),
6324/// message: "unbound" }` triples (compile_node Unquote/UnquoteSplice +
6325/// substitute Unquote/UnquoteSplice) behind ONE named primitive. Pairs the
6326/// structural variant with `crate::domain::suggest`'s bounded edit-distance
6327/// scan over the candidate set so a typo in `,name` against a macro's params
6328/// (or against a substitution scope's live bindings) surfaces as
6329/// `"compile error in ,xs: unbound; did you mean ,x?"` instead of the bare
6330/// `"unbound"`. The candidate set is per-call — params during compile,
6331/// `bindings.keys()` during substitute — so the operator's hint is always
6332/// drawn from the in-scope name set, never a stale snapshot.
6333///
6334/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6335/// variants are EXACTLY the two reachable syntactic markers
6336/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6337/// boundary (rather than `&'static str`) lands the same compile-time
6338/// closed-set guarantee `defmacro_arity` / `defmacro_non_symbol_name` /
6339/// `defmacro_non_list_params` get from threading `MacroDefHead`: the
6340/// closed set is encoded in the type system, so a regression that drifts
6341/// the marker (e.g. a fourth `prefix: ",,"` call site) becomes a type
6342/// error at the call site, not a runtime substring drift. `name` is the
6343/// offender from source; the hint is `Option<String>` because the matched
6344/// candidate borrows from a transient `Vec<&str>` we built locally —
6345/// copying the matched name into the variant is the cheapest way to keep
6346/// `LispError` lifetime-free.
6347///
6348/// Theory anchor: THEORY.md §VI.1 — generation over composition; four inline
6349/// copies in one module is well past the three-times rule. THEORY.md §V.1 —
6350/// knowable platform; the structural variant exposes `prefix` / `name` /
6351/// `hint` as first-class fields so authoring tools (LSP, REPL,
6352/// `tatara-check`) bind to the data shape instead of substring-parsing the
6353/// rendered diagnostic.
6354fn unbound_template_var(prefix: UnquoteForm, name: &str, candidates: &[&str]) -> LispError {
6355 LispError::UnboundTemplateVar {
6356 prefix,
6357 name: name.to_string(),
6358 hint: crate::domain::suggest(name, candidates).map(str::to_string),
6359 }
6360}
6361
6362/// Lift the four inline `LispError::Compile { form: "unquote" /
6363/// "unquote-splice", message: "only bound symbols may appear after `,` /
6364/// `,@`" }` triples in this module (compile_node Unquote / UnquoteSplice +
6365/// substitute Unquote / UnquoteSplice-inside-list) behind ONE named
6366/// primitive. Sibling of `unbound_template_var`: that helper fires when the
6367/// slot IS a symbol but the symbol isn't bound; this helper fires when the
6368/// slot isn't a symbol at all. Together they close every distinct
6369/// typed-entry template-gate failure mode for the no-evaluator template
6370/// language: each is a structural variant of `LispError`, not a
6371/// `Compile`-shaped substring.
6372///
6373/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6374/// variants are EXACTLY the two reachable syntactic markers
6375/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6376/// boundary (rather than `&'static str`) lands the same compile-time
6377/// closed-set guarantee `unbound_template_var` carries: the closed set is
6378/// encoded in the type system. The inner is the offending `Sexp` routed
6379/// through `crate::domain::sexp_witness` — the typed joint projection
6380/// pairing `SexpShape` (structural shape) with `Sexp::Display`
6381/// (renderable literal) at ONE call boundary. Authoring tools bind to
6382/// BOTH `got.shape` (e.g. `SexpShape::List`) AND `got.display` (e.g.
6383/// `"(list 1 2)"`) jointly — same posture as `splice_outside_list`
6384/// after its prior-run promotion to `SexpWitness`. The two template-
6385/// gate `,X/,@X` rejection variants now share ONE typed witness
6386/// identity at their `got` slot.
6387///
6388/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6389/// inline copies in one module is past the three-times rule. THEORY.md
6390/// §V.1 — knowable platform; the structural variant exposes `prefix` /
6391/// `got` as first-class fields so authoring tools (LSP, REPL,
6392/// `tatara-check`) bind to the data shape instead of substring-parsing
6393/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6394/// a non-symbol unquote target is exactly the failure mode the
6395/// typed-entry gate exists to reject.
6396fn non_symbol_unquote_target(prefix: UnquoteForm, got: &Sexp) -> LispError {
6397 LispError::NonSymbolUnquoteTarget {
6398 prefix,
6399 got: got.witness(),
6400 }
6401}
6402
6403/// Project the inner of a `,X` / `,@X` form to its bound symbol name, or
6404/// raise the structural `LispError::NonSymbolUnquoteTarget` rejection at
6405/// the typed-entry template-gate boundary. ONE named primitive every
6406/// `,X` / `,@X` resolution site in the substrate shares — the inline
6407/// `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(form,
6408/// inner))?` pattern appeared four times across `compile_node`
6409/// (bytecode-path Unquote / UnquoteSplice arms) AND `substitute`
6410/// (substitute-path Unquote / list-inner UnquoteSplice arms), well past
6411/// the three-times-rule trigger. After this lift the four sites collapse
6412/// to a single `unquote_target_symbol(inner, form)?` call, and the
6413/// substrate's understanding of "an unquote target's first gate is `must
6414/// be a symbol`" lives in ONE function — a regression that drifts the
6415/// gate's posture (e.g. accepts non-symbol targets at the bytecode path
6416/// but not the substitute path) becomes a type-level change at this
6417/// helper, not a silent four-site divergence.
6418///
6419/// Sibling of `non_symbol_unquote_target` (the error builder this gate
6420/// calls on failure) and `unbound_template_var` (the typed-entry
6421/// template-gate's SECOND gate — fires once `unquote_target_symbol`
6422/// projects the symbol successfully but the symbol isn't bound in the
6423/// in-scope name set). Together the three close the substrate's
6424/// understanding of the two-step typed-entry template-gate: gate-1 is
6425/// `must-be-a-symbol`, gate-2 is `must-be-bound-in-scope`. With this
6426/// lift, gate-1 lives at ONE call boundary across all four template-
6427/// gate sites — bytecode path AND substitute path AND both `,X` and
6428/// `,@X` forms.
6429///
6430/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6431/// variants are EXACTLY the two reachable syntactic markers
6432/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6433/// helper boundary (rather than `&'static str`) lands the same
6434/// compile-time closed-set guarantee `non_symbol_unquote_target` and
6435/// `unbound_template_var` get from their `UnquoteForm` slots — a
6436/// regression that drifts the marker (e.g. a third pseudo-marker call
6437/// site) becomes a type error at the call site, not a runtime
6438/// substring drift. The returned `&'a str` borrows from `inner` — the
6439/// caller feeds it directly into `params.iter().position(|p| *p ==
6440/// name)` (`compile_node`) or `bindings.get(name)` (`substitute`)
6441/// without an intermediate allocation.
6442///
6443/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6444/// inline copies of the gate-1 projection (`compile_node`
6445/// Unquote/UnquoteSplice + `substitute` Unquote + `substitute`
6446/// list-inner UnquoteSplice) is past the three-times rule. THEORY.md
6447/// §V.1 — knowable platform; the gate's identity becomes a NAMED
6448/// primitive consumer-binding rather than a four-times-inlined
6449/// match-and-reject snippet — authoring surfaces (REPL, LSP,
6450/// `tatara-check`) that want to surface "the typed-entry template-gate
6451/// rejected your form because the unquote target wasn't a symbol" bind
6452/// to ONE function. THEORY.md §II.1 invariant 1 — typed entry; an
6453/// unquote target that isn't a symbol is exactly the failure mode the
6454/// typed-entry template-gate exists to reject. THEORY.md §II.1
6455/// invariant 2 — free middle; both bytecode AND substitute expansion
6456/// paths now project through the SAME gate-1 primitive, so a macro
6457/// that compiles under one strategy compiles under the other (the
6458/// gate's posture is uniform across the two strategies, no
6459/// per-strategy drift can creep in).
6460fn unquote_target_symbol(inner: &Sexp, form: UnquoteForm) -> Result<&str> {
6461 inner
6462 .as_symbol()
6463 .ok_or_else(|| non_symbol_unquote_target(form, inner))
6464}
6465
6466/// Gate-2 for the bytecode-template compile path: resolve a template
6467/// variable name to its index inside the macro's static param list, or
6468/// raise the structural `LispError::UnboundTemplateVar` rejection. ONE
6469/// named primitive that the two `compile_node` sites — `Sexp::Unquote(_)`
6470/// and `Sexp::UnquoteSplice(_)` arms — share. Before this lift the same
6471/// `params.iter().position(|p| *p == name).ok_or_else(|| unbound_template_var(
6472/// FORM, name, params))?` projection was inlined twice in one match
6473/// block; after this lift the two sites collapse to a single
6474/// `resolve_param_index(name, params, form)?` call and the
6475/// `Subst(idx)` / `Splice(idx)` ops push from a uniform projection
6476/// boundary.
6477///
6478/// Sibling of `resolve_binding`: the same gate-2 contract on the
6479/// substitute path. Together the two close the typed-entry template
6480/// gate's gate-2 (must-be-bound-in-scope) primitive across BOTH
6481/// expansion strategies — gate-1 (`unquote_target_symbol`) projects the
6482/// inner to a symbol name; gate-2 looks the name up in the in-scope
6483/// candidate set. The two paths' candidate sets differ structurally
6484/// (compile path: `&[&str]` of macro params, returning `usize`;
6485/// substitute path: `&HashMap<String, Sexp>` of live bindings, returning
6486/// `&Sexp`), so the gate-2 primitive bifurcates by path — but the
6487/// rejection shape (`LispError::UnboundTemplateVar { prefix, name, hint }`
6488/// with `crate::domain::suggest`-driven hint) is identical across both
6489/// paths. A regression that drifts gate-2's posture (e.g., accepts an
6490/// unbound `,name` at the bytecode path but not the substitute path) is
6491/// now a type-level change at this helper, not a silent four-site
6492/// divergence.
6493///
6494/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6495/// variants are EXACTLY the two reachable syntactic markers
6496/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6497/// helper boundary (rather than `&'static str`) lands the same
6498/// compile-time closed-set guarantee `unquote_target_symbol`,
6499/// `unbound_template_var`, and `non_symbol_unquote_target` carry — a
6500/// regression that drifts the marker becomes a type error at the call
6501/// site, not a runtime substring drift.
6502///
6503/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6504/// inline copies of the gate-2 projection in one match block, paired
6505/// with the two substitute-path inline copies, is four copies in two
6506/// functions — past the three-times rule once the structural shape is
6507/// named. THEORY.md §V.1 — knowable platform; the gate's identity
6508/// becomes a NAMED primitive consumer-binding rather than a
6509/// twice-inlined position-and-reject snippet — authoring surfaces
6510/// (REPL, LSP, `tatara-check`) that want to surface "the typed-entry
6511/// template-gate rejected your form because the name isn't bound in
6512/// scope" bind to ONE function per path. THEORY.md §II.1 invariant 1 —
6513/// typed entry; an unbound template variable is exactly the failure
6514/// mode the typed-entry template-gate exists to reject. THEORY.md
6515/// §II.1 invariant 2 — free middle; both expansion strategies'
6516/// gate-2 emit the SAME structural variant, so a macro that compiles
6517/// under one strategy compiles under the other.
6518fn resolve_param_index(name: &str, params: &[&str], form: UnquoteForm) -> Result<usize> {
6519 params
6520 .iter()
6521 .position(|p| *p == name)
6522 .ok_or_else(|| unbound_template_var(form, name, params))
6523}
6524
6525/// Gate-2 for the substitute expansion path: resolve a template
6526/// variable name to its bound `Sexp` value inside the runtime bindings
6527/// map, or raise the structural `LispError::UnboundTemplateVar`
6528/// rejection. ONE named primitive that the two `substitute` sites —
6529/// the top-level `Sexp::Unquote(_)` arm and the list-inner
6530/// `Sexp::UnquoteSplice(_)` arm — share. Before this lift the same
6531/// `bindings.get(sym).<cloned>?.ok_or_else(|| unbound_template_var(
6532/// FORM, sym, &bound_names(bindings)))` projection was inlined twice
6533/// across the substitute walker; after this lift the two sites
6534/// collapse to a single `resolve_binding(bindings, sym, form)?` call
6535/// (with a trailing `.cloned()` at the top-level arm because that arm
6536/// returns an owned `Sexp` while the list-inner arm consumes the
6537/// `&Sexp` borrow directly).
6538///
6539/// Sibling of `resolve_param_index`: the same gate-2 contract on the
6540/// bytecode-template compile path. Together the two close the
6541/// typed-entry template gate's gate-2 (must-be-bound-in-scope)
6542/// primitive across BOTH expansion strategies. The candidate set on
6543/// the substitute path is the live bindings' keys (built fresh per
6544/// call via `bound_names`) — never a stale snapshot, so the
6545/// suggest-driven hint is always drawn from the actual in-scope name
6546/// set the operator sees.
6547///
6548/// The returned `&'a Sexp` borrows from `bindings` — the list-inner
6549/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6550/// splice-expansion match without an intermediate allocation. The
6551/// top-level caller's owned-Sexp obligation is satisfied by the
6552/// `.cloned()` projection at the call site, which is a single typed
6553/// `Sexp::clone` and not a redundant lookup.
6554///
6555/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6556/// `resolve_param_index` and `unquote_target_symbol`. A regression
6557/// that drifts the marker becomes a type error at the call site, not
6558/// a runtime substring drift.
6559///
6560/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6561/// inline copies of the gate-2 projection in the substitute walker,
6562/// paired with the two compile-path inline copies, is four copies in
6563/// two functions — past the three-times rule once the structural
6564/// shape is named. THEORY.md §V.1 — knowable platform; the gate's
6565/// identity becomes a NAMED primitive consumer-binding rather than a
6566/// twice-inlined lookup-and-reject snippet. THEORY.md §II.1
6567/// invariant 1 — typed entry; an unbound template variable is exactly
6568/// the failure mode the typed-entry template-gate exists to reject.
6569/// THEORY.md §II.1 invariant 2 — free middle; both expansion
6570/// strategies' gate-2 emit the SAME structural variant.
6571fn resolve_binding<'a>(
6572 bindings: &'a HashMap<String, Sexp>,
6573 name: &str,
6574 form: UnquoteForm,
6575) -> Result<&'a Sexp> {
6576 bindings
6577 .get(name)
6578 .ok_or_else(|| unbound_template_var(form, name, &bound_names(bindings)))
6579}
6580
6581/// Compose gate-1 + gate-2 for the bytecode-template compile path into ONE
6582/// named primitive: project the unquote `inner` to a symbol name
6583/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6584/// index inside the macro's static param list (gate-2, via
6585/// `resolve_param_index`). Sibling of `resolve_unquote_in_bindings`: the
6586/// same gate-1+gate-2 composition on the substitute expansion path.
6587///
6588/// Before this lift, the two `compile_node` arms (`Sexp::Unquote(_)` and
6589/// `Sexp::UnquoteSplice(_)`) threaded `form: UnquoteForm` through TWO
6590/// helper calls each — once into `unquote_target_symbol(inner, form)?`
6591/// (gate-1) AND once into `resolve_param_index(name, params, form)?`
6592/// (gate-2). The marker's typed identity was re-asserted at the call site
6593/// twice per arm — four `UnquoteForm::Unquote` / `UnquoteForm::Splice`
6594/// literal occurrences across the two arms, for what is structurally ONE
6595/// marker-identity per syntactic-marker arm. After this lift each arm
6596/// threads the marker ONCE through ONE call, and the gate-1-then-gate-2
6597/// sequencing lives in the helper body, not at the call site.
6598///
6599/// The composition is load-bearing: gate-1 (must-be-a-symbol) MUST fire
6600/// before gate-2 (must-be-bound-in-scope) — a non-symbol inner is
6601/// structurally a different failure mode (`LispError::NonSymbolUnquoteTarget`,
6602/// which carries the offending `SexpWitness`) than an unbound symbol
6603/// (`LispError::UnboundTemplateVar`, which carries a `name: String` plus
6604/// a `crate::domain::suggest`-driven hint over the candidate set). A
6605/// regression that reorders or skips gate-1 would emit
6606/// `LispError::UnboundTemplateVar { name: "(list 1 2)", ... }` for a
6607/// non-symbol inner (re-treating the rendered list literal as a bound-
6608/// name lookup key), which is exactly the diagnostic-confusion this
6609/// composition exists to rule out. Naming the composition as one
6610/// primitive makes the sequencing structural — the helper body IS the
6611/// proof that gate-1 ran before gate-2.
6612///
6613/// `form` is `UnquoteForm` — the closed-set typed enum threaded through
6614/// the composition once and passed onward to both gate-1 and gate-2's
6615/// rejection-builders. Same posture as `unquote_target_symbol`,
6616/// `resolve_param_index`, `resolve_binding`, `non_symbol_unquote_target`,
6617/// and `unbound_template_var` — a regression that drifts the marker
6618/// becomes a type error at the helper boundary, not a runtime substring
6619/// drift, AND the marker can no longer drift BETWEEN gate-1 and gate-2
6620/// at a single call site (which the prior pre-lift shape allowed:
6621/// `unquote_target_symbol(inner, UnquoteForm::Unquote)?` followed by
6622/// `resolve_param_index(name, params, UnquoteForm::Splice)?` would
6623/// type-check but render a misleading diagnostic).
6624///
6625/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6626/// gate-1+gate-2 SEQUENCE is itself a named primitive once both halves
6627/// have been named (two prior runs landed the halves; this run lands the
6628/// composition). THEORY.md §V.1 — knowable platform; the gate's
6629/// composition is now load-bearing in the type system — gate-1 cannot be
6630/// silently skipped, gate-2 cannot be silently reordered before gate-1,
6631/// and the marker cannot drift between the two halves. THEORY.md §II.1
6632/// invariant 1 — typed entry; the typed-entry template gate's full
6633/// rejection chain (non-symbol → unbound-symbol) is now ONE primitive.
6634/// THEORY.md §II.1 invariant 2 — free middle; both expansion strategies
6635/// expose the gate's identity as ONE primitive per path, so a macro that
6636/// passes the gate under one strategy passes under the other (no per-
6637/// strategy composition drift can creep in).
6638fn resolve_unquote_in_params(inner: &Sexp, params: &[&str], form: UnquoteForm) -> Result<usize> {
6639 let name = unquote_target_symbol(inner, form)?;
6640 resolve_param_index(name, params, form)
6641}
6642
6643/// Compose gate-1 + gate-2 for the substitute expansion path into ONE
6644/// named primitive: project the unquote `inner` to a symbol name
6645/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6646/// bound `Sexp` value inside the runtime bindings map (gate-2, via
6647/// `resolve_binding`). Sibling of `resolve_unquote_in_params`: the same
6648/// gate-1+gate-2 composition on the bytecode-template compile path.
6649///
6650/// Before this lift, the substitute walker's two unquote sites (the
6651/// top-level `Sexp::Unquote(_)` arm and the list-inner
6652/// `Sexp::UnquoteSplice(_)` arm) threaded `form: UnquoteForm` through
6653/// TWO helper calls each — once into `unquote_target_symbol(inner,
6654/// form)?` (gate-1) AND once into `resolve_binding(bindings, name,
6655/// form)?` (gate-2). After this lift each site threads the marker
6656/// ONCE through ONE call. Same composition contract as
6657/// `resolve_unquote_in_params` — gate-1 fires before gate-2 by the
6658/// helper body's `?`-then-call sequencing, NOT by call-site discipline.
6659///
6660/// The returned `&'a Sexp` borrows from `bindings` so the list-inner
6661/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6662/// splice-expansion match without an intermediate allocation; the
6663/// top-level caller's owned-Sexp obligation is satisfied by a
6664/// `.cloned()` projection at the call site (one typed `Sexp::clone`,
6665/// no redundant lookup).
6666///
6667/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6668/// `resolve_unquote_in_params` and all the helpers it composes. After
6669/// this lift, the marker's identity flows through the substitute path's
6670/// typed-entry template gate via ONE explicit pass per call site, not
6671/// two; the gate's gate-1+gate-2 sequencing is structural across both
6672/// expansion strategies.
6673///
6674/// Theory anchor: same as `resolve_unquote_in_params`. THEORY.md §VI.1
6675/// (generation over composition; named composition of named gates),
6676/// THEORY.md §V.1 (knowable platform; gate composition is type-system
6677/// load-bearing), THEORY.md §II.1 invariant 1 (typed entry; the full
6678/// rejection chain is ONE primitive), THEORY.md §II.1 invariant 2
6679/// (free middle; both strategies share the same composition shape).
6680fn resolve_unquote_in_bindings<'a>(
6681 inner: &Sexp,
6682 bindings: &'a HashMap<String, Sexp>,
6683 form: UnquoteForm,
6684) -> Result<&'a Sexp> {
6685 let name = unquote_target_symbol(inner, form)?;
6686 resolve_binding(bindings, name, form)
6687}
6688
6689/// Lift the lone `LispError::Compile { form: "unquote-splice", message:
6690/// "`,@` may only appear inside a list" }` triple — the substitute path's
6691/// top-level `,@X` rejection — behind ONE named primitive. Sibling of
6692/// `non_symbol_unquote_target` and `unbound_template_var`: those helpers
6693/// fire when the slot inside a `,X` / `,@X` is malformed (non-symbol or
6694/// unbound symbol); this helper fires when the `,@X` form itself is
6695/// ill-positioned (no containing list to flatten into). Together the three
6696/// close every distinct typed-entry template-gate failure mode for the
6697/// no-evaluator template language: each is a structural variant of
6698/// `LispError`, not a `Compile`-shaped substring.
6699///
6700/// `inner` is the offending `Sexp` projected through `Display` so the
6701/// operator sees the literal value they wrote — `xs`, `(list 1 2)`, `5` —
6702/// instead of just the bare "may only appear inside a list" verdict. The
6703/// helper takes `&Sexp` (parallel to `non_symbol_unquote_target`) and
6704/// projects through `to_string()` at the variant boundary; the `prefix:
6705/// &'static str` slot is implicit (always `,@`) and absent from the variant
6706/// itself, parallel to how `OddKwargs { dangling }` names ONE failure mode
6707/// without a syntactic-marker slot.
6708///
6709/// Used by both the substitute path (top-level `,@X` body) AND the bytecode
6710/// path's `compile_template` gate (top-level `,@X` body — closing the prior
6711/// silent-divergence where the bytecode interpreter's outermost stack frame
6712/// absorbed the splice). After this lift `,@-outside-list` is rejected on
6713/// both paths with ONE structural variant — the typed-entry template gate
6714/// is fully structural across both expansion strategies.
6715///
6716/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6717/// emission sites (substitute + compile_template) for one failure mode is
6718/// past the three-times rule once the structural shape is named. THEORY.md
6719/// §V.1 — knowable platform; the structural variant exposes `got` as a
6720/// first-class field so authoring tools (LSP, REPL, `tatara-check`) bind to
6721/// the data shape instead of substring-parsing the rendered diagnostic.
6722/// THEORY.md §II.1 invariant 1 — typed entry; a `,@X` at a position with no
6723/// containing list is exactly the failure mode the typed-entry gate exists
6724/// to reject. THEORY.md §II.1 invariant 2 — free middle; both expansion
6725/// paths now reject the same set of templates, so a macro that registers
6726/// successfully has the same expansion behavior under either strategy.
6727fn splice_outside_list(inner: &Sexp) -> LispError {
6728 LispError::SpliceOutsideList {
6729 got: inner.witness(),
6730 }
6731}
6732
6733/// Lift the two inline `LispError::Compile { form: format!("call to
6734/// {macro_name}"), message: format!("missing required arg: {name}") }`
6735/// triples — `bind_args` (substitute path) AND `apply_compiled` (bytecode
6736/// path) — behind ONE named primitive. Sibling of the typed-entry kwargs
6737/// `MissingKwarg { key }` lift: that variant fires when a `(<head> :key
6738/// value …)` kwargs form omits a required keyword; this variant fires when
6739/// a `(<macroname> a b …)` call omits a required positional param. Together
6740/// they close every distinct typed-entry missing-required surface in the
6741/// substrate — kwargs-gate AND macro-call-gate now share a single
6742/// structural-variant idiom.
6743///
6744/// Same single emission shape across both expansion strategies — before
6745/// this lift the same failure mode emitted byte-identical
6746/// `LispError::Compile { … }` triples at TWO call sites; after this lift
6747/// both sites share ONE structural variant. Two strategies that picked
6748/// different code paths now emit the same structural variant for the same
6749/// failure mode (THEORY.md §II.1 invariant 2 — free middle: which strategy
6750/// you picked must not change which inputs you reject OR how the rejection
6751/// is shaped). Same posture as `splice_outside_list`'s path-uniform
6752/// rejection across substitute + compile_template.
6753///
6754/// `macro_name` and `name` are `&str` borrows from the call-site / param
6755/// list; the variant's owned `String`s are formed at the boundary so
6756/// `LispError` stays lifetime-free.
6757///
6758/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6759/// inline copies of one shape is past the three-times-rule trigger once
6760/// the structural variant is named (the test count gives this the
6761/// fail-before-pass-after edge). THEORY.md §V.1 — knowable platform; the
6762/// structural variant exposes `macro_name` / `param` as first-class
6763/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the data
6764/// shape instead of substring-parsing the rendered diagnostic. THEORY.md
6765/// §II.1 invariant 1 — typed entry; a macro call with too few args is
6766/// exactly the failure mode the typed-entry gate exists to reject.
6767fn missing_macro_arg(macro_name: &str, param: &str) -> LispError {
6768 LispError::MissingMacroArg {
6769 macro_name: macro_name.to_string(),
6770 param: param.to_string(),
6771 }
6772}
6773
6774/// Mirror at the call-site of `missing_macro_arg`: that helper fires when
6775/// the macro CALL supplies TOO FEW args for the required arity (a required
6776/// slot has no arg); this helper fires when the macro CALL supplies TOO
6777/// MANY args for a rest-less param list (the surplus has nowhere to bind).
6778/// Together they close the typed-entry macro-call-gate's positional-arity
6779/// surface in both directions; together with the definition-site
6780/// `RestParamTrailingTokens` (lifted by the prior-run typed-promotion
6781/// lineage at the parse_params boundary), every distinct way a macro
6782/// definition + call pair can MISCOUNT args is now a named structural
6783/// rejection.
6784///
6785/// `expected` is the rest-less binder's fixed maximum arity
6786/// (`required.len() + optional.len()`); `got` is the actual call-site arg
6787/// count. Both are surfaced at the variant boundary so authoring tools
6788/// (REPL, LSP, `tatara-check`) name the "you supplied {got} args but the
6789/// macro takes at most {expected}" quick-fix from one structural projection
6790/// rather than re-deriving either count from the source. `macro_name` is
6791/// `&str` borrowed from the call-site; the variant's owned `String` is
6792/// formed at the boundary so `LispError` stays lifetime-free — same posture
6793/// as `missing_macro_arg`.
6794///
6795/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6796/// rest-less surplus-args gate is a SINGLE-OWNER named rejection, not a
6797/// silent truncation re-asserted at every consumer that walks the bound
6798/// values. THEORY.md §V.1 — knowable platform; the structural variant
6799/// exposes `macro_name` / `expected` / `got` as first-class fields so
6800/// authoring tools bind to the data shape instead of substring-parsing
6801/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a
6802/// macro call with too many args (and no `&rest` slot to absorb them) is
6803/// exactly the failure mode the typed-entry gate exists to reject —
6804/// silently dropping `args[expected..]` is structurally indistinguishable
6805/// from honoring them, the asymmetry this gate closes. THEORY.md §II.1
6806/// invariant 2 — free middle; both expansion strategies route through the
6807/// SHARED `MacroParams::bind`, so the new rejection lands once and the
6808/// substitute + bytecode paths inherit it unable to drift.
6809fn too_many_macro_args(macro_name: &str, expected: usize, got: usize) -> LispError {
6810 LispError::TooManyMacroArgs {
6811 macro_name: macro_name.to_string(),
6812 expected,
6813 got,
6814 }
6815}
6816
6817/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6818/// "expected symbol" }` triple in `parse_params` behind ONE named
6819/// primitive. Sibling of `missing_macro_arg`: that helper fires when the
6820/// macro CALL is malformed (call-site missing a positional arg); this
6821/// helper fires when the macro DEFINITION is malformed (definition-site
6822/// has a non-symbol where a param name should be). Together they open
6823/// the defmacro-syntax-gate / macro-call-gate split — call-site
6824/// rejections vs. definition-site rejections — each as its own
6825/// structural-variant family on `LispError`.
6826///
6827/// `position` is the loop index inside `parse_params`, i.e. the 0-based
6828/// index of the offending element within the param list (`(defmacro f
6829/// (a 5 b) …)` — position 1 is the literal `5`); naming it lets an LSP
6830/// quick-fix point at the exact list element instead of the whole
6831/// param list. `got` is the offending `Sexp` projected through
6832/// `Display` so the operator sees the literal value they wrote
6833/// (`5`, `:foo`, `(nested)`) at the variant boundary; the helper takes
6834/// `&Sexp` (parallel to `non_symbol_unquote_target` and
6835/// `splice_outside_list`) and projects through `to_string()` so the
6836/// variant stays lifetime-free.
6837///
6838/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6839/// inline copy still earns a named primitive once the structural shape
6840/// is named (the test count gives this the fail-before-pass-after edge,
6841/// parallel to how `OddKwargs` was lifted from a single site for the
6842/// structural-completeness payoff). THEORY.md §V.1 — knowable platform;
6843/// the structural variant exposes `position` / `got` as first-class
6844/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the
6845/// data shape instead of substring-parsing the rendered diagnostic.
6846/// THEORY.md §II.1 invariant 1 — typed entry; a non-symbol element
6847/// inside a defmacro param list is exactly the failure mode the
6848/// typed-entry gate exists to reject — and it must reject DEFINITIONS
6849/// as readily as it rejects CALLS.
6850fn non_symbol_param(position: usize, got: &Sexp) -> LispError {
6851 LispError::NonSymbolParam {
6852 position,
6853 got: got.witness(),
6854 }
6855}
6856
6857/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6858/// "&rest needs a name" }` triple in `parse_params` behind ONE named
6859/// primitive. Sibling of `non_symbol_param`: that helper fires when a
6860/// NON-`&rest` element at a param position isn't a symbol; this helper
6861/// fires specifically on the post-`&rest` follower slot, where the
6862/// failure mode bifurcates into "missing entirely" (`got = None`) vs.
6863/// "present but not a symbol" (`got = Some(...)`). Together, the two
6864/// helpers close the `parse_params` walker — every distinct failure
6865/// mode the walker can emit is now a structural variant of `LispError`,
6866/// not a `Compile`-shaped substring.
6867///
6868/// `rest_position` is the loop index inside `parse_params` at which
6869/// the `&rest` marker was matched, i.e. the 0-based index of `&rest`
6870/// within the param list (`(defmacro f (a &rest 5) …)` — rest_position
6871/// 1 is `&rest`, the offender follows at 2); naming the marker
6872/// position lets an LSP quick-fix point at the `&rest` form itself
6873/// rather than at the next list element. `got` is `Option<&Sexp>`
6874/// because the follower slot bifurcates: `None` when the marker was
6875/// the param list's last element (no follower at all), `Some(sexp)`
6876/// when a follower exists but isn't a symbol; the helper projects
6877/// through `to_string()` at the variant boundary so the variant stays
6878/// lifetime-free.
6879///
6880/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6881/// inline copy still earns a named primitive once the structural shape
6882/// is named (the test count gives this the fail-before-pass-after
6883/// edge, parallel to how `non_symbol_param` was lifted from a single
6884/// site for the structural-completeness payoff). THEORY.md §V.1 —
6885/// knowable platform; the structural variant exposes `rest_position` /
6886/// `got` as first-class fields so authoring tools (LSP, REPL,
6887/// `tatara-check`) bind to the data shape instead of substring-parsing
6888/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6889/// a `&rest` marker followed by no name (or by a non-symbol) is
6890/// exactly the failure mode the typed-entry gate exists to reject —
6891/// and the gate must reject DEFINITIONS as readily as it rejects
6892/// CALLS.
6893fn rest_param_missing_name(rest_position: usize, got: Option<&Sexp>) -> LispError {
6894 LispError::RestParamMissingName {
6895 rest_position,
6896 got: got.map(Sexp::witness),
6897 }
6898}
6899
6900/// The third and final `parse_params` definition-site rejection — a
6901/// `&rest <name>` followed by further tokens. Sibling of `non_symbol_param`
6902/// (a param slot that isn't a symbol) and `rest_param_missing_name` (the
6903/// post-`&rest` follower is missing or malformed): this helper fires once
6904/// the rest name is bound and the walker finds the param list does not end
6905/// there. The `&rest` name absorbs every remaining call arg, so it is
6906/// structurally the LAST param a list can name; trailing tokens are
6907/// unrepresentable in `MacroParams` and were previously dropped silently.
6908///
6909/// `rest_position` is the loop index of the `&rest` marker (parallel to
6910/// `rest_param_missing_name`); `trailing` is the non-empty token run after
6911/// the bound rest name — the helper records its length and the typed
6912/// witness of its first element. The caller guarantees `trailing` is
6913/// non-empty (it is only built when `list[i + 2..].first()` is `Some`), so
6914/// `trailing[0]` does not panic.
6915///
6916/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6917/// unrepresentable"; a param list with tokens after `&rest <name>` is
6918/// nonsense `MacroParams` cannot hold, so the gate must REJECT it rather
6919/// than truncate to the representable prefix. THEORY.md §II.1 invariant 1 —
6920/// typed entry; the gate rejects malformed DEFINITIONS as readily as
6921/// malformed calls. THEORY.md §VI.1 — generation over composition; this
6922/// closes the `parse_params` walker's last uncovered failure mode, making
6923/// the sibling docs' "every distinct failure mode is a structural variant"
6924/// claim finally true.
6925fn rest_param_trailing_tokens(rest_position: usize, trailing: &[Sexp]) -> LispError {
6926 LispError::RestParamTrailingTokens {
6927 rest_position,
6928 extra: trailing.len(),
6929 first: trailing[0].witness(),
6930 }
6931}
6932
6933/// A `&optional` marker appeared a SECOND time in one param list —
6934/// `(defmacro f (a &optional b &optional c) …)`. The lambda-list has exactly
6935/// ONE optional section (between the required run and the rest); a second
6936/// `&optional` is nonsense `MacroParams` cannot hold (its `optional` field is
6937/// one flat run, not a sequence of sections). Without this gate the parser
6938/// would otherwise treat the second `&optional` as an optional param literally
6939/// NAMED `&optional`, binding call args to a marker symbol — exactly the kind
6940/// of silent misalignment the typed shape exists to forbid.
6941///
6942/// Sibling of `rest_param_trailing_tokens` (the rest-section ordering gate):
6943/// both reject a param list whose marker structure the canonical lambda-list
6944/// ordering cannot represent. `first_position` is the loop index of the
6945/// first `&optional`, `second_position` the second — naming both lets an LSP
6946/// quick-fix point at the redundant marker to delete.
6947///
6948/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6949/// unrepresentable"; a param list with two `&optional` sections is nonsense
6950/// `MacroParams` cannot hold, so the gate must REJECT rather than bind args
6951/// to a marker symbol. THEORY.md §II.1 invariant 1 — typed entry; the gate
6952/// rejects malformed DEFINITIONS as readily as malformed calls.
6953fn optional_marker_repeated(first_position: usize, second_position: usize) -> LispError {
6954 LispError::OptionalMarkerRepeated {
6955 first_position,
6956 second_position,
6957 }
6958}
6959
6960/// An `&optional` section entry that's a `Sexp::List` did NOT match the only
6961/// admissible shape `(NAME DEFAULT)` — exactly two elements with a symbol
6962/// head. This helper builds the structural rejection from the loop position,
6963/// the offending list form (projected through `crate::domain::sexp_witness`
6964/// to carry both `SexpShape::List` and the literal display), and the typed
6965/// `OptionalParamMalformedReason` naming which of the four malformed shapes
6966/// fired (empty list / missing default / extra elements / non-symbol name).
6967///
6968/// Sibling of `optional_marker_repeated` (the `&optional`-section marker
6969/// gate) and `non_symbol_param` (the bare-symbol gate): the three together
6970/// close every distinct typed-entry rejection the optional section can
6971/// emit. The bare-symbol form `&optional x` is still routed through
6972/// `non_symbol_param`'s sibling acceptance path; the list form `&optional
6973/// (x default)` is admitted iff this gate accepts the spec.
6974///
6975/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6976/// unrepresentable"; an `&optional` list spec of any other shape is
6977/// nonsense `MacroParams` cannot hold, so the gate must REJECT rather than
6978/// bind args to a marker symbol or drop the extras silently. THEORY.md
6979/// §II.1 invariant 1 — typed entry; a malformed default-form spec is
6980/// exactly the failure mode the typed-entry gate exists to reject — and
6981/// the gate must reject DEFINITIONS as readily as it rejects CALLS.
6982fn optional_param_malformed(
6983 position: usize,
6984 got: &Sexp,
6985 reason: crate::error::OptionalParamMalformedReason,
6986) -> LispError {
6987 LispError::OptionalParamMalformed {
6988 position,
6989 got: got.witness(),
6990 reason,
6991 }
6992}
6993
6994/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6995/// "(defmacro name (params) body) required" }` triple in
6996/// `macro_def_from` behind ONE named primitive. Sibling of
6997/// `non_symbol_param` and `rest_param_missing_name`: those helpers
6998/// fire INSIDE `parse_params`, AFTER the arity gate has passed; this
6999/// helper fires AT the arity gate itself, BEFORE name / params / body
7000/// validation can run. Together the three close `macro_def_from`'s
7001/// outermost rejection chain — every distinct failure mode the gate
7002/// can emit at the top level becomes a structural variant of
7003/// `LispError`, not a `Compile`-shaped substring.
7004///
7005/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7006/// projected through `MacroDefHead::from_keyword` at the top of
7007/// `macro_def_from`. The helper threads `head` straight into the
7008/// variant's typed `head: MacroDefHead` slot — no `&'static str`
7009/// projection at the helper boundary; the projection through
7010/// `MacroDefHead::keyword()` happens at Display rendering time via
7011/// `MacroDefHead`'s Display impl inside the variant's `#[error(...)]`
7012/// annotation. Same posture as how
7013/// `compiler_spec.rs::compiler_spec_io_err` threads
7014/// `CompilerSpecIoStage` straight into
7015/// `LispError::CompilerSpecIo.stage`. `arity` is `usize` (the length
7016/// of the form including the head element).
7017///
7018/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7019/// inline copy still earns a named primitive once the structural
7020/// shape is named (the test count gives this the fail-before/pass-
7021/// after edge, parallel to how `non_symbol_param` and
7022/// `rest_param_missing_name` were lifted from a single site for the
7023/// structural-completeness payoff). THEORY.md §V.1 — knowable
7024/// platform; the structural variant exposes `head` / `arity` as
7025/// first-class fields so authoring tools (LSP, REPL, `tatara-check`)
7026/// bind to the data shape instead of substring-parsing the rendered
7027/// diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a defmacro
7028/// form with too few elements is exactly the failure mode the typed-
7029/// entry gate exists to reject — and the gate must reject
7030/// DEFINITIONS as readily as it rejects CALLS. THEORY.md §II.1
7031/// invariant 2 — free middle; the arity gate fires inside
7032/// `macro_def_from` BEFORE either expansion strategy runs, so both
7033/// `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
7034/// (substitute) reject the SAME malformed defmacro at the SAME gate.
7035fn defmacro_arity(head: MacroDefHead, arity: usize) -> LispError {
7036 LispError::DefmacroArity { head, arity }
7037}
7038
7039/// Lift the lone `LispError::Compile { form: head.to_string(), message:
7040/// "expected name symbol" }` triple in `macro_def_from` behind ONE
7041/// named primitive. Sibling of `defmacro_arity`, `non_symbol_param`,
7042/// and `rest_param_missing_name`: those helpers fire at the OUTERMOST
7043/// arity gate (`defmacro_arity`) or INSIDE `parse_params`
7044/// (`non_symbol_param`, `rest_param_missing_name`); this helper fires
7045/// AFTER the arity gate has passed but BEFORE `parse_params` runs —
7046/// at the second of three `macro_def_from` rejection points
7047/// (arity → name-symbol → param-list → parse_params).
7048///
7049/// Walking a malformed `(defmacro …)` from the outside in, the gate
7050/// fires:
7051/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
7052/// elements (`(defmacro)`, `(defmacro f)`).
7053/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't a
7054/// symbol (`(defmacro 5 () body)`, `(defmacro :foo () body)`).
7055/// 3. The `expected param list` gate (NEXT LIFT) if list[2] isn't a
7056/// list (`(defmacro f x body)`).
7057/// 4. Inside `parse_params`: `non_symbol_param` and
7058/// `rest_param_missing_name`.
7059///
7060/// After this lift step 2 is structural; the only remaining
7061/// `Compile`-shaped site in `macro_def_from` is step 3 (`expected
7062/// param list`).
7063///
7064/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7065/// projected through `MacroDefHead::from_keyword` at the top of
7066/// `macro_def_from`. The helper threads `head` straight into the
7067/// variant's typed `head: MacroDefHead` slot — same posture as
7068/// `defmacro_arity` after the typed-slot promotion. `got` is `&Sexp`
7069/// at the call site (a borrow into the form's name slot); the helper
7070/// projects through `crate::domain::sexp_witness` — the typed joint
7071/// projection (`SexpShape` + `Sexp::Display`) — so the variant's
7072/// `got: SexpWitness` slot carries BOTH structural shape AND
7073/// renderable literal across the boundary, parallel to how
7074/// `non_symbol_param` and `non_symbol_unquote_target` project their
7075/// `&Sexp` arguments. The fourth consumer of the typed `SexpWitness`
7076/// primitive on the substrate's Sexp-display-source rejection
7077/// surface.
7078///
7079/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7080/// inline copy still earns a named primitive once the structural
7081/// shape is named (the test count gives this the fail-before/pass-
7082/// after edge, parallel to how `defmacro_arity`, `non_symbol_param`,
7083/// and `rest_param_missing_name` were lifted from a single site for
7084/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7085/// platform; the structural variant exposes `head` / `got` as
7086/// first-class fields so authoring tools (LSP, REPL,
7087/// `tatara-check`) bind to the data shape instead of substring-
7088/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7089/// typed entry; a defmacro form whose name slot isn't a symbol is
7090/// exactly the failure mode the typed-entry gate exists to reject —
7091/// and the gate must reject DEFINITIONS as readily as it rejects
7092/// CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7093/// name-symbol gate fires inside `macro_def_from` BEFORE either
7094/// expansion strategy runs, so both `Expander::new()` (bytecode) and
7095/// `Expander::new_substitute_only()` (substitute) reject the SAME
7096/// malformed defmacro at the SAME gate.
7097fn defmacro_non_symbol_name(head: MacroDefHead, got: &Sexp) -> LispError {
7098 LispError::DefmacroNonSymbolName {
7099 head,
7100 got: got.witness(),
7101 }
7102}
7103
7104/// Lift the lone `LispError::Compile { form: head.to_string(), message:
7105/// "expected param list" }` triple in `macro_def_from` behind ONE
7106/// named primitive. Sibling of `defmacro_arity`,
7107/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7108/// `rest_param_missing_name`: those helpers fire at the OUTERMOST
7109/// arity gate (`defmacro_arity`), at the second `macro_def_from`
7110/// rejection point (`defmacro_non_symbol_name`), or INSIDE
7111/// `parse_params` (`non_symbol_param`, `rest_param_missing_name`);
7112/// this helper fires AFTER both the arity gate AND the name-symbol
7113/// gate have passed but BEFORE `parse_params` runs — at the third
7114/// of three `macro_def_from` rejection points
7115/// (arity → name-symbol → param-list → parse_params).
7116///
7117/// Walking a malformed `(defmacro …)` from the outside in, the gate
7118/// fires:
7119/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
7120/// elements (`(defmacro)`, `(defmacro f)`).
7121/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't
7122/// a symbol (`(defmacro 5 () body)`).
7123/// 3. `defmacro_non_list_params(head, &list[2])` if list[2] isn't
7124/// a list (`(defmacro f x body)`, `(defmacro f 5 body)`).
7125/// 4. Inside `parse_params`: `non_symbol_param` and
7126/// `rest_param_missing_name`.
7127///
7128/// After this lift step 3 is structural; every inline
7129/// `LispError::Compile { … }` triple in `macro_def_from` has been
7130/// lifted to a structural variant — the entire `macro_def_from`
7131/// rejection chain is structurally typed for failure modes.
7132///
7133/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7134/// projected through `MacroDefHead::from_keyword` at the top of
7135/// `macro_def_from`. The helper threads `head` straight into the
7136/// variant's typed `head: MacroDefHead` slot — same posture as
7137/// `defmacro_arity` and `defmacro_non_symbol_name` after the
7138/// typed-slot promotion. `got` is `&Sexp` at the call site (a
7139/// borrow into the form's param-list slot); the helper projects
7140/// through `crate::domain::sexp_witness(_)` — the typed joint
7141/// primitive that pairs the offending `Sexp`'s `SexpShape` with its
7142/// `Sexp::Display` projection in ONE owned `SexpWitness` value, so
7143/// authoring tools bind to both the structural shape AND the rendered
7144/// literal across the variant slot. Same posture as `non_symbol_param`,
7145/// `non_symbol_unquote_target`, `splice_outside_list`, and
7146/// `defmacro_non_symbol_name`'s helpers after the typed-witness
7147/// promotion of their `got` slots.
7148///
7149/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7150/// inline copy still earns a named primitive once the structural
7151/// shape is named (the test count gives this the fail-before/pass-
7152/// after edge, parallel to how `defmacro_arity`,
7153/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7154/// `rest_param_missing_name` were lifted from a single site for
7155/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7156/// platform; the structural variant exposes `head` / `got` as
7157/// first-class fields so authoring tools (LSP, REPL,
7158/// `tatara-check`) bind to the data shape instead of substring-
7159/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7160/// typed entry; a defmacro form whose param-list slot isn't a list
7161/// is exactly the failure mode the typed-entry gate exists to
7162/// reject — and the gate must reject DEFINITIONS as readily as it
7163/// rejects CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7164/// param-list gate fires inside `macro_def_from` BEFORE either
7165/// expansion strategy runs, so both `Expander::new()` (bytecode)
7166/// and `Expander::new_substitute_only()` (substitute) reject the
7167/// SAME malformed defmacro at the SAME gate.
7168fn defmacro_non_list_params(head: MacroDefHead, got: &Sexp) -> LispError {
7169 LispError::DefmacroNonListParams {
7170 head,
7171 got: got.witness(),
7172 }
7173}
7174
7175/// Project a `bindings: &HashMap<String, Sexp>` into the `&[&str]` candidate
7176/// set `crate::domain::suggest` wants. Cold path — only allocated when an
7177/// `,name` / `,@name` substitution misses, i.e. when we're already on the
7178/// diagnostic side of the substitute walker.
7179fn bound_names(bindings: &HashMap<String, Sexp>) -> Vec<&str> {
7180 bindings.keys().map(String::as_str).collect()
7181}
7182
7183#[cfg(test)]
7184mod tests {
7185 use super::*;
7186 use crate::reader::read;
7187
7188 fn parse(src: &str) -> Sexp {
7189 read(src).unwrap().into_iter().next().unwrap()
7190 }
7191
7192 #[test]
7193 fn identity_macro() {
7194 let mut e = Expander::new();
7195 let forms = read("(defmacro id (x) `,x) (id 42)").unwrap();
7196 let out = e.expand_program(forms).unwrap();
7197 assert_eq!(out.len(), 1);
7198 assert_eq!(out[0], Sexp::int(42));
7199 }
7200
7201 #[test]
7202 fn wrap_macro_duplicates_arg() {
7203 let mut e = Expander::new();
7204 let forms = read("(defmacro wrap (x) `(list ,x ,x)) (wrap hello)").unwrap();
7205 let out = e.expand_program(forms).unwrap();
7206 assert_eq!(out[0], parse("(list hello hello)"));
7207 }
7208
7209 #[test]
7210 fn rest_param_splices_with_at() {
7211 let mut e = Expander::new();
7212 let forms = read("(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)").unwrap();
7213 let out = e.expand_program(forms).unwrap();
7214 assert_eq!(out[0], parse("(foo a b c)"));
7215 }
7216
7217 #[test]
7218 fn nested_macro_expansion() {
7219 let mut e = Expander::new();
7220 let forms = read(
7221 "(defmacro twice (x) `(list ,x ,x))
7222 (defmacro quad (x) `(twice ,x))
7223 (quad hey)",
7224 )
7225 .unwrap();
7226 let out = e.expand_program(forms).unwrap();
7227 assert_eq!(out[0], parse("(list hey hey)"));
7228 }
7229
7230 #[test]
7231 fn unbound_unquote_errors() {
7232 let mut e = Expander::new();
7233 let forms = read("(defmacro bad (x) `(list ,y)) (bad 1)").unwrap();
7234 assert!(e.expand_program(forms).is_err());
7235 }
7236
7237 #[test]
7238 fn missing_required_arg_errors() {
7239 let mut e = Expander::new();
7240 let forms = read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap();
7241 assert!(e.expand_program(forms).is_err());
7242 }
7243
7244 #[test]
7245 fn defpoint_template_treated_as_defmacro() {
7246 let mut e = Expander::new();
7247 let forms = read(
7248 "(defpoint-template obs (name) `(defpoint ,name :class (Gate Observability)))
7249 (obs grafana)",
7250 )
7251 .unwrap();
7252 let out = e.expand_program(forms).unwrap();
7253 assert_eq!(
7254 out[0],
7255 parse("(defpoint grafana :class (Gate Observability))")
7256 );
7257 }
7258
7259 #[test]
7260 fn defcheck_treated_as_defmacro() {
7261 let mut e = Expander::new();
7262 let forms = read(
7263 "(defcheck pair (a b) `(do (yaml-parses ,a) (yaml-parses ,b)))
7264 (pair \"x.yaml\" \"y.yaml\")",
7265 )
7266 .unwrap();
7267 let out = e.expand_program(forms).unwrap();
7268 assert_eq!(
7269 out[0],
7270 parse("(do (yaml-parses \"x.yaml\") (yaml-parses \"y.yaml\"))")
7271 );
7272 }
7273
7274 #[test]
7275 fn empty_rest_splices_nothing() {
7276 let mut e = Expander::new();
7277 let forms = read("(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)").unwrap();
7278 let out = e.expand_program(forms).unwrap();
7279 assert_eq!(out[0], parse("(list 1)"));
7280 }
7281
7282 #[test]
7283 fn macro_expanded_inside_list() {
7284 // A macro call nested in a list position also expands.
7285 let mut e = Expander::new();
7286 let forms = read("(defmacro two () `(list 1 2)) (outer (two))").unwrap();
7287 let out = e.expand_program(forms).unwrap();
7288 assert_eq!(out[0], parse("(outer (list 1 2))"));
7289 }
7290
7291 // ── Compiled-template bytecode equivalence + speedup ──────────────
7292
7293 #[test]
7294 fn compiled_template_matches_substitute_path() {
7295 // Same program, two expanders with different strategies — outputs must agree.
7296 let src = "
7297 (defmacro wrap (x) `(list ,x ,x))
7298 (defmacro call (f &rest args) `(,f ,@args))
7299 (defmacro twice (x) `(list ,x ,x))
7300 (defmacro quad (x) `(twice ,x))
7301 (wrap hello)
7302 (call foo a b c)
7303 (quad hey)
7304 (outer (wrap deep))
7305 ";
7306 let forms = read(src).unwrap();
7307 let mut fast = Expander::new();
7308 let mut slow = Expander::new_substitute_only();
7309 let out_fast = fast.expand_program(forms.clone()).unwrap();
7310 let out_slow = slow.expand_program(forms).unwrap();
7311 assert_eq!(out_fast, out_slow);
7312 }
7313
7314 #[test]
7315 fn literal_subtree_compiles_to_single_literal_op() {
7316 // Macro body where only one leaf is a substitution — the rest of the
7317 // template is literal, so the compiler should prune large chunks to
7318 // a single Literal op.
7319 let def = MacroDef {
7320 name: "label".into(),
7321 params: MacroParams {
7322 required: vec!["x".into()],
7323 optional: Vec::new(),
7324 rest: None,
7325 },
7326 body: Sexp::Quasiquote(Box::new(parse(
7327 "(observed (at timestamp) (in region) (value ,x) (tags (one two three)))",
7328 ))),
7329 };
7330 let compiled = compile_template(&def).expect("compile");
7331 // The template is ONE list. After compile:
7332 // BeginList,
7333 // Literal((observed (at timestamp) (in region))), // wait — `observed` is a list too
7334 // ...
7335 // EndList
7336 // Point is: many subtrees should be single Literals. We simply count
7337 // that the op stream is SHORTER than the full Sexp size.
7338 let ops_count = compiled.ops.len();
7339 assert!(
7340 ops_count < 15,
7341 "expected pruned op stream, got {ops_count} ops: {:?}",
7342 compiled.ops
7343 );
7344 }
7345
7346 /// The cache-friendly workload the two tests below share: 10 macros, and
7347 /// calls that hit only 10 distinct (macro, args) pairs, so a memo should
7348 /// serve 99.9% of them.
7349 fn cache_friendly_workload(calls: usize) -> String {
7350 let macros = "
7351 (defmacro m1 (a b) `(list ,a ,b))
7352 (defmacro m2 (x) `(if ,x true false))
7353 (defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
7354 (defmacro m4 (f &rest args) `(,f ,@args))
7355 (defmacro m5 (x) `(and ,x (not (not ,x))))
7356 (defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
7357 (defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
7358 (defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
7359 (defmacro m9 (x) `(loop (times 10) (eval ,x)))
7360 (defmacro m10 (f g &rest args) `(,f (,g ,@args)))
7361 ";
7362 let mut call_src = String::with_capacity(calls * 12);
7363 for i in 0..calls {
7364 match i % 10 {
7365 0 => call_src.push_str("(m1 a b)\n"),
7366 1 => call_src.push_str("(m2 true)\n"),
7367 2 => call_src.push_str("(m3 x y z)\n"),
7368 3 => call_src.push_str("(m4 f a b c d e)\n"),
7369 4 => call_src.push_str("(m5 y)\n"),
7370 5 => call_src.push_str("(m6 a b)\n"),
7371 6 => call_src.push_str("(m7 answer)\n"),
7372 7 => call_src.push_str("(m8 p q)\n"),
7373 8 => call_src.push_str("(m9 body)\n"),
7374 _ => call_src.push_str("(m10 f g a b c)\n"),
7375 }
7376 }
7377 let all_src = format!("{macros}\n{call_src}");
7378 all_src
7379 }
7380
7381 /// **All three expansion strategies produce identical output.**
7382 ///
7383 /// The load-bearing invariant — THEORY.md §II.1's free middle. Fully
7384 /// deterministic, so it belongs in the ordinary suite. It used to be
7385 /// welded to the timing assertion below, which meant a benchmark losing a
7386 /// core to a neighbouring test reported *this* as failing, and the
7387 /// agreement property is the one you actually want a red build for.
7388 #[test]
7389 fn expansion_layers_agree_on_output() {
7390 let forms = read(&cache_friendly_workload(10_000)).unwrap();
7391
7392 let out_subst = Expander::new_substitute_only()
7393 .expand_program(forms.clone())
7394 .unwrap();
7395 let out_byte = Expander::new_bytecode_no_cache()
7396 .expand_program(forms.clone())
7397 .unwrap();
7398 let mut byte_cache = Expander::new();
7399 let out_cached = byte_cache.expand_program(forms).unwrap();
7400
7401 assert_eq!(out_subst, out_byte);
7402 assert_eq!(out_subst, out_cached);
7403
7404 // Cache captured the 10 unique (macro, args) pairs (plus some inner
7405 // expansions — macros that expand into calls to other macros).
7406 let cache_size = byte_cache.cache_size();
7407 assert!(
7408 (10..=50).contains(&cache_size),
7409 "expected ~10 unique cache entries, got {cache_size}"
7410 );
7411 }
7412
7413 /// **The expansion strategies, counted.**
7414 ///
7415 /// Each strategy answers every macro application from exactly one layer of
7416 /// `Expander::apply`, so the work each one does is a count, and the count
7417 /// is a fact of the code and the workload rather than of the machine.
7418 ///
7419 /// # Why this is not a wall-clock comparison any more
7420 ///
7421 /// It was one, best-of-5 with a 1.25× gate against substitution and a
7422 /// non-regression bound against bytecode. On shared GitHub runners the
7423 /// substitution ratio measured 0.97× against 1.57× locally on the same
7424 /// commit, and the test blocked a release twice with no code change behind
7425 /// it. Wall time on this path is dominated by reading, walking and
7426 /// allocating the output tree (the memo saves ~1.17× over bytecode in
7427 /// release, ~1.04× in debug), so the margin the gate needed was never
7428 /// larger than the runner's noise.
7429 ///
7430 /// What the gate was for is catching the fast path being disabled or
7431 /// wired wrong, and the counts say that exactly: a cache that never hits,
7432 /// a bytecode dispatch that falls back to substitution, or a cache that
7433 /// stores but is never consulted each move a count by thousands. The
7434 /// timings are still printed for a reader on a quiet machine.
7435 #[test]
7436 fn the_expansion_cache_answers_repeat_calls_without_expanding() {
7437 use std::time::Instant;
7438
7439 const CALLS: u64 = 10_000;
7440 let forms = read(&cache_friendly_workload(usize::try_from(CALLS).unwrap())).unwrap();
7441
7442 let run = |mut e: Expander| -> (ExpansionCounts, usize, std::time::Duration) {
7443 let t = Instant::now();
7444 let out = e.expand_program(forms.clone()).unwrap();
7445 let d = t.elapsed();
7446 std::hint::black_box(out);
7447 (e.expansion_counts(), e.cache_size(), d)
7448 };
7449
7450 let (subst, _, t_subst) = run(Expander::new_substitute_only());
7451 let (byte, _, t_byte) = run(Expander::new_bytecode_no_cache());
7452 let (cached, entries, t_cached) = run(Expander::new());
7453 eprintln!(
7454 "\n=== macroexpand: {CALLS} calls × 10 unique (macro, args) pairs ===\n\
7455 substitute only : {subst:?} in {t_subst:?}\n\
7456 bytecode no cache : {byte:?} in {t_byte:?}\n\
7457 bytecode + cache : {cached:?} in {t_cached:?}\n"
7458 );
7459
7460 let applications = |c: &ExpansionCounts| c.cache_hits + c.compiled + c.substituted;
7461 let total = applications(&subst);
7462 assert!(
7463 total >= CALLS,
7464 "every top-level call is at least one application, got {total}"
7465 );
7466 assert_eq!(
7467 applications(&byte),
7468 total,
7469 "strategies apply the same calls"
7470 );
7471 assert_eq!(
7472 applications(&cached),
7473 total,
7474 "strategies apply the same calls"
7475 );
7476
7477 assert_eq!(
7478 subst,
7479 ExpansionCounts {
7480 substituted: total,
7481 ..Default::default()
7482 },
7483 "substitute-only must never touch the bytecode or the memo"
7484 );
7485 assert_eq!(
7486 byte,
7487 ExpansionCounts {
7488 compiled: total,
7489 ..Default::default()
7490 },
7491 "bytecode-no-cache must run every application through the bytecode"
7492 );
7493 assert_eq!(
7494 cached.substituted, 0,
7495 "a compiled macro fell back to substitution"
7496 );
7497 assert!(
7498 cached.compiled <= 50,
7499 "the memo should leave only the ~10 unique pairs (plus inner expansions) \
7500 to the bytecode, got {} of {total}",
7501 cached.compiled
7502 );
7503 assert_eq!(
7504 cached.compiled,
7505 u64::try_from(entries).unwrap(),
7506 "every bytecode run on a fresh memo is a miss that fills one entry"
7507 );
7508 }
7509
7510 #[test]
7511 fn cache_respects_arg_changes() {
7512 // Cache must not return stale results when args differ.
7513 let src = "
7514 (defmacro wrap (x) `(list ,x ,x))
7515 (wrap a)
7516 (wrap b)
7517 (wrap a) ;; same as first — cached hit
7518 ";
7519 let mut e = Expander::new();
7520 let out = e.expand_program(read(src).unwrap()).unwrap();
7521 assert_eq!(out.len(), 3);
7522 assert_eq!(out[0], parse("(list a a)"));
7523 assert_eq!(out[1], parse("(list b b)"));
7524 assert_eq!(out[2], parse("(list a a)"));
7525 // Two distinct args → 2 cache entries.
7526 assert_eq!(e.cache_size(), 2);
7527 }
7528
7529 #[test]
7530 fn clear_cache_empties_memo() {
7531 let mut e = Expander::new();
7532 let out = e
7533 .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7534 .unwrap();
7535 assert_eq!(out.len(), 2);
7536 assert_eq!(e.cache_size(), 2);
7537 e.clear_cache();
7538 assert_eq!(e.cache_size(), 0);
7539 }
7540
7541 // ── Unbound template-var: structural variant + did-you-mean hint ──
7542
7543 /// Helper for the unbound-template-var tests — pins the variant shape
7544 /// and carries any error context up to the assert site for legibility.
7545 fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7546 match err {
7547 LispError::UnboundTemplateVar { prefix, name, hint } => {
7548 (*prefix, name.as_str(), hint.as_deref())
7549 }
7550 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7551 }
7552 }
7553
7554 #[test]
7555 fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7556 // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7557 // Path: compile_node Unquote (the bytecode-template compile, default
7558 // expander).
7559 let mut e = Expander::new();
7560 let err = e
7561 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7562 .expect_err("unbound template var must error");
7563 let (prefix, name, hint) = unbound_var(&err);
7564 assert_eq!(prefix, UnquoteForm::Unquote);
7565 assert_eq!(name, "xs");
7566 assert_eq!(hint, Some("x"));
7567 }
7568
7569 #[test]
7570 fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7571 // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7572 // hints `,@args`. Path: compile_node UnquoteSplice.
7573 let mut e = Expander::new();
7574 let err = e
7575 .expand_program(
7576 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7577 )
7578 .expect_err("unbound splice must error");
7579 let (prefix, name, hint) = unbound_var(&err);
7580 assert_eq!(prefix, UnquoteForm::Splice);
7581 assert_eq!(name, "argz");
7582 assert_eq!(hint, Some("args"));
7583 }
7584
7585 #[test]
7586 fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7587 // Same shape but routed through the substitute-only expander — proves
7588 // the substitute path emits the same variant as the bytecode path.
7589 let mut e = Expander::new_substitute_only();
7590 let err = e
7591 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7592 .expect_err("substitute unbound must error");
7593 let (prefix, name, hint) = unbound_var(&err);
7594 assert_eq!(prefix, UnquoteForm::Unquote);
7595 assert_eq!(name, "xs");
7596 assert_eq!(hint, Some("x"));
7597 }
7598
7599 #[test]
7600 fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7601 // The substitute path's UnquoteSplice branch fires for splices that
7602 // appear inside a list during the recursive walk. `,@argz` against
7603 // `[args]` hints `,@args`.
7604 let mut e = Expander::new_substitute_only();
7605 let err = e
7606 .expand_program(
7607 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7608 )
7609 .expect_err("substitute splice unbound must error");
7610 let (prefix, name, hint) = unbound_var(&err);
7611 assert_eq!(prefix, UnquoteForm::Splice);
7612 assert_eq!(name, "argz");
7613 assert_eq!(hint, Some("args"));
7614 }
7615
7616 #[test]
7617 fn unbound_template_var_omits_hint_when_no_close_match() {
7618 // `,wholly-unrelated` against `[x]` — far past the bound, so no
7619 // hint. Negative control: a wrong hint is worse than no hint, so
7620 // the slot must stay empty when the substrate isn't confident.
7621 let mut e = Expander::new();
7622 let err = e
7623 .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7624 .expect_err("unrelated unbound must error");
7625 let (prefix, name, hint) = unbound_var(&err);
7626 assert_eq!(prefix, UnquoteForm::Unquote);
7627 assert_eq!(name, "wholly-unrelated");
7628 assert_eq!(hint, None);
7629 }
7630
7631 #[test]
7632 fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7633 // End-to-end through the Display impl — pins the rendered diagnostic
7634 // a downstream tool sees today (REPL, tatara-check). Hint stays
7635 // additive: the legacy `"unbound"` substring still appears, so any
7636 // assertion that pattern-matches on it keeps passing.
7637 let mut e = Expander::new();
7638 let err = e
7639 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7640 .expect_err("unbound must error");
7641 let msg = format!("{err}");
7642 assert!(
7643 msg.contains("did you mean ,x?"),
7644 "expected hint suffix in message, got: {msg}"
7645 );
7646 assert!(
7647 msg.contains("unbound"),
7648 "expected legacy `unbound` substring in message, got: {msg}"
7649 );
7650 assert!(
7651 msg.contains(",xs"),
7652 "expected the offending form in message, got: {msg}"
7653 );
7654 }
7655
7656 #[test]
7657 fn unbound_template_var_position_is_none_today() {
7658 // Negative control for the future-spans move: until `Sexp` carries
7659 // source positions, `position()` returns `None` for this variant.
7660 let mut e = Expander::new();
7661 let err = e
7662 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7663 .expect_err("unbound must error");
7664 assert_eq!(err.position(), None);
7665 }
7666
7667 // ── Non-symbol unquote target: structural variant ─────────────────
7668
7669 /// Helper for the non-symbol-unquote-target tests — pins the variant
7670 /// shape and carries any error context up to the assert site for
7671 /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7672 /// returns the `display` projection of the typed `SexpWitness` so the
7673 /// existing call sites stay byte-for-byte comparable to the legacy
7674 /// `got: String` shape.
7675 fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7676 match err {
7677 LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7678 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7679 }
7680 }
7681
7682 #[test]
7683 fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7684 // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7685 // compile_node Unquote (the bytecode-template compile, default
7686 // expander). Pins variant identity AND prefix AND the offending
7687 // literal so a regression that re-inlines the legacy
7688 // `LispError::Compile` shape fails-loudly here.
7689 let mut e = Expander::new();
7690 let err = e
7691 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7692 .expect_err("non-symbol unquote target must error");
7693 let (prefix, got) = non_symbol_target(&err);
7694 assert_eq!(prefix, UnquoteForm::Unquote);
7695 assert_eq!(got, "(list 1 2)");
7696 }
7697
7698 #[test]
7699 fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7700 // `,@5` — the inner is an int atom, not a symbol. Path:
7701 // compile_node UnquoteSplice. The integer literal round-trips
7702 // through the variant's `got` slot via `Sexp::Display`.
7703 let mut e = Expander::new();
7704 let err = e
7705 .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7706 .expect_err("non-symbol splice target must error");
7707 let (prefix, got) = non_symbol_target(&err);
7708 assert_eq!(prefix, UnquoteForm::Splice);
7709 assert_eq!(got, "5");
7710 }
7711
7712 #[test]
7713 fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7714 // Same shape as the bytecode path but routed through the
7715 // substitute-only expander — proves the substitute path emits the
7716 // same variant as the compile_node path. Pins that the lift is
7717 // path-uniform.
7718 let mut e = Expander::new_substitute_only();
7719 let err = e
7720 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7721 .expect_err("substitute non-symbol target must error");
7722 let (prefix, got) = non_symbol_target(&err);
7723 assert_eq!(prefix, UnquoteForm::Unquote);
7724 assert_eq!(got, "(list 1 2)");
7725 }
7726
7727 #[test]
7728 fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7729 // The substitute path's UnquoteSplice-inside-list branch fires for
7730 // splices that appear inside a list during the recursive walk.
7731 // `,@(list 1 2)` inside the body — the inner is a literal list, not
7732 // a symbol — emits the same variant as the compile_node path.
7733 let mut e = Expander::new_substitute_only();
7734 let err = e
7735 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7736 .expect_err("substitute non-symbol splice must error");
7737 let (prefix, got) = non_symbol_target(&err);
7738 assert_eq!(prefix, UnquoteForm::Splice);
7739 assert_eq!(got, "(list 1 2)");
7740 }
7741
7742 #[test]
7743 fn non_symbol_unquote_target_position_is_none_today() {
7744 // Negative control for the future-spans move: until `Sexp` carries
7745 // source positions, `position()` returns `None` for this variant.
7746 // A future run that gives `Sexp` source spans adds `pos:
7747 // Option<usize>` to ONE place; this test gives that change a
7748 // deliberate fail-before/pass-after delta.
7749 let mut e = Expander::new();
7750 let err = e
7751 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7752 .expect_err("non-symbol target must error");
7753 assert_eq!(err.position(), None);
7754 }
7755
7756 // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7757 //
7758 // The `unquote_target_symbol(inner, form)?` primitive lifts the
7759 // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7760 // form, inner))?` pattern that previously appeared at four call
7761 // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7762 // + `substitute` list-inner UnquoteSplice) behind ONE named
7763 // primitive. The tests below pin: (a) the Ok-arm borrows the
7764 // symbol name from `inner` for both UnquoteForm variants; (b) the
7765 // Err-arm routes through `non_symbol_unquote_target` and emits the
7766 // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7767 // the typed `SexpWitness` (joint shape + display identity) for the
7768 // closed set of reachable non-symbol shapes (int / keyword / list /
7769 // nil); (c) the helper is path-uniform — the same Ok / Err
7770 // contracts hold regardless of which call site invokes it. A
7771 // regression that re-inlines the gate-1 projection at any of the
7772 // four call sites can no longer drift independent of the others —
7773 // the helper IS the gate.
7774
7775 #[test]
7776 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7777 // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7778 // under `UnquoteForm::Unquote` projects through `as_symbol()`
7779 // to the borrowed `&str`. The returned slice's lifetime is
7780 // tied to `inner` so the caller can feed it directly into
7781 // `params.iter().position(...)` (`compile_node`) or
7782 // `bindings.get(...)` (`substitute`) without an intermediate
7783 // allocation. Fail-before/pass-after: this assert is meaningless
7784 // pre-lift because the helper does not exist; post-lift it
7785 // pins the typed gate-1 contract at the named primitive.
7786 let inner = Sexp::symbol("xs");
7787 let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7788 .expect("symbol inner must project to Ok");
7789 assert_eq!(name, "xs");
7790 }
7791
7792 #[test]
7793 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7794 // Sibling positive control: `UnquoteForm::Splice` shares the
7795 // gate-1 contract with `Unquote`. The helper is path-uniform
7796 // across both syntactic markers — a regression that bifurcates
7797 // the two arms (e.g., accepting non-symbols for `,@X` but not
7798 // `,X`) fails-loudly here. Pins that the closed-set
7799 // `UnquoteForm` enum's two variants share ONE projection
7800 // posture across the gate-1 boundary.
7801 let inner = Sexp::symbol("rest");
7802 let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7803 .expect("symbol inner must project to Ok under Splice");
7804 assert_eq!(name, "rest");
7805 }
7806
7807 #[test]
7808 fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7809 // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7810 // NOT a symbol — the gate-1 projection fires and routes through
7811 // `non_symbol_unquote_target` to the structural
7812 // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7813 // identity AND the typed `SexpWitness` joint identity (shape +
7814 // display literal): a regression that drops the witness shape
7815 // or display fails-loudly here.
7816 let inner = Sexp::int(5);
7817 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7818 .expect_err("int inner must error at gate-1");
7819 match err {
7820 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7821 assert_eq!(prefix, UnquoteForm::Unquote);
7822 assert_eq!(got.shape, crate::error::SexpShape::Int);
7823 assert_eq!(got.display, "5");
7824 }
7825 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7826 }
7827 }
7828
7829 #[test]
7830 fn unquote_target_symbol_rejects_list_inner_under_splice() {
7831 // Sibling negative control: `inner = (list 1 2)` is a list, not
7832 // a symbol — the gate-1 projection fires AND routes through
7833 // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7834 // both the variant identity AND the typed witness's joint
7835 // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7836 // future shape drift fails-loudly. Sibling of the Int / Unquote
7837 // pin: closes the gate-1 contract across the closed-set
7838 // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7839 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7840 let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7841 .expect_err("list inner must error at gate-1");
7842 match err {
7843 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7844 assert_eq!(prefix, UnquoteForm::Splice);
7845 assert_eq!(got.shape, crate::error::SexpShape::List);
7846 assert_eq!(got.display, "(list 1 2)");
7847 }
7848 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7849 }
7850 }
7851
7852 #[test]
7853 fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7854 // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7855 // The gate-1 projection rejects keywords AS WELL as ints and
7856 // lists — closes the closed-set of "non-symbol shapes the gate
7857 // rejects" across one more reachable variant. The typed witness
7858 // carries `SexpShape::Keyword` + display `:foo` jointly so
7859 // authoring tools (REPL, LSP) bind on the structural shape
7860 // directly.
7861 let inner = Sexp::keyword("foo");
7862 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7863 .expect_err("keyword inner must error at gate-1");
7864 match err {
7865 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7866 assert_eq!(prefix, UnquoteForm::Unquote);
7867 assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7868 assert_eq!(got.display, ":foo");
7869 }
7870 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7871 }
7872 }
7873
7874 #[test]
7875 fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7876 // Path-uniformity pin: end-to-end through ALL FOUR call sites
7877 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7878 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7879 // every non-symbol unquote target now routes through the SAME
7880 // `unquote_target_symbol(inner, form)?` helper. The four
7881 // end-to-end expansions below all reject with the SAME variant
7882 // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7883 // path-uniform rejection contract `non_symbol_unquote_target`'s
7884 // prior lift established (and that drove the bytecode-vs-
7885 // substitute reunification in 0e9c… and successors). A
7886 // regression that re-inlines the gate-1 projection at one of
7887 // the four sites can drift the four call sites independent of
7888 // each other — this test would catch that drift.
7889 let cases: &[(&str, UnquoteForm)] = &[
7890 // compile_node Unquote (bytecode-path)
7891 ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7892 // compile_node UnquoteSplice (bytecode-path)
7893 ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7894 ];
7895 for (src, expected_form) in cases {
7896 let mut e = Expander::new();
7897 let err = e
7898 .expand_program(read(src).unwrap())
7899 .expect_err("non-symbol unquote target must error end-to-end");
7900 match err {
7901 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7902 assert_eq!(prefix, *expected_form, "for src: {src}");
7903 }
7904 other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7905 }
7906 }
7907 // substitute Unquote (substitute-only path) — sibling pin to
7908 // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7909 let mut e_subst = Expander::new_substitute_only();
7910 let err = e_subst
7911 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7912 .expect_err("substitute Unquote must error end-to-end");
7913 assert!(
7914 matches!(
7915 err,
7916 LispError::NonSymbolUnquoteTarget {
7917 prefix: UnquoteForm::Unquote,
7918 ..
7919 }
7920 ),
7921 "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7922 );
7923 // substitute list-inner UnquoteSplice (substitute-only path) —
7924 // sibling pin to
7925 // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7926 let mut e_subst2 = Expander::new_substitute_only();
7927 let err = e_subst2
7928 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7929 .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7930 assert!(
7931 matches!(
7932 err,
7933 LispError::NonSymbolUnquoteTarget {
7934 prefix: UnquoteForm::Splice,
7935 ..
7936 }
7937 ),
7938 "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7939 );
7940 }
7941
7942 // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7943 // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7944 // (bytecode-template compile path) and `resolve_binding`
7945 // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7946 // unbound_template_var(FORM, name, candidates))` projections at
7947 // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7948 // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7949 // projection under both `UnquoteForm` variants — the helper returns
7950 // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7951 // for in-scope names; (b) Err-arm projection routes through
7952 // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7953 // variant with the correct `prefix` AND the suggest-driven `hint`;
7954 // (c) the helpers are path-uniform — both compile-path arms share
7955 // ONE `resolve_param_index`; both substitute-path arms share ONE
7956 // `resolve_binding`. A regression that re-inlines the gate-2
7957 // projection at any of the four call sites can no longer drift
7958 // independent of the others — the two helpers ARE the gate.
7959
7960 #[test]
7961 fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7962 // Positive control for the Ok-arm: `name = "x"` against
7963 // `params = ["a", "x", "rest"]` projects through
7964 // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7965 // the helper unwraps to `Ok(1)`. The returned index feeds
7966 // directly into `TemplateOp::Subst(idx)` at the compile site.
7967 let params = ["a", "x", "rest"];
7968 let idx = resolve_param_index("x", ¶ms, UnquoteForm::Unquote)
7969 .expect("bound name must project to Ok at gate-2");
7970 assert_eq!(idx, 1);
7971 }
7972
7973 #[test]
7974 fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7975 // Sibling positive control: `UnquoteForm::Splice` shares the
7976 // gate-2 contract with `Unquote`. The helper is path-uniform
7977 // across both syntactic markers on the compile path — a
7978 // regression that bifurcates the two arms fails-loudly here.
7979 let params = ["a", "x", "rest"];
7980 let idx = resolve_param_index("rest", ¶ms, UnquoteForm::Splice)
7981 .expect("bound name must project to Ok at gate-2 under Splice");
7982 assert_eq!(idx, 2);
7983 }
7984
7985 #[test]
7986 fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7987 // Negative control for the Err-arm: `name = "xs"` against
7988 // `params = ["x"]` — distance 1, bound 1 — routes through
7989 // `unbound_template_var` to the structural
7990 // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7991 // Pin the variant identity AND the prefix AND the suggest-driven
7992 // hint: a regression that drops the suggestion fails-loudly here.
7993 let params = ["x"];
7994 let err = resolve_param_index("xs", ¶ms, UnquoteForm::Unquote)
7995 .expect_err("unbound name must error at gate-2");
7996 match err {
7997 LispError::UnboundTemplateVar { prefix, name, hint } => {
7998 assert_eq!(prefix, UnquoteForm::Unquote);
7999 assert_eq!(name, "xs");
8000 assert_eq!(hint.as_deref(), Some("x"));
8001 }
8002 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8003 }
8004 }
8005
8006 #[test]
8007 fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
8008 // Sibling negative control: `name = "wholly-unrelated"` against
8009 // `params = ["x"]` — past the bounded edit distance, so no hint.
8010 // Pin that the suggest-driven hint stays empty under Splice when
8011 // the substrate isn't confident — a wrong hint is worse than no
8012 // hint. Closes the closed-set product of {hint, no-hint} ×
8013 // {Unquote, Splice} on the compile-path gate-2.
8014 let params = ["x"];
8015 let err = resolve_param_index("wholly-unrelated", ¶ms, UnquoteForm::Splice)
8016 .expect_err("unrelated unbound must error at gate-2");
8017 match err {
8018 LispError::UnboundTemplateVar { prefix, name, hint } => {
8019 assert_eq!(prefix, UnquoteForm::Splice);
8020 assert_eq!(name, "wholly-unrelated");
8021 assert_eq!(hint, None);
8022 }
8023 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8024 }
8025 }
8026
8027 #[test]
8028 fn resolve_binding_returns_value_for_bound_name_under_unquote() {
8029 // Positive control for the substitute-path Ok-arm: `name = "x"`
8030 // against a bindings map `{x: 42, y: "hi"}` projects through
8031 // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
8032 // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
8033 // `&Sexp` borrows from the bindings map — the top-level
8034 // `Sexp::Unquote(_)` substitute caller adds a single
8035 // `.cloned()` to satisfy its owned-`Sexp` return obligation.
8036 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8037 bindings.insert("x".to_string(), Sexp::int(42));
8038 bindings.insert("y".to_string(), Sexp::string("hi"));
8039 let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
8040 .expect("bound name must project to Ok at gate-2 (substitute)");
8041 assert_eq!(val, &Sexp::int(42));
8042 }
8043
8044 #[test]
8045 fn resolve_binding_returns_value_for_bound_name_under_splice() {
8046 // Sibling positive control: `UnquoteForm::Splice` shares the
8047 // gate-2 contract with `Unquote` on the substitute path too.
8048 // The bound value is a `Sexp::List` because the splice arm's
8049 // caller match expression expects `Sexp::List(items)` — but
8050 // the helper itself doesn't inspect the value's shape; it
8051 // just hands back the borrow. A regression that gate-checks
8052 // the value's shape inside `resolve_binding` (instead of at
8053 // the caller match arm) fails-loudly here.
8054 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8055 bindings.insert(
8056 "args".to_string(),
8057 Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
8058 );
8059 let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
8060 .expect("bound name must project to Ok at gate-2 under Splice");
8061 assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8062 }
8063
8064 #[test]
8065 fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
8066 // Negative control for the substitute-path Err-arm: `name =
8067 // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
8068 // routes through `unbound_template_var` to the structural
8069 // `LispError::UnboundTemplateVar` variant with `hint =
8070 // Some("x")`. The candidate set is drawn from
8071 // `bound_names(bindings)` — the live bindings' keys, never a
8072 // stale snapshot.
8073 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8074 bindings.insert("x".to_string(), Sexp::int(1));
8075 let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
8076 .expect_err("unbound name must error at gate-2 (substitute)");
8077 match err {
8078 LispError::UnboundTemplateVar { prefix, name, hint } => {
8079 assert_eq!(prefix, UnquoteForm::Unquote);
8080 assert_eq!(name, "xs");
8081 assert_eq!(hint.as_deref(), Some("x"));
8082 }
8083 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8084 }
8085 }
8086
8087 #[test]
8088 fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
8089 // Sibling negative control on the substitute path: past-bound
8090 // distance → no hint. Closes the closed-set product of
8091 // {hint, no-hint} × {Unquote, Splice} on the substitute-path
8092 // gate-2.
8093 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8094 bindings.insert("args".to_string(), Sexp::Nil);
8095 let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
8096 .expect_err("unrelated unbound must error at gate-2");
8097 match err {
8098 LispError::UnboundTemplateVar { prefix, name, hint } => {
8099 assert_eq!(prefix, UnquoteForm::Splice);
8100 assert_eq!(name, "wholly-unrelated");
8101 assert_eq!(hint, None);
8102 }
8103 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8104 }
8105 }
8106
8107 #[test]
8108 fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
8109 // Path-uniformity pin: end-to-end through ALL FOUR call sites
8110 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
8111 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
8112 // every unbound-template-var rejection now routes through one
8113 // of the TWO `resolve_param_index` / `resolve_binding` helpers
8114 // — `Expander::new()` runs the compile path, so its two arms
8115 // share `resolve_param_index`; `Expander::new_substitute_only()`
8116 // runs the substitute path, so its two arms share
8117 // `resolve_binding`. The four end-to-end expansions below all
8118 // reject with the SAME variant (`UnboundTemplateVar`) with the
8119 // expected `prefix` — pins that the lift preserves the
8120 // path-uniform rejection contract `unbound_template_var`'s
8121 // prior naming established. A regression that re-inlines the
8122 // gate-2 projection at one of the four sites can drift the
8123 // four call sites independent of each other — this test would
8124 // catch that drift.
8125 struct Case {
8126 src: &'static str,
8127 expander: fn() -> Expander,
8128 expected_form: UnquoteForm,
8129 }
8130 let cases: &[Case] = &[
8131 // compile_node Unquote (bytecode path) — uses resolve_param_index
8132 Case {
8133 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8134 expander: Expander::new,
8135 expected_form: UnquoteForm::Unquote,
8136 },
8137 // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
8138 Case {
8139 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8140 expander: Expander::new,
8141 expected_form: UnquoteForm::Splice,
8142 },
8143 // substitute Unquote (substitute-only path) — uses resolve_binding
8144 Case {
8145 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8146 expander: Expander::new_substitute_only,
8147 expected_form: UnquoteForm::Unquote,
8148 },
8149 // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8150 Case {
8151 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8152 expander: Expander::new_substitute_only,
8153 expected_form: UnquoteForm::Splice,
8154 },
8155 ];
8156 for case in cases {
8157 let mut e = (case.expander)();
8158 let err = e
8159 .expand_program(read(case.src).unwrap())
8160 .expect_err("unbound template var must error end-to-end");
8161 match err {
8162 LispError::UnboundTemplateVar { prefix, .. } => {
8163 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8164 }
8165 other => panic!(
8166 "expected UnboundTemplateVar for {}, got: {other:?}",
8167 case.src
8168 ),
8169 }
8170 }
8171 }
8172
8173 // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8174
8175 #[test]
8176 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8177 // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8178 // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8179 // returns its index. The combined helper returns the gate-2
8180 // result directly — pins that gate-1's Ok-arm threads into
8181 // gate-2's input without intermediate state.
8182 let inner = Sexp::symbol("x");
8183 let params = ["x", "y"];
8184 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8185 .expect("symbol-inner bound at index 0 must resolve");
8186 assert_eq!(idx, 0);
8187 }
8188
8189 #[test]
8190 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8191 // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8192 // marker doesn't change the projection — only the rejection
8193 // path's `prefix` slot.
8194 let inner = Sexp::symbol("args");
8195 let params = ["f", "args"];
8196 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8197 .expect("symbol-inner bound at index 1 must resolve");
8198 assert_eq!(idx, 1);
8199 }
8200
8201 #[test]
8202 fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8203 // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8204 // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8205 // BEFORE gate-2's param lookup runs. Pins that the composition's
8206 // sequencing is gate-1-then-gate-2: a regression that runs
8207 // gate-2 first would attempt to look up "(list 1 2)" as a param
8208 // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8209 // 2)", ... }` — a confusing diagnostic that would substring-grep
8210 // "unbound" instead of "expected symbol". This test pins the
8211 // structural floor: a non-symbol inner is rejected as a non-
8212 // symbol, never re-treated as a bound-name lookup key.
8213 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8214 let params = ["x"];
8215 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8216 .expect_err("non-symbol inner must reject at gate-1");
8217 match err {
8218 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8219 assert_eq!(prefix, UnquoteForm::Unquote);
8220 assert_eq!(got.display, "(list 1 2)");
8221 }
8222 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8223 }
8224 }
8225
8226 #[test]
8227 fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8228 // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8229 // symbol (gate-1 passes) but the name isn't in `params`, so
8230 // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8231 // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8232 // that the marker `prefix` is threaded into gate-2's rejection
8233 // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8234 // at the composition boundary would fail this Splice-marker
8235 // assertion).
8236 let inner = Sexp::symbol("missing");
8237 let params = ["x", "y"];
8238 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8239 .expect_err("unbound symbol must reject at gate-2");
8240 match err {
8241 LispError::UnboundTemplateVar { prefix, name, .. } => {
8242 assert_eq!(prefix, UnquoteForm::Splice);
8243 assert_eq!(name, "missing");
8244 }
8245 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8246 }
8247 }
8248
8249 #[test]
8250 fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8251 // Substitute-path sibling of `resolve_unquote_in_params_returns_
8252 // index_for_symbol_inner_under_unquote`. The combined helper
8253 // composes gate-1 (project inner to symbol) THEN gate-2 (look
8254 // up name in bindings). The returned `&Sexp` borrows from
8255 // `bindings` so the list-inner caller threads it straight into
8256 // the splice-expansion match without an intermediate allocation.
8257 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8258 bindings.insert("v".to_string(), Sexp::int(42));
8259 let inner = Sexp::symbol("v");
8260 let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8261 .expect("symbol-inner bound to 42 must resolve");
8262 assert_eq!(val, &Sexp::int(42));
8263 }
8264
8265 #[test]
8266 fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8267 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8268 // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8269 // sequencing on the substitute path: a non-symbol inner is
8270 // rejected as a non-symbol BEFORE the bindings map is consulted.
8271 let bindings: HashMap<String, Sexp> = HashMap::new();
8272 let inner = Sexp::int(5);
8273 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8274 .expect_err("non-symbol inner must reject at gate-1");
8275 match err {
8276 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8277 assert_eq!(prefix, UnquoteForm::Splice);
8278 assert_eq!(got.display, "5");
8279 }
8280 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8281 }
8282 }
8283
8284 #[test]
8285 fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8286 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8287 // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8288 // substitute path with the marker threaded into the rejection's
8289 // `prefix` slot.
8290 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8291 bindings.insert("known".to_string(), Sexp::Nil);
8292 let inner = Sexp::symbol("missing");
8293 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8294 .expect_err("unbound symbol must reject at gate-2");
8295 match err {
8296 LispError::UnboundTemplateVar { prefix, name, .. } => {
8297 assert_eq!(prefix, UnquoteForm::Unquote);
8298 assert_eq!(name, "missing");
8299 }
8300 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8301 }
8302 }
8303
8304 #[test]
8305 fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8306 // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8307 // composition (compile_node Unquote, compile_node UnquoteSplice,
8308 // substitute Unquote, substitute list-inner UnquoteSplice) now
8309 // share TWO composed primitives — `resolve_unquote_in_params`
8310 // on the bytecode path, `resolve_unquote_in_bindings` on the
8311 // substitute path — and ALL four reject gate-1 failures (non-
8312 // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8313 // variant carrying the expected `prefix` slot. Before the lift,
8314 // each site threaded `form` twice through two helper calls; this
8315 // test pins that the lift preserves the gate's rejection-shape
8316 // identity across all four sites for a non-symbol inner — i.e.
8317 // gate-1 fires identically across both expansion strategies.
8318 struct Case {
8319 src: &'static str,
8320 expander: fn() -> Expander,
8321 expected_form: UnquoteForm,
8322 }
8323 let cases: &[Case] = &[
8324 Case {
8325 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8326 expander: Expander::new,
8327 expected_form: UnquoteForm::Unquote,
8328 },
8329 Case {
8330 src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8331 expander: Expander::new,
8332 expected_form: UnquoteForm::Splice,
8333 },
8334 Case {
8335 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8336 expander: Expander::new_substitute_only,
8337 expected_form: UnquoteForm::Unquote,
8338 },
8339 Case {
8340 src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8341 expander: Expander::new_substitute_only,
8342 expected_form: UnquoteForm::Splice,
8343 },
8344 ];
8345 for case in cases {
8346 let mut e = (case.expander)();
8347 let err = e
8348 .expand_program(read(case.src).unwrap())
8349 .expect_err("non-symbol inner must error end-to-end");
8350 match err {
8351 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8352 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8353 }
8354 other => panic!(
8355 "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8356 case.src
8357 ),
8358 }
8359 }
8360 }
8361
8362 // ── Splice outside list: structural variant + path-uniform rejection ─
8363
8364 /// Helper for the splice-outside-list tests — pins the variant shape
8365 /// and carries the offending `got` field up to the assert site for
8366 /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8367 fn splice_outside_list_got(err: &LispError) -> &str {
8368 match err {
8369 LispError::SpliceOutsideList { got } => got.display.as_str(),
8370 other => panic!("expected SpliceOutsideList, got: {other:?}"),
8371 }
8372 }
8373
8374 #[test]
8375 fn splice_outside_list_in_substitute_emits_structural_variant() {
8376 // `,@xs` at the body's top level — there is no containing list to
8377 // splice into. Path: substitute (the `Expander::new_substitute_only`
8378 // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8379 // identity AND the offending inner so a regression that re-inlines
8380 // the legacy `LispError::Compile` shape fails-loudly here.
8381 let mut e = Expander::new_substitute_only();
8382 let err = e
8383 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8384 .expect_err("splice outside list must error");
8385 assert_eq!(splice_outside_list_got(&err), "xs");
8386 }
8387
8388 #[test]
8389 fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8390 // `,@(list 1 2)` at the body's top level — the inner is a literal
8391 // list, not a symbol. The structural variant carries the inner's
8392 // Sexp::Display projection so the operator sees the literal value
8393 // they wrote in the parenthetical.
8394 let mut e = Expander::new_substitute_only();
8395 let err = e
8396 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8397 .expect_err("splice outside list must error");
8398 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8399 }
8400
8401 #[test]
8402 fn splice_outside_list_in_compile_template_emits_structural_variant() {
8403 // The bytecode path's `compile_template` gate now rejects top-level
8404 // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8405 // where the bytecode interpreter's outermost stack frame absorbed
8406 // the splice. Pins that the bytecode path emits the SAME structural
8407 // variant the substitute path emits — `,@-outside-list` is rejected
8408 // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8409 // → `compile_template` gate.
8410 let mut e = Expander::new();
8411 let err = e
8412 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8413 .expect_err("compile-template splice outside list must error");
8414 assert_eq!(splice_outside_list_got(&err), "xs");
8415 }
8416
8417 #[test]
8418 fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8419 // Same shape as the substitute test but routed through the bytecode
8420 // path's `compile_template` gate. Proves the gate fires on a
8421 // non-symbol inner too — the slot's contents are irrelevant; only
8422 // the syntactic position matters.
8423 let mut e = Expander::new();
8424 let err = e
8425 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8426 .expect_err("compile-template splice outside list must error");
8427 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8428 }
8429
8430 #[test]
8431 fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8432 // Path-uniform rejection: the SAME source emits the SAME structural
8433 // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8434 // strategies. Before the `compile_template` gate, the bytecode path
8435 // silently produced a list while the substitute path errored —
8436 // expansion strategy was observable. After the gate, the gate is
8437 // strategy-uniform, so a macro that registers under one strategy
8438 // registers under the other.
8439 let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8440 let mut subst = Expander::new_substitute_only();
8441 let mut bytecode = Expander::new();
8442 let err_subst = subst
8443 .expand_program(read(src).unwrap())
8444 .expect_err("substitute must error");
8445 let err_byte = bytecode
8446 .expand_program(read(src).unwrap())
8447 .expect_err("bytecode must error");
8448 assert_eq!(splice_outside_list_got(&err_subst), "xs");
8449 assert_eq!(splice_outside_list_got(&err_byte), "xs");
8450 }
8451
8452 #[test]
8453 fn splice_outside_list_position_is_none_today() {
8454 // Negative control for the future-spans move: until `Sexp` carries
8455 // source positions, `position()` returns `None` for this variant.
8456 // A future run that gives `Sexp` source spans adds `pos:
8457 // Option<usize>` to ONE place; this test gives that change a
8458 // deliberate fail-before/pass-after delta.
8459 let mut e = Expander::new_substitute_only();
8460 let err = e
8461 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8462 .expect_err("splice outside list must error");
8463 assert_eq!(err.position(), None);
8464 }
8465
8466 #[test]
8467 fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8468 // End-to-end through the Display impl — pins the rendered diagnostic
8469 // a downstream tool sees today (REPL, tatara-check). The legacy
8470 // substring `"\`,@\` may only appear inside a list"` is preserved
8471 // verbatim AND the parenthetical `(got ,@xs)` names the offending
8472 // form; tools that pattern-match on the variant gain structural
8473 // binding to `got`.
8474 let mut e = Expander::new_substitute_only();
8475 let err = e
8476 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8477 .expect_err("splice outside list must error");
8478 let msg = format!("{err}");
8479 assert_eq!(
8480 msg,
8481 "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8482 );
8483 }
8484
8485 #[test]
8486 fn splice_inside_list_still_succeeds_under_both_paths() {
8487 // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8488 // continues to succeed under both paths — the new gate only fires
8489 // when the splice is the entire body. Pins that the gate is scoped
8490 // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8491 // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8492 // bare list-literal whose first symbol would also splice through.
8493 let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8494 let mut subst = Expander::new_substitute_only();
8495 let mut bytecode = Expander::new();
8496 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8497 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8498 assert_eq!(out_subst, out_byte);
8499 assert_eq!(out_subst[0], parse("(outer 1 2)"));
8500 }
8501
8502 // ── splice_value_into: the shared splice-result coercion ──
8503
8504 #[test]
8505 fn splice_value_into_list_flattens_elements_into_builder() {
8506 // The canonical splice arm: a bound LIST contributes its elements
8507 // in order, preserving anything already in the builder.
8508 let mut builder = vec![Sexp::symbol("outer")];
8509 splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8510 assert_eq!(
8511 builder,
8512 vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8513 );
8514 }
8515
8516 #[test]
8517 fn splice_value_into_nil_is_a_noop() {
8518 // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8519 // the builder is unchanged.
8520 let mut builder = vec![Sexp::symbol("outer")];
8521 splice_value_into(&mut builder, &Sexp::Nil);
8522 assert_eq!(builder, vec![Sexp::symbol("outer")]);
8523 }
8524
8525 #[test]
8526 fn splice_value_into_scalar_pushes_single_element() {
8527 // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8528 // splices as exactly one element. Pins the "free middle" coercion
8529 // every scalar shape (int, keyword, …) shares.
8530 let mut builder = vec![Sexp::symbol("outer")];
8531 splice_value_into(&mut builder, &Sexp::int(5));
8532 assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8533 let mut other: Vec<Sexp> = vec![];
8534 splice_value_into(&mut other, &Sexp::keyword("k"));
8535 assert_eq!(other, vec![Sexp::keyword("k")]);
8536 }
8537
8538 #[test]
8539 fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8540 // The point of the lift: the NON-list splice arms (scalar → single
8541 // element, nil → nothing) coerce identically under the substitute
8542 // AND bytecode strategies. Before the coercion was lifted to ONE
8543 // primitive these two arms lived inline at two sites; this test
8544 // pins that the two strategies cannot drift on the non-list arms.
8545 let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8546 let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8547 for src in [scalar, empty] {
8548 let mut subst = Expander::new_substitute_only();
8549 let mut bytecode = Expander::new();
8550 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8551 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8552 assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8553 }
8554 let mut e = Expander::new();
8555 assert_eq!(
8556 e.expand_program(read(scalar).unwrap()).unwrap()[0],
8557 parse("(outer 5)")
8558 );
8559 let mut e2 = Expander::new();
8560 assert_eq!(
8561 e2.expand_program(read(empty).unwrap()).unwrap()[0],
8562 parse("(outer)")
8563 );
8564 }
8565
8566 // ── Missing macro arg: structural variant + path-uniform rejection ──
8567
8568 /// Helper for the missing-macro-arg tests — pins the variant shape
8569 /// and carries the failing macro's name + un-bound param up to the
8570 /// assert site for legibility. Sibling of `unbound_var`,
8571 /// `non_symbol_target`, and `splice_outside_list_got`.
8572 fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8573 match err {
8574 LispError::MissingMacroArg { macro_name, param } => {
8575 (macro_name.as_str(), param.as_str())
8576 }
8577 other => panic!("expected MissingMacroArg, got: {other:?}"),
8578 }
8579 }
8580
8581 #[test]
8582 fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8583 // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8584 // `apply_compiled` (the bytecode-template path, default expander).
8585 // Pins variant identity AND macro_name AND the un-bound param so a
8586 // regression that re-inlines the legacy `LispError::Compile` shape
8587 // fails-loudly here.
8588 let mut e = Expander::new();
8589 let err = e
8590 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8591 .expect_err("missing required macro arg must error");
8592 let (macro_name, param) = missing_macro_arg_fields(&err);
8593 assert_eq!(macro_name, "need-two");
8594 assert_eq!(param, "b");
8595 }
8596
8597 #[test]
8598 fn missing_macro_arg_in_substitute_emits_structural_variant() {
8599 // Same shape as the bytecode test but routed through the
8600 // substitute-only expander → `bind_args` is the failing site.
8601 // Proves the substitute path emits the SAME structural variant the
8602 // bytecode path emits — `missing required arg` rejection is
8603 // path-uniform across both expansion strategies.
8604 let mut e = Expander::new_substitute_only();
8605 let err = e
8606 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8607 .expect_err("missing required macro arg must error");
8608 let (macro_name, param) = missing_macro_arg_fields(&err);
8609 assert_eq!(macro_name, "need-two");
8610 assert_eq!(param, "b");
8611 }
8612
8613 #[test]
8614 fn missing_macro_arg_first_position_is_named() {
8615 // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8616 // arg. The variant names `a`, not `b` — naming the LEFTMOST
8617 // un-bound param is the shape `bind_args` / `apply_compiled` both
8618 // emit (each iterates positionally and bails on the first missing
8619 // slot). Pins the leftmost-bail contract so a regression that
8620 // names the rightmost (or a surplus) param fails-loudly.
8621 let mut e = Expander::new();
8622 let err = e
8623 .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8624 .expect_err("missing first required arg must error");
8625 let (macro_name, param) = missing_macro_arg_fields(&err);
8626 assert_eq!(macro_name, "f");
8627 assert_eq!(param, "a");
8628 }
8629
8630 #[test]
8631 fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8632 // Path-uniform rejection: the SAME source emits the SAME structural
8633 // variant under both expansion strategies. Negative control for
8634 // the divergence-closing posture: a future refactor that drifts
8635 // either path's rejection shape (or drops one path's rejection
8636 // entirely) fails-loudly here. Sibling of
8637 // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8638 // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8639 // failure mode each.
8640 let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8641 let mut subst = Expander::new_substitute_only();
8642 let mut bytecode = Expander::new();
8643 let err_subst = subst
8644 .expand_program(read(src).unwrap())
8645 .expect_err("substitute must error");
8646 let err_byte = bytecode
8647 .expand_program(read(src).unwrap())
8648 .expect_err("bytecode must error");
8649 assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8650 assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8651 }
8652
8653 #[test]
8654 fn missing_macro_arg_position_is_none_today() {
8655 // Negative control for the future-spans move: until `Sexp` carries
8656 // source positions, `position()` returns `None` for this variant.
8657 // A future run that gives `Sexp` source spans adds `pos:
8658 // Option<usize>` to ONE place; this test gives that change a
8659 // deliberate fail-before/pass-after delta. Parallel to
8660 // `splice_outside_list_position_is_none_today`.
8661 let mut e = Expander::new();
8662 let err = e
8663 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8664 .expect_err("missing required macro arg must error");
8665 assert_eq!(err.position(), None);
8666 }
8667
8668 #[test]
8669 fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8670 // End-to-end through the Display impl — pins the rendered diagnostic
8671 // a downstream tool sees today (REPL, tatara-check). The legacy
8672 // substring `"missing required arg: {param}"` is preserved verbatim
8673 // AND the head clause names the failing macro via `"call to
8674 // {macro_name}"`; tools that pattern-match on the variant gain
8675 // structural binding to `macro_name` / `param`.
8676 let mut e = Expander::new();
8677 let err = e
8678 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8679 .expect_err("missing required macro arg must error");
8680 assert_eq!(
8681 format!("{err}"),
8682 "compile error in call to need-two: missing required arg: b"
8683 );
8684 }
8685
8686 #[test]
8687 fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8688 // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8689 // round-trip through the variant unchanged. Pinning this contract
8690 // means a regression that camelCases or lowercases either side
8691 // fails-loudly here. Parallel to the
8692 // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8693 // assertion for the kwarg-gate's symmetric surface.
8694 let mut e = Expander::new();
8695 let err = e
8696 .expand_program(
8697 read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8698 .unwrap(),
8699 )
8700 .expect_err("missing required macro arg must error");
8701 let (macro_name, param) = missing_macro_arg_fields(&err);
8702 assert_eq!(macro_name, "wrap-twice");
8703 assert_eq!(param, "body");
8704 }
8705
8706 #[test]
8707 fn rest_param_only_macro_with_no_args_still_succeeds() {
8708 // Negative control: a macro whose only param is `&rest` must NOT
8709 // error when called with zero args — the rest-param binds to the
8710 // empty list. The new structural variant fires only on REQUIRED
8711 // params; the `Param::Rest` arm in both `bind_args` and
8712 // `apply_compiled` continues to bind the empty tail. Pins that the
8713 // helper is scoped to required-param failure, not all
8714 // arity-mismatch shapes.
8715 let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8716 let mut subst = Expander::new_substitute_only();
8717 let mut bytecode = Expander::new();
8718 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8719 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8720 assert_eq!(out_subst, out_byte);
8721 assert_eq!(out_subst[0], parse("(list)"));
8722 }
8723
8724 // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8725 //
8726 // A rest-less param list has a FIXED maximum arity equal to
8727 // `required.len() + optional.len()`. Surplus call args have nowhere to
8728 // bind. Before this gate the surplus was silently truncated to the
8729 // slice the binder could consume — the typed-entry macro-call-gate
8730 // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8731 // too-many silently, an asymmetry the definition-side `&rest <name>
8732 // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8733 // After this gate the call-site arity surface is structurally
8734 // complete in both directions; the substitute + bytecode paths share
8735 // `MacroParams::bind`, so both inherit the rejection without drift.
8736
8737 /// Helper for the too-many-args tests — projects to (macro_name,
8738 /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8739 fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8740 match err {
8741 LispError::TooManyMacroArgs {
8742 macro_name,
8743 expected,
8744 got,
8745 } => (macro_name.as_str(), *expected, *got),
8746 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8747 }
8748 }
8749
8750 #[test]
8751 fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8752 // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8753 // nowhere to bind. The rest-less binder rejects via
8754 // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8755 // NOT silently drops `3`. Pins both the variant identity AND the
8756 // structural fields the typed gate exposes for authoring-tool
8757 // quick-fixes ("you supplied 3 args; the macro takes at most
8758 // 2").
8759 let mut e = Expander::new();
8760 let err = e
8761 .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8762 .expect_err("surplus arg on rest-less call must error");
8763 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8764 assert_eq!(macro_name, "f");
8765 assert_eq!(expected, 2);
8766 assert_eq!(got, 3);
8767 }
8768
8769 #[test]
8770 fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8771 // The rest-less binder's fixed maximum arity is `required.len() +
8772 // optional.len()` — the optional section CONTRIBUTES to capacity.
8773 // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8774 // args rejects with `expected: 2` (required + optional, NOT just
8775 // required). Pins the optional-counts-in-capacity contract so a
8776 // regression that omits optionals from the expected calculation
8777 // (and erroneously rejects 2-arg calls) fails-loudly here.
8778 let mut e = Expander::new();
8779 let err = e
8780 .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8781 .expect_err("surplus arg beyond required+optional must error");
8782 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8783 assert_eq!(macro_name, "f");
8784 assert_eq!(expected, 2);
8785 assert_eq!(got, 3);
8786 }
8787
8788 #[test]
8789 fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8790 // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8791 // with `expected: 3`. Pins that the capacity calculation scales
8792 // with optional.len(), not just at-most-one. Mixes a bare
8793 // optional with an optional carrying a default form — both shapes
8794 // contribute identically to capacity (the typed `OptionalParam`
8795 // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8796 let mut e = Expander::new();
8797 let err = e
8798 .expand_program(
8799 read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8800 )
8801 .expect_err("surplus arg beyond required+two-optional must error");
8802 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8803 assert_eq!(macro_name, "f");
8804 assert_eq!(expected, 3);
8805 assert_eq!(got, 4);
8806 }
8807
8808 #[test]
8809 fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8810 // Negative control: a rest-PRESENT param list has no maximum
8811 // arity — the `&rest` slot collects every trailing arg into a
8812 // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8813 // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8814 // `MacroParams.rest` is `None`. Pins the rest-present-path
8815 // remains permissive — a regression that wrongly fires the
8816 // too-many gate for any surplus (including the rest-collecting
8817 // path) would break every `&rest`-using macro.
8818 let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8819 let mut subst = Expander::new_substitute_only();
8820 let mut bytecode = Expander::new();
8821 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8822 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8823 assert_eq!(out_subst, out_byte);
8824 assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8825 }
8826
8827 #[test]
8828 fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8829 // Negative control: the rest-less gate fires STRICTLY when
8830 // `args.len() > expected` — at exact arity the binder accepts.
8831 // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8832 // a=1, b=2 successfully (the optional takes its supplied arg,
8833 // not the default). Pins the boundary condition so a regression
8834 // that flips the comparison to `>=` (rejecting exact-arity
8835 // calls) fails-loudly here.
8836 let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8837 let mut e = Expander::new();
8838 let out = e.expand_program(read(src).unwrap()).unwrap();
8839 assert_eq!(out[0], parse("(list 1 2)"));
8840 }
8841
8842 #[test]
8843 fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8844 // Path-uniform rejection: the SAME source emits the SAME
8845 // structural variant under both expansion strategies. The
8846 // shared `MacroParams::bind` makes the rejection lands once and
8847 // both paths inherit it. Mirror of
8848 // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8849 // both close `THEORY.md §II.1 invariant 2 — free middle` for
8850 // one failure mode each.
8851 let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8852 let mut subst = Expander::new_substitute_only();
8853 let mut bytecode = Expander::new();
8854 let err_subst = subst
8855 .expand_program(read(src).unwrap())
8856 .expect_err("substitute must error");
8857 let err_byte = bytecode
8858 .expand_program(read(src).unwrap())
8859 .expect_err("bytecode must error");
8860 assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8861 assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8862 }
8863
8864 #[test]
8865 fn too_many_macro_args_fires_after_missing_required_priority_held() {
8866 // Priority discipline: the required walk fires
8867 // `MissingMacroArg` BEFORE the rest-less surplus gate is
8868 // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8869 // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8870 // collision). The two failure modes are structurally disjoint:
8871 // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8872 // first-missing-required contract so a regression that swaps
8873 // the two gates' order would emit the wrong variant.
8874 let mut e = Expander::new();
8875 let err = e
8876 .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8877 .expect_err("missing required must error");
8878 assert!(
8879 matches!(err, LispError::MissingMacroArg { .. }),
8880 "expected MissingMacroArg (priority), got: {err:?}"
8881 );
8882 }
8883
8884 #[test]
8885 fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8886 // Degenerate case: a nullary macro `(defmacro f () ...)` has
8887 // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8888 // the gate fires even when the rest-less max-arity is zero —
8889 // i.e. the rejection is structural, not conditional on a
8890 // non-empty required+optional.
8891 let mut e = Expander::new();
8892 let err = e
8893 .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8894 .expect_err("nullary macro called with arg must error");
8895 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8896 assert_eq!(macro_name, "f");
8897 assert_eq!(expected, 0);
8898 assert_eq!(got, 1);
8899 }
8900
8901 #[test]
8902 fn too_many_macro_args_display_renders_legacy_compile_substring() {
8903 // The rendered Display matches the legacy `Compile`-shaped
8904 // diagnostic style — `"compile error in call to {macro_name}:
8905 // too many args: expected at most {expected}, got {got}"` — so
8906 // the existing `"compile error in call to"` substring authoring
8907 // tools' assertions key on stays unchanged. Pins the byte-level
8908 // rendered shape so a regression that drifts the prefix /
8909 // separator / labels fails-loudly here.
8910 let err = LispError::TooManyMacroArgs {
8911 macro_name: "pair".into(),
8912 expected: 2,
8913 got: 5,
8914 };
8915 assert_eq!(
8916 err.to_string(),
8917 "compile error in call to pair: too many args: expected at most 2, got 5"
8918 );
8919 }
8920
8921 #[test]
8922 fn too_many_macro_args_position_is_none_today() {
8923 // Negative control for the future-spans move: until `Sexp`
8924 // carries source positions, `position()` returns `None` for
8925 // this variant. A future run that gives `Sexp` source spans
8926 // adds `pos: Option<usize>` to ONE place; this test gives that
8927 // change a deliberate fail-before/pass-after delta. Parallel to
8928 // `missing_macro_arg_position_is_none_today`.
8929 let err = LispError::TooManyMacroArgs {
8930 macro_name: "pair".into(),
8931 expected: 2,
8932 got: 3,
8933 };
8934 assert_eq!(err.position(), None);
8935 }
8936
8937 /// Helper for the non-symbol-param tests — pins the variant shape and
8938 /// carries the failing position + offending element up to the assert
8939 /// site for legibility. Sibling of `missing_macro_arg_fields`.
8940 fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8941 match err {
8942 LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8943 other => panic!("expected NonSymbolParam, got: {other:?}"),
8944 }
8945 }
8946
8947 #[test]
8948 fn non_symbol_param_at_first_position_emits_structural_variant() {
8949 // `(defmacro f (5) ...)` — the first element of the param list is
8950 // an integer literal, not a symbol. Pins variant identity AND
8951 // that `position` is the loop index inside `parse_params` (0 for
8952 // the first slot) AND that `got` is the offending element via
8953 // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8954 // `LispError::Compile` shape (which named neither the position
8955 // nor the offending element) fails-loudly here.
8956 let mut e = Expander::new();
8957 let err = e
8958 .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8959 .expect_err("non-symbol param must error");
8960 let (position, got) = non_symbol_param_fields(&err);
8961 assert_eq!(position, 0);
8962 assert_eq!(got, "5");
8963 }
8964
8965 #[test]
8966 fn non_symbol_param_at_second_position_emits_structural_variant() {
8967 // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8968 // misfires. Pins that `position` advances with the loop index, so
8969 // an LSP quick-fix that wants to point at "the second element of
8970 // your param list" gains the index as data, no source re-parse
8971 // required.
8972 let mut e = Expander::new();
8973 let err = e
8974 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8975 .expect_err("non-symbol param must error");
8976 let (position, got) = non_symbol_param_fields(&err);
8977 assert_eq!(position, 1);
8978 assert_eq!(got, "5");
8979 }
8980
8981 #[test]
8982 fn non_symbol_param_carries_keyword_value_unchanged() {
8983 // `:k` at a param-list position. `Sexp::Display` for
8984 // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8985 // field round-trips the keyword form unchanged so an LSP that
8986 // surfaces "you wrote `:k` where a symbol was expected" gains
8987 // the literal keyword value as data, no re-parsing required.
8988 let mut e = Expander::new();
8989 let err = e
8990 .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8991 .expect_err("non-symbol param must error");
8992 let (position, got) = non_symbol_param_fields(&err);
8993 assert_eq!(position, 0);
8994 assert_eq!(got, ":k");
8995 }
8996
8997 #[test]
8998 fn non_symbol_param_carries_nested_list_value_unchanged() {
8999 // A nested list at a param-list position. `Sexp::Display` for
9000 // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
9001 // `got` field carries the nested form's full Display projection
9002 // unchanged so the operator sees what they wrote.
9003 let mut e = Expander::new();
9004 let err = e
9005 .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
9006 .expect_err("non-symbol param must error");
9007 let (position, got) = non_symbol_param_fields(&err);
9008 assert_eq!(position, 0);
9009 assert_eq!(got, "(nested)");
9010 }
9011
9012 #[test]
9013 fn non_symbol_param_in_defpoint_template_emits_same_variant() {
9014 // `defpoint-template` shares `parse_params` with `defmacro` (all
9015 // three head keywords route through `macro_def_from`). Pins that
9016 // the lift fires path-uniformly across the three head keywords
9017 // — `defmacro`, `defpoint-template`, `defcheck` — so the
9018 // structural-completeness floor holds for every defmacro-shaped
9019 // form, not just the one with the `defmacro` head literal.
9020 let mut e = Expander::new();
9021 let err = e
9022 .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
9023 .expect_err("non-symbol param must error");
9024 let (position, got) = non_symbol_param_fields(&err);
9025 assert_eq!(position, 0);
9026 assert_eq!(got, "5");
9027 }
9028
9029 #[test]
9030 fn non_symbol_param_in_defcheck_emits_same_variant() {
9031 // Sibling of the defpoint-template test — `defcheck` is the
9032 // third head keyword `macro_def_from` recognizes. All three
9033 // route through the same `parse_params` and now reject
9034 // non-symbol params with the same structural variant.
9035 let mut e = Expander::new();
9036 let err = e
9037 .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
9038 .expect_err("non-symbol param must error");
9039 let (position, got) = non_symbol_param_fields(&err);
9040 assert_eq!(position, 1);
9041 assert_eq!(got, "5");
9042 }
9043
9044 #[test]
9045 fn non_symbol_param_position_is_none_today() {
9046 // Negative control for the future-spans move: until `Sexp`
9047 // carries source positions, `position()` on `LispError` returns
9048 // `None` for this variant. A future run that gives `Sexp`
9049 // source spans adds `pos: Option<usize>` to ONE place; this
9050 // test gives that change a deliberate fail-before/pass-after
9051 // delta. Parallel to `missing_macro_arg_position_is_none_today`.
9052 let mut e = Expander::new();
9053 let err = e
9054 .expand_program(read("(defmacro f (5) `(list))").unwrap())
9055 .expect_err("non-symbol param must error");
9056 assert_eq!(err.position(), None);
9057 }
9058
9059 #[test]
9060 fn non_symbol_param_message_renders_legacy_substring_with_position() {
9061 // End-to-end through Display — pins the rendered diagnostic that
9062 // downstream tools (REPL, `tatara-check`) see today. Legacy
9063 // substrings `"defmacro params"` AND `"expected symbol"` are
9064 // preserved verbatim; the appended `at position {position}, got
9065 // {got}` clause is the new structural detail. Tools that
9066 // pattern-match on the variant gain structural binding to
9067 // `position` / `got`.
9068 let mut e = Expander::new();
9069 let err = e
9070 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
9071 .expect_err("non-symbol param must error");
9072 assert_eq!(
9073 format!("{err}"),
9074 "compile error in defmacro params: \
9075 expected symbol at position 1, got 5"
9076 );
9077 }
9078
9079 #[test]
9080 fn non_symbol_param_substitute_and_bytecode_paths_agree() {
9081 // Path-uniform rejection: the SAME source emits the SAME
9082 // structural variant under both expansion strategies. The
9083 // defmacro-syntax-gate fires inside `macro_def_from` →
9084 // `parse_params`, BEFORE either strategy's expansion path runs;
9085 // so both `Expander::new()` (bytecode) and
9086 // `Expander::new_substitute_only()` (substitute) reject the
9087 // SAME malformed defmacro at the SAME gate. Sibling of
9088 // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
9089 let src = "(defmacro f (a 5) `(,a))";
9090 let mut subst = Expander::new_substitute_only();
9091 let mut bytecode = Expander::new();
9092 let err_subst = subst
9093 .expand_program(read(src).unwrap())
9094 .expect_err("substitute must error");
9095 let err_byte = bytecode
9096 .expand_program(read(src).unwrap())
9097 .expect_err("bytecode must error");
9098 assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
9099 assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
9100 }
9101
9102 /// Helper for the rest-param-missing-name tests — pins the variant
9103 /// shape and carries the marker position + offending follower (or
9104 /// its absence) up to the assert site for legibility. Sibling of
9105 /// `non_symbol_param_fields`.
9106 fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
9107 match err {
9108 LispError::RestParamMissingName { rest_position, got } => {
9109 (*rest_position, got.as_ref().map(|w| w.display.as_str()))
9110 }
9111 other => panic!("expected RestParamMissingName, got: {other:?}"),
9112 }
9113 }
9114
9115 #[test]
9116 fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
9117 // `(defmacro f (&rest))` — the marker is the only param-list
9118 // element; nothing follows. Pins variant identity AND that
9119 // `rest_position == 0` (the first slot) AND that `got == None`
9120 // (no follower exists). A regression that re-inlines the legacy
9121 // `LispError::Compile` shape (which named neither field) fails-
9122 // loudly here.
9123 let mut e = Expander::new();
9124 let err = e
9125 .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
9126 .expect_err("&rest with no follower must error");
9127 let (rest_position, got) = rest_param_missing_name_fields(&err);
9128 assert_eq!(rest_position, 0);
9129 assert_eq!(got, None);
9130 }
9131
9132 #[test]
9133 fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
9134 // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
9135 // position 1 has no follower at all. Pins that `rest_position`
9136 // advances with the loop index, so an LSP quick-fix that wants to
9137 // point at "your `&rest` at position 1 has no name" gains the
9138 // marker position as data, no source re-parse required.
9139 let mut e = Expander::new();
9140 let err = e
9141 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9142 .expect_err("&rest with no follower must error");
9143 let (rest_position, got) = rest_param_missing_name_fields(&err);
9144 assert_eq!(rest_position, 1);
9145 assert_eq!(got, None);
9146 }
9147
9148 #[test]
9149 fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9150 // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9151 // `5` (an integer literal, not a symbol). Pins that the variant's
9152 // `got` field is `Some` and carries the offending follower's
9153 // `Sexp::Display` projection; the bifurcation between "missing
9154 // entirely" and "present but non-symbol" is in the renderable
9155 // detail, not in what the gate rejects.
9156 let mut e = Expander::new();
9157 let err = e
9158 .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9159 .expect_err("&rest followed by non-symbol must error");
9160 let (rest_position, got) = rest_param_missing_name_fields(&err);
9161 assert_eq!(rest_position, 0);
9162 assert_eq!(got, Some("5"));
9163 }
9164
9165 #[test]
9166 fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9167 // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9168 // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9169 // that the variant's `got` field round-trips the keyword form
9170 // unchanged so an LSP that surfaces "you wrote `:foo` where a
9171 // rest-name was expected" gains the literal keyword value as
9172 // data, no re-parsing required.
9173 let mut e = Expander::new();
9174 let err = e
9175 .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9176 .expect_err("&rest followed by keyword must error");
9177 let (rest_position, got) = rest_param_missing_name_fields(&err);
9178 assert_eq!(rest_position, 1);
9179 assert_eq!(got, Some(":foo"));
9180 }
9181
9182 #[test]
9183 fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9184 // `(defmacro f (&rest (nested)))` — nested-list follower at the
9185 // rest-name slot. `Sexp::Display` for `List(xs)` writes
9186 // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9187 // the nested form's full Display projection unchanged so the
9188 // operator sees what they wrote.
9189 let mut e = Expander::new();
9190 let err = e
9191 .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9192 .expect_err("&rest followed by list must error");
9193 let (rest_position, got) = rest_param_missing_name_fields(&err);
9194 assert_eq!(rest_position, 0);
9195 assert_eq!(got, Some("(nested)"));
9196 }
9197
9198 #[test]
9199 fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9200 // `defpoint-template` shares `parse_params` with `defmacro` (all
9201 // three head keywords route through `macro_def_from`). Pins that
9202 // the lift fires path-uniformly across the three head keywords —
9203 // a regression that handles `defpoint-template`'s param list
9204 // differently from `defmacro`'s would fail-loudly here.
9205 let mut e = Expander::new();
9206 let err = e
9207 .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9208 .expect_err("&rest with no follower must error");
9209 let (rest_position, got) = rest_param_missing_name_fields(&err);
9210 assert_eq!(rest_position, 1);
9211 assert_eq!(got, None);
9212 }
9213
9214 #[test]
9215 fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9216 // Sibling for the `defcheck` head; rounds out the three-head-
9217 // keyword coverage so the lift is path-uniform across
9218 // `defmacro` / `defpoint-template` / `defcheck`. After this
9219 // test the defmacro-syntax-gate rejects `&rest`-without-name
9220 // identically across all three head keywords — the
9221 // typed-entry surface is single-shape across the cluster.
9222 let mut e = Expander::new();
9223 let err = e
9224 .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9225 .expect_err("&rest followed by non-symbol must error");
9226 let (rest_position, got) = rest_param_missing_name_fields(&err);
9227 assert_eq!(rest_position, 0);
9228 assert_eq!(got, Some("5"));
9229 }
9230
9231 #[test]
9232 fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9233 // Path-uniform rejection: the SAME source emits the SAME
9234 // structural variant under both expansion strategies. The
9235 // defmacro-syntax-gate fires inside `macro_def_from` →
9236 // `parse_params`, BEFORE either strategy's expansion path
9237 // runs; so both `Expander::new()` (bytecode) and
9238 // `Expander::new_substitute_only()` (substitute) reject the
9239 // SAME malformed defmacro at the SAME gate. Sibling of
9240 // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9241 let src = "(defmacro f (a &rest 5) `(,a))";
9242 let mut subst = Expander::new_substitute_only();
9243 let mut bytecode = Expander::new();
9244 let err_subst = subst
9245 .expand_program(read(src).unwrap())
9246 .expect_err("substitute must error");
9247 let err_byte = bytecode
9248 .expand_program(read(src).unwrap())
9249 .expect_err("bytecode must error");
9250 assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9251 assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9252 }
9253
9254 #[test]
9255 fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9256 // End-to-end through Display — pins the rendered diagnostic
9257 // consumers see today (REPL, tatara-check) AND the new `(rest
9258 // marker at position {rest_position}, got {got})` clause. The
9259 // legacy `"&rest needs a name"` substring rides through
9260 // verbatim.
9261 let mut e = Expander::new();
9262 let err = e
9263 .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9264 .expect_err("&rest followed by non-symbol must error");
9265 assert_eq!(
9266 format!("{err}"),
9267 "compile error in defmacro params: &rest needs a name \
9268 (rest marker at position 1, got 5)"
9269 );
9270 }
9271
9272 #[test]
9273 fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9274 // Same as the prior test but for the "missing entirely" branch.
9275 // The renderable detail is `(rest marker at position
9276 // {rest_position}, none provided)` — naming the absence
9277 // structurally instead of an empty / partial parenthetical.
9278 let mut e = Expander::new();
9279 let err = e
9280 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9281 .expect_err("&rest with no follower must error");
9282 assert_eq!(
9283 format!("{err}"),
9284 "compile error in defmacro params: &rest needs a name \
9285 (rest marker at position 1, none provided)"
9286 );
9287 }
9288
9289 #[test]
9290 fn rest_param_missing_name_position_is_none_today() {
9291 // Pins that `position()` returns `None` so the future `pos:
9292 // Option<usize>` add (once `Sexp` carries source spans) lands
9293 // as a deliberate fail-before/pass-after delta rather than a
9294 // silent default. Parallel to
9295 // `non_symbol_param_position_is_none_today` and
9296 // `missing_macro_arg_position_is_none_today`.
9297 let err_missing = LispError::RestParamMissingName {
9298 rest_position: 1,
9299 got: None,
9300 };
9301 assert_eq!(err_missing.position(), None);
9302 let err_got = LispError::RestParamMissingName {
9303 rest_position: 0,
9304 got: Some(crate::error::SexpWitness::new(
9305 crate::error::SexpShape::Int,
9306 "5",
9307 )),
9308 };
9309 assert_eq!(err_got.position(), None);
9310 }
9311
9312 // --- RestParamTrailingTokens: the parse_params gate's third (and
9313 // final) definition-site failure mode ---
9314 //
9315 // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9316 // thing a param list can name. Before this variant `parse_params`
9317 // returned the moment it bound the rest name, SILENTLY DROPPING any
9318 // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9319 // there. These tests pin the loud rejection that replaces the silent
9320 // drop; the symbol `RestParamTrailingTokens` exists only after this
9321 // change, so the whole block is fail-before/pass-after by construction
9322 // (compile-time edge) and the end-to-end regression guard below pins
9323 // that the malformed defmacro no longer expands cleanly.
9324
9325 /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9326 /// shape and lifts the marker position, trailing count, and first
9327 /// offender's display up to the assert site.
9328 fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9329 match err {
9330 LispError::RestParamTrailingTokens {
9331 rest_position,
9332 extra,
9333 first,
9334 } => (*rest_position, *extra, first.display.as_str()),
9335 other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9336 }
9337 }
9338
9339 #[test]
9340 fn parse_params_rejects_single_trailing_token_after_rest_name() {
9341 // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9342 // follows. The rest name is bound at position 2, the marker at 1;
9343 // the lone trailing token `extra` is reported (extra == 1, first ==
9344 // "extra"). Before this variant `parse_params` returned at the rest
9345 // name and `extra` vanished.
9346 let err = parse_params(&read("a &rest c extra").unwrap())
9347 .expect_err("a trailing token after the rest name must error");
9348 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9349 }
9350
9351 #[test]
9352 fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9353 // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9354 // counts ALL of them (3), `first` is the first (`x`), and the
9355 // marker is at position 0. A regression that reports only the
9356 // first trailing token's presence (extra hard-coded to 1) fails
9357 // loudly here.
9358 let err = parse_params(&read("&rest c x y z").unwrap())
9359 .expect_err("multiple trailing tokens must error");
9360 assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9361 }
9362
9363 #[test]
9364 fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9365 // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9366 // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9367 // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9368 // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9369 // a well-formed rest-name followed by junk is
9370 // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9371 // witness's `Sexp::Display` projection.
9372 let err = parse_params(&read("a &rest c 5").unwrap())
9373 .expect_err("a trailing non-symbol after the rest name must error");
9374 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9375 }
9376
9377 #[test]
9378 fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9379 // The fidelity fix, end-to-end through `expand_program`: a defmacro
9380 // whose param list carries a stray token after `&rest <name>` now
9381 // ERRORS at the typed-entry gate instead of expanding as though the
9382 // stray token weren't there. This is the regression guard for the
9383 // silent-drop bug — before this change the same source expanded
9384 // cleanly and `extra` was discarded with no signal.
9385 let mut e = Expander::new();
9386 let err = e
9387 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9388 .expect_err("trailing token after &rest name must error");
9389 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9390 }
9391
9392 #[test]
9393 fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9394 // Path-uniform rejection: the gate fires inside `macro_def_from` →
9395 // `parse_params`, BEFORE either expansion strategy runs, so both
9396 // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9397 // (substitute) reject the SAME malformed defmacro at the SAME gate.
9398 // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9399 let src = "(defmacro f (a &rest xs extra) `(,a))";
9400 let mut subst = Expander::new_substitute_only();
9401 let mut bytecode = Expander::new();
9402 let err_subst = subst
9403 .expand_program(read(src).unwrap())
9404 .expect_err("substitute must error");
9405 let err_byte = bytecode
9406 .expand_program(read(src).unwrap())
9407 .expect_err("bytecode must error");
9408 assert_eq!(
9409 rest_param_trailing_tokens_fields(&err_subst),
9410 (1, 1, "extra")
9411 );
9412 assert_eq!(
9413 rest_param_trailing_tokens_fields(&err_byte),
9414 (1, 1, "extra")
9415 );
9416 }
9417
9418 #[test]
9419 fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9420 // End-to-end through Display — pins the rendered diagnostic AND the
9421 // new `(rest marker at position {n}, {extra} trailing after name,
9422 // first: {first})` clause. The `compile error in defmacro params:`
9423 // prefix matches the sibling `&rest needs a name` rendering's shape.
9424 let mut e = Expander::new();
9425 let err = e
9426 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9427 .expect_err("trailing token after &rest name must error");
9428 assert_eq!(
9429 format!("{err}"),
9430 "compile error in defmacro params: &rest name must be last \
9431 (rest marker at position 1, 1 trailing after name, first: extra)"
9432 );
9433 }
9434
9435 #[test]
9436 fn rest_param_trailing_tokens_position_is_none_today() {
9437 // Pins `position() == None` so the future `pos: Option<usize>` add
9438 // (once `Sexp` carries source spans) lands as a deliberate
9439 // fail-before/pass-after delta. Parallel to
9440 // `rest_param_missing_name_position_is_none_today`.
9441 let err = LispError::RestParamTrailingTokens {
9442 rest_position: 1,
9443 extra: 1,
9444 first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9445 };
9446 assert_eq!(err.position(), None);
9447 }
9448
9449 // --- MacroDefHead enum (the closed-set lift) ---
9450 //
9451 // The next nine tests pin the typed-enum lift that closes the
9452 // three-times rule on the `head: &str → &'static str` projection
9453 // idiom previously inlined at FOUR sites (the `matches!` gate at
9454 // the top of `macro_def_from` plus the projection match inside
9455 // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9456 // `defmacro_non_list_params`). Every test in this block names
9457 // `MacroDefHead` directly — the symbol exists only after the
9458 // lift, so the entire block is fail-before/pass-after by
9459 // construction (compile-time edge). Theory anchor: THEORY.md
9460 // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9461 // a TYPE rather than a `matches!` literal.
9462
9463 #[test]
9464 fn macro_def_head_from_keyword_recognizes_defmacro() {
9465 // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9466 // `Some(MacroDefHead::Defmacro)` — the first of the three
9467 // canonical macro-definition head keywords. A regression that
9468 // re-inlines a `matches!`-only gate (without the typed-enum
9469 // projection) deletes `from_keyword` and fails-loudly here.
9470 assert_eq!(
9471 MacroDefHead::from_keyword("defmacro"),
9472 Some(MacroDefHead::Defmacro)
9473 );
9474 }
9475
9476 #[test]
9477 fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9478 // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9479 // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9480 // of the three canonical head keywords. The `defpoint-template`
9481 // form is the K8s-as-processes authoring surface (see
9482 // tatara-process); `macro_def_from` must recognize it
9483 // identically to `defmacro` so the `(defpoint-template …)`
9484 // form's macro-style binding works the same way.
9485 assert_eq!(
9486 MacroDefHead::from_keyword("defpoint-template"),
9487 Some(MacroDefHead::DefpointTemplate)
9488 );
9489 }
9490
9491 #[test]
9492 fn macro_def_head_from_keyword_recognizes_defcheck() {
9493 // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9494 // `Some(MacroDefHead::Defcheck)` — the third and final
9495 // canonical head keyword. The `defcheck` form is the
9496 // workspace-coherence authoring surface (see
9497 // tatara-reconciler/checks.lisp); `macro_def_from` must
9498 // recognize it identically to `defmacro` so user-defined
9499 // checks inherit the macro-style binding semantics.
9500 assert_eq!(
9501 MacroDefHead::from_keyword("defcheck"),
9502 Some(MacroDefHead::Defcheck)
9503 );
9504 }
9505
9506 #[test]
9507 fn macro_def_head_from_keyword_rejects_unknown() {
9508 // Pins that `MacroDefHead::from_keyword` returns `None` for
9509 // anything outside the closed set — a non-symbol keyword
9510 // (`"if"`), a near-miss spelling (`"defmacroo"`,
9511 // `"defcheckk"`), and the empty string. `macro_def_from`
9512 // depends on this `None` projection to mean "this form is
9513 // not a defmacro form" and walk past — a regression that
9514 // accidentally accepts a near-miss head (e.g. via a
9515 // lower-cased `EqualFold` match) would route `(defmacroo …)`
9516 // through the arity gate, which is wrong. Pins all four
9517 // canonical near-miss / non-canonical inputs.
9518 assert_eq!(MacroDefHead::from_keyword("if"), None);
9519 assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9520 assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9521 assert_eq!(MacroDefHead::from_keyword(""), None);
9522 }
9523
9524 #[test]
9525 fn macro_def_head_keyword_round_trips_each_variant() {
9526 // Pins that `MacroDefHead::keyword` returns the canonical
9527 // `&'static str` literal for each variant. Together with
9528 // `from_keyword` this closes the bidirectional projection:
9529 // for every canonical head keyword `s`, `MacroDefHead::
9530 // from_keyword(s).unwrap().keyword() == s`. The `&'static
9531 // str` lifetime on the return type is load-bearing — it's
9532 // what lets the `LispError::Defmacro*` variants carry
9533 // `head: &'static str` slots without an arbitrary owned
9534 // `String`. Pinning the `: &'static str` binding here
9535 // makes the lifetime requirement load-bearing in the test.
9536 let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9537 let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9538 let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9539 assert_eq!(s_defmacro, "defmacro");
9540 assert_eq!(s_defpoint, "defpoint-template");
9541 assert_eq!(s_defcheck, "defcheck");
9542 }
9543
9544 #[test]
9545 fn macro_def_head_keyword_round_trips_through_from_keyword() {
9546 // Pins that the two halves of the projection compose to the
9547 // identity on the closed set: for every canonical head
9548 // keyword, projecting `&str → MacroDefHead → &'static str`
9549 // returns the original literal. Sibling of
9550 // `macro_def_head_keyword_round_trips_each_variant` —
9551 // together they pin both directions of the bidirection.
9552 for kw in ["defmacro", "defpoint-template", "defcheck"] {
9553 let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9554 assert_eq!(head.keyword(), kw);
9555 }
9556 }
9557
9558 #[test]
9559 fn macro_def_head_threads_through_defmacro_arity_helper() {
9560 // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9561 // and threads it through to the variant's typed `head` slot
9562 // unchanged — no `&str` projection at the helper boundary
9563 // (the projection through `MacroDefHead::keyword()` happens at
9564 // Display rendering time inside the `#[error(...)]`
9565 // annotation). A regression that drops the `MacroDefHead`
9566 // parameter type (e.g. by reverting to `head: &str`) breaks
9567 // compilation here. Pinning each of the three variants gives
9568 // the typed-head threading the same path-uniformity edge the
9569 // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9570 // for the call-site path through `macro_def_from`.
9571 for head in [
9572 MacroDefHead::Defmacro,
9573 MacroDefHead::DefpointTemplate,
9574 MacroDefHead::Defcheck,
9575 ] {
9576 let err = defmacro_arity(head, 2);
9577 match err {
9578 LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9579 other => panic!("expected DefmacroArity, got: {other:?}"),
9580 }
9581 }
9582 }
9583
9584 #[test]
9585 fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9586 // Sibling of the `defmacro_arity` threading test — pins that
9587 // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9588 // and threads it through to the variant's typed `head` slot
9589 // unchanged. The `got: &Sexp` parameter rides through
9590 // `crate::domain::sexp_witness` into the variant's typed
9591 // `got: SexpWitness` slot so BOTH the structural shape AND
9592 // the rendered literal are preserved across the helper
9593 // boundary, parallel to how `non_symbol_param` and
9594 // `non_symbol_unquote_target` project their `&Sexp` arguments
9595 // through the same typed joint primitive.
9596 let got = parse("5");
9597 for head in [
9598 MacroDefHead::Defmacro,
9599 MacroDefHead::DefpointTemplate,
9600 MacroDefHead::Defcheck,
9601 ] {
9602 let err = defmacro_non_symbol_name(head, &got);
9603 match err {
9604 LispError::DefmacroNonSymbolName { head: h, got: g } => {
9605 assert_eq!(h, head);
9606 assert_eq!(g.shape, crate::error::SexpShape::Int);
9607 assert_eq!(g.display, "5");
9608 }
9609 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9610 }
9611 }
9612 }
9613
9614 #[test]
9615 fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9616 // Sibling of the `defmacro_arity` and
9617 // `defmacro_non_symbol_name` threading tests — pins that
9618 // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9619 // and threads it through to the variant's typed `head` slot
9620 // unchanged. Together the three threading tests close the
9621 // typed-enum lift across all three error helpers — every
9622 // call site that constructs a `LispError::Defmacro*` variant
9623 // takes its `head` from a `MacroDefHead`, never from a `&str`
9624 // match. The `got: &Sexp` parameter rides through
9625 // `crate::domain::sexp_witness` into the variant's typed
9626 // `got: SexpWitness` slot so BOTH the structural shape AND
9627 // the rendered literal are preserved across the helper
9628 // boundary, parallel to how `defmacro_non_symbol_name`,
9629 // `non_symbol_param`, and `non_symbol_unquote_target` project
9630 // their `&Sexp` arguments through the same typed joint
9631 // primitive.
9632 let got = parse("x");
9633 for head in [
9634 MacroDefHead::Defmacro,
9635 MacroDefHead::DefpointTemplate,
9636 MacroDefHead::Defcheck,
9637 ] {
9638 let err = defmacro_non_list_params(head, &got);
9639 match err {
9640 LispError::DefmacroNonListParams { head: h, got: g } => {
9641 assert_eq!(h, head);
9642 assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9643 assert_eq!(g.display, "x");
9644 }
9645 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9646 }
9647 }
9648 }
9649
9650 /// Helper for the defmacro-arity tests — pins the variant shape and
9651 /// carries the head / arity up to the assert site for legibility.
9652 /// Sibling of `non_symbol_param_fields` and
9653 /// `rest_param_missing_name_fields`.
9654 fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9655 match err {
9656 LispError::DefmacroArity { head, arity } => (*head, *arity),
9657 other => panic!("expected DefmacroArity, got: {other:?}"),
9658 }
9659 }
9660
9661 #[test]
9662 fn defmacro_arity_with_head_only_emits_structural_variant() {
9663 // `(defmacro)` — only the head, no name / params / body. Pins
9664 // variant identity AND that `arity == 1` (just the head
9665 // element) AND that `head == "defmacro"`. A regression that
9666 // re-inlines the legacy `LispError::Compile` shape (which
9667 // named neither field) fails-loudly here.
9668 let mut e = Expander::new();
9669 let err = e
9670 .expand_program(read("(defmacro)").unwrap())
9671 .expect_err("defmacro arity gate must error");
9672 let (head, arity) = defmacro_arity_fields(&err);
9673 assert_eq!(head, MacroDefHead::Defmacro);
9674 assert_eq!(arity, 1);
9675 }
9676
9677 #[test]
9678 fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9679 // `(defmacro f)` — head + name, missing params + body. Pins
9680 // that `arity` advances with the actual form length (2 for
9681 // this case) so an LSP quick-fix that wants to surface "you
9682 // wrote 2 elements; need 4" gains the count as data, no
9683 // source re-parse required.
9684 let mut e = Expander::new();
9685 let err = e
9686 .expand_program(read("(defmacro f)").unwrap())
9687 .expect_err("defmacro arity gate must error");
9688 let (head, arity) = defmacro_arity_fields(&err);
9689 assert_eq!(head, MacroDefHead::Defmacro);
9690 assert_eq!(arity, 2);
9691 }
9692
9693 #[test]
9694 fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9695 // `(defmacro f ())` — head + name + params, missing body
9696 // (the most-complete partial defmacro that still trips the
9697 // arity gate). Pins that `arity == 3` exactly so an LSP
9698 // quick-fix that wants to surface "your defmacro is one
9699 // element short — body is missing" gains the count as data.
9700 let mut e = Expander::new();
9701 let err = e
9702 .expand_program(read("(defmacro f ())").unwrap())
9703 .expect_err("defmacro arity gate must error");
9704 let (head, arity) = defmacro_arity_fields(&err);
9705 assert_eq!(head, MacroDefHead::Defmacro);
9706 assert_eq!(arity, 3);
9707 }
9708
9709 #[test]
9710 fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9711 // `defpoint-template` shares `macro_def_from` with `defmacro`
9712 // (all three head keywords route through the same gate). Pins
9713 // that the lift fires path-uniformly across the three head
9714 // keywords AND that the variant's `head` slot carries the
9715 // actual head literal — `defpoint-template`, not `defmacro`
9716 // — so an LSP that wants to point at "your defpoint-template
9717 // form is missing elements" gains the head as data.
9718 let mut e = Expander::new();
9719 let err = e
9720 .expand_program(read("(defpoint-template t)").unwrap())
9721 .expect_err("defpoint-template arity gate must error");
9722 let (head, arity) = defmacro_arity_fields(&err);
9723 assert_eq!(head, MacroDefHead::DefpointTemplate);
9724 assert_eq!(arity, 2);
9725 }
9726
9727 #[test]
9728 fn defmacro_arity_in_defcheck_emits_same_variant() {
9729 // Sibling of the defpoint-template test — `defcheck` is the
9730 // third head keyword `macro_def_from` recognizes. All three
9731 // route through the same arity gate and now reject too-short
9732 // forms with the same structural variant.
9733 let mut e = Expander::new();
9734 let err = e
9735 .expand_program(read("(defcheck)").unwrap())
9736 .expect_err("defcheck arity gate must error");
9737 let (head, arity) = defmacro_arity_fields(&err);
9738 assert_eq!(head, MacroDefHead::Defcheck);
9739 assert_eq!(arity, 1);
9740 }
9741
9742 #[test]
9743 fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9744 // Path-uniform rejection: the SAME source emits the SAME
9745 // structural variant under both expansion strategies. The
9746 // arity gate fires inside `macro_def_from` BEFORE either
9747 // strategy's expansion path runs; so both `Expander::new()`
9748 // (bytecode) and `Expander::new_substitute_only()`
9749 // (substitute) reject the SAME malformed defmacro at the
9750 // SAME gate. Sibling of
9751 // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9752 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9753 let src = "(defmacro f)";
9754 let mut subst = Expander::new_substitute_only();
9755 let mut bytecode = Expander::new();
9756 let err_subst = subst
9757 .expand_program(read(src).unwrap())
9758 .expect_err("substitute must error");
9759 let err_byte = bytecode
9760 .expand_program(read(src).unwrap())
9761 .expect_err("bytecode must error");
9762 assert_eq!(
9763 defmacro_arity_fields(&err_subst),
9764 (MacroDefHead::Defmacro, 2)
9765 );
9766 assert_eq!(
9767 defmacro_arity_fields(&err_byte),
9768 (MacroDefHead::Defmacro, 2)
9769 );
9770 }
9771
9772 #[test]
9773 fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9774 // End-to-end through Display — pins the rendered diagnostic
9775 // consumers see today (REPL, `tatara-check`) AND the new
9776 // `(got {arity} elements, need 4)` clause. The legacy
9777 // `"(defmacro name (params) body) required"` substring
9778 // rides through verbatim. Tools that pattern-match on the
9779 // variant gain structural binding to `head` / `arity`.
9780 let mut e = Expander::new();
9781 let err = e
9782 .expand_program(read("(defmacro f)").unwrap())
9783 .expect_err("defmacro arity gate must error");
9784 assert_eq!(
9785 format!("{err}"),
9786 "compile error in defmacro: (defmacro name (params) body) required \
9787 (got 2 elements, need 4)"
9788 );
9789 }
9790
9791 #[test]
9792 fn defmacro_arity_position_is_none_today() {
9793 // Negative control for the future-spans move: until `Sexp`
9794 // carries source positions, `position()` on `LispError`
9795 // returns `None` for this variant. A future run that gives
9796 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9797 // this test gives that change a deliberate fail-before/pass-
9798 // after delta. Parallel to
9799 // `non_symbol_param_position_is_none_today` and
9800 // `rest_param_missing_name_position_is_none_today`.
9801 let mut e = Expander::new();
9802 let err = e
9803 .expand_program(read("(defmacro)").unwrap())
9804 .expect_err("defmacro arity gate must error");
9805 assert_eq!(err.position(), None);
9806 }
9807
9808 #[test]
9809 fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9810 // Negative control: a defmacro with exactly 4 elements (head
9811 // + name + params + body) passes the arity gate. Pins that
9812 // the lift is scoped to the arity-deficient case, not to
9813 // every defmacro form. After this test, a regression that
9814 // tightens the arity gate to >= 5 (e.g. spuriously requiring
9815 // a docstring slot) fails-loudly here.
9816 let mut e = Expander::new();
9817 let out = e
9818 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9819 .expect("well-formed defmacro must succeed");
9820 assert_eq!(out[0], Sexp::int(42));
9821 }
9822
9823 /// Helper for the defmacro-non-symbol-name tests — pins variant
9824 /// shape and carries the head / got up to the assert site for
9825 /// legibility. Sibling of `defmacro_arity_fields`,
9826 /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9827 fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9828 match err {
9829 LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9830 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9831 }
9832 }
9833
9834 #[test]
9835 fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9836 // `(defmacro 5 () body)` — the form passes the arity gate
9837 // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9838 // identity AND that `head == "defmacro"` AND that `got ==
9839 // "5"`. A regression that re-inlines the legacy
9840 // `LispError::Compile { form: "defmacro", message: "expected
9841 // name symbol" }` shape (which named the failure mode but
9842 // not the offending element) fails-loudly here.
9843 let mut e = Expander::new();
9844 let err = e
9845 .expand_program(read("(defmacro 5 () body)").unwrap())
9846 .expect_err("defmacro non-symbol name gate must error");
9847 let (head, got) = defmacro_non_symbol_name_fields(&err);
9848 assert_eq!(head, MacroDefHead::Defmacro);
9849 assert_eq!(got, "5");
9850 }
9851
9852 #[test]
9853 fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9854 // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9855 // not a symbol. Pins that `Sexp::Display` for
9856 // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9857 // carries the keyword form unchanged. An LSP that wants to
9858 // surface "you wrote `:foo` where a name symbol was expected"
9859 // gains the literal keyword value as data, no source re-parse
9860 // required.
9861 let mut e = Expander::new();
9862 let err = e
9863 .expand_program(read("(defmacro :foo () body)").unwrap())
9864 .expect_err("defmacro non-symbol name gate must error");
9865 let (head, got) = defmacro_non_symbol_name_fields(&err);
9866 assert_eq!(head, MacroDefHead::Defmacro);
9867 assert_eq!(got, ":foo");
9868 }
9869
9870 #[test]
9871 fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9872 // `(defmacro "name" () body)` — list[1] is the string
9873 // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9874 // for `Atom::String(s)` writes `"s"` (with quotes) and the
9875 // variant's `got` slot carries the quoted form unchanged.
9876 let mut e = Expander::new();
9877 let err = e
9878 .expand_program(read("(defmacro \"name\" () body)").unwrap())
9879 .expect_err("defmacro non-symbol name gate must error");
9880 let (head, got) = defmacro_non_symbol_name_fields(&err);
9881 assert_eq!(head, MacroDefHead::Defmacro);
9882 assert_eq!(got, "\"name\"");
9883 }
9884
9885 #[test]
9886 fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9887 // `(defmacro (nested) () body)` — list[1] is a nested list,
9888 // not a symbol. Pins that `Sexp::Display` for a list writes
9889 // `(elements)` and the variant's `got` slot carries the
9890 // parenthesized form unchanged.
9891 let mut e = Expander::new();
9892 let err = e
9893 .expand_program(read("(defmacro (nested) () body)").unwrap())
9894 .expect_err("defmacro non-symbol name gate must error");
9895 let (head, got) = defmacro_non_symbol_name_fields(&err);
9896 assert_eq!(head, MacroDefHead::Defmacro);
9897 assert_eq!(got, "(nested)");
9898 }
9899
9900 #[test]
9901 fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9902 // `defpoint-template` shares `macro_def_from` with `defmacro`
9903 // (all three head keywords route through the same gate).
9904 // Pins that the lift fires path-uniformly across the three
9905 // head keywords AND that the variant's `head` slot carries
9906 // the actual head literal — `defpoint-template`, not
9907 // `defmacro` — so an LSP that wants to point at "your
9908 // defpoint-template form's name slot isn't a symbol" gains
9909 // the head as data.
9910 let mut e = Expander::new();
9911 let err = e
9912 .expand_program(read("(defpoint-template 7 () body)").unwrap())
9913 .expect_err("defpoint-template non-symbol name gate must error");
9914 let (head, got) = defmacro_non_symbol_name_fields(&err);
9915 assert_eq!(head, MacroDefHead::DefpointTemplate);
9916 assert_eq!(got, "7");
9917 }
9918
9919 #[test]
9920 fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9921 // Sibling for the `defcheck` head — third head keyword
9922 // `macro_def_from` recognizes. Rounds out the three-head-
9923 // keyword coverage so the lift is path-uniform across
9924 // `defmacro` / `defpoint-template` / `defcheck`.
9925 let mut e = Expander::new();
9926 let err = e
9927 .expand_program(read("(defcheck :k () body)").unwrap())
9928 .expect_err("defcheck non-symbol name gate must error");
9929 let (head, got) = defmacro_non_symbol_name_fields(&err);
9930 assert_eq!(head, MacroDefHead::Defcheck);
9931 assert_eq!(got, ":k");
9932 }
9933
9934 #[test]
9935 fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9936 // Path-uniform rejection: the SAME source emits the SAME
9937 // structural variant under both expansion strategies. The
9938 // name-symbol gate fires inside `macro_def_from` BEFORE
9939 // either expansion strategy runs, so the gate is naturally
9940 // path-uniform; pinning it gives a regression that drifts
9941 // either strategy's handling of non-symbol-name defmacros (or
9942 // makes one strategy accept what the other rejects) a fail-
9943 // before/pass-after edge. Sibling of
9944 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9945 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9946 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9947 let src = "(defmacro 5 () body)";
9948 let mut subst = Expander::new_substitute_only();
9949 let mut bytecode = Expander::new();
9950 let err_subst = subst
9951 .expand_program(read(src).unwrap())
9952 .expect_err("substitute must error");
9953 let err_byte = bytecode
9954 .expand_program(read(src).unwrap())
9955 .expect_err("bytecode must error");
9956 assert_eq!(
9957 defmacro_non_symbol_name_fields(&err_subst),
9958 (MacroDefHead::Defmacro, "5")
9959 );
9960 assert_eq!(
9961 defmacro_non_symbol_name_fields(&err_byte),
9962 (MacroDefHead::Defmacro, "5")
9963 );
9964 }
9965
9966 #[test]
9967 fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9968 // End-to-end through Display — pins the rendered diagnostic
9969 // consumers see today (REPL, `tatara-check`) AND the new
9970 // `, got {got}` clause. The legacy `"expected name symbol"`
9971 // substring rides through verbatim; the prefix matches the
9972 // legacy `Compile { form: "defmacro", message: "expected name
9973 // symbol" }` byte-for-byte. Tools that pattern-match on the
9974 // variant gain structural binding to `head` / `got`.
9975 let mut e = Expander::new();
9976 let err = e
9977 .expand_program(read("(defmacro 5 () body)").unwrap())
9978 .expect_err("defmacro non-symbol name gate must error");
9979 assert_eq!(
9980 format!("{err}"),
9981 "compile error in defmacro: expected name symbol, got 5"
9982 );
9983 }
9984
9985 #[test]
9986 fn defmacro_non_symbol_name_position_is_none_today() {
9987 // Negative control for the future-spans move: until `Sexp`
9988 // carries source positions, `position()` on `LispError`
9989 // returns `None` for this variant. A future run that gives
9990 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9991 // this test gives that change a deliberate fail-before/pass-
9992 // after delta. Parallel to
9993 // `defmacro_arity_position_is_none_today`,
9994 // `non_symbol_param_position_is_none_today`, and
9995 // `rest_param_missing_name_position_is_none_today`.
9996 let mut e = Expander::new();
9997 let err = e
9998 .expand_program(read("(defmacro 5 () body)").unwrap())
9999 .expect_err("defmacro non-symbol name gate must error");
10000 assert_eq!(err.position(), None);
10001 }
10002
10003 #[test]
10004 fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
10005 // Negative control: a defmacro whose name slot IS a symbol
10006 // passes the name-symbol gate. Pins that the lift is scoped
10007 // to the non-symbol-name case, not to every defmacro form.
10008 // After this test, a regression that tightens the gate to
10009 // reject e.g. kebab-cased names fails-loudly here.
10010 let mut e = Expander::new();
10011 let out = e
10012 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10013 .expect("well-formed defmacro must succeed");
10014 assert_eq!(out[0], Sexp::int(42));
10015 }
10016
10017 #[test]
10018 fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
10019 // Pins the gate ordering: a 4-element defmacro whose name
10020 // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
10021 // `DefmacroArity`. The arity gate (>= 4 elements) admits
10022 // this form; the name-symbol gate is the next checkpoint.
10023 // A regression that swaps the gate ordering (e.g. checks
10024 // name-symbol before arity, so `(defmacro 5)` would emit
10025 // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
10026 // loudly here.
10027 let mut e = Expander::new();
10028 let err = e
10029 .expand_program(read("(defmacro 5 () body)").unwrap())
10030 .expect_err("name-symbol gate must error");
10031 assert!(
10032 matches!(err, LispError::DefmacroNonSymbolName { .. }),
10033 "expected DefmacroNonSymbolName, got: {err:?}"
10034 );
10035
10036 let err_arity = e
10037 .expand_program(read("(defmacro 5)").unwrap())
10038 .expect_err("arity gate must error");
10039 assert!(
10040 matches!(err_arity, LispError::DefmacroArity { .. }),
10041 "expected DefmacroArity (arity < 4 short-circuits before name check), \
10042 got: {err_arity:?}"
10043 );
10044 }
10045
10046 /// Helper for the defmacro-non-list-params tests — pins variant
10047 /// shape and carries the head / got up to the assert site for
10048 /// legibility. Sibling of `defmacro_arity_fields`,
10049 /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
10050 /// and `rest_param_missing_name_fields`.
10051 fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
10052 match err {
10053 LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
10054 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
10055 }
10056 }
10057
10058 #[test]
10059 fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
10060 // `(defmacro f x body)` — the form passes both the arity gate
10061 // (4 elements) AND the name-symbol gate (`f` is a symbol) but
10062 // list[2] is the symbol `x`, not a list. Pins variant identity
10063 // AND that `head == "defmacro"` AND that `got == "x"`. A
10064 // regression that re-inlines the legacy `LispError::Compile {
10065 // form: "defmacro", message: "expected param list" }` shape
10066 // (which named the failure mode but not the offending element)
10067 // fails-loudly here.
10068 let mut e = Expander::new();
10069 let err = e
10070 .expand_program(read("(defmacro f x body)").unwrap())
10071 .expect_err("defmacro non-list params gate must error");
10072 let (head, got) = defmacro_non_list_params_fields(&err);
10073 assert_eq!(head, MacroDefHead::Defmacro);
10074 assert_eq!(got, "x");
10075 }
10076
10077 #[test]
10078 fn defmacro_non_list_params_with_int_emits_structural_variant() {
10079 // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
10080 // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
10081 // variant's `got` slot carries the integer form unchanged. An
10082 // LSP that surfaces "you wrote `5` where a param list was
10083 // expected" gains the literal value as data, no source
10084 // re-parse required.
10085 let mut e = Expander::new();
10086 let err = e
10087 .expand_program(read("(defmacro f 5 body)").unwrap())
10088 .expect_err("defmacro non-list params gate must error");
10089 let (head, got) = defmacro_non_list_params_fields(&err);
10090 assert_eq!(head, MacroDefHead::Defmacro);
10091 assert_eq!(got, "5");
10092 }
10093
10094 #[test]
10095 fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
10096 // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
10097 // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
10098 // writes `:s` and the variant's `got` slot carries the
10099 // keyword form unchanged.
10100 let mut e = Expander::new();
10101 let err = e
10102 .expand_program(read("(defmacro f :foo body)").unwrap())
10103 .expect_err("defmacro non-list params gate must error");
10104 let (head, got) = defmacro_non_list_params_fields(&err);
10105 assert_eq!(head, MacroDefHead::Defmacro);
10106 assert_eq!(got, ":foo");
10107 }
10108
10109 #[test]
10110 fn defmacro_non_list_params_with_string_emits_structural_variant() {
10111 // `(defmacro f "params" body)` — list[2] is the string literal
10112 // `"params"`, not a list. Pins that `Sexp::Display` for
10113 // `Atom::String(s)` writes `"s"` (with quotes) and the
10114 // variant's `got` slot carries the quoted form unchanged.
10115 let mut e = Expander::new();
10116 let err = e
10117 .expand_program(read("(defmacro f \"params\" body)").unwrap())
10118 .expect_err("defmacro non-list params gate must error");
10119 let (head, got) = defmacro_non_list_params_fields(&err);
10120 assert_eq!(head, MacroDefHead::Defmacro);
10121 assert_eq!(got, "\"params\"");
10122 }
10123
10124 #[test]
10125 fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
10126 // `defpoint-template` shares `macro_def_from` with `defmacro`
10127 // (all three head keywords route through the same gate).
10128 // Pins that the lift fires path-uniformly across the three
10129 // head keywords AND that the variant's `head` slot carries
10130 // the actual head literal — `defpoint-template`, not
10131 // `defmacro` — so an LSP that wants to point at "your
10132 // defpoint-template form's param-list slot isn't a list"
10133 // gains the head as data.
10134 let mut e = Expander::new();
10135 let err = e
10136 .expand_program(read("(defpoint-template t x body)").unwrap())
10137 .expect_err("defpoint-template non-list params gate must error");
10138 let (head, got) = defmacro_non_list_params_fields(&err);
10139 assert_eq!(head, MacroDefHead::DefpointTemplate);
10140 assert_eq!(got, "x");
10141 }
10142
10143 #[test]
10144 fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10145 // Sibling for the `defcheck` head — third head keyword
10146 // `macro_def_from` recognizes. Rounds out the three-head-
10147 // keyword coverage so the lift is path-uniform across
10148 // `defmacro` / `defpoint-template` / `defcheck`.
10149 let mut e = Expander::new();
10150 let err = e
10151 .expand_program(read("(defcheck c 7 body)").unwrap())
10152 .expect_err("defcheck non-list params gate must error");
10153 let (head, got) = defmacro_non_list_params_fields(&err);
10154 assert_eq!(head, MacroDefHead::Defcheck);
10155 assert_eq!(got, "7");
10156 }
10157
10158 #[test]
10159 fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10160 // Path-uniform rejection: the SAME source emits the SAME
10161 // structural variant under both expansion strategies. The
10162 // param-list gate fires inside `macro_def_from` BEFORE either
10163 // expansion strategy runs, so the gate is naturally path-
10164 // uniform; pinning it gives a regression that drifts either
10165 // strategy's handling of non-list-params defmacros (or makes
10166 // one strategy accept what the other rejects) a fail-before/
10167 // pass-after edge. Sibling of
10168 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10169 // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10170 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10171 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10172 let src = "(defmacro f x body)";
10173 let mut subst = Expander::new_substitute_only();
10174 let mut bytecode = Expander::new();
10175 let err_subst = subst
10176 .expand_program(read(src).unwrap())
10177 .expect_err("substitute must error");
10178 let err_byte = bytecode
10179 .expand_program(read(src).unwrap())
10180 .expect_err("bytecode must error");
10181 assert_eq!(
10182 defmacro_non_list_params_fields(&err_subst),
10183 (MacroDefHead::Defmacro, "x")
10184 );
10185 assert_eq!(
10186 defmacro_non_list_params_fields(&err_byte),
10187 (MacroDefHead::Defmacro, "x")
10188 );
10189 }
10190
10191 #[test]
10192 fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10193 // End-to-end through Display — pins the rendered diagnostic
10194 // consumers see today (REPL, `tatara-check`) AND the new
10195 // `, got {got}` clause. The legacy `"expected param list"`
10196 // substring rides through verbatim; the prefix matches the
10197 // legacy `Compile { form: "defmacro", message: "expected
10198 // param list" }` byte-for-byte. Tools that pattern-match on
10199 // the variant gain structural binding to `head` / `got`.
10200 let mut e = Expander::new();
10201 let err = e
10202 .expand_program(read("(defmacro f x body)").unwrap())
10203 .expect_err("defmacro non-list params gate must error");
10204 assert_eq!(
10205 format!("{err}"),
10206 "compile error in defmacro: expected param list, got x"
10207 );
10208 }
10209
10210 #[test]
10211 fn defmacro_non_list_params_position_is_none_today() {
10212 // Negative control for the future-spans move: until `Sexp`
10213 // carries source positions, `position()` on `LispError`
10214 // returns `None` for this variant. A future run that gives
10215 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10216 // this test gives that change a deliberate fail-before/pass-
10217 // after delta. Parallel to
10218 // `defmacro_arity_position_is_none_today`,
10219 // `defmacro_non_symbol_name_position_is_none_today`,
10220 // `non_symbol_param_position_is_none_today`, and
10221 // `rest_param_missing_name_position_is_none_today`.
10222 let mut e = Expander::new();
10223 let err = e
10224 .expand_program(read("(defmacro f x body)").unwrap())
10225 .expect_err("defmacro non-list params gate must error");
10226 assert_eq!(err.position(), None);
10227 }
10228
10229 #[test]
10230 fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10231 // Negative control: a defmacro whose param-list slot IS a
10232 // list passes the param-list gate. Pins that the lift is
10233 // scoped to the non-list-params case, not to every defmacro
10234 // form. After this test, a regression that tightens the gate
10235 // to reject e.g. empty param lists fails-loudly here.
10236 let mut e = Expander::new();
10237 let out = e
10238 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10239 .expect("well-formed defmacro must succeed");
10240 assert_eq!(out[0], Sexp::int(42));
10241 }
10242
10243 #[test]
10244 fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10245 // Pins the gate ordering: a 4-element defmacro whose name
10246 // slot IS a symbol but whose param-list slot is non-list
10247 // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10248 // The name-symbol gate admits this form; the param-list gate
10249 // is the next checkpoint. A regression that swaps the gate
10250 // ordering (e.g. checks param-list before name-symbol, so
10251 // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10252 // instead of `DefmacroNonSymbolName`) fails-loudly here.
10253 let mut e = Expander::new();
10254 let err = e
10255 .expand_program(read("(defmacro f x body)").unwrap())
10256 .expect_err("param-list gate must error");
10257 assert!(
10258 matches!(err, LispError::DefmacroNonListParams { .. }),
10259 "expected DefmacroNonListParams, got: {err:?}"
10260 );
10261
10262 let err_name = e
10263 .expand_program(read("(defmacro 5 x body)").unwrap())
10264 .expect_err("name-symbol gate must error");
10265 assert!(
10266 matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10267 "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10268 got: {err_name:?}"
10269 );
10270 }
10271
10272 #[test]
10273 fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10274 // Pins the full gate ordering: a 4-element defmacro whose
10275 // first three slots are head/symbol/non-list fires
10276 // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10277 // gate (>= 4 elements) admits this form; the name-symbol
10278 // gate admits the symbol; the param-list gate is the third
10279 // checkpoint. A regression that drifts the gate sequence
10280 // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10281 // loudly here. Parallel to
10282 // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10283 // — together they pin the full
10284 // arity → name-symbol → param-list ordering inside
10285 // `macro_def_from`.
10286 let mut e = Expander::new();
10287 let err = e
10288 .expand_program(read("(defmacro f x body)").unwrap())
10289 .expect_err("param-list gate must error");
10290 assert!(
10291 matches!(err, LispError::DefmacroNonListParams { .. }),
10292 "expected DefmacroNonListParams, got: {err:?}"
10293 );
10294
10295 let err_arity = e
10296 .expand_program(read("(defmacro f x)").unwrap())
10297 .expect_err("arity gate must error");
10298 assert!(
10299 matches!(err_arity, LispError::DefmacroArity { .. }),
10300 "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10301 got: {err_arity:?}"
10302 );
10303 }
10304
10305 #[test]
10306 fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10307 // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10308 // at the parser level, so `as_symbol()` succeeds for it. The
10309 // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10310 // itself; the dedicated `&rest needs a name` rejection (a
10311 // separate failure mode in this cluster) handles malformed
10312 // rest-param shapes. Pins that the lift is scoped to
10313 // non-symbol elements at param-list positions, not to
10314 // every malformed-param shape.
10315 let mut e = Expander::new();
10316 let out = e
10317 .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10318 .expect("&rest with name must succeed");
10319 assert_eq!(out[0], parse("(list 1 2 3)"));
10320 }
10321
10322 #[test]
10323 fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10324 // End-to-end through the Display impl — pins the rendered diagnostic
10325 // a downstream tool sees today (REPL, tatara-check). The shape is
10326 // parallel to the existing `TypeMismatch` variant: form, expected
10327 // shape, offending literal — all three slots present.
10328 let mut e = Expander::new();
10329 let err = e
10330 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10331 .expect_err("non-symbol target must error");
10332 assert_eq!(
10333 format!("{err}"),
10334 "compile error in ,: expected symbol, got (list 1 2)"
10335 );
10336 }
10337
10338 // ── template_invariant_violation: structural lift ───────────────
10339 //
10340 // The four byte-identical inline `LispError::Compile { form:
10341 // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10342 // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10343 // final-no-value gates) were lifted to `template_invariant_violation`,
10344 // and the helper's emission was promoted from `LispError::Compile`-
10345 // shape to the structural `LispError::TemplateInvariant { macro_name,
10346 // kind: TemplateInvariantKind }` variant. The index payload of the
10347 // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10348 // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10349 // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10350 // is structurally unrepresentable. Display matches the legacy
10351 // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10352 // `TemplateInvariantKind::message()` projection so authoring-tool
10353 // substring greps see no drift across the lift.
10354 //
10355 // The tests below pin: (a) the helper produces the structural
10356 // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10357 // first-class; (b) the Subst / Splice gates thread the bad index
10358 // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10359 // unchanged; (c) the two REACHABLE invariant-violation paths through
10360 // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10361 // out-of-bounds idx — route through the helper end-to-end (the
10362 // EndList / no-value paths are guarded by `last_mut().unwrap()`
10363 // ahead of `pop().ok_or_else()` and are not reachable through any
10364 // single CompiledTemplate; they remain defensive against future
10365 // changes to the stack discipline); (d) the legacy Display
10366 // rendering matches byte-for-byte across the lift; (e) positive
10367 // controls: a well-formed CompiledTemplate routes PAST the helper
10368 // cleanly, and unrelated macro errors (missing-required-arg) do
10369 // NOT route through the helper.
10370
10371 #[test]
10372 fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10373 // Direct unit test of the helper: a fixed macro_name and a
10374 // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10375 // variant with the macro_name in the `macro_name` slot and the
10376 // kind passed through verbatim in the `kind` slot. A regression
10377 // that drifts the variant (e.g., back to `LispError::Compile`)
10378 // or swaps the slot positions fails-loudly here.
10379 let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10380 match err {
10381 LispError::TemplateInvariant { macro_name, kind } => {
10382 assert_eq!(macro_name, "test-macro");
10383 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10384 }
10385 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10386 }
10387 }
10388
10389 #[test]
10390 fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10391 // The Subst gate's `usize` idx lives INSIDE the
10392 // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10393 // than being substring-rendered into a free-form `message`
10394 // slot. Pin that the helper threads the bad index through the
10395 // typed variant unchanged; a regression that drops the index
10396 // payload (e.g., via a `usize -> ()` projection) fails here.
10397 let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10398 match err {
10399 LispError::TemplateInvariant { macro_name, kind } => {
10400 assert_eq!(macro_name, "wrap");
10401 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10402 }
10403 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10404 }
10405 }
10406
10407 #[test]
10408 fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10409 // Hand-crafted CompiledTemplate with a Subst(99) op against
10410 // an empty params list: `args_by_index` has length 0, so
10411 // `.get(99)` returns None and the `ok_or_else` triggers
10412 // through the helper. Fail-before-pass-after: this same input
10413 // pre-lift went through `LispError::Compile { form: macro_name,
10414 // message: format!("compiled template referenced bad param
10415 // index {idx}") }`; post-lift it routes through
10416 // `template_invariant_violation` and emits the structural
10417 // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10418 // variant with the bad index threaded through as typed data.
10419 let tmpl = CompiledTemplate {
10420 ops: vec![TemplateOp::Subst(99)],
10421 };
10422 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10423 .expect_err("bad idx must error");
10424 match err {
10425 LispError::TemplateInvariant { macro_name, kind } => {
10426 assert_eq!(macro_name, "test-macro");
10427 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10428 }
10429 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10430 }
10431 }
10432
10433 #[test]
10434 fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10435 // Hand-crafted CompiledTemplate with a Splice(42) op against
10436 // an empty params list. Sibling of the Subst-bad-idx test;
10437 // pins the Splice gate routes through the helper with the
10438 // typed `SpliceBadIndex(42)` kind carrying the bad index.
10439 let tmpl = CompiledTemplate {
10440 ops: vec![TemplateOp::Splice(42)],
10441 };
10442 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10443 .expect_err("bad splice idx must error");
10444 match err {
10445 LispError::TemplateInvariant { macro_name, kind } => {
10446 assert_eq!(macro_name, "call-macro");
10447 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10448 }
10449 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10450 }
10451 }
10452
10453 #[test]
10454 fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10455 // End-to-end through the `LispError` Display impl — pins the
10456 // rendered diagnostic byte-for-byte: `"compile error in
10457 // test-macro: compiled template referenced bad param index 99"`.
10458 // Authoring tools that substring-grep the rendered diagnostic
10459 // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10460 // see no drift across the lift. Parallel to how
10461 // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10462 // pins the sibling-file (compile.rs) lift's Display contract.
10463 let tmpl = CompiledTemplate {
10464 ops: vec![TemplateOp::Subst(99)],
10465 };
10466 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10467 .expect_err("bad idx must error");
10468 assert_eq!(
10469 format!("{err}"),
10470 "compile error in test-macro: compiled template referenced bad param index 99"
10471 );
10472 }
10473
10474 #[test]
10475 fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10476 // Sibling Display test for the Splice gate. Pins the message
10477 // byte-for-byte through the `LispError` Display impl: `"compile
10478 // error in call-macro: compiled template referenced bad splice
10479 // index 42"`.
10480 let tmpl = CompiledTemplate {
10481 ops: vec![TemplateOp::Splice(42)],
10482 };
10483 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10484 .expect_err("bad splice idx must error");
10485 assert_eq!(
10486 format!("{err}"),
10487 "compile error in call-macro: compiled template referenced bad splice index 42"
10488 );
10489 }
10490
10491 #[test]
10492 fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10493 // Positive control: a CompiledTemplate produced by the
10494 // bytecode compiler (`compile_template`) for a well-formed
10495 // macro never references an out-of-bounds index nor
10496 // unbalances the stack, so `apply_compiled` routes PAST the
10497 // helper cleanly. A regression that fires the helper on
10498 // well-formed bytecode (e.g., off-by-one in the index
10499 // resolution) would fail here. End-to-end through the public
10500 // `Expander` surface so the test exercises the same code
10501 // path users see.
10502 let mut e = Expander::new();
10503 let out = e
10504 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10505 .expect("well-formed macro expansion must not fire template-invariant-violation");
10506 assert_eq!(out.len(), 1);
10507 assert_eq!(out[0], Sexp::int(42));
10508 }
10509
10510 #[test]
10511 fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10512 // Negative control: the `missing_macro_arg` gate in the shared
10513 // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10514 // loop runs,
10515 // so a missing required arg routes through
10516 // `LispError::MissingMacroArg`, NOT through
10517 // `template_invariant_violation`. Pins the helper is
10518 // precisely scoped to bytecode-runtime invariant violations
10519 // (Subst / Splice / stack gates), not to macro-call arity
10520 // errors (the latter has its own structural variant). A
10521 // regression that conflates the two gate clusters would
10522 // route this case through `Compile { ... }` instead of
10523 // `MissingMacroArg` and fail-loudly here.
10524 let mut e = Expander::new();
10525 let err = e
10526 .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10527 .expect_err("missing required arg must error");
10528 assert!(
10529 matches!(err, LispError::MissingMacroArg { .. }),
10530 "expected MissingMacroArg, got: {err:?}"
10531 );
10532 }
10533
10534 // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10535 //
10536 // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10537 // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10538 // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10539 // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10540 // `apply_compiled`. The arms differ in the kind constructor
10541 // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10542 // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10543 // prelude is byte-identical modulo the constructor. These tests
10544 // pin the lifted helper's contract directly; the existing
10545 // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10546 // guards proving both production arms route through it without
10547 // behavior drift.
10548
10549 #[test]
10550 fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10551 // For an in-range index, the helper returns `Ok(&args[idx])`
10552 // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10553 // Pins the borrow-not-clone contract: a regression that drifts
10554 // the helper to clone+return (`Result<Sexp>` instead of
10555 // `Result<&Sexp>`) would allocate per lookup at the production
10556 // `Subst`/`Splice` hot path. The kind constructor must NOT
10557 // fire on the success path (`FnOnce`'s lazy semantics) — pin
10558 // that the test passes a constructor that would panic if
10559 // called, asserting the helper short-circuits before invoking
10560 // it on the in-range arm.
10561 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10562 let got = resolve_bound_arg(&args, 1, "m", |_| {
10563 panic!("kind constructor must not fire on the in-range path")
10564 })
10565 .expect("in-range lookup must succeed");
10566 assert!(
10567 std::ptr::eq(got, &args[1]),
10568 "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10569 );
10570 assert_eq!(*got, Sexp::int(2));
10571 }
10572
10573 #[test]
10574 fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10575 // For an out-of-range index, the helper raises the structural
10576 // `LispError::TemplateInvariant` variant with the caller-
10577 // supplied `SubstBadIndex` kind constructor applied to the bad
10578 // index. Pins the post-lift emission shape (variant identity
10579 // + the kind constructor threaded with the actual idx); a
10580 // regression that drops the idx payload (e.g., via a `usize ->
10581 // ()` projection) or hard-codes a different kind at the helper
10582 // boundary fails-loudly here. Fail-before-pass-after: this
10583 // assert is contradicted by the pre-lift code path (which
10584 // never called `resolve_bound_arg` because it didn't exist),
10585 // ratifies the post-lift one.
10586 let args: Vec<Sexp> = Vec::new();
10587 let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10588 .expect_err("out-of-range lookup must error");
10589 match err {
10590 LispError::TemplateInvariant { macro_name, kind } => {
10591 assert_eq!(macro_name, "test-macro");
10592 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10593 }
10594 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10595 }
10596 }
10597
10598 #[test]
10599 fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10600 // Path-uniformity for the per-call-site kind constructor: the
10601 // SAME out-of-range idx via the `SpliceBadIndex` constructor
10602 // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10603 // `SubstBadIndex(7)` variant. Pins that the constructor is
10604 // chosen per call site (not hard-coded at the helper boundary),
10605 // closing the structural matrix `{Subst, Splice} × {in-range,
10606 // out-of-range}` the two production arms span across the
10607 // bytecode-runtime's bound-arg-by-index reads. A regression
10608 // that hard-codes a single kind at the helper boundary would
10609 // emit the same variant identity for both call sites and
10610 // fail-loudly here.
10611 let args: Vec<Sexp> = Vec::new();
10612 let err = resolve_bound_arg(
10613 &args,
10614 7,
10615 "test-macro",
10616 TemplateInvariantKind::SpliceBadIndex,
10617 )
10618 .expect_err("out-of-range lookup must error");
10619 match err {
10620 LispError::TemplateInvariant { macro_name, kind } => {
10621 assert_eq!(macro_name, "test-macro");
10622 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10623 }
10624 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10625 }
10626 }
10627
10628 #[test]
10629 fn resolve_bound_arg_threads_macro_name_verbatim() {
10630 // Path-uniformity for the `macro_name` slot: the helper threads
10631 // the caller's borrow into the variant's owned `String` slot
10632 // verbatim. Pin two distinct macro names route through with no
10633 // mutual interference — a regression that hard-codes a single
10634 // macro_name at the helper boundary or swaps the parameter
10635 // ordering fails-loudly here. Same posture as
10636 // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10637 // pins the typed `stage` slot in the disk-persistence sibling
10638 // lift.
10639 let args: Vec<Sexp> = Vec::new();
10640 for name in ["wrap", "call-macro", "obs"] {
10641 let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10642 .expect_err("out-of-range lookup must error");
10643 match err {
10644 LispError::TemplateInvariant { macro_name, kind } => {
10645 assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10646 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10647 }
10648 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10649 }
10650 }
10651 }
10652
10653 #[test]
10654 fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10655 // Edge case: idx 0 with a single-element args_by_index returns
10656 // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10657 // — a regression that off-by-ones the lookup (e.g., `get(idx +
10658 // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10659 // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10660 // with_subst_kind_emits_typed_invariant` test.
10661 let args = vec![Sexp::int(42)];
10662 let got = resolve_bound_arg(&args, 0, "m", |_| {
10663 panic!("kind constructor must not fire on the in-range path")
10664 })
10665 .expect("idx-0 lookup must succeed");
10666 assert!(std::ptr::eq(got, &args[0]));
10667 assert_eq!(*got, Sexp::int(42));
10668 }
10669
10670 #[test]
10671 fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10672 // Edge case: idx `len - 1` is the highest valid index. Pin
10673 // that it routes through the success arm (NOT the error arm),
10674 // closing the in-range surface end-to-end with the lower-
10675 // bound sibling. A regression that off-by-ones the upper
10676 // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10677 // would fail here.
10678 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10679 let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10680 panic!("kind constructor must not fire on the in-range path")
10681 })
10682 .expect("last-element lookup must succeed");
10683 assert!(std::ptr::eq(got, args.last().unwrap()));
10684 assert_eq!(*got, Sexp::int(3));
10685 }
10686
10687 #[test]
10688 fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10689 // Boundary case: idx EQUAL to `args.len()` is out-of-range
10690 // (since `get` is 0-indexed). Pin that this routes through
10691 // the error arm with the kind constructor applied to the
10692 // EXACT idx that was tried. A regression that off-by-ones
10693 // the boundary (e.g., admits `idx == len`) would fail here.
10694 // This is the canonical off-by-one trap; the helper's
10695 // contract pins it at the variant-construction boundary.
10696 let args = vec![Sexp::int(1)];
10697 let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10698 .expect_err("idx == len must error");
10699 match err {
10700 LispError::TemplateInvariant { kind, .. } => {
10701 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10702 }
10703 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10704 }
10705 }
10706
10707 #[test]
10708 fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10709 // Boundary case: an empty `args_by_index` slice rejects every
10710 // idx (including 0). Pin that the helper's emission shape is
10711 // uniform regardless of which out-of-range idx fires the
10712 // rejection — `SubstBadIndex(0)` for an empty slice is the
10713 // bytecode-runtime mirror of a zero-arity macro template
10714 // referencing the 0-th param.
10715 let args: Vec<Sexp> = Vec::new();
10716 let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10717 .expect_err("empty slice rejects every idx");
10718 match err {
10719 LispError::TemplateInvariant { macro_name, kind } => {
10720 assert_eq!(macro_name, "zero-arity");
10721 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10722 }
10723 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10724 }
10725 }
10726
10727 #[test]
10728 fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10729 // End-to-end path-uniformity: a `Subst(99)` op against a
10730 // zero-arity macro routes the bytecode-runtime's bound-arg
10731 // lookup through `resolve_bound_arg` with the
10732 // `SubstBadIndex` constructor, emitting the structural
10733 // variant with `kind: SubstBadIndex(99)`. The pre-lift
10734 // sibling test `apply_compiled_subst_bad_idx_routes_through_
10735 // template_invariant_violation` pins that the same input
10736 // routes through `template_invariant_violation`; this test
10737 // pins that BOTH still hold under the post-lift composition
10738 // — `resolve_bound_arg` calls `template_invariant_violation`
10739 // internally on the rejection arm. A regression that drifts
10740 // ONE arm's projection from the other (e.g., swaps the
10741 // constructor at one call site, or short-circuits the
10742 // composition) would fail here.
10743 let tmpl = CompiledTemplate {
10744 ops: vec![TemplateOp::Subst(99)],
10745 };
10746 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10747 .expect_err("bad idx must error");
10748 match err {
10749 LispError::TemplateInvariant { macro_name, kind } => {
10750 assert_eq!(macro_name, "test-macro");
10751 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10752 }
10753 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10754 }
10755 }
10756
10757 #[test]
10758 fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10759 // Sibling end-to-end path-uniformity for the `Splice` arm:
10760 // the post-lift composition routes a `Splice(42)` op through
10761 // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10762 // emitting `kind: SpliceBadIndex(42)`. Together with the
10763 // `Subst` sibling test above, this pins the structural matrix
10764 // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10765 // the public `apply_compiled` surface, so a regression that
10766 // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10767 // accidentally emits `SubstBadIndex` after a copy-paste
10768 // refactor) fails-loudly here.
10769 let tmpl = CompiledTemplate {
10770 ops: vec![TemplateOp::Splice(42)],
10771 };
10772 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10773 .expect_err("bad splice idx must error");
10774 match err {
10775 LispError::TemplateInvariant { macro_name, kind } => {
10776 assert_eq!(macro_name, "call-macro");
10777 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10778 }
10779 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10780 }
10781 }
10782
10783 #[test]
10784 fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10785 // Positive control: a `Subst(0)` op against a one-arg macro
10786 // routes through `resolve_bound_arg`'s success arm and the
10787 // `Subst` post-lookup verb (clone + push) emits the bound
10788 // value verbatim. Pin the post-lift composition's success
10789 // path: the clone-and-push semantics live at the call site
10790 // (NOT in `resolve_bound_arg`, which only borrows), and a
10791 // regression that drifts the borrow contract (e.g., the
10792 // helper clones internally + the call site clones again)
10793 // would still pass observationally but would regress the
10794 // hot-path allocation count.
10795 let params = MacroParams {
10796 required: vec!["x".into()],
10797 optional: Vec::new(),
10798 rest: None,
10799 };
10800 let tmpl = CompiledTemplate {
10801 ops: vec![TemplateOp::Subst(0)],
10802 };
10803 let out = apply_compiled("id", ¶ms, &tmpl, &[Sexp::int(42)])
10804 .expect("in-range Subst must succeed");
10805 assert_eq!(out, Sexp::int(42));
10806 }
10807
10808 // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10809 //
10810 // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10811 // projection that appeared at FOUR sites inside `apply_compiled`'s
10812 // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10813 // named primitive. The expect message names the bytecode-runtime
10814 // invariant ("at least one stack frame during op-loop") so a
10815 // regression that drifts the loop's frame management (a new op that
10816 // pops without pushing, an early-return that bypasses EndList's
10817 // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10818 // These tests pin the projection's contract directly; the existing
10819 // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10820 // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10821 // guards proving the four sites still emit the canonical bytecode-
10822 // runtime output across the lift.
10823
10824 #[test]
10825 fn current_builder_mut_returns_the_top_frame_reference() {
10826 // The simplest projection: on a single-frame stack, the helper
10827 // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10828 // projection's identity end-to-end via a push that mutates
10829 // through the borrow and observe the original frame carries the
10830 // pushed value back.
10831 let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10832 current_builder_mut(&mut stack).push(Sexp::int(42));
10833 assert_eq!(stack.len(), 1);
10834 assert_eq!(stack[0], vec![Sexp::int(42)]);
10835 }
10836
10837 #[test]
10838 fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10839 // The projection MUST target the topmost frame, not the bottom
10840 // one — every `TemplateOp::BeginList` pushes a fresh frame the
10841 // subsequent ops emit into, and a regression that flipped the
10842 // projection to `first_mut` (or to a fixed bottom-frame
10843 // reference) would silently smear all op output into the
10844 // outermost result. Pin path-uniformity with the bytecode-
10845 // runtime's mid-list emission posture: with three frames on
10846 // the stack (one outer + two pending lists), the helper
10847 // returns a borrow into the third frame, leaving frames 0 and
10848 // 1 untouched.
10849 let mut stack: Vec<Vec<Sexp>> = vec![
10850 vec![Sexp::symbol("outer")],
10851 vec![Sexp::symbol("inner-a")],
10852 vec![Sexp::symbol("inner-b")],
10853 ];
10854 current_builder_mut(&mut stack).push(Sexp::int(99));
10855 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10856 assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10857 assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10858 }
10859
10860 #[test]
10861 fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10862 // Structural identity binding the lift to its pre-lift inline
10863 // shape: `current_builder_mut(&mut stack)` IS
10864 // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10865 // pointing at the same allocation. Pin pointer equality via
10866 // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10867 // out any allocation-shape drift across the lift.
10868 let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10869 let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10870 let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10871 assert!(
10872 std::ptr::eq(via_lift_ptr, via_inline_ptr),
10873 "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10874 );
10875 }
10876
10877 #[test]
10878 #[should_panic(
10879 expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10880 )]
10881 fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10882 // The bytecode-runtime invariant is encoded in the expect
10883 // message: an empty stack at the projection boundary is
10884 // structurally unreachable inside `apply_compiled`'s op-loop
10885 // (the outermost frame is seeded at entry and every BeginList
10886 // / EndList pair preserves the count >= 1). Pin that the
10887 // NAMED invariant fires on the failure path so a regression
10888 // that drifts the loop's frame management surfaces a
10889 // diagnostic-grade panic rather than a silent unwrap over
10890 // `None`. Authoring tools / future debug-mode hooks can
10891 // pattern-match on the named invariant string instead of
10892 // tracking down an unnamed unwrap site.
10893 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10894 let _ = current_builder_mut(&mut empty);
10895 }
10896
10897 #[test]
10898 fn current_builder_mut_routes_apply_compiled_literal_emit() {
10899 // End-to-end path-uniformity guard: a single-op program
10900 // `TemplateOp::Literal(s)` routes its push through
10901 // `current_builder_mut(&mut stack)` and the literal lands in
10902 // the outermost frame. After the op-loop completes the outer
10903 // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10904 // containing exactly one element, which `apply_compiled`'s
10905 // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10906 // the bound value. Pre-lift the same emission ran through
10907 // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10908 // runs through `current_builder_mut(&mut stack).push(s.clone())`
10909 // — the byte-identical outcome pins that the Literal arm's
10910 // routing through the new projection preserves the bytecode-
10911 // runtime's emission shape.
10912 let tmpl = CompiledTemplate {
10913 ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10914 };
10915 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10916 .expect("literal-only template must succeed");
10917 assert_eq!(out, Sexp::symbol("hello"));
10918 }
10919
10920 #[test]
10921 fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10922 // End-to-end path-uniformity guard for the post-EndList
10923 // parent-fold push: `(BeginList, Literal(a), Literal(b),
10924 // EndList)` builds an inner frame `[a, b]`, pops it on
10925 // EndList, then pushes `Sexp::List([a, b])` into the parent
10926 // (outer) frame via `current_builder_mut`. The outermost
10927 // `stack.pop()` then surfaces that list as the bound result.
10928 // Pre-lift the parent-fold push ran through
10929 // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10930 // lift it runs through `current_builder_mut(&mut stack).
10931 // push(Sexp::List(items))` — pin the byte-identical outcome
10932 // so a regression that drifts the parent-fold target (e.g.,
10933 // pushes onto the just-popped frame's pointer instead of the
10934 // new top) fails loudly here.
10935 let tmpl = CompiledTemplate {
10936 ops: vec![
10937 TemplateOp::BeginList,
10938 TemplateOp::Literal(Sexp::symbol("a")),
10939 TemplateOp::Literal(Sexp::symbol("b")),
10940 TemplateOp::EndList,
10941 ],
10942 };
10943 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10944 .expect("BeginList/EndList template must succeed");
10945 assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10946 }
10947
10948 #[test]
10949 fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10950 // End-to-end path-uniformity guard for BOTH index-reading
10951 // arms routing through the lifted projection: a one-required +
10952 // one-rest macro `(call f &rest args)` with template
10953 // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10954 // Subst's clone-and-push AND Splice's splice-value-into
10955 // emit-paths against the current builder via
10956 // `current_builder_mut(&mut stack)`. The composed result is
10957 // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10958 // Splice flattens `args = (1 2 3)` — and the byte-identical
10959 // outcome pins that BOTH Subst and Splice arms' emits route
10960 // through the SHARED projection. Sibling to
10961 // `apply_compiled_splice_in_range_routes_past_resolve_bound
10962 // _arg_into_splice_value_into` which already exercises this
10963 // shape end-to-end; the addition here is the path-uniformity
10964 // anchor for the `current_builder_mut` lift specifically.
10965 let params = MacroParams {
10966 required: vec!["f".into()],
10967 optional: Vec::new(),
10968 rest: Some("args".into()),
10969 };
10970 let tmpl = CompiledTemplate {
10971 ops: vec![
10972 TemplateOp::BeginList,
10973 TemplateOp::Subst(0),
10974 TemplateOp::Splice(1),
10975 TemplateOp::EndList,
10976 ],
10977 };
10978 let out = apply_compiled(
10979 "call",
10980 ¶ms,
10981 &tmpl,
10982 &[
10983 Sexp::symbol("foo"),
10984 Sexp::int(1),
10985 Sexp::int(2),
10986 Sexp::int(3),
10987 ],
10988 )
10989 .expect("Subst + Splice template must succeed");
10990 assert_eq!(
10991 out,
10992 Sexp::List(vec![
10993 Sexp::symbol("foo"),
10994 Sexp::int(1),
10995 Sexp::int(2),
10996 Sexp::int(3),
10997 ])
10998 );
10999 }
11000
11001 #[test]
11002 fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
11003 // Positive control for the `Splice` arm: a `&rest` macro that
11004 // splices a bound list routes through `resolve_bound_arg`'s
11005 // success arm and the `Splice` post-lookup verb
11006 // (`splice_value_into`) flattens the bound list into the
11007 // builder. Pin the composition's success path end-to-end:
11008 // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
11009 // the outer builder's `(call 1 2 3)` shape — the same output
11010 // `rest_param_splices_with_at` pins through the public
11011 // surface, here pinned with the bytecode-runtime composition
11012 // exposed directly.
11013 let params = MacroParams {
11014 required: vec!["f".into()],
11015 optional: Vec::new(),
11016 rest: Some("args".into()),
11017 };
11018 let tmpl = CompiledTemplate {
11019 ops: vec![
11020 TemplateOp::BeginList,
11021 TemplateOp::Subst(0),
11022 TemplateOp::Splice(1),
11023 TemplateOp::EndList,
11024 ],
11025 };
11026 let out = apply_compiled(
11027 "call",
11028 ¶ms,
11029 &tmpl,
11030 &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
11031 )
11032 .expect("in-range Splice must succeed");
11033 assert_eq!(
11034 out,
11035 Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
11036 );
11037 }
11038
11039 // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
11040 //
11041 // `pop_builder_frame(stack, macro_name, kind)` lifts the
11042 // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
11043 // kind))?` chain that recurred at two sites inside `apply_compiled`
11044 // (the `EndList` arm + the post-loop final pop) into ONE named
11045 // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
11046 // `current_builder_mut` (the top-frame-borrow projection — the same
11047 // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
11048 // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
11049 // also routes through `TemplateInvariantKind` as the per-call-site
11050 // rejection identity). These tests pin the primitive's contract
11051 // directly; the existing `apply_compiled_*` tests are the path-
11052 // uniformity guards proving the two sites route through it without
11053 // behavior drift.
11054
11055 #[test]
11056 fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
11057 // Happy path: a two-frame stack pops the topmost frame off and
11058 // returns it AS-IS while shrinking the stack by exactly one
11059 // element. Pin both the return value (the popped frame's
11060 // contents are byte-identical to what was pushed) AND the
11061 // mutation (`stack.len()` drops from 2 to 1).
11062 let mut stack: Vec<Vec<Sexp>> = vec![
11063 vec![Sexp::symbol("outer")],
11064 vec![Sexp::int(1), Sexp::int(2)],
11065 ];
11066 let popped =
11067 pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
11068 .expect("non-empty stack must pop cleanly");
11069 assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
11070 assert_eq!(stack.len(), 1);
11071 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
11072 }
11073
11074 #[test]
11075 fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
11076 // Empty-stack rejection: an empty stack flows through the
11077 // `EndListEmptyStack` kind constructor into a structural
11078 // `LispError::TemplateInvariant { macro_name, kind:
11079 // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
11080 // the same input would route through the inline
11081 // `stack.pop().ok_or_else(|| template_invariant_violation(_,
11082 // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
11083 // it routes through ONE named primitive both pop-emitting
11084 // sites share, and a regression that drops the kind threading
11085 // (e.g. unifies both kinds into one constant) fails here.
11086 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11087 let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
11088 .expect_err("empty stack must reject");
11089 match err {
11090 LispError::TemplateInvariant { macro_name, kind } => {
11091 assert_eq!(macro_name, "wrap");
11092 assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
11093 }
11094 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11095 }
11096 }
11097
11098 #[test]
11099 fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
11100 // Path-uniformity across the closed-set of pop-emitting kinds:
11101 // the same primitive threads `FinalNoValue` verbatim through
11102 // its `TemplateInvariantKind` slot, with the macro_name
11103 // identity preserved across the call boundary. Sibling of the
11104 // `EndListEmptyStack` test above — together they pin the
11105 // primitive's closed-set posture (BOTH reachable
11106 // pop-emitting kinds route through the same primitive, neither
11107 // is hard-coded), so a future `TemplateInvariantKind` variant
11108 // added for a new pop-emitting op (e.g. a hypothetical
11109 // `EndManyEmptyStack`) extends the primitive's reachability
11110 // mechanically by passing the new kind through the same slot.
11111 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11112 let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
11113 .expect_err("empty stack must reject");
11114 match err {
11115 LispError::TemplateInvariant { macro_name, kind } => {
11116 assert_eq!(macro_name, "id");
11117 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
11118 }
11119 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11120 }
11121 }
11122
11123 #[test]
11124 fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
11125 // Closed-set posture check: even kinds the production
11126 // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
11127 // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
11128 // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
11129 // this primitive's typed slot — they are NOT reachable here
11130 // from the production loop, but `TemplateInvariantKind` does
11131 // not distinguish "indexed" kinds from "stack-gate" kinds at
11132 // the helper's signature, so a regression that special-cases
11133 // one kind family would be a silent type-narrowing the closed-
11134 // set typed enum was lifted to prevent. Pin the universal
11135 // routing: ANY kind variant feeds through the primitive's
11136 // `kind` slot identically. Sibling assertion to
11137 // `template_invariant_violation_threads_subst_idx_through_typed_variant`
11138 // — same compose-the-kind-into-the-variant contract, one
11139 // composition step further down the substrate stack.
11140 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11141 let err = pop_builder_frame(
11142 &mut empty,
11143 "compose",
11144 TemplateInvariantKind::SubstBadIndex(42),
11145 )
11146 .expect_err("empty stack must reject regardless of kind family");
11147 match err {
11148 LispError::TemplateInvariant { macro_name, kind } => {
11149 assert_eq!(macro_name, "compose");
11150 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11151 }
11152 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11153 }
11154 }
11155
11156 #[test]
11157 fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11158 // Structural-identity binding the lift to its pre-lift inline
11159 // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11160 // `stack.pop().ok_or_else(|| template_invariant_violation(
11161 // macro_name, kind))?` — both reachable arms (success +
11162 // failure) must produce byte-identical outcomes. The success
11163 // arm checks the popped Vec contents AND the post-pop stack
11164 // length match the inline path; the failure arm checks the
11165 // emitted variant's identity AND its `macro_name` / `kind`
11166 // slots match the inline path. A regression that drifts the
11167 // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11168 // or a kind-rewrite at the helper boundary) fails on at least
11169 // one of the two arms.
11170 // Success arm:
11171 let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11172 let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11173 let via_lift = pop_builder_frame(
11174 &mut stack_lift,
11175 "macro",
11176 TemplateInvariantKind::EndListEmptyStack,
11177 )
11178 .expect("non-empty stack pops cleanly through lift");
11179 let via_inline = stack_inline.pop().ok_or_else(|| {
11180 template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11181 });
11182 assert_eq!(
11183 via_lift,
11184 via_inline.unwrap(),
11185 "popped frame must be byte-identical across lift vs inline"
11186 );
11187 assert_eq!(
11188 stack_lift.len(),
11189 stack_inline.len(),
11190 "post-pop stack length must be byte-identical across lift vs inline"
11191 );
11192 // Failure arm:
11193 let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11194 let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11195 let err_lift = pop_builder_frame(
11196 &mut empty_lift,
11197 "macro",
11198 TemplateInvariantKind::FinalNoValue,
11199 )
11200 .expect_err("empty stack rejects through lift");
11201 let err_inline = empty_inline
11202 .pop()
11203 .ok_or_else(|| {
11204 template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11205 })
11206 .expect_err("empty stack rejects through inline");
11207 match (err_lift, err_inline) {
11208 (
11209 LispError::TemplateInvariant {
11210 macro_name: m_lift,
11211 kind: k_lift,
11212 },
11213 LispError::TemplateInvariant {
11214 macro_name: m_inline,
11215 kind: k_inline,
11216 },
11217 ) => {
11218 assert_eq!(m_lift, m_inline);
11219 assert_eq!(k_lift, k_inline);
11220 }
11221 (l, i) => panic!(
11222 "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11223 ),
11224 }
11225 }
11226
11227 #[test]
11228 fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11229 // End-to-end path-uniformity guard for the `EndList` arm: a
11230 // `(BeginList, Literal(x), EndList)` program pushes a child
11231 // frame, populates it with one literal, then routes the child
11232 // frame OUT of the stack via `pop_builder_frame` (kind
11233 // `EndListEmptyStack` — unreachable on this valid input, but
11234 // the kind threads through the primitive identically). The
11235 // post-pop verb (`Sexp::List(items)` push into the parent via
11236 // `current_builder_mut`) yields the same `Sexp::List([x])`
11237 // shape the consumer projected pre-lift. Pin the byte-
11238 // identical outcome so a regression that drifts the EndList
11239 // arm's routing (e.g. swaps the kind constructor, or routes
11240 // through a different stack-mutating primitive) fails loudly
11241 // here. Sibling of
11242 // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11243 // — that test pins the parent-fold PUSH; this test pins the
11244 // child-frame POP that immediately precedes it. Together the
11245 // two close the EndList arm's path-uniformity across the
11246 // pop-then-push composition.
11247 let tmpl = CompiledTemplate {
11248 ops: vec![
11249 TemplateOp::BeginList,
11250 TemplateOp::Literal(Sexp::symbol("only")),
11251 TemplateOp::EndList,
11252 ],
11253 };
11254 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11255 .expect("BeginList/EndList one-literal template must succeed");
11256 assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11257 }
11258
11259 #[test]
11260 fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11261 // End-to-end path-uniformity guard for the post-loop final
11262 // pop: a single-op `Literal(s)` program emits `s` into the
11263 // outermost (seed) frame, then the post-loop tail routes the
11264 // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11265 // — unreachable on this valid input). The post-pop arity gate
11266 // (`top.len() == 1 { top.remove(0) }`) projects the literal
11267 // back as the bound value. Pre-lift the same emission ran
11268 // through the inline `stack.pop().ok_or_else(|| template_
11269 // invariant_violation(_, FinalNoValue))?` chain at the
11270 // post-loop tail; post-lift it routes through ONE named
11271 // primitive the EndList arm ALSO routes through, and the
11272 // single-literal outcome is byte-identical across both code
11273 // paths. Sibling of
11274 // `current_builder_mut_routes_apply_compiled_literal_emit` —
11275 // that test pins the EMIT into the seed frame; this test pins
11276 // the POP of the same seed frame at the post-loop tail.
11277 // Together the two close the Literal-only program's path-
11278 // uniformity across the emit-then-consume composition.
11279 let tmpl = CompiledTemplate {
11280 ops: vec![TemplateOp::Literal(Sexp::int(123))],
11281 };
11282 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11283 .expect("literal-only template must succeed");
11284 assert_eq!(out, Sexp::int(123));
11285 }
11286
11287 // ── MacroParams: the typed param-list primitive ─────────────────────
11288 //
11289 // `parse_params` now yields a `MacroParams { required, optional, rest }`
11290 // whose shape makes the canonical lambda-list ordering (required →
11291 // optional → rest, "&rest is last + at-most-one", "&optional at most
11292 // once") structural rather than a construction discipline a `Vec<Param>`
11293 // only happened to uphold. These tests pin the parser's mapping into the
11294 // typed shape, the flat-index contract `names()` exposes to the template
11295 // bytecode, and the single positional binder `bind()` both expansion
11296 // strategies now route through. The end-to-end `rest_param_splices_with_at`
11297 // and `compiled_template_matches_substitute_path` tests above are the
11298 // path-uniformity guards proving both strategies still agree.
11299
11300 #[test]
11301 fn parse_params_maps_required_then_rest_into_typed_shape() {
11302 // `(a b &rest c)` — two required, one rest. The rest name lands in
11303 // the `Option`, never in `required`.
11304 let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11305 assert_eq!(
11306 params,
11307 MacroParams {
11308 required: vec!["a".into(), "b".into()],
11309 optional: Vec::new(),
11310 rest: Some("c".into()),
11311 }
11312 );
11313 }
11314
11315 #[test]
11316 fn parse_params_rest_absent_leaves_none() {
11317 // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11318 // no representation in which a rest-less list carries a stray rest.
11319 let params = parse_params(&read("x y").unwrap()).unwrap();
11320 assert_eq!(
11321 params,
11322 MacroParams {
11323 required: vec!["x".into(), "y".into()],
11324 optional: Vec::new(),
11325 rest: None,
11326 }
11327 );
11328 }
11329
11330 #[test]
11331 fn parse_params_maps_optional_section_between_required_and_rest() {
11332 // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11333 // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11334 // marker switches collection from `required` to `optional`; `&rest`
11335 // remains terminal.
11336 let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11337 assert_eq!(
11338 params,
11339 MacroParams {
11340 required: vec!["a".into()],
11341 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11342 rest: Some("d".into()),
11343 }
11344 );
11345 }
11346
11347 #[test]
11348 fn parse_params_optional_with_no_rest_leaves_rest_none() {
11349 // `(&optional x)` — a leading `&optional` (zero required) with no
11350 // rest. `required` is empty, `x` is the sole optional, `rest` None.
11351 let params = parse_params(&read("&optional x").unwrap()).unwrap();
11352 assert_eq!(
11353 params,
11354 MacroParams {
11355 required: Vec::new(),
11356 optional: vec![OptionalParam::bare("x")],
11357 rest: None,
11358 }
11359 );
11360 }
11361
11362 #[test]
11363 fn parse_params_rejects_repeated_optional_marker() {
11364 // `(a &optional b &optional c)` — a second `&optional` is
11365 // unrepresentable (one flat optional section), so the gate REJECTS
11366 // rather than binding args to a marker symbol named `&optional`. The
11367 // two marker positions (1 and 3) are named.
11368 let err = parse_params(&read("a &optional b &optional c").unwrap())
11369 .expect_err("repeated &optional must error");
11370 assert!(
11371 matches!(
11372 err,
11373 LispError::OptionalMarkerRepeated {
11374 first_position: 1,
11375 second_position: 3,
11376 }
11377 ),
11378 "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11379 );
11380 }
11381
11382 #[test]
11383 fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11384 // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11385 // `&optional y` tail is REJECTED as trailing tokens (not silently
11386 // dropped, and not a repeated-optional error: the rest gate fires
11387 // first). Pins the interaction the prior run (3627426) signposted.
11388 let err = parse_params(&read("&rest xs &optional y").unwrap())
11389 .expect_err("tokens after &rest <name> must error");
11390 assert!(
11391 matches!(err, LispError::RestParamTrailingTokens { .. }),
11392 "expected RestParamTrailingTokens, got: {err:?}"
11393 );
11394 }
11395
11396 #[test]
11397 fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11398 // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11399 // depends on: required names at 0.., then optional names, then the
11400 // rest name last.
11401 let params = MacroParams {
11402 required: vec!["a".into(), "b".into()],
11403 optional: vec![OptionalParam::bare("c")],
11404 rest: Some("d".into()),
11405 };
11406 assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11407 // Optional names occupy the indices immediately after the required run.
11408 assert_eq!(params.names()[params.required.len()], "c");
11409 // The rest name is last, after required + optional — i.e. at the
11410 // structural `fixed_arity()` boundary the typed primitive names.
11411 assert_eq!(params.names()[params.fixed_arity()], "d");
11412 }
11413
11414 // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11415 // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11416 // marker algebra ────────────────────────────────────────────────
11417 //
11418 // The three `pub const`s close the Common-Lisp lambda-list keyword
11419 // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11420 // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11421 // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11422 // canonical `'&'` char shared as the LEAD byte of both markers.
11423 // Pre-lift the same two `&'static str` markers lived as two inline
11424 // `s == "..."` comparisons at `parse_params` — post-lift both
11425 // comparisons route through the typed constants so a delimiter swap
11426 // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11427 // lands at ONE constant on the typed algebra.
11428 //
11429 // These pins cover: (a) the exact `&'static str` / `char` values,
11430 // (b) the structural round-trip law binding each `&'static str`
11431 // marker to its shared `char` LEAD byte, (c) the pairwise
11432 // disjointness of the two `&'static str` markers, (d) the cross-
11433 // axis disjointness of the `char` LEAD byte against every sibling
11434 // outer-marker `char` the substrate's other closed-set algebras
11435 // specialise on, and (e) the path-uniformity of `parse_params`
11436 // through the typed constants.
11437
11438 #[test]
11439 fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11440 // Pins the constant's exact `&'static str` bytes so a typo
11441 // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11442 // surfaces immediately. Sibling-shape pin to
11443 // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11444 // on the peer `&optional` axis.
11445 assert_eq!(
11446 MacroParams::REST_MARKER,
11447 "&rest",
11448 "MacroParams::REST_MARKER drifted from the substrate- \
11449 canonical CL lambda-list `&rest` marker — the parser's \
11450 `parse_params` rest-slot dispatch AND every downstream \
11451 authoring / rendering surface binds to this ONE typed \
11452 constant.",
11453 );
11454 }
11455
11456 #[test]
11457 fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11458 // Pins the constant's exact `&'static str` bytes so a typo
11459 // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11460 // redefinition surfaces immediately. Sibling-shape pin to
11461 // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11462 // on the peer `&rest` axis.
11463 assert_eq!(
11464 MacroParams::OPTIONAL_MARKER,
11465 "&optional",
11466 "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11467 canonical CL lambda-list `&optional` marker — the parser's \
11468 `parse_params` optional-section dispatch AND every \
11469 downstream authoring / rendering surface binds to this \
11470 ONE typed constant.",
11471 );
11472 }
11473
11474 #[test]
11475 fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11476 // Pins the constant's exact `char` value so a typo (`'#'`,
11477 // `'@'`, `'!'`) or an accidental redefinition surfaces
11478 // immediately. Sibling-shape pin to
11479 // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11480 // `atom_bool_literal_lead_projects_canonical_hash_char`,
11481 // `atom_str_delimiter_projects_canonical_double_quote_char`
11482 // on the peer per-role LEAD-byte axes across the substrate's
11483 // closed-set outer algebras.
11484 assert_eq!(
11485 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11486 '&',
11487 "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11488 canonical `&` LEAD byte — the CL lambda-list-keyword \
11489 family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11490 typed constant as their common LEAD byte.",
11491 );
11492 }
11493
11494 #[test]
11495 fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11496 // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11497 // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11498 // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11499 // binding the two typed constants on the [`MacroParams`]
11500 // algebra. A regression that renames the `&'static str` (e.g.
11501 // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11502 // `'#'` for the `#!` shebang lead) without updating the other
11503 // fails HERE. Sibling-shape pin to
11504 // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11505 // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11506 assert!(
11507 MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11508 "MacroParams::REST_MARKER `{}` does NOT start with \
11509 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11510 typed constants have drifted apart on the [`MacroParams`] \
11511 algebra; the CL lambda-list-keyword family disjointness \
11512 contract can no longer bind to ONE shared LEAD byte.",
11513 MacroParams::REST_MARKER,
11514 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11515 );
11516 }
11517
11518 #[test]
11519 fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11520 // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11521 // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11522 // on the `&optional` axis. Both `&'static str` markers of the
11523 // CL lambda-list-keyword family MUST share the canonical `char`
11524 // LEAD byte so the typed-marker disjointness contract can bind
11525 // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11526 assert!(
11527 MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11528 "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11529 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11530 typed constants have drifted apart on the [`MacroParams`] \
11531 algebra; the CL lambda-list-keyword family disjointness \
11532 contract can no longer bind to ONE shared LEAD byte.",
11533 MacroParams::OPTIONAL_MARKER,
11534 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11535 );
11536 }
11537
11538 #[test]
11539 fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11540 // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11541 // the CL lambda-list-keyword algebra MUST differ so the
11542 // parser's typed dispatch cascade at `parse_params` (which
11543 // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11544 // silently route both dispatches through the same arm. A
11545 // regression that aliases the two markers (e.g. both to
11546 // `"&rest"` after a typo) would silently drop the optional
11547 // section's structural distinction — every `&optional` name
11548 // would misclassify as a rest-slot marker.
11549 assert_ne!(
11550 MacroParams::REST_MARKER,
11551 MacroParams::OPTIONAL_MARKER,
11552 "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11553 typed dispatch cascade at `parse_params` can no longer \
11554 distinguish the rest-slot boundary from the optional- \
11555 section boundary.",
11556 );
11557 }
11558
11559 #[test]
11560 fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11561 // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11562 // MUST NOT alias any sibling outer-marker `char` on the
11563 // substrate's other closed-set algebras — the Atom-payload
11564 // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11565 // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11566 // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11567 // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11568 // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11569 // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11570 // collision would silently break the reader's outer dispatch:
11571 // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11572 // with whichever marker it aliased. Sibling-shape pin to
11573 // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11574 // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11575 // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11576 // axis. A future outer-marker extension that collided with
11577 // `'&'` fails HERE at the cross-axis enumeration.
11578 use crate::ast::{Atom, QuoteForm, Sexp};
11579
11580 assert_ne!(
11581 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11582 Atom::STR_DELIMITER,
11583 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11584 bare `&rest` at a param-list position would ambiguously \
11585 begin a lambda-list keyword AND open a string.",
11586 );
11587 assert_ne!(
11588 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11589 Atom::STR_ESCAPE_LEAD,
11590 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11591 the reader's Str-escape lead byte would alias the CL \
11592 lambda-list-keyword LEAD byte.",
11593 );
11594 assert_ne!(
11595 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11596 Atom::KEYWORD_MARKER_LEAD,
11597 "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11598 — a bare `&rest` at a param-list position would \
11599 ambiguously begin a lambda-list keyword AND begin an \
11600 `:foo` keyword.",
11601 );
11602 assert_ne!(
11603 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11604 Atom::BOOL_LITERAL_LEAD,
11605 "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11606 a bare `&rest` at a param-list position would ambiguously \
11607 begin a lambda-list keyword AND classify as a Bool prefix.",
11608 );
11609 assert_ne!(
11610 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11611 Sexp::LIST_OPEN,
11612 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11613 `&rest` at a param-list position would ambiguously begin \
11614 a lambda-list keyword AND open a list.",
11615 );
11616 assert_ne!(
11617 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11618 Sexp::LIST_CLOSE,
11619 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11620 `&rest` at a param-list position would ambiguously begin \
11621 a lambda-list keyword AND close a list.",
11622 );
11623 assert_ne!(
11624 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11625 Sexp::COMMENT_LEAD,
11626 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11627 bare `&rest` at a param-list position would ambiguously \
11628 begin a lambda-list keyword AND begin a comment.",
11629 );
11630 assert_ne!(
11631 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11632 Sexp::COMMENT_TERM,
11633 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11634 reader's line-comment discard loop would terminate on the \
11635 SAME byte the parser's lambda-list-keyword LEAD dispatch \
11636 binds to.",
11637 );
11638 for qf in QuoteForm::ALL {
11639 assert_ne!(
11640 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11641 qf.lead_char(),
11642 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11643 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11644 param-list position would ambiguously begin a lambda- \
11645 list keyword AND begin a quote-family prefix.",
11646 );
11647 }
11648 assert_ne!(
11649 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11650 QuoteForm::SPLICE_DISCRIMINATOR,
11651 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11652 SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11653 peek byte would alias the CL lambda-list-keyword LEAD \
11654 byte.",
11655 );
11656 }
11657
11658 #[test]
11659 fn parse_params_recognizes_rest_marker_via_typed_constant() {
11660 // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11661 // `parse_params` MUST classify the typed constant
11662 // `MacroParams::REST_MARKER` as the rest-slot boundary —
11663 // authoring surfaces that assemble a param list through the
11664 // typed constant (rather than through an inline string literal
11665 // read from user source) must reach the SAME rest-slot binding
11666 // the reader-driven path does.
11667 //
11668 // The `read(REST_MARKER)` composition here is the load-bearing
11669 // structural link: we build the param-list source through the
11670 // typed constant, then parse it through the reader, then the
11671 // parser MUST bind `xs` at the `rest` slot. A regression that
11672 // drifts the parser's dispatch away from the typed constant
11673 // (e.g. re-inlines a string literal that goes stale against
11674 // the constant) fails HERE.
11675 let src = format!("a {} xs", MacroParams::REST_MARKER);
11676 let params = parse_params(&read(&src).unwrap()).unwrap();
11677 assert_eq!(
11678 params,
11679 MacroParams {
11680 required: vec!["a".into()],
11681 optional: Vec::new(),
11682 rest: Some("xs".into()),
11683 },
11684 "parse_params dispatch drifted away from REST_MARKER — the \
11685 typed constant no longer routes to the rest-slot arm.",
11686 );
11687 }
11688
11689 #[test]
11690 fn parse_params_recognizes_optional_marker_via_typed_constant() {
11691 // PATH-UNIFORMITY PIN: peer to
11692 // `parse_params_recognizes_rest_marker_via_typed_constant` on
11693 // the `&optional` axis. The parser's optional-section dispatch
11694 // at `parse_params` MUST classify the typed constant
11695 // `MacroParams::OPTIONAL_MARKER` as the section-switch
11696 // boundary that reroutes subsequent bare-symbol names from the
11697 // `required` bin to the `optional` bin.
11698 let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11699 let params = parse_params(&read(&src).unwrap()).unwrap();
11700 assert_eq!(
11701 params,
11702 MacroParams {
11703 required: vec!["a".into()],
11704 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11705 rest: None,
11706 },
11707 "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11708 the typed constant no longer routes to the optional- \
11709 section arm.",
11710 );
11711 }
11712
11713 // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11714 // the closed-set forced-arity ALL array + membership gate that closes
11715 // the CL lambda-list-keyword family at the typed [`MacroParams`]
11716 // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11717 // N]` forced-arity arrays across the substrate's other closed-set
11718 // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11719 // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11720 // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11721 // const` for the marker and every downstream family-wide contract
11722 // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11723 // gate) picks up the extension at ONE structural site.
11724 //
11725 // These pins cover: (a) the cardinality contract (the ALL array's
11726 // length matches the pre-lift per-role marker count), (b) the
11727 // ordering contract (each ALL entry matches its corresponding per-
11728 // role `pub const` by-index), (c) the family-wide structural round-
11729 // trip (every ALL element starts_with LEAD), (d) the family-wide
11730 // pairwise disjointness (every distinct ALL index pair yields
11731 // distinct markers), (e) the membership-gate acceptance side (every
11732 // ALL element classifies as `true`), and (f) the membership-gate
11733 // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11734 // names classify as `false`).
11735 //
11736 // Fail-before/pass-after: every test below references
11737 // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11738 // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11739 // on `MacroParams` before this lift — every assertion was a
11740 // compile-time error against the prior surface.
11741
11742 #[test]
11743 fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11744 // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11745 // entries — the two `&'static str` markers the parser's typed
11746 // dispatch specialises on today (`&rest` + `&optional`). A future
11747 // third marker (`&key`, `&aux`, `&body`) extends this arity to
11748 // 3 AND every downstream family-wide contract sweep picks up the
11749 // extension at ONE structural site. A regression that drops a
11750 // marker from the ALL array (silently narrowing the family) OR
11751 // aliases two markers to the same index (silently narrowing the
11752 // typed dispatch) fails HERE at the cardinality contract before
11753 // the structural round-trip / pairwise-disjointness contracts
11754 // even fire.
11755 assert_eq!(
11756 MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11757 2,
11758 "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11759 lambda-list-keyword family closure now names a different \
11760 number of markers than the two the parser's typed dispatch \
11761 specialises on.",
11762 );
11763 }
11764
11765 #[test]
11766 fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11767 // ORDERING PIN: each ALL entry matches its corresponding per-role
11768 // `pub const` by-index. A regression that swaps the ordering
11769 // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11770 // the cardinality + membership contracts intact but silently
11771 // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11772 // in canonical order (a future authoring surface that renders
11773 // "supported CL lambda-list keywords: &rest, &optional" would
11774 // silently render "supported CL lambda-list keywords: &optional,
11775 // &rest" instead). Pins the array's declaration-order binding
11776 // structurally so a reorder fails HERE at each element rather
11777 // than only at consumer sites downstream.
11778 assert_eq!(
11779 MacroParams::LAMBDA_LIST_KEYWORDS[0],
11780 MacroParams::REST_MARKER,
11781 "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11782 array's declaration-order binding to the per-role `pub \
11783 const` broke at the rest-slot marker slot.",
11784 );
11785 assert_eq!(
11786 MacroParams::LAMBDA_LIST_KEYWORDS[1],
11787 MacroParams::OPTIONAL_MARKER,
11788 "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11789 ALL array's declaration-order binding to the per-role \
11790 `pub const` broke at the optional-section marker slot.",
11791 );
11792 }
11793
11794 #[test]
11795 fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11796 // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11797 // array MUST start with the canonical LEAD byte. Where the two
11798 // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11799 // named ONE marker inline as a duplicate 3-line
11800 // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11801 // sweep routes the SAME contract through the closed-set ALL
11802 // array — a future third marker (`&key`, `&aux`, `&body`)
11803 // extends the array AND is automatically covered by this sweep
11804 // without adding a third `_prefixed_by_...` per-marker pin. The
11805 // per-marker pins stay as fine-grained fail-loud sites; this
11806 // pin is the algebra-wide closure the family-extending refactor
11807 // routes through.
11808 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11809 assert!(
11810 m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11811 "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11812 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11813 list-keyword family's structural round-trip contract \
11814 no longer binds every marker to its shared LEAD byte.",
11815 lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11816 );
11817 }
11818 }
11819
11820 #[test]
11821 fn macro_params_lambda_list_keywords_pairwise_distinct() {
11822 // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11823 // `(i, j)` in the ALL array yields distinct markers. Where the
11824 // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11825 // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11826 // family-wide sweep routes the SAME contract through the closed-
11827 // set ALL array so a future third marker automatically extends
11828 // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11829 // deriving per-marker `assert_ne!` calls. The pre-lift pin
11830 // stays as the fine-grained fail-loud site; this pin is the
11831 // family-wide closure the algebra-extending refactor routes
11832 // through.
11833 for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11834 for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11835 if i == j {
11836 continue;
11837 }
11838 assert_ne!(
11839 a, b,
11840 "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11841 LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11842 list-keyword family's pairwise disjointness \
11843 contract no longer binds distinct index pairs \
11844 to distinct markers.",
11845 );
11846 }
11847 }
11848 }
11849
11850 #[test]
11851 fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11852 // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11853 // MUST classify as `true` through the typed membership gate. The
11854 // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11855 // pin is the structural closure binding the gate's return value
11856 // to the ALL array's element set. A regression that specialises
11857 // the gate to a subset of the array (e.g. a `matches!(s,
11858 // "&rest")` inline that silently drops the `&optional` branch)
11859 // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11860 // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11861 // element of `Self::ALL` decodes through the projection back to
11862 // itself.
11863 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11864 assert!(
11865 MacroParams::is_lambda_list_keyword(m),
11866 "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11867 element `{m}` — the closed-set membership gate's \
11868 acceptance side drifted from the ALL array.",
11869 );
11870 }
11871 }
11872
11873 #[test]
11874 fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11875 // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11876 // byte `"&"` MUST classify as `false`. A future Clojure-compat
11877 // port might land a bare `&` marker — but until such an
11878 // extension explicitly lands on the ALL array, the substrate
11879 // MUST NOT silently classify the LEAD byte alone as a
11880 // recognised CL lambda-list keyword. Pins the gate's
11881 // rejection-side contract against a plausible future
11882 // near-miss extension.
11883 let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11884 assert!(
11885 !MacroParams::is_lambda_list_keyword(&bare_lead),
11886 "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11887 the closed-set membership gate silently classifies the bare `&` \
11888 LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11889 array containing only the two suffixed markers.",
11890 );
11891 }
11892
11893 #[test]
11894 fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11895 // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11896 // three plausible future CL lambda-list keywords (`&key` for
11897 // keyword-args, `&aux` for auxiliaries, `&body` for the
11898 // docstring-carrying tail of `defmacro` bodies) MUST classify as
11899 // `false` UNTIL they explicitly land on the ALL array. Pins the
11900 // gate's conservative rejection contract — the closed-set
11901 // membership gate is not a "starts with `&`" heuristic; it is a
11902 // typed enumeration over the currently-recognised markers. A
11903 // regression that silently loosens the gate to a `starts_with`
11904 // check (dropping the closed-set discipline) fails HERE.
11905 for candidate in ["&key", "&aux", "&body"] {
11906 assert!(
11907 !MacroParams::is_lambda_list_keyword(candidate),
11908 "is_lambda_list_keyword accepted the unrecognised \
11909 `&`-prefixed name `{candidate}` — the closed-set \
11910 membership gate silently loosened its acceptance beyond \
11911 the two markers the ALL array names today.",
11912 );
11913 }
11914 }
11915
11916 #[test]
11917 fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11918 // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11919 // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11920 // MUST classify as `false`. These are the shapes the parser's
11921 // bare-symbol dispatch cascade sees at `parse_params` when
11922 // walking a param list, so pinning them here binds the
11923 // membership gate's rejection contract to the parser's most
11924 // common non-marker input shapes.
11925 for candidate in ["a", "xs", "foo", ""] {
11926 assert!(
11927 !MacroParams::is_lambda_list_keyword(candidate),
11928 "is_lambda_list_keyword accepted the bare non-marker \
11929 input `{candidate}` — the closed-set membership gate's \
11930 rejection side no longer excludes bare identifiers \
11931 the parser routes through the fall-through cascade.",
11932 );
11933 }
11934 }
11935
11936 // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11937 //
11938 // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11939 // arithmetic that recurred three times inside `MacroParams::bind` — at
11940 // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11941 // to get the bound-values count), at the `rest_start` site (inside the
11942 // `if let Some(rest)` branch), and at the `expected` site (inside the
11943 // rest-less `else`). The latter two sites live in mutually-exclusive
11944 // branches yet name ONE structural concept; lifting them collapses the
11945 // arithmetic to one named primitive. These tests pin the primitive's
11946 // contract directly; the existing `bind_*` tests are the path-uniformity
11947 // guards proving `bind`'s sites route through the same value without
11948 // behavior drift.
11949 //
11950 // Fail-before/pass-after: every test below references
11951 // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11952 // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11953 // line was a compile-time error against the prior surface.
11954
11955 #[test]
11956 fn fixed_arity_is_zero_for_the_empty_param_list() {
11957 // `()` — a nullary macro has fixed arity 0, the rest-less binder
11958 // boundary at which the FIRST surplus arg already rejects.
11959 let params = MacroParams::default();
11960 assert_eq!(params.fixed_arity(), 0);
11961 }
11962
11963 #[test]
11964 fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11965 // `(a b c)` — three required, no optional, no rest. fixed_arity is
11966 // exactly the required length.
11967 let params = MacroParams {
11968 required: vec!["a".into(), "b".into(), "c".into()],
11969 optional: Vec::new(),
11970 rest: None,
11971 };
11972 assert_eq!(params.fixed_arity(), 3);
11973 }
11974
11975 #[test]
11976 fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11977 // `(&optional x y)` — two optional, no required. fixed_arity is the
11978 // optional length; the optional section participates in the fixed
11979 // run because supplied positional args bind to it.
11980 let params = MacroParams {
11981 required: Vec::new(),
11982 optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11983 rest: None,
11984 };
11985 assert_eq!(params.fixed_arity(), 2);
11986 }
11987
11988 #[test]
11989 fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11990 // `(a b &optional c d e)` — two required + three optional, no rest.
11991 // fixed_arity is 5: the maximum arity a rest-less call can supply.
11992 let params = MacroParams {
11993 required: vec!["a".into(), "b".into()],
11994 optional: vec![
11995 OptionalParam::bare("c"),
11996 OptionalParam::bare("d"),
11997 OptionalParam::bare("e"),
11998 ],
11999 rest: None,
12000 };
12001 assert_eq!(params.fixed_arity(), 5);
12002 }
12003
12004 #[test]
12005 fn fixed_arity_ignores_rest_slot_by_construction() {
12006 // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
12007 // arity (2). The `&rest` slot has NO maximum and is structurally
12008 // excluded from `fixed_arity`. Naming this invariant pins that a
12009 // regression that drifts the primitive to "required + optional +
12010 // rest.is_some() as usize" fails loudly here — that drift would
12011 // collapse `fixed_arity` into `names().len()`, losing the rest-start
12012 // vs total-bound-values distinction the typed shape relies on.
12013 let with_rest = MacroParams {
12014 required: vec!["a".into()],
12015 optional: vec![OptionalParam::bare("b")],
12016 rest: Some("r".into()),
12017 };
12018 let without_rest = MacroParams {
12019 required: vec!["a".into()],
12020 optional: vec![OptionalParam::bare("b")],
12021 rest: None,
12022 };
12023 assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
12024 assert_eq!(with_rest.fixed_arity(), 2);
12025 }
12026
12027 #[test]
12028 fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
12029 // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
12030 // — the rest-start reading of the primitive. Same arithmetic the
12031 // bytecode index would hit (`Subst(fixed_arity())` resolves to the
12032 // rest-bound `Sexp::List`).
12033 let params = MacroParams {
12034 required: vec!["a".into(), "b".into()],
12035 optional: vec![OptionalParam::bare("c")],
12036 rest: Some("r".into()),
12037 };
12038 assert_eq!(params.fixed_arity(), 3);
12039 assert_eq!(params.names()[params.fixed_arity()], "r");
12040 }
12041
12042 #[test]
12043 fn fixed_arity_equals_names_length_when_rest_is_absent() {
12044 // When `rest` is `None`, `names().len() == fixed_arity()` — there
12045 // is no rest-name slot to extend the flat run past the fixed
12046 // boundary. Pins the structural identity
12047 // `names().len() == fixed_arity() + usize::from(rest.is_some())`
12048 // for the rest-less case; the rest-present case is pinned by the
12049 // sibling test above (where the boundary is the rest-name index,
12050 // i.e. one short of `names().len()`).
12051 let params = MacroParams {
12052 required: vec!["a".into(), "b".into()],
12053 optional: vec![OptionalParam::bare("c")],
12054 rest: None,
12055 };
12056 assert_eq!(params.names().len(), params.fixed_arity());
12057 assert_eq!(params.names().len(), 3);
12058 }
12059
12060 #[test]
12061 fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
12062 // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
12063 // the rest-less binder rejects iff `args.len() > fixed_arity()`.
12064 // This pin is the path-uniformity guard binding the typed primitive
12065 // to the binder's rejection contract: a regression that drifts
12066 // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
12067 // surface a different boundary in the diagnostic without touching
12068 // the primitive — and this assertion fails loudly. Mirror of the
12069 // sibling rest-less surplus pin (`bind_rest_less_params_reject_
12070 // surplus_args`); this test pins WHAT the `expected` slot's value
12071 // structurally IS, that pin checks the variant SHAPE.
12072 let params = MacroParams {
12073 required: vec!["a".into(), "b".into()],
12074 optional: vec![OptionalParam::bare("c")],
12075 rest: None,
12076 };
12077 assert_eq!(params.fixed_arity(), 3);
12078 let err = params
12079 .bind(
12080 "m",
12081 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12082 )
12083 .expect_err("4 args against fixed_arity 3 must reject");
12084 match err {
12085 LispError::TooManyMacroArgs {
12086 expected,
12087 got,
12088 macro_name,
12089 } => {
12090 assert_eq!(expected, params.fixed_arity());
12091 assert_eq!(got, 4);
12092 assert_eq!(macro_name, "m");
12093 }
12094 other => panic!("expected TooManyMacroArgs, got {other:?}"),
12095 }
12096 }
12097
12098 #[test]
12099 fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
12100 // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
12101 // into the rest's `Sexp::List`. Pin that the rest list contents
12102 // are exactly the suffix beginning at `fixed_arity()` — the
12103 // primitive's rest-start reading IS the slice index the binder
12104 // consumes. A regression that drifts `bind`'s rest-collection
12105 // slice from `fixed_arity()` would surface as a misaligned rest
12106 // list (off-by-one in either direction) and this assertion fails
12107 // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
12108 // rejection_boundary` on the rest-PRESENT branch.
12109 let params = MacroParams {
12110 required: vec!["a".into()],
12111 optional: vec![OptionalParam::bare("b")],
12112 rest: Some("r".into()),
12113 };
12114 assert_eq!(params.fixed_arity(), 2);
12115 let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
12116 let vals = params.bind("m", &args).unwrap();
12117 // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
12118 let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
12119 assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
12120 }
12121
12122 #[test]
12123 fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
12124 // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
12125 // The rest slot collects the empty slice; bind succeeds, never
12126 // misaligning to an off-by-one underflow. Pin the boundary on the
12127 // rest-PRESENT path — the rest-less mirror is
12128 // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
12129 let params = MacroParams {
12130 required: vec!["a".into()],
12131 optional: vec![OptionalParam::bare("b")],
12132 rest: Some("r".into()),
12133 };
12134 assert_eq!(params.fixed_arity(), 2);
12135 let vals = params
12136 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12137 .expect("rest-present at exact fixed_arity must bind cleanly");
12138 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12139 }
12140
12141 #[test]
12142 fn bind_threads_required_positionally_and_collects_rest_as_list() {
12143 // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12144 // vec is parallel to `names()`, so the rest list sits at the rest's
12145 // flat index.
12146 let params = MacroParams {
12147 required: vec!["a".into(), "b".into()],
12148 optional: Vec::new(),
12149 rest: Some("c".into()),
12150 };
12151 let vals = params
12152 .bind(
12153 "m",
12154 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12155 )
12156 .unwrap();
12157 assert_eq!(
12158 vals,
12159 vec![
12160 Sexp::int(1),
12161 Sexp::int(2),
12162 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12163 ]
12164 );
12165 }
12166
12167 #[test]
12168 fn bind_supplied_optional_takes_its_positional_arg() {
12169 // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12170 // behaves exactly like a positional — only its ABSENCE differs.
12171 let params = MacroParams {
12172 required: vec!["a".into()],
12173 optional: vec![OptionalParam::bare("b")],
12174 rest: None,
12175 };
12176 let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12177 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12178 }
12179
12180 #[test]
12181 fn bind_unsupplied_optional_defaults_to_nil() {
12182 // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12183 // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12184 // no supplied default-form. The bound vec is still parallel to
12185 // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12186 // missing-arg error.
12187 let params = MacroParams {
12188 required: vec!["a".into()],
12189 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12190 rest: None,
12191 };
12192 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12193 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12194 }
12195
12196 #[test]
12197 fn bind_rest_collects_args_beyond_required_and_optional() {
12198 // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12199 // c=(3 4). The rest starts AFTER the required+optional run, so the
12200 // optional's supplied arg is not swept into the rest.
12201 let params = MacroParams {
12202 required: vec!["a".into()],
12203 optional: vec![OptionalParam::bare("b")],
12204 rest: Some("c".into()),
12205 };
12206 let vals = params
12207 .bind(
12208 "m",
12209 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12210 )
12211 .unwrap();
12212 assert_eq!(
12213 vals,
12214 vec![
12215 Sexp::int(1),
12216 Sexp::int(2),
12217 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12218 ]
12219 );
12220 }
12221
12222 #[test]
12223 fn bind_unsupplied_optional_then_empty_rest() {
12224 // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12225 // c=() (nothing left). Both the optional default AND the empty-rest
12226 // contract hold in the same bind.
12227 let params = MacroParams {
12228 required: vec!["a".into()],
12229 optional: vec![OptionalParam::bare("b")],
12230 rest: Some("c".into()),
12231 };
12232 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12233 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12234 }
12235
12236 #[test]
12237 fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12238 // Exactly-saturated required args + a rest that captures nothing →
12239 // the rest binds to the empty list, never errors. Mirrors the
12240 // splice contract `,@()` contributes nothing.
12241 let params = MacroParams {
12242 required: vec!["a".into()],
12243 optional: Vec::new(),
12244 rest: Some("c".into()),
12245 };
12246 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12247 assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12248 }
12249
12250 #[test]
12251 fn bind_missing_required_errors_before_any_rest_collection() {
12252 // A required name with no arg at its position is a
12253 // `MissingMacroArg` — the gate fires during the required walk,
12254 // before the rest is ever collected.
12255 let params = MacroParams {
12256 required: vec!["a".into(), "b".into()],
12257 optional: Vec::new(),
12258 rest: Some("c".into()),
12259 };
12260 let err = params
12261 .bind("m", &[Sexp::int(1)])
12262 .expect_err("missing required `b` must error");
12263 assert!(
12264 matches!(err, LispError::MissingMacroArg { .. }),
12265 "expected MissingMacroArg, got: {err:?}"
12266 );
12267 }
12268
12269 #[test]
12270 fn bind_missing_required_errors_even_with_optional_present() {
12271 // An absent REQUIRED arg errors even when the param list has an
12272 // optional section: the required walk fires `MissingMacroArg` before
12273 // the optional arm (which would otherwise default to nil) is reached.
12274 // Required absence is an error; optional absence is a nil default.
12275 let params = MacroParams {
12276 required: vec!["a".into(), "b".into()],
12277 optional: vec![OptionalParam::bare("c")],
12278 rest: None,
12279 };
12280 let err = params
12281 .bind("m", &[Sexp::int(1)])
12282 .expect_err("missing required `b` must error before optional defaulting");
12283 assert!(
12284 matches!(err, LispError::MissingMacroArg { .. }),
12285 "expected MissingMacroArg, got: {err:?}"
12286 );
12287 }
12288
12289 #[test]
12290 fn bind_rest_less_params_reject_surplus_args() {
12291 // The rest-less binder REJECTS surplus call args via the
12292 // structural `TooManyMacroArgs { macro_name, expected, got }`
12293 // rejection — the call-site mirror of `RestParamTrailingTokens`
12294 // (the definition-site rejection lifted at the parse_params
12295 // boundary). Closes the asymmetry where the typed-entry
12296 // macro-call-gate rejected too-few-args loudly
12297 // (`MissingMacroArg`) but silently truncated too-many. `expected`
12298 // is the rest-less binder's fixed maximum arity
12299 // (`required.len() + optional.len()`); `got` is the actual
12300 // call-site arg count.
12301 let params = MacroParams {
12302 required: vec!["a".into()],
12303 optional: Vec::new(),
12304 rest: None,
12305 };
12306 let err = params
12307 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12308 .expect_err("rest-less surplus must error");
12309 match err {
12310 LispError::TooManyMacroArgs {
12311 macro_name,
12312 expected,
12313 got,
12314 } => {
12315 assert_eq!(macro_name, "m");
12316 assert_eq!(expected, 1);
12317 assert_eq!(got, 2);
12318 }
12319 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12320 }
12321 }
12322
12323 // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12324 //
12325 // The `&optional` section now admits both bare-symbol entries (`x`) AND
12326 // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12327 // Option<Sexp>` slot makes the per-param default a FIELD on each
12328 // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12329 // to maintain in lock-step with `Vec<String>`. These tests pin: the
12330 // parser admits both shapes side-by-side; the four malformed list-spec
12331 // shapes (empty / missing-default / extra-elements / non-symbol-name)
12332 // are rejected via `OptionalParamMalformed` with the typed
12333 // `OptionalParamMalformedReason`; the binder consults the default form
12334 // when the arg is absent and ignores it when supplied; and the end-to-
12335 // end expansion agrees between the bytecode and substitute strategies
12336 // (invariant 2 — free middle).
12337
12338 #[test]
12339 fn parse_params_admits_optional_list_spec_with_default() {
12340 // `(a &optional (b 5))` — one bare optional becomes
12341 // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12342 // surrounding `MacroParams` shape is otherwise identical.
12343 let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12344 assert_eq!(
12345 params,
12346 MacroParams {
12347 required: vec!["a".into()],
12348 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12349 rest: None,
12350 }
12351 );
12352 }
12353
12354 #[test]
12355 fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12356 // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12357 // interleaves bare and list-form specs. Each lands in its own
12358 // `OptionalParam` entry; `names()` still yields the flat
12359 // required-then-optional-then-rest order.
12360 let params =
12361 parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12362 assert_eq!(
12363 params,
12364 MacroParams {
12365 required: vec!["a".into()],
12366 optional: vec![
12367 OptionalParam::bare("b"),
12368 OptionalParam::with_default("c", Sexp::string("x")),
12369 OptionalParam::bare("d"),
12370 OptionalParam::with_default("e", Sexp::int(9)),
12371 ],
12372 rest: Some("r".into()),
12373 }
12374 );
12375 assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12376 }
12377
12378 #[test]
12379 fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12380 // `(&optional (x (list 1 2)))` — the default form is itself a list.
12381 // Without an evaluator, the literal Sexp is parked verbatim into
12382 // `default`; the binder produces it for any absent call.
12383 let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12384 let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12385 assert_eq!(
12386 params,
12387 MacroParams {
12388 required: Vec::new(),
12389 optional: vec![OptionalParam::with_default("x", want_default)],
12390 rest: None,
12391 }
12392 );
12393 }
12394
12395 #[test]
12396 fn parse_params_rejects_empty_list_optional_spec() {
12397 // `(&optional ())` — a zero-element list is the empty-list rejection.
12398 // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12399 // fall through to `NonSymbolParam`, which mis-classifies the failure
12400 // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12401 // symbol" rejection).
12402 let err = parse_params(&read("&optional ()").unwrap())
12403 .expect_err("empty list optional spec must error");
12404 assert!(
12405 matches!(
12406 err,
12407 LispError::OptionalParamMalformed {
12408 position: 1,
12409 reason: crate::error::OptionalParamMalformedReason::EmptyList,
12410 ..
12411 }
12412 ),
12413 "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12414 );
12415 }
12416
12417 #[test]
12418 fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12419 // `(&optional (x))` — a one-element list. REJECTED with reason
12420 // `MissingDefault` rather than reinterpreted as `&optional x`, because
12421 // a parenthesized single-element spec is structurally ambiguous and
12422 // the bare-symbol form `x` IS the canonical "no default" shape.
12423 let err = parse_params(&read("&optional (x)").unwrap())
12424 .expect_err("one-element list optional spec must error");
12425 assert!(
12426 matches!(
12427 err,
12428 LispError::OptionalParamMalformed {
12429 position: 1,
12430 reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12431 ..
12432 }
12433 ),
12434 "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12435 );
12436 }
12437
12438 #[test]
12439 fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12440 // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12441 // supplied-p)` shape is not yet supported (no evaluator → no
12442 // supplied-p variable binding), so the third element is structurally
12443 // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12444 let err = parse_params(&read("&optional (x 5 6)").unwrap())
12445 .expect_err("three-element list optional spec must error");
12446 assert!(
12447 matches!(
12448 err,
12449 LispError::OptionalParamMalformed {
12450 position: 1,
12451 reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12452 ..
12453 }
12454 ),
12455 "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12456 );
12457 }
12458
12459 #[test]
12460 fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12461 // `(&optional (5 default))` — the name slot must be a symbol; a
12462 // numeric literal is REJECTED with reason `NonSymbolName`. Without
12463 // this branch the gate would silently populate
12464 // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12465 // breaking the invariant that param names are symbols.
12466 let err = parse_params(&read("&optional (5 default)").unwrap())
12467 .expect_err("non-symbol-name optional spec must error");
12468 assert!(
12469 matches!(
12470 err,
12471 LispError::OptionalParamMalformed {
12472 position: 1,
12473 reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12474 ..
12475 }
12476 ),
12477 "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12478 );
12479 }
12480
12481 #[test]
12482 fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12483 // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12484 // spec; default forms are an optional-section affordance. The gate
12485 // must fall through to `NonSymbolParam` (parity with the prior
12486 // behavior on lists in the required section), not silently admit
12487 // the list as a default-form spec.
12488 let err =
12489 parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12490 assert!(
12491 matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12492 "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12493 );
12494 }
12495
12496 #[test]
12497 fn bind_unsupplied_optional_with_default_takes_the_default() {
12498 // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12499 // default), not nil. The default form is consulted ONLY when the
12500 // call ran out of args.
12501 let params = MacroParams {
12502 required: vec!["a".into()],
12503 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12504 rest: None,
12505 };
12506 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12507 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12508 }
12509
12510 #[test]
12511 fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12512 // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12513 // supplied optional ALWAYS takes its arg; the default is the
12514 // absence-only fallback. Pins that the default form does not
12515 // shadow a supplied call arg.
12516 let params = MacroParams {
12517 required: Vec::new(),
12518 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12519 rest: None,
12520 };
12521 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12522 assert_eq!(vals, vec![Sexp::int(42)]);
12523 }
12524
12525 #[test]
12526 fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12527 // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12528 // (default), c=nil (bare floor), d="z" (default). The three
12529 // absence cases coexist in one bind: per-default fill, nil floor,
12530 // and a tail with a literal-string default.
12531 let params = MacroParams {
12532 required: vec!["a".into()],
12533 optional: vec![
12534 OptionalParam::with_default("b", Sexp::int(5)),
12535 OptionalParam::bare("c"),
12536 OptionalParam::with_default("d", Sexp::string("z")),
12537 ],
12538 rest: None,
12539 };
12540 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12541 assert_eq!(
12542 vals,
12543 vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12544 );
12545 }
12546
12547 // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12548 //
12549 // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12550 // two-arm fallback that previously inlined at `MacroParams::bind`'s
12551 // optional arm into ONE named accessor on the typed `OptionalParam`.
12552 // The constructor pair `bare` / `with_default` defines the typed
12553 // shapes of the `default` slot; this accessor names the symmetric
12554 // bound-value projection both shapes yield at the absence boundary.
12555 // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12556 // no-default floor; (b) `with_default(name, d).resolved_default()` is
12557 // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12558 // calls (the typed `default` field is not consumed); (d) path-
12559 // uniformity at the binder — `bind`'s optional arm routes through
12560 // `resolved_default` for both shapes; (e) end-to-end through both
12561 // expansion strategies, the absent-call binding agrees.
12562
12563 #[test]
12564 fn resolved_default_is_nil_for_bare_optional() {
12565 // `OptionalParam::bare(name).default` is `None`, so
12566 // `resolved_default()` projects to `Sexp::Nil` — the CL
12567 // `&optional` no-default-form floor. Fail-before/pass-after: this
12568 // assert is meaningless pre-lift because the helper does not
12569 // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12570 // at the named primitive. Sibling of `bare` itself: the
12571 // constructor defines the shape (`default: None`); the accessor
12572 // names the bound-value projection of that shape.
12573 let p = OptionalParam::bare("x");
12574 assert_eq!(p.resolved_default(), Sexp::Nil);
12575 }
12576
12577 #[test]
12578 fn resolved_default_clones_declared_default_for_with_default_optional() {
12579 // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12580 // `resolved_default()` projects to `d.clone()` — the declared
12581 // default form. Sibling of the bare-floor pin: the closed-set
12582 // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12583 // the two arms of `resolved_default`. Pins the closed-set
12584 // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12585 let p = OptionalParam::with_default("x", Sexp::int(5));
12586 assert_eq!(p.resolved_default(), Sexp::int(5));
12587 }
12588
12589 #[test]
12590 fn resolved_default_clones_arbitrary_sexp_default_form() {
12591 // The declared default can be any `Sexp` — a literal list, a
12592 // keyword, a string, a quasi-quoted form — because v0 has no
12593 // evaluator and the typed slot parks the literal verbatim. Pin
12594 // that `resolved_default()` is faithful to the parked literal
12595 // regardless of shape: a regression that special-cases an arm
12596 // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12597 // a `Sexp::Quote`) fails here. The accessor is exactly
12598 // `default.clone()` for the `Some` arm — no shape rewriting.
12599 let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12600 let p = OptionalParam::with_default("x", arbitrary.clone());
12601 assert_eq!(p.resolved_default(), arbitrary);
12602 }
12603
12604 #[test]
12605 fn resolved_default_is_clone_stable_across_repeated_calls() {
12606 // The accessor takes `&self` and projects through `Clone`, so
12607 // repeated calls yield IDENTICAL values — the typed `default`
12608 // field is not consumed. Pins that the accessor is idempotent
12609 // for the same `OptionalParam`, which is the contract the binder
12610 // relies on across multiple `bind` invocations of the same
12611 // macro: every call that leaves the optional unfilled yields
12612 // the SAME bound value, never a partially-consumed shape. A
12613 // regression that converted the accessor to `self.default.take()`
12614 // (consuming the field) would still type-check at the call site
12615 // but would silently desync repeated absent-call bindings; this
12616 // test catches that drift.
12617 let p = OptionalParam::with_default("x", Sexp::string("hi"));
12618 let first = p.resolved_default();
12619 let second = p.resolved_default();
12620 assert_eq!(first, second);
12621 assert_eq!(first, Sexp::string("hi"));
12622 }
12623
12624 #[test]
12625 fn resolved_default_is_the_binders_absent_optional_projection() {
12626 // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12627 // optional arm consults `param.resolved_default()` for any
12628 // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12629 // `Sexp::Nil` via the `None` arm; a with-default optional
12630 // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12631 // single bind call exercises both arms in one walk, and the
12632 // bound values vec is parallel to `names()` so position
12633 // checking pins the arm-to-slot mapping. A regression that
12634 // re-inlines the two-arm fallback at the binder, drifting it
12635 // independently from the accessor, would still type-check but
12636 // a future shape change to `resolved_default` (e.g., adding a
12637 // typed `&supplied-p` companion slot) would silently desync
12638 // the binder from the accessor — this test catches that drift.
12639 let params = MacroParams {
12640 required: Vec::new(),
12641 optional: vec![
12642 OptionalParam::bare("b"),
12643 OptionalParam::with_default("c", Sexp::int(5)),
12644 ],
12645 rest: None,
12646 };
12647 // Empty args → both optionals are absent → both arms fire.
12648 let vals = params.bind("m", &[]).unwrap();
12649 assert_eq!(vals.len(), 2);
12650 assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12651 assert_eq!(
12652 vals[1],
12653 OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12654 );
12655 // And the absolute identities pin the projection's arms:
12656 assert_eq!(vals[0], Sexp::Nil);
12657 assert_eq!(vals[1], Sexp::int(5));
12658 }
12659
12660 #[test]
12661 fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12662 // End-to-end path-uniformity: a macro with both an `&optional
12663 // (g "hi")` (with-default) and an `&optional h` (bare) param
12664 // expands the same way under bytecode AND substitute
12665 // strategies, because both strategies route through the SHARED
12666 // `MacroParams::bind` which now consults `resolved_default`
12667 // for absent slots. Pins that the accessor's contract is
12668 // structurally observable at the strategy boundary — a
12669 // regression that bifurcated the accessor's behavior between
12670 // the two paths (impossible, since they share `bind`) would
12671 // surface here.
12672 let src = r#"
12673 (defmacro greet (n &optional (g "hi") h)
12674 `(list ,g ,n ,h))
12675 (greet world)
12676 "#;
12677 let expected = vec![Sexp::List(vec![
12678 Sexp::symbol("list"),
12679 Sexp::string("hi"),
12680 Sexp::symbol("world"),
12681 Sexp::Nil,
12682 ])];
12683 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12684 let substitute = Expander::new_substitute_only()
12685 .expand_program(read(src).unwrap())
12686 .unwrap();
12687 assert_eq!(
12688 bytecode, expected,
12689 "bytecode resolved_default expansion drifted"
12690 );
12691 assert_eq!(
12692 substitute, expected,
12693 "substitute resolved_default expansion drifted"
12694 );
12695 assert_eq!(
12696 bytecode, substitute,
12697 "the two strategies disagree on resolved_default expansion"
12698 );
12699 }
12700
12701 #[test]
12702 fn resolved_default_supplied_optional_does_not_consult_accessor() {
12703 // A SUPPLIED optional binds to its CALL ARG, never to the
12704 // accessor's projection. Pins the contract: `resolved_default`
12705 // is the absence-only fallback; a present arg shadows the
12706 // accessor at the binder. Sibling negative control to
12707 // `resolved_default_is_the_binders_absent_optional_projection`:
12708 // that test exercises the absence arm at every slot; this test
12709 // exercises the presence arm at every slot and proves the
12710 // accessor's `Sexp::int(5)` projection is NOT consulted when
12711 // the optional is supplied with `Sexp::int(42)`. A regression
12712 // that wired the binder to always consult the accessor (the
12713 // wrong direction — the default would shadow supplied args) is
12714 // caught here.
12715 let params = MacroParams {
12716 required: Vec::new(),
12717 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12718 rest: None,
12719 };
12720 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12721 assert_eq!(vals, vec![Sexp::int(42)]);
12722 // And the accessor's would-be projection IS NOT the bound value:
12723 let p = OptionalParam::with_default("b", Sexp::int(5));
12724 assert_ne!(vals[0], p.resolved_default());
12725 }
12726
12727 #[test]
12728 fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12729 // The end-to-end path: a macro with `&optional (g "hi")` expands to
12730 // the default literal when unsupplied, and to the supplied arg when
12731 // present. Both the bytecode and substitute strategies must agree
12732 // (invariant 2 — free middle); they share `MacroParams::bind`, so
12733 // the default arm lands once in `bind` and both strategies inherit
12734 // it unable to drift. This is the test the prior run (611a682)
12735 // signposted as the next-change-that-benefits.
12736 let src = r#"
12737 (defmacro greet (n &optional (g "hi"))
12738 `(list ,g ,n))
12739 (greet world)
12740 (greet world there)
12741 "#;
12742 let expected = vec![
12743 Sexp::List(vec![
12744 Sexp::symbol("list"),
12745 Sexp::string("hi"),
12746 Sexp::symbol("world"),
12747 ]),
12748 Sexp::List(vec![
12749 Sexp::symbol("list"),
12750 Sexp::symbol("there"),
12751 Sexp::symbol("world"),
12752 ]),
12753 ];
12754 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12755 let substitute = Expander::new_substitute_only()
12756 .expand_program(read(src).unwrap())
12757 .unwrap();
12758 assert_eq!(
12759 bytecode, expected,
12760 "bytecode optional-default expansion drifted"
12761 );
12762 assert_eq!(
12763 substitute, expected,
12764 "substitute optional-default expansion drifted"
12765 );
12766 assert_eq!(
12767 bytecode, substitute,
12768 "the two strategies disagree on optional-default expansion"
12769 );
12770 }
12771
12772 #[test]
12773 fn optional_macro_expands_end_to_end_under_both_strategies() {
12774 // The end-to-end path: a macro with an `&optional` param expands
12775 // correctly whether the optional is supplied or defaulted, and the
12776 // bytecode and substitute strategies agree (invariant 2 — free
12777 // middle). `,b` resolves to the supplied arg when present, to
12778 // `Sexp::Nil` when absent (CL's `&optional` default).
12779 let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12780 // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12781 // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12782 // from a reader-produced empty list `()` even though both Display as
12783 // `()`.
12784 let expected = vec![
12785 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12786 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12787 ];
12788 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12789 let substitute = Expander::new_substitute_only()
12790 .expand_program(read(src).unwrap())
12791 .unwrap();
12792 assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12793 assert_eq!(
12794 substitute, expected,
12795 "substitute optional expansion drifted"
12796 );
12797 assert_eq!(
12798 bytecode, substitute,
12799 "the two strategies disagree on optional expansion"
12800 );
12801 }
12802
12803 // ── MacroDef::template_body: the shared body-projection primitive ──
12804 //
12805 // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12806 // => inner.as_ref(), other => other }` inline peel — present
12807 // byte-identically at the bytecode (`compile_template`) AND substitute
12808 // (`apply`'s fallback) path entries — into ONE named projection both
12809 // strategies share. The existing `compiled_template_matches_substitute
12810 // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12811 // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12812 // the four tests below pin the projection's contract DIRECTLY:
12813 // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12814 // body returns the body verbatim; (c) the borrow is rooted in the
12815 // body field (single-level peel); (d) the projection is the same
12816 // `&Sexp` both strategies route on (so a regression that drifts the
12817 // body-peel from one strategy to the other becomes a type-level
12818 // change at this helper, not a silent two-site divergence).
12819
12820 #[test]
12821 fn template_body_unwraps_outer_quasiquote_to_inner() {
12822 // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12823 // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12824 // and `template_body` peels the outer marker. The returned `&Sexp`
12825 // is the inner walker-payload both expansion strategies consume.
12826 let inner = Sexp::List(vec![
12827 Sexp::symbol("list"),
12828 Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12829 ]);
12830 let def = MacroDef {
12831 name: "f".into(),
12832 params: MacroParams::default(),
12833 body: Sexp::Quasiquote(Box::new(inner.clone())),
12834 };
12835 assert_eq!(def.template_body(), &inner);
12836 }
12837
12838 #[test]
12839 fn template_body_returns_non_quasiquote_body_verbatim() {
12840 // A body authored WITHOUT the outer `` ` `` affordance — a bare
12841 // `Sexp::List` body — returns verbatim. The "other" arm of the
12842 // legacy match. Pin parity with the pre-lift code path so a
12843 // regression that drifts the body-peel into "always peel
12844 // something" (which would break literal-body macros) fails here.
12845 let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12846 let def = MacroDef {
12847 name: "f".into(),
12848 params: MacroParams::default(),
12849 body: body.clone(),
12850 };
12851 assert_eq!(def.template_body(), &body);
12852 // Atom bodies too — the projection is a single-arm match, not a
12853 // recursive descent. A `Sexp::Atom` body is its own template payload.
12854 let atom_def = MacroDef {
12855 name: "g".into(),
12856 params: MacroParams::default(),
12857 body: Sexp::symbol("nil-template"),
12858 };
12859 assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12860 }
12861
12862 #[test]
12863 fn template_body_peels_single_level_only() {
12864 // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12865 // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12866 // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12867 // preamble declares "Nested quasi-quotes: Not yet supported"; the
12868 // single-level peel matches the legacy inline match's posture
12869 // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12870 // recursive loop). A regression that drifts to a recursive peel
12871 // would project too far and the inner `Sexp::Quasiquote` marker —
12872 // which the substitute walker treats as an atomic leaf returned
12873 // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12874 // => Ok(form.clone())`) — would silently disappear from the
12875 // expansion's emitted form. Pin the single-level contract here.
12876 let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12877 let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12878 let def = MacroDef {
12879 name: "nested".into(),
12880 params: MacroParams::default(),
12881 body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12882 };
12883 // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12884 assert_eq!(def.template_body(), &inner_qq);
12885 assert_ne!(def.template_body(), &inner_payload);
12886 }
12887
12888 #[test]
12889 fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12890 // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12891 // verbatim through the "other" arm. The two close-cousin shapes
12892 // share an outer-marker character (`'` vs `` ` ``) at the reader
12893 // boundary but differ semantically: a `Quote` body is a literal
12894 // template (no substitution semantics), a `Quasiquote` body is a
12895 // substitution-walker entry. A regression that conflated the two
12896 // — peeling Quote as if it were Quasiquote — would silently turn
12897 // every quoted-body macro into a template-walked macro. Pin the
12898 // discrimination.
12899 let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12900 let body = Sexp::Quote(Box::new(inner.clone()));
12901 let def = MacroDef {
12902 name: "quoted".into(),
12903 params: MacroParams::default(),
12904 body: body.clone(),
12905 };
12906 assert_eq!(def.template_body(), &body);
12907 // The inner is NOT what comes back — only Quasiquote-bodied macros
12908 // would peel to the inner.
12909 assert_ne!(def.template_body(), &inner);
12910 }
12911
12912 #[test]
12913 fn template_body_is_the_shared_projection_both_strategies_walk() {
12914 // End-to-end path-uniformity at the projection boundary: a macro
12915 // authored with the canonical quasi-quoted body expands
12916 // IDENTICALLY under bytecode and substitute strategies because
12917 // both route their walker's body through `template_body()` — the
12918 // SAME `&Sexp` projection. Sibling of
12919 // `compiled_template_matches_substitute_path` (which observes
12920 // agreement on the EMITTED form); this test pins agreement on
12921 // the projection ENTRY: `compile_template` and the substitute
12922 // fallback now consume the same `&Sexp` (`def.template_body()`),
12923 // never a divergent inline match the two paths could regress
12924 // independently.
12925 let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12926 let expected = vec![Sexp::List(vec![
12927 Sexp::symbol("list"),
12928 Sexp::int(5),
12929 Sexp::int(5),
12930 ])];
12931 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12932 let substitute = Expander::new_substitute_only()
12933 .expand_program(read(src).unwrap())
12934 .unwrap();
12935 assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12936 assert_eq!(substitute, expected, "substitute body-projection drifted");
12937 assert_eq!(
12938 bytecode, substitute,
12939 "the two strategies disagree on the body-projection's emission"
12940 );
12941 }
12942
12943 // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12944 //
12945 // `expand` lifts its macro-call recognition to route through the
12946 // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12947 // self.macros.get(h))` answers "is this form an invocation of any
12948 // registered macro?" in ONE structural query on the Sexp algebra,
12949 // and a HashMap-backed lookup as its classifier. Sibling consumer to
12950 // `macro_def_from` (the typed-macro-definition dispatcher already
12951 // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12952 // a closed-set enum classifier). With both in place, BOTH dispatch
12953 // sites in the macro expander project through the SAME family
12954 // primitive — each binding the classifier that fits its candidate
12955 // set. The tests below pin the consumer's path-uniformity contract
12956 // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12957 // (h))` dispatch observes the SAME `(def, args)` decomposition the
12958 // `Expander::expand` consumer routes through.
12959
12960 #[test]
12961 fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12962 // Structural identity: on a registered-macro call, the consumer's
12963 // expansion is observably equivalent to: classify the form via
12964 // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12965 // apply the def to args. Pin path-uniformity: a hand-rolled
12966 // `as_call_to_any` lookup against the same registry the expander
12967 // walks produces the SAME `MacroDef` reference for the SAME
12968 // input form. A regression that drifts the consumer back to an
12969 // inline `as_list + as_call + macros.get` chain (which would
12970 // fragment the family adoption) is caught structurally — the
12971 // hand-rolled `as_call_to_any` and the consumer's dispatch must
12972 // observe the same decomposition.
12973 let mut e = Expander::new();
12974 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12975 .unwrap();
12976 let call_form = parse("(wrap 42)");
12977
12978 // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12979 let (def_via_family, args_via_family) = call_form
12980 .as_call_to_any(|h| e.macros.get(h))
12981 .expect("registered macro call must decompose via as_call_to_any");
12982 assert_eq!(def_via_family.name, "wrap");
12983 assert_eq!(args_via_family, &[Sexp::int(42)]);
12984
12985 // Consumer's expand observes the SAME decomposition: the expanded
12986 // form is `(list 42 42)`, derived from the SAME def + args the
12987 // hand-rolled lookup found. Path-uniform with the family
12988 // primitive at the dispatch boundary.
12989 let expanded = e.expand(&call_form).unwrap();
12990 assert_eq!(
12991 expanded,
12992 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12993 );
12994 }
12995
12996 #[test]
12997 fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12998 // Path-uniformity for the non-registered-head path: `as_call_to_any
12999 // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
13000 // a registered macro, and the consumer falls through to the
13001 // children-walk (which expands any nested macro calls). Pin
13002 // both halves: the hand-rolled lookup returns `None`, AND the
13003 // consumer's expand walks into the children. A regression that
13004 // accidentally short-circuits the children-walk for non-macro
13005 // calls (e.g. by treating `as_call_to_any` = `None` as
13006 // "non-expandable" globally) would fail here.
13007 let mut e = Expander::new();
13008 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
13009 .unwrap();
13010 let outer = parse("(foo (wrap 5))");
13011
13012 // Hand-rolled family-primitive lookup rejects the outer head.
13013 assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
13014
13015 // Consumer walks children — the inner `(wrap 5)` IS a macro call
13016 // and expands to `(list 5 5)`; the outer `foo` head is preserved.
13017 let expanded = e.expand(&outer).unwrap();
13018 assert_eq!(
13019 expanded,
13020 Sexp::List(vec![
13021 Sexp::symbol("foo"),
13022 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
13023 ])
13024 );
13025 }
13026
13027 #[test]
13028 fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
13029 // Path-uniformity for the non-call path: every shape `as_call`
13030 // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
13031 // ALSO routes past `as_call_to_any` into the `as_list()`
13032 // fallthrough, where the not-a-list arm returns `form.clone()`
13033 // verbatim. Pin both halves: the hand-rolled lookup rejects
13034 // every non-call shape regardless of decoder, AND the consumer
13035 // preserves each shape unchanged. A regression that drifts the
13036 // consumer's dispatch order (e.g. checking `as_list()` BEFORE
13037 // `as_call_to_any` in a way that mis-handles Quote-family
13038 // wrappers) would fail here.
13039 let e = Expander::new();
13040 let shapes = [
13041 Sexp::symbol("foo"),
13042 Sexp::int(5),
13043 Sexp::keyword("k"),
13044 Sexp::string("s"),
13045 Sexp::boolean(true),
13046 Sexp::float(1.5),
13047 Sexp::Nil,
13048 Sexp::Quote(Box::new(Sexp::symbol("x"))),
13049 Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
13050 Sexp::Unquote(Box::new(Sexp::symbol("x"))),
13051 Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
13052 ];
13053 for s in &shapes {
13054 // Hand-rolled family-primitive lookup rejects non-call shapes
13055 // even for a promiscuous decoder — the call-shape gate fires
13056 // BEFORE the decoder runs.
13057 assert!(
13058 s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
13059 "non-call shape must yield None for as_call_to_any: {s}"
13060 );
13061 // Consumer preserves the shape verbatim — the not-a-list
13062 // arm at the fallthrough returns `form.clone()`.
13063 assert_eq!(
13064 e.expand(s).unwrap(),
13065 s.clone(),
13066 "non-call shape must round-trip unchanged through expand: {s}"
13067 );
13068 }
13069 }
13070
13071 #[test]
13072 fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
13073 // The empty list `()` has no operator and no children. Pin that
13074 // `as_call_to_any` rejects it (no head to feed the decoder), the
13075 // consumer falls through to `as_list()` which returns `Some(&[])`,
13076 // and the children-walk emits `Sexp::List(vec![])` (an empty
13077 // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
13078 // to be observationally identical, but the path is the
13079 // children-walk arm, NOT the not-a-list arm). Path-uniformity
13080 // gate for the singleton-list edge case the `compile_named_from_
13081 // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
13082 // `Some(&[])` for — same posture, different family member.
13083 let e = Expander::new();
13084 let empty = Sexp::List(vec![]);
13085
13086 // Hand-rolled family-primitive lookup rejects the empty list.
13087 assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
13088
13089 // Consumer walks children (zero of them) — output is the empty
13090 // list, same as input.
13091 assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
13092 }
13093
13094 // ── Expander::expand_and_collect_calls_to: program-level walk ───────
13095 //
13096 // `expand_and_collect_calls_to(forms, keyword, project)` composes the
13097 // expander's program-level expansion (`expand_program`) with the
13098 // substrate's slice-side typed-keyword projection (`iter_calls_to`)
13099 // and a caller-supplied per-form mapper into ONE method on the
13100 // `Expander` surface. Both `compile_typed::<T>` and
13101 // `compile_named_from_forms::<T>` (compile.rs) route through it; the
13102 // tests below pin its contract directly. The existing compile.rs
13103 // dispatch tests are the path-uniformity guards proving the two
13104 // production sites route through it without behavior drift.
13105
13106 #[test]
13107 fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
13108 // Three forms — two match `defmonitor`, one matches `defalert`.
13109 // The mapper records `args.len()` from each matching form's tail.
13110 // Pin: only the two `defmonitor` matches flow through `project`,
13111 // in source order (so the recorded lengths are 2, 1).
13112 let forms = vec![
13113 Sexp::List(vec![
13114 Sexp::symbol("defmonitor"),
13115 Sexp::keyword("name"),
13116 Sexp::string("first"),
13117 ]),
13118 Sexp::List(vec![
13119 Sexp::symbol("defalert"),
13120 Sexp::keyword("name"),
13121 Sexp::string("not-a-match"),
13122 ]),
13123 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
13124 ];
13125 let mut e = Expander::new();
13126 let lengths: Vec<usize> = e
13127 .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
13128 .expect("matching forms must compose");
13129 assert_eq!(lengths, vec![2, 1]);
13130 }
13131
13132 #[test]
13133 fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13134 // Path-uniformity for the soft-projection posture inherited from
13135 // `iter_calls_to`: non-matching forms are skipped silently, and
13136 // `project` is never invoked on them. Pin via a counter the
13137 // closure increments per invocation — the counter is the
13138 // observable that distinguishes "skipped silently" from
13139 // "projected to a no-op".
13140 let forms = vec![
13141 Sexp::symbol("bare-atom"),
13142 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13143 Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13144 Sexp::List(vec![]),
13145 Sexp::Nil,
13146 ];
13147 let mut count = 0usize;
13148 let mut e = Expander::new();
13149 let out: Vec<()> = e
13150 .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13151 count += 1;
13152 Ok(())
13153 })
13154 .expect("non-matching-only slice must collect to empty Vec");
13155 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13156 assert_eq!(count, 0, "project must never run for non-matching forms");
13157 }
13158
13159 #[test]
13160 fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13161 // Three matching forms; the closure errors on the SECOND. Pin: the
13162 // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13163 // form's error, the third form's `project` is NEVER invoked, and
13164 // the returned error is exactly the one the closure raised. This
13165 // mirrors `compile_named_from_forms`'s short-circuit on the first
13166 // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13167 // kwargs rejection.
13168 let forms = vec![
13169 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13170 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13171 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13172 ];
13173 let mut seen = Vec::new();
13174 let mut e = Expander::new();
13175 let err = e
13176 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13177 let n = args[0].as_int().expect("test args are ints");
13178 seen.push(n);
13179 if n == 2 {
13180 Err(LispError::Compile {
13181 form: "test".to_string(),
13182 message: "stop at two".to_string(),
13183 })
13184 } else {
13185 Ok(())
13186 }
13187 })
13188 .expect_err("project's error must short-circuit collect");
13189 // Only the first two forms were ever projected — the third
13190 // never reached `project`.
13191 assert_eq!(seen, vec![1, 2]);
13192 assert!(
13193 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13194 "short-circuit must propagate the project's error verbatim, got {err:?}"
13195 );
13196 }
13197
13198 #[test]
13199 fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13200 // The `expand_program` step runs BEFORE `iter_calls_to` walks
13201 // anything — so an `expand_program` error (e.g., a malformed
13202 // `defmacro` head) must short-circuit the entire composition
13203 // and `project` must NEVER run. Pin via the closure-running
13204 // counter: an error from the FIRST step never invokes `project`.
13205 // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13206 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13207 // fires during `expand_program`.
13208 let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13209 let mut count = 0usize;
13210 let mut e = Expander::new();
13211 let err = e
13212 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13213 count += 1;
13214 Ok(())
13215 })
13216 .expect_err("expand_program error must short-circuit before project");
13217 assert_eq!(
13218 count, 0,
13219 "project must never run when expand_program errors"
13220 );
13221 // Sanity: the error IS the expand_program-stage rejection, NOT
13222 // a project-stage error (path-uniformity for the ordering).
13223 let rendered = format!("{err}");
13224 assert!(
13225 rendered.contains("NAME") || rendered.contains("symbol"),
13226 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13227 );
13228 }
13229
13230 #[test]
13231 fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13232 // Boundary: empty forms slice yields zero items regardless of
13233 // keyword. Pin the degenerate boundary — empty in, empty out —
13234 // and the closure is never invoked. Sibling of
13235 // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13236 // one level up the composition: the program-level walk is
13237 // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13238 let mut count = 0usize;
13239 let mut e = Expander::new();
13240 let out: Vec<()> = e
13241 .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13242 count += 1;
13243 Ok(())
13244 })
13245 .expect("empty forms is not an error");
13246 assert!(out.is_empty());
13247 assert_eq!(count, 0);
13248 }
13249
13250 #[test]
13251 fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13252 // Critical ordering: `expand_program` runs BEFORE the keyword
13253 // filter, so a `defmacro` whose expansion produces a form with
13254 // a matching head IS visible to `project`. Pin the pipeline
13255 // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13256 // (emit-monitor "alpha") (defmonitor :name "beta")`
13257 // expands the macro call to `(defmonitor :name "alpha")` and
13258 // the keyword walk then sees TWO `defmonitor` forms (the
13259 // macro-emitted one AND the directly-authored one). A
13260 // pre-expansion filter would miss the macro-emitted form.
13261 let forms = read(
13262 "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13263 (emit-monitor \"alpha\")
13264 (defmonitor :name \"beta\")",
13265 )
13266 .unwrap();
13267 let mut e = Expander::new();
13268 let names: Vec<String> = e
13269 .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13270 // Each defmonitor form's tail is `(:name "X")`; project
13271 // the second element's string to capture the name.
13272 Ok(args[1].as_string().unwrap().to_string())
13273 })
13274 .expect("macroexpanded + directly-authored forms must both flow");
13275 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13276 }
13277
13278 #[test]
13279 fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13280 // The `keyword` argument flows straight into `iter_calls_to`'s
13281 // head-comparison gate — a regression that drifts the keyword
13282 // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13283 // or lowercases / trims the keyword en route) fails here. Pin
13284 // by passing TWO different keywords against the SAME forms
13285 // input and asserting each picks up only its matching forms.
13286 let forms = vec![
13287 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13288 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13289 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13290 Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13291 ];
13292
13293 let mut e = Expander::new();
13294 let monitors: Vec<i64> = e
13295 .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13296 Ok(args[0].as_int().unwrap())
13297 })
13298 .unwrap();
13299 assert_eq!(monitors, vec![1, 3]);
13300
13301 let mut e2 = Expander::new();
13302 let alerts: Vec<i64> = e2
13303 .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13304 Ok(args[0].as_int().unwrap())
13305 })
13306 .unwrap();
13307 assert_eq!(alerts, vec![2]);
13308
13309 // A keyword nothing matches collects to the empty Vec — same
13310 // shape as the empty-forms case at the slice level.
13311 let mut e3 = Expander::new();
13312 let none: Vec<i64> = e3
13313 .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13314 Ok(args[0].as_int().unwrap())
13315 })
13316 .unwrap();
13317 assert!(none.is_empty());
13318 }
13319
13320 #[test]
13321 fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13322 // Structural identity: for any (forms, keyword, project) triple
13323 // whose pieces succeed, the method's result equals the inlined
13324 // pre-lift pipeline `expand_program + iter_calls_to + map +
13325 // collect`. Pin shape AND ordering on a mixed forms input
13326 // (matching + non-matching + macroexpand-emitted matches) so a
13327 // regression that drifts the method's pipeline from the inlined
13328 // closed-form fails here. This is the lift's load-bearing
13329 // path-uniformity gate — the SAME assertion that holds for
13330 // both production consumers (compile.rs) holds at the
13331 // primitive's contract level.
13332 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13333 (foo :idx 1)
13334 (emit-foo 2)
13335 (bar :idx 99)
13336 (foo :idx 3)";
13337 let forms = read(src).unwrap();
13338
13339 // Inlined pre-lift pipeline.
13340 let mut exp_inline = Expander::new();
13341 let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13342 let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13343 .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13344 .collect::<Result<Vec<_>>>()
13345 .unwrap();
13346
13347 // Method pipeline.
13348 let mut exp_method = Expander::new();
13349 let via_method: Vec<i64> = exp_method
13350 .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13351 .unwrap();
13352
13353 assert_eq!(via_inline, via_method);
13354 assert_eq!(via_inline, vec![1, 2, 3]);
13355 }
13356
13357 // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13358 //
13359 // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13360 // composes `expand_program` with `iter_calls_to_any` and a per-form
13361 // mapper that receives BOTH the typed witness AND the args tail.
13362 // Closes the (keyword, classifier) 2×2 of expander-surface
13363 // compose-on-iter projections at the typed-decoded corner the prior
13364 // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13365 // sibling now ROUTES through this primitive via a constant-classifier
13366 // composition — the tests below pin both the typed-decoded primitive's
13367 // contract directly AND the routing identity binding the keyword
13368 // sibling to it.
13369 //
13370 // The existing `expand_and_collect_calls_to_*` tests above remain
13371 // load-bearing — they pin the keyword sibling's contract through
13372 // its ROUTED implementation, so a regression that drifts the routing
13373 // composition surfaces at the existing keyword tests too.
13374
13375 #[test]
13376 fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13377 {
13378 // The typed-decoded primitive's happy path: a closed-set
13379 // classifier decodes head symbols to a typed enum, and the
13380 // per-form projection receives `(decoded, args)` for every
13381 // matched form in source order. Sweep across THREE distinct
13382 // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13383 // a non-matching form to pin both the typed-witness threading
13384 // AND the source-order yield AND the rejection of non-classifier
13385 // forms in ONE assertion. The decoder runs once per call form
13386 // (`FnMut`); the projection runs once per matched form.
13387 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13388 enum Op {
13389 Foo,
13390 Bar,
13391 Baz,
13392 }
13393 let src = "(foo 1)
13394 (bar 2)
13395 (other 99)
13396 (baz 3)
13397 (foo 4)";
13398 let forms = read(src).unwrap();
13399 let mut e = Expander::new();
13400 let pairs: Vec<(Op, i64)> = e
13401 .expand_and_collect_calls_to_any(
13402 forms,
13403 |h| match h {
13404 "foo" => Some(Op::Foo),
13405 "bar" => Some(Op::Bar),
13406 "baz" => Some(Op::Baz),
13407 _ => None,
13408 },
13409 |op, args| Ok((op, args[0].as_int().unwrap())),
13410 )
13411 .unwrap();
13412 assert_eq!(
13413 pairs,
13414 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13415 );
13416 }
13417
13418 #[test]
13419 fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13420 // The soft-projection contract — every shape `iter_calls_to_any`
13421 // rejects (non-list, empty list, list whose head is not a
13422 // symbol, list whose head decodes through the classifier to
13423 // `None`) skips the projection silently. Pin a deliberately-
13424 // panicking projection so the assertion fires loudly if a
13425 // non-matching form reaches it. The decoder accepts only
13426 // `"defmonitor"`; every other shape (atom, list whose head is a
13427 // keyword, list whose head is a symbol the decoder rejects)
13428 // short-circuits before the projection runs.
13429 let src = r#":bare-keyword
13430 "bare-string"
13431 42
13432 ()
13433 (foo bar)
13434 (defmonitor :name "matches")"#;
13435 let forms = read(src).unwrap();
13436 let mut e = Expander::new();
13437 let lengths: Vec<usize> = e
13438 .expand_and_collect_calls_to_any(
13439 forms,
13440 |h| (h == "defmonitor").then_some(()),
13441 |(), args| {
13442 // The projection must run EXACTLY once — for the
13443 // single matched form. Any non-matching form that
13444 // reaches the projection panics here.
13445 assert_eq!(args.len(), 2, "projection ran on non-matching form");
13446 Ok(args.len())
13447 },
13448 )
13449 .unwrap();
13450 assert_eq!(lengths, vec![2]);
13451 }
13452
13453 #[test]
13454 fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13455 // The Result projection's short-circuit contract — when the
13456 // projection returns `Err` on a matched form, the walk
13457 // short-circuits and subsequent matched forms are NOT projected.
13458 // Pin the source-order short-circuit via a counter the
13459 // projection increments before deciding to fail: the counter
13460 // sits at exactly the index of the failing form (the second
13461 // match), proving the third match never reached the projection.
13462 let src = "(foo 1) (foo 2) (foo 3)";
13463 let forms = read(src).unwrap();
13464 let mut count = 0usize;
13465 let mut e = Expander::new();
13466 let err = e
13467 .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13468 forms,
13469 |h| (h == "foo").then_some(()),
13470 |(), args| {
13471 count += 1;
13472 let v = args[0].as_int().unwrap();
13473 if v == 2 {
13474 return Err(crate::error::LispError::Missing("test-failure"));
13475 }
13476 Ok(v)
13477 },
13478 )
13479 .expect_err("projection must short-circuit on first Err");
13480 assert_eq!(
13481 count, 2,
13482 "projection must have run on first AND failing form, then stopped",
13483 );
13484 // Sanity: the error is the project-stage rejection — the same
13485 // typed `LispError` the projection returned, propagated verbatim.
13486 assert!(
13487 matches!(err, crate::error::LispError::Missing("test-failure")),
13488 "expected the projection's typed Err verbatim, got {err:?}",
13489 );
13490 }
13491
13492 #[test]
13493 fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13494 // Ordering contract — `expand_program` runs BEFORE
13495 // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13496 // that emits a classifier-decoded body must have its expanded
13497 // body decoded by the classifier (not the unexpanded macro-call
13498 // head). Pin the ordering with a macro `(emit-foo n)` that
13499 // expands to `(foo :idx n)` — the classifier sees the expanded
13500 // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13501 // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13502 // — the SAME composition contract holds on the typed-decoded
13503 // sibling.
13504 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13505 (foo :idx 1)
13506 (emit-foo 2)
13507 (bar :idx 99)
13508 (foo :idx 3)";
13509 let forms = read(src).unwrap();
13510 let mut e = Expander::new();
13511 // Classifier returns `()` for "foo" only; the keyword sibling's
13512 // routing identity is the same composition we exercise here
13513 // directly.
13514 let idxs: Vec<i64> = e
13515 .expand_and_collect_calls_to_any(
13516 forms,
13517 |h| (h == "foo").then_some(()),
13518 |(), args| Ok(args[1].as_int().unwrap()),
13519 )
13520 .unwrap();
13521 // 1 from the literal call, 2 from the macro-emitted call, 3 from
13522 // the final literal call. The macro-emitted `(foo :idx 2)` is
13523 // present BECAUSE `expand_program` ran first.
13524 assert_eq!(idxs, vec![1, 2, 3]);
13525 }
13526
13527 #[test]
13528 fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13529 // The `FnMut` constraint — the slice walk calls the classifier
13530 // once per call form, and a decoder that captures mutable state
13531 // (a counter, a registry cache, a visited-set) maintains that
13532 // state across the walk. Pin the `FnMut` contract with a counter
13533 // closure that increments per shape-gate-passing form (calls
13534 // whose head is a symbol), asserting the counter equals the
13535 // total call count regardless of whether the classifier accepts
13536 // each form. Mirrors the parallel slice-side test
13537 // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13538 // — the SAME `FnMut` contract holds on the expander surface.
13539 let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13540 let forms = read(src).unwrap();
13541 let mut e = Expander::new();
13542 let mut classifier_calls = 0usize;
13543 let projected: Vec<i64> = e
13544 .expand_and_collect_calls_to_any(
13545 forms,
13546 |h| {
13547 classifier_calls += 1;
13548 (h == "foo").then_some(())
13549 },
13550 |(), args| Ok(args[0].as_int().unwrap()),
13551 )
13552 .unwrap();
13553 assert_eq!(
13554 classifier_calls, 5,
13555 "classifier must run once per call form (5 forms, all calls with symbol heads)",
13556 );
13557 assert_eq!(
13558 projected,
13559 vec![1, 3, 5],
13560 "projection must run only for classifier-accepted forms in source order",
13561 );
13562 }
13563
13564 #[test]
13565 fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13566 ) {
13567 // Structural identity: the keyword sibling
13568 // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13569 // primitive via a constant-classifier composition — the
13570 // post-lift composition law binding the two siblings:
13571 //
13572 // expand_and_collect_calls_to(forms, k, p) ==
13573 // expand_and_collect_calls_to_any(forms,
13574 // |h| (h == k).then_some(()),
13575 // |(), args| p(args))
13576 //
13577 // Pin shape AND ordering across THREE representative keyword
13578 // values: one that matches multiple forms, one that matches
13579 // none, one that matches exactly one form. Same mixed input
13580 // (literal + macroexpand-emitted + non-matching) the keyword
13581 // path-uniformity test exercises, so the routing identity holds
13582 // on the SAME input shape the keyword sibling's contract pins.
13583 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13584 (foo :idx 1)
13585 (emit-foo 2)
13586 (bar :idx 99)
13587 (foo :idx 3)";
13588 let forms = read(src).unwrap();
13589
13590 for keyword in ["foo", "bar", "absent"] {
13591 let mut exp_keyword = Expander::new();
13592 let via_keyword: Vec<i64> = exp_keyword
13593 .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13594 Ok(args[1].as_int().unwrap())
13595 })
13596 .unwrap();
13597 let mut exp_classifier = Expander::new();
13598 let via_classifier: Vec<i64> = exp_classifier
13599 .expand_and_collect_calls_to_any(
13600 forms.clone(),
13601 |h| (h == keyword).then_some(()),
13602 |(), args| Ok(args[1].as_int().unwrap()),
13603 )
13604 .unwrap();
13605 assert_eq!(
13606 via_keyword, via_classifier,
13607 "routing identity drifted for keyword {keyword:?}",
13608 );
13609 }
13610 }
13611
13612 #[test]
13613 fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13614 {
13615 // The expand_program-stage short-circuit contract — when
13616 // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13617 // param list is malformed), the walk short-circuits BEFORE the
13618 // classifier or projection runs. Pin the ordering with a
13619 // deliberately-panicking classifier AND projection: any
13620 // post-expand-program execution fires the panic. Mirrors the
13621 // parallel keyword test
13622 // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13623 // — the SAME composition contract holds on the typed-decoded
13624 // sibling.
13625 // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13626 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13627 // fires during `expand_program`. Same rejection shape the
13628 // parallel keyword test uses, so the ordering contract observed
13629 // here is structurally identical to the keyword sibling's.
13630 let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13631 let mut e = Expander::new();
13632 let err = e
13633 .expand_and_collect_calls_to_any::<(), _, _, ()>(
13634 forms,
13635 |_h| -> Option<()> {
13636 panic!("classifier must not run when expand_program errors");
13637 },
13638 |(), _args| {
13639 panic!("project must not run when expand_program errors");
13640 },
13641 )
13642 .expect_err("expand_program error must short-circuit before classifier or project");
13643 // Sanity: the error IS an expand_program-stage rejection — not a
13644 // classifier or projection error — so the ordering is observable.
13645 let rendered = format!("{err}");
13646 assert!(
13647 rendered.contains("NAME") || rendered.contains("symbol"),
13648 "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13649 );
13650 }
13651
13652 // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13653 //
13654 // The from-source sibling of `expand_and_collect_calls_to`: composes
13655 // `crate::reader::read(src)?` with the from-forms primitive in ONE
13656 // method on the `Expander` surface. All four typed-program dispatchers
13657 // (free-function `compile_typed` / `compile_named`, preloaded-expander
13658 // `RealizedCompiler::compile_typed` / `compile_named`) route through
13659 // it; the tests below pin its contract directly. The existing
13660 // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13661 // guards proving the four production sites route through it without
13662 // behavior drift.
13663
13664 #[test]
13665 fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13666 // Happy path: a source string with three top-level forms (two
13667 // matching `defmonitor`, one matching `defalert`) flows through
13668 // `read` then through the from-forms primitive. Pin the SAME
13669 // emission shape the from-forms test pins for the matching
13670 // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13671 let src = r#"(defmonitor :name "first")
13672 (defalert :name "not-a-match")
13673 (defmonitor :solo)"#;
13674 let mut e = Expander::new();
13675 let lengths: Vec<usize> = e
13676 .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13677 .expect("matching forms must compose");
13678 assert_eq!(lengths, vec![2, 1]);
13679 }
13680
13681 #[test]
13682 fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13683 // The reader runs BEFORE `expand_program` — so a reader error (an
13684 // unterminated string, an unbalanced paren, an unknown escape)
13685 // must short-circuit the entire composition and `expand_program`
13686 // / the keyword filter / `project` must NEVER run. Pin the
13687 // ordering: an unbalanced open-paren is rejected by the reader
13688 // at parse time, never reaches the from-forms primitive.
13689 let mut count = 0usize;
13690 let mut e = Expander::new();
13691 let err = e
13692 .expand_source_and_collect_calls_to::<(), _>(
13693 "(defmonitor :name \"unbalanced",
13694 "defmonitor",
13695 |_args| {
13696 count += 1;
13697 Ok(())
13698 },
13699 )
13700 .expect_err("reader error must short-circuit before expand_program");
13701 assert_eq!(
13702 count, 0,
13703 "project must never run when reader errors at parse time"
13704 );
13705 // Sanity: the error IS a reader-stage rejection — the rendered
13706 // diagnostic mentions the lexer's gate, not the expander's or
13707 // projector's (path-uniformity for the read-then-walk ordering).
13708 let rendered = format!("{err}");
13709 assert!(
13710 rendered.to_lowercase().contains("string")
13711 || rendered.to_lowercase().contains("paren")
13712 || rendered.to_lowercase().contains("eof")
13713 || rendered.to_lowercase().contains("unexpected")
13714 || rendered.to_lowercase().contains("unterminated")
13715 || rendered.to_lowercase().contains("unclosed"),
13716 "error must be the reader-stage rejection, got: {rendered}"
13717 );
13718 }
13719
13720 #[test]
13721 fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13722 ) {
13723 // Reader succeeds, but `expand_program` rejects a malformed
13724 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13725 // ordering: `expand_program` rejects BEFORE the keyword filter
13726 // walks anything, `project` must NEVER run. Sibling of the
13727 // from-forms test of the same name — one level up the
13728 // composition (sourced from `&str` rather than `Vec<Sexp>`).
13729 let mut count = 0usize;
13730 let mut e = Expander::new();
13731 let err = e
13732 .expand_source_and_collect_calls_to::<(), _>(
13733 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13734 "defmonitor",
13735 |_args| {
13736 count += 1;
13737 Ok(())
13738 },
13739 )
13740 .expect_err("expand_program error must short-circuit before project");
13741 assert_eq!(
13742 count, 0,
13743 "project must never run when expand_program errors"
13744 );
13745 let rendered = format!("{err}");
13746 assert!(
13747 rendered.contains("NAME") || rendered.contains("symbol"),
13748 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13749 );
13750 }
13751
13752 #[test]
13753 fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13754 // Reader + `expand_program` both succeed; the per-form `project`
13755 // errors on the SECOND matched form. Pin: the collect's short-
13756 // circuit fires at the second match's error, the third match's
13757 // `project` is NEVER invoked, and the returned error is exactly
13758 // the one the closure raised. Mirrors `compile_named`'s
13759 // short-circuit on the first `NamedFormMissingName` /
13760 // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13761 // from `&str`.
13762 let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13763 let mut seen = Vec::new();
13764 let mut e = Expander::new();
13765 let err = e
13766 .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13767 let n = args[1].as_int().expect("test args are ints");
13768 seen.push(n);
13769 if n == 2 {
13770 Err(LispError::Compile {
13771 form: "test".to_string(),
13772 message: "stop at two".to_string(),
13773 })
13774 } else {
13775 Ok(())
13776 }
13777 })
13778 .expect_err("project's error must short-circuit collect");
13779 assert_eq!(seen, vec![1, 2]);
13780 assert!(
13781 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13782 "short-circuit must propagate the project's error verbatim, got {err:?}"
13783 );
13784 }
13785
13786 #[test]
13787 fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13788 // Boundary: empty source string yields zero items regardless of
13789 // keyword. Pin the degenerate boundary — empty in, empty out —
13790 // and the closure is never invoked. Sibling of the from-forms
13791 // test of the same name, one level up the composition: the
13792 // from-source posture is fused-empty when `read` yields an
13793 // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13794 // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13795 // matches).
13796 let mut count = 0usize;
13797 let mut e = Expander::new();
13798 let out: Vec<()> = e
13799 .expand_source_and_collect_calls_to("", "anything", |_args| {
13800 count += 1;
13801 Ok(())
13802 })
13803 .expect("empty source is not an error");
13804 assert!(out.is_empty());
13805 assert_eq!(count, 0);
13806 }
13807
13808 #[test]
13809 fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13810 // Critical ordering preserved across the from-source posture:
13811 // `defmacro` source registers in `expand_program`, its call
13812 // sites expand to the matching keyword, AND the keyword walk
13813 // sees both the macro-emitted form and any directly-authored
13814 // matches in source order. Sibling of the from-forms test of
13815 // the same name, sourced from `&str`.
13816 let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13817 (emit-monitor \"alpha\")
13818 (defmonitor :name \"beta\")";
13819 let mut e = Expander::new();
13820 let names: Vec<String> = e
13821 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13822 Ok(args[1].as_string().unwrap().to_string())
13823 })
13824 .expect("macroexpanded + directly-authored forms must both flow");
13825 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13826 }
13827
13828 #[test]
13829 fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13830 // Structural identity: for any (src, keyword, project) triple
13831 // whose pieces succeed, the from-source method's result equals
13832 // the inlined pre-lift pipeline `read + expand_and_collect_
13833 // calls_to`. Pin shape AND ordering on a mixed source
13834 // (matching + non-matching + macroexpand-emitted matches) so a
13835 // regression that drifts the method's pipeline from the
13836 // inlined closed-form fails here. This is the lift's
13837 // load-bearing path-uniformity gate — the SAME assertion that
13838 // holds for all four production consumers (free-function
13839 // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13840 // holds at the primitive's contract level.
13841 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13842 (foo :idx 1)
13843 (emit-foo 2)
13844 (bar :idx 99)
13845 (foo :idx 3)";
13846
13847 // Inlined pre-lift pipeline.
13848 let mut exp_inline = Expander::new();
13849 let inline_forms = read(src).unwrap();
13850 let via_inline: Vec<i64> = exp_inline
13851 .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13852 .unwrap();
13853
13854 // From-source method pipeline.
13855 let mut exp_method = Expander::new();
13856 let via_method: Vec<i64> = exp_method
13857 .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13858 .unwrap();
13859
13860 assert_eq!(via_inline, via_method);
13861 assert_eq!(via_inline, vec![1, 2, 3]);
13862 }
13863
13864 #[test]
13865 fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13866 // The `keyword` argument flows straight into the from-forms
13867 // primitive's head-comparison gate, which inherits from
13868 // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13869 // lowercases / trims it en route through the read step) fails
13870 // here. Pin by passing TWO different keywords against the SAME
13871 // source and asserting each picks up only its matching forms.
13872 let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13873
13874 let mut e1 = Expander::new();
13875 let monitors: Vec<i64> = e1
13876 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13877 Ok(args[1].as_int().unwrap())
13878 })
13879 .unwrap();
13880 assert_eq!(monitors, vec![1, 3]);
13881
13882 let mut e2 = Expander::new();
13883 let alerts: Vec<i64> = e2
13884 .expand_source_and_collect_calls_to(src, "defalert", |args| {
13885 Ok(args[1].as_int().unwrap())
13886 })
13887 .unwrap();
13888 assert_eq!(alerts, vec![2]);
13889
13890 let mut e3 = Expander::new();
13891 let none: Vec<i64> = e3
13892 .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13893 Ok(args[1].as_int().unwrap())
13894 })
13895 .unwrap();
13896 assert!(none.is_empty());
13897 }
13898
13899 #[test]
13900 fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13901 // Path-uniformity for the soft-projection posture inherited
13902 // from `iter_calls_to`: non-matching forms in the source are
13903 // skipped silently, and `project` is never invoked on them.
13904 // Sibling of the from-forms test of the same name, sourced
13905 // from `&str` with a mix of atoms-and-lists that the reader
13906 // emits at the top level.
13907 let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13908 let mut count = 0usize;
13909 let mut e = Expander::new();
13910 let out: Vec<()> = e
13911 .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13912 count += 1;
13913 Ok(())
13914 })
13915 .expect("non-matching-only source must collect to empty Vec");
13916 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13917 assert_eq!(count, 0, "project must never run for non-matching forms");
13918 }
13919
13920 // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13921 //
13922 // The from-source posture of the typed-decoded classifier sibling on
13923 // the `Expander` surface — closes the (from-forms, from-source) ×
13924 // (keyword, classifier) 2×2 of compose-on-iter projections at the
13925 // from-source classifier corner the prior runs left open. The
13926 // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13927 // keyword) is the constant-classifier projection of this primitive:
13928 // its body composes this function with a `|h| (h == keyword).
13929 // then_some(())` decoder, so the from-source pipeline lives at ONE
13930 // site. These tests pin both the typed-decoded primitive's contract
13931 // directly AND the post-lift routing of
13932 // `expand_source_and_collect_calls_to` through it.
13933
13934 #[test]
13935 fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13936 ) {
13937 // Happy path: a source string with four top-level forms (two
13938 // matching "foo", one matching "bar", one rejected-head
13939 // "defmonitor") flows through `read` → `expand_program` → the
13940 // typed-decoded classifier walk. The closed-set classifier
13941 // `Op::from_keyword` admits "foo" and "bar" but rejects
13942 // "defmonitor". Pin the typed-decoded yield shape (decoded
13943 // witness alongside args tail) AND source-order preservation
13944 // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13945 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13946 enum Op {
13947 Foo,
13948 Bar,
13949 }
13950 fn op_from_keyword(head: &str) -> Option<Op> {
13951 match head {
13952 "foo" => Some(Op::Foo),
13953 "bar" => Some(Op::Bar),
13954 _ => None,
13955 }
13956 }
13957 let src = r#"(foo :idx 1)
13958 (defmonitor :idx 99)
13959 (bar :idx 2)
13960 (foo :idx 3)"#;
13961 let mut e = Expander::new();
13962 let yielded: Vec<(Op, i64)> = e
13963 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13964 Ok((op, args[1].as_int().expect("test args are ints")))
13965 })
13966 .expect("matching forms must compose through from-source classifier walk");
13967 assert_eq!(
13968 yielded,
13969 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13970 "yields must be in source order, decoded witness paired with per-form mapper output"
13971 );
13972 }
13973
13974 #[test]
13975 fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13976 ) {
13977 // The reader runs BEFORE `expand_program` AND BEFORE the
13978 // classifier — so a reader error (an unterminated string)
13979 // short-circuits the entire composition and both the classifier
13980 // AND `project` must NEVER run. Pin the ordering with BOTH
13981 // decoder and projection explicitly panicking — any post-reader
13982 // execution fires the panic. Sibling of
13983 // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13984 // one corner over in the 2×2 (the classifier version of the
13985 // same ordering pin).
13986 let mut e = Expander::new();
13987 let err = e
13988 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13989 "(defmonitor :name \"unbalanced",
13990 |_h: &str| -> Option<()> {
13991 panic!("classifier must not run when reader errors at parse time")
13992 },
13993 |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13994 )
13995 .expect_err("reader error must short-circuit before classifier");
13996 // Sanity: the error is a reader-stage rejection.
13997 let rendered = format!("{err}");
13998 assert!(
13999 rendered.to_lowercase().contains("string")
14000 || rendered.to_lowercase().contains("paren")
14001 || rendered.to_lowercase().contains("eof")
14002 || rendered.to_lowercase().contains("unexpected")
14003 || rendered.to_lowercase().contains("unterminated")
14004 || rendered.to_lowercase().contains("unclosed"),
14005 "error must be the reader-stage rejection, got: {rendered}"
14006 );
14007 }
14008
14009 #[test]
14010 fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
14011 ) {
14012 // Reader succeeds, but `expand_program` rejects a malformed
14013 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14014 // ordering: `expand_program` rejects BEFORE the classifier walks
14015 // anything, both decoder AND `project` must NEVER run. Sibling
14016 // of the keyword-from-source variant — one corner over in the
14017 // 2×2 with both classifier and projection explicitly panicking.
14018 let mut e = Expander::new();
14019 let err = e
14020 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
14021 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
14022 |_h: &str| -> Option<()> {
14023 panic!("classifier must not run when expand_program errors")
14024 },
14025 |(), _args| -> Result<()> {
14026 panic!("project must not run when expand_program errors")
14027 },
14028 )
14029 .expect_err("expand_program error must short-circuit before classifier");
14030 let rendered = format!("{err}");
14031 assert!(
14032 rendered.contains("NAME") || rendered.contains("symbol"),
14033 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14034 );
14035 }
14036
14037 #[test]
14038 fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
14039 // Path-uniformity for the soft-projection posture inherited
14040 // from `iter_calls_to_any`: forms whose head the classifier
14041 // rejects are skipped silently, and `project` is never invoked
14042 // on them. Pin via an explicitly-panicking projection across a
14043 // mix of forms (atom, list-with-non-symbol-head, list with
14044 // unrecognized-by-classifier symbol head). A regression that
14045 // wrongly invokes `project` on a rejected form fires the panic.
14046 let src = r#"bare-atom
14047 (5 :not-a-symbol-head)
14048 (defmonitor :name "decoder-rejects-me")"#;
14049 let mut e = Expander::new();
14050 // The closed-set classifier admits NOTHING in this source.
14051 let out: Vec<()> = e
14052 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
14053 src,
14054 |_h: &str| -> Option<()> { None },
14055 |(), _args| -> Result<()> {
14056 panic!("project must not run for classifier-rejected forms")
14057 },
14058 )
14059 .expect("classifier-rejects-all source must collect to empty Vec");
14060 assert!(
14061 out.is_empty(),
14062 "no classifier-accepted forms — empty Vec, got {out:?}"
14063 );
14064 }
14065
14066 #[test]
14067 fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
14068 // Reader + `expand_program` + classifier all succeed; the
14069 // per-form `project` errors on the SECOND matched form. Pin: the
14070 // collect's short-circuit fires at the second match's error, the
14071 // third match's `project` is NEVER invoked, and the returned
14072 // error is exactly the one the closure raised. Sibling of the
14073 // keyword-from-source variant — one corner over in the 2×2.
14074 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14075 enum Op {
14076 Foo,
14077 }
14078 fn op_from_keyword(head: &str) -> Option<Op> {
14079 (head == "foo").then_some(Op::Foo)
14080 }
14081 let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
14082 let mut seen = Vec::new();
14083 let mut e = Expander::new();
14084 let err = e
14085 .expand_source_and_collect_calls_to_any::<(), _, _, _>(
14086 src,
14087 op_from_keyword,
14088 |_op: Op, args: &[Sexp]| -> Result<()> {
14089 let n = args[1].as_int().expect("test args are ints");
14090 seen.push(n);
14091 if n == 2 {
14092 Err(LispError::Compile {
14093 form: "test".to_string(),
14094 message: "stop at two".to_string(),
14095 })
14096 } else {
14097 Ok(())
14098 }
14099 },
14100 )
14101 .expect_err("project's error must short-circuit collect");
14102 assert_eq!(seen, vec![1, 2], "third match's project must never run");
14103 assert!(
14104 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
14105 "short-circuit must propagate the project's error verbatim, got {err:?}"
14106 );
14107 }
14108
14109 #[test]
14110 fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
14111 ) {
14112 // The post-lift composition law binding the keyword-from-source
14113 // sibling to the typed-decoded primitive:
14114 // `expand_source_and_collect_calls_to(src, k, project) ==
14115 // expand_source_and_collect_calls_to_any(src,
14116 // |h| (h == k).then_some(()),
14117 // |(), args| project(args))`
14118 // Pin shape AND ordering across THREE representative keywords
14119 // (matching some, matching exactly one, matching none) on the
14120 // SAME mixed source the keyword path-uniformity test exercises.
14121 // A regression that re-implements the keyword-from-source filter
14122 // as a parallel `read + expand_and_collect_calls_to` pipeline
14123 // (rather than routing through the typed-decoded primitive)
14124 // fails the value pin.
14125 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
14126 (foo :idx 1)
14127 (emit-foo 2)
14128 (bar :idx 99)
14129 (foo :idx 3)";
14130 for k in ["foo", "bar", "missing"] {
14131 let mut e_keyword = Expander::new();
14132 let via_keyword: Vec<i64> = e_keyword
14133 .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
14134 .unwrap();
14135
14136 let mut e_classifier = Expander::new();
14137 let via_classifier: Vec<i64> = e_classifier
14138 .expand_source_and_collect_calls_to_any(
14139 src,
14140 |h: &str| (h == k).then_some(()),
14141 |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14142 )
14143 .unwrap();
14144
14145 assert_eq!(
14146 via_keyword, via_classifier,
14147 "keyword from-source path must equal classifier from-source path for {k:?}"
14148 );
14149 }
14150 }
14151
14152 #[test]
14153 fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14154 // Critical ordering preserved across the typed-decoded
14155 // from-source posture: `defmacro` source registers in
14156 // `expand_program`, its call sites expand to a classifier-
14157 // accepted form, AND the classifier walk sees both the
14158 // macro-emitted form and any directly-authored matches in
14159 // source order. Sibling of the keyword-from-source ordering
14160 // test, one corner over in the 2×2.
14161 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14162 enum Op {
14163 Foo,
14164 }
14165 fn op_from_keyword(head: &str) -> Option<Op> {
14166 (head == "foo").then_some(Op::Foo)
14167 }
14168 let src = "(defmacro emit-foo (n) `(foo :name ,n))
14169 (emit-foo \"alpha\")
14170 (foo :name \"beta\")";
14171 let mut e = Expander::new();
14172 let names: Vec<(Op, String)> = e
14173 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14174 Ok((op, args[1].as_string().unwrap().to_string()))
14175 })
14176 .expect("macroexpanded + directly-authored forms must both flow through classifier");
14177 assert_eq!(
14178 names,
14179 vec![
14180 (Op::Foo, "alpha".to_string()),
14181 (Op::Foo, "beta".to_string()),
14182 ]
14183 );
14184 }
14185
14186 // ── expand_source_program — from-source × yield-all primitive ───
14187 //
14188 // Closes the 2×2 program-level walk family on the `Expander` surface:
14189 // from-forms × yield-all (`expand_program`), from-forms × keyword-
14190 // projected (`expand_and_collect_calls_to`), from-source × keyword-
14191 // projected (`expand_source_and_collect_calls_to`), AND now
14192 // from-source × yield-all (`expand_source_program`). Pins (a) the
14193 // happy path (mixed forms flow through reader then expand_program),
14194 // (b) reader-stage short-circuit (no expand_program side-effects),
14195 // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14196 // is rejected at its offending form), (d) defmacro absorption (the
14197 // primitive's side-effect on `self.macros` matches the from-forms
14198 // posture's side-effect verbatim), (e) the empty-source boundary,
14199 // and (f) structural identity vs. the inlined pre-lift `read +
14200 // expand_program` pipeline (path-uniformity gate).
14201
14202 #[test]
14203 fn expand_source_program_routes_through_reader_then_expand_program() {
14204 // Happy path: a source string with mixed forms (a `defmacro`
14205 // definition, a `defmonitor` form, a plain symbol literal) flows
14206 // through `read` then through the from-forms primitive. Pin the
14207 // SAME emission shape the from-forms test pins, sourced from a
14208 // `&str` rather than a `Vec<Sexp>`.
14209 let src = r#"(defmacro id (x) `,x)
14210 (defmonitor :name "alpha")
14211 bare-symbol"#;
14212 let mut e = Expander::new();
14213 let out = e
14214 .expand_source_program(src)
14215 .expect("mixed forms must compose");
14216 // `(defmacro id …)` is consumed as a side-effect (registers `id`
14217 // in `e.macros`), so the returned `Vec<Sexp>` contains the
14218 // remaining two top-level forms in source order.
14219 assert_eq!(out.len(), 2);
14220 assert_eq!(
14221 out[0].as_call_to("defmonitor").map(<[_]>::len),
14222 Some(2),
14223 "first surviving form is the defmonitor with two args, got: {:?}",
14224 out[0]
14225 );
14226 assert_eq!(
14227 out[1].as_symbol(),
14228 Some("bare-symbol"),
14229 "second surviving form is the bare symbol literal, got: {:?}",
14230 out[1]
14231 );
14232 // The defmacro side-effect landed in the expander's macro table.
14233 assert!(
14234 e.has("id"),
14235 "defmacro must register `id` in the expander's macro table"
14236 );
14237 }
14238
14239 #[test]
14240 fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14241 // The reader runs BEFORE `expand_program` — so a reader error (an
14242 // unterminated string, an unbalanced paren) must short-circuit
14243 // the entire composition and `expand_program` must NEVER run.
14244 // Pin the ordering: a `defmacro` BEFORE the unterminated string
14245 // is NOT registered, because the reader fails at parse time
14246 // before `expand_program` sees any form.
14247 let mut e = Expander::new();
14248 let err = e
14249 .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14250 .expect_err("reader error must short-circuit before expand_program");
14251 // Sanity: the error IS a reader-stage rejection.
14252 let rendered = format!("{err}").to_lowercase();
14253 assert!(
14254 rendered.contains("string")
14255 || rendered.contains("paren")
14256 || rendered.contains("eof")
14257 || rendered.contains("unexpected")
14258 || rendered.contains("unterminated")
14259 || rendered.contains("unclosed"),
14260 "error must be the reader-stage rejection, got: {err}"
14261 );
14262 // Path-uniformity gate: the `defmacro` lexically BEFORE the
14263 // unterminated string must NOT have been absorbed — `read`'s
14264 // failure short-circuits the entire pipeline before
14265 // `expand_program` walks any forms.
14266 assert!(
14267 !e.has("should-not-register"),
14268 "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14269 );
14270 }
14271
14272 #[test]
14273 fn expand_source_program_short_circuits_on_expand_program_error() {
14274 // Reader succeeds, but `expand_program` rejects a malformed
14275 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14276 // ordering: the expand_program-stage rejection bubbles up
14277 // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14278 let mut e = Expander::new();
14279 let err = e
14280 .expand_source_program("(defmacro 5 (x) `,x)")
14281 .expect_err("expand_program error must propagate");
14282 let rendered = format!("{err}");
14283 assert!(
14284 rendered.contains("NAME") || rendered.contains("symbol"),
14285 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14286 );
14287 }
14288
14289 #[test]
14290 fn expand_source_program_yields_empty_vec_for_empty_source() {
14291 // Boundary: empty source string yields zero items. Pin the
14292 // degenerate boundary — empty in, empty out — sibling of the
14293 // from-source × keyword-projected test of the same name, one
14294 // level down the composition (no keyword filter).
14295 let mut e = Expander::new();
14296 let out = e
14297 .expand_source_program("")
14298 .expect("empty source is not an error");
14299 assert!(
14300 out.is_empty(),
14301 "empty source must yield empty Vec, got: {out:?}"
14302 );
14303 }
14304
14305 #[test]
14306 fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14307 // Critical pipeline order: a `defmacro` in the source registers
14308 // AND a subsequent call to it expands AND the expanded form
14309 // surfaces in the returned Vec. Sourced from `&str`, the
14310 // from-source posture preserves the side-effect-then-expansion
14311 // semantics of the from-forms primitive verbatim.
14312 let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14313 (emit-monitor "alpha")
14314 (emit-monitor "beta")"#;
14315 let mut e = Expander::new();
14316 let out = e
14317 .expand_source_program(src)
14318 .expect("defmacro absorption then expansion must compose");
14319 // Two surviving forms (the `defmacro` itself is consumed as a
14320 // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14321 assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14322 for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14323 let args = out[i]
14324 .as_call_to("defmonitor")
14325 .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14326 assert_eq!(args[0].as_keyword(), Some("name"));
14327 assert_eq!(args[1].as_string(), Some(expected_name));
14328 }
14329 // The macro is now in the table — a subsequent call would
14330 // expand against this SAME expander.
14331 assert!(e.has("emit-monitor"));
14332 }
14333
14334 #[test]
14335 fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14336 // Structural identity: for any `src` whose pieces succeed, the
14337 // from-source method's result equals the inlined pre-lift
14338 // pipeline `read + expand_program`. Pin shape AND ordering on a
14339 // mixed source (defmacro definition + macro call + plain form)
14340 // so a regression that drifts the method's pipeline from the
14341 // inlined closed-form fails here. This is the lift's
14342 // load-bearing path-uniformity gate — the SAME assertion holds
14343 // for both production consumers (`RealizedCompiler::compile` +
14344 // `realize_in_memory`'s `:macros` loop) at the primitive's
14345 // contract level.
14346 let src = r#"(defmacro id (x) `,x)
14347 (id (foo 1 2))
14348 (bar)"#;
14349
14350 // Inlined pre-lift pipeline.
14351 let mut e_inline = Expander::new();
14352 let inline_forms = read(src).unwrap();
14353 let via_inline = e_inline
14354 .expand_program(inline_forms)
14355 .expect("inlined pipeline must succeed");
14356
14357 // From-source method pipeline.
14358 let mut e_method = Expander::new();
14359 let via_method = e_method
14360 .expand_source_program(src)
14361 .expect("from-source method pipeline must succeed");
14362
14363 assert_eq!(
14364 via_inline, via_method,
14365 "from-source method must emit byte-identical result to inlined read+expand_program"
14366 );
14367 // Both pipelines registered the same defmacro side-effect.
14368 assert_eq!(e_inline.has("id"), e_method.has("id"));
14369 assert!(e_method.has("id"));
14370 }
14371
14372 #[test]
14373 fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14374 // Per-`&mut self` absorption posture pin: a `defmacro` registered
14375 // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14376 // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14377 // build-up depends on — every iteration's `expand_source_program(
14378 // macro_src)?` lands its `defmacro` heads into the SAME mutable
14379 // `preloaded` expander, so a follow-up `:macros` source can
14380 // invoke macros defined in earlier ones.
14381 let mut e = Expander::new();
14382 let _ = e
14383 .expand_source_program("(defmacro outer (n) `(inner ,n))")
14384 .unwrap();
14385 assert!(e.has("outer"));
14386 // Call 2: defines `inner` AND invokes `outer`, which expands to
14387 // `(inner <n>)`. The expansion proves call 1's `outer` survived
14388 // into call 2's expansion.
14389 let out = e
14390 .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14391 .unwrap();
14392 assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14393 // (outer 42) → (inner 42) → (wrapped 42)
14394 let args = out[0]
14395 .as_call_to("wrapped")
14396 .expect("nested expansion must reach `wrapped`");
14397 assert_eq!(args[0].as_int(), Some(42));
14398 }
14399
14400 // ── Expander::register_macro_def: the macro-registration primitive ──
14401 //
14402 // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14403 // the byte-identical two-step block —
14404 //
14405 // if self.compile_templates {
14406 // self.templates.insert(def.name.clone(), compile_template(&def)?);
14407 // }
14408 // self.macros.insert(def.name.clone(), def);
14409 //
14410 // — that lived inline at `with_macros` (the bulk-from-iterator
14411 // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14412 // program-walk-time registration site) into ONE named method on the
14413 // `Expander` surface. The tests below pin:
14414 //
14415 // (a) the bytecode-default expander (`Expander::new()`) populates
14416 // BOTH `macros` AND `templates` keyed by `def.name`,
14417 // (b) the substitute-only expander (`Expander::new_substitute_only()`)
14418 // populates `macros` but SKIPS `templates` — the
14419 // `compile_templates: false` gate fires structurally,
14420 // (c) a template that fails to compile (`,foo` against `params:
14421 // []`) short-circuits BEFORE `self.macros.insert` runs, so the
14422 // failed registration leaves both tables untouched — no
14423 // partial-write window in which `self.macros.has(name)` is
14424 // true but `self.templates.has(name)` is missing,
14425 // (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14426 // expander produce structurally identical state (both tables'
14427 // sets of keys + the same `MacroDef` body under each key),
14428 // (e) `expand_program` of one `(defmacro …)` form and
14429 // `register_macro_def` of the parsed `MacroDef` produce
14430 // structurally identical state — closing path-uniformity
14431 // across the lift's two consumers AT the registration site.
14432 //
14433 // The tests bind on `Expander::has(name)` / `Expander::len()` for
14434 // the macros table and on bytecode-vs-substitute output equivalence
14435 // (a registered macro must expand to the SAME result regardless of
14436 // whether the bytecode path's `self.templates` entry exists) for the
14437 // templates table.
14438
14439 fn macro_def_id() -> MacroDef {
14440 // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14441 // one required param, a single-symbol unquote body. Compiles to
14442 // a valid `CompiledTemplate` (no unbound vars), so
14443 // `register_macro_def` succeeds on every Expander posture.
14444 MacroDef {
14445 name: "id".into(),
14446 params: MacroParams {
14447 required: vec!["x".into()],
14448 optional: vec![],
14449 rest: None,
14450 },
14451 // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14452 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14453 }
14454 }
14455
14456 fn macro_def_bad_template() -> MacroDef {
14457 // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14458 // against an EMPTY required-params list. `compile_template`
14459 // rejects it with `UnboundTemplateVar` because `unbound` is not
14460 // in `params.names()`. Used to exercise the
14461 // `compile_template`-failed-but-self.macros-still-pristine
14462 // path-uniformity pin.
14463 MacroDef {
14464 name: "bad".into(),
14465 params: MacroParams::default(),
14466 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14467 }
14468 }
14469
14470 #[test]
14471 fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14472 // The bytecode-default `Expander::new()` carries
14473 // `compile_templates: true`; `register_macro_def` MUST populate
14474 // BOTH `self.macros` (the substitute path's registry) AND
14475 // `self.templates` (the bytecode path's pre-compiled bytecode
14476 // index) keyed by `def.name`. Fail-before-pass-after: this
14477 // assert requires the new method to exist AND to compose the
14478 // two side-effects in canonical order on a single-call
14479 // registration — pre-lift `register_macro_def` did not exist.
14480 let mut e = Expander::new();
14481 e.register_macro_def(macro_def_id())
14482 .expect("well-formed MacroDef must register");
14483 assert!(
14484 e.has("id"),
14485 "self.macros must carry the registered name after register_macro_def"
14486 );
14487 assert!(
14488 e.templates.contains_key("id"),
14489 "self.templates must carry the compiled bytecode under the bytecode-default posture"
14490 );
14491 // The registered macro must expand correctly through the
14492 // bytecode strategy — proves the inserted template is the right
14493 // bytecode for the body.
14494 let out = e
14495 .expand_program(read("(id 42)").unwrap())
14496 .expect("registered macro must expand");
14497 assert_eq!(out.len(), 1);
14498 assert_eq!(out[0], Sexp::int(42));
14499 }
14500
14501 #[test]
14502 fn register_macro_def_substitute_only_skips_templates() {
14503 // `Expander::new_substitute_only()` carries `compile_templates:
14504 // false`; `register_macro_def` MUST populate `self.macros` for
14505 // the substitute path but SKIP `self.templates` (no bytecode
14506 // pre-compile). Pin the `compile_templates: false` gate fires
14507 // structurally — a regression that drifts the conditional from
14508 // the registration primitive (e.g. a future emitter that
14509 // unconditionally pre-compiles) would silently double the
14510 // benchmark baseline's allocation footprint.
14511 let mut e = Expander::new_substitute_only();
14512 e.register_macro_def(macro_def_id())
14513 .expect("well-formed MacroDef must register under substitute-only");
14514 assert!(
14515 e.has("id"),
14516 "self.macros must carry the registered name even under substitute-only"
14517 );
14518 assert!(
14519 !e.templates.contains_key("id"),
14520 "self.templates MUST be empty under compile_templates: false — the gate fires"
14521 );
14522 // The registered macro must still expand correctly through the
14523 // substitute strategy — proves the inserted MacroDef is the
14524 // right body for substitute-walking.
14525 let out = e
14526 .expand_program(read("(id 42)").unwrap())
14527 .expect("registered macro must expand via substitute path");
14528 assert_eq!(out.len(), 1);
14529 assert_eq!(out[0], Sexp::int(42));
14530 }
14531
14532 #[test]
14533 fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14534 // The structural ordering pin: when `compile_template(&def)?`
14535 // rejects (e.g. `,unbound` against empty params), the `?` MUST
14536 // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14537 // def)` runs, so a failed registration leaves BOTH tables
14538 // exactly as they were. Without this ordering a future
14539 // bytecode-strategy lookup would resolve `self.macros.get(name)`
14540 // (the body was inserted) AND find `self.templates.get(name)`
14541 // returning `None` (the pre-compile failed), silently coercing
14542 // the macro onto the substitute fallback path despite
14543 // `compile_templates: true`. The lift preserves the pre-lift
14544 // ordering (`compile_template` precedes `self.macros.insert`)
14545 // structurally — the test pins it across the lift.
14546 let mut e = Expander::new();
14547 let err = e
14548 .register_macro_def(macro_def_bad_template())
14549 .expect_err("unbound-template body must reject");
14550 // The rejection is the structural variant the bytecode
14551 // strategy's template-gate emits.
14552 assert!(
14553 matches!(err, LispError::UnboundTemplateVar { .. }),
14554 "expected UnboundTemplateVar, got: {err:?}"
14555 );
14556 // Both tables MUST be pristine — no partial write.
14557 assert!(
14558 !e.has("bad"),
14559 "self.macros must be untouched after compile_template failure"
14560 );
14561 assert!(
14562 !e.templates.contains_key("bad"),
14563 "self.templates must be untouched after compile_template failure"
14564 );
14565 }
14566
14567 #[test]
14568 fn with_macros_routes_through_register_macro_def_path_uniformity() {
14569 // Path-uniformity pin across the bulk-constructor consumer:
14570 // `with_macros([def])` MUST produce the same final state as
14571 // `Expander::new()` + `register_macro_def(def)`. A regression
14572 // that drifts the constructor's per-MacroDef inline block from
14573 // the registration primitive (e.g. a future emitter that
14574 // re-inlines the two-step block at `with_macros` rather than
14575 // delegating) would fail loudly here.
14576 let mut via_register = Expander::new();
14577 via_register
14578 .register_macro_def(macro_def_id())
14579 .expect("register must succeed");
14580 let mut via_with_macros =
14581 Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14582 assert_eq!(via_register.len(), via_with_macros.len());
14583 assert!(via_register.has("id"));
14584 assert!(via_with_macros.has("id"));
14585 // Both tables key on the same set across both postures.
14586 assert_eq!(
14587 via_register.templates.contains_key("id"),
14588 via_with_macros.templates.contains_key("id"),
14589 "self.templates key-presence must agree across with_macros and register_macro_def"
14590 );
14591 // Both registered expanders expand the registered macro
14592 // identically — the strongest behavioral parity.
14593 let out_a = via_register
14594 .expand_program(read("(id 99)").unwrap())
14595 .unwrap();
14596 let out_b = via_with_macros
14597 .expand_program(read("(id 99)").unwrap())
14598 .unwrap();
14599 assert_eq!(out_a, out_b);
14600 assert_eq!(out_a, vec![Sexp::int(99)]);
14601 }
14602
14603 #[test]
14604 fn expand_program_routes_through_register_macro_def_path_uniformity() {
14605 // Path-uniformity pin across the program-walk consumer:
14606 // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14607 // same final state as a direct
14608 // `register_macro_def(macro_def_id())`. A regression that
14609 // drifts `expand_program`'s `(defmacro …)`-head arm from the
14610 // registration primitive (e.g. a future emitter that re-inlines
14611 // the two-step block at `expand_program` rather than delegating)
14612 // would fail loudly here.
14613 let mut via_register = Expander::new();
14614 via_register
14615 .register_macro_def(macro_def_id())
14616 .expect("register must succeed");
14617 let mut via_expand_program = Expander::new();
14618 let yielded = via_expand_program
14619 .expand_program(read("(defmacro id (x) `,x)").unwrap())
14620 .expect("expand_program of one defmacro must succeed");
14621 // expand_program drops the (defmacro …) form from its returned
14622 // Vec<Sexp> (defmacro is a definition, not a program form), so
14623 // the yielded list is empty — pin the side-effect-only posture.
14624 assert!(
14625 yielded.is_empty(),
14626 "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14627 );
14628 assert!(via_register.has("id"));
14629 assert!(via_expand_program.has("id"));
14630 assert_eq!(via_register.len(), via_expand_program.len());
14631 // Both tables key on the same set across both postures.
14632 assert_eq!(
14633 via_register.templates.contains_key("id"),
14634 via_expand_program.templates.contains_key("id"),
14635 "self.templates key-presence must agree across expand_program and register_macro_def"
14636 );
14637 // Both registered expanders expand the registered macro
14638 // identically — the strongest behavioral parity, closing
14639 // path-uniformity across BOTH consumers (with_macros above,
14640 // expand_program here) at ONE primitive.
14641 let out_a = via_register
14642 .expand_program(read("(id 7)").unwrap())
14643 .unwrap();
14644 let out_b = via_expand_program
14645 .expand_program(read("(id 7)").unwrap())
14646 .unwrap();
14647 assert_eq!(out_a, out_b);
14648 assert_eq!(out_a, vec![Sexp::int(7)]);
14649 }
14650
14651 // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14652 //
14653 // The new typed-marker projection `Sexp::as_unquote` lifts the
14654 // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14655 // literal) pair into ONE structural query. These tests pin behavioral
14656 // parity end-to-end across BOTH expansion strategies:
14657 // * `compile_node` — the bytecode-template strategy's typed marker
14658 // dispatch routes through as_unquote at the compile step.
14659 // * `substitute` (top-level + list-inner) — the substitute-walker
14660 // fallback strategy's typed marker dispatch routes through
14661 // as_unquote at both per-form sites.
14662 // The structural invariant the prior runs' `expansion_layers_agree_on_
14663 // output_and_cache_wins` benchmark observes — bytecode AND substitute
14664 // produce byte-identical output — is anchored here at the macro-template
14665 // level for every distinct unquote-family shape the substitute and
14666 // bytecode strategies discriminate.
14667
14668 #[test]
14669 fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14670 // A template body whose only marker is a top-level `,x`. Both
14671 // expansion strategies route through `as_unquote` (compile_node for
14672 // bytecode, substitute for the fallback walker), each pairing
14673 // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14674 // Pin byte-identical output across both strategies — the
14675 // structural-invariant the new projection's lift was designed to
14676 // make load-bearing structural rather than per-site discipline.
14677 let src = "(defmacro id (x) ,x) (id 42)";
14678 let mut bc = Expander::new();
14679 let mut sub = Expander::new_substitute_only();
14680 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14681 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14682 assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14683 assert_eq!(out_bc, vec![Sexp::int(42)]);
14684 }
14685
14686 #[test]
14687 fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14688 // A template body whose marker is a list-inner `,@xs`. The substitute
14689 // strategy's list-inner Splice arm routes through `as_unquote`
14690 // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14691 // compile_node Splice arm routes through `as_unquote` matching the
14692 // same. Pin byte-identical output across both strategies for the
14693 // splice path — the projection's typed-marker pairing
14694 // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14695 // identical at every consumer post-lift.
14696 let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14697 let mut bc = Expander::new();
14698 let mut sub = Expander::new_substitute_only();
14699 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14700 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14701 assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14702 let expected = Sexp::List(vec![
14703 Sexp::symbol("list"),
14704 Sexp::int(0),
14705 Sexp::int(1),
14706 Sexp::int(2),
14707 Sexp::int(3),
14708 Sexp::int(99),
14709 ]);
14710 assert_eq!(out_bc, vec![expected]);
14711 }
14712
14713 #[test]
14714 fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14715 // The substitute strategy's top-level `,@x` arm rejects with
14716 // LispError::SpliceOutsideList (a splice marker with no containing
14717 // list to flatten into). Pre-lift the arm was
14718 // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14719 // post-lift the arm routes through `as_unquote` matching
14720 // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14721 // marker. Pin path-uniformity: the rejection MUST fire identically
14722 // through the new projection. The substitute-only strategy bypasses
14723 // the bytecode path, exposing this gate directly.
14724 //
14725 // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14726 // `,@xs` at top level (NOT wrapped in a containing list), so the
14727 // substitute fallback's top-level Splice arm fires.
14728 let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14729 let mut sub = Expander::new_substitute_only();
14730 let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14731 assert!(
14732 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14733 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14734 );
14735 }
14736
14737 #[test]
14738 fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14739 // A `,unbound` template body — the inner symbol isn't a param —
14740 // fires gate-2 (must-be-bound-in-scope). The typed marker
14741 // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14742 // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14743 // rejection-builder. Pre-lift the marker was bound at the per-arm
14744 // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14745 // typed projection. Pin: the rejection's `prefix` slot is
14746 // UnquoteForm::Unquote, structurally derived from the typed
14747 // projection's typed marker, NOT a literal at the arm body.
14748 let src = "(defmacro bad (x) ,unbound)";
14749 let mut bc = Expander::new();
14750 let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14751 match err {
14752 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14753 assert_eq!(
14754 prefix,
14755 UnquoteForm::Unquote,
14756 "typed marker drifted from UnquoteForm::Unquote at gate-2"
14757 );
14758 }
14759 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14760 }
14761 // Sibling negative control: `,@unbound` inside a containing list
14762 // threads UnquoteForm::Splice through the same projection. The
14763 // splice MUST be inside a list — `compile_template` rejects
14764 // top-level `,@X` bodies with SpliceOutsideList before compile_node
14765 // runs, so the typed-marker dispatch on Splice is observable only
14766 // for well-positioned splice bodies. Closes path-uniformity across
14767 // BOTH typed marker variants at the compile path.
14768 let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14769 let mut bc2 = Expander::new();
14770 let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14771 match err_splice {
14772 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14773 assert_eq!(
14774 prefix,
14775 UnquoteForm::Splice,
14776 "typed marker drifted from UnquoteForm::Splice at gate-2"
14777 );
14778 }
14779 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14780 }
14781 }
14782
14783 // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14784 //
14785 // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14786 // two arms and `substitute`'s top-level + list-inner arms through the
14787 // typed-marker projection but LEFT `compile_template`'s top-level
14788 // splice-outside-list gate and `contains_unquote`'s family check inline
14789 // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14790 // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14791 // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14792 // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14793 // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14794 // Every production-site recognizer of an unquote-family wrapper now
14795 // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14796 // variant) pairing for `,@-outside-list` is bound at ONE structural
14797 // query across all FOUR reachable splice-recognition sites, and a future
14798 // regression that drifts the marker pairing at any production site
14799 // becomes a type error at the helper boundary rather than a silent
14800 // per-site divergence.
14801
14802 #[test]
14803 fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14804 // The bytecode path's `compile_template` gate pre-rejects top-level
14805 // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14806 // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14807 // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14808 // match outside the projection itself. Post-lift the (Sexp variant,
14809 // UnquoteForm variant) pairing for the splice-outside-list gate is
14810 // bound at ONE projection function across all three reachable
14811 // emission sites (compile_template top-level, substitute top-level,
14812 // substitute list-inner). Path-uniformity guard at the new boundary:
14813 // the bytecode-path compile-time gate now routes through the same
14814 // shape `substitute`'s list-inner Splice arm uses.
14815 //
14816 // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14817 // peeled by `template_body`, leaving `,@xs` at top level for
14818 // `compile_template` to gate before `compile_node` walks anything.
14819 // The bytecode strategy is default-on (`Expander::new()` sets
14820 // `compile_templates: true`), so the gate fires at
14821 // `register_macro_def` time.
14822 let mut e = Expander::new();
14823 let err = e
14824 .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14825 .expect_err("compile_template must reject top-level ,@X via as_unquote");
14826 assert!(
14827 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14828 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14829 );
14830 }
14831
14832 #[test]
14833 fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14834 // Negative control on the typed-marker dispatch at `compile_template`'s
14835 // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14836 // valid template — `compile_node` lowers it to a single `Subst(idx)`
14837 // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14838 // gate MUST NOT fire on the Unquote variant — only Splice — so the
14839 // typed marker is observably load-bearing: a regression that drifts
14840 // the pattern from `UnquoteForm::Splice` to the wider
14841 // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14842 //
14843 // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14844 // level; `compile_template` admits it via the typed-marker gate,
14845 // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14846 let src = "(defmacro id (x) ,x) (id 42)";
14847 let mut e = Expander::new();
14848 let expanded = e
14849 .expand_program(read(src).unwrap())
14850 .expect("top-level ,X body must compile through as_unquote-typed gate");
14851 assert_eq!(expanded, vec![Sexp::int(42)]);
14852 }
14853
14854 #[test]
14855 fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14856 // The fast-path optimizer in `compile_node` short-circuits on
14857 // `contains_unquote(node)` — true iff the subtree carries an
14858 // unquote-family wrapper. Pre-lift the family check inlined
14859 // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14860 // routes through `as_unquote().is_some()`. Pin path-uniformity at
14861 // the family-recognition boundary: every production-site unquote
14862 // recognizer (compile_template's gate, compile_node's per-arm
14863 // dispatch, substitute's top-level + list-inner arms, AND this
14864 // fast-path predicate) now shares ONE typed-marker projection.
14865 //
14866 // Both variants must trigger the fast-path bail — the optimizer's
14867 // gate keys on the family, not the inner. The recursion into
14868 // Quote/Quasiquote/List subtrees ALSO observes the same family
14869 // gate at every level, so a `,@xs` buried under a `\`...` outer
14870 // wrapper still fires it.
14871 let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14872 let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14873 let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14874 Sexp::symbol("foo"),
14875 Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14876 ])));
14877 // The projection's `is_some()` face MUST agree with the pre-lift
14878 // `matches!()` discriminant on every variant — closed-set guarantee
14879 // shared with `as_unquote`'s contract pin in `ast.rs`.
14880 assert!(super::contains_unquote(&bare_unquote));
14881 assert!(super::contains_unquote(&bare_splice));
14882 assert!(super::contains_unquote(&nested));
14883 // Negative control: shapes the projection rejects (Nil, Atom,
14884 // bare List, Quote-family without inner unquote) must NOT trigger
14885 // the fast-path bail — the projection's None face stays observably
14886 // distinct from its Some face.
14887 assert!(!super::contains_unquote(&Sexp::Nil));
14888 assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14889 assert!(!super::contains_unquote(&Sexp::int(5)));
14890 assert!(!super::contains_unquote(&Sexp::List(vec![
14891 Sexp::symbol("plain"),
14892 Sexp::int(1),
14893 ])));
14894 assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14895 Sexp::symbol("inert")
14896 ))));
14897 }
14898
14899 #[test]
14900 fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14901 // Pin the lift: `contains_unquote`'s quote-family recognition
14902 // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14903 // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14904 // substitution-subset gate) at ONE site — not through the pair
14905 // of `as_unquote().is_some()` + inline
14906 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14907 // path-uniformity assertion: for every quote-family wrapper, the
14908 // function's behavior agrees with the manual composition through
14909 // the two typed-marker projections. A regression that re-inlines
14910 // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14911 // and just returns true for any `as_quote_form().is_some()`, or
14912 // restores the per-variant `Quote | Quasiquote` arm) drifts from
14913 // this composition and surfaces here.
14914 use crate::ast::QuoteForm;
14915
14916 // Sweep every QuoteForm variant under both axes:
14917 // * unquote-only-inner (inner = symbol):
14918 // Unquote(s) → true via `as_unquote_form() == Some`
14919 // UnquoteSplice(s) → true via `as_unquote_form() == Some`
14920 // Quote(s) → false (inner has no unquote, recurses to false)
14921 // Quasiquote(s) → false (same)
14922 // * unquote-inner-inside-quote-wrapper:
14923 // Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14924 // inner `Unquote` returns true through the
14925 // SAME projection — both arms share the
14926 // ONE typed-marker site post-lift)
14927 let inner_plain = Sexp::symbol("x");
14928 let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14929
14930 for qf in QuoteForm::ALL {
14931 let wrapped_plain = match qf {
14932 QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14933 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14934 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14935 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14936 };
14937 // Path-uniformity: behavior derives from the manual two-marker
14938 // composition through `as_quote_form` + `as_unquote_form`.
14939 // The substitution-subset gate (`as_unquote_form().is_some()`)
14940 // returns true for Unquote/UnquoteSplice and false for
14941 // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14942 let via_manual =
14943 qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14944 assert_eq!(
14945 super::contains_unquote(&wrapped_plain),
14946 via_manual,
14947 "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14948 );
14949
14950 let wrapped_unquote = match qf {
14951 QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14952 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14953 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14954 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14955 };
14956 // EVERY quote-family wrapper around an inner `Unquote` must
14957 // contain unquote — either the outer projects as `Some` on
14958 // the substitution subset (Unquote/UnquoteSplice arms), OR
14959 // the outer is Quote/Quasiquote and the recursion into the
14960 // inner re-fires the projection. The pre-lift Quote arm
14961 // recursed via the inline `Sexp::Quote(inner)` match;
14962 // post-lift the SAME recursion fires via `as_quote_form`'s
14963 // typed projection. Both yield `true`.
14964 assert!(
14965 super::contains_unquote(&wrapped_unquote),
14966 "contains_unquote missed an inner Unquote under {qf:?} — \
14967 the quote-family recursion through as_quote_form drifted"
14968 );
14969 }
14970
14971 // Negative control: a non-quote-family wrapper (List, Atom,
14972 // Nil) must NOT route through `as_quote_form` at all. The
14973 // `if let Some(...) = node.as_quote_form()` gate stays `None`
14974 // and falls through to the List/_ arms. This is the structural
14975 // partition: the projection's `None` face MUST agree with
14976 // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14977 // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14978 let nested_in_list = Sexp::List(vec![
14979 Sexp::symbol("outer"),
14980 Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14981 ]);
14982 assert!(
14983 super::contains_unquote(&nested_in_list),
14984 "contains_unquote failed to descend into a List subtree containing a \
14985 Quasiquote(Unquote(_)) — list recursion arm drifted"
14986 );
14987 }
14988
14989 // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14990 // × typed-decoded classifier) ─────────────────────────────────────
14991 //
14992 // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14993 // the dispatcher matrix on the `Expander` surface — pre-lift the
14994 // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14995 // + expand_and_collect_calls_to_any) AND the constant-keyword
14996 // column closed on the named axis (expand_to_named +
14997 // expand_source_to_named) but the (named × classifier) cell was
14998 // open — every consumer that wanted to walk a program by typed-
14999 // decoded classifier AND extract the NAME slot per matched form
15000 // had to compose `expand_and_collect_calls_to_any` with
15001 // `split_name_slot` inline. Post-lift the cell is ONE named primitive
15002 // on the `Expander` surface, and the constant-`T::KEYWORD` named
15003 // sibling routes through it as the (constant-classifier × named)
15004 // specialization.
15005 //
15006 // The tests below pin: (a) the happy path yields the decoded triple
15007 // `(T, &str, &[Sexp])` for every matched form in source order; (b)
15008 // non-matching forms are skipped (soft-projection contract inherited
15009 // from the typed-decoded primitive); (c) the `NamedFormMissingName`
15010 // variant fires for matched forms with no NAME slot, threading the
15011 // classifier-supplied `&'static str` keyword; (d) the
15012 // `NamedFormNonSymbolName` variant fires for matched forms with a
15013 // non-symbol-or-string NAME slot, again threading the classifier-
15014 // supplied keyword; (e) the projection's `Err` short-circuits at
15015 // the first failing matched form; (f) `expand_program` runs BEFORE
15016 // the classifier filter walks — a `(defmacro …)` emitting a named
15017 // form is decoded by the classifier on the EXPANDED head, not the
15018 // macro-call head; (g) the from-source sibling agrees with `read +
15019 // from-forms` on the same source.
15020
15021 #[test]
15022 fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
15023 ) {
15024 // The typed-decoded named primitive's happy path: a closed-set
15025 // classifier decodes head symbols to a typed enum PAIRED with
15026 // its canonical static keyword, and the per-form projection
15027 // receives `(decoded, name, spec_args)` for every matched form
15028 // in source order. Sweep across TWO distinct classifier outcomes
15029 // — `Monitor` / `Notify` — interleaved with a non-matching form
15030 // to pin the typed-witness threading AND the NAME-slot
15031 // projection AND the source-order yield AND the rejection of
15032 // non-classifier forms in ONE assertion.
15033 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15034 enum Kind {
15035 Monitor,
15036 Notify,
15037 }
15038 let src = r#"(defmonitor cpu :threshold 80)
15039 (other-form 99)
15040 (defnotify email :to "ops@example.com")
15041 (defmonitor mem :threshold 90)"#;
15042 let forms = read(src).unwrap();
15043 let mut e = Expander::new();
15044 let rows: Vec<(Kind, String, usize)> = e
15045 .expand_and_collect_named_calls_to_any(
15046 forms,
15047 |h| match h {
15048 "defmonitor" => Some((Kind::Monitor, "defmonitor")),
15049 "defnotify" => Some((Kind::Notify, "defnotify")),
15050 _ => None,
15051 },
15052 |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
15053 )
15054 .unwrap();
15055 assert_eq!(
15056 rows,
15057 vec![
15058 (Kind::Monitor, "cpu".to_string(), 2),
15059 (Kind::Notify, "email".to_string(), 2),
15060 (Kind::Monitor, "mem".to_string(), 2),
15061 ],
15062 );
15063 }
15064
15065 #[test]
15066 fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
15067 // Soft-projection contract — every shape the typed-decoded
15068 // primitive rejects (non-list, empty list, list whose head is
15069 // not a symbol, list whose head decodes to `None`) skips the
15070 // projection silently. The named gate inside the wrapper
15071 // projection runs ONLY for classifier-matched forms; a non-
15072 // matching form that reaches the projection panics here.
15073 let src = r#":bare-keyword
15074 "bare-string"
15075 42
15076 ()
15077 (foo bar)
15078 (defmonitor cpu :threshold 80)"#;
15079 let forms = read(src).unwrap();
15080 let mut e = Expander::new();
15081 let names: Vec<String> = e
15082 .expand_and_collect_named_calls_to_any(
15083 forms,
15084 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15085 |(), name, _args| Ok(name.to_string()),
15086 )
15087 .unwrap();
15088 assert_eq!(names, vec!["cpu".to_string()]);
15089 }
15090
15091 #[test]
15092 fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
15093 ) {
15094 // `(defmonitor)` matches the classifier but has no NAME slot —
15095 // `split_name_slot` fires `NamedFormMissingName { keyword:
15096 // "defmonitor" }`. The classifier's `&'static str` keyword is
15097 // threaded VERBATIM through the named gate; a regression that
15098 // hardcodes a different keyword (e.g. the matched head text or
15099 // a default) would fail the structural variant identity
15100 // assertion. Fail-before-pass-after: pre-lift the primitive did
15101 // not exist; the inline `expand_and_collect_calls_to_any +
15102 // split_name_slot` composition at the consumer site was the
15103 // only path, with the keyword bound at the consumer's call
15104 // boundary rather than at the named primitive's body.
15105 let forms = read("(defmonitor)").unwrap();
15106 let mut e = Expander::new();
15107 let err = e
15108 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15109 forms,
15110 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15111 |(), _name, _args| Ok(()),
15112 )
15113 .unwrap_err();
15114 assert!(
15115 matches!(
15116 err,
15117 crate::error::LispError::NamedFormMissingName {
15118 keyword: "defmonitor"
15119 }
15120 ),
15121 "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
15122 );
15123 }
15124
15125 #[test]
15126 fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
15127 ) {
15128 // `(defmonitor 42 :threshold 80)` matches the classifier but
15129 // the NAME slot is an int — `split_name_slot` fires
15130 // `NamedFormNonSymbolName { keyword: "defmonitor", got:
15131 // SexpShape::Int }`. The classifier's `&'static str` keyword is
15132 // threaded through the structural rejection variant verbatim,
15133 // and the typed `SexpShape` projection on the offending slot
15134 // is preserved across the named primitive.
15135 let forms = read("(defmonitor 42 :threshold 80)").unwrap();
15136 let mut e = Expander::new();
15137 let err = e
15138 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15139 forms,
15140 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15141 |(), _name, _args| Ok(()),
15142 )
15143 .unwrap_err();
15144 assert!(
15145 matches!(
15146 err,
15147 crate::error::LispError::NamedFormNonSymbolName {
15148 keyword: "defmonitor",
15149 got: crate::error::SexpShape::Int,
15150 }
15151 ),
15152 "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15153 );
15154 }
15155
15156 #[test]
15157 fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15158 // Result projection's short-circuit contract — when the
15159 // projection returns `Err` on a matched named form, the walk
15160 // stops and subsequent matched forms are NOT projected. Pin
15161 // source-order short-circuit via a counter the projection
15162 // increments before deciding to fail: the counter sits at
15163 // exactly the index of the failing form (second match),
15164 // proving the third match never reached the projection.
15165 let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15166 let mut count = 0usize;
15167 let mut e = Expander::new();
15168 let err = e
15169 .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15170 forms,
15171 |h| (h == "defmon").then_some(((), "defmon")),
15172 |(), name, _args| {
15173 count += 1;
15174 if name == "b" {
15175 return Err(crate::error::LispError::Missing("test-failure"));
15176 }
15177 Ok(name.to_string())
15178 },
15179 )
15180 .expect_err("projection must short-circuit on first Err");
15181 assert_eq!(
15182 count, 2,
15183 "projection must have run on first matched form AND the failing form, then stopped"
15184 );
15185 assert!(
15186 matches!(err, crate::error::LispError::Missing("test-failure")),
15187 "expected the projection's typed Err verbatim, got: {err:?}"
15188 );
15189 }
15190
15191 #[test]
15192 fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15193 // Ordering contract — `expand_program` runs BEFORE the
15194 // typed-decoded named walk. A `(defmacro …)` form that emits a
15195 // classifier-decoded named form must have its EXPANDED head
15196 // decoded by the classifier, not the macro-call head. Pin the
15197 // ordering with a macro `(emit-mon n thr)` expanding to
15198 // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15199 // expanded `defmonitor` head and the NAME slot's symbol value
15200 // from the macro arg.
15201 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15202 (defmonitor cpu :threshold 80)
15203 (emit-mon mem 90)
15204 (other-form 99)
15205 (emit-mon disk 70)"#;
15206 let forms = read(src).unwrap();
15207 let mut e = Expander::new();
15208 let names: Vec<String> = e
15209 .expand_and_collect_named_calls_to_any(
15210 forms,
15211 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15212 |(), name, _args| Ok(name.to_string()),
15213 )
15214 .unwrap();
15215 // `cpu` from the literal call, `mem` from the first macro
15216 // emit, `disk` from the second macro emit. The macro-emitted
15217 // forms are present BECAUSE `expand_program` ran first.
15218 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15219 }
15220
15221 #[test]
15222 fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15223 // From-source posture parity: feeding source through
15224 // `expand_source_and_collect_named_calls_to_any` is byte-
15225 // identical to feeding `read(src)?` through the from-forms
15226 // sibling on a fresh expander, because the from-source posture
15227 // is `read(src)? + from-forms` by construction. A regression
15228 // that drifts the from-source posture's pipeline from the
15229 // from-forms posture's pipeline would fail here.
15230 let src = r#"(defmonitor cpu :threshold 80)
15231 (defmonitor mem :threshold 90)"#;
15232 let via_src: Vec<String> = Expander::new()
15233 .expand_source_and_collect_named_calls_to_any(
15234 src,
15235 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15236 |(), name, _args| Ok(name.to_string()),
15237 )
15238 .unwrap();
15239 let forms = read(src).unwrap();
15240 let via_forms: Vec<String> = Expander::new()
15241 .expand_and_collect_named_calls_to_any(
15242 forms,
15243 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15244 |(), name, _args| Ok(name.to_string()),
15245 )
15246 .unwrap();
15247 assert_eq!(via_src, via_forms);
15248 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15249 }
15250
15251 #[test]
15252 fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15253 ) {
15254 // `?`-routing through `read` short-circuits BEFORE the
15255 // classifier or the named gate runs. Unbalanced paren — reader
15256 // error fires; classifier panic-decoder MUST NOT execute,
15257 // proving the read step gates the entire pipeline.
15258 let mut e = Expander::new();
15259 let err = e
15260 .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15261 "(defmonitor cpu :threshold 80",
15262 |_h| panic!("classifier must not run when reader fails"),
15263 |(), _name, _args| Ok(()),
15264 )
15265 .unwrap_err();
15266 // The named gate's variants MUST NOT fire — the reader error
15267 // is structurally distinct.
15268 assert!(
15269 !matches!(
15270 err,
15271 crate::error::LispError::NamedFormMissingName { .. }
15272 | crate::error::LispError::NamedFormNonSymbolName { .. }
15273 ),
15274 "expected reader error, not named-gate variant; got: {err:?}"
15275 );
15276 }
15277
15278 // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15279 // constant-keyword × untyped R) ─────────────────────────────────
15280 //
15281 // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15282 // cell of the `Expander` typed-walk family — pre-lift the cell was
15283 // reachable ONLY through `expand_to_named<T>` with the
15284 // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15285 // AND the `T::compile_from_args`-based projection through
15286 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15287 // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15288 // method that takes the keyword and the `(name, args) -> R`
15289 // projection as caller-supplied parameters, AND
15290 // `expand_to_named<T>` routes through it as the typed
15291 // `T::KEYWORD`-constant specialization — so the named-form
15292 // `split_name_slot` composition lives at ONE site
15293 // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15294 // than at TWO sites pre-lift (the bare-kwargs path through
15295 // `named_form_projection<T>` AND the classifier path).
15296 //
15297 // The tests below pin: (a) the happy path yields the `(name, args)`
15298 // pair for every matched form in source order; (b) non-matching
15299 // forms are skipped (soft-projection contract inherited from the
15300 // named typed-decoded primitive); (c) the `NamedFormMissingName`
15301 // variant fires for matched forms with no NAME slot, threading the
15302 // primitive's `&'static str` keyword; (d) the
15303 // `NamedFormNonSymbolName` variant fires for matched forms with a
15304 // non-symbol-or-string NAME slot, again threading the primitive's
15305 // keyword; (e) the projection's `Err` short-circuits at the first
15306 // failing matched form; (f) `expand_program` runs BEFORE the
15307 // keyword filter walks — a `(defmacro …)` emitting a named form
15308 // has its EXPANDED head matched by the constant-keyword filter,
15309 // not the macro-call head; (g) the from-source sibling agrees with
15310 // `read + from-forms` on the same source; (h) the constant-
15311 // classifier composition law binds the runtime-keyword cell
15312 // (`expand_and_collect_named_calls_to`) to the typed-decoded
15313 // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15314 // constant-classifier decoder.
15315
15316 #[test]
15317 fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15318 ) {
15319 // The constant-keyword named primitive's happy path: a runtime
15320 // `&'static str` keyword filters matched forms, and the per-
15321 // form projection receives `(name, spec_args)` for every
15322 // matched form in source order. Three matched forms
15323 // interleaved with a non-matcher pin the source-order yield,
15324 // the NAME-slot projection, AND the rejection of non-keyword
15325 // forms in ONE assertion.
15326 let src = r#"(defmonitor cpu :threshold 80)
15327 (other-form 99)
15328 (defmonitor mem :threshold 90 :unit "MiB")
15329 (defmonitor disk :threshold 70)"#;
15330 let forms = read(src).unwrap();
15331 let mut e = Expander::new();
15332 let rows: Vec<(String, usize)> = e
15333 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15334 Ok((name.to_string(), spec_args.len()))
15335 })
15336 .unwrap();
15337 assert_eq!(
15338 rows,
15339 vec![
15340 ("cpu".to_string(), 2),
15341 ("mem".to_string(), 4),
15342 ("disk".to_string(), 2),
15343 ],
15344 );
15345 }
15346
15347 #[test]
15348 fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15349 // Soft-projection contract — every shape the named typed-
15350 // decoded primitive rejects (non-list, empty list, list whose
15351 // head is not a symbol, list whose head doesn't match the
15352 // constant keyword) skips the projection silently. The named
15353 // gate inside the wrapper projection runs ONLY for matched
15354 // forms; a non-matching form that reaches the projection
15355 // panics here.
15356 let src = r#":bare-keyword
15357 "bare-string"
15358 42
15359 ()
15360 (foo bar)
15361 (defmonitor cpu :threshold 80)"#;
15362 let forms = read(src).unwrap();
15363 let mut e = Expander::new();
15364 let names: Vec<String> = e
15365 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15366 Ok(name.to_string())
15367 })
15368 .unwrap();
15369 assert_eq!(names, vec!["cpu".to_string()]);
15370 }
15371
15372 #[test]
15373 fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15374 // The named-form gate's missing-NAME variant must thread the
15375 // primitive's `&'static str` keyword (NOT a hardcoded literal,
15376 // NOT the projection's own copy) — the `NamedFormMissingName
15377 // { keyword }` slot inside `split_name_slot` is bound at the
15378 // call site BY THE PRIMITIVE itself, via the constant-
15379 // classifier decoder threading `((), keyword)` into
15380 // `expand_and_collect_named_calls_to_any`.
15381 let src = r#"(defmonitor)"#;
15382 let forms = read(src).unwrap();
15383 let mut e = Expander::new();
15384 let err = e
15385 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15386 .unwrap_err();
15387 assert!(
15388 matches!(
15389 err,
15390 crate::error::LispError::NamedFormMissingName {
15391 keyword: "defmonitor"
15392 }
15393 ),
15394 "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15395 );
15396 }
15397
15398 #[test]
15399 fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15400 ) {
15401 // The named-form gate's non-symbol-NAME variant must thread
15402 // the primitive's `&'static str` keyword AND the typed-shape
15403 // witness (`SexpShape::Int` for an integer NAME slot). The
15404 // shape projection through `split_name_slot`'s
15405 // `as_symbol_or_string()` gate fires on the first matched
15406 // form whose NAME slot is not a symbol or string.
15407 let src = r#"(defmonitor 42 :threshold 80)"#;
15408 let forms = read(src).unwrap();
15409 let mut e = Expander::new();
15410 let err = e
15411 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15412 .unwrap_err();
15413 match err {
15414 crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15415 assert_eq!(keyword, "defmonitor");
15416 assert_eq!(got, crate::error::SexpShape::Int);
15417 }
15418 other => panic!(
15419 "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15420 ),
15421 }
15422 }
15423
15424 #[test]
15425 fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15426 // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15427 // the first failing projection. The second matched form's
15428 // projection error MUST fire AND the third matched form's
15429 // projection MUST NOT run. Pin both halves with a counter +
15430 // a fail-on-call sentinel.
15431 let src = r#"(defmonitor cpu :threshold 80)
15432 (defmonitor mem :threshold 90)
15433 (defmonitor disk :threshold 70)"#;
15434 let forms = read(src).unwrap();
15435 let mut count: usize = 0;
15436 let mut e = Expander::new();
15437 let err = e
15438 .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15439 count += 1;
15440 if name == "mem" {
15441 Err(crate::error::LispError::Compile {
15442 form: "test".into(),
15443 message: format!("rejecting at NAME={name}"),
15444 })
15445 } else {
15446 Ok(name.to_string())
15447 }
15448 })
15449 .unwrap_err();
15450 assert_eq!(count, 2, "third matched form must not be projected");
15451 match err {
15452 crate::error::LispError::Compile { message, .. } => {
15453 assert!(message.contains("NAME=mem"));
15454 }
15455 other => panic!("expected projection-driven Compile error; got: {other:?}"),
15456 }
15457 }
15458
15459 #[test]
15460 fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15461 // Ordering contract — `expand_program` runs BEFORE the
15462 // constant-keyword named walk. A `(defmacro …)` form that
15463 // emits a matched named form must have its EXPANDED head
15464 // matched by the constant-keyword filter, not the macro-call
15465 // head. Pin the ordering with a macro `(emit-mon n thr)`
15466 // expanding to `(defmonitor ,n :threshold ,thr)` — the
15467 // constant-keyword filter matches the expanded `defmonitor`
15468 // head and the NAME slot projects the symbol-author value.
15469 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15470 (defmonitor cpu :threshold 80)
15471 (emit-mon mem 90)
15472 (other-form 99)
15473 (emit-mon disk 70)"#;
15474 let forms = read(src).unwrap();
15475 let mut e = Expander::new();
15476 let names: Vec<String> = e
15477 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15478 Ok(name.to_string())
15479 })
15480 .unwrap();
15481 // `cpu` from the literal call, `mem` from the first macro
15482 // emit, `disk` from the second macro emit. The macro-emitted
15483 // forms are present BECAUSE `expand_program` ran first.
15484 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15485 }
15486
15487 #[test]
15488 fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15489 // From-source posture parity: feeding source through
15490 // `expand_source_and_collect_named_calls_to` is byte-identical
15491 // to feeding `read(src)?` through the from-forms sibling on a
15492 // fresh expander, because the from-source posture is
15493 // `read(src)? + from-forms` by construction. A regression that
15494 // drifts the from-source posture's pipeline from the from-
15495 // forms posture's pipeline would fail here.
15496 let src = r#"(defmonitor cpu :threshold 80)
15497 (defmonitor mem :threshold 90)"#;
15498 let via_src: Vec<String> = Expander::new()
15499 .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15500 Ok(name.to_string())
15501 })
15502 .unwrap();
15503 let forms = read(src).unwrap();
15504 let via_forms: Vec<String> = Expander::new()
15505 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15506 Ok(name.to_string())
15507 })
15508 .unwrap();
15509 assert_eq!(via_src, via_forms);
15510 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15511 }
15512
15513 #[test]
15514 fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15515 ) {
15516 // `?`-routing through `read` short-circuits BEFORE the
15517 // constant-keyword filter, the named gate, or the projection
15518 // runs. Unbalanced paren — reader error fires; the projection
15519 // panic-payload MUST NOT execute, proving the read step gates
15520 // the entire pipeline.
15521 let mut e = Expander::new();
15522 let err = e
15523 .expand_source_and_collect_named_calls_to::<(), _>(
15524 "(defmonitor cpu :threshold 80",
15525 "defmonitor",
15526 |_name, _args| panic!("projection must not run when reader fails"),
15527 )
15528 .unwrap_err();
15529 // The named gate's variants MUST NOT fire — the reader error
15530 // is structurally distinct.
15531 assert!(
15532 !matches!(
15533 err,
15534 crate::error::LispError::NamedFormMissingName { .. }
15535 | crate::error::LispError::NamedFormNonSymbolName { .. }
15536 ),
15537 "expected reader error, not named-gate variant; got: {err:?}",
15538 );
15539 }
15540
15541 #[test]
15542 fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15543 ) {
15544 // Composition-identity test pinning the runtime-keyword named
15545 // cell (`expand_and_collect_named_calls_to`) to the typed-
15546 // decoded named-classifier cell
15547 // (`expand_and_collect_named_calls_to_any`) via the constant-
15548 // classifier decoder shape. Post-lift the identity:
15549 //
15550 // expand_and_collect_named_calls_to(forms, kw, project) ==
15551 // expand_and_collect_named_calls_to_any(forms,
15552 // |h| (h == kw).then_some(((), kw)),
15553 // |(), name, args| project(name, args))
15554 //
15555 // Pinning the identity here makes the typed-decoded named-
15556 // classifier primitive the CANONICAL composition point the
15557 // runtime-keyword sibling routes through — parallel to how
15558 // `expand_and_collect_calls_to` routes through
15559 // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15560 // then_some(())` decoder on the bare-kwargs axis. A future
15561 // regression that drifts ONE cell's NAME-slot rejection chain
15562 // from the other becomes loudly visible at this assertion.
15563 let src = r#"(defmonitor cpu :threshold 80)
15564 (other-form 99)
15565 (defmonitor mem :threshold 90)"#;
15566 let forms = read(src).unwrap();
15567 let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15568 .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15569 Ok((name.to_string(), args.len()))
15570 })
15571 .unwrap();
15572 let via_classifier: Vec<(String, usize)> = Expander::new()
15573 .expand_and_collect_named_calls_to_any(
15574 forms,
15575 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15576 |(), name, args| Ok((name.to_string(), args.len())),
15577 )
15578 .unwrap();
15579 assert_eq!(via_constant_keyword, via_classifier);
15580 assert_eq!(
15581 via_constant_keyword,
15582 vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15583 );
15584 }
15585
15586 #[test]
15587 fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15588 ) {
15589 // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15590 // (the typed constant-keyword named cell) must yield the same
15591 // payload as `expand_and_collect_named_calls_to(forms,
15592 // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15593 // NamedDefinition })` — the lift's structural identity.
15594 //
15595 // Pre-lift `expand_to_named<T>` routed through
15596 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15597 // named_form_projection::<T>)` (the bare-kwargs constant
15598 // primitive with `named_form_projection<T>` doing the NAME
15599 // extraction internally). Post-lift `expand_to_named<T>`
15600 // routes through the named constant-keyword primitive (which
15601 // itself routes through the classifier primitive via a
15602 // constant-classifier decoder) — so the `split_name_slot`
15603 // composition lives at ONE site (the `_any` primitive body)
15604 // post-lift rather than at TWO sites.
15605 use crate::compile::NamedDefinition;
15606 use crate::compiler_spec::CompilerSpec;
15607 let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15608 (defcompiler beta-compiler :name "y" :dialect "standard")"#;
15609 let forms = read(src).unwrap();
15610 let via_expand_to_named = Expander::new()
15611 .expand_to_named::<CompilerSpec>(forms.clone())
15612 .unwrap();
15613 let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15614 .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15615 let spec =
15616 <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15617 Ok(NamedDefinition {
15618 name: name.to_string(),
15619 spec,
15620 })
15621 })
15622 .unwrap();
15623 assert_eq!(via_expand_to_named.len(), 2);
15624 assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15625 for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15626 assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15627 assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15628 }
15629 assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15630 assert_eq!(via_expand_to_named[0].spec.name, "x");
15631 assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15632 assert_eq!(via_expand_to_named[1].spec.name, "y");
15633 }
15634
15635 // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15636
15637 #[test]
15638 fn expander_default_max_expansion_depth_is_the_module_constant() {
15639 // Both constructors seed `max_expansion_depth` from the module
15640 // constant — a regression that drifts one constructor's default
15641 // from the constant (e.g. a hardcoded `256` at ONE site with the
15642 // other left uninitialized after a `Default` derive addition)
15643 // fails this pin.
15644 assert_eq!(
15645 Expander::new().max_expansion_depth(),
15646 DEFAULT_MAX_EXPANSION_DEPTH
15647 );
15648 assert_eq!(
15649 Expander::new_substitute_only().max_expansion_depth(),
15650 DEFAULT_MAX_EXPANSION_DEPTH
15651 );
15652 assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15653 }
15654
15655 #[test]
15656 fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15657 let mut e = Expander::new();
15658 e.set_max_expansion_depth(7);
15659 assert_eq!(e.max_expansion_depth(), 7);
15660 }
15661
15662 #[test]
15663 fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15664 // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15665 // pre-lift stack-overflowed the process — a below-floor abort.
15666 // Post-lift the expander returns a typed
15667 // `LispError::ExpansionDepthExceeded` at the ceiling with
15668 // `macro_name` populated from the offending call's head.
15669 let mut e = Expander::new();
15670 e.set_max_expansion_depth(4);
15671 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15672 let err = e.expand_program(forms).unwrap_err();
15673 match err {
15674 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15675 assert_eq!(macro_name, "loop");
15676 assert_eq!(limit, 4);
15677 }
15678 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15679 }
15680 }
15681
15682 #[test]
15683 fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15684 // The substitute strategy shares the same `expand` outer walker
15685 // with the bytecode strategy, so the ceiling fires at the SAME
15686 // gate regardless of which apply-layer is active. The pin makes
15687 // that shared-outer-walker property structural: a regression
15688 // that ever branches the depth guard on strategy (e.g. moves it
15689 // into `apply_compiled` alone) fails this test on the
15690 // substitute expander.
15691 let mut e = Expander::new_substitute_only();
15692 e.set_max_expansion_depth(4);
15693 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15694 let err = e.expand_program(forms).unwrap_err();
15695 match err {
15696 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15697 assert_eq!(macro_name, "loop");
15698 assert_eq!(limit, 4);
15699 }
15700 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15701 }
15702 }
15703
15704 #[test]
15705 fn expand_lawful_nested_macros_within_ceiling_succeed() {
15706 // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15707 // the low single digits because the tree-child arm doesn't
15708 // count — only the post-apply re-expansion path bumps the
15709 // counter. A ceiling of 3 is enough for a two-deep nested
15710 // expansion. This pin is the fail-before/pass-after negative
15711 // control for the recursive-macro rejection tests above: the
15712 // depth counter must NOT reject lawful macro-nesting within
15713 // the ceiling, and only reject the runaway shape.
15714 let mut e = Expander::new();
15715 e.set_max_expansion_depth(3);
15716 let forms = read(
15717 "(defmacro twice (x) `(list ,x ,x))
15718 (twice (twice hey))",
15719 )
15720 .unwrap();
15721 let out = e.expand_program(forms).unwrap();
15722 assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15723 }
15724
15725 #[test]
15726 fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15727 // A macro-free lawful deep tree — five levels of plain nesting —
15728 // is expanded at a ceiling well below the tree's height. The
15729 // tree-child arm does not accrete depth, so tree height is
15730 // orthogonal to the ceiling; only macro re-expansion counts.
15731 // A regression that ever bumps depth on tree-child descent
15732 // fails this pin.
15733 let mut e = Expander::new();
15734 e.set_max_expansion_depth(2);
15735 let forms = read("(a (b (c (d (e f)))))").unwrap();
15736 let out = e.expand_program(forms).unwrap();
15737 assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15738 }
15739
15740 #[test]
15741 fn expansion_depth_exceeded_position_is_none() {
15742 // The variant joins the non-positional cohort — the offending
15743 // macro's byte offset is not what a runaway diagnostic anchors
15744 // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15745 // keeps the variant in the `position() -> None` cohort so a
15746 // future span-carrying edit lands the field in ONE place
15747 // across every non-positional variant simultaneously.
15748 let err = LispError::ExpansionDepthExceeded {
15749 macro_name: "loop".to_string(),
15750 limit: 256,
15751 };
15752 assert_eq!(err.position(), None);
15753 }
15754
15755 #[test]
15756 fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15757 // Display projection carries both `macro_name` and `limit`, so
15758 // authoring surfaces that substring-grep the rendered
15759 // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15760 // the variant boundary and never need to substring-parse
15761 // free-form text to recover the identity of the offending
15762 // macro or the ceiling it breached.
15763 let err = LispError::ExpansionDepthExceeded {
15764 macro_name: "loop".to_string(),
15765 limit: 256,
15766 };
15767 let rendered = err.to_string();
15768 assert!(rendered.contains("loop"), "rendered: {rendered}");
15769 assert!(rendered.contains("256"), "rendered: {rendered}");
15770 assert!(
15771 rendered.contains("macro expansion depth exceeded"),
15772 "rendered: {rendered}"
15773 );
15774 }
15775
15776 // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15777
15778 #[test]
15779 fn expander_default_max_cache_entries_is_the_module_constant() {
15780 // Both cache-enabling constructors seed `max_cache_entries` from
15781 // the module constant — a regression that drifts one
15782 // constructor's default from the constant (e.g. a hardcoded
15783 // `8192` at ONE site with the other left uninitialized after a
15784 // `Default` derive addition) fails this pin. The peer
15785 // constructor `new_substitute_only` seeds the same ceiling so
15786 // the field stays initialized under `#[derive(Clone)]` (a
15787 // zero-initialized clone would be a silent bifurcation of the
15788 // cache-ceiling surface).
15789 assert_eq!(
15790 Expander::new().max_cache_entries(),
15791 DEFAULT_MAX_CACHE_ENTRIES
15792 );
15793 assert_eq!(
15794 Expander::new_substitute_only().max_cache_entries(),
15795 DEFAULT_MAX_CACHE_ENTRIES
15796 );
15797 assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15798 }
15799
15800 #[test]
15801 fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15802 // Setter mirrors accessor — a regression that ever forgets to
15803 // write through to the field, or writes to a shadow field the
15804 // accessor does not read, fails this pin.
15805 let mut e = Expander::new();
15806 e.set_max_cache_entries(7);
15807 assert_eq!(e.max_cache_entries(), 7);
15808 }
15809
15810 #[test]
15811 fn expand_cache_size_is_bounded_by_max_cache_entries() {
15812 // Five distinct `(name, args)` pairs expanded against a cache
15813 // ceiling of `2` populate the cache with EXACTLY the first two
15814 // insertions and skip the remaining three. Every expansion
15815 // still returns the correct result — the cache is a pure
15816 // PERFORMANCE optimization; the ceiling gates memoization,
15817 // never correctness. LOAD-BEARING true-arm catch on the
15818 // bounded-cache contract: a regression that ignored the
15819 // ceiling (unbounded insert) would land `cache_size() == 5`
15820 // here; a regression that skipped insertion entirely (broken
15821 // cache) would still see correct expansion output but would
15822 // fail the interior `cache_size() == 2` pin.
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 three)
15830 (id four)
15831 (id five)
15832 ";
15833 let out = e.expand_program(read(src).unwrap()).unwrap();
15834 assert_eq!(out.len(), 5);
15835 assert_eq!(out[0], parse("one"));
15836 assert_eq!(out[1], parse("two"));
15837 assert_eq!(out[2], parse("three"));
15838 assert_eq!(out[3], parse("four"));
15839 assert_eq!(out[4], parse("five"));
15840 assert_eq!(
15841 e.cache_size(),
15842 2,
15843 "cache grew past the max_cache_entries ceiling",
15844 );
15845 }
15846
15847 #[test]
15848 fn expand_zero_cache_ceiling_disables_caching_effectively() {
15849 // Zero ceiling admits every fresh `(name, args)` pair through
15850 // the compute path and never grows the cache — the same
15851 // observable behavior as `set_cache_enabled(false)` but
15852 // reached through the ceiling knob. Correctness is preserved:
15853 // the miss path always recomputes the same value the cache
15854 // would have returned, so an operator that dials the ceiling
15855 // to `0` (e.g. to hunt a cache-related regression) never
15856 // bifurcates the output surface. Peer to
15857 // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15858 // ceilings admit the zero endpoint as a well-defined
15859 // extremum.
15860 let mut e = Expander::new();
15861 e.set_max_cache_entries(0);
15862 let src = "
15863 (defmacro id (x) `,x)
15864 (id one)
15865 (id two)
15866 ";
15867 let out = e.expand_program(read(src).unwrap()).unwrap();
15868 assert_eq!(out.len(), 2);
15869 assert_eq!(out[0], parse("one"));
15870 assert_eq!(out[1], parse("two"));
15871 assert_eq!(
15872 e.cache_size(),
15873 0,
15874 "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15875 );
15876 }
15877
15878 #[test]
15879 fn expand_cached_hits_survive_past_the_ceiling() {
15880 // Once a `(name, args)` pair sits IN the cache under the
15881 // ceiling, subsequent calls hit it — the ceiling only gates
15882 // NEW-KEY inserts, never lookups. This test seeds the cache
15883 // with two entries under a ceiling of `2`, then re-issues the
15884 // SAME calls to prove the cached hits still fire (cache_size
15885 // stays at `2` because no new keys are inserted, and the
15886 // expansion output is stable). LOAD-BEARING true-arm catch
15887 // that the ceiling gates the INSERT path alone — a regression
15888 // that ever refused cache LOOKUPS at the ceiling would
15889 // silently bifurcate correctness on the second call.
15890 let mut e = Expander::new();
15891 e.set_max_cache_entries(2);
15892 let src = "
15893 (defmacro id (x) `,x)
15894 (id one)
15895 (id two)
15896 (id one)
15897 (id two)
15898 ";
15899 let out = e.expand_program(read(src).unwrap()).unwrap();
15900 assert_eq!(out.len(), 4);
15901 assert_eq!(out[0], parse("one"));
15902 assert_eq!(out[1], parse("two"));
15903 assert_eq!(out[2], parse("one"));
15904 assert_eq!(out[3], parse("two"));
15905 assert_eq!(
15906 e.cache_size(),
15907 2,
15908 "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15909 );
15910 }
15911
15912 #[test]
15913 fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15914 // After the cache reaches its ceiling and subsequent inserts
15915 // are skipped, `clear_cache` empties the memo and RE-OPENS the
15916 // insert path — the next expansion populates the freshly-
15917 // empty cache without operator intervention on the ceiling
15918 // itself. The pin binds the operator-facing recovery path
15919 // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15920 // insertion until the operator clears the cache via
15921 // [`Expander::clear_cache`]".
15922 let mut e = Expander::new();
15923 e.set_max_cache_entries(1);
15924 let src_fill = "
15925 (defmacro id (x) `,x)
15926 (id one)
15927 (id two)
15928 ";
15929 let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15930 assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15931 e.clear_cache();
15932 assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15933 let out = e
15934 .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15935 .unwrap();
15936 assert_eq!(out.len(), 1);
15937 assert_eq!(out[0], parse("three"));
15938 assert_eq!(
15939 e.cache_size(),
15940 1,
15941 "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15942 );
15943 }
15944
15945 // ── max_expansion_size: bounded-output ceiling ─────────────────────
15946
15947 #[test]
15948 fn expander_default_max_expansion_size_is_the_module_constant() {
15949 // Both constructors seed `max_expansion_size` from the module
15950 // constant — a regression that drifts one constructor's
15951 // default from the constant (e.g. a hardcoded `65_536` at
15952 // ONE site with the other left uninitialized after a
15953 // `Default` derive addition) fails this pin. Peer to the
15954 // `max_expansion_depth` + `max_cache_entries` default-
15955 // coherence pins one RESOURCE-DIMENSION axis over.
15956 assert_eq!(
15957 Expander::new().max_expansion_size(),
15958 DEFAULT_MAX_EXPANSION_SIZE
15959 );
15960 assert_eq!(
15961 Expander::new_substitute_only().max_expansion_size(),
15962 DEFAULT_MAX_EXPANSION_SIZE
15963 );
15964 assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15965 }
15966
15967 #[test]
15968 fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15969 // Setter mirrors accessor — a regression that ever forgets to
15970 // write through to the field, or writes to a shadow field the
15971 // accessor does not read, fails this pin. Peer to the
15972 // setter/accessor coherence pins on the depth + cache axes.
15973 let mut e = Expander::new();
15974 e.set_max_expansion_size(64);
15975 assert_eq!(e.max_expansion_size(), 64);
15976 }
15977
15978 #[test]
15979 fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15980 // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15981 // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15982 // (outer List + 5 children). With a ceiling of `4` this
15983 // crosses the OUTPUT-SIZE threshold at the first apply and
15984 // the expander returns a typed
15985 // `LispError::ExpansionSizeExceeded` with `macro_name`,
15986 // `size`, and `limit` all populated. Pre-lift the runaway
15987 // just accumulated in a growing tree until the process
15988 // heap ran out; post-lift the rejection sits at the same
15989 // call boundary as every other macro-expansion failure.
15990 let mut e = Expander::new();
15991 e.set_max_expansion_size(4);
15992 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15993 let err = e.expand_program(forms).unwrap_err();
15994 match err {
15995 LispError::ExpansionSizeExceeded {
15996 macro_name,
15997 size,
15998 limit,
15999 } => {
16000 assert_eq!(macro_name, "bomb");
16001 assert_eq!(size, 6);
16002 assert_eq!(limit, 4);
16003 }
16004 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16005 }
16006 }
16007
16008 #[test]
16009 fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
16010 // The substitute strategy shares the same `expand_with_depth`
16011 // outer walker with the bytecode strategy, so the ceiling
16012 // fires at the SAME gate regardless of which apply-layer is
16013 // active. The pin makes that shared-outer-walker property
16014 // structural: a regression that ever branches the size guard
16015 // on strategy (e.g. moves it into `apply_compiled` alone)
16016 // fails this test on the substitute expander. Peer to
16017 // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
16018 // one RESOURCE axis over.
16019 let mut e = Expander::new_substitute_only();
16020 e.set_max_expansion_size(4);
16021 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
16022 let err = e.expand_program(forms).unwrap_err();
16023 match err {
16024 LispError::ExpansionSizeExceeded {
16025 macro_name,
16026 size,
16027 limit,
16028 } => {
16029 assert_eq!(macro_name, "bomb");
16030 assert_eq!(size, 6);
16031 assert_eq!(limit, 4);
16032 }
16033 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16034 }
16035 }
16036
16037 #[test]
16038 fn expand_lawful_output_within_size_ceiling_succeeds() {
16039 // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
16040 // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
16041 // the output sits AT the ceiling (`<=` admits equality; only
16042 // `>` rejects), so the expansion succeeds. Fail-before/pass-
16043 // after negative control for the bomb rejection tests: a
16044 // regression that ever rejected at-ceiling output would fail
16045 // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
16046 // one RESOURCE axis over.
16047 let mut e = Expander::new();
16048 e.set_max_expansion_size(4);
16049 let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
16050 let out = e.expand_program(forms).unwrap();
16051 assert_eq!(out[0], parse("(list hey hey)"));
16052 }
16053
16054 #[test]
16055 fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
16056 // A macro-free lawful large tree is NOT gated by the size
16057 // ceiling — the ceiling gates macro-`apply` outputs alone,
16058 // not tree-child descents through non-macro forms. A `usize::MAX`
16059 // ceiling would trivially admit anything; this pin uses a
16060 // deliberately small ceiling of `4` on a tree with 6 nodes
16061 // to catch a regression that ever bumps the ceiling on tree
16062 // descent. A tree-only walk should NEVER fire the ceiling.
16063 // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
16064 // one RESOURCE axis over.
16065 let mut e = Expander::new();
16066 e.set_max_expansion_size(4);
16067 // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
16068 let forms = read("(a (b (c d)))").unwrap();
16069 let out = e.expand_program(forms).unwrap();
16070 assert_eq!(out[0], parse("(a (b (c d)))"));
16071 }
16072
16073 #[test]
16074 fn expansion_size_exceeded_position_is_none() {
16075 // The variant joins the non-positional cohort — the offending
16076 // macro's byte offset is not what an expansion-bomb diagnostic
16077 // anchors to; the (offending macro name, observed size,
16078 // configured ceiling) triple is. This pin keeps the variant
16079 // in the `position() -> None` cohort so a future span-carrying
16080 // edit lands the field in ONE place across every non-positional
16081 // variant simultaneously. Peer to
16082 // `expansion_depth_exceeded_position_is_none` one RESOURCE
16083 // axis over.
16084 let err = LispError::ExpansionSizeExceeded {
16085 macro_name: "bomb".to_string(),
16086 size: 512,
16087 limit: 256,
16088 };
16089 assert_eq!(err.position(), None);
16090 }
16091
16092 #[test]
16093 fn expansion_size_exceeded_display_matches_typed_variant_shape() {
16094 // Display projection carries `macro_name`, `size`, AND
16095 // `limit`, so authoring surfaces that substring-grep the
16096 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16097 // THREE halves at the variant boundary and never need to
16098 // substring-parse free-form text to recover the identity of
16099 // the offending macro, the observed size, or the ceiling it
16100 // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
16101 // one RESOURCE axis over.
16102 let err = LispError::ExpansionSizeExceeded {
16103 macro_name: "bomb".to_string(),
16104 size: 512,
16105 limit: 256,
16106 };
16107 let rendered = err.to_string();
16108 assert!(rendered.contains("bomb"), "rendered: {rendered}");
16109 assert!(rendered.contains("512"), "rendered: {rendered}");
16110 assert!(rendered.contains("256"), "rendered: {rendered}");
16111 assert!(
16112 rendered.contains("macro expansion output size exceeded"),
16113 "rendered: {rendered}"
16114 );
16115 }
16116
16117 #[test]
16118 fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
16119 // A lawful outer macro that expands to a call of a bomb inner
16120 // macro rejects at the INNER call — `macro_name` at the
16121 // variant boundary is the name of the macro whose `apply`
16122 // output crossed the ceiling, not the outermost caller.
16123 // Load-bearing name-provenance pin: an operator whose bomb
16124 // sits N levels deep in a compose chain needs to see the
16125 // INNER macro's name in the diagnostic, not the wrapping
16126 // shape. A regression that ever populates `macro_name` from
16127 // the outermost caller's head fails this pin.
16128 let mut e = Expander::new();
16129 e.set_max_expansion_size(4);
16130 // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
16131 // output that crosses the ceiling. Expected: the diagnostic
16132 // names `bomb`, not `wrapper`.
16133 let src = "
16134 (defmacro bomb (x) `(list ,x ,x ,x ,x))
16135 (defmacro wrapper (x) `(bomb ,x))
16136 (wrapper hey)
16137 ";
16138 let err = e.expand_program(read(src).unwrap()).unwrap_err();
16139 match err {
16140 LispError::ExpansionSizeExceeded {
16141 macro_name,
16142 size,
16143 limit,
16144 } => {
16145 assert_eq!(macro_name, "bomb");
16146 assert_eq!(size, 6);
16147 assert_eq!(limit, 4);
16148 }
16149 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16150 }
16151 }
16152
16153 // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16154 // Peer to depth (recursion length) + cache (memoization width) +
16155 // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16156 // axis over — the three prior guards fire at EXPAND time; this
16157 // one fires at REGISTER time. Closes the expander's RESOURCE
16158 // surface at a fourth typed dimension across two pipeline stages.
16159
16160 #[test]
16161 fn expander_default_max_macro_body_size_is_the_module_constant() {
16162 // Both constructors seed `max_macro_body_size` from the
16163 // module constant — a regression that drifts one
16164 // constructor's default from the constant (e.g. a hardcoded
16165 // `16_384` at ONE site with the other left uninitialized
16166 // after a `Default` derive addition) fails this pin. Peer
16167 // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16168 // pins one PIPELINE-STAGE axis over.
16169 assert_eq!(
16170 Expander::new().max_macro_body_size(),
16171 DEFAULT_MAX_MACRO_BODY_SIZE
16172 );
16173 assert_eq!(
16174 Expander::new_substitute_only().max_macro_body_size(),
16175 DEFAULT_MAX_MACRO_BODY_SIZE
16176 );
16177 assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16178 }
16179
16180 #[test]
16181 fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16182 // Setter mirrors accessor — a regression that ever forgets to
16183 // write through to the field, or writes to a shadow field the
16184 // accessor does not read, fails this pin. Peer to the
16185 // setter/accessor coherence pins on the depth + cache +
16186 // expansion-size axes.
16187 let mut e = Expander::new();
16188 e.set_max_macro_body_size(64);
16189 assert_eq!(e.max_macro_body_size(), 64);
16190 }
16191
16192 #[test]
16193 fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16194 // `(defmacro huge (x) `(list a b c d e))` has body
16195 // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16196 // list of 6 nodes (outer List + 5 children); the outer
16197 // quasi-quote wrapper adds one → 8 nodes total for
16198 // `def.body.node_count()`. With a ceiling of `4` this
16199 // crosses the REGISTRATION-time BODY-SIZE threshold and the
16200 // expander returns a typed `LispError::MacroBodySizeExceeded`
16201 // with `macro_name`, `size`, and `limit` all populated. Peer
16202 // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16203 // one PIPELINE-STAGE axis over.
16204 let mut e = Expander::new();
16205 e.set_max_macro_body_size(4);
16206 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16207 let err = e.expand_program(forms).unwrap_err();
16208 match err {
16209 LispError::MacroBodySizeExceeded {
16210 macro_name,
16211 size,
16212 limit,
16213 } => {
16214 assert_eq!(macro_name, "huge");
16215 assert_eq!(size, 8);
16216 assert_eq!(limit, 4);
16217 }
16218 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16219 }
16220 }
16221
16222 #[test]
16223 fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16224 // The substitute strategy shares the same
16225 // `register_macro_def` outer registration entry with the
16226 // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16227 // ceiling fires at the SAME gate regardless of which
16228 // apply-layer is active. The pin makes that shared-registration
16229 // property structural: a regression that ever branches the
16230 // body-size guard on strategy (e.g. moves it into the
16231 // `compile_templates` arm alone) fails this test on the
16232 // substitute expander. Peer to
16233 // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16234 // one PIPELINE-STAGE axis over.
16235 let mut e = Expander::new_substitute_only();
16236 e.set_max_macro_body_size(4);
16237 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16238 let err = e.expand_program(forms).unwrap_err();
16239 match err {
16240 LispError::MacroBodySizeExceeded {
16241 macro_name,
16242 size,
16243 limit,
16244 } => {
16245 assert_eq!(macro_name, "huge");
16246 assert_eq!(size, 8);
16247 assert_eq!(limit, 4);
16248 }
16249 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16250 }
16251 }
16252
16253 #[test]
16254 fn register_macro_body_at_ceiling_admits() {
16255 // `(defmacro twice (x) `(list ,x))` has body
16256 // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16257 // list (List + `list` atom + `,x` unquote which is
16258 // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16259 // With a ceiling of `5` the body sits AT the ceiling
16260 // (`<=` admits equality; only `>` rejects), so the
16261 // registration succeeds and the subsequent expansion
16262 // proceeds normally. Fail-before/pass-after negative
16263 // control for the body-bomb rejection tests: a regression
16264 // that ever rejected at-ceiling bodies would fail this pin.
16265 let mut e = Expander::new();
16266 e.set_max_macro_body_size(5);
16267 let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16268 let out = e.expand_program(forms).unwrap();
16269 assert_eq!(out[0], parse("(list hey)"));
16270 }
16271
16272 #[test]
16273 fn register_failure_leaves_both_tables_pristine() {
16274 // A rejected registration MUST leave BOTH `self.macros`
16275 // AND `self.templates` exactly as they were — no
16276 // partial-write window in which one table carries the
16277 // over-ceiling entry while the other does not, or in
16278 // which the template pre-compile already ran. This pin
16279 // catches a regression that ever moves the body-size
16280 // gate below `self.templates.insert` or below
16281 // `self.macros.insert`.
16282 let mut e = Expander::new();
16283 e.set_max_macro_body_size(4);
16284 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16285 assert!(e.expand_program(forms).is_err());
16286 assert!(
16287 !e.has("huge"),
16288 "macros table must stay pristine after rejection"
16289 );
16290 assert_eq!(e.len(), 0);
16291 }
16292
16293 #[test]
16294 fn macro_body_size_exceeded_position_is_none() {
16295 // The variant joins the non-positional cohort — the
16296 // offending macro's byte offset is not what a body-bomb
16297 // diagnostic anchors to; the (offending macro name,
16298 // observed body size, configured ceiling) triple is.
16299 // This pin keeps the variant in the `position() -> None`
16300 // cohort so a future span-carrying edit lands the field
16301 // in ONE place across every non-positional variant
16302 // simultaneously. Peer to
16303 // `expansion_{depth,size}_exceeded_position_is_none` one
16304 // PIPELINE-STAGE axis over.
16305 let err = LispError::MacroBodySizeExceeded {
16306 macro_name: "huge".to_string(),
16307 size: 512,
16308 limit: 256,
16309 };
16310 assert_eq!(err.position(), None);
16311 }
16312
16313 #[test]
16314 fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16315 // Display projection carries `macro_name`, `size`, AND
16316 // `limit`, so authoring surfaces that substring-grep the
16317 // rendered diagnostic (`tatara-check`, REPL, LSP) see
16318 // ALL THREE halves at the variant boundary and never need
16319 // to substring-parse free-form text to recover the
16320 // identity of the offending macro, the observed body
16321 // size, or the ceiling it breached. Peer to
16322 // `expansion_size_exceeded_display_matches_typed_variant_shape`
16323 // one PIPELINE-STAGE axis over.
16324 let err = LispError::MacroBodySizeExceeded {
16325 macro_name: "huge".to_string(),
16326 size: 512,
16327 limit: 256,
16328 };
16329 let rendered = err.to_string();
16330 assert!(rendered.contains("huge"), "rendered: {rendered}");
16331 assert!(rendered.contains("512"), "rendered: {rendered}");
16332 assert!(rendered.contains("256"), "rendered: {rendered}");
16333 assert!(
16334 rendered.contains("macro body size exceeded"),
16335 "rendered: {rendered}"
16336 );
16337 }
16338
16339 #[test]
16340 fn lawful_macro_body_within_ceiling_registers_and_expands() {
16341 // A lawful hand-authored macro body sits well below any
16342 // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16343 // is deliberately generous so operators never brush it on
16344 // real authoring workloads. This pin locks the default
16345 // ceiling's non-interference posture: a regression that
16346 // ever lowered the default enough to reject the reference
16347 // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16348 // fail here.
16349 let mut e = Expander::new();
16350 let forms = read(
16351 "(defmacro when (cond x) `(if ,cond ,x))
16352 (when #t hey)",
16353 )
16354 .unwrap();
16355 let out = e.expand_program(forms).unwrap();
16356 assert_eq!(out[0], parse("(if #t hey)"));
16357 }
16358
16359 #[test]
16360 fn register_macro_def_direct_call_respects_body_size_ceiling() {
16361 // `register_macro_def` is the substrate primitive both
16362 // `expand_program`'s `defmacro` recognition AND
16363 // `with_macros`'s bulk load route through. The body-size
16364 // ceiling fires on the primitive itself, not on any one
16365 // consumer — a regression that ever moved the gate into
16366 // `expand_program`'s arm alone would fail this pin.
16367 let mut e = Expander::new();
16368 e.set_max_macro_body_size(4);
16369 let def = MacroDef {
16370 name: "huge".to_string(),
16371 params: MacroParams {
16372 required: vec!["x".to_string()],
16373 optional: Vec::new(),
16374 rest: None,
16375 },
16376 body: Sexp::List(vec![
16377 Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16378 Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16379 Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16380 Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16381 Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16382 ]),
16383 };
16384 // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16385 let err = e.register_macro_def(def).unwrap_err();
16386 match err {
16387 LispError::MacroBodySizeExceeded {
16388 macro_name,
16389 size,
16390 limit,
16391 } => {
16392 assert_eq!(macro_name, "huge");
16393 assert_eq!(size, 6);
16394 assert_eq!(limit, 4);
16395 }
16396 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16397 }
16398 assert!(!e.has("huge"));
16399 }
16400
16401 // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16402 // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16403 // axis over — where body-size bounds a per-registration SIZE,
16404 // this ceiling bounds cumulative registration COUNT. Peer to
16405 // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16406 // — the two entry-count guards close the two pipeline stages
16407 // symmetrically. Closes the expander's RESOURCE surface at a
16408 // FIFTH typed dimension across two pipeline stages: the
16409 // REGISTER-time face is now a two-dimensional (size × count)
16410 // closure symmetric with the EXPAND-time (depth + count + size)
16411 // triple.
16412
16413 #[test]
16414 fn expander_default_max_registered_macros_is_the_module_constant() {
16415 // Both constructors seed `max_registered_macros` from the
16416 // module constant — a regression that drifts one
16417 // constructor's default from the constant (e.g. a hardcoded
16418 // `4096` at ONE site with the other left uninitialized
16419 // after a `Default` derive addition) fails this pin. Peer
16420 // to the four prior default-coherence pins on depth,
16421 // cache-entries, expansion-size, and body-size.
16422 assert_eq!(
16423 Expander::new().max_registered_macros(),
16424 DEFAULT_MAX_REGISTERED_MACROS
16425 );
16426 assert_eq!(
16427 Expander::new_substitute_only().max_registered_macros(),
16428 DEFAULT_MAX_REGISTERED_MACROS
16429 );
16430 assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16431 }
16432
16433 #[test]
16434 fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16435 // Setter mirrors accessor — a regression that ever forgets
16436 // to write through to the field, or writes to a shadow
16437 // field the accessor does not read, fails this pin. Peer
16438 // to the four prior setter/accessor coherence pins.
16439 let mut e = Expander::new();
16440 e.set_max_registered_macros(64);
16441 assert_eq!(e.max_registered_macros(), 64);
16442 }
16443
16444 #[test]
16445 fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16446 // With a ceiling of `2`, the first two fresh registrations
16447 // land in the table and the third fresh key rejects with
16448 // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16449 // count: 2, limit: 2 }`. `count` is the pre-insert table
16450 // length (which equals `limit` at rejection in the common
16451 // case); `macro_name` is the OFFENDING call's name (the
16452 // fresh key we could not admit), not the outermost caller.
16453 let mut e = Expander::new();
16454 e.set_max_registered_macros(2);
16455 let forms = read(
16456 "(defmacro a (x) `(list ,x))
16457 (defmacro b (x) `(list ,x))
16458 (defmacro c (x) `(list ,x))",
16459 )
16460 .unwrap();
16461 let err = e.expand_program(forms).unwrap_err();
16462 match err {
16463 LispError::RegisteredMacrosExceeded {
16464 macro_name,
16465 count,
16466 limit,
16467 } => {
16468 assert_eq!(macro_name, "c");
16469 assert_eq!(count, 2);
16470 assert_eq!(limit, 2);
16471 }
16472 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16473 }
16474 // First two admitted; third rejected leaves the table at
16475 // its pre-third-call size.
16476 assert!(e.has("a"));
16477 assert!(e.has("b"));
16478 assert!(!e.has("c"));
16479 assert_eq!(e.len(), 2);
16480 }
16481
16482 #[test]
16483 fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16484 // The substitute strategy shares the same
16485 // `register_macro_def` outer registration entry with the
16486 // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16487 // ceiling fires at the SAME gate regardless of which
16488 // apply-layer is active. The pin makes that shared-
16489 // registration property structural: a regression that ever
16490 // branches the table-count guard on strategy (e.g. moves
16491 // it into the `compile_templates` arm alone) fails this
16492 // test on the substitute expander. Peer to the four prior
16493 // shared-registration pins.
16494 let mut e = Expander::new_substitute_only();
16495 e.set_max_registered_macros(2);
16496 let forms = read(
16497 "(defmacro a (x) `(list ,x))
16498 (defmacro b (x) `(list ,x))
16499 (defmacro c (x) `(list ,x))",
16500 )
16501 .unwrap();
16502 let err = e.expand_program(forms).unwrap_err();
16503 match err {
16504 LispError::RegisteredMacrosExceeded {
16505 macro_name,
16506 count,
16507 limit,
16508 } => {
16509 assert_eq!(macro_name, "c");
16510 assert_eq!(count, 2);
16511 assert_eq!(limit, 2);
16512 }
16513 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16514 }
16515 assert!(e.has("a"));
16516 assert!(e.has("b"));
16517 assert!(!e.has("c"));
16518 assert_eq!(e.len(), 2);
16519 }
16520
16521 #[test]
16522 fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16523 // OVERWRITES (a re-registration of an already-registered
16524 // key) do not grow the table and MUST be admitted at any
16525 // ceiling — operators redefining a macro at capacity is a
16526 // legitimate authoring pattern. This pin catches a
16527 // regression that ever treats overwrites the same as fresh
16528 // keys (a `contains_key` check that inverts, or a
16529 // `set_max_registered_macros(0)` that would reject even
16530 // pre-registered overwrites).
16531 let mut e = Expander::new();
16532 // Land two macros with a permissive ceiling.
16533 e.expand_program(
16534 read(
16535 "(defmacro a (x) `(list ,x))
16536 (defmacro b (x) `(list ,x))",
16537 )
16538 .unwrap(),
16539 )
16540 .unwrap();
16541 assert_eq!(e.len(), 2);
16542 // Drop the ceiling to exactly the current size — a fresh
16543 // third key would now reject, but an overwrite of `a` MUST
16544 // admit.
16545 e.set_max_registered_macros(2);
16546 let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16547 e.expand_program(forms).unwrap();
16548 assert_eq!(e.len(), 2, "overwrite must not grow the table");
16549 // Re-expand a call to `a` — the NEW definition (two
16550 // required params) is active, and the OLD definition (one
16551 // required param) is gone. Under the new definition,
16552 // `(a 1 2)` expands to `(pair 1 2)`.
16553 let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16554 assert_eq!(expanded[0], parse("(pair foo bar)"));
16555 }
16556
16557 #[test]
16558 fn register_at_table_ceiling_admits_up_to_but_not_past() {
16559 // Fail-before/pass-after negative control on the exact
16560 // boundary: with a ceiling of `3`, exactly three fresh keys
16561 // admit and the fourth rejects. A regression that ever
16562 // shifted the boundary by one (e.g. keyed on `>` instead of
16563 // `>=`, or checked `len() > cap` before insert instead of
16564 // `len() >= cap`) would fail one of the two arms — either
16565 // the third admission or the fourth rejection.
16566 let mut e = Expander::new();
16567 e.set_max_registered_macros(3);
16568 // Three fresh keys admit.
16569 e.expand_program(
16570 read(
16571 "(defmacro a (x) `(list ,x))
16572 (defmacro b (x) `(list ,x))
16573 (defmacro c (x) `(list ,x))",
16574 )
16575 .unwrap(),
16576 )
16577 .unwrap();
16578 assert_eq!(e.len(), 3);
16579 // Fourth fresh key rejects.
16580 let err = e
16581 .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16582 .unwrap_err();
16583 match err {
16584 LispError::RegisteredMacrosExceeded {
16585 macro_name,
16586 count,
16587 limit,
16588 } => {
16589 assert_eq!(macro_name, "d");
16590 assert_eq!(count, 3);
16591 assert_eq!(limit, 3);
16592 }
16593 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16594 }
16595 // Table stays at pre-rejection size — no partial write.
16596 assert_eq!(e.len(), 3);
16597 assert!(!e.has("d"));
16598 }
16599
16600 #[test]
16601 fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16602 // A rejected registration MUST leave BOTH `self.macros` AND
16603 // `self.templates` exactly as they were — no partial-write
16604 // window in which one table carries the over-ceiling entry
16605 // while the other does not, or in which the template
16606 // pre-compile already ran. Table-count gate fires BEFORE
16607 // body-size gate walks the body AND BEFORE any insert lands,
16608 // so a regression that ever moved the table-count gate
16609 // below `self.templates.insert` or below
16610 // `self.macros.insert` would fail this pin. Peer to
16611 // `register_failure_leaves_both_tables_pristine` on the
16612 // body-size axis.
16613 let mut e = Expander::new();
16614 e.set_max_registered_macros(1);
16615 // Land one macro at ceiling.
16616 e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16617 .unwrap();
16618 assert!(e.has("a"));
16619 // Reject a fresh second key.
16620 let err = e
16621 .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16622 .unwrap_err();
16623 assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16624 // Table stays at pre-rejection state — `b` is NOT present
16625 // in either self.macros OR self.templates.
16626 assert_eq!(e.len(), 1);
16627 assert!(e.has("a"));
16628 assert!(!e.has("b"));
16629 // Expanding a call to `a` still works (templates table for
16630 // `a` untouched by the failed `b` registration).
16631 let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16632 assert_eq!(expanded[0], parse("(list hey)"));
16633 }
16634
16635 #[test]
16636 fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16637 // Zero ceiling is the "contract-test that this expander
16638 // accepts no new macros" posture — every fresh key rejects
16639 // at the first `defmacro` because `0 >= 0`. Peer to the
16640 // depth-ceiling zero-admits-and-rejects-every-macro-call
16641 // pin one RESOURCE axis over.
16642 let mut e = Expander::new();
16643 e.set_max_registered_macros(0);
16644 let err = e
16645 .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16646 .unwrap_err();
16647 match err {
16648 LispError::RegisteredMacrosExceeded {
16649 macro_name,
16650 count,
16651 limit,
16652 } => {
16653 assert_eq!(macro_name, "a");
16654 assert_eq!(count, 0);
16655 assert_eq!(limit, 0);
16656 }
16657 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16658 }
16659 assert!(e.is_empty());
16660 }
16661
16662 #[test]
16663 fn registered_macros_exceeded_position_is_none() {
16664 // The variant joins the non-positional cohort — the
16665 // offending macro's byte offset is not what a table-count
16666 // diagnostic anchors to; the (offending fresh macro name,
16667 // observed table count, configured ceiling) triple is.
16668 // This pin keeps the variant in the `position() -> None`
16669 // cohort so a future span-carrying edit lands the field
16670 // in ONE place across every non-positional variant
16671 // simultaneously. Peer to the four prior
16672 // `*_exceeded_position_is_none` pins on the resource-
16673 // ceiling cohort.
16674 let err = LispError::RegisteredMacrosExceeded {
16675 macro_name: "fresh-1000".to_string(),
16676 count: 4096,
16677 limit: 4096,
16678 };
16679 assert_eq!(err.position(), None);
16680 }
16681
16682 #[test]
16683 fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16684 // Display projection carries `macro_name`, `count`, AND
16685 // `limit`, so authoring surfaces that substring-grep the
16686 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16687 // THREE halves at the variant boundary and never need to
16688 // substring-parse free-form text to recover the identity
16689 // of the offending fresh key, the observed table count, or
16690 // the ceiling it breached. Peer to the four prior
16691 // `*_exceeded_display_matches_typed_variant_shape` pins.
16692 let err = LispError::RegisteredMacrosExceeded {
16693 macro_name: "fresh-1000".to_string(),
16694 count: 4096,
16695 limit: 4096,
16696 };
16697 let rendered = err.to_string();
16698 assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16699 assert!(rendered.contains("4096"), "rendered: {rendered}");
16700 assert!(
16701 rendered.contains("registered macros count exceeded"),
16702 "rendered: {rendered}"
16703 );
16704 }
16705
16706 #[test]
16707 fn lawful_typescape_within_ceiling_registers_and_expands() {
16708 // A lawful hand-authored typescape (a handful of macros)
16709 // sits well below any realistic ceiling — the default
16710 // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16711 // so operators never brush it on real authoring workloads.
16712 // This pin locks the default ceiling's non-interference
16713 // posture: a regression that ever lowered the default
16714 // enough to reject a small typescape would fail here. Peer
16715 // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16716 let mut e = Expander::new();
16717 let forms = read(
16718 "(defmacro when (cond x) `(if ,cond ,x))
16719 (defmacro twice (x) `(list ,x ,x))
16720 (defmacro pair (x y) `(list ,x ,y))
16721 (when #t (twice hey))",
16722 )
16723 .unwrap();
16724 let out = e.expand_program(forms).unwrap();
16725 assert_eq!(out[0], parse("(if #t (list hey hey))"));
16726 assert_eq!(e.len(), 3);
16727 }
16728
16729 #[test]
16730 fn register_macro_def_direct_call_respects_table_ceiling() {
16731 // `register_macro_def` is the substrate primitive both
16732 // `expand_program`'s `defmacro` recognition AND
16733 // `with_macros`'s bulk load route through. The table-count
16734 // ceiling fires on the primitive itself, not on any one
16735 // consumer — a regression that ever moved the gate into
16736 // `expand_program`'s arm alone would fail this pin. Peer
16737 // to `register_macro_def_direct_call_respects_body_size_ceiling`
16738 // one RESOURCE-DIMENSION axis over.
16739 let mut e = Expander::new();
16740 e.set_max_registered_macros(1);
16741 let def_a = MacroDef {
16742 name: "a".to_string(),
16743 params: MacroParams {
16744 required: vec!["x".to_string()],
16745 optional: Vec::new(),
16746 rest: None,
16747 },
16748 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16749 };
16750 let def_b = MacroDef {
16751 name: "b".to_string(),
16752 params: MacroParams {
16753 required: vec!["x".to_string()],
16754 optional: Vec::new(),
16755 rest: None,
16756 },
16757 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16758 };
16759 // First admits — table sits at ceiling.
16760 e.register_macro_def(def_a).unwrap();
16761 assert!(e.has("a"));
16762 assert_eq!(e.len(), 1);
16763 // Second rejects — fresh key past ceiling.
16764 let err = e.register_macro_def(def_b).unwrap_err();
16765 match err {
16766 LispError::RegisteredMacrosExceeded {
16767 macro_name,
16768 count,
16769 limit,
16770 } => {
16771 assert_eq!(macro_name, "b");
16772 assert_eq!(count, 1);
16773 assert_eq!(limit, 1);
16774 }
16775 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16776 }
16777 assert!(!e.has("b"));
16778 assert_eq!(e.len(), 1);
16779 }
16780
16781 #[test]
16782 fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16783 // Direct `register_macro_def` calls must ALSO admit
16784 // overwrites at ceiling — the overwrite discipline lives
16785 // on the primitive, not on `expand_program`'s arm alone.
16786 // Sibling of the source-level overwrite admission test.
16787 let mut e = Expander::new();
16788 e.set_max_registered_macros(1);
16789 let def_a_v1 = MacroDef {
16790 name: "a".to_string(),
16791 params: MacroParams {
16792 required: vec!["x".to_string()],
16793 optional: Vec::new(),
16794 rest: None,
16795 },
16796 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16797 };
16798 let def_a_v2 = MacroDef {
16799 name: "a".to_string(),
16800 params: MacroParams {
16801 required: vec!["x".to_string(), "y".to_string()],
16802 optional: Vec::new(),
16803 rest: None,
16804 },
16805 body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16806 };
16807 e.register_macro_def(def_a_v1).unwrap();
16808 assert_eq!(e.len(), 1);
16809 // Overwrite of `a` at ceiling admits — no growth.
16810 e.register_macro_def(def_a_v2).unwrap();
16811 assert_eq!(e.len(), 1);
16812 }
16813
16814 // ── `MacroParams::total_arity` — the total-slots projection ──
16815 //
16816 // `total_arity()` names `required.len() + optional.len() +
16817 // rest.is_some() as usize` on the typed `MacroParams` algebra —
16818 // the `Vec`-free peer of `names().len()` AND the axis both
16819 // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16820 // REGISTER-time arity-ceiling gate consult. These pins cover:
16821 // structural identity vs. `names().len()`, the
16822 // `fixed_arity + rest.is_some() as usize` decomposition, and the
16823 // three canonical shapes (empty, all-required, mixed with rest).
16824 #[test]
16825 fn macro_params_total_arity_matches_names_len() {
16826 // The structural identity `total_arity() == names().len()`
16827 // — the `Vec`-free primitive's projection equals the flat-
16828 // index list's length. A regression that ever miscounts
16829 // one slot (drops rest, double-counts optional) fails this
16830 // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16831 let params = MacroParams {
16832 required: vec!["a".into(), "b".into()],
16833 optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16834 rest: Some("e".into()),
16835 };
16836 assert_eq!(params.total_arity(), params.names().len());
16837 assert_eq!(params.total_arity(), 5);
16838 }
16839
16840 #[test]
16841 fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16842 // The `fixed_arity + rest.is_some() as usize` decomposition
16843 // — pins the composition on the [`MacroParams`] algebra so
16844 // a future consumer that wants "total slots" via
16845 // `fixed_arity + rest?` binds to the same arithmetic
16846 // `total_arity` names.
16847 let rest_none = MacroParams {
16848 required: vec!["a".into()],
16849 optional: vec![OptionalParam::bare("b")],
16850 rest: None,
16851 };
16852 assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16853 assert_eq!(rest_none.total_arity(), 2);
16854 let rest_some = MacroParams {
16855 required: vec!["a".into()],
16856 optional: vec![OptionalParam::bare("b")],
16857 rest: Some("r".into()),
16858 };
16859 assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16860 assert_eq!(rest_some.total_arity(), 3);
16861 }
16862
16863 #[test]
16864 fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16865 // Nullary macro edge: `(defmacro nullary () BODY)` has
16866 // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16867 // ceiling the arity gate keys on `>` (equality admits), so
16868 // this nullary registration succeeds while any non-nullary
16869 // one rejects — the exact contract the arity-gate
16870 // zero-admits-nullary test below relies on.
16871 let params = MacroParams::default();
16872 assert_eq!(params.total_arity(), 0);
16873 }
16874
16875 // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16876 // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16877 // DIMENSION axis over — where body-size bounds a per-registration
16878 // BODY node count, this ceiling bounds a per-registration PARAM
16879 // slot count. Peer to registered-macros (REGISTER-time TABLE
16880 // COUNT) one RESOURCE-DIMENSION axis over — where
16881 // registered-macros bounds cumulative registration COUNT, this
16882 // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16883 // the three REGISTER-time ceilings close the (per-body SIZE,
16884 // per-body ARITY, per-table COUNT) three-corner surface.
16885
16886 #[test]
16887 fn expander_default_max_macro_arity_is_the_module_constant() {
16888 // Both constructors seed `max_macro_arity` from the module
16889 // constant — a regression that drifts one constructor's
16890 // default from the constant (e.g. a hardcoded `128` at ONE
16891 // site with the other left uninitialized after a `Default`
16892 // derive addition) fails this pin. Peer to the five prior
16893 // default-coherence pins on depth, cache-entries,
16894 // expansion-size, body-size, and registered-macros.
16895 assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16896 assert_eq!(
16897 Expander::new_substitute_only().max_macro_arity(),
16898 DEFAULT_MAX_MACRO_ARITY
16899 );
16900 assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16901 }
16902
16903 #[test]
16904 fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16905 // Setter mirrors accessor — a regression that ever forgets to
16906 // write through to the field, or writes to a shadow field the
16907 // accessor does not read, fails this pin. Peer to the five
16908 // prior setter/accessor coherence pins.
16909 let mut e = Expander::new();
16910 e.set_max_macro_arity(16);
16911 assert_eq!(e.max_macro_arity(), 16);
16912 }
16913
16914 #[test]
16915 fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16916 // `(defmacro huge (a b c) `,a)` declares 3 required params.
16917 // Under `set_max_macro_arity(2)` the arity gate fires:
16918 // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16919 // arity: 3, limit: 2 }`. `arity` is the observed
16920 // `def.params.total_arity()`, NOT `required.len()` alone —
16921 // even an optional-only or rest-only overrun trips the gate
16922 // (pinned separately). Peer to the register-time body-size
16923 // rejection tests one RESOURCE-DIMENSION axis over.
16924 let mut e = Expander::new();
16925 e.set_max_macro_arity(2);
16926 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16927 let err = e.expand_program(forms).unwrap_err();
16928 match err {
16929 LispError::MacroArityExceeded {
16930 macro_name,
16931 arity,
16932 limit,
16933 } => {
16934 assert_eq!(macro_name, "huge");
16935 assert_eq!(arity, 3);
16936 assert_eq!(limit, 2);
16937 }
16938 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16939 }
16940 assert!(!e.has("huge"));
16941 }
16942
16943 #[test]
16944 fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16945 // The substitute strategy shares the same
16946 // `register_macro_def` outer registration entry with the
16947 // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16948 // fires at the SAME gate regardless of which apply-layer is
16949 // active. Peer to
16950 // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16951 let mut e = Expander::new_substitute_only();
16952 e.set_max_macro_arity(2);
16953 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16954 let err = e.expand_program(forms).unwrap_err();
16955 match err {
16956 LispError::MacroArityExceeded {
16957 macro_name,
16958 arity,
16959 limit,
16960 } => {
16961 assert_eq!(macro_name, "huge");
16962 assert_eq!(arity, 3);
16963 assert_eq!(limit, 2);
16964 }
16965 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16966 }
16967 assert!(!e.has("huge"));
16968 }
16969
16970 #[test]
16971 fn register_arity_gate_counts_optional_slots() {
16972 // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16973 // optional + 0 rest = arity 3 via `total_arity`. Under a
16974 // ceiling of `2` the gate rejects, matching the required-only
16975 // arity-bomb — the arity axis is `total_arity()`, not
16976 // `required.len()`. A regression that drops the optional slots
16977 // from the arity computation would ADMIT this shape and fail
16978 // this pin.
16979 let mut e = Expander::new();
16980 e.set_max_macro_arity(2);
16981 let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16982 let err = e.expand_program(forms).unwrap_err();
16983 match err {
16984 LispError::MacroArityExceeded {
16985 macro_name,
16986 arity,
16987 limit,
16988 } => {
16989 assert_eq!(macro_name, "foo");
16990 assert_eq!(arity, 3);
16991 assert_eq!(limit, 2);
16992 }
16993 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16994 }
16995 }
16996
16997 #[test]
16998 fn register_arity_gate_counts_rest_slot() {
16999 // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
17000 // optional + 1 rest = arity 3 via `total_arity`. Under a
17001 // ceiling of `2` the gate rejects, matching the required-only
17002 // arity-bomb — the arity axis includes the rest slot as ONE
17003 // named param, not zero. A regression that drops the rest slot
17004 // from the arity computation would ADMIT this shape and fail
17005 // this pin.
17006 let mut e = Expander::new();
17007 e.set_max_macro_arity(2);
17008 let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
17009 let err = e.expand_program(forms).unwrap_err();
17010 match err {
17011 LispError::MacroArityExceeded {
17012 macro_name,
17013 arity,
17014 limit,
17015 } => {
17016 assert_eq!(macro_name, "spread");
17017 assert_eq!(arity, 3);
17018 assert_eq!(limit, 2);
17019 }
17020 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17021 }
17022 }
17023
17024 #[test]
17025 fn register_arity_at_ceiling_admits() {
17026 // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
17027 // 2 via `total_arity`. Under a ceiling of `2` the arity gate
17028 // keys on `>` (equality admits; only strict overrun rejects),
17029 // so the registration succeeds and the expansion proceeds
17030 // normally. Fail-before/pass-after negative control on the
17031 // exact boundary: a regression that ever shifted the boundary
17032 // by one (e.g. keyed on `>=` instead of `>`) would fail this
17033 // pin. Peer to `register_macro_body_at_ceiling_admits` on the
17034 // body-size axis.
17035 let mut e = Expander::new();
17036 e.set_max_macro_arity(2);
17037 let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
17038 let out = e.expand_program(forms).unwrap();
17039 assert_eq!(out[0], parse("(list a b)"));
17040 assert_eq!(e.len(), 1);
17041 }
17042
17043 #[test]
17044 fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
17045 // A rejected arity-gated registration MUST leave BOTH
17046 // `self.macros` AND `self.templates` exactly as they were —
17047 // no partial-write window in which one table carries the
17048 // over-ceiling entry while the other does not, or in which
17049 // the template pre-compile already ran. Arity gate fires
17050 // BEFORE the body-size gate walks the body AND BEFORE any
17051 // insert lands, so a regression that ever moved the arity
17052 // gate below `self.templates.insert` or below
17053 // `self.macros.insert` would fail this pin. Peer to
17054 // `register_failure_leaves_both_tables_pristine` (body-size)
17055 // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
17056 // (table-count).
17057 let mut e = Expander::new();
17058 e.set_max_macro_arity(1);
17059 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17060 assert!(e.expand_program(forms).is_err());
17061 assert!(
17062 !e.has("huge"),
17063 "macros table must stay pristine after rejection"
17064 );
17065 assert_eq!(e.len(), 0);
17066 // Expanding a NEW lawful macro afterwards still works — the
17067 // failed arity-gate rejection has not corrupted expander
17068 // state.
17069 let out = e
17070 .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
17071 .unwrap();
17072 assert_eq!(out[0], parse("hey"));
17073 }
17074
17075 #[test]
17076 fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
17077 // Zero ceiling is the "contract-test that this expander
17078 // accepts only nullary macros" posture — every fresh
17079 // non-nullary key rejects at the arity gate because
17080 // `total_arity() > 0`, while nullary `(defmacro nullary ()
17081 // BODY)` still admits (its arity is exactly 0, and only
17082 // strict overrun rejects). Peer to the body-size zero-rejects-
17083 // every-non-empty-body pin one RESOURCE-DIMENSION axis over
17084 // (though body-size zero rejects even nullary-body shapes,
17085 // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
17086 // preserves the nullary registration surface).
17087 let mut e = Expander::new();
17088 e.set_max_macro_arity(0);
17089 // Nullary admits — arity 0 <= 0.
17090 e.expand_program(read("(defmacro nullary () `unit)").unwrap())
17091 .unwrap();
17092 assert!(e.has("nullary"));
17093 // Non-nullary rejects — arity 1 > 0.
17094 let err = e
17095 .expand_program(read("(defmacro id (x) `,x)").unwrap())
17096 .unwrap_err();
17097 match err {
17098 LispError::MacroArityExceeded {
17099 macro_name,
17100 arity,
17101 limit,
17102 } => {
17103 assert_eq!(macro_name, "id");
17104 assert_eq!(arity, 1);
17105 assert_eq!(limit, 0);
17106 }
17107 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17108 }
17109 assert!(!e.has("id"));
17110 }
17111
17112 #[test]
17113 fn macro_arity_exceeded_position_is_none() {
17114 // The variant joins the non-positional cohort — the offending
17115 // macro's byte offset is not what an arity-bomb diagnostic
17116 // anchors to; the (macro name, observed arity, configured
17117 // ceiling) triple is. This pin keeps the variant in the
17118 // `position() -> None` cohort so a future span-carrying edit
17119 // lands the field in ONE place across every non-positional
17120 // variant simultaneously. Peer to the five prior
17121 // `*_exceeded_position_is_none` pins on the resource-ceiling
17122 // cohort.
17123 let err = LispError::MacroArityExceeded {
17124 macro_name: "huge".to_string(),
17125 arity: 512,
17126 limit: 128,
17127 };
17128 assert_eq!(err.position(), None);
17129 }
17130
17131 #[test]
17132 fn macro_arity_exceeded_display_matches_typed_variant_shape() {
17133 // Display projection carries `macro_name`, `arity`, AND
17134 // `limit`, so authoring surfaces that substring-grep the
17135 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
17136 // THREE halves at the variant boundary and never need to
17137 // substring-parse free-form text to recover the identity of
17138 // the offending macro, the observed arity, or the ceiling it
17139 // breached. Peer to the five prior
17140 // `*_exceeded_display_matches_typed_variant_shape` pins.
17141 let err = LispError::MacroArityExceeded {
17142 macro_name: "huge".to_string(),
17143 arity: 512,
17144 limit: 128,
17145 };
17146 let rendered = err.to_string();
17147 assert!(rendered.contains("huge"), "rendered: {rendered}");
17148 assert!(rendered.contains("512"), "rendered: {rendered}");
17149 assert!(rendered.contains("128"), "rendered: {rendered}");
17150 assert!(
17151 rendered.contains("macro arity exceeded"),
17152 "rendered: {rendered}"
17153 );
17154 }
17155
17156 #[test]
17157 fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17158 // A lawful hand-authored macro arity sits well below any
17159 // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17160 // is deliberately generous so operators never brush it on
17161 // real authoring workloads. This pin locks the default
17162 // ceiling's non-interference posture: a regression that
17163 // ever lowered the default enough to reject the reference
17164 // multi-slot `(defmacro when (cond x) …)` shape would fail
17165 // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17166 // and `lawful_typescape_within_ceiling_registers_and_expands`.
17167 let mut e = Expander::new();
17168 let forms = read(
17169 "(defmacro when (cond x) `(if ,cond ,x))
17170 (defmacro triple (x y z) `(list ,x ,y ,z))
17171 (when #t (triple a b c))",
17172 )
17173 .unwrap();
17174 let out = e.expand_program(forms).unwrap();
17175 assert_eq!(out[0], parse("(if #t (list a b c))"));
17176 }
17177
17178 #[test]
17179 fn register_macro_def_direct_call_respects_arity_ceiling() {
17180 // `register_macro_def` is the substrate primitive both
17181 // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17182 // bulk load route through. The arity ceiling fires on the
17183 // primitive itself, not on any one consumer — a regression
17184 // that ever moved the gate into `expand_program`'s arm alone
17185 // would fail this pin. Peer to
17186 // `register_macro_def_direct_call_respects_body_size_ceiling`
17187 // AND `register_macro_def_direct_call_respects_table_ceiling`
17188 // one RESOURCE-DIMENSION axis over.
17189 let mut e = Expander::new();
17190 e.set_max_macro_arity(2);
17191 let def = MacroDef {
17192 name: "huge".to_string(),
17193 params: MacroParams {
17194 required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17195 optional: Vec::new(),
17196 rest: None,
17197 },
17198 body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17199 };
17200 // Arity 3 > ceiling 2 — rejected before body-size gate.
17201 let err = e.register_macro_def(def).unwrap_err();
17202 match err {
17203 LispError::MacroArityExceeded {
17204 macro_name,
17205 arity,
17206 limit,
17207 } => {
17208 assert_eq!(macro_name, "huge");
17209 assert_eq!(arity, 3);
17210 assert_eq!(limit, 2);
17211 }
17212 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17213 }
17214 assert!(!e.has("huge"));
17215 }
17216
17217 #[test]
17218 fn arity_gate_fires_before_body_size_gate() {
17219 // ORDERING PIN — the arity gate fires BEFORE the body-size
17220 // gate. With both `set_max_macro_arity(1)` AND
17221 // `set_max_macro_body_size(2)` set below a shape that violates
17222 // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17223 // arity 2 AND body node count > 2 — the arity gate's
17224 // rejection wins, so the returned variant is
17225 // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17226 // regression that ever swapped the two gates' order would fail
17227 // this pin. The ordering isn't cosmetic: arity is O(1)
17228 // (three-arm addition on `def.params` fields), body-size is
17229 // O(body_nodes) (`def.body.node_count()` walks the AST), so
17230 // the O(1)-first order minimises the cost of rejecting the
17231 // worst case (a large-body large-arity macro).
17232 let mut e = Expander::new();
17233 e.set_max_macro_arity(1);
17234 e.set_max_macro_body_size(2);
17235 let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17236 let err = e.expand_program(forms).unwrap_err();
17237 assert!(
17238 matches!(err, LispError::MacroArityExceeded { .. }),
17239 "arity gate must fire before body-size gate; got: {err:?}"
17240 );
17241 }
17242
17243 #[test]
17244 fn arity_gate_fires_after_table_count_gate() {
17245 // ORDERING PIN — the table-count gate fires BEFORE the arity
17246 // gate. Under `set_max_registered_macros(1)` at capacity AND
17247 // `set_max_macro_arity(1)`, a fresh over-arity registration
17248 // trips the table-count gate FIRST (its `HashMap::len`
17249 // comparison is O(1) cache-friendly and gates cumulative
17250 // registration cost); the arity gate is O(1) but on `def`,
17251 // not on shared state, so it sits AFTER the table-count gate
17252 // on the O(1)-first ordering of the REGISTER-time chain. A
17253 // regression that ever swapped the two would fail this pin.
17254 let mut e = Expander::new();
17255 e.set_max_registered_macros(1);
17256 e.set_max_macro_arity(1);
17257 // Land one macro at table ceiling.
17258 e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17259 .unwrap();
17260 assert_eq!(e.len(), 1);
17261 // Fresh registration with BOTH violations — table-count wins.
17262 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17263 let err = e.expand_program(forms).unwrap_err();
17264 assert!(
17265 matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17266 "table-count gate must fire before arity gate; got: {err:?}"
17267 );
17268 }
17269
17270 // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17271
17272 #[test]
17273 fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17274 // Pin the (constant × aggregation) corner: the const-form
17275 // aggregate MUST bind every field to its individually-defined
17276 // module constant. A re-tuning of ONE module constant
17277 // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17278 // has to propagate through the aggregate at ONE site; this
17279 // test fires the divergence loudly if a future edit sets a
17280 // module constant to a fresh value but forgets to update the
17281 // matching aggregate slot. Field-by-field equality carries
17282 // the message better than one deep-equality assertion.
17283 assert_eq!(
17284 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17285 DEFAULT_MAX_EXPANSION_DEPTH
17286 );
17287 assert_eq!(
17288 DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17289 DEFAULT_MAX_CACHE_ENTRIES
17290 );
17291 assert_eq!(
17292 DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17293 DEFAULT_MAX_EXPANSION_SIZE
17294 );
17295 assert_eq!(
17296 DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17297 DEFAULT_MAX_MACRO_BODY_SIZE
17298 );
17299 assert_eq!(
17300 DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17301 DEFAULT_MAX_REGISTERED_MACROS
17302 );
17303 assert_eq!(
17304 DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17305 DEFAULT_MAX_MACRO_ARITY
17306 );
17307 }
17308
17309 #[test]
17310 fn resource_limits_default_impl_matches_const_form() {
17311 // The two projections of the shipped posture — the `const`
17312 // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17313 // `Default::default()` form — must reach the SAME value. Pins
17314 // that a future edit which flips one to a new default cannot
17315 // silently drift the other.
17316 assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17317 }
17318
17319 #[test]
17320 fn expander_new_resource_limits_matches_shipped_defaults() {
17321 // Fresh-expander posture on the bundled-getter surface — the
17322 // six ceilings [`Expander::new`] seeds MUST snapshot to
17323 // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17324 // individual `expander_default_max_*_is_the_module_constant`
17325 // pins one AGGREGATION axis over — where each individual
17326 // pin catches ONE field drift, this pin catches ANY of the
17327 // six drifting AND the snapshot projection itself losing a
17328 // field.
17329 assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17330 }
17331
17332 #[test]
17333 fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17334 // Substitute-only expander seeds the SAME six ceilings from
17335 // the SAME module constants as [`Expander::new`] — the two
17336 // constructors share the resource-ceiling posture even
17337 // though they diverge on `compile_templates` +
17338 // `cache_enabled`. Pins that a future constructor which
17339 // forgets to seed one of the six ceilings from its module
17340 // constant fails loudly through the bundled getter.
17341 assert_eq!(
17342 Expander::new_substitute_only().resource_limits(),
17343 DEFAULT_RESOURCE_LIMITS
17344 );
17345 }
17346
17347 #[test]
17348 fn resource_limits_snapshot_reflects_each_individual_setter() {
17349 // Coherence pin between the six individual setters and the
17350 // bundled getter — flipping ONE ceiling via its individual
17351 // setter must project through [`Expander::resource_limits`]
17352 // to the SAME value the individual getter returns. A
17353 // regression that leaves ONE field out of the snapshot
17354 // projection (a copy-paste omission in the struct literal
17355 // constructor) fires here. Distinct low-non-default values
17356 // so a stray field carrying the default fails loudly.
17357 let mut e = Expander::new();
17358 e.set_max_expansion_depth(3);
17359 e.set_max_cache_entries(5);
17360 e.set_max_expansion_size(7);
17361 e.set_max_macro_body_size(11);
17362 e.set_max_registered_macros(13);
17363 e.set_max_macro_arity(17);
17364 let snap = e.resource_limits();
17365 assert_eq!(snap.max_expansion_depth, 3);
17366 assert_eq!(snap.max_cache_entries, 5);
17367 assert_eq!(snap.max_expansion_size, 7);
17368 assert_eq!(snap.max_macro_body_size, 11);
17369 assert_eq!(snap.max_registered_macros, 13);
17370 assert_eq!(snap.max_macro_arity, 17);
17371 // And the individual getters see the same values — the
17372 // snapshot is a projection, not a divergent copy.
17373 assert_eq!(e.max_expansion_depth(), 3);
17374 assert_eq!(e.max_cache_entries(), 5);
17375 assert_eq!(e.max_expansion_size(), 7);
17376 assert_eq!(e.max_macro_body_size(), 11);
17377 assert_eq!(e.max_registered_macros(), 13);
17378 assert_eq!(e.max_macro_arity(), 17);
17379 }
17380
17381 #[test]
17382 fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17383 // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17384 // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17385 // must reach every individual `max_*` getter. A regression
17386 // that drops one field from the destructuring assignment in
17387 // the bulk setter fires here. Distinct low-non-default values
17388 // so a stray field carrying the previous value fails loudly.
17389 let mut e = Expander::new();
17390 e.set_resource_limits(ResourceLimits {
17391 max_expansion_depth: 2,
17392 max_cache_entries: 4,
17393 max_expansion_size: 8,
17394 max_macro_body_size: 16,
17395 max_registered_macros: 32,
17396 max_macro_arity: 64,
17397 });
17398 assert_eq!(e.max_expansion_depth(), 2);
17399 assert_eq!(e.max_cache_entries(), 4);
17400 assert_eq!(e.max_expansion_size(), 8);
17401 assert_eq!(e.max_macro_body_size(), 16);
17402 assert_eq!(e.max_registered_macros(), 32);
17403 assert_eq!(e.max_macro_arity(), 64);
17404 }
17405
17406 #[test]
17407 fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17408 // Round-trip theorem — for any starting expander, the
17409 // composition `set_resource_limits(resource_limits())` is
17410 // the identity on the resource-ceiling surface. This is the
17411 // ONE typed statement that pins the (getter, setter) pair as
17412 // strict projections of the same six-field surface, no
17413 // hidden lossy field, no ordering asymmetry between the
17414 // struct literal and the destructuring pattern.
17415 let mut e = Expander::new();
17416 e.set_max_expansion_depth(9);
17417 e.set_max_cache_entries(19);
17418 e.set_max_expansion_size(29);
17419 e.set_max_macro_body_size(39);
17420 e.set_max_registered_macros(49);
17421 e.set_max_macro_arity(59);
17422 let snap = e.resource_limits();
17423 // Reset to defaults so any leftover state would show up in
17424 // the post-round-trip snapshot as the default rather than
17425 // the intended value.
17426 e.set_resource_limits(ResourceLimits::default());
17427 assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17428 // Apply the captured snapshot — the expander must land at
17429 // the same six values `snap` recorded.
17430 e.set_resource_limits(snap);
17431 assert_eq!(e.resource_limits(), snap);
17432 }
17433
17434 #[test]
17435 fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17436 // The `Copy` posture on [`ResourceLimits`] gives callers the
17437 // cheap ergonomic of a struct-update literal — take the
17438 // shipped posture and override ONE ceiling — that a chain of
17439 // six independent setters cannot express in ONE typed expression.
17440 // Pins the intended use-shape so a future refactor which
17441 // removes `Copy` from the struct fails to compile at THIS
17442 // site rather than at every downstream consumer.
17443 let mut e = Expander::new();
17444 e.set_resource_limits(ResourceLimits {
17445 max_macro_arity: 4,
17446 ..DEFAULT_RESOURCE_LIMITS
17447 });
17448 // Overridden field carries the fresh value.
17449 assert_eq!(e.max_macro_arity(), 4);
17450 // Every other field carries the shipped default — the
17451 // struct-update literal is a full projection, not a
17452 // selective mutation.
17453 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17454 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17455 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17456 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17457 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17458 }
17459
17460 #[test]
17461 fn expander_default_derives_resource_limits_from_bundled_field_default() {
17462 // Post the [`Expander::limits`]-field consolidation the derived
17463 // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17464 // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17465 // `Expander::default().resource_limits()` lands at the shipped
17466 // ceiling posture verbatim. Pre-consolidation the six independent
17467 // `max_*: usize` fields defaulted to the bare `usize` zero, so
17468 // `Expander::default()` yielded a below-floor posture with every
17469 // ceiling clamped to `0` — a shape no consumer wanted and every
17470 // test path avoided by routing through [`Expander::new`] instead.
17471 // The consolidation is what turned that latent below-floor
17472 // derived-`Default` shape into a lawful shipped posture on the
17473 // structural axis. Peer of
17474 // `expander_new_resource_limits_matches_shipped_defaults` one
17475 // CONSTRUCTOR axis over — where that test pins the explicit
17476 // [`Expander::new`] constructor, this test pins the derived-
17477 // [`Default`] constructor route to the SAME six values through
17478 // the SAME bundled projection. Sibling of
17479 // `resource_limits_default_impl_matches_const_form` one
17480 // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17481 // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17482 // derived `Default` to project through it.
17483 assert_eq!(
17484 Expander::default().resource_limits(),
17485 DEFAULT_RESOURCE_LIMITS
17486 );
17487 }
17488
17489 #[test]
17490 fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17491 // Behavioral pin — the bundled setter is not a decorative
17492 // getter/setter pair; the six ceilings it propagates must
17493 // reach the SAME check sites the individual setters do. Set
17494 // `max_macro_arity` to 1 via the bulk setter and confirm the
17495 // arity gate fires on a two-arg macro registration. A
17496 // regression that wired the bulk setter to a shadow field
17497 // rather than the actual `Expander` state fails here.
17498 let mut e = Expander::new();
17499 e.set_resource_limits(ResourceLimits {
17500 max_macro_arity: 1,
17501 ..DEFAULT_RESOURCE_LIMITS
17502 });
17503 let forms = read("(defmacro two (a b) `,a)").unwrap();
17504 let err = e.expand_program(forms).unwrap_err();
17505 assert!(
17506 matches!(
17507 err,
17508 LispError::MacroArityExceeded {
17509 arity: 2,
17510 limit: 1,
17511 ..
17512 }
17513 ),
17514 "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17515 );
17516 }
17517
17518 // ── Expander::with_limits — at-construction resource-posture ───────
17519
17520 #[test]
17521 fn expander_with_limits_seeds_the_provided_resource_posture() {
17522 // Round-trip pin at CONSTRUCTION time — the bundle the caller
17523 // passes into [`Expander::with_limits`] MUST project back
17524 // through [`Expander::resource_limits`] verbatim. Peer of the
17525 // `set_resource_limits(l); resource_limits() == l` round-trip
17526 // one CONSTRUCTOR-STAGE axis over — that pin covers the
17527 // POST-construction assignment; this pin covers the
17528 // AT-construction assignment. A regression that dropped the
17529 // `limits` argument on the floor (a copy-paste that left
17530 // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17531 // instead of `limits`) fires here on every non-default field.
17532 // Distinct low-non-default values so a stray field defaulting
17533 // fails loudly.
17534 let want = ResourceLimits {
17535 max_expansion_depth: 3,
17536 max_cache_entries: 5,
17537 max_expansion_size: 7,
17538 max_macro_body_size: 11,
17539 max_registered_macros: 13,
17540 max_macro_arity: 17,
17541 };
17542 assert_eq!(Expander::with_limits(want).resource_limits(), want);
17543 }
17544
17545 #[test]
17546 fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17547 // Delegation identity — [`Expander::new`] delegates to
17548 // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17549 // the two constructors MUST agree on the resource-ceiling
17550 // projection. Pins the "no drift between the two paths to a
17551 // fresh default-posture expander" theorem: a future refactor
17552 // that breaks the delegation (an inline field literal reappearing
17553 // in [`Expander::new`], an accidental hardcoded const at ONE
17554 // site with the other unchanged) fires here. Peer of
17555 // `resource_limits_default_impl_matches_const_form` one
17556 // TYPE-LEVEL axis over — that test pins the two projections of
17557 // the shipped posture on [`ResourceLimits`] itself; this test
17558 // pins the two constructor paths on [`Expander`] to reach the
17559 // SAME six values through the SAME bundle.
17560 assert_eq!(
17561 Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17562 Expander::new().resource_limits()
17563 );
17564 }
17565
17566 #[test]
17567 fn expander_with_limits_composes_with_struct_update_override() {
17568 // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17569 // "the shipped posture with ONE ceiling overridden" as ONE
17570 // struct-update literal AND thread that composition through
17571 // [`Expander::with_limits`] as ONE typed call — the ergonomic
17572 // shape that six independent setter invocations cannot express
17573 // in ONE expression. Overridden ceiling carries the fresh value,
17574 // every other ceiling carries its shipped default. A future
17575 // refactor that removes `Copy` from [`ResourceLimits`] fails to
17576 // compile at THIS site rather than at every downstream consumer.
17577 let e = Expander::with_limits(ResourceLimits {
17578 max_macro_arity: 4,
17579 ..DEFAULT_RESOURCE_LIMITS
17580 });
17581 assert_eq!(e.max_macro_arity(), 4);
17582 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17583 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17584 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17585 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17586 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17587 }
17588
17589 #[test]
17590 fn expander_with_limits_carries_the_new_execution_strategy() {
17591 // The (compile_templates, cache_enabled) execution-strategy pair
17592 // MUST carry the [`Expander::new`] default posture — bytecode +
17593 // cache both on — regardless of which resource posture the
17594 // caller passes in. Cache-count observation is the least-
17595 // invasive check available on the public surface: a fresh
17596 // `with_limits` expander that expands a repeat-call site must
17597 // populate at least one cache entry (bytecode + cache both on),
17598 // and a substitute-only sibling would leave `cache_size()` at
17599 // zero even after every call site expanded. A regression that
17600 // wired the wrong execution strategy into `with_limits`
17601 // (compile_templates: false, cache_enabled: false) fires here.
17602 let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17603 let forms = read(
17604 "(defmacro id (x) `,x)
17605 (id one)
17606 (id one)",
17607 )
17608 .unwrap();
17609 e.expand_program(forms).unwrap();
17610 assert!(
17611 e.cache_size() >= 1,
17612 "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17613 e.cache_size()
17614 );
17615 }
17616
17617 #[test]
17618 fn expander_with_limits_fires_the_arity_gate_from_construction() {
17619 // Behavioral pin — the at-construction bundle assignment MUST
17620 // reach the SAME check sites the individual + bulk POST-
17621 // construction setters do. Construct an expander with
17622 // `max_macro_arity: 1` directly and confirm the arity gate
17623 // fires on a two-arg macro registration. Sibling of
17624 // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17625 // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17626 // construction setter reaching the gate; this pin covers the
17627 // AT-construction assignment reaching the SAME gate through
17628 // the SAME `self.limits.max_macro_arity` load. A regression
17629 // that wired `with_limits` to a shadow field rather than
17630 // `self.limits` fails here.
17631 let mut e = Expander::with_limits(ResourceLimits {
17632 max_macro_arity: 1,
17633 ..DEFAULT_RESOURCE_LIMITS
17634 });
17635 let forms = read("(defmacro two (a b) `,a)").unwrap();
17636 let err = e.expand_program(forms).unwrap_err();
17637 assert!(
17638 matches!(
17639 err,
17640 LispError::MacroArityExceeded {
17641 arity: 2,
17642 limit: 1,
17643 ..
17644 }
17645 ),
17646 "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17647 );
17648 }
17649
17650 // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17651
17652 #[test]
17653 fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17654 // Field-level pin — the ceiling-lifted preset carries
17655 // [`usize::MAX`] on every axis of the six-field surface. Pinned
17656 // as six independent field asserts (rather than one struct
17657 // equality) so a drift on ONE field carries a distinct-name
17658 // failure that names the drifting axis. A future extension
17659 // that adds a SEVENTH ceiling requires an additional field
17660 // assert here in lockstep with the const literal — rustc's
17661 // field-exhaustiveness on the const forces the new field to
17662 // appear, and this pin exercises it.
17663 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17664 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17665 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17666 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17667 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17668 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17669 }
17670
17671 #[test]
17672 fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17673 // Structural-distinctness pin — the two constants
17674 // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17675 // sit at DISTINCT points on the posture axis of the (posture ×
17676 // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17677 // six-field surface — every shipped `DEFAULT_MAX_*` module
17678 // constant is a concrete positive value strictly less than
17679 // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17680 // shipped default up to [`usize::MAX`] (collapsing the two
17681 // presets on that field) fires here loudly rather than
17682 // silently letting the two presets become indistinguishable
17683 // on that axis. A regression which flipped ONE unbounded-const
17684 // field back to its default value also fires here — either
17685 // half of the disagreement breaks the theorem.
17686 assert_ne!(
17687 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17688 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17689 );
17690 assert_ne!(
17691 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17692 DEFAULT_RESOURCE_LIMITS.max_cache_entries
17693 );
17694 assert_ne!(
17695 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17696 DEFAULT_RESOURCE_LIMITS.max_expansion_size
17697 );
17698 assert_ne!(
17699 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17700 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17701 );
17702 assert_ne!(
17703 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17704 DEFAULT_RESOURCE_LIMITS.max_registered_macros
17705 );
17706 assert_ne!(
17707 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17708 DEFAULT_RESOURCE_LIMITS.max_macro_arity
17709 );
17710 }
17711
17712 #[test]
17713 fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17714 // Round-trip pin at CONSTRUCTION time — an expander built at
17715 // the ceiling-lifted preset MUST project back through
17716 // [`Expander::resource_limits`] to the SAME constant verbatim.
17717 // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17718 // one PRESET-POSTURE axis over — that pin exercises the
17719 // constructor's carry via a distinct-low-value bundle; this
17720 // pin exercises it via the ceiling-lifted preset constant.
17721 // Cross-pin the six-field snapshot equals the ceiling-lifted
17722 // constant so a regression that broke either the constant OR
17723 // the constructor's carry fires here.
17724 assert_eq!(
17725 Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17726 UNBOUNDED_RESOURCE_LIMITS
17727 );
17728 }
17729
17730 #[test]
17731 fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17732 // Behavioral pin — the ceiling-lifted preset MUST reach the
17733 // register-time body-size gate as an actually-lifted ceiling.
17734 // Register a macro whose body node count strictly exceeds
17735 // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17736 // shipped default would fail [`register_macro_def`] with a
17737 // [`LispError::MacroBodySizeExceeded`]) and confirm the
17738 // registration succeeds under the ceiling-lifted preset. Only
17739 // the body-size ceiling being lifted lets a body of that
17740 // magnitude land; the arity ceiling is left at 1 param via
17741 // struct-update override to prove the preset's other five
17742 // ceilings STILL project cleanly at [`usize::MAX`] alongside
17743 // the ceiling under test. A regression that failed to lift
17744 // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17745 // in the const literal) fires here on the axis it dropped.
17746 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17747 // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17748 // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17749 let mut body = String::from("(defmacro huge (k) `(");
17750 for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17751 body.push_str(",k ");
17752 }
17753 body.push_str("))");
17754 let forms = read(&body).unwrap();
17755 e.expand_program(forms)
17756 .expect("unbounded body-size ceiling must admit a body over the default limit");
17757 assert!(e.has("huge"));
17758 }
17759
17760 #[test]
17761 fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17762 // Behavioral pin sibling of the body-size pin above one
17763 // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17764 // the ceiling-lifted preset MUST also reach the arity gate as
17765 // an actually-lifted ceiling. Register a macro whose param
17766 // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17767 // 129-slot lambda list which under the shipped default fails
17768 // with [`LispError::MacroArityExceeded`]) and confirm the
17769 // registration succeeds under the ceiling-lifted preset.
17770 use std::fmt::Write as _;
17771 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17772 let mut src = String::from("(defmacro many-arity (");
17773 for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17774 write!(src, "a-{i} ").unwrap();
17775 }
17776 src.push_str(") `,a-0)");
17777 let forms = read(&src).unwrap();
17778 e.expand_program(forms)
17779 .expect("unbounded arity ceiling must admit a param list over the default limit");
17780 assert!(e.has("many-arity"));
17781 }
17782
17783 #[test]
17784 fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17785 // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17786 // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17787 // struct-update literal on the ceiling-lifted preset AND thread
17788 // that composition through [`Expander::with_limits`] as ONE
17789 // typed call. This is the "only THIS ceiling gates" fixture
17790 // shape a chain of six independent setters cannot express in
17791 // ONE expression, and the shape the last commit named as a
17792 // future benefit of the (getter, setter, at-construction)
17793 // three-corner face closing at ONE bundle.
17794 //
17795 // Set only `max_macro_arity: 1`; confirm the arity gate fires
17796 // on a two-arg registration while the five other lifted
17797 // ceilings project through the getter as [`usize::MAX`]. A
17798 // regression that broke the ceiling-lifted preset would EITHER
17799 // fail to project [`usize::MAX`] on one of the five other
17800 // fields OR fail to gate the isolated arity ceiling at 1.
17801 let mut e = Expander::with_limits(ResourceLimits {
17802 max_macro_arity: 1,
17803 ..UNBOUNDED_RESOURCE_LIMITS
17804 });
17805 assert_eq!(e.max_macro_arity(), 1);
17806 assert_eq!(e.max_expansion_depth(), usize::MAX);
17807 assert_eq!(e.max_cache_entries(), usize::MAX);
17808 assert_eq!(e.max_expansion_size(), usize::MAX);
17809 assert_eq!(e.max_macro_body_size(), usize::MAX);
17810 assert_eq!(e.max_registered_macros(), usize::MAX);
17811 let forms = read("(defmacro two (a b) `,a)").unwrap();
17812 let err = e.expand_program(forms).unwrap_err();
17813 assert!(
17814 matches!(
17815 err,
17816 LispError::MacroArityExceeded {
17817 arity: 2,
17818 limit: 1,
17819 ..
17820 }
17821 ),
17822 "struct-update override on the ceiling-lifted preset must \
17823 leave the isolated arity ceiling gating; got: {err:?}"
17824 );
17825 }
17826
17827 #[test]
17828 fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17829 // Coherence pin between the AT-construction constructor and
17830 // the POST-construction bulk setter — both paths that reach the
17831 // ceiling-lifted preset must land at the SAME six values on
17832 // the expander state. Pre-lift a caller wanting the unbounded
17833 // posture composed six individual setter invocations at the
17834 // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17835 // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17836 // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17837 // constant. A regression that wired the bulk setter to a
17838 // shadow field, or the ceiling-lifted preset to a stale field
17839 // literal, fires here on the constructor-vs-setter identity.
17840 let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17841 let mut b = Expander::new();
17842 b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17843 assert_eq!(a.resource_limits(), b.resource_limits());
17844 assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17845 }
17846
17847 // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17848
17849 /// A hand-authored asymmetric posture used across the lattice-law
17850 /// tests. Every field is a distinct positive value strictly less
17851 /// than [`usize::MAX`] and distinct from every corresponding
17852 /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17853 /// (meet, join, absorption, distributivity) discriminate across
17854 /// all three postures unambiguously.
17855 const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17856 max_expansion_depth: 7,
17857 max_cache_entries: 11,
17858 max_expansion_size: 13,
17859 max_macro_body_size: 17,
17860 max_registered_macros: 19,
17861 max_macro_arity: 23,
17862 };
17863
17864 /// A second asymmetric posture whose per-axis values are on both
17865 /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17866 /// smaller (so the meet picks THIS posture on those axes and the
17867 /// join picks `MID` on those axes) and three axes larger (mirror).
17868 /// Together the two postures exercise every ordering combination
17869 /// on the six-axis pointwise `min`/`max` cascade.
17870 const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17871 max_expansion_depth: 3, // smaller than MID's 7
17872 max_cache_entries: 29, // larger than MID's 11
17873 max_expansion_size: 5, // smaller than MID's 13
17874 max_macro_body_size: 31, // larger than MID's 17
17875 max_registered_macros: 2, // smaller than MID's 19
17876 max_macro_arity: 41, // larger than MID's 23
17877 };
17878
17879 #[test]
17880 fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17881 // Concrete-preset pin — the meet of the (shipped default,
17882 // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17883 // structurally: every `DEFAULT_MAX_*` module constant is a
17884 // concrete positive value strictly less than [`usize::MAX`],
17885 // so on every axis the pointwise-`min` picks the DEFAULT side.
17886 // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17887 // one COMBINATOR axis over on the same shipped-preset-pair
17888 // surface.
17889 assert_eq!(
17890 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17891 DEFAULT_RESOURCE_LIMITS,
17892 );
17893 assert_eq!(
17894 UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17895 DEFAULT_RESOURCE_LIMITS,
17896 );
17897 }
17898
17899 #[test]
17900 fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17901 // Concrete-preset pin — the join of the (shipped default,
17902 // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17903 // preset structurally: every axis's pointwise-`max` picks the
17904 // [`usize::MAX`] side over any concrete default. Peer of
17905 // `strictest_of_default_and_unbounded_projects_the_default`.
17906 assert_eq!(
17907 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17908 UNBOUNDED_RESOURCE_LIMITS,
17909 );
17910 assert_eq!(
17911 UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17912 UNBOUNDED_RESOURCE_LIMITS,
17913 );
17914 }
17915
17916 #[test]
17917 fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17918 // Field-level pin — the meet operation projects the pointwise
17919 // `min` across the six-field surface. Pinned as six
17920 // independent field asserts (rather than one struct equality)
17921 // so a drift on ONE axis carries a distinct-name failure that
17922 // names the drifting axis; a future SEVENTH ceiling extension
17923 // requires an additional field assert here in lockstep.
17924 let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17925 assert_eq!(m.max_expansion_depth, 3);
17926 assert_eq!(m.max_cache_entries, 11);
17927 assert_eq!(m.max_expansion_size, 5);
17928 assert_eq!(m.max_macro_body_size, 17);
17929 assert_eq!(m.max_registered_macros, 2);
17930 assert_eq!(m.max_macro_arity, 23);
17931 }
17932
17933 #[test]
17934 fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17935 // Field-level pin — the join operation projects the pointwise
17936 // `max` across the six-field surface. Pinned as six
17937 // independent field asserts so a drift on ONE axis carries a
17938 // distinct-name failure that names the drifting axis.
17939 let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17940 assert_eq!(j.max_expansion_depth, 7);
17941 assert_eq!(j.max_cache_entries, 29);
17942 assert_eq!(j.max_expansion_size, 13);
17943 assert_eq!(j.max_macro_body_size, 31);
17944 assert_eq!(j.max_registered_macros, 19);
17945 assert_eq!(j.max_macro_arity, 41);
17946 }
17947
17948 #[test]
17949 fn resource_limits_strictest_is_idempotent() {
17950 // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17951 // its own meet with itself; a regression that changed the
17952 // per-axis primitive to something other than pointwise `min`
17953 // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17954 // equal values) fires here.
17955 assert_eq!(
17956 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17957 DEFAULT_RESOURCE_LIMITS,
17958 );
17959 assert_eq!(
17960 UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17961 UNBOUNDED_RESOURCE_LIMITS,
17962 );
17963 assert_eq!(
17964 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17965 HAND_AUTHORED_MID_POSTURE,
17966 );
17967 }
17968
17969 #[test]
17970 fn resource_limits_most_permissive_is_idempotent() {
17971 // Lattice law — `a ∨ a = a`. Sibling of the strictest
17972 // idempotence pin one COMBINATOR axis over.
17973 assert_eq!(
17974 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17975 DEFAULT_RESOURCE_LIMITS,
17976 );
17977 assert_eq!(
17978 UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17979 UNBOUNDED_RESOURCE_LIMITS,
17980 );
17981 assert_eq!(
17982 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17983 HAND_AUTHORED_MID_POSTURE,
17984 );
17985 }
17986
17987 #[test]
17988 fn resource_limits_strictest_is_commutative() {
17989 // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17990 // on every axis, so the composition inherits commutativity
17991 // structurally. A regression that broke ordering (e.g. a
17992 // per-axis "prefer left on tie") fires here.
17993 assert_eq!(
17994 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17995 HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17996 );
17997 assert_eq!(
17998 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17999 HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
18000 );
18001 }
18002
18003 #[test]
18004 fn resource_limits_most_permissive_is_commutative() {
18005 // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
18006 // commutativity pin one COMBINATOR axis over.
18007 assert_eq!(
18008 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18009 HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
18010 );
18011 assert_eq!(
18012 DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
18013 HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
18014 );
18015 }
18016
18017 #[test]
18018 fn resource_limits_strictest_is_associative() {
18019 // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
18020 // is associative on every axis, so the composition inherits
18021 // associativity structurally. A regression that broke
18022 // grouping (e.g. an accidental accumulator that skewed the
18023 // order of `min` invocations) fires here.
18024 let a = DEFAULT_RESOURCE_LIMITS;
18025 let b = HAND_AUTHORED_MID_POSTURE;
18026 let c = HAND_AUTHORED_OTHER_POSTURE;
18027 assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
18028 }
18029
18030 #[test]
18031 fn resource_limits_most_permissive_is_associative() {
18032 // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
18033 // strictest associativity pin one COMBINATOR axis over.
18034 let a = DEFAULT_RESOURCE_LIMITS;
18035 let b = HAND_AUTHORED_MID_POSTURE;
18036 let c = HAND_AUTHORED_OTHER_POSTURE;
18037 assert_eq!(
18038 a.most_permissive(b).most_permissive(c),
18039 a.most_permissive(b.most_permissive(c)),
18040 );
18041 }
18042
18043 #[test]
18044 fn resource_limits_meet_and_join_satisfy_absorption() {
18045 // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
18046 // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
18047 // and associativity they define the lattice axioms; the two
18048 // absorption laws are the operator PAIR's interlock (each
18049 // undoes the other's contribution on the shared operand).
18050 // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
18051 // AND `max(a, min(a, b)) = a` inherits this at the pointwise
18052 // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
18053 let a = HAND_AUTHORED_MID_POSTURE;
18054 let b = HAND_AUTHORED_OTHER_POSTURE;
18055 assert_eq!(a.strictest(a.most_permissive(b)), a);
18056 assert_eq!(a.most_permissive(a.strictest(b)), a);
18057 }
18058
18059 #[test]
18060 fn resource_limits_meet_distributes_over_join() {
18061 // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
18062 // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
18063 // on every axis, so the composition inherits distributivity
18064 // structurally. A regression that broke the pointwise
18065 // structure (e.g. a per-axis operation that used SUBTRACTION
18066 // rather than MIN/MAX) fires here — subtraction does NOT
18067 // distribute over addition and would break the law on the
18068 // asymmetric-posture witness pair.
18069 let a = DEFAULT_RESOURCE_LIMITS;
18070 let b = HAND_AUTHORED_MID_POSTURE;
18071 let c = HAND_AUTHORED_OTHER_POSTURE;
18072 assert_eq!(
18073 a.strictest(b.most_permissive(c)),
18074 a.strictest(b).most_permissive(a.strictest(c)),
18075 );
18076 }
18077
18078 #[test]
18079 fn resource_limits_join_distributes_over_meet() {
18080 // Distributive-lattice pin, sibling of the meet-over-join
18081 // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
18082 // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
18083 // distributivity identities hold; pin both so a regression
18084 // that broke ONE without the other cannot slip through.
18085 let a = DEFAULT_RESOURCE_LIMITS;
18086 let b = HAND_AUTHORED_MID_POSTURE;
18087 let c = HAND_AUTHORED_OTHER_POSTURE;
18088 assert_eq!(
18089 a.most_permissive(b.strictest(c)),
18090 a.most_permissive(b).strictest(a.most_permissive(c)),
18091 );
18092 }
18093
18094 #[test]
18095 fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
18096 // Meet-lower-bound pin — the meet lies at or below BOTH
18097 // operands on every axis. This is the structural GLB property
18098 // of the meet in the pointwise partial order (tighter-first):
18099 // any admissible input under the meet is admissible under
18100 // both operands, so the meet's ceiling must not exceed either
18101 // operand's ceiling on any axis.
18102 let a = HAND_AUTHORED_MID_POSTURE;
18103 let b = HAND_AUTHORED_OTHER_POSTURE;
18104 let m = a.strictest(b);
18105 assert!(m.max_expansion_depth <= a.max_expansion_depth);
18106 assert!(m.max_expansion_depth <= b.max_expansion_depth);
18107 assert!(m.max_cache_entries <= a.max_cache_entries);
18108 assert!(m.max_cache_entries <= b.max_cache_entries);
18109 assert!(m.max_expansion_size <= a.max_expansion_size);
18110 assert!(m.max_expansion_size <= b.max_expansion_size);
18111 assert!(m.max_macro_body_size <= a.max_macro_body_size);
18112 assert!(m.max_macro_body_size <= b.max_macro_body_size);
18113 assert!(m.max_registered_macros <= a.max_registered_macros);
18114 assert!(m.max_registered_macros <= b.max_registered_macros);
18115 assert!(m.max_macro_arity <= a.max_macro_arity);
18116 assert!(m.max_macro_arity <= b.max_macro_arity);
18117 }
18118
18119 #[test]
18120 fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
18121 // Join-upper-bound pin — the join lies at or above BOTH
18122 // operands on every axis. Sibling of the strictest-lower-
18123 // bound pin one COMBINATOR axis over; the structural LUB
18124 // property of the join in the pointwise partial order.
18125 let a = HAND_AUTHORED_MID_POSTURE;
18126 let b = HAND_AUTHORED_OTHER_POSTURE;
18127 let j = a.most_permissive(b);
18128 assert!(j.max_expansion_depth >= a.max_expansion_depth);
18129 assert!(j.max_expansion_depth >= b.max_expansion_depth);
18130 assert!(j.max_cache_entries >= a.max_cache_entries);
18131 assert!(j.max_cache_entries >= b.max_cache_entries);
18132 assert!(j.max_expansion_size >= a.max_expansion_size);
18133 assert!(j.max_expansion_size >= b.max_expansion_size);
18134 assert!(j.max_macro_body_size >= a.max_macro_body_size);
18135 assert!(j.max_macro_body_size >= b.max_macro_body_size);
18136 assert!(j.max_registered_macros >= a.max_registered_macros);
18137 assert!(j.max_registered_macros >= b.max_registered_macros);
18138 assert!(j.max_macro_arity >= a.max_macro_arity);
18139 assert!(j.max_macro_arity >= b.max_macro_arity);
18140 }
18141
18142 #[test]
18143 fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18144 // Const-fn pin — the meet is evaluable in const context, so a
18145 // caller can stitch a new `pub const` preset from two shipped
18146 // presets without deferring the composition to runtime. Pinned
18147 // as a `const` binding at the item level so the compiler
18148 // exercises the const-fn path structurally; the runtime
18149 // assertion is redundant for a `const` value that already
18150 // evaluated at compile time, but pins the identity as a typed
18151 // theorem alongside every OTHER `strictest` pin above.
18152 const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18153 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18154 assert_eq!(
18155 DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18156 min_usize(
18157 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18158 HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18159 ),
18160 );
18161 // Extra check: the composition MUST project the MID posture's
18162 // small value on every axis where MID is smaller than DEFAULT
18163 // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18164 // strictly less than every `DEFAULT_MAX_*` constant, so the
18165 // meet projects MID verbatim.
18166 assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18167 }
18168
18169 #[test]
18170 fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18171 // Const-fn pin sibling of the strictest const-composition pin
18172 // one COMBINATOR axis over — the join is also `const fn`, so
18173 // a caller can stitch a new `pub const` preset via the LUB
18174 // combinator at compile time.
18175 const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18176 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18177 assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18178 }
18179
18180 // ── ResourceLimits::leq — pointwise partial order ─────────────────
18181
18182 #[test]
18183 fn resource_limits_leq_is_pointwise_field_conjunction() {
18184 // Field-level pin — the leq relation projects the pointwise
18185 // `<=` conjunction across the six-field surface. `LOOSER` is
18186 // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18187 // per-axis tighten on ANY field alone flips the relation to
18188 // false. Pinned as six independent tighten-per-axis asserts so
18189 // a drift on ONE axis carries a distinct-name failure that
18190 // names the drifting axis; a future SEVENTH ceiling extension
18191 // requires an additional per-axis tighten assert here in
18192 // lockstep.
18193 const LOOSER: ResourceLimits = ResourceLimits {
18194 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18195 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18196 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18197 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18198 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18199 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18200 };
18201 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18202 assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18203
18204 // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18205 // on that axis; the leq must flip to false. Every other axis
18206 // stays at MID, so LOOSER still dominates on those axes —
18207 // isolating the single-axis violation.
18208 let exceed_depth = ResourceLimits {
18209 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18210 ..HAND_AUTHORED_MID_POSTURE
18211 };
18212 assert!(!exceed_depth.leq(LOOSER));
18213 let exceed_cache = ResourceLimits {
18214 max_cache_entries: LOOSER.max_cache_entries + 1,
18215 ..HAND_AUTHORED_MID_POSTURE
18216 };
18217 assert!(!exceed_cache.leq(LOOSER));
18218 let exceed_expansion = ResourceLimits {
18219 max_expansion_size: LOOSER.max_expansion_size + 1,
18220 ..HAND_AUTHORED_MID_POSTURE
18221 };
18222 assert!(!exceed_expansion.leq(LOOSER));
18223 let exceed_body = ResourceLimits {
18224 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18225 ..HAND_AUTHORED_MID_POSTURE
18226 };
18227 assert!(!exceed_body.leq(LOOSER));
18228 let exceed_registered = ResourceLimits {
18229 max_registered_macros: LOOSER.max_registered_macros + 1,
18230 ..HAND_AUTHORED_MID_POSTURE
18231 };
18232 assert!(!exceed_registered.leq(LOOSER));
18233 let exceed_arity = ResourceLimits {
18234 max_macro_arity: LOOSER.max_macro_arity + 1,
18235 ..HAND_AUTHORED_MID_POSTURE
18236 };
18237 assert!(!exceed_arity.leq(LOOSER));
18238 }
18239
18240 #[test]
18241 fn resource_limits_leq_is_reflexive() {
18242 // Partial-order law — `a ≤ a` for every posture. Reflexivity
18243 // is the identity axiom on any lattice's underlying partial
18244 // order; a regression that changed the per-axis primitive to
18245 // strict `<` (or that added an accidental "prefer other on
18246 // tie" branch) fires here on every shipped posture.
18247 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18248 assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18249 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18250 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18251 }
18252
18253 #[test]
18254 fn resource_limits_leq_is_antisymmetric() {
18255 // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18256 // dominance witness on a partial order pins the two operands
18257 // to structural equality. On the pointwise `<=` composition
18258 // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18259 // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18260 // is field-conjunction. Sibling to reflexivity one AXIOM axis
18261 // over on the partial-order axiom surface.
18262 //
18263 // Two distinct constructions of the SAME six-field posture
18264 // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18265 // spread of the shipped preset) — mutual leq holds; struct
18266 // equality holds; antisymmetry pin closes.
18267 let a = DEFAULT_RESOURCE_LIMITS;
18268 let b = ResourceLimits {
18269 max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18270 max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18271 max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18272 max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18273 max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18274 max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18275 };
18276 assert!(a.leq(b));
18277 assert!(b.leq(a));
18278 assert_eq!(a, b);
18279 }
18280
18281 #[test]
18282 fn resource_limits_leq_is_transitive() {
18283 // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18284 // is the composition axiom on any lattice's partial order.
18285 // Pointwise `<=` on [`usize`] is transitive per axis, so the
18286 // six-field conjunction inherits it structurally.
18287 //
18288 // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18289 // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18290 // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18291 const LOOSER: ResourceLimits = ResourceLimits {
18292 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18293 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18294 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18295 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18296 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18297 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18298 };
18299 const LOOSEST: ResourceLimits = ResourceLimits {
18300 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18301 max_cache_entries: LOOSER.max_cache_entries + 1,
18302 max_expansion_size: LOOSER.max_expansion_size + 1,
18303 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18304 max_registered_macros: LOOSER.max_registered_macros + 1,
18305 max_macro_arity: LOOSER.max_macro_arity + 1,
18306 };
18307 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18308 assert!(LOOSER.leq(LOOSEST));
18309 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18310 }
18311
18312 #[test]
18313 fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18314 // Concrete-preset pin — the leq relation on the shipped
18315 // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18316 // `DEFAULT_MAX_*` module constant is a concrete positive
18317 // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18318 // fails (every `DEFAULT_MAX_*` is strictly less than
18319 // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18320 // the matching default). Peer of
18321 // `strictest_of_default_and_unbounded_projects_the_default`
18322 // one PRIMITIVE-KIND axis over on the same shipped-preset-
18323 // pair surface: where the meet composition projects DEFAULT,
18324 // this relation asserts DEFAULT is the tighter of the two.
18325 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18326 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18327 }
18328
18329 #[test]
18330 fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18331 // Partial-order NON-total pin — the two hand-authored asymmetric
18332 // postures have MID smaller on three axes (depth, expansion,
18333 // registered) and OTHER smaller on the other three (cache,
18334 // body, arity); neither dominates the other, so BOTH
18335 // directions of the leq relation fail. The pin discriminates
18336 // a regression that promoted `leq` from partial to total
18337 // (e.g. an accidental `||` swap in the conjunction, which
18338 // would make every pair comparable).
18339 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18340 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18341 }
18342
18343 #[test]
18344 fn resource_limits_leq_agrees_with_meet() {
18345 // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18346 // classification lattice one crate over
18347 // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18348 // agreement axiom binding the partial order to the meet
18349 // combinator, and the default-impl body on
18350 // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18351 // through it. This pin discharges the same axiom on the
18352 // ResourceLimits algebra as a two-direction structural
18353 // equality across the strict-order preset pair AND the
18354 // reflexivity witness.
18355 //
18356 // Forward (a ≤ b ⇒ a ⊓ b = a):
18357 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18358 assert_eq!(
18359 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18360 DEFAULT_RESOURCE_LIMITS,
18361 );
18362 // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18363 assert_eq!(
18364 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18365 DEFAULT_RESOURCE_LIMITS,
18366 );
18367 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18368
18369 // Non-agreement witness — the incomparable asymmetric postures
18370 // have meet distinct from EITHER operand, and leq false in
18371 // both directions; agreement holds on the negative side too.
18372 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18373 assert_ne!(
18374 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18375 HAND_AUTHORED_MID_POSTURE,
18376 );
18377 }
18378
18379 #[test]
18380 fn resource_limits_leq_agrees_with_join() {
18381 // Lattice cross-check axiom, sibling of the meet-agreement
18382 // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18383 //
18384 // Forward (a ≤ b ⇒ a ⊔ b = b):
18385 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18386 assert_eq!(
18387 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18388 UNBOUNDED_RESOURCE_LIMITS,
18389 );
18390 // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18391 assert_eq!(
18392 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18393 DEFAULT_RESOURCE_LIMITS,
18394 );
18395 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18396
18397 // Non-agreement witness — the incomparable asymmetric postures
18398 // have join distinct from EITHER operand.
18399 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18400 assert_ne!(
18401 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18402 HAND_AUTHORED_OTHER_POSTURE,
18403 );
18404 }
18405
18406 #[test]
18407 fn resource_limits_strictest_is_leq_both_operands() {
18408 // Meet-lower-bound pin, typed lattice-relation companion to
18409 // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18410 // one PRIMITIVE-KIND axis over — where the per-axis pin
18411 // discharged the six independent `<=` asserts inline, this pin
18412 // routes through the typed `leq` primitive, so the
18413 // greatest-lower-bound property is stated in the LATTICE
18414 // vocabulary (meet-is-leq-both-operands) rather than the
18415 // six-fold field-conjunction vocabulary.
18416 let a = HAND_AUTHORED_MID_POSTURE;
18417 let b = HAND_AUTHORED_OTHER_POSTURE;
18418 let m = a.strictest(b);
18419 assert!(m.leq(a));
18420 assert!(m.leq(b));
18421 }
18422
18423 #[test]
18424 fn resource_limits_most_permissive_is_geq_both_operands() {
18425 // Join-upper-bound pin, typed lattice-relation companion to
18426 // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18427 // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18428 // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18429 // meet-lower-bound pin one COMBINATOR axis over.
18430 let a = HAND_AUTHORED_MID_POSTURE;
18431 let b = HAND_AUTHORED_OTHER_POSTURE;
18432 let j = a.most_permissive(b);
18433 assert!(a.leq(j));
18434 assert!(b.leq(j));
18435 }
18436
18437 #[test]
18438 fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18439 // Const-fn pin — the leq relation is evaluable in const
18440 // context, so a caller can pin a preset-relation identity at
18441 // compile time. Sibling of the const-fn composition pins on
18442 // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18443 // over: those two operations compose two presets into a
18444 // const-time third, and this operation decides a const-time
18445 // yes/no on two presets. `const _: ()` at item scope is a
18446 // compile-time proof — the compiler rejects the module if
18447 // either identity does not hold; a runtime `assert!` over the
18448 // same expression would fire at test time on the same value
18449 // clippy's `assertions_on_constants` correctly names as a
18450 // redundant late check on a compile-time constant.
18451 //
18452 // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18453 // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18454 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18455 // Reverse direction fails — [`usize::MAX`] exceeds every
18456 // concrete `DEFAULT_MAX_*` constant, so the partial order is
18457 // STRICT on the shipped preset pair:
18458 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18459 }
18460
18461 // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18462
18463 #[test]
18464 fn empty_resource_limits_binds_every_ceiling_to_zero() {
18465 // Field-level pin — the zero preset carries `0` on every axis
18466 // of the six-field surface. Pinned as six independent field
18467 // asserts (rather than one struct equality) so a drift on ONE
18468 // field carries a distinct-name failure that names the drifting
18469 // axis. A future extension that adds a SEVENTH ceiling requires
18470 // an additional field assert here in lockstep with the const
18471 // literal — rustc's field-exhaustiveness on the const forces
18472 // the new field to appear, and this pin exercises the seed
18473 // value. Structural peer of
18474 // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18475 // one LATTICE-POLE axis over on the (bottom, top)
18476 // bounded-lattice preset-pair surface.
18477 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18478 assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18479 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18480 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18481 assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18482 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18483 }
18484
18485 #[test]
18486 fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18487 // Structural-distinctness pin — the two constants
18488 // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18489 // at DISTINCT points on the preset-posture axis, and the
18490 // disagreement is EXHAUSTIVE on the six-field surface — every
18491 // shipped `DEFAULT_MAX_*` module constant is a concrete
18492 // strictly-positive value, so every axis distinguishes DEFAULT
18493 // from the all-zero bottom. Peer of
18494 // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18495 // one LATTICE-POLE axis over.
18496 assert_ne!(
18497 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18498 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18499 );
18500 assert_ne!(
18501 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18502 DEFAULT_RESOURCE_LIMITS.max_cache_entries
18503 );
18504 assert_ne!(
18505 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18506 DEFAULT_RESOURCE_LIMITS.max_expansion_size
18507 );
18508 assert_ne!(
18509 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18510 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18511 );
18512 assert_ne!(
18513 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18514 DEFAULT_RESOURCE_LIMITS.max_registered_macros
18515 );
18516 assert_ne!(
18517 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18518 DEFAULT_RESOURCE_LIMITS.max_macro_arity
18519 );
18520 }
18521
18522 #[test]
18523 fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18524 // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18525 // disagrees on every axis (`0 != usize::MAX` on every field).
18526 // Structural companion to the DEFAULT-vs-EMPTY and
18527 // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18528 // over — together the three pin the six-field surface at the
18529 // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18530 assert_ne!(
18531 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18532 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18533 );
18534 assert_ne!(
18535 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18536 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18537 );
18538 assert_ne!(
18539 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18540 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18541 );
18542 assert_ne!(
18543 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18544 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18545 );
18546 assert_ne!(
18547 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18548 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18549 );
18550 assert_ne!(
18551 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18552 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18553 );
18554 }
18555
18556 #[test]
18557 fn empty_resource_limits_is_the_join_identity() {
18558 // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18559 // pointwise `max` against 0 returns the posture verbatim, so
18560 // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18561 // the join monoid. Peer of
18562 // `empty_resource_limits_is_the_meet_annihilator` one
18563 // COMBINATOR axis over on the (meet, join) surface.
18564 //
18565 // Cross-preset exhaustion — verify the identity against every
18566 // named preset (DEFAULT, UNBOUNDED, both hand-authored
18567 // asymmetric postures) so a regression that broke ONE axis of
18568 // the algebra fires here on the operand whose field crosses
18569 // that axis's boundary.
18570 assert_eq!(
18571 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18572 DEFAULT_RESOURCE_LIMITS,
18573 );
18574 assert_eq!(
18575 EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18576 DEFAULT_RESOURCE_LIMITS,
18577 );
18578 assert_eq!(
18579 UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18580 UNBOUNDED_RESOURCE_LIMITS,
18581 );
18582 assert_eq!(
18583 EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18584 UNBOUNDED_RESOURCE_LIMITS,
18585 );
18586 assert_eq!(
18587 HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18588 HAND_AUTHORED_MID_POSTURE,
18589 );
18590 assert_eq!(
18591 HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18592 HAND_AUTHORED_OTHER_POSTURE,
18593 );
18594 // Idempotence at the identity itself — sibling witness of the
18595 // `most_permissive_is_idempotent` general pin one PRESET axis
18596 // over.
18597 assert_eq!(
18598 EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18599 EMPTY_RESOURCE_LIMITS,
18600 );
18601 }
18602
18603 #[test]
18604 fn empty_resource_limits_is_the_meet_annihilator() {
18605 // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18606 // pointwise `min` against 0 returns 0 on every axis, so
18607 // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18608 // element) of the meet operation. Peer of
18609 // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18610 // axis over — the identity on one side of the bounded lattice
18611 // is the annihilator on the other side.
18612 assert_eq!(
18613 DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18614 EMPTY_RESOURCE_LIMITS,
18615 );
18616 assert_eq!(
18617 EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18618 EMPTY_RESOURCE_LIMITS,
18619 );
18620 assert_eq!(
18621 UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18622 EMPTY_RESOURCE_LIMITS,
18623 );
18624 assert_eq!(
18625 EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18626 EMPTY_RESOURCE_LIMITS,
18627 );
18628 assert_eq!(
18629 HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18630 EMPTY_RESOURCE_LIMITS,
18631 );
18632 assert_eq!(
18633 HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18634 EMPTY_RESOURCE_LIMITS,
18635 );
18636 }
18637
18638 #[test]
18639 fn empty_resource_limits_is_the_lattice_minimum() {
18640 // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18641 // posture's field is a `usize` and `0 <= x` holds for every
18642 // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18643 // every posture on every axis of the pointwise `<=` conjunction.
18644 // Peer of the UNBOUNDED-is-maximum property on the
18645 // partial-order face one LATTICE-POLE axis over — those two
18646 // pin the (min, max) bounds of the bounded lattice.
18647 assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18648 assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18649 assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18650 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18651 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18652 // Reverse direction FAILS on every non-EMPTY preset (each has
18653 // at least one field strictly greater than 0). The partial
18654 // order is STRICT between EMPTY and every other named preset,
18655 // structurally exhausting the diagonal of the
18656 // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18657 // face at the BOTTOM row.
18658 assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18659 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18660 assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18661 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18662 }
18663
18664 #[test]
18665 fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18666 // Const-fn pin — the bounded-lattice identity + annihilator +
18667 // minimum laws hold at COMPILE time. Sibling of
18668 // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18669 // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18670 // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18671 // time; this pin fixes the bounded-lattice pole role of the
18672 // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18673 // is a compile-time proof — a regression that broke either
18674 // identity element or the bottom-is-minimum invariant fires at
18675 // rustc time, not test-run time.
18676 //
18677 // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18678 // three-preset ordering chain on the shipped preset diagonal).
18679 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18680 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18681 // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18682 // below EMPTY (each has a strictly-positive field).
18683 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18684 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18685 // Join-identity + meet-annihilator laws cross-pinned through
18686 // the `leq` primitive at const time — the round-trip identity
18687 // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18688 // leq asserts above discharge both algebraic laws by the
18689 // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18690 // cross-check axiom already pinned in this cohort.
18691 }
18692
18693 #[test]
18694 fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18695 // Fold-identity behavioral pin — a slice of postures folded
18696 // through [`ResourceLimits::most_permissive`] with EMPTY as
18697 // the seed produces the pointwise least upper bound across
18698 // the slice on every axis. The join-identity property
18699 // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18700 // contributes nothing to the fold's result — every field's
18701 // pointwise `max` against the seed's `0` returns the field's
18702 // value verbatim, so the fold reduces to the joined-across-
18703 // -operands upper bound. This is the destination usage the
18704 // constant's docstring names as its fold-identity role.
18705 //
18706 // A regression that seeded from ANY non-empty posture (e.g.
18707 // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18708 // on every axis where the seed's field exceeds the pointwise
18709 // max of the slice — fires as a distinct-axis inequality here.
18710 let postures: [ResourceLimits; 3] = [
18711 HAND_AUTHORED_MID_POSTURE,
18712 HAND_AUTHORED_OTHER_POSTURE,
18713 DEFAULT_RESOURCE_LIMITS,
18714 ];
18715 let joined = postures
18716 .iter()
18717 .copied()
18718 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18719 // Every operand is `leq` the joined result — the least
18720 // upper bound dominates each contributor on every axis.
18721 assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18722 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18723 assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18724 // Field-level pin — each axis is the pointwise `max` of the
18725 // three sources' values. Six independent field asserts so a
18726 // drift on ONE axis carries a distinct-name failure.
18727 assert_eq!(
18728 joined.max_expansion_depth,
18729 HAND_AUTHORED_MID_POSTURE
18730 .max_expansion_depth
18731 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18732 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18733 );
18734 assert_eq!(
18735 joined.max_cache_entries,
18736 HAND_AUTHORED_MID_POSTURE
18737 .max_cache_entries
18738 .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18739 .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18740 );
18741 assert_eq!(
18742 joined.max_expansion_size,
18743 HAND_AUTHORED_MID_POSTURE
18744 .max_expansion_size
18745 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18746 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18747 );
18748 assert_eq!(
18749 joined.max_macro_body_size,
18750 HAND_AUTHORED_MID_POSTURE
18751 .max_macro_body_size
18752 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18753 .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18754 );
18755 assert_eq!(
18756 joined.max_registered_macros,
18757 HAND_AUTHORED_MID_POSTURE
18758 .max_registered_macros
18759 .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18760 .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18761 );
18762 assert_eq!(
18763 joined.max_macro_arity,
18764 HAND_AUTHORED_MID_POSTURE
18765 .max_macro_arity
18766 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18767 .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18768 );
18769 // Empty-slice edge case — folding over an EMPTY slice returns
18770 // the seed verbatim (the identity element as the null aggregate).
18771 // A wrapper-type approach (`Option<ResourceLimits>` for the
18772 // aggregate) would need a distinct absent branch here; the
18773 // typed identity-element approach resolves it structurally.
18774 let empty_slice: [ResourceLimits; 0] = [];
18775 assert_eq!(
18776 empty_slice
18777 .iter()
18778 .copied()
18779 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18780 EMPTY_RESOURCE_LIMITS,
18781 );
18782 }
18783
18784 #[test]
18785 fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18786 // Dual fold-identity pin — a slice of postures folded through
18787 // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18788 // produces the pointwise greatest lower bound across the slice
18789 // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18790 // pin one COMBINATOR axis over — the identity element for
18791 // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18792 // identity element for join is the meet's annihilator
18793 // (`EMPTY`); the two together bind the bounded-lattice's
18794 // aggregation surface at BOTH poles.
18795 let postures: [ResourceLimits; 3] = [
18796 HAND_AUTHORED_MID_POSTURE,
18797 HAND_AUTHORED_OTHER_POSTURE,
18798 DEFAULT_RESOURCE_LIMITS,
18799 ];
18800 let met = postures
18801 .iter()
18802 .copied()
18803 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18804 // The greatest lower bound sits `leq` every operand.
18805 assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18806 assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18807 assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18808 // Field-level pin — each axis is the pointwise `min` of the
18809 // three sources' values (the UNBOUNDED seed's `usize::MAX`
18810 // never wins the min against a concrete positive value).
18811 assert_eq!(
18812 met.max_expansion_depth,
18813 HAND_AUTHORED_MID_POSTURE
18814 .max_expansion_depth
18815 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18816 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18817 );
18818 assert_eq!(
18819 met.max_cache_entries,
18820 HAND_AUTHORED_MID_POSTURE
18821 .max_cache_entries
18822 .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18823 .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18824 );
18825 assert_eq!(
18826 met.max_expansion_size,
18827 HAND_AUTHORED_MID_POSTURE
18828 .max_expansion_size
18829 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18830 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18831 );
18832 assert_eq!(
18833 met.max_macro_body_size,
18834 HAND_AUTHORED_MID_POSTURE
18835 .max_macro_body_size
18836 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18837 .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18838 );
18839 assert_eq!(
18840 met.max_registered_macros,
18841 HAND_AUTHORED_MID_POSTURE
18842 .max_registered_macros
18843 .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18844 .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18845 );
18846 assert_eq!(
18847 met.max_macro_arity,
18848 HAND_AUTHORED_MID_POSTURE
18849 .max_macro_arity
18850 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18851 .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18852 );
18853 // Empty-slice edge — folding over an empty slice returns the
18854 // UNBOUNDED seed verbatim (the meet-identity as the null
18855 // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18856 let empty_slice: [ResourceLimits; 0] = [];
18857 assert_eq!(
18858 empty_slice
18859 .iter()
18860 .copied()
18861 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18862 UNBOUNDED_RESOURCE_LIMITS,
18863 );
18864 }
18865
18866 #[test]
18867 fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18868 // Null-aggregate pin — the N-ary meet over an empty slice is
18869 // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18870 // typed identity-element approach resolves the empty-input case
18871 // at the algebra layer; a wrapper-type approach
18872 // (`Option<ResourceLimits>` for the aggregate) would need a
18873 // distinct absent branch here, and a panic-on-empty seed-from-
18874 // -first-element approach would abort the caller.
18875 assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18876 }
18877
18878 #[test]
18879 fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18880 // Null-aggregate dual pin — the N-ary join over an empty slice
18881 // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18882 // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18883 // (N-ary meet, N-ary join) surface — the identity element for
18884 // join is the meet's ANNIHILATOR and vice versa; the two
18885 // together bind both bounded-lattice poles as null aggregates.
18886 assert_eq!(
18887 ResourceLimits::most_permissive_of(&[]),
18888 EMPTY_RESOURCE_LIMITS,
18889 );
18890 }
18891
18892 #[test]
18893 fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18894 // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18895 // input is the input verbatim, since the meet-identity seed
18896 // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18897 // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18898 // against [`usize::MAX`]. This is the identity-law-at-N=1
18899 // pin: the fold's seed does not distort the 1-input case.
18900 assert_eq!(
18901 ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18902 HAND_AUTHORED_MID_POSTURE,
18903 );
18904 assert_eq!(
18905 ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18906 HAND_AUTHORED_OTHER_POSTURE,
18907 );
18908 assert_eq!(
18909 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18910 DEFAULT_RESOURCE_LIMITS,
18911 );
18912 }
18913
18914 #[test]
18915 fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18916 // Join-identity absorption pin at N=1 — dual of
18917 // `strictest_of_single_element`, using the join-identity seed
18918 // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18919 // `EMPTY.most_permissive(a) == a` against any non-zero value.
18920 assert_eq!(
18921 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18922 HAND_AUTHORED_MID_POSTURE,
18923 );
18924 assert_eq!(
18925 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18926 HAND_AUTHORED_OTHER_POSTURE,
18927 );
18928 assert_eq!(
18929 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18930 DEFAULT_RESOURCE_LIMITS,
18931 );
18932 }
18933
18934 #[test]
18935 fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18936 // N=2 reduction pin — the N-ary meet on a 2-element slice
18937 // reduces to the pairwise combinator, since
18938 // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18939 // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18940 // every input pair the algebra distinguishes.
18941 assert_eq!(
18942 ResourceLimits::strictest_of(
18943 &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18944 ),
18945 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18946 );
18947 assert_eq!(
18948 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18949 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18950 );
18951 }
18952
18953 #[test]
18954 fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18955 // N=2 reduction dual pin — the N-ary join on a 2-element slice
18956 // reduces to the pairwise combinator.
18957 assert_eq!(
18958 ResourceLimits::most_permissive_of(&[
18959 HAND_AUTHORED_MID_POSTURE,
18960 HAND_AUTHORED_OTHER_POSTURE,
18961 ]),
18962 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18963 );
18964 assert_eq!(
18965 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18966 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18967 );
18968 }
18969
18970 #[test]
18971 fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18972 // Method-agrees-with-fold pin — the sole behavioral claim the
18973 // N-ary aggregator makes is that it IS the inline
18974 // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18975 // pre-lift caller composed at every consumption site. A drift
18976 // between the method's implementation and the inline fold
18977 // would break this equality on every non-empty slice.
18978 let postures: [ResourceLimits; 3] = [
18979 HAND_AUTHORED_MID_POSTURE,
18980 HAND_AUTHORED_OTHER_POSTURE,
18981 DEFAULT_RESOURCE_LIMITS,
18982 ];
18983 assert_eq!(
18984 ResourceLimits::strictest_of(&postures),
18985 postures
18986 .iter()
18987 .copied()
18988 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18989 );
18990 }
18991
18992 #[test]
18993 fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18994 // Method-agrees-with-fold dual pin.
18995 let postures: [ResourceLimits; 3] = [
18996 HAND_AUTHORED_MID_POSTURE,
18997 HAND_AUTHORED_OTHER_POSTURE,
18998 DEFAULT_RESOURCE_LIMITS,
18999 ];
19000 assert_eq!(
19001 ResourceLimits::most_permissive_of(&postures),
19002 postures
19003 .iter()
19004 .copied()
19005 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
19006 );
19007 }
19008
19009 #[test]
19010 fn resource_limits_strictest_of_is_order_independent() {
19011 // Order-independence pin — the fold inherits commutativity AND
19012 // associativity of the pairwise `strictest`, so any permutation
19013 // of the input slice yields the same aggregate. Reversal is
19014 // the maximal permutation for a 3-element input (indices
19015 // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
19016 // discriminate here.
19017 let forward = ResourceLimits::strictest_of(&[
19018 HAND_AUTHORED_MID_POSTURE,
19019 HAND_AUTHORED_OTHER_POSTURE,
19020 DEFAULT_RESOURCE_LIMITS,
19021 ]);
19022 let reversed = ResourceLimits::strictest_of(&[
19023 DEFAULT_RESOURCE_LIMITS,
19024 HAND_AUTHORED_OTHER_POSTURE,
19025 HAND_AUTHORED_MID_POSTURE,
19026 ]);
19027 assert_eq!(forward, reversed);
19028 }
19029
19030 #[test]
19031 fn resource_limits_most_permissive_of_is_order_independent() {
19032 let forward = ResourceLimits::most_permissive_of(&[
19033 HAND_AUTHORED_MID_POSTURE,
19034 HAND_AUTHORED_OTHER_POSTURE,
19035 DEFAULT_RESOURCE_LIMITS,
19036 ]);
19037 let reversed = ResourceLimits::most_permissive_of(&[
19038 DEFAULT_RESOURCE_LIMITS,
19039 HAND_AUTHORED_OTHER_POSTURE,
19040 HAND_AUTHORED_MID_POSTURE,
19041 ]);
19042 assert_eq!(forward, reversed);
19043 }
19044
19045 #[test]
19046 fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
19047 // Compile-time-composition pin — `strictest_of` is `const fn`,
19048 // so a caller binds an N-ary aggregate to a `pub const` and the
19049 // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
19050 // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
19051 // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
19052 // identity already pinned pairwise, verified here through the
19053 // N-ary method's const-context evaluation. The N-ary const-fn
19054 // peer of the pairwise const-fn pins on
19055 // [`ResourceLimits::strictest`]; a regression that turned
19056 // `strictest_of` into a runtime `fn` would break the `const`
19057 // binding below at compile time.
19058 const AGGREGATED: ResourceLimits =
19059 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
19060 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19061 }
19062
19063 #[test]
19064 fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
19065 // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
19066 // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
19067 // picks DEFAULT on every axis.
19068 const AGGREGATED: ResourceLimits =
19069 ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
19070 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19071 }
19072
19073 #[test]
19074 fn resource_limits_strictest_of_result_is_leq_every_operand() {
19075 // Lattice-relation pin — the N-ary meet sits `leq` every
19076 // operand. This is the N-ary extension of the pairwise
19077 // `strictest_is_leq_both_operands` pin; the meet aggregate
19078 // dominates from below (the greatest lower bound of the slice
19079 // sits at-or-below each input on every axis).
19080 let postures: [ResourceLimits; 3] = [
19081 HAND_AUTHORED_MID_POSTURE,
19082 HAND_AUTHORED_OTHER_POSTURE,
19083 DEFAULT_RESOURCE_LIMITS,
19084 ];
19085 let met = ResourceLimits::strictest_of(&postures);
19086 for p in postures {
19087 assert!(met.leq(p));
19088 }
19089 }
19090
19091 #[test]
19092 fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
19093 // Lattice-relation dual pin — every operand sits `leq` the
19094 // N-ary join. The join aggregate is the least upper bound of
19095 // the slice.
19096 let postures: [ResourceLimits; 3] = [
19097 HAND_AUTHORED_MID_POSTURE,
19098 HAND_AUTHORED_OTHER_POSTURE,
19099 DEFAULT_RESOURCE_LIMITS,
19100 ];
19101 let joined = ResourceLimits::most_permissive_of(&postures);
19102 for p in postures {
19103 assert!(p.leq(joined));
19104 }
19105 }
19106
19107 #[test]
19108 fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
19109 // Behavioral pin — an expander constructed from the N-ary meet
19110 // of a slice of postures MUST gate at the tightest ceiling
19111 // across the slice on every axis. Peer of
19112 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19113 // one AGGREGATION-ARITY axis over: where the pairwise pin
19114 // composes exactly two postures, this pin composes THREE
19115 // through the N-ary aggregator, confirming the arity extension
19116 // preserves the "meet gates at tightest" behavioral contract.
19117 //
19118 // The slice tightens macro_arity to 1 (the tightest across the
19119 // three postures) via the middle operand; every other axis
19120 // stays at usize::MAX (the outer two operands supply
19121 // usize::MAX; the middle operand's struct-update literal
19122 // preserves usize::MAX everywhere except macro_arity).
19123 let tightened = ResourceLimits::strictest_of(&[
19124 UNBOUNDED_RESOURCE_LIMITS,
19125 ResourceLimits {
19126 max_macro_arity: 1,
19127 ..UNBOUNDED_RESOURCE_LIMITS
19128 },
19129 UNBOUNDED_RESOURCE_LIMITS,
19130 ]);
19131 assert_eq!(tightened.max_macro_arity, 1);
19132 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19133 assert_eq!(tightened.max_cache_entries, usize::MAX);
19134 assert_eq!(tightened.max_expansion_size, usize::MAX);
19135 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19136 assert_eq!(tightened.max_registered_macros, usize::MAX);
19137 let mut e = Expander::with_limits(tightened);
19138 let forms = read("(defmacro two (a b) `,a)").unwrap();
19139 let err = e.expand_program(forms).unwrap_err();
19140 assert!(
19141 matches!(
19142 err,
19143 LispError::MacroArityExceeded {
19144 arity: 2,
19145 limit: 1,
19146 ..
19147 }
19148 ),
19149 "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19150 );
19151 }
19152
19153 #[test]
19154 fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19155 // Behavioral pin — an expander constructed from the meet of
19156 // two postures MUST gate at the tighter of the two ceilings
19157 // on every axis. Build the meet of the ceiling-lifted preset
19158 // and a hand-authored posture whose `max_macro_arity` sits at
19159 // 1; the meet inherits the 1-slot arity ceiling (tighter than
19160 // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19161 // other axis (tighter than the hand-authored posture's small
19162 // values — no, wait: the OTHER posture's small values ARE
19163 // tighter. So the meet inherits 1 on arity and the OTHER
19164 // small values elsewhere.) Confirm the constructed expander
19165 // rejects a 2-arg macro registration with the arity gate.
19166 let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19167 max_macro_arity: 1,
19168 ..UNBOUNDED_RESOURCE_LIMITS
19169 });
19170 assert_eq!(tightened.max_macro_arity, 1);
19171 // Other five axes remain at `usize::MAX` (both operands carry
19172 // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19173 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19174 assert_eq!(tightened.max_cache_entries, usize::MAX);
19175 assert_eq!(tightened.max_expansion_size, usize::MAX);
19176 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19177 assert_eq!(tightened.max_registered_macros, usize::MAX);
19178 let mut e = Expander::with_limits(tightened);
19179 let forms = read("(defmacro two (a b) `,a)").unwrap();
19180 let err = e.expand_program(forms).unwrap_err();
19181 assert!(
19182 matches!(
19183 err,
19184 LispError::MacroArityExceeded {
19185 arity: 2,
19186 limit: 1,
19187 ..
19188 }
19189 ),
19190 "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19191 );
19192 }
19193
19194 // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19195
19196 /// A tighter-than-MID lower-bracket witness — every axis strictly
19197 /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19198 /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19199 /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19200 /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19201 /// pair.
19202 const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19203 max_expansion_depth: 2,
19204 max_cache_entries: 3,
19205 max_expansion_size: 5,
19206 max_macro_body_size: 7,
19207 max_registered_macros: 11,
19208 max_macro_arity: 13,
19209 };
19210
19211 /// A looser-than-MID upper-bracket witness — every axis strictly larger
19212 /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19213 /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19214 /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19215 /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19216 /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19217 /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19218 /// CEILING`.
19219 const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19220 max_expansion_depth: 47,
19221 max_cache_entries: 53,
19222 max_expansion_size: 59,
19223 max_macro_body_size: 61,
19224 max_registered_macros: 67,
19225 max_macro_arity: 71,
19226 };
19227
19228 #[test]
19229 fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19230 // In-range identity — when the input already sits within the
19231 // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19232 // axis), the clamp is the identity function: the pointwise
19233 // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19234 // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19235 // per-axis picks `a`). The primary invariant on the "input
19236 // already satisfies the bracket" arm of the three-arm bracket-
19237 // membership surface.
19238 //
19239 // Prerequisite pins — the two hand-authored bounds bracket
19240 // `MID` strictly on every axis (structural check that discharges
19241 // the "already in range" premise). A regression that inflated
19242 // `FLOOR` or deflated `CEILING` past `MID` would fail these
19243 // asserts before the clamp identity was tested, making the
19244 // premise's failure name itself rather than showing up as a
19245 // downstream misleading clamp mismatch.
19246 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19247 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19248 let clamped =
19249 HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19250 assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19251 }
19252
19253 #[test]
19254 fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19255 // Below-lower floor — when the input is tighter than the
19256 // bracket floor on every axis (`a.leq(lower)` per-axis), the
19257 // clamp lifts the input up to `lower`: the pointwise `max(a,
19258 // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19259 // then the pointwise `min(lower, upper) == lower` (since
19260 // `lower ≤ upper` per-axis picks `lower`). Peer of
19261 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19262 // one BRACKET-POSITION axis over on the (in-range, below-lower,
19263 // above-upper) three-arm bracket-membership surface.
19264 //
19265 // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19266 // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19267 // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19268 // MUST return `FLOOR`.
19269 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19270 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19271 let clamped =
19272 EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19273 assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19274 }
19275
19276 #[test]
19277 fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19278 // Above-upper ceiling — when the input is looser than the
19279 // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19280 // clamp lowers the input down to `upper`: the pointwise
19281 // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19282 // `a`), then the pointwise `min(a, upper) == upper` (since
19283 // `upper ≤ a` per-axis picks `upper`). Peer of the
19284 // below-lower arm one BRACKET-POSITION axis over on the three-
19285 // arm bracket-membership surface.
19286 //
19287 // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19288 // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19289 // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19290 // MUST return `CEILING`.
19291 assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19292 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19293 let clamped =
19294 UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19295 assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19296 }
19297
19298 #[test]
19299 fn resource_limits_clamp_result_sits_within_the_bracket() {
19300 // Bracket-membership contract — the DEFINING invariant on the
19301 // clamp primitive: for every well-formed bracket
19302 // (`lower.leq(upper)`) and every input `a`, the result sits
19303 // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19304 // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19305 // positions of the bracket-membership surface via the four
19306 // known-preset inputs (below-lower → in-range → above-upper
19307 // → asymmetric-crossing) so a regression that inflated the
19308 // ceiling arm or deflated the floor arm exhausts all four
19309 // input positions and names the failing arm.
19310 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19311 let inputs = [
19312 EMPTY_RESOURCE_LIMITS, // below-lower
19313 HAND_AUTHORED_MID_POSTURE, // in-range
19314 UNBOUNDED_RESOURCE_LIMITS, // above-upper
19315 HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19316 ];
19317 for a in inputs {
19318 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19319 assert!(
19320 HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19321 "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19322 );
19323 assert!(
19324 clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19325 "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19326 );
19327 }
19328 }
19329
19330 #[test]
19331 fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19332 // Extrema-bracket identity — clamping against the bounded-
19333 // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19334 // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19335 // identity function on every posture: every posture already
19336 // sits within the widest possible bracket. Cross-checks the
19337 // clamp primitive against the bounded-lattice identity elements
19338 // previously lifted; a regression that swapped the two extrema
19339 // in the impl would collapse every input to
19340 // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19341 // every non-EMPTY input.
19342 for a in [
19343 DEFAULT_RESOURCE_LIMITS,
19344 HAND_AUTHORED_MID_POSTURE,
19345 HAND_AUTHORED_OTHER_POSTURE,
19346 EMPTY_RESOURCE_LIMITS,
19347 UNBOUNDED_RESOURCE_LIMITS,
19348 ] {
19349 assert_eq!(
19350 a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19351 a,
19352 "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19353 );
19354 }
19355 }
19356
19357 #[test]
19358 fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19359 // Degenerate-bracket collapse — a zero-width bracket
19360 // (`lower == upper`) collapses every input to the single point.
19361 // The pointwise `max(a, x)` lifts every axis to at least `x`,
19362 // then the pointwise `min(., x)` clamps every axis to exactly
19363 // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19364 // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19365 // == x` when `x.leq(a)`).
19366 for x in [
19367 HAND_AUTHORED_MID_POSTURE,
19368 HAND_AUTHORED_OTHER_POSTURE,
19369 DEFAULT_RESOURCE_LIMITS,
19370 ] {
19371 for a in [
19372 EMPTY_RESOURCE_LIMITS,
19373 HAND_AUTHORED_MID_POSTURE,
19374 HAND_AUTHORED_OTHER_POSTURE,
19375 UNBOUNDED_RESOURCE_LIMITS,
19376 ] {
19377 assert_eq!(
19378 a.clamp(x, x),
19379 x,
19380 "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19381 );
19382 }
19383 }
19384 }
19385
19386 #[test]
19387 fn resource_limits_clamp_is_idempotent() {
19388 // Idempotence — re-applying the same bracket to an already-
19389 // clamped posture returns the same result. The clamp primitive
19390 // is idempotent because its output already sits within the
19391 // `[lower, upper]` bracket (pinned by
19392 // `resource_limits_clamp_result_sits_within_the_bracket`), so
19393 // the second application hits the in-range identity arm. Peer
19394 // of the idempotence pins on `strictest` and `most_permissive`
19395 // one ARITY axis over (2-input primitive → 3-input bracket) on
19396 // the lattice-algebra idempotence-law surface.
19397 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19398 for a in [
19399 EMPTY_RESOURCE_LIMITS,
19400 HAND_AUTHORED_MID_POSTURE,
19401 HAND_AUTHORED_OTHER_POSTURE,
19402 UNBOUNDED_RESOURCE_LIMITS,
19403 ] {
19404 let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19405 let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19406 assert_eq!(
19407 once, twice,
19408 "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19409 );
19410 }
19411 }
19412
19413 #[test]
19414 fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19415 // Const-fn pin — the bracket combinator is evaluable in const
19416 // context, so a caller can pre-clamp a shipped preset against
19417 // an operator-supplied policy bracket at compile time. Sibling
19418 // of the const-fn composition pins on `strictest` /
19419 // `most_permissive` / `strictest_of` / `most_permissive_of`
19420 // one PRIMITIVE-KIND axis over: those four combinators compose
19421 // presets at const time, and this bracket combinator projects
19422 // a preset into a const-time bracket.
19423 //
19424 // A regression to a runtime `fn` here would fail the `pub
19425 // const` binding below at compile time — a stronger guarantee
19426 // than a runtime `assert!` because the const binding is
19427 // evaluated once at compile time, not per test invocation.
19428 const CLAMPED_DEFAULT: ResourceLimits =
19429 DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19430 assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19431
19432 // Compile-time bracket-membership pin — const `assert!` on the
19433 // lattice-relation contract discharges the "clamp result sits
19434 // within the bracket" invariant at compile time on the
19435 // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19436 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19437 const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19438 }
19439
19440 #[test]
19441 fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19442 // Structural-equivalence pin — the sole behavioral claim the
19443 // clamp primitive makes is that it IS the
19444 // `self.most_permissive(lower).strictest(upper)` two-step
19445 // cascade the pre-lift caller composed. A drift between the
19446 // method and the inline cascade would break this equality on
19447 // every input; the pin exhausts the four bracket-position arms
19448 // of the input surface.
19449 for a in [
19450 EMPTY_RESOURCE_LIMITS,
19451 HAND_AUTHORED_MID_POSTURE,
19452 HAND_AUTHORED_OTHER_POSTURE,
19453 UNBOUNDED_RESOURCE_LIMITS,
19454 ] {
19455 let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19456 let via_cascade = a
19457 .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19458 .strictest(HAND_AUTHORED_CLAMP_CEILING);
19459 assert_eq!(via_method, via_cascade);
19460 }
19461 }
19462
19463 #[test]
19464 fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19465 // Behavioral pin — an expander constructed from a preset
19466 // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19467 // bracket ceiling's arity axis. Confirms the clamp primitive
19468 // reaches through to the runtime gates that consume
19469 // `ResourceLimits`, so a future consumer that pre-clamps a
19470 // preset against a policy bracket inherits the bracket's
19471 // ceilings at every guard. Peer of
19472 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19473 // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19474 // N-ary-meet, bracket) combinator-arity face.
19475 //
19476 // The bracket ceiling tightens `max_macro_arity` to 1; every
19477 // other axis stays at `usize::MAX` (the CEILING preset carries
19478 // `usize::MAX` on those, and the input UNBOUNDED preset also
19479 // carries `usize::MAX` on every axis — the clamp's `strictest`
19480 // arm picks `usize::MAX` from either side).
19481 let bracket_ceiling = ResourceLimits {
19482 max_macro_arity: 1,
19483 ..UNBOUNDED_RESOURCE_LIMITS
19484 };
19485 let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19486 assert_eq!(clamped.max_macro_arity, 1);
19487 assert_eq!(clamped.max_expansion_depth, usize::MAX);
19488 assert_eq!(clamped.max_cache_entries, usize::MAX);
19489 assert_eq!(clamped.max_expansion_size, usize::MAX);
19490 assert_eq!(clamped.max_macro_body_size, usize::MAX);
19491 assert_eq!(clamped.max_registered_macros, usize::MAX);
19492 let mut e = Expander::with_limits(clamped);
19493 let forms = read("(defmacro two (a b) `,a)").unwrap();
19494 let err = e.expand_program(forms).unwrap_err();
19495 assert!(
19496 matches!(
19497 err,
19498 LispError::MacroArityExceeded {
19499 arity: 2,
19500 limit: 1,
19501 ..
19502 }
19503 ),
19504 "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19505 );
19506 }
19507
19508 // ── ResourceLimits::within — bracket-membership predicate ─────────
19509
19510 #[test]
19511 fn resource_limits_within_of_posture_in_range_is_true() {
19512 // In-range identity — when the input already sits within the
19513 // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19514 // a.leq(upper)`), the predicate returns `true`. Peer of
19515 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19516 // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19517 // face of the bracket-primitive surface.
19518 //
19519 // Prerequisite pins — the two hand-authored bounds bracket
19520 // `MID` strictly on every axis, structurally discharging the
19521 // "already in range" premise.
19522 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19523 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19524 assert!(HAND_AUTHORED_MID_POSTURE
19525 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19526 }
19527
19528 #[test]
19529 fn resource_limits_within_of_posture_below_lower_is_false() {
19530 // Below-lower rejection — when the input is strictly tighter
19531 // than the bracket floor on some axis (`!lower.leq(a)`), the
19532 // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19533 // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19534 // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19535 // making the conjunction reject regardless of `EMPTY`'s relation
19536 // to `CEILING`. Peer of
19537 // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19538 // one PRIMITIVE-KIND axis over.
19539 assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19540 assert!(
19541 !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19542 );
19543 }
19544
19545 #[test]
19546 fn resource_limits_within_of_posture_above_upper_is_false() {
19547 // Above-upper rejection — when the input is strictly looser
19548 // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19549 // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19550 // every axis at `usize::MAX`, strictly above
19551 // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19552 // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19553 // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19554 // one PRIMITIVE-KIND axis over.
19555 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19556 assert!(!UNBOUNDED_RESOURCE_LIMITS
19557 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19558 }
19559
19560 #[test]
19561 fn resource_limits_within_with_lattice_extrema_is_true() {
19562 // Extrema-bracket identity — every posture sits within the
19563 // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19564 // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19565 // against the bounded-lattice identity elements (`EMPTY` is
19566 // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19567 // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19568 // one PRIMITIVE-KIND axis over — the predicate view of the
19569 // "clamp is the identity" property.
19570 for a in [
19571 DEFAULT_RESOURCE_LIMITS,
19572 HAND_AUTHORED_MID_POSTURE,
19573 HAND_AUTHORED_OTHER_POSTURE,
19574 EMPTY_RESOURCE_LIMITS,
19575 UNBOUNDED_RESOURCE_LIMITS,
19576 ] {
19577 assert!(
19578 a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19579 "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19580 );
19581 }
19582 }
19583
19584 #[test]
19585 fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19586 // Degenerate-bracket collapse — a zero-width bracket admits
19587 // only the single point at the bounds. `a.within(x, x)` holds
19588 // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19589 // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19590 // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19591 // unconditionally, and the predicate distinguishes the "was
19592 // already equal" case from the "would have been clamped" case.
19593 let bounds = [
19594 HAND_AUTHORED_MID_POSTURE,
19595 HAND_AUTHORED_OTHER_POSTURE,
19596 DEFAULT_RESOURCE_LIMITS,
19597 EMPTY_RESOURCE_LIMITS,
19598 UNBOUNDED_RESOURCE_LIMITS,
19599 ];
19600 for x in bounds {
19601 for a in bounds {
19602 assert_eq!(
19603 a.within(x, x),
19604 a == x,
19605 "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19606 );
19607 }
19608 }
19609 }
19610
19611 #[test]
19612 fn resource_limits_within_of_self_is_reflexive() {
19613 // Reflexive-bracket identity — every posture sits within the
19614 // zero-width bracket at itself, since `a.leq(a)` holds by
19615 // reflexivity of `leq`. The reflexive-bracket peer of the
19616 // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19617 // ARITY axis over (pairwise → three-input).
19618 for a in [
19619 EMPTY_RESOURCE_LIMITS,
19620 DEFAULT_RESOURCE_LIMITS,
19621 HAND_AUTHORED_MID_POSTURE,
19622 HAND_AUTHORED_OTHER_POSTURE,
19623 UNBOUNDED_RESOURCE_LIMITS,
19624 ] {
19625 assert!(
19626 a.within(a, a),
19627 "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19628 );
19629 }
19630 }
19631
19632 #[test]
19633 fn resource_limits_within_agrees_with_clamp_fixed_point() {
19634 // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19635 // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19636 // binding the `within` predicate to the `clamp` combinator,
19637 // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19638 // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19639 // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19640 // pinned by `resource_limits_leq_agrees_with_join`) axioms
19641 // binding the `leq` relation to its combinator peers.
19642 //
19643 // Exercised on all four bracket-position arms of the input
19644 // surface (below-lower / in-range / above-upper / asymmetric-
19645 // crossing) so a regression that broke either direction of the
19646 // fixed-point equivalence exhausts every position and names the
19647 // failing arm.
19648 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19649 for a in [
19650 EMPTY_RESOURCE_LIMITS, // below-lower — clamp != a, within false
19651 HAND_AUTHORED_MID_POSTURE, // in-range — clamp == a, within true
19652 UNBOUNDED_RESOURCE_LIMITS, // above-upper — clamp != a, within false
19653 HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19654 ] {
19655 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19656 let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19657 let clamp_fixed = clamped == a;
19658 assert_eq!(
19659 within_holds, clamp_fixed,
19660 "within(lower, upper) must agree with clamp fixed-point; \
19661 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19662 );
19663 }
19664 }
19665
19666 #[test]
19667 fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19668 // Structural-equivalence pin — the sole behavioral claim the
19669 // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19670 // two-primitive conjunction the pre-lift caller composed. A drift
19671 // between the method and the inline conjunction would break this
19672 // equality on some input; the pin exhausts the four bracket-
19673 // position arms of the input surface. Peer of
19674 // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19675 // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19676 for a in [
19677 EMPTY_RESOURCE_LIMITS,
19678 HAND_AUTHORED_MID_POSTURE,
19679 HAND_AUTHORED_OTHER_POSTURE,
19680 UNBOUNDED_RESOURCE_LIMITS,
19681 ] {
19682 let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19683 let via_conjunction =
19684 HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19685 assert_eq!(via_method, via_conjunction, "a={a:?}");
19686 }
19687 }
19688
19689 #[test]
19690 fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19691 // Const-fn pin — the bracket predicate is evaluable in const
19692 // context, so a caller can pin a preset-bracket-membership
19693 // identity at compile time. Sibling of the const-fn evaluability
19694 // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19695 // relation and combinator both evaluate at const time, and the
19696 // predicate closing the (combinator, predicate) row does too.
19697 //
19698 // A regression to a runtime `fn` here would fail the `const _:
19699 // () = assert!(...)` bindings below at compile time.
19700 const _: () = assert!(
19701 DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19702 );
19703 const _: () = assert!(
19704 DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19705 );
19706 const _: () = assert!(
19707 !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19708 );
19709 }
19710
19711 #[test]
19712 fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19713 // Behavioral pin — a caller that CHECKS bracket-membership
19714 // before constructing an expander can safely reach through to
19715 // the runtime gates: if `within` reports true, `clamp` is a
19716 // no-op (by the fixed-point theorem), so the expander's
19717 // ceilings are the input posture's own. Peer of
19718 // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19719 // one PRIMITIVE-KIND axis over — the predicate view of the
19720 // "clamp reaches through to the runtime gates" property.
19721 //
19722 // The candidate sits at the bracket ceiling (arity=1, other
19723 // axes at usize::MAX), so it MUST sit within the [EMPTY,
19724 // bracket_ceiling] bracket — the maximal within-membership arm
19725 // of the fixed-point theorem. Every other axis at usize::MAX
19726 // clears the register-time gates, so the arity gate at 1 is
19727 // the one that trips on a 2-parameter macro registration.
19728 let bracket_ceiling = ResourceLimits {
19729 max_macro_arity: 1,
19730 ..UNBOUNDED_RESOURCE_LIMITS
19731 };
19732 let candidate = ResourceLimits {
19733 max_macro_arity: 1,
19734 ..UNBOUNDED_RESOURCE_LIMITS
19735 };
19736 assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19737 // Since candidate sits within the bracket, clamp is the
19738 // identity; the expander inherits candidate's ceilings verbatim.
19739 let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19740 assert_eq!(clamped, candidate);
19741 let mut e = Expander::with_limits(clamped);
19742 let forms = read("(defmacro two (a b) `,a)").unwrap();
19743 let err = e.expand_program(forms).unwrap_err();
19744 assert!(
19745 matches!(
19746 err,
19747 LispError::MacroArityExceeded {
19748 arity: 2,
19749 limit: 1,
19750 ..
19751 }
19752 ),
19753 "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19754 );
19755 }
19756
19757 // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19758 // N-ary boolean-conjunction peers of the pairwise `leq` relation,
19759 // closing the (combinator, predicate) row on the N-ary-aggregation
19760 // face of the lattice-algebra surface. See the docstring on
19761 // `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19762 // bracket) × (combinator, predicate) placement.
19763
19764 #[test]
19765 fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19766 // Empty-conjunction identity — every posture is trivially a
19767 // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19768 // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19769 // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19770 // identity element of the underlying operation (`true` for
19771 // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19772 assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19773 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19774 assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19775 assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19776 }
19777
19778 #[test]
19779 fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19780 // Empty-conjunction dual — every posture is trivially an upper
19781 // bound of the empty set. Peer of
19782 // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19783 // COMBINATOR-DIRECTION axis over, AND peer of
19784 // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19785 // slice identity one PRIMITIVE-KIND axis over.
19786 assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19787 assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19788 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19789 assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19790 }
19791
19792 #[test]
19793 fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19794 // 1-input identity — the singleton predicate reduces to the
19795 // pairwise partial-order relation. The N-ary predicate on a
19796 // 1-element slice is the pairwise `leq` verbatim, mirroring
19797 // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19798 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19799 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19800 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19801 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19802 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19803 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19804 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19805 ];
19806 for (a, b) in cases {
19807 assert_eq!(
19808 a.is_lower_bound_of(&[b]),
19809 a.leq(b),
19810 "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19811 );
19812 }
19813 }
19814
19815 #[test]
19816 fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19817 // 1-input dual identity — the singleton upper-bound predicate
19818 // is the pairwise `leq` with the direction flipped
19819 // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19820 // BELOW it.
19821 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19822 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19823 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19824 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19825 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19826 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19827 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19828 ];
19829 for (a, b) in cases {
19830 assert_eq!(
19831 a.is_upper_bound_of(&[b]),
19832 b.leq(a),
19833 "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19834 );
19835 }
19836 }
19837
19838 #[test]
19839 fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19840 // Definitional bridge — the N-ary meet is always a lower bound
19841 // of the slice it aggregates. Pins the link between the N-ary
19842 // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19843 // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19844 // bound set the predicate CHARACTERIZES.
19845 let postures: [ResourceLimits; 3] = [
19846 HAND_AUTHORED_MID_POSTURE,
19847 HAND_AUTHORED_OTHER_POSTURE,
19848 DEFAULT_RESOURCE_LIMITS,
19849 ];
19850 let met = ResourceLimits::strictest_of(&postures);
19851 assert!(met.is_lower_bound_of(&postures));
19852 }
19853
19854 #[test]
19855 fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19856 // Dual definitional bridge — the N-ary join is always an upper
19857 // bound of the slice it aggregates.
19858 let postures: [ResourceLimits; 3] = [
19859 HAND_AUTHORED_MID_POSTURE,
19860 HAND_AUTHORED_OTHER_POSTURE,
19861 DEFAULT_RESOURCE_LIMITS,
19862 ];
19863 let joined = ResourceLimits::most_permissive_of(&postures);
19864 assert!(joined.is_upper_bound_of(&postures));
19865 }
19866
19867 #[test]
19868 fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19869 // Universal-bottom witness — the lattice bottom is a common
19870 // lower bound of every slice, since `EMPTY.leq(a) == true` for
19871 // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19872 // The bounded-lattice specialization of the general "the meet-
19873 // identity is a lower bound of every set" theorem.
19874 let postures: [ResourceLimits; 4] = [
19875 HAND_AUTHORED_MID_POSTURE,
19876 HAND_AUTHORED_OTHER_POSTURE,
19877 DEFAULT_RESOURCE_LIMITS,
19878 UNBOUNDED_RESOURCE_LIMITS,
19879 ];
19880 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19881 // The three preset-carried postures each individually — the
19882 // universal-bottom witness holds on the singleton slice too.
19883 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19884 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19885 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19886 }
19887
19888 #[test]
19889 fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19890 // Universal-top witness — the lattice top is a common upper
19891 // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19892 // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19893 // per-axis).
19894 let postures: [ResourceLimits; 4] = [
19895 HAND_AUTHORED_MID_POSTURE,
19896 HAND_AUTHORED_OTHER_POSTURE,
19897 DEFAULT_RESOURCE_LIMITS,
19898 EMPTY_RESOURCE_LIMITS,
19899 ];
19900 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19901 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19902 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19903 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19904 }
19905
19906 #[test]
19907 fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19908 // Any-operand short-circuit — if `self` fails `leq` against ANY
19909 // operand in the slice, the N-ary conjunction rejects. The dual
19910 // of the "meet is a lower bound" witness on the false side:
19911 // moving `self` STRICTLY ABOVE an operand on any axis costs the
19912 // lower-bound property.
19913 //
19914 // MID sits above the join (element-wise) on some axes and below
19915 // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19916 // (fails against OTHER's smaller-axis values) NOR is OTHER a
19917 // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19918 // values).
19919 assert!(!HAND_AUTHORED_MID_POSTURE
19920 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19921 assert!(!HAND_AUTHORED_OTHER_POSTURE
19922 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19923 // A UNIVERSAL rejection — the lattice top is a lower bound
19924 // ONLY of the empty set OR of slices whose every element equals
19925 // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19926 // top's per-axis usize::MAX loses `leq` on every axis whose
19927 // preset value is strictly smaller.
19928 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19929 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19930 }
19931
19932 #[test]
19933 fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19934 // Dual any-operand short-circuit — if ANY operand fails `leq`
19935 // against `self`, the N-ary conjunction rejects.
19936 assert!(!HAND_AUTHORED_MID_POSTURE
19937 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19938 assert!(!HAND_AUTHORED_OTHER_POSTURE
19939 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19940 // The bottom cannot bound anything from above except the empty
19941 // set OR a slice of EMPTY-only postures.
19942 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19943 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19944 }
19945
19946 #[test]
19947 fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19948 // Method-vs-scaffold cross-check — the lifted N-ary predicate
19949 // agrees with the pre-lift two-primitive
19950 // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19951 // preset-carried posture across the shipped bounded-lattice
19952 // extrema AND the two hand-authored asymmetric witnesses. Pins
19953 // the lift is a semantic no-op — post-lift the consumer routes
19954 // through ONE name instead of composing the conjunction inline,
19955 // but the two paths compute the same boolean.
19956 let candidates: [ResourceLimits; 5] = [
19957 EMPTY_RESOURCE_LIMITS,
19958 DEFAULT_RESOURCE_LIMITS,
19959 UNBOUNDED_RESOURCE_LIMITS,
19960 HAND_AUTHORED_MID_POSTURE,
19961 HAND_AUTHORED_OTHER_POSTURE,
19962 ];
19963 let slice: [ResourceLimits; 3] = [
19964 HAND_AUTHORED_MID_POSTURE,
19965 HAND_AUTHORED_OTHER_POSTURE,
19966 DEFAULT_RESOURCE_LIMITS,
19967 ];
19968 for c in candidates {
19969 let direct = slice.iter().all(|p| c.leq(*p));
19970 assert_eq!(
19971 c.is_lower_bound_of(&slice),
19972 direct,
19973 "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19974 );
19975 }
19976 }
19977
19978 #[test]
19979 fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19980 // Dual method-vs-scaffold cross-check.
19981 let candidates: [ResourceLimits; 5] = [
19982 EMPTY_RESOURCE_LIMITS,
19983 DEFAULT_RESOURCE_LIMITS,
19984 UNBOUNDED_RESOURCE_LIMITS,
19985 HAND_AUTHORED_MID_POSTURE,
19986 HAND_AUTHORED_OTHER_POSTURE,
19987 ];
19988 let slice: [ResourceLimits; 3] = [
19989 HAND_AUTHORED_MID_POSTURE,
19990 HAND_AUTHORED_OTHER_POSTURE,
19991 DEFAULT_RESOURCE_LIMITS,
19992 ];
19993 for c in candidates {
19994 let direct = slice.iter().all(|p| p.leq(c));
19995 assert_eq!(
19996 c.is_upper_bound_of(&slice),
19997 direct,
19998 "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19999 );
20000 }
20001 }
20002
20003 #[test]
20004 fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
20005 // Const-fn pin — the N-ary lower-bound predicate is evaluable
20006 // in const context, so a caller can pin an N-ary-bound-
20007 // membership identity at compile time. Sibling of the const-fn
20008 // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
20009 // axis over, AND sibling of `strictest_of`'s const-fn pin one
20010 // COMBINATOR-KIND axis over.
20011 //
20012 // A regression to a runtime `fn` here would fail the `const _:
20013 // () = assert!(...)` bindings below at compile time.
20014 const _: () = assert!(EMPTY_RESOURCE_LIMITS
20015 .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
20016 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
20017 const _: () =
20018 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
20019 }
20020
20021 #[test]
20022 fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
20023 // Const-fn dual pin.
20024 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
20025 .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
20026 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
20027 const _: () =
20028 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
20029 }
20030
20031 /// The full canonical preset roster the strict-order pins sweep
20032 /// through — the (bottom, middle, top) bounded-lattice preset
20033 /// triple plus the two hand-authored incomparable asymmetric
20034 /// postures. Every strict-order law below quantifies over this
20035 /// slice, so a regression that breaks strict `<` on any preset
20036 /// pair discriminates at ONE named fixture rather than at a
20037 /// per-test ad-hoc literal.
20038 const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
20039 EMPTY_RESOURCE_LIMITS,
20040 DEFAULT_RESOURCE_LIMITS,
20041 UNBOUNDED_RESOURCE_LIMITS,
20042 HAND_AUTHORED_MID_POSTURE,
20043 HAND_AUTHORED_OTHER_POSTURE,
20044 ];
20045
20046 #[test]
20047 fn resource_limits_lt_is_irreflexive() {
20048 // Strict partial-order law — `a < a == false` for every
20049 // posture. Irreflexivity is the strict-relation axiom peer of
20050 // reflexivity on `leq` one STRICTNESS axis over on the
20051 // partial-order axiom surface; a regression that dropped the
20052 // antisymmetric leg (`!other.leq(self)`) — leaving the body at
20053 // `self.leq(other)` — would fire here on every preset since
20054 // `leq` is reflexive.
20055 for &a in STRICT_ORDER_ROSTER {
20056 assert!(!a.lt(a), "irreflexivity failed on {a:?}");
20057 }
20058 }
20059
20060 #[test]
20061 fn resource_limits_lt_is_asymmetric() {
20062 // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
20063 // pair. The two directions of the strict relation cannot both
20064 // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
20065 // requires `b.leq(a)`. Sweeps every ordered pair in the
20066 // canonical preset roster; discriminates a regression that
20067 // dropped the antisymmetric leg in either direction.
20068 for &a in STRICT_ORDER_ROSTER {
20069 for &b in STRICT_ORDER_ROSTER {
20070 if a.lt(b) {
20071 assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
20072 }
20073 }
20074 }
20075 }
20076
20077 #[test]
20078 fn resource_limits_lt_is_transitive() {
20079 // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
20080 // every ordered triple in the canonical preset roster; each
20081 // consequence is checked whenever both antecedents hold. On
20082 // the (bottom, middle, top) diagonal the canonical witness
20083 // `EMPTY < DEFAULT < UNBOUNDED` composes to
20084 // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
20085 // the bounded-lattice diagonal explicitly; the sweep pins the
20086 // same law on every other transitively-related triple in the
20087 // roster.
20088 for &a in STRICT_ORDER_ROSTER {
20089 for &b in STRICT_ORDER_ROSTER {
20090 for &c in STRICT_ORDER_ROSTER {
20091 if a.lt(b) && b.lt(c) {
20092 assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20093 }
20094 }
20095 }
20096 }
20097 }
20098
20099 #[test]
20100 fn resource_limits_lt_refines_leq() {
20101 // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
20102 // a refinement of the non-strict one; a witness of strict `<`
20103 // is also a witness of `≤`. Sweeps every ordered pair in the
20104 // canonical preset roster.
20105 for &a in STRICT_ORDER_ROSTER {
20106 for &b in STRICT_ORDER_ROSTER {
20107 if a.lt(b) {
20108 assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
20109 }
20110 }
20111 }
20112 }
20113
20114 #[test]
20115 fn resource_limits_lt_agrees_with_leq_minus_equality() {
20116 // Antisymmetry consequence — on ANY antisymmetric partial
20117 // order (which the pointwise `≤` on `ResourceLimits` is),
20118 // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
20119 // antisymmetric-leq encoding against the leq-and-not-equal
20120 // encoding across the full 5×5 preset matrix; a regression to
20121 // a non-antisymmetric preorder encoding would fire here.
20122 for &a in STRICT_ORDER_ROSTER {
20123 for &b in STRICT_ORDER_ROSTER {
20124 let shipped = a.lt(b);
20125 let alt = a.leq(b) && a != b;
20126 assert_eq!(
20127 shipped, alt,
20128 "encoding cross-check failed on ({a:?}, {b:?})",
20129 );
20130 }
20131 }
20132 }
20133
20134 #[test]
20135 fn resource_limits_lt_of_bottom_diagonal_pinned() {
20136 // Concrete-preset pin — the strict chain across the (bottom,
20137 // middle, top) preset triple on the bounded-lattice diagonal.
20138 // Every `DEFAULT_MAX_*` module constant is a concrete positive
20139 // value strictly less than [`usize::MAX`] and strictly greater
20140 // than `0`, so on every axis the three endpoints are strictly
20141 // ordered; the six-axis pointwise conjunction lifts the
20142 // per-axis strict chain to the posture-level strict chain.
20143 //
20144 // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20145 // strict_order` one STRICTNESS axis over — where the non-
20146 // strict pin asserted "leq holds one way and not the other"
20147 // via two `leq` calls, this pin asserts the strict-chain
20148 // conclusion via one `lt` call per link and pinpoints the
20149 // three-preset chain on the bounded-lattice diagonal.
20150 assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20151 assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20152 assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20153 }
20154
20155 #[test]
20156 fn resource_limits_lt_rejects_incomparable_postures() {
20157 // Partial-order non-total pin — the two hand-authored
20158 // asymmetric postures have MID smaller on three axes and OTHER
20159 // smaller on the other three, so neither `leq` direction
20160 // holds; strict `<` therefore fails in BOTH directions. The
20161 // pin discriminates a regression that promoted strict `<`
20162 // from partial to total (e.g. treating incomparable pairs as
20163 // strictly `<` in some canonical direction), and pairs with
20164 // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20165 // one STRICTNESS axis over.
20166 assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20167 assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20168 }
20169
20170 #[test]
20171 fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20172 // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20173 // !b.leq(a))` at every pair by definition. Sibling of the
20174 // `leq_minus_equality` pin one ENCODING axis over: that pin
20175 // routes through the equality-based encoding (an
20176 // antisymmetry consequence), this pin routes through the
20177 // definitional antisymmetric-leq encoding. A regression that
20178 // shipped a different (e.g. axis-wise strict) encoding of
20179 // `lt` diverges from this cross-check on the incomparable
20180 // asymmetric-preset pair.
20181 for &a in STRICT_ORDER_ROSTER {
20182 for &b in STRICT_ORDER_ROSTER {
20183 let shipped = a.lt(b);
20184 let direct = a.leq(b) && !b.leq(a);
20185 assert_eq!(
20186 shipped, direct,
20187 "direct-encoding cross-check failed on ({a:?}, {b:?})",
20188 );
20189 }
20190 }
20191 }
20192
20193 #[test]
20194 fn resource_limits_gt_is_dual_of_lt() {
20195 // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20196 // shipped `gt` body delegates to `lt` verbatim so this
20197 // agreement holds definitionally, but the pin discriminates a
20198 // regression that re-implemented `gt` independently and
20199 // dropped the direction flip. Sweeps the full 5×5 preset
20200 // matrix so incomparable + strictly-ordered + equal pairs
20201 // are all exercised.
20202 for &a in STRICT_ORDER_ROSTER {
20203 for &b in STRICT_ORDER_ROSTER {
20204 assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20205 }
20206 }
20207 }
20208
20209 #[test]
20210 fn resource_limits_gt_of_top_diagonal_pinned() {
20211 // Concrete-preset pin — the strict chain across the (top,
20212 // middle, bottom) preset triple on the bounded-lattice
20213 // diagonal, walked in the "above" direction. Peer of
20214 // `resource_limits_lt_of_bottom_diagonal_pinned` one
20215 // DIRECTION axis over.
20216 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20217 assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20218 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20219 }
20220
20221 #[test]
20222 fn resource_limits_gt_is_irreflexive() {
20223 // Strict partial-order law dual — `a > a == false` for every
20224 // posture. Inherits from the irreflexivity of `lt` via the
20225 // shipped `gt` body's delegation, but pinned explicitly so a
20226 // future non-delegating re-implementation of `gt` still has
20227 // its irreflexivity gated at the type-level test cohort.
20228 for &a in STRICT_ORDER_ROSTER {
20229 assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20230 }
20231 }
20232
20233 #[test]
20234 fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20235 // Const-fn pin — the strict `<` relation is evaluable in
20236 // const context, so a caller can pin a preset-strict-order
20237 // identity at compile time. Sibling of the const-fn
20238 // evaluability pin on `leq` one STRICTNESS axis over.
20239 //
20240 // A regression to a runtime `fn` here would fail the `const
20241 // _: () = assert!(...)` bindings below at compile time.
20242 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20243 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20244 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20245 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20246 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20247 }
20248
20249 #[test]
20250 fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20251 // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20252 // DIRECTION axis over.
20253 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20254 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20255 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20256 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20257 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20258 }
20259
20260 #[test]
20261 fn resource_limits_geq_is_dual_of_leq() {
20262 // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20263 // The shipped `geq` body delegates to `leq` verbatim so this
20264 // agreement holds definitionally, but the pin discriminates a
20265 // regression that re-implemented `geq` independently and
20266 // dropped the direction flip. Sweeps the full 5×5 preset
20267 // matrix so incomparable + strictly-ordered + equal pairs are
20268 // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20269 // one STRICTNESS axis over on the (below, above) × (non-strict,
20270 // strict) 2×2 pairwise partial-order face.
20271 for &a in STRICT_ORDER_ROSTER {
20272 for &b in STRICT_ORDER_ROSTER {
20273 assert_eq!(
20274 a.geq(b),
20275 b.leq(a),
20276 "geq/leq duality failed on ({a:?}, {b:?})",
20277 );
20278 }
20279 }
20280 }
20281
20282 #[test]
20283 fn resource_limits_geq_is_reflexive() {
20284 // Non-strict partial-order law — `a ≥ a == true` for every
20285 // posture. Reflexivity is the non-strict-relation axiom peer of
20286 // irreflexivity on `gt` one STRICTNESS axis over on the
20287 // partial-order axiom surface; a regression that hardened the
20288 // delegation into a strict `!other.leq(self)` would fire here
20289 // on every preset since the reflexive pair collapses.
20290 for &a in STRICT_ORDER_ROSTER {
20291 assert!(a.geq(a), "reflexivity failed on {a:?}");
20292 }
20293 }
20294
20295 #[test]
20296 fn resource_limits_geq_is_antisymmetric() {
20297 // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20298 // Antisymmetry is inherited from `leq` via the shipped `geq`
20299 // body's `other.leq(self)` delegation (which combined with
20300 // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20301 // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20302 // on any regression that let two DISTINCT postures satisfy
20303 // `geq` in both directions.
20304 for &a in STRICT_ORDER_ROSTER {
20305 for &b in STRICT_ORDER_ROSTER {
20306 if a.geq(b) && b.geq(a) {
20307 assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20308 }
20309 }
20310 }
20311 }
20312
20313 #[test]
20314 fn resource_limits_geq_is_transitive() {
20315 // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20316 // Inherits from the transitivity of `leq` via the shipped
20317 // delegation; sweeps every ordered triple in the canonical
20318 // preset roster. Peer of `resource_limits_lt_is_transitive`
20319 // one STRICTNESS axis over and `resource_limits_leq_is_
20320 // transitive` one DIRECTION axis over.
20321 for &a in STRICT_ORDER_ROSTER {
20322 for &b in STRICT_ORDER_ROSTER {
20323 for &c in STRICT_ORDER_ROSTER {
20324 if a.geq(b) && b.geq(c) {
20325 assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20326 }
20327 }
20328 }
20329 }
20330 }
20331
20332 #[test]
20333 fn resource_limits_geq_refines_gt() {
20334 // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20335 // relation is a refinement of the non-strict one in the
20336 // "above" direction, exactly as `lt` refines `leq` in the
20337 // "below" direction (pinned by
20338 // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20339 // A witness of strict `>` is also a witness of `≥`.
20340 for &a in STRICT_ORDER_ROSTER {
20341 for &b in STRICT_ORDER_ROSTER {
20342 if a.gt(b) {
20343 assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20344 }
20345 }
20346 }
20347 }
20348
20349 #[test]
20350 fn resource_limits_geq_agrees_with_gt_plus_equality() {
20351 // Antisymmetry consequence peer — on ANY antisymmetric partial
20352 // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20353 // delegation against the strict-or-equal encoding across the
20354 // full 5×5 preset matrix; a regression that shipped a non-
20355 // antisymmetric preorder encoding for `geq` fires here. Peer
20356 // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20357 // DIRECTION-AND-STRICTNESS axis over.
20358 for &a in STRICT_ORDER_ROSTER {
20359 for &b in STRICT_ORDER_ROSTER {
20360 let shipped = a.geq(b);
20361 let alt = a.gt(b) || a == b;
20362 assert_eq!(
20363 shipped, alt,
20364 "encoding cross-check failed on ({a:?}, {b:?})",
20365 );
20366 }
20367 }
20368 }
20369
20370 #[test]
20371 fn resource_limits_geq_of_top_diagonal_pinned() {
20372 // Concrete-preset pin — the non-strict chain across the (top,
20373 // middle, bottom) preset triple on the bounded-lattice
20374 // diagonal, walked in the "above" direction. Peer of
20375 // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20376 // axis over: the non-strict pin also holds at the reflexive
20377 // diagonal, so every posture sits above ITSELF via `geq` in
20378 // addition to sitting above the strictly-tighter presets.
20379 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20380 assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20381 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20382 assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20383 assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20384 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20385 }
20386
20387 #[test]
20388 fn resource_limits_geq_rejects_incomparable_postures() {
20389 // Partial-order non-total pin — the two hand-authored
20390 // asymmetric postures have MID smaller on three axes and OTHER
20391 // smaller on the other three, so neither `leq` direction holds
20392 // and therefore neither `geq` direction holds. The pin
20393 // discriminates a regression that promoted `geq` from partial
20394 // to total, and pairs with
20395 // `resource_limits_lt_rejects_incomparable_postures` one
20396 // STRICTNESS-AND-DIRECTION axis over.
20397 assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20398 assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20399 }
20400
20401 #[test]
20402 fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20403 // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20404 // at every pair. The pairwise "above" relation is exactly the
20405 // 1-input specialization of the N-ary upper-bound predicate;
20406 // the pin cross-checks that the two named entries agree on
20407 // the single-element case, which discriminates a regression
20408 // that drifted one from the other. Peer of the single-element
20409 // identity docstring on `is_upper_bound_of` one ARITY axis
20410 // over.
20411 for &a in STRICT_ORDER_ROSTER {
20412 for &b in STRICT_ORDER_ROSTER {
20413 assert_eq!(
20414 a.geq(b),
20415 a.is_upper_bound_of(&[b]),
20416 "arity-reduction failed on ({a:?}, {b:?})",
20417 );
20418 }
20419 }
20420 }
20421
20422 #[test]
20423 fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20424 // Const-fn pin — the non-strict `≥` relation is evaluable in
20425 // const context, so a caller can pin a preset-non-strict-order
20426 // identity at compile time. Sibling of the const-fn evaluability
20427 // pins on `leq` one DIRECTION axis over AND on `gt` one
20428 // STRICTNESS axis over.
20429 //
20430 // A regression to a runtime `fn` here would fail the `const _:
20431 // () = assert!(...)` bindings below at compile time.
20432 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20433 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20434 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20435 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20436 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20437 }
20438
20439 #[test]
20440 fn resource_limits_is_incomparable_is_irreflexive() {
20441 // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20442 // false` on every posture. Every posture is COMPARABLE to
20443 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20444 // conjunction's `!self.leq(other)` leg forces the verdict to
20445 // `false` at every diagonal input. Discriminates a regression
20446 // that dropped the `!` on `self.leq(other)` (leaving the body
20447 // at `self.leq(other) && !self.geq(other)`) — that would fold
20448 // the diagonal to `false` on the reflexive `leq` arm but
20449 // silently flip the antichain corner on non-diagonal cells.
20450 for &a in STRICT_ORDER_ROSTER {
20451 assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20452 }
20453 }
20454
20455 #[test]
20456 fn resource_limits_is_incomparable_is_symmetric() {
20457 // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20458 // b.is_incomparable(a)` on every pair. The composition
20459 // `!self.leq(other) && !self.geq(other)` folds through the
20460 // `self.leq(other) == other.geq(self)` dual identity on `geq`
20461 // and the conjunction's commutativity into an order-
20462 // independent projection on its two inputs. Sweeps every
20463 // ordered pair in the canonical preset roster;
20464 // discriminates a regression that broke either leg's dual
20465 // identity — one direction would fold to `false` on cells the
20466 // other still folded to `true`.
20467 for &a in STRICT_ORDER_ROSTER {
20468 for &b in STRICT_ORDER_ROSTER {
20469 assert_eq!(
20470 a.is_incomparable(b),
20471 b.is_incomparable(a),
20472 "symmetry failed on ({a:?}, {b:?})",
20473 );
20474 }
20475 }
20476 }
20477
20478 #[test]
20479 fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20480 // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20481 // a.geq(b))` on every pair. The antichain corner is the SET-
20482 // COMPLEMENT of the union of the two pointwise-domination
20483 // directions on the (comparable, incomparable) two-cell
20484 // partition of the ordered pair × verdict surface. Sweeps
20485 // every ordered pair in the canonical preset roster;
20486 // discriminates a regression that drifted the antichain
20487 // characterization off the substrate's `!leq && !geq`
20488 // composition (e.g., a shortcut through `!self.leq(other) ||
20489 // !self.geq(other)` — the OR-form falsely reports
20490 // incomparability on non-equal comparable pairs).
20491 for &a in STRICT_ORDER_ROSTER {
20492 for &b in STRICT_ORDER_ROSTER {
20493 let via_named = a.is_incomparable(b);
20494 let via_composition = !(a.leq(b) || a.geq(b));
20495 assert_eq!(
20496 via_named, via_composition,
20497 "De Morgan dual failed on ({a:?}, {b:?})",
20498 );
20499 }
20500 }
20501 }
20502
20503 #[test]
20504 fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20505 // Two-cell partition pin — on every ordered pair EITHER
20506 // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20507 // both. The (comparable, incomparable) partition of the
20508 // ordered pair × verdict surface is exhaustive-and-disjoint —
20509 // a substrate-level EXCLUSIVITY THEOREM the projection pins
20510 // once. Sweeps every ordered pair in the canonical preset
20511 // roster; discriminates a regression that let the two cells
20512 // overlap (e.g., an implementation that reported the antichain
20513 // corner as `true` on the comparable diagonal).
20514 for &a in STRICT_ORDER_ROSTER {
20515 for &b in STRICT_ORDER_ROSTER {
20516 let comparable = a.leq(b) || a.geq(b);
20517 let incomparable = a.is_incomparable(b);
20518 assert!(
20519 comparable != incomparable,
20520 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20521 );
20522 }
20523 }
20524 }
20525
20526 #[test]
20527 fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20528 // Bottom-pole absorption pin — `a.is_incomparable(
20529 // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20530 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20531 // `EMPTY.leq(a)` holds on every axis (usize zero is the
20532 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20533 // dual identity, which falsifies the `!self.geq(other)` leg.
20534 // Sweeps the canonical preset roster; pairs with the top-
20535 // pole absorption sibling one POLE axis over.
20536 for &a in STRICT_ORDER_ROSTER {
20537 assert!(
20538 !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20539 "bottom-pole absorption failed on {a:?}",
20540 );
20541 assert!(
20542 !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20543 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20544 );
20545 }
20546 }
20547
20548 #[test]
20549 fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20550 // Top-pole absorption pin — `a.is_incomparable(
20551 // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20552 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20553 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20554 // usize::MAX` unconditionally), which falsifies the
20555 // `!self.leq(other)` leg. Sweeps the canonical preset
20556 // roster; pairs with the bottom-pole absorption sibling one
20557 // POLE axis over.
20558 for &a in STRICT_ORDER_ROSTER {
20559 assert!(
20560 !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20561 "top-pole absorption failed on {a:?}",
20562 );
20563 assert!(
20564 !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20565 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20566 );
20567 }
20568 }
20569
20570 #[test]
20571 fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20572 // Antichain load-bearing arm — the two hand-authored
20573 // asymmetric postures sit on distinct branches (each is
20574 // smaller on three axes and larger on the other three), so
20575 // neither pointwise-domination direction closes and the
20576 // antichain verdict HOLDS in both orderings. Pairs with
20577 // `resource_limits_geq_rejects_incomparable_postures` +
20578 // `resource_limits_lt_rejects_incomparable_postures` one COVER
20579 // axis over: those pin the two comparable-direction predicates
20580 // FALSIFY on this pair; THIS pin confirms the antichain
20581 // verdict HOLDS on the SAME pair — the two are the negation-
20582 // paired sides of the (comparable, incomparable) two-cell
20583 // partition.
20584 assert!(
20585 HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20586 "antichain arm failed on (MID, OTHER)",
20587 );
20588 assert!(
20589 HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20590 "antichain arm failed on flipped (OTHER, MID)",
20591 );
20592 }
20593
20594 #[test]
20595 fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20596 // Shipped preset comparability pin — every ordered pair drawn
20597 // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20598 // is COMPARABLE (in both directions the pairwise partial-order
20599 // relation gives a verdict). The three shipped presets form a
20600 // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20601 // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20602 // corner MUST fold to `false` on every ordered pair drawn from
20603 // them. Discriminates a regression that promoted a shipped
20604 // preset off the total-order chain by drifting ONE field
20605 // silently.
20606 const TRIANGLE: &[ResourceLimits] = &[
20607 EMPTY_RESOURCE_LIMITS,
20608 DEFAULT_RESOURCE_LIMITS,
20609 UNBOUNDED_RESOURCE_LIMITS,
20610 ];
20611 for &a in TRIANGLE {
20612 for &b in TRIANGLE {
20613 assert!(
20614 !a.is_incomparable(b),
20615 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20616 );
20617 }
20618 }
20619 }
20620
20621 #[test]
20622 fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20623 // Const-fn pin — the antichain characterization is evaluable
20624 // in const context, so a caller can pin an antichain-
20625 // disagreement identity at compile time. Sibling of the
20626 // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20627 // one COVER axis over on the pairwise-relation surface.
20628 //
20629 // A regression to a runtime `fn` here would fail the `const _:
20630 // () = assert!(...)` bindings below at compile time.
20631 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20632 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20633 const _: () =
20634 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20635 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20636 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20637 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20638 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20639 }
20640
20641 // ── ResourceLimits::is_comparable ─────────────────────────────────
20642 //
20643 // The COMPARABLE CORNER on the pairwise-relation surface — the
20644 // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20645 // axis over on the (comparable, incomparable) two-cell partition
20646 // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20647 // for the algebra; the pins below fix each documented property
20648 // exactly once so a regression on any leg fires on a NAMED test
20649 // rather than an ad-hoc downstream break.
20650
20651 #[test]
20652 fn resource_limits_is_comparable_is_reflexive() {
20653 // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20654 // true` on every posture. Every posture is COMPARABLE to
20655 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20656 // the disjunction's `self.leq(other)` leg holds at every
20657 // diagonal input. Sibling of `is_incomparable`'s
20658 // irreflexivity one CELL axis over on the (comparable,
20659 // incomparable) partition — the diagonal MUST fold to `true`
20660 // here iff it folds to `false` there.
20661 for &a in STRICT_ORDER_ROSTER {
20662 assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20663 }
20664 }
20665
20666 #[test]
20667 fn resource_limits_is_comparable_is_symmetric() {
20668 // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20669 // b.is_comparable(a)` on every pair. The composition
20670 // `self.leq(other) || self.geq(other)` folds through the
20671 // `self.leq(other) == other.geq(self)` dual identity on
20672 // `geq` and the disjunction's commutativity into an order-
20673 // independent projection on its two inputs. Both cells of an
20674 // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20675 // must themselves be symmetric — the two verdicts flip in
20676 // lockstep on argument swap.
20677 for &a in STRICT_ORDER_ROSTER {
20678 for &b in STRICT_ORDER_ROSTER {
20679 assert_eq!(
20680 a.is_comparable(b),
20681 b.is_comparable(a),
20682 "symmetry failed on ({a:?}, {b:?})",
20683 );
20684 }
20685 }
20686 }
20687
20688 #[test]
20689 fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20690 // De Morgan dual pin — `a.is_comparable(b) ==
20691 // !a.is_incomparable(b)` on every pair. The comparable corner
20692 // is the SET-COMPLEMENT of the antichain corner on the
20693 // (comparable, incomparable) two-cell partition of the
20694 // ordered pair × verdict surface. The DIRECT dual of
20695 // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20696 // one CELL axis over: that test pins the antichain corner as
20697 // the negation of the `leq || geq` disjunction; THIS test
20698 // pins the comparability corner as the negation of the
20699 // antichain projection. Together the two pin BOTH SIDES of
20700 // the partition, closing it as a substrate-level
20701 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20702 for &a in STRICT_ORDER_ROSTER {
20703 for &b in STRICT_ORDER_ROSTER {
20704 let via_named = a.is_comparable(b);
20705 let via_negated_dual = !a.is_incomparable(b);
20706 assert_eq!(
20707 via_named, via_negated_dual,
20708 "De Morgan dual failed on ({a:?}, {b:?})",
20709 );
20710 }
20711 }
20712 }
20713
20714 #[test]
20715 fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20716 // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20717 // a.geq(b)` on every pair. Discriminates a regression that
20718 // drifted the comparability body off the substrate's `leq ||
20719 // geq` composition (e.g., a shortcut through `a.leq(b) &&
20720 // a.geq(b)` — the AND-form falsely reports comparability
20721 // only on the equality diagonal, dropping the strict-order
20722 // cells). Anchors the two-primitive delegation as a
20723 // substrate-level identity.
20724 for &a in STRICT_ORDER_ROSTER {
20725 for &b in STRICT_ORDER_ROSTER {
20726 let via_named = a.is_comparable(b);
20727 let via_composition = a.leq(b) || a.geq(b);
20728 assert_eq!(
20729 via_named, via_composition,
20730 "leq||geq composition failed on ({a:?}, {b:?})",
20731 );
20732 }
20733 }
20734 }
20735
20736 #[test]
20737 fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20738 // Bottom-pole absorption pin — `a.is_comparable(
20739 // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20740 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20741 // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20742 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20743 // dual identity, which satisfies the `self.geq(other)` leg.
20744 // The DIRECT COMPLEMENT of
20745 // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20746 // one CELL axis over on the SAME preset roster — where the
20747 // antichain verdict FOLDS FALSE at the bottom pole, the
20748 // comparability verdict FOLDS TRUE.
20749 for &a in STRICT_ORDER_ROSTER {
20750 assert!(
20751 a.is_comparable(EMPTY_RESOURCE_LIMITS),
20752 "bottom-pole absorption failed on {a:?}",
20753 );
20754 assert!(
20755 EMPTY_RESOURCE_LIMITS.is_comparable(a),
20756 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20757 );
20758 }
20759 }
20760
20761 #[test]
20762 fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20763 // Top-pole absorption pin — `a.is_comparable(
20764 // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20765 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20766 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20767 // usize::MAX` unconditionally), which satisfies the
20768 // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20769 // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20770 // one CELL axis over on the SAME preset roster.
20771 for &a in STRICT_ORDER_ROSTER {
20772 assert!(
20773 a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20774 "top-pole absorption failed on {a:?}",
20775 );
20776 assert!(
20777 UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20778 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20779 );
20780 }
20781 }
20782
20783 #[test]
20784 fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20785 // Antichain load-bearing arm — the two hand-authored
20786 // asymmetric postures sit on distinct branches (each is
20787 // smaller on three axes and larger on the other three), so
20788 // NEITHER pointwise-domination direction closes and the
20789 // comparability verdict FALSIFIES in both orderings. The
20790 // DIRECT NEGATION of
20791 // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20792 // one CELL axis over on the SAME hand-authored pair — the
20793 // two are the negation-paired sides of the (comparable,
20794 // incomparable) two-cell partition, and pinning BOTH cells'
20795 // verdicts on the SAME load-bearing antichain pair closes
20796 // the partition as a substrate-level EXCLUSIVITY THEOREM.
20797 assert!(
20798 !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20799 "antichain arm failed on (MID, OTHER)",
20800 );
20801 assert!(
20802 !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20803 "antichain arm failed on flipped (OTHER, MID)",
20804 );
20805 }
20806
20807 #[test]
20808 fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20809 // Shipped preset comparability pin — every ordered pair
20810 // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20811 // triangle is COMPARABLE (in at least one direction the
20812 // pairwise partial-order relation gives a verdict). The
20813 // three shipped presets form a TOTALLY ORDERED chain on the
20814 // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20815 // every field), so the comparability corner MUST fold to
20816 // `true` on every ordered pair drawn from them. The DIRECT
20817 // COMPLEMENT of
20818 // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20819 // one CELL axis over on the SAME triangle. Discriminates a
20820 // regression that promoted a shipped preset off the total-
20821 // order chain by drifting ONE field silently.
20822 const TRIANGLE: &[ResourceLimits] = &[
20823 EMPTY_RESOURCE_LIMITS,
20824 DEFAULT_RESOURCE_LIMITS,
20825 UNBOUNDED_RESOURCE_LIMITS,
20826 ];
20827 for &a in TRIANGLE {
20828 for &b in TRIANGLE {
20829 assert!(
20830 a.is_comparable(b),
20831 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20832 );
20833 }
20834 }
20835 }
20836
20837 #[test]
20838 fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20839 // Exhaustive-and-disjoint partition pin — on every ordered
20840 // pair EXACTLY ONE of `a.is_comparable(b)` OR
20841 // `a.is_incomparable(b)` holds. The (comparable,
20842 // incomparable) partition of the ordered pair × verdict
20843 // surface is exhaustive-and-disjoint — a substrate-level
20844 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20845 // together pin. Complements
20846 // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20847 // one CELL axis over: that test pins the partition against
20848 // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20849 // the partition against the NAMED comparability projection —
20850 // a regression that drifted `is_comparable` off the
20851 // composition without breaking the composition-based test
20852 // would still fire here.
20853 for &a in STRICT_ORDER_ROSTER {
20854 for &b in STRICT_ORDER_ROSTER {
20855 let comparable = a.is_comparable(b);
20856 let incomparable = a.is_incomparable(b);
20857 assert!(
20858 comparable != incomparable,
20859 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20860 );
20861 }
20862 }
20863 }
20864
20865 #[test]
20866 fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20867 // Const-fn pin — the comparability characterization is
20868 // evaluable in const context, so a caller can pin a
20869 // comparability-agreement identity at compile time. Sibling
20870 // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20871 // / `gt` / `is_incomparable` one COVER axis over on the
20872 // pairwise-relation surface.
20873 //
20874 // A regression to a runtime `fn` here would fail the `const _:
20875 // () = assert!(...)` bindings below at compile time.
20876 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20877 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20878 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20879 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20880 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20881 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20882 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20883 }
20884
20885 // ── ResourceLimits::partial_cmp ───────────────────────────────────
20886 //
20887 // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20888 // relation surface — the JOINT LIFT of the (comparable, incomparable)
20889 // two-cell partition together with the ordering-direction refinement
20890 // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20891 // algebra; the pins below fix each documented property exactly once
20892 // so a regression on any leg fires on a NAMED test rather than an
20893 // ad-hoc downstream break.
20894
20895 #[test]
20896 fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20897 // Diagonal-equality pin — `a.partial_cmp(a) ==
20898 // Some(Ordering::Equal)` on every posture. The `leq`
20899 // reflexivity closes both arms of the equality-guarding
20900 // conjunction, so the diagonal cell of the (Less, Equal,
20901 // Greater, None) 4-tile partition pins to the Equal tile.
20902 // Sibling of `is_comparable_is_reflexive` one PROJECTION
20903 // axis over — where the projection folds TRUE at the
20904 // diagonal, THIS method refines the verdict to `Equal`.
20905 for &a in STRICT_ORDER_ROSTER {
20906 assert_eq!(
20907 a.partial_cmp(a),
20908 Some(Ordering::Equal),
20909 "diagonal-equality failed on {a:?}",
20910 );
20911 }
20912 }
20913
20914 #[test]
20915 fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20916 // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20917 // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20918 // Swapping the argument pair FLIPS the ordering-direction
20919 // verdict (`Less <-> Greater`), fixes the diagonal-equality
20920 // arm, and fixes the antichain arm. Discriminates
20921 // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20922 // argument swap) one POSTURE axis over — the FULL verdict
20923 // carries directional content that inverts under swap where
20924 // the projections do not.
20925 for &a in STRICT_ORDER_ROSTER {
20926 for &b in STRICT_ORDER_ROSTER {
20927 let forward = a.partial_cmp(b);
20928 let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20929 assert_eq!(
20930 forward, swapped_reversed,
20931 "swap-reverse failed on ({a:?}, {b:?})",
20932 );
20933 }
20934 }
20935 }
20936
20937 #[test]
20938 fn resource_limits_partial_cmp_none_iff_incomparable() {
20939 // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20940 // == a.is_incomparable(b)` on every pair. The `None` arm of
20941 // the return coincides EXACTLY with the antichain-corner
20942 // projection. Anchors the joint-lift claim: THIS method
20943 // subsumes `is_incomparable` at its `is_none()` projection
20944 // without drifting the antichain characterization.
20945 for &a in STRICT_ORDER_ROSTER {
20946 for &b in STRICT_ORDER_ROSTER {
20947 let via_partial_cmp = a.partial_cmp(b).is_none();
20948 let via_is_incomparable = a.is_incomparable(b);
20949 assert_eq!(
20950 via_partial_cmp, via_is_incomparable,
20951 "none-iff-incomparable failed on ({a:?}, {b:?})",
20952 );
20953 }
20954 }
20955 }
20956
20957 #[test]
20958 fn resource_limits_partial_cmp_some_iff_comparable() {
20959 // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20960 // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20961 // the return coincides EXACTLY with the comparability-corner
20962 // projection. The DUAL of the antichain-arm pin one CELL axis
20963 // over on the (comparable, incomparable) partition — together
20964 // the two pins close the JOINT LIFT of both cells at THIS
20965 // method.
20966 for &a in STRICT_ORDER_ROSTER {
20967 for &b in STRICT_ORDER_ROSTER {
20968 let via_partial_cmp = a.partial_cmp(b).is_some();
20969 let via_is_comparable = a.is_comparable(b);
20970 assert_eq!(
20971 via_partial_cmp, via_is_comparable,
20972 "some-iff-comparable failed on ({a:?}, {b:?})",
20973 );
20974 }
20975 }
20976 }
20977
20978 #[test]
20979 fn resource_limits_partial_cmp_less_iff_lt() {
20980 // Strict-less agreement pin — `a.partial_cmp(b) ==
20981 // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20982 // the (Less, Equal, Greater, None) 4-tile partition binds to
20983 // the named `lt` companion primitive at exactly one dispatch
20984 // site. A regression that drifted the arm off `lt` (e.g., to
20985 // `leq` — including the equality diagonal) would fire here.
20986 for &a in STRICT_ORDER_ROSTER {
20987 for &b in STRICT_ORDER_ROSTER {
20988 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20989 let via_lt = a.lt(b);
20990 assert_eq!(
20991 via_partial_cmp, via_lt,
20992 "less-iff-lt failed on ({a:?}, {b:?})",
20993 );
20994 }
20995 }
20996 }
20997
20998 #[test]
20999 fn resource_limits_partial_cmp_greater_iff_gt() {
21000 // Strict-greater agreement pin — `a.partial_cmp(b) ==
21001 // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
21002 // less agreement pin one DIRECTION axis over on the (Less,
21003 // Greater) strict-order pair — a regression that dropped the
21004 // asymmetry between the two arms (e.g., collapsed both to
21005 // `Less`) would fire here.
21006 for &a in STRICT_ORDER_ROSTER {
21007 for &b in STRICT_ORDER_ROSTER {
21008 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
21009 let via_gt = a.gt(b);
21010 assert_eq!(
21011 via_partial_cmp, via_gt,
21012 "greater-iff-gt failed on ({a:?}, {b:?})",
21013 );
21014 }
21015 }
21016 }
21017
21018 #[test]
21019 fn resource_limits_partial_cmp_equal_iff_eq() {
21020 // Equality agreement pin — `a.partial_cmp(b) ==
21021 // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
21022 // 4-tile partition binds through the pointwise antisymmetric-
21023 // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
21024 // pinned by `resource_limits_leq_is_antisymmetric`) to the
21025 // `PartialEq::eq` companion. Discriminates a regression that
21026 // drifted the Equal arm off the antisymmetric conjunction.
21027 for &a in STRICT_ORDER_ROSTER {
21028 for &b in STRICT_ORDER_ROSTER {
21029 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
21030 let via_eq = a == b;
21031 assert_eq!(
21032 via_partial_cmp, via_eq,
21033 "equal-iff-eq failed on ({a:?}, {b:?})",
21034 );
21035 }
21036 }
21037 }
21038
21039 #[test]
21040 fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
21041 // Bottom-to-top-pole ordering pin — every strictly-ascending
21042 // pair on the shipped preset chain (EMPTY <= DEFAULT <=
21043 // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
21044 // shipped presets form a TOTALLY ORDERED chain on the
21045 // pointwise partial-order (every DEFAULT_MAX_* is a concrete
21046 // positive value strictly less than usize::MAX), so the
21047 // strict-less arm fires on every ordered pair drawn in
21048 // ascending direction.
21049 assert_eq!(
21050 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21051 Some(Ordering::Less),
21052 );
21053 assert_eq!(
21054 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21055 Some(Ordering::Less),
21056 );
21057 assert_eq!(
21058 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21059 Some(Ordering::Less),
21060 );
21061 }
21062
21063 #[test]
21064 fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
21065 // Top-to-bottom-pole ordering pin — every strictly-descending
21066 // pair on the shipped preset chain folds to
21067 // `Some(Ordering::Greater)`. The DUAL of the ascending pin
21068 // one DIRECTION axis over — together the two pins close the
21069 // (ascending, descending) direction pair on the shipped
21070 // preset chain at the ordering-direction refinement.
21071 assert_eq!(
21072 DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21073 Some(Ordering::Greater),
21074 );
21075 assert_eq!(
21076 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21077 Some(Ordering::Greater),
21078 );
21079 assert_eq!(
21080 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21081 Some(Ordering::Greater),
21082 );
21083 }
21084
21085 #[test]
21086 fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
21087 // Antichain load-bearing arm — the two hand-authored
21088 // asymmetric postures sit on distinct branches (each is
21089 // smaller on three axes and larger on the other three), so
21090 // NEITHER pointwise-domination direction closes and the
21091 // fall-through `None` arm fires in both orderings. The DIRECT
21092 // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
21093 // one PROJECTION axis over on the SAME hand-authored pair —
21094 // where the projection folds TRUE, THIS method folds to
21095 // `None`. The two are the negation-paired sides of the
21096 // (Some(_), None) two-cell partition of `Option<Ordering>`.
21097 assert_eq!(
21098 HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
21099 None,
21100 );
21101 assert_eq!(
21102 HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
21103 None,
21104 );
21105 }
21106
21107 #[test]
21108 fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
21109 // Const-fn pin — the full-verdict projection is evaluable in
21110 // const context, so a caller can pin an ordering-direction
21111 // identity at compile time. Sibling of the const-fn
21112 // evaluability pins on `leq` / `geq` / `lt` / `gt` /
21113 // `is_incomparable` / `is_comparable` one COVER axis over on
21114 // the pairwise-relation surface.
21115 //
21116 // `matches!` in const context requires the pattern's variants
21117 // to be const-constructible — `Ordering::Less` / `Equal` /
21118 // `Greater` are plain enum constructors, so the assertions
21119 // below stay inside stable const-fn scope. A regression to a
21120 // runtime `fn` here would fail the `const _: () = assert!(
21121 // matches!(...))` bindings below at compile time.
21122 const _: () = assert!(matches!(
21123 EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21124 Some(Ordering::Equal),
21125 ));
21126 const _: () = assert!(matches!(
21127 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21128 Some(Ordering::Less),
21129 ));
21130 const _: () = assert!(matches!(
21131 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21132 Some(Ordering::Greater),
21133 ));
21134 const _: () = assert!(matches!(
21135 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21136 Some(Ordering::Less),
21137 ));
21138 const _: () = assert!(matches!(
21139 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21140 Some(Ordering::Less),
21141 ));
21142 }
21143
21144 #[test]
21145 fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21146 // Empty-slice vacuous truth — the empty conjunction is vacuously
21147 // true; the empty slice contains no distinct pairs to reject.
21148 // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21149 // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21150 // (pairwise, N-ary) primitive surface.
21151 assert!(ResourceLimits::is_chain(&[]));
21152 }
21153
21154 #[test]
21155 fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21156 // Empty-slice vacuous truth — the empty conjunction is vacuously
21157 // true; the empty slice contains no distinct pairs to reject.
21158 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21159 // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21160 // set-level pair AGREE at the empty slice on the vacuous-truth
21161 // verdict — the two cells coincide at the empty face of the
21162 // set-level verdict surface.
21163 assert!(ResourceLimits::is_antichain(&[]));
21164 }
21165
21166 #[test]
21167 fn resource_limits_is_chain_singleton_is_vacuously_true() {
21168 // Singleton vacuous truth — a one-element slice contains no
21169 // distinct pairs, so the outer-and-inner-index conjunction never
21170 // enters the inner loop and returns `true` vacuously. Pinned on
21171 // every canonical preset AND on both hand-authored asymmetric
21172 // postures so the vacuous-truth verdict holds regardless of the
21173 // singleton element's lattice position.
21174 assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21175 assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21176 assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21177 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21178 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21179 }
21180
21181 #[test]
21182 fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21183 // Singleton vacuous truth — a one-element slice contains no
21184 // distinct pairs, so the outer-and-inner-index conjunction never
21185 // enters the inner loop and returns `true` vacuously. Peer of
21186 // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21187 // at singleton slices — the (chain, antichain) verdict surface
21188 // coincides at every slice with strictly fewer than two distinct
21189 // pairs.
21190 assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21191 assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21192 assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21193 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21194 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21195 }
21196
21197 #[test]
21198 fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21199 // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21200 // (`a.is_comparable(a) == true`), so the single distinct index
21201 // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21202 // conjunction holds at every diagonal-duplicate slice.
21203 // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21204 // over, whose diagonal-duplicate verdict is `false` via
21205 // `is_incomparable`'s IRREFLEXIVITY.
21206 assert!(ResourceLimits::is_chain(&[
21207 DEFAULT_RESOURCE_LIMITS,
21208 DEFAULT_RESOURCE_LIMITS,
21209 ]));
21210 assert!(ResourceLimits::is_chain(&[
21211 HAND_AUTHORED_MID_POSTURE,
21212 HAND_AUTHORED_MID_POSTURE,
21213 ]));
21214 }
21215
21216 #[test]
21217 fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21218 // Diagonal-duplicate rejection — `is_incomparable` is
21219 // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21220 // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21221 // false` and the conjunction rejects at every diagonal-duplicate
21222 // slice. The reflexivity/irreflexivity divergence between the
21223 // two pair-level projections PROPAGATES to the set-level
21224 // projections at any slice with a duplicated element — the two
21225 // set-level cells DIVERGE at every diagonal-duplicate slice, the
21226 // mirror of their AGREEMENT at every empty-or-singleton slice
21227 // one CARDINALITY axis over.
21228 assert!(!ResourceLimits::is_antichain(&[
21229 DEFAULT_RESOURCE_LIMITS,
21230 DEFAULT_RESOURCE_LIMITS,
21231 ]));
21232 assert!(!ResourceLimits::is_antichain(&[
21233 HAND_AUTHORED_MID_POSTURE,
21234 HAND_AUTHORED_MID_POSTURE,
21235 ]));
21236 }
21237
21238 #[test]
21239 fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21240 // Ordered-chain closure — the shipped-preset triple on the
21241 // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21242 // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21243 // the three is comparable. Pinned in every permutation of the
21244 // triple so the doubly-indexed all-pairs walk closes on every
21245 // enumeration of the same chain.
21246 assert!(ResourceLimits::is_chain(&[
21247 EMPTY_RESOURCE_LIMITS,
21248 DEFAULT_RESOURCE_LIMITS,
21249 UNBOUNDED_RESOURCE_LIMITS,
21250 ]));
21251 assert!(ResourceLimits::is_chain(&[
21252 UNBOUNDED_RESOURCE_LIMITS,
21253 DEFAULT_RESOURCE_LIMITS,
21254 EMPTY_RESOURCE_LIMITS,
21255 ]));
21256 assert!(ResourceLimits::is_chain(&[
21257 DEFAULT_RESOURCE_LIMITS,
21258 EMPTY_RESOURCE_LIMITS,
21259 UNBOUNDED_RESOURCE_LIMITS,
21260 ]));
21261 }
21262
21263 #[test]
21264 fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21265 // Chain rejection — the shipped-preset pair on the bounded-
21266 // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21267 // the single distinct pair is COMPARABLE and the antichain
21268 // conjunction rejects. The DIRECT FALSIFICATION of
21269 // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21270 // over on the SAME lattice diagonal — the two set-level cells
21271 // carry the negation-paired verdict at every strictly-chained
21272 // slice, the mirror of their AGREEMENT at every empty-or-
21273 // singleton slice one CARDINALITY axis over.
21274 assert!(!ResourceLimits::is_antichain(&[
21275 EMPTY_RESOURCE_LIMITS,
21276 DEFAULT_RESOURCE_LIMITS,
21277 ]));
21278 assert!(!ResourceLimits::is_antichain(&[
21279 DEFAULT_RESOURCE_LIMITS,
21280 UNBOUNDED_RESOURCE_LIMITS,
21281 ]));
21282 assert!(!ResourceLimits::is_antichain(&[
21283 EMPTY_RESOURCE_LIMITS,
21284 DEFAULT_RESOURCE_LIMITS,
21285 UNBOUNDED_RESOURCE_LIMITS,
21286 ]));
21287 }
21288
21289 #[test]
21290 fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21291 // Antichain rejection — the two hand-authored asymmetric
21292 // postures sit on distinct branches (each is smaller on three
21293 // axes and larger on the other three), so neither pointwise-
21294 // domination direction closes and the pair is INCOMPARABLE. The
21295 // chain conjunction rejects at the first incomparable pair.
21296 // Pinned in both orderings so the doubly-indexed pair walk
21297 // rejects regardless of index order (the diagonal-adjacent
21298 // pair-level rejection is symmetric on argument swap).
21299 assert!(!ResourceLimits::is_chain(&[
21300 HAND_AUTHORED_MID_POSTURE,
21301 HAND_AUTHORED_OTHER_POSTURE,
21302 ]));
21303 assert!(!ResourceLimits::is_chain(&[
21304 HAND_AUTHORED_OTHER_POSTURE,
21305 HAND_AUTHORED_MID_POSTURE,
21306 ]));
21307 }
21308
21309 #[test]
21310 fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21311 // Hand-authored antichain closure — the two hand-authored
21312 // asymmetric postures sit on distinct branches (each is smaller
21313 // on three axes and larger on the other three), so neither
21314 // pointwise-domination direction closes and the pair is
21315 // INCOMPARABLE. The antichain conjunction accepts every distinct
21316 // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21317 // rejection one CELL axis over on the SAME hand-authored pair —
21318 // pinning BOTH cells' verdicts on the SAME load-bearing
21319 // antichain pair closes the (chain, antichain) two-cell face on
21320 // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21321 assert!(ResourceLimits::is_antichain(&[
21322 HAND_AUTHORED_MID_POSTURE,
21323 HAND_AUTHORED_OTHER_POSTURE,
21324 ]));
21325 assert!(ResourceLimits::is_antichain(&[
21326 HAND_AUTHORED_OTHER_POSTURE,
21327 HAND_AUTHORED_MID_POSTURE,
21328 ]));
21329 }
21330
21331 #[test]
21332 fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21333 // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21334 // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21335 // per-axis values are all much larger than either hand-authored
21336 // posture's, so DEFAULT dominates both pointwise), but the
21337 // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21338 // The set-level chain verdict is stricter than the union of its
21339 // pair-level verdicts — one antichain pair anywhere in the
21340 // slice falsifies the whole projection.
21341 assert!(!ResourceLimits::is_chain(&[
21342 DEFAULT_RESOURCE_LIMITS,
21343 HAND_AUTHORED_MID_POSTURE,
21344 HAND_AUTHORED_OTHER_POSTURE,
21345 ]));
21346 }
21347
21348 #[test]
21349 fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21350 // Mixed-set rejection — even though the second-and-third pair
21351 // is incomparable (the hand-authored antichain pair), the first
21352 // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21353 // conjunction rejects. Pins the three-cell (chain, antichain,
21354 // mixed) set-level face at the mixed-set cell: the same slice
21355 // both `is_chain` AND `is_antichain` reject at, and the third
21356 // cell of the three-cell face the two set-level projections
21357 // carry the ends of.
21358 assert!(!ResourceLimits::is_antichain(&[
21359 DEFAULT_RESOURCE_LIMITS,
21360 HAND_AUTHORED_MID_POSTURE,
21361 HAND_AUTHORED_OTHER_POSTURE,
21362 ]));
21363 }
21364
21365 #[test]
21366 fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21367 // De Morgan mirror at arity 2 — on any two-element slice `[a,
21368 // b]` with `a != b`, the set-level (chain, antichain) two-cell
21369 // face reduces to the pair-level (comparable, incomparable)
21370 // two-cell partition and the two cells become MUTUALLY
21371 // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21372 // set-level De Morgan mirror of the pair-level identity
21373 // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21374 // pins one CARDINALITY axis over.
21375 let presets: [ResourceLimits; 5] = [
21376 EMPTY_RESOURCE_LIMITS,
21377 DEFAULT_RESOURCE_LIMITS,
21378 UNBOUNDED_RESOURCE_LIMITS,
21379 HAND_AUTHORED_MID_POSTURE,
21380 HAND_AUTHORED_OTHER_POSTURE,
21381 ];
21382 for a in presets {
21383 for b in presets {
21384 if a == b {
21385 continue;
21386 }
21387 let pair = [a, b];
21388 let chain = ResourceLimits::is_chain(&pair);
21389 let antichain = ResourceLimits::is_antichain(&pair);
21390 assert_ne!(
21391 chain, antichain,
21392 "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21393 );
21394 }
21395 }
21396 }
21397
21398 #[test]
21399 fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21400 // Empty-and-singleton agreement — the two set-level projections
21401 // carry the SAME vacuous-truth verdict at every slice with
21402 // strictly fewer than two distinct pairs (empty AND singleton).
21403 // Pinned as a substrate-level identity so a future rewrite of
21404 // either projection cannot silently drift from the shared
21405 // vacuous-truth verdict.
21406 assert_eq!(
21407 ResourceLimits::is_chain(&[]),
21408 ResourceLimits::is_antichain(&[]),
21409 );
21410 let presets: [ResourceLimits; 5] = [
21411 EMPTY_RESOURCE_LIMITS,
21412 DEFAULT_RESOURCE_LIMITS,
21413 UNBOUNDED_RESOURCE_LIMITS,
21414 HAND_AUTHORED_MID_POSTURE,
21415 HAND_AUTHORED_OTHER_POSTURE,
21416 ];
21417 for a in presets {
21418 let single = [a];
21419 assert_eq!(
21420 ResourceLimits::is_chain(&single),
21421 ResourceLimits::is_antichain(&single),
21422 "singleton agreement failed on {a:?}",
21423 );
21424 }
21425 }
21426
21427 #[test]
21428 fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21429 // Const-fn pin — the set-level chain projection is evaluable in
21430 // const context, so a caller can pin a chain-membership
21431 // identity at compile time. Sibling of the const-fn
21432 // evaluability pins on `is_comparable` one CARDINALITY axis
21433 // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21434 // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21435 const _: () = assert!(ResourceLimits::is_chain(&[]));
21436 const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21437 const _: () = assert!(ResourceLimits::is_chain(&[
21438 EMPTY_RESOURCE_LIMITS,
21439 DEFAULT_RESOURCE_LIMITS,
21440 UNBOUNDED_RESOURCE_LIMITS,
21441 ]));
21442 }
21443
21444 #[test]
21445 fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21446 // Const-fn pin — the set-level antichain projection is
21447 // evaluable in const context, so a caller can pin an antichain-
21448 // membership identity at compile time. Sibling of the const-fn
21449 // evaluability pins on `is_incomparable` one CARDINALITY axis
21450 // over and on `is_chain` one COVER-COMPLEMENT axis over.
21451 const _: () = assert!(ResourceLimits::is_antichain(&[]));
21452 const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21453 const _: () = assert!(ResourceLimits::is_antichain(&[
21454 HAND_AUTHORED_MID_POSTURE,
21455 HAND_AUTHORED_OTHER_POSTURE,
21456 ]));
21457 }
21458
21459 #[test]
21460 fn resource_limits_is_mixed_empty_slice_is_false() {
21461 // Empty-slice contract — is_chain and is_antichain BOTH return
21462 // `true` vacuously, so `!true && !true == false`. The empty
21463 // slice is NOT mixed. Sibling posture to the vacuous-truth
21464 // verdicts of is_chain + is_antichain one CELL axis over on the
21465 // (chain, antichain, mixed) three-cell face.
21466 assert!(!ResourceLimits::is_mixed(&[]));
21467 }
21468
21469 #[test]
21470 fn resource_limits_is_mixed_singleton_is_false() {
21471 // Singleton contract — a one-element slice contains no distinct
21472 // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21473 // Pinned on every canonical preset AND on both hand-authored
21474 // asymmetric postures so the false-at-singleton verdict holds
21475 // regardless of the singleton element's lattice position.
21476 assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21477 assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21478 assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21479 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21480 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21481 }
21482
21483 #[test]
21484 fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21485 // Diagonal-duplicate contract — is_chain returns `true` via
21486 // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21487 // is_incomparable's IRREFLEXIVITY; the conjunction
21488 // `!true && !false == false`. The diagonal-duplicate slice is a
21489 // CHAIN (per the reflexivity arm), not mixed.
21490 assert!(!ResourceLimits::is_mixed(&[
21491 DEFAULT_RESOURCE_LIMITS,
21492 DEFAULT_RESOURCE_LIMITS,
21493 ]));
21494 assert!(!ResourceLimits::is_mixed(&[
21495 HAND_AUTHORED_MID_POSTURE,
21496 HAND_AUTHORED_MID_POSTURE,
21497 ]));
21498 }
21499
21500 #[test]
21501 fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21502 // Chain rejection — the shipped-preset triple on the bounded-
21503 // lattice diagonal is an ascending chain, is_chain returns
21504 // `true`, and `!true && …` short-circuits to `false`. A PURE
21505 // chain is not mixed. Pinned in every permutation of the triple
21506 // so the projection factors through the ordering-agnostic
21507 // sibling is_chain verdict on the same chain.
21508 assert!(!ResourceLimits::is_mixed(&[
21509 EMPTY_RESOURCE_LIMITS,
21510 DEFAULT_RESOURCE_LIMITS,
21511 UNBOUNDED_RESOURCE_LIMITS,
21512 ]));
21513 assert!(!ResourceLimits::is_mixed(&[
21514 UNBOUNDED_RESOURCE_LIMITS,
21515 DEFAULT_RESOURCE_LIMITS,
21516 EMPTY_RESOURCE_LIMITS,
21517 ]));
21518 }
21519
21520 #[test]
21521 fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21522 // Antichain rejection — the hand-authored antichain pair binds
21523 // is_antichain to `true`, and `!… && !true == false`. A PURE
21524 // antichain is not mixed. Pinned in both orderings so the
21525 // projection factors through the argument-swap-symmetric
21526 // sibling is_antichain verdict on the same antichain.
21527 assert!(!ResourceLimits::is_mixed(&[
21528 HAND_AUTHORED_MID_POSTURE,
21529 HAND_AUTHORED_OTHER_POSTURE,
21530 ]));
21531 assert!(!ResourceLimits::is_mixed(&[
21532 HAND_AUTHORED_OTHER_POSTURE,
21533 HAND_AUTHORED_MID_POSTURE,
21534 ]));
21535 }
21536
21537 #[test]
21538 fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21539 ) {
21540 // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21541 // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21542 // while the (MID, OTHER) pair is incomparable (falsifying
21543 // is_chain); the conjunction `!false && !false == true`. This
21544 // is the SAME fixture the sibling is_chain + is_antichain BOTH
21545 // reject at — the DISCRIMINATING positive-arm catch for the
21546 // (mixed) cell of the three-cell face the two set-level
21547 // projections carry the ends of.
21548 assert!(ResourceLimits::is_mixed(&[
21549 DEFAULT_RESOURCE_LIMITS,
21550 HAND_AUTHORED_MID_POSTURE,
21551 HAND_AUTHORED_OTHER_POSTURE,
21552 ]));
21553 }
21554
21555 #[test]
21556 fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21557 ) {
21558 // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21559 // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21560 // returns `true` on every pair OR triple drawn from the shipped
21561 // presets whose elements are pairwise distinct. The three cells
21562 // partition the set-level verdict surface at every non-
21563 // degenerate slice. Substrate-level identity: pinning it here
21564 // catches a future rewrite of any of the three projections that
21565 // silently drifts from the partition contract.
21566 let presets: [ResourceLimits; 5] = [
21567 EMPTY_RESOURCE_LIMITS,
21568 DEFAULT_RESOURCE_LIMITS,
21569 UNBOUNDED_RESOURCE_LIMITS,
21570 HAND_AUTHORED_MID_POSTURE,
21571 HAND_AUTHORED_OTHER_POSTURE,
21572 ];
21573 // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21574 // one distinct pair), so is_mixed is always false and the
21575 // partition reduces to the (chain, antichain) two-cell face
21576 // that is_chain + is_antichain already pinned MUTUALLY
21577 // EXCLUSIVE. Cross-check that the three-cell projection
21578 // AGREES with the two-cell projection at arity 2.
21579 for a in presets {
21580 for b in presets {
21581 if a == b {
21582 continue;
21583 }
21584 let pair = [a, b];
21585 let chain = ResourceLimits::is_chain(&pair);
21586 let antichain = ResourceLimits::is_antichain(&pair);
21587 let mixed = ResourceLimits::is_mixed(&pair);
21588 let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21589 assert_eq!(
21590 true_count, 1,
21591 "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21592 );
21593 assert!(
21594 !mixed,
21595 "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21596 );
21597 }
21598 }
21599 // Arity-3 sweep on distinct triples — the mixed cell opens
21600 // whenever the triple carries both a comparable and an
21601 // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21602 for a in presets {
21603 for b in presets {
21604 for c in presets {
21605 if a == b || b == c || a == c {
21606 continue;
21607 }
21608 let triple = [a, b, c];
21609 let chain = ResourceLimits::is_chain(&triple);
21610 let antichain = ResourceLimits::is_antichain(&triple);
21611 let mixed = ResourceLimits::is_mixed(&triple);
21612 let true_count =
21613 usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21614 assert_eq!(
21615 true_count, 1,
21616 "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21617 );
21618 }
21619 }
21620 }
21621 }
21622
21623 #[test]
21624 fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21625 // Const-fn pin — the set-level mixed projection is evaluable
21626 // in const context, so a caller can pin a mixed-set identity
21627 // at compile time. Sibling of the const-fn evaluability pins
21628 // on is_chain + is_antichain one CELL axis over.
21629 const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21630 const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21631 const _: () = assert!(ResourceLimits::is_mixed(&[
21632 DEFAULT_RESOURCE_LIMITS,
21633 HAND_AUTHORED_MID_POSTURE,
21634 HAND_AUTHORED_OTHER_POSTURE,
21635 ]));
21636 }
21637
21638 // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21639
21640 #[test]
21641 fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21642 // Empty-slice vacuous truth — the empty conjunction is vacuously
21643 // true; the empty slice contains no consecutive pairs to reject.
21644 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21645 // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21646 // level, sequence-level) projection pair AGREE at the empty slice
21647 // on the vacuous-truth verdict.
21648 assert!(ResourceLimits::is_ascending(&[]));
21649 }
21650
21651 #[test]
21652 fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21653 // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21654 // PRIMITIVE axis over: both cells of the (ascending, descending)
21655 // sequence-level pair AGREE at the empty slice.
21656 assert!(ResourceLimits::is_descending(&[]));
21657 }
21658
21659 #[test]
21660 fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21661 // Singleton vacuous truth — a one-element slice contains no
21662 // consecutive pairs, so the walk never enters the loop and
21663 // returns `true` vacuously. Pinned on every canonical preset AND
21664 // on both hand-authored asymmetric postures.
21665 assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21666 assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21667 assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21668 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21669 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21670 }
21671
21672 #[test]
21673 fn resource_limits_is_descending_singleton_is_vacuously_true() {
21674 assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21675 assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21676 assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21677 assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21678 assert!(ResourceLimits::is_descending(&[
21679 HAND_AUTHORED_OTHER_POSTURE
21680 ]));
21681 }
21682
21683 #[test]
21684 fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21685 // Diagonal-duplicate identity — `leq` is REFLEXIVE
21686 // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21687 // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21688 // the conjunction holds at every diagonal-duplicate slice.
21689 // AGREES with is_chain's diagonal-duplicate verdict — the two
21690 // projections diverge only where the ORDER matters, and
21691 // duplicated elements carry no ordering information.
21692 assert!(ResourceLimits::is_ascending(&[
21693 DEFAULT_RESOURCE_LIMITS,
21694 DEFAULT_RESOURCE_LIMITS,
21695 ]));
21696 assert!(ResourceLimits::is_ascending(&[
21697 HAND_AUTHORED_MID_POSTURE,
21698 HAND_AUTHORED_MID_POSTURE,
21699 ]));
21700 }
21701
21702 #[test]
21703 fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21704 // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21705 // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21706 // AGREES with is_ascending's diagonal-duplicate verdict — both
21707 // leq and geq carry reflexivity, so the sequence-level (T, T)
21708 // corner opens at every diagonal-duplicate slice, distinct from
21709 // the set-level (T, F) verdict is_chain / is_antichain carry on
21710 // the same slice.
21711 assert!(ResourceLimits::is_descending(&[
21712 DEFAULT_RESOURCE_LIMITS,
21713 DEFAULT_RESOURCE_LIMITS,
21714 ]));
21715 assert!(ResourceLimits::is_descending(&[
21716 HAND_AUTHORED_MID_POSTURE,
21717 HAND_AUTHORED_MID_POSTURE,
21718 ]));
21719 }
21720
21721 #[test]
21722 fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21723 // Ordered ascending closure — the shipped-preset triple on the
21724 // bounded-lattice diagonal enumerated leq-ascending has
21725 // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21726 // true`, so every consecutive pair passes the conjunction.
21727 // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21728 // the walk closes at every non-degenerate ascending enumeration
21729 // length.
21730 assert!(ResourceLimits::is_ascending(&[
21731 EMPTY_RESOURCE_LIMITS,
21732 DEFAULT_RESOURCE_LIMITS,
21733 ]));
21734 assert!(ResourceLimits::is_ascending(&[
21735 DEFAULT_RESOURCE_LIMITS,
21736 UNBOUNDED_RESOURCE_LIMITS,
21737 ]));
21738 assert!(ResourceLimits::is_ascending(&[
21739 EMPTY_RESOURCE_LIMITS,
21740 DEFAULT_RESOURCE_LIMITS,
21741 UNBOUNDED_RESOURCE_LIMITS,
21742 ]));
21743 }
21744
21745 #[test]
21746 fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21747 // Ordered descending closure — the reversed enumeration is a
21748 // geq-monotone descending sequence. The DIRECT MIRROR of
21749 // is_ascending's ascending closure one PERMUTATION axis over.
21750 assert!(ResourceLimits::is_descending(&[
21751 DEFAULT_RESOURCE_LIMITS,
21752 EMPTY_RESOURCE_LIMITS,
21753 ]));
21754 assert!(ResourceLimits::is_descending(&[
21755 UNBOUNDED_RESOURCE_LIMITS,
21756 DEFAULT_RESOURCE_LIMITS,
21757 ]));
21758 assert!(ResourceLimits::is_descending(&[
21759 UNBOUNDED_RESOURCE_LIMITS,
21760 DEFAULT_RESOURCE_LIMITS,
21761 EMPTY_RESOURCE_LIMITS,
21762 ]));
21763 }
21764
21765 #[test]
21766 fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21767 // Order-sensitivity — the reversed enumeration of the same
21768 // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21769 // the first consecutive pair. is_chain accepts BOTH orderings
21770 // (order-INSENSITIVE); this projection accepts only the leq-
21771 // sorted one — the DISCRIMINATING arm between the sequence-level
21772 // and set-level projections.
21773 assert!(!ResourceLimits::is_ascending(&[
21774 UNBOUNDED_RESOURCE_LIMITS,
21775 DEFAULT_RESOURCE_LIMITS,
21776 EMPTY_RESOURCE_LIMITS,
21777 ]));
21778 }
21779
21780 #[test]
21781 fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21782 // The direct falsification of is_ascending's ascending closure
21783 // one CELL axis over on the SAME slice.
21784 assert!(!ResourceLimits::is_descending(&[
21785 EMPTY_RESOURCE_LIMITS,
21786 DEFAULT_RESOURCE_LIMITS,
21787 UNBOUNDED_RESOURCE_LIMITS,
21788 ]));
21789 }
21790
21791 #[test]
21792 fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21793 // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21794 // is a permutation of the shipped-preset chain, so it IS a chain
21795 // (is_chain accepts it), but the first consecutive pair binds
21796 // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21797 // walk rejects. Also is_descending rejects — the third
21798 // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21799 // the DISCRIMINATING arm between the sequence-level and set-
21800 // level projections at a slice that is a chain but neither
21801 // ascending nor descending.
21802 let slice = [
21803 DEFAULT_RESOURCE_LIMITS,
21804 EMPTY_RESOURCE_LIMITS,
21805 UNBOUNDED_RESOURCE_LIMITS,
21806 ];
21807 assert!(ResourceLimits::is_chain(&slice));
21808 assert!(!ResourceLimits::is_ascending(&slice));
21809 assert!(!ResourceLimits::is_descending(&slice));
21810 }
21811
21812 #[test]
21813 fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21814 // The two hand-authored asymmetric postures are incomparable
21815 // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21816 // consecutive pair rejects. Pinned in both orderings — the pair-
21817 // level leq verdict rejects on the antichain pair regardless of
21818 // which side is `self`.
21819 assert!(!ResourceLimits::is_ascending(&[
21820 HAND_AUTHORED_MID_POSTURE,
21821 HAND_AUTHORED_OTHER_POSTURE,
21822 ]));
21823 assert!(!ResourceLimits::is_ascending(&[
21824 HAND_AUTHORED_OTHER_POSTURE,
21825 HAND_AUTHORED_MID_POSTURE,
21826 ]));
21827 }
21828
21829 #[test]
21830 fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21831 assert!(!ResourceLimits::is_descending(&[
21832 HAND_AUTHORED_MID_POSTURE,
21833 HAND_AUTHORED_OTHER_POSTURE,
21834 ]));
21835 assert!(!ResourceLimits::is_descending(&[
21836 HAND_AUTHORED_OTHER_POSTURE,
21837 HAND_AUTHORED_MID_POSTURE,
21838 ]));
21839 }
21840
21841 #[test]
21842 fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21843 // Transitivity theorem — for every slice `postures`,
21844 // `is_ascending(postures) == true` implies `is_chain(postures)
21845 // == true`. The pointwise partial order is TRANSITIVE, so a
21846 // consecutive-pair leq-chain closes under transitive composition
21847 // into the all-pairs leq-chain. Substrate-level THEOREM: the
21848 // sequence-level ascending verdict is strictly STRONGER than the
21849 // set-level chain verdict. Same identity for is_descending —
21850 // both sequence-level projections imply the set-level chain
21851 // projection.
21852 let presets: [ResourceLimits; 5] = [
21853 EMPTY_RESOURCE_LIMITS,
21854 DEFAULT_RESOURCE_LIMITS,
21855 UNBOUNDED_RESOURCE_LIMITS,
21856 HAND_AUTHORED_MID_POSTURE,
21857 HAND_AUTHORED_OTHER_POSTURE,
21858 ];
21859 // Every arity-2 slice from the 5×5 preset matrix witnesses the
21860 // implication in both directions of the (ascending, descending)
21861 // pair.
21862 for a in presets {
21863 for b in presets {
21864 let slice = [a, b];
21865 if ResourceLimits::is_ascending(&slice) {
21866 assert!(
21867 ResourceLimits::is_chain(&slice),
21868 "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21869 );
21870 }
21871 if ResourceLimits::is_descending(&slice) {
21872 assert!(
21873 ResourceLimits::is_chain(&slice),
21874 "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21875 );
21876 }
21877 }
21878 }
21879 // Load-bearing arity-3 witness — the shipped ascending chain
21880 // triple satisfies is_ascending AND is_chain; the descending
21881 // reversal satisfies is_descending AND is_chain.
21882 let asc = [
21883 EMPTY_RESOURCE_LIMITS,
21884 DEFAULT_RESOURCE_LIMITS,
21885 UNBOUNDED_RESOURCE_LIMITS,
21886 ];
21887 assert!(ResourceLimits::is_ascending(&asc));
21888 assert!(ResourceLimits::is_chain(&asc));
21889 let desc = [
21890 UNBOUNDED_RESOURCE_LIMITS,
21891 DEFAULT_RESOURCE_LIMITS,
21892 EMPTY_RESOURCE_LIMITS,
21893 ];
21894 assert!(ResourceLimits::is_descending(&desc));
21895 assert!(ResourceLimits::is_chain(&desc));
21896 }
21897
21898 #[test]
21899 fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21900 // Empty-and-singleton agreement — the two sequence-level
21901 // projections carry the SAME vacuous-truth verdict at every
21902 // slice with fewer than two consecutive pairs. Pinned as a
21903 // substrate-level identity so a future rewrite of either
21904 // projection cannot silently drift from the shared vacuous-
21905 // truth verdict.
21906 assert_eq!(
21907 ResourceLimits::is_ascending(&[]),
21908 ResourceLimits::is_descending(&[]),
21909 );
21910 let presets: [ResourceLimits; 5] = [
21911 EMPTY_RESOURCE_LIMITS,
21912 DEFAULT_RESOURCE_LIMITS,
21913 UNBOUNDED_RESOURCE_LIMITS,
21914 HAND_AUTHORED_MID_POSTURE,
21915 HAND_AUTHORED_OTHER_POSTURE,
21916 ];
21917 for a in presets {
21918 let single = [a];
21919 assert_eq!(
21920 ResourceLimits::is_ascending(&single),
21921 ResourceLimits::is_descending(&single),
21922 "empty-and-singleton agreement failed on singleton {a:?}",
21923 );
21924 let dup = [a, a];
21925 assert_eq!(
21926 ResourceLimits::is_ascending(&dup),
21927 ResourceLimits::is_descending(&dup),
21928 "diagonal-duplicate agreement failed on {a:?}",
21929 );
21930 }
21931 }
21932
21933 #[test]
21934 fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21935 // Const-fn pin — the sequence-level ascending projection is
21936 // evaluable in const context, so a caller can pin an ascending-
21937 // sequence identity at compile time.
21938 const _: () = assert!(ResourceLimits::is_ascending(&[]));
21939 const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21940 const _: () = assert!(ResourceLimits::is_ascending(&[
21941 EMPTY_RESOURCE_LIMITS,
21942 DEFAULT_RESOURCE_LIMITS,
21943 UNBOUNDED_RESOURCE_LIMITS,
21944 ]));
21945 const _: () = assert!(!ResourceLimits::is_ascending(&[
21946 UNBOUNDED_RESOURCE_LIMITS,
21947 EMPTY_RESOURCE_LIMITS,
21948 ]));
21949 }
21950
21951 #[test]
21952 fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21953 // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21954 // PRIMITIVE axis over.
21955 const _: () = assert!(ResourceLimits::is_descending(&[]));
21956 const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21957 const _: () = assert!(ResourceLimits::is_descending(&[
21958 UNBOUNDED_RESOURCE_LIMITS,
21959 DEFAULT_RESOURCE_LIMITS,
21960 EMPTY_RESOURCE_LIMITS,
21961 ]));
21962 const _: () = assert!(!ResourceLimits::is_descending(&[
21963 EMPTY_RESOURCE_LIMITS,
21964 UNBOUNDED_RESOURCE_LIMITS,
21965 ]));
21966 }
21967}