tatara_lisp/macro_expand.rs
1//! Macro expander — rewrites `defmacro` / `defpoint-template` calls into
2//! their quasi-quoted templates.
3//!
4//! Semantics (v0, no evaluator):
5//!
6//! ```lisp
7//! (defmacro wrap (x) `(list ,x ,x)) ; or defpoint-template
8//! (wrap hello) ; expands to (list hello hello)
9//! ```
10//!
11//! Supported:
12//! - Required params: `(name a b c)`
13//! - Optional params: `(name a &optional b c)` — unsupplied bind to `()`
14//! - Rest param: `(name a &rest rest)`
15//! - Quasi-quote body: `` `(…) ``
16//! - Unquote substitution: `,x`
17//! - Splice substitution: `,@x` (splices a bound list into the outer list)
18//! - Recursive expansion: macro bodies may call other macros.
19//!
20//! Not yet supported (no evaluator):
21//! - Arbitrary expressions under `,` — only bound symbol lookups.
22//! - Nested quasi-quotes.
23//! - Hygiene / gensym — param names capture aggressively.
24
25use std::cmp::Ordering;
26use std::collections::hash_map::DefaultHasher;
27use std::collections::HashMap;
28use std::hash::{Hash, Hasher};
29use std::sync::{Arc, Mutex};
30
31use crate::ast::Sexp;
32use crate::error::{LispError, MacroDefHead, Result, TemplateInvariantKind, UnquoteForm};
33
34/// Default ceiling on `Expander::expand`'s recursive re-entry into a
35/// macro-call form. A macro whose expansion contains another call to
36/// itself — the canonical runaway `(defmacro loop (x) `(loop ,x))` —
37/// pre-lift stack-overflowed the process (a below-floor abort with no
38/// `Result` witness). The ceiling turns that abort into a
39/// [`LispError::ExpansionDepthExceeded`] rejection at the expander
40/// boundary. `256` is large enough that no lawful hand-authored
41/// expansion nesting hits it (nested `when` / `let` / template
42/// compositions cap out in single digits in practice) and small
43/// enough that a runaway is rejected in microseconds instead of
44/// consuming a process stack. Consumers that need a tighter or looser
45/// ceiling call [`Expander::set_max_expansion_depth`].
46pub const DEFAULT_MAX_EXPANSION_DEPTH: usize = 256;
47
48/// Default ceiling on the expansion cache's entry count. Peer to
49/// [`DEFAULT_MAX_EXPANSION_DEPTH`] one RESOURCE axis over on the
50/// expander surface — where the depth ceiling bounds RECURSION (a
51/// runaway macro's stack), this ceiling bounds MEMORY (the memoized
52/// `apply(macro, args)` result table). Pre-lift the cache was an
53/// unbounded [`HashMap`] shared across [`Expander`] clones via
54/// [`std::sync::Arc`]; a long-running host (REPL, LSP,
55/// `tatara-check`, MCP server) that expanded many distinct
56/// `(macro, args)` pairs would accrete cache entries without bound —
57/// the same below-floor resource-drift the depth ceiling closed on
58/// the recursion axis. Post-lift the [`Expander::apply`] insert path
59/// consults this ceiling before growing the cache; once the entry
60/// count reaches the ceiling, new expansions still succeed (the
61/// cache remains a pure PERFORMANCE optimization — never a
62/// CORRECTNESS gate) but skip cache insertion until the operator
63/// clears the cache via [`Expander::clear_cache`]. Consumers that
64/// need a tighter ceiling (embedded / test harnesses) call
65/// [`Expander::set_max_cache_entries`] with a lower cap; consumers
66/// that want unbounded caching (batch compilation runs where memory
67/// is not the concern) call it with [`usize::MAX`].
68///
69/// `8192` is large enough that lawful macro authoring — a small
70/// closed set of macros, each expanded against a bounded set of
71/// argument shapes — never brushes the ceiling on a session-length
72/// workload (a typescape with 20 macros × 32 distinct arg-shapes ×
73/// several call sites still sits under 1K entries), and small
74/// enough that a pathological expansion loop (a macro emitting
75/// unique arg-shapes on every call) is rejected before it exhausts
76/// process memory. The default is deliberately generous — the
77/// ceiling exists to prevent unbounded drift, not to force operators
78/// to tune it. Consumers that want a tighter policy configure it
79/// explicitly.
80pub const DEFAULT_MAX_CACHE_ENTRIES: usize = 8192;
81
82/// Default ceiling on a single macro-expansion output's structural
83/// node count. Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`] and
84/// [`DEFAULT_MAX_CACHE_ENTRIES`] one RESOURCE-DIMENSION axis over on
85/// the expander surface — where the depth ceiling bounds RECURSION
86/// LENGTH (a runaway macro's stack) and the cache ceiling bounds
87/// MEMOIZATION WIDTH (the memoized `apply(macro, args)` table's
88/// entry count), this ceiling bounds OUTPUT SIZE (the "expansion
89/// bomb" axis: a well-defined macro whose depth stays small while
90/// the produced tree grows past any reasonable working-set budget).
91/// Pre-lift the canonical
92/// `(defmacro bomb (x) `(list ,x ,x ,x ,x ,x ,x ,x ,x))` composed
93/// under itself a handful of times exhausted the process heap while
94/// the depth counter was still at low single digits and the cache
95/// entry count sat under any reasonable ceiling — a below-floor
96/// resource-drift the depth AND cache ceilings both admitted through.
97/// Post-lift the [`Expander::expand_with_depth`] site consults this
98/// ceiling on every freshly-applied macro output via
99/// [`crate::ast::Sexp::node_count`]; once an `apply` result exceeds
100/// the ceiling the expander returns
101/// [`crate::error::LispError::ExpansionSizeExceeded`] with
102/// `macro_name`, `size`, and `limit` populated. Consumers that need
103/// a tighter or looser ceiling call
104/// [`Expander::set_max_expansion_size`].
105///
106/// `65_536` is large enough that lawful macro-authored output — a
107/// domain-language body composed from a few dozen typed primitives,
108/// each expanded against a bounded set of arg-shapes — never brushes
109/// the ceiling on a session-length workload (a typical `defpoint` /
110/// `defmonitor` expansion sits under 100 nodes, and even a
111/// hand-authored `defcheck` cascade tops out in the low thousands),
112/// and small enough that a pathological "output bomb" (a macro
113/// producing a 2^N-node blob from a small input) is rejected in
114/// milliseconds instead of consuming multiple gigabytes of process
115/// memory in the worst case. The default is deliberately generous —
116/// the ceiling exists to prevent unbounded drift, not to force
117/// operators to tune it. Consumers that want a tighter policy
118/// configure it explicitly.
119pub const DEFAULT_MAX_EXPANSION_SIZE: usize = 65_536;
120
121/// Default ceiling on a registered macro's BODY structural node count.
122/// Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`], [`DEFAULT_MAX_CACHE_ENTRIES`],
123/// and [`DEFAULT_MAX_EXPANSION_SIZE`] one PIPELINE-STAGE axis over on
124/// the expander surface — the three prior guards close the RESOURCE
125/// surface at EXPAND time (recursion length, memoization width,
126/// single-`apply` output size); this ceiling closes it at REGISTER time
127/// (the SIZE the body itself contributes to storage in
128/// [`Expander::macros`], to [`compile_template`]'s pre-expansion walk,
129/// and to every subsequent [`substitute`] walk that recurses through
130/// the body-shaped template). Pre-lift a pathological
131/// `(defmacro huge (x) `<template-of-N-million-nodes>)` would land the
132/// body in the macros table before any `apply` ran and inflate the
133/// per-registration cost proportional to `N` — a below-floor
134/// resource-drift the three expand-time ceilings all admitted through
135/// because none of them fires at the registration boundary. Post-lift
136/// the [`Expander::register_macro_def`] site consults this ceiling
137/// against `def.body.node_count()`; once the observed size exceeds
138/// the ceiling the expander returns
139/// [`crate::error::LispError::MacroBodySizeExceeded`] with
140/// `macro_name`, `size`, and `limit` populated, and BOTH the
141/// [`Expander::macros`] AND [`Expander::templates`] tables stay
142/// exactly as they were — no partial-write window in which the
143/// registration succeeded halfway.
144///
145/// `16_384` is large enough that lawful hand-authored macro bodies
146/// (an outer quasi-quote wrapping a few dozen nested lists with a
147/// handful of substitution slots) never brush the ceiling on any
148/// realistic authoring workload — even a heavily-composed
149/// `defalertpolicy` template with multi-page cascades tops out in the
150/// low thousands — and small enough that a pathological "authoring
151/// bomb" (a code-generator that unrolls a `Vec<TypedEntity>` into a
152/// giant literal body) is rejected at the registration boundary in
153/// microseconds instead of leaking a multi-megabyte template into the
154/// live macros table. The default is deliberately generous — the
155/// ceiling exists to prevent unbounded drift, not to force operators
156/// to tune it. Consumers that want a tighter or looser ceiling call
157/// [`Expander::set_max_macro_body_size`].
158pub const DEFAULT_MAX_MACRO_BODY_SIZE: usize = 16_384;
159
160/// Default ceiling on the [`Expander::macros`] table's entry count.
161/// Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one RESOURCE-DIMENSION axis
162/// over on the REGISTER-time surface — where the body-size ceiling
163/// bounds a single macro's BODY SIZE (the per-registration authoring
164/// bomb), this ceiling bounds TABLE ENTRY COUNT (the cumulative
165/// registration bomb: a code-generator emitting unbounded fresh
166/// `(defmacro N (x) `(list ,x))` heads whose bodies each sit
167/// comfortably under [`DEFAULT_MAX_MACRO_BODY_SIZE`] yet collectively
168/// saturate process memory). Also peer to [`DEFAULT_MAX_CACHE_ENTRIES`]
169/// one PIPELINE-STAGE axis over — where the cache-entries ceiling
170/// bounds EXPAND-time memoization width, this ceiling bounds
171/// REGISTER-time macro-table width. The two entry-count guards close
172/// the two pipeline stages symmetrically. Pre-lift the macros table
173/// was an unbounded [`HashMap`]; a long-running host (REPL, LSP,
174/// `tatara-check`, MCP server) that ingested a source stream of
175/// distinct `(defmacro …)` heads would accrete entries without
176/// bound — a below-floor resource-drift the four prior guards (depth,
177/// cache-entries, expansion-size, body-size) all admitted through
178/// because none of them fires against the CUMULATIVE registration
179/// count.
180///
181/// Overwrites (a re-registration of an already-registered key) do not
182/// grow the table and never trigger this ceiling — operators can
183/// redefine a macro at capacity without hitting the gate. Only FRESH
184/// keys are counted against the ceiling.
185///
186/// `4096` is large enough that lawful hand-authored typescapes never
187/// brush the ceiling on any realistic authoring workload — a
188/// typescape with 20-100 macros × several call sites sits well under
189/// 1K entries, and even a library-heavy project composing hundreds of
190/// domain macros stays under a fraction of the default — and small
191/// enough that a pathological registration bomb (a code-generator
192/// unrolling a `Vec<TypedEntity>` into a giant `(defmacro fresh-N …)`
193/// stream) is rejected at the registration boundary in microseconds
194/// instead of leaking a multi-megabyte macros table into the live
195/// expander. The default is deliberately generous — the ceiling
196/// exists to prevent unbounded drift, not to force operators to tune
197/// it. Consumers that want a tighter or looser ceiling call
198/// [`Expander::set_max_registered_macros`].
199pub const DEFAULT_MAX_REGISTERED_MACROS: usize = 4096;
200
201/// Default ceiling on a registered macro's lambda-list ARITY — the
202/// count of parameter slots (`required` + `optional` + `rest?`) the
203/// [`MacroDef`] declares. Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one
204/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
205/// body-size bounds a per-registration BODY node count (the AST-nodes
206/// of the template a macro rewrites INTO), this ceiling bounds a
207/// per-registration PARAM slot count (the fresh symbols
208/// [`compile_template`]'s `,name`-index resolution AND
209/// [`MacroParams::bind`]'s per-index binder walk have to thread through
210/// on every call). Peer to [`DEFAULT_MAX_REGISTERED_MACROS`] one
211/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
212/// the registered-macros ceiling bounds cumulative registration COUNT
213/// (the table-scoped resource), this ceiling bounds a per-registration
214/// PARAM-LIST WIDTH (the per-entry resource). Together the three
215/// REGISTER-time ceilings close the (per-body SIZE, per-body ARITY,
216/// per-table COUNT) three-corner surface — a `Vec<TypedEntity>`
217/// arity-bomb code-generator emitting a
218/// `(defmacro huge (a-1 a-2 … a-N-million) `,a-1)` head with a
219/// single-node body slips past `max_macro_body_size` and past
220/// `max_registered_macros` (one fresh key), yet each subsequent call
221/// pays O(N) walking the required-run binder — a below-floor
222/// resource-drift the two prior REGISTER-time guards admit through
223/// because neither of them fires against the PER-REGISTRATION
224/// PARAM-LIST WIDTH.
225///
226/// The default is deliberately generous — lawful hand-authored macros
227/// carry a handful of required params + a small optional run + at
228/// most a single `&rest` slot (`observability-stack`'s `defalertpolicy`
229/// carries under 8 slots; even keyword-heavy authoring surfaces sit
230/// well under 32 slots). `128` sits an order of magnitude past any
231/// realistic authoring workload yet an order of magnitude BELOW a
232/// pathological code-generator's arity-bomb output, so the ceiling
233/// gates the drift without brushing lawful traffic. Consumers that
234/// want a tighter or looser ceiling call
235/// [`Expander::set_max_macro_arity`].
236///
237/// Frontier inspiration: Common Lisp's `LAMBDA-PARAMETERS-LIMIT`
238/// standard variable (CLHS §3.4.1) — the runtime-reflectable
239/// "upper exclusive bound on the number of parameters that may
240/// appear in a lambda list" every conforming implementation carries.
241/// SBCL's is `4611686018427387903`, ECL's is `4096`, CLisp's is `4096`.
242/// `DEFAULT_MAX_MACRO_ARITY` is the substrate's typed-Rust peer at
243/// compile time, translated through pleme-io primitives: a
244/// `pub const usize` on the typed macro-expander algebra rather than
245/// a runtime-mutable global, wired into the substrate's typed
246/// [`crate::error::LispError`] Result algebra at
247/// [`Expander::register_macro_def`] rather than raised as an
248/// implementation-defined condition.
249pub const DEFAULT_MAX_MACRO_ARITY: usize = 128;
250
251/// Typed snapshot of the six `Expander` resource ceilings — the
252/// bundled peer of the six [`Expander::max_expansion_depth`] /
253/// [`Expander::max_cache_entries`] / [`Expander::max_expansion_size`] /
254/// [`Expander::max_macro_body_size`] / [`Expander::max_registered_macros`] /
255/// [`Expander::max_macro_arity`] individual getters. The six knobs form
256/// ONE resource surface (the (PIPELINE-STAGE × RESOURCE-DIMENSION)
257/// grid the last six commits closed corner-by-corner) yet the
258/// pre-lift `Expander` surface exposed them only as SIX independent
259/// projections — inspecting the full posture required six calls, and
260/// bulk-configuring the expander (a preset for CI, a preset for the
261/// REPL, a preset for a test harness) required six independent
262/// setter invocations whose ordering the type system did not gate.
263///
264/// Post-lift the typed bundle binds the six knobs at ONE typed value
265/// on the `Expander` surface. A `#[derive(Clone, Copy, Debug,
266/// PartialEq, Eq)]` posture — every ceiling is a `Copy` scalar —
267/// gives callers a cheap snapshot they can serialize, compare, or
268/// pass around; the `Copy` implementation guarantees the bundle
269/// never carries a hidden allocation. Adding a SEVENTH ceiling
270/// extends this struct AND [`DEFAULT_RESOURCE_LIMITS`] in lockstep
271/// via rustc's field-exhaustiveness on the `..Default::default()`-free
272/// literal construction — no independent-field-drift window.
273///
274/// Peer of the six `DEFAULT_MAX_*` module constants one AGGREGATION
275/// axis over: where each constant carries ONE ceiling, this struct
276/// carries the SIX-fold cross-product. [`DEFAULT_RESOURCE_LIMITS`]
277/// pins the (defaults × aggregation) corner so a consumer that wants
278/// the shipped posture in one value does not re-derive the six
279/// individual constants at its call site.
280///
281/// Theory grounding: THEORY.md §II.1 — typed entry / typed exit for
282/// the resource-limit configuration surface. The pre-lift six
283/// independent knobs were a set of typed scalars but their
284/// composition into an "Expander resource posture" was untyped —
285/// callers assembled the posture at their call site with no
286/// compile-time exhaustiveness check that all six were considered.
287/// Frontier inspiration: `tokio::runtime::Builder`'s bundled knob
288/// struct — a runtime's ceiling posture is ONE typed value rather
289/// than a chain of independent method calls; translation through
290/// pleme-io primitives is the plain `Copy` snapshot below, no
291/// builder-pattern indirection.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub struct ResourceLimits {
294 /// See [`Expander::max_expansion_depth`].
295 pub max_expansion_depth: usize,
296 /// See [`Expander::max_cache_entries`].
297 pub max_cache_entries: usize,
298 /// See [`Expander::max_expansion_size`].
299 pub max_expansion_size: usize,
300 /// See [`Expander::max_macro_body_size`].
301 pub max_macro_body_size: usize,
302 /// See [`Expander::max_registered_macros`].
303 pub max_registered_macros: usize,
304 /// See [`Expander::max_macro_arity`].
305 pub max_macro_arity: usize,
306}
307
308impl Default for ResourceLimits {
309 /// The shipped posture — every field seeded from its matching
310 /// `DEFAULT_MAX_*` module constant. Pinned equal to
311 /// [`DEFAULT_RESOURCE_LIMITS`] as a compile-time identity so a
312 /// future re-tuning of ONE default lands in ONE place and both
313 /// projections pick it up mechanically.
314 fn default() -> Self {
315 DEFAULT_RESOURCE_LIMITS
316 }
317}
318
319/// The shipped [`ResourceLimits`] posture — every field bound to its
320/// matching `DEFAULT_MAX_*` module constant at ONE typed value. This
321/// is the `const` peer of [`ResourceLimits::default`]; the two are
322/// pinned equal so a call-site that wants the shipped posture as a
323/// compile-time value binds through the `const` and a call-site that
324/// wants it as a runtime `Default::default()` value binds through the
325/// trait impl — both routes reach the SAME six numbers.
326///
327/// Adding a SEVENTH ceiling extends this constant AND the struct AND
328/// [`Expander`]'s constructors in lockstep — rustc's
329/// field-exhaustiveness on this `..Default::default()`-free literal
330/// forces the new field to appear here, and the trait impl above
331/// reuses this constant so no independent-default-drift window opens
332/// between the two projections.
333pub const DEFAULT_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
334 max_expansion_depth: DEFAULT_MAX_EXPANSION_DEPTH,
335 max_cache_entries: DEFAULT_MAX_CACHE_ENTRIES,
336 max_expansion_size: DEFAULT_MAX_EXPANSION_SIZE,
337 max_macro_body_size: DEFAULT_MAX_MACRO_BODY_SIZE,
338 max_registered_macros: DEFAULT_MAX_REGISTERED_MACROS,
339 max_macro_arity: DEFAULT_MAX_MACRO_ARITY,
340};
341
342/// The unbounded [`ResourceLimits`] posture — every ceiling seeded to
343/// [`usize::MAX`] at ONE typed value. This is the CEILING-LIFTED peer of
344/// [`DEFAULT_RESOURCE_LIMITS`] one PRESET-POSTURE axis over on the
345/// named-preset surface: where the default constant binds each field to
346/// the shipped `DEFAULT_MAX_*` module constant, this constant binds
347/// each field to the raw `usize::MAX` sentinel that every
348/// [`Expander::set_max_expansion_depth`] / [`Expander::set_max_cache_entries`] /
349/// [`Expander::set_max_expansion_size`] / [`Expander::set_max_macro_body_size`] /
350/// [`Expander::set_max_registered_macros`] / [`Expander::set_max_macro_arity`]
351/// setter's doc names as "the ceiling is effectively lifted" —
352/// admitting any lawful value the type system supports.
353///
354/// A test-harness fixture that wants "exercise the raw expander with no
355/// ceiling gating anything" pre-lift composed the posture at its call
356/// site as six independent [`usize::MAX`] literals (or six independent
357/// setter invocations post-`Expander::new`) whose exhaustiveness the
358/// type system did NOT gate — a copy-paste that dropped one of the six
359/// lifts left THAT ceiling at its default and gated the fixture
360/// silently. Post-lift the posture is ONE named typed constant every
361/// such fixture routes through, AND rustc's field-exhaustiveness on
362/// the `..Default::default()`-free literal here forces a SEVENTH
363/// ceiling extension to appear at this site in lockstep with
364/// [`DEFAULT_RESOURCE_LIMITS`] and the [`ResourceLimits`] struct
365/// itself — no independent-preset-drift window.
366///
367/// Peer to [`DEFAULT_RESOURCE_LIMITS`] on the (posture × aggregation)
368/// grid: both constants live at the AGGREGATION corner (six ceilings
369/// as ONE typed value); the posture axis distinguishes the SHIPPED
370/// default from the CEILING-LIFTED unbounded preset. Disagrees with
371/// [`DEFAULT_RESOURCE_LIMITS`] on every field (every default is a
372/// concrete non-[`usize::MAX`] positive constant, so the six-way
373/// disagreement is structurally exhaustive) — pinned as a typed
374/// theorem in the [`ResourceLimits`] test cohort.
375///
376/// Frontier inspiration: `tokio::runtime::Builder`'s pattern of
377/// exposing named preset thunks (`multi_thread`, `current_thread`)
378/// alongside the field-level configurators, so a caller that wants a
379/// standard posture binds through ONE named entry rather than
380/// composing the field literals at the call site. Translation
381/// through pleme-io primitives is the plain `const` snapshot below,
382/// no builder-pattern indirection — the `Copy` posture on
383/// [`ResourceLimits`] already gives callers the struct-update
384/// literal (`ResourceLimits { max_expansion_depth: 4,
385/// ..UNBOUNDED_RESOURCE_LIMITS }`) that isolates the ONE ceiling
386/// still gating from the five lifted.
387pub const UNBOUNDED_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
388 max_expansion_depth: usize::MAX,
389 max_cache_entries: usize::MAX,
390 max_expansion_size: usize::MAX,
391 max_macro_body_size: usize::MAX,
392 max_registered_macros: usize::MAX,
393 max_macro_arity: usize::MAX,
394};
395
396/// The zero [`ResourceLimits`] posture — every ceiling seeded to `0` at
397/// ONE typed value. This is the BOTTOM peer of
398/// [`UNBOUNDED_RESOURCE_LIMITS`] one LATTICE-POLE axis over on the
399/// (bottom, top) bounded-lattice preset-pair surface: where the
400/// ceiling-lifted preset carries every gate at [`usize::MAX`]
401/// (admitting every lawful `usize`-indexed value the type system
402/// supports), this preset carries every gate at `0` (rejecting every
403/// non-empty input past the first depth step, cache slot, macro-body
404/// node, macro registration, or arity slot). Together the two close
405/// the (BOTTOM, TOP) preset-posture pair on the bounded-lattice
406/// diagonal, and [`DEFAULT_RESOURCE_LIMITS`] sits strictly between
407/// them (`EMPTY.leq(DEFAULT) && DEFAULT.leq(UNBOUNDED)`).
408///
409/// **Bounded-lattice identity elements**: [`ResourceLimits`] under the
410/// pointwise `min` / `max` operations forms a BOUNDED lattice; each
411/// operation acquires its identity element from the OPPOSITE pole of
412/// the preset-pair diagonal, and its ANNIHILATOR from the SAME pole.
413/// * [`Self::strictest`] (meet, pointwise `min`) —
414/// [`UNBOUNDED_RESOURCE_LIMITS`] is the identity
415/// (`a.strictest(UNBOUNDED) == a` for every posture, since pointwise
416/// `min` against [`usize::MAX`] returns `a`); this constant is the
417/// ANNIHILATOR (`a.strictest(EMPTY) == EMPTY` for every posture,
418/// since pointwise `min` against `0` returns `0`).
419/// * [`Self::most_permissive`] (join, pointwise `max`) — this
420/// constant is the identity (`a.most_permissive(EMPTY) == a` for
421/// every posture, since pointwise `max` against `0` returns `a`);
422/// [`UNBOUNDED_RESOURCE_LIMITS`] is the ANNIHILATOR
423/// (`a.most_permissive(UNBOUNDED) == UNBOUNDED`).
424/// * [`Self::leq`] (partial order) — this constant is the MINIMUM
425/// (`EMPTY.leq(a) == true` for every posture, since `0 <= x` holds
426/// for every `usize x`); [`UNBOUNDED_RESOURCE_LIMITS`] is the
427/// MAXIMUM (`a.leq(UNBOUNDED) == true` for every posture, since
428/// `x <= usize::MAX` holds for every `usize x`).
429///
430/// **Fold-identity use case**: a caller aggregating a slice of
431/// postures through [`Self::most_permissive`] (computing the pointwise
432/// least upper bound across the slice) seeds the fold from this
433/// constant — the join-identity property guarantees the seed does not
434/// distort the fold's result:
435///
436/// ```rust,ignore
437/// let joined = postures.iter().copied()
438/// .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
439/// ```
440///
441/// The dual pattern (folding through [`Self::strictest`] to compute
442/// the pointwise greatest lower bound) seeds from
443/// [`UNBOUNDED_RESOURCE_LIMITS`] — the meet-identity from the OPPOSITE
444/// pole. Pre-lift, an aggregator either (a) picked ONE arbitrary
445/// element of the slice as the seed and folded through the tail — a
446/// non-total fixup that panicked on an empty slice — or (b) seeded
447/// from `Some(first)`-style option wrapper — a runtime `Option` that
448/// carried a type-level admission the aggregate might be absent even
449/// though the algebra defines it exhaustively via its identity. Post-
450/// lift the fold takes ONE typed identity element and handles empty
451/// slices as the algebraic identity value — a boundary case which the
452/// bounded-lattice structure resolves at TYPE level rather than the
453/// caller resolving at CALL SITE with a wrapper type.
454///
455/// Adding a SEVENTH ceiling extends this constant AND the struct AND
456/// [`DEFAULT_RESOURCE_LIMITS`] AND [`UNBOUNDED_RESOURCE_LIMITS`] in
457/// lockstep — rustc's field-exhaustiveness on this
458/// `..Default::default()`-free literal forces the new field to appear
459/// at this site, and the identity-law tests pin `0` as the seventh
460/// field's value at compile time via the const-fn peer of the
461/// existing lattice-law const-fn pins.
462///
463/// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
464/// proofs; the identity element of a lattice operation is itself a
465/// typed named entry whose composition with any other element is the
466/// element (join) or the identity (meet). THEORY.md §V.1 — knowable
467/// platform; the bounded-lattice bottom binds at ONE typed constant
468/// rather than a six-primitive inline literal every consumer that
469/// needs the fold identity carries at its call site. Frontier
470/// inspiration: the algebraic-datatypes tradition of a `Monoid` typeclass
471/// (Haskell's `mempty`, Scala's `Monoid.empty`, Rust's various
472/// `Default::default` for `Sum`/`Product` newtypes) — a caller
473/// aggregating values through a binary combinator binds its identity
474/// element at ONE named entry rather than composing the identity
475/// literal at every fold site. Translation through pleme-io primitives
476/// is the plain `const` snapshot below, no typeclass indirection — the
477/// `Copy` posture on [`ResourceLimits`] already gives callers the
478/// struct-update literal (`ResourceLimits { max_macro_arity: 3,
479/// ..EMPTY_RESOURCE_LIMITS }`) that isolates the ONE ceiling admitting
480/// non-zero input from the five sealed at `0`.
481pub const EMPTY_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
482 max_expansion_depth: 0,
483 max_cache_entries: 0,
484 max_expansion_size: 0,
485 max_macro_body_size: 0,
486 max_registered_macros: 0,
487 max_macro_arity: 0,
488};
489
490/// Pointwise `min` / `max` primitives the [`ResourceLimits`] lattice binds
491/// its meet ([`ResourceLimits::strictest`]) and join
492/// ([`ResourceLimits::most_permissive`]) operations to. Named at module
493/// scope as `const fn` so both operations are themselves `const fn` on
494/// stable — composing two postures at compile time (a caller stitching a
495/// new `pub const` preset from two shipped presets) needs no runtime
496/// evaluation. Ternary form rather than [`usize::min`] / [`usize::max`]
497/// so the two `const fn` remain valid on any tatara MSRV whose stability
498/// on the primitive const-fn methods has not been established.
499const fn min_usize(a: usize, b: usize) -> usize {
500 if a <= b {
501 a
502 } else {
503 b
504 }
505}
506
507const fn max_usize(a: usize, b: usize) -> usize {
508 if a >= b {
509 a
510 } else {
511 b
512 }
513}
514
515impl ResourceLimits {
516 /// Pointwise-`min` across the six ceilings — the STRICTEST posture
517 /// whose admissible input set is the intersection of `self`'s and
518 /// `other`'s admissible input sets. `a.strictest(b)` admits an
519 /// input iff BOTH `a` AND `b` admit it: on every field the
520 /// per-axis ceiling is the smaller of the two, so a value that
521 /// clears the meet clears BOTH postures, and a value that any
522 /// posture rejects the meet also rejects.
523 ///
524 /// The meet of the `ResourceLimits` lattice under the pointwise
525 /// partial order `a ≤ b iff every field of a ≤ every field of b`
526 /// (tighter-first — a POSTURE admits FEWER inputs on that field's
527 /// axis when the ceiling is smaller). Peer of
528 /// [`Self::most_permissive`] one COMBINATOR axis over on the
529 /// lattice-algebra surface: where `most_permissive` is the join
530 /// (pointwise-`max`, LUB), this is the meet (pointwise-`min`, GLB).
531 /// Together the two operations close the (meet, join) lattice
532 /// operator pair on the [`ResourceLimits`] posture algebra.
533 ///
534 /// The concrete identity on the shipped preset pair holds
535 /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.strictest(`
536 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`DEFAULT_RESOURCE_LIMITS`].
537 /// Every `DEFAULT_MAX_*` module constant is a concrete positive
538 /// value strictly less than [`usize::MAX`], so on every axis the
539 /// pointwise-`min` picks the DEFAULT side; the six-axis pin holds
540 /// as a typed theorem in the test cohort.
541 ///
542 /// Idempotent (`a.strictest(a) == a`), commutative
543 /// (`a.strictest(b) == b.strictest(a)`), associative
544 /// (`(a.strictest(b)).strictest(c) == a.strictest(b.strictest(c))`),
545 /// and satisfies the absorption identity with the join
546 /// (`a.strictest(a.most_permissive(b)) == a`) — all pinned as
547 /// typed theorems in the test cohort. Distributive over the join
548 /// (`a.strictest(b.most_permissive(c)) ==
549 /// a.strictest(b).most_permissive(a.strictest(c))`) because
550 /// pointwise-`min` and pointwise-`max` on [`usize`] each
551 /// distribute over the other on every axis.
552 ///
553 /// `const fn` so a caller can compose two named presets into a
554 /// third at compile time (`pub const CI_RESOURCE_LIMITS:
555 /// ResourceLimits = SOME_OPERATOR_PRESET.strictest(
556 /// SOME_CI_PRESET);`) rather than deferring the composition to
557 /// a runtime `Default::default()` chain.
558 ///
559 /// Pre-lift, a caller wanting the tightest common posture across
560 /// two shipped presets composed the intersection at its call site
561 /// as six independent `usize::min(a.max_X, b.max_X)` invocations
562 /// stitched into a fresh `ResourceLimits { ... }` literal — the
563 /// same six-inline-primitive shape the pre-`ResourceLimits`
564 /// bundled `Expander` posture required its callers to carry, and
565 /// the same exhaustiveness gap the `..Default::default()`-free
566 /// literal on the bundled struct exists to close: a copy-paste
567 /// that dropped ONE of the six `min` invocations left THAT
568 /// ceiling at whichever posture the caller happened to seed the
569 /// literal with, and the type system did not gate the drop.
570 /// Post-lift the composition binds at ONE typed method on the
571 /// posture algebra — a caller writes
572 /// `operator_preset.strictest(ci_preset)` and rustc's
573 /// field-exhaustiveness at the const's construction guarantees
574 /// every ceiling is threaded through the `min` primitive.
575 ///
576 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
577 /// preserves proofs; the tightest posture of two preset-carried
578 /// resource proofs is itself a `ResourceLimits` whose proof
579 /// content is the intersection of the two, and this method is
580 /// the typed named entry composing them. THEORY.md §V.1 —
581 /// knowable platform; the pointwise-`min` combinator becomes a
582 /// TYPE-level operation on the posture algebra rather than an
583 /// inline six-primitive cascade at every consumer that composes
584 /// two presets. `tatara-lattice`'s `meet` / `join` operators on
585 /// `Classification` (see `pleme-io/tatara` workspace crate
586 /// docstring: "Lattice algebra over Classification — `meet` /
587 /// `join` / `leq` / `Baseline`") close the SAME shape one layer
588 /// down: an entity's classification is a lattice; a resource
589 /// posture is a lattice; both algebras carry the (meet, join)
590 /// operator pair as their fundamental composition primitives.
591 #[must_use]
592 pub const fn strictest(self, other: Self) -> Self {
593 Self {
594 max_expansion_depth: min_usize(self.max_expansion_depth, other.max_expansion_depth),
595 max_cache_entries: min_usize(self.max_cache_entries, other.max_cache_entries),
596 max_expansion_size: min_usize(self.max_expansion_size, other.max_expansion_size),
597 max_macro_body_size: min_usize(self.max_macro_body_size, other.max_macro_body_size),
598 max_registered_macros: min_usize(
599 self.max_registered_macros,
600 other.max_registered_macros,
601 ),
602 max_macro_arity: min_usize(self.max_macro_arity, other.max_macro_arity),
603 }
604 }
605
606 /// Pointwise-`max` across the six ceilings — the MOST-PERMISSIVE
607 /// posture whose admissible input set is the union of `self`'s
608 /// and `other`'s admissible input sets. `a.most_permissive(b)`
609 /// admits an input iff EITHER `a` OR `b` admits it: on every
610 /// field the per-axis ceiling is the larger of the two, so a
611 /// value that either posture accepts the join accepts, and a
612 /// value the join rejects both postures reject.
613 ///
614 /// The join of the `ResourceLimits` lattice under the pointwise
615 /// partial order `a ≤ b iff every field of a ≤ every field of b`
616 /// (tighter-first). Peer of [`Self::strictest`] one COMBINATOR
617 /// axis over on the lattice-algebra surface: where `strictest` is
618 /// the meet (pointwise-`min`, GLB), this is the join
619 /// (pointwise-`max`, LUB). Together the two operations close the
620 /// (meet, join) lattice operator pair on the [`ResourceLimits`]
621 /// posture algebra.
622 ///
623 /// The concrete identity on the shipped preset pair holds
624 /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.most_permissive(`
625 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`UNBOUNDED_RESOURCE_LIMITS`].
626 /// Every `DEFAULT_MAX_*` module constant is a concrete positive
627 /// value strictly less than [`usize::MAX`], so on every axis the
628 /// pointwise-`max` picks the UNBOUNDED side; the six-axis pin
629 /// holds as a typed theorem in the test cohort.
630 ///
631 /// Idempotent, commutative, associative, and satisfies the
632 /// absorption identity with the meet
633 /// (`a.most_permissive(a.strictest(b)) == a`) — all pinned as
634 /// typed theorems in the test cohort. Distributive over the meet
635 /// (`a.most_permissive(b.strictest(c)) ==
636 /// a.most_permissive(b).strictest(a.most_permissive(c))`).
637 ///
638 /// `const fn` so a caller can compose two named presets into a
639 /// third at compile time rather than deferring the composition
640 /// to a runtime `Default::default()` chain.
641 ///
642 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
643 /// preserves proofs; the most-permissive posture of two preset-
644 /// carried resource proofs is itself a `ResourceLimits` whose
645 /// proof content is the union of the two, and this method is the
646 /// typed named entry composing them. THEORY.md §V.1 — knowable
647 /// platform; the pointwise-`max` combinator becomes a TYPE-level
648 /// operation on the posture algebra rather than an inline
649 /// six-primitive cascade at every consumer that composes two
650 /// presets.
651 #[must_use]
652 pub const fn most_permissive(self, other: Self) -> Self {
653 Self {
654 max_expansion_depth: max_usize(self.max_expansion_depth, other.max_expansion_depth),
655 max_cache_entries: max_usize(self.max_cache_entries, other.max_cache_entries),
656 max_expansion_size: max_usize(self.max_expansion_size, other.max_expansion_size),
657 max_macro_body_size: max_usize(self.max_macro_body_size, other.max_macro_body_size),
658 max_registered_macros: max_usize(
659 self.max_registered_macros,
660 other.max_registered_macros,
661 ),
662 max_macro_arity: max_usize(self.max_macro_arity, other.max_macro_arity),
663 }
664 }
665
666 /// Pointwise partial-order relation across the six ceilings — `self`
667 /// is at least as tight as `other` iff every field of `self` is at
668 /// most the matching field of `other`. `a.leq(b)` holds iff every
669 /// input the `a` posture admits the `b` posture also admits: `a`'s
670 /// per-axis ceilings all sit at or below `b`'s per-axis ceilings, so
671 /// any value that clears `a`'s tighter gates also clears `b`'s
672 /// looser gates.
673 ///
674 /// The partial order underneath the [`ResourceLimits`] lattice.
675 /// Peer of [`Self::strictest`] (meet, GLB) and [`Self::most_permissive`]
676 /// (join, LUB) one PRIMITIVE-KIND axis over on the lattice-algebra
677 /// surface: where those two are the (meet, join) COMBINATORS, this
678 /// is the RELATION they are defined against. Together the three
679 /// close the (meet, join, leq) primitive triple on the
680 /// [`ResourceLimits`] posture algebra — the same triple
681 /// `tatara-lattice`'s [`Lattice`] trait exposes as its three
682 /// required methods for every classification lattice one crate over
683 /// (see `tatara-lattice/src/lib.rs`).
684 ///
685 /// **Meet-agreement** (`a.leq(b) ⇔ a.strictest(b) == a`): if `a` is
686 /// tighter on every axis than `b`, then the pointwise `min` picks
687 /// `a` on every axis, so `a.strictest(b) == a`; conversely, if
688 /// `a.strictest(b) == a`, every field's `min` picked `a`, so every
689 /// field of `a` is at most the matching field of `b`. Pinned as
690 /// `resource_limits_leq_agrees_with_meet` in the test cohort.
691 ///
692 /// **Join-agreement** (`a.leq(b) ⇔ a.most_permissive(b) == b`):
693 /// symmetric statement on the LUB side; pinned as
694 /// `resource_limits_leq_agrees_with_join`. Together the two
695 /// agreements are the canonical lattice cross-check axiom
696 /// `a ≤ b ⇔ a ⊓ b = a ⇔ a ⊔ b = b` documented in
697 /// `tatara-lattice/src/lib.rs`'s preamble.
698 ///
699 /// Reflexive (`a.leq(a) == true`), antisymmetric (`a.leq(b) &&
700 /// b.leq(a) → a == b`), transitive (`a.leq(b) && b.leq(c) →
701 /// a.leq(c)`) — all pinned as typed theorems in the test cohort.
702 /// NOT total: two postures with reversed per-axis orderings (some
703 /// axes where `a` is tighter, some where `b` is tighter) are
704 /// incomparable — `!a.leq(b) && !b.leq(a)` — pinned by
705 /// `resource_limits_leq_is_not_total_on_asymmetric_postures`.
706 ///
707 /// **Bounds pins in terms of leq**:
708 /// * `a.strictest(b).leq(a) && a.strictest(b).leq(b)` (meet is
709 /// the greatest lower bound) — pinned by
710 /// `resource_limits_strictest_is_leq_both_operands`, the typed
711 /// lattice-relation companion to the per-axis
712 /// `resource_limits_strictest_is_dominated_by_both_operands_pointwise`.
713 /// * `a.leq(a.most_permissive(b)) && b.leq(a.most_permissive(b))`
714 /// (join is the least upper bound) — pinned by
715 /// `resource_limits_most_permissive_is_geq_both_operands`, the
716 /// typed lattice-relation companion to the per-axis
717 /// `resource_limits_most_permissive_dominates_both_operands_pointwise`.
718 ///
719 /// **Concrete-preset pin**: [`DEFAULT_RESOURCE_LIMITS`]`.leq(`
720 /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == true` (every
721 /// `DEFAULT_MAX_*` module constant is a concrete positive value at
722 /// most [`usize::MAX`]) AND
723 /// `!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS)` (every
724 /// `DEFAULT_MAX_*` is strictly less than [`usize::MAX`], so on
725 /// every axis [`usize::MAX`] exceeds the matching default) — the
726 /// partial order is a STRICT order on the shipped preset pair.
727 /// Pinned by `resource_limits_leq_of_default_and_unbounded_is_a_strict_order`.
728 ///
729 /// `const fn` so a caller can pin a preset-relation identity at
730 /// compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
731 /// .leq(UNBOUNDED_RESOURCE_LIMITS));`) — the typed lattice-relation
732 /// peer of the `const fn` composition ability on
733 /// [`Self::strictest`] / [`Self::most_permissive`].
734 ///
735 /// Pre-lift, a caller wanting to decide whether one posture was
736 /// tighter than another either (a) composed six independent
737 /// per-axis `<=` comparisons at the call site stitched into a
738 /// six-fold `&&` — the same six-inline-primitive shape the
739 /// pre-`ResourceLimits` bundled Expander posture required its
740 /// callers to carry, and the same exhaustiveness gap the
741 /// `..Default::default()`-free literal on the bundled struct exists
742 /// to close: a copy-paste that dropped ONE of the six comparisons
743 /// silently admitted a posture that violated tightness on THAT
744 /// axis — or (b) invoked [`Self::strictest`] and compared the result
745 /// to `self` — a correct but roundabout composition of the partial-
746 /// order primitive against its lattice-companion. Post-lift the
747 /// relation binds at ONE typed method on the posture algebra; a
748 /// caller writes `tighter.leq(looser)` and rustc's exhaustiveness
749 /// on the six-field conjunction below guarantees every ceiling is
750 /// threaded through the `<=` primitive.
751 ///
752 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
753 /// preserves proofs; the partial-order relation on two preset-
754 /// carried resource proofs is itself a typed named entry composing
755 /// their tightness ordering. THEORY.md §V.1 — knowable platform;
756 /// the pointwise `<=` combinator becomes a TYPE-level operation on
757 /// the posture algebra rather than an inline six-primitive
758 /// conjunction at every consumer that compares two presets.
759 /// `tatara-lattice`'s [`Lattice::leq`] method on `Classification`
760 /// closes the SAME shape one layer down — an entity's
761 /// classification is a lattice with a `leq` partial order; a
762 /// resource posture is a lattice with a `leq` partial order; both
763 /// algebras carry the (meet, join, leq) primitive triple as their
764 /// fundamental relation-and-combinator surface.
765 #[must_use]
766 pub const fn leq(self, other: Self) -> bool {
767 self.max_expansion_depth <= other.max_expansion_depth
768 && self.max_cache_entries <= other.max_cache_entries
769 && self.max_expansion_size <= other.max_expansion_size
770 && self.max_macro_body_size <= other.max_macro_body_size
771 && self.max_registered_macros <= other.max_registered_macros
772 && self.max_macro_arity <= other.max_macro_arity
773 }
774
775 /// Pointwise-`min` fold across a slice of postures — the STRICTEST
776 /// posture whose admissible input set is the intersection of every
777 /// operand's admissible input sets. `ResourceLimits::strictest_of(&[a,
778 /// b, c])` admits an input iff EVERY operand admits it: on every
779 /// field the per-axis ceiling is the smallest across the whole slice,
780 /// so a value that clears the aggregated meet clears EVERY operand,
781 /// and a value that ANY operand rejects the aggregated meet also
782 /// rejects.
783 ///
784 /// The N-ary MEET aggregation on the [`ResourceLimits`] lattice —
785 /// the extension of [`Self::strictest`] from a 2-input pairwise
786 /// combinator to an N-input fold across a slice. Seeded from
787 /// [`UNBOUNDED_RESOURCE_LIMITS`] (the meet-identity — its per-axis
788 /// [`usize::MAX`] never wins the pointwise `min` against a concrete
789 /// positive value, so the seed contributes nothing to the fold's
790 /// result), which makes the empty-slice case aggregate to the
791 /// identity element rather than requiring an `Option<Self>` wrapper
792 /// the caller inspects at every consumption site. Peer of
793 /// [`Self::most_permissive_of`] one COMBINATOR axis over on the
794 /// N-ary aggregation surface: where `most_permissive_of` is the
795 /// N-ary JOIN (LUB across the slice, seeded from the join-identity
796 /// [`EMPTY_RESOURCE_LIMITS`]), this is the N-ary MEET (GLB across
797 /// the slice, seeded from the meet-identity). Together the two
798 /// close the (N-ary meet, N-ary join) aggregation pair the pairwise
799 /// combinators [`Self::strictest`] / [`Self::most_permissive`]
800 /// extend from 2 to N.
801 ///
802 /// **Empty-slice identity**: `strictest_of(&[]) ==
803 /// UNBOUNDED_RESOURCE_LIMITS` — the null aggregate binds structurally
804 /// to the meet-identity element. Pre-lift a caller composing "the
805 /// tightest posture across THIS SLICE" from the pairwise `strictest`
806 /// primitive either (a) seeded the fold from a non-empty first
807 /// element and panicked on empty input, or (b) wrapped the aggregate
808 /// in `Option<Self>` — a runtime type-level admission the aggregate
809 /// might be absent even though the algebra's bounded-lattice
810 /// structure defines the null aggregate exhaustively via its
811 /// identity element. Post-lift the null aggregate is
812 /// [`UNBOUNDED_RESOURCE_LIMITS`] at the TYPE level, not a wrapper
813 /// the consumer inspects.
814 ///
815 /// **Single-element identity**: `strictest_of(&[a]) == a` — the
816 /// 1-input aggregate is the input verbatim, since
817 /// `UNBOUNDED.strictest(a) == a` for every posture `a` by the
818 /// meet-identity law.
819 ///
820 /// **Two-element identity**: `strictest_of(&[a, b]) ==
821 /// a.strictest(b)` — the 2-input aggregate reduces to the pairwise
822 /// combinator, since `UNBOUNDED.strictest(a).strictest(b) ==
823 /// a.strictest(b)`.
824 ///
825 /// **Order-independent**: `strictest_of(&[a, b, c]) ==
826 /// strictest_of(&[c, b, a])` — the fold inherits the commutativity
827 /// AND associativity of the pairwise `strictest`, so any permutation
828 /// of the slice yields the same aggregate.
829 ///
830 /// `const fn` so a caller can compose an arbitrary-length preset
831 /// slice into a third preset at compile time (`pub const
832 /// COMBINED_PRESET: ResourceLimits = ResourceLimits::strictest_of(
833 /// &[SOME_OPERATOR_PRESET, SOME_CI_PRESET, SOME_HOSTILE_PRESET]);`)
834 /// rather than deferring the composition to a runtime chain — the
835 /// N-ary const-fn peer of the pairwise const-fn on
836 /// [`Self::strictest`]. `Copy` on [`ResourceLimits`] lets the const-
837 /// fn loop index the slice by value without an explicit `.clone()`
838 /// (which const-fn would not permit anyway).
839 ///
840 /// Pre-lift, the SAME `.iter().copied().fold(UNBOUNDED, strictest)`
841 /// three-primitive cascade appeared verbatim in
842 /// `unbounded_resource_limits_seeds_strictest_fold_over_slice` AND at
843 /// every future consumer that would compose "the tightest across
844 /// this set of presets" — a PRIME DIRECTIVE ≥2 trigger, since the
845 /// fold docstring on [`UNBOUNDED_RESOURCE_LIMITS`] explicitly names
846 /// this pattern as the constant's fold-identity use case. Post-lift
847 /// the N-ary aggregation binds at ONE typed method the algebra
848 /// exposes, and every future consumer routes through ONE name whose
849 /// signature carries the identity-element choice INTO the method
850 /// (rather than the consumer having to remember which seed pairs
851 /// with which combinator at each call site).
852 ///
853 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
854 /// preserves proofs; the N-ary aggregation of N preset-carried
855 /// resource proofs is itself a `ResourceLimits` whose proof content
856 /// is the intersection of every operand's, and this method is the
857 /// typed named entry composing them. THEORY.md §V.1 — knowable
858 /// platform; the N-ary meet becomes a TYPE-level operation on the
859 /// posture algebra rather than an inline fold cascade at every
860 /// consumer that aggregates a slice of presets — with the
861 /// meet-identity seed baked in so the consumer cannot pair the join-
862 /// identity (`EMPTY`) with the meet combinator (a silent
863 /// distortion that would collapse every axis to `0`). Frontier
864 /// inspiration: the algebraic-datatypes tradition of a `Monoid`
865 /// typeclass exposing `mconcat` / `Monoid.combineAll` alongside the
866 /// pairwise `mappend` — the N-ary aggregation of a collection
867 /// through a binary combinator is a first-class named method the
868 /// typeclass carries. Translation through pleme-io primitives is
869 /// the plain `const fn` slice-fold below, no typeclass indirection
870 /// — the meet-identity element already exists as a named `pub const`
871 /// on the algebra so the fold picks it up structurally.
872 #[must_use]
873 pub const fn strictest_of(postures: &[Self]) -> Self {
874 let mut acc = UNBOUNDED_RESOURCE_LIMITS;
875 let mut i = 0;
876 while i < postures.len() {
877 acc = acc.strictest(postures[i]);
878 i += 1;
879 }
880 acc
881 }
882
883 /// Pointwise-`max` fold across a slice of postures — the MOST-
884 /// PERMISSIVE posture whose admissible input set is the union of
885 /// every operand's admissible input sets.
886 /// `ResourceLimits::most_permissive_of(&[a, b, c])` admits an input
887 /// iff ANY operand admits it: on every field the per-axis ceiling
888 /// is the largest across the whole slice.
889 ///
890 /// The N-ary JOIN aggregation on the [`ResourceLimits`] lattice —
891 /// the extension of [`Self::most_permissive`] from a 2-input
892 /// pairwise combinator to an N-input fold across a slice. Seeded
893 /// from [`EMPTY_RESOURCE_LIMITS`] (the join-identity — every field
894 /// at `0`, which never wins the pointwise `max` against any
895 /// non-zero value, so the seed contributes nothing to the fold's
896 /// result), which makes the empty-slice case aggregate to the
897 /// identity element. Peer of [`Self::strictest_of`] one COMBINATOR
898 /// axis over on the N-ary aggregation surface: where `strictest_of`
899 /// is the N-ary MEET (seeded from the meet-identity
900 /// [`UNBOUNDED_RESOURCE_LIMITS`]), this is the N-ary JOIN (seeded
901 /// from the join-identity). Together the two close the (N-ary meet,
902 /// N-ary join) aggregation pair.
903 ///
904 /// **Empty-slice identity**: `most_permissive_of(&[]) ==
905 /// EMPTY_RESOURCE_LIMITS` — the null aggregate binds structurally
906 /// to the join-identity element.
907 ///
908 /// **Single-element identity**: `most_permissive_of(&[a]) == a` —
909 /// the 1-input aggregate is the input verbatim, since
910 /// `EMPTY.most_permissive(a) == a` for every posture `a` by the
911 /// join-identity law.
912 ///
913 /// **Two-element identity**: `most_permissive_of(&[a, b]) ==
914 /// a.most_permissive(b)` — the 2-input aggregate reduces to the
915 /// pairwise combinator.
916 ///
917 /// **Order-independent**: `most_permissive_of(&[a, b, c]) ==
918 /// most_permissive_of(&[c, b, a])` — the fold inherits the
919 /// commutativity AND associativity of the pairwise
920 /// `most_permissive`.
921 ///
922 /// `const fn` so a caller can compose an arbitrary-length preset
923 /// slice at compile time — the N-ary const-fn peer of the pairwise
924 /// const-fn on [`Self::most_permissive`].
925 ///
926 /// Pre-lift, the SAME `.iter().copied().fold(EMPTY,
927 /// most_permissive)` three-primitive cascade appeared verbatim in
928 /// `empty_resource_limits_seeds_most_permissive_fold_over_slice`
929 /// AND at every future consumer that would compose "the loosest
930 /// across this set of presets" — a PRIME DIRECTIVE ≥2 trigger.
931 /// Post-lift the N-ary aggregation binds at ONE typed method with
932 /// the join-identity seed baked in so the consumer cannot pair the
933 /// meet-identity (`UNBOUNDED`) with the join combinator (a silent
934 /// distortion that would inflate every axis to [`usize::MAX`]).
935 ///
936 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
937 /// preserves proofs; the N-ary join of N preset-carried resource
938 /// proofs is itself a `ResourceLimits`. THEORY.md §V.1 — knowable
939 /// platform; the N-ary join becomes a TYPE-level operation on the
940 /// posture algebra rather than an inline fold cascade at every
941 /// consumer that aggregates a slice of presets.
942 #[must_use]
943 pub const fn most_permissive_of(postures: &[Self]) -> Self {
944 let mut acc = EMPTY_RESOURCE_LIMITS;
945 let mut i = 0;
946 while i < postures.len() {
947 acc = acc.most_permissive(postures[i]);
948 i += 1;
949 }
950 acc
951 }
952
953 /// Pointwise projection of `self` into the closed range `[lower, upper]`
954 /// — the bounded-lattice bracket combinator. `a.clamp(lower, upper)`
955 /// returns the posture whose per-axis ceiling is the input's own
956 /// ceiling raised to at least `lower`'s and then lowered to at most
957 /// `upper`'s: on every field the result is `min(max(a, lower), upper)`.
958 ///
959 /// The BRACKET operation on the [`ResourceLimits`] lattice — the
960 /// composition of [`Self::most_permissive`] with `lower` followed by
961 /// [`Self::strictest`] with `upper`. Peer of the (meet, join) pairwise
962 /// combinators one ARITY axis over (2 → 3 inputs) on the lattice-
963 /// algebra surface: where those two combine a `self` with ONE bound,
964 /// this combines a `self` with TWO bounds and closes the (unbounded-
965 /// side, bounded) row on the combinator-arity face. The bounded-lattice
966 /// analog of [`Ord::clamp`] on total orders, and the natural typed
967 /// entry a caller enforces "this posture must sit within the
968 /// `[ORG_MIN, ORG_MAX]` operator bracket" through.
969 ///
970 /// **In-range identity**: if `lower.leq(self) && self.leq(upper)` (the
971 /// input already sits within the bracket), then `a.clamp(lower, upper)
972 /// == a` — the pointwise `max(a, lower)` picks `a` on every axis (since
973 /// `lower ≤ a` per-axis), then pointwise `min(..., upper)` picks `a`
974 /// again (since `a ≤ upper` per-axis). Pinned as
975 /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`.
976 ///
977 /// **Below-lower floor**: if `self.leq(lower)` and `lower.leq(upper)`
978 /// (input tighter than the bracket floor, and floor within ceiling),
979 /// then `a.clamp(lower, upper) == lower` — the pointwise `max` lifts
980 /// every axis to `lower`, then the pointwise `min` keeps `lower` (since
981 /// `lower ≤ upper` per-axis). Pinned as
982 /// `resource_limits_clamp_of_posture_below_lower_returns_lower`.
983 ///
984 /// **Above-upper ceiling**: if `upper.leq(self)` and `lower.leq(upper)`
985 /// (input looser than the bracket ceiling), then `a.clamp(lower, upper)
986 /// == upper` — the pointwise `max` keeps `self` on every axis (since
987 /// `lower ≤ self` per-axis via transitivity through `upper`), then the
988 /// pointwise `min` lowers every axis to `upper`. Pinned as
989 /// `resource_limits_clamp_of_posture_above_upper_returns_upper`.
990 ///
991 /// **Bracket-membership** (`lower.leq(a.clamp(lower, upper)) &&
992 /// a.clamp(lower, upper).leq(upper)` for every `a` when `lower.leq(upper)`):
993 /// the result always sits within the `[lower, upper]` bracket
994 /// regardless of the input's position relative to the bracket. This is
995 /// the DEFINING contract of the clamp primitive — every consumer that
996 /// invokes it does so precisely to guarantee the result sits within
997 /// the bracket, and the type system now carries that guarantee at ONE
998 /// named entry. Pinned as
999 /// `resource_limits_clamp_result_sits_within_the_bracket`.
1000 ///
1001 /// **Extrema-bracket identity**: `a.clamp(EMPTY_RESOURCE_LIMITS,
1002 /// UNBOUNDED_RESOURCE_LIMITS) == a` — clamping against the bounded-
1003 /// lattice extrema is the identity function, since every posture
1004 /// already sits within the widest possible bracket. This is the
1005 /// bounded-lattice cross-check with the identity elements previously
1006 /// lifted (`EMPTY_RESOURCE_LIMITS` as the leq-minimum and
1007 /// `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum). Pinned as
1008 /// `resource_limits_clamp_with_lattice_extrema_returns_the_input`.
1009 ///
1010 /// **Degenerate-bracket collapse**: `a.clamp(x, x) == x` for every `a,
1011 /// x` — a zero-width bracket collapses every input to the single point
1012 /// `x`. Pinned as
1013 /// `resource_limits_clamp_with_equal_bounds_returns_the_bound`.
1014 ///
1015 /// **Idempotence**: `a.clamp(lower, upper).clamp(lower, upper) ==
1016 /// a.clamp(lower, upper)` — re-applying the same bracket to an
1017 /// already-clamped posture returns the same result, since the clamped
1018 /// value already sits within the bracket. Pinned as
1019 /// `resource_limits_clamp_is_idempotent`.
1020 ///
1021 /// `const fn` so a caller can pre-clamp a shipped preset against an
1022 /// operator-supplied policy bracket at compile time (`pub const
1023 /// POLICY_CLAMPED: ResourceLimits = SOME_PRESET.clamp(
1024 /// ORG_MIN_PRESET, ORG_MAX_PRESET);`) — the typed bracket-combinator
1025 /// peer of the const-fn composition ability on [`Self::strictest`] /
1026 /// [`Self::most_permissive`].
1027 ///
1028 /// Pre-lift, a caller wanting to bracket a posture within a
1029 /// `[lower, upper]` range composed the two-step cascade at its call
1030 /// site as `self.most_permissive(lower).strictest(upper)` — the same
1031 /// PRIME DIRECTIVE ≥2 pattern the pairwise combinators already lifted
1032 /// one arity down from the six-inline-primitive per-axis cascade.
1033 /// The `resource_limits_clamp_agrees_with_direct_two_step_cascade`
1034 /// pin below composes the pre-lift cascade at ONE explicit assertion
1035 /// site (its sole purpose is to pin the method-vs-cascade equivalence)
1036 /// — every future consumer that enforces a
1037 /// policy bracket (an operator-supplied `[ORG_MIN, ORG_MAX]`, a
1038 /// hostile-input `[EMPTY, HOSTILE_MAX]`, a CI-time `[TEST_MIN,
1039 /// TEST_MAX]`) would compose the same cascade at its call site with
1040 /// no exhaustiveness gate on the composition order. Post-lift the
1041 /// bracket binds at ONE typed method whose signature carries the
1042 /// bounds' roles (`lower` first, `upper` second) into the method
1043 /// rather than the consumer having to remember which side pairs with
1044 /// which combinator at each call site — a copy-paste that swapped the
1045 /// two combinators would clip the input to `min(max(a, upper), lower)`
1046 /// (the WRONG bracket order that returns `lower` for every input
1047 /// looser than `lower` regardless of `upper`'s position), a silent
1048 /// distortion the type system did not gate pre-lift and now does
1049 /// through the method's parameter order.
1050 ///
1051 /// **Non-lattice-bracket behaviour** (`!lower.leq(upper)`): the four
1052 /// documented invariants above are stated for the well-formed bracket
1053 /// case where `lower.leq(upper)`. When the two bounds are pointwise-
1054 /// incomparable OR strictly reversed (`upper.leq(lower) && lower !=
1055 /// upper`), the result is pointwise `min(max(a, lower), upper)` which
1056 /// is well-defined but no longer sits within a "bracket" in the
1057 /// intuitive sense — for a reversed bound the pointwise `max` pushes
1058 /// the value up to `lower`, then the pointwise `min` pushes it down to
1059 /// `upper`, producing `upper` at every axis (since `upper < lower`
1060 /// per-axis makes `min(anything ≥ lower, upper) == upper`). This
1061 /// matches [`Ord::clamp`]'s "min ≤ max is a precondition, otherwise
1062 /// behaviour is unspecified but well-defined" contract on total orders;
1063 /// callers routing preset pairs through `clamp` maintain the
1064 /// `lower.leq(upper)` invariant at their bounds' construction site,
1065 /// not at every clamp call site.
1066 ///
1067 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1068 /// proofs; the bracket projection of a preset-carried resource proof
1069 /// against two bound preset-carried resource proofs is itself a
1070 /// `ResourceLimits` whose proof content is the input's proof intersected
1071 /// with the ceiling's and unioned with the floor's, and this method is
1072 /// the typed named entry composing them. THEORY.md §V.1 — knowable
1073 /// platform; the bracket combinator becomes a TYPE-level operation on
1074 /// the posture algebra rather than an inline two-step composition at
1075 /// every consumer that enforces a policy range — with the (lower,
1076 /// upper) parameter order baked in so the consumer cannot swap the
1077 /// two combinators (a silent distortion that would clip the input to
1078 /// the wrong bracket order). Frontier inspiration: [`Ord::clamp`] on
1079 /// total orders — the stdlib exposes a first-class named bracket
1080 /// combinator alongside the pairwise `min` / `max`, and the bounded-
1081 /// lattice extension is the same shape one dimension up (per-axis
1082 /// pointwise on a Cartesian product of total orders). Translation
1083 /// through pleme-io primitives is the plain `const fn` composition
1084 /// below, no trait indirection — the pairwise combinators already
1085 /// exist as `const fn` on the algebra so the bracket picks them up
1086 /// structurally.
1087 #[must_use]
1088 pub const fn clamp(self, lower: Self, upper: Self) -> Self {
1089 self.most_permissive(lower).strictest(upper)
1090 }
1091
1092 /// Pointwise bracket-membership predicate — `self` sits within the closed
1093 /// range `[lower, upper]` iff every axis's ceiling satisfies
1094 /// `lower ≤ self ≤ upper`. `a.within(lower, upper)` holds iff
1095 /// `lower.leq(self) && self.leq(upper)`: `self`'s per-axis ceilings all
1096 /// sit at or above `lower`'s AND at or below `upper`'s per-axis ceilings.
1097 ///
1098 /// The boolean PREDICATE peer of the BRACKET COMBINATOR [`Self::clamp`]
1099 /// one PRIMITIVE-KIND axis over on the bracket-primitive surface: where
1100 /// `clamp` PROJECTS an arbitrary input into the `[lower, upper]` bracket,
1101 /// this DECIDES whether the input already sits within it. Peer of
1102 /// [`Self::leq`] one ARITY axis over on the lattice-relation surface:
1103 /// where `leq` is the pairwise partial-order RELATION between two
1104 /// postures, this is the three-input CONTAINMENT RELATION deciding
1105 /// whether a posture sits between two bounds. Together the (leq, within)
1106 /// pair closes the (2-input, 3-input) row on the relation-arity face of
1107 /// the [`ResourceLimits`] lattice-algebra surface — the boolean twin of
1108 /// the (`strictest`+`most_permissive`, `clamp`) combinator-arity closure.
1109 ///
1110 /// **Clamp fixed-point theorem** (`a.within(lower, upper) ⇔
1111 /// a.clamp(lower, upper) == a`): the input sits within the bracket iff
1112 /// the clamp is the identity on it. The forward direction is the
1113 /// in-range identity arm of [`Self::clamp`] (already pinned as
1114 /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`);
1115 /// the reverse follows from the bracket-membership contract on `clamp`
1116 /// (the clamp result always sits within the bracket, so if the clamp
1117 /// equals the input, the input also sits within the bracket). Pinned as
1118 /// `resource_limits_within_agrees_with_clamp_fixed_point` — the CANONICAL
1119 /// cross-check axiom binding the predicate to the combinator, analogous
1120 /// to the meet-agreement (`a.leq(b) ⇔ a.strictest(b) == a`) and
1121 /// join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`) axioms binding
1122 /// the `leq` relation to its combinator peers.
1123 ///
1124 /// **In-range identity**: if `lower.leq(self) && self.leq(upper)`
1125 /// (the input already sits within the bracket), then `a.within(lower,
1126 /// upper) == true`. Pinned as
1127 /// `resource_limits_within_of_posture_in_range_is_true`.
1128 ///
1129 /// **Below-lower rejection**: if the input is strictly tighter than the
1130 /// bracket floor on any axis (`!lower.leq(self)`), then
1131 /// `a.within(lower, upper) == false` regardless of `self`'s relation to
1132 /// `upper` — the boolean conjunction rejects the input. Pinned as
1133 /// `resource_limits_within_of_posture_below_lower_is_false`.
1134 ///
1135 /// **Above-upper rejection**: if the input is strictly looser than the
1136 /// bracket ceiling on any axis (`!self.leq(upper)`), then
1137 /// `a.within(lower, upper) == false`. Pinned as
1138 /// `resource_limits_within_of_posture_above_upper_is_false`.
1139 ///
1140 /// **Extrema-bracket identity**: `a.within(EMPTY_RESOURCE_LIMITS,
1141 /// UNBOUNDED_RESOURCE_LIMITS) == true` for every posture, since every
1142 /// posture sits within the widest possible bracket — the bounded-lattice
1143 /// cross-check with the identity elements previously lifted. Pinned as
1144 /// `resource_limits_within_with_lattice_extrema_is_true`.
1145 ///
1146 /// **Degenerate-bracket collapse**: `a.within(x, x) ⇔ a == x` — a
1147 /// zero-width bracket admits only the single point `x`. Follows from
1148 /// antisymmetry of `leq`: `x.leq(a) && a.leq(x) → a == x`. Pinned as
1149 /// `resource_limits_within_of_equal_bounds_iff_equal_to_bound`.
1150 ///
1151 /// **Reflexive-bracket identity**: `a.within(a, a) == true` for every
1152 /// posture — every input trivially sits within the zero-width bracket
1153 /// at itself, since `a.leq(a)` holds by reflexivity of `leq`. Pinned as
1154 /// `resource_limits_within_of_self_is_reflexive`.
1155 ///
1156 /// `const fn` so a caller can pin a preset-bracket-membership identity
1157 /// at compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
1158 /// .within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS));`) — the
1159 /// typed bracket-predicate peer of the const-fn evaluability on
1160 /// [`Self::leq`] and [`Self::clamp`].
1161 ///
1162 /// Pre-lift, a caller wanting to decide whether a posture sat within a
1163 /// `[lower, upper]` bracket composed the two-primitive scaffolding at its
1164 /// call site as `lower.leq(a) && a.leq(upper)` — the same PRIME DIRECTIVE
1165 /// ≥2 pattern the pairwise `strictest` / `most_permissive` / `leq`
1166 /// combinators already lifted one arity down from six-inline-primitive
1167 /// per-axis cascades. The clamp test cohort exercises this shape at
1168 /// every prerequisite pin (`FLOOR.leq(MID) && MID.leq(CEILING)`) AND at
1169 /// the bracket-membership contract's core assertion (`FLOOR.leq(clamped)
1170 /// && clamped.leq(CEILING)`); post-lift both bindings route through ONE
1171 /// named method whose signature carries the containment relation into the
1172 /// type system rather than the consumer composing the two-primitive
1173 /// conjunction at every call site — a copy-paste that swapped the
1174 /// bounds would test `upper.leq(a) && a.leq(lower)` (the WRONG
1175 /// containment direction returning `true` only for postures that
1176 /// simultaneously exceed the ceiling AND fall below the floor, i.e.
1177 /// only for the impossible `upper.leq(a) && a.leq(lower)` case when
1178 /// `lower.leq(upper)`), a silent distortion the type system did not gate
1179 /// pre-lift and now does through the method's parameter order.
1180 ///
1181 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1182 /// proofs; the bracket-membership predicate on three preset-carried
1183 /// resource proofs is itself a typed named entry composing the input's
1184 /// containment ordering. THEORY.md §V.1 — knowable platform; the
1185 /// pointwise `≤`-conjunction combinator becomes a TYPE-level operation
1186 /// on the posture algebra rather than an inline two-primitive
1187 /// conjunction at every consumer that decides bracket membership — with
1188 /// the (lower, upper) parameter order baked in so the consumer cannot
1189 /// swap the two bounds. Frontier inspiration: the interval-containment
1190 /// predicate is the natural boolean twin of stdlib's [`Ord::clamp`] —
1191 /// stdlib lacks an `is_in_range(min, max)` method, but the bounded-
1192 /// lattice extension of the pattern is straightforward on the pointwise
1193 /// partial order and closes the (combinator, predicate) row on the
1194 /// bracket-primitive surface. Translation through pleme-io primitives is
1195 /// the plain `const fn` `leq`-conjunction below, no trait indirection —
1196 /// the partial-order relation already exists as a `const fn` on the
1197 /// algebra so the predicate picks it up structurally.
1198 #[must_use]
1199 pub const fn within(self, lower: Self, upper: Self) -> bool {
1200 lower.leq(self) && self.leq(upper)
1201 }
1202
1203 /// Boolean `leq`-conjunction across a slice of postures — `self` sits
1204 /// at-or-below every operand in `postures`. `a.is_lower_bound_of(&[b,
1205 /// c, d])` holds iff `a.leq(b) && a.leq(c) && a.leq(d)`: on every
1206 /// operand `self`'s per-axis ceilings sit at-or-below the operand's,
1207 /// so `self` is a common lower bound for the slice.
1208 ///
1209 /// The N-ary boolean PREDICATE peer of the pairwise partial-order
1210 /// relation [`Self::leq`] one ARITY axis over on the lattice-relation
1211 /// surface: where `leq` decides the two-input partial-order between
1212 /// two postures, this decides the (1 + N)-input containment-from-below
1213 /// between `self` and a slice of postures. Peer of
1214 /// [`Self::strictest_of`] one PRIMITIVE-KIND axis over on the N-ary
1215 /// aggregation surface: where `strictest_of` COMPUTES the meet (the
1216 /// greatest lower bound of the slice), this DECIDES whether `self` is
1217 /// a lower bound of the slice (member of the lower-bound set the meet
1218 /// is the largest element of). Together `strictest_of` and
1219 /// `is_lower_bound_of` close the (combinator, predicate) row on the
1220 /// N-ary-aggregation face, exactly the way (`strictest`,
1221 /// `most_permissive`) ↔ `leq` closes the pairwise row and (`clamp` ↔
1222 /// `within`) closes the bracket row on the (combinator, predicate) ×
1223 /// (pairwise, N-ary, bracket) primitive surface.
1224 ///
1225 /// **Empty-slice vacuous truth**: `a.is_lower_bound_of(&[]) == true`
1226 /// for every posture — the empty conjunction is vacuously true, since
1227 /// every posture is trivially a lower bound of the empty set. Peer of
1228 /// `strictest_of(&[]) == UNBOUNDED_RESOURCE_LIMITS`'s empty-slice
1229 /// identity: the empty slice aggregates to the identity element of
1230 /// the underlying operation (`true` for boolean conjunction,
1231 /// [`UNBOUNDED_RESOURCE_LIMITS`] for pointwise `min`), so the empty
1232 /// aggregate never rejects. Pinned as
1233 /// `resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true`.
1234 ///
1235 /// **Single-element identity**: `a.is_lower_bound_of(&[b]) ==
1236 /// a.leq(b)` — the 1-input predicate reduces to the pairwise
1237 /// relation, the same way `strictest_of(&[a]) == a` reduces the
1238 /// 1-input N-ary combinator to the operand verbatim. Pinned as
1239 /// `resource_limits_is_lower_bound_of_single_element_reduces_to_leq`.
1240 ///
1241 /// **Meet witness**: `strictest_of(&postures).is_lower_bound_of(&
1242 /// postures) == true` for every slice — the N-ary meet is always a
1243 /// lower bound of the slice it aggregates. The definitional link
1244 /// between the N-ary COMBINATOR and the N-ary PREDICATE: the
1245 /// aggregate `strictest_of` produces is a member of the lower-bound
1246 /// set `is_lower_bound_of` characterizes. Pinned as
1247 /// `resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice`.
1248 ///
1249 /// **Universal-bottom witness**: `EMPTY_RESOURCE_LIMITS
1250 /// .is_lower_bound_of(&postures) == true` for every slice — the
1251 /// lattice bottom is a common lower bound of every set, since
1252 /// `EMPTY.leq(a) == true` for every posture by the lattice-bottom
1253 /// axiom (`0 ≤ x` per-axis). Pinned as
1254 /// `resource_limits_empty_is_universal_lower_bound_of_every_slice`.
1255 ///
1256 /// **Any-operand rejection**: if `!self.leq(postures[i])` for any
1257 /// `i`, then `a.is_lower_bound_of(&postures) == false` — the
1258 /// conjunction short-circuits on the first violating operand and
1259 /// rejects. Pinned as
1260 /// `resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self`.
1261 ///
1262 /// **Peer** — [`Self::is_upper_bound_of`] one COMBINATOR-DIRECTION
1263 /// axis over: the two close the (`is_lower_bound_of`,
1264 /// `is_upper_bound_of`) N-ary boolean-conjunction pair the pairwise
1265 /// `leq` extends from 2 to N, itself peer of the (`strictest_of`,
1266 /// `most_permissive_of`) N-ary combinator pair one PRIMITIVE-KIND
1267 /// axis over.
1268 ///
1269 /// `const fn` so a caller can pin an N-ary-bound-membership identity
1270 /// at compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS
1271 /// .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS,
1272 /// UNBOUNDED_RESOURCE_LIMITS]));`) — the N-ary predicate peer of the
1273 /// const-fn evaluability on [`Self::leq`] and [`Self::within`].
1274 /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the
1275 /// slice by value without an explicit `.clone()` (which const-fn
1276 /// would not permit anyway).
1277 ///
1278 /// Pre-lift, a caller wanting to decide whether a posture was a
1279 /// common lower bound for a slice composed the
1280 /// `postures.iter().all(|p| self.leq(*p))` two-primitive scaffolding
1281 /// at every call site — the same PRIME DIRECTIVE ≥2 pattern the
1282 /// pairwise `leq` combinator already lifted one arity down from the
1283 /// six-inline-primitive per-axis cascade, and the same shape the
1284 /// pairwise `within` combinator already lifted one arity down from
1285 /// the two-primitive `lower.leq(a) && a.leq(upper)` conjunction.
1286 /// Post-lift the N-ary predicate binds at ONE typed method whose
1287 /// signature carries the containment direction (`self ≤ every
1288 /// operand`) into the type system rather than the consumer composing
1289 /// the `all()`-conjunction at every call site — a copy-paste that
1290 /// flipped the leq direction would test `postures[i].leq(self)` (the
1291 /// dual `is_upper_bound_of` question returning `true` only when
1292 /// `self` DOMINATES rather than UNDERLIES the slice), a silent
1293 /// distortion the method's parameter direction now gates.
1294 ///
1295 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1296 /// preserves proofs; the N-ary containment-from-below predicate on
1297 /// (1 + N) preset-carried resource proofs is itself a typed named
1298 /// entry composing the pairwise partial-order relation. THEORY.md
1299 /// §V.1 — knowable platform; the N-ary `leq`-conjunction becomes a
1300 /// TYPE-level operation on the posture algebra rather than an inline
1301 /// two-primitive conjunction at every consumer that decides
1302 /// N-ary-bound membership. Frontier inspiration: the algebraic-
1303 /// datatypes tradition of a `Foldable` typeclass exposing `all` /
1304 /// `Data.Foldable.all` alongside the pairwise relation — the N-ary
1305 /// traversal of a collection through a boolean-conjunction seed is a
1306 /// first-class named method the typeclass carries. Translation
1307 /// through pleme-io primitives is the plain `const fn` slice-walk
1308 /// below, no typeclass indirection — the partial-order relation
1309 /// already exists as a `const fn` on the algebra so the predicate
1310 /// picks it up structurally.
1311 #[must_use]
1312 pub const fn is_lower_bound_of(self, postures: &[Self]) -> bool {
1313 let mut i = 0;
1314 while i < postures.len() {
1315 if !self.leq(postures[i]) {
1316 return false;
1317 }
1318 i += 1;
1319 }
1320 true
1321 }
1322
1323 /// Boolean `leq`-conjunction across a slice of postures — every operand
1324 /// in `postures` sits at-or-below `self`. `a.is_upper_bound_of(&[b, c,
1325 /// d])` holds iff `b.leq(a) && c.leq(a) && d.leq(a)`: on every operand
1326 /// the operand's per-axis ceilings sit at-or-below `self`'s, so `self`
1327 /// is a common upper bound for the slice.
1328 ///
1329 /// The DUAL N-ary predicate of [`Self::is_lower_bound_of`] one
1330 /// COMBINATOR-DIRECTION axis over: the two close the (lower, upper)
1331 /// N-ary boolean-conjunction pair the pairwise `leq` extends from 2
1332 /// to N, itself peer of the (`strictest_of`, `most_permissive_of`)
1333 /// N-ary combinator pair one PRIMITIVE-KIND axis over on the N-ary-
1334 /// aggregation face of the (combinator, predicate) × (pairwise,
1335 /// N-ary, bracket) primitive surface. Peer of
1336 /// [`Self::most_permissive_of`] one PRIMITIVE-KIND axis over on the
1337 /// N-ary aggregation surface: where `most_permissive_of` COMPUTES
1338 /// the join (the least upper bound of the slice), this DECIDES
1339 /// whether `self` is an upper bound of the slice (member of the
1340 /// upper-bound set the join is the smallest element of).
1341 ///
1342 /// **Empty-slice vacuous truth**: `a.is_upper_bound_of(&[]) == true`
1343 /// for every posture — the empty conjunction is vacuously true. Peer
1344 /// of `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
1345 /// slice identity: the empty aggregate binds structurally to the
1346 /// identity element of the underlying operation. Pinned as
1347 /// `resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true`.
1348 ///
1349 /// **Single-element identity**: `a.is_upper_bound_of(&[b]) ==
1350 /// b.leq(a)` — the 1-input predicate reduces to the pairwise
1351 /// relation with the direction flipped (since `self` sits ABOVE the
1352 /// operand rather than BELOW it). Pinned as
1353 /// `resource_limits_is_upper_bound_of_single_element_reduces_to_leq`.
1354 ///
1355 /// **Join witness**: `most_permissive_of(&postures).is_upper_bound_of(&
1356 /// postures) == true` for every slice — the N-ary join is always an
1357 /// upper bound of the slice it aggregates. The dual definitional link
1358 /// between N-ary COMBINATOR and N-ary PREDICATE: the aggregate
1359 /// `most_permissive_of` produces is a member of the upper-bound set
1360 /// `is_upper_bound_of` characterizes. Pinned as
1361 /// `resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice`.
1362 ///
1363 /// **Universal-top witness**: `UNBOUNDED_RESOURCE_LIMITS
1364 /// .is_upper_bound_of(&postures) == true` for every slice — the
1365 /// lattice top is a common upper bound of every set, since
1366 /// `a.leq(UNBOUNDED) == true` for every posture by the lattice-top
1367 /// axiom (`x ≤ usize::MAX` per-axis). Pinned as
1368 /// `resource_limits_unbounded_is_universal_upper_bound_of_every_slice`.
1369 ///
1370 /// **Any-operand rejection**: if `!postures[i].leq(self)` for any
1371 /// `i`, then `a.is_upper_bound_of(&postures) == false` — the
1372 /// conjunction short-circuits and rejects. Pinned as
1373 /// `resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self`.
1374 ///
1375 /// `const fn` so a caller can pin an N-ary-bound-membership identity
1376 /// at compile time. See [`Self::is_lower_bound_of`] for the shared
1377 /// pre-lift ≥2 pattern (`postures.iter().all(|p| p.leq(*self))`), the
1378 /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1379 /// `Foldable::all` frontier inspiration — this method carries the
1380 /// dual `postures[i].leq(self)` direction rather than
1381 /// `self.leq(postures[i])`.
1382 #[must_use]
1383 pub const fn is_upper_bound_of(self, postures: &[Self]) -> bool {
1384 let mut i = 0;
1385 while i < postures.len() {
1386 if !postures[i].leq(self) {
1387 return false;
1388 }
1389 i += 1;
1390 }
1391 true
1392 }
1393
1394 /// Strict pointwise partial-order relation across the six ceilings —
1395 /// `self` is STRICTLY tighter than `other` iff `self.leq(other)` AND
1396 /// `self != other`. `a.lt(b)` holds iff every input the `a` posture
1397 /// admits the `b` posture also admits AND there is at least one axis
1398 /// on which `a`'s ceiling is strictly smaller than `b`'s: `a`'s per-
1399 /// axis ceilings all sit at-or-below `b`'s AND at least one field
1400 /// closes the inequality strictly, so `b` strictly extends `a`'s
1401 /// admissible input set on at least one axis.
1402 ///
1403 /// The STRICT peer of [`Self::leq`] one STRICTNESS axis over on the
1404 /// pairwise partial-order surface: where `leq` is the REFLEXIVE
1405 /// non-strict `≤` relation (`a.leq(a) == true`), this is the
1406 /// IRREFLEXIVE strict `<` relation (`a.lt(a) == false`). Together
1407 /// the two close the (non-strict, strict) partial-order pair on the
1408 /// pairwise-relation face of the (combinator, predicate) ×
1409 /// (strictness) primitive surface — itself peer of the
1410 /// (`strictest`, `most_permissive`) meet/join combinator pair one
1411 /// PRIMITIVE-KIND axis over (both bind against the same pointwise
1412 /// `≤` ordering).
1413 ///
1414 /// Encoded as `self.leq(other) && !other.leq(self)` — the two-
1415 /// primitive antisymmetric encoding of strict `<` on any partial
1416 /// order, pointwise or otherwise. Equivalent on the
1417 /// [`ResourceLimits`] pointwise partial order (which is
1418 /// antisymmetric) to `self.leq(other) && self != other`:
1419 /// antisymmetry says `self.leq(other) && other.leq(self) ⇒ self ==
1420 /// other`, so `self.leq(other) && !other.leq(self)` iff
1421 /// `self.leq(other) && self != other`. The antisymmetric-leq
1422 /// encoding is chosen because it does NOT rely on
1423 /// [`PartialEq::eq`] being `const fn` (which the derived impl is
1424 /// not on stable) — both [`Self::leq`] calls are already `const fn`
1425 /// on the algebra.
1426 ///
1427 /// **Irreflexivity**: `a.lt(a) == false` for every posture. A
1428 /// posture is never strictly tighter than itself, since
1429 /// `a.leq(a) && !a.leq(a)` short-circuits `false` on the second
1430 /// conjunct. Pinned as
1431 /// `resource_limits_lt_is_irreflexive`.
1432 ///
1433 /// **Asymmetry**: `a.lt(b) ⇒ !b.lt(a)` for every pair. The two
1434 /// directions of the strict relation cannot both hold, since
1435 /// `a.lt(b)` implies `!b.leq(a)`, and `b.lt(a)` requires
1436 /// `b.leq(a)` — the two are contradictory. Pinned as
1437 /// `resource_limits_lt_is_asymmetric`.
1438 ///
1439 /// **Transitivity**: `a.lt(b) && b.lt(c) ⇒ a.lt(c)` for every
1440 /// triple. Inherits from the transitivity of [`Self::leq`] on both
1441 /// conjuncts, with the strictness carried through by the outer
1442 /// antisymmetric leg (if `c.leq(a)` held, transitivity of `leq`
1443 /// would give `b.leq(a)` contradicting `a.lt(b)`). Pinned as
1444 /// `resource_limits_lt_is_transitive`.
1445 ///
1446 /// **Refines [`Self::leq`]**: `a.lt(b) ⇒ a.leq(b)` for every pair
1447 /// — the strict relation refines the non-strict one. Conversely
1448 /// `a.leq(b) && !a.lt(b) ⇒ a == b` (the reflexive tie is exactly
1449 /// the non-strict-minus-strict gap). Pinned as
1450 /// `resource_limits_lt_refines_leq` AND
1451 /// `resource_limits_lt_agrees_with_leq_minus_equality`.
1452 ///
1453 /// **Bounded-lattice diagonal**:
1454 /// `EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS)` AND
1455 /// `DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` AND
1456 /// `EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` — the
1457 /// strict chain across the (bottom, middle, top) preset triple on
1458 /// the bounded-lattice diagonal. Every `DEFAULT_MAX_*` module
1459 /// constant is a concrete positive value strictly less than
1460 /// [`usize::MAX`] and strictly greater than `0`, so the two
1461 /// endpoints and the interior are strictly ordered on every axis.
1462 /// Pinned as `resource_limits_lt_of_bottom_diagonal_pinned`.
1463 ///
1464 /// **Incomparable rejection**: on the two hand-authored asymmetric
1465 /// postures (three axes smaller in each direction), both
1466 /// directions of the strict relation fail (`!a.lt(b) && !b.lt(a)`)
1467 /// because neither `leq` direction holds. The predicate does NOT
1468 /// promote incomparable pairs to a comparison verdict. Pinned as
1469 /// `resource_limits_lt_rejects_incomparable_postures`.
1470 ///
1471 /// **Antisymmetric-encoding cross-check**: `a.lt(b) == (a.leq(b)
1472 /// && !b.leq(a))` at every pair by definition, AND `a.lt(b) ==
1473 /// (a.leq(b) && a != b)` at every pair by antisymmetry. Both
1474 /// encodings are pinned equal to the shipped body across the
1475 /// (empty, default, unbounded, mid, other) 5×5 preset matrix.
1476 /// Pinned as
1477 /// `resource_limits_lt_agrees_with_direct_antisymmetric_encoding`.
1478 ///
1479 /// `const fn` so a caller can pin a strict-order identity at
1480 /// compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(
1481 /// DEFAULT_RESOURCE_LIMITS));`) rather than deferring the
1482 /// composition to a runtime chain — sibling of the const-fn
1483 /// evaluability pin on [`Self::leq`] one STRICTNESS axis over.
1484 ///
1485 /// Pre-lift, a caller wanting "is this posture STRICTLY tighter
1486 /// than that one?" composed the two-primitive
1487 /// `a.leq(b) && !b.leq(a)` (or equivalently `a.leq(b) && a != b`)
1488 /// conjunction at its call site — a PRIME DIRECTIVE ≥2 pattern
1489 /// once two consumers need it, and one that couples the strictness
1490 /// interpretation to the caller's chosen encoding
1491 /// (antisymmetric-leq vs. leq-and-not-equal, which coincide on
1492 /// pointwise partial orders but diverge on non-antisymmetric
1493 /// preorders). Post-lift the strict relation binds at ONE typed
1494 /// method the algebra exposes, and the encoding is a private
1495 /// implementation detail no caller can drift from.
1496 ///
1497 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1498 /// preserves proofs; the strict-tightening relation between two
1499 /// preset-carried resource proofs is itself a typed named `bool`
1500 /// predicate composing them via the underlying `leq`. THEORY.md
1501 /// §V.1 — knowable platform; the strict `<` relation becomes a
1502 /// TYPE-level primitive on the posture algebra rather than an
1503 /// inline two-primitive conjunction at every consumer that
1504 /// decides "did we strictly tighten?" `tatara-lattice`'s partial
1505 /// order on `Classification` closes the SAME shape one layer
1506 /// down: an entity's classification lattice carries a strict `<`
1507 /// peer of its `leq` for the same reason a resource posture does.
1508 /// Frontier inspiration: [`PartialOrd::lt`] on Rust's partial-
1509 /// order trait; [`Ord::lt`] on the total-order trait — the
1510 /// strict variant of `≤` is a first-class named method the
1511 /// standard library exposes rather than leaving each consumer to
1512 /// compose `partial_cmp(&other) == Some(Less)` at its call site.
1513 /// Translation through pleme-io primitives is the plain `const
1514 /// fn` antisymmetric encoding below, no trait indirection — the
1515 /// non-strict [`Self::leq`] already exists as a `const fn` on the
1516 /// algebra so the strict variant picks it up structurally.
1517 #[must_use]
1518 pub const fn lt(self, other: Self) -> bool {
1519 self.leq(other) && !other.leq(self)
1520 }
1521
1522 /// Strict pointwise partial-order relation dual of [`Self::lt`] —
1523 /// `a.gt(b) == b.lt(a)`. `self` is STRICTLY looser than `other`
1524 /// iff `other.leq(self)` AND `self != other`: on every axis
1525 /// `self`'s ceiling sits at-or-above `other`'s, and at least one
1526 /// axis closes the inequality strictly, so `self` strictly extends
1527 /// `other`'s admissible input set on at least one axis.
1528 ///
1529 /// The DIRECTION peer of [`Self::lt`] one DIRECTION axis over on
1530 /// the strict-pairwise-relation face — where `lt` is the strict
1531 /// "below" relation (`a < b`), this is the strict "above"
1532 /// relation (`a > b`). Together the two close the (below, above)
1533 /// direction pair on the strict pairwise relation, exactly as
1534 /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] close
1535 /// the (below, above) direction pair on the non-strict N-ary
1536 /// relation one ARITY axis over.
1537 ///
1538 /// Same laws as [`Self::lt`] with directions flipped:
1539 /// irreflexive (`a.gt(a) == false`), asymmetric
1540 /// (`a.gt(b) ⇒ !b.gt(a)`), transitive
1541 /// (`a.gt(b) && b.gt(c) ⇒ a.gt(c)`); refines a hypothetical `geq`
1542 /// (the reflexive `≥`) exactly as `lt` refines `leq`. Pinned on
1543 /// the shipped preset triangle: `UNBOUNDED_RESOURCE_LIMITS.gt(
1544 /// DEFAULT_RESOURCE_LIMITS) && DEFAULT_RESOURCE_LIMITS.gt(
1545 /// EMPTY_RESOURCE_LIMITS)`.
1546 ///
1547 /// Encoded as `other.lt(self)` — one primitive delegation to
1548 /// [`Self::lt`] so the strict-relation encoding lives at exactly
1549 /// one implementation site, and a future re-derivation of `lt`
1550 /// (e.g. to a different antisymmetry encoding) propagates to `gt`
1551 /// mechanically rather than requiring a per-method fix-up.
1552 ///
1553 /// `const fn` for the same compile-time-pin reasons as
1554 /// [`Self::lt`]. See [`Self::lt`] for the full docstring, the
1555 /// pre-lift `a.leq(b) && !b.leq(a)` two-primitive PRIME DIRECTIVE
1556 /// ≥2 pattern, the theory anchor (THEORY.md §II.1 invariant 5,
1557 /// §V.1), and the [`Ord::gt`] frontier inspiration.
1558 #[must_use]
1559 pub const fn gt(self, other: Self) -> bool {
1560 other.lt(self)
1561 }
1562
1563 /// Non-strict pointwise partial-order relation dual of
1564 /// [`Self::leq`] — `a.geq(b) == b.leq(a)`. `self` is AT-OR-LOOSER-
1565 /// THAN `other` iff on every axis `self`'s ceiling sits at-or-above
1566 /// `other`'s: every input the `other` posture admits the `self`
1567 /// posture also admits, so `self` dominates `other`'s admissible
1568 /// input set pointwise (possibly with equality on some or every
1569 /// axis).
1570 ///
1571 /// The DIRECTION peer of [`Self::leq`] one DIRECTION axis over on
1572 /// the non-strict pairwise-relation face — where `leq` is the
1573 /// non-strict "below" relation (`a ≤ b`), this is the non-strict
1574 /// "above" relation (`a ≥ b`). Together the two close the
1575 /// (below, above) direction pair on the non-strict pairwise
1576 /// relation, exactly as [`Self::lt`] / [`Self::gt`] close the
1577 /// (below, above) direction pair on the STRICT pairwise relation
1578 /// one STRICTNESS axis over. The STRICTNESS peer of [`Self::gt`]
1579 /// one STRICTNESS axis over on the "above" column of the
1580 /// (direction × strictness) 2×2 partial-order face — where `gt` is
1581 /// the IRREFLEXIVE strict `>` relation (`a.gt(a) == false`), this
1582 /// is the REFLEXIVE non-strict `≥` relation (`a.geq(a) == true`).
1583 /// The four methods `(leq, lt, gt, geq)` EXHAUSTIVELY close the
1584 /// (below, above) × (non-strict, strict) 2×2 pairwise partial-order
1585 /// face on [`ResourceLimits`], with each method the single-axis
1586 /// peer of two others (one DIRECTION axis over AND one STRICTNESS
1587 /// axis over).
1588 ///
1589 /// Same laws as [`Self::leq`] with directions flipped:
1590 /// reflexive (`a.geq(a) == true`), antisymmetric
1591 /// (`a.geq(b) && b.geq(a) ⇒ a == b`), transitive
1592 /// (`a.geq(b) && b.geq(c) ⇒ a.geq(c)`); refines [`Self::gt`]
1593 /// (`a.gt(b) ⇒ a.geq(b)`) exactly as `leq` is refined by `lt`.
1594 /// NOT total on incomparable postures: on the two hand-authored
1595 /// asymmetric postures both directions of `geq` fail — the
1596 /// non-strict relation does not promote incomparable pairs to a
1597 /// comparison verdict any more than the strict one does. Pinned on
1598 /// the shipped preset triangle:
1599 /// `UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS) &&
1600 /// DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS)`.
1601 ///
1602 /// Encoded as `other.leq(self)` — one primitive delegation to
1603 /// [`Self::leq`] so the non-strict-relation encoding lives at
1604 /// exactly one implementation site, and a future re-derivation of
1605 /// `leq` (e.g. to a different pointwise conjunction) propagates to
1606 /// `geq` mechanically rather than requiring a per-method fix-up.
1607 /// Mirrors [`Self::gt`]'s `other.lt(self)` delegation one
1608 /// STRICTNESS axis over.
1609 ///
1610 /// **N-ary-predicate link**: `a.geq(b) == a.is_upper_bound_of(&[b])`
1611 /// — the pairwise "above" relation is exactly the 1-input
1612 /// specialization of the N-ary upper-bound predicate. The
1613 /// (`geq`, `is_upper_bound_of`) pair sits on the same ARITY axis
1614 /// as (`leq`, `is_lower_bound_of`) one DIRECTION axis over — every
1615 /// pairwise partial-order relation extends to an N-ary bound-
1616 /// membership predicate the substrate carries as a named entry.
1617 ///
1618 /// `const fn` for the same compile-time-pin reasons as
1619 /// [`Self::leq`] (`const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
1620 /// .geq(DEFAULT_RESOURCE_LIMITS));`). See [`Self::leq`] for the
1621 /// full docstring, the pre-lift six-inline-`>=`-conjunction pattern
1622 /// callers wrote before the pairwise relation was named, the
1623 /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1624 /// [`PartialOrd::ge`] frontier inspiration.
1625 #[must_use]
1626 pub const fn geq(self, other: Self) -> bool {
1627 other.leq(self)
1628 }
1629
1630 /// Antichain characterization on the [`ResourceLimits`] partial
1631 /// order — `true` iff `self` and `other` are INCOMPARABLE, i.e.,
1632 /// NEITHER `self.leq(other)` NOR `self.geq(other)` holds. The two
1633 /// postures then sit on distinct branches of the lattice: at least
1634 /// one axis has `self` strictly below `other`, and at least one
1635 /// axis has `self` strictly above `other`, so neither pointwise-
1636 /// domination direction closes and neither preset dominates the
1637 /// other on every field.
1638 ///
1639 /// The ANTICHAIN CORNER on the (leq, geq, lt, gt, incomparable)
1640 /// pairwise-relation surface — the corner that the four pairwise
1641 /// partial-order predicates leave open. On a TOTAL order every
1642 /// pair of elements sits at exactly one of `{Less, Equal,
1643 /// Greater}`, so `is_incomparable` would fold to `false`
1644 /// unconditionally; on the [`ResourceLimits`] pointwise partial
1645 /// order the antichain is non-empty (witnessed by
1646 /// `HAND_AUTHORED_MID_POSTURE` + `HAND_AUTHORED_OTHER_POSTURE`,
1647 /// which the just-lifted [`Self::leq`] / [`Self::geq`] / [`Self::lt`]
1648 /// / [`Self::gt`] tests already pin as neither-`leq`-nor-`geq`), so
1649 /// the corner carries a genuine bit distinct from
1650 /// `!self.leq(other) && !self.geq(other)` inlined at the callsite.
1651 /// The (leq, geq, lt, gt) 2×2 pairwise partial-order face the just-
1652 /// lifted [`Self::geq`] EXHAUSTIVELY closes at its (below, above) ×
1653 /// (non-strict, strict) grid pins the COMPARABLE cases; THIS
1654 /// projection pins the DISJOINT antichain complement one COVER
1655 /// axis over — the (comparable, incomparable) two-cell partition of
1656 /// the total (ordered pair × verdict) surface.
1657 ///
1658 /// Symmetric on its argument order: `a.is_incomparable(b) ==
1659 /// b.is_incomparable(a)` — the composition `!self.leq(other) &&
1660 /// !self.geq(other)` folds through `self.leq(other) ==
1661 /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1662 /// conjunction's commutativity, so the projection is
1663 /// order-independent on its two inputs. This SYMMETRY POSTURE
1664 /// distinguishes it from [`Self::leq`] / [`Self::lt`] (both
1665 /// antisymmetric — swapping inputs FLIPS the verdict) and from
1666 /// [`Self::within`] (asymmetric — the first input is the target
1667 /// and the last two are the bracket bounds).
1668 ///
1669 /// Irreflexive on its own argument: `a.is_incomparable(a) ==
1670 /// false` — every posture is comparable to itself via
1671 /// [`Self::leq`]'s reflexivity (`a.leq(a) == true`), so the
1672 /// conjunction's `!self.leq(other)` leg forces the verdict to
1673 /// `false` at every diagonal input. Sibling posture to
1674 /// [`Self::lt`]'s irreflexivity one COVER axis over: `lt` is
1675 /// irreflexive because the strict relation rules out equality;
1676 /// THIS projection is irreflexive because the comparable
1677 /// relation already includes equality.
1678 ///
1679 /// **Bottom-pole absorption**: `a.is_incomparable(
1680 /// EMPTY_RESOURCE_LIMITS) == false` on every posture `a` —
1681 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1682 /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1683 /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1684 /// identity, which falsifies the `!self.geq(other)` leg. The
1685 /// bounded-lattice bottom is comparable to every posture. Pinned
1686 /// on the canonical preset roster.
1687 ///
1688 /// **Top-pole absorption**: `a.is_incomparable(
1689 /// UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a` —
1690 /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1691 /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1692 /// usize::MAX` unconditionally), which falsifies the
1693 /// `!self.leq(other)` leg. The bounded-lattice top is comparable
1694 /// to every posture. Pinned on the canonical preset roster.
1695 ///
1696 /// **Antichain load-bearing arm**:
1697 /// `HAND_AUTHORED_MID_POSTURE.is_incomparable(
1698 /// HAND_AUTHORED_OTHER_POSTURE) == true` — the two hand-authored
1699 /// asymmetric postures sit on distinct branches (each is smaller
1700 /// on three axes and larger on the other three), so neither
1701 /// pointwise-domination direction closes and the antichain
1702 /// verdict is `true`. Pairs with
1703 /// `resource_limits_geq_rejects_incomparable_postures` +
1704 /// `resource_limits_leq_rejects_incomparable_postures` one COVER
1705 /// axis over: those pin the two comparable-direction predicates
1706 /// FALSIFY on this pair; THIS projection pins the antichain
1707 /// verdict HOLDS on the SAME pair — the two are the negation-
1708 /// paired sides of the (comparable, incomparable) two-cell
1709 /// partition.
1710 ///
1711 /// **De Morgan duality**: `a.is_incomparable(b) == !(a.leq(b) ||
1712 /// a.geq(b))` — the antichain corner is the SET-COMPLEMENT of
1713 /// the union of the two pointwise-domination directions. Pinned
1714 /// by `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1715 /// which sweeps every ordered pair in the preset roster and
1716 /// confirms the equivalence at every cell.
1717 ///
1718 /// Encoded as `!self.leq(other) && !self.geq(other)` — one
1719 /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1720 /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1721 /// antichain characterization lives at exactly one implementation
1722 /// site, and a future re-derivation of [`Self::leq`] (e.g. to a
1723 /// different pointwise conjunction) propagates to
1724 /// `is_incomparable` mechanically rather than requiring a per-
1725 /// method fix-up. Mirrors [`Self::gt`]'s `other.lt(self)`
1726 /// delegation and [`Self::geq`]'s `other.leq(self)` delegation one
1727 /// COVER axis over on the pairwise-relation surface.
1728 ///
1729 /// `const fn` for the same compile-time-pin reasons as
1730 /// [`Self::leq`] (`const _: () =
1731 /// assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(
1732 /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin an antichain-
1733 /// disagreement identity at compile time — a build-break rather
1734 /// than a runtime `assert!` on the first execution.
1735 ///
1736 /// Pre-lift, a caller that wanted "these two postures are
1737 /// incomparable on the pointwise partial-order" composed
1738 /// `!a.leq(b) && !b.leq(a)` at the callsite (or the algebraically
1739 /// equivalent `!a.leq(b) && !a.geq(b)` after `geq` landed), a
1740 /// two-primitive composition that appeared verbatim at every
1741 /// prospective antichain-detection site pre-lift — a ≥2 PRIME
1742 /// DIRECTIVE trigger with the (comparable, incomparable) two-cell
1743 /// partition of the ordered pair × verdict surface as its
1744 /// substrate posture. Post-lift the antichain characterization is
1745 /// ONE named primitive on the [`ResourceLimits`] surface,
1746 /// `const fn`-composable into a compile-time bound, and the two-
1747 /// primitive `!leq && !geq` composition lives at ONE
1748 /// implementation site.
1749 ///
1750 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1751 /// preserves proofs; the antichain characterization is the
1752 /// SET-COMPLEMENT of the union of the two comparable-direction
1753 /// predicates, and pinning the projection at ONE typed primitive
1754 /// makes the (comparable, incomparable) two-cell partition of the
1755 /// ordered-pair × verdict surface a substrate-level THEOREM
1756 /// rather than a per-consumer inline `!leq && !geq` sanity check.
1757 /// THEORY.md §V.1 — knowable platform; the antichain-detection
1758 /// corner was an unnamed two-primitive composition recurring at
1759 /// every prospective "these two postures sit on distinct
1760 /// branches?" callsite pre-lift.
1761 ///
1762 /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1763 /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` returning
1764 /// `Nothing` on incomparable inputs — the antichain corner is
1765 /// exactly the `isNothing` projection of that method; Julia's
1766 /// `Base.Order.NoOrder` sentinel on the same abstract; Rust's own
1767 /// `PartialOrd::partial_cmp` returning `Option<Ordering>` whose
1768 /// `None` arm is the antichain corner; Coq's `PreOrder` +
1769 /// classical LEM-derived `~ (leq a b) /\ ~ (leq b a)` proof
1770 /// obligation on the antichain arm. Translation through pleme-io
1771 /// primitives: the antichain characterization on the closed-set-
1772 /// paired [`ResourceLimits`] posture binds through the just-
1773 /// lifted [`Self::leq`] and [`Self::geq`] primitives (both const-
1774 /// evaluable, both delegating to [`Self::leq`]) with a two-arm
1775 /// conjunction on the negated forms — no new dep, no supertrait
1776 /// bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no
1777 /// allocation, `const fn` throughout so the antichain verdict is
1778 /// a compile-time expression at every preset-tuple call site.
1779 #[must_use]
1780 pub const fn is_incomparable(self, other: Self) -> bool {
1781 !self.leq(other) && !self.geq(other)
1782 }
1783
1784 /// Pointwise partial-order COMPARABILITY relation across the six
1785 /// ceilings — `true` iff `self` and `other` sit on the SAME lattice
1786 /// branch, i.e., EITHER `self.leq(other)` OR `self.geq(other)`
1787 /// holds. The two postures then admit a pointwise-domination
1788 /// verdict in at least one direction: every axis has `self` at-or-
1789 /// below `other` (the `leq` branch) or every axis has `self`
1790 /// at-or-above `other` (the `geq` branch), so at least one
1791 /// direction closes and one preset dominates the other on every
1792 /// field.
1793 ///
1794 /// The COMPARABLE CORNER on the (leq, geq, lt, gt, incomparable,
1795 /// comparable) pairwise-relation surface — the DIRECT POSITIVE
1796 /// DUAL of [`Self::is_incomparable`] one COVER-COMPLEMENT axis
1797 /// over on the (comparable, incomparable) two-cell partition of
1798 /// the ordered pair × verdict surface. The just-lifted
1799 /// [`Self::is_incomparable`] projection pins the ANTICHAIN half of
1800 /// that partition (the pairs where NEITHER pointwise-domination
1801 /// direction closes); THIS projection pins the DUAL COMPARABLE
1802 /// half (the pairs where AT LEAST ONE direction closes) —
1803 /// EXHAUSTIVELY CLOSING the (comparable, incomparable) two-cell
1804 /// partition at its FINAL SECOND tile on the pairwise-relation
1805 /// surface. The (leq, geq, lt, gt) 2×2 pairwise partial-order face
1806 /// carries the four DIRECTIONAL comparable predicates one COVER
1807 /// axis over; THIS projection folds them into ONE POSITIVE
1808 /// COMPARABILITY verdict — the SET-UNION of the two pointwise-
1809 /// domination directions.
1810 ///
1811 /// Symmetric on its argument order: `a.is_comparable(b) ==
1812 /// b.is_comparable(a)` — the composition `self.leq(other) ||
1813 /// self.geq(other)` folds through `self.leq(other) ==
1814 /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1815 /// disjunction's commutativity, so the projection is order-
1816 /// independent on its two inputs. This SYMMETRY POSTURE mirrors
1817 /// [`Self::is_incomparable`]'s symmetry one CELL axis over on the
1818 /// (comparable, incomparable) partition: both cells of an
1819 /// EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface must
1820 /// themselves be symmetric — the two verdicts flip in lockstep on
1821 /// argument swap. Distinguishes it from [`Self::leq`] / [`Self::lt`]
1822 /// (both antisymmetric — swapping inputs FLIPS the verdict).
1823 ///
1824 /// Reflexive on its own argument: `a.is_comparable(a) == true` —
1825 /// every posture is comparable to itself via [`Self::leq`]'s
1826 /// reflexivity (`a.leq(a) == true`), so the disjunction's
1827 /// `self.leq(other)` leg holds at every diagonal input. Sibling
1828 /// posture to [`Self::leq`]'s reflexivity one COVER axis over:
1829 /// the diagonal COMPARABLE cell is a substrate-level THEOREM
1830 /// falling out of the partial-order axioms, and THIS projection
1831 /// pins it as ONE positive verdict rather than the caller re-
1832 /// deriving `a.leq(a) || a.geq(a) == true` at every diagonal
1833 /// callsite.
1834 ///
1835 /// **Bottom-pole absorption**: `a.is_comparable(
1836 /// EMPTY_RESOURCE_LIMITS) == true` on every posture `a` —
1837 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1838 /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1839 /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1840 /// identity, which satisfies the `self.geq(other)` leg. The
1841 /// bounded-lattice bottom is comparable to every posture. The
1842 /// COMPLEMENT of [`Self::is_incomparable`]'s bottom-pole
1843 /// absorption pin one CELL axis over — where the antichain
1844 /// verdict FOLDS FALSE at the bottom pole, the comparability
1845 /// verdict FOLDS TRUE. Pinned on the canonical preset roster.
1846 ///
1847 /// **Top-pole absorption**: `a.is_comparable(
1848 /// UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a` —
1849 /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1850 /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1851 /// usize::MAX` unconditionally), which satisfies the
1852 /// `self.leq(other)` leg. The bounded-lattice top is comparable
1853 /// to every posture. The COMPLEMENT of [`Self::is_incomparable`]'s
1854 /// top-pole absorption pin one CELL axis over; pinned on the
1855 /// canonical preset roster.
1856 ///
1857 /// **Antichain load-bearing arm**:
1858 /// `HAND_AUTHORED_MID_POSTURE.is_comparable(
1859 /// HAND_AUTHORED_OTHER_POSTURE) == false` — the two hand-authored
1860 /// asymmetric postures sit on distinct branches (each is smaller
1861 /// on three axes and larger on the other three), so NEITHER
1862 /// pointwise-domination direction closes and the comparability
1863 /// verdict is `false`. The DIRECT FALSIFICATION of
1864 /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin one CELL
1865 /// axis over on the SAME pair — the two are the negation-paired
1866 /// sides of the (comparable, incomparable) two-cell partition,
1867 /// and pinning BOTH cells' verdicts on the SAME load-bearing
1868 /// antichain pair closes the partition as a substrate-level
1869 /// EXCLUSIVITY THEOREM rather than one direction's assertion.
1870 ///
1871 /// **De Morgan duality**: `a.is_comparable(b) == !a.is_incomparable(b)`
1872 /// — the comparable corner is the SET-COMPLEMENT of the antichain
1873 /// corner. Pinned by
1874 /// `resource_limits_is_comparable_is_de_morgan_dual_of_incomparable`
1875 /// which sweeps every ordered pair in the preset roster and
1876 /// confirms the equivalence at every cell. Together with
1877 /// `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1878 /// (which pins the OTHER direction on
1879 /// `a.leq(b) || a.geq(b)` composition), the two projections carry
1880 /// the (comparable, incomparable) two-cell partition as an
1881 /// EXHAUSTIVE-AND-DISJOINT decomposition of the ordered pair ×
1882 /// verdict surface.
1883 ///
1884 /// Encoded as `self.leq(other) || self.geq(other)` — one
1885 /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1886 /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1887 /// comparability characterization lives at exactly one
1888 /// implementation site, and a future re-derivation of [`Self::leq`]
1889 /// (e.g. to a different pointwise conjunction) propagates to
1890 /// `is_comparable` mechanically rather than requiring a per-
1891 /// method fix-up. Mirrors [`Self::is_incomparable`]'s
1892 /// `!self.leq(other) && !self.geq(other)` delegation one COVER
1893 /// axis over on the pairwise-relation surface — the two share
1894 /// the SAME two-primitive substrate and diverge only on the
1895 /// negation posture (`!… && !…` vs `… || …`), which De Morgan's
1896 /// law pins as the SAME projection.
1897 ///
1898 /// `const fn` for the same compile-time-pin reasons as
1899 /// [`Self::is_incomparable`] (`const _: () =
1900 /// assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(
1901 /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin a
1902 /// comparability-agreement identity at compile time — a build-
1903 /// break rather than a runtime `assert!` on the first execution.
1904 ///
1905 /// Pre-lift, a caller that wanted "these two postures are
1906 /// comparable on the pointwise partial-order" composed
1907 /// `a.leq(b) || a.geq(b)` at the callsite — a two-primitive
1908 /// composition that appeared verbatim at every prospective
1909 /// comparability-detection site pre-lift, and the NEGATION of the
1910 /// same two-primitive composition [`Self::is_incomparable`]
1911 /// lifted one COVER axis over — a ≥2 PRIME DIRECTIVE trigger with
1912 /// the (comparable, incomparable) two-cell partition of the
1913 /// ordered pair × verdict surface as its substrate posture. Post-
1914 /// lift the comparability characterization is ONE named primitive
1915 /// on the [`ResourceLimits`] surface, `const fn`-composable into
1916 /// a compile-time bound, and the two-primitive `leq || geq`
1917 /// composition lives at ONE implementation site.
1918 ///
1919 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1920 /// preserves proofs; the comparability characterization is the
1921 /// SET-UNION of the two pointwise-domination directions, and
1922 /// pinning the projection at ONE typed primitive makes the
1923 /// (comparable, incomparable) two-cell partition of the ordered-
1924 /// pair × verdict surface a substrate-level THEOREM proved on
1925 /// BOTH SIDES rather than one side asserted and the other
1926 /// inferred at consumer sites. THEORY.md §V.1 — knowable
1927 /// platform; the comparability-detection corner was an unnamed
1928 /// two-primitive composition recurring at every prospective
1929 /// "these two postures sit on the same branch?" callsite pre-
1930 /// lift.
1931 ///
1932 /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1933 /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` — the
1934 /// comparable corner is exactly the `isJust` projection of that
1935 /// method (the DUAL of `is_incomparable`'s `isNothing` projection
1936 /// on the SAME method); Julia's `Base.Order.lt` + `Base.Order.gt`
1937 /// disjunction on the pairwise-comparability check; Rust's own
1938 /// `PartialOrd::partial_cmp` whose `Some(_)` arm is the
1939 /// comparable corner; Coq's `PreOrder` + classical LEM-derived
1940 /// `leq a b \/ leq b a` proof obligation on the comparable arm.
1941 /// Translation through pleme-io primitives: the comparability
1942 /// characterization on the closed-set-paired [`ResourceLimits`]
1943 /// posture binds through the just-lifted [`Self::leq`] and
1944 /// [`Self::geq`] primitives (both const-evaluable, both
1945 /// delegating to [`Self::leq`]) with a two-arm disjunction on the
1946 /// direct forms — no new dep, no supertrait bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
1947 /// throughout so the comparability verdict is a compile-time
1948 /// expression at every preset-tuple call site.
1949 #[must_use]
1950 pub const fn is_comparable(self, other: Self) -> bool {
1951 self.leq(other) || self.geq(other)
1952 }
1953
1954 /// Pointwise partial-order VERDICT across the six ceilings — the
1955 /// `Option<Ordering>` corner on the pairwise-relation surface. Folds
1956 /// the four possible outcomes ([`Ordering::Less`], [`Ordering::Equal`],
1957 /// [`Ordering::Greater`], INCOMPARABLE) into ONE typed value, so a
1958 /// caller that wants "how do these two postures relate?" gets the
1959 /// FULL verdict in a single call rather than composing
1960 /// [`Self::leq`] / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] /
1961 /// `PartialEq::eq` at its callsite.
1962 ///
1963 /// The `Option<Ordering>` PROJECTION on the pairwise-relation surface —
1964 /// the DIRECT LIFT of the frontier inspiration the just-lifted
1965 /// [`Self::is_incomparable`] + [`Self::is_comparable`] pair explicitly
1966 /// named (Rust's `PartialOrd::partial_cmp`, Haskell's
1967 /// `Data.PartialOrd.compare :: a -> a -> Maybe Ordering`, Julia's
1968 /// `Base.Order`, Coq's `PreOrder` decidability obligation). Those two
1969 /// projections pinned ONE ARM each of the surface — the antichain
1970 /// arm ([`Self::is_incomparable`] → `is_none()`) and the comparability
1971 /// arm ([`Self::is_comparable`] → `is_some()`); THIS lift closes the
1972 /// surface at its FULL VERDICT corner by returning the ordering
1973 /// direction on the comparable arm and `None` on the antichain arm —
1974 /// the (Less, Equal, Greater, None) 4-tile partition of the ordered
1975 /// pair × verdict surface at ONE named primitive rather than a
1976 /// four-way conditional composition at every consumer that wanted
1977 /// the full verdict.
1978 ///
1979 /// **Antichain-arm identity**: `a.partial_cmp(b).is_none() ==
1980 /// a.is_incomparable(b)` — the `None` arm of the return coincides
1981 /// EXACTLY with the antichain-corner projection [`Self::is_incomparable`]
1982 /// lifts. On the antichain the two-arm conjunction `!self.leq(other)
1983 /// && !self.geq(other)` holds; neither branch of THIS method's
1984 /// disjunction closes, so the fall-through arm binds. Pinned as a
1985 /// substrate-level identity in the test cohort.
1986 ///
1987 /// **Comparability-arm identity**: `a.partial_cmp(b).is_some() ==
1988 /// a.is_comparable(b)` — the `Some(_)` arm of the return coincides
1989 /// EXACTLY with the comparability-corner projection [`Self::is_comparable`]
1990 /// lifts. On every comparable pair at least one of the two
1991 /// pointwise-domination legs closes, so the method returns a
1992 /// concrete [`Ordering`] variant. Together with the antichain-arm
1993 /// identity, THIS method is the JOINT LIFT of the (comparable,
1994 /// incomparable) two-cell partition into ONE `Option<Ordering>`
1995 /// value.
1996 ///
1997 /// **Equality on the diagonal**: `a.partial_cmp(a) ==
1998 /// Some(Ordering::Equal)` on every posture — the `leq` reflexivity
1999 /// (`a.leq(a) == true`) AND the `geq` reflexivity (`a.geq(a) ==
2000 /// true`) both hold at the diagonal, so both branches of the
2001 /// conjunction close and the equality arm fires. The diagonal cell
2002 /// of the (Less, Equal, Greater, None) 4-tile partition on the
2003 /// ordered pair × verdict surface pins to the Equal tile.
2004 ///
2005 /// **Ordering-direction agreement**: `a.partial_cmp(b) ==
2006 /// Some(Ordering::Less)` iff `a.lt(b)` (strict less); `a.partial_cmp(b)
2007 /// == Some(Ordering::Greater)` iff `a.gt(b)` (strict greater);
2008 /// `a.partial_cmp(b) == Some(Ordering::Equal)` iff `a == b` (via
2009 /// antisymmetry `a.leq(b) && a.geq(b) → a == b`). The four-arm
2010 /// dispatch below binds each verdict tile to its named companion
2011 /// primitive at exactly one implementation site, so a caller that
2012 /// wants the strict-less verdict binds through either `a.lt(b)` OR
2013 /// `matches!(a.partial_cmp(b), Some(Ordering::Less))` and both route
2014 /// to the same substrate.
2015 ///
2016 /// **Antisymmetry under argument swap**: `a.partial_cmp(b) ==
2017 /// b.partial_cmp(a).map(Ordering::reverse)` — swapping the argument
2018 /// pair FLIPS the ordering-direction verdict (`Less <-> Greater`,
2019 /// `Equal` fixed, `None` fixed). Discriminates [`Self::is_incomparable`] /
2020 /// [`Self::is_comparable`] (both SYMMETRIC on argument swap) one
2021 /// POSTURE axis over: the projections carry no directional content
2022 /// and are argument-order-independent; the FULL verdict carries
2023 /// directional content and inverts under swap. Pinned in the test
2024 /// cohort.
2025 ///
2026 /// **Bottom-pole absorption**: `EMPTY_RESOURCE_LIMITS.partial_cmp(a)`
2027 /// returns `Some(Ordering::Less)` for every posture `a` strictly
2028 /// above [`EMPTY_RESOURCE_LIMITS`] on the pointwise partial order,
2029 /// AND `Some(Ordering::Equal)` at `a == EMPTY_RESOURCE_LIMITS`.
2030 /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so on
2031 /// every posture the `EMPTY.leq(a)` leg closes; the antisymmetric
2032 /// leg discriminates the strict-less arm (concrete positive posture)
2033 /// from the equality arm (`a == EMPTY`). The DIRECT REFINEMENT of
2034 /// [`Self::is_comparable`]'s bottom-pole absorption pin one
2035 /// DIRECTION-CONTENT axis over — where the comparability projection
2036 /// folds TRUE at the bottom pole across every posture, THIS method
2037 /// discriminates the equality-vs-strict-less refinement on the
2038 /// SAME preset roster.
2039 ///
2040 /// **Top-pole absorption**: symmetric statement — `a.partial_cmp(
2041 /// UNBOUNDED_RESOURCE_LIMITS)` returns `Some(Ordering::Less)` for
2042 /// every posture `a` strictly below the top and
2043 /// `Some(Ordering::Equal)` at `a == UNBOUNDED`. Pinned on the
2044 /// canonical preset roster.
2045 ///
2046 /// **Antichain load-bearing arm**: `HAND_AUTHORED_MID_POSTURE
2047 /// .partial_cmp(HAND_AUTHORED_OTHER_POSTURE) == None` — the two
2048 /// hand-authored asymmetric postures sit on distinct branches
2049 /// (each is smaller on three axes and larger on the other three),
2050 /// so NEITHER pointwise-domination direction closes and the
2051 /// fall-through `None` arm fires. The DIRECT LIFT of
2052 /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin AND
2053 /// [`Self::is_comparable`]'s antichain-arm FALSIFIES pin one
2054 /// PROJECTION axis over on the SAME hand-authored pair — the two
2055 /// projections pinned each ARM's cell verdict; THIS method pins
2056 /// the FULL `Option<Ordering>` return at the antichain corner
2057 /// (`None`).
2058 ///
2059 /// Encoded as `if self.leq(other) && other.leq(self) { Equal } else
2060 /// if self.leq(other) { Less } else if other.leq(self) { Greater }
2061 /// else { None }` — a four-arm dispatch on the (`self.leq(other)`,
2062 /// `other.leq(self)`) 2×2 boolean truth-table, with each arm binding
2063 /// to its named companion primitive at exactly one implementation
2064 /// site so a future re-derivation of [`Self::leq`] propagates to
2065 /// EVERY ordering-direction verdict mechanically rather than
2066 /// requiring a per-arm fix-up. The `self.leq(other) && other.leq(self)`
2067 /// conjunction is the pointwise antisymmetric-equality
2068 /// characterization — pinned as a substrate-level identity by the
2069 /// `resource_limits_leq_is_antisymmetric` test cohort, so the
2070 /// Equal-arm binding above is a substrate-level THEOREM rather than
2071 /// an ad-hoc composition.
2072 ///
2073 /// `const fn` for the same compile-time-pin reasons as [`Self::leq`]
2074 /// / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] / [`Self::is_incomparable`]
2075 /// / [`Self::is_comparable`] one COVER axis over on the pairwise-
2076 /// relation surface (`const _: () = assert!(matches!(
2077 /// EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
2078 /// Some(Ordering::Less)));`) — a caller can pin a full-verdict
2079 /// identity at compile time as a build-break rather than a runtime
2080 /// `assert!` on the first execution. [`Ordering`] itself is
2081 /// `const`-constructible (the three variants are plain enum
2082 /// constructors), so the return-value cascade below stays inside
2083 /// stable const-fn scope with no runtime allocation, no supertrait
2084 /// bound, and no new dep.
2085 ///
2086 /// Pre-lift, a caller that wanted the FULL pointwise ordering
2087 /// verdict either (a) composed four separate calls
2088 /// (`a.lt(b)` / `a == b` / `a.gt(b)` / `a.is_incomparable(b)`) at
2089 /// its callsite and dispatched on the resulting 4-way conditional —
2090 /// a per-consumer FOUR-primitive composition whose exhaustiveness
2091 /// the type system did NOT gate (a dropped arm silently miscategorized
2092 /// the pair) — or (b) invoked [`Self::is_comparable`] to get the
2093 /// `is_some`/`is_none` distinction and THEN chose one of `lt` / `gt`
2094 /// / equality on the `is_some` arm — a two-stage composition that
2095 /// double-evaluates the `leq` / `geq` primitives on every consumer.
2096 /// Post-lift the full verdict is ONE named primitive returning
2097 /// `Option<Ordering>`, `const fn`-composable into a compile-time
2098 /// bound, and the two-primitive `leq(self, other)` + `leq(other, self)`
2099 /// substrate binds at ONE implementation site — a ≥2 PRIME
2100 /// DIRECTIVE trigger, since the four-way ordering dispatch appeared
2101 /// inline at every prospective full-verdict callsite pre-lift.
2102 ///
2103 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2104 /// preserves proofs; the FULL pointwise partial-order verdict on
2105 /// two preset-carried resource proofs is itself a typed
2106 /// `Option<Ordering>` value whose four arms each carry a distinct
2107 /// substrate-level relation proof (`Less` ⇒ strict-less on every
2108 /// axis; `Equal` ⇒ pointwise equality on every axis; `Greater` ⇒
2109 /// strict-greater on every axis; `None` ⇒ antichain — asymmetric
2110 /// per-axis orderings). Pinning the projection at ONE typed
2111 /// primitive makes the (Less, Equal, Greater, None) 4-tile
2112 /// partition of the ordered pair × verdict surface a substrate-
2113 /// level THEOREM rather than a per-consumer inline four-way
2114 /// dispatch. THEORY.md §V.1 — knowable platform; the full-verdict
2115 /// projection was an unnamed four-primitive composition recurring
2116 /// at every prospective "how do these two postures relate?"
2117 /// callsite pre-lift.
2118 ///
2119 /// Frontier inspiration: Rust's own `PartialOrd::partial_cmp :: fn
2120 /// partial_cmp(&self, &Self) -> Option<Ordering>` — the CANONICAL
2121 /// `Option<Ordering>` corner on any partial-order-carrying type;
2122 /// Haskell's `Data.PartialOrd.compare :: PartialOrd a => a -> a
2123 /// -> Maybe Ordering`; Julia's `Base.Order.NoOrder` sentinel folded
2124 /// into `Option<Ordering>` on the `Base.Order`-compatible partial
2125 /// order; Coq's `PreOrder` + `Decidable` typeclass composition
2126 /// giving `forall a b, {leq a b} + {leq b a} + {~(leq a b) /\
2127 /// ~(leq b a)}` — the three-way decidable sum where the third
2128 /// disjunct is the antichain corner. Translation through pleme-io
2129 /// primitives: the full-verdict projection binds through the just-
2130 /// lifted [`Self::leq`] primitive (invoked twice — once in each
2131 /// argument order — to derive the (`self.leq(other)`,
2132 /// `other.leq(self)`) 2×2 truth-table the four-arm dispatch reads
2133 /// against), no new dep, no supertrait bound (`Sized + Copy +
2134 /// 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
2135 /// throughout. This method does NOT wire an `impl PartialOrd for
2136 /// ResourceLimits` — the derive would give lexicographic ordering
2137 /// on the six fields (WRONG for the pointwise lattice), and manually
2138 /// implementing the trait would expose `<` / `<=` / `>` / `>=`
2139 /// operators that overlap the named `lt` / `leq` / `gt` / `geq`
2140 /// primitives already lifted one COVER axis over. Keeping the
2141 /// `Option<Ordering>` corner at an inherent method preserves the
2142 /// tight name-directed surface without introducing an operator
2143 /// alias for each of the four directional predicates.
2144 #[must_use]
2145 pub const fn partial_cmp(self, other: Self) -> Option<Ordering> {
2146 let self_leq = self.leq(other);
2147 let other_leq = other.leq(self);
2148 if self_leq && other_leq {
2149 Some(Ordering::Equal)
2150 } else if self_leq {
2151 Some(Ordering::Less)
2152 } else if other_leq {
2153 Some(Ordering::Greater)
2154 } else {
2155 None
2156 }
2157 }
2158
2159 /// Boolean `is_comparable`-conjunction across every distinct pair of a
2160 /// slice of postures — `postures` is a CHAIN (a totally-ordered subset of
2161 /// the pointwise partial-order lattice) iff every distinct pair
2162 /// `(postures[i], postures[j])` sits on the same lattice branch
2163 /// (`postures[i].is_comparable(postures[j])`).
2164 ///
2165 /// The SET-LEVEL N-ary peer of [`Self::is_comparable`] one CARDINALITY
2166 /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2167 /// primitive surface. Where [`Self::is_comparable`] decides the pairwise
2168 /// question "do THESE TWO postures sit on the same branch?", THIS
2169 /// projection decides the set-level question "does EVERY PAIR in this
2170 /// slice sit on the same branch?" — the natural lift of the pair-level
2171 /// verdict to the collection level, matching the arity extension the
2172 /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] pair carries
2173 /// one PRIMITIVE-KIND axis over on the (leq, comparable) × (pairwise,
2174 /// N-ary) primitive surface.
2175 ///
2176 /// **Empty-slice vacuous truth**: `ResourceLimits::is_chain(&[]) == true`
2177 /// — the empty conjunction is vacuously true; the empty slice contains
2178 /// no distinct pairs to reject. Peer of
2179 /// `is_lower_bound_of(&[]) == true`'s empty-slice identity one
2180 /// PRIMITIVE-KIND axis over. Pinned as
2181 /// `resource_limits_is_chain_empty_slice_is_vacuously_true`.
2182 ///
2183 /// **Singleton vacuous truth**: `ResourceLimits::is_chain(&[a]) == true`
2184 /// for every posture `a` — a one-element slice contains no distinct
2185 /// pairs, so the outer-and-inner-index conjunction never enters the
2186 /// inner loop and returns `true` vacuously. Pinned as
2187 /// `resource_limits_is_chain_singleton_is_vacuously_true`.
2188 ///
2189 /// **Diagonal-duplicate identity**: `ResourceLimits::is_chain(&[a, a])
2190 /// == true` for every posture `a` — [`Self::is_comparable`] is
2191 /// REFLEXIVE (`a.is_comparable(a) == true`), so the single distinct
2192 /// index pair `(0, 1)` binds `a.is_comparable(a) == true` and the
2193 /// conjunction holds. Distinguishes it from [`Self::is_antichain`] one
2194 /// COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2195 /// `false` (via [`Self::is_incomparable`]'s IRREFLEXIVITY). Pinned as
2196 /// `resource_limits_is_chain_of_diagonal_duplicate_is_true`.
2197 ///
2198 /// **Ordered-chain closure**: `ResourceLimits::is_chain(
2199 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2200 /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple on
2201 /// the bounded-lattice diagonal is an ascending chain (`EMPTY <=
2202 /// DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among the
2203 /// three is comparable. Pinned as
2204 /// `resource_limits_is_chain_holds_on_the_shipped_preset_triple`.
2205 ///
2206 /// **Antichain rejection**: `ResourceLimits::is_chain(
2207 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2208 /// — the two hand-authored asymmetric postures sit on distinct branches
2209 /// (each is smaller on three axes and larger on the other three), so
2210 /// neither pointwise-domination direction closes and the pair is
2211 /// INCOMPARABLE. The chain conjunction rejects at the first
2212 /// incomparable pair. Pinned as
2213 /// `resource_limits_is_chain_rejects_the_hand_authored_antichain_pair`.
2214 ///
2215 /// **Mixed-set rejection**: `ResourceLimits::is_chain(
2216 /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2217 /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the first two
2218 /// AND the first-and-third pairs are comparable in isolation, the
2219 /// second-and-third pair is incomparable, so the whole-set conjunction
2220 /// rejects. The set-level chain verdict is stricter than the union of
2221 /// its pair-level verdicts — one antichain pair anywhere in the slice
2222 /// falsifies the whole projection. Pinned as
2223 /// `resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair`.
2224 ///
2225 /// **`most_permissive_of` closure witness**: on every slice `postures`
2226 /// where `ResourceLimits::is_chain(postures)` holds, the N-ary join
2227 /// `most_permissive_of(postures)` is a MEMBER of the slice (not just a
2228 /// bound above it) — the maximum element of a chain sits inside the
2229 /// chain. The set-level verdict thus underwrites the pointwise-`max`
2230 /// identification of the chain's top element. NOT pinned as a test in
2231 /// this cohort (the general witness needs a slice-membership predicate
2232 /// not yet lifted); recorded here as the future callsite the chain
2233 /// verdict unlocks.
2234 ///
2235 /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2236 /// i+1..n`) with one [`Self::is_comparable`] delegation per distinct
2237 /// pair. The `j = i + 1` inner starting index skips the diagonal AND
2238 /// the mirror pairs (`(j, i)` pairs the outer loop already visited from
2239 /// the `(i, j)` direction) — [`Self::is_comparable`] is SYMMETRIC on
2240 /// argument swap, so the mirror pair carries no additional information
2241 /// and the (n choose 2) unique-pair count is the tight iteration budget
2242 /// for the set-level verdict. `postures[i].is_comparable(postures[j])`
2243 /// (not `.leq(...) || .geq(...)` directly) so the primitive delegation
2244 /// lives at exactly one implementation site — a future re-derivation of
2245 /// [`Self::is_comparable`] (e.g. to a different comparability
2246 /// characterization) propagates to `is_chain` mechanically rather than
2247 /// requiring a per-callsite fix-up.
2248 ///
2249 /// `const fn` for the same compile-time-pin reasons as
2250 /// [`Self::is_comparable`] one CARDINALITY axis over on the (comparable,
2251 /// incomparable) × (pair-level, set-level) primitive surface (`const _:
2252 /// () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS,
2253 /// DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));`), so a
2254 /// caller can pin a chain-membership identity at compile time — a
2255 /// build-break rather than a runtime `assert!` on the first execution.
2256 /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the slice
2257 /// by value without an explicit `.clone()` (which const-fn would not
2258 /// permit anyway).
2259 ///
2260 /// Pre-lift, a caller wanting "is this slice a chain?" composed the
2261 /// doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2262 /// postures[i + 1..].iter().all(|b| a.is_comparable(*b)))` two-primitive
2263 /// scaffolding at every prospective callsite — a PRIME DIRECTIVE ≥2
2264 /// pattern once two consumers need the set-level chain verdict, and one
2265 /// whose exhaustiveness the type system did NOT gate (a slice iterator
2266 /// that stopped at the first `false` still needed the caller to bind
2267 /// the outer-and-inner-index conjunction correctly, and a copy-paste
2268 /// that swapped `postures[i]` with `postures[j]` would test the SAME
2269 /// pair via [`Self::is_comparable`]'s symmetry — silent no-op — but a
2270 /// copy-paste that inverted the primitive delegation to
2271 /// [`Self::is_incomparable`] would test the DUAL question and silently
2272 /// distort the verdict). Post-lift the chain verdict binds at ONE typed
2273 /// method the algebra exposes, and the doubly-indexed all-pairs
2274 /// scaffolding lives at ONE implementation site.
2275 ///
2276 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2277 /// proofs; the set-level chain verdict on N preset-carried resource
2278 /// proofs is itself a typed named `bool` predicate composing them via
2279 /// the underlying pair-level [`Self::is_comparable`] projection.
2280 /// THEORY.md §V.1 — knowable platform; the doubly-indexed all-pairs
2281 /// comparability conjunction becomes a TYPE-level operation on the
2282 /// posture algebra rather than an inline two-primitive scaffolding at
2283 /// every consumer that decides "is this slice a totally-ordered
2284 /// subset?"
2285 ///
2286 /// Frontier inspiration: Haskell's `Data.List.isSorted`
2287 /// (adjacent-pair-comparability variant on `Ord a`); Coq's `Sorted`
2288 /// inductive on lists over a `TotalOrder`; Idris's `Sorted` predicate
2289 /// over `Ord`-carrying lists; the order-theoretic notion of a CHAIN
2290 /// (a totally-ordered subset of a poset) canonicalized in
2291 /// Birkhoff's *Lattice Theory* and every subsequent poset text.
2292 /// Translation through pleme-io primitives: on a `PartialOrd`-derived
2293 /// carrier the frontier `isSorted` variants reduce to adjacent-pair
2294 /// comparability because total-order transitivity closes the chain; on
2295 /// a PARTIAL order (the pointwise lattice) the adjacent-pair check is
2296 /// INSUFFICIENT (three postures `a, b, c` where `a` and `c` are
2297 /// comparable but `b` is incomparable to at least one of them satisfy
2298 /// adjacent-pair comparability without forming a chain), so this
2299 /// projection walks EVERY distinct pair rather than only the adjacent
2300 /// ones — the strict partial-order generalization of the frontier
2301 /// total-order predicate. No new dep, no supertrait bound, `const fn`
2302 /// throughout.
2303 #[must_use]
2304 pub const fn is_chain(postures: &[Self]) -> bool {
2305 let n = postures.len();
2306 let mut i = 0;
2307 while i < n {
2308 let mut j = i + 1;
2309 while j < n {
2310 if !postures[i].is_comparable(postures[j]) {
2311 return false;
2312 }
2313 j += 1;
2314 }
2315 i += 1;
2316 }
2317 true
2318 }
2319
2320 /// Boolean `is_incomparable`-conjunction across every distinct pair of a
2321 /// slice of postures — `postures` is an ANTICHAIN (a pairwise-
2322 /// incomparable subset of the pointwise partial-order lattice) iff every
2323 /// distinct pair `(postures[i], postures[j])` sits on distinct branches
2324 /// (`postures[i].is_incomparable(postures[j])`).
2325 ///
2326 /// The SET-LEVEL N-ary peer of [`Self::is_incomparable`] one CARDINALITY
2327 /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2328 /// primitive surface — the DIRECT SET-LEVEL DUAL of [`Self::is_chain`]
2329 /// one COVER-COMPLEMENT axis over on the SAME set-level cardinality
2330 /// slice. Together the two close the (chain, antichain) two-cell face
2331 /// on the set-level pairwise-relation surface, exactly ONE CARDINALITY
2332 /// axis over from the (comparable, incomparable) two-cell partition of
2333 /// the ordered-pair × verdict surface [`Self::is_comparable`] and
2334 /// [`Self::is_incomparable`] closed one arity down.
2335 ///
2336 /// The (chain, antichain) pair is NOT a partition of the set-level
2337 /// verdict surface: a slice of ≥3 postures whose pair-level verdicts
2338 /// mix comparable and incomparable pairs is neither a chain nor an
2339 /// antichain, so the two set-level projections carry a THREE-cell
2340 /// face (chain, antichain, mixed) at the set level whose ends the two
2341 /// projections lifted here pin. Distinguishes it from the pair-level
2342 /// (comparable, incomparable) partition [`Self::is_comparable`] +
2343 /// [`Self::is_incomparable`] EXHAUSTIVELY closed one arity down: at
2344 /// arity 2 every pair is exactly one of comparable-or-incomparable, and
2345 /// the two projections partition the ordered-pair × verdict surface; at
2346 /// arity ≥3 the mixed-set cell opens up between them and the two
2347 /// set-level projections carry the ends of that three-cell face.
2348 ///
2349 /// **Empty-slice vacuous truth**: `ResourceLimits::is_antichain(&[])
2350 /// == true` — the empty conjunction is vacuously true; the empty slice
2351 /// contains no distinct pairs to reject. Peer of [`Self::is_chain`]'s
2352 /// empty-slice identity one COVER-COMPLEMENT axis over: both cells of
2353 /// the (chain, antichain) set-level pair AGREE at the empty slice on
2354 /// the vacuous-truth verdict — the two cells coincide at the empty
2355 /// face of the set-level verdict surface. Pinned as
2356 /// `resource_limits_is_antichain_empty_slice_is_vacuously_true`.
2357 ///
2358 /// **Singleton vacuous truth**: `ResourceLimits::is_antichain(&[a])
2359 /// == true` for every posture `a` — a one-element slice contains no
2360 /// distinct pairs, so the outer-and-inner-index conjunction never
2361 /// enters the inner loop and returns `true` vacuously. Peer of
2362 /// [`Self::is_chain`]'s singleton-vacuous-truth identity: both cells
2363 /// AGREE at singleton slices too — the (chain, antichain) verdict
2364 /// surface coincides at every slice with strictly fewer than two
2365 /// distinct pairs. Pinned as
2366 /// `resource_limits_is_antichain_singleton_is_vacuously_true`.
2367 ///
2368 /// **Diagonal-duplicate rejection**: `ResourceLimits::is_antichain(
2369 /// &[a, a]) == false` for every posture `a` — [`Self::is_incomparable`]
2370 /// is IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
2371 /// distinct index pair `(0, 1)` binds `a.is_incomparable(a) == false`
2372 /// and the conjunction rejects. Distinguishes it from [`Self::is_chain`]
2373 /// one COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2374 /// `true` (via [`Self::is_comparable`]'s REFLEXIVITY). The reflexivity/
2375 /// irreflexivity divergence between the two pair-level projections
2376 /// PROPAGATES to the set-level projections at any slice with a
2377 /// duplicated element — the two set-level cells DIVERGE at every
2378 /// diagonal-duplicate slice, the mirror of their AGREEMENT at every
2379 /// empty-or-singleton slice one CARDINALITY axis over. Pinned as
2380 /// `resource_limits_is_antichain_of_diagonal_duplicate_is_false`.
2381 ///
2382 /// **Hand-authored antichain closure**: `ResourceLimits::is_antichain(
2383 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2384 /// — the two hand-authored asymmetric postures sit on distinct
2385 /// branches (each is smaller on three axes and larger on the other
2386 /// three), so neither pointwise-domination direction closes and the
2387 /// pair is INCOMPARABLE. The antichain conjunction accepts every
2388 /// distinct pair of the slice. Pinned as
2389 /// `resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair`.
2390 ///
2391 /// **Chain rejection**: `ResourceLimits::is_antichain(
2392 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]) == false` — the
2393 /// shipped-preset pair on the bounded-lattice diagonal is a strict
2394 /// chain (`EMPTY.lt(DEFAULT)`), so the single distinct pair is
2395 /// COMPARABLE and the antichain conjunction rejects. The DIRECT
2396 /// FALSIFICATION of [`Self::is_chain`]'s ordered-chain closure one
2397 /// CELL axis over on the SAME slice — the two set-level cells carry
2398 /// the negation-paired verdict at every strictly-chained slice, the
2399 /// mirror of their AGREEMENT at every empty-or-singleton slice one
2400 /// CARDINALITY axis over. Pinned as
2401 /// `resource_limits_is_antichain_rejects_the_shipped_preset_pair`.
2402 ///
2403 /// **Mixed-set rejection**: `ResourceLimits::is_antichain(
2404 /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2405 /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the
2406 /// second-and-third pair is incomparable, the first pair
2407 /// (`DEFAULT`-and-`MID`) is comparable, so the whole-set conjunction
2408 /// rejects. Pins the three-cell (chain, antichain, mixed) set-level
2409 /// face at the mixed-set cell: the same slice both [`Self::is_chain`]
2410 /// AND [`Self::is_antichain`] reject at, and the third cell of the
2411 /// three-cell face the two set-level projections carry the ends of.
2412 /// Pinned as
2413 /// `resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair`.
2414 ///
2415 /// **De Morgan mirror at arity 2**: on any two-element slice `[a, b]`
2416 /// (`a != b`), `ResourceLimits::is_antichain(&[a, b]) ==
2417 /// !ResourceLimits::is_chain(&[a, b])` — at exactly two DISTINCT
2418 /// elements the set-level (chain, antichain) two-cell face reduces to
2419 /// the pair-level (comparable, incomparable) two-cell partition and
2420 /// the two cells become MUTUALLY EXCLUSIVE (the mixed cell requires
2421 /// ≥3 elements to open). The set-level De Morgan mirror of the
2422 /// pair-level identity `a.is_comparable(b) == !a.is_incomparable(b)`
2423 /// [`Self::is_comparable`] pins one CARDINALITY axis over. Pinned as
2424 /// `resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs`.
2425 ///
2426 /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2427 /// i+1..n`) with one [`Self::is_incomparable`] delegation per distinct
2428 /// pair — the SAME iteration structure as [`Self::is_chain`] one
2429 /// PRIMITIVE-DELEGATION axis over, with only the inner primitive
2430 /// swapped between the two. The two set-level projections share the
2431 /// SAME two-primitive substrate (the doubly-indexed pair walk + one
2432 /// pair-level projection) and diverge only on the pair-level primitive
2433 /// they delegate to (`is_comparable` vs `is_incomparable`), which De
2434 /// Morgan's law pins as the pair-level negation-paired substrate.
2435 ///
2436 /// `const fn` for the same compile-time-pin reasons as
2437 /// [`Self::is_incomparable`] one CARDINALITY axis over on the
2438 /// (comparable, incomparable) × (pair-level, set-level) primitive
2439 /// surface (`const _: () = assert!(ResourceLimits::is_antichain(
2440 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]));`), so
2441 /// a caller can pin an antichain-membership identity at compile time
2442 /// — a build-break rather than a runtime `assert!` on the first
2443 /// execution.
2444 ///
2445 /// Pre-lift, a caller wanting "is this slice an antichain?" composed
2446 /// the doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2447 /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))` two-
2448 /// primitive scaffolding at every prospective callsite — the SAME
2449 /// PRIME DIRECTIVE ≥2 pattern [`Self::is_chain`]'s pre-lift shape
2450 /// carried, with only the pair-level primitive swapped. Post-lift the
2451 /// antichain verdict binds at ONE typed method the algebra exposes,
2452 /// and the doubly-indexed all-pairs scaffolding lives at ONE
2453 /// implementation site — the same site [`Self::is_chain`] delegates
2454 /// to, factored down to a per-projection choice of pair-level
2455 /// primitive.
2456 ///
2457 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2458 /// proofs; the set-level antichain verdict on N preset-carried
2459 /// resource proofs is itself a typed named `bool` predicate composing
2460 /// them via the underlying pair-level [`Self::is_incomparable`]
2461 /// projection. THEORY.md §V.1 — knowable platform; the doubly-indexed
2462 /// all-pairs incomparability conjunction becomes a TYPE-level
2463 /// operation on the posture algebra rather than an inline two-
2464 /// primitive scaffolding at every consumer that decides "is this
2465 /// slice a pairwise-incomparable subset?"
2466 ///
2467 /// Frontier inspiration: the order-theoretic notion of an ANTICHAIN
2468 /// (a pairwise-incomparable subset of a poset) canonicalized in
2469 /// Dilworth's theorem (a poset's width — the maximum antichain size —
2470 /// equals the minimum number of chains covering the poset) and the
2471 /// Robinson–Schensted correspondence's Young-tableau chain/antichain
2472 /// duality; Haskell's `Data.List` idiom of
2473 /// `all (uncurry incomparable) . pairs` for a partial-order carrier;
2474 /// Coq's `Antichain` inductive on lists over a `PartialOrder`.
2475 /// Translation through pleme-io primitives: the doubly-indexed pair
2476 /// walk directly, with [`Self::is_incomparable`] as the pair-level
2477 /// primitive, no new dep, no supertrait bound, `const fn` throughout.
2478 /// The (chain, antichain) set-level pair opens the door to Dilworth's
2479 /// width invariant on the [`ResourceLimits`] lattice at a future
2480 /// callsite — the maximum antichain size is a substrate-level
2481 /// dimension the two set-level projections lift here jointly
2482 /// characterize.
2483 #[must_use]
2484 pub const fn is_antichain(postures: &[Self]) -> bool {
2485 let n = postures.len();
2486 let mut i = 0;
2487 while i < n {
2488 let mut j = i + 1;
2489 while j < n {
2490 if !postures[i].is_incomparable(postures[j]) {
2491 return false;
2492 }
2493 j += 1;
2494 }
2495 i += 1;
2496 }
2497 true
2498 }
2499
2500 /// The MIDDLE-CELL corner of the (chain, antichain, mixed) three-cell
2501 /// face on the set-level pairwise-relation surface — `postures` is a
2502 /// MIXED slice iff it is NEITHER a chain NOR an antichain, computed
2503 /// as the boolean conjunction of the negations of the just-lifted
2504 /// [`Self::is_chain`] and [`Self::is_antichain`] projections.
2505 ///
2506 /// EXHAUSTIVELY CLOSES the (chain, antichain, mixed) three-cell face
2507 /// at its previously-open middle cell — the two set-level ENDS the
2508 /// just-lifted [`Self::is_chain`] / [`Self::is_antichain`] pair
2509 /// pinned already carried the (chain, antichain) two ends; this
2510 /// projection binds the third cell (F, F) as a NAMED typed `bool`.
2511 /// The three cells now sit at (T, F) — pure chain, (F, T) — pure
2512 /// antichain, and (F, F) — mixed. The fourth cell (T, T) is only
2513 /// achievable at the degenerate empty-or-singleton slice where BOTH
2514 /// projections agree on the vacuous-truth verdict — [`Self::is_mixed`]
2515 /// reports `false` there too, aligning with the intuition that a
2516 /// slice with strictly fewer than two distinct pairs is neither
2517 /// chain-mixed nor antichain-mixed.
2518 ///
2519 /// **Empty-slice contract**: `ResourceLimits::is_mixed(&[]) == false`
2520 /// — [`Self::is_chain`] and [`Self::is_antichain`] BOTH return `true`
2521 /// vacuously, so `!true && !true == false`. The empty slice is NOT
2522 /// mixed. Pinned as
2523 /// `resource_limits_is_mixed_empty_slice_is_false`.
2524 ///
2525 /// **Singleton contract**: `ResourceLimits::is_mixed(&[a]) == false`
2526 /// for every posture `a` — a one-element slice contains no distinct
2527 /// pairs; both [`Self::is_chain`] and [`Self::is_antichain`] return
2528 /// `true` vacuously. Pinned as
2529 /// `resource_limits_is_mixed_singleton_is_false`.
2530 ///
2531 /// **Diagonal-duplicate contract**: `ResourceLimits::is_mixed(&[a, a])
2532 /// == false` for every posture `a` — [`Self::is_chain`] returns
2533 /// `true` via [`Self::is_comparable`]'s REFLEXIVITY;
2534 /// [`Self::is_antichain`] returns `false` via
2535 /// [`Self::is_incomparable`]'s IRREFLEXIVITY; the conjunction is
2536 /// `!true && !false == false`. The diagonal-duplicate slice is a
2537 /// CHAIN, not mixed. Pinned as
2538 /// `resource_limits_is_mixed_of_diagonal_duplicate_is_false`.
2539 ///
2540 /// **Chain rejection**: `ResourceLimits::is_mixed(
2541 /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2542 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the shipped-preset triple
2543 /// on the bounded-lattice diagonal is an ascending chain,
2544 /// [`Self::is_chain`] returns `true`, and `!true && …` short-
2545 /// circuits to `false`. Pinned as
2546 /// `resource_limits_is_mixed_rejects_the_shipped_preset_triple`.
2547 ///
2548 /// **Antichain rejection**: `ResourceLimits::is_mixed(
2549 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) ==
2550 /// false` — the hand-authored antichain pair binds
2551 /// [`Self::is_antichain`] `true`, and `!… && !true == false`. Pinned
2552 /// as
2553 /// `resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair`.
2554 ///
2555 /// **Mixed-set closure (LOAD-BEARING `true`-arm catch)**:
2556 /// `ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS,
2557 /// HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2558 /// — the (DEFAULT, MID) pair is comparable (falsifying
2559 /// [`Self::is_antichain`]) while the (MID, OTHER) pair is
2560 /// incomparable (falsifying [`Self::is_chain`]); the conjunction
2561 /// `!false && !false == true`. This is the SAME fixture the sibling
2562 /// [`Self::is_chain`] and [`Self::is_antichain`] BOTH reject at —
2563 /// pinning the DISCRIMINATING positive-arm catch for the (mixed)
2564 /// cell of the three-cell face. Pinned as
2565 /// `resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair`.
2566 ///
2567 /// **Trichotomy exhaustiveness at arity ≥3 with distinct
2568 /// elements**: for every slice `postures` with `len() >= 2` and
2569 /// pairwise-distinct elements, EXACTLY ONE of [`Self::is_chain`],
2570 /// [`Self::is_antichain`], [`Self::is_mixed`] returns `true` — the
2571 /// three cells partition the set-level verdict surface at every
2572 /// non-degenerate slice. At arity < 2 (empty and singleton) the
2573 /// verdict surface degenerates to the vacuous-truth (T, T, F)
2574 /// cell; at arity ≥2 with any duplicated element the verdict
2575 /// surface degenerates to the chain-only (T, F, F) cell (chain
2576 /// wins via reflexivity, antichain drops via irreflexivity).
2577 /// Pinned as
2578 /// `resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices`.
2579 ///
2580 /// Encoded as the boolean conjunction of the negations of the two
2581 /// just-lifted set-level projections — one implementation site, ZERO
2582 /// new pair-level primitive delegations, `const fn` throughout via
2583 /// the underlying `const fn` bodies. The doubly-indexed all-pairs
2584 /// walk lives at ONE site (the [`Self::is_chain`] +
2585 /// [`Self::is_antichain`] implementations), and this projection
2586 /// factors THROUGH both without reopening it.
2587 ///
2588 /// Pre-lift, a caller wanting "is this slice a mixed subset (neither
2589 /// chain nor antichain)?" composed the doubly-indexed
2590 /// `!postures.iter().enumerate().all(|(i, a)|
2591 /// postures[i + 1..].iter().all(|b| a.is_comparable(*b))) &&
2592 /// !postures.iter().enumerate().all(|(i, a)|
2593 /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))`
2594 /// two-scaffolding conjunction at every prospective callsite —
2595 /// the SAME PRIME DIRECTIVE ≥2 pattern the pair of just-lifted
2596 /// projections' pre-lift shapes carried, doubled. Post-lift the
2597 /// mixed-set verdict binds at ONE typed method the algebra exposes.
2598 ///
2599 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2600 /// preserves proofs; the set-level mixed-set verdict on N preset-
2601 /// carried resource proofs is itself a typed named `bool` predicate
2602 /// composing them via the underlying set-level [`Self::is_chain`] +
2603 /// [`Self::is_antichain`] projections. THEORY.md §V.1 — knowable
2604 /// platform; the doubly-negated set-level conjunction becomes a
2605 /// TYPE-level operation on the posture algebra rather than an inline
2606 /// negation-composition at every consumer that decides "is this
2607 /// slice a mixed subset?"
2608 ///
2609 /// Frontier inspiration: the order-theoretic notion of a MIXED
2610 /// subset (a subset that is neither a chain nor an antichain) sits
2611 /// at the interior of the (chain, antichain, mixed) trichotomy that
2612 /// canonicalizes the SET-LEVEL verdict surface of a poset — a slice
2613 /// whose Hasse-diagram induced subgraph carries both order-comparable
2614 /// AND order-incomparable edges. Dilworth's width invariant and the
2615 /// dual Mirsky theorem (a poset's height equals the minimum number of
2616 /// antichains covering it) both take mixed sets as their non-trivial
2617 /// inputs — a chain has width 1 and a mixed set of width ≥2 is
2618 /// exactly where the width invariant becomes informative. Haskell's
2619 /// `Data.List` idiom of `not . isChain && not . isAntichain`
2620 /// composition on a partial-order carrier. Translation through
2621 /// pleme-io primitives: the boolean conjunction of the negations of
2622 /// the just-lifted set-level projections, no new dep, no supertrait
2623 /// bound, `const fn` throughout.
2624 #[must_use]
2625 pub const fn is_mixed(postures: &[Self]) -> bool {
2626 !Self::is_chain(postures) && !Self::is_antichain(postures)
2627 }
2628
2629 /// Boolean `leq`-conjunction across every CONSECUTIVE pair of a slice
2630 /// of postures — `postures` is ASCENDING (a leq-monotone sequence on
2631 /// the pointwise partial order) iff every consecutive pair
2632 /// `(postures[i], postures[i + 1])` satisfies
2633 /// `postures[i].leq(postures[i + 1])`.
2634 ///
2635 /// The SEQUENCE-LEVEL N-ary peer of [`Self::leq`] one CARDINALITY axis
2636 /// over on the (pairwise, sequence-level) primitive surface — the
2637 /// DIRECT SEQUENCE-LEVEL LIFT of the pair-level partial-order primitive
2638 /// through a consecutive-index walk. Opens a NEW AXIS one CONSECUTIVE-
2639 /// VS-ALL-PAIRS step over from the just-lifted set-level projections
2640 /// [`Self::is_chain`] / [`Self::is_antichain`] / [`Self::is_mixed`]:
2641 /// where the set-level triple quantifies over EVERY distinct pair
2642 /// `(i, j)` with `j > i`, this projection quantifies over only the
2643 /// `n - 1` CONSECUTIVE pairs `(i, i + 1)`. The two axes lift the same
2644 /// pair-level primitive family through DIFFERENT quantification shapes
2645 /// — the set-level triple carries UNORDERED verdicts about the whole
2646 /// slice's pairwise-relation graph, while this projection carries an
2647 /// ORDER-SENSITIVE verdict about the slice's specific enumeration.
2648 ///
2649 /// **Transitivity theorem — `is_ascending ⇒ is_chain`**: for every
2650 /// slice `postures`, `Self::is_ascending(postures) == true` implies
2651 /// `Self::is_chain(postures) == true`. The pointwise partial order is
2652 /// TRANSITIVE (`a.leq(b) && b.leq(c) ⇒ a.leq(c)` — verified structurally
2653 /// by [`Self::leq`]'s six-axis pointwise `<=` conjunction, since each
2654 /// `usize <=` is transitive), so a consecutive-pair leq-chain closes
2655 /// under transitive composition into the all-pairs leq-chain; every
2656 /// (i, j) pair with j > i decomposes into the composition of
2657 /// consecutive-pair leqs (i, i+1), (i+1, i+2), ..., (j-1, j), which
2658 /// transitivity collapses into `postures[i].leq(postures[j])`, and
2659 /// `leq` implies `is_comparable`. This is a LOAD-BEARING substrate-
2660 /// level theorem: the sequence-level ascending verdict is strictly
2661 /// STRONGER than the set-level chain verdict (a chain admits any
2662 /// permutation of a leq-sorted enumeration; an ascending sequence
2663 /// admits exactly the leq-sorted one). Pinned as
2664 /// `resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice`.
2665 ///
2666 /// **Empty-slice vacuous truth**: `ResourceLimits::is_ascending(&[])
2667 /// == true` — the empty conjunction is vacuously true; the empty
2668 /// slice contains no consecutive pairs to reject. Peer of
2669 /// [`Self::is_chain`]'s empty-slice identity one CONSECUTIVE-VS-ALL-
2670 /// PAIRS axis over: both cells of the (set-level, sequence-level)
2671 /// projection pair AGREE at the empty slice on the vacuous-truth
2672 /// verdict — the two cells coincide at the empty face of the N-ary
2673 /// verdict surface. Pinned as
2674 /// `resource_limits_is_ascending_empty_slice_is_vacuously_true`.
2675 ///
2676 /// **Singleton vacuous truth**: `ResourceLimits::is_ascending(&[a])
2677 /// == true` for every posture `a` — a one-element slice contains no
2678 /// consecutive pairs, so the walk never enters the loop and returns
2679 /// `true` vacuously. Peer of [`Self::is_chain`]'s singleton-vacuous-
2680 /// truth identity: both projections AGREE at singleton slices too.
2681 /// Pinned as
2682 /// `resource_limits_is_ascending_singleton_is_vacuously_true`.
2683 ///
2684 /// **Diagonal-duplicate identity**: `ResourceLimits::is_ascending(
2685 /// &[a, a]) == true` for every posture `a` — [`Self::leq`] is
2686 /// REFLEXIVE (`a.leq(a) == true` via each axis's `<=` reflexivity), so
2687 /// the single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
2688 /// the conjunction holds. AGREES with [`Self::is_chain`]'s diagonal-
2689 /// duplicate verdict at every diagonal-duplicate slice — the two
2690 /// projections diverge only where the ORDER matters, and duplicated
2691 /// elements carry no ordering information to distinguish them.
2692 /// Pinned as `resource_limits_is_ascending_of_diagonal_duplicate_is_true`.
2693 ///
2694 /// **Ascending shipped-preset triple closure**: `ResourceLimits::
2695 /// is_ascending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2696 /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple
2697 /// on the bounded-lattice diagonal is a leq-monotone ascending
2698 /// sequence (`EMPTY.leq(DEFAULT).leq(UNBOUNDED)` pointwise), so every
2699 /// consecutive pair satisfies `leq` and the conjunction closes.
2700 /// Pinned as
2701 /// `resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple`.
2702 ///
2703 /// **Descending shipped-preset triple rejection**: `ResourceLimits::
2704 /// is_ascending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2705 /// EMPTY_RESOURCE_LIMITS]) == false` — the reversed enumeration of the
2706 /// same shipped-preset chain is a leq-monotone DESCENDING sequence, so
2707 /// the first consecutive pair binds `UNBOUNDED.leq(DEFAULT) == false`
2708 /// and the walk rejects at index 0. The DIRECT ORDER-SENSITIVITY
2709 /// FALSIFICATION of the ascending closure one PERMUTATION axis over on
2710 /// the SAME chain — [`Self::is_chain`] accepts BOTH orderings (its
2711 /// verdict is order-INSENSITIVE), while this projection accepts only
2712 /// the leq-sorted one. Pinned as
2713 /// `resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple`.
2714 ///
2715 /// **Non-monotone chain permutation rejection**: `ResourceLimits::
2716 /// is_ascending(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,
2717 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the third permutation of
2718 /// the shipped-preset chain has `DEFAULT.leq(EMPTY) == false` at the
2719 /// first consecutive pair, so the walk rejects, EVEN THOUGH the slice
2720 /// is a chain ([`Self::is_chain`] accepts it). Pins the DISCRIMINATING
2721 /// arm between the sequence-level and set-level projections: a chain
2722 /// permutation that is neither ascending nor descending falsifies BOTH
2723 /// sequence-level projections while both set-level projections accept
2724 /// it. Pinned as
2725 /// `resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation`.
2726 ///
2727 /// **Hand-authored antichain rejection**: `ResourceLimits::is_ascending(
2728 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2729 /// — the two hand-authored asymmetric postures are incomparable
2730 /// (neither `MID.leq(OTHER)` nor `OTHER.leq(MID)`), so the single
2731 /// consecutive pair rejects. Pinned as
2732 /// `resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair`.
2733 ///
2734 /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`) with
2735 /// one [`Self::leq`] delegation per consecutive pair — the SAME walk
2736 /// structure the standard-library `<[T]>::is_sorted_by` idiom carries
2737 /// on a totally-ordered carrier, generalized to the partial-order
2738 /// [`Self::leq`] primitive here. `const fn` throughout via the
2739 /// underlying `const fn` [`Self::leq`] body — a caller can pin an
2740 /// ascending-sequence identity at compile time
2741 /// (`const _: () = assert!(ResourceLimits::is_ascending(&[
2742 /// EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2743 /// UNBOUNDED_RESOURCE_LIMITS]));`) — a build-break rather than a
2744 /// runtime `assert!` on the first execution.
2745 ///
2746 /// Pre-lift, a caller wanting "is this slice a leq-monotone ascending
2747 /// sequence?" composed the singly-indexed
2748 /// `postures.windows(2).all(|w| w[0].leq(w[1]))` two-primitive
2749 /// scaffolding at every prospective callsite — the SAME PRIME
2750 /// DIRECTIVE ≥2 pattern the set-level projections' pre-lift shapes
2751 /// carried, one CARDINALITY axis over. Post-lift the ascending
2752 /// verdict binds at ONE typed method the algebra exposes, and the
2753 /// consecutive-pair walk lives at ONE implementation site — the peer
2754 /// [`Self::is_descending`] shares the SAME walk structure and diverges
2755 /// only on the pair-level primitive it delegates to (`geq` in place of
2756 /// `leq`).
2757 ///
2758 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2759 /// proofs; the sequence-level ascending verdict on N preset-carried
2760 /// resource proofs is itself a typed named `bool` predicate composing
2761 /// them via the underlying pair-level [`Self::leq`] projection.
2762 /// THEORY.md §V.1 — knowable platform; the consecutive-pair leq
2763 /// conjunction becomes a TYPE-level operation on the posture algebra
2764 /// rather than an inline windowed scaffolding at every consumer that
2765 /// decides "is this slice a leq-monotone ascending sequence?"
2766 ///
2767 /// Frontier inspiration: the order-theoretic notion of a MONOTONE
2768 /// SEQUENCE on a poset — the standard-library `Iterator::is_sorted`
2769 /// / `<[T]>::is_sorted_by` idioms carry the totally-ordered case;
2770 /// Haskell's `Data.List` idiom of `and . zipWith leq xs . tail xs`
2771 /// on a partial-order carrier; Coq's `Sorted` inductive on lists over
2772 /// a `Relation`. Translation through pleme-io primitives: the singly-
2773 /// indexed consecutive-pair walk directly, with [`Self::leq`] as the
2774 /// pair-level primitive, no new dep, no supertrait bound, `const fn`
2775 /// throughout. The (ascending, descending) sequence-level pair opens
2776 /// the door to `is_strictly_ascending` / `is_strictly_descending` via
2777 /// [`Self::lt`] / [`Self::gt`] one STRICTNESS axis over, and to
2778 /// `is_monotone` (`is_ascending || is_descending`) one DIRECTION-
2779 /// AGNOSTIC axis over, at future callsites.
2780 #[must_use]
2781 pub const fn is_ascending(postures: &[Self]) -> bool {
2782 let n = postures.len();
2783 let mut i = 0;
2784 while i + 1 < n {
2785 if !postures[i].leq(postures[i + 1]) {
2786 return false;
2787 }
2788 i += 1;
2789 }
2790 true
2791 }
2792
2793 /// Boolean `geq`-conjunction across every CONSECUTIVE pair of a slice
2794 /// of postures — `postures` is DESCENDING (a geq-monotone sequence on
2795 /// the pointwise partial order) iff every consecutive pair
2796 /// `(postures[i], postures[i + 1])` satisfies
2797 /// `postures[i].geq(postures[i + 1])`.
2798 ///
2799 /// The DIRECT SEQUENCE-LEVEL DUAL of [`Self::is_ascending`] one PAIR-
2800 /// LEVEL-PRIMITIVE axis over — where `is_ascending` delegates to
2801 /// [`Self::leq`], this projection delegates to [`Self::geq`]. The
2802 /// two together close the (ascending, descending) two-cell face on
2803 /// the sequence-level pairwise-relation surface, exactly one
2804 /// CONSECUTIVE-VS-ALL-PAIRS axis over from the set-level (chain,
2805 /// antichain) two-cell face [`Self::is_chain`] / [`Self::is_antichain`]
2806 /// pin, and exactly one PAIR-LEVEL-PRIMITIVE axis over from the
2807 /// (leq, geq) pair-level two-cell face [`Self::leq`] / [`Self::geq`]
2808 /// pin one CARDINALITY axis down.
2809 ///
2810 /// The (ascending, descending) pair is NOT a partition of the
2811 /// sequence-level verdict surface: a slice of ≥3 postures whose
2812 /// consecutive-pair verdicts mix leq and geq (or contain any
2813 /// incomparable consecutive pair) is neither ascending nor
2814 /// descending, so the two sequence-level projections carry a THREE-
2815 /// cell face (ascending, descending, non-monotone) at the sequence
2816 /// level whose ends the two projections lifted here pin. The
2817 /// (T, T) corner is achievable at every diagonal-duplicate slice
2818 /// AND at every empty-or-singleton slice — [`Self::leq`] and
2819 /// [`Self::geq`] are BOTH REFLEXIVE, so a repeated element passes
2820 /// both consecutive-pair conjunctions.
2821 ///
2822 /// **Transitivity theorem — `is_descending ⇒ is_chain`**: for every
2823 /// slice `postures`, `Self::is_descending(postures) == true` implies
2824 /// `Self::is_chain(postures) == true`, by the SAME transitive
2825 /// composition [`Self::is_ascending`] carries one PAIR-LEVEL-PRIMITIVE
2826 /// axis over — the pointwise partial order is transitive under `geq`
2827 /// (by [`Self::geq`]'s composition `other.leq(self)` and `leq`'s six-
2828 /// axis transitivity), so a consecutive-pair geq-chain closes under
2829 /// transitive composition into an all-pairs geq-chain, and `geq`
2830 /// implies `is_comparable`. Pinned as
2831 /// `resource_limits_is_descending_implies_is_chain_on_every_shipped_slice`.
2832 ///
2833 /// **Empty-slice vacuous truth**: `ResourceLimits::is_descending(&[])
2834 /// == true` — the empty conjunction is vacuously true. Peer of
2835 /// [`Self::is_ascending`]'s empty-slice identity one PAIR-LEVEL-
2836 /// PRIMITIVE axis over: both cells of the (ascending, descending)
2837 /// sequence-level pair AGREE at the empty slice. Pinned as
2838 /// `resource_limits_is_descending_empty_slice_is_vacuously_true`.
2839 ///
2840 /// **Singleton vacuous truth**: `ResourceLimits::is_descending(&[a])
2841 /// == true` for every posture `a`. Pinned as
2842 /// `resource_limits_is_descending_singleton_is_vacuously_true`.
2843 ///
2844 /// **Diagonal-duplicate identity**: `ResourceLimits::is_descending(
2845 /// &[a, a]) == true` for every posture `a` — [`Self::geq`] is
2846 /// REFLEXIVE, so the single consecutive pair binds `a.geq(a) == true`.
2847 /// AGREES with [`Self::is_ascending`]'s diagonal-duplicate verdict —
2848 /// both leq and geq carry reflexivity, so the sequence-level (T, T)
2849 /// corner opens at every diagonal-duplicate slice, distinct from the
2850 /// set-level (T, F) verdict [`Self::is_chain`] / [`Self::is_antichain`]
2851 /// carry on the same slice. Pinned as
2852 /// `resource_limits_is_descending_of_diagonal_duplicate_is_true`.
2853 ///
2854 /// **Descending shipped-preset triple closure**: `ResourceLimits::
2855 /// is_descending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2856 /// EMPTY_RESOURCE_LIMITS]) == true` — the reversed enumeration of the
2857 /// shipped-preset chain is a geq-monotone descending sequence. The
2858 /// DIRECT MIRROR of [`Self::is_ascending`]'s ascending closure one
2859 /// PERMUTATION axis over — pinning the two closures on the SAME chain
2860 /// in REVERSED orderings closes the (ascending, descending) sequence-
2861 /// level pair on the shipped chain. Pinned as
2862 /// `resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple`.
2863 ///
2864 /// **Ascending shipped-preset triple rejection**: `ResourceLimits::
2865 /// is_descending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2866 /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the ascending-sorted
2867 /// enumeration binds `EMPTY.geq(DEFAULT) == false`. The DIRECT
2868 /// FALSIFICATION of [`Self::is_ascending`]'s ascending closure one
2869 /// CELL axis over on the SAME slice. Pinned as
2870 /// `resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple`.
2871 ///
2872 /// **Hand-authored antichain rejection**: `ResourceLimits::is_descending(
2873 /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2874 /// — the two hand-authored asymmetric postures are incomparable, so
2875 /// the single consecutive pair rejects. Pinned as
2876 /// `resource_limits_is_descending_rejects_the_hand_authored_antichain_pair`.
2877 ///
2878 /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`)
2879 /// with one [`Self::geq`] delegation per consecutive pair — the SAME
2880 /// iteration structure as [`Self::is_ascending`] one PAIR-LEVEL-
2881 /// PRIMITIVE axis over, with only the inner primitive swapped between
2882 /// the two. `const fn` throughout via the underlying `const fn`
2883 /// [`Self::geq`] body.
2884 ///
2885 /// Pre-lift, a caller wanting "is this slice a geq-monotone descending
2886 /// sequence?" composed the singly-indexed
2887 /// `postures.windows(2).all(|w| w[0].geq(w[1]))` two-primitive
2888 /// scaffolding at every prospective callsite. Post-lift the
2889 /// descending verdict binds at ONE typed method the algebra exposes.
2890 ///
2891 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2892 /// proofs. THEORY.md §V.1 — knowable platform.
2893 ///
2894 /// Frontier inspiration: `Iterator::is_sorted_by` with a reversed
2895 /// comparator on a totally-ordered carrier; Coq's `StronglySorted`
2896 /// on a `<=` relation applied with the argument order flipped.
2897 /// Translation through pleme-io primitives: the singly-indexed
2898 /// consecutive-pair walk directly, with [`Self::geq`] as the pair-
2899 /// level primitive.
2900 #[must_use]
2901 pub const fn is_descending(postures: &[Self]) -> bool {
2902 let n = postures.len();
2903 let mut i = 0;
2904 while i + 1 < n {
2905 if !postures[i].geq(postures[i + 1]) {
2906 return false;
2907 }
2908 i += 1;
2909 }
2910 true
2911 }
2912}
2913
2914/// Cache key: (macro name, SipHash-2-4 of args). We hash `Sexp` directly via
2915/// its manual `Hash` impl — no serde_json round-trip per cache lookup.
2916type CacheKey = (String, u64);
2917
2918/// A registered macro definition.
2919#[derive(Debug, Clone)]
2920pub struct MacroDef {
2921 pub name: String,
2922 pub params: MacroParams,
2923 /// The template body (usually a Quasiquote).
2924 pub body: Sexp,
2925}
2926
2927impl MacroDef {
2928 /// Project the macro body to its substitution-walked form: the inner of
2929 /// the outer `Sexp::Quasiquote` when `(defmacro NAME (PARAMS) `(...))`
2930 /// authored the body through the canonical quasi-quote affordance, OR
2931 /// `&self.body` verbatim when authored without one. The two expansion
2932 /// strategies — bytecode (`compile_template`) and substitute (`apply`'s
2933 /// substitute fallback) — both walk this projection, never the raw
2934 /// `body`, because the outer quasi-quote is the syntactic "you're
2935 /// inside a template" marker and the substitution semantics operate on
2936 /// what's INSIDE it. Naming the projection lifts the inline
2937 /// `match &def.body { Sexp::Quasiquote(inner) => inner.as_ref(),
2938 /// other => other }` peel that appeared verbatim at BOTH sites — well
2939 /// past the ≥2 PRIME-DIRECTIVE trigger — into ONE function the two
2940 /// strategies share, so a regression that drifts ONE strategy's
2941 /// body-projection from the other (e.g. one path peels twice and the
2942 /// other peels once, or one path treats `Sexp::Quote(...)` as a
2943 /// template marker and the other doesn't) becomes structurally
2944 /// impossible: there is exactly one implementation both strategies
2945 /// call.
2946 ///
2947 /// Single-level peel by design: a nested `` ``form `` body unwraps to
2948 /// `` `form `` (the inner quasi-quote stays as-is), matching the v0
2949 /// "no nested quasi-quotes" scope the module preamble declares. A
2950 /// non-quasi-quote body — `(defmacro NAME (PARAMS) BODY)` where BODY
2951 /// is a plain `Sexp::List` / `Sexp::Atom` — returns `&self.body`
2952 /// verbatim, the "other" arm of the legacy match. The borrow is
2953 /// strictly `&'a Sexp` rooted in `&'a self.body` (no clone, no
2954 /// allocation); both `compile_node` (bytecode path) and `substitute`
2955 /// (substitute path) consume the projection immediately and never
2956 /// outlive the borrow.
2957 ///
2958 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
2959 /// inline copies of the body-peel match is the ≥2 trigger, and the
2960 /// substrate names the projection ONCE so authoring surfaces and
2961 /// future expansion strategies (a third interpreter? a JIT? a
2962 /// debugger that wants to render the body without the outer
2963 /// quasi-quote marker?) bind to ONE primitive. THEORY.md §II.1
2964 /// invariant 2 — free middle; the two expansion strategies emit
2965 /// IDENTICAL output for the same (macro, args) pair, and sharing one
2966 /// body-projection makes that per-strategy agreement structural at
2967 /// the entry to the walker, not a two-site discipline the
2968 /// `expansion_layers_agree_on_output_and_cache_wins` benchmark only
2969 /// observes after the fact.
2970 #[must_use]
2971 pub fn template_body(&self) -> &Sexp {
2972 match &self.body {
2973 Sexp::Quasiquote(inner) => inner.as_ref(),
2974 other => other,
2975 }
2976 }
2977}
2978
2979/// A macro's parameter list — structurally "zero or more required
2980/// positional params, then zero or more `&optional` params, then an OPTIONAL
2981/// single `&rest` param." This is the canonical Lisp lambda-list ordering
2982/// (Common Lisp `(req* &optional opt* &rest r)`), made a TYPE.
2983///
2984/// This shape promotes the invariants the reader ([`parse_params`])
2985/// previously upheld only by construction — `&rest` is LAST, there is AT MOST
2986/// ONE of it, and (now) `&optional` params sit strictly between the required
2987/// run and the rest — from *unobserved discipline* to *unrepresentable
2988/// state*. The prior representation `Vec<Param>` admitted `[Rest, Required]`
2989/// (a `&rest` in the middle) and `[Rest, Rest]` (two of them); both are
2990/// nonsense the binder cannot honor, yet the type permitted them. The flat
2991/// param INDEX that the bytecode references (`Subst(idx)` / `Splice(idx)`)
2992/// and the positional binder both walk would silently misalign on such a
2993/// `Vec` — a `Rest` at index 0 of `[Rest, Required]` makes the binder grab
2994/// every arg, then fail to bind the trailing `Required`, mapping the
2995/// template's index-1 substitution onto the wrong value. `MacroParams`
2996/// cannot express either shape: `rest` is exactly one `Option<String>`,
2997/// always conceptually after every `required` then every `optional` name,
2998/// and the three kinds live in distinct fields whose order is fixed by the
2999/// struct, not by a discipline the binder trusts a `Vec` to have upheld.
3000///
3001/// `optional` differs from `required` in the binder, not the index contract:
3002/// a required name with no arg at its position is a `MissingMacroArg`
3003/// rejection; an optional name with no arg binds to its declared default form
3004/// — `Sexp::Nil` when none was given, the parsed default literal when one was.
3005/// Both shapes — `&optional x` and `&optional (x 5)` — are now structural in
3006/// the typed [`OptionalParam`] entry rather than smeared across a flat
3007/// `Vec<String>` the binder would have had to discover the default for
3008/// elsewhere.
3009///
3010/// The flat-index contract the template bytecode depends on is preserved by
3011/// [`MacroParams::names`]: index `0..required.len()` are the required names
3012/// in order, the next `optional.len()` indices are the optional names, and
3013/// the final index (if present) is the rest name — the canonical lambda-list
3014/// order. [`MacroParams::bind`] produces the per-index bound values in that
3015/// same order, so the name-keyed (`bind_args` → `substitute`) and
3016/// index-keyed (`apply_compiled`) expansion strategies share ONE binder and
3017/// can never drift.
3018///
3019/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3020/// unrepresentable"; the lambda-list ordering (required → optional → rest,
3021/// rest-is-last, at-most-one-rest) becomes structural. THEORY.md §VI.1 —
3022/// generation over composition; the positional binding loop (verbatim in
3023/// both `bind_args` and `apply_compiled`, the ≥2 PRIME-DIRECTIVE trigger) is
3024/// lifted to ONE owner, `bind`, which the optional arm extends in one place.
3025#[derive(Debug, Clone, Default, PartialEq)]
3026pub struct MacroParams {
3027 pub required: Vec<String>,
3028 pub optional: Vec<OptionalParam>,
3029 pub rest: Option<String>,
3030}
3031
3032/// One entry in a macro's `&optional` section — a name plus an optional
3033/// default form. The two surface shapes the reader admits collapse into this
3034/// single typed shape:
3035///
3036/// * `&optional x` ⇒ `OptionalParam { name: "x", default: None }`
3037/// * `&optional (x 5)` ⇒ `OptionalParam { name: "x", default: Some(Int(5)) }`
3038///
3039/// The `default: Option<Sexp>` slot makes the per-param default-form a
3040/// FIELD on each optional entry, not a discipline a sibling `Vec<Sexp>` would
3041/// have had to maintain in lock-step with `Vec<String>`. Without this shape
3042/// the binder cannot tell "no arg supplied, no default declared → bind nil"
3043/// from "no arg supplied, default `5` declared → bind `5`": both would
3044/// collapse onto `Sexp::Nil`, the precise silent misalignment the typed
3045/// shape exists to forbid.
3046///
3047/// The default is the LITERAL `Sexp` — there is no evaluator in v0, so a
3048/// `(x (foo 1))` spec parks `(foo 1)` verbatim as the bound value when `x`'s
3049/// arg is absent. This is the no-evaluator floor of CL semantics: any
3050/// arbitrary form is admitted at the gate, what it MEANS is the next layer's
3051/// concern. The default is parsed exactly once at `defmacro`/
3052/// `defpoint-template`/`defcheck` time (inside `parse_params`); every call
3053/// to that macro consumes the same parsed `Sexp` via `Clone`, never re-
3054/// reading the source.
3055///
3056/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3057/// unrepresentable"; the (name, default?) pair is one entry rather than two
3058/// parallel `Vec`s a regression could desynchronize. THEORY.md §VI.1 —
3059/// generation over composition; the binder's optional arm consults
3060/// `param.default` in ONE place, so the substitute and bytecode strategies
3061/// inherit identical default-resolution semantics from the shared `bind`.
3062#[derive(Debug, Clone, PartialEq)]
3063pub struct OptionalParam {
3064 pub name: String,
3065 pub default: Option<Sexp>,
3066}
3067
3068impl OptionalParam {
3069 /// `&optional x` — a bare optional name with no default. An absent
3070 /// argument binds to `Sexp::Nil` (the no-default-form floor).
3071 #[must_use]
3072 pub fn bare(name: impl Into<String>) -> Self {
3073 Self {
3074 name: name.into(),
3075 default: None,
3076 }
3077 }
3078
3079 /// `&optional (x DEFAULT)` — an optional with a default form. An absent
3080 /// argument binds to `default.clone()`.
3081 #[must_use]
3082 pub fn with_default(name: impl Into<String>, default: Sexp) -> Self {
3083 Self {
3084 name: name.into(),
3085 default: Some(default),
3086 }
3087 }
3088
3089 /// The bound value when an absent call leaves this optional slot unfilled:
3090 /// the declared default form (cloned) when one was authored, OR the
3091 /// canonical `Sexp::Nil` floor when none was — the CL `&optional` no-
3092 /// default-form floor. ONE named primitive on the typed [`OptionalParam`]
3093 /// every absent-call binder consults; before this lift the same two-arm
3094 /// fallback `param.default.clone().unwrap_or(Sexp::Nil)` lived inline at
3095 /// [`MacroParams::bind`]'s optional arm, and a future absence-resolver
3096 /// (the kwarg-gate's typed-default fill? a future `&supplied-p` slot's
3097 /// "was this defaulted?" bit?) would have had to re-derive the same
3098 /// two-arm fallback at every site that walks the optional run.
3099 ///
3100 /// The projection IS the structural identity binding the typed
3101 /// `default: Option<Sexp>` slot to its bound-value contract:
3102 /// * `bare(name).resolved_default()` is `Sexp::Nil` (the no-default
3103 /// floor — `default.is_none()`).
3104 /// * `with_default(name, d).resolved_default()` is `d.clone()` (the
3105 /// declared default — `default = Some(d)` projected through Clone).
3106 ///
3107 /// `resolved_default()` is the typed accessor companion to the
3108 /// `bare` / `with_default` constructors: those two constructors define
3109 /// the ONLY admissible shapes of the typed `default` slot, and this
3110 /// accessor names the BOUND-VALUE projection both shapes yield at the
3111 /// binder's absence arm. Together the three close the `OptionalParam`'s
3112 /// self-contained typed surface — every authored shape lands through ONE
3113 /// of two constructors, and every absent-call binder reads through this
3114 /// ONE accessor.
3115 ///
3116 /// Returns an owned `Sexp` (not `&Sexp`) because the binder pushes the
3117 /// resolved default into a fresh `Vec<Sexp>` slot at every absent call;
3118 /// the `default.clone()` projection is the same allocation the pre-lift
3119 /// inline expression performed, just named at the typed boundary. The
3120 /// `Sexp::Nil` floor is a free per-call construction (a unit variant
3121 /// with no payload), so the no-default path is free of allocation
3122 /// beyond the function return slot.
3123 ///
3124 /// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid
3125 /// states unrepresentable"; the "no-default-form floor" structural
3126 /// concept becomes a NAMED projection on [`OptionalParam`] rather than
3127 /// re-derived `param.default.clone().unwrap_or(Sexp::Nil)` arithmetic
3128 /// at every site that walks the bound optional run. Authoring tools
3129 /// (REPL, LSP, `tatara-check`) that want to render "this optional
3130 /// binds to {default-form|nil} when absent" bind to ONE method on the
3131 /// typed param. THEORY.md §VI.1 — generation over composition; the
3132 /// constructor pair `bare` / `with_default` defines the typed shapes
3133 /// and the `resolved_default` accessor names the symmetric
3134 /// bound-value projection — the typed accessor companion. THEORY.md
3135 /// §II.1 invariant 2 — free middle; both expansion strategies route
3136 /// through the SHARED `MacroParams::bind`, so the new accessor is
3137 /// exposed to the bytecode and substitute paths uniformly via that
3138 /// shared binder.
3139 #[must_use]
3140 pub fn resolved_default(&self) -> Sexp {
3141 self.default.clone().unwrap_or(Sexp::Nil)
3142 }
3143}
3144
3145/// The value carrier a macro call binds its arguments in.
3146///
3147/// [`MacroParams::bind_carrier`] is the single positional binder every
3148/// expansion path routes through; this trait is the only thing that varies
3149/// between those paths. It names the two — and only two — places a binder
3150/// must SYNTHESIZE a value the caller never wrote:
3151///
3152/// * [`Self::lift_default`] — an `&optional` slot the call left unfilled
3153/// binds to the declared default form (or the `Sexp::Nil` floor). The
3154/// default is authored as plain `Sexp` at `defmacro` time, so each
3155/// carrier says how a template literal enters it.
3156/// * [`Self::collect_rest`] — the `&rest` slot collects the surplus args
3157/// into ONE value. `Sexp` wraps them in `Sexp::List`; a span-carrying
3158/// carrier additionally has to decide what span the synthesized list
3159/// wears.
3160///
3161/// Everything else in the binder — the required run, the arity rejections,
3162/// the required→optional→rest ordering — is carrier-independent and lives
3163/// exactly once.
3164///
3165/// `Site` is the per-call synthesis context. It is `()` for `Sexp`, whose
3166/// values carry no position; a span-carrying carrier sets it to the macro
3167/// CALL SITE, because a value the call never supplied has no source position
3168/// of its own and the call site is the honest place to point an error at.
3169///
3170/// Theory anchor: THEORY.md §V.1 — knowable platform; "which lambda-list
3171/// shapes does this expansion path accept?" stops being a per-path answer.
3172/// THEORY.md §VI.1 — generation over composition.
3173pub trait MacroArgCarrier: Clone {
3174 /// Per-call synthesis context — `()` when the carrier needs none.
3175 type Site: Copy;
3176
3177 /// Lift a template-authored default form into this carrier, for an
3178 /// `&optional` slot the call left unfilled.
3179 fn lift_default(default: &Sexp, site: Self::Site) -> Self;
3180
3181 /// Collect the `&rest` surplus into one carrier value.
3182 fn collect_rest(items: Vec<Self>, site: Self::Site) -> Self;
3183}
3184
3185impl MacroArgCarrier for Sexp {
3186 /// `Sexp` carries no position, so there is nothing to thread — the unit
3187 /// site is the structural statement that this carrier synthesizes values
3188 /// without context.
3189 type Site = ();
3190
3191 fn lift_default(default: &Sexp, (): ()) -> Self {
3192 default.clone()
3193 }
3194
3195 fn collect_rest(items: Vec<Self>, (): ()) -> Self {
3196 Sexp::List(items)
3197 }
3198}
3199
3200impl MacroParams {
3201 /// Canonical `&rest` marker — the ONE typed `&'static str` on
3202 /// [`MacroParams`] the parser's rest-slot dispatch specialises on.
3203 /// The Common-Lisp lambda-list `&rest` keyword names the position
3204 /// at which the parser stops collecting `required` / `optional`
3205 /// names and binds every subsequent arg into a `Sexp::List` at the
3206 /// [`Self::rest`] slot. Sibling posture to [`Self::OPTIONAL_MARKER`]
3207 /// (`"&optional"`) on the same lambda-list-keyword algebra layer:
3208 /// the two constants are the closed set of Common-Lisp
3209 /// lambda-list-keyword `&'static str` markers the parser's typed
3210 /// dispatch specialises on, sharing the canonical `char` lead byte
3211 /// [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3212 ///
3213 /// Pre-lift the same `"&rest"` bytes lived inline as a single
3214 /// `s == "&rest"` comparison inside [`parse_params`], with the two
3215 /// typed `&'static str` markers the CL lambda-list algebra
3216 /// specialises on smeared across TWO parallel `s == "..."` inline
3217 /// comparisons at the SAME dispatch cascade. Post-lift the
3218 /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3219 /// binds at ONE constant on the typed [`MacroParams`] algebra that
3220 /// both the parser AND any future authoring / rendering surface
3221 /// route through; a refactor that swaps the marker (e.g. a
3222 /// Clojure-compat port to `&` ONLY, an Elisp-compat port that
3223 /// keeps `&rest` as-is, a Scheme R7RS-style port to `.` for the
3224 /// dotted-pair rest slot) touches ONE constant rather than the
3225 /// two inline literals scattered across the parser AND the
3226 /// diagnostic message surfaces.
3227 ///
3228 /// Structural round-trip contract:
3229 /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3230 /// — the projection law binding the `&'static str` marker to its
3231 /// canonical `char` lead byte on the typed algebra, pinned by
3232 /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`.
3233 ///
3234 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3235 /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3236 /// binds at ONE `pub const` on the typed [`MacroParams`] algebra
3237 /// regardless of which parser / authoring surface reaches in.
3238 /// THEORY.md §V.1 — knowable platform; the canonical CL lambda-
3239 /// list `&rest` keyword becomes a TYPE-level constant on the
3240 /// typed macro-params algebra rather than an inline `"&rest"`
3241 /// literal at the parser's rest-slot dispatch arm.
3242 pub const REST_MARKER: &'static str = "&rest";
3243
3244 /// Canonical `&optional` marker — the ONE typed `&'static str` on
3245 /// [`MacroParams`] the parser's optional-section dispatch
3246 /// specialises on. The Common-Lisp lambda-list `&optional` keyword
3247 /// names the position at which the parser switches subsequent
3248 /// bare-symbol names from the `required` bin to the `optional`
3249 /// bin, until either the end of the param list OR the
3250 /// [`Self::REST_MARKER`] terminal is reached. Sibling posture to
3251 /// [`Self::REST_MARKER`] (`"&rest"`) on the same lambda-list-
3252 /// keyword algebra layer: the two constants are the closed set of
3253 /// CL lambda-list-keyword `&'static str` markers the parser's
3254 /// typed dispatch specialises on, sharing the canonical `char`
3255 /// lead byte [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3256 ///
3257 /// Structural round-trip contract:
3258 /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3259 /// — pinned by
3260 /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3261 ///
3262 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3263 /// (`&optional` section-switch marker, canonical `&'static str`)
3264 /// pairing binds at ONE `pub const` on the typed [`MacroParams`]
3265 /// algebra regardless of which parser / authoring surface reaches
3266 /// in. THEORY.md §V.1 — knowable platform; the canonical CL
3267 /// lambda-list `&optional` keyword becomes a TYPE-level constant
3268 /// on the typed macro-params algebra rather than an inline
3269 /// `"&optional"` literal at the parser's optional-section
3270 /// dispatch arm.
3271 pub const OPTIONAL_MARKER: &'static str = "&optional";
3272
3273 /// Canonical `&` LEAD byte shared across [`Self::REST_MARKER`]
3274 /// (`"&rest"`) and [`Self::OPTIONAL_MARKER`] (`"&optional"`) — the
3275 /// ONE canonical `char` on the [`MacroParams`] algebra that the
3276 /// Common-Lisp lambda-list-keyword disjointness contract binds to.
3277 ///
3278 /// Sibling posture to the closed set of `pub const` per-role
3279 /// canonical bytes on the substrate's other closed-set outer
3280 /// algebras: [`crate::ast::Atom::STR_DELIMITER`] (`'"'`),
3281 /// [`crate::ast::Atom::STR_ESCAPE_LEAD`] (`'\\'`),
3282 /// [`crate::ast::Atom::KEYWORD_MARKER_LEAD`] (`':'`),
3283 /// [`crate::ast::Atom::BOOL_LITERAL_LEAD`] (`'#'`),
3284 /// [`crate::ast::Sexp::LIST_OPEN`] (`'('`),
3285 /// [`crate::ast::Sexp::LIST_CLOSE`] (`')'`),
3286 /// [`crate::ast::Sexp::COMMENT_LEAD`] (`';'`),
3287 /// [`crate::ast::Sexp::COMMENT_TERM`] (`'\n'`),
3288 /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`] (`'@'`), and
3289 /// every `crate::ast::QuoteForm::lead_char` projection — every
3290 /// canonical per-role byte the substrate's typed algebras
3291 /// specialise on is now a `pub const` on its owning closed-set
3292 /// algebra. This constant closes the CL lambda-list-keyword
3293 /// family lead byte at the SAME algebra as its two `&'static str`
3294 /// marker projections so a delimiter swap (e.g. a Racket-compat
3295 /// port from CL `&rest` / `&optional` to `#!rest` / `#!optional`
3296 /// keyword-args, a Clojure-compat port to `&` ONLY as the rest
3297 /// marker) lands at ONE constant on the algebra.
3298 ///
3299 /// Structural round-trip contract:
3300 /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3301 /// AND
3302 /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3303 /// — the projection law binding the two `&'static str` markers to
3304 /// their shared canonical `char` lead byte, pinned by
3305 /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
3306 /// and
3307 /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3308 ///
3309 /// Disjointness contract: `LAMBDA_LIST_KEYWORD_LEAD`'s byte MUST
3310 /// differ from every sibling outer-marker `char` the substrate's
3311 /// other closed-set algebras specialise on. A collision would
3312 /// silently break the reader's outer dispatch: an `&`-prefixed
3313 /// bare atom `&rest` / `&optional` would collide with whichever
3314 /// marker it aliased. Pinned structurally at
3315 /// `macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`.
3316 ///
3317 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3318 /// (CL lambda-list-keyword LEAD byte, canonical `'&'`) pairing
3319 /// binds at ONE constant on the typed [`MacroParams`] algebra
3320 /// regardless of which of the two `&'static str` marker consumers
3321 /// reaches in. THEORY.md §V.1 — knowable platform; the canonical
3322 /// CL lambda-list-keyword LEAD byte becomes a TYPE-level constant
3323 /// on the substrate algebra rather than an inline `'&'` glyph at
3324 /// every docstring pinning the family disjointness contract.
3325 pub const LAMBDA_LIST_KEYWORD_LEAD: char = '&';
3326
3327 /// Closed-set forced-arity ALL array over the CL lambda-list-keyword
3328 /// `&'static str` markers the parser's typed dispatch specialises on
3329 /// — the [`Self::REST_MARKER`] (`"&rest"`) rest-slot marker followed
3330 /// by the [`Self::OPTIONAL_MARKER`] (`"&optional"`) optional-section
3331 /// marker, in the canonical CL lambda-list ordering (`&optional`
3332 /// binds AFTER `&required`, `&rest` binds AFTER `&optional`, so the
3333 /// declaration walk fires `REST_MARKER` FIRST as the terminal-arm
3334 /// early return; the ALL array's ordering keys on marker
3335 /// declaration order in this file, not on parse-time ordering).
3336 ///
3337 /// Sibling posture to the closed set of `pub const ALL: [Self; N]`
3338 /// forced-arity arrays on the substrate's other closed-set outer
3339 /// algebras: [`crate::ast::AtomKind::ALL`] (`[Self; 6]`),
3340 /// [`crate::ast::QuoteForm::ALL`] (`[Self; 4]`),
3341 /// [`crate::error::SexpShape::ALL`] (`[Self; 12]`),
3342 /// [`crate::error::UnquoteForm::ALL`] (`[Self; 2]`),
3343 /// [`crate::error::MacroDefHead::ALL`] (`[Self; 3]`) — every
3344 /// closed-set algebra the substrate carries pins its cardinality at
3345 /// the declaration site via a `pub const ALL` array whose forced
3346 /// arity fails compilation if a new variant lands without being
3347 /// added to the set. `LAMBDA_LIST_KEYWORDS` closes the same shape
3348 /// on the CL lambda-list-keyword family: a third marker (`&key`
3349 /// for keyword-args, `&aux` for auxiliaries, `&body` for the
3350 /// docstring-carrying tail of `defmacro` bodies) extends this ONE
3351 /// `pub const` + the matching per-role marker `pub const` above,
3352 /// AND rustc enforces every downstream consumer's closed-set
3353 /// sweep picks up the new marker at the SAME structural site.
3354 ///
3355 /// Pre-lift the two `&'static str` markers were named on the typed
3356 /// algebra but the family they close was NOT — the two structural
3357 /// round-trip pins (`_rest_marker_prefixed_by_lambda_list_keyword_
3358 /// lead` AND `_optional_marker_prefixed_by_lambda_list_keyword_
3359 /// lead`) each named ONE marker inline as a duplicate 3-line
3360 /// `assert!(A.starts_with(LEAD), ...)` shape, and the pairwise-
3361 /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3362 /// named the two markers as a hand-rolled `assert_ne!` pair.
3363 /// Post-lift the ALL array binds the family at the typed algebra so
3364 /// every family-wide contract (LEAD-byte round-trip, pairwise
3365 /// disjointness, membership gate) routes through the ONE array and
3366 /// a future `&key` / `&aux` extension automatically extends the
3367 /// sweep without re-deriving per-marker assertions.
3368 ///
3369 /// Structural round-trip contract: every element `m` of
3370 /// `Self::LAMBDA_LIST_KEYWORDS` satisfies
3371 /// `m.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)` — pinned by
3372 /// `macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead`.
3373 /// Cardinality contract: `Self::LAMBDA_LIST_KEYWORDS.len() == 2`
3374 /// pins the current family size at the type level; a future third
3375 /// marker extends the array's arity AND updates every downstream
3376 /// pin at ONE structural site. Pairwise-disjointness contract:
3377 /// every distinct index pair `(i, j)` in `Self::LAMBDA_LIST_KEYWORDS`
3378 /// satisfies `Self::LAMBDA_LIST_KEYWORDS[i] !=
3379 /// Self::LAMBDA_LIST_KEYWORDS[j]` — pinned by
3380 /// `macro_params_lambda_list_keywords_pairwise_distinct`.
3381 ///
3382 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3383 /// CL lambda-list-keyword family closure binds at ONE `pub const`
3384 /// on the typed [`MacroParams`] algebra regardless of which
3385 /// consumer (parser dispatch, family-wide contract sweep,
3386 /// authoring / rendering surface, `is_lambda_list_keyword`
3387 /// membership gate) reaches in. THEORY.md §V.1 — knowable
3388 /// platform; the family's cardinality becomes a TYPE-level
3389 /// constant on the substrate algebra rather than a per-consumer
3390 /// hand-rolled enumeration of the two markers. THEORY.md §VI.1 —
3391 /// generation over composition; the family-wide contract sweeps
3392 /// (LEAD-byte round-trip, pairwise disjointness, membership gate)
3393 /// emerge from the composition of TWO substrate primitives (this
3394 /// `pub const` array + the per-role `pub const REST_MARKER` /
3395 /// `pub const OPTIONAL_MARKER`) rather than as per-marker inline
3396 /// assertions the current pins duplicate structurally.
3397 ///
3398 /// Frontier inspiration: Common Lisp's `LAMBDA-LIST-KEYWORDS`
3399 /// standard variable (CLHS §3.4.1) — the runtime-reflectable list
3400 /// of every reserved lambda-list keyword the implementation
3401 /// recognises; `LAMBDA_LIST_KEYWORDS` is the substrate's typed-Rust
3402 /// peer at compile time, with the closed-set ALL array standing in
3403 /// for CL's runtime-mutable list. Translation through pleme-io
3404 /// primitives: a `pub const [&'static str; N]` on the typed
3405 /// [`MacroParams`] algebra rather than a runtime-mutable list;
3406 /// forced-arity closure at the type layer with the same
3407 /// family-reflective read surface.
3408 pub const LAMBDA_LIST_KEYWORDS: [&'static str; 2] = [Self::REST_MARKER, Self::OPTIONAL_MARKER];
3409
3410 /// Typed membership gate over the closed set
3411 /// [`Self::LAMBDA_LIST_KEYWORDS`] — `true` iff `s` matches some
3412 /// element of the ALL array byte-for-byte, `false` for every other
3413 /// input.
3414 ///
3415 /// The typed "does this symbol name a CL lambda-list keyword?"
3416 /// projection on the [`MacroParams`] algebra. Sibling posture to the
3417 /// closed set of per-algebra membership / classifier gates the
3418 /// substrate carries:
3419 /// [`crate::ast::QuoteForm::from_lead_char`] (per-char quote-family
3420 /// dispatch — Some/None on a closed 3-char set),
3421 /// [`crate::ast::Sexp::is_bare_atom_boundary`] (six-fold
3422 /// bare-atom-boundary sweep), and
3423 /// [`crate::ast::Atom::decode_str_escape`] (Str-escape decode table
3424 /// on a closed 6-arm set) — every family-membership gate the
3425 /// substrate carries binds at ONE method on the closed-set
3426 /// algebra so a family extension lands at ONE place plus the
3427 /// per-role marker without re-deriving per-consumer inline
3428 /// enumerations.
3429 ///
3430 /// Pre-lift the "is this symbol a CL lambda-list keyword?" concept
3431 /// existed structurally but was NOT named on the algebra — the
3432 /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3433 /// AND the cross-axis disjointness sweep
3434 /// (`_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`)
3435 /// each named the concept implicitly as a hand-rolled per-marker
3436 /// `assert_ne!` sweep. Post-lift the membership gate binds at ONE
3437 /// typed method on the algebra a future consumer (a parser-time
3438 /// "this symbol looks like a lambda-list keyword but isn't
3439 /// recognised" hint, an authoring-surface completion bar that
3440 /// suggests `&rest` / `&optional` at param-list positions, a
3441 /// pretty-printer that colors CL lambda-list keywords distinctly)
3442 /// binds against without re-deriving the per-marker enumeration
3443 /// at every consumer site.
3444 ///
3445 /// Contract:
3446 /// * Every element of [`Self::LAMBDA_LIST_KEYWORDS`] classifies
3447 /// as `true` — pinned by
3448 /// `macro_params_is_lambda_list_keyword_accepts_every_marker`.
3449 /// * The bare CL lambda-list-keyword LEAD byte
3450 /// ([`Self::LAMBDA_LIST_KEYWORD_LEAD`]) as a 1-char string
3451 /// classifies as `false` — pinned by
3452 /// `macro_params_is_lambda_list_keyword_rejects_bare_lead_byte`.
3453 /// * Symbols starting with the LEAD byte but naming an
3454 /// unrecognised keyword (`"&key"`, `"&aux"`, `"&body"`)
3455 /// classify as `false` — pinned by
3456 /// `macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names`.
3457 ///
3458 /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3459 /// closed-set membership gate binds at ONE typed method on the
3460 /// [`MacroParams`] algebra regardless of which consumer reaches
3461 /// in. THEORY.md §III — the typescape; the "is this a CL
3462 /// lambda-list keyword?" projection becomes a TYPE projection on
3463 /// the substrate algebra rather than per-consumer inline
3464 /// enumeration of the two markers. THEORY.md §V.1 — knowable
3465 /// platform; a future third marker (`&key`, `&aux`, `&body`)
3466 /// extends [`Self::LAMBDA_LIST_KEYWORDS`] ONCE and every
3467 /// membership-gate consumer picks up the new marker for free.
3468 ///
3469 /// Frontier inspiration: Common Lisp's `(find X LAMBDA-LIST-KEYWORDS)`
3470 /// pattern for testing lambda-list keyword membership at runtime;
3471 /// `is_lambda_list_keyword` is the substrate's typed-Rust peer at
3472 /// compile time. Translation through pleme-io primitives: a linear
3473 /// sweep over the [`Self::LAMBDA_LIST_KEYWORDS`] `pub const` array
3474 /// rather than a runtime `find` over a mutable list; ordinary
3475 /// slice `contains` on the `&'static str` array.
3476 #[must_use]
3477 pub fn is_lambda_list_keyword(s: &str) -> bool {
3478 Self::LAMBDA_LIST_KEYWORDS.contains(&s)
3479 }
3480
3481 /// The flat, ordered param-name list the template bytecode indexes into:
3482 /// every `required` name in order, then every `optional` name in order,
3483 /// then the `rest` name if present. `names()[i]` is the param `Subst(i)`
3484 /// / `Splice(i)` reference.
3485 #[must_use]
3486 pub fn names(&self) -> Vec<&str> {
3487 self.required
3488 .iter()
3489 .map(String::as_str)
3490 .chain(self.optional.iter().map(|p| p.name.as_str()))
3491 .chain(self.rest.as_deref())
3492 .collect()
3493 }
3494
3495 /// The rest-less maximum arity of this param list: `required.len() +
3496 /// optional.len()`. Two equivalent readings collapse into ONE primitive
3497 /// on the typed `MacroParams`:
3498 ///
3499 /// * The **rest-start boundary**: when `self.rest` is `Some`, the
3500 /// `&rest` slot collects `args[fixed_arity()..]` into a
3501 /// `Sexp::List` (the empty slice when the call is exactly
3502 /// saturated). `fixed_arity()` IS that slice's start index.
3503 /// * The **rest-less maximum arity**: when `self.rest` is `None`,
3504 /// `args.len() > fixed_arity()` is the surplus-args rejection
3505 /// boundary [`bind`](Self::bind) checks before raising
3506 /// `LispError::TooManyMacroArgs` (the call-site mirror of
3507 /// `RestParamTrailingTokens`'s definition-site rejection).
3508 ///
3509 /// Both readings ARE the same arithmetic; [`bind`](Self::bind)
3510 /// previously inlined the same `self.required.len() +
3511 /// self.optional.len()` expression THREE times — once inside the
3512 /// `Vec::with_capacity(required + optional + rest?)` slot, once at
3513 /// the `rest_start` site (inside `if let Some(_rest_name) =
3514 /// self.rest`), and once at the `expected` site (inside the
3515 /// rest-less `else`). The latter two live in mutually-exclusive
3516 /// branches yet name ONE structural concept; lifting the arithmetic
3517 /// to a single named primitive makes that concept first-class on
3518 /// the typed param list.
3519 ///
3520 /// `fixed_arity()` IGNORES the `rest` slot by construction — a
3521 /// `&rest` param has no maximum and is not part of the fixed run.
3522 /// `names().len() == fixed_arity() + usize::from(self.rest.is_some())`
3523 /// is the structural identity binding this primitive to
3524 /// [`names`](Self::names) and to the `Vec::with_capacity` hint
3525 /// [`bind`](Self::bind) computes for the bound-values vec.
3526 ///
3527 /// Theory anchor: THEORY.md §V.1 — knowable platform; the structural
3528 /// "rest-start boundary / rest-less max arity" concept becomes a
3529 /// named `&MacroParams` projection rather than re-derived arithmetic
3530 /// at every site that walks the bound run. Authoring tools (REPL,
3531 /// LSP, `tatara-check`) that want to render "this macro takes
3532 /// between `required.len()` and `fixed_arity()` args (or unbounded
3533 /// if `rest.is_some()`)" bind to ONE method on the typed param
3534 /// list. THEORY.md §VI.1 — generation over composition; three
3535 /// inline copies of the same arithmetic in one function is past
3536 /// the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
3537 /// named. THEORY.md §II.1 invariant 2 — free middle; both
3538 /// expansion strategies route through the SHARED `bind`, so the
3539 /// new primitive is exposed to the bytecode and substitute paths
3540 /// uniformly — no per-strategy drift in how the boundary is
3541 /// computed.
3542 #[must_use]
3543 pub fn fixed_arity(&self) -> usize {
3544 self.required.len() + self.optional.len()
3545 }
3546
3547 /// The TOTAL lambda-list arity of this param list — every named
3548 /// slot the reader accepted, in the canonical CL lambda-list
3549 /// ordering: `required.len() + optional.len() + rest.is_some() as
3550 /// usize`. Peer to [`Self::fixed_arity`] one REST-SLOT axis over
3551 /// on the `MacroParams` algebra — where [`Self::fixed_arity`]
3552 /// ignores the rest slot (the rest-less maximum-arity /
3553 /// rest-start-boundary reading, unbounded on the call-time surface
3554 /// when `self.rest.is_some()`), this projection INCLUDES the rest
3555 /// slot as one additional named param (the total-slots reading,
3556 /// bounded on the type-time surface regardless of `rest`).
3557 ///
3558 /// `Self::total_arity() == Self::names().len()` — a `Vec`-free
3559 /// projection of the [`Self::names`] flat-index list's length. Two
3560 /// equivalent readings collapse into ONE named primitive on the
3561 /// typed [`MacroParams`] algebra:
3562 ///
3563 /// * The **`Vec::with_capacity` hint**: [`Self::bind`] previously
3564 /// inlined `self.fixed_arity() + usize::from(self.rest.is_some())`
3565 /// as the bound-vec capacity hint. That expression IS this
3566 /// primitive.
3567 /// * The **REGISTER-time arity ceiling**: on the arity-bomb
3568 /// `(defmacro huge (a-1 a-2 … a-N) `,a-1)` [`Expander::register_macro_def`]
3569 /// compares `def.params.total_arity()` against
3570 /// [`Expander::max_macro_arity`] BEFORE the body-size gate walks
3571 /// the body or any insert lands. `total_arity()` IS that gate's
3572 /// axis.
3573 ///
3574 /// The structural identity
3575 /// `names().len() == fixed_arity() + usize::from(rest.is_some())
3576 /// == total_arity()`
3577 /// binds the primitive to [`Self::names`] and [`Self::fixed_arity`]
3578 /// on the [`MacroParams`] algebra — pinned by
3579 /// `macro_params_total_arity_matches_names_len` AND
3580 /// `macro_params_total_arity_equals_fixed_arity_plus_rest_bit`.
3581 ///
3582 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
3583 /// "how many named slots does this macro declare?" projection
3584 /// becomes a NAMED `&MacroParams` accessor rather than re-derived
3585 /// arithmetic at [`Self::bind`]'s capacity hint AND at the
3586 /// [`Expander::register_macro_def`] arity-ceiling gate.
3587 /// THEORY.md §VI.1 — generation over composition; two inline copies
3588 /// of the same three-arm arithmetic across two consumers is the
3589 /// ≥2 PRIME-DIRECTIVE trigger once the structural shape is named.
3590 /// THEORY.md §II.1 invariant 2 — free middle; both the binder AND
3591 /// the arity-guard consult ONE primitive, so a regression that
3592 /// drifts the guard's arity computation from the binder's capacity
3593 /// hint (e.g. one counts `rest` as one slot, the other counts it
3594 /// as zero) becomes structurally impossible.
3595 #[must_use]
3596 pub fn total_arity(&self) -> usize {
3597 self.fixed_arity() + usize::from(self.rest.is_some())
3598 }
3599
3600 /// Bind call args to params positionally, returning the per-index bound
3601 /// values parallel to [`names`](Self::names): each required name takes
3602 /// the arg at its position (a missing one is
3603 /// [`missing_macro_arg`](self::missing_macro_arg)); each optional name
3604 /// takes the arg at its position, or — when the call ran out of args —
3605 /// its declared default form (`Sexp::Nil` when none was declared,
3606 /// matching CL's `&optional` floor); and a present `rest` collects every
3607 /// arg beyond the required+optional run into a `Sexp::List` (the empty
3608 /// list when none remain). Args beyond a REST-LESS param list have
3609 /// nowhere to bind and are rejected
3610 /// ([`too_many_macro_args`](self::too_many_macro_args)) rather than
3611 /// silently truncated. This is the single binding loop every expansion
3612 /// strategy shares — `apply_compiled` consumes the index vec directly,
3613 /// `bind_args` zips it against `names()` into the name-keyed map — and
3614 /// it is the `Sexp` instantiation of the carrier-generic
3615 /// [`Self::bind_carrier`].
3616 fn bind(&self, macro_name: &str, args: &[Sexp]) -> Result<Vec<Sexp>> {
3617 self.bind_carrier(macro_name, args, ())
3618 }
3619
3620 /// The ONE positional binder, generic over the value carrier a macro call
3621 /// binds its arguments in — [`Sexp`] for the plain `Sexp`→`Sexp`
3622 /// expander, `Spanned` for the span-preserving one, and any future
3623 /// carrier a consumer implements [`MacroArgCarrier`] for.
3624 ///
3625 /// Genericising this loop is what makes ONE lambda-list semantics
3626 /// (required run → `&optional` run with per-param defaults → at-most-one
3627 /// `&rest`, plus the too-few / too-many arity rejections) reachable from
3628 /// every expansion path, instead of each path restating the loop against
3629 /// its own value type. Before this lift the span-preserving expander
3630 /// carried its own `bind_spanned_args` that knew only `Required`/`Rest`
3631 /// — so `(defmacro f (a &optional b) …)` parsed on one path and was
3632 /// rejected on the other, and a surplus argument was a hard error on one
3633 /// path and silently dropped on the other. Both divergences are now
3634 /// structurally impossible: there is exactly one loop.
3635 ///
3636 /// `site` is the carrier's synthesis context — `()` for `Sexp` (a
3637 /// default form and a rest list need no context to construct), the
3638 /// macro CALL SITE span for `Spanned` (a value the CALL never supplied
3639 /// has no source position of its own, so it is stamped at the call).
3640 ///
3641 /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
3642 /// binding loop had reached three copies (plain, spanned, and
3643 /// `tatara-lisp-eval`'s macro-time env binder), well past the ≥2
3644 /// PRIME-DIRECTIVE trigger.
3645 pub fn bind_carrier<V: MacroArgCarrier>(
3646 &self,
3647 macro_name: &str,
3648 args: &[V],
3649 site: V::Site,
3650 ) -> Result<Vec<V>> {
3651 let mut out = Vec::with_capacity(self.total_arity());
3652 for (i, name) in self.required.iter().enumerate() {
3653 let arg = args
3654 .get(i)
3655 .cloned()
3656 .ok_or_else(|| missing_macro_arg(macro_name, name))?;
3657 out.push(arg);
3658 }
3659 let opt_start = self.required.len();
3660 for (j, param) in self.optional.iter().enumerate() {
3661 // Absent optional slot binds to the typed `resolved_default()`
3662 // projection on `OptionalParam`: the declared default form when
3663 // one was authored, OR the `Sexp::Nil` no-default floor when
3664 // none was. The two-arm fallback `param.default.clone().
3665 // unwrap_or(Sexp::Nil)` previously inlined here is now ONE named
3666 // accessor on the typed param both expansion strategies share via
3667 // `MacroParams::bind_carrier`.
3668 let arg = match args.get(opt_start + j) {
3669 Some(supplied) => supplied.clone(),
3670 None => V::lift_default(¶m.resolved_default(), site),
3671 };
3672 out.push(arg);
3673 }
3674 if let Some(_rest_name) = self.rest.as_ref() {
3675 // The `&rest` slot collects args[fixed_arity()..] (the empty
3676 // slice when the call is exactly saturated); the boundary is
3677 // the typed `fixed_arity()` primitive both branches share.
3678 let rest = args.get(self.fixed_arity()..).unwrap_or(&[]).to_vec();
3679 out.push(V::collect_rest(rest, site));
3680 } else {
3681 // No `&rest` slot — the param list has a FIXED maximum arity
3682 // of `fixed_arity()`. Surplus args have nowhere to bind;
3683 // reject rather than silently truncate. Closes the call-site
3684 // mirror of `RestParamTrailingTokens` (the definition-site
3685 // rejection lifted by the prior-run typed-promotion lineage),
3686 // so the typed-entry macro-call-gate is structurally complete
3687 // in both directions: too-few (`MissingMacroArg`) AND too-many
3688 // (`TooManyMacroArgs`).
3689 let expected = self.fixed_arity();
3690 if args.len() > expected {
3691 return Err(too_many_macro_args(macro_name, expected, args.len()));
3692 }
3693 }
3694 Ok(out)
3695 }
3696}
3697
3698/// Macro environment. Collects `defmacro` forms and rewrites callers.
3699///
3700/// Expansion strategy is tunable per-expander:
3701/// - **Compiled (default)** — every registered macro's template is walked once
3702/// and flattened into a linear `CompiledTemplate` (a tiny bytecode: Literal,
3703/// Subst(index), Splice(index), BeginList, EndList). Expansion of a call
3704/// is then a linear pass with no HashMap lookups and no recursion through
3705/// the template Sexp. Purely-literal subtrees compile to a single
3706/// `Literal(Sexp)` op — huge win for macros where most of the body is fixed.
3707/// - **Substitute-only** — runs the name-keyed `substitute` walker. Slower
3708/// but proves equivalence; used in the benchmark test to measure the
3709/// compiled-vs-substituted speedup.
3710#[derive(Clone, Default)]
3711pub struct Expander {
3712 macros: HashMap<String, MacroDef>,
3713 /// Pre-compiled template bytecodes, populated when `compile_templates`.
3714 templates: HashMap<String, CompiledTemplate>,
3715 /// When true, register a CompiledTemplate alongside each macro and dispatch
3716 /// expansion through the bytecode interpreter.
3717 compile_templates: bool,
3718 /// Memoization of `apply(macro, args)` — repeated calls with identical
3719 /// args skip expansion entirely. Shared across clones so realizations of
3720 /// the same `CompilerSpec` benefit across .compile() invocations.
3721 cache: Arc<Mutex<HashMap<CacheKey, Sexp>>>,
3722 /// Toggle caching. Default on — caching is the actual performance win
3723 /// the bytecode layer enables.
3724 cache_enabled: bool,
3725 /// The six resource ceilings, held as ONE typed [`ResourceLimits`] bundle
3726 /// rather than six sibling `max_*: usize` fields. Peer of the six
3727 /// getter/setter pairs one AGGREGATION axis over — where each pair
3728 /// projects ONE ceiling, this field carries the SIX-fold cross-product
3729 /// so [`Self::resource_limits`] returns `self.limits` verbatim and
3730 /// [`Self::set_resource_limits`] assigns through the bundle in ONE move.
3731 ///
3732 /// Adding a SEVENTH ceiling touches [`ResourceLimits`] + the matching
3733 /// `DEFAULT_MAX_*` module constant + the paired individual getter/setter
3734 /// — the [`Expander`] struct, its constructors, AND
3735 /// [`Self::resource_limits`] / [`Self::set_resource_limits`] stay
3736 /// exactly as they are. That's the compounding win of the
3737 /// consolidation: the (Expander, constructors, bulk getter, bulk
3738 /// setter) four-corner face is field-shape-agnostic — it composes with
3739 /// the bundle by value, not by field-by-field literal.
3740 ///
3741 /// Consumers on the read path (`register_macro_def`'s three REGISTER-time
3742 /// gates, `expand_with_depth`'s depth + output-size gates, `apply`'s
3743 /// cache-entries gate) dereference through `self.limits.max_*`; the
3744 /// projection is a `Copy usize` load — same shape as reading a bare
3745 /// field, no `Deref`, no `Arc`, no lock.
3746 limits: ResourceLimits,
3747}
3748
3749impl Expander {
3750 /// Default-posture expander with an operator-supplied resource-ceiling
3751 /// bundle — the AGGREGATION-axis peer of
3752 /// [`Self::set_resource_limits`] one CONSTRUCTOR-STAGE axis over.
3753 /// Where [`Self::set_resource_limits`] applies the bundled six ceilings
3754 /// POST-construction on an already-built expander, this constructor
3755 /// binds them AT construction so a call-site that wants a fresh
3756 /// expander with a non-default posture composes ONE typed call
3757 /// (`Expander::with_limits(limits)`) rather than the two-step
3758 /// (`let mut e = Expander::new(); e.set_resource_limits(limits);`)
3759 /// dance the post-construction sibling still admits.
3760 ///
3761 /// The (compile_templates, cache_enabled) execution-strategy pair
3762 /// carries the [`Self::new`] default posture (`true, true`) — the
3763 /// bytecode-with-cache strategy — since callers that want the
3764 /// substitute-only strategy still route through
3765 /// [`Self::new_substitute_only`]. Post-lift both convenience
3766 /// constructors delegate their `macros` / `templates` / `cache` field
3767 /// literals through THIS primitive at the resource-posture axis: the
3768 /// four-out-of-six field literals (`HashMap::new()` × 2 +
3769 /// `Arc::new(Mutex::new(HashMap::new()))` + `limits`) lived
3770 /// byte-identical across [`Self::new`] and [`Self::new_substitute_only`]
3771 /// pre-lift; the two divergent flags (`compile_templates`,
3772 /// `cache_enabled`) still differ between the two constructors, and the
3773 /// substitute-only variant flips them AFTER threading through
3774 /// `with_limits` — same shape as [`Self::new_bytecode_no_cache`] does
3775 /// on the `cache_enabled` flag.
3776 ///
3777 /// Round-trip identity: `Expander::with_limits(limits).resource_limits()
3778 /// == limits` for any `ResourceLimits` — pinned as a typed theorem in
3779 /// the test cohort. The at-construction assignment is a direct `Copy`
3780 /// of the bundled field (no per-field cascade), so a mid-construction
3781 /// panic window on the six knobs is structurally impossible. Peer of
3782 /// the `Expander::with_limits(DEFAULT_RESOURCE_LIMITS) ==
3783 /// Expander::new()` delegation identity — pinned on the resource-
3784 /// posture projection so a future refactor that drifts the two
3785 /// constructors on the ceiling axis fails loudly.
3786 ///
3787 /// Frontier inspiration: Cranelift's `settings::Flags::new(builder)` —
3788 /// codegen ceilings (opt level, allocator strategy, feature enables)
3789 /// bind at CONSTRUCTION-time on a typed `Flags` bundle rather than
3790 /// composed through post-construction setters on the `Isa`. Same
3791 /// posture as this lift: the (getter, setter, at-construction)
3792 /// three-corner face on the resource-posture surface now binds at ONE
3793 /// typed bundle across ALL three surfaces.
3794 ///
3795 /// Theory grounding: THEORY.md §II.1 invariants 1 + 2 — typed entry +
3796 /// free middle. Pre-lift the "expander with a chosen resource posture"
3797 /// concept was untyped: consumers composed [`Self::new`] +
3798 /// [`Self::set_resource_limits`] at their call site with no
3799 /// compile-time gate that BOTH steps land atomically. Post-lift the
3800 /// concept has ONE named entry point on the `Expander` surface, and a
3801 /// future consumer (`RealizedCompiler` builder that ships a
3802 /// CI-tightened preset, an LSP session that ships a REPL-permissive
3803 /// preset, a test-harness fixture that ships an `UNBOUNDED`-style
3804 /// preset) binds through ONE typed constructor.
3805 pub fn with_limits(limits: ResourceLimits) -> Self {
3806 Self {
3807 macros: HashMap::new(),
3808 templates: HashMap::new(),
3809 compile_templates: true,
3810 cache: Arc::new(Mutex::new(HashMap::new())),
3811 cache_enabled: true,
3812 limits,
3813 }
3814 }
3815
3816 /// Default expander — compiled bytecode + expansion cache enabled.
3817 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3818 /// so the (macros, templates, cache, limits) field-shape agreement
3819 /// with every other constructor on this `Expander` surface lives at
3820 /// ONE literal.
3821 pub fn new() -> Self {
3822 Self::with_limits(DEFAULT_RESOURCE_LIMITS)
3823 }
3824
3825 /// Expander using the legacy substitute path (no template compilation,
3826 /// no cache). Kept for benchmarking + equivalence testing.
3827 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3828 /// and flips the two divergent execution-strategy flags — same
3829 /// composition shape as [`Self::new_bytecode_no_cache`] one flag over.
3830 pub fn new_substitute_only() -> Self {
3831 let mut e = Self::with_limits(DEFAULT_RESOURCE_LIMITS);
3832 e.compile_templates = false;
3833 e.cache_enabled = false;
3834 e
3835 }
3836
3837 /// Expander with bytecode on but expansion cache off — isolates the cache
3838 /// contribution from the bytecode infrastructure. Benchmark baseline.
3839 pub fn new_bytecode_no_cache() -> Self {
3840 let mut e = Self::new();
3841 e.cache_enabled = false;
3842 e
3843 }
3844
3845 /// Toggle the expansion cache at runtime.
3846 pub fn set_cache_enabled(&mut self, enabled: bool) {
3847 self.cache_enabled = enabled;
3848 }
3849
3850 /// How many entries are currently cached.
3851 pub fn cache_size(&self) -> usize {
3852 self.cache.lock().unwrap().len()
3853 }
3854
3855 /// Clear the expansion cache (e.g., after redefining a macro).
3856 pub fn clear_cache(&self) {
3857 self.cache.lock().unwrap().clear();
3858 }
3859
3860 /// The configured ceiling on `expand`'s recursive re-entry into a
3861 /// macro-call form. Defaults to [`DEFAULT_MAX_EXPANSION_DEPTH`].
3862 #[must_use]
3863 pub fn max_expansion_depth(&self) -> usize {
3864 self.limits.max_expansion_depth
3865 }
3866
3867 /// Set the ceiling on `expand`'s recursive re-entry into a macro-call
3868 /// form. Tests bind low values (e.g. `4`) to pin the runaway-rejection
3869 /// shape without paying for a 256-round walk; consumers that compose
3870 /// deep proven macro cascades can raise it. Zero is admitted and
3871 /// makes every first macro-call return
3872 /// [`LispError::ExpansionDepthExceeded`] immediately — useful for
3873 /// contract-tests that assert "this reader path never expands a
3874 /// macro."
3875 pub fn set_max_expansion_depth(&mut self, depth: usize) {
3876 self.limits.max_expansion_depth = depth;
3877 }
3878
3879 /// The configured ceiling on the expansion cache's entry count.
3880 /// Defaults to [`DEFAULT_MAX_CACHE_ENTRIES`]. Peer to
3881 /// [`Self::max_expansion_depth`] one RESOURCE axis over.
3882 #[must_use]
3883 pub fn max_cache_entries(&self) -> usize {
3884 self.limits.max_cache_entries
3885 }
3886
3887 /// Set the ceiling on the expansion cache's entry count. Tests bind
3888 /// low values (e.g. `2`) to pin the bounded-cache contract without
3889 /// paying for an 8K-entry walk; consumers that expand many distinct
3890 /// argument shapes can raise it. Zero is admitted and effectively
3891 /// disables caching (every fresh `(name, args)` pair skips the
3892 /// insert path) — useful for contract-tests that assert "this
3893 /// expander realizes without ever caching." [`usize::MAX`] is the
3894 /// unbounded-cache mode for batch compilation runs where memory is
3895 /// not the concern.
3896 pub fn set_max_cache_entries(&mut self, cap: usize) {
3897 self.limits.max_cache_entries = cap;
3898 }
3899
3900 /// The configured ceiling on a single macro-`apply` output's
3901 /// [`crate::ast::Sexp::node_count`]. Defaults to
3902 /// [`DEFAULT_MAX_EXPANSION_SIZE`]. Peer to
3903 /// [`Self::max_expansion_depth`] and [`Self::max_cache_entries`]
3904 /// one RESOURCE-DIMENSION axis over.
3905 #[must_use]
3906 pub fn max_expansion_size(&self) -> usize {
3907 self.limits.max_expansion_size
3908 }
3909
3910 /// Set the ceiling on a single macro-`apply` output's node count.
3911 /// Tests bind low values (e.g. `8`) to pin the "expansion bomb"
3912 /// rejection shape without materializing a 64K-node blob;
3913 /// consumers that compose deep proven macro cascades can raise it.
3914 /// [`usize::MAX`] admits any lawful output (the ceiling is
3915 /// effectively lifted); operators that want strict "your macros
3916 /// must fit in N nodes" contract-tests set it exactly.
3917 pub fn set_max_expansion_size(&mut self, size: usize) {
3918 self.limits.max_expansion_size = size;
3919 }
3920
3921 /// The configured ceiling on a registered macro's BODY
3922 /// [`crate::ast::Sexp::node_count`]. Defaults to
3923 /// [`DEFAULT_MAX_MACRO_BODY_SIZE`]. Peer to
3924 /// [`Self::max_expansion_depth`], [`Self::max_cache_entries`],
3925 /// and [`Self::max_expansion_size`] one PIPELINE-STAGE axis over
3926 /// (the three prior guards fire at EXPAND time; this one fires
3927 /// at REGISTER time).
3928 #[must_use]
3929 pub fn max_macro_body_size(&self) -> usize {
3930 self.limits.max_macro_body_size
3931 }
3932
3933 /// Set the ceiling on a registered macro's BODY node count.
3934 /// Tests bind low values (e.g. `4`) to pin the authoring-bomb
3935 /// rejection shape without materializing a 16K-node blob;
3936 /// consumers that author deep proven macro-body templates can
3937 /// raise it. [`usize::MAX`] admits any lawful body (the ceiling
3938 /// is effectively lifted). Zero is admitted and rejects every
3939 /// non-empty body immediately (a `Nil` body is 1 node, so the
3940 /// smallest lawful body is still rejected under a zero
3941 /// ceiling) — useful for contract-tests that assert "this
3942 /// expander refuses every registration."
3943 pub fn set_max_macro_body_size(&mut self, size: usize) {
3944 self.limits.max_macro_body_size = size;
3945 }
3946
3947 /// The configured ceiling on the [`Self::macros`] table's entry
3948 /// count. Defaults to [`DEFAULT_MAX_REGISTERED_MACROS`]. Peer to
3949 /// [`Self::max_macro_body_size`] one RESOURCE-DIMENSION axis over
3950 /// on the REGISTER-time surface, and to [`Self::max_cache_entries`]
3951 /// one PIPELINE-STAGE axis over.
3952 #[must_use]
3953 pub fn max_registered_macros(&self) -> usize {
3954 self.limits.max_registered_macros
3955 }
3956
3957 /// Set the ceiling on the [`Self::macros`] table's entry count.
3958 /// Tests bind low values (e.g. `3`) to pin the registration-bomb
3959 /// rejection shape without materializing a 4K-entry macros table;
3960 /// consumers that compose deep proven typescapes can raise it.
3961 /// [`usize::MAX`] admits any lawful cumulative registration count
3962 /// (the ceiling is effectively lifted). Zero is admitted and
3963 /// rejects every FRESH registration immediately — useful for
3964 /// contract-tests that assert "this expander accepts no new
3965 /// macros" — while still admitting overwrites of any macro that
3966 /// was registered before the ceiling was set (a re-registration
3967 /// of an existing key never grows the table).
3968 pub fn set_max_registered_macros(&mut self, cap: usize) {
3969 self.limits.max_registered_macros = cap;
3970 }
3971
3972 /// The configured ceiling on a registered macro's lambda-list
3973 /// arity. Defaults to [`DEFAULT_MAX_MACRO_ARITY`]. Peer to
3974 /// [`Self::max_macro_body_size`] and [`Self::max_registered_macros`]
3975 /// one RESOURCE-DIMENSION axis over on the REGISTER-time surface.
3976 #[must_use]
3977 pub fn max_macro_arity(&self) -> usize {
3978 self.limits.max_macro_arity
3979 }
3980
3981 /// Set the ceiling on a registered macro's lambda-list arity —
3982 /// the [`MacroParams::total_arity`] projection of `def.params`.
3983 /// Tests bind low values (e.g. `2`) to pin the arity-bomb rejection
3984 /// shape without materializing a 128-slot authoring blob;
3985 /// consumers that author high-arity domain macros (a `defschema`
3986 /// with 40 typed slots) can raise it. [`usize::MAX`] admits any
3987 /// lawful arity (the ceiling is effectively lifted). Zero is
3988 /// admitted and rejects every non-nullary macro registration
3989 /// immediately — a `(defmacro nullary () BODY)` is still admitted
3990 /// (its `total_arity()` is `0`, and only strict overrun rejects) —
3991 /// useful for contract-tests that assert "this expander accepts
3992 /// only nullary macros."
3993 pub fn set_max_macro_arity(&mut self, cap: usize) {
3994 self.limits.max_macro_arity = cap;
3995 }
3996
3997 /// Snapshot the six configured resource ceilings as ONE typed
3998 /// [`ResourceLimits`] value — the bundled peer of the six
3999 /// [`Self::max_expansion_depth`] / [`Self::max_cache_entries`] /
4000 /// [`Self::max_expansion_size`] / [`Self::max_macro_body_size`] /
4001 /// [`Self::max_registered_macros`] / [`Self::max_macro_arity`]
4002 /// individual getters. Consumers that want to inspect the full
4003 /// resource posture (a test that compares a preset expander to a
4004 /// reference posture, a diagnostic that renders "the six ceilings"
4005 /// as one line, a caller that clones + selectively overrides one
4006 /// field via a struct-update literal) bind to this projection at
4007 /// ONE call site rather than composing the six individual getters.
4008 ///
4009 /// Post the [`Self::limits`]-field consolidation the projection is
4010 /// a direct `Copy` of the bundled field — a `usize`-sized fixed
4011 /// return with no per-field literal walk. Adding a SEVENTH ceiling
4012 /// extends [`ResourceLimits`] and the paired individual
4013 /// getter/setter; this bulk getter is field-shape-agnostic and
4014 /// stays exactly as it is.
4015 #[must_use]
4016 pub fn resource_limits(&self) -> ResourceLimits {
4017 self.limits
4018 }
4019
4020 /// Bulk-replace the six configured resource ceilings from ONE typed
4021 /// [`ResourceLimits`] value — the bundled peer of the six
4022 /// [`Self::set_max_expansion_depth`] /
4023 /// [`Self::set_max_cache_entries`] /
4024 /// [`Self::set_max_expansion_size`] /
4025 /// [`Self::set_max_macro_body_size`] /
4026 /// [`Self::set_max_registered_macros`] /
4027 /// [`Self::set_max_macro_arity`] individual setters. Presets
4028 /// (a hardened CI expander, a permissive REPL expander, a
4029 /// throttled test harness) compose the six ceilings ONCE at the
4030 /// preset call site and apply them ATOMICALLY here rather than
4031 /// threading six independent setter invocations whose ordering
4032 /// the type system does not gate.
4033 ///
4034 /// Round-trip identity: `let l = e.resource_limits(); e.set_resource_limits(l);`
4035 /// leaves `e` in an equivalent posture — pinned as a typed theorem
4036 /// in the [`ResourceLimits`] test cohort. Post the [`Self::limits`]-
4037 /// field consolidation the assignment is a direct `Copy` of the
4038 /// bundled field, structurally atomic — no per-field destructure +
4039 /// six-way write cascade in which a mid-cascade panic could leave
4040 /// the six knobs in a half-updated posture. Sibling of
4041 /// [`Self::resource_limits`]; the pair closes the (getter, setter)
4042 /// face on the aggregation axis.
4043 pub fn set_resource_limits(&mut self, limits: ResourceLimits) {
4044 self.limits = limits;
4045 }
4046
4047 pub fn with_macros<I: IntoIterator<Item = MacroDef>>(defs: I) -> Result<Self> {
4048 let mut e = Self::new();
4049 for d in defs {
4050 e.register_macro_def(d)?;
4051 }
4052 Ok(e)
4053 }
4054
4055 /// Register a parsed [`MacroDef`] into this expander's macro tables —
4056 /// the single named primitive on the `Expander` surface every
4057 /// macro-registration consumer routes through.
4058 ///
4059 /// The registration discipline is a two-step composition:
4060 /// 1. When [`Self::compile_templates`](Self::new) is on (the
4061 /// `Self::new` default; flipped off by [`Self::new_substitute_only`]),
4062 /// [`compile_template`] pre-compiles the macro body to a typed
4063 /// [`CompiledTemplate`] bytecode and inserts it into `self.templates`
4064 /// keyed by `def.name`.
4065 /// 2. The `MacroDef` is moved into `self.macros` keyed by `def.name` —
4066 /// always, regardless of `compile_templates`, because the substitute
4067 /// strategy reads `self.macros` exclusively while the bytecode
4068 /// strategy consults `self.templates` first and falls back to
4069 /// `self.macros` for the body and params.
4070 ///
4071 /// The order is structural: `compile_template` borrows `&def` while
4072 /// `self.macros.insert(def.name.clone(), def)` consumes `def` — the
4073 /// template pre-compile MUST precede the move into `self.macros`, and the
4074 /// `def.name.clone()` projection captures the key for the moved insert.
4075 /// `?`-routing through `compile_template` preserves the structural
4076 /// ordering of the rejection chain end-to-end: a template-compile error
4077 /// (`UnboundTemplateVar` for an unbound `,name`, `NonSymbolUnquoteTarget`
4078 /// for `,5` / `,(nested)`, et al.) short-circuits BEFORE `self.macros`
4079 /// is mutated, so a failed registration leaves both tables exactly as
4080 /// they were — no partial-write window in which `self.macros.has(name)`
4081 /// is true but `self.templates.has(name)` is missing (a regression that
4082 /// would silently coerce the bytecode strategy onto the substitute path
4083 /// for that one macro despite `compile_templates: true`).
4084 ///
4085 /// Before this lift the same two-step block —
4086 ///
4087 /// ```ignore
4088 /// if self.compile_templates {
4089 /// self.templates.insert(def.name.clone(), compile_template(&def)?);
4090 /// }
4091 /// self.macros.insert(def.name.clone(), def);
4092 /// ```
4093 ///
4094 /// — lived byte-identical (modulo `self`/`e` and `def`/`d` substitutions)
4095 /// at TWO sites: [`Self::with_macros`] (the constructor that
4096 /// bulk-registers an `IntoIterator<Item = MacroDef>`, e.g. a curated
4097 /// preloaded set the caller assembled out-of-band) and
4098 /// [`Self::expand_program`]'s `(defmacro …)`-head arm (the program-level
4099 /// walker that recognizes a `defmacro` / `defpoint-template` / `defcheck`
4100 /// head via [`macro_def_from`] and registers it as a side-effect of
4101 /// walking the program). After this lift the registration block lives in
4102 /// ONE method on the `Expander`; both consumers and any future
4103 /// macro-registration surface bind to ONE primitive instead of
4104 /// re-deriving the two-step discipline at every call site.
4105 ///
4106 /// `pub` so authoring surfaces (an LSP that incrementally registers a
4107 /// `(defmacro …)` head as the user finishes typing it without a full
4108 /// program re-expand, a REPL `:define-macro` command that registers a
4109 /// pre-parsed `MacroDef` directly, a future "library merge" operation
4110 /// that absorbs another expander's macro set MacroDef-by-MacroDef) can
4111 /// register a typed `MacroDef` without round-tripping through source
4112 /// serialization first. Sibling of [`Self::with_macros`] (the
4113 /// bulk-from-iterator constructor — itself the
4114 /// `defs.into_iter().try_fold((), |_, d| self.register_macro_def(d))`
4115 /// shape on a fresh expander) and [`Self::expand_program`] (the
4116 /// source-level walker that recognizes `(defmacro …)` heads via
4117 /// [`macro_def_from`] — itself the program-level fold-over-defmacro-heads
4118 /// of this method). All three end up at this primitive.
4119 ///
4120 /// Returns `Result<()>` so the consumer's rejection chain composes with
4121 /// `?`-routing — `with_macros` short-circuits its bulk loop on the first
4122 /// `compile_template` failure; `expand_program` short-circuits its
4123 /// program walk at the offending `(defmacro …)` form. Infallibility on
4124 /// the `compile_templates: false` path is preserved (`compile_template`
4125 /// is gated behind the conditional), so a substitute-only expander never
4126 /// emits the `compile_template`-side rejection chain.
4127 ///
4128 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4129 /// byte-identical inline copies of the registration block across
4130 /// `with_macros` and `expand_program` is past the ≥2 PRIME-DIRECTIVE
4131 /// trigger once the structural shape is named. THEORY.md §V.1 — knowable
4132 /// platform; the macro-registration discipline becomes a NAMED primitive
4133 /// on the substrate's `Expander` surface rather than a per-consumer
4134 /// inline duplication that future emitters (an LSP, a REPL, a library-
4135 /// merge operator) would have had to re-derive. THEORY.md §II.1
4136 /// invariant 2 — free middle; both consumers route through the SAME
4137 /// registration primitive, so a regression that drifts ONE consumer's
4138 /// discipline from the other (one path skips the template pre-compile,
4139 /// one path inserts the `self.macros` entry in a different order, a
4140 /// future third side-effect — logging, attestation, metrics — emitted
4141 /// at one site but not the other) cannot reach the substrate's runtime:
4142 /// there is exactly one implementation both consumers route through.
4143 ///
4144 /// Frontier inspiration: Racket's `(define-syntax name template)` at
4145 /// REPL is exactly this — register a typed macro into the live
4146 /// namespace, no source round-trip; the substrate's `register_macro_def`
4147 /// is the Rust-typed peer of that surface, lifted onto the `Expander`'s
4148 /// table-level algebra (`macros: HashMap<String, MacroDef>` +
4149 /// `templates: HashMap<String, CompiledTemplate>`). MLIR's
4150 /// `OpRegistry::registerOp<Op>()` — typed-op registration into a
4151 /// dialect's live table at construction-time AND at JIT-walk-time
4152 /// through ONE registration entry point; `register_macro_def` is the
4153 /// pleme-io peer of that registration entry point on the macro-table
4154 /// algebra.
4155 pub fn register_macro_def(&mut self, def: MacroDef) -> Result<()> {
4156 // Reject a registration-bomb (a code-generator emitting
4157 // unbounded fresh `(defmacro fresh-N (x) `(list ,x))` heads
4158 // whose bodies each sit comfortably under
4159 // `max_macro_body_size` yet collectively saturate process
4160 // memory) at the cheapest gate on the REGISTER-time surface
4161 // — a `HashMap::len` comparison, no body walk. Peer to the
4162 // body-size gate below one RESOURCE-DIMENSION axis over
4163 // (COUNT vs. SIZE) and to the expand-time cache-entries gate
4164 // in `apply` one PIPELINE-STAGE axis over (REGISTER-time
4165 // COUNT vs. EXPAND-time COUNT). Fires BEFORE the body-size
4166 // gate walks `def.body.node_count()` or any insert lands, so
4167 // a failed registration leaves BOTH tables exactly as they
4168 // were. Overwrites (a re-registration of an already-
4169 // registered key) are ADMITTED at any table size — a
4170 // re-`defmacro` never grows the table, so the ceiling cannot
4171 // gate operator redefinitions. Keyed on `>=` (a table
4172 // already at ceiling rejects a fresh key) so
4173 // `max_registered_macros` names the LARGEST admissible
4174 // `self.macros.len()` — same posture the expand-time
4175 // cache-entries gate holds on the memoization cache.
4176 let is_overwrite = self.macros.contains_key(&def.name);
4177 if !is_overwrite && self.macros.len() >= self.limits.max_registered_macros {
4178 return Err(LispError::RegisteredMacrosExceeded {
4179 macro_name: def.name.clone(),
4180 count: self.macros.len(),
4181 limit: self.limits.max_registered_macros,
4182 });
4183 }
4184 // Reject an arity-bomb `(defmacro huge (a-1 a-2 … a-N-million)
4185 // `,a-1)` — a code-generator whose macro body sits at ONE node
4186 // (well under `max_macro_body_size`) yet whose param list
4187 // carries millions of fresh names the per-index binder walks on
4188 // every call. Peer to the body-size gate below one RESOURCE-
4189 // DIMENSION axis over on the REGISTER-time surface (per-body
4190 // ARITY vs. per-body SIZE), AND peer to the table-count gate
4191 // above one RESOURCE-DIMENSION axis over on the REGISTER-time
4192 // surface (per-registration WIDTH vs. cumulative COUNT). Fires
4193 // BEFORE the body-size gate walks `def.body.node_count()` or
4194 // any insert lands, so a failed registration leaves BOTH tables
4195 // exactly as they were. Cheaper than the body-size gate — a
4196 // three-arm addition on `def.params` fields, no AST walk —
4197 // hence positioned BEFORE it on the O(1)-first ordering of the
4198 // REGISTER-time chain. Keyed on `>` (equality is admitted, only
4199 // strict overrun rejects) so `max_macro_arity` names the
4200 // LARGEST admissible `def.params.total_arity()` — same posture
4201 // the body-size gate holds on the body node count.
4202 let arity = def.params.total_arity();
4203 if arity > self.limits.max_macro_arity {
4204 return Err(LispError::MacroArityExceeded {
4205 macro_name: def.name.clone(),
4206 arity,
4207 limit: self.limits.max_macro_arity,
4208 });
4209 }
4210 // Reject an authoring-bomb `(defmacro huge (x) `<template-of-
4211 // N-million-nodes>)` at the REGISTRATION boundary — the
4212 // PIPELINE-STAGE peer of the EXPAND-time output-size gate in
4213 // `expand_with_depth`. Fires BEFORE `compile_template` walks
4214 // the body or `self.macros.insert` lands the entry, so a
4215 // failed registration leaves BOTH tables exactly as they
4216 // were (no partial-write window in which the body-size
4217 // rejection happened but the template pre-compile already
4218 // consumed cycles or the macros table already carries the
4219 // over-ceiling entry). Keyed on `>` rather than `>=` so
4220 // `max_macro_body_size` names the LARGEST admissible body —
4221 // equality is admitted, only strict overrun rejects — same
4222 // posture the expand-time output-size gate holds.
4223 let body_size = def.body.node_count();
4224 if body_size > self.limits.max_macro_body_size {
4225 return Err(LispError::MacroBodySizeExceeded {
4226 macro_name: def.name.clone(),
4227 size: body_size,
4228 limit: self.limits.max_macro_body_size,
4229 });
4230 }
4231 if self.compile_templates {
4232 self.templates
4233 .insert(def.name.clone(), compile_template(&def)?);
4234 }
4235 self.macros.insert(def.name.clone(), def);
4236 Ok(())
4237 }
4238
4239 /// Expand a whole program. Returns the list of top-level forms after
4240 /// `defmacro` definitions are registered and all macro calls expanded.
4241 pub fn expand_program(&mut self, forms: Vec<Sexp>) -> Result<Vec<Sexp>> {
4242 let mut out = Vec::new();
4243 for form in forms {
4244 if let Some(def) = macro_def_from(&form)? {
4245 self.register_macro_def(def)?;
4246 continue;
4247 }
4248 out.push(self.expand(&form)?);
4249 }
4250 Ok(out)
4251 }
4252
4253 /// Read a source string into top-level forms via [`crate::reader::read`],
4254 /// then route the forms through [`expand_program`](Self::expand_program) —
4255 /// the from-source posture of the yield-all-forms-after-expansion primitive,
4256 /// in ONE method on the `Expander` surface.
4257 ///
4258 /// Before this lift the same two-step chain — `let forms = read(src)?;
4259 /// <expander>.expand_program(forms)` — lived inline at two sites in
4260 /// [`crate::compiler_spec`]: [`RealizedCompiler::compile`](crate::compiler_spec::RealizedCompiler::compile)
4261 /// (the public from-source untyped-expansion entry on a realized compiler,
4262 /// returning the expanded `Vec<Sexp>` for untyped consumers like
4263 /// `tatara-check`'s per-form dispatcher) AND [`realize_in_memory`](crate::compiler_spec::realize_in_memory)'s
4264 /// `:macros` library load loop (the per-spec-macro source absorption that
4265 /// builds the preloaded expander's macro library through `expand_program`'s
4266 /// `defmacro` recognition side-effect). After this lift the read-then-expand
4267 /// composition lives in ONE method on the `Expander`; each of the two
4268 /// consumers binds it with the per-site expander posture that fits its call
4269 /// boundary — `self.preloaded.clone()` for `RealizedCompiler::compile`'s
4270 /// per-call clone, `&mut preloaded` for `realize_in_memory`'s shared
4271 /// build-up.
4272 ///
4273 /// Sibling of [`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)
4274 /// — that method stacks the typed-keyword projection on top of the
4275 /// from-source pipeline (`read + expand_program + iter_calls_to(_,
4276 /// keyword) + map + collect`); this method is the bare yield-all-forms
4277 /// from-source primitive (`read + expand_program`) the typed dispatchers
4278 /// stack their keyword projection atop. The two together close the
4279 /// from-source posture of the program-level walk family on the
4280 /// `Expander` surface: bare (this method) vs. typed-keyword-projected
4281 /// (the sibling).
4282 ///
4283 /// Closes the 2×2 program-level walk family on the `Expander` surface:
4284 /// from-forms × yield-all ([`expand_program`](Self::expand_program)),
4285 /// from-forms × keyword-projected ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)),
4286 /// from-source × keyword-projected ([`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)),
4287 /// AND now from-source × yield-all (this method). The four together name
4288 /// the canonical surfaces a dispatcher composes with to extract an
4289 /// expanded program from either a pre-parsed `Vec<Sexp>` (from-forms
4290 /// posture, for callers composing with another `Sexp`-producing surface)
4291 /// or a raw `&str` (from-source posture, for callers consuming
4292 /// authoring-surface source text directly), with or without a
4293 /// typed-keyword filter on the result.
4294 ///
4295 /// `?`-routing through `read` preserves the structural ordering of the
4296 /// rejection chain end-to-end: a reader error (lexer / parser /
4297 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4298 /// `expand_program` runs; an `expand_program` error
4299 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4300 /// `RestParamMissingName`, et al.) short-circuits at the offending form.
4301 /// Each consumer's rejection chain remains exactly what it was pre-lift,
4302 /// now sourced from ONE composition point rather than two.
4303 ///
4304 /// `defmacro`-registration side-effects fire on `&mut self` exactly as
4305 /// they do for the from-forms primitive — `realize_in_memory`'s per-spec
4306 /// build-up depends on every `defmacro` in every `:macros` source
4307 /// landing in `self.macros` (and, when `compile_templates` is on, in
4308 /// `self.templates`); `RealizedCompiler::compile`'s per-call clone
4309 /// posture isolates absorption to the cloned expander, so a
4310 /// `defmacro` in the user's source does NOT leak into the persistent
4311 /// realized compiler. Both postures' absorption semantics are
4312 /// preserved by routing through this primitive instead of inlining
4313 /// the two-step chain.
4314 ///
4315 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4316 /// inline copies of the `let forms = read(src)?; <expander>.expand_program(forms)`
4317 /// two-step chain across `RealizedCompiler::compile` and
4318 /// `realize_in_memory` is past the ≥2 PRIME-DIRECTIVE trigger once the
4319 /// structural shape is named. THEORY.md §V.1 — knowable platform; the
4320 /// read-then-expand composition becomes a NAMED primitive on the
4321 /// substrate's `Expander` surface rather than a re-derived two-step
4322 /// inline pipeline at every consumer. THEORY.md §II.1 invariant 2 —
4323 /// free middle; both consumers route through the SAME composition
4324 /// primitive, so a regression that drifts ONE consumer's pipeline
4325 /// shape from the other cannot reach the substrate's runtime.
4326 ///
4327 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4328 /// namespace — the from-source-string entry to namespace-level
4329 /// program evaluation is the Racket idiom; the substrate's
4330 /// `expand_source_program` is the Rust-typed peer of that, sourced
4331 /// from `&str` and yielding the post-macroexpansion `Vec<Sexp>`
4332 /// without a typed-keyword filter — exactly the shape an untyped
4333 /// consumer (`tatara-check`'s per-form dispatcher, a REPL's
4334 /// "expand this buffer" command, an LSP's "show me the expanded
4335 /// program" handler) binds to.
4336 pub fn expand_source_program(&mut self, src: &str) -> Result<Vec<Sexp>> {
4337 let forms = crate::reader::read(src)?;
4338 self.expand_program(forms)
4339 }
4340
4341 /// Compose the expander's program-level expansion with the substrate's
4342 /// slice-side typed-keyword projection ([`iter_calls_to`]) and a
4343 /// caller-supplied per-form projection — `expand_program(forms)?` followed
4344 /// by `iter_calls_to(&expanded, keyword).map(project).collect()`, in ONE
4345 /// method on the `Expander` surface. Both [`compile_typed`](crate::compile::compile_typed)
4346 /// and [`compile_named_from_forms`](crate::compile::compile_named_from_forms)
4347 /// route through this primitive — they differ only in the per-form
4348 /// projection `F`: `T::compile_from_args` for the bare-kwargs form
4349 /// (`compile_typed`), and the NAME-then-`T::compile_from_args` split
4350 /// (`compile_named_from_forms`).
4351 ///
4352 /// Before this lift each dispatcher opened the same three-step pipeline
4353 /// inline — `let mut exp = Expander::new(); let expanded =
4354 /// exp.expand_program(forms)?; iter_calls_to(&expanded, T::KEYWORD)
4355 /// .map(<per-site>).collect()` — past the ≥2 PRIME-DIRECTIVE trigger
4356 /// once the structural shape is named. After this lift the pipeline
4357 /// lives in ONE method on the `Expander` surface and the two
4358 /// dispatchers thread their per-site projection through `F`; a
4359 /// regression that drifts ONE dispatcher's pipeline from the other (a
4360 /// future emitter that re-derives `expand_program + iter_calls_to`
4361 /// inline rather than composing through this primitive, a future
4362 /// preloaded-expander consumer that wants the same walk but on its
4363 /// own `Expander` rather than a fresh one) is no longer a silent
4364 /// two-site divergence: the method binds the composition once and
4365 /// every consumer threads the per-site projection through `F`.
4366 ///
4367 /// Method on `Expander` (not a free function) so the primitive
4368 /// composes with the broader expander surface: a preloaded expander
4369 /// (e.g., one that has already absorbed a set of `defmacro` forms
4370 /// via `expand_program` or `with_macros`) can call this method on
4371 /// its own state to walk a follow-up program — the
4372 /// `compiler_spec`'s realize path is the natural future consumer for
4373 /// that shape. `compile_typed` and `compile_named_from_forms`
4374 /// instantiate a fresh `Expander::new()` and dispatch through this
4375 /// method on it; a future preloaded consumer dispatches through it
4376 /// on the preloaded expander directly. ONE primitive, two postures
4377 /// (fresh vs. preloaded), no per-posture duplication of the
4378 /// `expand_program + iter_calls_to + map + collect` pipeline.
4379 ///
4380 /// `F` is `FnMut(&[Sexp]) -> Result<R>` — the per-form projection
4381 /// that fits the consumer's call site. The standard-library
4382 /// `Iterator::map` bound is `FnMut`, so a closure that captures
4383 /// mutable state (a future dispatcher that threads a running index
4384 /// or a borrowed accumulator into the projection) composes
4385 /// naturally. The `Result<R>` projection short-circuits on the
4386 /// first error via `Iterator::collect::<Result<Vec<R>, _>>()`, so
4387 /// the per-form rejection chain (`compile_named_from_forms`'s
4388 /// `NamedFormMissingName` for the missing NAME slot,
4389 /// `NamedFormNonSymbolName` for the non-symbol NAME slot,
4390 /// `T::compile_from_args`'s typed-entry kwargs gate, AND
4391 /// `compile_typed`'s bare-kwargs `T::compile_from_args` rejection)
4392 /// fires in the same order under the new shape.
4393 ///
4394 /// `R` is owned by construction — the iterator's `&[Sexp]` items
4395 /// borrow from the local `expanded: Vec<Sexp>` and that borrow
4396 /// ends when the `.collect()` consumes the iterator, so a `R`
4397 /// that borrowed from `expanded` would fail to compile here. The
4398 /// two production consumers (`Vec<T>`, `Vec<NamedDefinition<T>>`)
4399 /// are both owned-`R` shapes, matching the borrow's structural
4400 /// constraint.
4401 ///
4402 /// Sibling of [`expand_program`](Self::expand_program) — that
4403 /// method names the FIRST step of the pipeline (program-level
4404 /// macroexpansion, yielding `Vec<Sexp>`); this method composes it
4405 /// with the SECOND step (slice-side typed-keyword projection +
4406 /// per-form mapper). The split lets a consumer that wants the
4407 /// expanded forms WITHOUT the keyword filter (`tatara-check`'s
4408 /// per-form dispatcher walks every form, not just matched
4409 /// keywords) bind to `expand_program` directly; a consumer that
4410 /// wants both halves composed binds to this method.
4411 ///
4412 /// Closes the substrate's program-level walk family on the
4413 /// `Expander` surface: `expand_program` (yield-all-forms-after-
4414 /// expansion), `expand` (per-form recursive expansion), `apply`
4415 /// (per-call substitution), AND now `expand_and_collect_calls_to`
4416 /// (typed-keyword projection of the expanded forms with a per-form
4417 /// mapper). Together the four name the canonical surfaces a
4418 /// dispatcher composes with to extract a typed program from a
4419 /// post-expansion form set.
4420 ///
4421 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4422 /// two inline copies of the `Expander::new() + expand_program +
4423 /// iter_calls_to(_, T::KEYWORD) + map + collect` pipeline across
4424 /// `compile_typed` and `compile_named_from_forms` is past the ≥2
4425 /// PRIME-DIRECTIVE trigger once the structural shape is named.
4426 /// THEORY.md §V.1 — knowable platform; the program-level walk's
4427 /// typed-keyword-projection composition becomes a NAMED primitive
4428 /// on the substrate's `Expander` surface rather than a re-derived
4429 /// three-step inline pipeline at every consumer. Authoring tools
4430 /// (REPL, LSP, `tatara-check`) that want to walk a program by
4431 /// typed keyword bind to ONE method on the `Expander` instead of
4432 /// re-implementing the composition. THEORY.md §II.1 invariant 1 —
4433 /// typed entry; the typed-keyword filter over an expanded program
4434 /// IS the rust-level typed-entry-batch gate, and naming its
4435 /// single shape lifts the gate from two-site duplication to one
4436 /// rust method the substrate's diagnostic promotions hang off of.
4437 /// THEORY.md §II.1 invariant 2 — free middle; both consumers
4438 /// route through the SAME composition primitive, each binding the
4439 /// per-form projection that fits its call site — a regression
4440 /// that drifts ONE dispatcher's pipeline shape from the other
4441 /// cannot reach the substrate's runtime.
4442 ///
4443 /// Frontier inspiration: MLIR's `Region::walk<Op>(callback)` —
4444 /// every typed rewriter that wants "for every Op of kind K in this
4445 /// region, run callback" binds to ONE typed walker that composes
4446 /// the kind filter with the per-op visitor; the substrate's
4447 /// `expand_and_collect_calls_to` is the unstructured-projection
4448 /// peer of that walker, lifted onto the post-expansion `&[Sexp]`
4449 /// algebra with the per-form projection as the visitor. Racket's
4450 /// `syntax-parse` `~seq (keyword args ...) ...` ellipsis-form —
4451 /// the program-level typed-keyword filter with per-match handler
4452 /// is the closed-form sibling of `~seq`'s repeated-pattern
4453 /// matcher, translated through pleme-io primitives as ONE method
4454 /// on `Expander` composing `expand_program` with `iter_calls_to`
4455 /// and a per-form mapper. GHC Core's `everything :: forall b. (b
4456 /// -> b -> b) -> GenericQ b -> GenericQ b` — the typed-IR rewriter's
4457 /// program-level fold over a typed selector is named ONCE and
4458 /// every consumer threads the per-node projection; the substrate's
4459 /// `expand_and_collect_calls_to` is the keyword-projected sibling
4460 /// of that fold on the `&[Sexp]` algebra.
4461 pub fn expand_and_collect_calls_to<R, F>(
4462 &mut self,
4463 forms: Vec<Sexp>,
4464 keyword: &str,
4465 mut project: F,
4466 ) -> Result<Vec<R>>
4467 where
4468 F: FnMut(&[Sexp]) -> Result<R>,
4469 {
4470 // Routes through the typed-decoded classifier sibling
4471 // [`Self::expand_and_collect_calls_to_any`] with a constant-
4472 // classifier decoder — the same constant-classifier composition
4473 // [`crate::ast::iter_calls_to`] uses to route through
4474 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
4475 // discarded `()` typed witness (`then_some(())`) is consumed by
4476 // the wrapper projection `|(), args| project(args)` so the
4477 // keyword consumer's per-form mapper sees only the args tail,
4478 // matching the pre-lift signature exactly. Both the keyword AND
4479 // classifier expander walks now share ONE pipeline implementation
4480 // (`expand_program + iter_calls_to_any + map + collect`); a
4481 // regression that drifts a future debug-mode logger, span-aware
4482 // borrow walker, or fused-iterator invariant from one expander
4483 // walk to the other becomes structurally impossible.
4484 self.expand_and_collect_calls_to_any(
4485 forms,
4486 |h| (h == keyword).then_some(()),
4487 move |(), args| project(args),
4488 )
4489 }
4490
4491 /// Compose the expander's program-level expansion with the substrate's
4492 /// slice-side typed-decoded classifier projection ([`iter_calls_to_any`])
4493 /// and a caller-supplied per-form projection — `expand_program(forms)?`
4494 /// followed by `iter_calls_to_any(&expanded, decode).map(|(decoded, args)|
4495 /// project(decoded, args)).collect()`, in ONE method on the `Expander`
4496 /// surface. The typed-decoded sibling of
4497 /// [`Self::expand_and_collect_calls_to`] — where that method filters by
4498 /// ONE constant keyword, this method filters AND TYPES by a caller-
4499 /// supplied classifier, yielding the typed witness alongside the per-form
4500 /// projection's args input.
4501 ///
4502 /// Closes the substrate's program-level walk family on the `Expander`
4503 /// surface at the typed-decoded corner the prior runs' slice-side
4504 /// `iter_calls_to_any` lift left open — the (keyword, classifier) 2×2
4505 /// of compose-on-iter projections:
4506 ///
4507 /// | | per-form algebra | slice algebra | expander surface |
4508 /// |----------------|-----------------------------|------------------------------|-------------------------------------|
4509 /// | keyword | [`Sexp::as_call_to`] | [`iter_calls_to`] | [`Self::expand_and_collect_calls_to`] |
4510 /// | classifier `F` | [`Sexp::as_call_to_any`] | [`iter_calls_to_any`] | `expand_and_collect_calls_to_any` (this) |
4511 ///
4512 /// The keyword corner now ROUTES through the classifier corner at every
4513 /// row: [`Sexp::as_call_to`] = constant-classifier projection of
4514 /// [`Sexp::as_call_to_any`]; [`iter_calls_to`] = constant-classifier
4515 /// projection of [`iter_calls_to_any`]; [`Self::expand_and_collect_calls_to`]
4516 /// = constant-classifier projection of THIS method. The substrate's
4517 /// soft-dispatch family is structurally complete at every algebra
4518 /// level — per-form, slice, AND expander — so a regression that drifts
4519 /// a future debug-mode logger, span-aware borrow walker, or
4520 /// fused-iterator invariant from one walk to its sibling at any algebra
4521 /// level becomes structurally impossible.
4522 ///
4523 /// Two plausible future consumer shapes the typed-decoded expander walk
4524 /// admits with no boilerplate:
4525 /// * **Closed-set classifier** — `expand_and_collect_calls_to_any(forms,
4526 /// MacroDefHead::from_keyword, |head, args| dispatch(head, args))`
4527 /// macroexpands a program and walks each `(defmacro …)` /
4528 /// `(defpoint-template …)` / `(defcheck …)` form decoded to the typed
4529 /// `MacroDefHead` enum with its args tail. Future `tatara-check`
4530 /// consumers that want "for every macro-definition form in this
4531 /// buffer, dispatch by typed kind" bind to ONE method on the
4532 /// `Expander` rather than the three-step `expand_program +
4533 /// iter_calls_to_any + map + collect` pipeline at each consumer site.
4534 /// * **Live-registry classifier** — `expand_and_collect_calls_to_any(forms,
4535 /// |h| registry.lookup(h), |handler, args| handler.handle(args))`
4536 /// macroexpands a program and walks each form whose head matches a
4537 /// runtime registry, decoded to a handler reference. Future
4538 /// `tatara-check`-shaped runtime dispatchers (the kind already named
4539 /// by [`iter_calls_to_any`]'s docstring as "the natural future
4540 /// consumer") bind to ONE method on the `Expander` rather than the
4541 /// three-step pipeline.
4542 ///
4543 /// The closed-form composition binding the keyword sibling to this typed-
4544 /// decoded primitive is the structural identity every consumer can pin
4545 /// against:
4546 ///
4547 /// ```ignore
4548 /// expand_and_collect_calls_to(forms, k, p) ==
4549 /// expand_and_collect_calls_to_any(forms,
4550 /// |h| (h == k).then_some(()),
4551 /// |(), args| p(args))
4552 /// ```
4553 ///
4554 /// `D` is `FnMut(&str) -> Option<T>` — the typed-decoded classifier the
4555 /// substrate's per-form algebra already shapes ([`Sexp::as_call_to_any`]
4556 /// uses `FnOnce`, the slice algebra and this method use `FnMut` for the
4557 /// same reason: the walk calls the decoder once per matching form, and a
4558 /// decoder that captures mutable state (a counter, a registry cache, a
4559 /// visited-set) maintains that state across the program-level walk).
4560 /// `F` is `FnMut(T, &[Sexp]) -> Result<R>` — the per-form projection that
4561 /// receives BOTH the typed witness AND the args tail; the keyword sibling
4562 /// discards the (unit) typed witness via `|(), args| project(args)`. The
4563 /// `Result<R>` projection short-circuits on the first error via
4564 /// `Iterator::collect::<Result<Vec<R>, _>>()` so the per-form rejection
4565 /// chain fires in source order at the first failing matched form.
4566 ///
4567 /// `R` is owned by construction — same constraint as the keyword
4568 /// sibling. `T` is owned because the underlying [`iter_calls_to_any`]
4569 /// requires `T: 'a` where `'a` is the post-expansion `Vec<Sexp>` borrow
4570 /// lifetime; consumers projecting to a typed `Copy` enum
4571 /// (`MacroDefHead`, a future `AuthoringDirective`) get the value
4572 /// directly per form, consumers projecting to a borrowed `&'static str`
4573 /// (a closed-set head) project to `&'static str` and inherit the static
4574 /// lifetime through the classifier, consumers projecting to a `&Handler`
4575 /// (a live-registry classifier) bind through the registry's owned
4576 /// borrow with `&self` outliving the walk.
4577 ///
4578 /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
4579 /// keyword sibling [`Self::expand_and_collect_calls_to`] is now a
4580 /// CONSEQUENCE of this typed-decoded primitive + a constant-classifier
4581 /// decoder, parallel to how [`iter_calls_to`] is a consequence of
4582 /// [`iter_calls_to_any`] on the slice algebra. The substrate's
4583 /// program-level walk family is no longer two parallel implementations
4584 /// the type system fails to bind (a future regression that drifts the
4585 /// keyword pipeline's instrumentation from the classifier pipeline's
4586 /// instrumentation cannot reach the runtime — both compose through ONE
4587 /// `expand_program + iter_calls_to_any + map + collect` body, with the
4588 /// keyword filter expressed as a closed-form classifier). THEORY.md
4589 /// §V.1 — knowable platform; the typed-decoded expander walk becomes a
4590 /// NAMED primitive on the substrate's `Expander` surface rather than a
4591 /// future re-derived three-step inline pipeline per consumer.
4592 /// Authoring tools (REPL, LSP, `tatara-check`) that want to walk a
4593 /// program by typed-decoded classifier bind to ONE method on the
4594 /// `Expander` instead of re-implementing the composition. THEORY.md
4595 /// §II.1 invariant 2 — free middle; both expander-surface dispatchers
4596 /// (keyword + classifier) route through the SAME `expand_program +
4597 /// iter_calls_to_any + map + collect` composition primitive.
4598 ///
4599 /// Frontier inspiration: MLIR's `Region::walk<OpInterface,
4600 /// OpInterface2, …>([&](auto op) { … })` — the typed-IR walk over a
4601 /// region yielding ops decoded to their typed interface witness with
4602 /// per-op callback is the MLIR idiom; the substrate's
4603 /// `expand_and_collect_calls_to_any` is the unstructured-Rust peer on
4604 /// the post-expansion `&[Sexp]` algebra, with `decode: FnMut(&str) ->
4605 /// Option<T>` standing in for MLIR's typed-interface dyn-cast bag and
4606 /// `project: FnMut(T, &[Sexp]) -> Result<R>` standing in for the
4607 /// typed-op callback. Racket's `syntax-parse` `~or* (~datum defX)
4608 /// (~datum defY) (head args …)` over an ellipsis-form combined with a
4609 /// `#:with name:id` typed-witness binder — the program-level
4610 /// typed-decoded filter with per-match handler is the closed-form
4611 /// sibling of `syntax-parse`'s typed-choice repeater, translated
4612 /// through pleme-io primitives as ONE method on `Expander`. GHC Core's
4613 /// `everythingBut :: (a -> a -> a) -> GenericQ (a, Bool) -> GenericQ a`
4614 /// — the typed-IR rewriter's program-level fold over a typed
4615 /// kind-with-stop selector is named ONCE and every consumer threads
4616 /// the per-node typed projection; this method is the typed-decoded
4617 /// peer on the `Vec<Sexp>` algebra, with the classifier playing
4618 /// `everythingBut`'s typed-selector role.
4619 pub fn expand_and_collect_calls_to_any<R, F, D, T>(
4620 &mut self,
4621 forms: Vec<Sexp>,
4622 decode: D,
4623 mut project: F,
4624 ) -> Result<Vec<R>>
4625 where
4626 D: FnMut(&str) -> Option<T>,
4627 F: FnMut(T, &[Sexp]) -> Result<R>,
4628 {
4629 let expanded = self.expand_program(forms)?;
4630 crate::ast::iter_calls_to_any(&expanded, decode)
4631 .map(|(decoded, args)| project(decoded, args))
4632 .collect()
4633 }
4634
4635 /// Read a source string into top-level forms via [`crate::reader::read`],
4636 /// then route the forms through
4637 /// [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to) — the
4638 /// from-source posture of the program-level walk, in ONE method on the
4639 /// `Expander` surface.
4640 ///
4641 /// Before this lift the same two-step chain — `let forms = read(src)?;
4642 /// <expander>.expand_and_collect_calls_to(forms, keyword, project)` — lived
4643 /// inline at four sites: the two free-function typed dispatchers
4644 /// ([`compile_typed`](crate::compile::compile_typed) and
4645 /// [`compile_named`](crate::compile::compile_named) via
4646 /// [`compile_named_from_forms`](crate::compile::compile_named_from_forms))
4647 /// AND the two preloaded-expander methods on
4648 /// [`RealizedCompiler`](crate::compiler_spec::RealizedCompiler)
4649 /// (`compile_typed`, `compile_named`). After this lift the read-then-walk
4650 /// composition lives in ONE method on the `Expander`; each of the four
4651 /// dispatchers binds it with the per-site `(expander posture, projection)`
4652 /// pair that fits its call boundary — `Expander::new()` for the
4653 /// fresh-expander dispatchers, `self.preloaded.clone()` for the
4654 /// preloaded-expander dispatchers; `T::compile_from_args` for the
4655 /// bare-kwargs projection, `named_form_projection::<T>` for the
4656 /// NAME-then-kwargs projection.
4657 ///
4658 /// Sibling of [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)
4659 /// — that method takes a pre-parsed `Vec<Sexp>` (the from-forms posture,
4660 /// for callers that have already read or that compose with another
4661 /// `Sexp`-producing surface like a macro-expanded subform); this method
4662 /// takes a `&str` (the from-source posture, for callers consuming
4663 /// authoring-surface source text directly). Both compose with the same
4664 /// `expand_program + iter_calls_to + map + collect` pipeline through ONE
4665 /// from-forms primitive — the from-source posture stacks one read step on
4666 /// top, projecting `crate::reader::read`'s `Result<Vec<Sexp>>` into the
4667 /// from-forms primitive via `?`.
4668 ///
4669 /// `?`-routing through `read` preserves the structural ordering of the
4670 /// rejection chain end-to-end: a reader error (lexer / parser /
4671 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4672 /// `expand_program` runs; an `expand_program` error
4673 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4674 /// `RestParamMissingName`, et al.) short-circuits BEFORE the keyword
4675 /// filter walks anything; a per-form `project` error
4676 /// (`NamedFormMissingName`, `NamedFormNonSymbolName`,
4677 /// `T::compile_from_args`'s typed-entry kwargs gate) short-circuits at the
4678 /// first failing matched form via `Iterator::collect::<Result<Vec<R>, _>>()`.
4679 /// Each dispatcher's rejection chain remains exactly what it was pre-lift,
4680 /// now sourced from ONE composition point rather than four.
4681 ///
4682 /// Closes the program-level walk family on the `Expander` surface across
4683 /// BOTH the from-forms posture
4684 /// ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to))
4685 /// AND the from-source posture (this method) — together with
4686 /// `expand_program` (yield-all-forms-after-expansion), `expand` (per-form
4687 /// recursive expansion), and `apply` (per-call substitution), they name
4688 /// the canonical surfaces a dispatcher composes with to extract a typed
4689 /// program from a post-expansion form set. A future dispatcher that wants
4690 /// "read this source, then walk every call to keyword K and project each
4691 /// to R" — a debug-mode REPL command, an LSP "find all typed-domain
4692 /// definitions in this buffer" handler, a `tatara-check` command that
4693 /// dispatches each typed `(defX …)` form in `checks.lisp` to its
4694 /// registered domain — binds to ONE method on the `Expander` instead of
4695 /// re-deriving the two-step `read + expand_and_collect_calls_to` chain.
4696 ///
4697 /// Theory anchor: THEORY.md §VI.1 — generation over composition; four
4698 /// inline copies of the `read(src)? + <expander>.expand_and_collect_calls_to`
4699 /// chain across the two fresh-expander dispatchers and the two
4700 /// preloaded-expander dispatchers is past the ≥2 PRIME-DIRECTIVE trigger
4701 /// once the structural shape is named. THEORY.md §V.1 — knowable
4702 /// platform; the read-then-walk composition becomes a NAMED primitive on
4703 /// the substrate's `Expander` surface rather than a re-derived two-step
4704 /// inline pipeline at every dispatcher. THEORY.md §II.1 invariant 2 —
4705 /// free middle; all four dispatchers route through the SAME composition
4706 /// primitive, so a regression that drifts ONE dispatcher's read-then-walk
4707 /// pipeline from the others cannot reach the substrate's runtime — the
4708 /// type system binds all consumers to the from-source primitive's single
4709 /// emission shape.
4710 ///
4711 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4712 /// namespace populated with the preloaded compiler's `require`d macros —
4713 /// the from-source-string entry to typed-program evaluation inside a
4714 /// namespace that carries the macro library is the Racket idiom; the
4715 /// substrate's `expand_source_and_collect_calls_to` is the Rust-typed
4716 /// peer of that, with the typed-keyword projection composed in.
4717 ///
4718 /// Post-lift the body routes through the typed-decoded sibling
4719 /// [`Self::expand_source_and_collect_calls_to_any`] with a
4720 /// constant-classifier decoder — the same constant-classifier
4721 /// composition [`Self::expand_and_collect_calls_to`] uses to route
4722 /// through [`Self::expand_and_collect_calls_to_any`] on the
4723 /// from-forms axis, and that [`crate::ast::iter_calls_to`] uses to
4724 /// route through [`crate::ast::iter_calls_to_any`] on the slice
4725 /// algebra. The discarded `()` typed witness (`then_some(())`) is
4726 /// consumed by the wrapper projection `|(), args| project(args)`
4727 /// so the keyword consumer's per-form mapper sees only the args
4728 /// tail. Both the keyword AND classifier from-source walks now
4729 /// share ONE pipeline implementation (`read + expand_program +
4730 /// iter_calls_to_any + map + collect`) — a regression that drifts
4731 /// a future debug-mode logger, span-aware borrow walker, or
4732 /// fused-iterator invariant from one from-source walk to the
4733 /// other becomes structurally impossible.
4734 pub fn expand_source_and_collect_calls_to<R, F>(
4735 &mut self,
4736 src: &str,
4737 keyword: &str,
4738 mut project: F,
4739 ) -> Result<Vec<R>>
4740 where
4741 F: FnMut(&[Sexp]) -> Result<R>,
4742 {
4743 self.expand_source_and_collect_calls_to_any(
4744 src,
4745 |h| (h == keyword).then_some(()),
4746 move |(), args| project(args),
4747 )
4748 }
4749
4750 /// Read a source string into top-level forms via [`crate::reader::read`],
4751 /// then route the forms through
4752 /// [`expand_and_collect_calls_to_any`](Self::expand_and_collect_calls_to_any) —
4753 /// the from-source posture of the typed-decoded classifier walk, in
4754 /// ONE method on the `Expander` surface. The typed-decoded sibling of
4755 /// [`Self::expand_source_and_collect_calls_to`] — where that method
4756 /// filters by ONE constant keyword, this method filters AND TYPES by
4757 /// a caller-supplied classifier, yielding the typed witness alongside
4758 /// the per-form projection's args input. Sourced from `&str` rather
4759 /// than a `Vec<Sexp>`.
4760 ///
4761 /// Closes the substrate's program-level walk family on the `Expander`
4762 /// surface across BOTH input postures × BOTH projection forms — the
4763 /// (from-forms, from-source) × (keyword, classifier) 2×2 of
4764 /// compose-on-iter projections:
4765 ///
4766 /// | | from-forms (`Vec<Sexp>`) | from-source (`&str`) |
4767 /// |----------------|---------------------------------------------------------------|---------------------------------------------------------------------|
4768 /// | keyword | [`Self::expand_and_collect_calls_to`] | [`Self::expand_source_and_collect_calls_to`] |
4769 /// | classifier `F` | [`Self::expand_and_collect_calls_to_any`] | `expand_source_and_collect_calls_to_any` (this) |
4770 ///
4771 /// Each row's keyword corner now ROUTES through the classifier corner
4772 /// at every column: [`Self::expand_source_and_collect_calls_to`]'s
4773 /// body composes this typed-decoded primitive with a
4774 /// `|h| (h == keyword).then_some(())` decoder and the wrapper
4775 /// projection `|(), args| project(args)`, parallel to how
4776 /// [`Self::expand_and_collect_calls_to`] routes through
4777 /// [`Self::expand_and_collect_calls_to_any`] on the from-forms axis
4778 /// and [`crate::ast::iter_calls_to`] routes through
4779 /// [`crate::ast::iter_calls_to_any`] on the slice algebra. The 2×2
4780 /// reduces to ONE pipeline implementation
4781 /// (`read + expand_program + iter_calls_to_any + map + collect`)
4782 /// every consumer threads its `(decoder, project)` pair through.
4783 ///
4784 /// Future consumer shapes the typed-decoded from-source primitive
4785 /// admits:
4786 /// * **Closed-set classifier** — `expand_source_and_collect_calls_to_any(
4787 /// src, MacroDefHead::from_keyword, |head, args| { … })` walks a
4788 /// source buffer dispatching every `(defmacro …)` / `(defpoint-template …)` /
4789 /// `(defcheck …)` form to its typed `MacroDefHead` arm — exactly the
4790 /// shape a future LSP "find every macro-definition form in this
4791 /// buffer, decode each by typed kind" handler reaches for, sourced
4792 /// from authoring text directly rather than a pre-parsed
4793 /// `Vec<Sexp>`.
4794 /// * **Live-registry classifier** — `expand_source_and_collect_calls_to_any(
4795 /// src, |h| registry.get(h), |handler, args| handler.compile(args))`
4796 /// walks a source buffer dispatching every form whose head is
4797 /// registered to its typed handler — exactly the shape
4798 /// `tatara-check`'s "macroexpand checks.lisp and dispatch every
4799 /// `(defX …)` form through the registered domain dispatcher"
4800 /// walker reaches for, sourced from disk-loaded source text
4801 /// rather than the round-tripped `Vec<Sexp>` the typed
4802 /// dispatchers consume.
4803 ///
4804 /// `?`-routing through `read` preserves the structural ordering of
4805 /// the rejection chain end-to-end: a reader error (lexer / parser /
4806 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4807 /// `expand_program` runs; an `expand_program` error short-circuits
4808 /// BEFORE the classifier filter walks anything; a per-form `project`
4809 /// error short-circuits at the first failing matched form via
4810 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
4811 /// rejection chain inherits the from-forms typed-decoded primitive's
4812 /// shape verbatim, now sourced from `&str` via ONE composition point.
4813 ///
4814 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4815 /// the (from-forms, from-source) × (keyword, classifier) 2×2 closes
4816 /// at the from-source classifier corner this method establishes —
4817 /// the prior runs' from-forms classifier sibling
4818 /// (`expand_and_collect_calls_to_any`, run e47d043) AND from-source
4819 /// keyword sibling (`expand_source_and_collect_calls_to`, run
4820 /// b477d81) AND slice-side classifier sibling (`iter_calls_to_any`,
4821 /// run 38625e3) named three of the four corners; this lift names
4822 /// the fourth so the family is structurally complete at every
4823 /// algebra level. THEORY.md §V.1 — knowable platform; the
4824 /// read-then-classifier-walk composition becomes a NAMED primitive
4825 /// on the substrate's `Expander` surface rather than a future
4826 /// re-derived `read(src)? + expand_and_collect_calls_to_any(…)`
4827 /// two-step chain at every consumer that wants to walk source text
4828 /// by typed-decoded classifier. THEORY.md §II.1 invariant 2 — free
4829 /// middle; the from-source keyword filter
4830 /// (`expand_source_and_collect_calls_to`) now routes through the
4831 /// from-source classifier filter via the constant-classifier
4832 /// composition, so a regression that drifts the keyword filter's
4833 /// instrumentation from the classifier filter's instrumentation
4834 /// becomes structurally impossible. The pre-lift inline
4835 /// `read(src)? + expand_and_collect_calls_to(forms, keyword, project)`
4836 /// chain is now a TYPED CONSEQUENCE of the typed-decoded primitive
4837 /// composed with a constant-classifier decoder, not a parallel
4838 /// implementation the type system happens to not catch.
4839 ///
4840 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4841 /// namespace + typed-syntax-class dispatch — the from-source-string
4842 /// entry to typed-program evaluation inside a namespace that carries
4843 /// the macro library, dispatching by typed syntax-class is the
4844 /// Racket idiom; the substrate's
4845 /// `expand_source_and_collect_calls_to_any` is the Rust-typed peer
4846 /// of that, with the typed-decoded classifier composed in. MLIR's
4847 /// `parseSourceFile<Op>(srcFile, ctx)` then `mod.walk<OpInterface>(
4848 /// callback)` — the parse-then-typed-walk over a source buffer
4849 /// dispatching by typed-interface witness is the MLIR idiom; this
4850 /// method is the substrate's typed `&[Sexp]`-algebra peer.
4851 pub fn expand_source_and_collect_calls_to_any<R, F, D, T>(
4852 &mut self,
4853 src: &str,
4854 decode: D,
4855 project: F,
4856 ) -> Result<Vec<R>>
4857 where
4858 D: FnMut(&str) -> Option<T>,
4859 F: FnMut(T, &[Sexp]) -> Result<R>,
4860 {
4861 let forms = crate::reader::read(src)?;
4862 self.expand_and_collect_calls_to_any(forms, decode, project)
4863 }
4864
4865 /// Compose the expander's program-level expansion with the substrate's
4866 /// typed-decoded classifier walk AND the named-form NAME-shape gate
4867 /// ([`crate::compile::split_name_slot`]) — the from-forms posture of
4868 /// the (named NAME-then-kwargs × typed-decoded classifier) cell of
4869 /// the typed-dispatcher matrix on the `Expander` surface, sibling of
4870 /// [`Self::expand_to_named`] (the constant-`T::KEYWORD` × named cell)
4871 /// and of [`Self::expand_and_collect_calls_to_any`] (the from-forms
4872 /// classifier × bare-kwargs cell).
4873 ///
4874 /// Routes through [`Self::expand_and_collect_calls_to_any`] with a
4875 /// wrapper projection that composes [`crate::compile::split_name_slot`]
4876 /// (the named-form arity + NAME-shape gate on the substrate's
4877 /// `&[Sexp]` algebra) with the caller-supplied per-form projection.
4878 /// The decoder yields `Option<(T, &'static str)>` — the typed witness
4879 /// PAIRED with the canonical static keyword the named-form structural
4880 /// rejection variants ([`crate::error::LispError::NamedFormMissingName`],
4881 /// [`crate::error::LispError::NamedFormNonSymbolName`]) carry as
4882 /// `&'static str` slots. Threading the `&'static` constraint through
4883 /// the decoder's return type pins the same compile-time discipline
4884 /// [`crate::compile::split_name_slot`]'s `keyword: &'static str`
4885 /// parameter pins at the gate boundary — a typo in the canonical
4886 /// keyword can never drift into the diagnostic at runtime, same posture
4887 /// as the constant-`T::KEYWORD` cell where `T::KEYWORD` is
4888 /// `&'static str` by trait construction.
4889 ///
4890 /// Closes the (named × typed-decoded classifier) corner of the
4891 /// typed-dispatcher matrix on the `Expander` surface that the prior
4892 /// run's [`crate::compile::split_name_slot`] lift (dd50801) named as
4893 /// the future change the slice-side gate's named lift enables. The
4894 /// 2×2 of compose-on-iter projections on the named-form axis becomes
4895 /// structurally complete on the `Expander` surface:
4896 ///
4897 /// | | constant `T::KEYWORD` | typed-decoded classifier |
4898 /// |----------------|-------------------------------------|----------------------------------------------|
4899 /// | from-forms | [`Self::expand_to_named`] | `expand_and_collect_named_calls_to_any` (this) |
4900 /// | from-source | [`Self::expand_source_to_named`] | [`Self::expand_source_and_collect_named_calls_to_any`] |
4901 ///
4902 /// The constant-`T::KEYWORD` column is the typed CONSEQUENCE of the
4903 /// classifier column: an `expand_to_named::<T>(forms)` call composes
4904 ///
4905 /// ```ignore
4906 /// expand_and_collect_named_calls_to_any(forms,
4907 /// |h| (h == T::KEYWORD).then_some(((), T::KEYWORD)),
4908 /// |(), name, spec_args| {
4909 /// let spec = T::compile_from_args(spec_args)?;
4910 /// Ok(NamedDefinition { name: name.to_string(), spec })
4911 /// })
4912 /// ```
4913 ///
4914 /// Both columns route through ONE composition point on the
4915 /// `Expander` surface (the typed-decoded classifier walk inside
4916 /// [`Self::expand_and_collect_calls_to_any`]) and ONE gate body on
4917 /// the `&[Sexp]` algebra ([`crate::compile::split_name_slot`]). A
4918 /// regression that drifts ONE cell's NAME-slot rejection chain from
4919 /// the others is structurally impossible — every cell binds to the
4920 /// SAME `split_name_slot` body, the SAME `expand_program +
4921 /// iter_calls_to_any + map + collect` pipeline. The
4922 /// pre-lift `NamedFormMissingName` / `NamedFormNonSymbolName`
4923 /// rejection chain inside `compile::named_form_projection` (the
4924 /// `pub(crate)` typed-domain SPECIALIZATION that
4925 /// [`Self::expand_to_named`] routes through) fires identically
4926 /// through this classifier primitive — both consumers compose
4927 /// [`crate::compile::split_name_slot`] with their per-site typed
4928 /// continuation.
4929 ///
4930 /// Future consumer shapes the typed-decoded named-form primitive
4931 /// admits with no boilerplate:
4932 /// * **Closed-set classifier** — a `tatara-check` runner that
4933 /// dispatches every `(defmonitor NAME …)` / `(defnotify NAME …)`
4934 /// / `(defalertpolicy NAME …)` form in `checks.lisp` through ONE
4935 /// classifier (a closed-set enum `Domain::{Monitor, Notify,
4936 /// AlertPolicy}` with `Domain::from_keyword`) decoding each
4937 /// head to its typed kind PAIRED with its canonical static
4938 /// keyword, then dispatches in the per-form projection — binds
4939 /// to ONE method rather than two-step `expand_and_collect_calls_to_any
4940 /// + split_name_slot` composition at each consumer site.
4941 /// * **Live-registry classifier** — a future `tatara-check`-shaped
4942 /// runtime dispatcher: `expand_and_collect_named_calls_to_any(forms,
4943 /// |h| registry.get(h).map(|d| (d, d.keyword())), |dispatcher,
4944 /// name, args| dispatcher.compile(name, args))` walks a program
4945 /// dispatching every named form whose head is in a runtime
4946 /// registry, decoded to a handler reference AND its canonical
4947 /// keyword (sourced from the dispatcher itself, NOT from the
4948 /// matched head text). The `&'static str` discipline on the
4949 /// keyword slot is preserved through the dispatcher's own
4950 /// `keyword() -> &'static str` accessor.
4951 /// * **`compile_named_any` free function family** — the natural
4952 /// fresh-expander free-function + preloaded-`RealizedCompiler`
4953 /// consumer pair, sibling of the existing `compile_typed_any`
4954 /// / `RealizedCompiler::compile_typed_any` pair. A future run
4955 /// lands those as 3-line composes on top of this primitive.
4956 ///
4957 /// `D` is `FnMut(&str) -> Option<(T, &'static str)>` — the typed-
4958 /// decoded classifier yields a typed witness `T` AND its canonical
4959 /// static keyword for the gate. `F` is `FnMut(T, &str, &[Sexp]) ->
4960 /// Result<R>` — the per-form projection receives the typed witness
4961 /// `T` ALONGSIDE the NAME slot's BORROWED `&str` projection (sourced
4962 /// from [`Sexp::as_symbol_or_string`], which accepts BOTH symbol and
4963 /// string NAME-author surfaces) AND the spec args tail. Consumers
4964 /// that need owned ownership of the NAME (`NamedDefinition.name:
4965 /// String`, JSON-serialized payloads) `.to_string()` themselves —
4966 /// pushing the clone to the consumer boundary keeps the primitive
4967 /// allocation-free. The `Result<R>` projection short-circuits on
4968 /// the first error via `Iterator::collect::<Result<Vec<R>, _>>()`
4969 /// inside [`Self::expand_and_collect_calls_to_any`].
4970 ///
4971 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4972 /// the (named × typed-decoded classifier) cell becomes a NAMED
4973 /// primitive on the `Expander` surface composed FROM TWO substrate
4974 /// primitives ([`Self::expand_and_collect_calls_to_any`] +
4975 /// [`crate::compile::split_name_slot`]) rather than re-derived
4976 /// inline at every named-classifier consumer's call site.
4977 /// THEORY.md §II.1 invariant 1 — typed entry; the typed-decoded
4978 /// classifier-filtered + NAME-shape-gated + caller-projected walk
4979 /// IS the typed-entry-batch gate for named-form dispatch at the
4980 /// `Expander` surface, with the `&'static str` keyword discipline
4981 /// preserved through the decoder's return type. THEORY.md §II.1
4982 /// invariant 2 — free middle; ALL four cells of the named-form
4983 /// dispatcher matrix on the `Expander` surface route through ONE
4984 /// composition point — a regression that drifts ONE cell's
4985 /// instrumentation from the others is structurally impossible.
4986 /// THEORY.md §V.1 — knowable platform; the named-form classifier
4987 /// walk becomes a discoverable primitive that LSP / REPL /
4988 /// `tatara-check` consumers bind to ONE method on the `Expander`
4989 /// instead of re-implementing the two-step `expand_and_collect_calls_to_any
4990 /// + split_name_slot` composition.
4991 ///
4992 /// Frontier inspiration: MLIR's `Region::walk<NamedOpInterface>(
4993 /// [&](auto op) { auto name = op.getName(); … })` — the typed-IR
4994 /// walk over a region yielding ops decoded to their typed interface
4995 /// witness with the named-symbol accessor pre-extracted is the
4996 /// MLIR idiom for named typed dispatch; this method is the
4997 /// substrate's unstructured-Rust peer with the typed-decoded
4998 /// classifier composed in and the NAME slot extracted via the
4999 /// substrate's `split_name_slot` gate. Racket's `syntax-parse`
5000 /// `~or* ((~datum defX) name:id arg ...) ((~datum defY) name:id
5001 /// arg ...) ((~datum defZ) name:id arg ...)` over an ellipsis-form
5002 /// — the typed-choice repeater with named-slot binder PAIRED with
5003 /// per-arm dispatch — is the Racket idiom; this primitive is the
5004 /// substrate's Rust-typed peer with the closed-set classifier
5005 /// playing the `~or*` typed-choice role and the NAME slot extracted
5006 /// by the substrate's slice-side gate.
5007 pub fn expand_and_collect_named_calls_to_any<R, F, D, T>(
5008 &mut self,
5009 forms: Vec<Sexp>,
5010 decode: D,
5011 mut project: F,
5012 ) -> Result<Vec<R>>
5013 where
5014 D: FnMut(&str) -> Option<(T, &'static str)>,
5015 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5016 {
5017 // Routes through the slice-side typed-decoded named projection
5018 // [`crate::ast::iter_named_calls_to_any`] — the same
5019 // `expand_program + iter_*_to_any + map + collect` shape
5020 // [`Self::expand_and_collect_calls_to_any`] uses on the bare-kwargs
5021 // axis. Pre-lift this method routed through the bare expander
5022 // surface and welded [`crate::compile::split_name_slot`] inside
5023 // the projection closure; post-lift the named gate composition
5024 // lives at the slice level (`iter_named_calls_to_any`'s body)
5025 // and the Expander surface inherits it through delegation. Both
5026 // rows of the Expander surface (bare-kwargs, named) now share
5027 // ONE pipeline skeleton on the slice algebra — a regression that
5028 // drifts a future debug-mode logger, span-aware borrow walker,
5029 // or fused-iterator invariant from one row to the other becomes
5030 // structurally impossible at the slice boundary.
5031 let expanded = self.expand_program(forms)?;
5032 crate::ast::iter_named_calls_to_any(&expanded, decode)
5033 .map(|maybe_triple| {
5034 let (decoded, name, spec_args) = maybe_triple?;
5035 project(decoded, name, spec_args)
5036 })
5037 .collect()
5038 }
5039
5040 /// Read a source string into top-level forms via [`crate::reader::read`],
5041 /// then route the forms through
5042 /// [`Self::expand_and_collect_named_calls_to_any`] — the from-source
5043 /// posture of the (named × typed-decoded classifier) cell on the
5044 /// `Expander` surface, sibling of
5045 /// [`Self::expand_source_and_collect_calls_to_any`] (the from-source
5046 /// classifier × bare-kwargs cell).
5047 ///
5048 /// Closes the (from-forms, from-source) × (constant `T::KEYWORD`,
5049 /// typed-decoded classifier) 2×2 on the named-form axis of the
5050 /// `Expander` surface — together with [`Self::expand_to_named`],
5051 /// [`Self::expand_source_to_named`], and
5052 /// [`Self::expand_and_collect_named_calls_to_any`], every cell of the
5053 /// matrix binds to ONE composition point.
5054 ///
5055 /// `?`-routing through `read` preserves the structural ordering of
5056 /// the rejection chain end-to-end: a reader error (lexer / parser /
5057 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5058 /// `expand_program` runs; an `expand_program` error short-circuits
5059 /// BEFORE the classifier filter walks anything; the named-form gate
5060 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5061 /// [`crate::compile::split_name_slot`] fires for the first matched
5062 /// form that violates either condition; a per-form `project` error
5063 /// short-circuits at the first failing matched form via
5064 /// `Iterator::collect::<Result<Vec<R>, _>>()`.
5065 ///
5066 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to_any`].
5067 /// THEORY.md §VI.1 (generation over composition; the from-source
5068 /// posture inherits the named-form gate composition through
5069 /// delegation rather than re-deriving the `read(src)? + walk + gate`
5070 /// chain at every from-source named-classifier consumer's call
5071 /// site), THEORY.md §II.1 invariant 2 (free middle; the from-source
5072 /// posture and the from-forms posture route through the SAME named-
5073 /// form composition).
5074 pub fn expand_source_and_collect_named_calls_to_any<R, F, D, T>(
5075 &mut self,
5076 src: &str,
5077 decode: D,
5078 project: F,
5079 ) -> Result<Vec<R>>
5080 where
5081 D: FnMut(&str) -> Option<(T, &'static str)>,
5082 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5083 {
5084 let forms = crate::reader::read(src)?;
5085 self.expand_and_collect_named_calls_to_any(forms, decode, project)
5086 }
5087
5088 /// Macroexpand a pre-parsed program through `self` and project every
5089 /// `(keyword NAME :k v …)` form into `R` via a caller-supplied
5090 /// `(name, args) -> Result<R>` projection — the constant-keyword
5091 /// sibling of [`Self::expand_and_collect_named_calls_to_any`] on the
5092 /// named-form axis of the `Expander` surface, parallel to how
5093 /// [`Self::expand_and_collect_calls_to`] is the constant-keyword
5094 /// sibling of [`Self::expand_and_collect_calls_to_any`] on the
5095 /// bare-kwargs axis.
5096 ///
5097 /// Routes through the classifier sibling with a constant-classifier
5098 /// decoder that yields a discarded `()` typed witness paired with
5099 /// the `&'static str` keyword — the same `(T, &'static str)` decoder
5100 /// shape [`crate::compile::split_name_slot`] (composed inside the
5101 /// classifier primitive) pins at the slice-side gate boundary for
5102 /// the named-form structural rejection chain
5103 /// (`NamedFormMissingName.keyword`, `NamedFormNonSymbolName.keyword`).
5104 /// `&'static` is the same lifetime discipline the typed-decoded
5105 /// classifier signature enforces; the named gate's verbatim keyword
5106 /// threading inherits it through the constant-classifier composition.
5107 ///
5108 /// Projection signature `FnMut(&str, &[Sexp]) -> Result<R>` receives
5109 /// the BORROWED NAME slot and the BORROWED spec args tail. Consumers
5110 /// that need owned ownership (`NamedDefinition.name: String`) call
5111 /// `.to_string()` themselves — pushing the clone to the consumer
5112 /// boundary keeps the primitive allocation-free, matching the
5113 /// classifier sibling's projection signature on the NAME slot.
5114 ///
5115 /// Closes the (named NAME-then-kwargs × constant-keyword) cell of
5116 /// the `Expander` typed-walk family at the runtime-keyword × untyped
5117 /// `R` corner — pre-lift the cell was reachable ONLY through
5118 /// [`Self::expand_to_named`] with the `T: TataraDomain` type
5119 /// parameter baking `T::KEYWORD` AND the `T::compile_from_args`-
5120 /// based `crate::compile::named_form_projection<T>` projection
5121 /// into the dispatch through `expand_and_collect_calls_to(forms,
5122 /// T::KEYWORD, named_form_projection::<T>)`. Post-lift the cell
5123 /// surfaces as ONE method that takes the keyword and the
5124 /// `(name, args) -> R` projection as caller-supplied parameters,
5125 /// and [`Self::expand_to_named`] routes through it as the typed
5126 /// `T::KEYWORD`-constant specialization. The split lets a consumer
5127 /// that wants "walk every form whose head is a runtime keyword `kw`
5128 /// and project `(NAME, spec_args) -> R` for an arbitrary `R`" (a
5129 /// future REPL `:walk-named <kw>` command, an LSP "find-named-
5130 /// declarations-of-keyword `kw`" handler, a `tatara-check` runner
5131 /// dispatching on a single typed keyword whose projection isn't
5132 /// `T::compile_from_args`) bind to ONE method on the `Expander`
5133 /// surface rather than re-deriving the
5134 /// `expand_and_collect_named_calls_to_any(forms, |h| (h ==
5135 /// kw).then_some(((), kw)), |(), name, args| project(name, args))`
5136 /// two-line composition inline.
5137 ///
5138 /// Sibling of [`Self::expand_source_and_collect_named_calls_to`]
5139 /// — that method stacks a [`crate::reader::read`] step on top of
5140 /// this one (the from-source posture); this method takes a
5141 /// pre-parsed `Vec<Sexp>` (the from-forms posture). Together with
5142 /// [`Self::expand_and_collect_named_calls_to_any`] /
5143 /// [`Self::expand_source_and_collect_named_calls_to_any`], the four
5144 /// cells of the (named × {constant-keyword, classifier} ×
5145 /// {from-forms, from-source}) sub-matrix close on the `Expander`
5146 /// surface — each cell binds to ONE composition point, with the
5147 /// constant-keyword cells routing through the classifier cells via
5148 /// constant-classifier decoding.
5149 ///
5150 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
5151 /// the `split_name_slot` composition lived at TWO sites pre-lift
5152 /// (`crate::compile::named_form_projection` body, which
5153 /// [`Self::expand_to_named`] routes through, AND
5154 /// [`Self::expand_and_collect_named_calls_to_any`] body, which the
5155 /// typed-decoded classifier consumers route through) — past the
5156 /// ≥2 PRIME-DIRECTIVE trigger once the named-form gate is named.
5157 /// THEORY.md §V.1 — knowable platform; the named constant-keyword
5158 /// walk becomes a NAMED primitive on the `Expander` surface
5159 /// composable by any future consumer (REPL, LSP, `tatara-check`)
5160 /// instead of a re-derived two-line `expand_and_collect_named_
5161 /// calls_to_any(forms, constant-decoder, wrapper-projection)`
5162 /// composition. THEORY.md §II.1 invariant 2 — free middle; both
5163 /// the typed (`expand_to_named<T>`) and untyped (this method)
5164 /// constant-keyword named cells route through the SAME named
5165 /// classifier primitive — a regression that drifts ONE cell's
5166 /// NAME-slot rejection chain (`NamedFormMissingName`,
5167 /// `NamedFormNonSymbolName`) from the other becomes structurally
5168 /// impossible.
5169 ///
5170 /// Frontier inspiration: Racket's `syntax-parse` `((~datum kw)
5171 /// name:id arg ...)` arm — the named NAME-slot binder under a
5172 /// constant `~datum` keyword paired with a per-match handler is
5173 /// the Racket idiom; this method is the substrate's Rust-typed
5174 /// peer with the constant-keyword filter composed at the
5175 /// classifier corner. MLIR's `Region::walk<NamedOpKind>([&](auto
5176 /// op) { auto name = op.getName(); … })` against a single
5177 /// operation kind — the typed-IR walk yielding ops decoded to
5178 /// their typed kind with the named-symbol accessor pre-extracted,
5179 /// specialized on a constant kind; this method is the substrate's
5180 /// unstructured-`R` peer with the constant-classifier composition
5181 /// pinned at the call boundary.
5182 pub fn expand_and_collect_named_calls_to<R, F>(
5183 &mut self,
5184 forms: Vec<Sexp>,
5185 keyword: &'static str,
5186 mut project: F,
5187 ) -> Result<Vec<R>>
5188 where
5189 F: FnMut(&str, &[Sexp]) -> Result<R>,
5190 {
5191 // Routes through the typed-decoded named-classifier sibling
5192 // [`Self::expand_and_collect_named_calls_to_any`] with a
5193 // constant-classifier decoder — the same constant-classifier
5194 // composition [`Self::expand_and_collect_calls_to`] uses to
5195 // route through [`Self::expand_and_collect_calls_to_any`] on
5196 // the bare-kwargs axis, and that
5197 // [`crate::ast::iter_calls_to`] uses to route through
5198 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
5199 // discarded `()` typed witness (`then_some(((), keyword))`)
5200 // is consumed by the wrapper projection
5201 // `|(), name, args| project(name, args)` so the keyword
5202 // consumer's per-form mapper sees only `(name, spec_args)`,
5203 // matching the bare projection signature.
5204 self.expand_and_collect_named_calls_to_any(
5205 forms,
5206 |h| (h == keyword).then_some(((), keyword)),
5207 move |(), name, args| project(name, args),
5208 )
5209 }
5210
5211 /// Read a source string into top-level forms via [`crate::reader::read`],
5212 /// then route the forms through
5213 /// [`Self::expand_and_collect_named_calls_to`] — the from-source
5214 /// posture of the (named × constant-keyword) cell on the
5215 /// `Expander` surface.
5216 ///
5217 /// Composes [`crate::reader::read`] with
5218 /// [`Self::expand_and_collect_named_calls_to`] — the
5219 /// `(keyword, project)` binding is bound in ONE place (the
5220 /// from-forms row) and this from-source sibling inherits the
5221 /// binding through delegation, mirroring how
5222 /// [`Self::expand_source_and_collect_calls_to`] composes
5223 /// [`crate::reader::read`] with [`Self::expand_and_collect_calls_to`]
5224 /// on the bare-kwargs axis.
5225 ///
5226 /// `?`-routing through `read` preserves the structural ordering of
5227 /// the rejection chain end-to-end: a reader error (lexer / parser /
5228 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5229 /// `expand_program` runs; an `expand_program` error short-circuits
5230 /// BEFORE the keyword filter walks anything; the named-form gate
5231 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5232 /// [`crate::compile::split_name_slot`] fires for the first matched
5233 /// form that violates either condition; a per-form `project` error
5234 /// short-circuits at the first failing matched form via
5235 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
5236 /// rejection chain inherits the constant-keyword named primitive's
5237 /// shape verbatim, now sourced from `&str` via ONE composition
5238 /// point.
5239 ///
5240 /// Sibling of [`Self::expand_and_collect_named_calls_to`] (the
5241 /// from-forms posture) and of [`Self::expand_source_and_collect_calls_to`]
5242 /// (the from-source × bare-kwargs cell on the constant-keyword
5243 /// row). The four cells of (named × {constant, classifier} ×
5244 /// {from-forms, from-source}) close on the `Expander` surface
5245 /// post-lift, each routing through ONE composition point.
5246 ///
5247 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to`].
5248 /// THEORY.md §VI.1 (generation over composition; the from-source
5249 /// posture inherits the constant-keyword named composition through
5250 /// delegation rather than re-deriving the
5251 /// `read(src)? + expand_and_collect_named_calls_to` chain at every
5252 /// from-source named-keyword consumer's call site),
5253 /// THEORY.md §II.1 invariant 2 (free middle; the from-source
5254 /// posture and the from-forms posture route through the SAME
5255 /// composition).
5256 pub fn expand_source_and_collect_named_calls_to<R, F>(
5257 &mut self,
5258 src: &str,
5259 keyword: &'static str,
5260 mut project: F,
5261 ) -> Result<Vec<R>>
5262 where
5263 F: FnMut(&str, &[Sexp]) -> Result<R>,
5264 {
5265 // From-source = from-forms × constant-classifier specialization
5266 // of the named classifier primitive's from-source sibling. The
5267 // discarded `()` typed witness and the `move |(), name, args|
5268 // project(name, args)` wrapper mirror the from-forms posture's
5269 // composition — both postures route through the SAME named
5270 // classifier primitive on the `Expander` surface, the
5271 // from-source row stacking one `read` step on top.
5272 self.expand_source_and_collect_named_calls_to_any(
5273 src,
5274 |h| (h == keyword).then_some(((), keyword)),
5275 move |(), name, args| project(name, args),
5276 )
5277 }
5278
5279 /// Expand a single form. Top-level macro calls are rewritten; recurses
5280 /// into list children.
5281 ///
5282 /// Routes the macro-call dispatch surface through the substrate's
5283 /// typed-decoded call decomposition: `as_call_to_any(|h|
5284 /// self.macros.get(h))` answers "is this form an invocation of any
5285 /// registered macro, decoded to `(&MacroDef, args)`?" in ONE
5286 /// structural query on the `Sexp` algebra. Pre-lift the same site
5287 /// opened the three-step chain `as_list() + as_call() + self.macros.
5288 /// get(head)` inline — `as_list()` for the children-walk fallthrough,
5289 /// `as_call()` for the (head, args) decomposition (which itself
5290 /// re-derives `as_list()` internally), and `self.macros.get(head)`
5291 /// for the registry lookup; post-lift the call-recognition runs as
5292 /// ONE `as_call_to_any` projection with the HashMap lookup as its
5293 /// classifier, and the `as_list()` fallthrough fires only on the
5294 /// not-a-macro-call path. Sibling consumer to `macro_def_from` — the
5295 /// typed-macro-definition dispatcher that routes through
5296 /// `as_call_to_any(MacroDefHead::from_keyword)` with the closed-set
5297 /// enum classifier. With both in place, BOTH dispatch sites in the
5298 /// macro expander (definition-recognition + call-recognition)
5299 /// project through the SAME family primitive on the `Sexp` algebra,
5300 /// each binding the classifier that fits its candidate set — closed
5301 /// enum for the static head-set, HashMap lookup for the live
5302 /// registry. A regression that drifts ONE site from the other (a
5303 /// future emitter that re-derives `as_list()` + `head.as_symbol()` +
5304 /// `self.macros.get(_)` inline rather than routing through the
5305 /// family) is no longer a silent two-site divergence.
5306 pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
5307 self.expand_with_depth(form, 0)
5308 }
5309
5310 /// The depth-carrying peer of [`Self::expand`]. Each
5311 /// post-apply re-expansion increments `depth`; each tree-child
5312 /// descent passes `depth` through unchanged. The runaway
5313 /// `(defmacro loop (x) `(loop ,x))` accretes depth on the
5314 /// re-expansion path — one unit per round — and hits the ceiling
5315 /// in `max_expansion_depth` rounds; lawful nested macros
5316 /// (`(when1 (when1 x))`) accrete depth in the low single digits
5317 /// because tree traversal doesn't count.
5318 ///
5319 /// The `depth >= limit` check sits INSIDE the macro-call arm
5320 /// (`Some((def, args)) = form.as_call_to_any(…)`) rather than at
5321 /// the top of the function so `def.name.clone()` is available at
5322 /// the rejection site — the operator gets `macro_name` populated
5323 /// from the actual offending call rather than a `<unknown>`
5324 /// fallback. Tree-child descent is not gated on the ceiling
5325 /// because the tree-child arm does not accrete depth; a lawfully
5326 /// deep tree is bounded by the reader's own stack, not by this
5327 /// ceiling.
5328 ///
5329 /// The peer OUTPUT-SIZE gate (`expanded.node_count() >
5330 /// self.limits.max_expansion_size`) sits INSIDE the same macro-call arm,
5331 /// between the `apply` and the re-expansion recursion, so a
5332 /// single macro's `apply` output whose structural size crosses
5333 /// the ceiling is rejected AT the offending `apply` boundary
5334 /// (with `macro_name` populated from `def.name`) before the
5335 /// runaway tree feeds back into further expansion. The gate is
5336 /// keyed on `>` rather than `>=` so `max_expansion_size` names
5337 /// the LARGEST admissible output — the ceiling admits equality,
5338 /// only strict overrun rejects. Together with the depth ceiling
5339 /// (recursion length) and the cache ceiling (memoization width),
5340 /// this closes the expander's RESOURCE surface at three typed
5341 /// dimensions.
5342 fn expand_with_depth(&self, form: &Sexp, depth: usize) -> Result<Sexp> {
5343 if let Some((def, args)) = form.as_call_to_any(|h| self.macros.get(h)) {
5344 if depth >= self.limits.max_expansion_depth {
5345 return Err(LispError::ExpansionDepthExceeded {
5346 macro_name: def.name.clone(),
5347 limit: self.limits.max_expansion_depth,
5348 });
5349 }
5350 let expanded = self.apply(def, args)?;
5351 // Reject a single `apply` output whose structural node
5352 // count crosses the OUTPUT-SIZE ceiling — the RESOURCE
5353 // axis peer of the depth (recursion length) and
5354 // cache-entries (memoization width) ceilings. Catches
5355 // the canonical "expansion bomb" where a well-defined
5356 // macro produces a 2^N-node blob from a small input
5357 // while `depth` stays in the low single digits and the
5358 // cache entry count sits under any reasonable ceiling.
5359 let expanded_size = expanded.node_count();
5360 if expanded_size > self.limits.max_expansion_size {
5361 return Err(LispError::ExpansionSizeExceeded {
5362 macro_name: def.name.clone(),
5363 size: expanded_size,
5364 limit: self.limits.max_expansion_size,
5365 });
5366 }
5367 // Recurse — the expansion itself may contain more macro calls.
5368 return self.expand_with_depth(&expanded, depth + 1);
5369 }
5370 // Not a macro call — expand children if this is a list; otherwise
5371 // (atom / Nil / quote-family wrapper) return the form verbatim.
5372 let Some(list) = form.as_list() else {
5373 return Ok(form.clone());
5374 };
5375 let mut out = Vec::with_capacity(list.len());
5376 for item in list {
5377 out.push(self.expand_with_depth(item, depth)?);
5378 }
5379 Ok(Sexp::List(out))
5380 }
5381
5382 /// Apply a macro to its argument list.
5383 ///
5384 /// Three-layer fast path:
5385 /// 1. If `cache_enabled`, hash `(name, args)` and consult the memo table.
5386 /// 2. If a compiled template exists, run the bytecode interpreter.
5387 /// 3. Otherwise fall back to the name-keyed substitute walker.
5388 fn apply(&self, def: &MacroDef, args: &[Sexp]) -> Result<Sexp> {
5389 // Layer 1: expansion cache.
5390 let cache_key = if self.cache_enabled {
5391 args_cache_key(&def.name, args)
5392 } else {
5393 None
5394 };
5395 if let Some(ref key) = cache_key {
5396 if let Some(cached) = self.cache.lock().unwrap().get(key) {
5397 return Ok(cached.clone());
5398 }
5399 }
5400
5401 // Layer 2: compiled bytecode.
5402 let result = if let Some(tmpl) = self.templates.get(&def.name) {
5403 apply_compiled(&def.name, &def.params, tmpl, args)?
5404 } else {
5405 // Layer 3: substitute fallback. Walk the body's substitution
5406 // projection — the inner of the outer quasi-quote when present,
5407 // the body verbatim otherwise — through the shared
5408 // `MacroDef::template_body` primitive both strategies route on.
5409 let bindings = bind_args(&def.name, &def.params, args)?;
5410 substitute(def.template_body(), &bindings)?
5411 };
5412
5413 // Populate cache on miss — capped by `max_cache_entries` so the
5414 // memoization table cannot drift unboundedly across a long-
5415 // running host session. When the cap is reached the freshly-
5416 // computed result is returned verbatim; correctness is
5417 // unaffected because caching is a pure PERFORMANCE optimization
5418 // (the miss path always recomputes the same value the cache
5419 // would have returned).
5420 if let Some(key) = cache_key {
5421 let mut cache = self.cache.lock().unwrap();
5422 if cache.len() < self.limits.max_cache_entries {
5423 cache.insert(key, result.clone());
5424 }
5425 }
5426 Ok(result)
5427 }
5428
5429 pub fn has(&self, name: &str) -> bool {
5430 self.macros.contains_key(name)
5431 }
5432
5433 pub fn len(&self) -> usize {
5434 self.macros.len()
5435 }
5436
5437 pub fn is_empty(&self) -> bool {
5438 self.macros.is_empty()
5439 }
5440}
5441
5442// ── Compiled template bytecode ───────────────────────────────────────
5443
5444/// One op in the template bytecode. Emitted during compilation; consumed at
5445/// expansion to materialize a form without HashMap lookups or recursion.
5446#[derive(Clone, Debug, PartialEq)]
5447pub enum TemplateOp {
5448 /// Push a literal Sexp. Used for atoms and entirely-literal subtrees.
5449 Literal(Sexp),
5450 /// Push the bound arg at the given param index.
5451 Subst(usize),
5452 /// If the bound arg is a list, append its items to the current list; else
5453 /// push it as a single item.
5454 Splice(usize),
5455 /// Begin a new List — pushes a fresh builder onto the expansion stack.
5456 BeginList,
5457 /// End the current List — pops the builder, wraps as `Sexp::List`.
5458 EndList,
5459}
5460
5461/// Pre-compiled template. Built once per macro, interpreted many times.
5462#[derive(Clone, Debug, Default, PartialEq)]
5463pub struct CompiledTemplate {
5464 pub ops: Vec<TemplateOp>,
5465}
5466
5467/// Walk a macro definition's template body and emit linear bytecode.
5468/// Purely-literal subtrees compile to a single `Literal(clone)` op.
5469///
5470/// Compilation can fail if the template references a name that isn't a
5471/// declared parameter — same semantic as the substitute path.
5472///
5473/// Top-level `,@X` bodies (the splice is the entire body, not nested inside
5474/// a `(... ,@xs ...)` list) are rejected here at compile time so the
5475/// bytecode path agrees with the substitute path's emission-time rejection
5476/// (`splice_outside_list`). Without this gate the bytecode interpreter's
5477/// outermost stack frame silently absorbed the splice's items, and the same
5478/// macro emitted different output across paths — `compiled_template_matches
5479/// _substitute_path` only covered well-positioned splice bodies. After this
5480/// gate every `,@-outside-list` body is rejected at registration time on
5481/// both paths with ONE structural variant (`LispError::SpliceOutsideList`).
5482///
5483/// The gate routes through [`Sexp::as_unquote`] — the typed-marker
5484/// projection that pairs `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at
5485/// ONE structural query — matching `Some((UnquoteForm::Splice, inner))`
5486/// rather than the per-arm `Sexp::UnquoteSplice(inner)` literal that
5487/// recurred at three sites pre-lift (`compile_node` Subst/Splice arms,
5488/// `substitute` top-level + list-inner). Sibling shape to `substitute`'s
5489/// list-inner Splice arm — both use the same `Some((UnquoteForm::Splice,
5490/// inner))` shape; `substitute`'s top-level arm uses the wider
5491/// `Some((kind, inner))` and dispatches inside on `kind`. After this lift
5492/// every production-site recognizer of "is this an `,@X` form" routes
5493/// through ONE typed-marker projection rather than re-deriving the
5494/// (Sexp variant, UnquoteForm variant) pair inline.
5495pub fn compile_template(def: &MacroDef) -> Result<CompiledTemplate> {
5496 // Walk the body's substitution projection — the inner of the outer
5497 // quasi-quote when present, the body verbatim otherwise — through the
5498 // shared `MacroDef::template_body` primitive the substitute path also
5499 // routes on. Same projection, both strategies, by construction.
5500 let body = def.template_body();
5501 if let Some((UnquoteForm::Splice, inner)) = body.as_unquote() {
5502 return Err(splice_outside_list(inner));
5503 }
5504 let names = def.params.names();
5505 let mut ops = Vec::new();
5506 compile_node(body, &names, &mut ops)?;
5507 Ok(CompiledTemplate { ops })
5508}
5509
5510fn compile_node(node: &Sexp, params: &[&str], ops: &mut Vec<TemplateOp>) -> Result<()> {
5511 // Fast-path literal: if the subtree has no Unquote/UnquoteSplice, emit a
5512 // single Literal op. This is the big win for macros where most of the
5513 // template is fixed structure.
5514 if !contains_unquote(node) {
5515 ops.push(TemplateOp::Literal(node.clone()));
5516 return Ok(());
5517 }
5518 // Routes the `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice(inner)` arms
5519 // through [`Sexp::as_unquote`] — the typed-marker projection that
5520 // pairs `Sexp::Unquote ↔ UnquoteForm::Unquote` and
5521 // `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at ONE site. The per-form
5522 // `TemplateOp` emission (`Subst` vs `Splice`) keys on the same typed
5523 // `form` value the gate-1+gate-2 composition `resolve_unquote_in_params`
5524 // threads through. Pre-lift the (Sexp variant, UnquoteForm variant)
5525 // pairing was bound per-arm — a future emitter that matched
5526 // `Sexp::Unquote(_)` but threaded `UnquoteForm::Splice` into
5527 // `resolve_unquote_in_params` (or vice versa) would type-check but
5528 // render a misleading diagnostic at the gate-1 / gate-2 rejection.
5529 // Post-lift the pair is bound at ONE projection function and the
5530 // `match form` mechanically lowers it to the bytecode op.
5531 if let Some((form, inner)) = node.as_unquote() {
5532 let idx = resolve_unquote_in_params(inner, params, form)?;
5533 ops.push(match form {
5534 UnquoteForm::Unquote => TemplateOp::Subst(idx),
5535 UnquoteForm::Splice => TemplateOp::Splice(idx),
5536 });
5537 return Ok(());
5538 }
5539 match node {
5540 Sexp::List(items) => {
5541 ops.push(TemplateOp::BeginList);
5542 for item in items {
5543 compile_node(item, params, ops)?;
5544 }
5545 ops.push(TemplateOp::EndList);
5546 }
5547 _ => ops.push(TemplateOp::Literal(node.clone())),
5548 }
5549 Ok(())
5550}
5551
5552fn contains_unquote(node: &Sexp) -> bool {
5553 // Route every quote-family wrapper recognition — both the
5554 // unquote-only subset (Unquote/UnquoteSplice → short-circuit `true`)
5555 // AND the quote-only subset (Quote/Quasiquote → recurse into inner) —
5556 // through [`Sexp::as_quote_form`]'s typed-marker projection. Pre-lift
5557 // the recognizer split into two arms: `as_unquote().is_some()` (the
5558 // substitution subset gate) + the inline
5559 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm (the remaining
5560 // quote-only subset). Post-lift both arms route through ONE
5561 // projection and the (Sexp variant, QuoteForm variant) pairing binds
5562 // at the closed-set algebra. The `form.as_unquote_form().is_some()`
5563 // gate is the SAME 2-of-4 subset projection [`Sexp::as_unquote`]
5564 // derives from, so recognizing "is this an unquote-family wrapper"
5565 // and "is this a quote-only wrapper" share ONE typed dispatch site;
5566 // rustc enforces that a future `Sexp` wrapper extension carry
5567 // through both `QuoteForm::ALL` AND `QuoteForm::as_unquote_form`'s
5568 // arm. Sibling posture to `Hash for Sexp`'s four-arm
5569 // `hash_discriminator` collapse, `Display for Sexp`'s `prefix`
5570 // collapse, `interop`'s `iac_forge_tag` collapse, and `domain`'s
5571 // `sexp_shape` collapse — every production-site quote-family
5572 // recognizer now routes through ONE projection on the algebra.
5573 if let Some((form, inner)) = node.as_quote_form() {
5574 return form.as_unquote_form().is_some() || contains_unquote(inner);
5575 }
5576 match node {
5577 Sexp::List(items) => items.iter().any(contains_unquote),
5578 _ => false,
5579 }
5580}
5581
5582/// Splice a resolved template value into an in-progress list builder —
5583/// the SHARED coercion both expansion strategies apply once `,@name`'s
5584/// gate-1 (must-be-a-symbol) and gate-2 (must-be-bound-in-scope) have
5585/// resolved the bound value. ONE named primitive the bytecode path
5586/// (`apply_compiled`'s `TemplateOp::Splice` arm) AND the substitute path
5587/// (`substitute`'s list-inner `Sexp::UnquoteSplice` arm) share. Before
5588/// this lift the three-arm coercion —
5589///
5590/// ```ignore
5591/// match value {
5592/// Sexp::List(items) => builder.extend(items.iter().cloned()),
5593/// Sexp::Nil => {}
5594/// other => builder.push(other.clone()),
5595/// }
5596/// ```
5597///
5598/// — was inlined at BOTH sites; the splice RESULT semantics (the last
5599/// inline-duplicated piece of the splice path after the prior runs lifted
5600/// gate-1, gate-2, and their composition) lived in two places that MUST
5601/// agree. After this lift the coercion lives in ONE function, so a
5602/// regression that drifts one strategy's splice posture from the other —
5603/// e.g. changing the `Sexp::Nil` arm to push an empty list at the
5604/// bytecode path but not the substitute path, or coercing a non-list
5605/// scalar differently across the two strategies — becomes structurally
5606/// impossible: there is exactly one implementation both strategies call.
5607///
5608/// The coercion's three arms ARE the no-evaluator template language's
5609/// splice contract: a bound LIST flattens its elements into the builder
5610/// (the canonical splice), a bound NIL contributes nothing (splicing the
5611/// empty list), and any other bound value splices as a single element (a
5612/// scalar `,@x` degrades to `,x` rather than erroring — invariant 2's
5613/// "free middle" lets the macro author rely on this without a
5614/// mid-rewrite type check; the typed-exit gate re-validates the
5615/// assembled form). Naming the contract once gives a future gate-3
5616/// (typed-shape enforcement on bound splice targets) ONE site to wrap
5617/// rather than two inline arms to keep in lockstep.
5618///
5619/// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the two
5620/// expansion strategies MUST produce identical output for the same
5621/// (macro, args) pair, and naming the splice coercion once makes that
5622/// per-strategy agreement structural rather than a two-site discipline
5623/// the `expansion_layers_agree_on_output_and_cache_wins` benchmark only
5624/// observes after the fact. THEORY.md §V.1 — knowable platform; the
5625/// splice RESULT semantics becomes a NAMED primitive authoring tools and
5626/// future runs bind to. THEORY.md §VI.1 — generation over composition;
5627/// the two-site coercion is lifted to ONE function, closing the last
5628/// inline-duplicated piece of the splice path the prior runs' gate lifts
5629/// (02173dc gate-1, 68da647 gate-2, b456f1f composition) left behind.
5630fn splice_value_into(builder: &mut Vec<Sexp>, value: &Sexp) {
5631 match value {
5632 Sexp::List(items) => builder.extend(items.iter().cloned()),
5633 Sexp::Nil => {}
5634 other => builder.push(other.clone()),
5635 }
5636}
5637
5638/// Promote the previously `LispError::Compile`-shaped helper into the
5639/// structural `LispError::TemplateInvariant { macro_name, kind }` variant.
5640/// The four reachable bytecode-runtime invariant violations in
5641/// `apply_compiled` — Subst-bad-index, Splice-bad-index, EndList-empty-
5642/// stack, final-no-value — funnel through ONE emission shape keyed on
5643/// the closed-set `TemplateInvariantKind` enum. The index payload of
5644/// the Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
5645/// / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
5646/// kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
5647/// is structurally unrepresentable — the type system encodes "this gate
5648/// has an index, that gate does not."
5649///
5650/// Display matches the legacy `Compile`-shaped diagnostic byte-for-byte
5651/// across all four kinds (`"compile error in {macro_name}: <invariant>"`)
5652/// via the closed-set `TemplateInvariantKind::message()` projection, so
5653/// authoring-tool substring greps (`tatara-check`, REPL) see no drift
5654/// across the lift.
5655///
5656/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
5657/// of bytecode-invariant failure modes becomes a TYPE rather than a
5658/// free-form `message: String` slot. THEORY.md §VI.1 — generation over
5659/// composition; the typed enum lands the structural-completeness floor
5660/// for the bytecode-runtime surface, parallel to how `CompilerSpecIoStage`
5661/// lands the structural-completeness floor for the disk-persistence
5662/// surface (`compiler_spec.rs`, the immediately prior claude-routine
5663/// lift on a sibling file). THEORY.md §II.1 invariant 5 (composition
5664/// preserves proofs): a well-formed bytecode invariant is the proof
5665/// that drives the interpreter; the structural variant makes the
5666/// proof's REJECTION shape first-class — authoring tools (REPL, LSP,
5667/// `tatara-check`) pattern-match on the `kind` slot and bind to the
5668/// gate identity directly instead of substring-parsing the rendered
5669/// diagnostic.
5670fn template_invariant_violation(macro_name: &str, kind: TemplateInvariantKind) -> LispError {
5671 LispError::TemplateInvariant {
5672 macro_name: macro_name.into(),
5673 kind,
5674 }
5675}
5676
5677/// Look up a bound-arg by its template-bytecode index, or raise the
5678/// structural `LispError::TemplateInvariant` rejection with the
5679/// caller-supplied `kind` constructor applied to the bad index. ONE
5680/// named primitive both bytecode-runtime arms that read a bound arg
5681/// by index — [`TemplateOp::Subst`] (single-value push) AND
5682/// [`TemplateOp::Splice`] (list-splicing) — route through.
5683///
5684/// Before this lift the same `args_by_index.get(*idx).ok_or_else(||
5685/// template_invariant_violation(macro_name, KIND(*idx)))?` projection
5686/// appeared at BOTH arms of [`apply_compiled`], differing only in the
5687/// kind constructor: [`TemplateInvariantKind::SubstBadIndex`] at the
5688/// `Subst` arm, [`TemplateInvariantKind::SpliceBadIndex`] at the
5689/// `Splice` arm. The arms also diverged on what they did with the
5690/// returned `&Sexp` — `Subst` cloned and pushed, `Splice` consumed
5691/// the borrow through [`splice_value_into`] — but the lookup-and-
5692/// reject prelude was byte-identical modulo the kind, well past the
5693/// ≥2 PRIME-DIRECTIVE trigger.
5694///
5695/// After this lift the lookup-and-reject shape lives in ONE function;
5696/// the two arms thread the per-call-site kind constructor through the
5697/// helper and apply their respective post-lookup verbs at the call
5698/// site. The `kind: FnOnce(usize) -> TemplateInvariantKind` parameter
5699/// encodes the closed-set bytecode-runtime "this gate has an index"
5700/// surface at the type level — only the two
5701/// [`TemplateInvariantKind`] variants whose payload IS the bad index
5702/// (`SubstBadIndex(usize)` and `SpliceBadIndex(usize)`) construct
5703/// directly through `FnOnce(usize) -> TemplateInvariantKind`; the
5704/// stack-gate variants ([`TemplateInvariantKind::EndListEmptyStack`]
5705/// and [`TemplateInvariantKind::FinalNoValue`]) carry no payload and
5706/// would not type-check at this boundary, so the invalid combination
5707/// "stack-gate kind reached from an op-index lookup" is structurally
5708/// unrepresentable at the helper's call boundary the same way
5709/// [`TemplateInvariantKind`]'s closed-set shape makes it
5710/// unrepresentable in the variant itself.
5711///
5712/// Sibling of [`template_invariant_violation`]: that helper builds the
5713/// typed [`LispError::TemplateInvariant`] variant from a fully-formed
5714/// `kind`; this helper composes the index-keyed lookup with the
5715/// variant-builder, so the kind constructor doesn't have to be evaluated
5716/// eagerly at the call site (lazy via `FnOnce`, only fires on the bad-
5717/// index path). A future fifth bytecode op that reads a bound arg by
5718/// index (a hypothetical [`TemplateOp::Conditional`] that branches on a
5719/// bound boolean, a [`TemplateOp::Project`] that extracts a sub-field
5720/// of a bound `Sexp::List`) extends the family in ONE call to
5721/// `resolve_bound_arg` with the new kind constructor (`KIND(usize) ->
5722/// TemplateInvariantKind`) — the bytecode-runtime's bound-arg-by-index
5723/// projection becomes ONE structural primitive consumers compose with.
5724///
5725/// The returned `&'a Sexp` borrows from `args_by_index` verbatim —
5726/// `Subst`'s arm consumes it through `.clone()` (the consumer pushes
5727/// an owned value into the builder); `Splice`'s arm consumes it
5728/// through [`splice_value_into`] (the consumer borrows for the
5729/// per-arm coercion). The borrow's lifetime `'a` is the unified
5730/// lifetime of `args_by_index`, matching the call site's borrow
5731/// posture.
5732///
5733/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5734/// inline copies of the index-lookup-and-reject prelude across the
5735/// `apply_compiled` body's `Subst` and `Splice` arms is past the ≥2
5736/// PRIME-DIRECTIVE trigger once the structural shape is named.
5737/// THEORY.md §V.1 — knowable platform / "make invalid states
5738/// unrepresentable"; the bytecode-runtime bound-arg-by-index lookup
5739/// becomes a NAMED primitive on the substrate's `&[Sexp]` algebra
5740/// rather than a re-derived `get + ok_or_else + template_invariant_
5741/// violation` chain at every op-arm that reads by index. A future
5742/// authoring tool (REPL, LSP, `tatara-check`) that wants to surface
5743/// "this bytecode op's bound-arg lookup misfired at idx N" binds to
5744/// ONE function. THEORY.md §II.1 invariant 2 — free middle; both
5745/// expansion strategies route through the SHARED `MacroParams::bind`,
5746/// AND the bytecode strategy's op-arms route through this SHARED
5747/// `resolve_bound_arg` lookup — the bytecode-runtime's
5748/// proof-of-well-formedness is now structurally uniform across the
5749/// two reachable index-lookup ops, so a regression that drifts ONE
5750/// arm's posture (e.g. accepts an out-of-range idx at one arm but
5751/// not the other, or swaps the kind constructor at a single arm) is
5752/// no longer a silent two-site divergence.
5753fn resolve_bound_arg<'a>(
5754 args_by_index: &'a [Sexp],
5755 idx: usize,
5756 macro_name: &str,
5757 kind: impl FnOnce(usize) -> TemplateInvariantKind,
5758) -> Result<&'a Sexp> {
5759 args_by_index
5760 .get(idx)
5761 .ok_or_else(|| template_invariant_violation(macro_name, kind(idx)))
5762}
5763
5764/// Project the bytecode-runtime stack to its in-progress builder frame —
5765/// the `&mut Vec<Sexp>` every value-emitting op writes into. ONE named
5766/// primitive both push-emitting arms (`TemplateOp::Literal` /
5767/// `TemplateOp::Subst` / post-`EndList` parent fold) AND the splice-
5768/// emitting arm (`TemplateOp::Splice`) route through.
5769///
5770/// Before this lift the same `stack.last_mut().unwrap()` projection
5771/// appeared at FOUR sites inside [`apply_compiled`]'s op-loop:
5772///
5773/// * `TemplateOp::Literal` — pushes the literal `Sexp` into the
5774/// current builder.
5775/// * `TemplateOp::Subst` — pushes the cloned bound-arg into the
5776/// current builder.
5777/// * `TemplateOp::Splice` — splices the bound-arg into the current
5778/// builder via [`splice_value_into`].
5779/// * `TemplateOp::EndList` — after popping the just-finished list
5780/// frame, pushes the folded `Sexp::List(items)` into the parent
5781/// builder (the new current frame).
5782///
5783/// Four byte-identical re-derivations of the same projection, well past
5784/// the ≥2 PRIME-DIRECTIVE trigger. After this lift the four sites
5785/// collapse to a single `current_builder_mut(&mut stack).{push|extend}`
5786/// call, and the bytecode-runtime invariant the projection rests on
5787/// — "the op-loop always sees at least one stack frame" — lives in ONE
5788/// expect message rather than four silent `.unwrap()` calls.
5789///
5790/// The expect rationale: [`apply_compiled`] seeds the stack with the
5791/// outermost frame at entry (`vec![Vec::with_capacity(1)]`); every
5792/// `TemplateOp::BeginList` pushes a NEW frame and every
5793/// `TemplateOp::EndList` pops it, so the count stays at OR ABOVE 1
5794/// throughout the op-loop. Stack-depleting failure modes are caught
5795/// upstream by their own structural variants:
5796/// [`TemplateInvariantKind::EndListEmptyStack`] fires inside
5797/// [`apply_compiled`]'s `EndList` arm via [`Vec::pop`]'s `Option`
5798/// gate, BEFORE the parent-fold push runs against
5799/// `current_builder_mut`; [`TemplateInvariantKind::FinalNoValue`]
5800/// fires AFTER the op-loop completes, on the outermost `stack.pop()`
5801/// that returns the assembled result. So a reachable
5802/// `current_builder_mut(&mut stack)` always observes a non-empty
5803/// stack, and the `expect` is a structural-invariant marker, not a
5804/// load-bearing rejection path.
5805///
5806/// Sibling of [`resolve_bound_arg`] (the bytecode-runtime bound-arg
5807/// lookup primitive lifted in the prior claude-routine run on this
5808/// module — 492a235) and [`template_invariant_violation`] (the
5809/// structural-variant error builder for the bytecode-runtime's
5810/// closed-set invariant-violation surface). Together the three primitives
5811/// name the bytecode-runtime's substrate-level operations: lookup-a-
5812/// bound-arg ([`resolve_bound_arg`]), build-the-invariant-rejection
5813/// ([`template_invariant_violation`]), and project-to-the-current-
5814/// builder (this lift). A future bytecode op that emits ONE OR MORE
5815/// values into the current builder — a hypothetical
5816/// `TemplateOp::SpliceMany(indices: Vec<usize>)` that splices a batch,
5817/// a `TemplateOp::PushQuoted(form: Sexp)` that wraps before push, a
5818/// span-annotated emit-with-position op — composes with ONE call to
5819/// [`current_builder_mut`] and the per-op post-projection verb
5820/// (`.push(…)`, `.extend(…)`, `splice_value_into(…, _)`); a future
5821/// instrumentation hook that wants to log every op's emit before
5822/// it lands in the builder wraps ONE call boundary, not four inline
5823/// `stack.last_mut().unwrap()` sites.
5824///
5825/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
5826/// inline copies of the top-of-stack projection in one function is
5827/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5828/// named. THEORY.md §V.1 — knowable platform; the bytecode-runtime's
5829/// current-builder projection becomes a NAMED primitive on the
5830/// substrate's `&mut [Vec<Sexp>]` slice algebra rather than a re-derived
5831/// `last_mut + unwrap` chain at every op-arm that emits into the
5832/// builder. The expect message names the invariant
5833/// ("bytecode-runtime invariant: at least one stack frame during
5834/// op-loop") so a regression that drifts the loop's frame management
5835/// surfaces a NAMED panic, not a silent `unwrap` over `None`.
5836/// THEORY.md §II.1 invariant 2 — free middle; both expansion
5837/// strategies route through the SHARED `MacroParams::bind` upstream
5838/// AND the bytecode strategy's op-arms now route through this SHARED
5839/// `current_builder_mut` projection downstream — the bytecode-runtime's
5840/// substrate-level surface (lookup + emit) is named in two
5841/// composable primitives the op-arms compose with.
5842fn current_builder_mut(stack: &mut [Vec<Sexp>]) -> &mut Vec<Sexp> {
5843 stack
5844 .last_mut()
5845 .expect("bytecode-runtime invariant: at least one stack frame during op-loop")
5846}
5847
5848/// Pop the top stack frame off the bytecode-runtime stack, or raise the
5849/// structural [`LispError::TemplateInvariant`] rejection with the
5850/// supplied [`TemplateInvariantKind`] when the stack is empty — ONE
5851/// named primitive both pop-emitting sites in [`apply_compiled`] route
5852/// through.
5853///
5854/// Before this lift two byte-identical
5855/// `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
5856/// kind))?` chains lived inline in [`apply_compiled`]:
5857///
5858/// * `TemplateOp::EndList` arm — pops the just-finished list frame
5859/// before the parent-fold push, with kind
5860/// [`TemplateInvariantKind::EndListEmptyStack`] guarding the
5861/// unreachable empty-stack failure mode.
5862/// * Post-loop final pop — consumes the outermost frame that
5863/// accumulated the template's result, with kind
5864/// [`TemplateInvariantKind::FinalNoValue`] guarding the
5865/// unreachable seed-frame-already-popped failure mode.
5866///
5867/// Two byte-identical re-derivations of the same projection inside one
5868/// function, past the ≥2 PRIME-DIRECTIVE trigger once the structural
5869/// shape is named. After this lift the two sites collapse to a single
5870/// `pop_builder_frame(&mut stack, macro_name, KIND)?` call, and the
5871/// bytecode-runtime invariant the projection rests on — "an empty-stack
5872/// pop is a structural-variant rejection, not a silent `Option::None`"
5873/// — lives in ONE composition point rather than two.
5874///
5875/// Sibling of [`current_builder_mut`] (the bytecode-runtime stack's
5876/// *project-to-top-frame* primitive — the borrow face, never resizes
5877/// the stack) and [`resolve_bound_arg`] (the bound-arg-by-index
5878/// lookup primitive with per-call-site `TemplateInvariantKind`
5879/// constructor). Where `current_builder_mut` borrows the in-progress
5880/// top frame for emission and never panics on a reachable input
5881/// (callers route past it only when the seed frame is present), this
5882/// primitive *consumes* the top frame off the stack and projects its
5883/// absence into a structural `LispError::TemplateInvariant` rejection
5884/// — the failure-on-empty face of the same `&mut Vec<Vec<Sexp>>`
5885/// algebra. The pair —
5886/// [`current_builder_mut`] + [`pop_builder_frame`] — close the
5887/// substrate's bytecode-runtime stack-frame projection algebra at the
5888/// borrow/consume boundary: borrow-the-top-frame for emission (no
5889/// rejection — the invariant rests on the seed-frame contract),
5890/// consume-the-top-frame for finalization (rejection-routed —
5891/// the absence projects through `TemplateInvariantKind` into a
5892/// structural variant of [`LispError`]). Together with
5893/// [`resolve_bound_arg`] (lookup args by index, with per-call-site
5894/// kind constructor) and [`template_invariant_violation`] (the typed
5895/// rejection emitter the three primitives share), the four
5896/// substrate-level operations name the bytecode-runtime's named
5897/// projection surface: lookup-a-bound-arg, project-to-the-current-
5898/// builder, consume-a-finished-builder-frame, build-the-invariant-
5899/// rejection.
5900///
5901/// `kind: TemplateInvariantKind` is the closed-set typed enum whose
5902/// four variants are EXACTLY the four reachable bytecode-runtime
5903/// invariant-violation modes (`SubstBadIndex(usize)` /
5904/// `SpliceBadIndex(usize)` from the index-lookup sibling primitive,
5905/// `EndListEmptyStack` from the `EndList` arm, `FinalNoValue` from
5906/// the post-loop final pop). The Subst / Splice indexed variants
5907/// thread their `usize` payload INSIDE the variant, so the invalid
5908/// combination "stack-gate kind with an op-index" (a hypothetical
5909/// `EndListEmptyStack(99)` carrying a Subst-style payload) is
5910/// structurally unrepresentable at this helper's boundary — the
5911/// caller cannot misroute an indexed kind through `pop_builder_frame`
5912/// at compile time because the kind's data shape is part of its
5913/// variant identity. Same closed-set guarantee
5914/// [`template_invariant_violation`] gives the four kinds; this
5915/// helper composes that guarantee with the `stack.pop()` projection
5916/// at the two pop-emitting sites.
5917///
5918/// The future-run extensions ride this floor: a future bytecode op
5919/// that consumes one or more finished frames — a hypothetical
5920/// `TemplateOp::EndMany(n: usize)` that pops `n` frames into a
5921/// flattened list, a span-aware `TemplateOp::EndListWithSpan(pos)`
5922/// that pops with a position-annotated rejection — composes with
5923/// ONE call (or a fold over N calls) to [`pop_builder_frame`]
5924/// without re-deriving the stack-pop-and-reject shape. A future
5925/// instrumentation hook (a debug-mode logger that records every
5926/// frame consumption, a span-aware pop that threads `Sexp` positions
5927/// through, a multi-frame fold that pops N frames in one step)
5928/// wraps ONE call boundary rather than keeping two inline chains in
5929/// lockstep at the production op-loop sites.
5930///
5931/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5932/// inline copies of the stack-pop-and-reject projection across the
5933/// `apply_compiled` body's `EndList` arm and post-loop final-pop is
5934/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5935/// named — the same threshold [`resolve_bound_arg`] (the index-lookup
5936/// sibling) crossed in the prior claude-routine run on this file
5937/// (492a235) and [`current_builder_mut`] (the top-frame-borrow
5938/// sibling) crossed two runs ago (c6a5a9d). THEORY.md §V.1 — knowable
5939/// platform / "make invalid states unrepresentable"; the bytecode-
5940/// runtime stack-frame consume operation becomes a NAMED primitive
5941/// on the substrate's `&mut Vec<Vec<Sexp>>` algebra rather than a
5942/// re-derived `pop + ok_or_else + template_invariant_violation`
5943/// chain at every op-arm that consumes a frame. THEORY.md §II.1
5944/// invariant 2 — free middle; both pop-emitting sites route through
5945/// the SHARED `pop_builder_frame` projection, so a regression that
5946/// drifts ONE site's posture (e.g. accepts an empty-stack pop at the
5947/// `EndList` arm but not the final pop, or swaps the kind constructor
5948/// at a single site) is no longer a silent two-site divergence — the
5949/// type system binds both sites to ONE composition point.
5950///
5951/// Frontier inspiration: MLIR's `Block::eraseFromParent()` against a
5952/// region's block list — the structured-IR's block-consumption
5953/// operation is a named typed primitive that yields a typed
5954/// `LogicalResult` rejection rather than a silent `nullptr` projection
5955/// when the parent region is empty; the substrate's
5956/// `pop_builder_frame` is the unstructured-projection peer on the
5957/// substrate's `&mut Vec<Vec<Sexp>>` stack-frame algebra, with
5958/// `TemplateInvariantKind` standing in for MLIR's `LogicalResult`'s
5959/// closed-set rejection identity. GHC Core's `popTickish` /
5960/// `stackPop` family — every Core-IR transform that consumes a stack
5961/// frame off the rewriter's working stack binds to one named pop
5962/// primitive that threads a typed `WantedFailure` rejection when the
5963/// stack is empty; translated through pleme-io primitives as ONE
5964/// `pop_builder_frame(stack, macro_name, kind)` call with
5965/// `TemplateInvariantKind` carrying the closed-set rejection
5966/// identity.
5967fn pop_builder_frame(
5968 stack: &mut Vec<Vec<Sexp>>,
5969 macro_name: &str,
5970 kind: TemplateInvariantKind,
5971) -> Result<Vec<Sexp>> {
5972 stack
5973 .pop()
5974 .ok_or_else(|| template_invariant_violation(macro_name, kind))
5975}
5976
5977/// Execute a pre-compiled template against the macro's argument list.
5978fn apply_compiled(
5979 macro_name: &str,
5980 params: &MacroParams,
5981 tmpl: &CompiledTemplate,
5982 args: &[Sexp],
5983) -> Result<Sexp> {
5984 // Resolve args by param index through the shared positional binder —
5985 // identical semantics to the `bind_args` (substitute) path by construction.
5986 let args_by_index = params.bind(macro_name, args)?;
5987
5988 // Run the bytecode against a stack of in-progress list builders. The
5989 // outermost frame accumulates the single result the template yields.
5990 // Each emit-into-builder arm routes through the shared
5991 // `current_builder_mut` projection — the bytecode-runtime invariant
5992 // "at least one stack frame during the op-loop" lives in ONE expect
5993 // message rather than four silent `.unwrap()` calls.
5994 let mut stack: Vec<Vec<Sexp>> = vec![Vec::with_capacity(1)];
5995 for op in &tmpl.ops {
5996 match op {
5997 TemplateOp::Literal(s) => current_builder_mut(&mut stack).push(s.clone()),
5998 TemplateOp::Subst(idx) => {
5999 // Bound-arg-by-index lookup routes through the shared
6000 // `resolve_bound_arg` projection with `SubstBadIndex` as
6001 // the per-call-site kind constructor; the post-lookup
6002 // verb (clone + push into the current builder) is the
6003 // Subst arm's per-op shape.
6004 let v = resolve_bound_arg(
6005 &args_by_index,
6006 *idx,
6007 macro_name,
6008 TemplateInvariantKind::SubstBadIndex,
6009 )?
6010 .clone();
6011 current_builder_mut(&mut stack).push(v);
6012 }
6013 TemplateOp::Splice(idx) => {
6014 // Sibling lookup through `resolve_bound_arg` with
6015 // `SpliceBadIndex` as the per-call-site kind constructor;
6016 // the post-lookup verb (`splice_value_into` against the
6017 // current builder) consumes the borrow directly without
6018 // an intermediate clone.
6019 let v = resolve_bound_arg(
6020 &args_by_index,
6021 *idx,
6022 macro_name,
6023 TemplateInvariantKind::SpliceBadIndex,
6024 )?;
6025 splice_value_into(current_builder_mut(&mut stack), v);
6026 }
6027 TemplateOp::BeginList => stack.push(Vec::new()),
6028 TemplateOp::EndList => {
6029 // Pop the just-finished list frame through the shared
6030 // `pop_builder_frame` projection with
6031 // `EndListEmptyStack` as the per-call-site kind
6032 // constructor; the post-pop verb (folded
6033 // `Sexp::List(items)` push into the parent frame via
6034 // `current_builder_mut`) is the EndList arm's per-op
6035 // shape — sibling of the Subst/Splice arms' index-
6036 // lookup-then-emit posture, with the index-lookup
6037 // primitive (`resolve_bound_arg`) and the frame-
6038 // consume primitive (`pop_builder_frame`) BOTH routing
6039 // through the same `TemplateInvariantKind` closed-set
6040 // rejection identity.
6041 let items = pop_builder_frame(
6042 &mut stack,
6043 macro_name,
6044 TemplateInvariantKind::EndListEmptyStack,
6045 )?;
6046 current_builder_mut(&mut stack).push(Sexp::List(items));
6047 }
6048 }
6049 }
6050 // Final pop consumes the outermost (seed) frame via the same
6051 // shared projection — `FinalNoValue` as the per-call-site kind
6052 // constructor pins this site's structural-variant identity, the
6053 // post-pop verb (the `top.len() == 1` arity gate below) is the
6054 // post-loop tail's per-site shape.
6055 let mut top = pop_builder_frame(&mut stack, macro_name, TemplateInvariantKind::FinalNoValue)?;
6056 if top.len() == 1 {
6057 Ok(top.remove(0))
6058 } else {
6059 Ok(Sexp::List(top))
6060 }
6061}
6062
6063/// Hash of `(macro_name, args)` for cache keying — hot path, kept lean.
6064/// Uses `DefaultHasher` (SipHash-2-4) — fast enough that the cache hit rate
6065/// needed to net a win is low even for cheap macros.
6066fn args_cache_key(macro_name: &str, args: &[Sexp]) -> Option<CacheKey> {
6067 let mut h = DefaultHasher::new();
6068 args.len().hash(&mut h);
6069 for a in args {
6070 a.hash(&mut h);
6071 }
6072 Some((macro_name.to_string(), h.finish()))
6073}
6074
6075/// `pub(crate)` so the span-preserving expander recognizes a macro
6076/// definition through THIS function rather than a second copy of the
6077/// `defmacro`-head / name / param-list / body decomposition. A `MacroDef`
6078/// retains no spans (macros are keyed by name), so the spanned path lowers
6079/// its form and lands here — one recognizer, one lambda-list parser, one
6080/// error taxonomy.
6081pub(crate) fn macro_def_from(form: &Sexp) -> Result<Option<MacroDef>> {
6082 // Route the typed-macro-definition dispatch surface through the
6083 // substrate's typed-decoded call decomposition: `as_call_to_any`
6084 // performs the `as_list + head_symbol + MacroDefHead::from_keyword`
6085 // three-step chain in ONE structural query on the `Sexp` algebra.
6086 // The legacy diagnostic anchors on `list.len()` (the FULL form arity
6087 // including the head) — preserved here as `args.len() + 1` so
6088 // `LispError::DefmacroArity.arity` carries the same value across the
6089 // lift.
6090 let Some((head, args)) = form.as_call_to_any(MacroDefHead::from_keyword) else {
6091 return Ok(None);
6092 };
6093 if args.len() < 3 {
6094 return Err(defmacro_arity(head, args.len() + 1));
6095 }
6096 let name = args[0]
6097 .as_symbol()
6098 .ok_or_else(|| defmacro_non_symbol_name(head, &args[0]))?
6099 .to_string();
6100 let param_list = args[1]
6101 .as_list()
6102 .ok_or_else(|| defmacro_non_list_params(head, &args[1]))?;
6103 let params = parse_params(param_list)?;
6104 let body = args[2].clone();
6105 Ok(Some(MacroDef { name, params, body }))
6106}
6107
6108fn parse_params(list: &[Sexp]) -> Result<MacroParams> {
6109 let mut required = Vec::new();
6110 let mut optional: Vec<OptionalParam> = Vec::new();
6111 let mut optional_marker: Option<usize> = None;
6112 let mut i = 0;
6113 while i < list.len() {
6114 // In the optional section a `(name default)` LIST form is a valid spec
6115 // alongside a bare-symbol spec. The list form is only meaningful here,
6116 // so the dispatch fires before the `as_symbol()` gate that would
6117 // otherwise reject it as `NonSymbolParam`.
6118 if optional_marker.is_some() {
6119 if let Sexp::List(items) = &list[i] {
6120 optional.push(parse_optional_list_spec(i, &list[i], items)?);
6121 i += 1;
6122 continue;
6123 }
6124 }
6125 let s = list[i]
6126 .as_symbol()
6127 .ok_or_else(|| non_symbol_param(i, &list[i]))?;
6128 if s == MacroParams::REST_MARKER {
6129 let Some(next) = list.get(i + 1) else {
6130 return Err(rest_param_missing_name(i, None));
6131 };
6132 let Some(name) = next.as_symbol() else {
6133 return Err(rest_param_missing_name(i, Some(next)));
6134 };
6135 let trailing = &list[i + 2..];
6136 if !trailing.is_empty() {
6137 return Err(rest_param_trailing_tokens(i, trailing));
6138 }
6139 return Ok(MacroParams {
6140 required,
6141 optional,
6142 rest: Some(name.to_string()),
6143 });
6144 }
6145 if s == MacroParams::OPTIONAL_MARKER {
6146 if let Some(first) = optional_marker {
6147 return Err(optional_marker_repeated(first, i));
6148 }
6149 optional_marker = Some(i);
6150 i += 1;
6151 continue;
6152 }
6153 if optional_marker.is_some() {
6154 optional.push(OptionalParam::bare(s));
6155 } else {
6156 required.push(s.to_string());
6157 }
6158 i += 1;
6159 }
6160 Ok(MacroParams {
6161 required,
6162 optional,
6163 rest: None,
6164 })
6165}
6166
6167/// Project a `Sexp::List` in the `&optional` section to a typed
6168/// [`OptionalParam`]. The only admissible shape is `(NAME DEFAULT)` — a
6169/// list of exactly TWO elements whose first element is a symbol. Every
6170/// other list shape is the structural rejection
6171/// [`LispError::OptionalParamMalformed`], with a typed `reason`
6172/// ([`OptionalParamMalformedReason`]) naming WHICH way the spec is
6173/// malformed — empty, missing-default, extra-elements, or non-symbol name.
6174///
6175/// `position` is the loop index inside `parse_params`, mirroring the
6176/// `position`/`rest_position`/`first_position` slots on the sibling
6177/// `parse_params` rejection variants. `list_form` is the offending
6178/// `Sexp::List` itself, projected through `crate::domain::sexp_witness` so
6179/// the variant carries BOTH `SexpShape::List` AND the rendered form (for
6180/// LSP / REPL / `tatara-check` consumption). `items` is the list body,
6181/// avoiding a re-`as_list()` at the call boundary.
6182fn parse_optional_list_spec(
6183 position: usize,
6184 list_form: &Sexp,
6185 items: &[Sexp],
6186) -> Result<OptionalParam> {
6187 use crate::error::OptionalParamMalformedReason as R;
6188 // Arity dispatch — threads through the typed classifier on the
6189 // closed-set algebra so the accept-arity binding (arity ==
6190 // `R::OPTIONAL_PARAM_SPEC_ARITY`) is derived from ONE typed const
6191 // rather than the bare literal `2`. Post-classification the arity
6192 // is exactly the accept target; the head-symbol gate is the only
6193 // remaining rejection axis.
6194 if let Some(reason) = R::classify_arity(items.len()) {
6195 return Err(optional_param_malformed(position, list_form, reason));
6196 }
6197 let Some(name) = items[0].as_symbol() else {
6198 return Err(optional_param_malformed(
6199 position,
6200 list_form,
6201 R::NonSymbolName,
6202 ));
6203 };
6204 Ok(OptionalParam::with_default(name, items[1].clone()))
6205}
6206
6207fn bind_args(
6208 macro_name: &str,
6209 params: &MacroParams,
6210 args: &[Sexp],
6211) -> Result<HashMap<String, Sexp>> {
6212 // Zip the shared positional binding (parallel to `names()`) into the
6213 // name-keyed map the `substitute` path looks substitutions up in.
6214 let vals = params.bind(macro_name, args)?;
6215 Ok(params
6216 .names()
6217 .into_iter()
6218 .map(String::from)
6219 .zip(vals)
6220 .collect())
6221}
6222
6223/// Substitute `,name` and `,@name` within a template.
6224/// `,@name` only makes sense inside a List — it splices the bound list into
6225/// the containing list.
6226///
6227/// Routes both unquote-family sites — the top-level `,X` / `,@X`
6228/// recognition AND the list-inner per-item splice recognition — through
6229/// the substrate's typed-marker projection [`Sexp::as_unquote`]. Pre-lift
6230/// each site opened its own `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice
6231/// (inner)` arm paired with a `UnquoteForm::Unquote` / `UnquoteForm::
6232/// Splice` literal; post-lift the (Sexp variant, UnquoteForm variant)
6233/// pairing is bound at ONE projection function the type system threads
6234/// through `(UnquoteForm, &Sexp)`, eliminating the silent two-site
6235/// pairing drift the prior shape allowed.
6236fn substitute(form: &Sexp, bindings: &HashMap<String, Sexp>) -> Result<Sexp> {
6237 if let Some((kind, inner)) = form.as_unquote() {
6238 return match kind {
6239 UnquoteForm::Unquote => resolve_unquote_in_bindings(inner, bindings, kind).cloned(),
6240 UnquoteForm::Splice => Err(splice_outside_list(inner)),
6241 };
6242 }
6243 match form {
6244 Sexp::List(items) => {
6245 let mut out: Vec<Sexp> = Vec::with_capacity(items.len());
6246 for item in items {
6247 if let Some((UnquoteForm::Splice, inner)) = item.as_unquote() {
6248 let val = resolve_unquote_in_bindings(inner, bindings, UnquoteForm::Splice)?;
6249 splice_value_into(&mut out, val);
6250 } else {
6251 out.push(substitute(item, bindings)?);
6252 }
6253 }
6254 Ok(Sexp::List(out))
6255 }
6256 _ => Ok(form.clone()),
6257 }
6258}
6259
6260/// Lift the four inline `LispError::Compile { form: format!("{prefix}{name}"),
6261/// message: "unbound" }` triples (compile_node Unquote/UnquoteSplice +
6262/// substitute Unquote/UnquoteSplice) behind ONE named primitive. Pairs the
6263/// structural variant with `crate::domain::suggest`'s bounded edit-distance
6264/// scan over the candidate set so a typo in `,name` against a macro's params
6265/// (or against a substitution scope's live bindings) surfaces as
6266/// `"compile error in ,xs: unbound; did you mean ,x?"` instead of the bare
6267/// `"unbound"`. The candidate set is per-call — params during compile,
6268/// `bindings.keys()` during substitute — so the operator's hint is always
6269/// drawn from the in-scope name set, never a stale snapshot.
6270///
6271/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6272/// variants are EXACTLY the two reachable syntactic markers
6273/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6274/// boundary (rather than `&'static str`) lands the same compile-time
6275/// closed-set guarantee `defmacro_arity` / `defmacro_non_symbol_name` /
6276/// `defmacro_non_list_params` get from threading `MacroDefHead`: the
6277/// closed set is encoded in the type system, so a regression that drifts
6278/// the marker (e.g. a fourth `prefix: ",,"` call site) becomes a type
6279/// error at the call site, not a runtime substring drift. `name` is the
6280/// offender from source; the hint is `Option<String>` because the matched
6281/// candidate borrows from a transient `Vec<&str>` we built locally —
6282/// copying the matched name into the variant is the cheapest way to keep
6283/// `LispError` lifetime-free.
6284///
6285/// Theory anchor: THEORY.md §VI.1 — generation over composition; four inline
6286/// copies in one module is well past the three-times rule. THEORY.md §V.1 —
6287/// knowable platform; the structural variant exposes `prefix` / `name` /
6288/// `hint` as first-class fields so authoring tools (LSP, REPL,
6289/// `tatara-check`) bind to the data shape instead of substring-parsing the
6290/// rendered diagnostic.
6291fn unbound_template_var(prefix: UnquoteForm, name: &str, candidates: &[&str]) -> LispError {
6292 LispError::UnboundTemplateVar {
6293 prefix,
6294 name: name.to_string(),
6295 hint: crate::domain::suggest(name, candidates).map(str::to_string),
6296 }
6297}
6298
6299/// Lift the four inline `LispError::Compile { form: "unquote" /
6300/// "unquote-splice", message: "only bound symbols may appear after `,` /
6301/// `,@`" }` triples in this module (compile_node Unquote / UnquoteSplice +
6302/// substitute Unquote / UnquoteSplice-inside-list) behind ONE named
6303/// primitive. Sibling of `unbound_template_var`: that helper fires when the
6304/// slot IS a symbol but the symbol isn't bound; this helper fires when the
6305/// slot isn't a symbol at all. Together they close every distinct
6306/// typed-entry template-gate failure mode for the no-evaluator template
6307/// language: each is a structural variant of `LispError`, not a
6308/// `Compile`-shaped substring.
6309///
6310/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6311/// variants are EXACTLY the two reachable syntactic markers
6312/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6313/// boundary (rather than `&'static str`) lands the same compile-time
6314/// closed-set guarantee `unbound_template_var` carries: the closed set is
6315/// encoded in the type system. The inner is the offending `Sexp` routed
6316/// through `crate::domain::sexp_witness` — the typed joint projection
6317/// pairing `SexpShape` (structural shape) with `Sexp::Display`
6318/// (renderable literal) at ONE call boundary. Authoring tools bind to
6319/// BOTH `got.shape` (e.g. `SexpShape::List`) AND `got.display` (e.g.
6320/// `"(list 1 2)"`) jointly — same posture as `splice_outside_list`
6321/// after its prior-run promotion to `SexpWitness`. The two template-
6322/// gate `,X/,@X` rejection variants now share ONE typed witness
6323/// identity at their `got` slot.
6324///
6325/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6326/// inline copies in one module is past the three-times rule. THEORY.md
6327/// §V.1 — knowable platform; the structural variant exposes `prefix` /
6328/// `got` as first-class fields so authoring tools (LSP, REPL,
6329/// `tatara-check`) bind to the data shape instead of substring-parsing
6330/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6331/// a non-symbol unquote target is exactly the failure mode the
6332/// typed-entry gate exists to reject.
6333fn non_symbol_unquote_target(prefix: UnquoteForm, got: &Sexp) -> LispError {
6334 LispError::NonSymbolUnquoteTarget {
6335 prefix,
6336 got: got.witness(),
6337 }
6338}
6339
6340/// Project the inner of a `,X` / `,@X` form to its bound symbol name, or
6341/// raise the structural `LispError::NonSymbolUnquoteTarget` rejection at
6342/// the typed-entry template-gate boundary. ONE named primitive every
6343/// `,X` / `,@X` resolution site in the substrate shares — the inline
6344/// `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(form,
6345/// inner))?` pattern appeared four times across `compile_node`
6346/// (bytecode-path Unquote / UnquoteSplice arms) AND `substitute`
6347/// (substitute-path Unquote / list-inner UnquoteSplice arms), well past
6348/// the three-times-rule trigger. After this lift the four sites collapse
6349/// to a single `unquote_target_symbol(inner, form)?` call, and the
6350/// substrate's understanding of "an unquote target's first gate is `must
6351/// be a symbol`" lives in ONE function — a regression that drifts the
6352/// gate's posture (e.g. accepts non-symbol targets at the bytecode path
6353/// but not the substitute path) becomes a type-level change at this
6354/// helper, not a silent four-site divergence.
6355///
6356/// Sibling of `non_symbol_unquote_target` (the error builder this gate
6357/// calls on failure) and `unbound_template_var` (the typed-entry
6358/// template-gate's SECOND gate — fires once `unquote_target_symbol`
6359/// projects the symbol successfully but the symbol isn't bound in the
6360/// in-scope name set). Together the three close the substrate's
6361/// understanding of the two-step typed-entry template-gate: gate-1 is
6362/// `must-be-a-symbol`, gate-2 is `must-be-bound-in-scope`. With this
6363/// lift, gate-1 lives at ONE call boundary across all four template-
6364/// gate sites — bytecode path AND substitute path AND both `,X` and
6365/// `,@X` forms.
6366///
6367/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6368/// variants are EXACTLY the two reachable syntactic markers
6369/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6370/// helper boundary (rather than `&'static str`) lands the same
6371/// compile-time closed-set guarantee `non_symbol_unquote_target` and
6372/// `unbound_template_var` get from their `UnquoteForm` slots — a
6373/// regression that drifts the marker (e.g. a third pseudo-marker call
6374/// site) becomes a type error at the call site, not a runtime
6375/// substring drift. The returned `&'a str` borrows from `inner` — the
6376/// caller feeds it directly into `params.iter().position(|p| *p ==
6377/// name)` (`compile_node`) or `bindings.get(name)` (`substitute`)
6378/// without an intermediate allocation.
6379///
6380/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6381/// inline copies of the gate-1 projection (`compile_node`
6382/// Unquote/UnquoteSplice + `substitute` Unquote + `substitute`
6383/// list-inner UnquoteSplice) is past the three-times rule. THEORY.md
6384/// §V.1 — knowable platform; the gate's identity becomes a NAMED
6385/// primitive consumer-binding rather than a four-times-inlined
6386/// match-and-reject snippet — authoring surfaces (REPL, LSP,
6387/// `tatara-check`) that want to surface "the typed-entry template-gate
6388/// rejected your form because the unquote target wasn't a symbol" bind
6389/// to ONE function. THEORY.md §II.1 invariant 1 — typed entry; an
6390/// unquote target that isn't a symbol is exactly the failure mode the
6391/// typed-entry template-gate exists to reject. THEORY.md §II.1
6392/// invariant 2 — free middle; both bytecode AND substitute expansion
6393/// paths now project through the SAME gate-1 primitive, so a macro
6394/// that compiles under one strategy compiles under the other (the
6395/// gate's posture is uniform across the two strategies, no
6396/// per-strategy drift can creep in).
6397fn unquote_target_symbol(inner: &Sexp, form: UnquoteForm) -> Result<&str> {
6398 inner
6399 .as_symbol()
6400 .ok_or_else(|| non_symbol_unquote_target(form, inner))
6401}
6402
6403/// Gate-2 for the bytecode-template compile path: resolve a template
6404/// variable name to its index inside the macro's static param list, or
6405/// raise the structural `LispError::UnboundTemplateVar` rejection. ONE
6406/// named primitive that the two `compile_node` sites — `Sexp::Unquote(_)`
6407/// and `Sexp::UnquoteSplice(_)` arms — share. Before this lift the same
6408/// `params.iter().position(|p| *p == name).ok_or_else(|| unbound_template_var(
6409/// FORM, name, params))?` projection was inlined twice in one match
6410/// block; after this lift the two sites collapse to a single
6411/// `resolve_param_index(name, params, form)?` call and the
6412/// `Subst(idx)` / `Splice(idx)` ops push from a uniform projection
6413/// boundary.
6414///
6415/// Sibling of `resolve_binding`: the same gate-2 contract on the
6416/// substitute path. Together the two close the typed-entry template
6417/// gate's gate-2 (must-be-bound-in-scope) primitive across BOTH
6418/// expansion strategies — gate-1 (`unquote_target_symbol`) projects the
6419/// inner to a symbol name; gate-2 looks the name up in the in-scope
6420/// candidate set. The two paths' candidate sets differ structurally
6421/// (compile path: `&[&str]` of macro params, returning `usize`;
6422/// substitute path: `&HashMap<String, Sexp>` of live bindings, returning
6423/// `&Sexp`), so the gate-2 primitive bifurcates by path — but the
6424/// rejection shape (`LispError::UnboundTemplateVar { prefix, name, hint }`
6425/// with `crate::domain::suggest`-driven hint) is identical across both
6426/// paths. A regression that drifts gate-2's posture (e.g., accepts an
6427/// unbound `,name` at the bytecode path but not the substitute path) is
6428/// now a type-level change at this helper, not a silent four-site
6429/// divergence.
6430///
6431/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6432/// variants are EXACTLY the two reachable syntactic markers
6433/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6434/// helper boundary (rather than `&'static str`) lands the same
6435/// compile-time closed-set guarantee `unquote_target_symbol`,
6436/// `unbound_template_var`, and `non_symbol_unquote_target` carry — a
6437/// regression that drifts the marker becomes a type error at the call
6438/// site, not a runtime substring drift.
6439///
6440/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6441/// inline copies of the gate-2 projection in one match block, paired
6442/// with the two substitute-path inline copies, is four copies in two
6443/// functions — past the three-times rule once the structural shape is
6444/// named. THEORY.md §V.1 — knowable platform; the gate's identity
6445/// becomes a NAMED primitive consumer-binding rather than a
6446/// twice-inlined position-and-reject snippet — authoring surfaces
6447/// (REPL, LSP, `tatara-check`) that want to surface "the typed-entry
6448/// template-gate rejected your form because the name isn't bound in
6449/// scope" bind to ONE function per path. THEORY.md §II.1 invariant 1 —
6450/// typed entry; an unbound template variable is exactly the failure
6451/// mode the typed-entry template-gate exists to reject. THEORY.md
6452/// §II.1 invariant 2 — free middle; both expansion strategies'
6453/// gate-2 emit the SAME structural variant, so a macro that compiles
6454/// under one strategy compiles under the other.
6455fn resolve_param_index(name: &str, params: &[&str], form: UnquoteForm) -> Result<usize> {
6456 params
6457 .iter()
6458 .position(|p| *p == name)
6459 .ok_or_else(|| unbound_template_var(form, name, params))
6460}
6461
6462/// Gate-2 for the substitute expansion path: resolve a template
6463/// variable name to its bound `Sexp` value inside the runtime bindings
6464/// map, or raise the structural `LispError::UnboundTemplateVar`
6465/// rejection. ONE named primitive that the two `substitute` sites —
6466/// the top-level `Sexp::Unquote(_)` arm and the list-inner
6467/// `Sexp::UnquoteSplice(_)` arm — share. Before this lift the same
6468/// `bindings.get(sym).<cloned>?.ok_or_else(|| unbound_template_var(
6469/// FORM, sym, &bound_names(bindings)))` projection was inlined twice
6470/// across the substitute walker; after this lift the two sites
6471/// collapse to a single `resolve_binding(bindings, sym, form)?` call
6472/// (with a trailing `.cloned()` at the top-level arm because that arm
6473/// returns an owned `Sexp` while the list-inner arm consumes the
6474/// `&Sexp` borrow directly).
6475///
6476/// Sibling of `resolve_param_index`: the same gate-2 contract on the
6477/// bytecode-template compile path. Together the two close the
6478/// typed-entry template gate's gate-2 (must-be-bound-in-scope)
6479/// primitive across BOTH expansion strategies. The candidate set on
6480/// the substitute path is the live bindings' keys (built fresh per
6481/// call via `bound_names`) — never a stale snapshot, so the
6482/// suggest-driven hint is always drawn from the actual in-scope name
6483/// set the operator sees.
6484///
6485/// The returned `&'a Sexp` borrows from `bindings` — the list-inner
6486/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6487/// splice-expansion match without an intermediate allocation. The
6488/// top-level caller's owned-Sexp obligation is satisfied by the
6489/// `.cloned()` projection at the call site, which is a single typed
6490/// `Sexp::clone` and not a redundant lookup.
6491///
6492/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6493/// `resolve_param_index` and `unquote_target_symbol`. A regression
6494/// that drifts the marker becomes a type error at the call site, not
6495/// a runtime substring drift.
6496///
6497/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6498/// inline copies of the gate-2 projection in the substitute walker,
6499/// paired with the two compile-path inline copies, is four copies in
6500/// two functions — past the three-times rule once the structural
6501/// shape is named. THEORY.md §V.1 — knowable platform; the gate's
6502/// identity becomes a NAMED primitive consumer-binding rather than a
6503/// twice-inlined lookup-and-reject snippet. THEORY.md §II.1
6504/// invariant 1 — typed entry; an unbound template variable is exactly
6505/// the failure mode the typed-entry template-gate exists to reject.
6506/// THEORY.md §II.1 invariant 2 — free middle; both expansion
6507/// strategies' gate-2 emit the SAME structural variant.
6508fn resolve_binding<'a>(
6509 bindings: &'a HashMap<String, Sexp>,
6510 name: &str,
6511 form: UnquoteForm,
6512) -> Result<&'a Sexp> {
6513 bindings
6514 .get(name)
6515 .ok_or_else(|| unbound_template_var(form, name, &bound_names(bindings)))
6516}
6517
6518/// Compose gate-1 + gate-2 for the bytecode-template compile path into ONE
6519/// named primitive: project the unquote `inner` to a symbol name
6520/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6521/// index inside the macro's static param list (gate-2, via
6522/// `resolve_param_index`). Sibling of `resolve_unquote_in_bindings`: the
6523/// same gate-1+gate-2 composition on the substitute expansion path.
6524///
6525/// Before this lift, the two `compile_node` arms (`Sexp::Unquote(_)` and
6526/// `Sexp::UnquoteSplice(_)`) threaded `form: UnquoteForm` through TWO
6527/// helper calls each — once into `unquote_target_symbol(inner, form)?`
6528/// (gate-1) AND once into `resolve_param_index(name, params, form)?`
6529/// (gate-2). The marker's typed identity was re-asserted at the call site
6530/// twice per arm — four `UnquoteForm::Unquote` / `UnquoteForm::Splice`
6531/// literal occurrences across the two arms, for what is structurally ONE
6532/// marker-identity per syntactic-marker arm. After this lift each arm
6533/// threads the marker ONCE through ONE call, and the gate-1-then-gate-2
6534/// sequencing lives in the helper body, not at the call site.
6535///
6536/// The composition is load-bearing: gate-1 (must-be-a-symbol) MUST fire
6537/// before gate-2 (must-be-bound-in-scope) — a non-symbol inner is
6538/// structurally a different failure mode (`LispError::NonSymbolUnquoteTarget`,
6539/// which carries the offending `SexpWitness`) than an unbound symbol
6540/// (`LispError::UnboundTemplateVar`, which carries a `name: String` plus
6541/// a `crate::domain::suggest`-driven hint over the candidate set). A
6542/// regression that reorders or skips gate-1 would emit
6543/// `LispError::UnboundTemplateVar { name: "(list 1 2)", ... }` for a
6544/// non-symbol inner (re-treating the rendered list literal as a bound-
6545/// name lookup key), which is exactly the diagnostic-confusion this
6546/// composition exists to rule out. Naming the composition as one
6547/// primitive makes the sequencing structural — the helper body IS the
6548/// proof that gate-1 ran before gate-2.
6549///
6550/// `form` is `UnquoteForm` — the closed-set typed enum threaded through
6551/// the composition once and passed onward to both gate-1 and gate-2's
6552/// rejection-builders. Same posture as `unquote_target_symbol`,
6553/// `resolve_param_index`, `resolve_binding`, `non_symbol_unquote_target`,
6554/// and `unbound_template_var` — a regression that drifts the marker
6555/// becomes a type error at the helper boundary, not a runtime substring
6556/// drift, AND the marker can no longer drift BETWEEN gate-1 and gate-2
6557/// at a single call site (which the prior pre-lift shape allowed:
6558/// `unquote_target_symbol(inner, UnquoteForm::Unquote)?` followed by
6559/// `resolve_param_index(name, params, UnquoteForm::Splice)?` would
6560/// type-check but render a misleading diagnostic).
6561///
6562/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6563/// gate-1+gate-2 SEQUENCE is itself a named primitive once both halves
6564/// have been named (two prior runs landed the halves; this run lands the
6565/// composition). THEORY.md §V.1 — knowable platform; the gate's
6566/// composition is now load-bearing in the type system — gate-1 cannot be
6567/// silently skipped, gate-2 cannot be silently reordered before gate-1,
6568/// and the marker cannot drift between the two halves. THEORY.md §II.1
6569/// invariant 1 — typed entry; the typed-entry template gate's full
6570/// rejection chain (non-symbol → unbound-symbol) is now ONE primitive.
6571/// THEORY.md §II.1 invariant 2 — free middle; both expansion strategies
6572/// expose the gate's identity as ONE primitive per path, so a macro that
6573/// passes the gate under one strategy passes under the other (no per-
6574/// strategy composition drift can creep in).
6575fn resolve_unquote_in_params(inner: &Sexp, params: &[&str], form: UnquoteForm) -> Result<usize> {
6576 let name = unquote_target_symbol(inner, form)?;
6577 resolve_param_index(name, params, form)
6578}
6579
6580/// Compose gate-1 + gate-2 for the substitute expansion path into ONE
6581/// named primitive: project the unquote `inner` to a symbol name
6582/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6583/// bound `Sexp` value inside the runtime bindings map (gate-2, via
6584/// `resolve_binding`). Sibling of `resolve_unquote_in_params`: the same
6585/// gate-1+gate-2 composition on the bytecode-template compile path.
6586///
6587/// Before this lift, the substitute walker's two unquote sites (the
6588/// top-level `Sexp::Unquote(_)` arm and the list-inner
6589/// `Sexp::UnquoteSplice(_)` arm) threaded `form: UnquoteForm` through
6590/// TWO helper calls each — once into `unquote_target_symbol(inner,
6591/// form)?` (gate-1) AND once into `resolve_binding(bindings, name,
6592/// form)?` (gate-2). After this lift each site threads the marker
6593/// ONCE through ONE call. Same composition contract as
6594/// `resolve_unquote_in_params` — gate-1 fires before gate-2 by the
6595/// helper body's `?`-then-call sequencing, NOT by call-site discipline.
6596///
6597/// The returned `&'a Sexp` borrows from `bindings` so the list-inner
6598/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6599/// splice-expansion match without an intermediate allocation; the
6600/// top-level caller's owned-Sexp obligation is satisfied by a
6601/// `.cloned()` projection at the call site (one typed `Sexp::clone`,
6602/// no redundant lookup).
6603///
6604/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6605/// `resolve_unquote_in_params` and all the helpers it composes. After
6606/// this lift, the marker's identity flows through the substitute path's
6607/// typed-entry template gate via ONE explicit pass per call site, not
6608/// two; the gate's gate-1+gate-2 sequencing is structural across both
6609/// expansion strategies.
6610///
6611/// Theory anchor: same as `resolve_unquote_in_params`. THEORY.md §VI.1
6612/// (generation over composition; named composition of named gates),
6613/// THEORY.md §V.1 (knowable platform; gate composition is type-system
6614/// load-bearing), THEORY.md §II.1 invariant 1 (typed entry; the full
6615/// rejection chain is ONE primitive), THEORY.md §II.1 invariant 2
6616/// (free middle; both strategies share the same composition shape).
6617fn resolve_unquote_in_bindings<'a>(
6618 inner: &Sexp,
6619 bindings: &'a HashMap<String, Sexp>,
6620 form: UnquoteForm,
6621) -> Result<&'a Sexp> {
6622 let name = unquote_target_symbol(inner, form)?;
6623 resolve_binding(bindings, name, form)
6624}
6625
6626/// Lift the lone `LispError::Compile { form: "unquote-splice", message:
6627/// "`,@` may only appear inside a list" }` triple — the substitute path's
6628/// top-level `,@X` rejection — behind ONE named primitive. Sibling of
6629/// `non_symbol_unquote_target` and `unbound_template_var`: those helpers
6630/// fire when the slot inside a `,X` / `,@X` is malformed (non-symbol or
6631/// unbound symbol); this helper fires when the `,@X` form itself is
6632/// ill-positioned (no containing list to flatten into). Together the three
6633/// close every distinct typed-entry template-gate failure mode for the
6634/// no-evaluator template language: each is a structural variant of
6635/// `LispError`, not a `Compile`-shaped substring.
6636///
6637/// `inner` is the offending `Sexp` projected through `Display` so the
6638/// operator sees the literal value they wrote — `xs`, `(list 1 2)`, `5` —
6639/// instead of just the bare "may only appear inside a list" verdict. The
6640/// helper takes `&Sexp` (parallel to `non_symbol_unquote_target`) and
6641/// projects through `to_string()` at the variant boundary; the `prefix:
6642/// &'static str` slot is implicit (always `,@`) and absent from the variant
6643/// itself, parallel to how `OddKwargs { dangling }` names ONE failure mode
6644/// without a syntactic-marker slot.
6645///
6646/// Used by both the substitute path (top-level `,@X` body) AND the bytecode
6647/// path's `compile_template` gate (top-level `,@X` body — closing the prior
6648/// silent-divergence where the bytecode interpreter's outermost stack frame
6649/// absorbed the splice). After this lift `,@-outside-list` is rejected on
6650/// both paths with ONE structural variant — the typed-entry template gate
6651/// is fully structural across both expansion strategies.
6652///
6653/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6654/// emission sites (substitute + compile_template) for one failure mode is
6655/// past the three-times rule once the structural shape is named. THEORY.md
6656/// §V.1 — knowable platform; the structural variant exposes `got` as a
6657/// first-class field so authoring tools (LSP, REPL, `tatara-check`) bind to
6658/// the data shape instead of substring-parsing the rendered diagnostic.
6659/// THEORY.md §II.1 invariant 1 — typed entry; a `,@X` at a position with no
6660/// containing list is exactly the failure mode the typed-entry gate exists
6661/// to reject. THEORY.md §II.1 invariant 2 — free middle; both expansion
6662/// paths now reject the same set of templates, so a macro that registers
6663/// successfully has the same expansion behavior under either strategy.
6664fn splice_outside_list(inner: &Sexp) -> LispError {
6665 LispError::SpliceOutsideList {
6666 got: inner.witness(),
6667 }
6668}
6669
6670/// Lift the two inline `LispError::Compile { form: format!("call to
6671/// {macro_name}"), message: format!("missing required arg: {name}") }`
6672/// triples — `bind_args` (substitute path) AND `apply_compiled` (bytecode
6673/// path) — behind ONE named primitive. Sibling of the typed-entry kwargs
6674/// `MissingKwarg { key }` lift: that variant fires when a `(<head> :key
6675/// value …)` kwargs form omits a required keyword; this variant fires when
6676/// a `(<macroname> a b …)` call omits a required positional param. Together
6677/// they close every distinct typed-entry missing-required surface in the
6678/// substrate — kwargs-gate AND macro-call-gate now share a single
6679/// structural-variant idiom.
6680///
6681/// Same single emission shape across both expansion strategies — before
6682/// this lift the same failure mode emitted byte-identical
6683/// `LispError::Compile { … }` triples at TWO call sites; after this lift
6684/// both sites share ONE structural variant. Two strategies that picked
6685/// different code paths now emit the same structural variant for the same
6686/// failure mode (THEORY.md §II.1 invariant 2 — free middle: which strategy
6687/// you picked must not change which inputs you reject OR how the rejection
6688/// is shaped). Same posture as `splice_outside_list`'s path-uniform
6689/// rejection across substitute + compile_template.
6690///
6691/// `macro_name` and `name` are `&str` borrows from the call-site / param
6692/// list; the variant's owned `String`s are formed at the boundary so
6693/// `LispError` stays lifetime-free.
6694///
6695/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6696/// inline copies of one shape is past the three-times-rule trigger once
6697/// the structural variant is named (the test count gives this the
6698/// fail-before-pass-after edge). THEORY.md §V.1 — knowable platform; the
6699/// structural variant exposes `macro_name` / `param` as first-class
6700/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the data
6701/// shape instead of substring-parsing the rendered diagnostic. THEORY.md
6702/// §II.1 invariant 1 — typed entry; a macro call with too few args is
6703/// exactly the failure mode the typed-entry gate exists to reject.
6704fn missing_macro_arg(macro_name: &str, param: &str) -> LispError {
6705 LispError::MissingMacroArg {
6706 macro_name: macro_name.to_string(),
6707 param: param.to_string(),
6708 }
6709}
6710
6711/// Mirror at the call-site of `missing_macro_arg`: that helper fires when
6712/// the macro CALL supplies TOO FEW args for the required arity (a required
6713/// slot has no arg); this helper fires when the macro CALL supplies TOO
6714/// MANY args for a rest-less param list (the surplus has nowhere to bind).
6715/// Together they close the typed-entry macro-call-gate's positional-arity
6716/// surface in both directions; together with the definition-site
6717/// `RestParamTrailingTokens` (lifted by the prior-run typed-promotion
6718/// lineage at the parse_params boundary), every distinct way a macro
6719/// definition + call pair can MISCOUNT args is now a named structural
6720/// rejection.
6721///
6722/// `expected` is the rest-less binder's fixed maximum arity
6723/// (`required.len() + optional.len()`); `got` is the actual call-site arg
6724/// count. Both are surfaced at the variant boundary so authoring tools
6725/// (REPL, LSP, `tatara-check`) name the "you supplied {got} args but the
6726/// macro takes at most {expected}" quick-fix from one structural projection
6727/// rather than re-deriving either count from the source. `macro_name` is
6728/// `&str` borrowed from the call-site; the variant's owned `String` is
6729/// formed at the boundary so `LispError` stays lifetime-free — same posture
6730/// as `missing_macro_arg`.
6731///
6732/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6733/// rest-less surplus-args gate is a SINGLE-OWNER named rejection, not a
6734/// silent truncation re-asserted at every consumer that walks the bound
6735/// values. THEORY.md §V.1 — knowable platform; the structural variant
6736/// exposes `macro_name` / `expected` / `got` as first-class fields so
6737/// authoring tools bind to the data shape instead of substring-parsing
6738/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a
6739/// macro call with too many args (and no `&rest` slot to absorb them) is
6740/// exactly the failure mode the typed-entry gate exists to reject —
6741/// silently dropping `args[expected..]` is structurally indistinguishable
6742/// from honoring them, the asymmetry this gate closes. THEORY.md §II.1
6743/// invariant 2 — free middle; both expansion strategies route through the
6744/// SHARED `MacroParams::bind`, so the new rejection lands once and the
6745/// substitute + bytecode paths inherit it unable to drift.
6746fn too_many_macro_args(macro_name: &str, expected: usize, got: usize) -> LispError {
6747 LispError::TooManyMacroArgs {
6748 macro_name: macro_name.to_string(),
6749 expected,
6750 got,
6751 }
6752}
6753
6754/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6755/// "expected symbol" }` triple in `parse_params` behind ONE named
6756/// primitive. Sibling of `missing_macro_arg`: that helper fires when the
6757/// macro CALL is malformed (call-site missing a positional arg); this
6758/// helper fires when the macro DEFINITION is malformed (definition-site
6759/// has a non-symbol where a param name should be). Together they open
6760/// the defmacro-syntax-gate / macro-call-gate split — call-site
6761/// rejections vs. definition-site rejections — each as its own
6762/// structural-variant family on `LispError`.
6763///
6764/// `position` is the loop index inside `parse_params`, i.e. the 0-based
6765/// index of the offending element within the param list (`(defmacro f
6766/// (a 5 b) …)` — position 1 is the literal `5`); naming it lets an LSP
6767/// quick-fix point at the exact list element instead of the whole
6768/// param list. `got` is the offending `Sexp` projected through
6769/// `Display` so the operator sees the literal value they wrote
6770/// (`5`, `:foo`, `(nested)`) at the variant boundary; the helper takes
6771/// `&Sexp` (parallel to `non_symbol_unquote_target` and
6772/// `splice_outside_list`) and projects through `to_string()` so the
6773/// variant stays lifetime-free.
6774///
6775/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6776/// inline copy still earns a named primitive once the structural shape
6777/// is named (the test count gives this the fail-before-pass-after edge,
6778/// parallel to how `OddKwargs` was lifted from a single site for the
6779/// structural-completeness payoff). THEORY.md §V.1 — knowable platform;
6780/// the structural variant exposes `position` / `got` as first-class
6781/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the
6782/// data shape instead of substring-parsing the rendered diagnostic.
6783/// THEORY.md §II.1 invariant 1 — typed entry; a non-symbol element
6784/// inside a defmacro param list is exactly the failure mode the
6785/// typed-entry gate exists to reject — and it must reject DEFINITIONS
6786/// as readily as it rejects CALLS.
6787fn non_symbol_param(position: usize, got: &Sexp) -> LispError {
6788 LispError::NonSymbolParam {
6789 position,
6790 got: got.witness(),
6791 }
6792}
6793
6794/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6795/// "&rest needs a name" }` triple in `parse_params` behind ONE named
6796/// primitive. Sibling of `non_symbol_param`: that helper fires when a
6797/// NON-`&rest` element at a param position isn't a symbol; this helper
6798/// fires specifically on the post-`&rest` follower slot, where the
6799/// failure mode bifurcates into "missing entirely" (`got = None`) vs.
6800/// "present but not a symbol" (`got = Some(...)`). Together, the two
6801/// helpers close the `parse_params` walker — every distinct failure
6802/// mode the walker can emit is now a structural variant of `LispError`,
6803/// not a `Compile`-shaped substring.
6804///
6805/// `rest_position` is the loop index inside `parse_params` at which
6806/// the `&rest` marker was matched, i.e. the 0-based index of `&rest`
6807/// within the param list (`(defmacro f (a &rest 5) …)` — rest_position
6808/// 1 is `&rest`, the offender follows at 2); naming the marker
6809/// position lets an LSP quick-fix point at the `&rest` form itself
6810/// rather than at the next list element. `got` is `Option<&Sexp>`
6811/// because the follower slot bifurcates: `None` when the marker was
6812/// the param list's last element (no follower at all), `Some(sexp)`
6813/// when a follower exists but isn't a symbol; the helper projects
6814/// through `to_string()` at the variant boundary so the variant stays
6815/// lifetime-free.
6816///
6817/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6818/// inline copy still earns a named primitive once the structural shape
6819/// is named (the test count gives this the fail-before-pass-after
6820/// edge, parallel to how `non_symbol_param` was lifted from a single
6821/// site for the structural-completeness payoff). THEORY.md §V.1 —
6822/// knowable platform; the structural variant exposes `rest_position` /
6823/// `got` as first-class fields so authoring tools (LSP, REPL,
6824/// `tatara-check`) bind to the data shape instead of substring-parsing
6825/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6826/// a `&rest` marker followed by no name (or by a non-symbol) is
6827/// exactly the failure mode the typed-entry gate exists to reject —
6828/// and the gate must reject DEFINITIONS as readily as it rejects
6829/// CALLS.
6830fn rest_param_missing_name(rest_position: usize, got: Option<&Sexp>) -> LispError {
6831 LispError::RestParamMissingName {
6832 rest_position,
6833 got: got.map(Sexp::witness),
6834 }
6835}
6836
6837/// The third and final `parse_params` definition-site rejection — a
6838/// `&rest <name>` followed by further tokens. Sibling of `non_symbol_param`
6839/// (a param slot that isn't a symbol) and `rest_param_missing_name` (the
6840/// post-`&rest` follower is missing or malformed): this helper fires once
6841/// the rest name is bound and the walker finds the param list does not end
6842/// there. The `&rest` name absorbs every remaining call arg, so it is
6843/// structurally the LAST param a list can name; trailing tokens are
6844/// unrepresentable in `MacroParams` and were previously dropped silently.
6845///
6846/// `rest_position` is the loop index of the `&rest` marker (parallel to
6847/// `rest_param_missing_name`); `trailing` is the non-empty token run after
6848/// the bound rest name — the helper records its length and the typed
6849/// witness of its first element. The caller guarantees `trailing` is
6850/// non-empty (it is only built when `list[i + 2..].first()` is `Some`), so
6851/// `trailing[0]` does not panic.
6852///
6853/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6854/// unrepresentable"; a param list with tokens after `&rest <name>` is
6855/// nonsense `MacroParams` cannot hold, so the gate must REJECT it rather
6856/// than truncate to the representable prefix. THEORY.md §II.1 invariant 1 —
6857/// typed entry; the gate rejects malformed DEFINITIONS as readily as
6858/// malformed calls. THEORY.md §VI.1 — generation over composition; this
6859/// closes the `parse_params` walker's last uncovered failure mode, making
6860/// the sibling docs' "every distinct failure mode is a structural variant"
6861/// claim finally true.
6862fn rest_param_trailing_tokens(rest_position: usize, trailing: &[Sexp]) -> LispError {
6863 LispError::RestParamTrailingTokens {
6864 rest_position,
6865 extra: trailing.len(),
6866 first: trailing[0].witness(),
6867 }
6868}
6869
6870/// A `&optional` marker appeared a SECOND time in one param list —
6871/// `(defmacro f (a &optional b &optional c) …)`. The lambda-list has exactly
6872/// ONE optional section (between the required run and the rest); a second
6873/// `&optional` is nonsense `MacroParams` cannot hold (its `optional` field is
6874/// one flat run, not a sequence of sections). Without this gate the parser
6875/// would otherwise treat the second `&optional` as an optional param literally
6876/// NAMED `&optional`, binding call args to a marker symbol — exactly the kind
6877/// of silent misalignment the typed shape exists to forbid.
6878///
6879/// Sibling of `rest_param_trailing_tokens` (the rest-section ordering gate):
6880/// both reject a param list whose marker structure the canonical lambda-list
6881/// ordering cannot represent. `first_position` is the loop index of the
6882/// first `&optional`, `second_position` the second — naming both lets an LSP
6883/// quick-fix point at the redundant marker to delete.
6884///
6885/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6886/// unrepresentable"; a param list with two `&optional` sections is nonsense
6887/// `MacroParams` cannot hold, so the gate must REJECT rather than bind args
6888/// to a marker symbol. THEORY.md §II.1 invariant 1 — typed entry; the gate
6889/// rejects malformed DEFINITIONS as readily as malformed calls.
6890fn optional_marker_repeated(first_position: usize, second_position: usize) -> LispError {
6891 LispError::OptionalMarkerRepeated {
6892 first_position,
6893 second_position,
6894 }
6895}
6896
6897/// An `&optional` section entry that's a `Sexp::List` did NOT match the only
6898/// admissible shape `(NAME DEFAULT)` — exactly two elements with a symbol
6899/// head. This helper builds the structural rejection from the loop position,
6900/// the offending list form (projected through `crate::domain::sexp_witness`
6901/// to carry both `SexpShape::List` and the literal display), and the typed
6902/// `OptionalParamMalformedReason` naming which of the four malformed shapes
6903/// fired (empty list / missing default / extra elements / non-symbol name).
6904///
6905/// Sibling of `optional_marker_repeated` (the `&optional`-section marker
6906/// gate) and `non_symbol_param` (the bare-symbol gate): the three together
6907/// close every distinct typed-entry rejection the optional section can
6908/// emit. The bare-symbol form `&optional x` is still routed through
6909/// `non_symbol_param`'s sibling acceptance path; the list form `&optional
6910/// (x default)` is admitted iff this gate accepts the spec.
6911///
6912/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6913/// unrepresentable"; an `&optional` list spec of any other shape is
6914/// nonsense `MacroParams` cannot hold, so the gate must REJECT rather than
6915/// bind args to a marker symbol or drop the extras silently. THEORY.md
6916/// §II.1 invariant 1 — typed entry; a malformed default-form spec is
6917/// exactly the failure mode the typed-entry gate exists to reject — and
6918/// the gate must reject DEFINITIONS as readily as it rejects CALLS.
6919fn optional_param_malformed(
6920 position: usize,
6921 got: &Sexp,
6922 reason: crate::error::OptionalParamMalformedReason,
6923) -> LispError {
6924 LispError::OptionalParamMalformed {
6925 position,
6926 got: got.witness(),
6927 reason,
6928 }
6929}
6930
6931/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6932/// "(defmacro name (params) body) required" }` triple in
6933/// `macro_def_from` behind ONE named primitive. Sibling of
6934/// `non_symbol_param` and `rest_param_missing_name`: those helpers
6935/// fire INSIDE `parse_params`, AFTER the arity gate has passed; this
6936/// helper fires AT the arity gate itself, BEFORE name / params / body
6937/// validation can run. Together the three close `macro_def_from`'s
6938/// outermost rejection chain — every distinct failure mode the gate
6939/// can emit at the top level becomes a structural variant of
6940/// `LispError`, not a `Compile`-shaped substring.
6941///
6942/// `head` is `MacroDefHead` (the typed closed-set enum), having been
6943/// projected through `MacroDefHead::from_keyword` at the top of
6944/// `macro_def_from`. The helper threads `head` straight into the
6945/// variant's typed `head: MacroDefHead` slot — no `&'static str`
6946/// projection at the helper boundary; the projection through
6947/// `MacroDefHead::keyword()` happens at Display rendering time via
6948/// `MacroDefHead`'s Display impl inside the variant's `#[error(...)]`
6949/// annotation. Same posture as how
6950/// `compiler_spec.rs::compiler_spec_io_err` threads
6951/// `CompilerSpecIoStage` straight into
6952/// `LispError::CompilerSpecIo.stage`. `arity` is `usize` (the length
6953/// of the form including the head element).
6954///
6955/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6956/// inline copy still earns a named primitive once the structural
6957/// shape is named (the test count gives this the fail-before/pass-
6958/// after edge, parallel to how `non_symbol_param` and
6959/// `rest_param_missing_name` were lifted from a single site for the
6960/// structural-completeness payoff). THEORY.md §V.1 — knowable
6961/// platform; the structural variant exposes `head` / `arity` as
6962/// first-class fields so authoring tools (LSP, REPL, `tatara-check`)
6963/// bind to the data shape instead of substring-parsing the rendered
6964/// diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a defmacro
6965/// form with too few elements is exactly the failure mode the typed-
6966/// entry gate exists to reject — and the gate must reject
6967/// DEFINITIONS as readily as it rejects CALLS. THEORY.md §II.1
6968/// invariant 2 — free middle; the arity gate fires inside
6969/// `macro_def_from` BEFORE either expansion strategy runs, so both
6970/// `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
6971/// (substitute) reject the SAME malformed defmacro at the SAME gate.
6972fn defmacro_arity(head: MacroDefHead, arity: usize) -> LispError {
6973 LispError::DefmacroArity { head, arity }
6974}
6975
6976/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6977/// "expected name symbol" }` triple in `macro_def_from` behind ONE
6978/// named primitive. Sibling of `defmacro_arity`, `non_symbol_param`,
6979/// and `rest_param_missing_name`: those helpers fire at the OUTERMOST
6980/// arity gate (`defmacro_arity`) or INSIDE `parse_params`
6981/// (`non_symbol_param`, `rest_param_missing_name`); this helper fires
6982/// AFTER the arity gate has passed but BEFORE `parse_params` runs —
6983/// at the second of three `macro_def_from` rejection points
6984/// (arity → name-symbol → param-list → parse_params).
6985///
6986/// Walking a malformed `(defmacro …)` from the outside in, the gate
6987/// fires:
6988/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
6989/// elements (`(defmacro)`, `(defmacro f)`).
6990/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't a
6991/// symbol (`(defmacro 5 () body)`, `(defmacro :foo () body)`).
6992/// 3. The `expected param list` gate (NEXT LIFT) if list[2] isn't a
6993/// list (`(defmacro f x body)`).
6994/// 4. Inside `parse_params`: `non_symbol_param` and
6995/// `rest_param_missing_name`.
6996///
6997/// After this lift step 2 is structural; the only remaining
6998/// `Compile`-shaped site in `macro_def_from` is step 3 (`expected
6999/// param list`).
7000///
7001/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7002/// projected through `MacroDefHead::from_keyword` at the top of
7003/// `macro_def_from`. The helper threads `head` straight into the
7004/// variant's typed `head: MacroDefHead` slot — same posture as
7005/// `defmacro_arity` after the typed-slot promotion. `got` is `&Sexp`
7006/// at the call site (a borrow into the form's name slot); the helper
7007/// projects through `crate::domain::sexp_witness` — the typed joint
7008/// projection (`SexpShape` + `Sexp::Display`) — so the variant's
7009/// `got: SexpWitness` slot carries BOTH structural shape AND
7010/// renderable literal across the boundary, parallel to how
7011/// `non_symbol_param` and `non_symbol_unquote_target` project their
7012/// `&Sexp` arguments. The fourth consumer of the typed `SexpWitness`
7013/// primitive on the substrate's Sexp-display-source rejection
7014/// surface.
7015///
7016/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7017/// inline copy still earns a named primitive once the structural
7018/// shape is named (the test count gives this the fail-before/pass-
7019/// after edge, parallel to how `defmacro_arity`, `non_symbol_param`,
7020/// and `rest_param_missing_name` were lifted from a single site for
7021/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7022/// platform; the structural variant exposes `head` / `got` as
7023/// first-class fields so authoring tools (LSP, REPL,
7024/// `tatara-check`) bind to the data shape instead of substring-
7025/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7026/// typed entry; a defmacro form whose name slot isn't a symbol is
7027/// exactly the failure mode the typed-entry gate exists to reject —
7028/// and the gate must reject DEFINITIONS as readily as it rejects
7029/// CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7030/// name-symbol gate fires inside `macro_def_from` BEFORE either
7031/// expansion strategy runs, so both `Expander::new()` (bytecode) and
7032/// `Expander::new_substitute_only()` (substitute) reject the SAME
7033/// malformed defmacro at the SAME gate.
7034fn defmacro_non_symbol_name(head: MacroDefHead, got: &Sexp) -> LispError {
7035 LispError::DefmacroNonSymbolName {
7036 head,
7037 got: got.witness(),
7038 }
7039}
7040
7041/// Lift the lone `LispError::Compile { form: head.to_string(), message:
7042/// "expected param list" }` triple in `macro_def_from` behind ONE
7043/// named primitive. Sibling of `defmacro_arity`,
7044/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7045/// `rest_param_missing_name`: those helpers fire at the OUTERMOST
7046/// arity gate (`defmacro_arity`), at the second `macro_def_from`
7047/// rejection point (`defmacro_non_symbol_name`), or INSIDE
7048/// `parse_params` (`non_symbol_param`, `rest_param_missing_name`);
7049/// this helper fires AFTER both the arity gate AND the name-symbol
7050/// gate have passed but BEFORE `parse_params` runs — at the third
7051/// of three `macro_def_from` rejection points
7052/// (arity → name-symbol → param-list → parse_params).
7053///
7054/// Walking a malformed `(defmacro …)` from the outside in, the gate
7055/// fires:
7056/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
7057/// elements (`(defmacro)`, `(defmacro f)`).
7058/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't
7059/// a symbol (`(defmacro 5 () body)`).
7060/// 3. `defmacro_non_list_params(head, &list[2])` if list[2] isn't
7061/// a list (`(defmacro f x body)`, `(defmacro f 5 body)`).
7062/// 4. Inside `parse_params`: `non_symbol_param` and
7063/// `rest_param_missing_name`.
7064///
7065/// After this lift step 3 is structural; every inline
7066/// `LispError::Compile { … }` triple in `macro_def_from` has been
7067/// lifted to a structural variant — the entire `macro_def_from`
7068/// rejection chain is structurally typed for failure modes.
7069///
7070/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7071/// projected through `MacroDefHead::from_keyword` at the top of
7072/// `macro_def_from`. The helper threads `head` straight into the
7073/// variant's typed `head: MacroDefHead` slot — same posture as
7074/// `defmacro_arity` and `defmacro_non_symbol_name` after the
7075/// typed-slot promotion. `got` is `&Sexp` at the call site (a
7076/// borrow into the form's param-list slot); the helper projects
7077/// through `crate::domain::sexp_witness(_)` — the typed joint
7078/// primitive that pairs the offending `Sexp`'s `SexpShape` with its
7079/// `Sexp::Display` projection in ONE owned `SexpWitness` value, so
7080/// authoring tools bind to both the structural shape AND the rendered
7081/// literal across the variant slot. Same posture as `non_symbol_param`,
7082/// `non_symbol_unquote_target`, `splice_outside_list`, and
7083/// `defmacro_non_symbol_name`'s helpers after the typed-witness
7084/// promotion of their `got` slots.
7085///
7086/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7087/// inline copy still earns a named primitive once the structural
7088/// shape is named (the test count gives this the fail-before/pass-
7089/// after edge, parallel to how `defmacro_arity`,
7090/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7091/// `rest_param_missing_name` were lifted from a single site for
7092/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7093/// platform; the structural variant exposes `head` / `got` as
7094/// first-class fields so authoring tools (LSP, REPL,
7095/// `tatara-check`) bind to the data shape instead of substring-
7096/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7097/// typed entry; a defmacro form whose param-list slot isn't a list
7098/// is exactly the failure mode the typed-entry gate exists to
7099/// reject — and the gate must reject DEFINITIONS as readily as it
7100/// rejects CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7101/// param-list gate fires inside `macro_def_from` BEFORE either
7102/// expansion strategy runs, so both `Expander::new()` (bytecode)
7103/// and `Expander::new_substitute_only()` (substitute) reject the
7104/// SAME malformed defmacro at the SAME gate.
7105fn defmacro_non_list_params(head: MacroDefHead, got: &Sexp) -> LispError {
7106 LispError::DefmacroNonListParams {
7107 head,
7108 got: got.witness(),
7109 }
7110}
7111
7112/// Project a `bindings: &HashMap<String, Sexp>` into the `&[&str]` candidate
7113/// set `crate::domain::suggest` wants. Cold path — only allocated when an
7114/// `,name` / `,@name` substitution misses, i.e. when we're already on the
7115/// diagnostic side of the substitute walker.
7116fn bound_names(bindings: &HashMap<String, Sexp>) -> Vec<&str> {
7117 bindings.keys().map(String::as_str).collect()
7118}
7119
7120#[cfg(test)]
7121mod tests {
7122 use super::*;
7123 use crate::reader::read;
7124
7125 fn parse(src: &str) -> Sexp {
7126 read(src).unwrap().into_iter().next().unwrap()
7127 }
7128
7129 #[test]
7130 fn identity_macro() {
7131 let mut e = Expander::new();
7132 let forms = read("(defmacro id (x) `,x) (id 42)").unwrap();
7133 let out = e.expand_program(forms).unwrap();
7134 assert_eq!(out.len(), 1);
7135 assert_eq!(out[0], Sexp::int(42));
7136 }
7137
7138 #[test]
7139 fn wrap_macro_duplicates_arg() {
7140 let mut e = Expander::new();
7141 let forms = read("(defmacro wrap (x) `(list ,x ,x)) (wrap hello)").unwrap();
7142 let out = e.expand_program(forms).unwrap();
7143 assert_eq!(out[0], parse("(list hello hello)"));
7144 }
7145
7146 #[test]
7147 fn rest_param_splices_with_at() {
7148 let mut e = Expander::new();
7149 let forms = read("(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)").unwrap();
7150 let out = e.expand_program(forms).unwrap();
7151 assert_eq!(out[0], parse("(foo a b c)"));
7152 }
7153
7154 #[test]
7155 fn nested_macro_expansion() {
7156 let mut e = Expander::new();
7157 let forms = read(
7158 "(defmacro twice (x) `(list ,x ,x))
7159 (defmacro quad (x) `(twice ,x))
7160 (quad hey)",
7161 )
7162 .unwrap();
7163 let out = e.expand_program(forms).unwrap();
7164 assert_eq!(out[0], parse("(list hey hey)"));
7165 }
7166
7167 #[test]
7168 fn unbound_unquote_errors() {
7169 let mut e = Expander::new();
7170 let forms = read("(defmacro bad (x) `(list ,y)) (bad 1)").unwrap();
7171 assert!(e.expand_program(forms).is_err());
7172 }
7173
7174 #[test]
7175 fn missing_required_arg_errors() {
7176 let mut e = Expander::new();
7177 let forms = read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap();
7178 assert!(e.expand_program(forms).is_err());
7179 }
7180
7181 #[test]
7182 fn defpoint_template_treated_as_defmacro() {
7183 let mut e = Expander::new();
7184 let forms = read(
7185 "(defpoint-template obs (name) `(defpoint ,name :class (Gate Observability)))
7186 (obs grafana)",
7187 )
7188 .unwrap();
7189 let out = e.expand_program(forms).unwrap();
7190 assert_eq!(
7191 out[0],
7192 parse("(defpoint grafana :class (Gate Observability))")
7193 );
7194 }
7195
7196 #[test]
7197 fn defcheck_treated_as_defmacro() {
7198 let mut e = Expander::new();
7199 let forms = read(
7200 "(defcheck pair (a b) `(do (yaml-parses ,a) (yaml-parses ,b)))
7201 (pair \"x.yaml\" \"y.yaml\")",
7202 )
7203 .unwrap();
7204 let out = e.expand_program(forms).unwrap();
7205 assert_eq!(
7206 out[0],
7207 parse("(do (yaml-parses \"x.yaml\") (yaml-parses \"y.yaml\"))")
7208 );
7209 }
7210
7211 #[test]
7212 fn empty_rest_splices_nothing() {
7213 let mut e = Expander::new();
7214 let forms = read("(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)").unwrap();
7215 let out = e.expand_program(forms).unwrap();
7216 assert_eq!(out[0], parse("(list 1)"));
7217 }
7218
7219 #[test]
7220 fn macro_expanded_inside_list() {
7221 // A macro call nested in a list position also expands.
7222 let mut e = Expander::new();
7223 let forms = read("(defmacro two () `(list 1 2)) (outer (two))").unwrap();
7224 let out = e.expand_program(forms).unwrap();
7225 assert_eq!(out[0], parse("(outer (list 1 2))"));
7226 }
7227
7228 // ── Compiled-template bytecode equivalence + speedup ──────────────
7229
7230 #[test]
7231 fn compiled_template_matches_substitute_path() {
7232 // Same program, two expanders with different strategies — outputs must agree.
7233 let src = "
7234 (defmacro wrap (x) `(list ,x ,x))
7235 (defmacro call (f &rest args) `(,f ,@args))
7236 (defmacro twice (x) `(list ,x ,x))
7237 (defmacro quad (x) `(twice ,x))
7238 (wrap hello)
7239 (call foo a b c)
7240 (quad hey)
7241 (outer (wrap deep))
7242 ";
7243 let forms = read(src).unwrap();
7244 let mut fast = Expander::new();
7245 let mut slow = Expander::new_substitute_only();
7246 let out_fast = fast.expand_program(forms.clone()).unwrap();
7247 let out_slow = slow.expand_program(forms).unwrap();
7248 assert_eq!(out_fast, out_slow);
7249 }
7250
7251 #[test]
7252 fn literal_subtree_compiles_to_single_literal_op() {
7253 // Macro body where only one leaf is a substitution — the rest of the
7254 // template is literal, so the compiler should prune large chunks to
7255 // a single Literal op.
7256 let def = MacroDef {
7257 name: "label".into(),
7258 params: MacroParams {
7259 required: vec!["x".into()],
7260 optional: Vec::new(),
7261 rest: None,
7262 },
7263 body: Sexp::Quasiquote(Box::new(parse(
7264 "(observed (at timestamp) (in region) (value ,x) (tags (one two three)))",
7265 ))),
7266 };
7267 let compiled = compile_template(&def).expect("compile");
7268 // The template is ONE list. After compile:
7269 // BeginList,
7270 // Literal((observed (at timestamp) (in region))), // wait — `observed` is a list too
7271 // ...
7272 // EndList
7273 // Point is: many subtrees should be single Literals. We simply count
7274 // that the op stream is SHORTER than the full Sexp size.
7275 let ops_count = compiled.ops.len();
7276 assert!(
7277 ops_count < 15,
7278 "expected pruned op stream, got {ops_count} ops: {:?}",
7279 compiled.ops
7280 );
7281 }
7282
7283 /// Three-way benchmark: substitute-only vs bytecode-no-cache vs bytecode-cache.
7284 /// Each path must produce identical output; the cache should show a real,
7285 /// visible speedup because the workload (10 000 calls across 10 unique
7286 /// (macro, args) pairs = 99.9% cache hit rate) is cache-friendly.
7287 #[test]
7288 fn expansion_layers_agree_on_output_and_cache_wins() {
7289 use std::time::Instant;
7290
7291 let macros = "
7292 (defmacro m1 (a b) `(list ,a ,b))
7293 (defmacro m2 (x) `(if ,x true false))
7294 (defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
7295 (defmacro m4 (f &rest args) `(,f ,@args))
7296 (defmacro m5 (x) `(and ,x (not (not ,x))))
7297 (defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
7298 (defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
7299 (defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
7300 (defmacro m9 (x) `(loop (times 10) (eval ,x)))
7301 (defmacro m10 (f g &rest args) `(,f (,g ,@args)))
7302 ";
7303 let mut call_src = String::with_capacity(80_000);
7304 for i in 0..10_000 {
7305 match i % 10 {
7306 0 => call_src.push_str("(m1 a b)\n"),
7307 1 => call_src.push_str("(m2 true)\n"),
7308 2 => call_src.push_str("(m3 x y z)\n"),
7309 3 => call_src.push_str("(m4 f a b c d e)\n"),
7310 4 => call_src.push_str("(m5 y)\n"),
7311 5 => call_src.push_str("(m6 a b)\n"),
7312 6 => call_src.push_str("(m7 answer)\n"),
7313 7 => call_src.push_str("(m8 p q)\n"),
7314 8 => call_src.push_str("(m9 body)\n"),
7315 _ => call_src.push_str("(m10 f g a b c)\n"),
7316 }
7317 }
7318 let all_src = format!("{macros}\n{call_src}");
7319 let forms = read(&all_src).unwrap();
7320
7321 let mut subst = Expander::new_substitute_only();
7322 let t0 = Instant::now();
7323 let out_subst = subst.expand_program(forms.clone()).unwrap();
7324 let t_subst = t0.elapsed();
7325
7326 let mut byte_no_cache = Expander::new_bytecode_no_cache();
7327 let t0 = Instant::now();
7328 let out_byte = byte_no_cache.expand_program(forms.clone()).unwrap();
7329 let t_byte = t0.elapsed();
7330
7331 let mut byte_cache = Expander::new();
7332 let t0 = Instant::now();
7333 let out_cached = byte_cache.expand_program(forms).unwrap();
7334 let t_cached = t0.elapsed();
7335
7336 // Rigorous: all three paths agree.
7337 assert_eq!(out_subst, out_byte);
7338 assert_eq!(out_subst, out_cached);
7339
7340 // Cache captured the 10 unique (macro, args) pairs (plus some inner
7341 // expansions — macros that expand into calls to other macros).
7342 let cache_size = byte_cache.cache_size();
7343 assert!(
7344 (10..=50).contains(&cache_size),
7345 "expected ~10 unique cache entries, got {cache_size}"
7346 );
7347
7348 eprintln!(
7349 "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs ===\n\
7350 substitute only : {t_subst:?}\n\
7351 bytecode no cache : {t_byte:?}\n\
7352 bytecode + cache : {t_cached:?} (cache_size={cache_size})\n\
7353 cache speedup vs subst : {:.2}×\n\
7354 cache speedup vs byte : {:.2}×\n",
7355 t_subst.as_secs_f64() / t_cached.as_secs_f64(),
7356 t_byte.as_secs_f64() / t_cached.as_secs_f64(),
7357 );
7358
7359 // The cache MUST win against both baselines for this cache-friendly
7360 // workload. Using a 1.5× threshold so the test is stable across hosts.
7361 assert!(
7362 t_cached < t_subst,
7363 "cache should beat substitute ({t_cached:?} vs {t_subst:?})"
7364 );
7365 assert!(
7366 t_cached < t_byte,
7367 "cache should beat bytecode-no-cache ({t_cached:?} vs {t_byte:?})"
7368 );
7369 }
7370
7371 #[test]
7372 fn cache_respects_arg_changes() {
7373 // Cache must not return stale results when args differ.
7374 let src = "
7375 (defmacro wrap (x) `(list ,x ,x))
7376 (wrap a)
7377 (wrap b)
7378 (wrap a) ;; same as first — cached hit
7379 ";
7380 let mut e = Expander::new();
7381 let out = e.expand_program(read(src).unwrap()).unwrap();
7382 assert_eq!(out.len(), 3);
7383 assert_eq!(out[0], parse("(list a a)"));
7384 assert_eq!(out[1], parse("(list b b)"));
7385 assert_eq!(out[2], parse("(list a a)"));
7386 // Two distinct args → 2 cache entries.
7387 assert_eq!(e.cache_size(), 2);
7388 }
7389
7390 #[test]
7391 fn clear_cache_empties_memo() {
7392 let mut e = Expander::new();
7393 let out = e
7394 .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7395 .unwrap();
7396 assert_eq!(out.len(), 2);
7397 assert_eq!(e.cache_size(), 2);
7398 e.clear_cache();
7399 assert_eq!(e.cache_size(), 0);
7400 }
7401
7402 // ── Unbound template-var: structural variant + did-you-mean hint ──
7403
7404 /// Helper for the unbound-template-var tests — pins the variant shape
7405 /// and carries any error context up to the assert site for legibility.
7406 fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7407 match err {
7408 LispError::UnboundTemplateVar { prefix, name, hint } => {
7409 (*prefix, name.as_str(), hint.as_deref())
7410 }
7411 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7412 }
7413 }
7414
7415 #[test]
7416 fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7417 // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7418 // Path: compile_node Unquote (the bytecode-template compile, default
7419 // expander).
7420 let mut e = Expander::new();
7421 let err = e
7422 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7423 .expect_err("unbound template var must error");
7424 let (prefix, name, hint) = unbound_var(&err);
7425 assert_eq!(prefix, UnquoteForm::Unquote);
7426 assert_eq!(name, "xs");
7427 assert_eq!(hint, Some("x"));
7428 }
7429
7430 #[test]
7431 fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7432 // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7433 // hints `,@args`. Path: compile_node UnquoteSplice.
7434 let mut e = Expander::new();
7435 let err = e
7436 .expand_program(
7437 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7438 )
7439 .expect_err("unbound splice must error");
7440 let (prefix, name, hint) = unbound_var(&err);
7441 assert_eq!(prefix, UnquoteForm::Splice);
7442 assert_eq!(name, "argz");
7443 assert_eq!(hint, Some("args"));
7444 }
7445
7446 #[test]
7447 fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7448 // Same shape but routed through the substitute-only expander — proves
7449 // the substitute path emits the same variant as the bytecode path.
7450 let mut e = Expander::new_substitute_only();
7451 let err = e
7452 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7453 .expect_err("substitute unbound must error");
7454 let (prefix, name, hint) = unbound_var(&err);
7455 assert_eq!(prefix, UnquoteForm::Unquote);
7456 assert_eq!(name, "xs");
7457 assert_eq!(hint, Some("x"));
7458 }
7459
7460 #[test]
7461 fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7462 // The substitute path's UnquoteSplice branch fires for splices that
7463 // appear inside a list during the recursive walk. `,@argz` against
7464 // `[args]` hints `,@args`.
7465 let mut e = Expander::new_substitute_only();
7466 let err = e
7467 .expand_program(
7468 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7469 )
7470 .expect_err("substitute splice unbound must error");
7471 let (prefix, name, hint) = unbound_var(&err);
7472 assert_eq!(prefix, UnquoteForm::Splice);
7473 assert_eq!(name, "argz");
7474 assert_eq!(hint, Some("args"));
7475 }
7476
7477 #[test]
7478 fn unbound_template_var_omits_hint_when_no_close_match() {
7479 // `,wholly-unrelated` against `[x]` — far past the bound, so no
7480 // hint. Negative control: a wrong hint is worse than no hint, so
7481 // the slot must stay empty when the substrate isn't confident.
7482 let mut e = Expander::new();
7483 let err = e
7484 .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7485 .expect_err("unrelated unbound must error");
7486 let (prefix, name, hint) = unbound_var(&err);
7487 assert_eq!(prefix, UnquoteForm::Unquote);
7488 assert_eq!(name, "wholly-unrelated");
7489 assert_eq!(hint, None);
7490 }
7491
7492 #[test]
7493 fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7494 // End-to-end through the Display impl — pins the rendered diagnostic
7495 // a downstream tool sees today (REPL, tatara-check). Hint stays
7496 // additive: the legacy `"unbound"` substring still appears, so any
7497 // assertion that pattern-matches on it keeps passing.
7498 let mut e = Expander::new();
7499 let err = e
7500 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7501 .expect_err("unbound must error");
7502 let msg = format!("{err}");
7503 assert!(
7504 msg.contains("did you mean ,x?"),
7505 "expected hint suffix in message, got: {msg}"
7506 );
7507 assert!(
7508 msg.contains("unbound"),
7509 "expected legacy `unbound` substring in message, got: {msg}"
7510 );
7511 assert!(
7512 msg.contains(",xs"),
7513 "expected the offending form in message, got: {msg}"
7514 );
7515 }
7516
7517 #[test]
7518 fn unbound_template_var_position_is_none_today() {
7519 // Negative control for the future-spans move: until `Sexp` carries
7520 // source positions, `position()` returns `None` for this variant.
7521 let mut e = Expander::new();
7522 let err = e
7523 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7524 .expect_err("unbound must error");
7525 assert_eq!(err.position(), None);
7526 }
7527
7528 // ── Non-symbol unquote target: structural variant ─────────────────
7529
7530 /// Helper for the non-symbol-unquote-target tests — pins the variant
7531 /// shape and carries any error context up to the assert site for
7532 /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7533 /// returns the `display` projection of the typed `SexpWitness` so the
7534 /// existing call sites stay byte-for-byte comparable to the legacy
7535 /// `got: String` shape.
7536 fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7537 match err {
7538 LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7539 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7540 }
7541 }
7542
7543 #[test]
7544 fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7545 // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7546 // compile_node Unquote (the bytecode-template compile, default
7547 // expander). Pins variant identity AND prefix AND the offending
7548 // literal so a regression that re-inlines the legacy
7549 // `LispError::Compile` shape fails-loudly here.
7550 let mut e = Expander::new();
7551 let err = e
7552 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7553 .expect_err("non-symbol unquote target must error");
7554 let (prefix, got) = non_symbol_target(&err);
7555 assert_eq!(prefix, UnquoteForm::Unquote);
7556 assert_eq!(got, "(list 1 2)");
7557 }
7558
7559 #[test]
7560 fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7561 // `,@5` — the inner is an int atom, not a symbol. Path:
7562 // compile_node UnquoteSplice. The integer literal round-trips
7563 // through the variant's `got` slot via `Sexp::Display`.
7564 let mut e = Expander::new();
7565 let err = e
7566 .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7567 .expect_err("non-symbol splice target must error");
7568 let (prefix, got) = non_symbol_target(&err);
7569 assert_eq!(prefix, UnquoteForm::Splice);
7570 assert_eq!(got, "5");
7571 }
7572
7573 #[test]
7574 fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7575 // Same shape as the bytecode path but routed through the
7576 // substitute-only expander — proves the substitute path emits the
7577 // same variant as the compile_node path. Pins that the lift is
7578 // path-uniform.
7579 let mut e = Expander::new_substitute_only();
7580 let err = e
7581 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7582 .expect_err("substitute non-symbol target must error");
7583 let (prefix, got) = non_symbol_target(&err);
7584 assert_eq!(prefix, UnquoteForm::Unquote);
7585 assert_eq!(got, "(list 1 2)");
7586 }
7587
7588 #[test]
7589 fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7590 // The substitute path's UnquoteSplice-inside-list branch fires for
7591 // splices that appear inside a list during the recursive walk.
7592 // `,@(list 1 2)` inside the body — the inner is a literal list, not
7593 // a symbol — emits the same variant as the compile_node path.
7594 let mut e = Expander::new_substitute_only();
7595 let err = e
7596 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7597 .expect_err("substitute non-symbol splice must error");
7598 let (prefix, got) = non_symbol_target(&err);
7599 assert_eq!(prefix, UnquoteForm::Splice);
7600 assert_eq!(got, "(list 1 2)");
7601 }
7602
7603 #[test]
7604 fn non_symbol_unquote_target_position_is_none_today() {
7605 // Negative control for the future-spans move: until `Sexp` carries
7606 // source positions, `position()` returns `None` for this variant.
7607 // A future run that gives `Sexp` source spans adds `pos:
7608 // Option<usize>` to ONE place; this test gives that change a
7609 // deliberate fail-before/pass-after delta.
7610 let mut e = Expander::new();
7611 let err = e
7612 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7613 .expect_err("non-symbol target must error");
7614 assert_eq!(err.position(), None);
7615 }
7616
7617 // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7618 //
7619 // The `unquote_target_symbol(inner, form)?` primitive lifts the
7620 // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7621 // form, inner))?` pattern that previously appeared at four call
7622 // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7623 // + `substitute` list-inner UnquoteSplice) behind ONE named
7624 // primitive. The tests below pin: (a) the Ok-arm borrows the
7625 // symbol name from `inner` for both UnquoteForm variants; (b) the
7626 // Err-arm routes through `non_symbol_unquote_target` and emits the
7627 // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7628 // the typed `SexpWitness` (joint shape + display identity) for the
7629 // closed set of reachable non-symbol shapes (int / keyword / list /
7630 // nil); (c) the helper is path-uniform — the same Ok / Err
7631 // contracts hold regardless of which call site invokes it. A
7632 // regression that re-inlines the gate-1 projection at any of the
7633 // four call sites can no longer drift independent of the others —
7634 // the helper IS the gate.
7635
7636 #[test]
7637 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7638 // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7639 // under `UnquoteForm::Unquote` projects through `as_symbol()`
7640 // to the borrowed `&str`. The returned slice's lifetime is
7641 // tied to `inner` so the caller can feed it directly into
7642 // `params.iter().position(...)` (`compile_node`) or
7643 // `bindings.get(...)` (`substitute`) without an intermediate
7644 // allocation. Fail-before/pass-after: this assert is meaningless
7645 // pre-lift because the helper does not exist; post-lift it
7646 // pins the typed gate-1 contract at the named primitive.
7647 let inner = Sexp::symbol("xs");
7648 let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7649 .expect("symbol inner must project to Ok");
7650 assert_eq!(name, "xs");
7651 }
7652
7653 #[test]
7654 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7655 // Sibling positive control: `UnquoteForm::Splice` shares the
7656 // gate-1 contract with `Unquote`. The helper is path-uniform
7657 // across both syntactic markers — a regression that bifurcates
7658 // the two arms (e.g., accepting non-symbols for `,@X` but not
7659 // `,X`) fails-loudly here. Pins that the closed-set
7660 // `UnquoteForm` enum's two variants share ONE projection
7661 // posture across the gate-1 boundary.
7662 let inner = Sexp::symbol("rest");
7663 let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7664 .expect("symbol inner must project to Ok under Splice");
7665 assert_eq!(name, "rest");
7666 }
7667
7668 #[test]
7669 fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7670 // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7671 // NOT a symbol — the gate-1 projection fires and routes through
7672 // `non_symbol_unquote_target` to the structural
7673 // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7674 // identity AND the typed `SexpWitness` joint identity (shape +
7675 // display literal): a regression that drops the witness shape
7676 // or display fails-loudly here.
7677 let inner = Sexp::int(5);
7678 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7679 .expect_err("int inner must error at gate-1");
7680 match err {
7681 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7682 assert_eq!(prefix, UnquoteForm::Unquote);
7683 assert_eq!(got.shape, crate::error::SexpShape::Int);
7684 assert_eq!(got.display, "5");
7685 }
7686 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7687 }
7688 }
7689
7690 #[test]
7691 fn unquote_target_symbol_rejects_list_inner_under_splice() {
7692 // Sibling negative control: `inner = (list 1 2)` is a list, not
7693 // a symbol — the gate-1 projection fires AND routes through
7694 // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7695 // both the variant identity AND the typed witness's joint
7696 // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7697 // future shape drift fails-loudly. Sibling of the Int / Unquote
7698 // pin: closes the gate-1 contract across the closed-set
7699 // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7700 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7701 let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7702 .expect_err("list inner must error at gate-1");
7703 match err {
7704 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7705 assert_eq!(prefix, UnquoteForm::Splice);
7706 assert_eq!(got.shape, crate::error::SexpShape::List);
7707 assert_eq!(got.display, "(list 1 2)");
7708 }
7709 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7710 }
7711 }
7712
7713 #[test]
7714 fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7715 // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7716 // The gate-1 projection rejects keywords AS WELL as ints and
7717 // lists — closes the closed-set of "non-symbol shapes the gate
7718 // rejects" across one more reachable variant. The typed witness
7719 // carries `SexpShape::Keyword` + display `:foo` jointly so
7720 // authoring tools (REPL, LSP) bind on the structural shape
7721 // directly.
7722 let inner = Sexp::keyword("foo");
7723 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7724 .expect_err("keyword inner must error at gate-1");
7725 match err {
7726 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7727 assert_eq!(prefix, UnquoteForm::Unquote);
7728 assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7729 assert_eq!(got.display, ":foo");
7730 }
7731 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7732 }
7733 }
7734
7735 #[test]
7736 fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7737 // Path-uniformity pin: end-to-end through ALL FOUR call sites
7738 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7739 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7740 // every non-symbol unquote target now routes through the SAME
7741 // `unquote_target_symbol(inner, form)?` helper. The four
7742 // end-to-end expansions below all reject with the SAME variant
7743 // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7744 // path-uniform rejection contract `non_symbol_unquote_target`'s
7745 // prior lift established (and that drove the bytecode-vs-
7746 // substitute reunification in 0e9c… and successors). A
7747 // regression that re-inlines the gate-1 projection at one of
7748 // the four sites can drift the four call sites independent of
7749 // each other — this test would catch that drift.
7750 let cases: &[(&str, UnquoteForm)] = &[
7751 // compile_node Unquote (bytecode-path)
7752 ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7753 // compile_node UnquoteSplice (bytecode-path)
7754 ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7755 ];
7756 for (src, expected_form) in cases {
7757 let mut e = Expander::new();
7758 let err = e
7759 .expand_program(read(src).unwrap())
7760 .expect_err("non-symbol unquote target must error end-to-end");
7761 match err {
7762 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7763 assert_eq!(prefix, *expected_form, "for src: {src}");
7764 }
7765 other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7766 }
7767 }
7768 // substitute Unquote (substitute-only path) — sibling pin to
7769 // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7770 let mut e_subst = Expander::new_substitute_only();
7771 let err = e_subst
7772 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7773 .expect_err("substitute Unquote must error end-to-end");
7774 assert!(
7775 matches!(
7776 err,
7777 LispError::NonSymbolUnquoteTarget {
7778 prefix: UnquoteForm::Unquote,
7779 ..
7780 }
7781 ),
7782 "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7783 );
7784 // substitute list-inner UnquoteSplice (substitute-only path) —
7785 // sibling pin to
7786 // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7787 let mut e_subst2 = Expander::new_substitute_only();
7788 let err = e_subst2
7789 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7790 .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7791 assert!(
7792 matches!(
7793 err,
7794 LispError::NonSymbolUnquoteTarget {
7795 prefix: UnquoteForm::Splice,
7796 ..
7797 }
7798 ),
7799 "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7800 );
7801 }
7802
7803 // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7804 // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7805 // (bytecode-template compile path) and `resolve_binding`
7806 // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7807 // unbound_template_var(FORM, name, candidates))` projections at
7808 // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7809 // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7810 // projection under both `UnquoteForm` variants — the helper returns
7811 // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7812 // for in-scope names; (b) Err-arm projection routes through
7813 // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7814 // variant with the correct `prefix` AND the suggest-driven `hint`;
7815 // (c) the helpers are path-uniform — both compile-path arms share
7816 // ONE `resolve_param_index`; both substitute-path arms share ONE
7817 // `resolve_binding`. A regression that re-inlines the gate-2
7818 // projection at any of the four call sites can no longer drift
7819 // independent of the others — the two helpers ARE the gate.
7820
7821 #[test]
7822 fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7823 // Positive control for the Ok-arm: `name = "x"` against
7824 // `params = ["a", "x", "rest"]` projects through
7825 // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7826 // the helper unwraps to `Ok(1)`. The returned index feeds
7827 // directly into `TemplateOp::Subst(idx)` at the compile site.
7828 let params = ["a", "x", "rest"];
7829 let idx = resolve_param_index("x", ¶ms, UnquoteForm::Unquote)
7830 .expect("bound name must project to Ok at gate-2");
7831 assert_eq!(idx, 1);
7832 }
7833
7834 #[test]
7835 fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7836 // Sibling positive control: `UnquoteForm::Splice` shares the
7837 // gate-2 contract with `Unquote`. The helper is path-uniform
7838 // across both syntactic markers on the compile path — a
7839 // regression that bifurcates the two arms fails-loudly here.
7840 let params = ["a", "x", "rest"];
7841 let idx = resolve_param_index("rest", ¶ms, UnquoteForm::Splice)
7842 .expect("bound name must project to Ok at gate-2 under Splice");
7843 assert_eq!(idx, 2);
7844 }
7845
7846 #[test]
7847 fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7848 // Negative control for the Err-arm: `name = "xs"` against
7849 // `params = ["x"]` — distance 1, bound 1 — routes through
7850 // `unbound_template_var` to the structural
7851 // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7852 // Pin the variant identity AND the prefix AND the suggest-driven
7853 // hint: a regression that drops the suggestion fails-loudly here.
7854 let params = ["x"];
7855 let err = resolve_param_index("xs", ¶ms, UnquoteForm::Unquote)
7856 .expect_err("unbound name must error at gate-2");
7857 match err {
7858 LispError::UnboundTemplateVar { prefix, name, hint } => {
7859 assert_eq!(prefix, UnquoteForm::Unquote);
7860 assert_eq!(name, "xs");
7861 assert_eq!(hint.as_deref(), Some("x"));
7862 }
7863 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7864 }
7865 }
7866
7867 #[test]
7868 fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7869 // Sibling negative control: `name = "wholly-unrelated"` against
7870 // `params = ["x"]` — past the bounded edit distance, so no hint.
7871 // Pin that the suggest-driven hint stays empty under Splice when
7872 // the substrate isn't confident — a wrong hint is worse than no
7873 // hint. Closes the closed-set product of {hint, no-hint} ×
7874 // {Unquote, Splice} on the compile-path gate-2.
7875 let params = ["x"];
7876 let err = resolve_param_index("wholly-unrelated", ¶ms, UnquoteForm::Splice)
7877 .expect_err("unrelated unbound must error at gate-2");
7878 match err {
7879 LispError::UnboundTemplateVar { prefix, name, hint } => {
7880 assert_eq!(prefix, UnquoteForm::Splice);
7881 assert_eq!(name, "wholly-unrelated");
7882 assert_eq!(hint, None);
7883 }
7884 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7885 }
7886 }
7887
7888 #[test]
7889 fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7890 // Positive control for the substitute-path Ok-arm: `name = "x"`
7891 // against a bindings map `{x: 42, y: "hi"}` projects through
7892 // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7893 // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7894 // `&Sexp` borrows from the bindings map — the top-level
7895 // `Sexp::Unquote(_)` substitute caller adds a single
7896 // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7897 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7898 bindings.insert("x".to_string(), Sexp::int(42));
7899 bindings.insert("y".to_string(), Sexp::string("hi"));
7900 let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7901 .expect("bound name must project to Ok at gate-2 (substitute)");
7902 assert_eq!(val, &Sexp::int(42));
7903 }
7904
7905 #[test]
7906 fn resolve_binding_returns_value_for_bound_name_under_splice() {
7907 // Sibling positive control: `UnquoteForm::Splice` shares the
7908 // gate-2 contract with `Unquote` on the substitute path too.
7909 // The bound value is a `Sexp::List` because the splice arm's
7910 // caller match expression expects `Sexp::List(items)` — but
7911 // the helper itself doesn't inspect the value's shape; it
7912 // just hands back the borrow. A regression that gate-checks
7913 // the value's shape inside `resolve_binding` (instead of at
7914 // the caller match arm) fails-loudly here.
7915 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7916 bindings.insert(
7917 "args".to_string(),
7918 Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
7919 );
7920 let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
7921 .expect("bound name must project to Ok at gate-2 under Splice");
7922 assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
7923 }
7924
7925 #[test]
7926 fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
7927 // Negative control for the substitute-path Err-arm: `name =
7928 // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
7929 // routes through `unbound_template_var` to the structural
7930 // `LispError::UnboundTemplateVar` variant with `hint =
7931 // Some("x")`. The candidate set is drawn from
7932 // `bound_names(bindings)` — the live bindings' keys, never a
7933 // stale snapshot.
7934 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7935 bindings.insert("x".to_string(), Sexp::int(1));
7936 let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
7937 .expect_err("unbound name must error at gate-2 (substitute)");
7938 match err {
7939 LispError::UnboundTemplateVar { prefix, name, hint } => {
7940 assert_eq!(prefix, UnquoteForm::Unquote);
7941 assert_eq!(name, "xs");
7942 assert_eq!(hint.as_deref(), Some("x"));
7943 }
7944 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7945 }
7946 }
7947
7948 #[test]
7949 fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
7950 // Sibling negative control on the substitute path: past-bound
7951 // distance → no hint. Closes the closed-set product of
7952 // {hint, no-hint} × {Unquote, Splice} on the substitute-path
7953 // gate-2.
7954 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7955 bindings.insert("args".to_string(), Sexp::Nil);
7956 let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
7957 .expect_err("unrelated unbound must error at gate-2");
7958 match err {
7959 LispError::UnboundTemplateVar { prefix, name, hint } => {
7960 assert_eq!(prefix, UnquoteForm::Splice);
7961 assert_eq!(name, "wholly-unrelated");
7962 assert_eq!(hint, None);
7963 }
7964 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7965 }
7966 }
7967
7968 #[test]
7969 fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
7970 // Path-uniformity pin: end-to-end through ALL FOUR call sites
7971 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7972 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7973 // every unbound-template-var rejection now routes through one
7974 // of the TWO `resolve_param_index` / `resolve_binding` helpers
7975 // — `Expander::new()` runs the compile path, so its two arms
7976 // share `resolve_param_index`; `Expander::new_substitute_only()`
7977 // runs the substitute path, so its two arms share
7978 // `resolve_binding`. The four end-to-end expansions below all
7979 // reject with the SAME variant (`UnboundTemplateVar`) with the
7980 // expected `prefix` — pins that the lift preserves the
7981 // path-uniform rejection contract `unbound_template_var`'s
7982 // prior naming established. A regression that re-inlines the
7983 // gate-2 projection at one of the four sites can drift the
7984 // four call sites independent of each other — this test would
7985 // catch that drift.
7986 struct Case {
7987 src: &'static str,
7988 expander: fn() -> Expander,
7989 expected_form: UnquoteForm,
7990 }
7991 let cases: &[Case] = &[
7992 // compile_node Unquote (bytecode path) — uses resolve_param_index
7993 Case {
7994 src: "(defmacro w (x) `(list ,xs)) (w 1)",
7995 expander: Expander::new,
7996 expected_form: UnquoteForm::Unquote,
7997 },
7998 // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
7999 Case {
8000 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8001 expander: Expander::new,
8002 expected_form: UnquoteForm::Splice,
8003 },
8004 // substitute Unquote (substitute-only path) — uses resolve_binding
8005 Case {
8006 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8007 expander: Expander::new_substitute_only,
8008 expected_form: UnquoteForm::Unquote,
8009 },
8010 // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8011 Case {
8012 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8013 expander: Expander::new_substitute_only,
8014 expected_form: UnquoteForm::Splice,
8015 },
8016 ];
8017 for case in cases {
8018 let mut e = (case.expander)();
8019 let err = e
8020 .expand_program(read(case.src).unwrap())
8021 .expect_err("unbound template var must error end-to-end");
8022 match err {
8023 LispError::UnboundTemplateVar { prefix, .. } => {
8024 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8025 }
8026 other => panic!(
8027 "expected UnboundTemplateVar for {}, got: {other:?}",
8028 case.src
8029 ),
8030 }
8031 }
8032 }
8033
8034 // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8035
8036 #[test]
8037 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8038 // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8039 // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8040 // returns its index. The combined helper returns the gate-2
8041 // result directly — pins that gate-1's Ok-arm threads into
8042 // gate-2's input without intermediate state.
8043 let inner = Sexp::symbol("x");
8044 let params = ["x", "y"];
8045 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8046 .expect("symbol-inner bound at index 0 must resolve");
8047 assert_eq!(idx, 0);
8048 }
8049
8050 #[test]
8051 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8052 // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8053 // marker doesn't change the projection — only the rejection
8054 // path's `prefix` slot.
8055 let inner = Sexp::symbol("args");
8056 let params = ["f", "args"];
8057 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8058 .expect("symbol-inner bound at index 1 must resolve");
8059 assert_eq!(idx, 1);
8060 }
8061
8062 #[test]
8063 fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8064 // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8065 // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8066 // BEFORE gate-2's param lookup runs. Pins that the composition's
8067 // sequencing is gate-1-then-gate-2: a regression that runs
8068 // gate-2 first would attempt to look up "(list 1 2)" as a param
8069 // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8070 // 2)", ... }` — a confusing diagnostic that would substring-grep
8071 // "unbound" instead of "expected symbol". This test pins the
8072 // structural floor: a non-symbol inner is rejected as a non-
8073 // symbol, never re-treated as a bound-name lookup key.
8074 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8075 let params = ["x"];
8076 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8077 .expect_err("non-symbol inner must reject at gate-1");
8078 match err {
8079 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8080 assert_eq!(prefix, UnquoteForm::Unquote);
8081 assert_eq!(got.display, "(list 1 2)");
8082 }
8083 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8084 }
8085 }
8086
8087 #[test]
8088 fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8089 // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8090 // symbol (gate-1 passes) but the name isn't in `params`, so
8091 // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8092 // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8093 // that the marker `prefix` is threaded into gate-2's rejection
8094 // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8095 // at the composition boundary would fail this Splice-marker
8096 // assertion).
8097 let inner = Sexp::symbol("missing");
8098 let params = ["x", "y"];
8099 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8100 .expect_err("unbound symbol must reject at gate-2");
8101 match err {
8102 LispError::UnboundTemplateVar { prefix, name, .. } => {
8103 assert_eq!(prefix, UnquoteForm::Splice);
8104 assert_eq!(name, "missing");
8105 }
8106 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8107 }
8108 }
8109
8110 #[test]
8111 fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8112 // Substitute-path sibling of `resolve_unquote_in_params_returns_
8113 // index_for_symbol_inner_under_unquote`. The combined helper
8114 // composes gate-1 (project inner to symbol) THEN gate-2 (look
8115 // up name in bindings). The returned `&Sexp` borrows from
8116 // `bindings` so the list-inner caller threads it straight into
8117 // the splice-expansion match without an intermediate allocation.
8118 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8119 bindings.insert("v".to_string(), Sexp::int(42));
8120 let inner = Sexp::symbol("v");
8121 let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8122 .expect("symbol-inner bound to 42 must resolve");
8123 assert_eq!(val, &Sexp::int(42));
8124 }
8125
8126 #[test]
8127 fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8128 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8129 // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8130 // sequencing on the substitute path: a non-symbol inner is
8131 // rejected as a non-symbol BEFORE the bindings map is consulted.
8132 let bindings: HashMap<String, Sexp> = HashMap::new();
8133 let inner = Sexp::int(5);
8134 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8135 .expect_err("non-symbol inner must reject at gate-1");
8136 match err {
8137 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8138 assert_eq!(prefix, UnquoteForm::Splice);
8139 assert_eq!(got.display, "5");
8140 }
8141 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8142 }
8143 }
8144
8145 #[test]
8146 fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8147 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8148 // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8149 // substitute path with the marker threaded into the rejection's
8150 // `prefix` slot.
8151 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8152 bindings.insert("known".to_string(), Sexp::Nil);
8153 let inner = Sexp::symbol("missing");
8154 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8155 .expect_err("unbound symbol must reject at gate-2");
8156 match err {
8157 LispError::UnboundTemplateVar { prefix, name, .. } => {
8158 assert_eq!(prefix, UnquoteForm::Unquote);
8159 assert_eq!(name, "missing");
8160 }
8161 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8162 }
8163 }
8164
8165 #[test]
8166 fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8167 // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8168 // composition (compile_node Unquote, compile_node UnquoteSplice,
8169 // substitute Unquote, substitute list-inner UnquoteSplice) now
8170 // share TWO composed primitives — `resolve_unquote_in_params`
8171 // on the bytecode path, `resolve_unquote_in_bindings` on the
8172 // substitute path — and ALL four reject gate-1 failures (non-
8173 // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8174 // variant carrying the expected `prefix` slot. Before the lift,
8175 // each site threaded `form` twice through two helper calls; this
8176 // test pins that the lift preserves the gate's rejection-shape
8177 // identity across all four sites for a non-symbol inner — i.e.
8178 // gate-1 fires identically across both expansion strategies.
8179 struct Case {
8180 src: &'static str,
8181 expander: fn() -> Expander,
8182 expected_form: UnquoteForm,
8183 }
8184 let cases: &[Case] = &[
8185 Case {
8186 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8187 expander: Expander::new,
8188 expected_form: UnquoteForm::Unquote,
8189 },
8190 Case {
8191 src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8192 expander: Expander::new,
8193 expected_form: UnquoteForm::Splice,
8194 },
8195 Case {
8196 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8197 expander: Expander::new_substitute_only,
8198 expected_form: UnquoteForm::Unquote,
8199 },
8200 Case {
8201 src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8202 expander: Expander::new_substitute_only,
8203 expected_form: UnquoteForm::Splice,
8204 },
8205 ];
8206 for case in cases {
8207 let mut e = (case.expander)();
8208 let err = e
8209 .expand_program(read(case.src).unwrap())
8210 .expect_err("non-symbol inner must error end-to-end");
8211 match err {
8212 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8213 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8214 }
8215 other => panic!(
8216 "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8217 case.src
8218 ),
8219 }
8220 }
8221 }
8222
8223 // ── Splice outside list: structural variant + path-uniform rejection ─
8224
8225 /// Helper for the splice-outside-list tests — pins the variant shape
8226 /// and carries the offending `got` field up to the assert site for
8227 /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8228 fn splice_outside_list_got(err: &LispError) -> &str {
8229 match err {
8230 LispError::SpliceOutsideList { got } => got.display.as_str(),
8231 other => panic!("expected SpliceOutsideList, got: {other:?}"),
8232 }
8233 }
8234
8235 #[test]
8236 fn splice_outside_list_in_substitute_emits_structural_variant() {
8237 // `,@xs` at the body's top level — there is no containing list to
8238 // splice into. Path: substitute (the `Expander::new_substitute_only`
8239 // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8240 // identity AND the offending inner so a regression that re-inlines
8241 // the legacy `LispError::Compile` shape fails-loudly here.
8242 let mut e = Expander::new_substitute_only();
8243 let err = e
8244 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8245 .expect_err("splice outside list must error");
8246 assert_eq!(splice_outside_list_got(&err), "xs");
8247 }
8248
8249 #[test]
8250 fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8251 // `,@(list 1 2)` at the body's top level — the inner is a literal
8252 // list, not a symbol. The structural variant carries the inner's
8253 // Sexp::Display projection so the operator sees the literal value
8254 // they wrote in the parenthetical.
8255 let mut e = Expander::new_substitute_only();
8256 let err = e
8257 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8258 .expect_err("splice outside list must error");
8259 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8260 }
8261
8262 #[test]
8263 fn splice_outside_list_in_compile_template_emits_structural_variant() {
8264 // The bytecode path's `compile_template` gate now rejects top-level
8265 // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8266 // where the bytecode interpreter's outermost stack frame absorbed
8267 // the splice. Pins that the bytecode path emits the SAME structural
8268 // variant the substitute path emits — `,@-outside-list` is rejected
8269 // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8270 // → `compile_template` gate.
8271 let mut e = Expander::new();
8272 let err = e
8273 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8274 .expect_err("compile-template splice outside list must error");
8275 assert_eq!(splice_outside_list_got(&err), "xs");
8276 }
8277
8278 #[test]
8279 fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8280 // Same shape as the substitute test but routed through the bytecode
8281 // path's `compile_template` gate. Proves the gate fires on a
8282 // non-symbol inner too — the slot's contents are irrelevant; only
8283 // the syntactic position matters.
8284 let mut e = Expander::new();
8285 let err = e
8286 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8287 .expect_err("compile-template splice outside list must error");
8288 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8289 }
8290
8291 #[test]
8292 fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8293 // Path-uniform rejection: the SAME source emits the SAME structural
8294 // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8295 // strategies. Before the `compile_template` gate, the bytecode path
8296 // silently produced a list while the substitute path errored —
8297 // expansion strategy was observable. After the gate, the gate is
8298 // strategy-uniform, so a macro that registers under one strategy
8299 // registers under the other.
8300 let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8301 let mut subst = Expander::new_substitute_only();
8302 let mut bytecode = Expander::new();
8303 let err_subst = subst
8304 .expand_program(read(src).unwrap())
8305 .expect_err("substitute must error");
8306 let err_byte = bytecode
8307 .expand_program(read(src).unwrap())
8308 .expect_err("bytecode must error");
8309 assert_eq!(splice_outside_list_got(&err_subst), "xs");
8310 assert_eq!(splice_outside_list_got(&err_byte), "xs");
8311 }
8312
8313 #[test]
8314 fn splice_outside_list_position_is_none_today() {
8315 // Negative control for the future-spans move: until `Sexp` carries
8316 // source positions, `position()` returns `None` for this variant.
8317 // A future run that gives `Sexp` source spans adds `pos:
8318 // Option<usize>` to ONE place; this test gives that change a
8319 // deliberate fail-before/pass-after delta.
8320 let mut e = Expander::new_substitute_only();
8321 let err = e
8322 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8323 .expect_err("splice outside list must error");
8324 assert_eq!(err.position(), None);
8325 }
8326
8327 #[test]
8328 fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8329 // End-to-end through the Display impl — pins the rendered diagnostic
8330 // a downstream tool sees today (REPL, tatara-check). The legacy
8331 // substring `"\`,@\` may only appear inside a list"` is preserved
8332 // verbatim AND the parenthetical `(got ,@xs)` names the offending
8333 // form; tools that pattern-match on the variant gain structural
8334 // binding to `got`.
8335 let mut e = Expander::new_substitute_only();
8336 let err = e
8337 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8338 .expect_err("splice outside list must error");
8339 let msg = format!("{err}");
8340 assert_eq!(
8341 msg,
8342 "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8343 );
8344 }
8345
8346 #[test]
8347 fn splice_inside_list_still_succeeds_under_both_paths() {
8348 // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8349 // continues to succeed under both paths — the new gate only fires
8350 // when the splice is the entire body. Pins that the gate is scoped
8351 // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8352 // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8353 // bare list-literal whose first symbol would also splice through.
8354 let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8355 let mut subst = Expander::new_substitute_only();
8356 let mut bytecode = Expander::new();
8357 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8358 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8359 assert_eq!(out_subst, out_byte);
8360 assert_eq!(out_subst[0], parse("(outer 1 2)"));
8361 }
8362
8363 // ── splice_value_into: the shared splice-result coercion ──
8364
8365 #[test]
8366 fn splice_value_into_list_flattens_elements_into_builder() {
8367 // The canonical splice arm: a bound LIST contributes its elements
8368 // in order, preserving anything already in the builder.
8369 let mut builder = vec![Sexp::symbol("outer")];
8370 splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8371 assert_eq!(
8372 builder,
8373 vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8374 );
8375 }
8376
8377 #[test]
8378 fn splice_value_into_nil_is_a_noop() {
8379 // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8380 // the builder is unchanged.
8381 let mut builder = vec![Sexp::symbol("outer")];
8382 splice_value_into(&mut builder, &Sexp::Nil);
8383 assert_eq!(builder, vec![Sexp::symbol("outer")]);
8384 }
8385
8386 #[test]
8387 fn splice_value_into_scalar_pushes_single_element() {
8388 // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8389 // splices as exactly one element. Pins the "free middle" coercion
8390 // every scalar shape (int, keyword, …) shares.
8391 let mut builder = vec![Sexp::symbol("outer")];
8392 splice_value_into(&mut builder, &Sexp::int(5));
8393 assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8394 let mut other: Vec<Sexp> = vec![];
8395 splice_value_into(&mut other, &Sexp::keyword("k"));
8396 assert_eq!(other, vec![Sexp::keyword("k")]);
8397 }
8398
8399 #[test]
8400 fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8401 // The point of the lift: the NON-list splice arms (scalar → single
8402 // element, nil → nothing) coerce identically under the substitute
8403 // AND bytecode strategies. Before the coercion was lifted to ONE
8404 // primitive these two arms lived inline at two sites; this test
8405 // pins that the two strategies cannot drift on the non-list arms.
8406 let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8407 let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8408 for src in [scalar, empty] {
8409 let mut subst = Expander::new_substitute_only();
8410 let mut bytecode = Expander::new();
8411 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8412 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8413 assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8414 }
8415 let mut e = Expander::new();
8416 assert_eq!(
8417 e.expand_program(read(scalar).unwrap()).unwrap()[0],
8418 parse("(outer 5)")
8419 );
8420 let mut e2 = Expander::new();
8421 assert_eq!(
8422 e2.expand_program(read(empty).unwrap()).unwrap()[0],
8423 parse("(outer)")
8424 );
8425 }
8426
8427 // ── Missing macro arg: structural variant + path-uniform rejection ──
8428
8429 /// Helper for the missing-macro-arg tests — pins the variant shape
8430 /// and carries the failing macro's name + un-bound param up to the
8431 /// assert site for legibility. Sibling of `unbound_var`,
8432 /// `non_symbol_target`, and `splice_outside_list_got`.
8433 fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8434 match err {
8435 LispError::MissingMacroArg { macro_name, param } => {
8436 (macro_name.as_str(), param.as_str())
8437 }
8438 other => panic!("expected MissingMacroArg, got: {other:?}"),
8439 }
8440 }
8441
8442 #[test]
8443 fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8444 // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8445 // `apply_compiled` (the bytecode-template path, default expander).
8446 // Pins variant identity AND macro_name AND the un-bound param so a
8447 // regression that re-inlines the legacy `LispError::Compile` shape
8448 // fails-loudly here.
8449 let mut e = Expander::new();
8450 let err = e
8451 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8452 .expect_err("missing required macro arg must error");
8453 let (macro_name, param) = missing_macro_arg_fields(&err);
8454 assert_eq!(macro_name, "need-two");
8455 assert_eq!(param, "b");
8456 }
8457
8458 #[test]
8459 fn missing_macro_arg_in_substitute_emits_structural_variant() {
8460 // Same shape as the bytecode test but routed through the
8461 // substitute-only expander → `bind_args` is the failing site.
8462 // Proves the substitute path emits the SAME structural variant the
8463 // bytecode path emits — `missing required arg` rejection is
8464 // path-uniform across both expansion strategies.
8465 let mut e = Expander::new_substitute_only();
8466 let err = e
8467 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8468 .expect_err("missing required macro arg must error");
8469 let (macro_name, param) = missing_macro_arg_fields(&err);
8470 assert_eq!(macro_name, "need-two");
8471 assert_eq!(param, "b");
8472 }
8473
8474 #[test]
8475 fn missing_macro_arg_first_position_is_named() {
8476 // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8477 // arg. The variant names `a`, not `b` — naming the LEFTMOST
8478 // un-bound param is the shape `bind_args` / `apply_compiled` both
8479 // emit (each iterates positionally and bails on the first missing
8480 // slot). Pins the leftmost-bail contract so a regression that
8481 // names the rightmost (or a surplus) param fails-loudly.
8482 let mut e = Expander::new();
8483 let err = e
8484 .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8485 .expect_err("missing first required arg must error");
8486 let (macro_name, param) = missing_macro_arg_fields(&err);
8487 assert_eq!(macro_name, "f");
8488 assert_eq!(param, "a");
8489 }
8490
8491 #[test]
8492 fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8493 // Path-uniform rejection: the SAME source emits the SAME structural
8494 // variant under both expansion strategies. Negative control for
8495 // the divergence-closing posture: a future refactor that drifts
8496 // either path's rejection shape (or drops one path's rejection
8497 // entirely) fails-loudly here. Sibling of
8498 // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8499 // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8500 // failure mode each.
8501 let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8502 let mut subst = Expander::new_substitute_only();
8503 let mut bytecode = Expander::new();
8504 let err_subst = subst
8505 .expand_program(read(src).unwrap())
8506 .expect_err("substitute must error");
8507 let err_byte = bytecode
8508 .expand_program(read(src).unwrap())
8509 .expect_err("bytecode must error");
8510 assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8511 assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8512 }
8513
8514 #[test]
8515 fn missing_macro_arg_position_is_none_today() {
8516 // Negative control for the future-spans move: until `Sexp` carries
8517 // source positions, `position()` returns `None` for this variant.
8518 // A future run that gives `Sexp` source spans adds `pos:
8519 // Option<usize>` to ONE place; this test gives that change a
8520 // deliberate fail-before/pass-after delta. Parallel to
8521 // `splice_outside_list_position_is_none_today`.
8522 let mut e = Expander::new();
8523 let err = e
8524 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8525 .expect_err("missing required macro arg must error");
8526 assert_eq!(err.position(), None);
8527 }
8528
8529 #[test]
8530 fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8531 // End-to-end through the Display impl — pins the rendered diagnostic
8532 // a downstream tool sees today (REPL, tatara-check). The legacy
8533 // substring `"missing required arg: {param}"` is preserved verbatim
8534 // AND the head clause names the failing macro via `"call to
8535 // {macro_name}"`; tools that pattern-match on the variant gain
8536 // structural binding to `macro_name` / `param`.
8537 let mut e = Expander::new();
8538 let err = e
8539 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8540 .expect_err("missing required macro arg must error");
8541 assert_eq!(
8542 format!("{err}"),
8543 "compile error in call to need-two: missing required arg: b"
8544 );
8545 }
8546
8547 #[test]
8548 fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8549 // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8550 // round-trip through the variant unchanged. Pinning this contract
8551 // means a regression that camelCases or lowercases either side
8552 // fails-loudly here. Parallel to the
8553 // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8554 // assertion for the kwarg-gate's symmetric surface.
8555 let mut e = Expander::new();
8556 let err = e
8557 .expand_program(
8558 read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8559 .unwrap(),
8560 )
8561 .expect_err("missing required macro arg must error");
8562 let (macro_name, param) = missing_macro_arg_fields(&err);
8563 assert_eq!(macro_name, "wrap-twice");
8564 assert_eq!(param, "body");
8565 }
8566
8567 #[test]
8568 fn rest_param_only_macro_with_no_args_still_succeeds() {
8569 // Negative control: a macro whose only param is `&rest` must NOT
8570 // error when called with zero args — the rest-param binds to the
8571 // empty list. The new structural variant fires only on REQUIRED
8572 // params; the `Param::Rest` arm in both `bind_args` and
8573 // `apply_compiled` continues to bind the empty tail. Pins that the
8574 // helper is scoped to required-param failure, not all
8575 // arity-mismatch shapes.
8576 let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8577 let mut subst = Expander::new_substitute_only();
8578 let mut bytecode = Expander::new();
8579 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8580 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8581 assert_eq!(out_subst, out_byte);
8582 assert_eq!(out_subst[0], parse("(list)"));
8583 }
8584
8585 // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8586 //
8587 // A rest-less param list has a FIXED maximum arity equal to
8588 // `required.len() + optional.len()`. Surplus call args have nowhere to
8589 // bind. Before this gate the surplus was silently truncated to the
8590 // slice the binder could consume — the typed-entry macro-call-gate
8591 // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8592 // too-many silently, an asymmetry the definition-side `&rest <name>
8593 // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8594 // After this gate the call-site arity surface is structurally
8595 // complete in both directions; the substitute + bytecode paths share
8596 // `MacroParams::bind`, so both inherit the rejection without drift.
8597
8598 /// Helper for the too-many-args tests — projects to (macro_name,
8599 /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8600 fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8601 match err {
8602 LispError::TooManyMacroArgs {
8603 macro_name,
8604 expected,
8605 got,
8606 } => (macro_name.as_str(), *expected, *got),
8607 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8608 }
8609 }
8610
8611 #[test]
8612 fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8613 // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8614 // nowhere to bind. The rest-less binder rejects via
8615 // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8616 // NOT silently drops `3`. Pins both the variant identity AND the
8617 // structural fields the typed gate exposes for authoring-tool
8618 // quick-fixes ("you supplied 3 args; the macro takes at most
8619 // 2").
8620 let mut e = Expander::new();
8621 let err = e
8622 .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8623 .expect_err("surplus arg on rest-less call must error");
8624 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8625 assert_eq!(macro_name, "f");
8626 assert_eq!(expected, 2);
8627 assert_eq!(got, 3);
8628 }
8629
8630 #[test]
8631 fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8632 // The rest-less binder's fixed maximum arity is `required.len() +
8633 // optional.len()` — the optional section CONTRIBUTES to capacity.
8634 // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8635 // args rejects with `expected: 2` (required + optional, NOT just
8636 // required). Pins the optional-counts-in-capacity contract so a
8637 // regression that omits optionals from the expected calculation
8638 // (and erroneously rejects 2-arg calls) fails-loudly here.
8639 let mut e = Expander::new();
8640 let err = e
8641 .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8642 .expect_err("surplus arg beyond required+optional must error");
8643 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8644 assert_eq!(macro_name, "f");
8645 assert_eq!(expected, 2);
8646 assert_eq!(got, 3);
8647 }
8648
8649 #[test]
8650 fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8651 // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8652 // with `expected: 3`. Pins that the capacity calculation scales
8653 // with optional.len(), not just at-most-one. Mixes a bare
8654 // optional with an optional carrying a default form — both shapes
8655 // contribute identically to capacity (the typed `OptionalParam`
8656 // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8657 let mut e = Expander::new();
8658 let err = e
8659 .expand_program(
8660 read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8661 )
8662 .expect_err("surplus arg beyond required+two-optional must error");
8663 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8664 assert_eq!(macro_name, "f");
8665 assert_eq!(expected, 3);
8666 assert_eq!(got, 4);
8667 }
8668
8669 #[test]
8670 fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8671 // Negative control: a rest-PRESENT param list has no maximum
8672 // arity — the `&rest` slot collects every trailing arg into a
8673 // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8674 // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8675 // `MacroParams.rest` is `None`. Pins the rest-present-path
8676 // remains permissive — a regression that wrongly fires the
8677 // too-many gate for any surplus (including the rest-collecting
8678 // path) would break every `&rest`-using macro.
8679 let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8680 let mut subst = Expander::new_substitute_only();
8681 let mut bytecode = Expander::new();
8682 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8683 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8684 assert_eq!(out_subst, out_byte);
8685 assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8686 }
8687
8688 #[test]
8689 fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8690 // Negative control: the rest-less gate fires STRICTLY when
8691 // `args.len() > expected` — at exact arity the binder accepts.
8692 // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8693 // a=1, b=2 successfully (the optional takes its supplied arg,
8694 // not the default). Pins the boundary condition so a regression
8695 // that flips the comparison to `>=` (rejecting exact-arity
8696 // calls) fails-loudly here.
8697 let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8698 let mut e = Expander::new();
8699 let out = e.expand_program(read(src).unwrap()).unwrap();
8700 assert_eq!(out[0], parse("(list 1 2)"));
8701 }
8702
8703 #[test]
8704 fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8705 // Path-uniform rejection: the SAME source emits the SAME
8706 // structural variant under both expansion strategies. The
8707 // shared `MacroParams::bind` makes the rejection lands once and
8708 // both paths inherit it. Mirror of
8709 // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8710 // both close `THEORY.md §II.1 invariant 2 — free middle` for
8711 // one failure mode each.
8712 let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8713 let mut subst = Expander::new_substitute_only();
8714 let mut bytecode = Expander::new();
8715 let err_subst = subst
8716 .expand_program(read(src).unwrap())
8717 .expect_err("substitute must error");
8718 let err_byte = bytecode
8719 .expand_program(read(src).unwrap())
8720 .expect_err("bytecode must error");
8721 assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8722 assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8723 }
8724
8725 #[test]
8726 fn too_many_macro_args_fires_after_missing_required_priority_held() {
8727 // Priority discipline: the required walk fires
8728 // `MissingMacroArg` BEFORE the rest-less surplus gate is
8729 // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8730 // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8731 // collision). The two failure modes are structurally disjoint:
8732 // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8733 // first-missing-required contract so a regression that swaps
8734 // the two gates' order would emit the wrong variant.
8735 let mut e = Expander::new();
8736 let err = e
8737 .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8738 .expect_err("missing required must error");
8739 assert!(
8740 matches!(err, LispError::MissingMacroArg { .. }),
8741 "expected MissingMacroArg (priority), got: {err:?}"
8742 );
8743 }
8744
8745 #[test]
8746 fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8747 // Degenerate case: a nullary macro `(defmacro f () ...)` has
8748 // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8749 // the gate fires even when the rest-less max-arity is zero —
8750 // i.e. the rejection is structural, not conditional on a
8751 // non-empty required+optional.
8752 let mut e = Expander::new();
8753 let err = e
8754 .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8755 .expect_err("nullary macro called with arg must error");
8756 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8757 assert_eq!(macro_name, "f");
8758 assert_eq!(expected, 0);
8759 assert_eq!(got, 1);
8760 }
8761
8762 #[test]
8763 fn too_many_macro_args_display_renders_legacy_compile_substring() {
8764 // The rendered Display matches the legacy `Compile`-shaped
8765 // diagnostic style — `"compile error in call to {macro_name}:
8766 // too many args: expected at most {expected}, got {got}"` — so
8767 // the existing `"compile error in call to"` substring authoring
8768 // tools' assertions key on stays unchanged. Pins the byte-level
8769 // rendered shape so a regression that drifts the prefix /
8770 // separator / labels fails-loudly here.
8771 let err = LispError::TooManyMacroArgs {
8772 macro_name: "pair".into(),
8773 expected: 2,
8774 got: 5,
8775 };
8776 assert_eq!(
8777 err.to_string(),
8778 "compile error in call to pair: too many args: expected at most 2, got 5"
8779 );
8780 }
8781
8782 #[test]
8783 fn too_many_macro_args_position_is_none_today() {
8784 // Negative control for the future-spans move: until `Sexp`
8785 // carries source positions, `position()` returns `None` for
8786 // this variant. A future run that gives `Sexp` source spans
8787 // adds `pos: Option<usize>` to ONE place; this test gives that
8788 // change a deliberate fail-before/pass-after delta. Parallel to
8789 // `missing_macro_arg_position_is_none_today`.
8790 let err = LispError::TooManyMacroArgs {
8791 macro_name: "pair".into(),
8792 expected: 2,
8793 got: 3,
8794 };
8795 assert_eq!(err.position(), None);
8796 }
8797
8798 /// Helper for the non-symbol-param tests — pins the variant shape and
8799 /// carries the failing position + offending element up to the assert
8800 /// site for legibility. Sibling of `missing_macro_arg_fields`.
8801 fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8802 match err {
8803 LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8804 other => panic!("expected NonSymbolParam, got: {other:?}"),
8805 }
8806 }
8807
8808 #[test]
8809 fn non_symbol_param_at_first_position_emits_structural_variant() {
8810 // `(defmacro f (5) ...)` — the first element of the param list is
8811 // an integer literal, not a symbol. Pins variant identity AND
8812 // that `position` is the loop index inside `parse_params` (0 for
8813 // the first slot) AND that `got` is the offending element via
8814 // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8815 // `LispError::Compile` shape (which named neither the position
8816 // nor the offending element) fails-loudly here.
8817 let mut e = Expander::new();
8818 let err = e
8819 .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8820 .expect_err("non-symbol param must error");
8821 let (position, got) = non_symbol_param_fields(&err);
8822 assert_eq!(position, 0);
8823 assert_eq!(got, "5");
8824 }
8825
8826 #[test]
8827 fn non_symbol_param_at_second_position_emits_structural_variant() {
8828 // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8829 // misfires. Pins that `position` advances with the loop index, so
8830 // an LSP quick-fix that wants to point at "the second element of
8831 // your param list" gains the index as data, no source re-parse
8832 // required.
8833 let mut e = Expander::new();
8834 let err = e
8835 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8836 .expect_err("non-symbol param must error");
8837 let (position, got) = non_symbol_param_fields(&err);
8838 assert_eq!(position, 1);
8839 assert_eq!(got, "5");
8840 }
8841
8842 #[test]
8843 fn non_symbol_param_carries_keyword_value_unchanged() {
8844 // `:k` at a param-list position. `Sexp::Display` for
8845 // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8846 // field round-trips the keyword form unchanged so an LSP that
8847 // surfaces "you wrote `:k` where a symbol was expected" gains
8848 // the literal keyword value as data, no re-parsing required.
8849 let mut e = Expander::new();
8850 let err = e
8851 .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8852 .expect_err("non-symbol param must error");
8853 let (position, got) = non_symbol_param_fields(&err);
8854 assert_eq!(position, 0);
8855 assert_eq!(got, ":k");
8856 }
8857
8858 #[test]
8859 fn non_symbol_param_carries_nested_list_value_unchanged() {
8860 // A nested list at a param-list position. `Sexp::Display` for
8861 // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8862 // `got` field carries the nested form's full Display projection
8863 // unchanged so the operator sees what they wrote.
8864 let mut e = Expander::new();
8865 let err = e
8866 .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8867 .expect_err("non-symbol param must error");
8868 let (position, got) = non_symbol_param_fields(&err);
8869 assert_eq!(position, 0);
8870 assert_eq!(got, "(nested)");
8871 }
8872
8873 #[test]
8874 fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8875 // `defpoint-template` shares `parse_params` with `defmacro` (all
8876 // three head keywords route through `macro_def_from`). Pins that
8877 // the lift fires path-uniformly across the three head keywords
8878 // — `defmacro`, `defpoint-template`, `defcheck` — so the
8879 // structural-completeness floor holds for every defmacro-shaped
8880 // form, not just the one with the `defmacro` head literal.
8881 let mut e = Expander::new();
8882 let err = e
8883 .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8884 .expect_err("non-symbol param must error");
8885 let (position, got) = non_symbol_param_fields(&err);
8886 assert_eq!(position, 0);
8887 assert_eq!(got, "5");
8888 }
8889
8890 #[test]
8891 fn non_symbol_param_in_defcheck_emits_same_variant() {
8892 // Sibling of the defpoint-template test — `defcheck` is the
8893 // third head keyword `macro_def_from` recognizes. All three
8894 // route through the same `parse_params` and now reject
8895 // non-symbol params with the same structural variant.
8896 let mut e = Expander::new();
8897 let err = e
8898 .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8899 .expect_err("non-symbol param must error");
8900 let (position, got) = non_symbol_param_fields(&err);
8901 assert_eq!(position, 1);
8902 assert_eq!(got, "5");
8903 }
8904
8905 #[test]
8906 fn non_symbol_param_position_is_none_today() {
8907 // Negative control for the future-spans move: until `Sexp`
8908 // carries source positions, `position()` on `LispError` returns
8909 // `None` for this variant. A future run that gives `Sexp`
8910 // source spans adds `pos: Option<usize>` to ONE place; this
8911 // test gives that change a deliberate fail-before/pass-after
8912 // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8913 let mut e = Expander::new();
8914 let err = e
8915 .expand_program(read("(defmacro f (5) `(list))").unwrap())
8916 .expect_err("non-symbol param must error");
8917 assert_eq!(err.position(), None);
8918 }
8919
8920 #[test]
8921 fn non_symbol_param_message_renders_legacy_substring_with_position() {
8922 // End-to-end through Display — pins the rendered diagnostic that
8923 // downstream tools (REPL, `tatara-check`) see today. Legacy
8924 // substrings `"defmacro params"` AND `"expected symbol"` are
8925 // preserved verbatim; the appended `at position {position}, got
8926 // {got}` clause is the new structural detail. Tools that
8927 // pattern-match on the variant gain structural binding to
8928 // `position` / `got`.
8929 let mut e = Expander::new();
8930 let err = e
8931 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8932 .expect_err("non-symbol param must error");
8933 assert_eq!(
8934 format!("{err}"),
8935 "compile error in defmacro params: \
8936 expected symbol at position 1, got 5"
8937 );
8938 }
8939
8940 #[test]
8941 fn non_symbol_param_substitute_and_bytecode_paths_agree() {
8942 // Path-uniform rejection: the SAME source emits the SAME
8943 // structural variant under both expansion strategies. The
8944 // defmacro-syntax-gate fires inside `macro_def_from` →
8945 // `parse_params`, BEFORE either strategy's expansion path runs;
8946 // so both `Expander::new()` (bytecode) and
8947 // `Expander::new_substitute_only()` (substitute) reject the
8948 // SAME malformed defmacro at the SAME gate. Sibling of
8949 // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
8950 let src = "(defmacro f (a 5) `(,a))";
8951 let mut subst = Expander::new_substitute_only();
8952 let mut bytecode = Expander::new();
8953 let err_subst = subst
8954 .expand_program(read(src).unwrap())
8955 .expect_err("substitute must error");
8956 let err_byte = bytecode
8957 .expand_program(read(src).unwrap())
8958 .expect_err("bytecode must error");
8959 assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
8960 assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
8961 }
8962
8963 /// Helper for the rest-param-missing-name tests — pins the variant
8964 /// shape and carries the marker position + offending follower (or
8965 /// its absence) up to the assert site for legibility. Sibling of
8966 /// `non_symbol_param_fields`.
8967 fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
8968 match err {
8969 LispError::RestParamMissingName { rest_position, got } => {
8970 (*rest_position, got.as_ref().map(|w| w.display.as_str()))
8971 }
8972 other => panic!("expected RestParamMissingName, got: {other:?}"),
8973 }
8974 }
8975
8976 #[test]
8977 fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
8978 // `(defmacro f (&rest))` — the marker is the only param-list
8979 // element; nothing follows. Pins variant identity AND that
8980 // `rest_position == 0` (the first slot) AND that `got == None`
8981 // (no follower exists). A regression that re-inlines the legacy
8982 // `LispError::Compile` shape (which named neither field) fails-
8983 // loudly here.
8984 let mut e = Expander::new();
8985 let err = e
8986 .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
8987 .expect_err("&rest with no follower must error");
8988 let (rest_position, got) = rest_param_missing_name_fields(&err);
8989 assert_eq!(rest_position, 0);
8990 assert_eq!(got, None);
8991 }
8992
8993 #[test]
8994 fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
8995 // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
8996 // position 1 has no follower at all. Pins that `rest_position`
8997 // advances with the loop index, so an LSP quick-fix that wants to
8998 // point at "your `&rest` at position 1 has no name" gains the
8999 // marker position as data, no source re-parse required.
9000 let mut e = Expander::new();
9001 let err = e
9002 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9003 .expect_err("&rest with no follower must error");
9004 let (rest_position, got) = rest_param_missing_name_fields(&err);
9005 assert_eq!(rest_position, 1);
9006 assert_eq!(got, None);
9007 }
9008
9009 #[test]
9010 fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9011 // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9012 // `5` (an integer literal, not a symbol). Pins that the variant's
9013 // `got` field is `Some` and carries the offending follower's
9014 // `Sexp::Display` projection; the bifurcation between "missing
9015 // entirely" and "present but non-symbol" is in the renderable
9016 // detail, not in what the gate rejects.
9017 let mut e = Expander::new();
9018 let err = e
9019 .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9020 .expect_err("&rest followed by non-symbol must error");
9021 let (rest_position, got) = rest_param_missing_name_fields(&err);
9022 assert_eq!(rest_position, 0);
9023 assert_eq!(got, Some("5"));
9024 }
9025
9026 #[test]
9027 fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9028 // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9029 // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9030 // that the variant's `got` field round-trips the keyword form
9031 // unchanged so an LSP that surfaces "you wrote `:foo` where a
9032 // rest-name was expected" gains the literal keyword value as
9033 // data, no re-parsing required.
9034 let mut e = Expander::new();
9035 let err = e
9036 .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9037 .expect_err("&rest followed by keyword must error");
9038 let (rest_position, got) = rest_param_missing_name_fields(&err);
9039 assert_eq!(rest_position, 1);
9040 assert_eq!(got, Some(":foo"));
9041 }
9042
9043 #[test]
9044 fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9045 // `(defmacro f (&rest (nested)))` — nested-list follower at the
9046 // rest-name slot. `Sexp::Display` for `List(xs)` writes
9047 // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9048 // the nested form's full Display projection unchanged so the
9049 // operator sees what they wrote.
9050 let mut e = Expander::new();
9051 let err = e
9052 .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9053 .expect_err("&rest followed by list must error");
9054 let (rest_position, got) = rest_param_missing_name_fields(&err);
9055 assert_eq!(rest_position, 0);
9056 assert_eq!(got, Some("(nested)"));
9057 }
9058
9059 #[test]
9060 fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9061 // `defpoint-template` shares `parse_params` with `defmacro` (all
9062 // three head keywords route through `macro_def_from`). Pins that
9063 // the lift fires path-uniformly across the three head keywords —
9064 // a regression that handles `defpoint-template`'s param list
9065 // differently from `defmacro`'s would fail-loudly here.
9066 let mut e = Expander::new();
9067 let err = e
9068 .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9069 .expect_err("&rest with no follower must error");
9070 let (rest_position, got) = rest_param_missing_name_fields(&err);
9071 assert_eq!(rest_position, 1);
9072 assert_eq!(got, None);
9073 }
9074
9075 #[test]
9076 fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9077 // Sibling for the `defcheck` head; rounds out the three-head-
9078 // keyword coverage so the lift is path-uniform across
9079 // `defmacro` / `defpoint-template` / `defcheck`. After this
9080 // test the defmacro-syntax-gate rejects `&rest`-without-name
9081 // identically across all three head keywords — the
9082 // typed-entry surface is single-shape across the cluster.
9083 let mut e = Expander::new();
9084 let err = e
9085 .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9086 .expect_err("&rest followed by non-symbol must error");
9087 let (rest_position, got) = rest_param_missing_name_fields(&err);
9088 assert_eq!(rest_position, 0);
9089 assert_eq!(got, Some("5"));
9090 }
9091
9092 #[test]
9093 fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9094 // Path-uniform rejection: the SAME source emits the SAME
9095 // structural variant under both expansion strategies. The
9096 // defmacro-syntax-gate fires inside `macro_def_from` →
9097 // `parse_params`, BEFORE either strategy's expansion path
9098 // runs; so both `Expander::new()` (bytecode) and
9099 // `Expander::new_substitute_only()` (substitute) reject the
9100 // SAME malformed defmacro at the SAME gate. Sibling of
9101 // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9102 let src = "(defmacro f (a &rest 5) `(,a))";
9103 let mut subst = Expander::new_substitute_only();
9104 let mut bytecode = Expander::new();
9105 let err_subst = subst
9106 .expand_program(read(src).unwrap())
9107 .expect_err("substitute must error");
9108 let err_byte = bytecode
9109 .expand_program(read(src).unwrap())
9110 .expect_err("bytecode must error");
9111 assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9112 assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9113 }
9114
9115 #[test]
9116 fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9117 // End-to-end through Display — pins the rendered diagnostic
9118 // consumers see today (REPL, tatara-check) AND the new `(rest
9119 // marker at position {rest_position}, got {got})` clause. The
9120 // legacy `"&rest needs a name"` substring rides through
9121 // verbatim.
9122 let mut e = Expander::new();
9123 let err = e
9124 .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9125 .expect_err("&rest followed by non-symbol must error");
9126 assert_eq!(
9127 format!("{err}"),
9128 "compile error in defmacro params: &rest needs a name \
9129 (rest marker at position 1, got 5)"
9130 );
9131 }
9132
9133 #[test]
9134 fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9135 // Same as the prior test but for the "missing entirely" branch.
9136 // The renderable detail is `(rest marker at position
9137 // {rest_position}, none provided)` — naming the absence
9138 // structurally instead of an empty / partial parenthetical.
9139 let mut e = Expander::new();
9140 let err = e
9141 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9142 .expect_err("&rest with no follower must error");
9143 assert_eq!(
9144 format!("{err}"),
9145 "compile error in defmacro params: &rest needs a name \
9146 (rest marker at position 1, none provided)"
9147 );
9148 }
9149
9150 #[test]
9151 fn rest_param_missing_name_position_is_none_today() {
9152 // Pins that `position()` returns `None` so the future `pos:
9153 // Option<usize>` add (once `Sexp` carries source spans) lands
9154 // as a deliberate fail-before/pass-after delta rather than a
9155 // silent default. Parallel to
9156 // `non_symbol_param_position_is_none_today` and
9157 // `missing_macro_arg_position_is_none_today`.
9158 let err_missing = LispError::RestParamMissingName {
9159 rest_position: 1,
9160 got: None,
9161 };
9162 assert_eq!(err_missing.position(), None);
9163 let err_got = LispError::RestParamMissingName {
9164 rest_position: 0,
9165 got: Some(crate::error::SexpWitness::new(
9166 crate::error::SexpShape::Int,
9167 "5",
9168 )),
9169 };
9170 assert_eq!(err_got.position(), None);
9171 }
9172
9173 // --- RestParamTrailingTokens: the parse_params gate's third (and
9174 // final) definition-site failure mode ---
9175 //
9176 // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9177 // thing a param list can name. Before this variant `parse_params`
9178 // returned the moment it bound the rest name, SILENTLY DROPPING any
9179 // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9180 // there. These tests pin the loud rejection that replaces the silent
9181 // drop; the symbol `RestParamTrailingTokens` exists only after this
9182 // change, so the whole block is fail-before/pass-after by construction
9183 // (compile-time edge) and the end-to-end regression guard below pins
9184 // that the malformed defmacro no longer expands cleanly.
9185
9186 /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9187 /// shape and lifts the marker position, trailing count, and first
9188 /// offender's display up to the assert site.
9189 fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9190 match err {
9191 LispError::RestParamTrailingTokens {
9192 rest_position,
9193 extra,
9194 first,
9195 } => (*rest_position, *extra, first.display.as_str()),
9196 other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9197 }
9198 }
9199
9200 #[test]
9201 fn parse_params_rejects_single_trailing_token_after_rest_name() {
9202 // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9203 // follows. The rest name is bound at position 2, the marker at 1;
9204 // the lone trailing token `extra` is reported (extra == 1, first ==
9205 // "extra"). Before this variant `parse_params` returned at the rest
9206 // name and `extra` vanished.
9207 let err = parse_params(&read("a &rest c extra").unwrap())
9208 .expect_err("a trailing token after the rest name must error");
9209 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9210 }
9211
9212 #[test]
9213 fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9214 // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9215 // counts ALL of them (3), `first` is the first (`x`), and the
9216 // marker is at position 0. A regression that reports only the
9217 // first trailing token's presence (extra hard-coded to 1) fails
9218 // loudly here.
9219 let err = parse_params(&read("&rest c x y z").unwrap())
9220 .expect_err("multiple trailing tokens must error");
9221 assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9222 }
9223
9224 #[test]
9225 fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9226 // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9227 // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9228 // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9229 // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9230 // a well-formed rest-name followed by junk is
9231 // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9232 // witness's `Sexp::Display` projection.
9233 let err = parse_params(&read("a &rest c 5").unwrap())
9234 .expect_err("a trailing non-symbol after the rest name must error");
9235 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9236 }
9237
9238 #[test]
9239 fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9240 // The fidelity fix, end-to-end through `expand_program`: a defmacro
9241 // whose param list carries a stray token after `&rest <name>` now
9242 // ERRORS at the typed-entry gate instead of expanding as though the
9243 // stray token weren't there. This is the regression guard for the
9244 // silent-drop bug — before this change the same source expanded
9245 // cleanly and `extra` was discarded with no signal.
9246 let mut e = Expander::new();
9247 let err = e
9248 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9249 .expect_err("trailing token after &rest name must error");
9250 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9251 }
9252
9253 #[test]
9254 fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9255 // Path-uniform rejection: the gate fires inside `macro_def_from` →
9256 // `parse_params`, BEFORE either expansion strategy runs, so both
9257 // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9258 // (substitute) reject the SAME malformed defmacro at the SAME gate.
9259 // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9260 let src = "(defmacro f (a &rest xs extra) `(,a))";
9261 let mut subst = Expander::new_substitute_only();
9262 let mut bytecode = Expander::new();
9263 let err_subst = subst
9264 .expand_program(read(src).unwrap())
9265 .expect_err("substitute must error");
9266 let err_byte = bytecode
9267 .expand_program(read(src).unwrap())
9268 .expect_err("bytecode must error");
9269 assert_eq!(
9270 rest_param_trailing_tokens_fields(&err_subst),
9271 (1, 1, "extra")
9272 );
9273 assert_eq!(
9274 rest_param_trailing_tokens_fields(&err_byte),
9275 (1, 1, "extra")
9276 );
9277 }
9278
9279 #[test]
9280 fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9281 // End-to-end through Display — pins the rendered diagnostic AND the
9282 // new `(rest marker at position {n}, {extra} trailing after name,
9283 // first: {first})` clause. The `compile error in defmacro params:`
9284 // prefix matches the sibling `&rest needs a name` rendering's shape.
9285 let mut e = Expander::new();
9286 let err = e
9287 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9288 .expect_err("trailing token after &rest name must error");
9289 assert_eq!(
9290 format!("{err}"),
9291 "compile error in defmacro params: &rest name must be last \
9292 (rest marker at position 1, 1 trailing after name, first: extra)"
9293 );
9294 }
9295
9296 #[test]
9297 fn rest_param_trailing_tokens_position_is_none_today() {
9298 // Pins `position() == None` so the future `pos: Option<usize>` add
9299 // (once `Sexp` carries source spans) lands as a deliberate
9300 // fail-before/pass-after delta. Parallel to
9301 // `rest_param_missing_name_position_is_none_today`.
9302 let err = LispError::RestParamTrailingTokens {
9303 rest_position: 1,
9304 extra: 1,
9305 first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9306 };
9307 assert_eq!(err.position(), None);
9308 }
9309
9310 // --- MacroDefHead enum (the closed-set lift) ---
9311 //
9312 // The next nine tests pin the typed-enum lift that closes the
9313 // three-times rule on the `head: &str → &'static str` projection
9314 // idiom previously inlined at FOUR sites (the `matches!` gate at
9315 // the top of `macro_def_from` plus the projection match inside
9316 // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9317 // `defmacro_non_list_params`). Every test in this block names
9318 // `MacroDefHead` directly — the symbol exists only after the
9319 // lift, so the entire block is fail-before/pass-after by
9320 // construction (compile-time edge). Theory anchor: THEORY.md
9321 // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9322 // a TYPE rather than a `matches!` literal.
9323
9324 #[test]
9325 fn macro_def_head_from_keyword_recognizes_defmacro() {
9326 // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9327 // `Some(MacroDefHead::Defmacro)` — the first of the three
9328 // canonical macro-definition head keywords. A regression that
9329 // re-inlines a `matches!`-only gate (without the typed-enum
9330 // projection) deletes `from_keyword` and fails-loudly here.
9331 assert_eq!(
9332 MacroDefHead::from_keyword("defmacro"),
9333 Some(MacroDefHead::Defmacro)
9334 );
9335 }
9336
9337 #[test]
9338 fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9339 // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9340 // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9341 // of the three canonical head keywords. The `defpoint-template`
9342 // form is the K8s-as-processes authoring surface (see
9343 // tatara-process); `macro_def_from` must recognize it
9344 // identically to `defmacro` so the `(defpoint-template …)`
9345 // form's macro-style binding works the same way.
9346 assert_eq!(
9347 MacroDefHead::from_keyword("defpoint-template"),
9348 Some(MacroDefHead::DefpointTemplate)
9349 );
9350 }
9351
9352 #[test]
9353 fn macro_def_head_from_keyword_recognizes_defcheck() {
9354 // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9355 // `Some(MacroDefHead::Defcheck)` — the third and final
9356 // canonical head keyword. The `defcheck` form is the
9357 // workspace-coherence authoring surface (see
9358 // tatara-reconciler/checks.lisp); `macro_def_from` must
9359 // recognize it identically to `defmacro` so user-defined
9360 // checks inherit the macro-style binding semantics.
9361 assert_eq!(
9362 MacroDefHead::from_keyword("defcheck"),
9363 Some(MacroDefHead::Defcheck)
9364 );
9365 }
9366
9367 #[test]
9368 fn macro_def_head_from_keyword_rejects_unknown() {
9369 // Pins that `MacroDefHead::from_keyword` returns `None` for
9370 // anything outside the closed set — a non-symbol keyword
9371 // (`"if"`), a near-miss spelling (`"defmacroo"`,
9372 // `"defcheckk"`), and the empty string. `macro_def_from`
9373 // depends on this `None` projection to mean "this form is
9374 // not a defmacro form" and walk past — a regression that
9375 // accidentally accepts a near-miss head (e.g. via a
9376 // lower-cased `EqualFold` match) would route `(defmacroo …)`
9377 // through the arity gate, which is wrong. Pins all four
9378 // canonical near-miss / non-canonical inputs.
9379 assert_eq!(MacroDefHead::from_keyword("if"), None);
9380 assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9381 assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9382 assert_eq!(MacroDefHead::from_keyword(""), None);
9383 }
9384
9385 #[test]
9386 fn macro_def_head_keyword_round_trips_each_variant() {
9387 // Pins that `MacroDefHead::keyword` returns the canonical
9388 // `&'static str` literal for each variant. Together with
9389 // `from_keyword` this closes the bidirectional projection:
9390 // for every canonical head keyword `s`, `MacroDefHead::
9391 // from_keyword(s).unwrap().keyword() == s`. The `&'static
9392 // str` lifetime on the return type is load-bearing — it's
9393 // what lets the `LispError::Defmacro*` variants carry
9394 // `head: &'static str` slots without an arbitrary owned
9395 // `String`. Pinning the `: &'static str` binding here
9396 // makes the lifetime requirement load-bearing in the test.
9397 let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9398 let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9399 let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9400 assert_eq!(s_defmacro, "defmacro");
9401 assert_eq!(s_defpoint, "defpoint-template");
9402 assert_eq!(s_defcheck, "defcheck");
9403 }
9404
9405 #[test]
9406 fn macro_def_head_keyword_round_trips_through_from_keyword() {
9407 // Pins that the two halves of the projection compose to the
9408 // identity on the closed set: for every canonical head
9409 // keyword, projecting `&str → MacroDefHead → &'static str`
9410 // returns the original literal. Sibling of
9411 // `macro_def_head_keyword_round_trips_each_variant` —
9412 // together they pin both directions of the bidirection.
9413 for kw in ["defmacro", "defpoint-template", "defcheck"] {
9414 let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9415 assert_eq!(head.keyword(), kw);
9416 }
9417 }
9418
9419 #[test]
9420 fn macro_def_head_threads_through_defmacro_arity_helper() {
9421 // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9422 // and threads it through to the variant's typed `head` slot
9423 // unchanged — no `&str` projection at the helper boundary
9424 // (the projection through `MacroDefHead::keyword()` happens at
9425 // Display rendering time inside the `#[error(...)]`
9426 // annotation). A regression that drops the `MacroDefHead`
9427 // parameter type (e.g. by reverting to `head: &str`) breaks
9428 // compilation here. Pinning each of the three variants gives
9429 // the typed-head threading the same path-uniformity edge the
9430 // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9431 // for the call-site path through `macro_def_from`.
9432 for head in [
9433 MacroDefHead::Defmacro,
9434 MacroDefHead::DefpointTemplate,
9435 MacroDefHead::Defcheck,
9436 ] {
9437 let err = defmacro_arity(head, 2);
9438 match err {
9439 LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9440 other => panic!("expected DefmacroArity, got: {other:?}"),
9441 }
9442 }
9443 }
9444
9445 #[test]
9446 fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9447 // Sibling of the `defmacro_arity` threading test — pins that
9448 // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9449 // and threads it through to the variant's typed `head` slot
9450 // unchanged. The `got: &Sexp` parameter rides through
9451 // `crate::domain::sexp_witness` into the variant's typed
9452 // `got: SexpWitness` slot so BOTH the structural shape AND
9453 // the rendered literal are preserved across the helper
9454 // boundary, parallel to how `non_symbol_param` and
9455 // `non_symbol_unquote_target` project their `&Sexp` arguments
9456 // through the same typed joint primitive.
9457 let got = parse("5");
9458 for head in [
9459 MacroDefHead::Defmacro,
9460 MacroDefHead::DefpointTemplate,
9461 MacroDefHead::Defcheck,
9462 ] {
9463 let err = defmacro_non_symbol_name(head, &got);
9464 match err {
9465 LispError::DefmacroNonSymbolName { head: h, got: g } => {
9466 assert_eq!(h, head);
9467 assert_eq!(g.shape, crate::error::SexpShape::Int);
9468 assert_eq!(g.display, "5");
9469 }
9470 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9471 }
9472 }
9473 }
9474
9475 #[test]
9476 fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9477 // Sibling of the `defmacro_arity` and
9478 // `defmacro_non_symbol_name` threading tests — pins that
9479 // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9480 // and threads it through to the variant's typed `head` slot
9481 // unchanged. Together the three threading tests close the
9482 // typed-enum lift across all three error helpers — every
9483 // call site that constructs a `LispError::Defmacro*` variant
9484 // takes its `head` from a `MacroDefHead`, never from a `&str`
9485 // match. The `got: &Sexp` parameter rides through
9486 // `crate::domain::sexp_witness` into the variant's typed
9487 // `got: SexpWitness` slot so BOTH the structural shape AND
9488 // the rendered literal are preserved across the helper
9489 // boundary, parallel to how `defmacro_non_symbol_name`,
9490 // `non_symbol_param`, and `non_symbol_unquote_target` project
9491 // their `&Sexp` arguments through the same typed joint
9492 // primitive.
9493 let got = parse("x");
9494 for head in [
9495 MacroDefHead::Defmacro,
9496 MacroDefHead::DefpointTemplate,
9497 MacroDefHead::Defcheck,
9498 ] {
9499 let err = defmacro_non_list_params(head, &got);
9500 match err {
9501 LispError::DefmacroNonListParams { head: h, got: g } => {
9502 assert_eq!(h, head);
9503 assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9504 assert_eq!(g.display, "x");
9505 }
9506 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9507 }
9508 }
9509 }
9510
9511 /// Helper for the defmacro-arity tests — pins the variant shape and
9512 /// carries the head / arity up to the assert site for legibility.
9513 /// Sibling of `non_symbol_param_fields` and
9514 /// `rest_param_missing_name_fields`.
9515 fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9516 match err {
9517 LispError::DefmacroArity { head, arity } => (*head, *arity),
9518 other => panic!("expected DefmacroArity, got: {other:?}"),
9519 }
9520 }
9521
9522 #[test]
9523 fn defmacro_arity_with_head_only_emits_structural_variant() {
9524 // `(defmacro)` — only the head, no name / params / body. Pins
9525 // variant identity AND that `arity == 1` (just the head
9526 // element) AND that `head == "defmacro"`. A regression that
9527 // re-inlines the legacy `LispError::Compile` shape (which
9528 // named neither field) fails-loudly here.
9529 let mut e = Expander::new();
9530 let err = e
9531 .expand_program(read("(defmacro)").unwrap())
9532 .expect_err("defmacro arity gate must error");
9533 let (head, arity) = defmacro_arity_fields(&err);
9534 assert_eq!(head, MacroDefHead::Defmacro);
9535 assert_eq!(arity, 1);
9536 }
9537
9538 #[test]
9539 fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9540 // `(defmacro f)` — head + name, missing params + body. Pins
9541 // that `arity` advances with the actual form length (2 for
9542 // this case) so an LSP quick-fix that wants to surface "you
9543 // wrote 2 elements; need 4" gains the count as data, no
9544 // source re-parse required.
9545 let mut e = Expander::new();
9546 let err = e
9547 .expand_program(read("(defmacro f)").unwrap())
9548 .expect_err("defmacro arity gate must error");
9549 let (head, arity) = defmacro_arity_fields(&err);
9550 assert_eq!(head, MacroDefHead::Defmacro);
9551 assert_eq!(arity, 2);
9552 }
9553
9554 #[test]
9555 fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9556 // `(defmacro f ())` — head + name + params, missing body
9557 // (the most-complete partial defmacro that still trips the
9558 // arity gate). Pins that `arity == 3` exactly so an LSP
9559 // quick-fix that wants to surface "your defmacro is one
9560 // element short — body is missing" gains the count as data.
9561 let mut e = Expander::new();
9562 let err = e
9563 .expand_program(read("(defmacro f ())").unwrap())
9564 .expect_err("defmacro arity gate must error");
9565 let (head, arity) = defmacro_arity_fields(&err);
9566 assert_eq!(head, MacroDefHead::Defmacro);
9567 assert_eq!(arity, 3);
9568 }
9569
9570 #[test]
9571 fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9572 // `defpoint-template` shares `macro_def_from` with `defmacro`
9573 // (all three head keywords route through the same gate). Pins
9574 // that the lift fires path-uniformly across the three head
9575 // keywords AND that the variant's `head` slot carries the
9576 // actual head literal — `defpoint-template`, not `defmacro`
9577 // — so an LSP that wants to point at "your defpoint-template
9578 // form is missing elements" gains the head as data.
9579 let mut e = Expander::new();
9580 let err = e
9581 .expand_program(read("(defpoint-template t)").unwrap())
9582 .expect_err("defpoint-template arity gate must error");
9583 let (head, arity) = defmacro_arity_fields(&err);
9584 assert_eq!(head, MacroDefHead::DefpointTemplate);
9585 assert_eq!(arity, 2);
9586 }
9587
9588 #[test]
9589 fn defmacro_arity_in_defcheck_emits_same_variant() {
9590 // Sibling of the defpoint-template test — `defcheck` is the
9591 // third head keyword `macro_def_from` recognizes. All three
9592 // route through the same arity gate and now reject too-short
9593 // forms with the same structural variant.
9594 let mut e = Expander::new();
9595 let err = e
9596 .expand_program(read("(defcheck)").unwrap())
9597 .expect_err("defcheck arity gate must error");
9598 let (head, arity) = defmacro_arity_fields(&err);
9599 assert_eq!(head, MacroDefHead::Defcheck);
9600 assert_eq!(arity, 1);
9601 }
9602
9603 #[test]
9604 fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9605 // Path-uniform rejection: the SAME source emits the SAME
9606 // structural variant under both expansion strategies. The
9607 // arity gate fires inside `macro_def_from` BEFORE either
9608 // strategy's expansion path runs; so both `Expander::new()`
9609 // (bytecode) and `Expander::new_substitute_only()`
9610 // (substitute) reject the SAME malformed defmacro at the
9611 // SAME gate. Sibling of
9612 // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9613 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9614 let src = "(defmacro f)";
9615 let mut subst = Expander::new_substitute_only();
9616 let mut bytecode = Expander::new();
9617 let err_subst = subst
9618 .expand_program(read(src).unwrap())
9619 .expect_err("substitute must error");
9620 let err_byte = bytecode
9621 .expand_program(read(src).unwrap())
9622 .expect_err("bytecode must error");
9623 assert_eq!(
9624 defmacro_arity_fields(&err_subst),
9625 (MacroDefHead::Defmacro, 2)
9626 );
9627 assert_eq!(
9628 defmacro_arity_fields(&err_byte),
9629 (MacroDefHead::Defmacro, 2)
9630 );
9631 }
9632
9633 #[test]
9634 fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9635 // End-to-end through Display — pins the rendered diagnostic
9636 // consumers see today (REPL, `tatara-check`) AND the new
9637 // `(got {arity} elements, need 4)` clause. The legacy
9638 // `"(defmacro name (params) body) required"` substring
9639 // rides through verbatim. Tools that pattern-match on the
9640 // variant gain structural binding to `head` / `arity`.
9641 let mut e = Expander::new();
9642 let err = e
9643 .expand_program(read("(defmacro f)").unwrap())
9644 .expect_err("defmacro arity gate must error");
9645 assert_eq!(
9646 format!("{err}"),
9647 "compile error in defmacro: (defmacro name (params) body) required \
9648 (got 2 elements, need 4)"
9649 );
9650 }
9651
9652 #[test]
9653 fn defmacro_arity_position_is_none_today() {
9654 // Negative control for the future-spans move: until `Sexp`
9655 // carries source positions, `position()` on `LispError`
9656 // returns `None` for this variant. A future run that gives
9657 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9658 // this test gives that change a deliberate fail-before/pass-
9659 // after delta. Parallel to
9660 // `non_symbol_param_position_is_none_today` and
9661 // `rest_param_missing_name_position_is_none_today`.
9662 let mut e = Expander::new();
9663 let err = e
9664 .expand_program(read("(defmacro)").unwrap())
9665 .expect_err("defmacro arity gate must error");
9666 assert_eq!(err.position(), None);
9667 }
9668
9669 #[test]
9670 fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9671 // Negative control: a defmacro with exactly 4 elements (head
9672 // + name + params + body) passes the arity gate. Pins that
9673 // the lift is scoped to the arity-deficient case, not to
9674 // every defmacro form. After this test, a regression that
9675 // tightens the arity gate to >= 5 (e.g. spuriously requiring
9676 // a docstring slot) fails-loudly here.
9677 let mut e = Expander::new();
9678 let out = e
9679 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9680 .expect("well-formed defmacro must succeed");
9681 assert_eq!(out[0], Sexp::int(42));
9682 }
9683
9684 /// Helper for the defmacro-non-symbol-name tests — pins variant
9685 /// shape and carries the head / got up to the assert site for
9686 /// legibility. Sibling of `defmacro_arity_fields`,
9687 /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9688 fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9689 match err {
9690 LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9691 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9692 }
9693 }
9694
9695 #[test]
9696 fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9697 // `(defmacro 5 () body)` — the form passes the arity gate
9698 // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9699 // identity AND that `head == "defmacro"` AND that `got ==
9700 // "5"`. A regression that re-inlines the legacy
9701 // `LispError::Compile { form: "defmacro", message: "expected
9702 // name symbol" }` shape (which named the failure mode but
9703 // not the offending element) fails-loudly here.
9704 let mut e = Expander::new();
9705 let err = e
9706 .expand_program(read("(defmacro 5 () body)").unwrap())
9707 .expect_err("defmacro non-symbol name gate must error");
9708 let (head, got) = defmacro_non_symbol_name_fields(&err);
9709 assert_eq!(head, MacroDefHead::Defmacro);
9710 assert_eq!(got, "5");
9711 }
9712
9713 #[test]
9714 fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9715 // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9716 // not a symbol. Pins that `Sexp::Display` for
9717 // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9718 // carries the keyword form unchanged. An LSP that wants to
9719 // surface "you wrote `:foo` where a name symbol was expected"
9720 // gains the literal keyword value as data, no source re-parse
9721 // required.
9722 let mut e = Expander::new();
9723 let err = e
9724 .expand_program(read("(defmacro :foo () body)").unwrap())
9725 .expect_err("defmacro non-symbol name gate must error");
9726 let (head, got) = defmacro_non_symbol_name_fields(&err);
9727 assert_eq!(head, MacroDefHead::Defmacro);
9728 assert_eq!(got, ":foo");
9729 }
9730
9731 #[test]
9732 fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9733 // `(defmacro "name" () body)` — list[1] is the string
9734 // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9735 // for `Atom::String(s)` writes `"s"` (with quotes) and the
9736 // variant's `got` slot carries the quoted form unchanged.
9737 let mut e = Expander::new();
9738 let err = e
9739 .expand_program(read("(defmacro \"name\" () body)").unwrap())
9740 .expect_err("defmacro non-symbol name gate must error");
9741 let (head, got) = defmacro_non_symbol_name_fields(&err);
9742 assert_eq!(head, MacroDefHead::Defmacro);
9743 assert_eq!(got, "\"name\"");
9744 }
9745
9746 #[test]
9747 fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9748 // `(defmacro (nested) () body)` — list[1] is a nested list,
9749 // not a symbol. Pins that `Sexp::Display` for a list writes
9750 // `(elements)` and the variant's `got` slot carries the
9751 // parenthesized form unchanged.
9752 let mut e = Expander::new();
9753 let err = e
9754 .expand_program(read("(defmacro (nested) () body)").unwrap())
9755 .expect_err("defmacro non-symbol name gate must error");
9756 let (head, got) = defmacro_non_symbol_name_fields(&err);
9757 assert_eq!(head, MacroDefHead::Defmacro);
9758 assert_eq!(got, "(nested)");
9759 }
9760
9761 #[test]
9762 fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9763 // `defpoint-template` shares `macro_def_from` with `defmacro`
9764 // (all three head keywords route through the same gate).
9765 // Pins that the lift fires path-uniformly across the three
9766 // head keywords AND that the variant's `head` slot carries
9767 // the actual head literal — `defpoint-template`, not
9768 // `defmacro` — so an LSP that wants to point at "your
9769 // defpoint-template form's name slot isn't a symbol" gains
9770 // the head as data.
9771 let mut e = Expander::new();
9772 let err = e
9773 .expand_program(read("(defpoint-template 7 () body)").unwrap())
9774 .expect_err("defpoint-template non-symbol name gate must error");
9775 let (head, got) = defmacro_non_symbol_name_fields(&err);
9776 assert_eq!(head, MacroDefHead::DefpointTemplate);
9777 assert_eq!(got, "7");
9778 }
9779
9780 #[test]
9781 fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9782 // Sibling for the `defcheck` head — third head keyword
9783 // `macro_def_from` recognizes. Rounds out the three-head-
9784 // keyword coverage so the lift is path-uniform across
9785 // `defmacro` / `defpoint-template` / `defcheck`.
9786 let mut e = Expander::new();
9787 let err = e
9788 .expand_program(read("(defcheck :k () body)").unwrap())
9789 .expect_err("defcheck non-symbol name gate must error");
9790 let (head, got) = defmacro_non_symbol_name_fields(&err);
9791 assert_eq!(head, MacroDefHead::Defcheck);
9792 assert_eq!(got, ":k");
9793 }
9794
9795 #[test]
9796 fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9797 // Path-uniform rejection: the SAME source emits the SAME
9798 // structural variant under both expansion strategies. The
9799 // name-symbol gate fires inside `macro_def_from` BEFORE
9800 // either expansion strategy runs, so the gate is naturally
9801 // path-uniform; pinning it gives a regression that drifts
9802 // either strategy's handling of non-symbol-name defmacros (or
9803 // makes one strategy accept what the other rejects) a fail-
9804 // before/pass-after edge. Sibling of
9805 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9806 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9807 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9808 let src = "(defmacro 5 () body)";
9809 let mut subst = Expander::new_substitute_only();
9810 let mut bytecode = Expander::new();
9811 let err_subst = subst
9812 .expand_program(read(src).unwrap())
9813 .expect_err("substitute must error");
9814 let err_byte = bytecode
9815 .expand_program(read(src).unwrap())
9816 .expect_err("bytecode must error");
9817 assert_eq!(
9818 defmacro_non_symbol_name_fields(&err_subst),
9819 (MacroDefHead::Defmacro, "5")
9820 );
9821 assert_eq!(
9822 defmacro_non_symbol_name_fields(&err_byte),
9823 (MacroDefHead::Defmacro, "5")
9824 );
9825 }
9826
9827 #[test]
9828 fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9829 // End-to-end through Display — pins the rendered diagnostic
9830 // consumers see today (REPL, `tatara-check`) AND the new
9831 // `, got {got}` clause. The legacy `"expected name symbol"`
9832 // substring rides through verbatim; the prefix matches the
9833 // legacy `Compile { form: "defmacro", message: "expected name
9834 // symbol" }` byte-for-byte. Tools that pattern-match on the
9835 // variant gain structural binding to `head` / `got`.
9836 let mut e = Expander::new();
9837 let err = e
9838 .expand_program(read("(defmacro 5 () body)").unwrap())
9839 .expect_err("defmacro non-symbol name gate must error");
9840 assert_eq!(
9841 format!("{err}"),
9842 "compile error in defmacro: expected name symbol, got 5"
9843 );
9844 }
9845
9846 #[test]
9847 fn defmacro_non_symbol_name_position_is_none_today() {
9848 // Negative control for the future-spans move: until `Sexp`
9849 // carries source positions, `position()` on `LispError`
9850 // returns `None` for this variant. A future run that gives
9851 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9852 // this test gives that change a deliberate fail-before/pass-
9853 // after delta. Parallel to
9854 // `defmacro_arity_position_is_none_today`,
9855 // `non_symbol_param_position_is_none_today`, and
9856 // `rest_param_missing_name_position_is_none_today`.
9857 let mut e = Expander::new();
9858 let err = e
9859 .expand_program(read("(defmacro 5 () body)").unwrap())
9860 .expect_err("defmacro non-symbol name gate must error");
9861 assert_eq!(err.position(), None);
9862 }
9863
9864 #[test]
9865 fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9866 // Negative control: a defmacro whose name slot IS a symbol
9867 // passes the name-symbol gate. Pins that the lift is scoped
9868 // to the non-symbol-name case, not to every defmacro form.
9869 // After this test, a regression that tightens the gate to
9870 // reject e.g. kebab-cased names fails-loudly here.
9871 let mut e = Expander::new();
9872 let out = e
9873 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9874 .expect("well-formed defmacro must succeed");
9875 assert_eq!(out[0], Sexp::int(42));
9876 }
9877
9878 #[test]
9879 fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9880 // Pins the gate ordering: a 4-element defmacro whose name
9881 // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9882 // `DefmacroArity`. The arity gate (>= 4 elements) admits
9883 // this form; the name-symbol gate is the next checkpoint.
9884 // A regression that swaps the gate ordering (e.g. checks
9885 // name-symbol before arity, so `(defmacro 5)` would emit
9886 // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9887 // loudly here.
9888 let mut e = Expander::new();
9889 let err = e
9890 .expand_program(read("(defmacro 5 () body)").unwrap())
9891 .expect_err("name-symbol gate must error");
9892 assert!(
9893 matches!(err, LispError::DefmacroNonSymbolName { .. }),
9894 "expected DefmacroNonSymbolName, got: {err:?}"
9895 );
9896
9897 let err_arity = e
9898 .expand_program(read("(defmacro 5)").unwrap())
9899 .expect_err("arity gate must error");
9900 assert!(
9901 matches!(err_arity, LispError::DefmacroArity { .. }),
9902 "expected DefmacroArity (arity < 4 short-circuits before name check), \
9903 got: {err_arity:?}"
9904 );
9905 }
9906
9907 /// Helper for the defmacro-non-list-params tests — pins variant
9908 /// shape and carries the head / got up to the assert site for
9909 /// legibility. Sibling of `defmacro_arity_fields`,
9910 /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9911 /// and `rest_param_missing_name_fields`.
9912 fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9913 match err {
9914 LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9915 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9916 }
9917 }
9918
9919 #[test]
9920 fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
9921 // `(defmacro f x body)` — the form passes both the arity gate
9922 // (4 elements) AND the name-symbol gate (`f` is a symbol) but
9923 // list[2] is the symbol `x`, not a list. Pins variant identity
9924 // AND that `head == "defmacro"` AND that `got == "x"`. A
9925 // regression that re-inlines the legacy `LispError::Compile {
9926 // form: "defmacro", message: "expected param list" }` shape
9927 // (which named the failure mode but not the offending element)
9928 // fails-loudly here.
9929 let mut e = Expander::new();
9930 let err = e
9931 .expand_program(read("(defmacro f x body)").unwrap())
9932 .expect_err("defmacro non-list params gate must error");
9933 let (head, got) = defmacro_non_list_params_fields(&err);
9934 assert_eq!(head, MacroDefHead::Defmacro);
9935 assert_eq!(got, "x");
9936 }
9937
9938 #[test]
9939 fn defmacro_non_list_params_with_int_emits_structural_variant() {
9940 // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
9941 // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
9942 // variant's `got` slot carries the integer form unchanged. An
9943 // LSP that surfaces "you wrote `5` where a param list was
9944 // expected" gains the literal value as data, no source
9945 // re-parse required.
9946 let mut e = Expander::new();
9947 let err = e
9948 .expand_program(read("(defmacro f 5 body)").unwrap())
9949 .expect_err("defmacro non-list params gate must error");
9950 let (head, got) = defmacro_non_list_params_fields(&err);
9951 assert_eq!(head, MacroDefHead::Defmacro);
9952 assert_eq!(got, "5");
9953 }
9954
9955 #[test]
9956 fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
9957 // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
9958 // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
9959 // writes `:s` and the variant's `got` slot carries the
9960 // keyword form unchanged.
9961 let mut e = Expander::new();
9962 let err = e
9963 .expand_program(read("(defmacro f :foo body)").unwrap())
9964 .expect_err("defmacro non-list params gate must error");
9965 let (head, got) = defmacro_non_list_params_fields(&err);
9966 assert_eq!(head, MacroDefHead::Defmacro);
9967 assert_eq!(got, ":foo");
9968 }
9969
9970 #[test]
9971 fn defmacro_non_list_params_with_string_emits_structural_variant() {
9972 // `(defmacro f "params" body)` — list[2] is the string literal
9973 // `"params"`, not a list. Pins that `Sexp::Display` for
9974 // `Atom::String(s)` writes `"s"` (with quotes) and the
9975 // variant's `got` slot carries the quoted form unchanged.
9976 let mut e = Expander::new();
9977 let err = e
9978 .expand_program(read("(defmacro f \"params\" body)").unwrap())
9979 .expect_err("defmacro non-list params gate must error");
9980 let (head, got) = defmacro_non_list_params_fields(&err);
9981 assert_eq!(head, MacroDefHead::Defmacro);
9982 assert_eq!(got, "\"params\"");
9983 }
9984
9985 #[test]
9986 fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
9987 // `defpoint-template` shares `macro_def_from` with `defmacro`
9988 // (all three head keywords route through the same gate).
9989 // Pins that the lift fires path-uniformly across the three
9990 // head keywords AND that the variant's `head` slot carries
9991 // the actual head literal — `defpoint-template`, not
9992 // `defmacro` — so an LSP that wants to point at "your
9993 // defpoint-template form's param-list slot isn't a list"
9994 // gains the head as data.
9995 let mut e = Expander::new();
9996 let err = e
9997 .expand_program(read("(defpoint-template t x body)").unwrap())
9998 .expect_err("defpoint-template non-list params gate must error");
9999 let (head, got) = defmacro_non_list_params_fields(&err);
10000 assert_eq!(head, MacroDefHead::DefpointTemplate);
10001 assert_eq!(got, "x");
10002 }
10003
10004 #[test]
10005 fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10006 // Sibling for the `defcheck` head — third head keyword
10007 // `macro_def_from` recognizes. Rounds out the three-head-
10008 // keyword coverage so the lift is path-uniform across
10009 // `defmacro` / `defpoint-template` / `defcheck`.
10010 let mut e = Expander::new();
10011 let err = e
10012 .expand_program(read("(defcheck c 7 body)").unwrap())
10013 .expect_err("defcheck non-list params gate must error");
10014 let (head, got) = defmacro_non_list_params_fields(&err);
10015 assert_eq!(head, MacroDefHead::Defcheck);
10016 assert_eq!(got, "7");
10017 }
10018
10019 #[test]
10020 fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10021 // Path-uniform rejection: the SAME source emits the SAME
10022 // structural variant under both expansion strategies. The
10023 // param-list gate fires inside `macro_def_from` BEFORE either
10024 // expansion strategy runs, so the gate is naturally path-
10025 // uniform; pinning it gives a regression that drifts either
10026 // strategy's handling of non-list-params defmacros (or makes
10027 // one strategy accept what the other rejects) a fail-before/
10028 // pass-after edge. Sibling of
10029 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10030 // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10031 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10032 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10033 let src = "(defmacro f x body)";
10034 let mut subst = Expander::new_substitute_only();
10035 let mut bytecode = Expander::new();
10036 let err_subst = subst
10037 .expand_program(read(src).unwrap())
10038 .expect_err("substitute must error");
10039 let err_byte = bytecode
10040 .expand_program(read(src).unwrap())
10041 .expect_err("bytecode must error");
10042 assert_eq!(
10043 defmacro_non_list_params_fields(&err_subst),
10044 (MacroDefHead::Defmacro, "x")
10045 );
10046 assert_eq!(
10047 defmacro_non_list_params_fields(&err_byte),
10048 (MacroDefHead::Defmacro, "x")
10049 );
10050 }
10051
10052 #[test]
10053 fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10054 // End-to-end through Display — pins the rendered diagnostic
10055 // consumers see today (REPL, `tatara-check`) AND the new
10056 // `, got {got}` clause. The legacy `"expected param list"`
10057 // substring rides through verbatim; the prefix matches the
10058 // legacy `Compile { form: "defmacro", message: "expected
10059 // param list" }` byte-for-byte. Tools that pattern-match on
10060 // the variant gain structural binding to `head` / `got`.
10061 let mut e = Expander::new();
10062 let err = e
10063 .expand_program(read("(defmacro f x body)").unwrap())
10064 .expect_err("defmacro non-list params gate must error");
10065 assert_eq!(
10066 format!("{err}"),
10067 "compile error in defmacro: expected param list, got x"
10068 );
10069 }
10070
10071 #[test]
10072 fn defmacro_non_list_params_position_is_none_today() {
10073 // Negative control for the future-spans move: until `Sexp`
10074 // carries source positions, `position()` on `LispError`
10075 // returns `None` for this variant. A future run that gives
10076 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10077 // this test gives that change a deliberate fail-before/pass-
10078 // after delta. Parallel to
10079 // `defmacro_arity_position_is_none_today`,
10080 // `defmacro_non_symbol_name_position_is_none_today`,
10081 // `non_symbol_param_position_is_none_today`, and
10082 // `rest_param_missing_name_position_is_none_today`.
10083 let mut e = Expander::new();
10084 let err = e
10085 .expand_program(read("(defmacro f x body)").unwrap())
10086 .expect_err("defmacro non-list params gate must error");
10087 assert_eq!(err.position(), None);
10088 }
10089
10090 #[test]
10091 fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10092 // Negative control: a defmacro whose param-list slot IS a
10093 // list passes the param-list gate. Pins that the lift is
10094 // scoped to the non-list-params case, not to every defmacro
10095 // form. After this test, a regression that tightens the gate
10096 // to reject e.g. empty param lists fails-loudly here.
10097 let mut e = Expander::new();
10098 let out = e
10099 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10100 .expect("well-formed defmacro must succeed");
10101 assert_eq!(out[0], Sexp::int(42));
10102 }
10103
10104 #[test]
10105 fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10106 // Pins the gate ordering: a 4-element defmacro whose name
10107 // slot IS a symbol but whose param-list slot is non-list
10108 // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10109 // The name-symbol gate admits this form; the param-list gate
10110 // is the next checkpoint. A regression that swaps the gate
10111 // ordering (e.g. checks param-list before name-symbol, so
10112 // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10113 // instead of `DefmacroNonSymbolName`) fails-loudly here.
10114 let mut e = Expander::new();
10115 let err = e
10116 .expand_program(read("(defmacro f x body)").unwrap())
10117 .expect_err("param-list gate must error");
10118 assert!(
10119 matches!(err, LispError::DefmacroNonListParams { .. }),
10120 "expected DefmacroNonListParams, got: {err:?}"
10121 );
10122
10123 let err_name = e
10124 .expand_program(read("(defmacro 5 x body)").unwrap())
10125 .expect_err("name-symbol gate must error");
10126 assert!(
10127 matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10128 "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10129 got: {err_name:?}"
10130 );
10131 }
10132
10133 #[test]
10134 fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10135 // Pins the full gate ordering: a 4-element defmacro whose
10136 // first three slots are head/symbol/non-list fires
10137 // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10138 // gate (>= 4 elements) admits this form; the name-symbol
10139 // gate admits the symbol; the param-list gate is the third
10140 // checkpoint. A regression that drifts the gate sequence
10141 // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10142 // loudly here. Parallel to
10143 // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10144 // — together they pin the full
10145 // arity → name-symbol → param-list ordering inside
10146 // `macro_def_from`.
10147 let mut e = Expander::new();
10148 let err = e
10149 .expand_program(read("(defmacro f x body)").unwrap())
10150 .expect_err("param-list gate must error");
10151 assert!(
10152 matches!(err, LispError::DefmacroNonListParams { .. }),
10153 "expected DefmacroNonListParams, got: {err:?}"
10154 );
10155
10156 let err_arity = e
10157 .expand_program(read("(defmacro f x)").unwrap())
10158 .expect_err("arity gate must error");
10159 assert!(
10160 matches!(err_arity, LispError::DefmacroArity { .. }),
10161 "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10162 got: {err_arity:?}"
10163 );
10164 }
10165
10166 #[test]
10167 fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10168 // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10169 // at the parser level, so `as_symbol()` succeeds for it. The
10170 // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10171 // itself; the dedicated `&rest needs a name` rejection (a
10172 // separate failure mode in this cluster) handles malformed
10173 // rest-param shapes. Pins that the lift is scoped to
10174 // non-symbol elements at param-list positions, not to
10175 // every malformed-param shape.
10176 let mut e = Expander::new();
10177 let out = e
10178 .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10179 .expect("&rest with name must succeed");
10180 assert_eq!(out[0], parse("(list 1 2 3)"));
10181 }
10182
10183 #[test]
10184 fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10185 // End-to-end through the Display impl — pins the rendered diagnostic
10186 // a downstream tool sees today (REPL, tatara-check). The shape is
10187 // parallel to the existing `TypeMismatch` variant: form, expected
10188 // shape, offending literal — all three slots present.
10189 let mut e = Expander::new();
10190 let err = e
10191 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10192 .expect_err("non-symbol target must error");
10193 assert_eq!(
10194 format!("{err}"),
10195 "compile error in ,: expected symbol, got (list 1 2)"
10196 );
10197 }
10198
10199 // ── template_invariant_violation: structural lift ───────────────
10200 //
10201 // The four byte-identical inline `LispError::Compile { form:
10202 // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10203 // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10204 // final-no-value gates) were lifted to `template_invariant_violation`,
10205 // and the helper's emission was promoted from `LispError::Compile`-
10206 // shape to the structural `LispError::TemplateInvariant { macro_name,
10207 // kind: TemplateInvariantKind }` variant. The index payload of the
10208 // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10209 // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10210 // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10211 // is structurally unrepresentable. Display matches the legacy
10212 // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10213 // `TemplateInvariantKind::message()` projection so authoring-tool
10214 // substring greps see no drift across the lift.
10215 //
10216 // The tests below pin: (a) the helper produces the structural
10217 // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10218 // first-class; (b) the Subst / Splice gates thread the bad index
10219 // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10220 // unchanged; (c) the two REACHABLE invariant-violation paths through
10221 // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10222 // out-of-bounds idx — route through the helper end-to-end (the
10223 // EndList / no-value paths are guarded by `last_mut().unwrap()`
10224 // ahead of `pop().ok_or_else()` and are not reachable through any
10225 // single CompiledTemplate; they remain defensive against future
10226 // changes to the stack discipline); (d) the legacy Display
10227 // rendering matches byte-for-byte across the lift; (e) positive
10228 // controls: a well-formed CompiledTemplate routes PAST the helper
10229 // cleanly, and unrelated macro errors (missing-required-arg) do
10230 // NOT route through the helper.
10231
10232 #[test]
10233 fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10234 // Direct unit test of the helper: a fixed macro_name and a
10235 // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10236 // variant with the macro_name in the `macro_name` slot and the
10237 // kind passed through verbatim in the `kind` slot. A regression
10238 // that drifts the variant (e.g., back to `LispError::Compile`)
10239 // or swaps the slot positions fails-loudly here.
10240 let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10241 match err {
10242 LispError::TemplateInvariant { macro_name, kind } => {
10243 assert_eq!(macro_name, "test-macro");
10244 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10245 }
10246 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10247 }
10248 }
10249
10250 #[test]
10251 fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10252 // The Subst gate's `usize` idx lives INSIDE the
10253 // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10254 // than being substring-rendered into a free-form `message`
10255 // slot. Pin that the helper threads the bad index through the
10256 // typed variant unchanged; a regression that drops the index
10257 // payload (e.g., via a `usize -> ()` projection) fails here.
10258 let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10259 match err {
10260 LispError::TemplateInvariant { macro_name, kind } => {
10261 assert_eq!(macro_name, "wrap");
10262 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10263 }
10264 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10265 }
10266 }
10267
10268 #[test]
10269 fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10270 // Hand-crafted CompiledTemplate with a Subst(99) op against
10271 // an empty params list: `args_by_index` has length 0, so
10272 // `.get(99)` returns None and the `ok_or_else` triggers
10273 // through the helper. Fail-before-pass-after: this same input
10274 // pre-lift went through `LispError::Compile { form: macro_name,
10275 // message: format!("compiled template referenced bad param
10276 // index {idx}") }`; post-lift it routes through
10277 // `template_invariant_violation` and emits the structural
10278 // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10279 // variant with the bad index threaded through as typed data.
10280 let tmpl = CompiledTemplate {
10281 ops: vec![TemplateOp::Subst(99)],
10282 };
10283 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10284 .expect_err("bad idx must error");
10285 match err {
10286 LispError::TemplateInvariant { macro_name, kind } => {
10287 assert_eq!(macro_name, "test-macro");
10288 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10289 }
10290 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10291 }
10292 }
10293
10294 #[test]
10295 fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10296 // Hand-crafted CompiledTemplate with a Splice(42) op against
10297 // an empty params list. Sibling of the Subst-bad-idx test;
10298 // pins the Splice gate routes through the helper with the
10299 // typed `SpliceBadIndex(42)` kind carrying the bad index.
10300 let tmpl = CompiledTemplate {
10301 ops: vec![TemplateOp::Splice(42)],
10302 };
10303 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10304 .expect_err("bad splice idx must error");
10305 match err {
10306 LispError::TemplateInvariant { macro_name, kind } => {
10307 assert_eq!(macro_name, "call-macro");
10308 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10309 }
10310 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10311 }
10312 }
10313
10314 #[test]
10315 fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10316 // End-to-end through the `LispError` Display impl — pins the
10317 // rendered diagnostic byte-for-byte: `"compile error in
10318 // test-macro: compiled template referenced bad param index 99"`.
10319 // Authoring tools that substring-grep the rendered diagnostic
10320 // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10321 // see no drift across the lift. Parallel to how
10322 // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10323 // pins the sibling-file (compile.rs) lift's Display contract.
10324 let tmpl = CompiledTemplate {
10325 ops: vec![TemplateOp::Subst(99)],
10326 };
10327 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10328 .expect_err("bad idx must error");
10329 assert_eq!(
10330 format!("{err}"),
10331 "compile error in test-macro: compiled template referenced bad param index 99"
10332 );
10333 }
10334
10335 #[test]
10336 fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10337 // Sibling Display test for the Splice gate. Pins the message
10338 // byte-for-byte through the `LispError` Display impl: `"compile
10339 // error in call-macro: compiled template referenced bad splice
10340 // index 42"`.
10341 let tmpl = CompiledTemplate {
10342 ops: vec![TemplateOp::Splice(42)],
10343 };
10344 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10345 .expect_err("bad splice idx must error");
10346 assert_eq!(
10347 format!("{err}"),
10348 "compile error in call-macro: compiled template referenced bad splice index 42"
10349 );
10350 }
10351
10352 #[test]
10353 fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10354 // Positive control: a CompiledTemplate produced by the
10355 // bytecode compiler (`compile_template`) for a well-formed
10356 // macro never references an out-of-bounds index nor
10357 // unbalances the stack, so `apply_compiled` routes PAST the
10358 // helper cleanly. A regression that fires the helper on
10359 // well-formed bytecode (e.g., off-by-one in the index
10360 // resolution) would fail here. End-to-end through the public
10361 // `Expander` surface so the test exercises the same code
10362 // path users see.
10363 let mut e = Expander::new();
10364 let out = e
10365 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10366 .expect("well-formed macro expansion must not fire template-invariant-violation");
10367 assert_eq!(out.len(), 1);
10368 assert_eq!(out[0], Sexp::int(42));
10369 }
10370
10371 #[test]
10372 fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10373 // Negative control: the `missing_macro_arg` gate in the shared
10374 // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10375 // loop runs,
10376 // so a missing required arg routes through
10377 // `LispError::MissingMacroArg`, NOT through
10378 // `template_invariant_violation`. Pins the helper is
10379 // precisely scoped to bytecode-runtime invariant violations
10380 // (Subst / Splice / stack gates), not to macro-call arity
10381 // errors (the latter has its own structural variant). A
10382 // regression that conflates the two gate clusters would
10383 // route this case through `Compile { ... }` instead of
10384 // `MissingMacroArg` and fail-loudly here.
10385 let mut e = Expander::new();
10386 let err = e
10387 .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10388 .expect_err("missing required arg must error");
10389 assert!(
10390 matches!(err, LispError::MissingMacroArg { .. }),
10391 "expected MissingMacroArg, got: {err:?}"
10392 );
10393 }
10394
10395 // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10396 //
10397 // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10398 // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10399 // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10400 // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10401 // `apply_compiled`. The arms differ in the kind constructor
10402 // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10403 // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10404 // prelude is byte-identical modulo the constructor. These tests
10405 // pin the lifted helper's contract directly; the existing
10406 // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10407 // guards proving both production arms route through it without
10408 // behavior drift.
10409
10410 #[test]
10411 fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10412 // For an in-range index, the helper returns `Ok(&args[idx])`
10413 // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10414 // Pins the borrow-not-clone contract: a regression that drifts
10415 // the helper to clone+return (`Result<Sexp>` instead of
10416 // `Result<&Sexp>`) would allocate per lookup at the production
10417 // `Subst`/`Splice` hot path. The kind constructor must NOT
10418 // fire on the success path (`FnOnce`'s lazy semantics) — pin
10419 // that the test passes a constructor that would panic if
10420 // called, asserting the helper short-circuits before invoking
10421 // it on the in-range arm.
10422 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10423 let got = resolve_bound_arg(&args, 1, "m", |_| {
10424 panic!("kind constructor must not fire on the in-range path")
10425 })
10426 .expect("in-range lookup must succeed");
10427 assert!(
10428 std::ptr::eq(got, &args[1]),
10429 "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10430 );
10431 assert_eq!(*got, Sexp::int(2));
10432 }
10433
10434 #[test]
10435 fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10436 // For an out-of-range index, the helper raises the structural
10437 // `LispError::TemplateInvariant` variant with the caller-
10438 // supplied `SubstBadIndex` kind constructor applied to the bad
10439 // index. Pins the post-lift emission shape (variant identity
10440 // + the kind constructor threaded with the actual idx); a
10441 // regression that drops the idx payload (e.g., via a `usize ->
10442 // ()` projection) or hard-codes a different kind at the helper
10443 // boundary fails-loudly here. Fail-before-pass-after: this
10444 // assert is contradicted by the pre-lift code path (which
10445 // never called `resolve_bound_arg` because it didn't exist),
10446 // ratifies the post-lift one.
10447 let args: Vec<Sexp> = Vec::new();
10448 let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10449 .expect_err("out-of-range lookup must error");
10450 match err {
10451 LispError::TemplateInvariant { macro_name, kind } => {
10452 assert_eq!(macro_name, "test-macro");
10453 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10454 }
10455 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10456 }
10457 }
10458
10459 #[test]
10460 fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10461 // Path-uniformity for the per-call-site kind constructor: the
10462 // SAME out-of-range idx via the `SpliceBadIndex` constructor
10463 // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10464 // `SubstBadIndex(7)` variant. Pins that the constructor is
10465 // chosen per call site (not hard-coded at the helper boundary),
10466 // closing the structural matrix `{Subst, Splice} × {in-range,
10467 // out-of-range}` the two production arms span across the
10468 // bytecode-runtime's bound-arg-by-index reads. A regression
10469 // that hard-codes a single kind at the helper boundary would
10470 // emit the same variant identity for both call sites and
10471 // fail-loudly here.
10472 let args: Vec<Sexp> = Vec::new();
10473 let err = resolve_bound_arg(
10474 &args,
10475 7,
10476 "test-macro",
10477 TemplateInvariantKind::SpliceBadIndex,
10478 )
10479 .expect_err("out-of-range lookup must error");
10480 match err {
10481 LispError::TemplateInvariant { macro_name, kind } => {
10482 assert_eq!(macro_name, "test-macro");
10483 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10484 }
10485 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10486 }
10487 }
10488
10489 #[test]
10490 fn resolve_bound_arg_threads_macro_name_verbatim() {
10491 // Path-uniformity for the `macro_name` slot: the helper threads
10492 // the caller's borrow into the variant's owned `String` slot
10493 // verbatim. Pin two distinct macro names route through with no
10494 // mutual interference — a regression that hard-codes a single
10495 // macro_name at the helper boundary or swaps the parameter
10496 // ordering fails-loudly here. Same posture as
10497 // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10498 // pins the typed `stage` slot in the disk-persistence sibling
10499 // lift.
10500 let args: Vec<Sexp> = Vec::new();
10501 for name in ["wrap", "call-macro", "obs"] {
10502 let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10503 .expect_err("out-of-range lookup must error");
10504 match err {
10505 LispError::TemplateInvariant { macro_name, kind } => {
10506 assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10507 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10508 }
10509 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10510 }
10511 }
10512 }
10513
10514 #[test]
10515 fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10516 // Edge case: idx 0 with a single-element args_by_index returns
10517 // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10518 // — a regression that off-by-ones the lookup (e.g., `get(idx +
10519 // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10520 // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10521 // with_subst_kind_emits_typed_invariant` test.
10522 let args = vec![Sexp::int(42)];
10523 let got = resolve_bound_arg(&args, 0, "m", |_| {
10524 panic!("kind constructor must not fire on the in-range path")
10525 })
10526 .expect("idx-0 lookup must succeed");
10527 assert!(std::ptr::eq(got, &args[0]));
10528 assert_eq!(*got, Sexp::int(42));
10529 }
10530
10531 #[test]
10532 fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10533 // Edge case: idx `len - 1` is the highest valid index. Pin
10534 // that it routes through the success arm (NOT the error arm),
10535 // closing the in-range surface end-to-end with the lower-
10536 // bound sibling. A regression that off-by-ones the upper
10537 // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10538 // would fail here.
10539 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10540 let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10541 panic!("kind constructor must not fire on the in-range path")
10542 })
10543 .expect("last-element lookup must succeed");
10544 assert!(std::ptr::eq(got, args.last().unwrap()));
10545 assert_eq!(*got, Sexp::int(3));
10546 }
10547
10548 #[test]
10549 fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10550 // Boundary case: idx EQUAL to `args.len()` is out-of-range
10551 // (since `get` is 0-indexed). Pin that this routes through
10552 // the error arm with the kind constructor applied to the
10553 // EXACT idx that was tried. A regression that off-by-ones
10554 // the boundary (e.g., admits `idx == len`) would fail here.
10555 // This is the canonical off-by-one trap; the helper's
10556 // contract pins it at the variant-construction boundary.
10557 let args = vec![Sexp::int(1)];
10558 let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10559 .expect_err("idx == len must error");
10560 match err {
10561 LispError::TemplateInvariant { kind, .. } => {
10562 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10563 }
10564 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10565 }
10566 }
10567
10568 #[test]
10569 fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10570 // Boundary case: an empty `args_by_index` slice rejects every
10571 // idx (including 0). Pin that the helper's emission shape is
10572 // uniform regardless of which out-of-range idx fires the
10573 // rejection — `SubstBadIndex(0)` for an empty slice is the
10574 // bytecode-runtime mirror of a zero-arity macro template
10575 // referencing the 0-th param.
10576 let args: Vec<Sexp> = Vec::new();
10577 let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10578 .expect_err("empty slice rejects every idx");
10579 match err {
10580 LispError::TemplateInvariant { macro_name, kind } => {
10581 assert_eq!(macro_name, "zero-arity");
10582 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10583 }
10584 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10585 }
10586 }
10587
10588 #[test]
10589 fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10590 // End-to-end path-uniformity: a `Subst(99)` op against a
10591 // zero-arity macro routes the bytecode-runtime's bound-arg
10592 // lookup through `resolve_bound_arg` with the
10593 // `SubstBadIndex` constructor, emitting the structural
10594 // variant with `kind: SubstBadIndex(99)`. The pre-lift
10595 // sibling test `apply_compiled_subst_bad_idx_routes_through_
10596 // template_invariant_violation` pins that the same input
10597 // routes through `template_invariant_violation`; this test
10598 // pins that BOTH still hold under the post-lift composition
10599 // — `resolve_bound_arg` calls `template_invariant_violation`
10600 // internally on the rejection arm. A regression that drifts
10601 // ONE arm's projection from the other (e.g., swaps the
10602 // constructor at one call site, or short-circuits the
10603 // composition) would fail here.
10604 let tmpl = CompiledTemplate {
10605 ops: vec![TemplateOp::Subst(99)],
10606 };
10607 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10608 .expect_err("bad idx must error");
10609 match err {
10610 LispError::TemplateInvariant { macro_name, kind } => {
10611 assert_eq!(macro_name, "test-macro");
10612 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10613 }
10614 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10615 }
10616 }
10617
10618 #[test]
10619 fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10620 // Sibling end-to-end path-uniformity for the `Splice` arm:
10621 // the post-lift composition routes a `Splice(42)` op through
10622 // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10623 // emitting `kind: SpliceBadIndex(42)`. Together with the
10624 // `Subst` sibling test above, this pins the structural matrix
10625 // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10626 // the public `apply_compiled` surface, so a regression that
10627 // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10628 // accidentally emits `SubstBadIndex` after a copy-paste
10629 // refactor) fails-loudly here.
10630 let tmpl = CompiledTemplate {
10631 ops: vec![TemplateOp::Splice(42)],
10632 };
10633 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10634 .expect_err("bad splice idx must error");
10635 match err {
10636 LispError::TemplateInvariant { macro_name, kind } => {
10637 assert_eq!(macro_name, "call-macro");
10638 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10639 }
10640 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10641 }
10642 }
10643
10644 #[test]
10645 fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10646 // Positive control: a `Subst(0)` op against a one-arg macro
10647 // routes through `resolve_bound_arg`'s success arm and the
10648 // `Subst` post-lookup verb (clone + push) emits the bound
10649 // value verbatim. Pin the post-lift composition's success
10650 // path: the clone-and-push semantics live at the call site
10651 // (NOT in `resolve_bound_arg`, which only borrows), and a
10652 // regression that drifts the borrow contract (e.g., the
10653 // helper clones internally + the call site clones again)
10654 // would still pass observationally but would regress the
10655 // hot-path allocation count.
10656 let params = MacroParams {
10657 required: vec!["x".into()],
10658 optional: Vec::new(),
10659 rest: None,
10660 };
10661 let tmpl = CompiledTemplate {
10662 ops: vec![TemplateOp::Subst(0)],
10663 };
10664 let out = apply_compiled("id", ¶ms, &tmpl, &[Sexp::int(42)])
10665 .expect("in-range Subst must succeed");
10666 assert_eq!(out, Sexp::int(42));
10667 }
10668
10669 // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10670 //
10671 // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10672 // projection that appeared at FOUR sites inside `apply_compiled`'s
10673 // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10674 // named primitive. The expect message names the bytecode-runtime
10675 // invariant ("at least one stack frame during op-loop") so a
10676 // regression that drifts the loop's frame management (a new op that
10677 // pops without pushing, an early-return that bypasses EndList's
10678 // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10679 // These tests pin the projection's contract directly; the existing
10680 // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10681 // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10682 // guards proving the four sites still emit the canonical bytecode-
10683 // runtime output across the lift.
10684
10685 #[test]
10686 fn current_builder_mut_returns_the_top_frame_reference() {
10687 // The simplest projection: on a single-frame stack, the helper
10688 // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10689 // projection's identity end-to-end via a push that mutates
10690 // through the borrow and observe the original frame carries the
10691 // pushed value back.
10692 let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10693 current_builder_mut(&mut stack).push(Sexp::int(42));
10694 assert_eq!(stack.len(), 1);
10695 assert_eq!(stack[0], vec![Sexp::int(42)]);
10696 }
10697
10698 #[test]
10699 fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10700 // The projection MUST target the topmost frame, not the bottom
10701 // one — every `TemplateOp::BeginList` pushes a fresh frame the
10702 // subsequent ops emit into, and a regression that flipped the
10703 // projection to `first_mut` (or to a fixed bottom-frame
10704 // reference) would silently smear all op output into the
10705 // outermost result. Pin path-uniformity with the bytecode-
10706 // runtime's mid-list emission posture: with three frames on
10707 // the stack (one outer + two pending lists), the helper
10708 // returns a borrow into the third frame, leaving frames 0 and
10709 // 1 untouched.
10710 let mut stack: Vec<Vec<Sexp>> = vec![
10711 vec![Sexp::symbol("outer")],
10712 vec![Sexp::symbol("inner-a")],
10713 vec![Sexp::symbol("inner-b")],
10714 ];
10715 current_builder_mut(&mut stack).push(Sexp::int(99));
10716 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10717 assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10718 assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10719 }
10720
10721 #[test]
10722 fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10723 // Structural identity binding the lift to its pre-lift inline
10724 // shape: `current_builder_mut(&mut stack)` IS
10725 // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10726 // pointing at the same allocation. Pin pointer equality via
10727 // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10728 // out any allocation-shape drift across the lift.
10729 let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10730 let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10731 let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10732 assert!(
10733 std::ptr::eq(via_lift_ptr, via_inline_ptr),
10734 "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10735 );
10736 }
10737
10738 #[test]
10739 #[should_panic(
10740 expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10741 )]
10742 fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10743 // The bytecode-runtime invariant is encoded in the expect
10744 // message: an empty stack at the projection boundary is
10745 // structurally unreachable inside `apply_compiled`'s op-loop
10746 // (the outermost frame is seeded at entry and every BeginList
10747 // / EndList pair preserves the count >= 1). Pin that the
10748 // NAMED invariant fires on the failure path so a regression
10749 // that drifts the loop's frame management surfaces a
10750 // diagnostic-grade panic rather than a silent unwrap over
10751 // `None`. Authoring tools / future debug-mode hooks can
10752 // pattern-match on the named invariant string instead of
10753 // tracking down an unnamed unwrap site.
10754 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10755 let _ = current_builder_mut(&mut empty);
10756 }
10757
10758 #[test]
10759 fn current_builder_mut_routes_apply_compiled_literal_emit() {
10760 // End-to-end path-uniformity guard: a single-op program
10761 // `TemplateOp::Literal(s)` routes its push through
10762 // `current_builder_mut(&mut stack)` and the literal lands in
10763 // the outermost frame. After the op-loop completes the outer
10764 // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10765 // containing exactly one element, which `apply_compiled`'s
10766 // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10767 // the bound value. Pre-lift the same emission ran through
10768 // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10769 // runs through `current_builder_mut(&mut stack).push(s.clone())`
10770 // — the byte-identical outcome pins that the Literal arm's
10771 // routing through the new projection preserves the bytecode-
10772 // runtime's emission shape.
10773 let tmpl = CompiledTemplate {
10774 ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10775 };
10776 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10777 .expect("literal-only template must succeed");
10778 assert_eq!(out, Sexp::symbol("hello"));
10779 }
10780
10781 #[test]
10782 fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10783 // End-to-end path-uniformity guard for the post-EndList
10784 // parent-fold push: `(BeginList, Literal(a), Literal(b),
10785 // EndList)` builds an inner frame `[a, b]`, pops it on
10786 // EndList, then pushes `Sexp::List([a, b])` into the parent
10787 // (outer) frame via `current_builder_mut`. The outermost
10788 // `stack.pop()` then surfaces that list as the bound result.
10789 // Pre-lift the parent-fold push ran through
10790 // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10791 // lift it runs through `current_builder_mut(&mut stack).
10792 // push(Sexp::List(items))` — pin the byte-identical outcome
10793 // so a regression that drifts the parent-fold target (e.g.,
10794 // pushes onto the just-popped frame's pointer instead of the
10795 // new top) fails loudly here.
10796 let tmpl = CompiledTemplate {
10797 ops: vec![
10798 TemplateOp::BeginList,
10799 TemplateOp::Literal(Sexp::symbol("a")),
10800 TemplateOp::Literal(Sexp::symbol("b")),
10801 TemplateOp::EndList,
10802 ],
10803 };
10804 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10805 .expect("BeginList/EndList template must succeed");
10806 assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10807 }
10808
10809 #[test]
10810 fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10811 // End-to-end path-uniformity guard for BOTH index-reading
10812 // arms routing through the lifted projection: a one-required +
10813 // one-rest macro `(call f &rest args)` with template
10814 // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10815 // Subst's clone-and-push AND Splice's splice-value-into
10816 // emit-paths against the current builder via
10817 // `current_builder_mut(&mut stack)`. The composed result is
10818 // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10819 // Splice flattens `args = (1 2 3)` — and the byte-identical
10820 // outcome pins that BOTH Subst and Splice arms' emits route
10821 // through the SHARED projection. Sibling to
10822 // `apply_compiled_splice_in_range_routes_past_resolve_bound
10823 // _arg_into_splice_value_into` which already exercises this
10824 // shape end-to-end; the addition here is the path-uniformity
10825 // anchor for the `current_builder_mut` lift specifically.
10826 let params = MacroParams {
10827 required: vec!["f".into()],
10828 optional: Vec::new(),
10829 rest: Some("args".into()),
10830 };
10831 let tmpl = CompiledTemplate {
10832 ops: vec![
10833 TemplateOp::BeginList,
10834 TemplateOp::Subst(0),
10835 TemplateOp::Splice(1),
10836 TemplateOp::EndList,
10837 ],
10838 };
10839 let out = apply_compiled(
10840 "call",
10841 ¶ms,
10842 &tmpl,
10843 &[
10844 Sexp::symbol("foo"),
10845 Sexp::int(1),
10846 Sexp::int(2),
10847 Sexp::int(3),
10848 ],
10849 )
10850 .expect("Subst + Splice template must succeed");
10851 assert_eq!(
10852 out,
10853 Sexp::List(vec![
10854 Sexp::symbol("foo"),
10855 Sexp::int(1),
10856 Sexp::int(2),
10857 Sexp::int(3),
10858 ])
10859 );
10860 }
10861
10862 #[test]
10863 fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10864 // Positive control for the `Splice` arm: a `&rest` macro that
10865 // splices a bound list routes through `resolve_bound_arg`'s
10866 // success arm and the `Splice` post-lookup verb
10867 // (`splice_value_into`) flattens the bound list into the
10868 // builder. Pin the composition's success path end-to-end:
10869 // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10870 // the outer builder's `(call 1 2 3)` shape — the same output
10871 // `rest_param_splices_with_at` pins through the public
10872 // surface, here pinned with the bytecode-runtime composition
10873 // exposed directly.
10874 let params = MacroParams {
10875 required: vec!["f".into()],
10876 optional: Vec::new(),
10877 rest: Some("args".into()),
10878 };
10879 let tmpl = CompiledTemplate {
10880 ops: vec![
10881 TemplateOp::BeginList,
10882 TemplateOp::Subst(0),
10883 TemplateOp::Splice(1),
10884 TemplateOp::EndList,
10885 ],
10886 };
10887 let out = apply_compiled(
10888 "call",
10889 ¶ms,
10890 &tmpl,
10891 &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10892 )
10893 .expect("in-range Splice must succeed");
10894 assert_eq!(
10895 out,
10896 Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10897 );
10898 }
10899
10900 // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10901 //
10902 // `pop_builder_frame(stack, macro_name, kind)` lifts the
10903 // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10904 // kind))?` chain that recurred at two sites inside `apply_compiled`
10905 // (the `EndList` arm + the post-loop final pop) into ONE named
10906 // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10907 // `current_builder_mut` (the top-frame-borrow projection — the same
10908 // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10909 // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10910 // also routes through `TemplateInvariantKind` as the per-call-site
10911 // rejection identity). These tests pin the primitive's contract
10912 // directly; the existing `apply_compiled_*` tests are the path-
10913 // uniformity guards proving the two sites route through it without
10914 // behavior drift.
10915
10916 #[test]
10917 fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
10918 // Happy path: a two-frame stack pops the topmost frame off and
10919 // returns it AS-IS while shrinking the stack by exactly one
10920 // element. Pin both the return value (the popped frame's
10921 // contents are byte-identical to what was pushed) AND the
10922 // mutation (`stack.len()` drops from 2 to 1).
10923 let mut stack: Vec<Vec<Sexp>> = vec![
10924 vec![Sexp::symbol("outer")],
10925 vec![Sexp::int(1), Sexp::int(2)],
10926 ];
10927 let popped =
10928 pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
10929 .expect("non-empty stack must pop cleanly");
10930 assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
10931 assert_eq!(stack.len(), 1);
10932 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10933 }
10934
10935 #[test]
10936 fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
10937 // Empty-stack rejection: an empty stack flows through the
10938 // `EndListEmptyStack` kind constructor into a structural
10939 // `LispError::TemplateInvariant { macro_name, kind:
10940 // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
10941 // the same input would route through the inline
10942 // `stack.pop().ok_or_else(|| template_invariant_violation(_,
10943 // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
10944 // it routes through ONE named primitive both pop-emitting
10945 // sites share, and a regression that drops the kind threading
10946 // (e.g. unifies both kinds into one constant) fails here.
10947 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10948 let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
10949 .expect_err("empty stack must reject");
10950 match err {
10951 LispError::TemplateInvariant { macro_name, kind } => {
10952 assert_eq!(macro_name, "wrap");
10953 assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
10954 }
10955 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10956 }
10957 }
10958
10959 #[test]
10960 fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
10961 // Path-uniformity across the closed-set of pop-emitting kinds:
10962 // the same primitive threads `FinalNoValue` verbatim through
10963 // its `TemplateInvariantKind` slot, with the macro_name
10964 // identity preserved across the call boundary. Sibling of the
10965 // `EndListEmptyStack` test above — together they pin the
10966 // primitive's closed-set posture (BOTH reachable
10967 // pop-emitting kinds route through the same primitive, neither
10968 // is hard-coded), so a future `TemplateInvariantKind` variant
10969 // added for a new pop-emitting op (e.g. a hypothetical
10970 // `EndManyEmptyStack`) extends the primitive's reachability
10971 // mechanically by passing the new kind through the same slot.
10972 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10973 let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
10974 .expect_err("empty stack must reject");
10975 match err {
10976 LispError::TemplateInvariant { macro_name, kind } => {
10977 assert_eq!(macro_name, "id");
10978 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10979 }
10980 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10981 }
10982 }
10983
10984 #[test]
10985 fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
10986 // Closed-set posture check: even kinds the production
10987 // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
10988 // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
10989 // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
10990 // this primitive's typed slot — they are NOT reachable here
10991 // from the production loop, but `TemplateInvariantKind` does
10992 // not distinguish "indexed" kinds from "stack-gate" kinds at
10993 // the helper's signature, so a regression that special-cases
10994 // one kind family would be a silent type-narrowing the closed-
10995 // set typed enum was lifted to prevent. Pin the universal
10996 // routing: ANY kind variant feeds through the primitive's
10997 // `kind` slot identically. Sibling assertion to
10998 // `template_invariant_violation_threads_subst_idx_through_typed_variant`
10999 // — same compose-the-kind-into-the-variant contract, one
11000 // composition step further down the substrate stack.
11001 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11002 let err = pop_builder_frame(
11003 &mut empty,
11004 "compose",
11005 TemplateInvariantKind::SubstBadIndex(42),
11006 )
11007 .expect_err("empty stack must reject regardless of kind family");
11008 match err {
11009 LispError::TemplateInvariant { macro_name, kind } => {
11010 assert_eq!(macro_name, "compose");
11011 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11012 }
11013 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11014 }
11015 }
11016
11017 #[test]
11018 fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11019 // Structural-identity binding the lift to its pre-lift inline
11020 // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11021 // `stack.pop().ok_or_else(|| template_invariant_violation(
11022 // macro_name, kind))?` — both reachable arms (success +
11023 // failure) must produce byte-identical outcomes. The success
11024 // arm checks the popped Vec contents AND the post-pop stack
11025 // length match the inline path; the failure arm checks the
11026 // emitted variant's identity AND its `macro_name` / `kind`
11027 // slots match the inline path. A regression that drifts the
11028 // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11029 // or a kind-rewrite at the helper boundary) fails on at least
11030 // one of the two arms.
11031 // Success arm:
11032 let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11033 let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11034 let via_lift = pop_builder_frame(
11035 &mut stack_lift,
11036 "macro",
11037 TemplateInvariantKind::EndListEmptyStack,
11038 )
11039 .expect("non-empty stack pops cleanly through lift");
11040 let via_inline = stack_inline.pop().ok_or_else(|| {
11041 template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11042 });
11043 assert_eq!(
11044 via_lift,
11045 via_inline.unwrap(),
11046 "popped frame must be byte-identical across lift vs inline"
11047 );
11048 assert_eq!(
11049 stack_lift.len(),
11050 stack_inline.len(),
11051 "post-pop stack length must be byte-identical across lift vs inline"
11052 );
11053 // Failure arm:
11054 let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11055 let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11056 let err_lift = pop_builder_frame(
11057 &mut empty_lift,
11058 "macro",
11059 TemplateInvariantKind::FinalNoValue,
11060 )
11061 .expect_err("empty stack rejects through lift");
11062 let err_inline = empty_inline
11063 .pop()
11064 .ok_or_else(|| {
11065 template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11066 })
11067 .expect_err("empty stack rejects through inline");
11068 match (err_lift, err_inline) {
11069 (
11070 LispError::TemplateInvariant {
11071 macro_name: m_lift,
11072 kind: k_lift,
11073 },
11074 LispError::TemplateInvariant {
11075 macro_name: m_inline,
11076 kind: k_inline,
11077 },
11078 ) => {
11079 assert_eq!(m_lift, m_inline);
11080 assert_eq!(k_lift, k_inline);
11081 }
11082 (l, i) => panic!(
11083 "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11084 ),
11085 }
11086 }
11087
11088 #[test]
11089 fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11090 // End-to-end path-uniformity guard for the `EndList` arm: a
11091 // `(BeginList, Literal(x), EndList)` program pushes a child
11092 // frame, populates it with one literal, then routes the child
11093 // frame OUT of the stack via `pop_builder_frame` (kind
11094 // `EndListEmptyStack` — unreachable on this valid input, but
11095 // the kind threads through the primitive identically). The
11096 // post-pop verb (`Sexp::List(items)` push into the parent via
11097 // `current_builder_mut`) yields the same `Sexp::List([x])`
11098 // shape the consumer projected pre-lift. Pin the byte-
11099 // identical outcome so a regression that drifts the EndList
11100 // arm's routing (e.g. swaps the kind constructor, or routes
11101 // through a different stack-mutating primitive) fails loudly
11102 // here. Sibling of
11103 // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11104 // — that test pins the parent-fold PUSH; this test pins the
11105 // child-frame POP that immediately precedes it. Together the
11106 // two close the EndList arm's path-uniformity across the
11107 // pop-then-push composition.
11108 let tmpl = CompiledTemplate {
11109 ops: vec![
11110 TemplateOp::BeginList,
11111 TemplateOp::Literal(Sexp::symbol("only")),
11112 TemplateOp::EndList,
11113 ],
11114 };
11115 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11116 .expect("BeginList/EndList one-literal template must succeed");
11117 assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11118 }
11119
11120 #[test]
11121 fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11122 // End-to-end path-uniformity guard for the post-loop final
11123 // pop: a single-op `Literal(s)` program emits `s` into the
11124 // outermost (seed) frame, then the post-loop tail routes the
11125 // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11126 // — unreachable on this valid input). The post-pop arity gate
11127 // (`top.len() == 1 { top.remove(0) }`) projects the literal
11128 // back as the bound value. Pre-lift the same emission ran
11129 // through the inline `stack.pop().ok_or_else(|| template_
11130 // invariant_violation(_, FinalNoValue))?` chain at the
11131 // post-loop tail; post-lift it routes through ONE named
11132 // primitive the EndList arm ALSO routes through, and the
11133 // single-literal outcome is byte-identical across both code
11134 // paths. Sibling of
11135 // `current_builder_mut_routes_apply_compiled_literal_emit` —
11136 // that test pins the EMIT into the seed frame; this test pins
11137 // the POP of the same seed frame at the post-loop tail.
11138 // Together the two close the Literal-only program's path-
11139 // uniformity across the emit-then-consume composition.
11140 let tmpl = CompiledTemplate {
11141 ops: vec![TemplateOp::Literal(Sexp::int(123))],
11142 };
11143 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11144 .expect("literal-only template must succeed");
11145 assert_eq!(out, Sexp::int(123));
11146 }
11147
11148 // ── MacroParams: the typed param-list primitive ─────────────────────
11149 //
11150 // `parse_params` now yields a `MacroParams { required, optional, rest }`
11151 // whose shape makes the canonical lambda-list ordering (required →
11152 // optional → rest, "&rest is last + at-most-one", "&optional at most
11153 // once") structural rather than a construction discipline a `Vec<Param>`
11154 // only happened to uphold. These tests pin the parser's mapping into the
11155 // typed shape, the flat-index contract `names()` exposes to the template
11156 // bytecode, and the single positional binder `bind()` both expansion
11157 // strategies now route through. The end-to-end `rest_param_splices_with_at`
11158 // and `compiled_template_matches_substitute_path` tests above are the
11159 // path-uniformity guards proving both strategies still agree.
11160
11161 #[test]
11162 fn parse_params_maps_required_then_rest_into_typed_shape() {
11163 // `(a b &rest c)` — two required, one rest. The rest name lands in
11164 // the `Option`, never in `required`.
11165 let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11166 assert_eq!(
11167 params,
11168 MacroParams {
11169 required: vec!["a".into(), "b".into()],
11170 optional: Vec::new(),
11171 rest: Some("c".into()),
11172 }
11173 );
11174 }
11175
11176 #[test]
11177 fn parse_params_rest_absent_leaves_none() {
11178 // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11179 // no representation in which a rest-less list carries a stray rest.
11180 let params = parse_params(&read("x y").unwrap()).unwrap();
11181 assert_eq!(
11182 params,
11183 MacroParams {
11184 required: vec!["x".into(), "y".into()],
11185 optional: Vec::new(),
11186 rest: None,
11187 }
11188 );
11189 }
11190
11191 #[test]
11192 fn parse_params_maps_optional_section_between_required_and_rest() {
11193 // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11194 // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11195 // marker switches collection from `required` to `optional`; `&rest`
11196 // remains terminal.
11197 let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11198 assert_eq!(
11199 params,
11200 MacroParams {
11201 required: vec!["a".into()],
11202 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11203 rest: Some("d".into()),
11204 }
11205 );
11206 }
11207
11208 #[test]
11209 fn parse_params_optional_with_no_rest_leaves_rest_none() {
11210 // `(&optional x)` — a leading `&optional` (zero required) with no
11211 // rest. `required` is empty, `x` is the sole optional, `rest` None.
11212 let params = parse_params(&read("&optional x").unwrap()).unwrap();
11213 assert_eq!(
11214 params,
11215 MacroParams {
11216 required: Vec::new(),
11217 optional: vec![OptionalParam::bare("x")],
11218 rest: None,
11219 }
11220 );
11221 }
11222
11223 #[test]
11224 fn parse_params_rejects_repeated_optional_marker() {
11225 // `(a &optional b &optional c)` — a second `&optional` is
11226 // unrepresentable (one flat optional section), so the gate REJECTS
11227 // rather than binding args to a marker symbol named `&optional`. The
11228 // two marker positions (1 and 3) are named.
11229 let err = parse_params(&read("a &optional b &optional c").unwrap())
11230 .expect_err("repeated &optional must error");
11231 assert!(
11232 matches!(
11233 err,
11234 LispError::OptionalMarkerRepeated {
11235 first_position: 1,
11236 second_position: 3,
11237 }
11238 ),
11239 "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11240 );
11241 }
11242
11243 #[test]
11244 fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11245 // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11246 // `&optional y` tail is REJECTED as trailing tokens (not silently
11247 // dropped, and not a repeated-optional error: the rest gate fires
11248 // first). Pins the interaction the prior run (3627426) signposted.
11249 let err = parse_params(&read("&rest xs &optional y").unwrap())
11250 .expect_err("tokens after &rest <name> must error");
11251 assert!(
11252 matches!(err, LispError::RestParamTrailingTokens { .. }),
11253 "expected RestParamTrailingTokens, got: {err:?}"
11254 );
11255 }
11256
11257 #[test]
11258 fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11259 // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11260 // depends on: required names at 0.., then optional names, then the
11261 // rest name last.
11262 let params = MacroParams {
11263 required: vec!["a".into(), "b".into()],
11264 optional: vec![OptionalParam::bare("c")],
11265 rest: Some("d".into()),
11266 };
11267 assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11268 // Optional names occupy the indices immediately after the required run.
11269 assert_eq!(params.names()[params.required.len()], "c");
11270 // The rest name is last, after required + optional — i.e. at the
11271 // structural `fixed_arity()` boundary the typed primitive names.
11272 assert_eq!(params.names()[params.fixed_arity()], "d");
11273 }
11274
11275 // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11276 // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11277 // marker algebra ────────────────────────────────────────────────
11278 //
11279 // The three `pub const`s close the Common-Lisp lambda-list keyword
11280 // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11281 // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11282 // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11283 // canonical `'&'` char shared as the LEAD byte of both markers.
11284 // Pre-lift the same two `&'static str` markers lived as two inline
11285 // `s == "..."` comparisons at `parse_params` — post-lift both
11286 // comparisons route through the typed constants so a delimiter swap
11287 // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11288 // lands at ONE constant on the typed algebra.
11289 //
11290 // These pins cover: (a) the exact `&'static str` / `char` values,
11291 // (b) the structural round-trip law binding each `&'static str`
11292 // marker to its shared `char` LEAD byte, (c) the pairwise
11293 // disjointness of the two `&'static str` markers, (d) the cross-
11294 // axis disjointness of the `char` LEAD byte against every sibling
11295 // outer-marker `char` the substrate's other closed-set algebras
11296 // specialise on, and (e) the path-uniformity of `parse_params`
11297 // through the typed constants.
11298
11299 #[test]
11300 fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11301 // Pins the constant's exact `&'static str` bytes so a typo
11302 // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11303 // surfaces immediately. Sibling-shape pin to
11304 // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11305 // on the peer `&optional` axis.
11306 assert_eq!(
11307 MacroParams::REST_MARKER,
11308 "&rest",
11309 "MacroParams::REST_MARKER drifted from the substrate- \
11310 canonical CL lambda-list `&rest` marker — the parser's \
11311 `parse_params` rest-slot dispatch AND every downstream \
11312 authoring / rendering surface binds to this ONE typed \
11313 constant.",
11314 );
11315 }
11316
11317 #[test]
11318 fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11319 // Pins the constant's exact `&'static str` bytes so a typo
11320 // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11321 // redefinition surfaces immediately. Sibling-shape pin to
11322 // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11323 // on the peer `&rest` axis.
11324 assert_eq!(
11325 MacroParams::OPTIONAL_MARKER,
11326 "&optional",
11327 "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11328 canonical CL lambda-list `&optional` marker — the parser's \
11329 `parse_params` optional-section dispatch AND every \
11330 downstream authoring / rendering surface binds to this \
11331 ONE typed constant.",
11332 );
11333 }
11334
11335 #[test]
11336 fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11337 // Pins the constant's exact `char` value so a typo (`'#'`,
11338 // `'@'`, `'!'`) or an accidental redefinition surfaces
11339 // immediately. Sibling-shape pin to
11340 // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11341 // `atom_bool_literal_lead_projects_canonical_hash_char`,
11342 // `atom_str_delimiter_projects_canonical_double_quote_char`
11343 // on the peer per-role LEAD-byte axes across the substrate's
11344 // closed-set outer algebras.
11345 assert_eq!(
11346 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11347 '&',
11348 "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11349 canonical `&` LEAD byte — the CL lambda-list-keyword \
11350 family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11351 typed constant as their common LEAD byte.",
11352 );
11353 }
11354
11355 #[test]
11356 fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11357 // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11358 // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11359 // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11360 // binding the two typed constants on the [`MacroParams`]
11361 // algebra. A regression that renames the `&'static str` (e.g.
11362 // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11363 // `'#'` for the `#!` shebang lead) without updating the other
11364 // fails HERE. Sibling-shape pin to
11365 // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11366 // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11367 assert!(
11368 MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11369 "MacroParams::REST_MARKER `{}` does NOT start with \
11370 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11371 typed constants have drifted apart on the [`MacroParams`] \
11372 algebra; the CL lambda-list-keyword family disjointness \
11373 contract can no longer bind to ONE shared LEAD byte.",
11374 MacroParams::REST_MARKER,
11375 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11376 );
11377 }
11378
11379 #[test]
11380 fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11381 // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11382 // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11383 // on the `&optional` axis. Both `&'static str` markers of the
11384 // CL lambda-list-keyword family MUST share the canonical `char`
11385 // LEAD byte so the typed-marker disjointness contract can bind
11386 // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11387 assert!(
11388 MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11389 "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11390 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11391 typed constants have drifted apart on the [`MacroParams`] \
11392 algebra; the CL lambda-list-keyword family disjointness \
11393 contract can no longer bind to ONE shared LEAD byte.",
11394 MacroParams::OPTIONAL_MARKER,
11395 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11396 );
11397 }
11398
11399 #[test]
11400 fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11401 // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11402 // the CL lambda-list-keyword algebra MUST differ so the
11403 // parser's typed dispatch cascade at `parse_params` (which
11404 // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11405 // silently route both dispatches through the same arm. A
11406 // regression that aliases the two markers (e.g. both to
11407 // `"&rest"` after a typo) would silently drop the optional
11408 // section's structural distinction — every `&optional` name
11409 // would misclassify as a rest-slot marker.
11410 assert_ne!(
11411 MacroParams::REST_MARKER,
11412 MacroParams::OPTIONAL_MARKER,
11413 "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11414 typed dispatch cascade at `parse_params` can no longer \
11415 distinguish the rest-slot boundary from the optional- \
11416 section boundary.",
11417 );
11418 }
11419
11420 #[test]
11421 fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11422 // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11423 // MUST NOT alias any sibling outer-marker `char` on the
11424 // substrate's other closed-set algebras — the Atom-payload
11425 // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11426 // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11427 // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11428 // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11429 // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11430 // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11431 // collision would silently break the reader's outer dispatch:
11432 // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11433 // with whichever marker it aliased. Sibling-shape pin to
11434 // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11435 // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11436 // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11437 // axis. A future outer-marker extension that collided with
11438 // `'&'` fails HERE at the cross-axis enumeration.
11439 use crate::ast::{Atom, QuoteForm, Sexp};
11440
11441 assert_ne!(
11442 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11443 Atom::STR_DELIMITER,
11444 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11445 bare `&rest` at a param-list position would ambiguously \
11446 begin a lambda-list keyword AND open a string.",
11447 );
11448 assert_ne!(
11449 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11450 Atom::STR_ESCAPE_LEAD,
11451 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11452 the reader's Str-escape lead byte would alias the CL \
11453 lambda-list-keyword LEAD byte.",
11454 );
11455 assert_ne!(
11456 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11457 Atom::KEYWORD_MARKER_LEAD,
11458 "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11459 — a bare `&rest` at a param-list position would \
11460 ambiguously begin a lambda-list keyword AND begin an \
11461 `:foo` keyword.",
11462 );
11463 assert_ne!(
11464 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11465 Atom::BOOL_LITERAL_LEAD,
11466 "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11467 a bare `&rest` at a param-list position would ambiguously \
11468 begin a lambda-list keyword AND classify as a Bool prefix.",
11469 );
11470 assert_ne!(
11471 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11472 Sexp::LIST_OPEN,
11473 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11474 `&rest` at a param-list position would ambiguously begin \
11475 a lambda-list keyword AND open a list.",
11476 );
11477 assert_ne!(
11478 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11479 Sexp::LIST_CLOSE,
11480 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11481 `&rest` at a param-list position would ambiguously begin \
11482 a lambda-list keyword AND close a list.",
11483 );
11484 assert_ne!(
11485 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11486 Sexp::COMMENT_LEAD,
11487 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11488 bare `&rest` at a param-list position would ambiguously \
11489 begin a lambda-list keyword AND begin a comment.",
11490 );
11491 assert_ne!(
11492 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11493 Sexp::COMMENT_TERM,
11494 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11495 reader's line-comment discard loop would terminate on the \
11496 SAME byte the parser's lambda-list-keyword LEAD dispatch \
11497 binds to.",
11498 );
11499 for qf in QuoteForm::ALL {
11500 assert_ne!(
11501 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11502 qf.lead_char(),
11503 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11504 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11505 param-list position would ambiguously begin a lambda- \
11506 list keyword AND begin a quote-family prefix.",
11507 );
11508 }
11509 assert_ne!(
11510 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11511 QuoteForm::SPLICE_DISCRIMINATOR,
11512 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11513 SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11514 peek byte would alias the CL lambda-list-keyword LEAD \
11515 byte.",
11516 );
11517 }
11518
11519 #[test]
11520 fn parse_params_recognizes_rest_marker_via_typed_constant() {
11521 // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11522 // `parse_params` MUST classify the typed constant
11523 // `MacroParams::REST_MARKER` as the rest-slot boundary —
11524 // authoring surfaces that assemble a param list through the
11525 // typed constant (rather than through an inline string literal
11526 // read from user source) must reach the SAME rest-slot binding
11527 // the reader-driven path does.
11528 //
11529 // The `read(REST_MARKER)` composition here is the load-bearing
11530 // structural link: we build the param-list source through the
11531 // typed constant, then parse it through the reader, then the
11532 // parser MUST bind `xs` at the `rest` slot. A regression that
11533 // drifts the parser's dispatch away from the typed constant
11534 // (e.g. re-inlines a string literal that goes stale against
11535 // the constant) fails HERE.
11536 let src = format!("a {} xs", MacroParams::REST_MARKER);
11537 let params = parse_params(&read(&src).unwrap()).unwrap();
11538 assert_eq!(
11539 params,
11540 MacroParams {
11541 required: vec!["a".into()],
11542 optional: Vec::new(),
11543 rest: Some("xs".into()),
11544 },
11545 "parse_params dispatch drifted away from REST_MARKER — the \
11546 typed constant no longer routes to the rest-slot arm.",
11547 );
11548 }
11549
11550 #[test]
11551 fn parse_params_recognizes_optional_marker_via_typed_constant() {
11552 // PATH-UNIFORMITY PIN: peer to
11553 // `parse_params_recognizes_rest_marker_via_typed_constant` on
11554 // the `&optional` axis. The parser's optional-section dispatch
11555 // at `parse_params` MUST classify the typed constant
11556 // `MacroParams::OPTIONAL_MARKER` as the section-switch
11557 // boundary that reroutes subsequent bare-symbol names from the
11558 // `required` bin to the `optional` bin.
11559 let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11560 let params = parse_params(&read(&src).unwrap()).unwrap();
11561 assert_eq!(
11562 params,
11563 MacroParams {
11564 required: vec!["a".into()],
11565 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11566 rest: None,
11567 },
11568 "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11569 the typed constant no longer routes to the optional- \
11570 section arm.",
11571 );
11572 }
11573
11574 // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11575 // the closed-set forced-arity ALL array + membership gate that closes
11576 // the CL lambda-list-keyword family at the typed [`MacroParams`]
11577 // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11578 // N]` forced-arity arrays across the substrate's other closed-set
11579 // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11580 // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11581 // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11582 // const` for the marker and every downstream family-wide contract
11583 // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11584 // gate) picks up the extension at ONE structural site.
11585 //
11586 // These pins cover: (a) the cardinality contract (the ALL array's
11587 // length matches the pre-lift per-role marker count), (b) the
11588 // ordering contract (each ALL entry matches its corresponding per-
11589 // role `pub const` by-index), (c) the family-wide structural round-
11590 // trip (every ALL element starts_with LEAD), (d) the family-wide
11591 // pairwise disjointness (every distinct ALL index pair yields
11592 // distinct markers), (e) the membership-gate acceptance side (every
11593 // ALL element classifies as `true`), and (f) the membership-gate
11594 // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11595 // names classify as `false`).
11596 //
11597 // Fail-before/pass-after: every test below references
11598 // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11599 // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11600 // on `MacroParams` before this lift — every assertion was a
11601 // compile-time error against the prior surface.
11602
11603 #[test]
11604 fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11605 // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11606 // entries — the two `&'static str` markers the parser's typed
11607 // dispatch specialises on today (`&rest` + `&optional`). A future
11608 // third marker (`&key`, `&aux`, `&body`) extends this arity to
11609 // 3 AND every downstream family-wide contract sweep picks up the
11610 // extension at ONE structural site. A regression that drops a
11611 // marker from the ALL array (silently narrowing the family) OR
11612 // aliases two markers to the same index (silently narrowing the
11613 // typed dispatch) fails HERE at the cardinality contract before
11614 // the structural round-trip / pairwise-disjointness contracts
11615 // even fire.
11616 assert_eq!(
11617 MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11618 2,
11619 "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11620 lambda-list-keyword family closure now names a different \
11621 number of markers than the two the parser's typed dispatch \
11622 specialises on.",
11623 );
11624 }
11625
11626 #[test]
11627 fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11628 // ORDERING PIN: each ALL entry matches its corresponding per-role
11629 // `pub const` by-index. A regression that swaps the ordering
11630 // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11631 // the cardinality + membership contracts intact but silently
11632 // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11633 // in canonical order (a future authoring surface that renders
11634 // "supported CL lambda-list keywords: &rest, &optional" would
11635 // silently render "supported CL lambda-list keywords: &optional,
11636 // &rest" instead). Pins the array's declaration-order binding
11637 // structurally so a reorder fails HERE at each element rather
11638 // than only at consumer sites downstream.
11639 assert_eq!(
11640 MacroParams::LAMBDA_LIST_KEYWORDS[0],
11641 MacroParams::REST_MARKER,
11642 "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11643 array's declaration-order binding to the per-role `pub \
11644 const` broke at the rest-slot marker slot.",
11645 );
11646 assert_eq!(
11647 MacroParams::LAMBDA_LIST_KEYWORDS[1],
11648 MacroParams::OPTIONAL_MARKER,
11649 "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11650 ALL array's declaration-order binding to the per-role \
11651 `pub const` broke at the optional-section marker slot.",
11652 );
11653 }
11654
11655 #[test]
11656 fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11657 // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11658 // array MUST start with the canonical LEAD byte. Where the two
11659 // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11660 // named ONE marker inline as a duplicate 3-line
11661 // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11662 // sweep routes the SAME contract through the closed-set ALL
11663 // array — a future third marker (`&key`, `&aux`, `&body`)
11664 // extends the array AND is automatically covered by this sweep
11665 // without adding a third `_prefixed_by_...` per-marker pin. The
11666 // per-marker pins stay as fine-grained fail-loud sites; this
11667 // pin is the algebra-wide closure the family-extending refactor
11668 // routes through.
11669 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11670 assert!(
11671 m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11672 "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11673 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11674 list-keyword family's structural round-trip contract \
11675 no longer binds every marker to its shared LEAD byte.",
11676 lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11677 );
11678 }
11679 }
11680
11681 #[test]
11682 fn macro_params_lambda_list_keywords_pairwise_distinct() {
11683 // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11684 // `(i, j)` in the ALL array yields distinct markers. Where the
11685 // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11686 // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11687 // family-wide sweep routes the SAME contract through the closed-
11688 // set ALL array so a future third marker automatically extends
11689 // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11690 // deriving per-marker `assert_ne!` calls. The pre-lift pin
11691 // stays as the fine-grained fail-loud site; this pin is the
11692 // family-wide closure the algebra-extending refactor routes
11693 // through.
11694 for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11695 for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11696 if i == j {
11697 continue;
11698 }
11699 assert_ne!(
11700 a, b,
11701 "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11702 LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11703 list-keyword family's pairwise disjointness \
11704 contract no longer binds distinct index pairs \
11705 to distinct markers.",
11706 );
11707 }
11708 }
11709 }
11710
11711 #[test]
11712 fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11713 // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11714 // MUST classify as `true` through the typed membership gate. The
11715 // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11716 // pin is the structural closure binding the gate's return value
11717 // to the ALL array's element set. A regression that specialises
11718 // the gate to a subset of the array (e.g. a `matches!(s,
11719 // "&rest")` inline that silently drops the `&optional` branch)
11720 // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11721 // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11722 // element of `Self::ALL` decodes through the projection back to
11723 // itself.
11724 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11725 assert!(
11726 MacroParams::is_lambda_list_keyword(m),
11727 "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11728 element `{m}` — the closed-set membership gate's \
11729 acceptance side drifted from the ALL array.",
11730 );
11731 }
11732 }
11733
11734 #[test]
11735 fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11736 // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11737 // byte `"&"` MUST classify as `false`. A future Clojure-compat
11738 // port might land a bare `&` marker — but until such an
11739 // extension explicitly lands on the ALL array, the substrate
11740 // MUST NOT silently classify the LEAD byte alone as a
11741 // recognised CL lambda-list keyword. Pins the gate's
11742 // rejection-side contract against a plausible future
11743 // near-miss extension.
11744 let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11745 assert!(
11746 !MacroParams::is_lambda_list_keyword(&bare_lead),
11747 "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11748 the closed-set membership gate silently classifies the bare `&` \
11749 LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11750 array containing only the two suffixed markers.",
11751 );
11752 }
11753
11754 #[test]
11755 fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11756 // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11757 // three plausible future CL lambda-list keywords (`&key` for
11758 // keyword-args, `&aux` for auxiliaries, `&body` for the
11759 // docstring-carrying tail of `defmacro` bodies) MUST classify as
11760 // `false` UNTIL they explicitly land on the ALL array. Pins the
11761 // gate's conservative rejection contract — the closed-set
11762 // membership gate is not a "starts with `&`" heuristic; it is a
11763 // typed enumeration over the currently-recognised markers. A
11764 // regression that silently loosens the gate to a `starts_with`
11765 // check (dropping the closed-set discipline) fails HERE.
11766 for candidate in ["&key", "&aux", "&body"] {
11767 assert!(
11768 !MacroParams::is_lambda_list_keyword(candidate),
11769 "is_lambda_list_keyword accepted the unrecognised \
11770 `&`-prefixed name `{candidate}` — the closed-set \
11771 membership gate silently loosened its acceptance beyond \
11772 the two markers the ALL array names today.",
11773 );
11774 }
11775 }
11776
11777 #[test]
11778 fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11779 // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11780 // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11781 // MUST classify as `false`. These are the shapes the parser's
11782 // bare-symbol dispatch cascade sees at `parse_params` when
11783 // walking a param list, so pinning them here binds the
11784 // membership gate's rejection contract to the parser's most
11785 // common non-marker input shapes.
11786 for candidate in ["a", "xs", "foo", ""] {
11787 assert!(
11788 !MacroParams::is_lambda_list_keyword(candidate),
11789 "is_lambda_list_keyword accepted the bare non-marker \
11790 input `{candidate}` — the closed-set membership gate's \
11791 rejection side no longer excludes bare identifiers \
11792 the parser routes through the fall-through cascade.",
11793 );
11794 }
11795 }
11796
11797 // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11798 //
11799 // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11800 // arithmetic that recurred three times inside `MacroParams::bind` — at
11801 // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11802 // to get the bound-values count), at the `rest_start` site (inside the
11803 // `if let Some(rest)` branch), and at the `expected` site (inside the
11804 // rest-less `else`). The latter two sites live in mutually-exclusive
11805 // branches yet name ONE structural concept; lifting them collapses the
11806 // arithmetic to one named primitive. These tests pin the primitive's
11807 // contract directly; the existing `bind_*` tests are the path-uniformity
11808 // guards proving `bind`'s sites route through the same value without
11809 // behavior drift.
11810 //
11811 // Fail-before/pass-after: every test below references
11812 // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11813 // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11814 // line was a compile-time error against the prior surface.
11815
11816 #[test]
11817 fn fixed_arity_is_zero_for_the_empty_param_list() {
11818 // `()` — a nullary macro has fixed arity 0, the rest-less binder
11819 // boundary at which the FIRST surplus arg already rejects.
11820 let params = MacroParams::default();
11821 assert_eq!(params.fixed_arity(), 0);
11822 }
11823
11824 #[test]
11825 fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11826 // `(a b c)` — three required, no optional, no rest. fixed_arity is
11827 // exactly the required length.
11828 let params = MacroParams {
11829 required: vec!["a".into(), "b".into(), "c".into()],
11830 optional: Vec::new(),
11831 rest: None,
11832 };
11833 assert_eq!(params.fixed_arity(), 3);
11834 }
11835
11836 #[test]
11837 fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11838 // `(&optional x y)` — two optional, no required. fixed_arity is the
11839 // optional length; the optional section participates in the fixed
11840 // run because supplied positional args bind to it.
11841 let params = MacroParams {
11842 required: Vec::new(),
11843 optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11844 rest: None,
11845 };
11846 assert_eq!(params.fixed_arity(), 2);
11847 }
11848
11849 #[test]
11850 fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11851 // `(a b &optional c d e)` — two required + three optional, no rest.
11852 // fixed_arity is 5: the maximum arity a rest-less call can supply.
11853 let params = MacroParams {
11854 required: vec!["a".into(), "b".into()],
11855 optional: vec![
11856 OptionalParam::bare("c"),
11857 OptionalParam::bare("d"),
11858 OptionalParam::bare("e"),
11859 ],
11860 rest: None,
11861 };
11862 assert_eq!(params.fixed_arity(), 5);
11863 }
11864
11865 #[test]
11866 fn fixed_arity_ignores_rest_slot_by_construction() {
11867 // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11868 // arity (2). The `&rest` slot has NO maximum and is structurally
11869 // excluded from `fixed_arity`. Naming this invariant pins that a
11870 // regression that drifts the primitive to "required + optional +
11871 // rest.is_some() as usize" fails loudly here — that drift would
11872 // collapse `fixed_arity` into `names().len()`, losing the rest-start
11873 // vs total-bound-values distinction the typed shape relies on.
11874 let with_rest = MacroParams {
11875 required: vec!["a".into()],
11876 optional: vec![OptionalParam::bare("b")],
11877 rest: Some("r".into()),
11878 };
11879 let without_rest = MacroParams {
11880 required: vec!["a".into()],
11881 optional: vec![OptionalParam::bare("b")],
11882 rest: None,
11883 };
11884 assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11885 assert_eq!(with_rest.fixed_arity(), 2);
11886 }
11887
11888 #[test]
11889 fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11890 // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11891 // — the rest-start reading of the primitive. Same arithmetic the
11892 // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11893 // rest-bound `Sexp::List`).
11894 let params = MacroParams {
11895 required: vec!["a".into(), "b".into()],
11896 optional: vec![OptionalParam::bare("c")],
11897 rest: Some("r".into()),
11898 };
11899 assert_eq!(params.fixed_arity(), 3);
11900 assert_eq!(params.names()[params.fixed_arity()], "r");
11901 }
11902
11903 #[test]
11904 fn fixed_arity_equals_names_length_when_rest_is_absent() {
11905 // When `rest` is `None`, `names().len() == fixed_arity()` — there
11906 // is no rest-name slot to extend the flat run past the fixed
11907 // boundary. Pins the structural identity
11908 // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11909 // for the rest-less case; the rest-present case is pinned by the
11910 // sibling test above (where the boundary is the rest-name index,
11911 // i.e. one short of `names().len()`).
11912 let params = MacroParams {
11913 required: vec!["a".into(), "b".into()],
11914 optional: vec![OptionalParam::bare("c")],
11915 rest: None,
11916 };
11917 assert_eq!(params.names().len(), params.fixed_arity());
11918 assert_eq!(params.names().len(), 3);
11919 }
11920
11921 #[test]
11922 fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
11923 // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
11924 // the rest-less binder rejects iff `args.len() > fixed_arity()`.
11925 // This pin is the path-uniformity guard binding the typed primitive
11926 // to the binder's rejection contract: a regression that drifts
11927 // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
11928 // surface a different boundary in the diagnostic without touching
11929 // the primitive — and this assertion fails loudly. Mirror of the
11930 // sibling rest-less surplus pin (`bind_rest_less_params_reject_
11931 // surplus_args`); this test pins WHAT the `expected` slot's value
11932 // structurally IS, that pin checks the variant SHAPE.
11933 let params = MacroParams {
11934 required: vec!["a".into(), "b".into()],
11935 optional: vec![OptionalParam::bare("c")],
11936 rest: None,
11937 };
11938 assert_eq!(params.fixed_arity(), 3);
11939 let err = params
11940 .bind(
11941 "m",
11942 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
11943 )
11944 .expect_err("4 args against fixed_arity 3 must reject");
11945 match err {
11946 LispError::TooManyMacroArgs {
11947 expected,
11948 got,
11949 macro_name,
11950 } => {
11951 assert_eq!(expected, params.fixed_arity());
11952 assert_eq!(got, 4);
11953 assert_eq!(macro_name, "m");
11954 }
11955 other => panic!("expected TooManyMacroArgs, got {other:?}"),
11956 }
11957 }
11958
11959 #[test]
11960 fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
11961 // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
11962 // into the rest's `Sexp::List`. Pin that the rest list contents
11963 // are exactly the suffix beginning at `fixed_arity()` — the
11964 // primitive's rest-start reading IS the slice index the binder
11965 // consumes. A regression that drifts `bind`'s rest-collection
11966 // slice from `fixed_arity()` would surface as a misaligned rest
11967 // list (off-by-one in either direction) and this assertion fails
11968 // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
11969 // rejection_boundary` on the rest-PRESENT branch.
11970 let params = MacroParams {
11971 required: vec!["a".into()],
11972 optional: vec![OptionalParam::bare("b")],
11973 rest: Some("r".into()),
11974 };
11975 assert_eq!(params.fixed_arity(), 2);
11976 let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
11977 let vals = params.bind("m", &args).unwrap();
11978 // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
11979 let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
11980 assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
11981 }
11982
11983 #[test]
11984 fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
11985 // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
11986 // The rest slot collects the empty slice; bind succeeds, never
11987 // misaligning to an off-by-one underflow. Pin the boundary on the
11988 // rest-PRESENT path — the rest-less mirror is
11989 // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
11990 let params = MacroParams {
11991 required: vec!["a".into()],
11992 optional: vec![OptionalParam::bare("b")],
11993 rest: Some("r".into()),
11994 };
11995 assert_eq!(params.fixed_arity(), 2);
11996 let vals = params
11997 .bind("m", &[Sexp::int(1), Sexp::int(2)])
11998 .expect("rest-present at exact fixed_arity must bind cleanly");
11999 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12000 }
12001
12002 #[test]
12003 fn bind_threads_required_positionally_and_collects_rest_as_list() {
12004 // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12005 // vec is parallel to `names()`, so the rest list sits at the rest's
12006 // flat index.
12007 let params = MacroParams {
12008 required: vec!["a".into(), "b".into()],
12009 optional: Vec::new(),
12010 rest: Some("c".into()),
12011 };
12012 let vals = params
12013 .bind(
12014 "m",
12015 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12016 )
12017 .unwrap();
12018 assert_eq!(
12019 vals,
12020 vec![
12021 Sexp::int(1),
12022 Sexp::int(2),
12023 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12024 ]
12025 );
12026 }
12027
12028 #[test]
12029 fn bind_supplied_optional_takes_its_positional_arg() {
12030 // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12031 // behaves exactly like a positional — only its ABSENCE differs.
12032 let params = MacroParams {
12033 required: vec!["a".into()],
12034 optional: vec![OptionalParam::bare("b")],
12035 rest: None,
12036 };
12037 let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12038 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12039 }
12040
12041 #[test]
12042 fn bind_unsupplied_optional_defaults_to_nil() {
12043 // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12044 // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12045 // no supplied default-form. The bound vec is still parallel to
12046 // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12047 // missing-arg error.
12048 let params = MacroParams {
12049 required: vec!["a".into()],
12050 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12051 rest: None,
12052 };
12053 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12054 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12055 }
12056
12057 #[test]
12058 fn bind_rest_collects_args_beyond_required_and_optional() {
12059 // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12060 // c=(3 4). The rest starts AFTER the required+optional run, so the
12061 // optional's supplied arg is not swept into the rest.
12062 let params = MacroParams {
12063 required: vec!["a".into()],
12064 optional: vec![OptionalParam::bare("b")],
12065 rest: Some("c".into()),
12066 };
12067 let vals = params
12068 .bind(
12069 "m",
12070 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12071 )
12072 .unwrap();
12073 assert_eq!(
12074 vals,
12075 vec![
12076 Sexp::int(1),
12077 Sexp::int(2),
12078 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12079 ]
12080 );
12081 }
12082
12083 #[test]
12084 fn bind_unsupplied_optional_then_empty_rest() {
12085 // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12086 // c=() (nothing left). Both the optional default AND the empty-rest
12087 // contract hold in the same bind.
12088 let params = MacroParams {
12089 required: vec!["a".into()],
12090 optional: vec![OptionalParam::bare("b")],
12091 rest: Some("c".into()),
12092 };
12093 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12094 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12095 }
12096
12097 #[test]
12098 fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12099 // Exactly-saturated required args + a rest that captures nothing →
12100 // the rest binds to the empty list, never errors. Mirrors the
12101 // splice contract `,@()` contributes nothing.
12102 let params = MacroParams {
12103 required: vec!["a".into()],
12104 optional: Vec::new(),
12105 rest: Some("c".into()),
12106 };
12107 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12108 assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12109 }
12110
12111 #[test]
12112 fn bind_missing_required_errors_before_any_rest_collection() {
12113 // A required name with no arg at its position is a
12114 // `MissingMacroArg` — the gate fires during the required walk,
12115 // before the rest is ever collected.
12116 let params = MacroParams {
12117 required: vec!["a".into(), "b".into()],
12118 optional: Vec::new(),
12119 rest: Some("c".into()),
12120 };
12121 let err = params
12122 .bind("m", &[Sexp::int(1)])
12123 .expect_err("missing required `b` must error");
12124 assert!(
12125 matches!(err, LispError::MissingMacroArg { .. }),
12126 "expected MissingMacroArg, got: {err:?}"
12127 );
12128 }
12129
12130 #[test]
12131 fn bind_missing_required_errors_even_with_optional_present() {
12132 // An absent REQUIRED arg errors even when the param list has an
12133 // optional section: the required walk fires `MissingMacroArg` before
12134 // the optional arm (which would otherwise default to nil) is reached.
12135 // Required absence is an error; optional absence is a nil default.
12136 let params = MacroParams {
12137 required: vec!["a".into(), "b".into()],
12138 optional: vec![OptionalParam::bare("c")],
12139 rest: None,
12140 };
12141 let err = params
12142 .bind("m", &[Sexp::int(1)])
12143 .expect_err("missing required `b` must error before optional defaulting");
12144 assert!(
12145 matches!(err, LispError::MissingMacroArg { .. }),
12146 "expected MissingMacroArg, got: {err:?}"
12147 );
12148 }
12149
12150 #[test]
12151 fn bind_rest_less_params_reject_surplus_args() {
12152 // The rest-less binder REJECTS surplus call args via the
12153 // structural `TooManyMacroArgs { macro_name, expected, got }`
12154 // rejection — the call-site mirror of `RestParamTrailingTokens`
12155 // (the definition-site rejection lifted at the parse_params
12156 // boundary). Closes the asymmetry where the typed-entry
12157 // macro-call-gate rejected too-few-args loudly
12158 // (`MissingMacroArg`) but silently truncated too-many. `expected`
12159 // is the rest-less binder's fixed maximum arity
12160 // (`required.len() + optional.len()`); `got` is the actual
12161 // call-site arg count.
12162 let params = MacroParams {
12163 required: vec!["a".into()],
12164 optional: Vec::new(),
12165 rest: None,
12166 };
12167 let err = params
12168 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12169 .expect_err("rest-less surplus must error");
12170 match err {
12171 LispError::TooManyMacroArgs {
12172 macro_name,
12173 expected,
12174 got,
12175 } => {
12176 assert_eq!(macro_name, "m");
12177 assert_eq!(expected, 1);
12178 assert_eq!(got, 2);
12179 }
12180 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12181 }
12182 }
12183
12184 // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12185 //
12186 // The `&optional` section now admits both bare-symbol entries (`x`) AND
12187 // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12188 // Option<Sexp>` slot makes the per-param default a FIELD on each
12189 // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12190 // to maintain in lock-step with `Vec<String>`. These tests pin: the
12191 // parser admits both shapes side-by-side; the four malformed list-spec
12192 // shapes (empty / missing-default / extra-elements / non-symbol-name)
12193 // are rejected via `OptionalParamMalformed` with the typed
12194 // `OptionalParamMalformedReason`; the binder consults the default form
12195 // when the arg is absent and ignores it when supplied; and the end-to-
12196 // end expansion agrees between the bytecode and substitute strategies
12197 // (invariant 2 — free middle).
12198
12199 #[test]
12200 fn parse_params_admits_optional_list_spec_with_default() {
12201 // `(a &optional (b 5))` — one bare optional becomes
12202 // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12203 // surrounding `MacroParams` shape is otherwise identical.
12204 let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12205 assert_eq!(
12206 params,
12207 MacroParams {
12208 required: vec!["a".into()],
12209 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12210 rest: None,
12211 }
12212 );
12213 }
12214
12215 #[test]
12216 fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12217 // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12218 // interleaves bare and list-form specs. Each lands in its own
12219 // `OptionalParam` entry; `names()` still yields the flat
12220 // required-then-optional-then-rest order.
12221 let params =
12222 parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12223 assert_eq!(
12224 params,
12225 MacroParams {
12226 required: vec!["a".into()],
12227 optional: vec![
12228 OptionalParam::bare("b"),
12229 OptionalParam::with_default("c", Sexp::string("x")),
12230 OptionalParam::bare("d"),
12231 OptionalParam::with_default("e", Sexp::int(9)),
12232 ],
12233 rest: Some("r".into()),
12234 }
12235 );
12236 assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12237 }
12238
12239 #[test]
12240 fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12241 // `(&optional (x (list 1 2)))` — the default form is itself a list.
12242 // Without an evaluator, the literal Sexp is parked verbatim into
12243 // `default`; the binder produces it for any absent call.
12244 let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12245 let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12246 assert_eq!(
12247 params,
12248 MacroParams {
12249 required: Vec::new(),
12250 optional: vec![OptionalParam::with_default("x", want_default)],
12251 rest: None,
12252 }
12253 );
12254 }
12255
12256 #[test]
12257 fn parse_params_rejects_empty_list_optional_spec() {
12258 // `(&optional ())` — a zero-element list is the empty-list rejection.
12259 // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12260 // fall through to `NonSymbolParam`, which mis-classifies the failure
12261 // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12262 // symbol" rejection).
12263 let err = parse_params(&read("&optional ()").unwrap())
12264 .expect_err("empty list optional spec must error");
12265 assert!(
12266 matches!(
12267 err,
12268 LispError::OptionalParamMalformed {
12269 position: 1,
12270 reason: crate::error::OptionalParamMalformedReason::EmptyList,
12271 ..
12272 }
12273 ),
12274 "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12275 );
12276 }
12277
12278 #[test]
12279 fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12280 // `(&optional (x))` — a one-element list. REJECTED with reason
12281 // `MissingDefault` rather than reinterpreted as `&optional x`, because
12282 // a parenthesized single-element spec is structurally ambiguous and
12283 // the bare-symbol form `x` IS the canonical "no default" shape.
12284 let err = parse_params(&read("&optional (x)").unwrap())
12285 .expect_err("one-element list optional spec must error");
12286 assert!(
12287 matches!(
12288 err,
12289 LispError::OptionalParamMalformed {
12290 position: 1,
12291 reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12292 ..
12293 }
12294 ),
12295 "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12296 );
12297 }
12298
12299 #[test]
12300 fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12301 // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12302 // supplied-p)` shape is not yet supported (no evaluator → no
12303 // supplied-p variable binding), so the third element is structurally
12304 // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12305 let err = parse_params(&read("&optional (x 5 6)").unwrap())
12306 .expect_err("three-element list optional spec must error");
12307 assert!(
12308 matches!(
12309 err,
12310 LispError::OptionalParamMalformed {
12311 position: 1,
12312 reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12313 ..
12314 }
12315 ),
12316 "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12317 );
12318 }
12319
12320 #[test]
12321 fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12322 // `(&optional (5 default))` — the name slot must be a symbol; a
12323 // numeric literal is REJECTED with reason `NonSymbolName`. Without
12324 // this branch the gate would silently populate
12325 // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12326 // breaking the invariant that param names are symbols.
12327 let err = parse_params(&read("&optional (5 default)").unwrap())
12328 .expect_err("non-symbol-name optional spec must error");
12329 assert!(
12330 matches!(
12331 err,
12332 LispError::OptionalParamMalformed {
12333 position: 1,
12334 reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12335 ..
12336 }
12337 ),
12338 "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12339 );
12340 }
12341
12342 #[test]
12343 fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12344 // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12345 // spec; default forms are an optional-section affordance. The gate
12346 // must fall through to `NonSymbolParam` (parity with the prior
12347 // behavior on lists in the required section), not silently admit
12348 // the list as a default-form spec.
12349 let err =
12350 parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12351 assert!(
12352 matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12353 "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12354 );
12355 }
12356
12357 #[test]
12358 fn bind_unsupplied_optional_with_default_takes_the_default() {
12359 // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12360 // default), not nil. The default form is consulted ONLY when the
12361 // call ran out of args.
12362 let params = MacroParams {
12363 required: vec!["a".into()],
12364 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12365 rest: None,
12366 };
12367 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12368 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12369 }
12370
12371 #[test]
12372 fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12373 // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12374 // supplied optional ALWAYS takes its arg; the default is the
12375 // absence-only fallback. Pins that the default form does not
12376 // shadow a supplied call arg.
12377 let params = MacroParams {
12378 required: Vec::new(),
12379 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12380 rest: None,
12381 };
12382 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12383 assert_eq!(vals, vec![Sexp::int(42)]);
12384 }
12385
12386 #[test]
12387 fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12388 // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12389 // (default), c=nil (bare floor), d="z" (default). The three
12390 // absence cases coexist in one bind: per-default fill, nil floor,
12391 // and a tail with a literal-string default.
12392 let params = MacroParams {
12393 required: vec!["a".into()],
12394 optional: vec![
12395 OptionalParam::with_default("b", Sexp::int(5)),
12396 OptionalParam::bare("c"),
12397 OptionalParam::with_default("d", Sexp::string("z")),
12398 ],
12399 rest: None,
12400 };
12401 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12402 assert_eq!(
12403 vals,
12404 vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12405 );
12406 }
12407
12408 // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12409 //
12410 // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12411 // two-arm fallback that previously inlined at `MacroParams::bind`'s
12412 // optional arm into ONE named accessor on the typed `OptionalParam`.
12413 // The constructor pair `bare` / `with_default` defines the typed
12414 // shapes of the `default` slot; this accessor names the symmetric
12415 // bound-value projection both shapes yield at the absence boundary.
12416 // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12417 // no-default floor; (b) `with_default(name, d).resolved_default()` is
12418 // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12419 // calls (the typed `default` field is not consumed); (d) path-
12420 // uniformity at the binder — `bind`'s optional arm routes through
12421 // `resolved_default` for both shapes; (e) end-to-end through both
12422 // expansion strategies, the absent-call binding agrees.
12423
12424 #[test]
12425 fn resolved_default_is_nil_for_bare_optional() {
12426 // `OptionalParam::bare(name).default` is `None`, so
12427 // `resolved_default()` projects to `Sexp::Nil` — the CL
12428 // `&optional` no-default-form floor. Fail-before/pass-after: this
12429 // assert is meaningless pre-lift because the helper does not
12430 // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12431 // at the named primitive. Sibling of `bare` itself: the
12432 // constructor defines the shape (`default: None`); the accessor
12433 // names the bound-value projection of that shape.
12434 let p = OptionalParam::bare("x");
12435 assert_eq!(p.resolved_default(), Sexp::Nil);
12436 }
12437
12438 #[test]
12439 fn resolved_default_clones_declared_default_for_with_default_optional() {
12440 // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12441 // `resolved_default()` projects to `d.clone()` — the declared
12442 // default form. Sibling of the bare-floor pin: the closed-set
12443 // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12444 // the two arms of `resolved_default`. Pins the closed-set
12445 // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12446 let p = OptionalParam::with_default("x", Sexp::int(5));
12447 assert_eq!(p.resolved_default(), Sexp::int(5));
12448 }
12449
12450 #[test]
12451 fn resolved_default_clones_arbitrary_sexp_default_form() {
12452 // The declared default can be any `Sexp` — a literal list, a
12453 // keyword, a string, a quasi-quoted form — because v0 has no
12454 // evaluator and the typed slot parks the literal verbatim. Pin
12455 // that `resolved_default()` is faithful to the parked literal
12456 // regardless of shape: a regression that special-cases an arm
12457 // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12458 // a `Sexp::Quote`) fails here. The accessor is exactly
12459 // `default.clone()` for the `Some` arm — no shape rewriting.
12460 let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12461 let p = OptionalParam::with_default("x", arbitrary.clone());
12462 assert_eq!(p.resolved_default(), arbitrary);
12463 }
12464
12465 #[test]
12466 fn resolved_default_is_clone_stable_across_repeated_calls() {
12467 // The accessor takes `&self` and projects through `Clone`, so
12468 // repeated calls yield IDENTICAL values — the typed `default`
12469 // field is not consumed. Pins that the accessor is idempotent
12470 // for the same `OptionalParam`, which is the contract the binder
12471 // relies on across multiple `bind` invocations of the same
12472 // macro: every call that leaves the optional unfilled yields
12473 // the SAME bound value, never a partially-consumed shape. A
12474 // regression that converted the accessor to `self.default.take()`
12475 // (consuming the field) would still type-check at the call site
12476 // but would silently desync repeated absent-call bindings; this
12477 // test catches that drift.
12478 let p = OptionalParam::with_default("x", Sexp::string("hi"));
12479 let first = p.resolved_default();
12480 let second = p.resolved_default();
12481 assert_eq!(first, second);
12482 assert_eq!(first, Sexp::string("hi"));
12483 }
12484
12485 #[test]
12486 fn resolved_default_is_the_binders_absent_optional_projection() {
12487 // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12488 // optional arm consults `param.resolved_default()` for any
12489 // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12490 // `Sexp::Nil` via the `None` arm; a with-default optional
12491 // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12492 // single bind call exercises both arms in one walk, and the
12493 // bound values vec is parallel to `names()` so position
12494 // checking pins the arm-to-slot mapping. A regression that
12495 // re-inlines the two-arm fallback at the binder, drifting it
12496 // independently from the accessor, would still type-check but
12497 // a future shape change to `resolved_default` (e.g., adding a
12498 // typed `&supplied-p` companion slot) would silently desync
12499 // the binder from the accessor — this test catches that drift.
12500 let params = MacroParams {
12501 required: Vec::new(),
12502 optional: vec![
12503 OptionalParam::bare("b"),
12504 OptionalParam::with_default("c", Sexp::int(5)),
12505 ],
12506 rest: None,
12507 };
12508 // Empty args → both optionals are absent → both arms fire.
12509 let vals = params.bind("m", &[]).unwrap();
12510 assert_eq!(vals.len(), 2);
12511 assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12512 assert_eq!(
12513 vals[1],
12514 OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12515 );
12516 // And the absolute identities pin the projection's arms:
12517 assert_eq!(vals[0], Sexp::Nil);
12518 assert_eq!(vals[1], Sexp::int(5));
12519 }
12520
12521 #[test]
12522 fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12523 // End-to-end path-uniformity: a macro with both an `&optional
12524 // (g "hi")` (with-default) and an `&optional h` (bare) param
12525 // expands the same way under bytecode AND substitute
12526 // strategies, because both strategies route through the SHARED
12527 // `MacroParams::bind` which now consults `resolved_default`
12528 // for absent slots. Pins that the accessor's contract is
12529 // structurally observable at the strategy boundary — a
12530 // regression that bifurcated the accessor's behavior between
12531 // the two paths (impossible, since they share `bind`) would
12532 // surface here.
12533 let src = r#"
12534 (defmacro greet (n &optional (g "hi") h)
12535 `(list ,g ,n ,h))
12536 (greet world)
12537 "#;
12538 let expected = vec![Sexp::List(vec![
12539 Sexp::symbol("list"),
12540 Sexp::string("hi"),
12541 Sexp::symbol("world"),
12542 Sexp::Nil,
12543 ])];
12544 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12545 let substitute = Expander::new_substitute_only()
12546 .expand_program(read(src).unwrap())
12547 .unwrap();
12548 assert_eq!(
12549 bytecode, expected,
12550 "bytecode resolved_default expansion drifted"
12551 );
12552 assert_eq!(
12553 substitute, expected,
12554 "substitute resolved_default expansion drifted"
12555 );
12556 assert_eq!(
12557 bytecode, substitute,
12558 "the two strategies disagree on resolved_default expansion"
12559 );
12560 }
12561
12562 #[test]
12563 fn resolved_default_supplied_optional_does_not_consult_accessor() {
12564 // A SUPPLIED optional binds to its CALL ARG, never to the
12565 // accessor's projection. Pins the contract: `resolved_default`
12566 // is the absence-only fallback; a present arg shadows the
12567 // accessor at the binder. Sibling negative control to
12568 // `resolved_default_is_the_binders_absent_optional_projection`:
12569 // that test exercises the absence arm at every slot; this test
12570 // exercises the presence arm at every slot and proves the
12571 // accessor's `Sexp::int(5)` projection is NOT consulted when
12572 // the optional is supplied with `Sexp::int(42)`. A regression
12573 // that wired the binder to always consult the accessor (the
12574 // wrong direction — the default would shadow supplied args) is
12575 // caught here.
12576 let params = MacroParams {
12577 required: Vec::new(),
12578 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12579 rest: None,
12580 };
12581 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12582 assert_eq!(vals, vec![Sexp::int(42)]);
12583 // And the accessor's would-be projection IS NOT the bound value:
12584 let p = OptionalParam::with_default("b", Sexp::int(5));
12585 assert_ne!(vals[0], p.resolved_default());
12586 }
12587
12588 #[test]
12589 fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12590 // The end-to-end path: a macro with `&optional (g "hi")` expands to
12591 // the default literal when unsupplied, and to the supplied arg when
12592 // present. Both the bytecode and substitute strategies must agree
12593 // (invariant 2 — free middle); they share `MacroParams::bind`, so
12594 // the default arm lands once in `bind` and both strategies inherit
12595 // it unable to drift. This is the test the prior run (611a682)
12596 // signposted as the next-change-that-benefits.
12597 let src = r#"
12598 (defmacro greet (n &optional (g "hi"))
12599 `(list ,g ,n))
12600 (greet world)
12601 (greet world there)
12602 "#;
12603 let expected = vec![
12604 Sexp::List(vec![
12605 Sexp::symbol("list"),
12606 Sexp::string("hi"),
12607 Sexp::symbol("world"),
12608 ]),
12609 Sexp::List(vec![
12610 Sexp::symbol("list"),
12611 Sexp::symbol("there"),
12612 Sexp::symbol("world"),
12613 ]),
12614 ];
12615 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12616 let substitute = Expander::new_substitute_only()
12617 .expand_program(read(src).unwrap())
12618 .unwrap();
12619 assert_eq!(
12620 bytecode, expected,
12621 "bytecode optional-default expansion drifted"
12622 );
12623 assert_eq!(
12624 substitute, expected,
12625 "substitute optional-default expansion drifted"
12626 );
12627 assert_eq!(
12628 bytecode, substitute,
12629 "the two strategies disagree on optional-default expansion"
12630 );
12631 }
12632
12633 #[test]
12634 fn optional_macro_expands_end_to_end_under_both_strategies() {
12635 // The end-to-end path: a macro with an `&optional` param expands
12636 // correctly whether the optional is supplied or defaulted, and the
12637 // bytecode and substitute strategies agree (invariant 2 — free
12638 // middle). `,b` resolves to the supplied arg when present, to
12639 // `Sexp::Nil` when absent (CL's `&optional` default).
12640 let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12641 // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12642 // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12643 // from a reader-produced empty list `()` even though both Display as
12644 // `()`.
12645 let expected = vec![
12646 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12647 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12648 ];
12649 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12650 let substitute = Expander::new_substitute_only()
12651 .expand_program(read(src).unwrap())
12652 .unwrap();
12653 assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12654 assert_eq!(
12655 substitute, expected,
12656 "substitute optional expansion drifted"
12657 );
12658 assert_eq!(
12659 bytecode, substitute,
12660 "the two strategies disagree on optional expansion"
12661 );
12662 }
12663
12664 // ── MacroDef::template_body: the shared body-projection primitive ──
12665 //
12666 // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12667 // => inner.as_ref(), other => other }` inline peel — present
12668 // byte-identically at the bytecode (`compile_template`) AND substitute
12669 // (`apply`'s fallback) path entries — into ONE named projection both
12670 // strategies share. The existing `compiled_template_matches_substitute
12671 // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12672 // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12673 // the four tests below pin the projection's contract DIRECTLY:
12674 // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12675 // body returns the body verbatim; (c) the borrow is rooted in the
12676 // body field (single-level peel); (d) the projection is the same
12677 // `&Sexp` both strategies route on (so a regression that drifts the
12678 // body-peel from one strategy to the other becomes a type-level
12679 // change at this helper, not a silent two-site divergence).
12680
12681 #[test]
12682 fn template_body_unwraps_outer_quasiquote_to_inner() {
12683 // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12684 // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12685 // and `template_body` peels the outer marker. The returned `&Sexp`
12686 // is the inner walker-payload both expansion strategies consume.
12687 let inner = Sexp::List(vec![
12688 Sexp::symbol("list"),
12689 Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12690 ]);
12691 let def = MacroDef {
12692 name: "f".into(),
12693 params: MacroParams::default(),
12694 body: Sexp::Quasiquote(Box::new(inner.clone())),
12695 };
12696 assert_eq!(def.template_body(), &inner);
12697 }
12698
12699 #[test]
12700 fn template_body_returns_non_quasiquote_body_verbatim() {
12701 // A body authored WITHOUT the outer `` ` `` affordance — a bare
12702 // `Sexp::List` body — returns verbatim. The "other" arm of the
12703 // legacy match. Pin parity with the pre-lift code path so a
12704 // regression that drifts the body-peel into "always peel
12705 // something" (which would break literal-body macros) fails here.
12706 let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12707 let def = MacroDef {
12708 name: "f".into(),
12709 params: MacroParams::default(),
12710 body: body.clone(),
12711 };
12712 assert_eq!(def.template_body(), &body);
12713 // Atom bodies too — the projection is a single-arm match, not a
12714 // recursive descent. A `Sexp::Atom` body is its own template payload.
12715 let atom_def = MacroDef {
12716 name: "g".into(),
12717 params: MacroParams::default(),
12718 body: Sexp::symbol("nil-template"),
12719 };
12720 assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12721 }
12722
12723 #[test]
12724 fn template_body_peels_single_level_only() {
12725 // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12726 // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12727 // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12728 // preamble declares "Nested quasi-quotes: Not yet supported"; the
12729 // single-level peel matches the legacy inline match's posture
12730 // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12731 // recursive loop). A regression that drifts to a recursive peel
12732 // would project too far and the inner `Sexp::Quasiquote` marker —
12733 // which the substitute walker treats as an atomic leaf returned
12734 // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12735 // => Ok(form.clone())`) — would silently disappear from the
12736 // expansion's emitted form. Pin the single-level contract here.
12737 let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12738 let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12739 let def = MacroDef {
12740 name: "nested".into(),
12741 params: MacroParams::default(),
12742 body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12743 };
12744 // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12745 assert_eq!(def.template_body(), &inner_qq);
12746 assert_ne!(def.template_body(), &inner_payload);
12747 }
12748
12749 #[test]
12750 fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12751 // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12752 // verbatim through the "other" arm. The two close-cousin shapes
12753 // share an outer-marker character (`'` vs `` ` ``) at the reader
12754 // boundary but differ semantically: a `Quote` body is a literal
12755 // template (no substitution semantics), a `Quasiquote` body is a
12756 // substitution-walker entry. A regression that conflated the two
12757 // — peeling Quote as if it were Quasiquote — would silently turn
12758 // every quoted-body macro into a template-walked macro. Pin the
12759 // discrimination.
12760 let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12761 let body = Sexp::Quote(Box::new(inner.clone()));
12762 let def = MacroDef {
12763 name: "quoted".into(),
12764 params: MacroParams::default(),
12765 body: body.clone(),
12766 };
12767 assert_eq!(def.template_body(), &body);
12768 // The inner is NOT what comes back — only Quasiquote-bodied macros
12769 // would peel to the inner.
12770 assert_ne!(def.template_body(), &inner);
12771 }
12772
12773 #[test]
12774 fn template_body_is_the_shared_projection_both_strategies_walk() {
12775 // End-to-end path-uniformity at the projection boundary: a macro
12776 // authored with the canonical quasi-quoted body expands
12777 // IDENTICALLY under bytecode and substitute strategies because
12778 // both route their walker's body through `template_body()` — the
12779 // SAME `&Sexp` projection. Sibling of
12780 // `compiled_template_matches_substitute_path` (which observes
12781 // agreement on the EMITTED form); this test pins agreement on
12782 // the projection ENTRY: `compile_template` and the substitute
12783 // fallback now consume the same `&Sexp` (`def.template_body()`),
12784 // never a divergent inline match the two paths could regress
12785 // independently.
12786 let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12787 let expected = vec![Sexp::List(vec![
12788 Sexp::symbol("list"),
12789 Sexp::int(5),
12790 Sexp::int(5),
12791 ])];
12792 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12793 let substitute = Expander::new_substitute_only()
12794 .expand_program(read(src).unwrap())
12795 .unwrap();
12796 assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12797 assert_eq!(substitute, expected, "substitute body-projection drifted");
12798 assert_eq!(
12799 bytecode, substitute,
12800 "the two strategies disagree on the body-projection's emission"
12801 );
12802 }
12803
12804 // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12805 //
12806 // `expand` lifts its macro-call recognition to route through the
12807 // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12808 // self.macros.get(h))` answers "is this form an invocation of any
12809 // registered macro?" in ONE structural query on the Sexp algebra,
12810 // and a HashMap-backed lookup as its classifier. Sibling consumer to
12811 // `macro_def_from` (the typed-macro-definition dispatcher already
12812 // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12813 // a closed-set enum classifier). With both in place, BOTH dispatch
12814 // sites in the macro expander project through the SAME family
12815 // primitive — each binding the classifier that fits its candidate
12816 // set. The tests below pin the consumer's path-uniformity contract
12817 // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12818 // (h))` dispatch observes the SAME `(def, args)` decomposition the
12819 // `Expander::expand` consumer routes through.
12820
12821 #[test]
12822 fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12823 // Structural identity: on a registered-macro call, the consumer's
12824 // expansion is observably equivalent to: classify the form via
12825 // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12826 // apply the def to args. Pin path-uniformity: a hand-rolled
12827 // `as_call_to_any` lookup against the same registry the expander
12828 // walks produces the SAME `MacroDef` reference for the SAME
12829 // input form. A regression that drifts the consumer back to an
12830 // inline `as_list + as_call + macros.get` chain (which would
12831 // fragment the family adoption) is caught structurally — the
12832 // hand-rolled `as_call_to_any` and the consumer's dispatch must
12833 // observe the same decomposition.
12834 let mut e = Expander::new();
12835 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12836 .unwrap();
12837 let call_form = parse("(wrap 42)");
12838
12839 // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12840 let (def_via_family, args_via_family) = call_form
12841 .as_call_to_any(|h| e.macros.get(h))
12842 .expect("registered macro call must decompose via as_call_to_any");
12843 assert_eq!(def_via_family.name, "wrap");
12844 assert_eq!(args_via_family, &[Sexp::int(42)]);
12845
12846 // Consumer's expand observes the SAME decomposition: the expanded
12847 // form is `(list 42 42)`, derived from the SAME def + args the
12848 // hand-rolled lookup found. Path-uniform with the family
12849 // primitive at the dispatch boundary.
12850 let expanded = e.expand(&call_form).unwrap();
12851 assert_eq!(
12852 expanded,
12853 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12854 );
12855 }
12856
12857 #[test]
12858 fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12859 // Path-uniformity for the non-registered-head path: `as_call_to_any
12860 // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12861 // a registered macro, and the consumer falls through to the
12862 // children-walk (which expands any nested macro calls). Pin
12863 // both halves: the hand-rolled lookup returns `None`, AND the
12864 // consumer's expand walks into the children. A regression that
12865 // accidentally short-circuits the children-walk for non-macro
12866 // calls (e.g. by treating `as_call_to_any` = `None` as
12867 // "non-expandable" globally) would fail here.
12868 let mut e = Expander::new();
12869 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12870 .unwrap();
12871 let outer = parse("(foo (wrap 5))");
12872
12873 // Hand-rolled family-primitive lookup rejects the outer head.
12874 assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12875
12876 // Consumer walks children — the inner `(wrap 5)` IS a macro call
12877 // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12878 let expanded = e.expand(&outer).unwrap();
12879 assert_eq!(
12880 expanded,
12881 Sexp::List(vec![
12882 Sexp::symbol("foo"),
12883 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12884 ])
12885 );
12886 }
12887
12888 #[test]
12889 fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12890 // Path-uniformity for the non-call path: every shape `as_call`
12891 // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12892 // ALSO routes past `as_call_to_any` into the `as_list()`
12893 // fallthrough, where the not-a-list arm returns `form.clone()`
12894 // verbatim. Pin both halves: the hand-rolled lookup rejects
12895 // every non-call shape regardless of decoder, AND the consumer
12896 // preserves each shape unchanged. A regression that drifts the
12897 // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12898 // `as_call_to_any` in a way that mis-handles Quote-family
12899 // wrappers) would fail here.
12900 let e = Expander::new();
12901 let shapes = [
12902 Sexp::symbol("foo"),
12903 Sexp::int(5),
12904 Sexp::keyword("k"),
12905 Sexp::string("s"),
12906 Sexp::boolean(true),
12907 Sexp::float(1.5),
12908 Sexp::Nil,
12909 Sexp::Quote(Box::new(Sexp::symbol("x"))),
12910 Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12911 Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12912 Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12913 ];
12914 for s in &shapes {
12915 // Hand-rolled family-primitive lookup rejects non-call shapes
12916 // even for a promiscuous decoder — the call-shape gate fires
12917 // BEFORE the decoder runs.
12918 assert!(
12919 s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
12920 "non-call shape must yield None for as_call_to_any: {s}"
12921 );
12922 // Consumer preserves the shape verbatim — the not-a-list
12923 // arm at the fallthrough returns `form.clone()`.
12924 assert_eq!(
12925 e.expand(s).unwrap(),
12926 s.clone(),
12927 "non-call shape must round-trip unchanged through expand: {s}"
12928 );
12929 }
12930 }
12931
12932 #[test]
12933 fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
12934 // The empty list `()` has no operator and no children. Pin that
12935 // `as_call_to_any` rejects it (no head to feed the decoder), the
12936 // consumer falls through to `as_list()` which returns `Some(&[])`,
12937 // and the children-walk emits `Sexp::List(vec![])` (an empty
12938 // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
12939 // to be observationally identical, but the path is the
12940 // children-walk arm, NOT the not-a-list arm). Path-uniformity
12941 // gate for the singleton-list edge case the `compile_named_from_
12942 // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
12943 // `Some(&[])` for — same posture, different family member.
12944 let e = Expander::new();
12945 let empty = Sexp::List(vec![]);
12946
12947 // Hand-rolled family-primitive lookup rejects the empty list.
12948 assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
12949
12950 // Consumer walks children (zero of them) — output is the empty
12951 // list, same as input.
12952 assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
12953 }
12954
12955 // ── Expander::expand_and_collect_calls_to: program-level walk ───────
12956 //
12957 // `expand_and_collect_calls_to(forms, keyword, project)` composes the
12958 // expander's program-level expansion (`expand_program`) with the
12959 // substrate's slice-side typed-keyword projection (`iter_calls_to`)
12960 // and a caller-supplied per-form mapper into ONE method on the
12961 // `Expander` surface. Both `compile_typed::<T>` and
12962 // `compile_named_from_forms::<T>` (compile.rs) route through it; the
12963 // tests below pin its contract directly. The existing compile.rs
12964 // dispatch tests are the path-uniformity guards proving the two
12965 // production sites route through it without behavior drift.
12966
12967 #[test]
12968 fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
12969 // Three forms — two match `defmonitor`, one matches `defalert`.
12970 // The mapper records `args.len()` from each matching form's tail.
12971 // Pin: only the two `defmonitor` matches flow through `project`,
12972 // in source order (so the recorded lengths are 2, 1).
12973 let forms = vec![
12974 Sexp::List(vec![
12975 Sexp::symbol("defmonitor"),
12976 Sexp::keyword("name"),
12977 Sexp::string("first"),
12978 ]),
12979 Sexp::List(vec![
12980 Sexp::symbol("defalert"),
12981 Sexp::keyword("name"),
12982 Sexp::string("not-a-match"),
12983 ]),
12984 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
12985 ];
12986 let mut e = Expander::new();
12987 let lengths: Vec<usize> = e
12988 .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
12989 .expect("matching forms must compose");
12990 assert_eq!(lengths, vec![2, 1]);
12991 }
12992
12993 #[test]
12994 fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
12995 // Path-uniformity for the soft-projection posture inherited from
12996 // `iter_calls_to`: non-matching forms are skipped silently, and
12997 // `project` is never invoked on them. Pin via a counter the
12998 // closure increments per invocation — the counter is the
12999 // observable that distinguishes "skipped silently" from
13000 // "projected to a no-op".
13001 let forms = vec![
13002 Sexp::symbol("bare-atom"),
13003 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13004 Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13005 Sexp::List(vec![]),
13006 Sexp::Nil,
13007 ];
13008 let mut count = 0usize;
13009 let mut e = Expander::new();
13010 let out: Vec<()> = e
13011 .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13012 count += 1;
13013 Ok(())
13014 })
13015 .expect("non-matching-only slice must collect to empty Vec");
13016 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13017 assert_eq!(count, 0, "project must never run for non-matching forms");
13018 }
13019
13020 #[test]
13021 fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13022 // Three matching forms; the closure errors on the SECOND. Pin: the
13023 // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13024 // form's error, the third form's `project` is NEVER invoked, and
13025 // the returned error is exactly the one the closure raised. This
13026 // mirrors `compile_named_from_forms`'s short-circuit on the first
13027 // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13028 // kwargs rejection.
13029 let forms = vec![
13030 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13031 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13032 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13033 ];
13034 let mut seen = Vec::new();
13035 let mut e = Expander::new();
13036 let err = e
13037 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13038 let n = args[0].as_int().expect("test args are ints");
13039 seen.push(n);
13040 if n == 2 {
13041 Err(LispError::Compile {
13042 form: "test".to_string(),
13043 message: "stop at two".to_string(),
13044 })
13045 } else {
13046 Ok(())
13047 }
13048 })
13049 .expect_err("project's error must short-circuit collect");
13050 // Only the first two forms were ever projected — the third
13051 // never reached `project`.
13052 assert_eq!(seen, vec![1, 2]);
13053 assert!(
13054 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13055 "short-circuit must propagate the project's error verbatim, got {err:?}"
13056 );
13057 }
13058
13059 #[test]
13060 fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13061 // The `expand_program` step runs BEFORE `iter_calls_to` walks
13062 // anything — so an `expand_program` error (e.g., a malformed
13063 // `defmacro` head) must short-circuit the entire composition
13064 // and `project` must NEVER run. Pin via the closure-running
13065 // counter: an error from the FIRST step never invokes `project`.
13066 // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13067 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13068 // fires during `expand_program`.
13069 let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13070 let mut count = 0usize;
13071 let mut e = Expander::new();
13072 let err = e
13073 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13074 count += 1;
13075 Ok(())
13076 })
13077 .expect_err("expand_program error must short-circuit before project");
13078 assert_eq!(
13079 count, 0,
13080 "project must never run when expand_program errors"
13081 );
13082 // Sanity: the error IS the expand_program-stage rejection, NOT
13083 // a project-stage error (path-uniformity for the ordering).
13084 let rendered = format!("{err}");
13085 assert!(
13086 rendered.contains("NAME") || rendered.contains("symbol"),
13087 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13088 );
13089 }
13090
13091 #[test]
13092 fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13093 // Boundary: empty forms slice yields zero items regardless of
13094 // keyword. Pin the degenerate boundary — empty in, empty out —
13095 // and the closure is never invoked. Sibling of
13096 // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13097 // one level up the composition: the program-level walk is
13098 // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13099 let mut count = 0usize;
13100 let mut e = Expander::new();
13101 let out: Vec<()> = e
13102 .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13103 count += 1;
13104 Ok(())
13105 })
13106 .expect("empty forms is not an error");
13107 assert!(out.is_empty());
13108 assert_eq!(count, 0);
13109 }
13110
13111 #[test]
13112 fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13113 // Critical ordering: `expand_program` runs BEFORE the keyword
13114 // filter, so a `defmacro` whose expansion produces a form with
13115 // a matching head IS visible to `project`. Pin the pipeline
13116 // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13117 // (emit-monitor "alpha") (defmonitor :name "beta")`
13118 // expands the macro call to `(defmonitor :name "alpha")` and
13119 // the keyword walk then sees TWO `defmonitor` forms (the
13120 // macro-emitted one AND the directly-authored one). A
13121 // pre-expansion filter would miss the macro-emitted form.
13122 let forms = read(
13123 "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13124 (emit-monitor \"alpha\")
13125 (defmonitor :name \"beta\")",
13126 )
13127 .unwrap();
13128 let mut e = Expander::new();
13129 let names: Vec<String> = e
13130 .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13131 // Each defmonitor form's tail is `(:name "X")`; project
13132 // the second element's string to capture the name.
13133 Ok(args[1].as_string().unwrap().to_string())
13134 })
13135 .expect("macroexpanded + directly-authored forms must both flow");
13136 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13137 }
13138
13139 #[test]
13140 fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13141 // The `keyword` argument flows straight into `iter_calls_to`'s
13142 // head-comparison gate — a regression that drifts the keyword
13143 // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13144 // or lowercases / trims the keyword en route) fails here. Pin
13145 // by passing TWO different keywords against the SAME forms
13146 // input and asserting each picks up only its matching forms.
13147 let forms = vec![
13148 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13149 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13150 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13151 Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13152 ];
13153
13154 let mut e = Expander::new();
13155 let monitors: Vec<i64> = e
13156 .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13157 Ok(args[0].as_int().unwrap())
13158 })
13159 .unwrap();
13160 assert_eq!(monitors, vec![1, 3]);
13161
13162 let mut e2 = Expander::new();
13163 let alerts: Vec<i64> = e2
13164 .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13165 Ok(args[0].as_int().unwrap())
13166 })
13167 .unwrap();
13168 assert_eq!(alerts, vec![2]);
13169
13170 // A keyword nothing matches collects to the empty Vec — same
13171 // shape as the empty-forms case at the slice level.
13172 let mut e3 = Expander::new();
13173 let none: Vec<i64> = e3
13174 .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13175 Ok(args[0].as_int().unwrap())
13176 })
13177 .unwrap();
13178 assert!(none.is_empty());
13179 }
13180
13181 #[test]
13182 fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13183 // Structural identity: for any (forms, keyword, project) triple
13184 // whose pieces succeed, the method's result equals the inlined
13185 // pre-lift pipeline `expand_program + iter_calls_to + map +
13186 // collect`. Pin shape AND ordering on a mixed forms input
13187 // (matching + non-matching + macroexpand-emitted matches) so a
13188 // regression that drifts the method's pipeline from the inlined
13189 // closed-form fails here. This is the lift's load-bearing
13190 // path-uniformity gate — the SAME assertion that holds for
13191 // both production consumers (compile.rs) holds at the
13192 // primitive's contract level.
13193 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13194 (foo :idx 1)
13195 (emit-foo 2)
13196 (bar :idx 99)
13197 (foo :idx 3)";
13198 let forms = read(src).unwrap();
13199
13200 // Inlined pre-lift pipeline.
13201 let mut exp_inline = Expander::new();
13202 let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13203 let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13204 .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13205 .collect::<Result<Vec<_>>>()
13206 .unwrap();
13207
13208 // Method pipeline.
13209 let mut exp_method = Expander::new();
13210 let via_method: Vec<i64> = exp_method
13211 .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13212 .unwrap();
13213
13214 assert_eq!(via_inline, via_method);
13215 assert_eq!(via_inline, vec![1, 2, 3]);
13216 }
13217
13218 // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13219 //
13220 // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13221 // composes `expand_program` with `iter_calls_to_any` and a per-form
13222 // mapper that receives BOTH the typed witness AND the args tail.
13223 // Closes the (keyword, classifier) 2×2 of expander-surface
13224 // compose-on-iter projections at the typed-decoded corner the prior
13225 // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13226 // sibling now ROUTES through this primitive via a constant-classifier
13227 // composition — the tests below pin both the typed-decoded primitive's
13228 // contract directly AND the routing identity binding the keyword
13229 // sibling to it.
13230 //
13231 // The existing `expand_and_collect_calls_to_*` tests above remain
13232 // load-bearing — they pin the keyword sibling's contract through
13233 // its ROUTED implementation, so a regression that drifts the routing
13234 // composition surfaces at the existing keyword tests too.
13235
13236 #[test]
13237 fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13238 {
13239 // The typed-decoded primitive's happy path: a closed-set
13240 // classifier decodes head symbols to a typed enum, and the
13241 // per-form projection receives `(decoded, args)` for every
13242 // matched form in source order. Sweep across THREE distinct
13243 // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13244 // a non-matching form to pin both the typed-witness threading
13245 // AND the source-order yield AND the rejection of non-classifier
13246 // forms in ONE assertion. The decoder runs once per call form
13247 // (`FnMut`); the projection runs once per matched form.
13248 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13249 enum Op {
13250 Foo,
13251 Bar,
13252 Baz,
13253 }
13254 let src = "(foo 1)
13255 (bar 2)
13256 (other 99)
13257 (baz 3)
13258 (foo 4)";
13259 let forms = read(src).unwrap();
13260 let mut e = Expander::new();
13261 let pairs: Vec<(Op, i64)> = e
13262 .expand_and_collect_calls_to_any(
13263 forms,
13264 |h| match h {
13265 "foo" => Some(Op::Foo),
13266 "bar" => Some(Op::Bar),
13267 "baz" => Some(Op::Baz),
13268 _ => None,
13269 },
13270 |op, args| Ok((op, args[0].as_int().unwrap())),
13271 )
13272 .unwrap();
13273 assert_eq!(
13274 pairs,
13275 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13276 );
13277 }
13278
13279 #[test]
13280 fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13281 // The soft-projection contract — every shape `iter_calls_to_any`
13282 // rejects (non-list, empty list, list whose head is not a
13283 // symbol, list whose head decodes through the classifier to
13284 // `None`) skips the projection silently. Pin a deliberately-
13285 // panicking projection so the assertion fires loudly if a
13286 // non-matching form reaches it. The decoder accepts only
13287 // `"defmonitor"`; every other shape (atom, list whose head is a
13288 // keyword, list whose head is a symbol the decoder rejects)
13289 // short-circuits before the projection runs.
13290 let src = r#":bare-keyword
13291 "bare-string"
13292 42
13293 ()
13294 (foo bar)
13295 (defmonitor :name "matches")"#;
13296 let forms = read(src).unwrap();
13297 let mut e = Expander::new();
13298 let lengths: Vec<usize> = e
13299 .expand_and_collect_calls_to_any(
13300 forms,
13301 |h| (h == "defmonitor").then_some(()),
13302 |(), args| {
13303 // The projection must run EXACTLY once — for the
13304 // single matched form. Any non-matching form that
13305 // reaches the projection panics here.
13306 assert_eq!(args.len(), 2, "projection ran on non-matching form");
13307 Ok(args.len())
13308 },
13309 )
13310 .unwrap();
13311 assert_eq!(lengths, vec![2]);
13312 }
13313
13314 #[test]
13315 fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13316 // The Result projection's short-circuit contract — when the
13317 // projection returns `Err` on a matched form, the walk
13318 // short-circuits and subsequent matched forms are NOT projected.
13319 // Pin the source-order short-circuit via a counter the
13320 // projection increments before deciding to fail: the counter
13321 // sits at exactly the index of the failing form (the second
13322 // match), proving the third match never reached the projection.
13323 let src = "(foo 1) (foo 2) (foo 3)";
13324 let forms = read(src).unwrap();
13325 let mut count = 0usize;
13326 let mut e = Expander::new();
13327 let err = e
13328 .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13329 forms,
13330 |h| (h == "foo").then_some(()),
13331 |(), args| {
13332 count += 1;
13333 let v = args[0].as_int().unwrap();
13334 if v == 2 {
13335 return Err(crate::error::LispError::Missing("test-failure"));
13336 }
13337 Ok(v)
13338 },
13339 )
13340 .expect_err("projection must short-circuit on first Err");
13341 assert_eq!(
13342 count, 2,
13343 "projection must have run on first AND failing form, then stopped",
13344 );
13345 // Sanity: the error is the project-stage rejection — the same
13346 // typed `LispError` the projection returned, propagated verbatim.
13347 assert!(
13348 matches!(err, crate::error::LispError::Missing("test-failure")),
13349 "expected the projection's typed Err verbatim, got {err:?}",
13350 );
13351 }
13352
13353 #[test]
13354 fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13355 // Ordering contract — `expand_program` runs BEFORE
13356 // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13357 // that emits a classifier-decoded body must have its expanded
13358 // body decoded by the classifier (not the unexpanded macro-call
13359 // head). Pin the ordering with a macro `(emit-foo n)` that
13360 // expands to `(foo :idx n)` — the classifier sees the expanded
13361 // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13362 // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13363 // — the SAME composition contract holds on the typed-decoded
13364 // sibling.
13365 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13366 (foo :idx 1)
13367 (emit-foo 2)
13368 (bar :idx 99)
13369 (foo :idx 3)";
13370 let forms = read(src).unwrap();
13371 let mut e = Expander::new();
13372 // Classifier returns `()` for "foo" only; the keyword sibling's
13373 // routing identity is the same composition we exercise here
13374 // directly.
13375 let idxs: Vec<i64> = e
13376 .expand_and_collect_calls_to_any(
13377 forms,
13378 |h| (h == "foo").then_some(()),
13379 |(), args| Ok(args[1].as_int().unwrap()),
13380 )
13381 .unwrap();
13382 // 1 from the literal call, 2 from the macro-emitted call, 3 from
13383 // the final literal call. The macro-emitted `(foo :idx 2)` is
13384 // present BECAUSE `expand_program` ran first.
13385 assert_eq!(idxs, vec![1, 2, 3]);
13386 }
13387
13388 #[test]
13389 fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13390 // The `FnMut` constraint — the slice walk calls the classifier
13391 // once per call form, and a decoder that captures mutable state
13392 // (a counter, a registry cache, a visited-set) maintains that
13393 // state across the walk. Pin the `FnMut` contract with a counter
13394 // closure that increments per shape-gate-passing form (calls
13395 // whose head is a symbol), asserting the counter equals the
13396 // total call count regardless of whether the classifier accepts
13397 // each form. Mirrors the parallel slice-side test
13398 // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13399 // — the SAME `FnMut` contract holds on the expander surface.
13400 let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13401 let forms = read(src).unwrap();
13402 let mut e = Expander::new();
13403 let mut classifier_calls = 0usize;
13404 let projected: Vec<i64> = e
13405 .expand_and_collect_calls_to_any(
13406 forms,
13407 |h| {
13408 classifier_calls += 1;
13409 (h == "foo").then_some(())
13410 },
13411 |(), args| Ok(args[0].as_int().unwrap()),
13412 )
13413 .unwrap();
13414 assert_eq!(
13415 classifier_calls, 5,
13416 "classifier must run once per call form (5 forms, all calls with symbol heads)",
13417 );
13418 assert_eq!(
13419 projected,
13420 vec![1, 3, 5],
13421 "projection must run only for classifier-accepted forms in source order",
13422 );
13423 }
13424
13425 #[test]
13426 fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13427 ) {
13428 // Structural identity: the keyword sibling
13429 // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13430 // primitive via a constant-classifier composition — the
13431 // post-lift composition law binding the two siblings:
13432 //
13433 // expand_and_collect_calls_to(forms, k, p) ==
13434 // expand_and_collect_calls_to_any(forms,
13435 // |h| (h == k).then_some(()),
13436 // |(), args| p(args))
13437 //
13438 // Pin shape AND ordering across THREE representative keyword
13439 // values: one that matches multiple forms, one that matches
13440 // none, one that matches exactly one form. Same mixed input
13441 // (literal + macroexpand-emitted + non-matching) the keyword
13442 // path-uniformity test exercises, so the routing identity holds
13443 // on the SAME input shape the keyword sibling's contract pins.
13444 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13445 (foo :idx 1)
13446 (emit-foo 2)
13447 (bar :idx 99)
13448 (foo :idx 3)";
13449 let forms = read(src).unwrap();
13450
13451 for keyword in ["foo", "bar", "absent"] {
13452 let mut exp_keyword = Expander::new();
13453 let via_keyword: Vec<i64> = exp_keyword
13454 .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13455 Ok(args[1].as_int().unwrap())
13456 })
13457 .unwrap();
13458 let mut exp_classifier = Expander::new();
13459 let via_classifier: Vec<i64> = exp_classifier
13460 .expand_and_collect_calls_to_any(
13461 forms.clone(),
13462 |h| (h == keyword).then_some(()),
13463 |(), args| Ok(args[1].as_int().unwrap()),
13464 )
13465 .unwrap();
13466 assert_eq!(
13467 via_keyword, via_classifier,
13468 "routing identity drifted for keyword {keyword:?}",
13469 );
13470 }
13471 }
13472
13473 #[test]
13474 fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13475 {
13476 // The expand_program-stage short-circuit contract — when
13477 // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13478 // param list is malformed), the walk short-circuits BEFORE the
13479 // classifier or projection runs. Pin the ordering with a
13480 // deliberately-panicking classifier AND projection: any
13481 // post-expand-program execution fires the panic. Mirrors the
13482 // parallel keyword test
13483 // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13484 // — the SAME composition contract holds on the typed-decoded
13485 // sibling.
13486 // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13487 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13488 // fires during `expand_program`. Same rejection shape the
13489 // parallel keyword test uses, so the ordering contract observed
13490 // here is structurally identical to the keyword sibling's.
13491 let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13492 let mut e = Expander::new();
13493 let err = e
13494 .expand_and_collect_calls_to_any::<(), _, _, ()>(
13495 forms,
13496 |_h| -> Option<()> {
13497 panic!("classifier must not run when expand_program errors");
13498 },
13499 |(), _args| {
13500 panic!("project must not run when expand_program errors");
13501 },
13502 )
13503 .expect_err("expand_program error must short-circuit before classifier or project");
13504 // Sanity: the error IS an expand_program-stage rejection — not a
13505 // classifier or projection error — so the ordering is observable.
13506 let rendered = format!("{err}");
13507 assert!(
13508 rendered.contains("NAME") || rendered.contains("symbol"),
13509 "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13510 );
13511 }
13512
13513 // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13514 //
13515 // The from-source sibling of `expand_and_collect_calls_to`: composes
13516 // `crate::reader::read(src)?` with the from-forms primitive in ONE
13517 // method on the `Expander` surface. All four typed-program dispatchers
13518 // (free-function `compile_typed` / `compile_named`, preloaded-expander
13519 // `RealizedCompiler::compile_typed` / `compile_named`) route through
13520 // it; the tests below pin its contract directly. The existing
13521 // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13522 // guards proving the four production sites route through it without
13523 // behavior drift.
13524
13525 #[test]
13526 fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13527 // Happy path: a source string with three top-level forms (two
13528 // matching `defmonitor`, one matching `defalert`) flows through
13529 // `read` then through the from-forms primitive. Pin the SAME
13530 // emission shape the from-forms test pins for the matching
13531 // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13532 let src = r#"(defmonitor :name "first")
13533 (defalert :name "not-a-match")
13534 (defmonitor :solo)"#;
13535 let mut e = Expander::new();
13536 let lengths: Vec<usize> = e
13537 .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13538 .expect("matching forms must compose");
13539 assert_eq!(lengths, vec![2, 1]);
13540 }
13541
13542 #[test]
13543 fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13544 // The reader runs BEFORE `expand_program` — so a reader error (an
13545 // unterminated string, an unbalanced paren, an unknown escape)
13546 // must short-circuit the entire composition and `expand_program`
13547 // / the keyword filter / `project` must NEVER run. Pin the
13548 // ordering: an unbalanced open-paren is rejected by the reader
13549 // at parse time, never reaches the from-forms primitive.
13550 let mut count = 0usize;
13551 let mut e = Expander::new();
13552 let err = e
13553 .expand_source_and_collect_calls_to::<(), _>(
13554 "(defmonitor :name \"unbalanced",
13555 "defmonitor",
13556 |_args| {
13557 count += 1;
13558 Ok(())
13559 },
13560 )
13561 .expect_err("reader error must short-circuit before expand_program");
13562 assert_eq!(
13563 count, 0,
13564 "project must never run when reader errors at parse time"
13565 );
13566 // Sanity: the error IS a reader-stage rejection — the rendered
13567 // diagnostic mentions the lexer's gate, not the expander's or
13568 // projector's (path-uniformity for the read-then-walk ordering).
13569 let rendered = format!("{err}");
13570 assert!(
13571 rendered.to_lowercase().contains("string")
13572 || rendered.to_lowercase().contains("paren")
13573 || rendered.to_lowercase().contains("eof")
13574 || rendered.to_lowercase().contains("unexpected")
13575 || rendered.to_lowercase().contains("unterminated")
13576 || rendered.to_lowercase().contains("unclosed"),
13577 "error must be the reader-stage rejection, got: {rendered}"
13578 );
13579 }
13580
13581 #[test]
13582 fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13583 ) {
13584 // Reader succeeds, but `expand_program` rejects a malformed
13585 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13586 // ordering: `expand_program` rejects BEFORE the keyword filter
13587 // walks anything, `project` must NEVER run. Sibling of the
13588 // from-forms test of the same name — one level up the
13589 // composition (sourced from `&str` rather than `Vec<Sexp>`).
13590 let mut count = 0usize;
13591 let mut e = Expander::new();
13592 let err = e
13593 .expand_source_and_collect_calls_to::<(), _>(
13594 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13595 "defmonitor",
13596 |_args| {
13597 count += 1;
13598 Ok(())
13599 },
13600 )
13601 .expect_err("expand_program error must short-circuit before project");
13602 assert_eq!(
13603 count, 0,
13604 "project must never run when expand_program errors"
13605 );
13606 let rendered = format!("{err}");
13607 assert!(
13608 rendered.contains("NAME") || rendered.contains("symbol"),
13609 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13610 );
13611 }
13612
13613 #[test]
13614 fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13615 // Reader + `expand_program` both succeed; the per-form `project`
13616 // errors on the SECOND matched form. Pin: the collect's short-
13617 // circuit fires at the second match's error, the third match's
13618 // `project` is NEVER invoked, and the returned error is exactly
13619 // the one the closure raised. Mirrors `compile_named`'s
13620 // short-circuit on the first `NamedFormMissingName` /
13621 // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13622 // from `&str`.
13623 let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13624 let mut seen = Vec::new();
13625 let mut e = Expander::new();
13626 let err = e
13627 .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13628 let n = args[1].as_int().expect("test args are ints");
13629 seen.push(n);
13630 if n == 2 {
13631 Err(LispError::Compile {
13632 form: "test".to_string(),
13633 message: "stop at two".to_string(),
13634 })
13635 } else {
13636 Ok(())
13637 }
13638 })
13639 .expect_err("project's error must short-circuit collect");
13640 assert_eq!(seen, vec![1, 2]);
13641 assert!(
13642 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13643 "short-circuit must propagate the project's error verbatim, got {err:?}"
13644 );
13645 }
13646
13647 #[test]
13648 fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13649 // Boundary: empty source string yields zero items regardless of
13650 // keyword. Pin the degenerate boundary — empty in, empty out —
13651 // and the closure is never invoked. Sibling of the from-forms
13652 // test of the same name, one level up the composition: the
13653 // from-source posture is fused-empty when `read` yields an
13654 // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13655 // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13656 // matches).
13657 let mut count = 0usize;
13658 let mut e = Expander::new();
13659 let out: Vec<()> = e
13660 .expand_source_and_collect_calls_to("", "anything", |_args| {
13661 count += 1;
13662 Ok(())
13663 })
13664 .expect("empty source is not an error");
13665 assert!(out.is_empty());
13666 assert_eq!(count, 0);
13667 }
13668
13669 #[test]
13670 fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13671 // Critical ordering preserved across the from-source posture:
13672 // `defmacro` source registers in `expand_program`, its call
13673 // sites expand to the matching keyword, AND the keyword walk
13674 // sees both the macro-emitted form and any directly-authored
13675 // matches in source order. Sibling of the from-forms test of
13676 // the same name, sourced from `&str`.
13677 let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13678 (emit-monitor \"alpha\")
13679 (defmonitor :name \"beta\")";
13680 let mut e = Expander::new();
13681 let names: Vec<String> = e
13682 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13683 Ok(args[1].as_string().unwrap().to_string())
13684 })
13685 .expect("macroexpanded + directly-authored forms must both flow");
13686 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13687 }
13688
13689 #[test]
13690 fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13691 // Structural identity: for any (src, keyword, project) triple
13692 // whose pieces succeed, the from-source method's result equals
13693 // the inlined pre-lift pipeline `read + expand_and_collect_
13694 // calls_to`. Pin shape AND ordering on a mixed source
13695 // (matching + non-matching + macroexpand-emitted matches) so a
13696 // regression that drifts the method's pipeline from the
13697 // inlined closed-form fails here. This is the lift's
13698 // load-bearing path-uniformity gate — the SAME assertion that
13699 // holds for all four production consumers (free-function
13700 // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13701 // holds at the primitive's contract level.
13702 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13703 (foo :idx 1)
13704 (emit-foo 2)
13705 (bar :idx 99)
13706 (foo :idx 3)";
13707
13708 // Inlined pre-lift pipeline.
13709 let mut exp_inline = Expander::new();
13710 let inline_forms = read(src).unwrap();
13711 let via_inline: Vec<i64> = exp_inline
13712 .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13713 .unwrap();
13714
13715 // From-source method pipeline.
13716 let mut exp_method = Expander::new();
13717 let via_method: Vec<i64> = exp_method
13718 .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13719 .unwrap();
13720
13721 assert_eq!(via_inline, via_method);
13722 assert_eq!(via_inline, vec![1, 2, 3]);
13723 }
13724
13725 #[test]
13726 fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13727 // The `keyword` argument flows straight into the from-forms
13728 // primitive's head-comparison gate, which inherits from
13729 // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13730 // lowercases / trims it en route through the read step) fails
13731 // here. Pin by passing TWO different keywords against the SAME
13732 // source and asserting each picks up only its matching forms.
13733 let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13734
13735 let mut e1 = Expander::new();
13736 let monitors: Vec<i64> = e1
13737 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13738 Ok(args[1].as_int().unwrap())
13739 })
13740 .unwrap();
13741 assert_eq!(monitors, vec![1, 3]);
13742
13743 let mut e2 = Expander::new();
13744 let alerts: Vec<i64> = e2
13745 .expand_source_and_collect_calls_to(src, "defalert", |args| {
13746 Ok(args[1].as_int().unwrap())
13747 })
13748 .unwrap();
13749 assert_eq!(alerts, vec![2]);
13750
13751 let mut e3 = Expander::new();
13752 let none: Vec<i64> = e3
13753 .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13754 Ok(args[1].as_int().unwrap())
13755 })
13756 .unwrap();
13757 assert!(none.is_empty());
13758 }
13759
13760 #[test]
13761 fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13762 // Path-uniformity for the soft-projection posture inherited
13763 // from `iter_calls_to`: non-matching forms in the source are
13764 // skipped silently, and `project` is never invoked on them.
13765 // Sibling of the from-forms test of the same name, sourced
13766 // from `&str` with a mix of atoms-and-lists that the reader
13767 // emits at the top level.
13768 let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13769 let mut count = 0usize;
13770 let mut e = Expander::new();
13771 let out: Vec<()> = e
13772 .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13773 count += 1;
13774 Ok(())
13775 })
13776 .expect("non-matching-only source must collect to empty Vec");
13777 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13778 assert_eq!(count, 0, "project must never run for non-matching forms");
13779 }
13780
13781 // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13782 //
13783 // The from-source posture of the typed-decoded classifier sibling on
13784 // the `Expander` surface — closes the (from-forms, from-source) ×
13785 // (keyword, classifier) 2×2 of compose-on-iter projections at the
13786 // from-source classifier corner the prior runs left open. The
13787 // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13788 // keyword) is the constant-classifier projection of this primitive:
13789 // its body composes this function with a `|h| (h == keyword).
13790 // then_some(())` decoder, so the from-source pipeline lives at ONE
13791 // site. These tests pin both the typed-decoded primitive's contract
13792 // directly AND the post-lift routing of
13793 // `expand_source_and_collect_calls_to` through it.
13794
13795 #[test]
13796 fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13797 ) {
13798 // Happy path: a source string with four top-level forms (two
13799 // matching "foo", one matching "bar", one rejected-head
13800 // "defmonitor") flows through `read` → `expand_program` → the
13801 // typed-decoded classifier walk. The closed-set classifier
13802 // `Op::from_keyword` admits "foo" and "bar" but rejects
13803 // "defmonitor". Pin the typed-decoded yield shape (decoded
13804 // witness alongside args tail) AND source-order preservation
13805 // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13806 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13807 enum Op {
13808 Foo,
13809 Bar,
13810 }
13811 fn op_from_keyword(head: &str) -> Option<Op> {
13812 match head {
13813 "foo" => Some(Op::Foo),
13814 "bar" => Some(Op::Bar),
13815 _ => None,
13816 }
13817 }
13818 let src = r#"(foo :idx 1)
13819 (defmonitor :idx 99)
13820 (bar :idx 2)
13821 (foo :idx 3)"#;
13822 let mut e = Expander::new();
13823 let yielded: Vec<(Op, i64)> = e
13824 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13825 Ok((op, args[1].as_int().expect("test args are ints")))
13826 })
13827 .expect("matching forms must compose through from-source classifier walk");
13828 assert_eq!(
13829 yielded,
13830 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13831 "yields must be in source order, decoded witness paired with per-form mapper output"
13832 );
13833 }
13834
13835 #[test]
13836 fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13837 ) {
13838 // The reader runs BEFORE `expand_program` AND BEFORE the
13839 // classifier — so a reader error (an unterminated string)
13840 // short-circuits the entire composition and both the classifier
13841 // AND `project` must NEVER run. Pin the ordering with BOTH
13842 // decoder and projection explicitly panicking — any post-reader
13843 // execution fires the panic. Sibling of
13844 // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13845 // one corner over in the 2×2 (the classifier version of the
13846 // same ordering pin).
13847 let mut e = Expander::new();
13848 let err = e
13849 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13850 "(defmonitor :name \"unbalanced",
13851 |_h: &str| -> Option<()> {
13852 panic!("classifier must not run when reader errors at parse time")
13853 },
13854 |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13855 )
13856 .expect_err("reader error must short-circuit before classifier");
13857 // Sanity: the error is a reader-stage rejection.
13858 let rendered = format!("{err}");
13859 assert!(
13860 rendered.to_lowercase().contains("string")
13861 || rendered.to_lowercase().contains("paren")
13862 || rendered.to_lowercase().contains("eof")
13863 || rendered.to_lowercase().contains("unexpected")
13864 || rendered.to_lowercase().contains("unterminated")
13865 || rendered.to_lowercase().contains("unclosed"),
13866 "error must be the reader-stage rejection, got: {rendered}"
13867 );
13868 }
13869
13870 #[test]
13871 fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13872 ) {
13873 // Reader succeeds, but `expand_program` rejects a malformed
13874 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13875 // ordering: `expand_program` rejects BEFORE the classifier walks
13876 // anything, both decoder AND `project` must NEVER run. Sibling
13877 // of the keyword-from-source variant — one corner over in the
13878 // 2×2 with both classifier and projection explicitly panicking.
13879 let mut e = Expander::new();
13880 let err = e
13881 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13882 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13883 |_h: &str| -> Option<()> {
13884 panic!("classifier must not run when expand_program errors")
13885 },
13886 |(), _args| -> Result<()> {
13887 panic!("project must not run when expand_program errors")
13888 },
13889 )
13890 .expect_err("expand_program error must short-circuit before classifier");
13891 let rendered = format!("{err}");
13892 assert!(
13893 rendered.contains("NAME") || rendered.contains("symbol"),
13894 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13895 );
13896 }
13897
13898 #[test]
13899 fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13900 // Path-uniformity for the soft-projection posture inherited
13901 // from `iter_calls_to_any`: forms whose head the classifier
13902 // rejects are skipped silently, and `project` is never invoked
13903 // on them. Pin via an explicitly-panicking projection across a
13904 // mix of forms (atom, list-with-non-symbol-head, list with
13905 // unrecognized-by-classifier symbol head). A regression that
13906 // wrongly invokes `project` on a rejected form fires the panic.
13907 let src = r#"bare-atom
13908 (5 :not-a-symbol-head)
13909 (defmonitor :name "decoder-rejects-me")"#;
13910 let mut e = Expander::new();
13911 // The closed-set classifier admits NOTHING in this source.
13912 let out: Vec<()> = e
13913 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13914 src,
13915 |_h: &str| -> Option<()> { None },
13916 |(), _args| -> Result<()> {
13917 panic!("project must not run for classifier-rejected forms")
13918 },
13919 )
13920 .expect("classifier-rejects-all source must collect to empty Vec");
13921 assert!(
13922 out.is_empty(),
13923 "no classifier-accepted forms — empty Vec, got {out:?}"
13924 );
13925 }
13926
13927 #[test]
13928 fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13929 // Reader + `expand_program` + classifier all succeed; the
13930 // per-form `project` errors on the SECOND matched form. Pin: the
13931 // collect's short-circuit fires at the second match's error, the
13932 // third match's `project` is NEVER invoked, and the returned
13933 // error is exactly the one the closure raised. Sibling of the
13934 // keyword-from-source variant — one corner over in the 2×2.
13935 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13936 enum Op {
13937 Foo,
13938 }
13939 fn op_from_keyword(head: &str) -> Option<Op> {
13940 (head == "foo").then_some(Op::Foo)
13941 }
13942 let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
13943 let mut seen = Vec::new();
13944 let mut e = Expander::new();
13945 let err = e
13946 .expand_source_and_collect_calls_to_any::<(), _, _, _>(
13947 src,
13948 op_from_keyword,
13949 |_op: Op, args: &[Sexp]| -> Result<()> {
13950 let n = args[1].as_int().expect("test args are ints");
13951 seen.push(n);
13952 if n == 2 {
13953 Err(LispError::Compile {
13954 form: "test".to_string(),
13955 message: "stop at two".to_string(),
13956 })
13957 } else {
13958 Ok(())
13959 }
13960 },
13961 )
13962 .expect_err("project's error must short-circuit collect");
13963 assert_eq!(seen, vec![1, 2], "third match's project must never run");
13964 assert!(
13965 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13966 "short-circuit must propagate the project's error verbatim, got {err:?}"
13967 );
13968 }
13969
13970 #[test]
13971 fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
13972 ) {
13973 // The post-lift composition law binding the keyword-from-source
13974 // sibling to the typed-decoded primitive:
13975 // `expand_source_and_collect_calls_to(src, k, project) ==
13976 // expand_source_and_collect_calls_to_any(src,
13977 // |h| (h == k).then_some(()),
13978 // |(), args| project(args))`
13979 // Pin shape AND ordering across THREE representative keywords
13980 // (matching some, matching exactly one, matching none) on the
13981 // SAME mixed source the keyword path-uniformity test exercises.
13982 // A regression that re-implements the keyword-from-source filter
13983 // as a parallel `read + expand_and_collect_calls_to` pipeline
13984 // (rather than routing through the typed-decoded primitive)
13985 // fails the value pin.
13986 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13987 (foo :idx 1)
13988 (emit-foo 2)
13989 (bar :idx 99)
13990 (foo :idx 3)";
13991 for k in ["foo", "bar", "missing"] {
13992 let mut e_keyword = Expander::new();
13993 let via_keyword: Vec<i64> = e_keyword
13994 .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
13995 .unwrap();
13996
13997 let mut e_classifier = Expander::new();
13998 let via_classifier: Vec<i64> = e_classifier
13999 .expand_source_and_collect_calls_to_any(
14000 src,
14001 |h: &str| (h == k).then_some(()),
14002 |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14003 )
14004 .unwrap();
14005
14006 assert_eq!(
14007 via_keyword, via_classifier,
14008 "keyword from-source path must equal classifier from-source path for {k:?}"
14009 );
14010 }
14011 }
14012
14013 #[test]
14014 fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14015 // Critical ordering preserved across the typed-decoded
14016 // from-source posture: `defmacro` source registers in
14017 // `expand_program`, its call sites expand to a classifier-
14018 // accepted form, AND the classifier walk sees both the
14019 // macro-emitted form and any directly-authored matches in
14020 // source order. Sibling of the keyword-from-source ordering
14021 // test, one corner over in the 2×2.
14022 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14023 enum Op {
14024 Foo,
14025 }
14026 fn op_from_keyword(head: &str) -> Option<Op> {
14027 (head == "foo").then_some(Op::Foo)
14028 }
14029 let src = "(defmacro emit-foo (n) `(foo :name ,n))
14030 (emit-foo \"alpha\")
14031 (foo :name \"beta\")";
14032 let mut e = Expander::new();
14033 let names: Vec<(Op, String)> = e
14034 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14035 Ok((op, args[1].as_string().unwrap().to_string()))
14036 })
14037 .expect("macroexpanded + directly-authored forms must both flow through classifier");
14038 assert_eq!(
14039 names,
14040 vec![
14041 (Op::Foo, "alpha".to_string()),
14042 (Op::Foo, "beta".to_string()),
14043 ]
14044 );
14045 }
14046
14047 // ── expand_source_program — from-source × yield-all primitive ───
14048 //
14049 // Closes the 2×2 program-level walk family on the `Expander` surface:
14050 // from-forms × yield-all (`expand_program`), from-forms × keyword-
14051 // projected (`expand_and_collect_calls_to`), from-source × keyword-
14052 // projected (`expand_source_and_collect_calls_to`), AND now
14053 // from-source × yield-all (`expand_source_program`). Pins (a) the
14054 // happy path (mixed forms flow through reader then expand_program),
14055 // (b) reader-stage short-circuit (no expand_program side-effects),
14056 // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14057 // is rejected at its offending form), (d) defmacro absorption (the
14058 // primitive's side-effect on `self.macros` matches the from-forms
14059 // posture's side-effect verbatim), (e) the empty-source boundary,
14060 // and (f) structural identity vs. the inlined pre-lift `read +
14061 // expand_program` pipeline (path-uniformity gate).
14062
14063 #[test]
14064 fn expand_source_program_routes_through_reader_then_expand_program() {
14065 // Happy path: a source string with mixed forms (a `defmacro`
14066 // definition, a `defmonitor` form, a plain symbol literal) flows
14067 // through `read` then through the from-forms primitive. Pin the
14068 // SAME emission shape the from-forms test pins, sourced from a
14069 // `&str` rather than a `Vec<Sexp>`.
14070 let src = r#"(defmacro id (x) `,x)
14071 (defmonitor :name "alpha")
14072 bare-symbol"#;
14073 let mut e = Expander::new();
14074 let out = e
14075 .expand_source_program(src)
14076 .expect("mixed forms must compose");
14077 // `(defmacro id …)` is consumed as a side-effect (registers `id`
14078 // in `e.macros`), so the returned `Vec<Sexp>` contains the
14079 // remaining two top-level forms in source order.
14080 assert_eq!(out.len(), 2);
14081 assert_eq!(
14082 out[0].as_call_to("defmonitor").map(<[_]>::len),
14083 Some(2),
14084 "first surviving form is the defmonitor with two args, got: {:?}",
14085 out[0]
14086 );
14087 assert_eq!(
14088 out[1].as_symbol(),
14089 Some("bare-symbol"),
14090 "second surviving form is the bare symbol literal, got: {:?}",
14091 out[1]
14092 );
14093 // The defmacro side-effect landed in the expander's macro table.
14094 assert!(
14095 e.has("id"),
14096 "defmacro must register `id` in the expander's macro table"
14097 );
14098 }
14099
14100 #[test]
14101 fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14102 // The reader runs BEFORE `expand_program` — so a reader error (an
14103 // unterminated string, an unbalanced paren) must short-circuit
14104 // the entire composition and `expand_program` must NEVER run.
14105 // Pin the ordering: a `defmacro` BEFORE the unterminated string
14106 // is NOT registered, because the reader fails at parse time
14107 // before `expand_program` sees any form.
14108 let mut e = Expander::new();
14109 let err = e
14110 .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14111 .expect_err("reader error must short-circuit before expand_program");
14112 // Sanity: the error IS a reader-stage rejection.
14113 let rendered = format!("{err}").to_lowercase();
14114 assert!(
14115 rendered.contains("string")
14116 || rendered.contains("paren")
14117 || rendered.contains("eof")
14118 || rendered.contains("unexpected")
14119 || rendered.contains("unterminated")
14120 || rendered.contains("unclosed"),
14121 "error must be the reader-stage rejection, got: {err}"
14122 );
14123 // Path-uniformity gate: the `defmacro` lexically BEFORE the
14124 // unterminated string must NOT have been absorbed — `read`'s
14125 // failure short-circuits the entire pipeline before
14126 // `expand_program` walks any forms.
14127 assert!(
14128 !e.has("should-not-register"),
14129 "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14130 );
14131 }
14132
14133 #[test]
14134 fn expand_source_program_short_circuits_on_expand_program_error() {
14135 // Reader succeeds, but `expand_program` rejects a malformed
14136 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14137 // ordering: the expand_program-stage rejection bubbles up
14138 // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14139 let mut e = Expander::new();
14140 let err = e
14141 .expand_source_program("(defmacro 5 (x) `,x)")
14142 .expect_err("expand_program error must propagate");
14143 let rendered = format!("{err}");
14144 assert!(
14145 rendered.contains("NAME") || rendered.contains("symbol"),
14146 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14147 );
14148 }
14149
14150 #[test]
14151 fn expand_source_program_yields_empty_vec_for_empty_source() {
14152 // Boundary: empty source string yields zero items. Pin the
14153 // degenerate boundary — empty in, empty out — sibling of the
14154 // from-source × keyword-projected test of the same name, one
14155 // level down the composition (no keyword filter).
14156 let mut e = Expander::new();
14157 let out = e
14158 .expand_source_program("")
14159 .expect("empty source is not an error");
14160 assert!(
14161 out.is_empty(),
14162 "empty source must yield empty Vec, got: {out:?}"
14163 );
14164 }
14165
14166 #[test]
14167 fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14168 // Critical pipeline order: a `defmacro` in the source registers
14169 // AND a subsequent call to it expands AND the expanded form
14170 // surfaces in the returned Vec. Sourced from `&str`, the
14171 // from-source posture preserves the side-effect-then-expansion
14172 // semantics of the from-forms primitive verbatim.
14173 let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14174 (emit-monitor "alpha")
14175 (emit-monitor "beta")"#;
14176 let mut e = Expander::new();
14177 let out = e
14178 .expand_source_program(src)
14179 .expect("defmacro absorption then expansion must compose");
14180 // Two surviving forms (the `defmacro` itself is consumed as a
14181 // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14182 assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14183 for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14184 let args = out[i]
14185 .as_call_to("defmonitor")
14186 .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14187 assert_eq!(args[0].as_keyword(), Some("name"));
14188 assert_eq!(args[1].as_string(), Some(expected_name));
14189 }
14190 // The macro is now in the table — a subsequent call would
14191 // expand against this SAME expander.
14192 assert!(e.has("emit-monitor"));
14193 }
14194
14195 #[test]
14196 fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14197 // Structural identity: for any `src` whose pieces succeed, the
14198 // from-source method's result equals the inlined pre-lift
14199 // pipeline `read + expand_program`. Pin shape AND ordering on a
14200 // mixed source (defmacro definition + macro call + plain form)
14201 // so a regression that drifts the method's pipeline from the
14202 // inlined closed-form fails here. This is the lift's
14203 // load-bearing path-uniformity gate — the SAME assertion holds
14204 // for both production consumers (`RealizedCompiler::compile` +
14205 // `realize_in_memory`'s `:macros` loop) at the primitive's
14206 // contract level.
14207 let src = r#"(defmacro id (x) `,x)
14208 (id (foo 1 2))
14209 (bar)"#;
14210
14211 // Inlined pre-lift pipeline.
14212 let mut e_inline = Expander::new();
14213 let inline_forms = read(src).unwrap();
14214 let via_inline = e_inline
14215 .expand_program(inline_forms)
14216 .expect("inlined pipeline must succeed");
14217
14218 // From-source method pipeline.
14219 let mut e_method = Expander::new();
14220 let via_method = e_method
14221 .expand_source_program(src)
14222 .expect("from-source method pipeline must succeed");
14223
14224 assert_eq!(
14225 via_inline, via_method,
14226 "from-source method must emit byte-identical result to inlined read+expand_program"
14227 );
14228 // Both pipelines registered the same defmacro side-effect.
14229 assert_eq!(e_inline.has("id"), e_method.has("id"));
14230 assert!(e_method.has("id"));
14231 }
14232
14233 #[test]
14234 fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14235 // Per-`&mut self` absorption posture pin: a `defmacro` registered
14236 // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14237 // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14238 // build-up depends on — every iteration's `expand_source_program(
14239 // macro_src)?` lands its `defmacro` heads into the SAME mutable
14240 // `preloaded` expander, so a follow-up `:macros` source can
14241 // invoke macros defined in earlier ones.
14242 let mut e = Expander::new();
14243 let _ = e
14244 .expand_source_program("(defmacro outer (n) `(inner ,n))")
14245 .unwrap();
14246 assert!(e.has("outer"));
14247 // Call 2: defines `inner` AND invokes `outer`, which expands to
14248 // `(inner <n>)`. The expansion proves call 1's `outer` survived
14249 // into call 2's expansion.
14250 let out = e
14251 .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14252 .unwrap();
14253 assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14254 // (outer 42) → (inner 42) → (wrapped 42)
14255 let args = out[0]
14256 .as_call_to("wrapped")
14257 .expect("nested expansion must reach `wrapped`");
14258 assert_eq!(args[0].as_int(), Some(42));
14259 }
14260
14261 // ── Expander::register_macro_def: the macro-registration primitive ──
14262 //
14263 // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14264 // the byte-identical two-step block —
14265 //
14266 // if self.compile_templates {
14267 // self.templates.insert(def.name.clone(), compile_template(&def)?);
14268 // }
14269 // self.macros.insert(def.name.clone(), def);
14270 //
14271 // — that lived inline at `with_macros` (the bulk-from-iterator
14272 // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14273 // program-walk-time registration site) into ONE named method on the
14274 // `Expander` surface. The tests below pin:
14275 //
14276 // (a) the bytecode-default expander (`Expander::new()`) populates
14277 // BOTH `macros` AND `templates` keyed by `def.name`,
14278 // (b) the substitute-only expander (`Expander::new_substitute_only()`)
14279 // populates `macros` but SKIPS `templates` — the
14280 // `compile_templates: false` gate fires structurally,
14281 // (c) a template that fails to compile (`,foo` against `params:
14282 // []`) short-circuits BEFORE `self.macros.insert` runs, so the
14283 // failed registration leaves both tables untouched — no
14284 // partial-write window in which `self.macros.has(name)` is
14285 // true but `self.templates.has(name)` is missing,
14286 // (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14287 // expander produce structurally identical state (both tables'
14288 // sets of keys + the same `MacroDef` body under each key),
14289 // (e) `expand_program` of one `(defmacro …)` form and
14290 // `register_macro_def` of the parsed `MacroDef` produce
14291 // structurally identical state — closing path-uniformity
14292 // across the lift's two consumers AT the registration site.
14293 //
14294 // The tests bind on `Expander::has(name)` / `Expander::len()` for
14295 // the macros table and on bytecode-vs-substitute output equivalence
14296 // (a registered macro must expand to the SAME result regardless of
14297 // whether the bytecode path's `self.templates` entry exists) for the
14298 // templates table.
14299
14300 fn macro_def_id() -> MacroDef {
14301 // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14302 // one required param, a single-symbol unquote body. Compiles to
14303 // a valid `CompiledTemplate` (no unbound vars), so
14304 // `register_macro_def` succeeds on every Expander posture.
14305 MacroDef {
14306 name: "id".into(),
14307 params: MacroParams {
14308 required: vec!["x".into()],
14309 optional: vec![],
14310 rest: None,
14311 },
14312 // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14313 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14314 }
14315 }
14316
14317 fn macro_def_bad_template() -> MacroDef {
14318 // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14319 // against an EMPTY required-params list. `compile_template`
14320 // rejects it with `UnboundTemplateVar` because `unbound` is not
14321 // in `params.names()`. Used to exercise the
14322 // `compile_template`-failed-but-self.macros-still-pristine
14323 // path-uniformity pin.
14324 MacroDef {
14325 name: "bad".into(),
14326 params: MacroParams::default(),
14327 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14328 }
14329 }
14330
14331 #[test]
14332 fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14333 // The bytecode-default `Expander::new()` carries
14334 // `compile_templates: true`; `register_macro_def` MUST populate
14335 // BOTH `self.macros` (the substitute path's registry) AND
14336 // `self.templates` (the bytecode path's pre-compiled bytecode
14337 // index) keyed by `def.name`. Fail-before-pass-after: this
14338 // assert requires the new method to exist AND to compose the
14339 // two side-effects in canonical order on a single-call
14340 // registration — pre-lift `register_macro_def` did not exist.
14341 let mut e = Expander::new();
14342 e.register_macro_def(macro_def_id())
14343 .expect("well-formed MacroDef must register");
14344 assert!(
14345 e.has("id"),
14346 "self.macros must carry the registered name after register_macro_def"
14347 );
14348 assert!(
14349 e.templates.contains_key("id"),
14350 "self.templates must carry the compiled bytecode under the bytecode-default posture"
14351 );
14352 // The registered macro must expand correctly through the
14353 // bytecode strategy — proves the inserted template is the right
14354 // bytecode for the body.
14355 let out = e
14356 .expand_program(read("(id 42)").unwrap())
14357 .expect("registered macro must expand");
14358 assert_eq!(out.len(), 1);
14359 assert_eq!(out[0], Sexp::int(42));
14360 }
14361
14362 #[test]
14363 fn register_macro_def_substitute_only_skips_templates() {
14364 // `Expander::new_substitute_only()` carries `compile_templates:
14365 // false`; `register_macro_def` MUST populate `self.macros` for
14366 // the substitute path but SKIP `self.templates` (no bytecode
14367 // pre-compile). Pin the `compile_templates: false` gate fires
14368 // structurally — a regression that drifts the conditional from
14369 // the registration primitive (e.g. a future emitter that
14370 // unconditionally pre-compiles) would silently double the
14371 // benchmark baseline's allocation footprint.
14372 let mut e = Expander::new_substitute_only();
14373 e.register_macro_def(macro_def_id())
14374 .expect("well-formed MacroDef must register under substitute-only");
14375 assert!(
14376 e.has("id"),
14377 "self.macros must carry the registered name even under substitute-only"
14378 );
14379 assert!(
14380 !e.templates.contains_key("id"),
14381 "self.templates MUST be empty under compile_templates: false — the gate fires"
14382 );
14383 // The registered macro must still expand correctly through the
14384 // substitute strategy — proves the inserted MacroDef is the
14385 // right body for substitute-walking.
14386 let out = e
14387 .expand_program(read("(id 42)").unwrap())
14388 .expect("registered macro must expand via substitute path");
14389 assert_eq!(out.len(), 1);
14390 assert_eq!(out[0], Sexp::int(42));
14391 }
14392
14393 #[test]
14394 fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14395 // The structural ordering pin: when `compile_template(&def)?`
14396 // rejects (e.g. `,unbound` against empty params), the `?` MUST
14397 // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14398 // def)` runs, so a failed registration leaves BOTH tables
14399 // exactly as they were. Without this ordering a future
14400 // bytecode-strategy lookup would resolve `self.macros.get(name)`
14401 // (the body was inserted) AND find `self.templates.get(name)`
14402 // returning `None` (the pre-compile failed), silently coercing
14403 // the macro onto the substitute fallback path despite
14404 // `compile_templates: true`. The lift preserves the pre-lift
14405 // ordering (`compile_template` precedes `self.macros.insert`)
14406 // structurally — the test pins it across the lift.
14407 let mut e = Expander::new();
14408 let err = e
14409 .register_macro_def(macro_def_bad_template())
14410 .expect_err("unbound-template body must reject");
14411 // The rejection is the structural variant the bytecode
14412 // strategy's template-gate emits.
14413 assert!(
14414 matches!(err, LispError::UnboundTemplateVar { .. }),
14415 "expected UnboundTemplateVar, got: {err:?}"
14416 );
14417 // Both tables MUST be pristine — no partial write.
14418 assert!(
14419 !e.has("bad"),
14420 "self.macros must be untouched after compile_template failure"
14421 );
14422 assert!(
14423 !e.templates.contains_key("bad"),
14424 "self.templates must be untouched after compile_template failure"
14425 );
14426 }
14427
14428 #[test]
14429 fn with_macros_routes_through_register_macro_def_path_uniformity() {
14430 // Path-uniformity pin across the bulk-constructor consumer:
14431 // `with_macros([def])` MUST produce the same final state as
14432 // `Expander::new()` + `register_macro_def(def)`. A regression
14433 // that drifts the constructor's per-MacroDef inline block from
14434 // the registration primitive (e.g. a future emitter that
14435 // re-inlines the two-step block at `with_macros` rather than
14436 // delegating) would fail loudly here.
14437 let mut via_register = Expander::new();
14438 via_register
14439 .register_macro_def(macro_def_id())
14440 .expect("register must succeed");
14441 let mut via_with_macros =
14442 Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14443 assert_eq!(via_register.len(), via_with_macros.len());
14444 assert!(via_register.has("id"));
14445 assert!(via_with_macros.has("id"));
14446 // Both tables key on the same set across both postures.
14447 assert_eq!(
14448 via_register.templates.contains_key("id"),
14449 via_with_macros.templates.contains_key("id"),
14450 "self.templates key-presence must agree across with_macros and register_macro_def"
14451 );
14452 // Both registered expanders expand the registered macro
14453 // identically — the strongest behavioral parity.
14454 let out_a = via_register
14455 .expand_program(read("(id 99)").unwrap())
14456 .unwrap();
14457 let out_b = via_with_macros
14458 .expand_program(read("(id 99)").unwrap())
14459 .unwrap();
14460 assert_eq!(out_a, out_b);
14461 assert_eq!(out_a, vec![Sexp::int(99)]);
14462 }
14463
14464 #[test]
14465 fn expand_program_routes_through_register_macro_def_path_uniformity() {
14466 // Path-uniformity pin across the program-walk consumer:
14467 // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14468 // same final state as a direct
14469 // `register_macro_def(macro_def_id())`. A regression that
14470 // drifts `expand_program`'s `(defmacro …)`-head arm from the
14471 // registration primitive (e.g. a future emitter that re-inlines
14472 // the two-step block at `expand_program` rather than delegating)
14473 // would fail loudly here.
14474 let mut via_register = Expander::new();
14475 via_register
14476 .register_macro_def(macro_def_id())
14477 .expect("register must succeed");
14478 let mut via_expand_program = Expander::new();
14479 let yielded = via_expand_program
14480 .expand_program(read("(defmacro id (x) `,x)").unwrap())
14481 .expect("expand_program of one defmacro must succeed");
14482 // expand_program drops the (defmacro …) form from its returned
14483 // Vec<Sexp> (defmacro is a definition, not a program form), so
14484 // the yielded list is empty — pin the side-effect-only posture.
14485 assert!(
14486 yielded.is_empty(),
14487 "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14488 );
14489 assert!(via_register.has("id"));
14490 assert!(via_expand_program.has("id"));
14491 assert_eq!(via_register.len(), via_expand_program.len());
14492 // Both tables key on the same set across both postures.
14493 assert_eq!(
14494 via_register.templates.contains_key("id"),
14495 via_expand_program.templates.contains_key("id"),
14496 "self.templates key-presence must agree across expand_program and register_macro_def"
14497 );
14498 // Both registered expanders expand the registered macro
14499 // identically — the strongest behavioral parity, closing
14500 // path-uniformity across BOTH consumers (with_macros above,
14501 // expand_program here) at ONE primitive.
14502 let out_a = via_register
14503 .expand_program(read("(id 7)").unwrap())
14504 .unwrap();
14505 let out_b = via_expand_program
14506 .expand_program(read("(id 7)").unwrap())
14507 .unwrap();
14508 assert_eq!(out_a, out_b);
14509 assert_eq!(out_a, vec![Sexp::int(7)]);
14510 }
14511
14512 // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14513 //
14514 // The new typed-marker projection `Sexp::as_unquote` lifts the
14515 // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14516 // literal) pair into ONE structural query. These tests pin behavioral
14517 // parity end-to-end across BOTH expansion strategies:
14518 // * `compile_node` — the bytecode-template strategy's typed marker
14519 // dispatch routes through as_unquote at the compile step.
14520 // * `substitute` (top-level + list-inner) — the substitute-walker
14521 // fallback strategy's typed marker dispatch routes through
14522 // as_unquote at both per-form sites.
14523 // The structural invariant the prior runs' `expansion_layers_agree_on_
14524 // output_and_cache_wins` benchmark observes — bytecode AND substitute
14525 // produce byte-identical output — is anchored here at the macro-template
14526 // level for every distinct unquote-family shape the substitute and
14527 // bytecode strategies discriminate.
14528
14529 #[test]
14530 fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14531 // A template body whose only marker is a top-level `,x`. Both
14532 // expansion strategies route through `as_unquote` (compile_node for
14533 // bytecode, substitute for the fallback walker), each pairing
14534 // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14535 // Pin byte-identical output across both strategies — the
14536 // structural-invariant the new projection's lift was designed to
14537 // make load-bearing structural rather than per-site discipline.
14538 let src = "(defmacro id (x) ,x) (id 42)";
14539 let mut bc = Expander::new();
14540 let mut sub = Expander::new_substitute_only();
14541 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14542 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14543 assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14544 assert_eq!(out_bc, vec![Sexp::int(42)]);
14545 }
14546
14547 #[test]
14548 fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14549 // A template body whose marker is a list-inner `,@xs`. The substitute
14550 // strategy's list-inner Splice arm routes through `as_unquote`
14551 // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14552 // compile_node Splice arm routes through `as_unquote` matching the
14553 // same. Pin byte-identical output across both strategies for the
14554 // splice path — the projection's typed-marker pairing
14555 // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14556 // identical at every consumer post-lift.
14557 let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14558 let mut bc = Expander::new();
14559 let mut sub = Expander::new_substitute_only();
14560 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14561 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14562 assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14563 let expected = Sexp::List(vec![
14564 Sexp::symbol("list"),
14565 Sexp::int(0),
14566 Sexp::int(1),
14567 Sexp::int(2),
14568 Sexp::int(3),
14569 Sexp::int(99),
14570 ]);
14571 assert_eq!(out_bc, vec![expected]);
14572 }
14573
14574 #[test]
14575 fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14576 // The substitute strategy's top-level `,@x` arm rejects with
14577 // LispError::SpliceOutsideList (a splice marker with no containing
14578 // list to flatten into). Pre-lift the arm was
14579 // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14580 // post-lift the arm routes through `as_unquote` matching
14581 // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14582 // marker. Pin path-uniformity: the rejection MUST fire identically
14583 // through the new projection. The substitute-only strategy bypasses
14584 // the bytecode path, exposing this gate directly.
14585 //
14586 // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14587 // `,@xs` at top level (NOT wrapped in a containing list), so the
14588 // substitute fallback's top-level Splice arm fires.
14589 let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14590 let mut sub = Expander::new_substitute_only();
14591 let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14592 assert!(
14593 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14594 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14595 );
14596 }
14597
14598 #[test]
14599 fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14600 // A `,unbound` template body — the inner symbol isn't a param —
14601 // fires gate-2 (must-be-bound-in-scope). The typed marker
14602 // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14603 // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14604 // rejection-builder. Pre-lift the marker was bound at the per-arm
14605 // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14606 // typed projection. Pin: the rejection's `prefix` slot is
14607 // UnquoteForm::Unquote, structurally derived from the typed
14608 // projection's typed marker, NOT a literal at the arm body.
14609 let src = "(defmacro bad (x) ,unbound)";
14610 let mut bc = Expander::new();
14611 let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14612 match err {
14613 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14614 assert_eq!(
14615 prefix,
14616 UnquoteForm::Unquote,
14617 "typed marker drifted from UnquoteForm::Unquote at gate-2"
14618 );
14619 }
14620 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14621 }
14622 // Sibling negative control: `,@unbound` inside a containing list
14623 // threads UnquoteForm::Splice through the same projection. The
14624 // splice MUST be inside a list — `compile_template` rejects
14625 // top-level `,@X` bodies with SpliceOutsideList before compile_node
14626 // runs, so the typed-marker dispatch on Splice is observable only
14627 // for well-positioned splice bodies. Closes path-uniformity across
14628 // BOTH typed marker variants at the compile path.
14629 let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14630 let mut bc2 = Expander::new();
14631 let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14632 match err_splice {
14633 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14634 assert_eq!(
14635 prefix,
14636 UnquoteForm::Splice,
14637 "typed marker drifted from UnquoteForm::Splice at gate-2"
14638 );
14639 }
14640 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14641 }
14642 }
14643
14644 // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14645 //
14646 // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14647 // two arms and `substitute`'s top-level + list-inner arms through the
14648 // typed-marker projection but LEFT `compile_template`'s top-level
14649 // splice-outside-list gate and `contains_unquote`'s family check inline
14650 // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14651 // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14652 // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14653 // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14654 // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14655 // Every production-site recognizer of an unquote-family wrapper now
14656 // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14657 // variant) pairing for `,@-outside-list` is bound at ONE structural
14658 // query across all FOUR reachable splice-recognition sites, and a future
14659 // regression that drifts the marker pairing at any production site
14660 // becomes a type error at the helper boundary rather than a silent
14661 // per-site divergence.
14662
14663 #[test]
14664 fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14665 // The bytecode path's `compile_template` gate pre-rejects top-level
14666 // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14667 // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14668 // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14669 // match outside the projection itself. Post-lift the (Sexp variant,
14670 // UnquoteForm variant) pairing for the splice-outside-list gate is
14671 // bound at ONE projection function across all three reachable
14672 // emission sites (compile_template top-level, substitute top-level,
14673 // substitute list-inner). Path-uniformity guard at the new boundary:
14674 // the bytecode-path compile-time gate now routes through the same
14675 // shape `substitute`'s list-inner Splice arm uses.
14676 //
14677 // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14678 // peeled by `template_body`, leaving `,@xs` at top level for
14679 // `compile_template` to gate before `compile_node` walks anything.
14680 // The bytecode strategy is default-on (`Expander::new()` sets
14681 // `compile_templates: true`), so the gate fires at
14682 // `register_macro_def` time.
14683 let mut e = Expander::new();
14684 let err = e
14685 .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14686 .expect_err("compile_template must reject top-level ,@X via as_unquote");
14687 assert!(
14688 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14689 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14690 );
14691 }
14692
14693 #[test]
14694 fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14695 // Negative control on the typed-marker dispatch at `compile_template`'s
14696 // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14697 // valid template — `compile_node` lowers it to a single `Subst(idx)`
14698 // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14699 // gate MUST NOT fire on the Unquote variant — only Splice — so the
14700 // typed marker is observably load-bearing: a regression that drifts
14701 // the pattern from `UnquoteForm::Splice` to the wider
14702 // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14703 //
14704 // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14705 // level; `compile_template` admits it via the typed-marker gate,
14706 // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14707 let src = "(defmacro id (x) ,x) (id 42)";
14708 let mut e = Expander::new();
14709 let expanded = e
14710 .expand_program(read(src).unwrap())
14711 .expect("top-level ,X body must compile through as_unquote-typed gate");
14712 assert_eq!(expanded, vec![Sexp::int(42)]);
14713 }
14714
14715 #[test]
14716 fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14717 // The fast-path optimizer in `compile_node` short-circuits on
14718 // `contains_unquote(node)` — true iff the subtree carries an
14719 // unquote-family wrapper. Pre-lift the family check inlined
14720 // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14721 // routes through `as_unquote().is_some()`. Pin path-uniformity at
14722 // the family-recognition boundary: every production-site unquote
14723 // recognizer (compile_template's gate, compile_node's per-arm
14724 // dispatch, substitute's top-level + list-inner arms, AND this
14725 // fast-path predicate) now shares ONE typed-marker projection.
14726 //
14727 // Both variants must trigger the fast-path bail — the optimizer's
14728 // gate keys on the family, not the inner. The recursion into
14729 // Quote/Quasiquote/List subtrees ALSO observes the same family
14730 // gate at every level, so a `,@xs` buried under a `\`...` outer
14731 // wrapper still fires it.
14732 let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14733 let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14734 let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14735 Sexp::symbol("foo"),
14736 Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14737 ])));
14738 // The projection's `is_some()` face MUST agree with the pre-lift
14739 // `matches!()` discriminant on every variant — closed-set guarantee
14740 // shared with `as_unquote`'s contract pin in `ast.rs`.
14741 assert!(super::contains_unquote(&bare_unquote));
14742 assert!(super::contains_unquote(&bare_splice));
14743 assert!(super::contains_unquote(&nested));
14744 // Negative control: shapes the projection rejects (Nil, Atom,
14745 // bare List, Quote-family without inner unquote) must NOT trigger
14746 // the fast-path bail — the projection's None face stays observably
14747 // distinct from its Some face.
14748 assert!(!super::contains_unquote(&Sexp::Nil));
14749 assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14750 assert!(!super::contains_unquote(&Sexp::int(5)));
14751 assert!(!super::contains_unquote(&Sexp::List(vec![
14752 Sexp::symbol("plain"),
14753 Sexp::int(1),
14754 ])));
14755 assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14756 Sexp::symbol("inert")
14757 ))));
14758 }
14759
14760 #[test]
14761 fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14762 // Pin the lift: `contains_unquote`'s quote-family recognition
14763 // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14764 // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14765 // substitution-subset gate) at ONE site — not through the pair
14766 // of `as_unquote().is_some()` + inline
14767 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14768 // path-uniformity assertion: for every quote-family wrapper, the
14769 // function's behavior agrees with the manual composition through
14770 // the two typed-marker projections. A regression that re-inlines
14771 // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14772 // and just returns true for any `as_quote_form().is_some()`, or
14773 // restores the per-variant `Quote | Quasiquote` arm) drifts from
14774 // this composition and surfaces here.
14775 use crate::ast::QuoteForm;
14776
14777 // Sweep every QuoteForm variant under both axes:
14778 // * unquote-only-inner (inner = symbol):
14779 // Unquote(s) → true via `as_unquote_form() == Some`
14780 // UnquoteSplice(s) → true via `as_unquote_form() == Some`
14781 // Quote(s) → false (inner has no unquote, recurses to false)
14782 // Quasiquote(s) → false (same)
14783 // * unquote-inner-inside-quote-wrapper:
14784 // Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14785 // inner `Unquote` returns true through the
14786 // SAME projection — both arms share the
14787 // ONE typed-marker site post-lift)
14788 let inner_plain = Sexp::symbol("x");
14789 let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14790
14791 for qf in QuoteForm::ALL {
14792 let wrapped_plain = match qf {
14793 QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14794 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14795 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14796 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14797 };
14798 // Path-uniformity: behavior derives from the manual two-marker
14799 // composition through `as_quote_form` + `as_unquote_form`.
14800 // The substitution-subset gate (`as_unquote_form().is_some()`)
14801 // returns true for Unquote/UnquoteSplice and false for
14802 // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14803 let via_manual =
14804 qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14805 assert_eq!(
14806 super::contains_unquote(&wrapped_plain),
14807 via_manual,
14808 "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14809 );
14810
14811 let wrapped_unquote = match qf {
14812 QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14813 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14814 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14815 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14816 };
14817 // EVERY quote-family wrapper around an inner `Unquote` must
14818 // contain unquote — either the outer projects as `Some` on
14819 // the substitution subset (Unquote/UnquoteSplice arms), OR
14820 // the outer is Quote/Quasiquote and the recursion into the
14821 // inner re-fires the projection. The pre-lift Quote arm
14822 // recursed via the inline `Sexp::Quote(inner)` match;
14823 // post-lift the SAME recursion fires via `as_quote_form`'s
14824 // typed projection. Both yield `true`.
14825 assert!(
14826 super::contains_unquote(&wrapped_unquote),
14827 "contains_unquote missed an inner Unquote under {qf:?} — \
14828 the quote-family recursion through as_quote_form drifted"
14829 );
14830 }
14831
14832 // Negative control: a non-quote-family wrapper (List, Atom,
14833 // Nil) must NOT route through `as_quote_form` at all. The
14834 // `if let Some(...) = node.as_quote_form()` gate stays `None`
14835 // and falls through to the List/_ arms. This is the structural
14836 // partition: the projection's `None` face MUST agree with
14837 // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14838 // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14839 let nested_in_list = Sexp::List(vec![
14840 Sexp::symbol("outer"),
14841 Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14842 ]);
14843 assert!(
14844 super::contains_unquote(&nested_in_list),
14845 "contains_unquote failed to descend into a List subtree containing a \
14846 Quasiquote(Unquote(_)) — list recursion arm drifted"
14847 );
14848 }
14849
14850 // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14851 // × typed-decoded classifier) ─────────────────────────────────────
14852 //
14853 // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14854 // the dispatcher matrix on the `Expander` surface — pre-lift the
14855 // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14856 // + expand_and_collect_calls_to_any) AND the constant-keyword
14857 // column closed on the named axis (expand_to_named +
14858 // expand_source_to_named) but the (named × classifier) cell was
14859 // open — every consumer that wanted to walk a program by typed-
14860 // decoded classifier AND extract the NAME slot per matched form
14861 // had to compose `expand_and_collect_calls_to_any` with
14862 // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14863 // on the `Expander` surface, and the constant-`T::KEYWORD` named
14864 // sibling routes through it as the (constant-classifier × named)
14865 // specialization.
14866 //
14867 // The tests below pin: (a) the happy path yields the decoded triple
14868 // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14869 // non-matching forms are skipped (soft-projection contract inherited
14870 // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14871 // variant fires for matched forms with no NAME slot, threading the
14872 // classifier-supplied `&'static str` keyword; (d) the
14873 // `NamedFormNonSymbolName` variant fires for matched forms with a
14874 // non-symbol-or-string NAME slot, again threading the classifier-
14875 // supplied keyword; (e) the projection's `Err` short-circuits at
14876 // the first failing matched form; (f) `expand_program` runs BEFORE
14877 // the classifier filter walks — a `(defmacro …)` emitting a named
14878 // form is decoded by the classifier on the EXPANDED head, not the
14879 // macro-call head; (g) the from-source sibling agrees with `read +
14880 // from-forms` on the same source.
14881
14882 #[test]
14883 fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14884 ) {
14885 // The typed-decoded named primitive's happy path: a closed-set
14886 // classifier decodes head symbols to a typed enum PAIRED with
14887 // its canonical static keyword, and the per-form projection
14888 // receives `(decoded, name, spec_args)` for every matched form
14889 // in source order. Sweep across TWO distinct classifier outcomes
14890 // — `Monitor` / `Notify` — interleaved with a non-matching form
14891 // to pin the typed-witness threading AND the NAME-slot
14892 // projection AND the source-order yield AND the rejection of
14893 // non-classifier forms in ONE assertion.
14894 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14895 enum Kind {
14896 Monitor,
14897 Notify,
14898 }
14899 let src = r#"(defmonitor cpu :threshold 80)
14900 (other-form 99)
14901 (defnotify email :to "ops@example.com")
14902 (defmonitor mem :threshold 90)"#;
14903 let forms = read(src).unwrap();
14904 let mut e = Expander::new();
14905 let rows: Vec<(Kind, String, usize)> = e
14906 .expand_and_collect_named_calls_to_any(
14907 forms,
14908 |h| match h {
14909 "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14910 "defnotify" => Some((Kind::Notify, "defnotify")),
14911 _ => None,
14912 },
14913 |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14914 )
14915 .unwrap();
14916 assert_eq!(
14917 rows,
14918 vec![
14919 (Kind::Monitor, "cpu".to_string(), 2),
14920 (Kind::Notify, "email".to_string(), 2),
14921 (Kind::Monitor, "mem".to_string(), 2),
14922 ],
14923 );
14924 }
14925
14926 #[test]
14927 fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
14928 // Soft-projection contract — every shape the typed-decoded
14929 // primitive rejects (non-list, empty list, list whose head is
14930 // not a symbol, list whose head decodes to `None`) skips the
14931 // projection silently. The named gate inside the wrapper
14932 // projection runs ONLY for classifier-matched forms; a non-
14933 // matching form that reaches the projection panics here.
14934 let src = r#":bare-keyword
14935 "bare-string"
14936 42
14937 ()
14938 (foo bar)
14939 (defmonitor cpu :threshold 80)"#;
14940 let forms = read(src).unwrap();
14941 let mut e = Expander::new();
14942 let names: Vec<String> = e
14943 .expand_and_collect_named_calls_to_any(
14944 forms,
14945 |h| (h == "defmonitor").then_some(((), "defmonitor")),
14946 |(), name, _args| Ok(name.to_string()),
14947 )
14948 .unwrap();
14949 assert_eq!(names, vec!["cpu".to_string()]);
14950 }
14951
14952 #[test]
14953 fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
14954 ) {
14955 // `(defmonitor)` matches the classifier but has no NAME slot —
14956 // `split_name_slot` fires `NamedFormMissingName { keyword:
14957 // "defmonitor" }`. The classifier's `&'static str` keyword is
14958 // threaded VERBATIM through the named gate; a regression that
14959 // hardcodes a different keyword (e.g. the matched head text or
14960 // a default) would fail the structural variant identity
14961 // assertion. Fail-before-pass-after: pre-lift the primitive did
14962 // not exist; the inline `expand_and_collect_calls_to_any +
14963 // split_name_slot` composition at the consumer site was the
14964 // only path, with the keyword bound at the consumer's call
14965 // boundary rather than at the named primitive's body.
14966 let forms = read("(defmonitor)").unwrap();
14967 let mut e = Expander::new();
14968 let err = e
14969 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
14970 forms,
14971 |h| (h == "defmonitor").then_some(((), "defmonitor")),
14972 |(), _name, _args| Ok(()),
14973 )
14974 .unwrap_err();
14975 assert!(
14976 matches!(
14977 err,
14978 crate::error::LispError::NamedFormMissingName {
14979 keyword: "defmonitor"
14980 }
14981 ),
14982 "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
14983 );
14984 }
14985
14986 #[test]
14987 fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
14988 ) {
14989 // `(defmonitor 42 :threshold 80)` matches the classifier but
14990 // the NAME slot is an int — `split_name_slot` fires
14991 // `NamedFormNonSymbolName { keyword: "defmonitor", got:
14992 // SexpShape::Int }`. The classifier's `&'static str` keyword is
14993 // threaded through the structural rejection variant verbatim,
14994 // and the typed `SexpShape` projection on the offending slot
14995 // is preserved across the named primitive.
14996 let forms = read("(defmonitor 42 :threshold 80)").unwrap();
14997 let mut e = Expander::new();
14998 let err = e
14999 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15000 forms,
15001 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15002 |(), _name, _args| Ok(()),
15003 )
15004 .unwrap_err();
15005 assert!(
15006 matches!(
15007 err,
15008 crate::error::LispError::NamedFormNonSymbolName {
15009 keyword: "defmonitor",
15010 got: crate::error::SexpShape::Int,
15011 }
15012 ),
15013 "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15014 );
15015 }
15016
15017 #[test]
15018 fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15019 // Result projection's short-circuit contract — when the
15020 // projection returns `Err` on a matched named form, the walk
15021 // stops and subsequent matched forms are NOT projected. Pin
15022 // source-order short-circuit via a counter the projection
15023 // increments before deciding to fail: the counter sits at
15024 // exactly the index of the failing form (second match),
15025 // proving the third match never reached the projection.
15026 let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15027 let mut count = 0usize;
15028 let mut e = Expander::new();
15029 let err = e
15030 .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15031 forms,
15032 |h| (h == "defmon").then_some(((), "defmon")),
15033 |(), name, _args| {
15034 count += 1;
15035 if name == "b" {
15036 return Err(crate::error::LispError::Missing("test-failure"));
15037 }
15038 Ok(name.to_string())
15039 },
15040 )
15041 .expect_err("projection must short-circuit on first Err");
15042 assert_eq!(
15043 count, 2,
15044 "projection must have run on first matched form AND the failing form, then stopped"
15045 );
15046 assert!(
15047 matches!(err, crate::error::LispError::Missing("test-failure")),
15048 "expected the projection's typed Err verbatim, got: {err:?}"
15049 );
15050 }
15051
15052 #[test]
15053 fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15054 // Ordering contract — `expand_program` runs BEFORE the
15055 // typed-decoded named walk. A `(defmacro …)` form that emits a
15056 // classifier-decoded named form must have its EXPANDED head
15057 // decoded by the classifier, not the macro-call head. Pin the
15058 // ordering with a macro `(emit-mon n thr)` expanding to
15059 // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15060 // expanded `defmonitor` head and the NAME slot's symbol value
15061 // from the macro arg.
15062 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15063 (defmonitor cpu :threshold 80)
15064 (emit-mon mem 90)
15065 (other-form 99)
15066 (emit-mon disk 70)"#;
15067 let forms = read(src).unwrap();
15068 let mut e = Expander::new();
15069 let names: Vec<String> = e
15070 .expand_and_collect_named_calls_to_any(
15071 forms,
15072 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15073 |(), name, _args| Ok(name.to_string()),
15074 )
15075 .unwrap();
15076 // `cpu` from the literal call, `mem` from the first macro
15077 // emit, `disk` from the second macro emit. The macro-emitted
15078 // forms are present BECAUSE `expand_program` ran first.
15079 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15080 }
15081
15082 #[test]
15083 fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15084 // From-source posture parity: feeding source through
15085 // `expand_source_and_collect_named_calls_to_any` is byte-
15086 // identical to feeding `read(src)?` through the from-forms
15087 // sibling on a fresh expander, because the from-source posture
15088 // is `read(src)? + from-forms` by construction. A regression
15089 // that drifts the from-source posture's pipeline from the
15090 // from-forms posture's pipeline would fail here.
15091 let src = r#"(defmonitor cpu :threshold 80)
15092 (defmonitor mem :threshold 90)"#;
15093 let via_src: Vec<String> = Expander::new()
15094 .expand_source_and_collect_named_calls_to_any(
15095 src,
15096 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15097 |(), name, _args| Ok(name.to_string()),
15098 )
15099 .unwrap();
15100 let forms = read(src).unwrap();
15101 let via_forms: Vec<String> = Expander::new()
15102 .expand_and_collect_named_calls_to_any(
15103 forms,
15104 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15105 |(), name, _args| Ok(name.to_string()),
15106 )
15107 .unwrap();
15108 assert_eq!(via_src, via_forms);
15109 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15110 }
15111
15112 #[test]
15113 fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15114 ) {
15115 // `?`-routing through `read` short-circuits BEFORE the
15116 // classifier or the named gate runs. Unbalanced paren — reader
15117 // error fires; classifier panic-decoder MUST NOT execute,
15118 // proving the read step gates the entire pipeline.
15119 let mut e = Expander::new();
15120 let err = e
15121 .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15122 "(defmonitor cpu :threshold 80",
15123 |_h| panic!("classifier must not run when reader fails"),
15124 |(), _name, _args| Ok(()),
15125 )
15126 .unwrap_err();
15127 // The named gate's variants MUST NOT fire — the reader error
15128 // is structurally distinct.
15129 assert!(
15130 !matches!(
15131 err,
15132 crate::error::LispError::NamedFormMissingName { .. }
15133 | crate::error::LispError::NamedFormNonSymbolName { .. }
15134 ),
15135 "expected reader error, not named-gate variant; got: {err:?}"
15136 );
15137 }
15138
15139 // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15140 // constant-keyword × untyped R) ─────────────────────────────────
15141 //
15142 // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15143 // cell of the `Expander` typed-walk family — pre-lift the cell was
15144 // reachable ONLY through `expand_to_named<T>` with the
15145 // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15146 // AND the `T::compile_from_args`-based projection through
15147 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15148 // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15149 // method that takes the keyword and the `(name, args) -> R`
15150 // projection as caller-supplied parameters, AND
15151 // `expand_to_named<T>` routes through it as the typed
15152 // `T::KEYWORD`-constant specialization — so the named-form
15153 // `split_name_slot` composition lives at ONE site
15154 // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15155 // than at TWO sites pre-lift (the bare-kwargs path through
15156 // `named_form_projection<T>` AND the classifier path).
15157 //
15158 // The tests below pin: (a) the happy path yields the `(name, args)`
15159 // pair for every matched form in source order; (b) non-matching
15160 // forms are skipped (soft-projection contract inherited from the
15161 // named typed-decoded primitive); (c) the `NamedFormMissingName`
15162 // variant fires for matched forms with no NAME slot, threading the
15163 // primitive's `&'static str` keyword; (d) the
15164 // `NamedFormNonSymbolName` variant fires for matched forms with a
15165 // non-symbol-or-string NAME slot, again threading the primitive's
15166 // keyword; (e) the projection's `Err` short-circuits at the first
15167 // failing matched form; (f) `expand_program` runs BEFORE the
15168 // keyword filter walks — a `(defmacro …)` emitting a named form
15169 // has its EXPANDED head matched by the constant-keyword filter,
15170 // not the macro-call head; (g) the from-source sibling agrees with
15171 // `read + from-forms` on the same source; (h) the constant-
15172 // classifier composition law binds the runtime-keyword cell
15173 // (`expand_and_collect_named_calls_to`) to the typed-decoded
15174 // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15175 // constant-classifier decoder.
15176
15177 #[test]
15178 fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15179 ) {
15180 // The constant-keyword named primitive's happy path: a runtime
15181 // `&'static str` keyword filters matched forms, and the per-
15182 // form projection receives `(name, spec_args)` for every
15183 // matched form in source order. Three matched forms
15184 // interleaved with a non-matcher pin the source-order yield,
15185 // the NAME-slot projection, AND the rejection of non-keyword
15186 // forms in ONE assertion.
15187 let src = r#"(defmonitor cpu :threshold 80)
15188 (other-form 99)
15189 (defmonitor mem :threshold 90 :unit "MiB")
15190 (defmonitor disk :threshold 70)"#;
15191 let forms = read(src).unwrap();
15192 let mut e = Expander::new();
15193 let rows: Vec<(String, usize)> = e
15194 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15195 Ok((name.to_string(), spec_args.len()))
15196 })
15197 .unwrap();
15198 assert_eq!(
15199 rows,
15200 vec![
15201 ("cpu".to_string(), 2),
15202 ("mem".to_string(), 4),
15203 ("disk".to_string(), 2),
15204 ],
15205 );
15206 }
15207
15208 #[test]
15209 fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15210 // Soft-projection contract — every shape the named typed-
15211 // decoded primitive rejects (non-list, empty list, list whose
15212 // head is not a symbol, list whose head doesn't match the
15213 // constant keyword) skips the projection silently. The named
15214 // gate inside the wrapper projection runs ONLY for matched
15215 // forms; a non-matching form that reaches the projection
15216 // panics here.
15217 let src = r#":bare-keyword
15218 "bare-string"
15219 42
15220 ()
15221 (foo bar)
15222 (defmonitor cpu :threshold 80)"#;
15223 let forms = read(src).unwrap();
15224 let mut e = Expander::new();
15225 let names: Vec<String> = e
15226 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15227 Ok(name.to_string())
15228 })
15229 .unwrap();
15230 assert_eq!(names, vec!["cpu".to_string()]);
15231 }
15232
15233 #[test]
15234 fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15235 // The named-form gate's missing-NAME variant must thread the
15236 // primitive's `&'static str` keyword (NOT a hardcoded literal,
15237 // NOT the projection's own copy) — the `NamedFormMissingName
15238 // { keyword }` slot inside `split_name_slot` is bound at the
15239 // call site BY THE PRIMITIVE itself, via the constant-
15240 // classifier decoder threading `((), keyword)` into
15241 // `expand_and_collect_named_calls_to_any`.
15242 let src = r#"(defmonitor)"#;
15243 let forms = read(src).unwrap();
15244 let mut e = Expander::new();
15245 let err = e
15246 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15247 .unwrap_err();
15248 assert!(
15249 matches!(
15250 err,
15251 crate::error::LispError::NamedFormMissingName {
15252 keyword: "defmonitor"
15253 }
15254 ),
15255 "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15256 );
15257 }
15258
15259 #[test]
15260 fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15261 ) {
15262 // The named-form gate's non-symbol-NAME variant must thread
15263 // the primitive's `&'static str` keyword AND the typed-shape
15264 // witness (`SexpShape::Int` for an integer NAME slot). The
15265 // shape projection through `split_name_slot`'s
15266 // `as_symbol_or_string()` gate fires on the first matched
15267 // form whose NAME slot is not a symbol or string.
15268 let src = r#"(defmonitor 42 :threshold 80)"#;
15269 let forms = read(src).unwrap();
15270 let mut e = Expander::new();
15271 let err = e
15272 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15273 .unwrap_err();
15274 match err {
15275 crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15276 assert_eq!(keyword, "defmonitor");
15277 assert_eq!(got, crate::error::SexpShape::Int);
15278 }
15279 other => panic!(
15280 "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15281 ),
15282 }
15283 }
15284
15285 #[test]
15286 fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15287 // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15288 // the first failing projection. The second matched form's
15289 // projection error MUST fire AND the third matched form's
15290 // projection MUST NOT run. Pin both halves with a counter +
15291 // a fail-on-call sentinel.
15292 let src = r#"(defmonitor cpu :threshold 80)
15293 (defmonitor mem :threshold 90)
15294 (defmonitor disk :threshold 70)"#;
15295 let forms = read(src).unwrap();
15296 let mut count: usize = 0;
15297 let mut e = Expander::new();
15298 let err = e
15299 .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15300 count += 1;
15301 if name == "mem" {
15302 Err(crate::error::LispError::Compile {
15303 form: "test".into(),
15304 message: format!("rejecting at NAME={name}"),
15305 })
15306 } else {
15307 Ok(name.to_string())
15308 }
15309 })
15310 .unwrap_err();
15311 assert_eq!(count, 2, "third matched form must not be projected");
15312 match err {
15313 crate::error::LispError::Compile { message, .. } => {
15314 assert!(message.contains("NAME=mem"));
15315 }
15316 other => panic!("expected projection-driven Compile error; got: {other:?}"),
15317 }
15318 }
15319
15320 #[test]
15321 fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15322 // Ordering contract — `expand_program` runs BEFORE the
15323 // constant-keyword named walk. A `(defmacro …)` form that
15324 // emits a matched named form must have its EXPANDED head
15325 // matched by the constant-keyword filter, not the macro-call
15326 // head. Pin the ordering with a macro `(emit-mon n thr)`
15327 // expanding to `(defmonitor ,n :threshold ,thr)` — the
15328 // constant-keyword filter matches the expanded `defmonitor`
15329 // head and the NAME slot projects the symbol-author value.
15330 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15331 (defmonitor cpu :threshold 80)
15332 (emit-mon mem 90)
15333 (other-form 99)
15334 (emit-mon disk 70)"#;
15335 let forms = read(src).unwrap();
15336 let mut e = Expander::new();
15337 let names: Vec<String> = e
15338 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15339 Ok(name.to_string())
15340 })
15341 .unwrap();
15342 // `cpu` from the literal call, `mem` from the first macro
15343 // emit, `disk` from the second macro emit. The macro-emitted
15344 // forms are present BECAUSE `expand_program` ran first.
15345 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15346 }
15347
15348 #[test]
15349 fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15350 // From-source posture parity: feeding source through
15351 // `expand_source_and_collect_named_calls_to` is byte-identical
15352 // to feeding `read(src)?` through the from-forms sibling on a
15353 // fresh expander, because the from-source posture is
15354 // `read(src)? + from-forms` by construction. A regression that
15355 // drifts the from-source posture's pipeline from the from-
15356 // forms posture's pipeline would fail here.
15357 let src = r#"(defmonitor cpu :threshold 80)
15358 (defmonitor mem :threshold 90)"#;
15359 let via_src: Vec<String> = Expander::new()
15360 .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15361 Ok(name.to_string())
15362 })
15363 .unwrap();
15364 let forms = read(src).unwrap();
15365 let via_forms: Vec<String> = Expander::new()
15366 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15367 Ok(name.to_string())
15368 })
15369 .unwrap();
15370 assert_eq!(via_src, via_forms);
15371 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15372 }
15373
15374 #[test]
15375 fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15376 ) {
15377 // `?`-routing through `read` short-circuits BEFORE the
15378 // constant-keyword filter, the named gate, or the projection
15379 // runs. Unbalanced paren — reader error fires; the projection
15380 // panic-payload MUST NOT execute, proving the read step gates
15381 // the entire pipeline.
15382 let mut e = Expander::new();
15383 let err = e
15384 .expand_source_and_collect_named_calls_to::<(), _>(
15385 "(defmonitor cpu :threshold 80",
15386 "defmonitor",
15387 |_name, _args| panic!("projection must not run when reader fails"),
15388 )
15389 .unwrap_err();
15390 // The named gate's variants MUST NOT fire — the reader error
15391 // is structurally distinct.
15392 assert!(
15393 !matches!(
15394 err,
15395 crate::error::LispError::NamedFormMissingName { .. }
15396 | crate::error::LispError::NamedFormNonSymbolName { .. }
15397 ),
15398 "expected reader error, not named-gate variant; got: {err:?}",
15399 );
15400 }
15401
15402 #[test]
15403 fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15404 ) {
15405 // Composition-identity test pinning the runtime-keyword named
15406 // cell (`expand_and_collect_named_calls_to`) to the typed-
15407 // decoded named-classifier cell
15408 // (`expand_and_collect_named_calls_to_any`) via the constant-
15409 // classifier decoder shape. Post-lift the identity:
15410 //
15411 // expand_and_collect_named_calls_to(forms, kw, project) ==
15412 // expand_and_collect_named_calls_to_any(forms,
15413 // |h| (h == kw).then_some(((), kw)),
15414 // |(), name, args| project(name, args))
15415 //
15416 // Pinning the identity here makes the typed-decoded named-
15417 // classifier primitive the CANONICAL composition point the
15418 // runtime-keyword sibling routes through — parallel to how
15419 // `expand_and_collect_calls_to` routes through
15420 // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15421 // then_some(())` decoder on the bare-kwargs axis. A future
15422 // regression that drifts ONE cell's NAME-slot rejection chain
15423 // from the other becomes loudly visible at this assertion.
15424 let src = r#"(defmonitor cpu :threshold 80)
15425 (other-form 99)
15426 (defmonitor mem :threshold 90)"#;
15427 let forms = read(src).unwrap();
15428 let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15429 .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15430 Ok((name.to_string(), args.len()))
15431 })
15432 .unwrap();
15433 let via_classifier: Vec<(String, usize)> = Expander::new()
15434 .expand_and_collect_named_calls_to_any(
15435 forms,
15436 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15437 |(), name, args| Ok((name.to_string(), args.len())),
15438 )
15439 .unwrap();
15440 assert_eq!(via_constant_keyword, via_classifier);
15441 assert_eq!(
15442 via_constant_keyword,
15443 vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15444 );
15445 }
15446
15447 #[test]
15448 fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15449 ) {
15450 // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15451 // (the typed constant-keyword named cell) must yield the same
15452 // payload as `expand_and_collect_named_calls_to(forms,
15453 // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15454 // NamedDefinition })` — the lift's structural identity.
15455 //
15456 // Pre-lift `expand_to_named<T>` routed through
15457 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15458 // named_form_projection::<T>)` (the bare-kwargs constant
15459 // primitive with `named_form_projection<T>` doing the NAME
15460 // extraction internally). Post-lift `expand_to_named<T>`
15461 // routes through the named constant-keyword primitive (which
15462 // itself routes through the classifier primitive via a
15463 // constant-classifier decoder) — so the `split_name_slot`
15464 // composition lives at ONE site (the `_any` primitive body)
15465 // post-lift rather than at TWO sites.
15466 use crate::compile::NamedDefinition;
15467 use crate::compiler_spec::CompilerSpec;
15468 let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15469 (defcompiler beta-compiler :name "y" :dialect "standard")"#;
15470 let forms = read(src).unwrap();
15471 let via_expand_to_named = Expander::new()
15472 .expand_to_named::<CompilerSpec>(forms.clone())
15473 .unwrap();
15474 let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15475 .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15476 let spec =
15477 <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15478 Ok(NamedDefinition {
15479 name: name.to_string(),
15480 spec,
15481 })
15482 })
15483 .unwrap();
15484 assert_eq!(via_expand_to_named.len(), 2);
15485 assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15486 for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15487 assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15488 assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15489 }
15490 assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15491 assert_eq!(via_expand_to_named[0].spec.name, "x");
15492 assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15493 assert_eq!(via_expand_to_named[1].spec.name, "y");
15494 }
15495
15496 // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15497
15498 #[test]
15499 fn expander_default_max_expansion_depth_is_the_module_constant() {
15500 // Both constructors seed `max_expansion_depth` from the module
15501 // constant — a regression that drifts one constructor's default
15502 // from the constant (e.g. a hardcoded `256` at ONE site with the
15503 // other left uninitialized after a `Default` derive addition)
15504 // fails this pin.
15505 assert_eq!(
15506 Expander::new().max_expansion_depth(),
15507 DEFAULT_MAX_EXPANSION_DEPTH
15508 );
15509 assert_eq!(
15510 Expander::new_substitute_only().max_expansion_depth(),
15511 DEFAULT_MAX_EXPANSION_DEPTH
15512 );
15513 assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15514 }
15515
15516 #[test]
15517 fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15518 let mut e = Expander::new();
15519 e.set_max_expansion_depth(7);
15520 assert_eq!(e.max_expansion_depth(), 7);
15521 }
15522
15523 #[test]
15524 fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15525 // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15526 // pre-lift stack-overflowed the process — a below-floor abort.
15527 // Post-lift the expander returns a typed
15528 // `LispError::ExpansionDepthExceeded` at the ceiling with
15529 // `macro_name` populated from the offending call's head.
15530 let mut e = Expander::new();
15531 e.set_max_expansion_depth(4);
15532 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15533 let err = e.expand_program(forms).unwrap_err();
15534 match err {
15535 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15536 assert_eq!(macro_name, "loop");
15537 assert_eq!(limit, 4);
15538 }
15539 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15540 }
15541 }
15542
15543 #[test]
15544 fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15545 // The substitute strategy shares the same `expand` outer walker
15546 // with the bytecode strategy, so the ceiling fires at the SAME
15547 // gate regardless of which apply-layer is active. The pin makes
15548 // that shared-outer-walker property structural: a regression
15549 // that ever branches the depth guard on strategy (e.g. moves it
15550 // into `apply_compiled` alone) fails this test on the
15551 // substitute expander.
15552 let mut e = Expander::new_substitute_only();
15553 e.set_max_expansion_depth(4);
15554 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15555 let err = e.expand_program(forms).unwrap_err();
15556 match err {
15557 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15558 assert_eq!(macro_name, "loop");
15559 assert_eq!(limit, 4);
15560 }
15561 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15562 }
15563 }
15564
15565 #[test]
15566 fn expand_lawful_nested_macros_within_ceiling_succeed() {
15567 // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15568 // the low single digits because the tree-child arm doesn't
15569 // count — only the post-apply re-expansion path bumps the
15570 // counter. A ceiling of 3 is enough for a two-deep nested
15571 // expansion. This pin is the fail-before/pass-after negative
15572 // control for the recursive-macro rejection tests above: the
15573 // depth counter must NOT reject lawful macro-nesting within
15574 // the ceiling, and only reject the runaway shape.
15575 let mut e = Expander::new();
15576 e.set_max_expansion_depth(3);
15577 let forms = read(
15578 "(defmacro twice (x) `(list ,x ,x))
15579 (twice (twice hey))",
15580 )
15581 .unwrap();
15582 let out = e.expand_program(forms).unwrap();
15583 assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15584 }
15585
15586 #[test]
15587 fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15588 // A macro-free lawful deep tree — five levels of plain nesting —
15589 // is expanded at a ceiling well below the tree's height. The
15590 // tree-child arm does not accrete depth, so tree height is
15591 // orthogonal to the ceiling; only macro re-expansion counts.
15592 // A regression that ever bumps depth on tree-child descent
15593 // fails this pin.
15594 let mut e = Expander::new();
15595 e.set_max_expansion_depth(2);
15596 let forms = read("(a (b (c (d (e f)))))").unwrap();
15597 let out = e.expand_program(forms).unwrap();
15598 assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15599 }
15600
15601 #[test]
15602 fn expansion_depth_exceeded_position_is_none() {
15603 // The variant joins the non-positional cohort — the offending
15604 // macro's byte offset is not what a runaway diagnostic anchors
15605 // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15606 // keeps the variant in the `position() -> None` cohort so a
15607 // future span-carrying edit lands the field in ONE place
15608 // across every non-positional variant simultaneously.
15609 let err = LispError::ExpansionDepthExceeded {
15610 macro_name: "loop".to_string(),
15611 limit: 256,
15612 };
15613 assert_eq!(err.position(), None);
15614 }
15615
15616 #[test]
15617 fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15618 // Display projection carries both `macro_name` and `limit`, so
15619 // authoring surfaces that substring-grep the rendered
15620 // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15621 // the variant boundary and never need to substring-parse
15622 // free-form text to recover the identity of the offending
15623 // macro or the ceiling it breached.
15624 let err = LispError::ExpansionDepthExceeded {
15625 macro_name: "loop".to_string(),
15626 limit: 256,
15627 };
15628 let rendered = err.to_string();
15629 assert!(rendered.contains("loop"), "rendered: {rendered}");
15630 assert!(rendered.contains("256"), "rendered: {rendered}");
15631 assert!(
15632 rendered.contains("macro expansion depth exceeded"),
15633 "rendered: {rendered}"
15634 );
15635 }
15636
15637 // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15638
15639 #[test]
15640 fn expander_default_max_cache_entries_is_the_module_constant() {
15641 // Both cache-enabling constructors seed `max_cache_entries` from
15642 // the module constant — a regression that drifts one
15643 // constructor's default from the constant (e.g. a hardcoded
15644 // `8192` at ONE site with the other left uninitialized after a
15645 // `Default` derive addition) fails this pin. The peer
15646 // constructor `new_substitute_only` seeds the same ceiling so
15647 // the field stays initialized under `#[derive(Clone)]` (a
15648 // zero-initialized clone would be a silent bifurcation of the
15649 // cache-ceiling surface).
15650 assert_eq!(
15651 Expander::new().max_cache_entries(),
15652 DEFAULT_MAX_CACHE_ENTRIES
15653 );
15654 assert_eq!(
15655 Expander::new_substitute_only().max_cache_entries(),
15656 DEFAULT_MAX_CACHE_ENTRIES
15657 );
15658 assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15659 }
15660
15661 #[test]
15662 fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15663 // Setter mirrors accessor — a regression that ever forgets to
15664 // write through to the field, or writes to a shadow field the
15665 // accessor does not read, fails this pin.
15666 let mut e = Expander::new();
15667 e.set_max_cache_entries(7);
15668 assert_eq!(e.max_cache_entries(), 7);
15669 }
15670
15671 #[test]
15672 fn expand_cache_size_is_bounded_by_max_cache_entries() {
15673 // Five distinct `(name, args)` pairs expanded against a cache
15674 // ceiling of `2` populate the cache with EXACTLY the first two
15675 // insertions and skip the remaining three. Every expansion
15676 // still returns the correct result — the cache is a pure
15677 // PERFORMANCE optimization; the ceiling gates memoization,
15678 // never correctness. LOAD-BEARING true-arm catch on the
15679 // bounded-cache contract: a regression that ignored the
15680 // ceiling (unbounded insert) would land `cache_size() == 5`
15681 // here; a regression that skipped insertion entirely (broken
15682 // cache) would still see correct expansion output but would
15683 // fail the interior `cache_size() == 2` pin.
15684 let mut e = Expander::new();
15685 e.set_max_cache_entries(2);
15686 let src = "
15687 (defmacro id (x) `,x)
15688 (id one)
15689 (id two)
15690 (id three)
15691 (id four)
15692 (id five)
15693 ";
15694 let out = e.expand_program(read(src).unwrap()).unwrap();
15695 assert_eq!(out.len(), 5);
15696 assert_eq!(out[0], parse("one"));
15697 assert_eq!(out[1], parse("two"));
15698 assert_eq!(out[2], parse("three"));
15699 assert_eq!(out[3], parse("four"));
15700 assert_eq!(out[4], parse("five"));
15701 assert_eq!(
15702 e.cache_size(),
15703 2,
15704 "cache grew past the max_cache_entries ceiling",
15705 );
15706 }
15707
15708 #[test]
15709 fn expand_zero_cache_ceiling_disables_caching_effectively() {
15710 // Zero ceiling admits every fresh `(name, args)` pair through
15711 // the compute path and never grows the cache — the same
15712 // observable behavior as `set_cache_enabled(false)` but
15713 // reached through the ceiling knob. Correctness is preserved:
15714 // the miss path always recomputes the same value the cache
15715 // would have returned, so an operator that dials the ceiling
15716 // to `0` (e.g. to hunt a cache-related regression) never
15717 // bifurcates the output surface. Peer to
15718 // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15719 // ceilings admit the zero endpoint as a well-defined
15720 // extremum.
15721 let mut e = Expander::new();
15722 e.set_max_cache_entries(0);
15723 let src = "
15724 (defmacro id (x) `,x)
15725 (id one)
15726 (id two)
15727 ";
15728 let out = e.expand_program(read(src).unwrap()).unwrap();
15729 assert_eq!(out.len(), 2);
15730 assert_eq!(out[0], parse("one"));
15731 assert_eq!(out[1], parse("two"));
15732 assert_eq!(
15733 e.cache_size(),
15734 0,
15735 "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15736 );
15737 }
15738
15739 #[test]
15740 fn expand_cached_hits_survive_past_the_ceiling() {
15741 // Once a `(name, args)` pair sits IN the cache under the
15742 // ceiling, subsequent calls hit it — the ceiling only gates
15743 // NEW-KEY inserts, never lookups. This test seeds the cache
15744 // with two entries under a ceiling of `2`, then re-issues the
15745 // SAME calls to prove the cached hits still fire (cache_size
15746 // stays at `2` because no new keys are inserted, and the
15747 // expansion output is stable). LOAD-BEARING true-arm catch
15748 // that the ceiling gates the INSERT path alone — a regression
15749 // that ever refused cache LOOKUPS at the ceiling would
15750 // silently bifurcate correctness on the second call.
15751 let mut e = Expander::new();
15752 e.set_max_cache_entries(2);
15753 let src = "
15754 (defmacro id (x) `,x)
15755 (id one)
15756 (id two)
15757 (id one)
15758 (id two)
15759 ";
15760 let out = e.expand_program(read(src).unwrap()).unwrap();
15761 assert_eq!(out.len(), 4);
15762 assert_eq!(out[0], parse("one"));
15763 assert_eq!(out[1], parse("two"));
15764 assert_eq!(out[2], parse("one"));
15765 assert_eq!(out[3], parse("two"));
15766 assert_eq!(
15767 e.cache_size(),
15768 2,
15769 "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15770 );
15771 }
15772
15773 #[test]
15774 fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15775 // After the cache reaches its ceiling and subsequent inserts
15776 // are skipped, `clear_cache` empties the memo and RE-OPENS the
15777 // insert path — the next expansion populates the freshly-
15778 // empty cache without operator intervention on the ceiling
15779 // itself. The pin binds the operator-facing recovery path
15780 // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15781 // insertion until the operator clears the cache via
15782 // [`Expander::clear_cache`]".
15783 let mut e = Expander::new();
15784 e.set_max_cache_entries(1);
15785 let src_fill = "
15786 (defmacro id (x) `,x)
15787 (id one)
15788 (id two)
15789 ";
15790 let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15791 assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15792 e.clear_cache();
15793 assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15794 let out = e
15795 .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15796 .unwrap();
15797 assert_eq!(out.len(), 1);
15798 assert_eq!(out[0], parse("three"));
15799 assert_eq!(
15800 e.cache_size(),
15801 1,
15802 "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15803 );
15804 }
15805
15806 // ── max_expansion_size: bounded-output ceiling ─────────────────────
15807
15808 #[test]
15809 fn expander_default_max_expansion_size_is_the_module_constant() {
15810 // Both constructors seed `max_expansion_size` from the module
15811 // constant — a regression that drifts one constructor's
15812 // default from the constant (e.g. a hardcoded `65_536` at
15813 // ONE site with the other left uninitialized after a
15814 // `Default` derive addition) fails this pin. Peer to the
15815 // `max_expansion_depth` + `max_cache_entries` default-
15816 // coherence pins one RESOURCE-DIMENSION axis over.
15817 assert_eq!(
15818 Expander::new().max_expansion_size(),
15819 DEFAULT_MAX_EXPANSION_SIZE
15820 );
15821 assert_eq!(
15822 Expander::new_substitute_only().max_expansion_size(),
15823 DEFAULT_MAX_EXPANSION_SIZE
15824 );
15825 assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15826 }
15827
15828 #[test]
15829 fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15830 // Setter mirrors accessor — a regression that ever forgets to
15831 // write through to the field, or writes to a shadow field the
15832 // accessor does not read, fails this pin. Peer to the
15833 // setter/accessor coherence pins on the depth + cache axes.
15834 let mut e = Expander::new();
15835 e.set_max_expansion_size(64);
15836 assert_eq!(e.max_expansion_size(), 64);
15837 }
15838
15839 #[test]
15840 fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15841 // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15842 // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15843 // (outer List + 5 children). With a ceiling of `4` this
15844 // crosses the OUTPUT-SIZE threshold at the first apply and
15845 // the expander returns a typed
15846 // `LispError::ExpansionSizeExceeded` with `macro_name`,
15847 // `size`, and `limit` all populated. Pre-lift the runaway
15848 // just accumulated in a growing tree until the process
15849 // heap ran out; post-lift the rejection sits at the same
15850 // call boundary as every other macro-expansion failure.
15851 let mut e = Expander::new();
15852 e.set_max_expansion_size(4);
15853 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15854 let err = e.expand_program(forms).unwrap_err();
15855 match err {
15856 LispError::ExpansionSizeExceeded {
15857 macro_name,
15858 size,
15859 limit,
15860 } => {
15861 assert_eq!(macro_name, "bomb");
15862 assert_eq!(size, 6);
15863 assert_eq!(limit, 4);
15864 }
15865 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15866 }
15867 }
15868
15869 #[test]
15870 fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15871 // The substitute strategy shares the same `expand_with_depth`
15872 // outer walker with the bytecode strategy, so the ceiling
15873 // fires at the SAME gate regardless of which apply-layer is
15874 // active. The pin makes that shared-outer-walker property
15875 // structural: a regression that ever branches the size guard
15876 // on strategy (e.g. moves it into `apply_compiled` alone)
15877 // fails this test on the substitute expander. Peer to
15878 // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15879 // one RESOURCE axis over.
15880 let mut e = Expander::new_substitute_only();
15881 e.set_max_expansion_size(4);
15882 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15883 let err = e.expand_program(forms).unwrap_err();
15884 match err {
15885 LispError::ExpansionSizeExceeded {
15886 macro_name,
15887 size,
15888 limit,
15889 } => {
15890 assert_eq!(macro_name, "bomb");
15891 assert_eq!(size, 6);
15892 assert_eq!(limit, 4);
15893 }
15894 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15895 }
15896 }
15897
15898 #[test]
15899 fn expand_lawful_output_within_size_ceiling_succeeds() {
15900 // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15901 // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15902 // the output sits AT the ceiling (`<=` admits equality; only
15903 // `>` rejects), so the expansion succeeds. Fail-before/pass-
15904 // after negative control for the bomb rejection tests: a
15905 // regression that ever rejected at-ceiling output would fail
15906 // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15907 // one RESOURCE axis over.
15908 let mut e = Expander::new();
15909 e.set_max_expansion_size(4);
15910 let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15911 let out = e.expand_program(forms).unwrap();
15912 assert_eq!(out[0], parse("(list hey hey)"));
15913 }
15914
15915 #[test]
15916 fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15917 // A macro-free lawful large tree is NOT gated by the size
15918 // ceiling — the ceiling gates macro-`apply` outputs alone,
15919 // not tree-child descents through non-macro forms. A `usize::MAX`
15920 // ceiling would trivially admit anything; this pin uses a
15921 // deliberately small ceiling of `4` on a tree with 6 nodes
15922 // to catch a regression that ever bumps the ceiling on tree
15923 // descent. A tree-only walk should NEVER fire the ceiling.
15924 // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
15925 // one RESOURCE axis over.
15926 let mut e = Expander::new();
15927 e.set_max_expansion_size(4);
15928 // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
15929 let forms = read("(a (b (c d)))").unwrap();
15930 let out = e.expand_program(forms).unwrap();
15931 assert_eq!(out[0], parse("(a (b (c d)))"));
15932 }
15933
15934 #[test]
15935 fn expansion_size_exceeded_position_is_none() {
15936 // The variant joins the non-positional cohort — the offending
15937 // macro's byte offset is not what an expansion-bomb diagnostic
15938 // anchors to; the (offending macro name, observed size,
15939 // configured ceiling) triple is. This pin keeps the variant
15940 // in the `position() -> None` cohort so a future span-carrying
15941 // edit lands the field in ONE place across every non-positional
15942 // variant simultaneously. Peer to
15943 // `expansion_depth_exceeded_position_is_none` one RESOURCE
15944 // axis over.
15945 let err = LispError::ExpansionSizeExceeded {
15946 macro_name: "bomb".to_string(),
15947 size: 512,
15948 limit: 256,
15949 };
15950 assert_eq!(err.position(), None);
15951 }
15952
15953 #[test]
15954 fn expansion_size_exceeded_display_matches_typed_variant_shape() {
15955 // Display projection carries `macro_name`, `size`, AND
15956 // `limit`, so authoring surfaces that substring-grep the
15957 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
15958 // THREE halves at the variant boundary and never need to
15959 // substring-parse free-form text to recover the identity of
15960 // the offending macro, the observed size, or the ceiling it
15961 // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
15962 // one RESOURCE axis over.
15963 let err = LispError::ExpansionSizeExceeded {
15964 macro_name: "bomb".to_string(),
15965 size: 512,
15966 limit: 256,
15967 };
15968 let rendered = err.to_string();
15969 assert!(rendered.contains("bomb"), "rendered: {rendered}");
15970 assert!(rendered.contains("512"), "rendered: {rendered}");
15971 assert!(rendered.contains("256"), "rendered: {rendered}");
15972 assert!(
15973 rendered.contains("macro expansion output size exceeded"),
15974 "rendered: {rendered}"
15975 );
15976 }
15977
15978 #[test]
15979 fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
15980 // A lawful outer macro that expands to a call of a bomb inner
15981 // macro rejects at the INNER call — `macro_name` at the
15982 // variant boundary is the name of the macro whose `apply`
15983 // output crossed the ceiling, not the outermost caller.
15984 // Load-bearing name-provenance pin: an operator whose bomb
15985 // sits N levels deep in a compose chain needs to see the
15986 // INNER macro's name in the diagnostic, not the wrapping
15987 // shape. A regression that ever populates `macro_name` from
15988 // the outermost caller's head fails this pin.
15989 let mut e = Expander::new();
15990 e.set_max_expansion_size(4);
15991 // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
15992 // output that crosses the ceiling. Expected: the diagnostic
15993 // names `bomb`, not `wrapper`.
15994 let src = "
15995 (defmacro bomb (x) `(list ,x ,x ,x ,x))
15996 (defmacro wrapper (x) `(bomb ,x))
15997 (wrapper hey)
15998 ";
15999 let err = e.expand_program(read(src).unwrap()).unwrap_err();
16000 match err {
16001 LispError::ExpansionSizeExceeded {
16002 macro_name,
16003 size,
16004 limit,
16005 } => {
16006 assert_eq!(macro_name, "bomb");
16007 assert_eq!(size, 6);
16008 assert_eq!(limit, 4);
16009 }
16010 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16011 }
16012 }
16013
16014 // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16015 // Peer to depth (recursion length) + cache (memoization width) +
16016 // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16017 // axis over — the three prior guards fire at EXPAND time; this
16018 // one fires at REGISTER time. Closes the expander's RESOURCE
16019 // surface at a fourth typed dimension across two pipeline stages.
16020
16021 #[test]
16022 fn expander_default_max_macro_body_size_is_the_module_constant() {
16023 // Both constructors seed `max_macro_body_size` from the
16024 // module constant — a regression that drifts one
16025 // constructor's default from the constant (e.g. a hardcoded
16026 // `16_384` at ONE site with the other left uninitialized
16027 // after a `Default` derive addition) fails this pin. Peer
16028 // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16029 // pins one PIPELINE-STAGE axis over.
16030 assert_eq!(
16031 Expander::new().max_macro_body_size(),
16032 DEFAULT_MAX_MACRO_BODY_SIZE
16033 );
16034 assert_eq!(
16035 Expander::new_substitute_only().max_macro_body_size(),
16036 DEFAULT_MAX_MACRO_BODY_SIZE
16037 );
16038 assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16039 }
16040
16041 #[test]
16042 fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16043 // Setter mirrors accessor — a regression that ever forgets to
16044 // write through to the field, or writes to a shadow field the
16045 // accessor does not read, fails this pin. Peer to the
16046 // setter/accessor coherence pins on the depth + cache +
16047 // expansion-size axes.
16048 let mut e = Expander::new();
16049 e.set_max_macro_body_size(64);
16050 assert_eq!(e.max_macro_body_size(), 64);
16051 }
16052
16053 #[test]
16054 fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16055 // `(defmacro huge (x) `(list a b c d e))` has body
16056 // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16057 // list of 6 nodes (outer List + 5 children); the outer
16058 // quasi-quote wrapper adds one → 8 nodes total for
16059 // `def.body.node_count()`. With a ceiling of `4` this
16060 // crosses the REGISTRATION-time BODY-SIZE threshold and the
16061 // expander returns a typed `LispError::MacroBodySizeExceeded`
16062 // with `macro_name`, `size`, and `limit` all populated. Peer
16063 // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16064 // one PIPELINE-STAGE axis over.
16065 let mut e = Expander::new();
16066 e.set_max_macro_body_size(4);
16067 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16068 let err = e.expand_program(forms).unwrap_err();
16069 match err {
16070 LispError::MacroBodySizeExceeded {
16071 macro_name,
16072 size,
16073 limit,
16074 } => {
16075 assert_eq!(macro_name, "huge");
16076 assert_eq!(size, 8);
16077 assert_eq!(limit, 4);
16078 }
16079 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16080 }
16081 }
16082
16083 #[test]
16084 fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16085 // The substitute strategy shares the same
16086 // `register_macro_def` outer registration entry with the
16087 // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16088 // ceiling fires at the SAME gate regardless of which
16089 // apply-layer is active. The pin makes that shared-registration
16090 // property structural: a regression that ever branches the
16091 // body-size guard on strategy (e.g. moves it into the
16092 // `compile_templates` arm alone) fails this test on the
16093 // substitute expander. Peer to
16094 // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16095 // one PIPELINE-STAGE axis over.
16096 let mut e = Expander::new_substitute_only();
16097 e.set_max_macro_body_size(4);
16098 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16099 let err = e.expand_program(forms).unwrap_err();
16100 match err {
16101 LispError::MacroBodySizeExceeded {
16102 macro_name,
16103 size,
16104 limit,
16105 } => {
16106 assert_eq!(macro_name, "huge");
16107 assert_eq!(size, 8);
16108 assert_eq!(limit, 4);
16109 }
16110 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16111 }
16112 }
16113
16114 #[test]
16115 fn register_macro_body_at_ceiling_admits() {
16116 // `(defmacro twice (x) `(list ,x))` has body
16117 // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16118 // list (List + `list` atom + `,x` unquote which is
16119 // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16120 // With a ceiling of `5` the body sits AT the ceiling
16121 // (`<=` admits equality; only `>` rejects), so the
16122 // registration succeeds and the subsequent expansion
16123 // proceeds normally. Fail-before/pass-after negative
16124 // control for the body-bomb rejection tests: a regression
16125 // that ever rejected at-ceiling bodies would fail this pin.
16126 let mut e = Expander::new();
16127 e.set_max_macro_body_size(5);
16128 let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16129 let out = e.expand_program(forms).unwrap();
16130 assert_eq!(out[0], parse("(list hey)"));
16131 }
16132
16133 #[test]
16134 fn register_failure_leaves_both_tables_pristine() {
16135 // A rejected registration MUST leave BOTH `self.macros`
16136 // AND `self.templates` exactly as they were — no
16137 // partial-write window in which one table carries the
16138 // over-ceiling entry while the other does not, or in
16139 // which the template pre-compile already ran. This pin
16140 // catches a regression that ever moves the body-size
16141 // gate below `self.templates.insert` or below
16142 // `self.macros.insert`.
16143 let mut e = Expander::new();
16144 e.set_max_macro_body_size(4);
16145 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16146 assert!(e.expand_program(forms).is_err());
16147 assert!(
16148 !e.has("huge"),
16149 "macros table must stay pristine after rejection"
16150 );
16151 assert_eq!(e.len(), 0);
16152 }
16153
16154 #[test]
16155 fn macro_body_size_exceeded_position_is_none() {
16156 // The variant joins the non-positional cohort — the
16157 // offending macro's byte offset is not what a body-bomb
16158 // diagnostic anchors to; the (offending macro name,
16159 // observed body size, configured ceiling) triple is.
16160 // This pin keeps the variant in the `position() -> None`
16161 // cohort so a future span-carrying edit lands the field
16162 // in ONE place across every non-positional variant
16163 // simultaneously. Peer to
16164 // `expansion_{depth,size}_exceeded_position_is_none` one
16165 // PIPELINE-STAGE axis over.
16166 let err = LispError::MacroBodySizeExceeded {
16167 macro_name: "huge".to_string(),
16168 size: 512,
16169 limit: 256,
16170 };
16171 assert_eq!(err.position(), None);
16172 }
16173
16174 #[test]
16175 fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16176 // Display projection carries `macro_name`, `size`, AND
16177 // `limit`, so authoring surfaces that substring-grep the
16178 // rendered diagnostic (`tatara-check`, REPL, LSP) see
16179 // ALL THREE halves at the variant boundary and never need
16180 // to substring-parse free-form text to recover the
16181 // identity of the offending macro, the observed body
16182 // size, or the ceiling it breached. Peer to
16183 // `expansion_size_exceeded_display_matches_typed_variant_shape`
16184 // one PIPELINE-STAGE axis over.
16185 let err = LispError::MacroBodySizeExceeded {
16186 macro_name: "huge".to_string(),
16187 size: 512,
16188 limit: 256,
16189 };
16190 let rendered = err.to_string();
16191 assert!(rendered.contains("huge"), "rendered: {rendered}");
16192 assert!(rendered.contains("512"), "rendered: {rendered}");
16193 assert!(rendered.contains("256"), "rendered: {rendered}");
16194 assert!(
16195 rendered.contains("macro body size exceeded"),
16196 "rendered: {rendered}"
16197 );
16198 }
16199
16200 #[test]
16201 fn lawful_macro_body_within_ceiling_registers_and_expands() {
16202 // A lawful hand-authored macro body sits well below any
16203 // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16204 // is deliberately generous so operators never brush it on
16205 // real authoring workloads. This pin locks the default
16206 // ceiling's non-interference posture: a regression that
16207 // ever lowered the default enough to reject the reference
16208 // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16209 // fail here.
16210 let mut e = Expander::new();
16211 let forms = read(
16212 "(defmacro when (cond x) `(if ,cond ,x))
16213 (when #t hey)",
16214 )
16215 .unwrap();
16216 let out = e.expand_program(forms).unwrap();
16217 assert_eq!(out[0], parse("(if #t hey)"));
16218 }
16219
16220 #[test]
16221 fn register_macro_def_direct_call_respects_body_size_ceiling() {
16222 // `register_macro_def` is the substrate primitive both
16223 // `expand_program`'s `defmacro` recognition AND
16224 // `with_macros`'s bulk load route through. The body-size
16225 // ceiling fires on the primitive itself, not on any one
16226 // consumer — a regression that ever moved the gate into
16227 // `expand_program`'s arm alone would fail this pin.
16228 let mut e = Expander::new();
16229 e.set_max_macro_body_size(4);
16230 let def = MacroDef {
16231 name: "huge".to_string(),
16232 params: MacroParams {
16233 required: vec!["x".to_string()],
16234 optional: Vec::new(),
16235 rest: None,
16236 },
16237 body: Sexp::List(vec![
16238 Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16239 Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16240 Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16241 Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16242 Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16243 ]),
16244 };
16245 // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16246 let err = e.register_macro_def(def).unwrap_err();
16247 match err {
16248 LispError::MacroBodySizeExceeded {
16249 macro_name,
16250 size,
16251 limit,
16252 } => {
16253 assert_eq!(macro_name, "huge");
16254 assert_eq!(size, 6);
16255 assert_eq!(limit, 4);
16256 }
16257 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16258 }
16259 assert!(!e.has("huge"));
16260 }
16261
16262 // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16263 // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16264 // axis over — where body-size bounds a per-registration SIZE,
16265 // this ceiling bounds cumulative registration COUNT. Peer to
16266 // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16267 // — the two entry-count guards close the two pipeline stages
16268 // symmetrically. Closes the expander's RESOURCE surface at a
16269 // FIFTH typed dimension across two pipeline stages: the
16270 // REGISTER-time face is now a two-dimensional (size × count)
16271 // closure symmetric with the EXPAND-time (depth + count + size)
16272 // triple.
16273
16274 #[test]
16275 fn expander_default_max_registered_macros_is_the_module_constant() {
16276 // Both constructors seed `max_registered_macros` from the
16277 // module constant — a regression that drifts one
16278 // constructor's default from the constant (e.g. a hardcoded
16279 // `4096` at ONE site with the other left uninitialized
16280 // after a `Default` derive addition) fails this pin. Peer
16281 // to the four prior default-coherence pins on depth,
16282 // cache-entries, expansion-size, and body-size.
16283 assert_eq!(
16284 Expander::new().max_registered_macros(),
16285 DEFAULT_MAX_REGISTERED_MACROS
16286 );
16287 assert_eq!(
16288 Expander::new_substitute_only().max_registered_macros(),
16289 DEFAULT_MAX_REGISTERED_MACROS
16290 );
16291 assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16292 }
16293
16294 #[test]
16295 fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16296 // Setter mirrors accessor — a regression that ever forgets
16297 // to write through to the field, or writes to a shadow
16298 // field the accessor does not read, fails this pin. Peer
16299 // to the four prior setter/accessor coherence pins.
16300 let mut e = Expander::new();
16301 e.set_max_registered_macros(64);
16302 assert_eq!(e.max_registered_macros(), 64);
16303 }
16304
16305 #[test]
16306 fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16307 // With a ceiling of `2`, the first two fresh registrations
16308 // land in the table and the third fresh key rejects with
16309 // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16310 // count: 2, limit: 2 }`. `count` is the pre-insert table
16311 // length (which equals `limit` at rejection in the common
16312 // case); `macro_name` is the OFFENDING call's name (the
16313 // fresh key we could not admit), not the outermost caller.
16314 let mut e = Expander::new();
16315 e.set_max_registered_macros(2);
16316 let forms = read(
16317 "(defmacro a (x) `(list ,x))
16318 (defmacro b (x) `(list ,x))
16319 (defmacro c (x) `(list ,x))",
16320 )
16321 .unwrap();
16322 let err = e.expand_program(forms).unwrap_err();
16323 match err {
16324 LispError::RegisteredMacrosExceeded {
16325 macro_name,
16326 count,
16327 limit,
16328 } => {
16329 assert_eq!(macro_name, "c");
16330 assert_eq!(count, 2);
16331 assert_eq!(limit, 2);
16332 }
16333 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16334 }
16335 // First two admitted; third rejected leaves the table at
16336 // its pre-third-call size.
16337 assert!(e.has("a"));
16338 assert!(e.has("b"));
16339 assert!(!e.has("c"));
16340 assert_eq!(e.len(), 2);
16341 }
16342
16343 #[test]
16344 fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16345 // The substitute strategy shares the same
16346 // `register_macro_def` outer registration entry with the
16347 // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16348 // ceiling fires at the SAME gate regardless of which
16349 // apply-layer is active. The pin makes that shared-
16350 // registration property structural: a regression that ever
16351 // branches the table-count guard on strategy (e.g. moves
16352 // it into the `compile_templates` arm alone) fails this
16353 // test on the substitute expander. Peer to the four prior
16354 // shared-registration pins.
16355 let mut e = Expander::new_substitute_only();
16356 e.set_max_registered_macros(2);
16357 let forms = read(
16358 "(defmacro a (x) `(list ,x))
16359 (defmacro b (x) `(list ,x))
16360 (defmacro c (x) `(list ,x))",
16361 )
16362 .unwrap();
16363 let err = e.expand_program(forms).unwrap_err();
16364 match err {
16365 LispError::RegisteredMacrosExceeded {
16366 macro_name,
16367 count,
16368 limit,
16369 } => {
16370 assert_eq!(macro_name, "c");
16371 assert_eq!(count, 2);
16372 assert_eq!(limit, 2);
16373 }
16374 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16375 }
16376 assert!(e.has("a"));
16377 assert!(e.has("b"));
16378 assert!(!e.has("c"));
16379 assert_eq!(e.len(), 2);
16380 }
16381
16382 #[test]
16383 fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16384 // OVERWRITES (a re-registration of an already-registered
16385 // key) do not grow the table and MUST be admitted at any
16386 // ceiling — operators redefining a macro at capacity is a
16387 // legitimate authoring pattern. This pin catches a
16388 // regression that ever treats overwrites the same as fresh
16389 // keys (a `contains_key` check that inverts, or a
16390 // `set_max_registered_macros(0)` that would reject even
16391 // pre-registered overwrites).
16392 let mut e = Expander::new();
16393 // Land two macros with a permissive ceiling.
16394 e.expand_program(
16395 read(
16396 "(defmacro a (x) `(list ,x))
16397 (defmacro b (x) `(list ,x))",
16398 )
16399 .unwrap(),
16400 )
16401 .unwrap();
16402 assert_eq!(e.len(), 2);
16403 // Drop the ceiling to exactly the current size — a fresh
16404 // third key would now reject, but an overwrite of `a` MUST
16405 // admit.
16406 e.set_max_registered_macros(2);
16407 let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16408 e.expand_program(forms).unwrap();
16409 assert_eq!(e.len(), 2, "overwrite must not grow the table");
16410 // Re-expand a call to `a` — the NEW definition (two
16411 // required params) is active, and the OLD definition (one
16412 // required param) is gone. Under the new definition,
16413 // `(a 1 2)` expands to `(pair 1 2)`.
16414 let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16415 assert_eq!(expanded[0], parse("(pair foo bar)"));
16416 }
16417
16418 #[test]
16419 fn register_at_table_ceiling_admits_up_to_but_not_past() {
16420 // Fail-before/pass-after negative control on the exact
16421 // boundary: with a ceiling of `3`, exactly three fresh keys
16422 // admit and the fourth rejects. A regression that ever
16423 // shifted the boundary by one (e.g. keyed on `>` instead of
16424 // `>=`, or checked `len() > cap` before insert instead of
16425 // `len() >= cap`) would fail one of the two arms — either
16426 // the third admission or the fourth rejection.
16427 let mut e = Expander::new();
16428 e.set_max_registered_macros(3);
16429 // Three fresh keys admit.
16430 e.expand_program(
16431 read(
16432 "(defmacro a (x) `(list ,x))
16433 (defmacro b (x) `(list ,x))
16434 (defmacro c (x) `(list ,x))",
16435 )
16436 .unwrap(),
16437 )
16438 .unwrap();
16439 assert_eq!(e.len(), 3);
16440 // Fourth fresh key rejects.
16441 let err = e
16442 .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16443 .unwrap_err();
16444 match err {
16445 LispError::RegisteredMacrosExceeded {
16446 macro_name,
16447 count,
16448 limit,
16449 } => {
16450 assert_eq!(macro_name, "d");
16451 assert_eq!(count, 3);
16452 assert_eq!(limit, 3);
16453 }
16454 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16455 }
16456 // Table stays at pre-rejection size — no partial write.
16457 assert_eq!(e.len(), 3);
16458 assert!(!e.has("d"));
16459 }
16460
16461 #[test]
16462 fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16463 // A rejected registration MUST leave BOTH `self.macros` AND
16464 // `self.templates` exactly as they were — no partial-write
16465 // window in which one table carries the over-ceiling entry
16466 // while the other does not, or in which the template
16467 // pre-compile already ran. Table-count gate fires BEFORE
16468 // body-size gate walks the body AND BEFORE any insert lands,
16469 // so a regression that ever moved the table-count gate
16470 // below `self.templates.insert` or below
16471 // `self.macros.insert` would fail this pin. Peer to
16472 // `register_failure_leaves_both_tables_pristine` on the
16473 // body-size axis.
16474 let mut e = Expander::new();
16475 e.set_max_registered_macros(1);
16476 // Land one macro at ceiling.
16477 e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16478 .unwrap();
16479 assert!(e.has("a"));
16480 // Reject a fresh second key.
16481 let err = e
16482 .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16483 .unwrap_err();
16484 assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16485 // Table stays at pre-rejection state — `b` is NOT present
16486 // in either self.macros OR self.templates.
16487 assert_eq!(e.len(), 1);
16488 assert!(e.has("a"));
16489 assert!(!e.has("b"));
16490 // Expanding a call to `a` still works (templates table for
16491 // `a` untouched by the failed `b` registration).
16492 let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16493 assert_eq!(expanded[0], parse("(list hey)"));
16494 }
16495
16496 #[test]
16497 fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16498 // Zero ceiling is the "contract-test that this expander
16499 // accepts no new macros" posture — every fresh key rejects
16500 // at the first `defmacro` because `0 >= 0`. Peer to the
16501 // depth-ceiling zero-admits-and-rejects-every-macro-call
16502 // pin one RESOURCE axis over.
16503 let mut e = Expander::new();
16504 e.set_max_registered_macros(0);
16505 let err = e
16506 .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16507 .unwrap_err();
16508 match err {
16509 LispError::RegisteredMacrosExceeded {
16510 macro_name,
16511 count,
16512 limit,
16513 } => {
16514 assert_eq!(macro_name, "a");
16515 assert_eq!(count, 0);
16516 assert_eq!(limit, 0);
16517 }
16518 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16519 }
16520 assert!(e.is_empty());
16521 }
16522
16523 #[test]
16524 fn registered_macros_exceeded_position_is_none() {
16525 // The variant joins the non-positional cohort — the
16526 // offending macro's byte offset is not what a table-count
16527 // diagnostic anchors to; the (offending fresh macro name,
16528 // observed table count, configured ceiling) triple is.
16529 // This pin keeps the variant in the `position() -> None`
16530 // cohort so a future span-carrying edit lands the field
16531 // in ONE place across every non-positional variant
16532 // simultaneously. Peer to the four prior
16533 // `*_exceeded_position_is_none` pins on the resource-
16534 // ceiling cohort.
16535 let err = LispError::RegisteredMacrosExceeded {
16536 macro_name: "fresh-1000".to_string(),
16537 count: 4096,
16538 limit: 4096,
16539 };
16540 assert_eq!(err.position(), None);
16541 }
16542
16543 #[test]
16544 fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16545 // Display projection carries `macro_name`, `count`, AND
16546 // `limit`, so authoring surfaces that substring-grep the
16547 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16548 // THREE halves at the variant boundary and never need to
16549 // substring-parse free-form text to recover the identity
16550 // of the offending fresh key, the observed table count, or
16551 // the ceiling it breached. Peer to the four prior
16552 // `*_exceeded_display_matches_typed_variant_shape` pins.
16553 let err = LispError::RegisteredMacrosExceeded {
16554 macro_name: "fresh-1000".to_string(),
16555 count: 4096,
16556 limit: 4096,
16557 };
16558 let rendered = err.to_string();
16559 assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16560 assert!(rendered.contains("4096"), "rendered: {rendered}");
16561 assert!(
16562 rendered.contains("registered macros count exceeded"),
16563 "rendered: {rendered}"
16564 );
16565 }
16566
16567 #[test]
16568 fn lawful_typescape_within_ceiling_registers_and_expands() {
16569 // A lawful hand-authored typescape (a handful of macros)
16570 // sits well below any realistic ceiling — the default
16571 // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16572 // so operators never brush it on real authoring workloads.
16573 // This pin locks the default ceiling's non-interference
16574 // posture: a regression that ever lowered the default
16575 // enough to reject a small typescape would fail here. Peer
16576 // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16577 let mut e = Expander::new();
16578 let forms = read(
16579 "(defmacro when (cond x) `(if ,cond ,x))
16580 (defmacro twice (x) `(list ,x ,x))
16581 (defmacro pair (x y) `(list ,x ,y))
16582 (when #t (twice hey))",
16583 )
16584 .unwrap();
16585 let out = e.expand_program(forms).unwrap();
16586 assert_eq!(out[0], parse("(if #t (list hey hey))"));
16587 assert_eq!(e.len(), 3);
16588 }
16589
16590 #[test]
16591 fn register_macro_def_direct_call_respects_table_ceiling() {
16592 // `register_macro_def` is the substrate primitive both
16593 // `expand_program`'s `defmacro` recognition AND
16594 // `with_macros`'s bulk load route through. The table-count
16595 // ceiling fires on the primitive itself, not on any one
16596 // consumer — a regression that ever moved the gate into
16597 // `expand_program`'s arm alone would fail this pin. Peer
16598 // to `register_macro_def_direct_call_respects_body_size_ceiling`
16599 // one RESOURCE-DIMENSION axis over.
16600 let mut e = Expander::new();
16601 e.set_max_registered_macros(1);
16602 let def_a = MacroDef {
16603 name: "a".to_string(),
16604 params: MacroParams {
16605 required: vec!["x".to_string()],
16606 optional: Vec::new(),
16607 rest: None,
16608 },
16609 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16610 };
16611 let def_b = MacroDef {
16612 name: "b".to_string(),
16613 params: MacroParams {
16614 required: vec!["x".to_string()],
16615 optional: Vec::new(),
16616 rest: None,
16617 },
16618 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16619 };
16620 // First admits — table sits at ceiling.
16621 e.register_macro_def(def_a).unwrap();
16622 assert!(e.has("a"));
16623 assert_eq!(e.len(), 1);
16624 // Second rejects — fresh key past ceiling.
16625 let err = e.register_macro_def(def_b).unwrap_err();
16626 match err {
16627 LispError::RegisteredMacrosExceeded {
16628 macro_name,
16629 count,
16630 limit,
16631 } => {
16632 assert_eq!(macro_name, "b");
16633 assert_eq!(count, 1);
16634 assert_eq!(limit, 1);
16635 }
16636 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16637 }
16638 assert!(!e.has("b"));
16639 assert_eq!(e.len(), 1);
16640 }
16641
16642 #[test]
16643 fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16644 // Direct `register_macro_def` calls must ALSO admit
16645 // overwrites at ceiling — the overwrite discipline lives
16646 // on the primitive, not on `expand_program`'s arm alone.
16647 // Sibling of the source-level overwrite admission test.
16648 let mut e = Expander::new();
16649 e.set_max_registered_macros(1);
16650 let def_a_v1 = MacroDef {
16651 name: "a".to_string(),
16652 params: MacroParams {
16653 required: vec!["x".to_string()],
16654 optional: Vec::new(),
16655 rest: None,
16656 },
16657 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16658 };
16659 let def_a_v2 = MacroDef {
16660 name: "a".to_string(),
16661 params: MacroParams {
16662 required: vec!["x".to_string(), "y".to_string()],
16663 optional: Vec::new(),
16664 rest: None,
16665 },
16666 body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16667 };
16668 e.register_macro_def(def_a_v1).unwrap();
16669 assert_eq!(e.len(), 1);
16670 // Overwrite of `a` at ceiling admits — no growth.
16671 e.register_macro_def(def_a_v2).unwrap();
16672 assert_eq!(e.len(), 1);
16673 }
16674
16675 // ── `MacroParams::total_arity` — the total-slots projection ──
16676 //
16677 // `total_arity()` names `required.len() + optional.len() +
16678 // rest.is_some() as usize` on the typed `MacroParams` algebra —
16679 // the `Vec`-free peer of `names().len()` AND the axis both
16680 // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16681 // REGISTER-time arity-ceiling gate consult. These pins cover:
16682 // structural identity vs. `names().len()`, the
16683 // `fixed_arity + rest.is_some() as usize` decomposition, and the
16684 // three canonical shapes (empty, all-required, mixed with rest).
16685 #[test]
16686 fn macro_params_total_arity_matches_names_len() {
16687 // The structural identity `total_arity() == names().len()`
16688 // — the `Vec`-free primitive's projection equals the flat-
16689 // index list's length. A regression that ever miscounts
16690 // one slot (drops rest, double-counts optional) fails this
16691 // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16692 let params = MacroParams {
16693 required: vec!["a".into(), "b".into()],
16694 optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16695 rest: Some("e".into()),
16696 };
16697 assert_eq!(params.total_arity(), params.names().len());
16698 assert_eq!(params.total_arity(), 5);
16699 }
16700
16701 #[test]
16702 fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16703 // The `fixed_arity + rest.is_some() as usize` decomposition
16704 // — pins the composition on the [`MacroParams`] algebra so
16705 // a future consumer that wants "total slots" via
16706 // `fixed_arity + rest?` binds to the same arithmetic
16707 // `total_arity` names.
16708 let rest_none = MacroParams {
16709 required: vec!["a".into()],
16710 optional: vec![OptionalParam::bare("b")],
16711 rest: None,
16712 };
16713 assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16714 assert_eq!(rest_none.total_arity(), 2);
16715 let rest_some = MacroParams {
16716 required: vec!["a".into()],
16717 optional: vec![OptionalParam::bare("b")],
16718 rest: Some("r".into()),
16719 };
16720 assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16721 assert_eq!(rest_some.total_arity(), 3);
16722 }
16723
16724 #[test]
16725 fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16726 // Nullary macro edge: `(defmacro nullary () BODY)` has
16727 // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16728 // ceiling the arity gate keys on `>` (equality admits), so
16729 // this nullary registration succeeds while any non-nullary
16730 // one rejects — the exact contract the arity-gate
16731 // zero-admits-nullary test below relies on.
16732 let params = MacroParams::default();
16733 assert_eq!(params.total_arity(), 0);
16734 }
16735
16736 // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16737 // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16738 // DIMENSION axis over — where body-size bounds a per-registration
16739 // BODY node count, this ceiling bounds a per-registration PARAM
16740 // slot count. Peer to registered-macros (REGISTER-time TABLE
16741 // COUNT) one RESOURCE-DIMENSION axis over — where
16742 // registered-macros bounds cumulative registration COUNT, this
16743 // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16744 // the three REGISTER-time ceilings close the (per-body SIZE,
16745 // per-body ARITY, per-table COUNT) three-corner surface.
16746
16747 #[test]
16748 fn expander_default_max_macro_arity_is_the_module_constant() {
16749 // Both constructors seed `max_macro_arity` from the module
16750 // constant — a regression that drifts one constructor's
16751 // default from the constant (e.g. a hardcoded `128` at ONE
16752 // site with the other left uninitialized after a `Default`
16753 // derive addition) fails this pin. Peer to the five prior
16754 // default-coherence pins on depth, cache-entries,
16755 // expansion-size, body-size, and registered-macros.
16756 assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16757 assert_eq!(
16758 Expander::new_substitute_only().max_macro_arity(),
16759 DEFAULT_MAX_MACRO_ARITY
16760 );
16761 assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16762 }
16763
16764 #[test]
16765 fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16766 // Setter mirrors accessor — a regression that ever forgets to
16767 // write through to the field, or writes to a shadow field the
16768 // accessor does not read, fails this pin. Peer to the five
16769 // prior setter/accessor coherence pins.
16770 let mut e = Expander::new();
16771 e.set_max_macro_arity(16);
16772 assert_eq!(e.max_macro_arity(), 16);
16773 }
16774
16775 #[test]
16776 fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16777 // `(defmacro huge (a b c) `,a)` declares 3 required params.
16778 // Under `set_max_macro_arity(2)` the arity gate fires:
16779 // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16780 // arity: 3, limit: 2 }`. `arity` is the observed
16781 // `def.params.total_arity()`, NOT `required.len()` alone —
16782 // even an optional-only or rest-only overrun trips the gate
16783 // (pinned separately). Peer to the register-time body-size
16784 // rejection tests one RESOURCE-DIMENSION axis over.
16785 let mut e = Expander::new();
16786 e.set_max_macro_arity(2);
16787 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16788 let err = e.expand_program(forms).unwrap_err();
16789 match err {
16790 LispError::MacroArityExceeded {
16791 macro_name,
16792 arity,
16793 limit,
16794 } => {
16795 assert_eq!(macro_name, "huge");
16796 assert_eq!(arity, 3);
16797 assert_eq!(limit, 2);
16798 }
16799 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16800 }
16801 assert!(!e.has("huge"));
16802 }
16803
16804 #[test]
16805 fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16806 // The substitute strategy shares the same
16807 // `register_macro_def` outer registration entry with the
16808 // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16809 // fires at the SAME gate regardless of which apply-layer is
16810 // active. Peer to
16811 // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16812 let mut e = Expander::new_substitute_only();
16813 e.set_max_macro_arity(2);
16814 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16815 let err = e.expand_program(forms).unwrap_err();
16816 match err {
16817 LispError::MacroArityExceeded {
16818 macro_name,
16819 arity,
16820 limit,
16821 } => {
16822 assert_eq!(macro_name, "huge");
16823 assert_eq!(arity, 3);
16824 assert_eq!(limit, 2);
16825 }
16826 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16827 }
16828 assert!(!e.has("huge"));
16829 }
16830
16831 #[test]
16832 fn register_arity_gate_counts_optional_slots() {
16833 // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16834 // optional + 0 rest = arity 3 via `total_arity`. Under a
16835 // ceiling of `2` the gate rejects, matching the required-only
16836 // arity-bomb — the arity axis is `total_arity()`, not
16837 // `required.len()`. A regression that drops the optional slots
16838 // from the arity computation would ADMIT this shape and fail
16839 // this pin.
16840 let mut e = Expander::new();
16841 e.set_max_macro_arity(2);
16842 let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16843 let err = e.expand_program(forms).unwrap_err();
16844 match err {
16845 LispError::MacroArityExceeded {
16846 macro_name,
16847 arity,
16848 limit,
16849 } => {
16850 assert_eq!(macro_name, "foo");
16851 assert_eq!(arity, 3);
16852 assert_eq!(limit, 2);
16853 }
16854 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16855 }
16856 }
16857
16858 #[test]
16859 fn register_arity_gate_counts_rest_slot() {
16860 // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16861 // optional + 1 rest = arity 3 via `total_arity`. Under a
16862 // ceiling of `2` the gate rejects, matching the required-only
16863 // arity-bomb — the arity axis includes the rest slot as ONE
16864 // named param, not zero. A regression that drops the rest slot
16865 // from the arity computation would ADMIT this shape and fail
16866 // this pin.
16867 let mut e = Expander::new();
16868 e.set_max_macro_arity(2);
16869 let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
16870 let err = e.expand_program(forms).unwrap_err();
16871 match err {
16872 LispError::MacroArityExceeded {
16873 macro_name,
16874 arity,
16875 limit,
16876 } => {
16877 assert_eq!(macro_name, "spread");
16878 assert_eq!(arity, 3);
16879 assert_eq!(limit, 2);
16880 }
16881 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16882 }
16883 }
16884
16885 #[test]
16886 fn register_arity_at_ceiling_admits() {
16887 // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16888 // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16889 // keys on `>` (equality admits; only strict overrun rejects),
16890 // so the registration succeeds and the expansion proceeds
16891 // normally. Fail-before/pass-after negative control on the
16892 // exact boundary: a regression that ever shifted the boundary
16893 // by one (e.g. keyed on `>=` instead of `>`) would fail this
16894 // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16895 // body-size axis.
16896 let mut e = Expander::new();
16897 e.set_max_macro_arity(2);
16898 let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16899 let out = e.expand_program(forms).unwrap();
16900 assert_eq!(out[0], parse("(list a b)"));
16901 assert_eq!(e.len(), 1);
16902 }
16903
16904 #[test]
16905 fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16906 // A rejected arity-gated registration MUST leave BOTH
16907 // `self.macros` AND `self.templates` exactly as they were —
16908 // no partial-write window in which one table carries the
16909 // over-ceiling entry while the other does not, or in which
16910 // the template pre-compile already ran. Arity gate fires
16911 // BEFORE the body-size gate walks the body AND BEFORE any
16912 // insert lands, so a regression that ever moved the arity
16913 // gate below `self.templates.insert` or below
16914 // `self.macros.insert` would fail this pin. Peer to
16915 // `register_failure_leaves_both_tables_pristine` (body-size)
16916 // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16917 // (table-count).
16918 let mut e = Expander::new();
16919 e.set_max_macro_arity(1);
16920 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16921 assert!(e.expand_program(forms).is_err());
16922 assert!(
16923 !e.has("huge"),
16924 "macros table must stay pristine after rejection"
16925 );
16926 assert_eq!(e.len(), 0);
16927 // Expanding a NEW lawful macro afterwards still works — the
16928 // failed arity-gate rejection has not corrupted expander
16929 // state.
16930 let out = e
16931 .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
16932 .unwrap();
16933 assert_eq!(out[0], parse("hey"));
16934 }
16935
16936 #[test]
16937 fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
16938 // Zero ceiling is the "contract-test that this expander
16939 // accepts only nullary macros" posture — every fresh
16940 // non-nullary key rejects at the arity gate because
16941 // `total_arity() > 0`, while nullary `(defmacro nullary ()
16942 // BODY)` still admits (its arity is exactly 0, and only
16943 // strict overrun rejects). Peer to the body-size zero-rejects-
16944 // every-non-empty-body pin one RESOURCE-DIMENSION axis over
16945 // (though body-size zero rejects even nullary-body shapes,
16946 // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
16947 // preserves the nullary registration surface).
16948 let mut e = Expander::new();
16949 e.set_max_macro_arity(0);
16950 // Nullary admits — arity 0 <= 0.
16951 e.expand_program(read("(defmacro nullary () `unit)").unwrap())
16952 .unwrap();
16953 assert!(e.has("nullary"));
16954 // Non-nullary rejects — arity 1 > 0.
16955 let err = e
16956 .expand_program(read("(defmacro id (x) `,x)").unwrap())
16957 .unwrap_err();
16958 match err {
16959 LispError::MacroArityExceeded {
16960 macro_name,
16961 arity,
16962 limit,
16963 } => {
16964 assert_eq!(macro_name, "id");
16965 assert_eq!(arity, 1);
16966 assert_eq!(limit, 0);
16967 }
16968 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16969 }
16970 assert!(!e.has("id"));
16971 }
16972
16973 #[test]
16974 fn macro_arity_exceeded_position_is_none() {
16975 // The variant joins the non-positional cohort — the offending
16976 // macro's byte offset is not what an arity-bomb diagnostic
16977 // anchors to; the (macro name, observed arity, configured
16978 // ceiling) triple is. This pin keeps the variant in the
16979 // `position() -> None` cohort so a future span-carrying edit
16980 // lands the field in ONE place across every non-positional
16981 // variant simultaneously. Peer to the five prior
16982 // `*_exceeded_position_is_none` pins on the resource-ceiling
16983 // cohort.
16984 let err = LispError::MacroArityExceeded {
16985 macro_name: "huge".to_string(),
16986 arity: 512,
16987 limit: 128,
16988 };
16989 assert_eq!(err.position(), None);
16990 }
16991
16992 #[test]
16993 fn macro_arity_exceeded_display_matches_typed_variant_shape() {
16994 // Display projection carries `macro_name`, `arity`, AND
16995 // `limit`, so authoring surfaces that substring-grep the
16996 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16997 // THREE halves at the variant boundary and never need to
16998 // substring-parse free-form text to recover the identity of
16999 // the offending macro, the observed arity, or the ceiling it
17000 // breached. Peer to the five prior
17001 // `*_exceeded_display_matches_typed_variant_shape` pins.
17002 let err = LispError::MacroArityExceeded {
17003 macro_name: "huge".to_string(),
17004 arity: 512,
17005 limit: 128,
17006 };
17007 let rendered = err.to_string();
17008 assert!(rendered.contains("huge"), "rendered: {rendered}");
17009 assert!(rendered.contains("512"), "rendered: {rendered}");
17010 assert!(rendered.contains("128"), "rendered: {rendered}");
17011 assert!(
17012 rendered.contains("macro arity exceeded"),
17013 "rendered: {rendered}"
17014 );
17015 }
17016
17017 #[test]
17018 fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17019 // A lawful hand-authored macro arity sits well below any
17020 // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17021 // is deliberately generous so operators never brush it on
17022 // real authoring workloads. This pin locks the default
17023 // ceiling's non-interference posture: a regression that
17024 // ever lowered the default enough to reject the reference
17025 // multi-slot `(defmacro when (cond x) …)` shape would fail
17026 // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17027 // and `lawful_typescape_within_ceiling_registers_and_expands`.
17028 let mut e = Expander::new();
17029 let forms = read(
17030 "(defmacro when (cond x) `(if ,cond ,x))
17031 (defmacro triple (x y z) `(list ,x ,y ,z))
17032 (when #t (triple a b c))",
17033 )
17034 .unwrap();
17035 let out = e.expand_program(forms).unwrap();
17036 assert_eq!(out[0], parse("(if #t (list a b c))"));
17037 }
17038
17039 #[test]
17040 fn register_macro_def_direct_call_respects_arity_ceiling() {
17041 // `register_macro_def` is the substrate primitive both
17042 // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17043 // bulk load route through. The arity ceiling fires on the
17044 // primitive itself, not on any one consumer — a regression
17045 // that ever moved the gate into `expand_program`'s arm alone
17046 // would fail this pin. Peer to
17047 // `register_macro_def_direct_call_respects_body_size_ceiling`
17048 // AND `register_macro_def_direct_call_respects_table_ceiling`
17049 // one RESOURCE-DIMENSION axis over.
17050 let mut e = Expander::new();
17051 e.set_max_macro_arity(2);
17052 let def = MacroDef {
17053 name: "huge".to_string(),
17054 params: MacroParams {
17055 required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17056 optional: Vec::new(),
17057 rest: None,
17058 },
17059 body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17060 };
17061 // Arity 3 > ceiling 2 — rejected before body-size gate.
17062 let err = e.register_macro_def(def).unwrap_err();
17063 match err {
17064 LispError::MacroArityExceeded {
17065 macro_name,
17066 arity,
17067 limit,
17068 } => {
17069 assert_eq!(macro_name, "huge");
17070 assert_eq!(arity, 3);
17071 assert_eq!(limit, 2);
17072 }
17073 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17074 }
17075 assert!(!e.has("huge"));
17076 }
17077
17078 #[test]
17079 fn arity_gate_fires_before_body_size_gate() {
17080 // ORDERING PIN — the arity gate fires BEFORE the body-size
17081 // gate. With both `set_max_macro_arity(1)` AND
17082 // `set_max_macro_body_size(2)` set below a shape that violates
17083 // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17084 // arity 2 AND body node count > 2 — the arity gate's
17085 // rejection wins, so the returned variant is
17086 // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17087 // regression that ever swapped the two gates' order would fail
17088 // this pin. The ordering isn't cosmetic: arity is O(1)
17089 // (three-arm addition on `def.params` fields), body-size is
17090 // O(body_nodes) (`def.body.node_count()` walks the AST), so
17091 // the O(1)-first order minimises the cost of rejecting the
17092 // worst case (a large-body large-arity macro).
17093 let mut e = Expander::new();
17094 e.set_max_macro_arity(1);
17095 e.set_max_macro_body_size(2);
17096 let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17097 let err = e.expand_program(forms).unwrap_err();
17098 assert!(
17099 matches!(err, LispError::MacroArityExceeded { .. }),
17100 "arity gate must fire before body-size gate; got: {err:?}"
17101 );
17102 }
17103
17104 #[test]
17105 fn arity_gate_fires_after_table_count_gate() {
17106 // ORDERING PIN — the table-count gate fires BEFORE the arity
17107 // gate. Under `set_max_registered_macros(1)` at capacity AND
17108 // `set_max_macro_arity(1)`, a fresh over-arity registration
17109 // trips the table-count gate FIRST (its `HashMap::len`
17110 // comparison is O(1) cache-friendly and gates cumulative
17111 // registration cost); the arity gate is O(1) but on `def`,
17112 // not on shared state, so it sits AFTER the table-count gate
17113 // on the O(1)-first ordering of the REGISTER-time chain. A
17114 // regression that ever swapped the two would fail this pin.
17115 let mut e = Expander::new();
17116 e.set_max_registered_macros(1);
17117 e.set_max_macro_arity(1);
17118 // Land one macro at table ceiling.
17119 e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17120 .unwrap();
17121 assert_eq!(e.len(), 1);
17122 // Fresh registration with BOTH violations — table-count wins.
17123 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17124 let err = e.expand_program(forms).unwrap_err();
17125 assert!(
17126 matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17127 "table-count gate must fire before arity gate; got: {err:?}"
17128 );
17129 }
17130
17131 // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17132
17133 #[test]
17134 fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17135 // Pin the (constant × aggregation) corner: the const-form
17136 // aggregate MUST bind every field to its individually-defined
17137 // module constant. A re-tuning of ONE module constant
17138 // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17139 // has to propagate through the aggregate at ONE site; this
17140 // test fires the divergence loudly if a future edit sets a
17141 // module constant to a fresh value but forgets to update the
17142 // matching aggregate slot. Field-by-field equality carries
17143 // the message better than one deep-equality assertion.
17144 assert_eq!(
17145 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17146 DEFAULT_MAX_EXPANSION_DEPTH
17147 );
17148 assert_eq!(
17149 DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17150 DEFAULT_MAX_CACHE_ENTRIES
17151 );
17152 assert_eq!(
17153 DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17154 DEFAULT_MAX_EXPANSION_SIZE
17155 );
17156 assert_eq!(
17157 DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17158 DEFAULT_MAX_MACRO_BODY_SIZE
17159 );
17160 assert_eq!(
17161 DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17162 DEFAULT_MAX_REGISTERED_MACROS
17163 );
17164 assert_eq!(
17165 DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17166 DEFAULT_MAX_MACRO_ARITY
17167 );
17168 }
17169
17170 #[test]
17171 fn resource_limits_default_impl_matches_const_form() {
17172 // The two projections of the shipped posture — the `const`
17173 // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17174 // `Default::default()` form — must reach the SAME value. Pins
17175 // that a future edit which flips one to a new default cannot
17176 // silently drift the other.
17177 assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17178 }
17179
17180 #[test]
17181 fn expander_new_resource_limits_matches_shipped_defaults() {
17182 // Fresh-expander posture on the bundled-getter surface — the
17183 // six ceilings [`Expander::new`] seeds MUST snapshot to
17184 // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17185 // individual `expander_default_max_*_is_the_module_constant`
17186 // pins one AGGREGATION axis over — where each individual
17187 // pin catches ONE field drift, this pin catches ANY of the
17188 // six drifting AND the snapshot projection itself losing a
17189 // field.
17190 assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17191 }
17192
17193 #[test]
17194 fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17195 // Substitute-only expander seeds the SAME six ceilings from
17196 // the SAME module constants as [`Expander::new`] — the two
17197 // constructors share the resource-ceiling posture even
17198 // though they diverge on `compile_templates` +
17199 // `cache_enabled`. Pins that a future constructor which
17200 // forgets to seed one of the six ceilings from its module
17201 // constant fails loudly through the bundled getter.
17202 assert_eq!(
17203 Expander::new_substitute_only().resource_limits(),
17204 DEFAULT_RESOURCE_LIMITS
17205 );
17206 }
17207
17208 #[test]
17209 fn resource_limits_snapshot_reflects_each_individual_setter() {
17210 // Coherence pin between the six individual setters and the
17211 // bundled getter — flipping ONE ceiling via its individual
17212 // setter must project through [`Expander::resource_limits`]
17213 // to the SAME value the individual getter returns. A
17214 // regression that leaves ONE field out of the snapshot
17215 // projection (a copy-paste omission in the struct literal
17216 // constructor) fires here. Distinct low-non-default values
17217 // so a stray field carrying the default fails loudly.
17218 let mut e = Expander::new();
17219 e.set_max_expansion_depth(3);
17220 e.set_max_cache_entries(5);
17221 e.set_max_expansion_size(7);
17222 e.set_max_macro_body_size(11);
17223 e.set_max_registered_macros(13);
17224 e.set_max_macro_arity(17);
17225 let snap = e.resource_limits();
17226 assert_eq!(snap.max_expansion_depth, 3);
17227 assert_eq!(snap.max_cache_entries, 5);
17228 assert_eq!(snap.max_expansion_size, 7);
17229 assert_eq!(snap.max_macro_body_size, 11);
17230 assert_eq!(snap.max_registered_macros, 13);
17231 assert_eq!(snap.max_macro_arity, 17);
17232 // And the individual getters see the same values — the
17233 // snapshot is a projection, not a divergent copy.
17234 assert_eq!(e.max_expansion_depth(), 3);
17235 assert_eq!(e.max_cache_entries(), 5);
17236 assert_eq!(e.max_expansion_size(), 7);
17237 assert_eq!(e.max_macro_body_size(), 11);
17238 assert_eq!(e.max_registered_macros(), 13);
17239 assert_eq!(e.max_macro_arity(), 17);
17240 }
17241
17242 #[test]
17243 fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17244 // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17245 // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17246 // must reach every individual `max_*` getter. A regression
17247 // that drops one field from the destructuring assignment in
17248 // the bulk setter fires here. Distinct low-non-default values
17249 // so a stray field carrying the previous value fails loudly.
17250 let mut e = Expander::new();
17251 e.set_resource_limits(ResourceLimits {
17252 max_expansion_depth: 2,
17253 max_cache_entries: 4,
17254 max_expansion_size: 8,
17255 max_macro_body_size: 16,
17256 max_registered_macros: 32,
17257 max_macro_arity: 64,
17258 });
17259 assert_eq!(e.max_expansion_depth(), 2);
17260 assert_eq!(e.max_cache_entries(), 4);
17261 assert_eq!(e.max_expansion_size(), 8);
17262 assert_eq!(e.max_macro_body_size(), 16);
17263 assert_eq!(e.max_registered_macros(), 32);
17264 assert_eq!(e.max_macro_arity(), 64);
17265 }
17266
17267 #[test]
17268 fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17269 // Round-trip theorem — for any starting expander, the
17270 // composition `set_resource_limits(resource_limits())` is
17271 // the identity on the resource-ceiling surface. This is the
17272 // ONE typed statement that pins the (getter, setter) pair as
17273 // strict projections of the same six-field surface, no
17274 // hidden lossy field, no ordering asymmetry between the
17275 // struct literal and the destructuring pattern.
17276 let mut e = Expander::new();
17277 e.set_max_expansion_depth(9);
17278 e.set_max_cache_entries(19);
17279 e.set_max_expansion_size(29);
17280 e.set_max_macro_body_size(39);
17281 e.set_max_registered_macros(49);
17282 e.set_max_macro_arity(59);
17283 let snap = e.resource_limits();
17284 // Reset to defaults so any leftover state would show up in
17285 // the post-round-trip snapshot as the default rather than
17286 // the intended value.
17287 e.set_resource_limits(ResourceLimits::default());
17288 assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17289 // Apply the captured snapshot — the expander must land at
17290 // the same six values `snap` recorded.
17291 e.set_resource_limits(snap);
17292 assert_eq!(e.resource_limits(), snap);
17293 }
17294
17295 #[test]
17296 fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17297 // The `Copy` posture on [`ResourceLimits`] gives callers the
17298 // cheap ergonomic of a struct-update literal — take the
17299 // shipped posture and override ONE ceiling — that a chain of
17300 // six independent setters cannot express in ONE typed expression.
17301 // Pins the intended use-shape so a future refactor which
17302 // removes `Copy` from the struct fails to compile at THIS
17303 // site rather than at every downstream consumer.
17304 let mut e = Expander::new();
17305 e.set_resource_limits(ResourceLimits {
17306 max_macro_arity: 4,
17307 ..DEFAULT_RESOURCE_LIMITS
17308 });
17309 // Overridden field carries the fresh value.
17310 assert_eq!(e.max_macro_arity(), 4);
17311 // Every other field carries the shipped default — the
17312 // struct-update literal is a full projection, not a
17313 // selective mutation.
17314 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17315 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17316 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17317 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17318 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17319 }
17320
17321 #[test]
17322 fn expander_default_derives_resource_limits_from_bundled_field_default() {
17323 // Post the [`Expander::limits`]-field consolidation the derived
17324 // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17325 // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17326 // `Expander::default().resource_limits()` lands at the shipped
17327 // ceiling posture verbatim. Pre-consolidation the six independent
17328 // `max_*: usize` fields defaulted to the bare `usize` zero, so
17329 // `Expander::default()` yielded a below-floor posture with every
17330 // ceiling clamped to `0` — a shape no consumer wanted and every
17331 // test path avoided by routing through [`Expander::new`] instead.
17332 // The consolidation is what turned that latent below-floor
17333 // derived-`Default` shape into a lawful shipped posture on the
17334 // structural axis. Peer of
17335 // `expander_new_resource_limits_matches_shipped_defaults` one
17336 // CONSTRUCTOR axis over — where that test pins the explicit
17337 // [`Expander::new`] constructor, this test pins the derived-
17338 // [`Default`] constructor route to the SAME six values through
17339 // the SAME bundled projection. Sibling of
17340 // `resource_limits_default_impl_matches_const_form` one
17341 // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17342 // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17343 // derived `Default` to project through it.
17344 assert_eq!(
17345 Expander::default().resource_limits(),
17346 DEFAULT_RESOURCE_LIMITS
17347 );
17348 }
17349
17350 #[test]
17351 fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17352 // Behavioral pin — the bundled setter is not a decorative
17353 // getter/setter pair; the six ceilings it propagates must
17354 // reach the SAME check sites the individual setters do. Set
17355 // `max_macro_arity` to 1 via the bulk setter and confirm the
17356 // arity gate fires on a two-arg macro registration. A
17357 // regression that wired the bulk setter to a shadow field
17358 // rather than the actual `Expander` state fails here.
17359 let mut e = Expander::new();
17360 e.set_resource_limits(ResourceLimits {
17361 max_macro_arity: 1,
17362 ..DEFAULT_RESOURCE_LIMITS
17363 });
17364 let forms = read("(defmacro two (a b) `,a)").unwrap();
17365 let err = e.expand_program(forms).unwrap_err();
17366 assert!(
17367 matches!(
17368 err,
17369 LispError::MacroArityExceeded {
17370 arity: 2,
17371 limit: 1,
17372 ..
17373 }
17374 ),
17375 "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17376 );
17377 }
17378
17379 // ── Expander::with_limits — at-construction resource-posture ───────
17380
17381 #[test]
17382 fn expander_with_limits_seeds_the_provided_resource_posture() {
17383 // Round-trip pin at CONSTRUCTION time — the bundle the caller
17384 // passes into [`Expander::with_limits`] MUST project back
17385 // through [`Expander::resource_limits`] verbatim. Peer of the
17386 // `set_resource_limits(l); resource_limits() == l` round-trip
17387 // one CONSTRUCTOR-STAGE axis over — that pin covers the
17388 // POST-construction assignment; this pin covers the
17389 // AT-construction assignment. A regression that dropped the
17390 // `limits` argument on the floor (a copy-paste that left
17391 // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17392 // instead of `limits`) fires here on every non-default field.
17393 // Distinct low-non-default values so a stray field defaulting
17394 // fails loudly.
17395 let want = ResourceLimits {
17396 max_expansion_depth: 3,
17397 max_cache_entries: 5,
17398 max_expansion_size: 7,
17399 max_macro_body_size: 11,
17400 max_registered_macros: 13,
17401 max_macro_arity: 17,
17402 };
17403 assert_eq!(Expander::with_limits(want).resource_limits(), want);
17404 }
17405
17406 #[test]
17407 fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17408 // Delegation identity — [`Expander::new`] delegates to
17409 // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17410 // the two constructors MUST agree on the resource-ceiling
17411 // projection. Pins the "no drift between the two paths to a
17412 // fresh default-posture expander" theorem: a future refactor
17413 // that breaks the delegation (an inline field literal reappearing
17414 // in [`Expander::new`], an accidental hardcoded const at ONE
17415 // site with the other unchanged) fires here. Peer of
17416 // `resource_limits_default_impl_matches_const_form` one
17417 // TYPE-LEVEL axis over — that test pins the two projections of
17418 // the shipped posture on [`ResourceLimits`] itself; this test
17419 // pins the two constructor paths on [`Expander`] to reach the
17420 // SAME six values through the SAME bundle.
17421 assert_eq!(
17422 Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17423 Expander::new().resource_limits()
17424 );
17425 }
17426
17427 #[test]
17428 fn expander_with_limits_composes_with_struct_update_override() {
17429 // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17430 // "the shipped posture with ONE ceiling overridden" as ONE
17431 // struct-update literal AND thread that composition through
17432 // [`Expander::with_limits`] as ONE typed call — the ergonomic
17433 // shape that six independent setter invocations cannot express
17434 // in ONE expression. Overridden ceiling carries the fresh value,
17435 // every other ceiling carries its shipped default. A future
17436 // refactor that removes `Copy` from [`ResourceLimits`] fails to
17437 // compile at THIS site rather than at every downstream consumer.
17438 let e = Expander::with_limits(ResourceLimits {
17439 max_macro_arity: 4,
17440 ..DEFAULT_RESOURCE_LIMITS
17441 });
17442 assert_eq!(e.max_macro_arity(), 4);
17443 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17444 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17445 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17446 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17447 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17448 }
17449
17450 #[test]
17451 fn expander_with_limits_carries_the_new_execution_strategy() {
17452 // The (compile_templates, cache_enabled) execution-strategy pair
17453 // MUST carry the [`Expander::new`] default posture — bytecode +
17454 // cache both on — regardless of which resource posture the
17455 // caller passes in. Cache-count observation is the least-
17456 // invasive check available on the public surface: a fresh
17457 // `with_limits` expander that expands a repeat-call site must
17458 // populate at least one cache entry (bytecode + cache both on),
17459 // and a substitute-only sibling would leave `cache_size()` at
17460 // zero even after every call site expanded. A regression that
17461 // wired the wrong execution strategy into `with_limits`
17462 // (compile_templates: false, cache_enabled: false) fires here.
17463 let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17464 let forms = read(
17465 "(defmacro id (x) `,x)
17466 (id one)
17467 (id one)",
17468 )
17469 .unwrap();
17470 e.expand_program(forms).unwrap();
17471 assert!(
17472 e.cache_size() >= 1,
17473 "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17474 e.cache_size()
17475 );
17476 }
17477
17478 #[test]
17479 fn expander_with_limits_fires_the_arity_gate_from_construction() {
17480 // Behavioral pin — the at-construction bundle assignment MUST
17481 // reach the SAME check sites the individual + bulk POST-
17482 // construction setters do. Construct an expander with
17483 // `max_macro_arity: 1` directly and confirm the arity gate
17484 // fires on a two-arg macro registration. Sibling of
17485 // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17486 // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17487 // construction setter reaching the gate; this pin covers the
17488 // AT-construction assignment reaching the SAME gate through
17489 // the SAME `self.limits.max_macro_arity` load. A regression
17490 // that wired `with_limits` to a shadow field rather than
17491 // `self.limits` fails here.
17492 let mut e = Expander::with_limits(ResourceLimits {
17493 max_macro_arity: 1,
17494 ..DEFAULT_RESOURCE_LIMITS
17495 });
17496 let forms = read("(defmacro two (a b) `,a)").unwrap();
17497 let err = e.expand_program(forms).unwrap_err();
17498 assert!(
17499 matches!(
17500 err,
17501 LispError::MacroArityExceeded {
17502 arity: 2,
17503 limit: 1,
17504 ..
17505 }
17506 ),
17507 "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17508 );
17509 }
17510
17511 // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17512
17513 #[test]
17514 fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17515 // Field-level pin — the ceiling-lifted preset carries
17516 // [`usize::MAX`] on every axis of the six-field surface. Pinned
17517 // as six independent field asserts (rather than one struct
17518 // equality) so a drift on ONE field carries a distinct-name
17519 // failure that names the drifting axis. A future extension
17520 // that adds a SEVENTH ceiling requires an additional field
17521 // assert here in lockstep with the const literal — rustc's
17522 // field-exhaustiveness on the const forces the new field to
17523 // appear, and this pin exercises it.
17524 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17525 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17526 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17527 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17528 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17529 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17530 }
17531
17532 #[test]
17533 fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17534 // Structural-distinctness pin — the two constants
17535 // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17536 // sit at DISTINCT points on the posture axis of the (posture ×
17537 // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17538 // six-field surface — every shipped `DEFAULT_MAX_*` module
17539 // constant is a concrete positive value strictly less than
17540 // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17541 // shipped default up to [`usize::MAX`] (collapsing the two
17542 // presets on that field) fires here loudly rather than
17543 // silently letting the two presets become indistinguishable
17544 // on that axis. A regression which flipped ONE unbounded-const
17545 // field back to its default value also fires here — either
17546 // half of the disagreement breaks the theorem.
17547 assert_ne!(
17548 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17549 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17550 );
17551 assert_ne!(
17552 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17553 DEFAULT_RESOURCE_LIMITS.max_cache_entries
17554 );
17555 assert_ne!(
17556 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17557 DEFAULT_RESOURCE_LIMITS.max_expansion_size
17558 );
17559 assert_ne!(
17560 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17561 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17562 );
17563 assert_ne!(
17564 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17565 DEFAULT_RESOURCE_LIMITS.max_registered_macros
17566 );
17567 assert_ne!(
17568 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17569 DEFAULT_RESOURCE_LIMITS.max_macro_arity
17570 );
17571 }
17572
17573 #[test]
17574 fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17575 // Round-trip pin at CONSTRUCTION time — an expander built at
17576 // the ceiling-lifted preset MUST project back through
17577 // [`Expander::resource_limits`] to the SAME constant verbatim.
17578 // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17579 // one PRESET-POSTURE axis over — that pin exercises the
17580 // constructor's carry via a distinct-low-value bundle; this
17581 // pin exercises it via the ceiling-lifted preset constant.
17582 // Cross-pin the six-field snapshot equals the ceiling-lifted
17583 // constant so a regression that broke either the constant OR
17584 // the constructor's carry fires here.
17585 assert_eq!(
17586 Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17587 UNBOUNDED_RESOURCE_LIMITS
17588 );
17589 }
17590
17591 #[test]
17592 fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17593 // Behavioral pin — the ceiling-lifted preset MUST reach the
17594 // register-time body-size gate as an actually-lifted ceiling.
17595 // Register a macro whose body node count strictly exceeds
17596 // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17597 // shipped default would fail [`register_macro_def`] with a
17598 // [`LispError::MacroBodySizeExceeded`]) and confirm the
17599 // registration succeeds under the ceiling-lifted preset. Only
17600 // the body-size ceiling being lifted lets a body of that
17601 // magnitude land; the arity ceiling is left at 1 param via
17602 // struct-update override to prove the preset's other five
17603 // ceilings STILL project cleanly at [`usize::MAX`] alongside
17604 // the ceiling under test. A regression that failed to lift
17605 // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17606 // in the const literal) fires here on the axis it dropped.
17607 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17608 // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17609 // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17610 let mut body = String::from("(defmacro huge (k) `(");
17611 for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17612 body.push_str(",k ");
17613 }
17614 body.push_str("))");
17615 let forms = read(&body).unwrap();
17616 e.expand_program(forms)
17617 .expect("unbounded body-size ceiling must admit a body over the default limit");
17618 assert!(e.has("huge"));
17619 }
17620
17621 #[test]
17622 fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17623 // Behavioral pin sibling of the body-size pin above one
17624 // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17625 // the ceiling-lifted preset MUST also reach the arity gate as
17626 // an actually-lifted ceiling. Register a macro whose param
17627 // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17628 // 129-slot lambda list which under the shipped default fails
17629 // with [`LispError::MacroArityExceeded`]) and confirm the
17630 // registration succeeds under the ceiling-lifted preset.
17631 use std::fmt::Write as _;
17632 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17633 let mut src = String::from("(defmacro many-arity (");
17634 for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17635 write!(src, "a-{i} ").unwrap();
17636 }
17637 src.push_str(") `,a-0)");
17638 let forms = read(&src).unwrap();
17639 e.expand_program(forms)
17640 .expect("unbounded arity ceiling must admit a param list over the default limit");
17641 assert!(e.has("many-arity"));
17642 }
17643
17644 #[test]
17645 fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17646 // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17647 // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17648 // struct-update literal on the ceiling-lifted preset AND thread
17649 // that composition through [`Expander::with_limits`] as ONE
17650 // typed call. This is the "only THIS ceiling gates" fixture
17651 // shape a chain of six independent setters cannot express in
17652 // ONE expression, and the shape the last commit named as a
17653 // future benefit of the (getter, setter, at-construction)
17654 // three-corner face closing at ONE bundle.
17655 //
17656 // Set only `max_macro_arity: 1`; confirm the arity gate fires
17657 // on a two-arg registration while the five other lifted
17658 // ceilings project through the getter as [`usize::MAX`]. A
17659 // regression that broke the ceiling-lifted preset would EITHER
17660 // fail to project [`usize::MAX`] on one of the five other
17661 // fields OR fail to gate the isolated arity ceiling at 1.
17662 let mut e = Expander::with_limits(ResourceLimits {
17663 max_macro_arity: 1,
17664 ..UNBOUNDED_RESOURCE_LIMITS
17665 });
17666 assert_eq!(e.max_macro_arity(), 1);
17667 assert_eq!(e.max_expansion_depth(), usize::MAX);
17668 assert_eq!(e.max_cache_entries(), usize::MAX);
17669 assert_eq!(e.max_expansion_size(), usize::MAX);
17670 assert_eq!(e.max_macro_body_size(), usize::MAX);
17671 assert_eq!(e.max_registered_macros(), usize::MAX);
17672 let forms = read("(defmacro two (a b) `,a)").unwrap();
17673 let err = e.expand_program(forms).unwrap_err();
17674 assert!(
17675 matches!(
17676 err,
17677 LispError::MacroArityExceeded {
17678 arity: 2,
17679 limit: 1,
17680 ..
17681 }
17682 ),
17683 "struct-update override on the ceiling-lifted preset must \
17684 leave the isolated arity ceiling gating; got: {err:?}"
17685 );
17686 }
17687
17688 #[test]
17689 fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17690 // Coherence pin between the AT-construction constructor and
17691 // the POST-construction bulk setter — both paths that reach the
17692 // ceiling-lifted preset must land at the SAME six values on
17693 // the expander state. Pre-lift a caller wanting the unbounded
17694 // posture composed six individual setter invocations at the
17695 // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17696 // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17697 // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17698 // constant. A regression that wired the bulk setter to a
17699 // shadow field, or the ceiling-lifted preset to a stale field
17700 // literal, fires here on the constructor-vs-setter identity.
17701 let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17702 let mut b = Expander::new();
17703 b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17704 assert_eq!(a.resource_limits(), b.resource_limits());
17705 assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17706 }
17707
17708 // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17709
17710 /// A hand-authored asymmetric posture used across the lattice-law
17711 /// tests. Every field is a distinct positive value strictly less
17712 /// than [`usize::MAX`] and distinct from every corresponding
17713 /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17714 /// (meet, join, absorption, distributivity) discriminate across
17715 /// all three postures unambiguously.
17716 const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17717 max_expansion_depth: 7,
17718 max_cache_entries: 11,
17719 max_expansion_size: 13,
17720 max_macro_body_size: 17,
17721 max_registered_macros: 19,
17722 max_macro_arity: 23,
17723 };
17724
17725 /// A second asymmetric posture whose per-axis values are on both
17726 /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17727 /// smaller (so the meet picks THIS posture on those axes and the
17728 /// join picks `MID` on those axes) and three axes larger (mirror).
17729 /// Together the two postures exercise every ordering combination
17730 /// on the six-axis pointwise `min`/`max` cascade.
17731 const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17732 max_expansion_depth: 3, // smaller than MID's 7
17733 max_cache_entries: 29, // larger than MID's 11
17734 max_expansion_size: 5, // smaller than MID's 13
17735 max_macro_body_size: 31, // larger than MID's 17
17736 max_registered_macros: 2, // smaller than MID's 19
17737 max_macro_arity: 41, // larger than MID's 23
17738 };
17739
17740 #[test]
17741 fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17742 // Concrete-preset pin — the meet of the (shipped default,
17743 // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17744 // structurally: every `DEFAULT_MAX_*` module constant is a
17745 // concrete positive value strictly less than [`usize::MAX`],
17746 // so on every axis the pointwise-`min` picks the DEFAULT side.
17747 // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17748 // one COMBINATOR axis over on the same shipped-preset-pair
17749 // surface.
17750 assert_eq!(
17751 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17752 DEFAULT_RESOURCE_LIMITS,
17753 );
17754 assert_eq!(
17755 UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17756 DEFAULT_RESOURCE_LIMITS,
17757 );
17758 }
17759
17760 #[test]
17761 fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17762 // Concrete-preset pin — the join of the (shipped default,
17763 // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17764 // preset structurally: every axis's pointwise-`max` picks the
17765 // [`usize::MAX`] side over any concrete default. Peer of
17766 // `strictest_of_default_and_unbounded_projects_the_default`.
17767 assert_eq!(
17768 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17769 UNBOUNDED_RESOURCE_LIMITS,
17770 );
17771 assert_eq!(
17772 UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17773 UNBOUNDED_RESOURCE_LIMITS,
17774 );
17775 }
17776
17777 #[test]
17778 fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17779 // Field-level pin — the meet operation projects the pointwise
17780 // `min` across the six-field surface. Pinned as six
17781 // independent field asserts (rather than one struct equality)
17782 // so a drift on ONE axis carries a distinct-name failure that
17783 // names the drifting axis; a future SEVENTH ceiling extension
17784 // requires an additional field assert here in lockstep.
17785 let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17786 assert_eq!(m.max_expansion_depth, 3);
17787 assert_eq!(m.max_cache_entries, 11);
17788 assert_eq!(m.max_expansion_size, 5);
17789 assert_eq!(m.max_macro_body_size, 17);
17790 assert_eq!(m.max_registered_macros, 2);
17791 assert_eq!(m.max_macro_arity, 23);
17792 }
17793
17794 #[test]
17795 fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17796 // Field-level pin — the join operation projects the pointwise
17797 // `max` across the six-field surface. Pinned as six
17798 // independent field asserts so a drift on ONE axis carries a
17799 // distinct-name failure that names the drifting axis.
17800 let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17801 assert_eq!(j.max_expansion_depth, 7);
17802 assert_eq!(j.max_cache_entries, 29);
17803 assert_eq!(j.max_expansion_size, 13);
17804 assert_eq!(j.max_macro_body_size, 31);
17805 assert_eq!(j.max_registered_macros, 19);
17806 assert_eq!(j.max_macro_arity, 41);
17807 }
17808
17809 #[test]
17810 fn resource_limits_strictest_is_idempotent() {
17811 // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17812 // its own meet with itself; a regression that changed the
17813 // per-axis primitive to something other than pointwise `min`
17814 // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17815 // equal values) fires here.
17816 assert_eq!(
17817 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17818 DEFAULT_RESOURCE_LIMITS,
17819 );
17820 assert_eq!(
17821 UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17822 UNBOUNDED_RESOURCE_LIMITS,
17823 );
17824 assert_eq!(
17825 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17826 HAND_AUTHORED_MID_POSTURE,
17827 );
17828 }
17829
17830 #[test]
17831 fn resource_limits_most_permissive_is_idempotent() {
17832 // Lattice law — `a ∨ a = a`. Sibling of the strictest
17833 // idempotence pin one COMBINATOR axis over.
17834 assert_eq!(
17835 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17836 DEFAULT_RESOURCE_LIMITS,
17837 );
17838 assert_eq!(
17839 UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17840 UNBOUNDED_RESOURCE_LIMITS,
17841 );
17842 assert_eq!(
17843 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17844 HAND_AUTHORED_MID_POSTURE,
17845 );
17846 }
17847
17848 #[test]
17849 fn resource_limits_strictest_is_commutative() {
17850 // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17851 // on every axis, so the composition inherits commutativity
17852 // structurally. A regression that broke ordering (e.g. a
17853 // per-axis "prefer left on tie") fires here.
17854 assert_eq!(
17855 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17856 HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17857 );
17858 assert_eq!(
17859 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17860 HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17861 );
17862 }
17863
17864 #[test]
17865 fn resource_limits_most_permissive_is_commutative() {
17866 // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17867 // commutativity pin one COMBINATOR axis over.
17868 assert_eq!(
17869 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17870 HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17871 );
17872 assert_eq!(
17873 DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17874 HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17875 );
17876 }
17877
17878 #[test]
17879 fn resource_limits_strictest_is_associative() {
17880 // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17881 // is associative on every axis, so the composition inherits
17882 // associativity structurally. A regression that broke
17883 // grouping (e.g. an accidental accumulator that skewed the
17884 // order of `min` invocations) fires here.
17885 let a = DEFAULT_RESOURCE_LIMITS;
17886 let b = HAND_AUTHORED_MID_POSTURE;
17887 let c = HAND_AUTHORED_OTHER_POSTURE;
17888 assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17889 }
17890
17891 #[test]
17892 fn resource_limits_most_permissive_is_associative() {
17893 // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17894 // strictest associativity pin one COMBINATOR axis over.
17895 let a = DEFAULT_RESOURCE_LIMITS;
17896 let b = HAND_AUTHORED_MID_POSTURE;
17897 let c = HAND_AUTHORED_OTHER_POSTURE;
17898 assert_eq!(
17899 a.most_permissive(b).most_permissive(c),
17900 a.most_permissive(b.most_permissive(c)),
17901 );
17902 }
17903
17904 #[test]
17905 fn resource_limits_meet_and_join_satisfy_absorption() {
17906 // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17907 // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17908 // and associativity they define the lattice axioms; the two
17909 // absorption laws are the operator PAIR's interlock (each
17910 // undoes the other's contribution on the shared operand).
17911 // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17912 // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17913 // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17914 let a = HAND_AUTHORED_MID_POSTURE;
17915 let b = HAND_AUTHORED_OTHER_POSTURE;
17916 assert_eq!(a.strictest(a.most_permissive(b)), a);
17917 assert_eq!(a.most_permissive(a.strictest(b)), a);
17918 }
17919
17920 #[test]
17921 fn resource_limits_meet_distributes_over_join() {
17922 // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
17923 // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
17924 // on every axis, so the composition inherits distributivity
17925 // structurally. A regression that broke the pointwise
17926 // structure (e.g. a per-axis operation that used SUBTRACTION
17927 // rather than MIN/MAX) fires here — subtraction does NOT
17928 // distribute over addition and would break the law on the
17929 // asymmetric-posture witness pair.
17930 let a = DEFAULT_RESOURCE_LIMITS;
17931 let b = HAND_AUTHORED_MID_POSTURE;
17932 let c = HAND_AUTHORED_OTHER_POSTURE;
17933 assert_eq!(
17934 a.strictest(b.most_permissive(c)),
17935 a.strictest(b).most_permissive(a.strictest(c)),
17936 );
17937 }
17938
17939 #[test]
17940 fn resource_limits_join_distributes_over_meet() {
17941 // Distributive-lattice pin, sibling of the meet-over-join
17942 // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
17943 // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
17944 // distributivity identities hold; pin both so a regression
17945 // that broke ONE without the other cannot slip through.
17946 let a = DEFAULT_RESOURCE_LIMITS;
17947 let b = HAND_AUTHORED_MID_POSTURE;
17948 let c = HAND_AUTHORED_OTHER_POSTURE;
17949 assert_eq!(
17950 a.most_permissive(b.strictest(c)),
17951 a.most_permissive(b).strictest(a.most_permissive(c)),
17952 );
17953 }
17954
17955 #[test]
17956 fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
17957 // Meet-lower-bound pin — the meet lies at or below BOTH
17958 // operands on every axis. This is the structural GLB property
17959 // of the meet in the pointwise partial order (tighter-first):
17960 // any admissible input under the meet is admissible under
17961 // both operands, so the meet's ceiling must not exceed either
17962 // operand's ceiling on any axis.
17963 let a = HAND_AUTHORED_MID_POSTURE;
17964 let b = HAND_AUTHORED_OTHER_POSTURE;
17965 let m = a.strictest(b);
17966 assert!(m.max_expansion_depth <= a.max_expansion_depth);
17967 assert!(m.max_expansion_depth <= b.max_expansion_depth);
17968 assert!(m.max_cache_entries <= a.max_cache_entries);
17969 assert!(m.max_cache_entries <= b.max_cache_entries);
17970 assert!(m.max_expansion_size <= a.max_expansion_size);
17971 assert!(m.max_expansion_size <= b.max_expansion_size);
17972 assert!(m.max_macro_body_size <= a.max_macro_body_size);
17973 assert!(m.max_macro_body_size <= b.max_macro_body_size);
17974 assert!(m.max_registered_macros <= a.max_registered_macros);
17975 assert!(m.max_registered_macros <= b.max_registered_macros);
17976 assert!(m.max_macro_arity <= a.max_macro_arity);
17977 assert!(m.max_macro_arity <= b.max_macro_arity);
17978 }
17979
17980 #[test]
17981 fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
17982 // Join-upper-bound pin — the join lies at or above BOTH
17983 // operands on every axis. Sibling of the strictest-lower-
17984 // bound pin one COMBINATOR axis over; the structural LUB
17985 // property of the join in the pointwise partial order.
17986 let a = HAND_AUTHORED_MID_POSTURE;
17987 let b = HAND_AUTHORED_OTHER_POSTURE;
17988 let j = a.most_permissive(b);
17989 assert!(j.max_expansion_depth >= a.max_expansion_depth);
17990 assert!(j.max_expansion_depth >= b.max_expansion_depth);
17991 assert!(j.max_cache_entries >= a.max_cache_entries);
17992 assert!(j.max_cache_entries >= b.max_cache_entries);
17993 assert!(j.max_expansion_size >= a.max_expansion_size);
17994 assert!(j.max_expansion_size >= b.max_expansion_size);
17995 assert!(j.max_macro_body_size >= a.max_macro_body_size);
17996 assert!(j.max_macro_body_size >= b.max_macro_body_size);
17997 assert!(j.max_registered_macros >= a.max_registered_macros);
17998 assert!(j.max_registered_macros >= b.max_registered_macros);
17999 assert!(j.max_macro_arity >= a.max_macro_arity);
18000 assert!(j.max_macro_arity >= b.max_macro_arity);
18001 }
18002
18003 #[test]
18004 fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18005 // Const-fn pin — the meet is evaluable in const context, so a
18006 // caller can stitch a new `pub const` preset from two shipped
18007 // presets without deferring the composition to runtime. Pinned
18008 // as a `const` binding at the item level so the compiler
18009 // exercises the const-fn path structurally; the runtime
18010 // assertion is redundant for a `const` value that already
18011 // evaluated at compile time, but pins the identity as a typed
18012 // theorem alongside every OTHER `strictest` pin above.
18013 const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18014 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18015 assert_eq!(
18016 DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18017 min_usize(
18018 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18019 HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18020 ),
18021 );
18022 // Extra check: the composition MUST project the MID posture's
18023 // small value on every axis where MID is smaller than DEFAULT
18024 // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18025 // strictly less than every `DEFAULT_MAX_*` constant, so the
18026 // meet projects MID verbatim.
18027 assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18028 }
18029
18030 #[test]
18031 fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18032 // Const-fn pin sibling of the strictest const-composition pin
18033 // one COMBINATOR axis over — the join is also `const fn`, so
18034 // a caller can stitch a new `pub const` preset via the LUB
18035 // combinator at compile time.
18036 const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18037 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18038 assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18039 }
18040
18041 // ── ResourceLimits::leq — pointwise partial order ─────────────────
18042
18043 #[test]
18044 fn resource_limits_leq_is_pointwise_field_conjunction() {
18045 // Field-level pin — the leq relation projects the pointwise
18046 // `<=` conjunction across the six-field surface. `LOOSER` is
18047 // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18048 // per-axis tighten on ANY field alone flips the relation to
18049 // false. Pinned as six independent tighten-per-axis asserts so
18050 // a drift on ONE axis carries a distinct-name failure that
18051 // names the drifting axis; a future SEVENTH ceiling extension
18052 // requires an additional per-axis tighten assert here in
18053 // lockstep.
18054 const LOOSER: ResourceLimits = ResourceLimits {
18055 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18056 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18057 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18058 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18059 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18060 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18061 };
18062 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18063 assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18064
18065 // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18066 // on that axis; the leq must flip to false. Every other axis
18067 // stays at MID, so LOOSER still dominates on those axes —
18068 // isolating the single-axis violation.
18069 let exceed_depth = ResourceLimits {
18070 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18071 ..HAND_AUTHORED_MID_POSTURE
18072 };
18073 assert!(!exceed_depth.leq(LOOSER));
18074 let exceed_cache = ResourceLimits {
18075 max_cache_entries: LOOSER.max_cache_entries + 1,
18076 ..HAND_AUTHORED_MID_POSTURE
18077 };
18078 assert!(!exceed_cache.leq(LOOSER));
18079 let exceed_expansion = ResourceLimits {
18080 max_expansion_size: LOOSER.max_expansion_size + 1,
18081 ..HAND_AUTHORED_MID_POSTURE
18082 };
18083 assert!(!exceed_expansion.leq(LOOSER));
18084 let exceed_body = ResourceLimits {
18085 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18086 ..HAND_AUTHORED_MID_POSTURE
18087 };
18088 assert!(!exceed_body.leq(LOOSER));
18089 let exceed_registered = ResourceLimits {
18090 max_registered_macros: LOOSER.max_registered_macros + 1,
18091 ..HAND_AUTHORED_MID_POSTURE
18092 };
18093 assert!(!exceed_registered.leq(LOOSER));
18094 let exceed_arity = ResourceLimits {
18095 max_macro_arity: LOOSER.max_macro_arity + 1,
18096 ..HAND_AUTHORED_MID_POSTURE
18097 };
18098 assert!(!exceed_arity.leq(LOOSER));
18099 }
18100
18101 #[test]
18102 fn resource_limits_leq_is_reflexive() {
18103 // Partial-order law — `a ≤ a` for every posture. Reflexivity
18104 // is the identity axiom on any lattice's underlying partial
18105 // order; a regression that changed the per-axis primitive to
18106 // strict `<` (or that added an accidental "prefer other on
18107 // tie" branch) fires here on every shipped posture.
18108 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18109 assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18110 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18111 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18112 }
18113
18114 #[test]
18115 fn resource_limits_leq_is_antisymmetric() {
18116 // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18117 // dominance witness on a partial order pins the two operands
18118 // to structural equality. On the pointwise `<=` composition
18119 // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18120 // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18121 // is field-conjunction. Sibling to reflexivity one AXIOM axis
18122 // over on the partial-order axiom surface.
18123 //
18124 // Two distinct constructions of the SAME six-field posture
18125 // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18126 // spread of the shipped preset) — mutual leq holds; struct
18127 // equality holds; antisymmetry pin closes.
18128 let a = DEFAULT_RESOURCE_LIMITS;
18129 let b = ResourceLimits {
18130 max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18131 max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18132 max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18133 max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18134 max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18135 max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18136 };
18137 assert!(a.leq(b));
18138 assert!(b.leq(a));
18139 assert_eq!(a, b);
18140 }
18141
18142 #[test]
18143 fn resource_limits_leq_is_transitive() {
18144 // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18145 // is the composition axiom on any lattice's partial order.
18146 // Pointwise `<=` on [`usize`] is transitive per axis, so the
18147 // six-field conjunction inherits it structurally.
18148 //
18149 // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18150 // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18151 // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18152 const LOOSER: ResourceLimits = ResourceLimits {
18153 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18154 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18155 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18156 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18157 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18158 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18159 };
18160 const LOOSEST: ResourceLimits = ResourceLimits {
18161 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18162 max_cache_entries: LOOSER.max_cache_entries + 1,
18163 max_expansion_size: LOOSER.max_expansion_size + 1,
18164 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18165 max_registered_macros: LOOSER.max_registered_macros + 1,
18166 max_macro_arity: LOOSER.max_macro_arity + 1,
18167 };
18168 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18169 assert!(LOOSER.leq(LOOSEST));
18170 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18171 }
18172
18173 #[test]
18174 fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18175 // Concrete-preset pin — the leq relation on the shipped
18176 // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18177 // `DEFAULT_MAX_*` module constant is a concrete positive
18178 // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18179 // fails (every `DEFAULT_MAX_*` is strictly less than
18180 // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18181 // the matching default). Peer of
18182 // `strictest_of_default_and_unbounded_projects_the_default`
18183 // one PRIMITIVE-KIND axis over on the same shipped-preset-
18184 // pair surface: where the meet composition projects DEFAULT,
18185 // this relation asserts DEFAULT is the tighter of the two.
18186 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18187 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18188 }
18189
18190 #[test]
18191 fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18192 // Partial-order NON-total pin — the two hand-authored asymmetric
18193 // postures have MID smaller on three axes (depth, expansion,
18194 // registered) and OTHER smaller on the other three (cache,
18195 // body, arity); neither dominates the other, so BOTH
18196 // directions of the leq relation fail. The pin discriminates
18197 // a regression that promoted `leq` from partial to total
18198 // (e.g. an accidental `||` swap in the conjunction, which
18199 // would make every pair comparable).
18200 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18201 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18202 }
18203
18204 #[test]
18205 fn resource_limits_leq_agrees_with_meet() {
18206 // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18207 // classification lattice one crate over
18208 // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18209 // agreement axiom binding the partial order to the meet
18210 // combinator, and the default-impl body on
18211 // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18212 // through it. This pin discharges the same axiom on the
18213 // ResourceLimits algebra as a two-direction structural
18214 // equality across the strict-order preset pair AND the
18215 // reflexivity witness.
18216 //
18217 // Forward (a ≤ b ⇒ a ⊓ b = a):
18218 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18219 assert_eq!(
18220 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18221 DEFAULT_RESOURCE_LIMITS,
18222 );
18223 // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18224 assert_eq!(
18225 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18226 DEFAULT_RESOURCE_LIMITS,
18227 );
18228 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18229
18230 // Non-agreement witness — the incomparable asymmetric postures
18231 // have meet distinct from EITHER operand, and leq false in
18232 // both directions; agreement holds on the negative side too.
18233 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18234 assert_ne!(
18235 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18236 HAND_AUTHORED_MID_POSTURE,
18237 );
18238 }
18239
18240 #[test]
18241 fn resource_limits_leq_agrees_with_join() {
18242 // Lattice cross-check axiom, sibling of the meet-agreement
18243 // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18244 //
18245 // Forward (a ≤ b ⇒ a ⊔ b = b):
18246 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18247 assert_eq!(
18248 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18249 UNBOUNDED_RESOURCE_LIMITS,
18250 );
18251 // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18252 assert_eq!(
18253 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18254 DEFAULT_RESOURCE_LIMITS,
18255 );
18256 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18257
18258 // Non-agreement witness — the incomparable asymmetric postures
18259 // have join distinct from EITHER operand.
18260 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18261 assert_ne!(
18262 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18263 HAND_AUTHORED_OTHER_POSTURE,
18264 );
18265 }
18266
18267 #[test]
18268 fn resource_limits_strictest_is_leq_both_operands() {
18269 // Meet-lower-bound pin, typed lattice-relation companion to
18270 // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18271 // one PRIMITIVE-KIND axis over — where the per-axis pin
18272 // discharged the six independent `<=` asserts inline, this pin
18273 // routes through the typed `leq` primitive, so the
18274 // greatest-lower-bound property is stated in the LATTICE
18275 // vocabulary (meet-is-leq-both-operands) rather than the
18276 // six-fold field-conjunction vocabulary.
18277 let a = HAND_AUTHORED_MID_POSTURE;
18278 let b = HAND_AUTHORED_OTHER_POSTURE;
18279 let m = a.strictest(b);
18280 assert!(m.leq(a));
18281 assert!(m.leq(b));
18282 }
18283
18284 #[test]
18285 fn resource_limits_most_permissive_is_geq_both_operands() {
18286 // Join-upper-bound pin, typed lattice-relation companion to
18287 // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18288 // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18289 // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18290 // meet-lower-bound pin one COMBINATOR axis over.
18291 let a = HAND_AUTHORED_MID_POSTURE;
18292 let b = HAND_AUTHORED_OTHER_POSTURE;
18293 let j = a.most_permissive(b);
18294 assert!(a.leq(j));
18295 assert!(b.leq(j));
18296 }
18297
18298 #[test]
18299 fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18300 // Const-fn pin — the leq relation is evaluable in const
18301 // context, so a caller can pin a preset-relation identity at
18302 // compile time. Sibling of the const-fn composition pins on
18303 // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18304 // over: those two operations compose two presets into a
18305 // const-time third, and this operation decides a const-time
18306 // yes/no on two presets. `const _: ()` at item scope is a
18307 // compile-time proof — the compiler rejects the module if
18308 // either identity does not hold; a runtime `assert!` over the
18309 // same expression would fire at test time on the same value
18310 // clippy's `assertions_on_constants` correctly names as a
18311 // redundant late check on a compile-time constant.
18312 //
18313 // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18314 // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18315 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18316 // Reverse direction fails — [`usize::MAX`] exceeds every
18317 // concrete `DEFAULT_MAX_*` constant, so the partial order is
18318 // STRICT on the shipped preset pair:
18319 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18320 }
18321
18322 // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18323
18324 #[test]
18325 fn empty_resource_limits_binds_every_ceiling_to_zero() {
18326 // Field-level pin — the zero preset carries `0` on every axis
18327 // of the six-field surface. Pinned as six independent field
18328 // asserts (rather than one struct equality) so a drift on ONE
18329 // field carries a distinct-name failure that names the drifting
18330 // axis. A future extension that adds a SEVENTH ceiling requires
18331 // an additional field assert here in lockstep with the const
18332 // literal — rustc's field-exhaustiveness on the const forces
18333 // the new field to appear, and this pin exercises the seed
18334 // value. Structural peer of
18335 // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18336 // one LATTICE-POLE axis over on the (bottom, top)
18337 // bounded-lattice preset-pair surface.
18338 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18339 assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18340 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18341 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18342 assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18343 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18344 }
18345
18346 #[test]
18347 fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18348 // Structural-distinctness pin — the two constants
18349 // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18350 // at DISTINCT points on the preset-posture axis, and the
18351 // disagreement is EXHAUSTIVE on the six-field surface — every
18352 // shipped `DEFAULT_MAX_*` module constant is a concrete
18353 // strictly-positive value, so every axis distinguishes DEFAULT
18354 // from the all-zero bottom. Peer of
18355 // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18356 // one LATTICE-POLE axis over.
18357 assert_ne!(
18358 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18359 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18360 );
18361 assert_ne!(
18362 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18363 DEFAULT_RESOURCE_LIMITS.max_cache_entries
18364 );
18365 assert_ne!(
18366 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18367 DEFAULT_RESOURCE_LIMITS.max_expansion_size
18368 );
18369 assert_ne!(
18370 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18371 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18372 );
18373 assert_ne!(
18374 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18375 DEFAULT_RESOURCE_LIMITS.max_registered_macros
18376 );
18377 assert_ne!(
18378 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18379 DEFAULT_RESOURCE_LIMITS.max_macro_arity
18380 );
18381 }
18382
18383 #[test]
18384 fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18385 // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18386 // disagrees on every axis (`0 != usize::MAX` on every field).
18387 // Structural companion to the DEFAULT-vs-EMPTY and
18388 // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18389 // over — together the three pin the six-field surface at the
18390 // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18391 assert_ne!(
18392 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18393 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18394 );
18395 assert_ne!(
18396 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18397 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18398 );
18399 assert_ne!(
18400 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18401 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18402 );
18403 assert_ne!(
18404 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18405 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18406 );
18407 assert_ne!(
18408 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18409 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18410 );
18411 assert_ne!(
18412 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18413 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18414 );
18415 }
18416
18417 #[test]
18418 fn empty_resource_limits_is_the_join_identity() {
18419 // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18420 // pointwise `max` against 0 returns the posture verbatim, so
18421 // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18422 // the join monoid. Peer of
18423 // `empty_resource_limits_is_the_meet_annihilator` one
18424 // COMBINATOR axis over on the (meet, join) surface.
18425 //
18426 // Cross-preset exhaustion — verify the identity against every
18427 // named preset (DEFAULT, UNBOUNDED, both hand-authored
18428 // asymmetric postures) so a regression that broke ONE axis of
18429 // the algebra fires here on the operand whose field crosses
18430 // that axis's boundary.
18431 assert_eq!(
18432 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18433 DEFAULT_RESOURCE_LIMITS,
18434 );
18435 assert_eq!(
18436 EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18437 DEFAULT_RESOURCE_LIMITS,
18438 );
18439 assert_eq!(
18440 UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18441 UNBOUNDED_RESOURCE_LIMITS,
18442 );
18443 assert_eq!(
18444 EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18445 UNBOUNDED_RESOURCE_LIMITS,
18446 );
18447 assert_eq!(
18448 HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18449 HAND_AUTHORED_MID_POSTURE,
18450 );
18451 assert_eq!(
18452 HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18453 HAND_AUTHORED_OTHER_POSTURE,
18454 );
18455 // Idempotence at the identity itself — sibling witness of the
18456 // `most_permissive_is_idempotent` general pin one PRESET axis
18457 // over.
18458 assert_eq!(
18459 EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18460 EMPTY_RESOURCE_LIMITS,
18461 );
18462 }
18463
18464 #[test]
18465 fn empty_resource_limits_is_the_meet_annihilator() {
18466 // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18467 // pointwise `min` against 0 returns 0 on every axis, so
18468 // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18469 // element) of the meet operation. Peer of
18470 // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18471 // axis over — the identity on one side of the bounded lattice
18472 // is the annihilator on the other side.
18473 assert_eq!(
18474 DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18475 EMPTY_RESOURCE_LIMITS,
18476 );
18477 assert_eq!(
18478 EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18479 EMPTY_RESOURCE_LIMITS,
18480 );
18481 assert_eq!(
18482 UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18483 EMPTY_RESOURCE_LIMITS,
18484 );
18485 assert_eq!(
18486 EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18487 EMPTY_RESOURCE_LIMITS,
18488 );
18489 assert_eq!(
18490 HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18491 EMPTY_RESOURCE_LIMITS,
18492 );
18493 assert_eq!(
18494 HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18495 EMPTY_RESOURCE_LIMITS,
18496 );
18497 }
18498
18499 #[test]
18500 fn empty_resource_limits_is_the_lattice_minimum() {
18501 // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18502 // posture's field is a `usize` and `0 <= x` holds for every
18503 // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18504 // every posture on every axis of the pointwise `<=` conjunction.
18505 // Peer of the UNBOUNDED-is-maximum property on the
18506 // partial-order face one LATTICE-POLE axis over — those two
18507 // pin the (min, max) bounds of the bounded lattice.
18508 assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18509 assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18510 assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18511 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18512 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18513 // Reverse direction FAILS on every non-EMPTY preset (each has
18514 // at least one field strictly greater than 0). The partial
18515 // order is STRICT between EMPTY and every other named preset,
18516 // structurally exhausting the diagonal of the
18517 // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18518 // face at the BOTTOM row.
18519 assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18520 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18521 assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18522 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18523 }
18524
18525 #[test]
18526 fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18527 // Const-fn pin — the bounded-lattice identity + annihilator +
18528 // minimum laws hold at COMPILE time. Sibling of
18529 // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18530 // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18531 // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18532 // time; this pin fixes the bounded-lattice pole role of the
18533 // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18534 // is a compile-time proof — a regression that broke either
18535 // identity element or the bottom-is-minimum invariant fires at
18536 // rustc time, not test-run time.
18537 //
18538 // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18539 // three-preset ordering chain on the shipped preset diagonal).
18540 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18541 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18542 // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18543 // below EMPTY (each has a strictly-positive field).
18544 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18545 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18546 // Join-identity + meet-annihilator laws cross-pinned through
18547 // the `leq` primitive at const time — the round-trip identity
18548 // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18549 // leq asserts above discharge both algebraic laws by the
18550 // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18551 // cross-check axiom already pinned in this cohort.
18552 }
18553
18554 #[test]
18555 fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18556 // Fold-identity behavioral pin — a slice of postures folded
18557 // through [`ResourceLimits::most_permissive`] with EMPTY as
18558 // the seed produces the pointwise least upper bound across
18559 // the slice on every axis. The join-identity property
18560 // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18561 // contributes nothing to the fold's result — every field's
18562 // pointwise `max` against the seed's `0` returns the field's
18563 // value verbatim, so the fold reduces to the joined-across-
18564 // -operands upper bound. This is the destination usage the
18565 // constant's docstring names as its fold-identity role.
18566 //
18567 // A regression that seeded from ANY non-empty posture (e.g.
18568 // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18569 // on every axis where the seed's field exceeds the pointwise
18570 // max of the slice — fires as a distinct-axis inequality here.
18571 let postures: [ResourceLimits; 3] = [
18572 HAND_AUTHORED_MID_POSTURE,
18573 HAND_AUTHORED_OTHER_POSTURE,
18574 DEFAULT_RESOURCE_LIMITS,
18575 ];
18576 let joined = postures
18577 .iter()
18578 .copied()
18579 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18580 // Every operand is `leq` the joined result — the least
18581 // upper bound dominates each contributor on every axis.
18582 assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18583 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18584 assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18585 // Field-level pin — each axis is the pointwise `max` of the
18586 // three sources' values. Six independent field asserts so a
18587 // drift on ONE axis carries a distinct-name failure.
18588 assert_eq!(
18589 joined.max_expansion_depth,
18590 HAND_AUTHORED_MID_POSTURE
18591 .max_expansion_depth
18592 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18593 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18594 );
18595 assert_eq!(
18596 joined.max_cache_entries,
18597 HAND_AUTHORED_MID_POSTURE
18598 .max_cache_entries
18599 .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18600 .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18601 );
18602 assert_eq!(
18603 joined.max_expansion_size,
18604 HAND_AUTHORED_MID_POSTURE
18605 .max_expansion_size
18606 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18607 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18608 );
18609 assert_eq!(
18610 joined.max_macro_body_size,
18611 HAND_AUTHORED_MID_POSTURE
18612 .max_macro_body_size
18613 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18614 .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18615 );
18616 assert_eq!(
18617 joined.max_registered_macros,
18618 HAND_AUTHORED_MID_POSTURE
18619 .max_registered_macros
18620 .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18621 .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18622 );
18623 assert_eq!(
18624 joined.max_macro_arity,
18625 HAND_AUTHORED_MID_POSTURE
18626 .max_macro_arity
18627 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18628 .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18629 );
18630 // Empty-slice edge case — folding over an EMPTY slice returns
18631 // the seed verbatim (the identity element as the null aggregate).
18632 // A wrapper-type approach (`Option<ResourceLimits>` for the
18633 // aggregate) would need a distinct absent branch here; the
18634 // typed identity-element approach resolves it structurally.
18635 let empty_slice: [ResourceLimits; 0] = [];
18636 assert_eq!(
18637 empty_slice
18638 .iter()
18639 .copied()
18640 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18641 EMPTY_RESOURCE_LIMITS,
18642 );
18643 }
18644
18645 #[test]
18646 fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18647 // Dual fold-identity pin — a slice of postures folded through
18648 // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18649 // produces the pointwise greatest lower bound across the slice
18650 // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18651 // pin one COMBINATOR axis over — the identity element for
18652 // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18653 // identity element for join is the meet's annihilator
18654 // (`EMPTY`); the two together bind the bounded-lattice's
18655 // aggregation surface at BOTH poles.
18656 let postures: [ResourceLimits; 3] = [
18657 HAND_AUTHORED_MID_POSTURE,
18658 HAND_AUTHORED_OTHER_POSTURE,
18659 DEFAULT_RESOURCE_LIMITS,
18660 ];
18661 let met = postures
18662 .iter()
18663 .copied()
18664 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18665 // The greatest lower bound sits `leq` every operand.
18666 assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18667 assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18668 assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18669 // Field-level pin — each axis is the pointwise `min` of the
18670 // three sources' values (the UNBOUNDED seed's `usize::MAX`
18671 // never wins the min against a concrete positive value).
18672 assert_eq!(
18673 met.max_expansion_depth,
18674 HAND_AUTHORED_MID_POSTURE
18675 .max_expansion_depth
18676 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18677 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18678 );
18679 assert_eq!(
18680 met.max_cache_entries,
18681 HAND_AUTHORED_MID_POSTURE
18682 .max_cache_entries
18683 .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18684 .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18685 );
18686 assert_eq!(
18687 met.max_expansion_size,
18688 HAND_AUTHORED_MID_POSTURE
18689 .max_expansion_size
18690 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18691 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18692 );
18693 assert_eq!(
18694 met.max_macro_body_size,
18695 HAND_AUTHORED_MID_POSTURE
18696 .max_macro_body_size
18697 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18698 .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18699 );
18700 assert_eq!(
18701 met.max_registered_macros,
18702 HAND_AUTHORED_MID_POSTURE
18703 .max_registered_macros
18704 .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18705 .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18706 );
18707 assert_eq!(
18708 met.max_macro_arity,
18709 HAND_AUTHORED_MID_POSTURE
18710 .max_macro_arity
18711 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18712 .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18713 );
18714 // Empty-slice edge — folding over an empty slice returns the
18715 // UNBOUNDED seed verbatim (the meet-identity as the null
18716 // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18717 let empty_slice: [ResourceLimits; 0] = [];
18718 assert_eq!(
18719 empty_slice
18720 .iter()
18721 .copied()
18722 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18723 UNBOUNDED_RESOURCE_LIMITS,
18724 );
18725 }
18726
18727 #[test]
18728 fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18729 // Null-aggregate pin — the N-ary meet over an empty slice is
18730 // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18731 // typed identity-element approach resolves the empty-input case
18732 // at the algebra layer; a wrapper-type approach
18733 // (`Option<ResourceLimits>` for the aggregate) would need a
18734 // distinct absent branch here, and a panic-on-empty seed-from-
18735 // -first-element approach would abort the caller.
18736 assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18737 }
18738
18739 #[test]
18740 fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18741 // Null-aggregate dual pin — the N-ary join over an empty slice
18742 // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18743 // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18744 // (N-ary meet, N-ary join) surface — the identity element for
18745 // join is the meet's ANNIHILATOR and vice versa; the two
18746 // together bind both bounded-lattice poles as null aggregates.
18747 assert_eq!(
18748 ResourceLimits::most_permissive_of(&[]),
18749 EMPTY_RESOURCE_LIMITS,
18750 );
18751 }
18752
18753 #[test]
18754 fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18755 // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18756 // input is the input verbatim, since the meet-identity seed
18757 // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18758 // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18759 // against [`usize::MAX`]. This is the identity-law-at-N=1
18760 // pin: the fold's seed does not distort the 1-input case.
18761 assert_eq!(
18762 ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18763 HAND_AUTHORED_MID_POSTURE,
18764 );
18765 assert_eq!(
18766 ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18767 HAND_AUTHORED_OTHER_POSTURE,
18768 );
18769 assert_eq!(
18770 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18771 DEFAULT_RESOURCE_LIMITS,
18772 );
18773 }
18774
18775 #[test]
18776 fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18777 // Join-identity absorption pin at N=1 — dual of
18778 // `strictest_of_single_element`, using the join-identity seed
18779 // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18780 // `EMPTY.most_permissive(a) == a` against any non-zero value.
18781 assert_eq!(
18782 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18783 HAND_AUTHORED_MID_POSTURE,
18784 );
18785 assert_eq!(
18786 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18787 HAND_AUTHORED_OTHER_POSTURE,
18788 );
18789 assert_eq!(
18790 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18791 DEFAULT_RESOURCE_LIMITS,
18792 );
18793 }
18794
18795 #[test]
18796 fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18797 // N=2 reduction pin — the N-ary meet on a 2-element slice
18798 // reduces to the pairwise combinator, since
18799 // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18800 // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18801 // every input pair the algebra distinguishes.
18802 assert_eq!(
18803 ResourceLimits::strictest_of(
18804 &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18805 ),
18806 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18807 );
18808 assert_eq!(
18809 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18810 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18811 );
18812 }
18813
18814 #[test]
18815 fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18816 // N=2 reduction dual pin — the N-ary join on a 2-element slice
18817 // reduces to the pairwise combinator.
18818 assert_eq!(
18819 ResourceLimits::most_permissive_of(&[
18820 HAND_AUTHORED_MID_POSTURE,
18821 HAND_AUTHORED_OTHER_POSTURE,
18822 ]),
18823 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18824 );
18825 assert_eq!(
18826 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18827 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18828 );
18829 }
18830
18831 #[test]
18832 fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18833 // Method-agrees-with-fold pin — the sole behavioral claim the
18834 // N-ary aggregator makes is that it IS the inline
18835 // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18836 // pre-lift caller composed at every consumption site. A drift
18837 // between the method's implementation and the inline fold
18838 // would break this equality on every non-empty slice.
18839 let postures: [ResourceLimits; 3] = [
18840 HAND_AUTHORED_MID_POSTURE,
18841 HAND_AUTHORED_OTHER_POSTURE,
18842 DEFAULT_RESOURCE_LIMITS,
18843 ];
18844 assert_eq!(
18845 ResourceLimits::strictest_of(&postures),
18846 postures
18847 .iter()
18848 .copied()
18849 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18850 );
18851 }
18852
18853 #[test]
18854 fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18855 // Method-agrees-with-fold dual pin.
18856 let postures: [ResourceLimits; 3] = [
18857 HAND_AUTHORED_MID_POSTURE,
18858 HAND_AUTHORED_OTHER_POSTURE,
18859 DEFAULT_RESOURCE_LIMITS,
18860 ];
18861 assert_eq!(
18862 ResourceLimits::most_permissive_of(&postures),
18863 postures
18864 .iter()
18865 .copied()
18866 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18867 );
18868 }
18869
18870 #[test]
18871 fn resource_limits_strictest_of_is_order_independent() {
18872 // Order-independence pin — the fold inherits commutativity AND
18873 // associativity of the pairwise `strictest`, so any permutation
18874 // of the input slice yields the same aggregate. Reversal is
18875 // the maximal permutation for a 3-element input (indices
18876 // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18877 // discriminate here.
18878 let forward = ResourceLimits::strictest_of(&[
18879 HAND_AUTHORED_MID_POSTURE,
18880 HAND_AUTHORED_OTHER_POSTURE,
18881 DEFAULT_RESOURCE_LIMITS,
18882 ]);
18883 let reversed = ResourceLimits::strictest_of(&[
18884 DEFAULT_RESOURCE_LIMITS,
18885 HAND_AUTHORED_OTHER_POSTURE,
18886 HAND_AUTHORED_MID_POSTURE,
18887 ]);
18888 assert_eq!(forward, reversed);
18889 }
18890
18891 #[test]
18892 fn resource_limits_most_permissive_of_is_order_independent() {
18893 let forward = ResourceLimits::most_permissive_of(&[
18894 HAND_AUTHORED_MID_POSTURE,
18895 HAND_AUTHORED_OTHER_POSTURE,
18896 DEFAULT_RESOURCE_LIMITS,
18897 ]);
18898 let reversed = ResourceLimits::most_permissive_of(&[
18899 DEFAULT_RESOURCE_LIMITS,
18900 HAND_AUTHORED_OTHER_POSTURE,
18901 HAND_AUTHORED_MID_POSTURE,
18902 ]);
18903 assert_eq!(forward, reversed);
18904 }
18905
18906 #[test]
18907 fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18908 // Compile-time-composition pin — `strictest_of` is `const fn`,
18909 // so a caller binds an N-ary aggregate to a `pub const` and the
18910 // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18911 // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18912 // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18913 // identity already pinned pairwise, verified here through the
18914 // N-ary method's const-context evaluation. The N-ary const-fn
18915 // peer of the pairwise const-fn pins on
18916 // [`ResourceLimits::strictest`]; a regression that turned
18917 // `strictest_of` into a runtime `fn` would break the `const`
18918 // binding below at compile time.
18919 const AGGREGATED: ResourceLimits =
18920 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
18921 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18922 }
18923
18924 #[test]
18925 fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
18926 // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
18927 // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
18928 // picks DEFAULT on every axis.
18929 const AGGREGATED: ResourceLimits =
18930 ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
18931 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18932 }
18933
18934 #[test]
18935 fn resource_limits_strictest_of_result_is_leq_every_operand() {
18936 // Lattice-relation pin — the N-ary meet sits `leq` every
18937 // operand. This is the N-ary extension of the pairwise
18938 // `strictest_is_leq_both_operands` pin; the meet aggregate
18939 // dominates from below (the greatest lower bound of the slice
18940 // sits at-or-below each input on every axis).
18941 let postures: [ResourceLimits; 3] = [
18942 HAND_AUTHORED_MID_POSTURE,
18943 HAND_AUTHORED_OTHER_POSTURE,
18944 DEFAULT_RESOURCE_LIMITS,
18945 ];
18946 let met = ResourceLimits::strictest_of(&postures);
18947 for p in postures {
18948 assert!(met.leq(p));
18949 }
18950 }
18951
18952 #[test]
18953 fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
18954 // Lattice-relation dual pin — every operand sits `leq` the
18955 // N-ary join. The join aggregate is the least upper bound of
18956 // the slice.
18957 let postures: [ResourceLimits; 3] = [
18958 HAND_AUTHORED_MID_POSTURE,
18959 HAND_AUTHORED_OTHER_POSTURE,
18960 DEFAULT_RESOURCE_LIMITS,
18961 ];
18962 let joined = ResourceLimits::most_permissive_of(&postures);
18963 for p in postures {
18964 assert!(p.leq(joined));
18965 }
18966 }
18967
18968 #[test]
18969 fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
18970 // Behavioral pin — an expander constructed from the N-ary meet
18971 // of a slice of postures MUST gate at the tightest ceiling
18972 // across the slice on every axis. Peer of
18973 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
18974 // one AGGREGATION-ARITY axis over: where the pairwise pin
18975 // composes exactly two postures, this pin composes THREE
18976 // through the N-ary aggregator, confirming the arity extension
18977 // preserves the "meet gates at tightest" behavioral contract.
18978 //
18979 // The slice tightens macro_arity to 1 (the tightest across the
18980 // three postures) via the middle operand; every other axis
18981 // stays at usize::MAX (the outer two operands supply
18982 // usize::MAX; the middle operand's struct-update literal
18983 // preserves usize::MAX everywhere except macro_arity).
18984 let tightened = ResourceLimits::strictest_of(&[
18985 UNBOUNDED_RESOURCE_LIMITS,
18986 ResourceLimits {
18987 max_macro_arity: 1,
18988 ..UNBOUNDED_RESOURCE_LIMITS
18989 },
18990 UNBOUNDED_RESOURCE_LIMITS,
18991 ]);
18992 assert_eq!(tightened.max_macro_arity, 1);
18993 assert_eq!(tightened.max_expansion_depth, usize::MAX);
18994 assert_eq!(tightened.max_cache_entries, usize::MAX);
18995 assert_eq!(tightened.max_expansion_size, usize::MAX);
18996 assert_eq!(tightened.max_macro_body_size, usize::MAX);
18997 assert_eq!(tightened.max_registered_macros, usize::MAX);
18998 let mut e = Expander::with_limits(tightened);
18999 let forms = read("(defmacro two (a b) `,a)").unwrap();
19000 let err = e.expand_program(forms).unwrap_err();
19001 assert!(
19002 matches!(
19003 err,
19004 LispError::MacroArityExceeded {
19005 arity: 2,
19006 limit: 1,
19007 ..
19008 }
19009 ),
19010 "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19011 );
19012 }
19013
19014 #[test]
19015 fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19016 // Behavioral pin — an expander constructed from the meet of
19017 // two postures MUST gate at the tighter of the two ceilings
19018 // on every axis. Build the meet of the ceiling-lifted preset
19019 // and a hand-authored posture whose `max_macro_arity` sits at
19020 // 1; the meet inherits the 1-slot arity ceiling (tighter than
19021 // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19022 // other axis (tighter than the hand-authored posture's small
19023 // values — no, wait: the OTHER posture's small values ARE
19024 // tighter. So the meet inherits 1 on arity and the OTHER
19025 // small values elsewhere.) Confirm the constructed expander
19026 // rejects a 2-arg macro registration with the arity gate.
19027 let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19028 max_macro_arity: 1,
19029 ..UNBOUNDED_RESOURCE_LIMITS
19030 });
19031 assert_eq!(tightened.max_macro_arity, 1);
19032 // Other five axes remain at `usize::MAX` (both operands carry
19033 // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19034 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19035 assert_eq!(tightened.max_cache_entries, usize::MAX);
19036 assert_eq!(tightened.max_expansion_size, usize::MAX);
19037 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19038 assert_eq!(tightened.max_registered_macros, usize::MAX);
19039 let mut e = Expander::with_limits(tightened);
19040 let forms = read("(defmacro two (a b) `,a)").unwrap();
19041 let err = e.expand_program(forms).unwrap_err();
19042 assert!(
19043 matches!(
19044 err,
19045 LispError::MacroArityExceeded {
19046 arity: 2,
19047 limit: 1,
19048 ..
19049 }
19050 ),
19051 "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19052 );
19053 }
19054
19055 // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19056
19057 /// A tighter-than-MID lower-bracket witness — every axis strictly
19058 /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19059 /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19060 /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19061 /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19062 /// pair.
19063 const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19064 max_expansion_depth: 2,
19065 max_cache_entries: 3,
19066 max_expansion_size: 5,
19067 max_macro_body_size: 7,
19068 max_registered_macros: 11,
19069 max_macro_arity: 13,
19070 };
19071
19072 /// A looser-than-MID upper-bracket witness — every axis strictly larger
19073 /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19074 /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19075 /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19076 /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19077 /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19078 /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19079 /// CEILING`.
19080 const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19081 max_expansion_depth: 47,
19082 max_cache_entries: 53,
19083 max_expansion_size: 59,
19084 max_macro_body_size: 61,
19085 max_registered_macros: 67,
19086 max_macro_arity: 71,
19087 };
19088
19089 #[test]
19090 fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19091 // In-range identity — when the input already sits within the
19092 // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19093 // axis), the clamp is the identity function: the pointwise
19094 // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19095 // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19096 // per-axis picks `a`). The primary invariant on the "input
19097 // already satisfies the bracket" arm of the three-arm bracket-
19098 // membership surface.
19099 //
19100 // Prerequisite pins — the two hand-authored bounds bracket
19101 // `MID` strictly on every axis (structural check that discharges
19102 // the "already in range" premise). A regression that inflated
19103 // `FLOOR` or deflated `CEILING` past `MID` would fail these
19104 // asserts before the clamp identity was tested, making the
19105 // premise's failure name itself rather than showing up as a
19106 // downstream misleading clamp mismatch.
19107 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19108 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19109 let clamped =
19110 HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19111 assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19112 }
19113
19114 #[test]
19115 fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19116 // Below-lower floor — when the input is tighter than the
19117 // bracket floor on every axis (`a.leq(lower)` per-axis), the
19118 // clamp lifts the input up to `lower`: the pointwise `max(a,
19119 // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19120 // then the pointwise `min(lower, upper) == lower` (since
19121 // `lower ≤ upper` per-axis picks `lower`). Peer of
19122 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19123 // one BRACKET-POSITION axis over on the (in-range, below-lower,
19124 // above-upper) three-arm bracket-membership surface.
19125 //
19126 // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19127 // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19128 // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19129 // MUST return `FLOOR`.
19130 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19131 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19132 let clamped =
19133 EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19134 assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19135 }
19136
19137 #[test]
19138 fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19139 // Above-upper ceiling — when the input is looser than the
19140 // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19141 // clamp lowers the input down to `upper`: the pointwise
19142 // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19143 // `a`), then the pointwise `min(a, upper) == upper` (since
19144 // `upper ≤ a` per-axis picks `upper`). Peer of the
19145 // below-lower arm one BRACKET-POSITION axis over on the three-
19146 // arm bracket-membership surface.
19147 //
19148 // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19149 // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19150 // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19151 // MUST return `CEILING`.
19152 assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19153 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19154 let clamped =
19155 UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19156 assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19157 }
19158
19159 #[test]
19160 fn resource_limits_clamp_result_sits_within_the_bracket() {
19161 // Bracket-membership contract — the DEFINING invariant on the
19162 // clamp primitive: for every well-formed bracket
19163 // (`lower.leq(upper)`) and every input `a`, the result sits
19164 // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19165 // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19166 // positions of the bracket-membership surface via the four
19167 // known-preset inputs (below-lower → in-range → above-upper
19168 // → asymmetric-crossing) so a regression that inflated the
19169 // ceiling arm or deflated the floor arm exhausts all four
19170 // input positions and names the failing arm.
19171 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19172 let inputs = [
19173 EMPTY_RESOURCE_LIMITS, // below-lower
19174 HAND_AUTHORED_MID_POSTURE, // in-range
19175 UNBOUNDED_RESOURCE_LIMITS, // above-upper
19176 HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19177 ];
19178 for a in inputs {
19179 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19180 assert!(
19181 HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19182 "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19183 );
19184 assert!(
19185 clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19186 "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19187 );
19188 }
19189 }
19190
19191 #[test]
19192 fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19193 // Extrema-bracket identity — clamping against the bounded-
19194 // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19195 // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19196 // identity function on every posture: every posture already
19197 // sits within the widest possible bracket. Cross-checks the
19198 // clamp primitive against the bounded-lattice identity elements
19199 // previously lifted; a regression that swapped the two extrema
19200 // in the impl would collapse every input to
19201 // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19202 // every non-EMPTY input.
19203 for a in [
19204 DEFAULT_RESOURCE_LIMITS,
19205 HAND_AUTHORED_MID_POSTURE,
19206 HAND_AUTHORED_OTHER_POSTURE,
19207 EMPTY_RESOURCE_LIMITS,
19208 UNBOUNDED_RESOURCE_LIMITS,
19209 ] {
19210 assert_eq!(
19211 a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19212 a,
19213 "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19214 );
19215 }
19216 }
19217
19218 #[test]
19219 fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19220 // Degenerate-bracket collapse — a zero-width bracket
19221 // (`lower == upper`) collapses every input to the single point.
19222 // The pointwise `max(a, x)` lifts every axis to at least `x`,
19223 // then the pointwise `min(., x)` clamps every axis to exactly
19224 // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19225 // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19226 // == x` when `x.leq(a)`).
19227 for x in [
19228 HAND_AUTHORED_MID_POSTURE,
19229 HAND_AUTHORED_OTHER_POSTURE,
19230 DEFAULT_RESOURCE_LIMITS,
19231 ] {
19232 for a in [
19233 EMPTY_RESOURCE_LIMITS,
19234 HAND_AUTHORED_MID_POSTURE,
19235 HAND_AUTHORED_OTHER_POSTURE,
19236 UNBOUNDED_RESOURCE_LIMITS,
19237 ] {
19238 assert_eq!(
19239 a.clamp(x, x),
19240 x,
19241 "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19242 );
19243 }
19244 }
19245 }
19246
19247 #[test]
19248 fn resource_limits_clamp_is_idempotent() {
19249 // Idempotence — re-applying the same bracket to an already-
19250 // clamped posture returns the same result. The clamp primitive
19251 // is idempotent because its output already sits within the
19252 // `[lower, upper]` bracket (pinned by
19253 // `resource_limits_clamp_result_sits_within_the_bracket`), so
19254 // the second application hits the in-range identity arm. Peer
19255 // of the idempotence pins on `strictest` and `most_permissive`
19256 // one ARITY axis over (2-input primitive → 3-input bracket) on
19257 // the lattice-algebra idempotence-law surface.
19258 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19259 for a in [
19260 EMPTY_RESOURCE_LIMITS,
19261 HAND_AUTHORED_MID_POSTURE,
19262 HAND_AUTHORED_OTHER_POSTURE,
19263 UNBOUNDED_RESOURCE_LIMITS,
19264 ] {
19265 let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19266 let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19267 assert_eq!(
19268 once, twice,
19269 "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19270 );
19271 }
19272 }
19273
19274 #[test]
19275 fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19276 // Const-fn pin — the bracket combinator is evaluable in const
19277 // context, so a caller can pre-clamp a shipped preset against
19278 // an operator-supplied policy bracket at compile time. Sibling
19279 // of the const-fn composition pins on `strictest` /
19280 // `most_permissive` / `strictest_of` / `most_permissive_of`
19281 // one PRIMITIVE-KIND axis over: those four combinators compose
19282 // presets at const time, and this bracket combinator projects
19283 // a preset into a const-time bracket.
19284 //
19285 // A regression to a runtime `fn` here would fail the `pub
19286 // const` binding below at compile time — a stronger guarantee
19287 // than a runtime `assert!` because the const binding is
19288 // evaluated once at compile time, not per test invocation.
19289 const CLAMPED_DEFAULT: ResourceLimits =
19290 DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19291 assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19292
19293 // Compile-time bracket-membership pin — const `assert!` on the
19294 // lattice-relation contract discharges the "clamp result sits
19295 // within the bracket" invariant at compile time on the
19296 // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19297 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19298 const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19299 }
19300
19301 #[test]
19302 fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19303 // Structural-equivalence pin — the sole behavioral claim the
19304 // clamp primitive makes is that it IS the
19305 // `self.most_permissive(lower).strictest(upper)` two-step
19306 // cascade the pre-lift caller composed. A drift between the
19307 // method and the inline cascade would break this equality on
19308 // every input; the pin exhausts the four bracket-position arms
19309 // of the input surface.
19310 for a in [
19311 EMPTY_RESOURCE_LIMITS,
19312 HAND_AUTHORED_MID_POSTURE,
19313 HAND_AUTHORED_OTHER_POSTURE,
19314 UNBOUNDED_RESOURCE_LIMITS,
19315 ] {
19316 let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19317 let via_cascade = a
19318 .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19319 .strictest(HAND_AUTHORED_CLAMP_CEILING);
19320 assert_eq!(via_method, via_cascade);
19321 }
19322 }
19323
19324 #[test]
19325 fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19326 // Behavioral pin — an expander constructed from a preset
19327 // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19328 // bracket ceiling's arity axis. Confirms the clamp primitive
19329 // reaches through to the runtime gates that consume
19330 // `ResourceLimits`, so a future consumer that pre-clamps a
19331 // preset against a policy bracket inherits the bracket's
19332 // ceilings at every guard. Peer of
19333 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19334 // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19335 // N-ary-meet, bracket) combinator-arity face.
19336 //
19337 // The bracket ceiling tightens `max_macro_arity` to 1; every
19338 // other axis stays at `usize::MAX` (the CEILING preset carries
19339 // `usize::MAX` on those, and the input UNBOUNDED preset also
19340 // carries `usize::MAX` on every axis — the clamp's `strictest`
19341 // arm picks `usize::MAX` from either side).
19342 let bracket_ceiling = ResourceLimits {
19343 max_macro_arity: 1,
19344 ..UNBOUNDED_RESOURCE_LIMITS
19345 };
19346 let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19347 assert_eq!(clamped.max_macro_arity, 1);
19348 assert_eq!(clamped.max_expansion_depth, usize::MAX);
19349 assert_eq!(clamped.max_cache_entries, usize::MAX);
19350 assert_eq!(clamped.max_expansion_size, usize::MAX);
19351 assert_eq!(clamped.max_macro_body_size, usize::MAX);
19352 assert_eq!(clamped.max_registered_macros, usize::MAX);
19353 let mut e = Expander::with_limits(clamped);
19354 let forms = read("(defmacro two (a b) `,a)").unwrap();
19355 let err = e.expand_program(forms).unwrap_err();
19356 assert!(
19357 matches!(
19358 err,
19359 LispError::MacroArityExceeded {
19360 arity: 2,
19361 limit: 1,
19362 ..
19363 }
19364 ),
19365 "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19366 );
19367 }
19368
19369 // ── ResourceLimits::within — bracket-membership predicate ─────────
19370
19371 #[test]
19372 fn resource_limits_within_of_posture_in_range_is_true() {
19373 // In-range identity — when the input already sits within the
19374 // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19375 // a.leq(upper)`), the predicate returns `true`. Peer of
19376 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19377 // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19378 // face of the bracket-primitive surface.
19379 //
19380 // Prerequisite pins — the two hand-authored bounds bracket
19381 // `MID` strictly on every axis, structurally discharging the
19382 // "already in range" premise.
19383 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19384 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19385 assert!(HAND_AUTHORED_MID_POSTURE
19386 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19387 }
19388
19389 #[test]
19390 fn resource_limits_within_of_posture_below_lower_is_false() {
19391 // Below-lower rejection — when the input is strictly tighter
19392 // than the bracket floor on some axis (`!lower.leq(a)`), the
19393 // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19394 // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19395 // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19396 // making the conjunction reject regardless of `EMPTY`'s relation
19397 // to `CEILING`. Peer of
19398 // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19399 // one PRIMITIVE-KIND axis over.
19400 assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19401 assert!(
19402 !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19403 );
19404 }
19405
19406 #[test]
19407 fn resource_limits_within_of_posture_above_upper_is_false() {
19408 // Above-upper rejection — when the input is strictly looser
19409 // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19410 // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19411 // every axis at `usize::MAX`, strictly above
19412 // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19413 // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19414 // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19415 // one PRIMITIVE-KIND axis over.
19416 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19417 assert!(!UNBOUNDED_RESOURCE_LIMITS
19418 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19419 }
19420
19421 #[test]
19422 fn resource_limits_within_with_lattice_extrema_is_true() {
19423 // Extrema-bracket identity — every posture sits within the
19424 // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19425 // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19426 // against the bounded-lattice identity elements (`EMPTY` is
19427 // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19428 // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19429 // one PRIMITIVE-KIND axis over — the predicate view of the
19430 // "clamp is the identity" property.
19431 for a in [
19432 DEFAULT_RESOURCE_LIMITS,
19433 HAND_AUTHORED_MID_POSTURE,
19434 HAND_AUTHORED_OTHER_POSTURE,
19435 EMPTY_RESOURCE_LIMITS,
19436 UNBOUNDED_RESOURCE_LIMITS,
19437 ] {
19438 assert!(
19439 a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19440 "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19441 );
19442 }
19443 }
19444
19445 #[test]
19446 fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19447 // Degenerate-bracket collapse — a zero-width bracket admits
19448 // only the single point at the bounds. `a.within(x, x)` holds
19449 // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19450 // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19451 // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19452 // unconditionally, and the predicate distinguishes the "was
19453 // already equal" case from the "would have been clamped" case.
19454 let bounds = [
19455 HAND_AUTHORED_MID_POSTURE,
19456 HAND_AUTHORED_OTHER_POSTURE,
19457 DEFAULT_RESOURCE_LIMITS,
19458 EMPTY_RESOURCE_LIMITS,
19459 UNBOUNDED_RESOURCE_LIMITS,
19460 ];
19461 for x in bounds {
19462 for a in bounds {
19463 assert_eq!(
19464 a.within(x, x),
19465 a == x,
19466 "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19467 );
19468 }
19469 }
19470 }
19471
19472 #[test]
19473 fn resource_limits_within_of_self_is_reflexive() {
19474 // Reflexive-bracket identity — every posture sits within the
19475 // zero-width bracket at itself, since `a.leq(a)` holds by
19476 // reflexivity of `leq`. The reflexive-bracket peer of the
19477 // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19478 // ARITY axis over (pairwise → three-input).
19479 for a in [
19480 EMPTY_RESOURCE_LIMITS,
19481 DEFAULT_RESOURCE_LIMITS,
19482 HAND_AUTHORED_MID_POSTURE,
19483 HAND_AUTHORED_OTHER_POSTURE,
19484 UNBOUNDED_RESOURCE_LIMITS,
19485 ] {
19486 assert!(
19487 a.within(a, a),
19488 "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19489 );
19490 }
19491 }
19492
19493 #[test]
19494 fn resource_limits_within_agrees_with_clamp_fixed_point() {
19495 // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19496 // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19497 // binding the `within` predicate to the `clamp` combinator,
19498 // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19499 // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19500 // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19501 // pinned by `resource_limits_leq_agrees_with_join`) axioms
19502 // binding the `leq` relation to its combinator peers.
19503 //
19504 // Exercised on all four bracket-position arms of the input
19505 // surface (below-lower / in-range / above-upper / asymmetric-
19506 // crossing) so a regression that broke either direction of the
19507 // fixed-point equivalence exhausts every position and names the
19508 // failing arm.
19509 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19510 for a in [
19511 EMPTY_RESOURCE_LIMITS, // below-lower — clamp != a, within false
19512 HAND_AUTHORED_MID_POSTURE, // in-range — clamp == a, within true
19513 UNBOUNDED_RESOURCE_LIMITS, // above-upper — clamp != a, within false
19514 HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19515 ] {
19516 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19517 let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19518 let clamp_fixed = clamped == a;
19519 assert_eq!(
19520 within_holds, clamp_fixed,
19521 "within(lower, upper) must agree with clamp fixed-point; \
19522 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19523 );
19524 }
19525 }
19526
19527 #[test]
19528 fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19529 // Structural-equivalence pin — the sole behavioral claim the
19530 // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19531 // two-primitive conjunction the pre-lift caller composed. A drift
19532 // between the method and the inline conjunction would break this
19533 // equality on some input; the pin exhausts the four bracket-
19534 // position arms of the input surface. Peer of
19535 // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19536 // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19537 for a in [
19538 EMPTY_RESOURCE_LIMITS,
19539 HAND_AUTHORED_MID_POSTURE,
19540 HAND_AUTHORED_OTHER_POSTURE,
19541 UNBOUNDED_RESOURCE_LIMITS,
19542 ] {
19543 let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19544 let via_conjunction =
19545 HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19546 assert_eq!(via_method, via_conjunction, "a={a:?}");
19547 }
19548 }
19549
19550 #[test]
19551 fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19552 // Const-fn pin — the bracket predicate is evaluable in const
19553 // context, so a caller can pin a preset-bracket-membership
19554 // identity at compile time. Sibling of the const-fn evaluability
19555 // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19556 // relation and combinator both evaluate at const time, and the
19557 // predicate closing the (combinator, predicate) row does too.
19558 //
19559 // A regression to a runtime `fn` here would fail the `const _:
19560 // () = assert!(...)` bindings below at compile time.
19561 const _: () = assert!(
19562 DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19563 );
19564 const _: () = assert!(
19565 DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19566 );
19567 const _: () = assert!(
19568 !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19569 );
19570 }
19571
19572 #[test]
19573 fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19574 // Behavioral pin — a caller that CHECKS bracket-membership
19575 // before constructing an expander can safely reach through to
19576 // the runtime gates: if `within` reports true, `clamp` is a
19577 // no-op (by the fixed-point theorem), so the expander's
19578 // ceilings are the input posture's own. Peer of
19579 // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19580 // one PRIMITIVE-KIND axis over — the predicate view of the
19581 // "clamp reaches through to the runtime gates" property.
19582 //
19583 // The candidate sits at the bracket ceiling (arity=1, other
19584 // axes at usize::MAX), so it MUST sit within the [EMPTY,
19585 // bracket_ceiling] bracket — the maximal within-membership arm
19586 // of the fixed-point theorem. Every other axis at usize::MAX
19587 // clears the register-time gates, so the arity gate at 1 is
19588 // the one that trips on a 2-parameter macro registration.
19589 let bracket_ceiling = ResourceLimits {
19590 max_macro_arity: 1,
19591 ..UNBOUNDED_RESOURCE_LIMITS
19592 };
19593 let candidate = ResourceLimits {
19594 max_macro_arity: 1,
19595 ..UNBOUNDED_RESOURCE_LIMITS
19596 };
19597 assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19598 // Since candidate sits within the bracket, clamp is the
19599 // identity; the expander inherits candidate's ceilings verbatim.
19600 let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19601 assert_eq!(clamped, candidate);
19602 let mut e = Expander::with_limits(clamped);
19603 let forms = read("(defmacro two (a b) `,a)").unwrap();
19604 let err = e.expand_program(forms).unwrap_err();
19605 assert!(
19606 matches!(
19607 err,
19608 LispError::MacroArityExceeded {
19609 arity: 2,
19610 limit: 1,
19611 ..
19612 }
19613 ),
19614 "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19615 );
19616 }
19617
19618 // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19619 // N-ary boolean-conjunction peers of the pairwise `leq` relation,
19620 // closing the (combinator, predicate) row on the N-ary-aggregation
19621 // face of the lattice-algebra surface. See the docstring on
19622 // `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19623 // bracket) × (combinator, predicate) placement.
19624
19625 #[test]
19626 fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19627 // Empty-conjunction identity — every posture is trivially a
19628 // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19629 // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19630 // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19631 // identity element of the underlying operation (`true` for
19632 // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19633 assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19634 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19635 assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19636 assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19637 }
19638
19639 #[test]
19640 fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19641 // Empty-conjunction dual — every posture is trivially an upper
19642 // bound of the empty set. Peer of
19643 // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19644 // COMBINATOR-DIRECTION axis over, AND peer of
19645 // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19646 // slice identity one PRIMITIVE-KIND axis over.
19647 assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19648 assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19649 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19650 assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19651 }
19652
19653 #[test]
19654 fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19655 // 1-input identity — the singleton predicate reduces to the
19656 // pairwise partial-order relation. The N-ary predicate on a
19657 // 1-element slice is the pairwise `leq` verbatim, mirroring
19658 // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19659 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19660 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19661 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19662 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19663 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19664 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19665 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19666 ];
19667 for (a, b) in cases {
19668 assert_eq!(
19669 a.is_lower_bound_of(&[b]),
19670 a.leq(b),
19671 "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19672 );
19673 }
19674 }
19675
19676 #[test]
19677 fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19678 // 1-input dual identity — the singleton upper-bound predicate
19679 // is the pairwise `leq` with the direction flipped
19680 // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19681 // BELOW it.
19682 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19683 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19684 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19685 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19686 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19687 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19688 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19689 ];
19690 for (a, b) in cases {
19691 assert_eq!(
19692 a.is_upper_bound_of(&[b]),
19693 b.leq(a),
19694 "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19695 );
19696 }
19697 }
19698
19699 #[test]
19700 fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19701 // Definitional bridge — the N-ary meet is always a lower bound
19702 // of the slice it aggregates. Pins the link between the N-ary
19703 // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19704 // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19705 // bound set the predicate CHARACTERIZES.
19706 let postures: [ResourceLimits; 3] = [
19707 HAND_AUTHORED_MID_POSTURE,
19708 HAND_AUTHORED_OTHER_POSTURE,
19709 DEFAULT_RESOURCE_LIMITS,
19710 ];
19711 let met = ResourceLimits::strictest_of(&postures);
19712 assert!(met.is_lower_bound_of(&postures));
19713 }
19714
19715 #[test]
19716 fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19717 // Dual definitional bridge — the N-ary join is always an upper
19718 // bound of the slice it aggregates.
19719 let postures: [ResourceLimits; 3] = [
19720 HAND_AUTHORED_MID_POSTURE,
19721 HAND_AUTHORED_OTHER_POSTURE,
19722 DEFAULT_RESOURCE_LIMITS,
19723 ];
19724 let joined = ResourceLimits::most_permissive_of(&postures);
19725 assert!(joined.is_upper_bound_of(&postures));
19726 }
19727
19728 #[test]
19729 fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19730 // Universal-bottom witness — the lattice bottom is a common
19731 // lower bound of every slice, since `EMPTY.leq(a) == true` for
19732 // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19733 // The bounded-lattice specialization of the general "the meet-
19734 // identity is a lower bound of every set" theorem.
19735 let postures: [ResourceLimits; 4] = [
19736 HAND_AUTHORED_MID_POSTURE,
19737 HAND_AUTHORED_OTHER_POSTURE,
19738 DEFAULT_RESOURCE_LIMITS,
19739 UNBOUNDED_RESOURCE_LIMITS,
19740 ];
19741 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19742 // The three preset-carried postures each individually — the
19743 // universal-bottom witness holds on the singleton slice too.
19744 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19745 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19746 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19747 }
19748
19749 #[test]
19750 fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19751 // Universal-top witness — the lattice top is a common upper
19752 // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19753 // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19754 // per-axis).
19755 let postures: [ResourceLimits; 4] = [
19756 HAND_AUTHORED_MID_POSTURE,
19757 HAND_AUTHORED_OTHER_POSTURE,
19758 DEFAULT_RESOURCE_LIMITS,
19759 EMPTY_RESOURCE_LIMITS,
19760 ];
19761 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19762 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19763 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19764 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19765 }
19766
19767 #[test]
19768 fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19769 // Any-operand short-circuit — if `self` fails `leq` against ANY
19770 // operand in the slice, the N-ary conjunction rejects. The dual
19771 // of the "meet is a lower bound" witness on the false side:
19772 // moving `self` STRICTLY ABOVE an operand on any axis costs the
19773 // lower-bound property.
19774 //
19775 // MID sits above the join (element-wise) on some axes and below
19776 // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19777 // (fails against OTHER's smaller-axis values) NOR is OTHER a
19778 // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19779 // values).
19780 assert!(!HAND_AUTHORED_MID_POSTURE
19781 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19782 assert!(!HAND_AUTHORED_OTHER_POSTURE
19783 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19784 // A UNIVERSAL rejection — the lattice top is a lower bound
19785 // ONLY of the empty set OR of slices whose every element equals
19786 // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19787 // top's per-axis usize::MAX loses `leq` on every axis whose
19788 // preset value is strictly smaller.
19789 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19790 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19791 }
19792
19793 #[test]
19794 fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19795 // Dual any-operand short-circuit — if ANY operand fails `leq`
19796 // against `self`, the N-ary conjunction rejects.
19797 assert!(!HAND_AUTHORED_MID_POSTURE
19798 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19799 assert!(!HAND_AUTHORED_OTHER_POSTURE
19800 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19801 // The bottom cannot bound anything from above except the empty
19802 // set OR a slice of EMPTY-only postures.
19803 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19804 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19805 }
19806
19807 #[test]
19808 fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19809 // Method-vs-scaffold cross-check — the lifted N-ary predicate
19810 // agrees with the pre-lift two-primitive
19811 // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19812 // preset-carried posture across the shipped bounded-lattice
19813 // extrema AND the two hand-authored asymmetric witnesses. Pins
19814 // the lift is a semantic no-op — post-lift the consumer routes
19815 // through ONE name instead of composing the conjunction inline,
19816 // but the two paths compute the same boolean.
19817 let candidates: [ResourceLimits; 5] = [
19818 EMPTY_RESOURCE_LIMITS,
19819 DEFAULT_RESOURCE_LIMITS,
19820 UNBOUNDED_RESOURCE_LIMITS,
19821 HAND_AUTHORED_MID_POSTURE,
19822 HAND_AUTHORED_OTHER_POSTURE,
19823 ];
19824 let slice: [ResourceLimits; 3] = [
19825 HAND_AUTHORED_MID_POSTURE,
19826 HAND_AUTHORED_OTHER_POSTURE,
19827 DEFAULT_RESOURCE_LIMITS,
19828 ];
19829 for c in candidates {
19830 let direct = slice.iter().all(|p| c.leq(*p));
19831 assert_eq!(
19832 c.is_lower_bound_of(&slice),
19833 direct,
19834 "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19835 );
19836 }
19837 }
19838
19839 #[test]
19840 fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19841 // Dual method-vs-scaffold cross-check.
19842 let candidates: [ResourceLimits; 5] = [
19843 EMPTY_RESOURCE_LIMITS,
19844 DEFAULT_RESOURCE_LIMITS,
19845 UNBOUNDED_RESOURCE_LIMITS,
19846 HAND_AUTHORED_MID_POSTURE,
19847 HAND_AUTHORED_OTHER_POSTURE,
19848 ];
19849 let slice: [ResourceLimits; 3] = [
19850 HAND_AUTHORED_MID_POSTURE,
19851 HAND_AUTHORED_OTHER_POSTURE,
19852 DEFAULT_RESOURCE_LIMITS,
19853 ];
19854 for c in candidates {
19855 let direct = slice.iter().all(|p| p.leq(c));
19856 assert_eq!(
19857 c.is_upper_bound_of(&slice),
19858 direct,
19859 "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19860 );
19861 }
19862 }
19863
19864 #[test]
19865 fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19866 // Const-fn pin — the N-ary lower-bound predicate is evaluable
19867 // in const context, so a caller can pin an N-ary-bound-
19868 // membership identity at compile time. Sibling of the const-fn
19869 // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19870 // axis over, AND sibling of `strictest_of`'s const-fn pin one
19871 // COMBINATOR-KIND axis over.
19872 //
19873 // A regression to a runtime `fn` here would fail the `const _:
19874 // () = assert!(...)` bindings below at compile time.
19875 const _: () = assert!(EMPTY_RESOURCE_LIMITS
19876 .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19877 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19878 const _: () =
19879 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19880 }
19881
19882 #[test]
19883 fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19884 // Const-fn dual pin.
19885 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19886 .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19887 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19888 const _: () =
19889 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19890 }
19891
19892 /// The full canonical preset roster the strict-order pins sweep
19893 /// through — the (bottom, middle, top) bounded-lattice preset
19894 /// triple plus the two hand-authored incomparable asymmetric
19895 /// postures. Every strict-order law below quantifies over this
19896 /// slice, so a regression that breaks strict `<` on any preset
19897 /// pair discriminates at ONE named fixture rather than at a
19898 /// per-test ad-hoc literal.
19899 const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19900 EMPTY_RESOURCE_LIMITS,
19901 DEFAULT_RESOURCE_LIMITS,
19902 UNBOUNDED_RESOURCE_LIMITS,
19903 HAND_AUTHORED_MID_POSTURE,
19904 HAND_AUTHORED_OTHER_POSTURE,
19905 ];
19906
19907 #[test]
19908 fn resource_limits_lt_is_irreflexive() {
19909 // Strict partial-order law — `a < a == false` for every
19910 // posture. Irreflexivity is the strict-relation axiom peer of
19911 // reflexivity on `leq` one STRICTNESS axis over on the
19912 // partial-order axiom surface; a regression that dropped the
19913 // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19914 // `self.leq(other)` — would fire here on every preset since
19915 // `leq` is reflexive.
19916 for &a in STRICT_ORDER_ROSTER {
19917 assert!(!a.lt(a), "irreflexivity failed on {a:?}");
19918 }
19919 }
19920
19921 #[test]
19922 fn resource_limits_lt_is_asymmetric() {
19923 // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
19924 // pair. The two directions of the strict relation cannot both
19925 // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
19926 // requires `b.leq(a)`. Sweeps every ordered pair in the
19927 // canonical preset roster; discriminates a regression that
19928 // dropped the antisymmetric leg in either direction.
19929 for &a in STRICT_ORDER_ROSTER {
19930 for &b in STRICT_ORDER_ROSTER {
19931 if a.lt(b) {
19932 assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
19933 }
19934 }
19935 }
19936 }
19937
19938 #[test]
19939 fn resource_limits_lt_is_transitive() {
19940 // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
19941 // every ordered triple in the canonical preset roster; each
19942 // consequence is checked whenever both antecedents hold. On
19943 // the (bottom, middle, top) diagonal the canonical witness
19944 // `EMPTY < DEFAULT < UNBOUNDED` composes to
19945 // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
19946 // the bounded-lattice diagonal explicitly; the sweep pins the
19947 // same law on every other transitively-related triple in the
19948 // roster.
19949 for &a in STRICT_ORDER_ROSTER {
19950 for &b in STRICT_ORDER_ROSTER {
19951 for &c in STRICT_ORDER_ROSTER {
19952 if a.lt(b) && b.lt(c) {
19953 assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
19954 }
19955 }
19956 }
19957 }
19958 }
19959
19960 #[test]
19961 fn resource_limits_lt_refines_leq() {
19962 // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
19963 // a refinement of the non-strict one; a witness of strict `<`
19964 // is also a witness of `≤`. Sweeps every ordered pair in the
19965 // canonical preset roster.
19966 for &a in STRICT_ORDER_ROSTER {
19967 for &b in STRICT_ORDER_ROSTER {
19968 if a.lt(b) {
19969 assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
19970 }
19971 }
19972 }
19973 }
19974
19975 #[test]
19976 fn resource_limits_lt_agrees_with_leq_minus_equality() {
19977 // Antisymmetry consequence — on ANY antisymmetric partial
19978 // order (which the pointwise `≤` on `ResourceLimits` is),
19979 // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
19980 // antisymmetric-leq encoding against the leq-and-not-equal
19981 // encoding across the full 5×5 preset matrix; a regression to
19982 // a non-antisymmetric preorder encoding would fire here.
19983 for &a in STRICT_ORDER_ROSTER {
19984 for &b in STRICT_ORDER_ROSTER {
19985 let shipped = a.lt(b);
19986 let alt = a.leq(b) && a != b;
19987 assert_eq!(
19988 shipped, alt,
19989 "encoding cross-check failed on ({a:?}, {b:?})",
19990 );
19991 }
19992 }
19993 }
19994
19995 #[test]
19996 fn resource_limits_lt_of_bottom_diagonal_pinned() {
19997 // Concrete-preset pin — the strict chain across the (bottom,
19998 // middle, top) preset triple on the bounded-lattice diagonal.
19999 // Every `DEFAULT_MAX_*` module constant is a concrete positive
20000 // value strictly less than [`usize::MAX`] and strictly greater
20001 // than `0`, so on every axis the three endpoints are strictly
20002 // ordered; the six-axis pointwise conjunction lifts the
20003 // per-axis strict chain to the posture-level strict chain.
20004 //
20005 // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20006 // strict_order` one STRICTNESS axis over — where the non-
20007 // strict pin asserted "leq holds one way and not the other"
20008 // via two `leq` calls, this pin asserts the strict-chain
20009 // conclusion via one `lt` call per link and pinpoints the
20010 // three-preset chain on the bounded-lattice diagonal.
20011 assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20012 assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20013 assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20014 }
20015
20016 #[test]
20017 fn resource_limits_lt_rejects_incomparable_postures() {
20018 // Partial-order non-total pin — the two hand-authored
20019 // asymmetric postures have MID smaller on three axes and OTHER
20020 // smaller on the other three, so neither `leq` direction
20021 // holds; strict `<` therefore fails in BOTH directions. The
20022 // pin discriminates a regression that promoted strict `<`
20023 // from partial to total (e.g. treating incomparable pairs as
20024 // strictly `<` in some canonical direction), and pairs with
20025 // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20026 // one STRICTNESS axis over.
20027 assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20028 assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20029 }
20030
20031 #[test]
20032 fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20033 // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20034 // !b.leq(a))` at every pair by definition. Sibling of the
20035 // `leq_minus_equality` pin one ENCODING axis over: that pin
20036 // routes through the equality-based encoding (an
20037 // antisymmetry consequence), this pin routes through the
20038 // definitional antisymmetric-leq encoding. A regression that
20039 // shipped a different (e.g. axis-wise strict) encoding of
20040 // `lt` diverges from this cross-check on the incomparable
20041 // asymmetric-preset pair.
20042 for &a in STRICT_ORDER_ROSTER {
20043 for &b in STRICT_ORDER_ROSTER {
20044 let shipped = a.lt(b);
20045 let direct = a.leq(b) && !b.leq(a);
20046 assert_eq!(
20047 shipped, direct,
20048 "direct-encoding cross-check failed on ({a:?}, {b:?})",
20049 );
20050 }
20051 }
20052 }
20053
20054 #[test]
20055 fn resource_limits_gt_is_dual_of_lt() {
20056 // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20057 // shipped `gt` body delegates to `lt` verbatim so this
20058 // agreement holds definitionally, but the pin discriminates a
20059 // regression that re-implemented `gt` independently and
20060 // dropped the direction flip. Sweeps the full 5×5 preset
20061 // matrix so incomparable + strictly-ordered + equal pairs
20062 // are all exercised.
20063 for &a in STRICT_ORDER_ROSTER {
20064 for &b in STRICT_ORDER_ROSTER {
20065 assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20066 }
20067 }
20068 }
20069
20070 #[test]
20071 fn resource_limits_gt_of_top_diagonal_pinned() {
20072 // Concrete-preset pin — the strict chain across the (top,
20073 // middle, bottom) preset triple on the bounded-lattice
20074 // diagonal, walked in the "above" direction. Peer of
20075 // `resource_limits_lt_of_bottom_diagonal_pinned` one
20076 // DIRECTION axis over.
20077 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20078 assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20079 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20080 }
20081
20082 #[test]
20083 fn resource_limits_gt_is_irreflexive() {
20084 // Strict partial-order law dual — `a > a == false` for every
20085 // posture. Inherits from the irreflexivity of `lt` via the
20086 // shipped `gt` body's delegation, but pinned explicitly so a
20087 // future non-delegating re-implementation of `gt` still has
20088 // its irreflexivity gated at the type-level test cohort.
20089 for &a in STRICT_ORDER_ROSTER {
20090 assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20091 }
20092 }
20093
20094 #[test]
20095 fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20096 // Const-fn pin — the strict `<` relation is evaluable in
20097 // const context, so a caller can pin a preset-strict-order
20098 // identity at compile time. Sibling of the const-fn
20099 // evaluability pin on `leq` one STRICTNESS axis over.
20100 //
20101 // A regression to a runtime `fn` here would fail the `const
20102 // _: () = assert!(...)` bindings below at compile time.
20103 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20104 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20105 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20106 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20107 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20108 }
20109
20110 #[test]
20111 fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20112 // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20113 // DIRECTION axis over.
20114 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20115 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20116 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20117 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20118 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20119 }
20120
20121 #[test]
20122 fn resource_limits_geq_is_dual_of_leq() {
20123 // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20124 // The shipped `geq` body delegates to `leq` verbatim so this
20125 // agreement holds definitionally, but the pin discriminates a
20126 // regression that re-implemented `geq` independently and
20127 // dropped the direction flip. Sweeps the full 5×5 preset
20128 // matrix so incomparable + strictly-ordered + equal pairs are
20129 // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20130 // one STRICTNESS axis over on the (below, above) × (non-strict,
20131 // strict) 2×2 pairwise partial-order face.
20132 for &a in STRICT_ORDER_ROSTER {
20133 for &b in STRICT_ORDER_ROSTER {
20134 assert_eq!(
20135 a.geq(b),
20136 b.leq(a),
20137 "geq/leq duality failed on ({a:?}, {b:?})",
20138 );
20139 }
20140 }
20141 }
20142
20143 #[test]
20144 fn resource_limits_geq_is_reflexive() {
20145 // Non-strict partial-order law — `a ≥ a == true` for every
20146 // posture. Reflexivity is the non-strict-relation axiom peer of
20147 // irreflexivity on `gt` one STRICTNESS axis over on the
20148 // partial-order axiom surface; a regression that hardened the
20149 // delegation into a strict `!other.leq(self)` would fire here
20150 // on every preset since the reflexive pair collapses.
20151 for &a in STRICT_ORDER_ROSTER {
20152 assert!(a.geq(a), "reflexivity failed on {a:?}");
20153 }
20154 }
20155
20156 #[test]
20157 fn resource_limits_geq_is_antisymmetric() {
20158 // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20159 // Antisymmetry is inherited from `leq` via the shipped `geq`
20160 // body's `other.leq(self)` delegation (which combined with
20161 // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20162 // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20163 // on any regression that let two DISTINCT postures satisfy
20164 // `geq` in both directions.
20165 for &a in STRICT_ORDER_ROSTER {
20166 for &b in STRICT_ORDER_ROSTER {
20167 if a.geq(b) && b.geq(a) {
20168 assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20169 }
20170 }
20171 }
20172 }
20173
20174 #[test]
20175 fn resource_limits_geq_is_transitive() {
20176 // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20177 // Inherits from the transitivity of `leq` via the shipped
20178 // delegation; sweeps every ordered triple in the canonical
20179 // preset roster. Peer of `resource_limits_lt_is_transitive`
20180 // one STRICTNESS axis over and `resource_limits_leq_is_
20181 // transitive` one DIRECTION axis over.
20182 for &a in STRICT_ORDER_ROSTER {
20183 for &b in STRICT_ORDER_ROSTER {
20184 for &c in STRICT_ORDER_ROSTER {
20185 if a.geq(b) && b.geq(c) {
20186 assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20187 }
20188 }
20189 }
20190 }
20191 }
20192
20193 #[test]
20194 fn resource_limits_geq_refines_gt() {
20195 // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20196 // relation is a refinement of the non-strict one in the
20197 // "above" direction, exactly as `lt` refines `leq` in the
20198 // "below" direction (pinned by
20199 // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20200 // A witness of strict `>` is also a witness of `≥`.
20201 for &a in STRICT_ORDER_ROSTER {
20202 for &b in STRICT_ORDER_ROSTER {
20203 if a.gt(b) {
20204 assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20205 }
20206 }
20207 }
20208 }
20209
20210 #[test]
20211 fn resource_limits_geq_agrees_with_gt_plus_equality() {
20212 // Antisymmetry consequence peer — on ANY antisymmetric partial
20213 // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20214 // delegation against the strict-or-equal encoding across the
20215 // full 5×5 preset matrix; a regression that shipped a non-
20216 // antisymmetric preorder encoding for `geq` fires here. Peer
20217 // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20218 // DIRECTION-AND-STRICTNESS axis over.
20219 for &a in STRICT_ORDER_ROSTER {
20220 for &b in STRICT_ORDER_ROSTER {
20221 let shipped = a.geq(b);
20222 let alt = a.gt(b) || a == b;
20223 assert_eq!(
20224 shipped, alt,
20225 "encoding cross-check failed on ({a:?}, {b:?})",
20226 );
20227 }
20228 }
20229 }
20230
20231 #[test]
20232 fn resource_limits_geq_of_top_diagonal_pinned() {
20233 // Concrete-preset pin — the non-strict chain across the (top,
20234 // middle, bottom) preset triple on the bounded-lattice
20235 // diagonal, walked in the "above" direction. Peer of
20236 // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20237 // axis over: the non-strict pin also holds at the reflexive
20238 // diagonal, so every posture sits above ITSELF via `geq` in
20239 // addition to sitting above the strictly-tighter presets.
20240 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20241 assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20242 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20243 assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20244 assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20245 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20246 }
20247
20248 #[test]
20249 fn resource_limits_geq_rejects_incomparable_postures() {
20250 // Partial-order non-total pin — the two hand-authored
20251 // asymmetric postures have MID smaller on three axes and OTHER
20252 // smaller on the other three, so neither `leq` direction holds
20253 // and therefore neither `geq` direction holds. The pin
20254 // discriminates a regression that promoted `geq` from partial
20255 // to total, and pairs with
20256 // `resource_limits_lt_rejects_incomparable_postures` one
20257 // STRICTNESS-AND-DIRECTION axis over.
20258 assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20259 assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20260 }
20261
20262 #[test]
20263 fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20264 // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20265 // at every pair. The pairwise "above" relation is exactly the
20266 // 1-input specialization of the N-ary upper-bound predicate;
20267 // the pin cross-checks that the two named entries agree on
20268 // the single-element case, which discriminates a regression
20269 // that drifted one from the other. Peer of the single-element
20270 // identity docstring on `is_upper_bound_of` one ARITY axis
20271 // over.
20272 for &a in STRICT_ORDER_ROSTER {
20273 for &b in STRICT_ORDER_ROSTER {
20274 assert_eq!(
20275 a.geq(b),
20276 a.is_upper_bound_of(&[b]),
20277 "arity-reduction failed on ({a:?}, {b:?})",
20278 );
20279 }
20280 }
20281 }
20282
20283 #[test]
20284 fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20285 // Const-fn pin — the non-strict `≥` relation is evaluable in
20286 // const context, so a caller can pin a preset-non-strict-order
20287 // identity at compile time. Sibling of the const-fn evaluability
20288 // pins on `leq` one DIRECTION axis over AND on `gt` one
20289 // STRICTNESS axis over.
20290 //
20291 // A regression to a runtime `fn` here would fail the `const _:
20292 // () = assert!(...)` bindings below at compile time.
20293 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20294 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20295 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20296 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20297 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20298 }
20299
20300 #[test]
20301 fn resource_limits_is_incomparable_is_irreflexive() {
20302 // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20303 // false` on every posture. Every posture is COMPARABLE to
20304 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20305 // conjunction's `!self.leq(other)` leg forces the verdict to
20306 // `false` at every diagonal input. Discriminates a regression
20307 // that dropped the `!` on `self.leq(other)` (leaving the body
20308 // at `self.leq(other) && !self.geq(other)`) — that would fold
20309 // the diagonal to `false` on the reflexive `leq` arm but
20310 // silently flip the antichain corner on non-diagonal cells.
20311 for &a in STRICT_ORDER_ROSTER {
20312 assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20313 }
20314 }
20315
20316 #[test]
20317 fn resource_limits_is_incomparable_is_symmetric() {
20318 // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20319 // b.is_incomparable(a)` on every pair. The composition
20320 // `!self.leq(other) && !self.geq(other)` folds through the
20321 // `self.leq(other) == other.geq(self)` dual identity on `geq`
20322 // and the conjunction's commutativity into an order-
20323 // independent projection on its two inputs. Sweeps every
20324 // ordered pair in the canonical preset roster;
20325 // discriminates a regression that broke either leg's dual
20326 // identity — one direction would fold to `false` on cells the
20327 // other still folded to `true`.
20328 for &a in STRICT_ORDER_ROSTER {
20329 for &b in STRICT_ORDER_ROSTER {
20330 assert_eq!(
20331 a.is_incomparable(b),
20332 b.is_incomparable(a),
20333 "symmetry failed on ({a:?}, {b:?})",
20334 );
20335 }
20336 }
20337 }
20338
20339 #[test]
20340 fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20341 // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20342 // a.geq(b))` on every pair. The antichain corner is the SET-
20343 // COMPLEMENT of the union of the two pointwise-domination
20344 // directions on the (comparable, incomparable) two-cell
20345 // partition of the ordered pair × verdict surface. Sweeps
20346 // every ordered pair in the canonical preset roster;
20347 // discriminates a regression that drifted the antichain
20348 // characterization off the substrate's `!leq && !geq`
20349 // composition (e.g., a shortcut through `!self.leq(other) ||
20350 // !self.geq(other)` — the OR-form falsely reports
20351 // incomparability on non-equal comparable pairs).
20352 for &a in STRICT_ORDER_ROSTER {
20353 for &b in STRICT_ORDER_ROSTER {
20354 let via_named = a.is_incomparable(b);
20355 let via_composition = !(a.leq(b) || a.geq(b));
20356 assert_eq!(
20357 via_named, via_composition,
20358 "De Morgan dual failed on ({a:?}, {b:?})",
20359 );
20360 }
20361 }
20362 }
20363
20364 #[test]
20365 fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20366 // Two-cell partition pin — on every ordered pair EITHER
20367 // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20368 // both. The (comparable, incomparable) partition of the
20369 // ordered pair × verdict surface is exhaustive-and-disjoint —
20370 // a substrate-level EXCLUSIVITY THEOREM the projection pins
20371 // once. Sweeps every ordered pair in the canonical preset
20372 // roster; discriminates a regression that let the two cells
20373 // overlap (e.g., an implementation that reported the antichain
20374 // corner as `true` on the comparable diagonal).
20375 for &a in STRICT_ORDER_ROSTER {
20376 for &b in STRICT_ORDER_ROSTER {
20377 let comparable = a.leq(b) || a.geq(b);
20378 let incomparable = a.is_incomparable(b);
20379 assert!(
20380 comparable != incomparable,
20381 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20382 );
20383 }
20384 }
20385 }
20386
20387 #[test]
20388 fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20389 // Bottom-pole absorption pin — `a.is_incomparable(
20390 // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20391 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20392 // `EMPTY.leq(a)` holds on every axis (usize zero is the
20393 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20394 // dual identity, which falsifies the `!self.geq(other)` leg.
20395 // Sweeps the canonical preset roster; pairs with the top-
20396 // pole absorption sibling one POLE axis over.
20397 for &a in STRICT_ORDER_ROSTER {
20398 assert!(
20399 !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20400 "bottom-pole absorption failed on {a:?}",
20401 );
20402 assert!(
20403 !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20404 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20405 );
20406 }
20407 }
20408
20409 #[test]
20410 fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20411 // Top-pole absorption pin — `a.is_incomparable(
20412 // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20413 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20414 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20415 // usize::MAX` unconditionally), which falsifies the
20416 // `!self.leq(other)` leg. Sweeps the canonical preset
20417 // roster; pairs with the bottom-pole absorption sibling one
20418 // POLE axis over.
20419 for &a in STRICT_ORDER_ROSTER {
20420 assert!(
20421 !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20422 "top-pole absorption failed on {a:?}",
20423 );
20424 assert!(
20425 !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20426 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20427 );
20428 }
20429 }
20430
20431 #[test]
20432 fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20433 // Antichain load-bearing arm — the two hand-authored
20434 // asymmetric postures sit on distinct branches (each is
20435 // smaller on three axes and larger on the other three), so
20436 // neither pointwise-domination direction closes and the
20437 // antichain verdict HOLDS in both orderings. Pairs with
20438 // `resource_limits_geq_rejects_incomparable_postures` +
20439 // `resource_limits_lt_rejects_incomparable_postures` one COVER
20440 // axis over: those pin the two comparable-direction predicates
20441 // FALSIFY on this pair; THIS pin confirms the antichain
20442 // verdict HOLDS on the SAME pair — the two are the negation-
20443 // paired sides of the (comparable, incomparable) two-cell
20444 // partition.
20445 assert!(
20446 HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20447 "antichain arm failed on (MID, OTHER)",
20448 );
20449 assert!(
20450 HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20451 "antichain arm failed on flipped (OTHER, MID)",
20452 );
20453 }
20454
20455 #[test]
20456 fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20457 // Shipped preset comparability pin — every ordered pair drawn
20458 // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20459 // is COMPARABLE (in both directions the pairwise partial-order
20460 // relation gives a verdict). The three shipped presets form a
20461 // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20462 // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20463 // corner MUST fold to `false` on every ordered pair drawn from
20464 // them. Discriminates a regression that promoted a shipped
20465 // preset off the total-order chain by drifting ONE field
20466 // silently.
20467 const TRIANGLE: &[ResourceLimits] = &[
20468 EMPTY_RESOURCE_LIMITS,
20469 DEFAULT_RESOURCE_LIMITS,
20470 UNBOUNDED_RESOURCE_LIMITS,
20471 ];
20472 for &a in TRIANGLE {
20473 for &b in TRIANGLE {
20474 assert!(
20475 !a.is_incomparable(b),
20476 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20477 );
20478 }
20479 }
20480 }
20481
20482 #[test]
20483 fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20484 // Const-fn pin — the antichain characterization is evaluable
20485 // in const context, so a caller can pin an antichain-
20486 // disagreement identity at compile time. Sibling of the
20487 // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20488 // one COVER axis over on the pairwise-relation surface.
20489 //
20490 // A regression to a runtime `fn` here would fail the `const _:
20491 // () = assert!(...)` bindings below at compile time.
20492 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20493 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20494 const _: () =
20495 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20496 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20497 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20498 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20499 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20500 }
20501
20502 // ── ResourceLimits::is_comparable ─────────────────────────────────
20503 //
20504 // The COMPARABLE CORNER on the pairwise-relation surface — the
20505 // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20506 // axis over on the (comparable, incomparable) two-cell partition
20507 // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20508 // for the algebra; the pins below fix each documented property
20509 // exactly once so a regression on any leg fires on a NAMED test
20510 // rather than an ad-hoc downstream break.
20511
20512 #[test]
20513 fn resource_limits_is_comparable_is_reflexive() {
20514 // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20515 // true` on every posture. Every posture is COMPARABLE to
20516 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20517 // the disjunction's `self.leq(other)` leg holds at every
20518 // diagonal input. Sibling of `is_incomparable`'s
20519 // irreflexivity one CELL axis over on the (comparable,
20520 // incomparable) partition — the diagonal MUST fold to `true`
20521 // here iff it folds to `false` there.
20522 for &a in STRICT_ORDER_ROSTER {
20523 assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20524 }
20525 }
20526
20527 #[test]
20528 fn resource_limits_is_comparable_is_symmetric() {
20529 // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20530 // b.is_comparable(a)` on every pair. The composition
20531 // `self.leq(other) || self.geq(other)` folds through the
20532 // `self.leq(other) == other.geq(self)` dual identity on
20533 // `geq` and the disjunction's commutativity into an order-
20534 // independent projection on its two inputs. Both cells of an
20535 // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20536 // must themselves be symmetric — the two verdicts flip in
20537 // lockstep on argument swap.
20538 for &a in STRICT_ORDER_ROSTER {
20539 for &b in STRICT_ORDER_ROSTER {
20540 assert_eq!(
20541 a.is_comparable(b),
20542 b.is_comparable(a),
20543 "symmetry failed on ({a:?}, {b:?})",
20544 );
20545 }
20546 }
20547 }
20548
20549 #[test]
20550 fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20551 // De Morgan dual pin — `a.is_comparable(b) ==
20552 // !a.is_incomparable(b)` on every pair. The comparable corner
20553 // is the SET-COMPLEMENT of the antichain corner on the
20554 // (comparable, incomparable) two-cell partition of the
20555 // ordered pair × verdict surface. The DIRECT dual of
20556 // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20557 // one CELL axis over: that test pins the antichain corner as
20558 // the negation of the `leq || geq` disjunction; THIS test
20559 // pins the comparability corner as the negation of the
20560 // antichain projection. Together the two pin BOTH SIDES of
20561 // the partition, closing it as a substrate-level
20562 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20563 for &a in STRICT_ORDER_ROSTER {
20564 for &b in STRICT_ORDER_ROSTER {
20565 let via_named = a.is_comparable(b);
20566 let via_negated_dual = !a.is_incomparable(b);
20567 assert_eq!(
20568 via_named, via_negated_dual,
20569 "De Morgan dual failed on ({a:?}, {b:?})",
20570 );
20571 }
20572 }
20573 }
20574
20575 #[test]
20576 fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20577 // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20578 // a.geq(b)` on every pair. Discriminates a regression that
20579 // drifted the comparability body off the substrate's `leq ||
20580 // geq` composition (e.g., a shortcut through `a.leq(b) &&
20581 // a.geq(b)` — the AND-form falsely reports comparability
20582 // only on the equality diagonal, dropping the strict-order
20583 // cells). Anchors the two-primitive delegation as a
20584 // substrate-level identity.
20585 for &a in STRICT_ORDER_ROSTER {
20586 for &b in STRICT_ORDER_ROSTER {
20587 let via_named = a.is_comparable(b);
20588 let via_composition = a.leq(b) || a.geq(b);
20589 assert_eq!(
20590 via_named, via_composition,
20591 "leq||geq composition failed on ({a:?}, {b:?})",
20592 );
20593 }
20594 }
20595 }
20596
20597 #[test]
20598 fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20599 // Bottom-pole absorption pin — `a.is_comparable(
20600 // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20601 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20602 // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20603 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20604 // dual identity, which satisfies the `self.geq(other)` leg.
20605 // The DIRECT COMPLEMENT of
20606 // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20607 // one CELL axis over on the SAME preset roster — where the
20608 // antichain verdict FOLDS FALSE at the bottom pole, the
20609 // comparability verdict FOLDS TRUE.
20610 for &a in STRICT_ORDER_ROSTER {
20611 assert!(
20612 a.is_comparable(EMPTY_RESOURCE_LIMITS),
20613 "bottom-pole absorption failed on {a:?}",
20614 );
20615 assert!(
20616 EMPTY_RESOURCE_LIMITS.is_comparable(a),
20617 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20618 );
20619 }
20620 }
20621
20622 #[test]
20623 fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20624 // Top-pole absorption pin — `a.is_comparable(
20625 // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20626 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20627 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20628 // usize::MAX` unconditionally), which satisfies the
20629 // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20630 // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20631 // one CELL axis over on the SAME preset roster.
20632 for &a in STRICT_ORDER_ROSTER {
20633 assert!(
20634 a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20635 "top-pole absorption failed on {a:?}",
20636 );
20637 assert!(
20638 UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20639 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20640 );
20641 }
20642 }
20643
20644 #[test]
20645 fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20646 // Antichain load-bearing arm — the two hand-authored
20647 // asymmetric postures sit on distinct branches (each is
20648 // smaller on three axes and larger on the other three), so
20649 // NEITHER pointwise-domination direction closes and the
20650 // comparability verdict FALSIFIES in both orderings. The
20651 // DIRECT NEGATION of
20652 // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20653 // one CELL axis over on the SAME hand-authored pair — the
20654 // two are the negation-paired sides of the (comparable,
20655 // incomparable) two-cell partition, and pinning BOTH cells'
20656 // verdicts on the SAME load-bearing antichain pair closes
20657 // the partition as a substrate-level EXCLUSIVITY THEOREM.
20658 assert!(
20659 !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20660 "antichain arm failed on (MID, OTHER)",
20661 );
20662 assert!(
20663 !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20664 "antichain arm failed on flipped (OTHER, MID)",
20665 );
20666 }
20667
20668 #[test]
20669 fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20670 // Shipped preset comparability pin — every ordered pair
20671 // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20672 // triangle is COMPARABLE (in at least one direction the
20673 // pairwise partial-order relation gives a verdict). The
20674 // three shipped presets form a TOTALLY ORDERED chain on the
20675 // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20676 // every field), so the comparability corner MUST fold to
20677 // `true` on every ordered pair drawn from them. The DIRECT
20678 // COMPLEMENT of
20679 // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20680 // one CELL axis over on the SAME triangle. Discriminates a
20681 // regression that promoted a shipped preset off the total-
20682 // order chain by drifting ONE field silently.
20683 const TRIANGLE: &[ResourceLimits] = &[
20684 EMPTY_RESOURCE_LIMITS,
20685 DEFAULT_RESOURCE_LIMITS,
20686 UNBOUNDED_RESOURCE_LIMITS,
20687 ];
20688 for &a in TRIANGLE {
20689 for &b in TRIANGLE {
20690 assert!(
20691 a.is_comparable(b),
20692 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20693 );
20694 }
20695 }
20696 }
20697
20698 #[test]
20699 fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20700 // Exhaustive-and-disjoint partition pin — on every ordered
20701 // pair EXACTLY ONE of `a.is_comparable(b)` OR
20702 // `a.is_incomparable(b)` holds. The (comparable,
20703 // incomparable) partition of the ordered pair × verdict
20704 // surface is exhaustive-and-disjoint — a substrate-level
20705 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20706 // together pin. Complements
20707 // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20708 // one CELL axis over: that test pins the partition against
20709 // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20710 // the partition against the NAMED comparability projection —
20711 // a regression that drifted `is_comparable` off the
20712 // composition without breaking the composition-based test
20713 // would still fire here.
20714 for &a in STRICT_ORDER_ROSTER {
20715 for &b in STRICT_ORDER_ROSTER {
20716 let comparable = a.is_comparable(b);
20717 let incomparable = a.is_incomparable(b);
20718 assert!(
20719 comparable != incomparable,
20720 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20721 );
20722 }
20723 }
20724 }
20725
20726 #[test]
20727 fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20728 // Const-fn pin — the comparability characterization is
20729 // evaluable in const context, so a caller can pin a
20730 // comparability-agreement identity at compile time. Sibling
20731 // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20732 // / `gt` / `is_incomparable` one COVER axis over on the
20733 // pairwise-relation surface.
20734 //
20735 // A regression to a runtime `fn` here would fail the `const _:
20736 // () = assert!(...)` bindings below at compile time.
20737 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20738 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20739 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20740 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20741 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20742 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20743 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20744 }
20745
20746 // ── ResourceLimits::partial_cmp ───────────────────────────────────
20747 //
20748 // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20749 // relation surface — the JOINT LIFT of the (comparable, incomparable)
20750 // two-cell partition together with the ordering-direction refinement
20751 // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20752 // algebra; the pins below fix each documented property exactly once
20753 // so a regression on any leg fires on a NAMED test rather than an
20754 // ad-hoc downstream break.
20755
20756 #[test]
20757 fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20758 // Diagonal-equality pin — `a.partial_cmp(a) ==
20759 // Some(Ordering::Equal)` on every posture. The `leq`
20760 // reflexivity closes both arms of the equality-guarding
20761 // conjunction, so the diagonal cell of the (Less, Equal,
20762 // Greater, None) 4-tile partition pins to the Equal tile.
20763 // Sibling of `is_comparable_is_reflexive` one PROJECTION
20764 // axis over — where the projection folds TRUE at the
20765 // diagonal, THIS method refines the verdict to `Equal`.
20766 for &a in STRICT_ORDER_ROSTER {
20767 assert_eq!(
20768 a.partial_cmp(a),
20769 Some(Ordering::Equal),
20770 "diagonal-equality failed on {a:?}",
20771 );
20772 }
20773 }
20774
20775 #[test]
20776 fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20777 // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20778 // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20779 // Swapping the argument pair FLIPS the ordering-direction
20780 // verdict (`Less <-> Greater`), fixes the diagonal-equality
20781 // arm, and fixes the antichain arm. Discriminates
20782 // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20783 // argument swap) one POSTURE axis over — the FULL verdict
20784 // carries directional content that inverts under swap where
20785 // the projections do not.
20786 for &a in STRICT_ORDER_ROSTER {
20787 for &b in STRICT_ORDER_ROSTER {
20788 let forward = a.partial_cmp(b);
20789 let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20790 assert_eq!(
20791 forward, swapped_reversed,
20792 "swap-reverse failed on ({a:?}, {b:?})",
20793 );
20794 }
20795 }
20796 }
20797
20798 #[test]
20799 fn resource_limits_partial_cmp_none_iff_incomparable() {
20800 // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20801 // == a.is_incomparable(b)` on every pair. The `None` arm of
20802 // the return coincides EXACTLY with the antichain-corner
20803 // projection. Anchors the joint-lift claim: THIS method
20804 // subsumes `is_incomparable` at its `is_none()` projection
20805 // without drifting the antichain characterization.
20806 for &a in STRICT_ORDER_ROSTER {
20807 for &b in STRICT_ORDER_ROSTER {
20808 let via_partial_cmp = a.partial_cmp(b).is_none();
20809 let via_is_incomparable = a.is_incomparable(b);
20810 assert_eq!(
20811 via_partial_cmp, via_is_incomparable,
20812 "none-iff-incomparable failed on ({a:?}, {b:?})",
20813 );
20814 }
20815 }
20816 }
20817
20818 #[test]
20819 fn resource_limits_partial_cmp_some_iff_comparable() {
20820 // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20821 // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20822 // the return coincides EXACTLY with the comparability-corner
20823 // projection. The DUAL of the antichain-arm pin one CELL axis
20824 // over on the (comparable, incomparable) partition — together
20825 // the two pins close the JOINT LIFT of both cells at THIS
20826 // method.
20827 for &a in STRICT_ORDER_ROSTER {
20828 for &b in STRICT_ORDER_ROSTER {
20829 let via_partial_cmp = a.partial_cmp(b).is_some();
20830 let via_is_comparable = a.is_comparable(b);
20831 assert_eq!(
20832 via_partial_cmp, via_is_comparable,
20833 "some-iff-comparable failed on ({a:?}, {b:?})",
20834 );
20835 }
20836 }
20837 }
20838
20839 #[test]
20840 fn resource_limits_partial_cmp_less_iff_lt() {
20841 // Strict-less agreement pin — `a.partial_cmp(b) ==
20842 // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20843 // the (Less, Equal, Greater, None) 4-tile partition binds to
20844 // the named `lt` companion primitive at exactly one dispatch
20845 // site. A regression that drifted the arm off `lt` (e.g., to
20846 // `leq` — including the equality diagonal) would fire here.
20847 for &a in STRICT_ORDER_ROSTER {
20848 for &b in STRICT_ORDER_ROSTER {
20849 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20850 let via_lt = a.lt(b);
20851 assert_eq!(
20852 via_partial_cmp, via_lt,
20853 "less-iff-lt failed on ({a:?}, {b:?})",
20854 );
20855 }
20856 }
20857 }
20858
20859 #[test]
20860 fn resource_limits_partial_cmp_greater_iff_gt() {
20861 // Strict-greater agreement pin — `a.partial_cmp(b) ==
20862 // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20863 // less agreement pin one DIRECTION axis over on the (Less,
20864 // Greater) strict-order pair — a regression that dropped the
20865 // asymmetry between the two arms (e.g., collapsed both to
20866 // `Less`) would fire here.
20867 for &a in STRICT_ORDER_ROSTER {
20868 for &b in STRICT_ORDER_ROSTER {
20869 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20870 let via_gt = a.gt(b);
20871 assert_eq!(
20872 via_partial_cmp, via_gt,
20873 "greater-iff-gt failed on ({a:?}, {b:?})",
20874 );
20875 }
20876 }
20877 }
20878
20879 #[test]
20880 fn resource_limits_partial_cmp_equal_iff_eq() {
20881 // Equality agreement pin — `a.partial_cmp(b) ==
20882 // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20883 // 4-tile partition binds through the pointwise antisymmetric-
20884 // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20885 // pinned by `resource_limits_leq_is_antisymmetric`) to the
20886 // `PartialEq::eq` companion. Discriminates a regression that
20887 // drifted the Equal arm off the antisymmetric conjunction.
20888 for &a in STRICT_ORDER_ROSTER {
20889 for &b in STRICT_ORDER_ROSTER {
20890 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20891 let via_eq = a == b;
20892 assert_eq!(
20893 via_partial_cmp, via_eq,
20894 "equal-iff-eq failed on ({a:?}, {b:?})",
20895 );
20896 }
20897 }
20898 }
20899
20900 #[test]
20901 fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20902 // Bottom-to-top-pole ordering pin — every strictly-ascending
20903 // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20904 // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20905 // shipped presets form a TOTALLY ORDERED chain on the
20906 // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20907 // positive value strictly less than usize::MAX), so the
20908 // strict-less arm fires on every ordered pair drawn in
20909 // ascending direction.
20910 assert_eq!(
20911 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20912 Some(Ordering::Less),
20913 );
20914 assert_eq!(
20915 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20916 Some(Ordering::Less),
20917 );
20918 assert_eq!(
20919 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20920 Some(Ordering::Less),
20921 );
20922 }
20923
20924 #[test]
20925 fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
20926 // Top-to-bottom-pole ordering pin — every strictly-descending
20927 // pair on the shipped preset chain folds to
20928 // `Some(Ordering::Greater)`. The DUAL of the ascending pin
20929 // one DIRECTION axis over — together the two pins close the
20930 // (ascending, descending) direction pair on the shipped
20931 // preset chain at the ordering-direction refinement.
20932 assert_eq!(
20933 DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20934 Some(Ordering::Greater),
20935 );
20936 assert_eq!(
20937 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20938 Some(Ordering::Greater),
20939 );
20940 assert_eq!(
20941 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20942 Some(Ordering::Greater),
20943 );
20944 }
20945
20946 #[test]
20947 fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
20948 // Antichain load-bearing arm — the two hand-authored
20949 // asymmetric postures sit on distinct branches (each is
20950 // smaller on three axes and larger on the other three), so
20951 // NEITHER pointwise-domination direction closes and the
20952 // fall-through `None` arm fires in both orderings. The DIRECT
20953 // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
20954 // one PROJECTION axis over on the SAME hand-authored pair —
20955 // where the projection folds TRUE, THIS method folds to
20956 // `None`. The two are the negation-paired sides of the
20957 // (Some(_), None) two-cell partition of `Option<Ordering>`.
20958 assert_eq!(
20959 HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
20960 None,
20961 );
20962 assert_eq!(
20963 HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
20964 None,
20965 );
20966 }
20967
20968 #[test]
20969 fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
20970 // Const-fn pin — the full-verdict projection is evaluable in
20971 // const context, so a caller can pin an ordering-direction
20972 // identity at compile time. Sibling of the const-fn
20973 // evaluability pins on `leq` / `geq` / `lt` / `gt` /
20974 // `is_incomparable` / `is_comparable` one COVER axis over on
20975 // the pairwise-relation surface.
20976 //
20977 // `matches!` in const context requires the pattern's variants
20978 // to be const-constructible — `Ordering::Less` / `Equal` /
20979 // `Greater` are plain enum constructors, so the assertions
20980 // below stay inside stable const-fn scope. A regression to a
20981 // runtime `fn` here would fail the `const _: () = assert!(
20982 // matches!(...))` bindings below at compile time.
20983 const _: () = assert!(matches!(
20984 EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20985 Some(Ordering::Equal),
20986 ));
20987 const _: () = assert!(matches!(
20988 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20989 Some(Ordering::Less),
20990 ));
20991 const _: () = assert!(matches!(
20992 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20993 Some(Ordering::Greater),
20994 ));
20995 const _: () = assert!(matches!(
20996 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20997 Some(Ordering::Less),
20998 ));
20999 const _: () = assert!(matches!(
21000 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21001 Some(Ordering::Less),
21002 ));
21003 }
21004
21005 #[test]
21006 fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21007 // Empty-slice vacuous truth — the empty conjunction is vacuously
21008 // true; the empty slice contains no distinct pairs to reject.
21009 // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21010 // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21011 // (pairwise, N-ary) primitive surface.
21012 assert!(ResourceLimits::is_chain(&[]));
21013 }
21014
21015 #[test]
21016 fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21017 // Empty-slice vacuous truth — the empty conjunction is vacuously
21018 // true; the empty slice contains no distinct pairs to reject.
21019 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21020 // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21021 // set-level pair AGREE at the empty slice on the vacuous-truth
21022 // verdict — the two cells coincide at the empty face of the
21023 // set-level verdict surface.
21024 assert!(ResourceLimits::is_antichain(&[]));
21025 }
21026
21027 #[test]
21028 fn resource_limits_is_chain_singleton_is_vacuously_true() {
21029 // Singleton vacuous truth — a one-element slice contains no
21030 // distinct pairs, so the outer-and-inner-index conjunction never
21031 // enters the inner loop and returns `true` vacuously. Pinned on
21032 // every canonical preset AND on both hand-authored asymmetric
21033 // postures so the vacuous-truth verdict holds regardless of the
21034 // singleton element's lattice position.
21035 assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21036 assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21037 assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21038 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21039 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21040 }
21041
21042 #[test]
21043 fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21044 // Singleton vacuous truth — a one-element slice contains no
21045 // distinct pairs, so the outer-and-inner-index conjunction never
21046 // enters the inner loop and returns `true` vacuously. Peer of
21047 // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21048 // at singleton slices — the (chain, antichain) verdict surface
21049 // coincides at every slice with strictly fewer than two distinct
21050 // pairs.
21051 assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21052 assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21053 assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21054 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21055 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21056 }
21057
21058 #[test]
21059 fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21060 // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21061 // (`a.is_comparable(a) == true`), so the single distinct index
21062 // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21063 // conjunction holds at every diagonal-duplicate slice.
21064 // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21065 // over, whose diagonal-duplicate verdict is `false` via
21066 // `is_incomparable`'s IRREFLEXIVITY.
21067 assert!(ResourceLimits::is_chain(&[
21068 DEFAULT_RESOURCE_LIMITS,
21069 DEFAULT_RESOURCE_LIMITS,
21070 ]));
21071 assert!(ResourceLimits::is_chain(&[
21072 HAND_AUTHORED_MID_POSTURE,
21073 HAND_AUTHORED_MID_POSTURE,
21074 ]));
21075 }
21076
21077 #[test]
21078 fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21079 // Diagonal-duplicate rejection — `is_incomparable` is
21080 // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21081 // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21082 // false` and the conjunction rejects at every diagonal-duplicate
21083 // slice. The reflexivity/irreflexivity divergence between the
21084 // two pair-level projections PROPAGATES to the set-level
21085 // projections at any slice with a duplicated element — the two
21086 // set-level cells DIVERGE at every diagonal-duplicate slice, the
21087 // mirror of their AGREEMENT at every empty-or-singleton slice
21088 // one CARDINALITY axis over.
21089 assert!(!ResourceLimits::is_antichain(&[
21090 DEFAULT_RESOURCE_LIMITS,
21091 DEFAULT_RESOURCE_LIMITS,
21092 ]));
21093 assert!(!ResourceLimits::is_antichain(&[
21094 HAND_AUTHORED_MID_POSTURE,
21095 HAND_AUTHORED_MID_POSTURE,
21096 ]));
21097 }
21098
21099 #[test]
21100 fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21101 // Ordered-chain closure — the shipped-preset triple on the
21102 // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21103 // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21104 // the three is comparable. Pinned in every permutation of the
21105 // triple so the doubly-indexed all-pairs walk closes on every
21106 // enumeration of the same chain.
21107 assert!(ResourceLimits::is_chain(&[
21108 EMPTY_RESOURCE_LIMITS,
21109 DEFAULT_RESOURCE_LIMITS,
21110 UNBOUNDED_RESOURCE_LIMITS,
21111 ]));
21112 assert!(ResourceLimits::is_chain(&[
21113 UNBOUNDED_RESOURCE_LIMITS,
21114 DEFAULT_RESOURCE_LIMITS,
21115 EMPTY_RESOURCE_LIMITS,
21116 ]));
21117 assert!(ResourceLimits::is_chain(&[
21118 DEFAULT_RESOURCE_LIMITS,
21119 EMPTY_RESOURCE_LIMITS,
21120 UNBOUNDED_RESOURCE_LIMITS,
21121 ]));
21122 }
21123
21124 #[test]
21125 fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21126 // Chain rejection — the shipped-preset pair on the bounded-
21127 // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21128 // the single distinct pair is COMPARABLE and the antichain
21129 // conjunction rejects. The DIRECT FALSIFICATION of
21130 // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21131 // over on the SAME lattice diagonal — the two set-level cells
21132 // carry the negation-paired verdict at every strictly-chained
21133 // slice, the mirror of their AGREEMENT at every empty-or-
21134 // singleton slice one CARDINALITY axis over.
21135 assert!(!ResourceLimits::is_antichain(&[
21136 EMPTY_RESOURCE_LIMITS,
21137 DEFAULT_RESOURCE_LIMITS,
21138 ]));
21139 assert!(!ResourceLimits::is_antichain(&[
21140 DEFAULT_RESOURCE_LIMITS,
21141 UNBOUNDED_RESOURCE_LIMITS,
21142 ]));
21143 assert!(!ResourceLimits::is_antichain(&[
21144 EMPTY_RESOURCE_LIMITS,
21145 DEFAULT_RESOURCE_LIMITS,
21146 UNBOUNDED_RESOURCE_LIMITS,
21147 ]));
21148 }
21149
21150 #[test]
21151 fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21152 // Antichain rejection — the two hand-authored asymmetric
21153 // postures sit on distinct branches (each is smaller on three
21154 // axes and larger on the other three), so neither pointwise-
21155 // domination direction closes and the pair is INCOMPARABLE. The
21156 // chain conjunction rejects at the first incomparable pair.
21157 // Pinned in both orderings so the doubly-indexed pair walk
21158 // rejects regardless of index order (the diagonal-adjacent
21159 // pair-level rejection is symmetric on argument swap).
21160 assert!(!ResourceLimits::is_chain(&[
21161 HAND_AUTHORED_MID_POSTURE,
21162 HAND_AUTHORED_OTHER_POSTURE,
21163 ]));
21164 assert!(!ResourceLimits::is_chain(&[
21165 HAND_AUTHORED_OTHER_POSTURE,
21166 HAND_AUTHORED_MID_POSTURE,
21167 ]));
21168 }
21169
21170 #[test]
21171 fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21172 // Hand-authored antichain closure — the two hand-authored
21173 // asymmetric postures sit on distinct branches (each is smaller
21174 // on three axes and larger on the other three), so neither
21175 // pointwise-domination direction closes and the pair is
21176 // INCOMPARABLE. The antichain conjunction accepts every distinct
21177 // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21178 // rejection one CELL axis over on the SAME hand-authored pair —
21179 // pinning BOTH cells' verdicts on the SAME load-bearing
21180 // antichain pair closes the (chain, antichain) two-cell face on
21181 // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21182 assert!(ResourceLimits::is_antichain(&[
21183 HAND_AUTHORED_MID_POSTURE,
21184 HAND_AUTHORED_OTHER_POSTURE,
21185 ]));
21186 assert!(ResourceLimits::is_antichain(&[
21187 HAND_AUTHORED_OTHER_POSTURE,
21188 HAND_AUTHORED_MID_POSTURE,
21189 ]));
21190 }
21191
21192 #[test]
21193 fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21194 // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21195 // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21196 // per-axis values are all much larger than either hand-authored
21197 // posture's, so DEFAULT dominates both pointwise), but the
21198 // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21199 // The set-level chain verdict is stricter than the union of its
21200 // pair-level verdicts — one antichain pair anywhere in the
21201 // slice falsifies the whole projection.
21202 assert!(!ResourceLimits::is_chain(&[
21203 DEFAULT_RESOURCE_LIMITS,
21204 HAND_AUTHORED_MID_POSTURE,
21205 HAND_AUTHORED_OTHER_POSTURE,
21206 ]));
21207 }
21208
21209 #[test]
21210 fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21211 // Mixed-set rejection — even though the second-and-third pair
21212 // is incomparable (the hand-authored antichain pair), the first
21213 // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21214 // conjunction rejects. Pins the three-cell (chain, antichain,
21215 // mixed) set-level face at the mixed-set cell: the same slice
21216 // both `is_chain` AND `is_antichain` reject at, and the third
21217 // cell of the three-cell face the two set-level projections
21218 // carry the ends of.
21219 assert!(!ResourceLimits::is_antichain(&[
21220 DEFAULT_RESOURCE_LIMITS,
21221 HAND_AUTHORED_MID_POSTURE,
21222 HAND_AUTHORED_OTHER_POSTURE,
21223 ]));
21224 }
21225
21226 #[test]
21227 fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21228 // De Morgan mirror at arity 2 — on any two-element slice `[a,
21229 // b]` with `a != b`, the set-level (chain, antichain) two-cell
21230 // face reduces to the pair-level (comparable, incomparable)
21231 // two-cell partition and the two cells become MUTUALLY
21232 // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21233 // set-level De Morgan mirror of the pair-level identity
21234 // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21235 // pins one CARDINALITY axis over.
21236 let presets: [ResourceLimits; 5] = [
21237 EMPTY_RESOURCE_LIMITS,
21238 DEFAULT_RESOURCE_LIMITS,
21239 UNBOUNDED_RESOURCE_LIMITS,
21240 HAND_AUTHORED_MID_POSTURE,
21241 HAND_AUTHORED_OTHER_POSTURE,
21242 ];
21243 for a in presets {
21244 for b in presets {
21245 if a == b {
21246 continue;
21247 }
21248 let pair = [a, b];
21249 let chain = ResourceLimits::is_chain(&pair);
21250 let antichain = ResourceLimits::is_antichain(&pair);
21251 assert_ne!(
21252 chain, antichain,
21253 "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21254 );
21255 }
21256 }
21257 }
21258
21259 #[test]
21260 fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21261 // Empty-and-singleton agreement — the two set-level projections
21262 // carry the SAME vacuous-truth verdict at every slice with
21263 // strictly fewer than two distinct pairs (empty AND singleton).
21264 // Pinned as a substrate-level identity so a future rewrite of
21265 // either projection cannot silently drift from the shared
21266 // vacuous-truth verdict.
21267 assert_eq!(
21268 ResourceLimits::is_chain(&[]),
21269 ResourceLimits::is_antichain(&[]),
21270 );
21271 let presets: [ResourceLimits; 5] = [
21272 EMPTY_RESOURCE_LIMITS,
21273 DEFAULT_RESOURCE_LIMITS,
21274 UNBOUNDED_RESOURCE_LIMITS,
21275 HAND_AUTHORED_MID_POSTURE,
21276 HAND_AUTHORED_OTHER_POSTURE,
21277 ];
21278 for a in presets {
21279 let single = [a];
21280 assert_eq!(
21281 ResourceLimits::is_chain(&single),
21282 ResourceLimits::is_antichain(&single),
21283 "singleton agreement failed on {a:?}",
21284 );
21285 }
21286 }
21287
21288 #[test]
21289 fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21290 // Const-fn pin — the set-level chain projection is evaluable in
21291 // const context, so a caller can pin a chain-membership
21292 // identity at compile time. Sibling of the const-fn
21293 // evaluability pins on `is_comparable` one CARDINALITY axis
21294 // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21295 // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21296 const _: () = assert!(ResourceLimits::is_chain(&[]));
21297 const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21298 const _: () = assert!(ResourceLimits::is_chain(&[
21299 EMPTY_RESOURCE_LIMITS,
21300 DEFAULT_RESOURCE_LIMITS,
21301 UNBOUNDED_RESOURCE_LIMITS,
21302 ]));
21303 }
21304
21305 #[test]
21306 fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21307 // Const-fn pin — the set-level antichain projection is
21308 // evaluable in const context, so a caller can pin an antichain-
21309 // membership identity at compile time. Sibling of the const-fn
21310 // evaluability pins on `is_incomparable` one CARDINALITY axis
21311 // over and on `is_chain` one COVER-COMPLEMENT axis over.
21312 const _: () = assert!(ResourceLimits::is_antichain(&[]));
21313 const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21314 const _: () = assert!(ResourceLimits::is_antichain(&[
21315 HAND_AUTHORED_MID_POSTURE,
21316 HAND_AUTHORED_OTHER_POSTURE,
21317 ]));
21318 }
21319
21320 #[test]
21321 fn resource_limits_is_mixed_empty_slice_is_false() {
21322 // Empty-slice contract — is_chain and is_antichain BOTH return
21323 // `true` vacuously, so `!true && !true == false`. The empty
21324 // slice is NOT mixed. Sibling posture to the vacuous-truth
21325 // verdicts of is_chain + is_antichain one CELL axis over on the
21326 // (chain, antichain, mixed) three-cell face.
21327 assert!(!ResourceLimits::is_mixed(&[]));
21328 }
21329
21330 #[test]
21331 fn resource_limits_is_mixed_singleton_is_false() {
21332 // Singleton contract — a one-element slice contains no distinct
21333 // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21334 // Pinned on every canonical preset AND on both hand-authored
21335 // asymmetric postures so the false-at-singleton verdict holds
21336 // regardless of the singleton element's lattice position.
21337 assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21338 assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21339 assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21340 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21341 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21342 }
21343
21344 #[test]
21345 fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21346 // Diagonal-duplicate contract — is_chain returns `true` via
21347 // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21348 // is_incomparable's IRREFLEXIVITY; the conjunction
21349 // `!true && !false == false`. The diagonal-duplicate slice is a
21350 // CHAIN (per the reflexivity arm), not mixed.
21351 assert!(!ResourceLimits::is_mixed(&[
21352 DEFAULT_RESOURCE_LIMITS,
21353 DEFAULT_RESOURCE_LIMITS,
21354 ]));
21355 assert!(!ResourceLimits::is_mixed(&[
21356 HAND_AUTHORED_MID_POSTURE,
21357 HAND_AUTHORED_MID_POSTURE,
21358 ]));
21359 }
21360
21361 #[test]
21362 fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21363 // Chain rejection — the shipped-preset triple on the bounded-
21364 // lattice diagonal is an ascending chain, is_chain returns
21365 // `true`, and `!true && …` short-circuits to `false`. A PURE
21366 // chain is not mixed. Pinned in every permutation of the triple
21367 // so the projection factors through the ordering-agnostic
21368 // sibling is_chain verdict on the same chain.
21369 assert!(!ResourceLimits::is_mixed(&[
21370 EMPTY_RESOURCE_LIMITS,
21371 DEFAULT_RESOURCE_LIMITS,
21372 UNBOUNDED_RESOURCE_LIMITS,
21373 ]));
21374 assert!(!ResourceLimits::is_mixed(&[
21375 UNBOUNDED_RESOURCE_LIMITS,
21376 DEFAULT_RESOURCE_LIMITS,
21377 EMPTY_RESOURCE_LIMITS,
21378 ]));
21379 }
21380
21381 #[test]
21382 fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21383 // Antichain rejection — the hand-authored antichain pair binds
21384 // is_antichain to `true`, and `!… && !true == false`. A PURE
21385 // antichain is not mixed. Pinned in both orderings so the
21386 // projection factors through the argument-swap-symmetric
21387 // sibling is_antichain verdict on the same antichain.
21388 assert!(!ResourceLimits::is_mixed(&[
21389 HAND_AUTHORED_MID_POSTURE,
21390 HAND_AUTHORED_OTHER_POSTURE,
21391 ]));
21392 assert!(!ResourceLimits::is_mixed(&[
21393 HAND_AUTHORED_OTHER_POSTURE,
21394 HAND_AUTHORED_MID_POSTURE,
21395 ]));
21396 }
21397
21398 #[test]
21399 fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21400 ) {
21401 // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21402 // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21403 // while the (MID, OTHER) pair is incomparable (falsifying
21404 // is_chain); the conjunction `!false && !false == true`. This
21405 // is the SAME fixture the sibling is_chain + is_antichain BOTH
21406 // reject at — the DISCRIMINATING positive-arm catch for the
21407 // (mixed) cell of the three-cell face the two set-level
21408 // projections carry the ends of.
21409 assert!(ResourceLimits::is_mixed(&[
21410 DEFAULT_RESOURCE_LIMITS,
21411 HAND_AUTHORED_MID_POSTURE,
21412 HAND_AUTHORED_OTHER_POSTURE,
21413 ]));
21414 }
21415
21416 #[test]
21417 fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21418 ) {
21419 // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21420 // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21421 // returns `true` on every pair OR triple drawn from the shipped
21422 // presets whose elements are pairwise distinct. The three cells
21423 // partition the set-level verdict surface at every non-
21424 // degenerate slice. Substrate-level identity: pinning it here
21425 // catches a future rewrite of any of the three projections that
21426 // silently drifts from the partition contract.
21427 let presets: [ResourceLimits; 5] = [
21428 EMPTY_RESOURCE_LIMITS,
21429 DEFAULT_RESOURCE_LIMITS,
21430 UNBOUNDED_RESOURCE_LIMITS,
21431 HAND_AUTHORED_MID_POSTURE,
21432 HAND_AUTHORED_OTHER_POSTURE,
21433 ];
21434 // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21435 // one distinct pair), so is_mixed is always false and the
21436 // partition reduces to the (chain, antichain) two-cell face
21437 // that is_chain + is_antichain already pinned MUTUALLY
21438 // EXCLUSIVE. Cross-check that the three-cell projection
21439 // AGREES with the two-cell projection at arity 2.
21440 for a in presets {
21441 for b in presets {
21442 if a == b {
21443 continue;
21444 }
21445 let pair = [a, b];
21446 let chain = ResourceLimits::is_chain(&pair);
21447 let antichain = ResourceLimits::is_antichain(&pair);
21448 let mixed = ResourceLimits::is_mixed(&pair);
21449 let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21450 assert_eq!(
21451 true_count, 1,
21452 "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21453 );
21454 assert!(
21455 !mixed,
21456 "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21457 );
21458 }
21459 }
21460 // Arity-3 sweep on distinct triples — the mixed cell opens
21461 // whenever the triple carries both a comparable and an
21462 // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21463 for a in presets {
21464 for b in presets {
21465 for c in presets {
21466 if a == b || b == c || a == c {
21467 continue;
21468 }
21469 let triple = [a, b, c];
21470 let chain = ResourceLimits::is_chain(&triple);
21471 let antichain = ResourceLimits::is_antichain(&triple);
21472 let mixed = ResourceLimits::is_mixed(&triple);
21473 let true_count =
21474 usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21475 assert_eq!(
21476 true_count, 1,
21477 "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21478 );
21479 }
21480 }
21481 }
21482 }
21483
21484 #[test]
21485 fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21486 // Const-fn pin — the set-level mixed projection is evaluable
21487 // in const context, so a caller can pin a mixed-set identity
21488 // at compile time. Sibling of the const-fn evaluability pins
21489 // on is_chain + is_antichain one CELL axis over.
21490 const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21491 const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21492 const _: () = assert!(ResourceLimits::is_mixed(&[
21493 DEFAULT_RESOURCE_LIMITS,
21494 HAND_AUTHORED_MID_POSTURE,
21495 HAND_AUTHORED_OTHER_POSTURE,
21496 ]));
21497 }
21498
21499 // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21500
21501 #[test]
21502 fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21503 // Empty-slice vacuous truth — the empty conjunction is vacuously
21504 // true; the empty slice contains no consecutive pairs to reject.
21505 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21506 // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21507 // level, sequence-level) projection pair AGREE at the empty slice
21508 // on the vacuous-truth verdict.
21509 assert!(ResourceLimits::is_ascending(&[]));
21510 }
21511
21512 #[test]
21513 fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21514 // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21515 // PRIMITIVE axis over: both cells of the (ascending, descending)
21516 // sequence-level pair AGREE at the empty slice.
21517 assert!(ResourceLimits::is_descending(&[]));
21518 }
21519
21520 #[test]
21521 fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21522 // Singleton vacuous truth — a one-element slice contains no
21523 // consecutive pairs, so the walk never enters the loop and
21524 // returns `true` vacuously. Pinned on every canonical preset AND
21525 // on both hand-authored asymmetric postures.
21526 assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21527 assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21528 assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21529 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21530 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21531 }
21532
21533 #[test]
21534 fn resource_limits_is_descending_singleton_is_vacuously_true() {
21535 assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21536 assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21537 assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21538 assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21539 assert!(ResourceLimits::is_descending(&[
21540 HAND_AUTHORED_OTHER_POSTURE
21541 ]));
21542 }
21543
21544 #[test]
21545 fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21546 // Diagonal-duplicate identity — `leq` is REFLEXIVE
21547 // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21548 // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21549 // the conjunction holds at every diagonal-duplicate slice.
21550 // AGREES with is_chain's diagonal-duplicate verdict — the two
21551 // projections diverge only where the ORDER matters, and
21552 // duplicated elements carry no ordering information.
21553 assert!(ResourceLimits::is_ascending(&[
21554 DEFAULT_RESOURCE_LIMITS,
21555 DEFAULT_RESOURCE_LIMITS,
21556 ]));
21557 assert!(ResourceLimits::is_ascending(&[
21558 HAND_AUTHORED_MID_POSTURE,
21559 HAND_AUTHORED_MID_POSTURE,
21560 ]));
21561 }
21562
21563 #[test]
21564 fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21565 // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21566 // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21567 // AGREES with is_ascending's diagonal-duplicate verdict — both
21568 // leq and geq carry reflexivity, so the sequence-level (T, T)
21569 // corner opens at every diagonal-duplicate slice, distinct from
21570 // the set-level (T, F) verdict is_chain / is_antichain carry on
21571 // the same slice.
21572 assert!(ResourceLimits::is_descending(&[
21573 DEFAULT_RESOURCE_LIMITS,
21574 DEFAULT_RESOURCE_LIMITS,
21575 ]));
21576 assert!(ResourceLimits::is_descending(&[
21577 HAND_AUTHORED_MID_POSTURE,
21578 HAND_AUTHORED_MID_POSTURE,
21579 ]));
21580 }
21581
21582 #[test]
21583 fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21584 // Ordered ascending closure — the shipped-preset triple on the
21585 // bounded-lattice diagonal enumerated leq-ascending has
21586 // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21587 // true`, so every consecutive pair passes the conjunction.
21588 // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21589 // the walk closes at every non-degenerate ascending enumeration
21590 // length.
21591 assert!(ResourceLimits::is_ascending(&[
21592 EMPTY_RESOURCE_LIMITS,
21593 DEFAULT_RESOURCE_LIMITS,
21594 ]));
21595 assert!(ResourceLimits::is_ascending(&[
21596 DEFAULT_RESOURCE_LIMITS,
21597 UNBOUNDED_RESOURCE_LIMITS,
21598 ]));
21599 assert!(ResourceLimits::is_ascending(&[
21600 EMPTY_RESOURCE_LIMITS,
21601 DEFAULT_RESOURCE_LIMITS,
21602 UNBOUNDED_RESOURCE_LIMITS,
21603 ]));
21604 }
21605
21606 #[test]
21607 fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21608 // Ordered descending closure — the reversed enumeration is a
21609 // geq-monotone descending sequence. The DIRECT MIRROR of
21610 // is_ascending's ascending closure one PERMUTATION axis over.
21611 assert!(ResourceLimits::is_descending(&[
21612 DEFAULT_RESOURCE_LIMITS,
21613 EMPTY_RESOURCE_LIMITS,
21614 ]));
21615 assert!(ResourceLimits::is_descending(&[
21616 UNBOUNDED_RESOURCE_LIMITS,
21617 DEFAULT_RESOURCE_LIMITS,
21618 ]));
21619 assert!(ResourceLimits::is_descending(&[
21620 UNBOUNDED_RESOURCE_LIMITS,
21621 DEFAULT_RESOURCE_LIMITS,
21622 EMPTY_RESOURCE_LIMITS,
21623 ]));
21624 }
21625
21626 #[test]
21627 fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21628 // Order-sensitivity — the reversed enumeration of the same
21629 // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21630 // the first consecutive pair. is_chain accepts BOTH orderings
21631 // (order-INSENSITIVE); this projection accepts only the leq-
21632 // sorted one — the DISCRIMINATING arm between the sequence-level
21633 // and set-level projections.
21634 assert!(!ResourceLimits::is_ascending(&[
21635 UNBOUNDED_RESOURCE_LIMITS,
21636 DEFAULT_RESOURCE_LIMITS,
21637 EMPTY_RESOURCE_LIMITS,
21638 ]));
21639 }
21640
21641 #[test]
21642 fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21643 // The direct falsification of is_ascending's ascending closure
21644 // one CELL axis over on the SAME slice.
21645 assert!(!ResourceLimits::is_descending(&[
21646 EMPTY_RESOURCE_LIMITS,
21647 DEFAULT_RESOURCE_LIMITS,
21648 UNBOUNDED_RESOURCE_LIMITS,
21649 ]));
21650 }
21651
21652 #[test]
21653 fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21654 // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21655 // is a permutation of the shipped-preset chain, so it IS a chain
21656 // (is_chain accepts it), but the first consecutive pair binds
21657 // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21658 // walk rejects. Also is_descending rejects — the third
21659 // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21660 // the DISCRIMINATING arm between the sequence-level and set-
21661 // level projections at a slice that is a chain but neither
21662 // ascending nor descending.
21663 let slice = [
21664 DEFAULT_RESOURCE_LIMITS,
21665 EMPTY_RESOURCE_LIMITS,
21666 UNBOUNDED_RESOURCE_LIMITS,
21667 ];
21668 assert!(ResourceLimits::is_chain(&slice));
21669 assert!(!ResourceLimits::is_ascending(&slice));
21670 assert!(!ResourceLimits::is_descending(&slice));
21671 }
21672
21673 #[test]
21674 fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21675 // The two hand-authored asymmetric postures are incomparable
21676 // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21677 // consecutive pair rejects. Pinned in both orderings — the pair-
21678 // level leq verdict rejects on the antichain pair regardless of
21679 // which side is `self`.
21680 assert!(!ResourceLimits::is_ascending(&[
21681 HAND_AUTHORED_MID_POSTURE,
21682 HAND_AUTHORED_OTHER_POSTURE,
21683 ]));
21684 assert!(!ResourceLimits::is_ascending(&[
21685 HAND_AUTHORED_OTHER_POSTURE,
21686 HAND_AUTHORED_MID_POSTURE,
21687 ]));
21688 }
21689
21690 #[test]
21691 fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21692 assert!(!ResourceLimits::is_descending(&[
21693 HAND_AUTHORED_MID_POSTURE,
21694 HAND_AUTHORED_OTHER_POSTURE,
21695 ]));
21696 assert!(!ResourceLimits::is_descending(&[
21697 HAND_AUTHORED_OTHER_POSTURE,
21698 HAND_AUTHORED_MID_POSTURE,
21699 ]));
21700 }
21701
21702 #[test]
21703 fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21704 // Transitivity theorem — for every slice `postures`,
21705 // `is_ascending(postures) == true` implies `is_chain(postures)
21706 // == true`. The pointwise partial order is TRANSITIVE, so a
21707 // consecutive-pair leq-chain closes under transitive composition
21708 // into the all-pairs leq-chain. Substrate-level THEOREM: the
21709 // sequence-level ascending verdict is strictly STRONGER than the
21710 // set-level chain verdict. Same identity for is_descending —
21711 // both sequence-level projections imply the set-level chain
21712 // projection.
21713 let presets: [ResourceLimits; 5] = [
21714 EMPTY_RESOURCE_LIMITS,
21715 DEFAULT_RESOURCE_LIMITS,
21716 UNBOUNDED_RESOURCE_LIMITS,
21717 HAND_AUTHORED_MID_POSTURE,
21718 HAND_AUTHORED_OTHER_POSTURE,
21719 ];
21720 // Every arity-2 slice from the 5×5 preset matrix witnesses the
21721 // implication in both directions of the (ascending, descending)
21722 // pair.
21723 for a in presets {
21724 for b in presets {
21725 let slice = [a, b];
21726 if ResourceLimits::is_ascending(&slice) {
21727 assert!(
21728 ResourceLimits::is_chain(&slice),
21729 "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21730 );
21731 }
21732 if ResourceLimits::is_descending(&slice) {
21733 assert!(
21734 ResourceLimits::is_chain(&slice),
21735 "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21736 );
21737 }
21738 }
21739 }
21740 // Load-bearing arity-3 witness — the shipped ascending chain
21741 // triple satisfies is_ascending AND is_chain; the descending
21742 // reversal satisfies is_descending AND is_chain.
21743 let asc = [
21744 EMPTY_RESOURCE_LIMITS,
21745 DEFAULT_RESOURCE_LIMITS,
21746 UNBOUNDED_RESOURCE_LIMITS,
21747 ];
21748 assert!(ResourceLimits::is_ascending(&asc));
21749 assert!(ResourceLimits::is_chain(&asc));
21750 let desc = [
21751 UNBOUNDED_RESOURCE_LIMITS,
21752 DEFAULT_RESOURCE_LIMITS,
21753 EMPTY_RESOURCE_LIMITS,
21754 ];
21755 assert!(ResourceLimits::is_descending(&desc));
21756 assert!(ResourceLimits::is_chain(&desc));
21757 }
21758
21759 #[test]
21760 fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21761 // Empty-and-singleton agreement — the two sequence-level
21762 // projections carry the SAME vacuous-truth verdict at every
21763 // slice with fewer than two consecutive pairs. Pinned as a
21764 // substrate-level identity so a future rewrite of either
21765 // projection cannot silently drift from the shared vacuous-
21766 // truth verdict.
21767 assert_eq!(
21768 ResourceLimits::is_ascending(&[]),
21769 ResourceLimits::is_descending(&[]),
21770 );
21771 let presets: [ResourceLimits; 5] = [
21772 EMPTY_RESOURCE_LIMITS,
21773 DEFAULT_RESOURCE_LIMITS,
21774 UNBOUNDED_RESOURCE_LIMITS,
21775 HAND_AUTHORED_MID_POSTURE,
21776 HAND_AUTHORED_OTHER_POSTURE,
21777 ];
21778 for a in presets {
21779 let single = [a];
21780 assert_eq!(
21781 ResourceLimits::is_ascending(&single),
21782 ResourceLimits::is_descending(&single),
21783 "empty-and-singleton agreement failed on singleton {a:?}",
21784 );
21785 let dup = [a, a];
21786 assert_eq!(
21787 ResourceLimits::is_ascending(&dup),
21788 ResourceLimits::is_descending(&dup),
21789 "diagonal-duplicate agreement failed on {a:?}",
21790 );
21791 }
21792 }
21793
21794 #[test]
21795 fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21796 // Const-fn pin — the sequence-level ascending projection is
21797 // evaluable in const context, so a caller can pin an ascending-
21798 // sequence identity at compile time.
21799 const _: () = assert!(ResourceLimits::is_ascending(&[]));
21800 const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21801 const _: () = assert!(ResourceLimits::is_ascending(&[
21802 EMPTY_RESOURCE_LIMITS,
21803 DEFAULT_RESOURCE_LIMITS,
21804 UNBOUNDED_RESOURCE_LIMITS,
21805 ]));
21806 const _: () = assert!(!ResourceLimits::is_ascending(&[
21807 UNBOUNDED_RESOURCE_LIMITS,
21808 EMPTY_RESOURCE_LIMITS,
21809 ]));
21810 }
21811
21812 #[test]
21813 fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21814 // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21815 // PRIMITIVE axis over.
21816 const _: () = assert!(ResourceLimits::is_descending(&[]));
21817 const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21818 const _: () = assert!(ResourceLimits::is_descending(&[
21819 UNBOUNDED_RESOURCE_LIMITS,
21820 DEFAULT_RESOURCE_LIMITS,
21821 EMPTY_RESOURCE_LIMITS,
21822 ]));
21823 const _: () = assert!(!ResourceLimits::is_descending(&[
21824 EMPTY_RESOURCE_LIMITS,
21825 UNBOUNDED_RESOURCE_LIMITS,
21826 ]));
21827 }
21828}