Skip to main content

logicaffeine_kernel/
term_store.rs

1//! Hash-consed term storage — the representation a kernel needs to be as fast as Lean's.
2//!
3//! `Term` is a `Box`-recursive TREE. Every walk over it is native recursion whose depth is
4//! the term's depth, every `clone` copies the whole subtree, and a subterm that occurs `n`
5//! times is `n` separate trees that get checked `n` times. Lean's `Expr` is the other thing:
6//! a maximally-shared DAG behind a structural hash, so constructing an identical subterm
7//! twice yields the SAME pointer, `a == b` is usually a pointer compare, and a cache keyed
8//! on that identity checks a shared subterm ONCE.
9//!
10//! This module is that representation, built and measured STANDALONE:
11//!
12//! * [`TermStore::intern`] turns a `Term` into a [`TermId`] — a `Copy` `u32` handle.
13//!   Identical structure interns to an identical id, so `a == b` is ONE integer compare
14//!   ([`TermStore::same`]) instead of a tree walk.
15//! * Every node carries its [`depth`](TermStore::depth), its unfolded
16//!   [`tree_size`](TermStore::tree_size), its O(1) [`structural_hash`](TermStore::structural_hash),
17//!   its hash-consed FREE-VARIABLE set, and the set of binder names below it. All are
18//!   computed compositionally at construction, so no query re-walks the term.
19//! * [`IdCache`] is a DENSE cache keyed on node identity — `Vec<Option<V>>` indexed by the
20//!   id, so a lookup is a bounds check and not a hash. [`TermStore::fold`] visits each
21//!   DISTINCT node exactly once, which is what "checked once, not once per occurrence" is.
22//! * [`TermStore::instantiate`] is SIMULTANEOUS capture-avoiding substitution — Lean's
23//!   `instantiateRev` shape: a whole argument vector in one pass, not one substitution per
24//!   binder. A node whose free variables do not meet the substitution's domain is returned
25//!   UNCHANGED, detected by a 64-bit mask intersection, which is what takes a deep spine
26//!   from quadratic to linear.
27//! * Nothing here recurses natively. Interning, extraction, folding, substitution and
28//!   normalisation are all explicit-stack loops, and the store's own `Drop` is a flat
29//!   `Vec` drop — so depth is bounded by the heap, not by 2 MiB of thread stack.
30//!
31//! ## What this module is NOT
32//!
33//! [`TermStore::beta_zeta_normalize`] is a MEASUREMENT INSTRUMENT, not a kernel tier. It
34//! implements β and ζ and nothing else — no δ, no ι, no η, no `Fix` unfolding, no universe
35//! algebra. It is not a replacement for `reduction::normalize`.
36//! `beta_delta_equivalent` now uses it behind a conservative conversion gateway:
37//! checked closed monomorphic definitions are expanded, unsupported shapes are refused,
38//! and only alpha-equal results prove conversion; all other answers use the main kernel.
39//!
40//! ## The measured case for wiring this in
41//!
42//! Four numbers, all measured on this box against the kernel as it stands.
43//!
44//! 1. **The deep-spine corpus case used to ABORT, and no longer does — CLOSED.**
45//!    `lean_conformance_t00_terms::a_deep_spine_costs_linearly_in_its_depth_and_not_quadratically`
46//!    died by SIGABRT — a stack overflow in `infer_type`'s own `App` arm — before it reached its
47//!    assertion. `MAX_TERM_DEPTH` is 1600; BISECTED in a child process, `infer_type` died at
48//!    **106** rungs of a Π telescope and at **52** rungs of the corpus's own saturated dependent
49//!    telescope, so the door was fifteen to thirty-one times too permissive and ADMITTED terms
50//!    the kernel then killed the process on. `double_check` died at the SAME 52, because it calls
51//!    `infer_type` first — so across that whole band the second kernel, whose entire job is to
52//!    disagree with the first, was never consulted. [`deepen_stack`] is now wired into
53//!    `infer_type`, `is_subtype`, `normalize_checked_at`, `recheck` and `double_check`, and a
54//!    whole-crate run of 1413 tests reports **zero** SIGABRT/SIGSEGV: the case now FAILS AN
55//!    ASSERTION, which is a verdict instead of a corpse. (The corpus case's own prose puts the
56//!    ceiling at "depth 4000", which is wrong by 77×.)
57//!
58//!    **One band is still open, and [`deepen_stack`] does not close it.** The door and the sizer
59//!    both measure the term they were HANDED. `infer_type`'s `Var` arm is
60//!    `ctx.get(name).cloned()`, and `Term`'s `Clone` is derived — so it recurses over the
61//!    BINDING's type, a depth neither ever saw. MEASURED in a child process: `Var("x")`, one node
62//!    deep, under `x : Nat → … → Nat` at 4000 arrows kills the process BARE and through
63//!    [`deepen_stack`], and is answered by [`deepen_stack_with_depth`]. Every arm inherits it, so
64//!    the input term's depth is a LOWER BOUND on what a kernel call descends, never the bound.
65//! 2. **A β-step on a spine pays FOUR Θ(size) walks, and the corpus's ratio is the square of
66//!    that.** ITEMISED AND COUNTED, in units a loaded box cannot move — node visits, by
67//!    `W_perf_extra::the_spines_four_walks_are_quadratic_in_node_visits_and_the_owned_step_is_linear`:
68//!
69//!    | per β-step on a spine | visits at depth 500 | at depth 1500 | ratio |
70//!    |---|---:|---:|---:|
71//!    | `substitute`'s `free_vars(replacement)` | 499 500 | 4 498 500 | 9.01× |
72//!    | `substitute`'s clone of the replacement | 499 500 | 4 498 500 | 9.01× |
73//!    | the driver's recursive `Drop` | 501 500 | 4 504 500 | 8.98× |
74//!    | Brent's `current == tortoise` (and its saves) | 513 500 | 4 550 366 | 8.86× |
75//!    | **all four** | **2 014 000** | **18 051 866** | **8.96×** |
76//!    | an OWNED step that moves instead of copying | 2 000 | 6 000 | **3.00×** |
77//!
78//!    The corpus case asks for `≤ 5.00×` at 3× the depth, and the kernel's own wall clock on this
79//!    box reads 6.9× to 12.3× across repeated runs of one binary (40–94 ms at depth 500, 428 ms
80//!    to 1.16 s at depth 1500). **Each of the four is quadratic ON ITS OWN**, so removing any
81//!    three leaves the ratio where it was — which is the measurement that says no constant factor
82//!    reaches it. Three of the four can go inside `type_checker.rs` and `reduction.rs`:
83//!    `free_vars(replacement)` is only needed when a BINDER in the body could capture, and on a
84//!    spine the body is `Var("x")` and has none; the clone and the `Drop` both go if the reducer
85//!    OWNS its term instead of borrowing it. **The fourth cannot.** Brent's cycle detection needs
86//!    an equality test on the WHOLE term, and a `Box`-recursive tree has no O(1) one — every
87//!    cheap surrogate (a bounded prefix compare, a depth-limited compare, a periodic compare) is
88//!    either unsound or useless on exactly this shape, because two spines of different lengths
89//!    agree for every rung of the shorter. A hash-consed store has both: `same` is a `u32`
90//!    compare and `structural_hash` is computed at construction. That is the measured case for
91//!    stage 2 below, and this store already reduces the same spine at **2.77×** (1.64 ms →
92//!    4.55 ms, interning included).
93//! 3. **`Nat` has no literal fast path.** `try_primitive_reduce` matches `Literal::Int` and
94//!    `Literal::BigInt` only, because `term::lit_bigint` returns `None` for `Literal::Nat`, so
95//!    `natLe` peels one `Succ` per unit. The two slowest tests in the kernel crate are both
96//!    this, at 61.7 s and 52.4 s of a 66 s suite against a median under 0.3 s. That fix lives
97//!    in `reduction.rs`, not here; `W_perf_extra`'s `a_nat_order_fast_path_must_agree_with_the_unary_unfolding`
98//!    is the specification it has to satisfy.
99//! 4. **The main kernel has no `whnf` and no cache of any kind.** `is_subtype` normalises BOTH
100//!    sides to a FULL normal form and then compares, with one `alpha_eq` tree walk as its only
101//!    shortcut, on every conversion. Lean's `isDefEq` is whnf-driven over a hash-consed `Expr`
102//!    with a `whnf` cache and an `isDefEq` cache. MEASURED: one convertible-but-not-α-equal pair
103//!    costs 6.55 ms once and 6.62 ms per call a hundred times over — a FLAT per-call cost, which
104//!    is what "no cache anywhere" looks like from outside. That is the largest remaining
105//!    algorithmic gap after (3), and [`TermStore::alpha_id`], [`IdCache`], [`TypeCache`] and
106//!    [`DefEqCache::scoped`] are the four pieces it needs — the last two carrying the lifetime
107//!    and the environment key that make a kernel cache SOUND rather than merely fast.
108//!
109//! ## The wiring plan
110//!
111//! **Three routes, and only one is open.**
112//!
113//! * *Change `Term`'s children from `Box` to `Rc`.* Hundreds of `Box::new` Term-child sites sit
114//!   in FROZEN test files. Closed by rule.
115//! * *Add a `Term::Shared(Rc<Term>)` variant.* Seven frozen kernel test files match `Term`
116//!   exhaustively and wildcard-free, deliberately — `lean_conformance_t00_terms.rs` says so in a doc
117//!   comment ("a new variant has to answer here"). Closed by rule.
118//! * *Keep `Term` as the BOUNDARY type and make the store the kernel's internal
119//!   representation.* **This is the recommendation.** Every frozen test builds a `Term`, calls a
120//!   kernel entry point, and compares a `Term`; none of them can observe what happens in
121//!   between. The boundary is EIGHT public functions taking `&Term`:
122//!   `type_checker::{infer_type, is_subtype, substitute}`,
123//!   `reduction::{normalize, normalize_at, normalize_checked}` (plus `normalize_checked_at`),
124//!   and `recheck::{recheck, double_check}`. Intern once on the way in, extract once on the way
125//!   out. The entry points already pay an O(size) `term.clone()` before their first step —
126//!   `normalize_checked_at` pays two — so interning replaces a cost rather than adding one.
127//!
128//! **Four stages, each independently green.**
129//!
130//! * **Stage 1 — conversion, `type_checker.rs` only.** Five `alpha_eq` call sites become
131//!   [`alpha_same`](TermStore::alpha_same), and `is_subtype` gains the cache it has never had:
132//!   `HashMap<(TermId, TermId), bool>`. The walk is replaced by a walk of the same order that
133//!   MEMOISES, so the second conversion mentioning a subterm is O(1) instead of O(size). Hold
134//!   the store thread-local behind the existing `INSIDE_KERNEL` door, so the outermost entry
135//!   creates it and `DoorGuard` drops it — a cache with the lifetime of one kernel call cannot
136//!   grow without bound or leak across a `Context` change.
137//! * **Stage 2 — the reducer, `reduction.rs`.** `normalize_checked_at` interns once and runs its
138//!   fixpoint and Brent cycle detection on `TermId`s: (2) above goes away outright. The 54
139//!   `reduce_step_at` arms port one at a time with `recheck_reduction_differential.rs` as the
140//!   oracle. Depth stops binding, because the traversal is this file's explicit stack.
141//! * **Stage 3 — `infer_type`, `type_checker.rs`.** Its 33 recursive self-calls become one
142//!   [`fold`](TermStore::fold) over an [`IdCache`] of types — which IS
143//!   `a_shared_subterm_is_checked_once_and_not_once_per_occurrence`, by construction rather than
144//!   by a special case — and the 16 KB frame that aborts the process disappears with the
145//!   recursion.
146//! * **Stage 4 — the second kernel, `recheck.rs`.** It gets its OWN `TermStore`. Do not share
147//!   one between the two kernels: six soundness bugs this campaign were miscounted identically
148//!   in both, so `double_check` agreed with the bug throughout. A shared store is a shared bug.
149//!
150//! ## What this file hands the kernel TODAY, with no representation change
151//!
152//! The four stages above all move the kernel onto `TermId`. These work against the
153//! `Box`-recursive `Term` the kernel already has, and each is one line at a call site. **The
154//! first three are WIRED IN** — `type_checker::{infer_type, is_subtype}`,
155//! `reduction::normalize_checked_at`, `recheck::{recheck, double_check}` and
156//! `reduction::reduce_step_opt`'s `Term::Let` arm all call into this file now:
157//!
158//! * [`deepen_stack`] — the kernel takes its own stack, sized from the term it was handed, so
159//!   the door's advertised limit becomes the real one and no admitted term can abort the
160//!   process. One line at the top of each public entry, with no re-indentation of its body.
161//! * [`zeta_or_share_opt`] — the whole occurrence-gated ζ rule, as one call, in the
162//!   `Option<Term>` shape `reduce_step_opt` has. Both kernels used to ζ-substitute
163//!   UNCONDITIONALLY, so a value bound once and used n times was written and checked n times,
164//!   which is `a_shared_subterm_is_checked_once_and_not_once_per_occurrence` exactly: **329
165//!   nodes against a ceiling of 97, and 92 under this rule — the case is green.**
166//!   [`occurrences_in_term`] is the gate underneath it and [`zeta_expand_head_term`] is what
167//!   conversion owes in return. All three are iterative, so none has the ceiling
168//!   `type_checker::body_stats` does.
169//! * [`kernel_stack_bytes`] — `recheck.rs`'s stack budget was a hard-coded 1 MiB that assumed a
170//!   2 MiB thread. Derived from this instead, the second kernel stops answering `Unsupported`
171//!   on a deep term the first kernel now checks happily.
172//! * [`deepen_stack_with_depth`] — **NOT yet wired, and it closes a hole the other three do
173//!   not.** The door and [`deepen_stack`] both measure the term they were HANDED;
174//!   `infer_type`'s `Var` arm walks the CONTEXT's type instead. MEASURED in a child process: a
175//!   one-node term `Var("x")` under a binding of depth 4000 kills the process both bare AND
176//!   through `deepen_stack`, and is answered when the sizer is told the context's depth.
177//! * [`TypeCache`] + [`TermStore::env_for`] — `inferType` memoised on `(environment, term)`,
178//!   which is `a_shared_subterm_is_checked_once_and_not_once_per_occurrence` on the CHECKING
179//!   side. The environment is in the key because `Term` names its variables and
180//!   `Context::extend` shadows; Lean's `Expr` is de Bruijn and does not need it, which is
181//!   exactly why porting its `inferType` cache across unchanged is unsound here.
182//!
183//! **Two things not to do.** Do not put the store in `Context` — `Context` is cloned on every
184//! `extend` and would copy the store with it. And do not key a CACHE on a term's identity alone:
185//! `Term` names its variables and `Context::extend` SHADOWS, so one `TermId` has one type per
186//! scope. [`TypeCache`] carries the [`EnvId`] that makes the key sound, and [`KernelCall`] gives
187//! it a lifetime it cannot escape.
188//!
189//! **The float question is SETTLED.** `Literal`'s `PartialEq` is hand-written in `term.rs` and
190//! compares `f64::to_bits()`, so it is total and reflexive and the store's interning key is the
191//! kernel's own equality. [`literal_identity_eq`] is the INDEPENDENT second implementation of
192//! that rule, locked against `term.rs`'s over an adversarial grid, and
193//! [`eq_faithful`](TermStore::eq_faithful) is the conservative guard that was correct under the
194//! old rule and stays correct under the new one.
195//!
196//! ## Example
197//!
198//! ```
199//! use logicaffeine_kernel::term_store::TermStore;
200//! use logicaffeine_kernel::Term;
201//!
202//! let mut store = TermStore::new();
203//! let x = Term::Var("x".to_string());
204//! let twice = Term::App(Box::new(x.clone()), Box::new(x.clone()));
205//!
206//! // The two occurrences of `x` are ONE node.
207//! let id = store.intern(&twice);
208//! assert_eq!(store.dag_size(id), 2);      // the App and the one shared Var
209//! assert_eq!(store.tree_size(id), 3);     // what a tree walk would count
210//!
211//! // Interning identical structure yields the identical handle: equality is O(1).
212//! assert_eq!(store.intern(&twice), id);
213//! assert_eq!(store.extract(id), twice);
214//! ```
215
216use std::collections::HashMap;
217
218use logicaffeine_base::{BigInt, Interner, Symbol};
219
220use crate::term::{Literal, Term, Universe};
221
222// ================================================================================================
223// HANDLES
224// ================================================================================================
225
226/// Identity of an interned term — a `Copy` handle whose equality IS structural equality.
227///
228/// The one caveat is floating-point, and it is exact: see [`TermStore::float_free`].
229#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
230pub struct TermId(u32);
231
232impl TermId {
233    /// The dense index this handle occupies — for `Vec`-keyed side tables.
234    pub fn index(self) -> usize {
235        self.0 as usize
236    }
237}
238
239/// Identity of an interned universe level.
240#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
241pub struct LevelId(u32);
242
243impl LevelId {
244    /// The dense index this handle occupies.
245    pub fn index(self) -> usize {
246        self.0 as usize
247    }
248}
249
250/// Identity of an interned literal.
251#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
252pub struct LitId(u32);
253
254impl LitId {
255    /// The dense index this handle occupies.
256    pub fn index(self) -> usize {
257        self.0 as usize
258    }
259}
260
261/// Identity of a hash-consed, sorted set of names.
262#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
263pub struct NameSetId(u32);
264
265/// Identity of a hash-consed LOCAL TYPING ENVIRONMENT — the fragment of a `Context` that
266/// decides a term's type, and therefore half of the key an `inferType` cache must have.
267///
268/// A `Term` names its variables. `Var("x")` interns to ONE [`TermId`] whatever scope it is in,
269/// and `Context::extend` SHADOWS, so the same id has type `Nat` under one context and `Bool`
270/// under another. A cache keyed on the term alone would hand the second context the first's
271/// answer — the kernel certifying a type the term does not have. That is not a hazard Lean
272/// shares: its `Expr` uses de Bruijn indices and globally-unique `FVarId`s, so two occurrences
273/// in different scopes are literally different `Expr`s and `inferType` may cache on the
274/// expression alone. Ours may not, and this is the missing half.
275#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
276pub struct EnvId(u32);
277
278impl EnvId {
279    /// The EMPTY environment — the key every CLOSED subterm gets, and the reason the cache
280    /// pays off: a closed subterm is checked once for the whole call however many scopes it
281    /// occurs under.
282    pub const EMPTY: EnvId = EnvId(0);
283
284    /// The dense index this handle occupies.
285    pub fn index(self) -> usize {
286        self.0 as usize
287    }
288}
289
290/// Identity of a hash-consed simultaneous substitution.
291#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
292pub struct SubstId(u32);
293
294impl SubstId {
295    /// The identity substitution, which every operation short-circuits on.
296    pub const EMPTY: SubstId = SubstId(0);
297}
298
299// ================================================================================================
300// FLAT NODES
301// ================================================================================================
302
303/// One term node with its children replaced by handles — the flat shape that makes the store
304/// a DAG rather than a tree.
305///
306/// The match arms below mirror `Term`'s thirteen kernel variants ONE FOR ONE, and every match over
307/// `Node` in this file is wildcard-free on purpose: a new `Term` variant must answer here
308/// rather than slip past under a `_`. `Term::Annotated` has no node: the store keys on the erased
309/// term, so an annotated term interns to its term's node.
310#[derive(Clone, PartialEq, Eq, Hash, Debug)]
311pub enum Node {
312    /// `Term::Sort`.
313    Sort(LevelId),
314    /// `Term::Var`.
315    Var(Symbol),
316    /// `Term::Global`.
317    Global(Symbol),
318    /// `Term::Const`.
319    Const {
320        /// The declaration name.
321        name: Symbol,
322        /// The explicit universe arguments.
323        levels: Box<[LevelId]>,
324    },
325    /// `Term::Pi`.
326    Pi {
327        /// The bound name.
328        param: Symbol,
329        /// The domain.
330        param_type: TermId,
331        /// The codomain, in which `param` is bound.
332        body_type: TermId,
333    },
334    /// `Term::Lambda`.
335    Lambda {
336        /// The bound name.
337        param: Symbol,
338        /// The domain.
339        param_type: TermId,
340        /// The body, in which `param` is bound.
341        body: TermId,
342    },
343    /// `Term::App`.
344    App(TermId, TermId),
345    /// `Term::Match`.
346    Match {
347        /// The scrutinee.
348        discriminant: TermId,
349        /// The return-type motive.
350        motive: TermId,
351        /// One branch per constructor, in declaration order.
352        cases: Box<[TermId]>,
353    },
354    /// `Term::Fix`.
355    Fix {
356        /// The self-reference name, bound in `body`.
357        name: Symbol,
358        /// The fixpoint body.
359        body: TermId,
360    },
361    /// `Term::MutualFix`.
362    MutualFix {
363        /// The block's definitions; every name is bound in every body.
364        defs: Box<[(Symbol, TermId)]>,
365        /// Which definition this occurrence denotes.
366        index: usize,
367    },
368    /// `Term::Let`.
369    Let {
370        /// The bound name, bound in `body`.
371        name: Symbol,
372        /// The ascribed type.
373        ty: TermId,
374        /// The bound value.
375        value: TermId,
376        /// The body.
377        body: TermId,
378    },
379    /// `Term::Lit`.
380    Lit(LitId),
381    /// `Term::Hole`.
382    Hole,
383    /// A DE BRUIJN LEVEL — a bound-variable occurrence in an α-CANONICAL form.
384    ///
385    /// [`TermStore::intern`] never produces this: no `Term` can spell it, which is exactly why
386    /// it is a node and not a reserved NAME. A reserved name could collide with a free variable
387    /// a user actually wrote, and the collision would silently conflate two terms that are not
388    /// α-equivalent. A node shape cannot collide with anything.
389    Bound(u32),
390}
391
392impl Node {
393    /// Visit this node's term children, in their canonical order.
394    ///
395    /// This is the SINGLE source of truth for child order. [`Node::child_count`] and
396    /// [`Node::rebuild_with`] are both defined against it, and
397    /// `term_store_extra::rebuild_with_round_trips_every_variant` locks the three together.
398    pub fn for_each_child(&self, f: &mut impl FnMut(TermId)) {
399        match self {
400            Node::Sort(_)
401            | Node::Var(_)
402            | Node::Global(_)
403            | Node::Const { .. }
404            | Node::Lit(_)
405            | Node::Hole
406            | Node::Bound(_) => {}
407            Node::Pi {
408                param_type,
409                body_type,
410                ..
411            } => {
412                f(*param_type);
413                f(*body_type);
414            }
415            Node::Lambda {
416                param_type, body, ..
417            } => {
418                f(*param_type);
419                f(*body);
420            }
421            Node::App(fun, arg) => {
422                f(*fun);
423                f(*arg);
424            }
425            Node::Match {
426                discriminant,
427                motive,
428                cases,
429            } => {
430                f(*discriminant);
431                f(*motive);
432                for c in cases.iter() {
433                    f(*c);
434                }
435            }
436            Node::Fix { body, .. } => f(*body),
437            Node::MutualFix { defs, .. } => {
438                for (_, b) in defs.iter() {
439                    f(*b);
440                }
441            }
442            Node::Let {
443                ty, value, body, ..
444            } => {
445                f(*ty);
446                f(*value);
447                f(*body);
448            }
449        }
450    }
451
452    /// How many term children this node has.
453    pub fn child_count(&self) -> usize {
454        let mut n = 0;
455        self.for_each_child(&mut |_| n += 1);
456        n
457    }
458
459    /// This node with its children replaced, positionally, by `children`.
460    ///
461    /// # Panics
462    ///
463    /// Panics if `children.len() != self.child_count()` — a caller that miscounts is a bug,
464    /// not a recoverable condition.
465    pub fn rebuild_with(&self, children: &[TermId]) -> Node {
466        assert_eq!(
467            children.len(),
468            self.child_count(),
469            "rebuild_with needs every child"
470        );
471        match self {
472            Node::Sort(_)
473            | Node::Var(_)
474            | Node::Global(_)
475            | Node::Const { .. }
476            | Node::Lit(_)
477            | Node::Hole
478            | Node::Bound(_) => self.clone(),
479            Node::Pi { param, .. } => Node::Pi {
480                param: *param,
481                param_type: children[0],
482                body_type: children[1],
483            },
484            Node::Lambda { param, .. } => Node::Lambda {
485                param: *param,
486                param_type: children[0],
487                body: children[1],
488            },
489            Node::App(_, _) => Node::App(children[0], children[1]),
490            Node::Match { .. } => Node::Match {
491                discriminant: children[0],
492                motive: children[1],
493                cases: children[2..].to_vec().into_boxed_slice(),
494            },
495            Node::Fix { name, .. } => Node::Fix {
496                name: *name,
497                body: children[0],
498            },
499            Node::MutualFix { defs, index } => Node::MutualFix {
500                defs: defs
501                    .iter()
502                    .zip(children.iter())
503                    .map(|((n, _), c)| (*n, *c))
504                    .collect::<Vec<_>>()
505                    .into_boxed_slice(),
506                index: *index,
507            },
508            Node::Let { name, .. } => Node::Let {
509                name: *name,
510                ty: children[0],
511                value: children[1],
512                body: children[2],
513            },
514        }
515    }
516
517    /// The names this node BINDS in its children, paired with which children they scope over.
518    ///
519    /// `(names, scoped)` — `names` are bound in exactly the children whose index is in
520    /// `scoped`. `Lambda`'s param scopes over the body but NOT over the domain; `Let`'s name
521    /// scopes over the body but not the type or value; `MutualFix`'s names scope over EVERY
522    /// body. Substitution and occurrence counting both read this, so shadowing is defined in
523    /// one place.
524    fn binding(&self) -> (Vec<Symbol>, Vec<usize>) {
525        match self {
526            Node::Sort(_)
527            | Node::Var(_)
528            | Node::Global(_)
529            | Node::Const { .. }
530            | Node::Lit(_)
531            | Node::Hole
532            | Node::Bound(_)
533            | Node::App(_, _)
534            | Node::Match { .. } => (Vec::new(), Vec::new()),
535            Node::Pi { param, .. } => (vec![*param], vec![1]),
536            Node::Lambda { param, .. } => (vec![*param], vec![1]),
537            Node::Fix { name, .. } => (vec![*name], vec![0]),
538            Node::MutualFix { defs, .. } => (
539                defs.iter().map(|(n, _)| *n).collect(),
540                (0..defs.len()).collect(),
541            ),
542            Node::Let { name, .. } => (vec![*name], vec![2]),
543        }
544    }
545
546    /// This node with its bound names replaced by `names`, in `binding()` order.
547    fn with_binders(&self, names: &[Symbol]) -> Node {
548        match self {
549            Node::Sort(_)
550            | Node::Var(_)
551            | Node::Global(_)
552            | Node::Const { .. }
553            | Node::Lit(_)
554            | Node::Hole
555            | Node::Bound(_)
556            | Node::App(_, _)
557            | Node::Match { .. } => self.clone(),
558            Node::Pi {
559                param_type,
560                body_type,
561                ..
562            } => Node::Pi {
563                param: names[0],
564                param_type: *param_type,
565                body_type: *body_type,
566            },
567            Node::Lambda {
568                param_type, body, ..
569            } => Node::Lambda {
570                param: names[0],
571                param_type: *param_type,
572                body: *body,
573            },
574            Node::Fix { body, .. } => Node::Fix {
575                name: names[0],
576                body: *body,
577            },
578            Node::MutualFix { defs, index } => Node::MutualFix {
579                defs: defs
580                    .iter()
581                    .zip(names.iter())
582                    .map(|((_, b), n)| (*n, *b))
583                    .collect::<Vec<_>>()
584                    .into_boxed_slice(),
585                index: *index,
586            },
587            Node::Let {
588                ty, value, body, ..
589            } => Node::Let {
590                name: names[0],
591                ty: *ty,
592                value: *value,
593                body: *body,
594            },
595        }
596    }
597
598    /// A small integer distinguishing the variant, for the structural hash.
599    fn tag(&self) -> u64 {
600        match self {
601            Node::Sort(_) => 1,
602            Node::Var(_) => 2,
603            Node::Global(_) => 3,
604            Node::Const { .. } => 4,
605            Node::Pi { .. } => 5,
606            Node::Lambda { .. } => 6,
607            Node::App(_, _) => 7,
608            Node::Match { .. } => 8,
609            Node::Fix { .. } => 9,
610            Node::MutualFix { .. } => 10,
611            Node::Let { .. } => 11,
612            Node::Lit(_) => 12,
613            Node::Hole => 13,
614            Node::Bound(_) => 14,
615        }
616    }
617}
618
619/// Which `Node` kind a subterm is — one per [`Node`] variant, and wildcard-free everywhere it is
620/// matched for the same reason [`Node`] is.
621///
622/// This exists so a caller can ask "does this subterm contain a `Match` anywhere?" in O(1)
623/// instead of walking it. The bit lives in `Info` and is ORed up from the children exactly
624/// once, at interning time.
625#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
626pub enum NodeShape {
627    /// [`Node::Sort`].
628    Sort,
629    /// [`Node::Var`].
630    Var,
631    /// [`Node::Global`].
632    Global,
633    /// [`Node::Const`].
634    Const,
635    /// [`Node::Pi`].
636    Pi,
637    /// [`Node::Lambda`].
638    Lambda,
639    /// [`Node::App`].
640    App,
641    /// [`Node::Match`].
642    Match,
643    /// [`Node::Fix`].
644    Fix,
645    /// [`Node::MutualFix`].
646    MutualFix,
647    /// [`Node::Let`].
648    Let,
649    /// [`Node::Lit`].
650    Lit,
651    /// [`Node::Hole`].
652    Hole,
653    /// [`Node::Bound`] — a de Bruijn level, which only an α-canonical form carries.
654    Bound,
655}
656
657impl NodeShape {
658    /// The single bit this shape occupies in a [`ShapeMask`].
659    const fn bit(self) -> u16 {
660        1u16 << (self as u16)
661    }
662
663    /// The shape of one node.
664    fn of(node: &Node) -> NodeShape {
665        match node {
666            Node::Sort(_) => NodeShape::Sort,
667            Node::Var(_) => NodeShape::Var,
668            Node::Global(_) => NodeShape::Global,
669            Node::Const { .. } => NodeShape::Const,
670            Node::Pi { .. } => NodeShape::Pi,
671            Node::Lambda { .. } => NodeShape::Lambda,
672            Node::App(_, _) => NodeShape::App,
673            Node::Match { .. } => NodeShape::Match,
674            Node::Fix { .. } => NodeShape::Fix,
675            Node::MutualFix { .. } => NodeShape::MutualFix,
676            Node::Let { .. } => NodeShape::Let,
677            Node::Lit(_) => NodeShape::Lit,
678            Node::Hole => NodeShape::Hole,
679            Node::Bound(_) => NodeShape::Bound,
680        }
681    }
682}
683
684/// The set of [`NodeShape`]s occurring anywhere in a subterm — one `u16`, computed once at
685/// interning time and answered in O(1) thereafter.
686///
687/// # The Lean design this copies
688///
689/// Lean's `Expr` carries a 64-bit `Expr.Data` word in every node, and four of its bits are
690/// exactly this idea: `hasFVar`, `hasExprMVar`, `hasLevelMVar`, `hasLevelParam`
691/// (`Lean/Expr.lean:121-124`). `Expr.mkAppData` propagates all four with a single OR of the two
692/// children's words — `((fData ||| aData) &&& (15 <<< 40)) ||| …` (`Lean/Expr.lean:193`) — so an
693/// application's bits cost one instruction rather than a traversal. What they BUY is visible at
694/// `Lean/Expr.lean:1661`: `let rec visit (e : Expr) := if !e.hasFVar then false else …`, a walk
695/// that stops before it starts.
696///
697/// Ours widens the idea from four hand-picked predicates to one bit per node kind, which is what
698/// lets [`beta_zeta_normal_form`] decide in O(1) whether a term can hold a redex it does not own.
699/// The cost is the same: one OR per child at interning time, in `TermStore::compute_info`.
700#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
701pub struct ShapeMask(u16);
702
703impl ShapeMask {
704    /// The empty mask — no shape occurs.
705    pub const NONE: ShapeMask = ShapeMask(0);
706
707    /// The shapes a term made only of `Sort`, `Var`, `Global`, `App`, `Lambda`, `Pi` and `Let`
708    /// can have — the admissible surface of [`beta_zeta_normal_form`]. Its complement is
709    /// `Const`, `Match`, `Fix`, `MutualFix`, `Lit`, `Hole` and `Bound`.
710    pub const BETA_ZETA: ShapeMask = ShapeMask(
711        NodeShape::Sort.bit()
712            | NodeShape::Var.bit()
713            | NodeShape::Global.bit()
714            | NodeShape::App.bit()
715            | NodeShape::Lambda.bit()
716            | NodeShape::Pi.bit()
717            | NodeShape::Let.bit(),
718    );
719
720    /// Whether `shape` occurs anywhere in the subterm.
721    pub fn contains(self, shape: NodeShape) -> bool {
722        self.0 & shape.bit() != 0
723    }
724
725    /// Whether every shape in this mask is also in `other`.
726    pub fn subset_of(self, other: ShapeMask) -> bool {
727        self.0 & !other.0 == 0
728    }
729
730    /// The union of two masks.
731    pub fn union(self, other: ShapeMask) -> ShapeMask {
732        ShapeMask(self.0 | other.0)
733    }
734
735    /// This mask with `shape` added.
736    pub fn with(self, shape: NodeShape) -> ShapeMask {
737        ShapeMask(self.0 | shape.bit())
738    }
739
740    /// The raw bits, for a caller that wants to store or compare them.
741    pub fn bits(self) -> u16 {
742        self.0
743    }
744}
745
746/// A borrowed view of one interned universe level.
747///
748/// Child handles belong to this store. Reading a view does not extract, clone,
749/// normalize, or recursively visit its payload.
750#[derive(Clone, Copy, Debug)]
751pub enum LevelView<'a> {
752    /// The strict proposition universe.
753    SProp,
754    /// The proposition universe.
755    Prop,
756    /// A concrete type universe.
757    Type(u32),
758    /// A borrowed universe variable name.
759    Var(&'a str),
760    /// Successor of an interned level.
761    Succ(LevelId),
762    /// Maximum of two interned levels.
763    Max(LevelId, LevelId),
764    /// Impredicative maximum of two interned levels.
765    IMax(LevelId, LevelId),
766}
767
768/// A universe level with its children replaced by handles.
769#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
770enum LevelNode {
771    SProp,
772    Prop,
773    Type(u32),
774    Var(Symbol),
775    Succ(LevelId),
776    Max(LevelId, LevelId),
777    IMax(LevelId, LevelId),
778}
779
780/// A literal, keyed so that it can be hash-consed.
781///
782/// `Float` is keyed on its RAW BITS — the only choice that yields an equivalence relation, and
783/// exactly the rule `Literal`'s hand-written `PartialEq` compares by, so the key is the kernel's
784/// own literal equality.
785#[derive(Clone, PartialEq, Eq, Hash, Debug)]
786enum LitKey {
787    Int(i64),
788    FloatBits(u64),
789    Text(Symbol),
790    Duration(i64),
791    Date(i32),
792    Moment(i64),
793    BigInt(BigInt),
794    Nat(BigInt),
795}
796
797/// Whether IEEE `==` and bit equality agree about this literal — the exact condition under
798/// which the store's id equality is a faithful decision procedure for `Term`'s `==`.
799///
800/// For every `f64` that is neither a NaN nor a zero, equal values have equal bit patterns and
801/// distinct bit patterns are distinct values, so the two relations coincide. The exceptions are
802/// exactly two, and they break the two halves of the contract `Term` claims:
803///
804/// * a **NaN** is `!=` itself, so `impl Eq for Literal {}` is a false claim — `Eq` promises
805///   reflexivity — and any algorithm that recovers "nothing changed" from `a == a` never fires;
806/// * **`0.0` and `-0.0`** are `==` with DIFFERENT bit patterns, so `Term` equality identifies two
807///   terms the kernel can still tell apart by `Display`, by serialisation, and — the moment a
808///   float primitive lands — by `1.0 / x`.
809fn float_eq_is_bit_eq(key: &LitKey) -> bool {
810    match key {
811        LitKey::FloatBits(b) => {
812            let x = f64::from_bits(*b);
813            !x.is_nan() && x != 0.0
814        }
815        LitKey::Int(_)
816        | LitKey::Text(_)
817        | LitKey::Duration(_)
818        | LitKey::Date(_)
819        | LitKey::Moment(_)
820        | LitKey::BigInt(_)
821        | LitKey::Nat(_) => true,
822    }
823}
824
825/// **THE ONE RULE.** Total, reflexive literal equality — bit-exact on floats — offered as a
826/// FUNCTION so `term.rs` and this store cannot hold two different opinions about it.
827///
828/// # Why a hash-consed kernel has no second option
829///
830/// Hash-consing is a QUOTIENT: interning maps a term to the equivalence class of its structure,
831/// and a quotient exists only for an EQUIVALENCE relation. `Literal`'s derived `PartialEq` uses
832/// IEEE `==` on `Literal::Float`, which is
833///
834/// * not reflexive (`NaN != NaN`), and
835/// * not a function of the representation (`0.0 == -0.0` holds between distinct bit patterns).
836///
837/// So it is not an equivalence relation, and **no hash-consing scheme can implement it** — not
838/// this one, not Lean's, not any. The choice is therefore not between two tastes: either
839/// `Literal`'s equality becomes bit-exact on floats, or the kernel can never key anything on
840/// term identity. Lean made the same call: `Literal.ofNat`/`ofScientific` compare by value and
841/// its `Expr` equality is structural, never IEEE.
842///
843/// # It is the rule `term.rs` now has, and this is the INDEPENDENT model it is locked against
844///
845/// `Literal`'s `PartialEq` is hand-written and compares `f64::to_bits()`, which makes the
846/// existing `impl Eq for Literal {}` an honest claim rather than an unsound contract. This
847/// function is deliberately NOT a call to it: it is a second implementation, wildcard-free on
848/// the diagonal where `term.rs`'s falls through to `_ => false`, and
849/// `W_perf_extra::the_two_implementations_of_the_one_literal_rule_agree_on_every_pair` runs the
850/// two against each other over a grid that contains three distinct NaN bit patterns, both zeros,
851/// and one value of every other variant. Two implementations that agree is evidence; one
852/// implementation compared with itself is not.
853pub fn literal_identity_eq(a: &Literal, b: &Literal) -> bool {
854    match (a, b) {
855        (Literal::Float(x), Literal::Float(y)) => x.to_bits() == y.to_bits(),
856        (Literal::Int(x), Literal::Int(y)) => x == y,
857        (Literal::Text(x), Literal::Text(y)) => x == y,
858        (Literal::Duration(x), Literal::Duration(y)) => x == y,
859        (Literal::Date(x), Literal::Date(y)) => x == y,
860        (Literal::Moment(x), Literal::Moment(y)) => x == y,
861        (Literal::BigInt(x), Literal::BigInt(y)) => x == y,
862        (Literal::Nat(x), Literal::Nat(y)) => x == y,
863        // Wildcard-free on the DIAGONAL, which is the half that decides anything; two different
864        // variants are different literals and always were.
865        (Literal::Float(_), _)
866        | (Literal::Int(_), _)
867        | (Literal::Text(_), _)
868        | (Literal::Duration(_), _)
869        | (Literal::Date(_), _)
870        | (Literal::Moment(_), _)
871        | (Literal::BigInt(_), _)
872        | (Literal::Nat(_), _) => false,
873    }
874}
875
876// ================================================================================================
877// PER-NODE ANNOTATIONS
878// ================================================================================================
879
880/// Everything the store knows about a node without re-walking it.
881#[derive(Clone, Copy, Debug)]
882struct Info {
883    /// Maximum height: 1 for a leaf, `1 + max(children)` otherwise. This is what lets a
884    /// caller size a traversal, or refuse a term, WITHOUT walking it.
885    depth: u32,
886    /// The node count a tree walk would report — what `Term`'s shape actually costs.
887    /// Saturating, because a DAG can denote a tree far larger than `u64`.
888    tree_size: u64,
889    /// O(1) structural hash. Equal ids have equal hashes; unequal ids almost never collide.
890    hash: u64,
891    /// Whether the subterm is free of `Literal::Float`. `false` is the ONLY condition under
892    /// which id equality and `Term` equality can disagree.
893    float_free: bool,
894    /// Whether id equality is a FAITHFUL decision procedure for `Term` equality on this
895    /// subterm — the precise condition [`TermStore::float_free`] over-approximates. See
896    /// [`TermStore::eq_faithful`].
897    eq_faithful: bool,
898    /// Free variable names, hash-consed and sorted.
899    free: NameSetId,
900    /// Every binder name occurring anywhere below, hash-consed and sorted — the exclusion
901    /// set a freshening pass needs.
902    binders: NameSetId,
903    /// Which [`NodeShape`]s occur anywhere below, ORed up from the children. See
904    /// [`ShapeMask`] for the Lean design this is copied from.
905    shapes: ShapeMask,
906    /// Every `Global` and `Const` NAME occurring anywhere below, hash-consed and sorted — the
907    /// δ SURFACE. It is the set a caller has to certify inert before a reducer that has no δ
908    /// rule may be trusted with the term.
909    consts: NameSetId,
910}
911
912/// A hash-consed simultaneous substitution `dom[i] ↦ to[i]`, sorted by `dom`.
913#[derive(Clone, Debug)]
914struct Subst {
915    dom: Box<[Symbol]>,
916    to: Box<[TermId]>,
917    /// Bloom mask of `dom`, for an O(1) "this node cannot be affected" test.
918    dom_mask: u64,
919    /// The union of the free variables of every `to`, for the capture test.
920    range_free: NameSetId,
921}
922
923/// The binder name every α-canonical binder carries. It is cosmetic: a canonical form's
924/// bound OCCURRENCES are [`Node::Bound`] nodes, so the name carries no information and cannot
925/// be confused with a user's.
926const CANON_BINDER: &str = "⟪⟫";
927
928/// The prefix a canonical bound occurrence prints with, for [`TermStore::extract`].
929const CANON_OCCURRENCE: &str = "⟪";
930
931const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
932const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
933
934fn mix(h: u64, x: u64) -> u64 {
935    (h ^ x).wrapping_mul(FNV_PRIME)
936}
937
938/// Hash a `BigInt` by its canonical little-endian magnitude and sign, so the hash is a
939/// function of the VALUE and not of the internal limb layout.
940fn bigint_hash(seed: u64, n: &BigInt) -> u64 {
941    let (negative, bytes) = n.to_le_bytes();
942    let mut h = mix(seed, u64::from(negative));
943    for b in bytes {
944        h = mix(h, u64::from(b));
945    }
946    h
947}
948
949/// The bloom bit of one name — `mask & bit == 0` proves the name is absent.
950fn name_bit(s: Symbol) -> u64 {
951    let mut h = s.index() as u64;
952    h ^= h >> 30;
953    h = h.wrapping_mul(0xbf58_476d_1ce4_e5b9);
954    h ^= h >> 27;
955    1u64 << (h & 63)
956}
957
958/// How often a name occurs — the only distinction an occurrence-gated ζ rule needs.
959#[derive(Clone, Copy, PartialEq, Eq, Debug)]
960pub enum Occurrences {
961    /// The name does not occur free at all.
962    Zero,
963    /// Exactly one free occurrence — substituting cannot duplicate anything.
964    Once,
965    /// Two or more free occurrences — substituting COPIES the value at each one.
966    Many,
967}
968
969/// The result of [`TermStore::beta_zeta_normalize`] — honest about a budget it did not finish
970/// inside, the way `reduction::normalize_checked` is.
971#[derive(Clone, Copy, PartialEq, Eq, Debug)]
972pub enum StoreNormalForm {
973    /// A β/ζ normal form was reached.
974    Reached(TermId),
975    /// The step budget ran out. The term is NOT known to be normal.
976    OutOfFuel {
977        /// The budget that was exhausted.
978        budget: u64,
979    },
980}
981
982/// A DAG whose unfolding is too large to materialise as a `Term`.
983#[derive(Clone, Copy, PartialEq, Eq, Debug)]
984pub struct TooLarge {
985    /// The tree node count the unfolding would have.
986    pub tree_size: u64,
987    /// The limit the caller set.
988    pub limit: u64,
989}
990
991// ================================================================================================
992// THE STORE
993// ================================================================================================
994
995/// A bounded DAG transformation stopped before admitting further work or a distinct node.
996#[derive(Clone, Copy, Debug, PartialEq, Eq)]
997pub(crate) enum StoreOperationLimit {
998    Work,
999    Nodes,
1000    Allocation,
1001}
1002
1003/// A hash-consed term store: maximal structural sharing, O(1) equality, O(1) annotations.
1004///
1005/// Every `mk_*` and [`intern`](TermStore::intern) call goes through one dedup table, so two
1006/// structurally identical subterms are ONE node with ONE id. Nothing in this type recurses
1007/// natively, and its `Drop` is a flat `Vec` drop.
1008pub struct TermStore {
1009    nodes: Vec<Node>,
1010    info: Vec<Info>,
1011    dedup: HashMap<Node, TermId>,
1012
1013    levels: Vec<LevelNode>,
1014    level_dedup: HashMap<LevelNode, LevelId>,
1015
1016    lits: Vec<LitKey>,
1017    lit_dedup: HashMap<LitKey, LitId>,
1018
1019    names: Interner,
1020    /// FNV hash of each symbol's STRING, so the structural hash is a function of structure
1021    /// and not of interning order — the property that makes it comparable across stores.
1022    name_hashes: Vec<u64>,
1023    level_hashes: Vec<u64>,
1024    lit_hashes: Vec<u64>,
1025
1026    sets: Vec<Box<[Symbol]>>,
1027    set_masks: Vec<u64>,
1028    set_dedup: HashMap<Box<[Symbol]>, NameSetId>,
1029
1030    substs: Vec<Subst>,
1031    subst_dedup: HashMap<(Box<[Symbol]>, Box<[TermId]>), SubstId>,
1032
1033    /// Hash-consed local typing environments, each sorted by name. [`EnvId::EMPTY`] is
1034    /// pre-interned at index 0.
1035    envs: Vec<Box<[(Symbol, TermId)]>>,
1036    env_dedup: HashMap<Box<[(Symbol, TermId)]>, EnvId>,
1037
1038    /// `(substitution, term) ↦ result`. Pure in both arguments and both are immutable, so
1039    /// the cache is valid for the life of the store.
1040    subst_cache: HashMap<(SubstId, TermId), TermId>,
1041    /// `(name, term) ↦ free occurrence count`, saturating.
1042    occ_cache: HashMap<(Symbol, TermId), u32>,
1043    /// `term ↦ β/ζ normal form`. Only COMPLETED results are entered.
1044    nf_cache: HashMap<(TermId, bool), TermId>,
1045    /// `(environment, level, term) ↦ α-canonical form`. Keyed on the LEVEL as well as the
1046    /// environment because two paths can bind the same names at the same levels after
1047    /// restriction and still sit at different depths, and the depth decides the levels a
1048    /// binder BELOW gets. Dropping the level from the key is unsound; it was tried.
1049    canon_cache: HashMap<(SubstId, u32, TermId), TermId>,
1050    /// `term ↦ α-canonical form` for the outermost call, where the environment is empty and the
1051    /// level is zero — the only entry point conversion ever asks for.
1052    alpha_cache: HashMap<TermId, TermId>,
1053    head_dependencies: HashMap<TermId, NameSetId>,
1054    /// Fresh-name counter, so a freshening pass never rescans from 1.
1055    fresh_counter: u64,
1056    /// Whether any [`substitute`](TermStore::substitute) since the last
1057    /// [`clear_caches`](TermStore::clear_caches) was asked to push a value into a body that
1058    /// BINDS one of the value's free names. See [`TermStore::rename_risk`].
1059    rename_risk: bool,
1060}
1061
1062impl Default for TermStore {
1063    fn default() -> Self {
1064        Self::new()
1065    }
1066}
1067
1068impl TermStore {
1069    /// An empty store, with the empty name set pre-interned at [`NameSetId`] 0 and the
1070    /// identity substitution at [`SubstId::EMPTY`].
1071    pub fn new() -> Self {
1072        let mut store = TermStore {
1073            nodes: Vec::new(),
1074            info: Vec::new(),
1075            dedup: HashMap::new(),
1076            levels: Vec::new(),
1077            level_dedup: HashMap::new(),
1078            lits: Vec::new(),
1079            lit_dedup: HashMap::new(),
1080            names: Interner::new(),
1081            name_hashes: Vec::new(),
1082            level_hashes: Vec::new(),
1083            lit_hashes: Vec::new(),
1084            sets: Vec::new(),
1085            set_masks: Vec::new(),
1086            set_dedup: HashMap::new(),
1087            substs: Vec::new(),
1088            subst_dedup: HashMap::new(),
1089            envs: Vec::new(),
1090            env_dedup: HashMap::new(),
1091            subst_cache: HashMap::new(),
1092            occ_cache: HashMap::new(),
1093            nf_cache: HashMap::new(),
1094            canon_cache: HashMap::new(),
1095            alpha_cache: HashMap::new(),
1096            head_dependencies: HashMap::new(),
1097            fresh_counter: 0,
1098            rename_risk: false,
1099        };
1100        let empty: Box<[Symbol]> = Vec::new().into_boxed_slice();
1101        store.sets.push(empty.clone());
1102        store.set_masks.push(0);
1103        store.set_dedup.insert(empty, NameSetId(0));
1104        store.substs.push(Subst {
1105            dom: Vec::new().into_boxed_slice(),
1106            to: Vec::new().into_boxed_slice(),
1107            dom_mask: 0,
1108            range_free: NameSetId(0),
1109        });
1110        store.subst_dedup.insert(
1111            (Vec::new().into_boxed_slice(), Vec::new().into_boxed_slice()),
1112            SubstId::EMPTY,
1113        );
1114        let empty_env: Box<[(Symbol, TermId)]> = Vec::new().into_boxed_slice();
1115        store.envs.push(empty_env.clone());
1116        store.env_dedup.insert(empty_env, EnvId::EMPTY);
1117        store
1118    }
1119
1120    /// How many DISTINCT nodes the store holds.
1121    pub fn len(&self) -> usize {
1122        self.nodes.len()
1123    }
1124
1125    /// How many distinct universe levels have been interned (without allocating).
1126    pub(crate) fn level_count(&self) -> usize {
1127        self.levels.len()
1128    }
1129
1130    /// Whether the store holds no nodes at all.
1131    pub fn is_empty(&self) -> bool {
1132        self.nodes.is_empty()
1133    }
1134
1135    /// Drop every memo table. Ids stay valid; only cached derived results are discarded.
1136    pub fn clear_caches(&mut self) {
1137        self.subst_cache.clear();
1138        self.occ_cache.clear();
1139        self.nf_cache.clear();
1140        self.canon_cache.clear();
1141        self.alpha_cache.clear();
1142        self.head_dependencies.clear();
1143        self.rename_risk = false;
1144    }
1145
1146    /// Whether any substitution this store has performed could have RENAMED a binder.
1147    ///
1148    /// It is set when [`substitute`](TermStore::substitute) is asked to push a value into a
1149    /// body that BINDS one of the value's free names — the exact condition under which a
1150    /// capture-avoiding pass has to invent a name — and it is a hard `false` otherwise.
1151    ///
1152    /// # Why a caller comparing two substitutions needs it
1153    ///
1154    /// `type_checker::substitute_avoiding` renames a binder when `replacement_fvs.contains(param)`
1155    /// and this store renames on the same criterion, so on a term where the condition never
1156    /// arises the two are the SAME FUNCTION and their answers are equal, not merely
1157    /// α-equivalent. Where it does arise they can still differ, because the two passes visit
1158    /// different binders: this store descends only where the substitution is live, while
1159    /// `substitute_avoiding` descends the whole body and renames a binder whose name is a free
1160    /// variable of the replacement even in a subtree the substituted variable never reaches.
1161    /// So the flag is the precondition [`beta_zeta_normal_form`] needs to promise an EQUAL
1162    /// answer rather than an α-equivalent one.
1163    pub fn rename_risk(&self) -> bool {
1164        self.rename_risk
1165    }
1166
1167    // -- names ----------------------------------------------------------------------------
1168
1169    /// Intern a name, returning its symbol.
1170    pub fn symbol(&mut self, name: &str) -> Symbol {
1171        self.names.intern(name)
1172    }
1173
1174    /// An already-interned name's symbol, without creating one.
1175    pub fn lookup_symbol(&self, name: &str) -> Option<Symbol> {
1176        self.names.lookup(name)
1177    }
1178
1179    /// The string a symbol denotes.
1180    pub fn name(&self, sym: Symbol) -> &str {
1181        self.names.resolve(sym)
1182    }
1183
1184    /// FNV hash of a symbol's STRING. Memoised in a dense table keyed on the symbol's index,
1185    /// so it is computed once per distinct name however many nodes mention it.
1186    fn sym_hash(&mut self, sym: Symbol) -> u64 {
1187        if self.name_hashes.len() <= sym.index() {
1188            self.name_hashes.resize(sym.index() + 1, 0);
1189        }
1190        if self.name_hashes[sym.index()] == 0 {
1191            let mut h = FNV_OFFSET;
1192            for b in self.names.resolve(sym).as_bytes() {
1193                h = mix(h, u64::from(*b));
1194            }
1195            // 0 is the "not yet computed" sentinel, so never store it.
1196            self.name_hashes[sym.index()] = if h == 0 { 1 } else { h };
1197        }
1198        self.name_hashes[sym.index()]
1199    }
1200
1201    // -- construction ---------------------------------------------------------------------
1202
1203    /// The node behind an id.
1204    pub fn node(&self, id: TermId) -> &Node {
1205        &self.nodes[id.index()]
1206    }
1207
1208    /// Borrow one universe node without materializing its recursive tree.
1209    ///
1210    /// As with node(), the handle must have come from this store.
1211    pub fn level_view(&self, id: LevelId) -> LevelView<'_> {
1212        match self.levels[id.index()] {
1213            LevelNode::SProp => LevelView::SProp,
1214            LevelNode::Prop => LevelView::Prop,
1215            LevelNode::Type(n) => LevelView::Type(n),
1216            LevelNode::Var(name) => LevelView::Var(self.name(name)),
1217            LevelNode::Succ(a) => LevelView::Succ(a),
1218            LevelNode::Max(a, b) => LevelView::Max(a, b),
1219            LevelNode::IMax(a, b) => LevelView::IMax(a, b),
1220        }
1221    }
1222
1223    /// Add a node, returning the existing id if an identical node is already stored.
1224    ///
1225    /// This is the ONE place a `TermId` is minted, which is what makes the store maximally
1226    /// shared by construction rather than by a later pass.
1227    pub fn add(&mut self, node: Node) -> TermId {
1228        if let Some(&id) = self.dedup.get(&node) {
1229            return id;
1230        }
1231        let info = self.compute_info(&node);
1232        let id = TermId(self.nodes.len() as u32);
1233        self.dedup.insert(node.clone(), id);
1234        self.nodes.push(node);
1235        self.info.push(info);
1236        id
1237    }
1238
1239    /// Add one already-owned node with admission before dedup hashing, metadata construction,
1240    /// node cloning or store growth. The caller admits any Node payload vector before creating
1241    /// it. Charges use cached child name-set lengths, never expanded term-tree size.
1242    pub(crate) fn add_bounded(
1243        &mut self,
1244        node: Node,
1245        work: &mut u64,
1246        node_limit: usize,
1247        allocation: &mut u64,
1248    ) -> Result<TermId, StoreOperationLimit> {
1249        let children = node.child_count() as u64;
1250        let mut hash_work = children.saturating_add(8);
1251        match &node {
1252            Node::Const { levels, .. } => hash_work = hash_work.saturating_add(levels.len() as u64),
1253            Node::MutualFix { defs, .. } => hash_work = hash_work.saturating_add(defs.len() as u64),
1254            _ => {}
1255        }
1256        Self::spend_operation_work(work, hash_work)?;
1257        // A dedup hit needs no metadata allocation and is valid at the distinct-node ceiling.
1258        if let Some(&id) = self.dedup.get(&node) {
1259            return Ok(id);
1260        }
1261        if node_limit > u32::MAX as usize || self.nodes.len() >= node_limit {
1262            return Err(StoreOperationLimit::Nodes);
1263        }
1264        self.admit_node_metadata(&node, work, allocation)?;
1265        Ok(self.add(node))
1266    }
1267
1268    /// Upper bounds the actual compute_info operations without allocating or expanding terms.
1269    /// Retains a known set id through empty/identical unions, so repeated shared children with
1270    /// the same sets cost linear work, even if their expanded terms have exponential size.
1271    fn admit_node_metadata(
1272        &self,
1273        node: &Node,
1274        work: &mut u64,
1275        allocation: &mut u64,
1276    ) -> Result<(), StoreOperationLimit> {
1277        #[derive(Clone, Copy)]
1278        enum SetBound {
1279            Known(NameSetId),
1280            Size(u64),
1281        }
1282        let length = |value: SetBound| match value {
1283            SetBound::Known(id) => self.set(id).len() as u64,
1284            SetBound::Size(n) => n,
1285        };
1286        let mut operations = 0u64;
1287        let mut set_allocations = 0u64;
1288        let mut units = 32u64;
1289        let mut union = |left: SetBound, right: SetBound| {
1290            operations = operations.saturating_add(1);
1291            if let (SetBound::Known(a), SetBound::Known(b)) = (left, right) {
1292                if a == b {
1293                    return left;
1294                }
1295            }
1296            let (a, b) = (length(left), length(right));
1297            if a == 0 {
1298                return right;
1299            }
1300            if b == 0 {
1301                return left;
1302            }
1303            let size = a.saturating_add(b);
1304            // Merge, temporary vector, boxed key, retained copy, hashing and mask pass.
1305            operations = operations.saturating_add(size.saturating_mul(8));
1306            units = units
1307                .saturating_add(size.saturating_mul(4))
1308                .saturating_add(16);
1309            set_allocations = set_allocations.saturating_add(1);
1310            SetBound::Size(size)
1311        };
1312        let mut binders = SetBound::Known(NameSetId(0));
1313        let mut constants = SetBound::Known(NameSetId(0));
1314        let mut free = SetBound::Known(NameSetId(0));
1315        let bound_count = match node {
1316            Node::MutualFix { defs, .. } => defs.len(),
1317            Node::Pi { .. } | Node::Lambda { .. } | Node::Let { .. } | Node::Fix { .. } => 1,
1318            _ => 0,
1319        } as u64;
1320        let mut child_index = 0usize;
1321        let mut scan_failure = None;
1322        let mut removal_work = 0u64;
1323        let mut removal_units = 0u64;
1324        let mut removal_sets = 0u64;
1325        node.for_each_child(&mut |child| {
1326            if scan_failure.is_some() {
1327                return;
1328            }
1329            let info = self.info[child.index()];
1330            binders = union(binders, SetBound::Known(info.binders));
1331            constants = union(constants, SetBound::Known(info.consts));
1332            let scoped = match node {
1333                Node::Pi { .. } | Node::Lambda { .. } => child_index == 1,
1334                Node::Let { .. } => child_index == 2,
1335                Node::Fix { .. } | Node::MutualFix { .. } => true,
1336                _ => false,
1337            };
1338            let original = self.set(info.free);
1339            let mut contribution = SetBound::Known(info.free);
1340            if scoped && !original.is_empty() {
1341                let scan = (original.len() as u64).saturating_mul(bound_count.saturating_add(1));
1342                if let Err(error) = Self::spend_operation_work(work, scan) {
1343                    scan_failure = Some(error);
1344                    return;
1345                }
1346                let mut removed = 0u64;
1347                for symbol in original {
1348                    let hidden = match node {
1349                        Node::Pi { param, .. }
1350                        | Node::Lambda { param, .. }
1351                        | Node::Let { name: param, .. }
1352                        | Node::Fix { name: param, .. } => symbol == param,
1353                        Node::MutualFix { defs, .. } => defs.iter().any(|(name, _)| name == symbol),
1354                        _ => false,
1355                    };
1356                    removed += u64::from(hidden);
1357                }
1358                // compute_info calls set_remove for EVERY binder, including misses.
1359                removal_work = removal_work.saturating_add(
1360                    (original.len() as u64)
1361                        .saturating_add(1)
1362                        .saturating_mul(bound_count)
1363                        .saturating_mul(8),
1364                );
1365                removal_units = removal_units.saturating_add(
1366                    (original.len() as u64)
1367                        .saturating_add(1)
1368                        .saturating_mul(removed)
1369                        .saturating_mul(4),
1370                );
1371                removal_sets = removal_sets.saturating_add(removed);
1372                if removed > 0 {
1373                    contribution = if removed == original.len() as u64 {
1374                        SetBound::Known(NameSetId(0))
1375                    } else {
1376                        SetBound::Size(original.len() as u64 - removed)
1377                    };
1378                }
1379            }
1380            free = union(free, contribution);
1381            child_index += 1;
1382        });
1383        // End the closure's exclusive borrows before accumulating other costs.
1384        drop(union);
1385        if let Some(error) = scan_failure {
1386            return Err(error);
1387        }
1388        Self::spend_operation_work(work, child_index as u64)?;
1389        let binder_size = length(binders).saturating_add(bound_count);
1390        // Insertion of this node's binders and own variable/global symbol.
1391        let own = usize::from(matches!(
1392            node,
1393            Node::Var(_) | Node::Global(_) | Node::Const { .. }
1394        )) as u64;
1395        let insertion_units = binder_size
1396            .saturating_add(length(constants))
1397            .saturating_add(1)
1398            .saturating_mul(bound_count.saturating_add(own))
1399            .saturating_mul(8);
1400        let mut payload = child_index as u64;
1401        let mut name_bytes = 0u64;
1402        let mut hash_slots = 0usize;
1403        let mut note_name = |name: Symbol| {
1404            name_bytes = name_bytes.saturating_add(self.name(name).len() as u64);
1405            hash_slots = hash_slots.max(
1406                name.index()
1407                    .saturating_add(1)
1408                    .saturating_sub(self.name_hashes.len()),
1409            );
1410        };
1411        match node {
1412            Node::Var(n)
1413            | Node::Global(n)
1414            | Node::Fix { name: n, .. }
1415            | Node::Pi { param: n, .. }
1416            | Node::Lambda { param: n, .. }
1417            | Node::Let { name: n, .. } => note_name(*n),
1418            Node::Const { name, levels } => {
1419                note_name(*name);
1420                payload = payload.saturating_add(levels.len() as u64);
1421            }
1422            Node::MutualFix { defs, .. } => {
1423                payload = payload.saturating_add(defs.len() as u64);
1424                for (name, _) in defs.iter() {
1425                    note_name(*name);
1426                }
1427            }
1428            _ => {}
1429        }
1430        drop(note_name);
1431        let set_upper = set_allocations
1432            .saturating_add(removal_sets)
1433            .saturating_add(bound_count)
1434            .saturating_add(own);
1435        Self::admit_name_set_inventory(self.sets.len(), set_upper)?;
1436        // Node payload copies, child_free/child_sets, binding/scoped vectors, table growth,
1437        // and name-hash memo growth. Units are conservative 64-byte allocation credits.
1438        units = units
1439            .saturating_add(removal_units)
1440            .saturating_add(insertion_units)
1441            .saturating_add(payload.saturating_mul(16))
1442            .saturating_add(bound_count.saturating_mul(8))
1443            .saturating_add(name_bytes / 64 + 1)
1444            .saturating_add((hash_slots as u64) / 4 + 1)
1445            .saturating_add(64);
1446        operations = operations
1447            .saturating_add(removal_work)
1448            .saturating_add(insertion_units)
1449            .saturating_add(payload.saturating_mul(8))
1450            .saturating_add(name_bytes)
1451            .saturating_add(hash_slots as u64)
1452            .saturating_add(32);
1453        Self::spend_operation_work(work, operations)?;
1454        Self::admit_operation_allocation(allocation, units)
1455    }
1456
1457    fn compute_info(&mut self, node: &Node) -> Info {
1458        let mut depth = 0u32;
1459        let mut tree_size = 1u64;
1460        let mut hash = mix(FNV_OFFSET, node.tag());
1461        let mut float_free = true;
1462        let mut eq_faithful = true;
1463        let mut binders: NameSetId = NameSetId(0);
1464        // One bit per node kind, ORed up from the children exactly as Lean's `mkAppData` ORs
1465        // its four `has*` bits (`Lean/Expr.lean:193`).
1466        let mut shapes = ShapeMask::NONE.with(NodeShape::of(node));
1467        let mut consts: NameSetId = NameSetId(0);
1468
1469        // The hash mixes NAMES and LEVELS by their own structural hashes, never by their
1470        // dense indices: an index depends on interning order, so a hash built from one would
1471        // differ between two stores holding the same term.
1472        match node {
1473            Node::Sort(l) => hash = mix(hash, self.level_hashes[l.index()]),
1474            Node::Var(s) | Node::Global(s) | Node::Fix { name: s, .. } => {
1475                let h = self.sym_hash(*s);
1476                hash = mix(hash, h);
1477            }
1478            Node::Const { name, levels } => {
1479                let h = self.sym_hash(*name);
1480                hash = mix(hash, h);
1481                for l in levels.iter() {
1482                    hash = mix(hash, self.level_hashes[l.index()]);
1483                }
1484            }
1485            Node::Pi { param, .. } | Node::Lambda { param, .. } | Node::Let { name: param, .. } => {
1486                let h = self.sym_hash(*param);
1487                hash = mix(hash, h);
1488            }
1489            Node::MutualFix { defs, index } => {
1490                hash = mix(hash, *index as u64);
1491                for (n, _) in defs.iter() {
1492                    let h = self.sym_hash(*n);
1493                    hash = mix(hash, h);
1494                }
1495            }
1496            Node::Lit(l) => {
1497                hash = mix(hash, self.lit_hashes[l.index()]);
1498                float_free = !matches!(self.lits[l.index()], LitKey::FloatBits(_));
1499                eq_faithful = float_eq_is_bit_eq(&self.lits[l.index()]);
1500            }
1501            Node::Bound(level) => hash = mix(hash, u64::from(*level)),
1502            Node::App(_, _) | Node::Match { .. } | Node::Hole => {}
1503        }
1504
1505        node.for_each_child(&mut |c| {
1506            let ci = self.info[c.index()];
1507            depth = depth.max(ci.depth);
1508            tree_size = tree_size.saturating_add(ci.tree_size);
1509            hash = mix(hash, ci.hash);
1510            float_free &= ci.float_free;
1511            eq_faithful &= ci.eq_faithful;
1512        });
1513        // Child metadata is Copy; release each borrow before interning its unions.
1514        // Walk the borrowed node directly instead of allocating per-node scratch vectors.
1515        node.for_each_child(&mut |c| {
1516            let ci = self.info[c.index()];
1517            binders = self.set_union(binders, ci.binders);
1518            consts = self.set_union(consts, ci.consts);
1519            shapes = shapes.union(ci.shapes);
1520        });
1521        // This node's own contribution to the δ surface.
1522        match node {
1523            Node::Global(n) => consts = self.set_insert(consts, *n),
1524            Node::Const { name, .. } => consts = self.set_insert(consts, *name),
1525            Node::Sort(_)
1526            | Node::Var(_)
1527            | Node::Pi { .. }
1528            | Node::Lambda { .. }
1529            | Node::App(_, _)
1530            | Node::Match { .. }
1531            | Node::Fix { .. }
1532            | Node::MutualFix { .. }
1533            | Node::Let { .. }
1534            | Node::Lit(_)
1535            | Node::Hole
1536            | Node::Bound(_) => {}
1537        }
1538
1539        // Free variables: a child in a binder's scope loses the bound names.
1540        let (bound, scoped) = node.binding();
1541        let mut free = NameSetId(0);
1542        let mut i = 0;
1543        node.for_each_child(&mut |c| {
1544            let cf = self.info[c.index()].free;
1545            let contribution = if scoped.contains(&i) {
1546                let mut s = cf;
1547                for &b in &bound {
1548                    s = self.set_remove(s, b);
1549                }
1550                s
1551            } else {
1552                cf
1553            };
1554            free = self.set_union(free, contribution);
1555            i += 1;
1556        });
1557        if let Node::Var(s) = node {
1558            free = self.set_singleton(*s);
1559        }
1560        for &b in &bound {
1561            binders = self.set_insert(binders, b);
1562        }
1563
1564        Info {
1565            depth: depth + 1,
1566            tree_size,
1567            hash,
1568            float_free,
1569            eq_faithful,
1570            free,
1571            binders,
1572            shapes,
1573            consts,
1574        }
1575    }
1576
1577    /// `Term::Sort(u)`.
1578    pub fn mk_sort(&mut self, u: &Universe) -> TermId {
1579        let l = self.intern_level(u);
1580        self.add(Node::Sort(l))
1581    }
1582
1583    /// `Term::Var(name)`.
1584    pub fn mk_var(&mut self, name: &str) -> TermId {
1585        let s = self.symbol(name);
1586        self.add(Node::Var(s))
1587    }
1588
1589    /// `Term::Var` from an already-interned name.
1590    pub fn mk_var_sym(&mut self, s: Symbol) -> TermId {
1591        self.add(Node::Var(s))
1592    }
1593
1594    /// `Term::Global(name)`.
1595    pub fn mk_global(&mut self, name: &str) -> TermId {
1596        let s = self.symbol(name);
1597        self.add(Node::Global(s))
1598    }
1599
1600    /// `Term::App(f, a)`.
1601    pub fn mk_app(&mut self, f: TermId, a: TermId) -> TermId {
1602        self.add(Node::App(f, a))
1603    }
1604
1605    /// `Term::Lambda`.
1606    pub fn mk_lambda(&mut self, param: &str, param_type: TermId, body: TermId) -> TermId {
1607        let p = self.symbol(param);
1608        self.add(Node::Lambda {
1609            param: p,
1610            param_type,
1611            body,
1612        })
1613    }
1614
1615    /// `Term::Pi`.
1616    pub fn mk_pi(&mut self, param: &str, param_type: TermId, body_type: TermId) -> TermId {
1617        let p = self.symbol(param);
1618        self.add(Node::Pi {
1619            param: p,
1620            param_type,
1621            body_type,
1622        })
1623    }
1624
1625    /// `Term::Let`.
1626    pub fn mk_let(&mut self, name: &str, ty: TermId, value: TermId, body: TermId) -> TermId {
1627        let n = self.symbol(name);
1628        self.add(Node::Let {
1629            name: n,
1630            ty,
1631            value,
1632            body,
1633        })
1634    }
1635
1636    /// `Term::Fix`.
1637    pub fn mk_fix(&mut self, name: &str, body: TermId) -> TermId {
1638        let n = self.symbol(name);
1639        self.add(Node::Fix { name: n, body })
1640    }
1641
1642    /// `Term::Lit`.
1643    pub fn mk_lit(&mut self, lit: &Literal) -> TermId {
1644        let l = self.intern_lit(lit);
1645        self.add(Node::Lit(l))
1646    }
1647
1648    /// `Term::Hole`.
1649    pub fn mk_hole(&mut self) -> TermId {
1650        self.add(Node::Hole)
1651    }
1652
1653    /// `Term::Const` — a universe-polymorphic global at explicit levels.
1654    pub fn mk_const(&mut self, name: &str, levels: &[Universe]) -> TermId {
1655        let n = self.symbol(name);
1656        let ls: Vec<LevelId> = levels.iter().map(|u| self.intern_level(u)).collect();
1657        self.add(Node::Const {
1658            name: n,
1659            levels: ls.into_boxed_slice(),
1660        })
1661    }
1662
1663    /// `Term::Match`.
1664    pub fn mk_match(&mut self, discriminant: TermId, motive: TermId, cases: &[TermId]) -> TermId {
1665        self.add(Node::Match {
1666            discriminant,
1667            motive,
1668            cases: cases.to_vec().into_boxed_slice(),
1669        })
1670    }
1671
1672    /// `Term::MutualFix` — every name in `defs` is bound in EVERY body.
1673    pub fn mk_mutual_fix(&mut self, defs: &[(&str, TermId)], index: usize) -> TermId {
1674        let ds: Vec<(Symbol, TermId)> = defs.iter().map(|(n, b)| (self.symbol(n), *b)).collect();
1675        self.add(Node::MutualFix {
1676            defs: ds.into_boxed_slice(),
1677            index,
1678        })
1679    }
1680
1681    /// Intern a universe level, for a caller building a [`Node::Const`] by hand.
1682    pub fn level_of(&mut self, u: &Universe) -> LevelId {
1683        self.intern_level(u)
1684    }
1685
1686    fn intern_level(&mut self, u: &Universe) -> LevelId {
1687        // Universe nesting is bounded by the level algebra, not by user terms, so this one
1688        // walk stays recursive by deliberate choice — see the module note on native
1689        // recursion, which is about TERMS.
1690        let node = match u {
1691            Universe::SProp => LevelNode::SProp,
1692            Universe::Prop => LevelNode::Prop,
1693            Universe::Type(n) => LevelNode::Type(*n),
1694            Universe::Var(v) => LevelNode::Var(self.names.intern(v)),
1695            Universe::Succ(a) => LevelNode::Succ(self.intern_level(a)),
1696            Universe::Max(a, b) => {
1697                let (x, y) = (self.intern_level(a), self.intern_level(b));
1698                LevelNode::Max(x, y)
1699            }
1700            Universe::IMax(a, b) => {
1701                let (x, y) = (self.intern_level(a), self.intern_level(b));
1702                LevelNode::IMax(x, y)
1703            }
1704        };
1705        if let Some(&id) = self.level_dedup.get(&node) {
1706            return id;
1707        }
1708        let hash = match node {
1709            LevelNode::SProp => mix(FNV_OFFSET, 101),
1710            LevelNode::Prop => mix(FNV_OFFSET, 102),
1711            LevelNode::Type(n) => mix(mix(FNV_OFFSET, 103), u64::from(n)),
1712            LevelNode::Var(s) => {
1713                let h = self.sym_hash(s);
1714                mix(mix(FNV_OFFSET, 104), h)
1715            }
1716            LevelNode::Succ(a) => mix(mix(FNV_OFFSET, 105), self.level_hashes[a.index()]),
1717            LevelNode::Max(a, b) => mix(
1718                mix(mix(FNV_OFFSET, 106), self.level_hashes[a.index()]),
1719                self.level_hashes[b.index()],
1720            ),
1721            LevelNode::IMax(a, b) => mix(
1722                mix(mix(FNV_OFFSET, 107), self.level_hashes[a.index()]),
1723                self.level_hashes[b.index()],
1724            ),
1725        };
1726        let id = LevelId(self.levels.len() as u32);
1727        self.levels.push(node);
1728        self.level_hashes.push(hash);
1729        self.level_dedup.insert(node, id);
1730        id
1731    }
1732
1733    fn extract_level(&self, id: LevelId) -> Universe {
1734        match self.levels[id.index()] {
1735            LevelNode::SProp => Universe::SProp,
1736            LevelNode::Prop => Universe::Prop,
1737            LevelNode::Type(n) => Universe::Type(n),
1738            LevelNode::Var(s) => Universe::Var(self.names.resolve(s).to_string()),
1739            LevelNode::Succ(a) => Universe::Succ(Box::new(self.extract_level(a))),
1740            LevelNode::Max(a, b) => Universe::Max(
1741                Box::new(self.extract_level(a)),
1742                Box::new(self.extract_level(b)),
1743            ),
1744            LevelNode::IMax(a, b) => Universe::IMax(
1745                Box::new(self.extract_level(a)),
1746                Box::new(self.extract_level(b)),
1747            ),
1748        }
1749    }
1750
1751    fn intern_lit(&mut self, lit: &Literal) -> LitId {
1752        let key = match lit {
1753            Literal::Int(x) => LitKey::Int(*x),
1754            Literal::Float(x) => LitKey::FloatBits(x.to_bits()),
1755            Literal::Text(s) => LitKey::Text(self.names.intern(s)),
1756            Literal::Duration(x) => LitKey::Duration(*x),
1757            Literal::Date(x) => LitKey::Date(*x),
1758            Literal::Moment(x) => LitKey::Moment(*x),
1759            Literal::BigInt(n) => LitKey::BigInt(n.clone()),
1760            Literal::Nat(n) => LitKey::Nat(n.clone()),
1761        };
1762        if let Some(&id) = self.lit_dedup.get(&key) {
1763            return id;
1764        }
1765        let hash = match &key {
1766            LitKey::Int(x) => mix(mix(FNV_OFFSET, 201), *x as u64),
1767            LitKey::FloatBits(b) => mix(mix(FNV_OFFSET, 202), *b),
1768            LitKey::Text(s) => {
1769                let h = self.sym_hash(*s);
1770                mix(mix(FNV_OFFSET, 203), h)
1771            }
1772            LitKey::Duration(x) => mix(mix(FNV_OFFSET, 204), *x as u64),
1773            LitKey::Date(x) => mix(mix(FNV_OFFSET, 205), i64::from(*x) as u64),
1774            LitKey::Moment(x) => mix(mix(FNV_OFFSET, 206), *x as u64),
1775            LitKey::BigInt(n) => bigint_hash(mix(FNV_OFFSET, 207), n),
1776            LitKey::Nat(n) => bigint_hash(mix(FNV_OFFSET, 208), n),
1777        };
1778        let id = LitId(self.lits.len() as u32);
1779        self.lits.push(key.clone());
1780        self.lit_hashes.push(hash);
1781        self.lit_dedup.insert(key, id);
1782        id
1783    }
1784
1785    /// Borrow the heap payload size needed when extracting a literal.
1786    /// This excludes the Term/enum headers and allocator overhead. The handle
1787    /// must belong to this store, as with node() and level_view().
1788    pub fn literal_payload_bytes(&self, id: LitId) -> usize {
1789        match &self.lits[id.index()] {
1790            LitKey::Text(name) => self.name(*name).len(),
1791            LitKey::BigInt(n) | LitKey::Nat(n) => n.clone_payload_bytes(),
1792            LitKey::Int(_)
1793            | LitKey::FloatBits(_)
1794            | LitKey::Duration(_)
1795            | LitKey::Date(_)
1796            | LitKey::Moment(_) => 0,
1797        }
1798    }
1799
1800    fn extract_lit(&self, id: LitId) -> Literal {
1801        match &self.lits[id.index()] {
1802            LitKey::Int(x) => Literal::Int(*x),
1803            LitKey::FloatBits(b) => Literal::Float(f64::from_bits(*b)),
1804            LitKey::Text(s) => Literal::Text(self.names.resolve(*s).to_string()),
1805            LitKey::Duration(x) => Literal::Duration(*x),
1806            LitKey::Date(x) => Literal::Date(*x),
1807            LitKey::Moment(x) => Literal::Moment(*x),
1808            LitKey::BigInt(n) => Literal::BigInt(n.clone()),
1809            LitKey::Nat(n) => Literal::Nat(n.clone()),
1810        }
1811    }
1812
1813    // -- name sets ------------------------------------------------------------------------
1814
1815    fn intern_set(&mut self, sorted: Vec<Symbol>) -> NameSetId {
1816        let boxed: Box<[Symbol]> = sorted.into_boxed_slice();
1817        if let Some(&id) = self.set_dedup.get(&boxed) {
1818            return id;
1819        }
1820        let mask = boxed.iter().fold(0u64, |m, &s| m | name_bit(s));
1821        let id = NameSetId(self.sets.len() as u32);
1822        self.sets.push(boxed.clone());
1823        self.set_masks.push(mask);
1824        self.set_dedup.insert(boxed, id);
1825        id
1826    }
1827
1828    fn set(&self, id: NameSetId) -> &[Symbol] {
1829        &self.sets[id.0 as usize]
1830    }
1831
1832    fn set_mask(&self, id: NameSetId) -> u64 {
1833        self.set_masks[id.0 as usize]
1834    }
1835
1836    fn set_singleton(&mut self, s: Symbol) -> NameSetId {
1837        self.intern_set(vec![s])
1838    }
1839
1840    fn set_union(&mut self, a: NameSetId, b: NameSetId) -> NameSetId {
1841        if a == b {
1842            return a;
1843        }
1844        if self.set(a).is_empty() {
1845            return b;
1846        }
1847        if self.set(b).is_empty() {
1848            return a;
1849        }
1850        let mut merged = Vec::with_capacity(self.set(a).len() + self.set(b).len());
1851        let (xs, ys) = (self.set(a), self.set(b));
1852        let (mut i, mut j) = (0, 0);
1853        while i < xs.len() || j < ys.len() {
1854            let take = match (xs.get(i), ys.get(j)) {
1855                (Some(x), Some(y)) if x < y => {
1856                    i += 1;
1857                    *x
1858                }
1859                (Some(x), Some(y)) if y < x => {
1860                    j += 1;
1861                    *y
1862                }
1863                (Some(x), Some(_)) => {
1864                    i += 1;
1865                    j += 1;
1866                    *x
1867                }
1868                (Some(x), None) => {
1869                    i += 1;
1870                    *x
1871                }
1872                (None, Some(y)) => {
1873                    j += 1;
1874                    *y
1875                }
1876                (None, None) => unreachable!(),
1877            };
1878            merged.push(take);
1879        }
1880        self.intern_set(merged)
1881    }
1882
1883    fn set_remove(&mut self, a: NameSetId, s: Symbol) -> NameSetId {
1884        if self.set_mask(a) & name_bit(s) == 0 {
1885            return a;
1886        }
1887        if !self.set(a).contains(&s) {
1888            return a;
1889        }
1890        let kept: Vec<Symbol> = self.set(a).iter().copied().filter(|&x| x != s).collect();
1891        self.intern_set(kept)
1892    }
1893
1894    fn set_insert(&mut self, a: NameSetId, s: Symbol) -> NameSetId {
1895        if self.set_mask(a) & name_bit(s) != 0 && self.set(a).contains(&s) {
1896            return a;
1897        }
1898        let mut v: Vec<Symbol> = self.set(a).to_vec();
1899        match v.binary_search(&s) {
1900            Ok(_) => return a,
1901            Err(pos) => v.insert(pos, s),
1902        }
1903        self.intern_set(v)
1904    }
1905
1906    fn set_contains(&self, a: NameSetId, s: Symbol) -> bool {
1907        self.set_mask(a) & name_bit(s) != 0 && self.set(a).binary_search(&s).is_ok()
1908    }
1909
1910    // -- annotations ----------------------------------------------------------------------
1911
1912    /// Maximum height — 1 for a leaf. O(1): computed when the node was built.
1913    pub fn depth(&self, id: TermId) -> u32 {
1914        self.info[id.index()].depth
1915    }
1916
1917    /// The node count an unfolding tree walk would report, saturating at `u64::MAX`. O(1).
1918    pub fn tree_size(&self, id: TermId) -> u64 {
1919        self.info[id.index()].tree_size
1920    }
1921
1922    /// O(1) structural hash. Equal ids have equal hashes.
1923    pub fn structural_hash(&self, id: TermId) -> u64 {
1924        self.info[id.index()].hash
1925    }
1926
1927    /// Whether the subterm contains no `Literal::Float`.
1928    ///
1929    /// The most conservative GUARD on using id equality as `Term` equality. `Literal` equality
1930    /// compares floats by bit identity, so id equality is faithful everywhere; this guard was
1931    /// correct under the old IEEE rule, when `NaN` and `±0.0` made the two part company, and
1932    /// stays correct under the current one. On a `float_free` term, `same(a, b)` and
1933    /// `extract(a) == extract(b)` agree EXACTLY.
1934    pub fn float_free(&self, id: TermId) -> bool {
1935        self.info[id.index()].float_free
1936    }
1937
1938    /// Whether id equality is a FAITHFUL decision procedure for `Term` equality on this
1939    /// subterm: for any two `eq_faithful` ids, `same(a, b)` and `extract(a) == extract(b)`
1940    /// agree in BOTH directions.
1941    ///
1942    /// This is the precise condition; [`float_free`](TermStore::float_free) is the conservative
1943    /// one. A float that is neither a NaN nor a zero compares the same way under IEEE `==` and
1944    /// under bit equality, so a term may hold as many `1.5`s and infinities as it likes and
1945    /// still be safe to compare by identity under either rule; `NaN` and `±0.0` are the literals
1946    /// on which the two rules differ.
1947    ///
1948    /// It is SUFFICIENT rather than necessary: `Literal`'s equality is bit identity (the rule
1949    /// [`literal_identity_eq`] models independently), so id equality is faithful everywhere and
1950    /// this predicate is conservative. `store.same(a, b) && store.eq_faithful(a)` is correct under
1951    /// both rules.
1952    pub fn eq_faithful(&self, id: TermId) -> bool {
1953        self.info[id.index()].eq_faithful
1954    }
1955
1956    /// Which [`NodeShape`]s occur anywhere in a subterm. O(1).
1957    ///
1958    /// This is Lean's `Expr.Data` `has*` bits widened to one per node kind — see [`ShapeMask`]
1959    /// for the citation and the trade-off. A reducer that does not implement ι can ask
1960    /// `!store.shapes(id).contains(NodeShape::Match)` without walking anything.
1961    pub fn shapes(&self, id: TermId) -> ShapeMask {
1962        self.info[id.index()].shapes
1963    }
1964
1965    /// Every `Global` and `Const` NAME occurring anywhere in a subterm, sorted and without
1966    /// duplicates — the δ SURFACE. O(1) to reach, O(k) to hand back.
1967    ///
1968    /// It is the question a caller with no `Context` cannot answer for itself: whether a term
1969    /// still holds a δ redex depends on what those names MEAN here, and only the caller knows.
1970    pub fn constants(&self, id: TermId) -> Vec<&str> {
1971        // Sorted by the NAME and not by the symbol, because a symbol's index is interning
1972        // order: the same term in two stores would otherwise hand back two different orders,
1973        // and a set that crosses a boundary has to have one spelling.
1974        let mut names: Vec<&str> = self
1975            .set(self.info[id.index()].consts)
1976            .iter()
1977            .map(|&s| self.names.resolve(s))
1978            .collect();
1979        names.sort_unstable();
1980        names
1981    }
1982
1983    /// Every BINDER name occurring anywhere in a subterm, sorted. O(1) to reach.
1984    ///
1985    /// The use is a guard rather than a query: a substitution pass that has to FRESHEN a binder
1986    /// mints a name this store chose, and a caller comparing its answer against another
1987    /// implementation's needs to know that no such name leaked into the result.
1988    pub fn binder_names(&self, id: TermId) -> Vec<&str> {
1989        self.set(self.info[id.index()].binders)
1990            .iter()
1991            .map(|&s| self.names.resolve(s))
1992            .collect()
1993    }
1994
1995    /// Whether every binder name in `inner` also occurs as a binder in `outer` — the test that
1996    /// says a reduction invented no name of its own. O(k), over two sorted hash-consed sets.
1997    pub fn binders_within(&self, inner: TermId, outer: TermId) -> bool {
1998        let a = self.info[inner.index()].binders;
1999        let b = self.info[outer.index()].binders;
2000        if a == b {
2001            return true;
2002        }
2003        self.set(a).iter().all(|&s| self.set_contains(b, s))
2004    }
2005
2006    /// Whether the only redexes this subterm can hold are β and ζ — so a reducer that
2007    /// implements exactly those two rules computes the same normal form as the full kernel.
2008    ///
2009    /// Two conditions, and the split between them is the point. The SHAPE condition is decided
2010    /// here, in O(1), from [`shapes`](TermStore::shapes): `Match` carries ι, `Fix` and
2011    /// `MutualFix` carry unrolling, `Const` carries δ plus a level well-formedness test, `Lit`
2012    /// is what the arithmetic primitives key their argument on, `Hole` is not a term, and
2013    /// `Bound` cannot be reached from a `Term` at all. The NAME condition belongs to the
2014    /// caller: whether a `Global` still has a δ body, or is one of the names a rule is keyed on
2015    /// rather than a shape, is a fact about the `Context` this store has never seen.
2016    pub fn is_beta_zeta_pure(&self, id: TermId, inert: &dyn Fn(&str) -> bool) -> bool {
2017        self.shapes(id).subset_of(ShapeMask::BETA_ZETA)
2018            && self
2019                .set(self.info[id.index()].consts)
2020                .iter()
2021                .all(|&c| inert(self.names.resolve(c)))
2022    }
2023
2024    /// The free variable names of a subterm, sorted. Resolves and collects O(k)
2025    /// names, where k is the number of free names; the underlying symbol set is cached.
2026    pub fn free_vars(&self, id: TermId) -> Vec<&str> {
2027        #[cfg(test)]
2028        FREE_NAME_COLLECTIONS.with(|count| count.set(count.get() + 1));
2029        self.set(self.info[id.index()].free)
2030            .iter()
2031            .map(|&s| {
2032                #[cfg(test)]
2033                FREE_NAME_RESOLUTIONS.with(|count| count.set(count.get() + 1));
2034                self.names.resolve(s)
2035            })
2036            .collect()
2037    }
2038
2039    /// Whether the exact free-name set is empty, including annotations and all
2040    /// binder scopes. O(1): the full free-symbol set was cached at construction.
2041    /// This says nothing about internal de Bruijn Bound nodes.
2042    pub fn free_names_are_empty(&self, id: TermId) -> bool {
2043        self.set(self.info[id.index()].free).is_empty()
2044    }
2045
2046    /// Borrow the cached free-name symbols without resolving or allocating names.
2047    /// Lexical dependencies observed by the lazy constructor machine.
2048    ///
2049    /// That machine never inspects lambda domains, let annotations, or match
2050    /// motives. This is NOT a typing or definitional-equality cache key: two
2051    /// closures sharing it may still have different types. It is valid only for
2052    /// evaluation to the same atomic constructor after the full terms have been
2053    /// checked. Constructor arguments, including proofs, are all retained.
2054    pub(crate) fn head_reduction_symbols(&mut self, root: TermId) -> &[Symbol] {
2055        if let Some(&set) = self.head_dependencies.get(&root) {
2056            return self.set(set);
2057        }
2058        let mut work = vec![(root, false)];
2059        while let Some((id, ready)) = work.pop() {
2060            if self.head_dependencies.contains_key(&id) {
2061                continue;
2062            }
2063            if !ready {
2064                work.push((id, true));
2065                match self.node(id) {
2066                    Node::Lambda { body, .. } | Node::Fix { body, .. } => work.push((*body, false)),
2067                    Node::App(f, a) => {
2068                        work.push((*f, false));
2069                        work.push((*a, false));
2070                    }
2071                    Node::Let { value, body, .. } => {
2072                        work.push((*value, false));
2073                        work.push((*body, false));
2074                    }
2075                    Node::Match {
2076                        discriminant,
2077                        cases,
2078                        ..
2079                    } => {
2080                        work.push((*discriminant, false));
2081                        work.extend(cases.iter().map(|c| (*c, false)));
2082                    }
2083                    _ => {}
2084                }
2085                continue;
2086            }
2087            let free = match self.node(id).clone() {
2088                Node::Lambda { param, body, .. } => {
2089                    self.set_remove(self.head_dependencies[&body], param)
2090                }
2091                Node::Fix { name, body } => self.set_remove(self.head_dependencies[&body], name),
2092                Node::App(f, a) => {
2093                    self.set_union(self.head_dependencies[&f], self.head_dependencies[&a])
2094                }
2095                Node::Let {
2096                    name, value, body, ..
2097                } => {
2098                    let body = self.set_remove(self.head_dependencies[&body], name);
2099                    self.set_union(self.head_dependencies[&value], body)
2100                }
2101                Node::Match {
2102                    discriminant,
2103                    cases,
2104                    ..
2105                } => {
2106                    let mut free = self.head_dependencies[&discriminant];
2107                    for case in cases {
2108                        free = self.set_union(free, self.head_dependencies[&case]);
2109                    }
2110                    free
2111                }
2112                _ => self.info[id.index()].free,
2113            };
2114            self.head_dependencies.insert(id, free);
2115        }
2116        self.set(self.head_dependencies[&root])
2117    }
2118
2119    /// Whether `name` occurs free. O(1) mask test, then a binary search.
2120    pub fn occurs_free(&self, id: TermId, name: &str) -> bool {
2121        match self.names.lookup(name) {
2122            Some(s) => self.set_contains(self.info[id.index()].free, s),
2123            None => false,
2124        }
2125    }
2126
2127    /// Whether `name` occurs free, by symbol.
2128    pub fn occurs_free_sym(&self, id: TermId, s: Symbol) -> bool {
2129        self.set_contains(self.info[id.index()].free, s)
2130    }
2131
2132    /// How many DISTINCT nodes `id` reaches — the DAG size, which is what a checker keyed on
2133    /// identity actually pays.
2134    pub fn dag_size(&self, id: TermId) -> usize {
2135        let mut seen = vec![false; self.nodes.len()];
2136        let mut stack = vec![id];
2137        let mut count = 0;
2138        while let Some(t) = stack.pop() {
2139            if seen[t.index()] {
2140                continue;
2141            }
2142            seen[t.index()] = true;
2143            count += 1;
2144            self.nodes[t.index()].for_each_child(&mut |c| stack.push(c));
2145        }
2146        count
2147    }
2148
2149    /// Whether two ids denote the same term — ONE integer compare.
2150    pub fn same(&self, a: TermId, b: TermId) -> bool {
2151        a == b
2152    }
2153
2154    // -- local typing environments -----------------------------------------------------------
2155
2156    /// Intern a local typing environment — `(name, type)` pairs, sorted by name.
2157    ///
2158    /// Two `Context`s that assign the same types to the same names get the same [`EnvId`],
2159    /// whatever order they were built in and whatever else they contain.
2160    ///
2161    /// # Panics
2162    ///
2163    /// Panics if a name appears twice. An environment is a FUNCTION from names to types; two
2164    /// entries for one name is a caller that has not decided which scope it is in, and silently
2165    /// keeping one of them is how a cache starts answering for the wrong context.
2166    pub fn env(&mut self, entries: &[(Symbol, TermId)]) -> EnvId {
2167        let mut sorted = entries.to_vec();
2168        sorted.sort_by_key(|(s, _)| *s);
2169        for w in sorted.windows(2) {
2170            assert_ne!(
2171                w[0].0,
2172                w[1].0,
2173                "a local typing environment assigns one type per name; `{}` was given two",
2174                self.names.resolve(w[0].0)
2175            );
2176        }
2177        let boxed: Box<[(Symbol, TermId)]> = sorted.into_boxed_slice();
2178        if let Some(&id) = self.env_dedup.get(&boxed) {
2179            return id;
2180        }
2181        let id = EnvId(self.envs.len() as u32);
2182        self.envs.push(boxed.clone());
2183        self.env_dedup.insert(boxed, id);
2184        id
2185    }
2186
2187    /// The environment fragment that DECIDES `id`'s type: the types `lookup` assigns to exactly
2188    /// the free variables of `id`, and nothing else.
2189    ///
2190    /// This is the key half that makes an `inferType` cache sound over a NAMED term
2191    /// representation, and it is O(|free variables|) rather than O(|context|) because the free
2192    /// set was computed at construction and is itself hash-consed.
2193    ///
2194    /// Two properties make it the RIGHT key rather than merely a safe one:
2195    ///
2196    /// * A **closed** subterm gets [`EnvId::EMPTY`] under every context, so it is checked ONCE
2197    ///   for the whole call however many scopes it occurs under. That is where the win is.
2198    /// * A variable the context does not bind is OMITTED, not defaulted, so "`x` is unbound" and
2199    ///   "`x : Nat`" are different keys and the cached refusal cannot leak into a scope that
2200    ///   binds it.
2201    pub fn env_for(
2202        &mut self,
2203        id: TermId,
2204        lookup: &mut impl FnMut(&str) -> Option<TermId>,
2205    ) -> EnvId {
2206        let free = self.info[id.index()].free;
2207        let names: Vec<Symbol> = self.set(free).to_vec();
2208        let mut entries: Vec<(Symbol, TermId)> = Vec::with_capacity(names.len());
2209        for &sym in &names {
2210            let resolved = self.names.resolve(sym);
2211            if let Some(ty) = lookup(resolved) {
2212                entries.push((sym, ty));
2213            }
2214        }
2215        self.env(&entries)
2216    }
2217
2218    /// The entries of an interned environment, sorted by name.
2219    pub fn env_entries(&self, e: EnvId) -> &[(Symbol, TermId)] {
2220        &self.envs[e.index()]
2221    }
2222
2223    /// How many environments have been interned — the measure that says a descent is reusing
2224    /// its scopes rather than minting a new one per binder.
2225    pub fn env_count(&self) -> usize {
2226        self.envs.len()
2227    }
2228
2229    // -- interning a `Term` ---------------------------------------------------------------
2230
2231    /// Intern a `Term`. Iterative: a term the native stack could not walk still interns.
2232    pub fn intern(&mut self, term: &Term) -> TermId {
2233        enum Task<'a> {
2234            Enter(&'a Term),
2235            Exit(&'a Term, usize),
2236        }
2237        let mut stack: Vec<Task> = vec![Task::Enter(term)];
2238        let mut out: Vec<TermId> = Vec::new();
2239        let mut kids: Vec<TermId> = Vec::new();
2240        while let Some(task) = stack.pop() {
2241            match task {
2242                Task::Enter(t) => {
2243                    #[cfg(test)]
2244                    INTERN_VISITS.with(|count| count.set(count.get().saturating_add(1)));
2245                    let mut n = 0usize;
2246                    term_for_each_child(t, &mut |_| n += 1);
2247                    stack.push(Task::Exit(t, n));
2248                    term_for_each_child(t, &mut |c| stack.push(Task::Enter(c)));
2249                }
2250                Task::Exit(t, n) => {
2251                    let base = out.len() - n;
2252                    kids.clear();
2253                    kids.extend_from_slice(&out[base..]);
2254                    kids.reverse();
2255                    out.truncate(base);
2256                    let node = self.node_from(t, &kids);
2257                    let id = self.add(node);
2258                    out.push(id);
2259                }
2260            }
2261        }
2262        debug_assert_eq!(out.len(), 1, "interning leaves exactly the root");
2263        out[0]
2264    }
2265
2266    /// Build the flat node for `t` given its already-interned children, in `for_each_child`
2267    /// order. Wildcard-free: a new `Term` variant must answer here.
2268    fn node_from(&mut self, t: &Term, kids: &[TermId]) -> Node {
2269        match t {
2270            Term::Sort(u) => {
2271                let l = self.intern_level(u);
2272                Node::Sort(l)
2273            }
2274            Term::Var(n) => Node::Var(self.names.intern(n)),
2275            Term::Global(n) => Node::Global(self.names.intern(n)),
2276            Term::Const { name, levels } => {
2277                let nm = self.names.intern(name);
2278                let ls: Vec<LevelId> = levels.iter().map(|u| self.intern_level(u)).collect();
2279                Node::Const {
2280                    name: nm,
2281                    levels: ls.into_boxed_slice(),
2282                }
2283            }
2284            Term::Pi { param, .. } => {
2285                let p = self.names.intern(param);
2286                Node::Pi {
2287                    param: p,
2288                    param_type: kids[0],
2289                    body_type: kids[1],
2290                }
2291            }
2292            Term::Lambda { param, .. } => {
2293                let p = self.names.intern(param);
2294                Node::Lambda {
2295                    param: p,
2296                    param_type: kids[0],
2297                    body: kids[1],
2298                }
2299            }
2300            Term::App(_, _) => Node::App(kids[0], kids[1]),
2301            Term::Match { .. } => Node::Match {
2302                discriminant: kids[0],
2303                motive: kids[1],
2304                cases: kids[2..].to_vec().into_boxed_slice(),
2305            },
2306            Term::Fix { name, .. } => {
2307                let n = self.names.intern(name);
2308                Node::Fix {
2309                    name: n,
2310                    body: kids[0],
2311                }
2312            }
2313            Term::MutualFix { defs, index } => {
2314                let ds: Vec<(Symbol, TermId)> = defs
2315                    .iter()
2316                    .zip(kids.iter())
2317                    .map(|((n, _), c)| (self.names.intern(n), *c))
2318                    .collect();
2319                Node::MutualFix {
2320                    defs: ds.into_boxed_slice(),
2321                    index: *index,
2322                }
2323            }
2324            Term::Let { name, .. } => {
2325                let n = self.names.intern(name);
2326                Node::Let {
2327                    name: n,
2328                    ty: kids[0],
2329                    value: kids[1],
2330                    body: kids[2],
2331                }
2332            }
2333            Term::Lit(l) => {
2334                let lid = self.intern_lit(l);
2335                Node::Lit(lid)
2336            }
2337            Term::Hole => Node::Hole,
2338            // The store keys on the ERASED term: an annotated term IS its term's node, so an
2339            // annotation never splits sharing and `intern(annotated) == intern(plain)`.
2340            Term::Annotated { .. } => self.node(kids[0]).clone(),
2341        }
2342    }
2343
2344    /// Materialise a `Term` from an id. Iterative — but note that the `Term` it returns is a
2345    /// TREE, so DROPPING it is native recursion again, and unfolding a DAG can be
2346    /// exponentially larger than the DAG. [`extract_bounded`](TermStore::extract_bounded) is
2347    /// the guarded form.
2348    pub fn extract(&self, id: TermId) -> Term {
2349        enum Task {
2350            Enter(TermId),
2351            Exit(TermId, usize),
2352        }
2353        let mut stack = vec![Task::Enter(id)];
2354        let mut out: Vec<Term> = Vec::new();
2355        while let Some(task) = stack.pop() {
2356            match task {
2357                Task::Enter(t) => {
2358                    let node = &self.nodes[t.index()];
2359                    let n = node.child_count();
2360                    stack.push(Task::Exit(t, n));
2361                    node.for_each_child(&mut |c| stack.push(Task::Enter(c)));
2362                }
2363                Task::Exit(t, n) => {
2364                    let base = out.len() - n;
2365                    let mut kids: Vec<Term> = out.split_off(base);
2366                    kids.reverse();
2367                    out.push(self.term_from(&self.nodes[t.index()], kids));
2368                }
2369            }
2370        }
2371        debug_assert_eq!(out.len(), 1, "extraction leaves exactly the root");
2372        out.pop().expect("extraction leaves exactly the root")
2373    }
2374
2375    /// [`extract`](TermStore::extract), refusing an unfolding bigger than `limit` nodes
2376    /// instead of trying to allocate it.
2377    pub fn extract_bounded(&self, id: TermId, limit: u64) -> Result<Term, TooLarge> {
2378        let size = self.tree_size(id);
2379        if size > limit {
2380            return Err(TooLarge {
2381                tree_size: size,
2382                limit,
2383            });
2384        }
2385        Ok(self.extract(id))
2386    }
2387
2388    fn term_from(&self, node: &Node, kids: Vec<Term>) -> Term {
2389        let mut kids = kids.into_iter();
2390        match node {
2391            Node::Sort(l) => Term::Sort(self.extract_level(*l)),
2392            Node::Var(s) => Term::Var(self.names.resolve(*s).to_string()),
2393            Node::Global(s) => Term::Global(self.names.resolve(*s).to_string()),
2394            Node::Const { name, levels } => Term::Const {
2395                name: self.names.resolve(*name).to_string(),
2396                levels: levels.iter().map(|&l| self.extract_level(l)).collect(),
2397            },
2398            Node::Pi { param, .. } => Term::Pi {
2399                param: self.names.resolve(*param).to_string(),
2400                param_type: Box::new(kids.next().expect("Pi has a domain")),
2401                body_type: Box::new(kids.next().expect("Pi has a codomain")),
2402            },
2403            Node::Lambda { param, .. } => Term::Lambda {
2404                param: self.names.resolve(*param).to_string(),
2405                param_type: Box::new(kids.next().expect("λ has a domain")),
2406                body: Box::new(kids.next().expect("λ has a body")),
2407            },
2408            Node::App(_, _) => Term::App(
2409                Box::new(kids.next().expect("App has a function")),
2410                Box::new(kids.next().expect("App has an argument")),
2411            ),
2412            Node::Match { .. } => Term::Match {
2413                discriminant: Box::new(kids.next().expect("match has a discriminant")),
2414                motive: Box::new(kids.next().expect("match has a motive")),
2415                cases: kids.collect(),
2416            },
2417            Node::Fix { name, .. } => Term::Fix {
2418                name: self.names.resolve(*name).to_string(),
2419                body: Box::new(kids.next().expect("fix has a body")),
2420            },
2421            Node::MutualFix { defs, index } => Term::MutualFix {
2422                defs: defs
2423                    .iter()
2424                    .map(|(n, _)| self.names.resolve(*n).to_string())
2425                    .zip(kids)
2426                    .collect(),
2427                index: *index,
2428            },
2429            Node::Let { name, .. } => Term::Let {
2430                name: self.names.resolve(*name).to_string(),
2431                ty: Box::new(kids.next().expect("let has a type")),
2432                value: Box::new(kids.next().expect("let has a value")),
2433                body: Box::new(kids.next().expect("let has a body")),
2434            },
2435            Node::Lit(l) => Term::Lit(self.extract_lit(*l)),
2436            Node::Hole => Term::Hole,
2437            // Only an α-CANONICAL form holds a `Bound`, and a canonical form is a KEY, not a
2438            // term: this spelling exists so extraction stays total and printable, and
2439            // `intern(extract(canonical))` is deliberately NOT the identity.
2440            Node::Bound(level) => Term::Var(format!("{CANON_OCCURRENCE}{level}⟫")),
2441        }
2442    }
2443
2444    // -- identity-keyed traversal ---------------------------------------------------------
2445
2446    /// Fold over the DAG, children before parents, visiting every DISTINCT node EXACTLY
2447    /// ONCE and remembering the answer in `cache`.
2448    ///
2449    /// This is the shape `infer_type` has to take for a shared subterm to be checked once
2450    /// instead of once per occurrence. Iterative, so depth costs heap and not stack.
2451    pub fn fold<V: Clone>(
2452        &self,
2453        root: TermId,
2454        cache: &mut IdCache<V>,
2455        f: &mut dyn FnMut(&TermStore, TermId, &Node, &[V]) -> V,
2456    ) -> V {
2457        enum Task {
2458            Enter(TermId),
2459            Exit(TermId, usize),
2460        }
2461        let mut stack = vec![Task::Enter(root)];
2462        let mut out: Vec<V> = Vec::new();
2463        let mut kids: Vec<V> = Vec::new();
2464        while let Some(task) = stack.pop() {
2465            match task {
2466                Task::Enter(t) => {
2467                    if let Some(v) = cache.get(t) {
2468                        out.push(v.clone());
2469                        continue;
2470                    }
2471                    let node = &self.nodes[t.index()];
2472                    stack.push(Task::Exit(t, node.child_count()));
2473                    node.for_each_child(&mut |c| stack.push(Task::Enter(c)));
2474                }
2475                Task::Exit(t, n) => {
2476                    let base = out.len() - n;
2477                    kids.clear();
2478                    kids.extend_from_slice(&out[base..]);
2479                    kids.reverse();
2480                    out.truncate(base);
2481                    let v = f(self, t, &self.nodes[t.index()], &kids);
2482                    cache.insert(t, v.clone());
2483                    out.push(v);
2484                }
2485            }
2486        }
2487        out.pop().expect("a fold leaves exactly the root")
2488    }
2489
2490    // -- occurrence counting --------------------------------------------------------------
2491
2492    /// How many FREE occurrences of `name` a subterm has, saturating at `u32::MAX`.
2493    ///
2494    /// Memoised on node identity, so a shared subterm is counted once. This is the measure
2495    /// an occurrence-gated ζ rule needs: a `let` whose name occurs at most once can be
2496    /// expanded for free, and one that occurs more must KEEP the `let` as its sharing node.
2497    pub fn occurrence_count(&mut self, id: TermId, name: Symbol) -> u32 {
2498        enum Task {
2499            Enter(TermId),
2500            Exit(TermId, usize),
2501        }
2502        if let Some(&n) = self.occ_cache.get(&(name, id)) {
2503            return n;
2504        }
2505        let mut stack = vec![Task::Enter(id)];
2506        let mut out: Vec<u32> = Vec::new();
2507        while let Some(task) = stack.pop() {
2508            match task {
2509                Task::Enter(t) => {
2510                    if let Some(&n) = self.occ_cache.get(&(name, t)) {
2511                        out.push(n);
2512                        continue;
2513                    }
2514                    if !self.occurs_free_sym(t, name) {
2515                        self.occ_cache.insert((name, t), 0);
2516                        out.push(0);
2517                        continue;
2518                    }
2519                    let node = &self.nodes[t.index()];
2520                    stack.push(Task::Exit(t, node.child_count()));
2521                    node.for_each_child(&mut |c| stack.push(Task::Enter(c)));
2522                }
2523                Task::Exit(t, n) => {
2524                    let base = out.len() - n;
2525                    let mut kids: Vec<u32> = out.split_off(base);
2526                    kids.reverse();
2527                    let node = &self.nodes[t.index()];
2528                    let total = if let Node::Var(_) = node {
2529                        1
2530                    } else {
2531                        let (bound, scoped) = node.binding();
2532                        let shadows = bound.contains(&name);
2533                        kids.iter()
2534                            .enumerate()
2535                            .filter(|(i, _)| !(shadows && scoped.contains(i)))
2536                            .fold(0u32, |acc, (_, &k)| acc.saturating_add(k))
2537                    };
2538                    self.occ_cache.insert((name, t), total);
2539                    out.push(total);
2540                }
2541            }
2542        }
2543        out.pop()
2544            .expect("occurrence counting leaves exactly the root")
2545    }
2546
2547    /// [`occurrence_count`](TermStore::occurrence_count), classified — all an occurrence gate
2548    /// needs to decide.
2549    pub fn occurrences(&mut self, id: TermId, name: Symbol) -> Occurrences {
2550        match self.occurrence_count(id, name) {
2551            0 => Occurrences::Zero,
2552            1 => Occurrences::Once,
2553            _ => Occurrences::Many,
2554        }
2555    }
2556
2557    // -- substitution ---------------------------------------------------------------------
2558
2559    fn subst_of(&mut self, mut pairs: Vec<(Symbol, TermId)>) -> SubstId {
2560        pairs.sort_by_key(|(s, _)| *s);
2561        pairs.dedup_by_key(|(s, _)| *s);
2562        let dom: Box<[Symbol]> = pairs.iter().map(|(s, _)| *s).collect();
2563        let to: Box<[TermId]> = pairs.iter().map(|(_, t)| *t).collect();
2564        if dom.is_empty() {
2565            return SubstId::EMPTY;
2566        }
2567        if let Some(&id) = self.subst_dedup.get(&(dom.clone(), to.clone())) {
2568            return id;
2569        }
2570        let dom_mask = dom.iter().fold(0u64, |m, &s| m | name_bit(s));
2571        let mut range_free = NameSetId(0);
2572        for &t in to.iter() {
2573            let f = self.info[t.index()].free;
2574            range_free = self.set_union(range_free, f);
2575        }
2576        let id = SubstId(self.substs.len() as u32);
2577        self.substs.push(Subst {
2578            dom: dom.clone(),
2579            to: to.clone(),
2580            dom_mask,
2581            range_free,
2582        });
2583        self.subst_dedup.insert((dom, to), id);
2584        id
2585    }
2586
2587    fn subst_without(&mut self, sid: SubstId, names: &[Symbol]) -> SubstId {
2588        let s = &self.substs[sid.0 as usize];
2589        if names.iter().all(|n| !s.dom.contains(n)) {
2590            return sid;
2591        }
2592        let pairs: Vec<(Symbol, TermId)> = s
2593            .dom
2594            .iter()
2595            .zip(s.to.iter())
2596            .filter(|(d, _)| !names.contains(d))
2597            .map(|(d, t)| (*d, *t))
2598            .collect();
2599        self.subst_of(pairs)
2600    }
2601
2602    fn subst_with(&mut self, sid: SubstId, name: Symbol, to: TermId) -> SubstId {
2603        let s = &self.substs[sid.0 as usize];
2604        let mut pairs: Vec<(Symbol, TermId)> = s
2605            .dom
2606            .iter()
2607            .zip(s.to.iter())
2608            .map(|(d, t)| (*d, *t))
2609            .collect();
2610        pairs.retain(|(d, _)| *d != name);
2611        pairs.push((name, to));
2612        self.subst_of(pairs)
2613    }
2614
2615    /// A name not free in `body`, not bound anywhere in `body`, and not touched by `sid`.
2616    fn fresh_like(&mut self, base: Symbol, body: TermId, sid: SubstId) -> Symbol {
2617        let info = self.info[body.index()];
2618        loop {
2619            self.fresh_counter += 1;
2620            let candidate = format!("{}✦{}", self.names.resolve(base), self.fresh_counter);
2621            let s = self.names.intern(&candidate);
2622            let sub = &self.substs[sid.0 as usize];
2623            let clashes = self.set_contains(info.free, s)
2624                || self.set_contains(info.binders, s)
2625                || self.set_contains(sub.range_free, s)
2626                || sub.dom.contains(&s);
2627            if !clashes {
2628                return s;
2629            }
2630        }
2631    }
2632
2633    /// Capture-avoiding substitution of ONE name.
2634    ///
2635    /// Freshening is STORE-SCOPED: a binder that has to be renamed takes the next name from
2636    /// this store's counter, so the same substitution asked for twice returns the identical id
2637    /// (the memo answers the second time), while the same substitution in a fresh store — or
2638    /// after [`clear_caches`](TermStore::clear_caches) — can pick a different fresh name and
2639    /// return an α-equivalent answer with a different id. Compare such answers with
2640    /// [`alpha_same`](TermStore::alpha_same), never with `==`.
2641    pub fn substitute(&mut self, body: TermId, name: Symbol, value: TermId) -> TermId {
2642        self.note_rename_risk(body, value);
2643        let sid = self.subst_of(vec![(name, value)]);
2644        self.apply(sid, body)
2645    }
2646
2647    /// Record whether this substitution could make a capture-avoiding pass invent a name —
2648    /// `binders(body) ∩ free(value) ≠ ∅`. O(1) when the bloom masks are disjoint, which is the
2649    /// common case; an exact walk of the smaller sorted set otherwise.
2650    fn note_rename_risk(&mut self, body: TermId, value: TermId) {
2651        if self.rename_risk {
2652            return;
2653        }
2654        let binders = self.info[body.index()].binders;
2655        let free = self.info[value.index()].free;
2656        if self.set_mask(binders) & self.set_mask(free) == 0 {
2657            return;
2658        }
2659        let hit = self
2660            .set(free)
2661            .iter()
2662            .any(|&s| self.set_contains(binders, s));
2663        self.rename_risk = hit;
2664    }
2665
2666    /// SIMULTANEOUS capture-avoiding substitution — Lean's `instantiateRev` shape.
2667    ///
2668    /// A whole telescope's arguments go in together, in ONE pass, so instantiating an
2669    /// `n`-binder telescope costs one walk and not `n`. `pairs` is treated as a simultaneous
2670    /// map: no `to` term is ever re-scanned for another pair's name.
2671    pub fn instantiate(&mut self, body: TermId, pairs: &[(Symbol, TermId)]) -> TermId {
2672        for &(_, value) in pairs {
2673            self.note_rename_risk(body, value);
2674        }
2675        let sid = self.subst_of(pairs.to_vec());
2676        self.apply(sid, body)
2677    }
2678
2679    /// Apply an interned substitution. Iterative, memoised on `(substitution, node)`.
2680    ///
2681    /// The line that makes a deep spine linear is the mask test: a node whose free variables
2682    /// cannot meet the substitution's domain is returned UNCHANGED in O(1), so a beta step
2683    /// near the head of a 200,000-deep spine does not re-walk the 199,999 nodes below it.
2684    fn apply(&mut self, sid: SubstId, root: TermId) -> TermId {
2685        enum Op {
2686            Visit(SubstId, TermId),
2687            Build {
2688                sid: SubstId,
2689                id: TermId,
2690                n: usize,
2691                binders: Option<Vec<Symbol>>,
2692            },
2693        }
2694        if sid == SubstId::EMPTY {
2695            return root;
2696        }
2697        let mut stack = vec![Op::Visit(sid, root)];
2698        let mut out: Vec<TermId> = Vec::new();
2699        let mut kids: Vec<TermId> = Vec::new();
2700        while let Some(op) = stack.pop() {
2701            match op {
2702                Op::Visit(s, t) => {
2703                    if s == SubstId::EMPTY {
2704                        out.push(t);
2705                        continue;
2706                    }
2707                    let info = self.info[t.index()];
2708                    let sub_mask = self.substs[s.0 as usize].dom_mask;
2709                    if self.set_mask(info.free) & sub_mask == 0 {
2710                        out.push(t);
2711                        continue;
2712                    }
2713                    if let Some(&r) = self.subst_cache.get(&(s, t)) {
2714                        out.push(r);
2715                        continue;
2716                    }
2717                    let free = self.set(info.free).to_vec();
2718                    let dom = self.substs[s.0 as usize].dom.clone();
2719                    if !free.iter().any(|v| dom.contains(v)) {
2720                        out.push(t);
2721                        continue;
2722                    }
2723                    // A variable in the domain resolves immediately.
2724                    if let Node::Var(v) = self.nodes[t.index()] {
2725                        let sub = &self.substs[s.0 as usize];
2726                        if let Some(pos) = sub.dom.iter().position(|d| *d == v) {
2727                            let r = sub.to[pos];
2728                            self.subst_cache.insert((s, t), r);
2729                            out.push(r);
2730                            continue;
2731                        }
2732                        out.push(t);
2733                        continue;
2734                    }
2735                    let node = self.nodes[t.index()].clone();
2736                    let (bound, scoped) = node.binding();
2737                    // Restrict under the binder, then rename it if the restricted
2738                    // substitution would carry the binder's own name into its scope.
2739                    let mut inner = self.subst_without(s, &bound);
2740                    let mut renamed: Option<Vec<Symbol>> = None;
2741                    if !bound.is_empty() && inner != SubstId::EMPTY {
2742                        // Freshness is judged against the WHOLE node, not one scoped child:
2743                        // a `MutualFix` binds every name in every body, so a name that is
2744                        // safe in body 0 and captured in body 1 would be a silent capture.
2745                        // The node's own free/binder sets are a superset of every child's,
2746                        // which is conservative in the only safe direction.
2747                        let mut new_names = Vec::with_capacity(bound.len());
2748                        let mut any = false;
2749                        for &b in &bound {
2750                            let carries =
2751                                self.set_contains(self.substs[inner.0 as usize].range_free, b);
2752                            if carries {
2753                                let fresh = self.fresh_like(b, t, inner);
2754                                let v = self.mk_var_sym(fresh);
2755                                inner = self.subst_with(inner, b, v);
2756                                new_names.push(fresh);
2757                                any = true;
2758                            } else {
2759                                new_names.push(b);
2760                            }
2761                        }
2762                        if any {
2763                            renamed = Some(new_names);
2764                        }
2765                    }
2766                    let n = node.child_count();
2767                    stack.push(Op::Build {
2768                        sid: s,
2769                        id: t,
2770                        n,
2771                        binders: renamed,
2772                    });
2773                    let mut i = 0usize;
2774                    let mut child_ops = Vec::with_capacity(n);
2775                    node.for_each_child(&mut |c| {
2776                        let cs = if scoped.contains(&i) { inner } else { s };
2777                        child_ops.push(Op::Visit(cs, c));
2778                        i += 1;
2779                    });
2780                    for op in child_ops {
2781                        stack.push(op);
2782                    }
2783                }
2784                Op::Build {
2785                    sid: s,
2786                    id: t,
2787                    n,
2788                    binders,
2789                } => {
2790                    let base = out.len() - n;
2791                    kids.clear();
2792                    kids.extend_from_slice(&out[base..]);
2793                    kids.reverse();
2794                    out.truncate(base);
2795                    let node = self.nodes[t.index()].clone();
2796                    let rebuilt = match &binders {
2797                        Some(names) => node.with_binders(names).rebuild_with(&kids),
2798                        None => node.rebuild_with(&kids),
2799                    };
2800                    let r = self.add(rebuilt);
2801                    self.subst_cache.insert((s, t), r);
2802                    out.push(r);
2803                }
2804            }
2805        }
2806        out.pop().expect("substitution leaves exactly the root")
2807    }
2808
2809    fn spend_operation_work(work: &mut u64, amount: u64) -> Result<(), StoreOperationLimit> {
2810        *work = work.checked_sub(amount).ok_or(StoreOperationLimit::Work)?;
2811        Ok(())
2812    }
2813
2814    /// Conservative cumulative auxiliary-allocation credits, in 64-byte units. The caller
2815    /// must share one finite allowance across all bounded operations in its private store.
2816    /// Credits cover payload copies and amortized collection growth, not exact RSS bytes.
2817    fn admit_operation_allocation(
2818        allocation: &mut u64,
2819        units: u64,
2820    ) -> Result<(), StoreOperationLimit> {
2821        *allocation = allocation
2822            .checked_sub(units)
2823            .ok_or(StoreOperationLimit::Allocation)?;
2824        Ok(())
2825    }
2826
2827    /// Admit a conservative number of new metadata sets before a separately bounded raw
2828    /// operation. This only reads the current dense inventory and allocates nothing.
2829    pub(crate) fn admit_name_set_growth(&self, additional: u64) -> Result<(), StoreOperationLimit> {
2830        Self::admit_name_set_inventory(self.sets.len(), additional)
2831    }
2832
2833    /// Check the dense NameSetId inventory before an operation may intern new sets.
2834    /// Kept separate from allocation credits: a caller can supply an enormous credit budget.
2835    fn admit_name_set_inventory(
2836        current: usize,
2837        additional: u64,
2838    ) -> Result<(), StoreOperationLimit> {
2839        if (current as u64)
2840            .checked_add(additional)
2841            .is_none_or(|n| n > u32::MAX as u64)
2842        {
2843            return Err(StoreOperationLimit::Nodes);
2844        }
2845        Ok(())
2846    }
2847
2848    /// Upper bound for subst_of's repeated range unions, including domain copies/sorting.
2849    fn admit_substitution_ranges(
2850        &self,
2851        sid: Option<SubstId>,
2852        pairs: &[(Symbol, TermId)],
2853        extra: usize,
2854        work: &mut u64,
2855        allocation: &mut u64,
2856    ) -> Result<(), StoreOperationLimit> {
2857        let (mut count, mut names) = (extra as u64, 0u64);
2858        if let Some(sid) = sid {
2859            let sub = &self.substs[sid.0 as usize];
2860            count = count.saturating_add(sub.to.len() as u64);
2861            for &child in sub.to.iter() {
2862                Self::spend_operation_work(work, 1)?;
2863                names = names.saturating_add(self.set(self.info[child.index()].free).len() as u64);
2864            }
2865        }
2866        for &(_, child) in pairs {
2867            Self::spend_operation_work(work, 1)?;
2868            count = count.saturating_add(1);
2869            names = names.saturating_add(self.set(self.info[child.index()].free).len() as u64);
2870        }
2871        // Each range union can intern at most one new set, including a restricted or
2872        // extended substitution. Admit before subst_of creates any intermediate union.
2873        Self::admit_name_set_inventory(self.sets.len(), count)?;
2874        // Each extra fresh variable contributes at most one free symbol.
2875        names = names.saturating_add(extra as u64);
2876        let units = count
2877            .saturating_add(1)
2878            .saturating_mul(names.saturating_add(count).saturating_add(1))
2879            .saturating_mul(8)
2880            .saturating_add(32);
2881        Self::spend_operation_work(work, units)?;
2882        Self::admit_operation_allocation(allocation, units)
2883    }
2884
2885    /// Capture-avoiding simultaneous substitution with admission before traversal/metadata
2886    /// allocation and before each fresh/rebuilt node. On failure the store retains bounded
2887    /// partial memo entries; no partial result is returned. This leaves `instantiate` unchanged.
2888    pub(crate) fn instantiate_bounded(
2889        &mut self,
2890        body: TermId,
2891        pairs: &[(Symbol, TermId)],
2892        work: &mut u64,
2893        node_limit: usize,
2894        allocation: &mut u64,
2895    ) -> Result<TermId, StoreOperationLimit> {
2896        if self.nodes.len() > node_limit
2897            || node_limit > u32::MAX as usize
2898            || self.substs.len() >= node_limit
2899            || self.subst_cache.len() >= node_limit
2900        {
2901            return Err(StoreOperationLimit::Nodes);
2902        }
2903        if pairs.len() > node_limit {
2904            return Err(StoreOperationLimit::Nodes);
2905        }
2906        self.admit_substitution_ranges(None, pairs, 0, work, allocation)?;
2907        // Substitution construction unions the ranges' free-name sets and copies the domain.
2908        Self::spend_operation_work(
2909            work,
2910            ((pairs.len() as u64) + 1).saturating_mul((self.nodes.len() as u64).saturating_add(1)),
2911        )?;
2912        for &(_, value) in pairs {
2913            self.note_rename_risk(body, value);
2914        }
2915        let sid = self.subst_of(pairs.to_vec());
2916        self.apply_bounded(sid, body, work, node_limit, allocation)
2917    }
2918
2919    fn fresh_like_bounded(
2920        &mut self,
2921        base: Symbol,
2922        body: TermId,
2923        sid: SubstId,
2924        work: &mut u64,
2925        allocation: &mut u64,
2926    ) -> Result<Symbol, StoreOperationLimit> {
2927        let info = self.info[body.index()];
2928        loop {
2929            Self::spend_operation_work(work, (self.substs[sid.0 as usize].dom.len() as u64) + 1)?;
2930            self.fresh_counter = self
2931                .fresh_counter
2932                .checked_add(1)
2933                .ok_or(StoreOperationLimit::Work)?;
2934            let bytes = (self.names.resolve(base).len() as u64).saturating_add(32);
2935            Self::spend_operation_work(work, bytes)?;
2936            Self::admit_operation_allocation(allocation, bytes.saturating_add(64))?;
2937            let candidate = format!("{}✦{}", self.names.resolve(base), self.fresh_counter);
2938            let symbol = self.names.intern(&candidate);
2939            let sub = &self.substs[sid.0 as usize];
2940            if !self.set_contains(info.free, symbol)
2941                && !self.set_contains(info.binders, symbol)
2942                && !self.set_contains(sub.range_free, symbol)
2943                && !sub.dom.contains(&symbol)
2944            {
2945                return Ok(symbol);
2946            }
2947        }
2948    }
2949
2950    fn apply_bounded(
2951        &mut self,
2952        sid: SubstId,
2953        root: TermId,
2954        work: &mut u64,
2955        node_limit: usize,
2956        allocation: &mut u64,
2957    ) -> Result<TermId, StoreOperationLimit> {
2958        enum Op {
2959            Visit(SubstId, TermId),
2960            Build {
2961                sid: SubstId,
2962                id: TermId,
2963                n: usize,
2964                binders: Option<Vec<Symbol>>,
2965            },
2966        }
2967        if sid == SubstId::EMPTY {
2968            return Ok(root);
2969        }
2970        Self::admit_operation_allocation(allocation, 16)?;
2971        let mut stack = vec![Op::Visit(sid, root)];
2972        let mut out: Vec<TermId> = Vec::new();
2973        let mut kids: Vec<TermId> = Vec::new();
2974        while let Some(op) = stack.pop() {
2975            Self::spend_operation_work(work, 1)?;
2976            if self.substs.len() >= node_limit || self.subst_cache.len() >= node_limit {
2977                return Err(StoreOperationLimit::Nodes);
2978            }
2979            // Every output/cache insertion below is preceded by capacity and cumulative
2980            // admission. Charge even cache hits conservatively, before Vec/HashMap growth.
2981            if out.len() >= node_limit || stack.len() > node_limit {
2982                return Err(StoreOperationLimit::Nodes);
2983            }
2984            Self::admit_operation_allocation(allocation, 8)?;
2985            match op {
2986                Op::Visit(s, t) => {
2987                    if s == SubstId::EMPTY {
2988                        out.push(t);
2989                        continue;
2990                    }
2991                    let info = self.info[t.index()];
2992                    let sub_mask = self.substs[s.0 as usize].dom_mask;
2993                    if self.set_mask(info.free) & sub_mask == 0 {
2994                        out.push(t);
2995                        continue;
2996                    }
2997                    if let Some(&r) = self.subst_cache.get(&(s, t)) {
2998                        out.push(r);
2999                        continue;
3000                    }
3001                    let free_len = self.set(info.free).len() as u64;
3002                    let dom_len = self.substs[s.0 as usize].dom.len() as u64;
3003                    Self::spend_operation_work(
3004                        work,
3005                        free_len
3006                            .saturating_add(1)
3007                            .saturating_mul(dom_len.saturating_add(1)),
3008                    )?;
3009                    Self::admit_operation_allocation(
3010                        allocation,
3011                        free_len.saturating_add(dom_len).saturating_add(8),
3012                    )?;
3013                    let free = self.set(info.free).to_vec();
3014                    let dom = self.substs[s.0 as usize].dom.clone();
3015                    if !free.iter().any(|v| dom.contains(v)) {
3016                        out.push(t);
3017                        continue;
3018                    }
3019                    // A variable in the domain resolves immediately.
3020                    if let Node::Var(v) = self.nodes[t.index()] {
3021                        let sub = &self.substs[s.0 as usize];
3022                        if let Some(pos) = sub.dom.iter().position(|d| *d == v) {
3023                            let r = sub.to[pos];
3024                            self.subst_cache.insert((s, t), r);
3025                            out.push(r);
3026                            continue;
3027                        }
3028                        out.push(t);
3029                        continue;
3030                    }
3031                    let n = self.nodes[t.index()].child_count();
3032                    if stack
3033                        .len()
3034                        .checked_add(n)
3035                        .and_then(|v| v.checked_add(1))
3036                        .is_none_or(|v| v > node_limit)
3037                        || n > node_limit.saturating_sub(out.len())
3038                    {
3039                        return Err(StoreOperationLimit::Nodes);
3040                    }
3041                    // Covers original-node clone, binding vectors, new_names and child ops.
3042                    Self::spend_operation_work(
3043                        work,
3044                        (n as u64).saturating_mul(8).saturating_add(8),
3045                    )?;
3046                    Self::admit_operation_allocation(
3047                        allocation,
3048                        (n as u64).saturating_mul(16).saturating_add(16),
3049                    )?;
3050                    let node = self.nodes[t.index()].clone();
3051                    let (bound, scoped) = node.binding();
3052                    // Restrict under the binder, then rename it if the restricted
3053                    // substitution would carry the binder's own name into its scope.
3054                    Self::spend_operation_work(
3055                        work,
3056                        (dom_len + 1)
3057                            .saturating_mul((bound.len() as u64).saturating_add(1))
3058                            .saturating_mul((self.nodes.len() as u64).saturating_add(1)),
3059                    )?;
3060                    self.admit_substitution_ranges(Some(s), &[], 0, work, allocation)?;
3061                    let mut inner = self.subst_without(s, &bound);
3062                    let mut renamed: Option<Vec<Symbol>> = None;
3063                    if !bound.is_empty() && inner != SubstId::EMPTY {
3064                        // Freshness is judged against the WHOLE node, not one scoped child:
3065                        // a `MutualFix` binds every name in every body, so a name that is
3066                        // safe in body 0 and captured in body 1 would be a silent capture.
3067                        // The node's own free/binder sets are a superset of every child's,
3068                        // which is conservative in the only safe direction.
3069                        let mut new_names = Vec::with_capacity(bound.len());
3070                        let mut any = false;
3071                        for &b in &bound {
3072                            let carries =
3073                                self.set_contains(self.substs[inner.0 as usize].range_free, b);
3074                            if carries {
3075                                let fresh =
3076                                    self.fresh_like_bounded(b, t, inner, work, allocation)?;
3077                                if self.nodes.len() >= node_limit {
3078                                    return Err(StoreOperationLimit::Nodes);
3079                                }
3080                                Self::spend_operation_work(work, 64)?;
3081                                Self::admit_operation_allocation(allocation, 64)?;
3082                                let v = self.add_bounded(
3083                                    Node::Var(fresh),
3084                                    work,
3085                                    node_limit,
3086                                    allocation,
3087                                )?;
3088                                Self::spend_operation_work(
3089                                    work,
3090                                    ((self.substs[inner.0 as usize].dom.len() as u64) + 2)
3091                                        .saturating_mul(
3092                                            (self.nodes.len() as u64).saturating_add(1),
3093                                        ),
3094                                )?;
3095                                if self.substs.len() >= node_limit {
3096                                    return Err(StoreOperationLimit::Nodes);
3097                                }
3098                                if self.substs[inner.0 as usize].dom.len() >= node_limit {
3099                                    return Err(StoreOperationLimit::Nodes);
3100                                }
3101                                self.admit_substitution_ranges(
3102                                    Some(inner),
3103                                    &[],
3104                                    1,
3105                                    work,
3106                                    allocation,
3107                                )?;
3108                                inner = self.subst_with(inner, b, v);
3109                                new_names.push(fresh);
3110                                any = true;
3111                            } else {
3112                                new_names.push(b);
3113                            }
3114                        }
3115                        if any {
3116                            renamed = Some(new_names);
3117                        }
3118                    }
3119                    let n = node.child_count();
3120                    stack.push(Op::Build {
3121                        sid: s,
3122                        id: t,
3123                        n,
3124                        binders: renamed,
3125                    });
3126                    let mut i = 0usize;
3127                    let mut child_ops = Vec::with_capacity(n);
3128                    node.for_each_child(&mut |c| {
3129                        let cs = if scoped.contains(&i) { inner } else { s };
3130                        child_ops.push(Op::Visit(cs, c));
3131                        i += 1;
3132                    });
3133                    for op in child_ops {
3134                        stack.push(op);
3135                    }
3136                }
3137                Op::Build {
3138                    sid: s,
3139                    id: t,
3140                    n,
3141                    binders,
3142                } => {
3143                    let base = out.len() - n;
3144                    Self::admit_operation_allocation(
3145                        allocation,
3146                        (n as u64).saturating_mul(8).saturating_add(16),
3147                    )?;
3148                    kids.clear();
3149                    kids.extend_from_slice(&out[base..]);
3150                    kids.reverse();
3151                    out.truncate(base);
3152                    Self::spend_operation_work(
3153                        work,
3154                        (n as u64).saturating_mul(8).saturating_add(8),
3155                    )?;
3156                    let node = self.nodes[t.index()].clone();
3157                    let rebuilt = match &binders {
3158                        Some(names) => node.with_binders(names).rebuild_with(&kids),
3159                        None => node.rebuild_with(&kids),
3160                    };
3161                    // Admission precedes compute_info, dedup insertion and all node allocation.
3162                    if self.nodes.len() >= node_limit {
3163                        return Err(StoreOperationLimit::Nodes);
3164                    }
3165                    let r = self.add_bounded(rebuilt, work, node_limit, allocation)?;
3166                    self.subst_cache.insert((s, t), r);
3167                    out.push(r);
3168                }
3169            }
3170        }
3171        Ok(out.pop().expect("substitution leaves exactly the root"))
3172    }
3173
3174    // -- α-canonical identity -------------------------------------------------------------
3175
3176    /// The α-CANONICAL form of a term: every binder name replaced by one reserved name and
3177    /// every bound occurrence by its de Bruijn LEVEL, so two terms are α-equivalent exactly
3178    /// when their canonical forms are the SAME id.
3179    ///
3180    /// This is what makes the store useful to a NAMED kernel. `same(a, b)` is structural
3181    /// equality, and the kernel's conversion checker does not want structural equality — it
3182    /// wants α. `type_checker::is_subtype` opens with an `alpha_eq(a, b)` fast path that walks
3183    /// both trees on EVERY conversion, which is the hottest path in the kernel; against a
3184    /// canonical id it is one integer compare plus a memo hit.
3185    ///
3186    /// Free variables keep their names, so `λx. y` and `λx. z` stay distinct — α renames BOUND
3187    /// names only.
3188    ///
3189    /// Levels rather than indices: a level-canonical subterm depends on how deep it sits, so
3190    /// `λx. x` canonicalises differently under one binder than under two, and the memo holds
3191    /// one entry per (environment, level, node). De Bruijn INDICES would make a closed subterm's
3192    /// canonical form position-independent and therefore shared everywhere it occurs — strictly
3193    /// more sharing, at the price of an abstraction pass that shifts. Recorded rather than done.
3194    pub fn alpha_id(&mut self, root: TermId) -> TermId {
3195        if let Some(&c) = self.alpha_cache.get(&root) {
3196            return c;
3197        }
3198        let c = self.canonicalise(SubstId::EMPTY, 0, root);
3199        self.alpha_cache.insert(root, c);
3200        c
3201    }
3202
3203    /// Whether two terms are α-equivalent — ONE integer compare, after each side's canonical
3204    /// form has been computed once.
3205    pub fn alpha_same(&mut self, a: TermId, b: TermId) -> bool {
3206        if a == b {
3207            return true;
3208        }
3209        // Alpha-renaming preserves shape and global references. These interned
3210        // annotations prove inequality without canonicalizing either DAG.
3211        // Saturated sizes are only a negative filter: equality proves nothing.
3212        let (a_info, b_info) = (self.info[a.index()], self.info[b.index()]);
3213        if a_info.depth != b_info.depth
3214            || a_info.tree_size != b_info.tree_size
3215            || a_info.consts != b_info.consts
3216        {
3217            return false;
3218        }
3219        self.alpha_id(a) == self.alpha_id(b)
3220    }
3221
3222    /// Whether a term is already its own α-canonical form — that is, whether canonicalising
3223    /// changes nothing. True for anything that binds nothing, and also for a term whose binders
3224    /// already carry the reserved name and bind no occurrence.
3225    pub fn is_alpha_canonical(&mut self, id: TermId) -> bool {
3226        self.alpha_id(id) == id
3227    }
3228
3229    fn canonicalise(&mut self, env: SubstId, level: u32, root: TermId) -> TermId {
3230        enum Op {
3231            Visit(SubstId, u32, TermId),
3232            Build {
3233                key: (SubstId, u32, TermId),
3234                n: usize,
3235                binders: usize,
3236            },
3237        }
3238        let canon_binder = self.symbol(CANON_BINDER);
3239        let mut stack = vec![Op::Visit(env, level, root)];
3240        let mut out: Vec<TermId> = Vec::new();
3241        let mut kids: Vec<TermId> = Vec::new();
3242        while let Some(op) = stack.pop() {
3243            match op {
3244                Op::Visit(e, l, t) => {
3245                    let info = self.info[t.index()];
3246                    // Already canonical: nothing below binds, and nothing below refers to a
3247                    // binder above. O(1), and it is the common case for a leaf or an atom.
3248                    let binds_nothing = self.set(info.binders).is_empty();
3249                    let untouched = e == SubstId::EMPTY
3250                        || self.set_mask(info.free) & self.substs[e.0 as usize].dom_mask == 0;
3251                    if binds_nothing && untouched {
3252                        out.push(t);
3253                        continue;
3254                    }
3255                    // Outer renaming cannot affect a subtree whose free names
3256                    // miss its domain. Inner binders install their own mappings.
3257                    // Preserve l: a closed subtree's new Bound levels still
3258                    // depend on its position, even with an empty environment.
3259                    let e = if untouched { SubstId::EMPTY } else { e };
3260                    if let Some(&c) = self.canon_cache.get(&(e, l, t)) {
3261                        out.push(c);
3262                        continue;
3263                    }
3264                    if let Node::Var(v) = self.nodes[t.index()] {
3265                        let sub = &self.substs[e.0 as usize];
3266                        let r = match sub.dom.iter().position(|d| *d == v) {
3267                            Some(pos) => sub.to[pos],
3268                            None => t,
3269                        };
3270                        self.canon_cache.insert((e, l, t), r);
3271                        out.push(r);
3272                        continue;
3273                    }
3274                    let node = self.nodes[t.index()].clone();
3275                    let (bound, scoped) = node.binding();
3276                    // Each bound name takes the NEXT level, in `binding()` order — so a mutual
3277                    // block's names occupy consecutive levels and a shadowing name overwrites
3278                    // the outer entry, which is exactly what makes the innermost binder win.
3279                    let mut inner = e;
3280                    for (i, &b) in bound.iter().enumerate() {
3281                        let marker = self.add(Node::Bound(l + i as u32));
3282                        inner = self.subst_with(inner, b, marker);
3283                    }
3284                    let inner_level = l + bound.len() as u32;
3285                    let n = node.child_count();
3286                    stack.push(Op::Build {
3287                        key: (e, l, t),
3288                        n,
3289                        binders: bound.len(),
3290                    });
3291                    let mut i = 0usize;
3292                    let mut child_ops = Vec::with_capacity(n);
3293                    node.for_each_child(&mut |c| {
3294                        if scoped.contains(&i) {
3295                            child_ops.push(Op::Visit(inner, inner_level, c));
3296                        } else {
3297                            child_ops.push(Op::Visit(e, l, c));
3298                        }
3299                        i += 1;
3300                    });
3301                    for op in child_ops {
3302                        stack.push(op);
3303                    }
3304                }
3305                Op::Build { key, n, binders } => {
3306                    let base = out.len() - n;
3307                    kids.clear();
3308                    kids.extend_from_slice(&out[base..]);
3309                    kids.reverse();
3310                    out.truncate(base);
3311                    let node = self.nodes[key.2.index()].clone();
3312                    let renamed = if binders == 0 {
3313                        node.rebuild_with(&kids)
3314                    } else {
3315                        let names = vec![canon_binder; binders];
3316                        node.with_binders(&names).rebuild_with(&kids)
3317                    };
3318                    let r = self.add(renamed);
3319                    self.canon_cache.insert(key, r);
3320                    out.push(r);
3321                }
3322            }
3323        }
3324        out.pop().expect("canonicalisation leaves exactly the root")
3325    }
3326
3327    // -- the measurement instrument -------------------------------------------------------
3328
3329    /// ζ-expand a `Let` at the head, whatever its occurrence count — what a conversion check
3330    /// must be able to do on demand once a `Let` is allowed to survive into a normal form.
3331    pub fn zeta_expand_head(&mut self, id: TermId) -> Option<TermId> {
3332        if let Node::Let {
3333            name, value, body, ..
3334        } = self.nodes[id.index()]
3335        {
3336            Some(self.substitute(body, name, value))
3337        } else {
3338            None
3339        }
3340    }
3341
3342    /// β/ζ normal form, with ζ OCCURRENCE-GATED: a `let` whose name occurs at most once in
3343    /// the normalised body is expanded, and one that occurs more KEEPS the `let` as its
3344    /// sharing node.
3345    ///
3346    /// The gate is re-evaluated AFTER the body is normalised, because reduction can erase
3347    /// occurrences — a body with three uses of `n` before reduction and one after must still
3348    /// expand.
3349    ///
3350    /// # The rule is `zeta_or_share_opt`'s, all three cases of it
3351    ///
3352    /// The occurrence count is only two of the three. The third is the SPECULATION: nothing
3353    /// inside a held binding stepping is not the same as the binding having no redex, because
3354    /// ζ is always available and firing it can unblock a rule that could not see past the
3355    /// binder. `let x := λz. y in x x` is stuck as long as the binding is held and reduces to
3356    /// `y` the moment it is expanded. So a held binding whose parts are all normal is EXPANDED
3357    /// SPECULATIVELY and kept only when its expansion is itself a normal form — which is the
3358    /// corpus's `quad b b b b`, whose head is a declaration with no body.
3359    ///
3360    /// The speculation is not re-entered while one is running, exactly as
3361    /// `zeta_or_share_opt`'s `Speculating` guard does it: a nest of `k` shared bindings would
3362    /// otherwise be `2^k` probes rather than a walk.
3363    ///
3364    /// # Why the rebuild is re-examined
3365    ///
3366    /// Normalising a node's children can put a redex AT the node: `(let b := λu. u in e) arg`
3367    /// holds its binding while `e` mentions `b` twice, and if normalising `e` drops that to one
3368    /// the binding expands to a λ — and the application above it, already past its own weak-head
3369    /// pass, is a β-redex nobody would look at again. So a rebuild whose head can move is sent
3370    /// back through the loop rather than accepted.
3371    ///
3372    /// NOT A KERNEL TIER: β and ζ only. No δ, no ι, no η, no `Fix`, no universe algebra.
3373    pub fn beta_zeta_normalize(&mut self, root: TermId, budget: u64) -> StoreNormalForm {
3374        enum Op {
3375            Norm(TermId),
3376            Build {
3377                orig: TermId,
3378                head: TermId,
3379                n: usize,
3380            },
3381            Seal(TermId),
3382            /// The held-binding speculation's verdict point: `expanded`'s normal form is on
3383            /// top of the output stack, and it decides whether `held` stands.
3384            Decide {
3385                orig: TermId,
3386                head: TermId,
3387                held: TermId,
3388                expanded: TermId,
3389            },
3390        }
3391        let mut fuel = budget;
3392        // `zeta_or_share_opt`'s `Speculating` guard, as a local rather than a thread-local:
3393        // this loop is the only thing that can re-enter it.
3394        let mut probing = false;
3395        let mut stack = vec![Op::Norm(root)];
3396        let mut out: Vec<TermId> = Vec::new();
3397        let mut kids: Vec<TermId> = Vec::new();
3398        while let Some(op) = stack.pop() {
3399            match op {
3400                Op::Norm(id) => {
3401                    // Keyed on the speculation flag too: a normal form computed while the
3402                    // speculation was suppressing nested probes is NOT the normal form the
3403                    // unsuppressed rule gives, and handing one back for the other is how a
3404                    // memo quietly changes an answer.
3405                    if let Some(&v) = self.nf_cache.get(&(id, probing)) {
3406                        out.push(v);
3407                        continue;
3408                    }
3409                    let head = match self.whnf(id, &mut fuel) {
3410                        Some(h) => h,
3411                        None => return StoreNormalForm::OutOfFuel { budget },
3412                    };
3413                    if let Some(&v) = self.nf_cache.get(&(head, probing)) {
3414                        self.nf_cache.insert((id, probing), v);
3415                        out.push(v);
3416                        continue;
3417                    }
3418                    let node = self.nodes[head.index()].clone();
3419                    stack.push(Op::Build {
3420                        orig: id,
3421                        head,
3422                        n: node.child_count(),
3423                    });
3424                    let mut child_ops = Vec::with_capacity(node.child_count());
3425                    node.for_each_child(&mut |c| child_ops.push(Op::Norm(c)));
3426                    for op in child_ops {
3427                        stack.push(op);
3428                    }
3429                }
3430                Op::Build { orig, head, n } => {
3431                    let base = out.len() - n;
3432                    kids.clear();
3433                    kids.extend_from_slice(&out[base..]);
3434                    kids.reverse();
3435                    out.truncate(base);
3436                    let node = self.nodes[head.index()].clone();
3437                    let rebuilt = self.add(node.rebuild_with(&kids));
3438                    // A child moved and left a redex at this node — see the doc above.
3439                    if rebuilt != head && self.head_has_redex(rebuilt) {
3440                        stack.push(Op::Seal(orig));
3441                        stack.push(Op::Seal(head));
3442                        stack.push(Op::Norm(rebuilt));
3443                        continue;
3444                    }
3445                    // The one shape whose gate can flip once its children are normal.
3446                    if let Node::Let {
3447                        name, value, body, ..
3448                    } = self.nodes[rebuilt.index()]
3449                    {
3450                        if self.occurrences(body, name) != Occurrences::Many {
3451                            if fuel == 0 {
3452                                return StoreNormalForm::OutOfFuel { budget };
3453                            }
3454                            fuel -= 1;
3455                            let expanded = self.substitute(body, name, value);
3456                            stack.push(Op::Seal(orig));
3457                            stack.push(Op::Seal(head));
3458                            stack.push(Op::Norm(expanded));
3459                            continue;
3460                        }
3461                        if !probing {
3462                            if fuel == 0 {
3463                                return StoreNormalForm::OutOfFuel { budget };
3464                            }
3465                            fuel -= 1;
3466                            let expanded = self.substitute(body, name, value);
3467                            probing = true;
3468                            stack.push(Op::Decide {
3469                                orig,
3470                                head,
3471                                held: rebuilt,
3472                                expanded,
3473                            });
3474                            stack.push(Op::Norm(expanded));
3475                            continue;
3476                        }
3477                    }
3478                    self.nf_cache.insert((orig, probing), rebuilt);
3479                    self.nf_cache.insert((head, probing), rebuilt);
3480                    out.push(rebuilt);
3481                }
3482                Op::Decide {
3483                    orig,
3484                    head,
3485                    held,
3486                    expanded,
3487                } => {
3488                    probing = false;
3489                    let probe = out.pop().expect("a verdict follows a normalised expansion");
3490                    if probe == expanded {
3491                        // The expansion is already a normal form, so holding the binding loses
3492                        // nothing and saves every copy of the value.
3493                        self.nf_cache.insert((orig, false), held);
3494                        self.nf_cache.insert((head, false), held);
3495                        self.nf_cache.insert((held, false), held);
3496                        out.push(held);
3497                    } else {
3498                        // ζ fires. The expansion is normalised AGAIN, outside the speculation,
3499                        // because the probe ran with nested probes suppressed and its answer is
3500                        // therefore only an oracle — never the result.
3501                        stack.push(Op::Seal(orig));
3502                        stack.push(Op::Seal(head));
3503                        stack.push(Op::Seal(held));
3504                        stack.push(Op::Norm(expanded));
3505                    }
3506                }
3507                Op::Seal(id) => {
3508                    let v = *out.last().expect("a seal follows a normalised result");
3509                    self.nf_cache.insert((id, probing), v);
3510                }
3511            }
3512        }
3513        StoreNormalForm::Reached(out.pop().expect("normalisation leaves exactly the root"))
3514    }
3515
3516    /// Whether a weak-head pass could still move this term — the head of its application spine
3517    /// is a λ with an argument, or a `let` that is applied or whose gate now says expand.
3518    ///
3519    /// Used to decide whether a REBUILT node has to go back through the loop. It is exact, not
3520    /// conservative, which is what keeps the re-examination from being an extra pass over every
3521    /// node of every term.
3522    fn head_has_redex(&mut self, id: TermId) -> bool {
3523        let mut h = id;
3524        let mut applied = false;
3525        while let Node::App(f, _) = self.nodes[h.index()] {
3526            h = f;
3527            applied = true;
3528        }
3529        match self.nodes[h.index()] {
3530            Node::Lambda { .. } => applied,
3531            Node::Let { name, body, .. } => {
3532                applied || self.occurrences(body, name) != Occurrences::Many
3533            }
3534            _ => false,
3535        }
3536    }
3537
3538    /// Weak head normal form under β and occurrence-gated ζ. `None` means the budget ran out.
3539    fn whnf(&mut self, id: TermId, fuel: &mut u64) -> Option<TermId> {
3540        let mut cur = id;
3541        let mut args: Vec<TermId> = Vec::new();
3542        loop {
3543            args.clear();
3544            let mut head = cur;
3545            while let Node::App(f, a) = self.nodes[head.index()] {
3546                args.push(a);
3547                head = f;
3548            }
3549            args.reverse();
3550            match self.nodes[head.index()] {
3551                Node::Lambda { param, body, .. } if !args.is_empty() => {
3552                    if *fuel == 0 {
3553                        return None;
3554                    }
3555                    *fuel -= 1;
3556                    let substituted = self.substitute(body, param, args[0]);
3557                    cur = args[1..]
3558                        .iter()
3559                        .fold(substituted, |acc, &a| self.mk_app(acc, a));
3560                }
3561                Node::Let {
3562                    name, value, body, ..
3563                } => {
3564                    if self.occurrences(body, name) == Occurrences::Many {
3565                        return Some(cur);
3566                    }
3567                    if *fuel == 0 {
3568                        return None;
3569                    }
3570                    *fuel -= 1;
3571                    let expanded = self.substitute(body, name, value);
3572                    cur = args.iter().fold(expanded, |acc, &a| self.mk_app(acc, a));
3573                }
3574                _ => return Some(cur),
3575            }
3576        }
3577    }
3578}
3579
3580/// Visit a `Term`'s children in the canonical order — the `Term`-side twin of
3581/// [`Node::for_each_child`], and wildcard-free for the same reason.
3582fn term_for_each_child<'a>(t: &'a Term, f: &mut impl FnMut(&'a Term)) {
3583    match t {
3584        Term::Sort(_)
3585        | Term::Var(_)
3586        | Term::Global(_)
3587        | Term::Const { .. }
3588        | Term::Lit(_)
3589        | Term::Hole => {}
3590        Term::Pi {
3591            param_type,
3592            body_type,
3593            ..
3594        } => {
3595            f(param_type);
3596            f(body_type);
3597        }
3598        Term::Lambda {
3599            param_type, body, ..
3600        } => {
3601            f(param_type);
3602            f(body);
3603        }
3604        Term::App(fun, arg) => {
3605            f(fun);
3606            f(arg);
3607        }
3608        Term::Match {
3609            discriminant,
3610            motive,
3611            cases,
3612        } => {
3613            f(discriminant);
3614            f(motive);
3615            for c in cases.iter() {
3616                f(c);
3617            }
3618        }
3619        Term::Fix { body, .. } => f(body),
3620        Term::MutualFix { defs, .. } => {
3621            for (_, b) in defs.iter() {
3622                f(b);
3623            }
3624        }
3625        Term::Let {
3626            ty, value, body, ..
3627        } => {
3628            f(ty);
3629            f(value);
3630            f(body);
3631        }
3632        Term::Annotated { term, .. } => f(term),
3633    }
3634}
3635
3636// ================================================================================================
3637// THE `Term`-SIDE PRIMITIVES — what the TREE kernel needs BEFORE any of this is wired in
3638// ================================================================================================
3639//
3640// Everything above prices the store. The three functions below are the parts of it the kernel
3641// can use TODAY, against the `Box`-recursive `Term` it already has, with no representation
3642// change and no call-site rewrite. Each replaces a NATIVELY RECURSIVE walk that has a depth
3643// ceiling of its own, and each is the primitive one specific corpus case is blocked on.
3644
3645/// Which of `t`'s children a binder on `t` hides `name` inside.
3646///
3647/// The `Term`-side twin of [`Node::binding`], written from the same specification — a binder
3648/// scopes over its BODY and not over its annotation or its bound value — and wildcard-free for
3649/// the same reason: a new `Term` variant has to answer here rather than be silently unbound.
3650enum Shadow {
3651    /// Nothing on this node binds `name`.
3652    None,
3653    /// The child at this index is inside a binder that rebinds `name`.
3654    Child(usize),
3655    /// Every child is — a mutual block binds all of its names over all of its bodies.
3656    Every,
3657}
3658
3659fn term_shadow(t: &Term, name: &str) -> Shadow {
3660    match t {
3661        Term::Pi { param, .. } | Term::Lambda { param, .. } => {
3662            if param == name {
3663                Shadow::Child(1)
3664            } else {
3665                Shadow::None
3666            }
3667        }
3668        Term::Let { name: bound, .. } => {
3669            if bound == name {
3670                Shadow::Child(2)
3671            } else {
3672                Shadow::None
3673            }
3674        }
3675        Term::Fix { name: bound, .. } => {
3676            if bound == name {
3677                Shadow::Child(0)
3678            } else {
3679                Shadow::None
3680            }
3681        }
3682        Term::MutualFix { defs, .. } => {
3683            if defs.iter().any(|(n, _)| n == name) {
3684                Shadow::Every
3685            } else {
3686                Shadow::None
3687            }
3688        }
3689        Term::Sort(_)
3690        | Term::Var(_)
3691        | Term::Global(_)
3692        | Term::Const { .. }
3693        | Term::Lit(_)
3694        | Term::Hole
3695        | Term::App(_, _)
3696        | Term::Match { .. }
3697        | Term::Annotated { .. } => Shadow::None,
3698    }
3699}
3700
3701#[cfg(test)]
3702thread_local! {
3703    static FREE_NAME_NODE_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
3704}
3705
3706#[cfg(test)]
3707pub(crate) fn take_free_name_node_visits() -> usize {
3708    FREE_NAME_NODE_VISITS.with(|n| n.replace(0))
3709}
3710
3711/// Collect exact free local names without recursion or materializing another term.
3712/// Domains and let values are outside their binder's scope; mutual definitions
3713/// share all binders. Counts preserve nested shadowing and repeated mutual names.
3714pub(crate) fn free_names_in_term(term: &Term) -> std::collections::HashSet<String> {
3715    let mut free = std::collections::HashSet::<&str>::new();
3716    visit_free_names_and_holes(term, |name| {
3717        if let Some(name) = name {
3718            free.insert(name);
3719        }
3720        true
3721    });
3722    free.into_iter().map(str::to_owned).collect()
3723}
3724
3725/// Whole-term closedness, including annotations and unselected branches.
3726/// The first free local or hole refuses admission; no graph or name set is built.
3727pub(crate) fn is_closed_without_holes(term: &Term) -> bool {
3728    visit_free_names_and_holes(term, |_| false)
3729}
3730
3731// Some(name) is a free local; None is a hole. Returning false stops the walk.
3732fn visit_free_names_and_holes<'a>(
3733    term: &'a Term,
3734    mut visit: impl FnMut(Option<&'a str>) -> bool,
3735) -> bool {
3736    enum Work<'a> {
3737        Visit(&'a Term),
3738        Bind(&'a str),
3739        Unbind(&'a str),
3740    }
3741    use Work::*;
3742    let mut work = vec![Visit(term)];
3743    let mut bound = std::collections::HashMap::<&str, usize>::new();
3744    while let Some(item) = work.pop() {
3745        let t = match item {
3746            Bind(name) => {
3747                *bound.entry(name).or_default() += 1;
3748                continue;
3749            }
3750            Unbind(name) => {
3751                let count = bound.get_mut(name).expect("balanced lexical scope");
3752                *count -= 1;
3753                if *count == 0 {
3754                    bound.remove(name);
3755                }
3756                continue;
3757            }
3758            Visit(t) => t,
3759        };
3760        #[cfg(test)]
3761        FREE_NAME_NODE_VISITS.with(|n| n.set(n.get() + 1));
3762        match t {
3763            Term::Var(name) => {
3764                if !bound.contains_key(name.as_str()) && !visit(Some(name)) {
3765                    return false;
3766                }
3767            }
3768            Term::Hole => {
3769                if !visit(None) {
3770                    return false;
3771                }
3772            }
3773            Term::Lambda {
3774                param,
3775                param_type,
3776                body,
3777            }
3778            | Term::Pi {
3779                param,
3780                param_type,
3781                body_type: body,
3782            } => {
3783                work.extend([Unbind(param), Visit(body), Bind(param), Visit(param_type)]);
3784            }
3785            Term::Let {
3786                name,
3787                ty,
3788                value,
3789                body,
3790            } => {
3791                work.extend([
3792                    Unbind(name),
3793                    Visit(body),
3794                    Bind(name),
3795                    Visit(value),
3796                    Visit(ty),
3797                ]);
3798            }
3799            Term::Fix { name, body } => {
3800                work.extend([Unbind(name), Visit(body), Bind(name)]);
3801            }
3802            Term::MutualFix { defs, .. } => {
3803                for (name, _) in defs {
3804                    work.push(Unbind(name));
3805                }
3806                for (_, body) in defs {
3807                    work.push(Visit(body));
3808                }
3809                for (name, _) in defs {
3810                    work.push(Bind(name));
3811                }
3812            }
3813            Term::App(_, _) | Term::Match { .. } | Term::Annotated { .. } => {
3814                term_for_each_child(t, &mut |child| work.push(Visit(child)));
3815            }
3816            Term::Sort(_) | Term::Global(_) | Term::Const { .. } | Term::Lit(_) => {}
3817        }
3818    }
3819    true
3820}
3821
3822/// Free occurrences of `name` in `body`, stopping once `stop_at` have been seen.
3823fn count_occurrences(body: &Term, name: &str, stop_at: u32) -> u32 {
3824    let mut seen = 0u32;
3825    let mut work: Vec<&Term> = vec![body];
3826    while let Some(t) = work.pop() {
3827        #[cfg(test)]
3828        FREE_NAME_NODE_VISITS.with(|n| n.set(n.get() + 1));
3829        if let Term::Var(v) = t {
3830            if v == name {
3831                seen = seen.saturating_add(1);
3832                if seen >= stop_at {
3833                    return seen;
3834                }
3835            }
3836            continue;
3837        }
3838        let hidden = match term_shadow(t, name) {
3839            Shadow::None => None,
3840            Shadow::Child(i) => Some(i),
3841            Shadow::Every => continue,
3842        };
3843        let mut i = 0usize;
3844        term_for_each_child(t, &mut |c| {
3845            if Some(i) != hidden {
3846                work.push(c);
3847            }
3848            i += 1;
3849        });
3850    }
3851    seen
3852}
3853
3854/// How many FREE occurrences of `name` a `Term` has, saturating at `u32::MAX`.
3855///
3856/// ITERATIVE, so it answers about a term too deep for the native stack.
3857/// `type_checker::body_stats` computes the same number by native recursion and therefore has a
3858/// ceiling of its own; the unit tests at the bottom of this file pin the two against each other
3859/// on every shape, so they can never drift.
3860pub fn occurrence_count_in_term(body: &Term, name: &str) -> u32 {
3861    #[cfg(test)]
3862    OCCURRENCE_HEAP_WALKS.with(|n| n.set(n.get() + 1));
3863    count_occurrences(body, name, u32::MAX)
3864}
3865
3866/// [`occurrence_count_in_term`], classified — everything an occurrence-gated ζ rule needs, and
3867/// strictly CHEAPER than the count, because it stops at the second occurrence.
3868///
3869/// This is the gate `reduction.rs`'s `Term::Let` arm and `type_checker.rs`'s `Term::Let` rule
3870/// are missing. Both ζ-substitute UNCONDITIONALLY today, so a value bound once and used n times
3871/// is written n times and checked n times — which is exactly what
3872/// `lean_conformance_t00_terms::a_shared_subterm_is_checked_once_and_not_once_per_occurrence` measures.
3873pub fn occurrences_in_term(body: &Term, name: &str) -> Occurrences {
3874    match count_occurrences(body, name, 2) {
3875        0 => Occurrences::Zero,
3876        1 => Occurrences::Once,
3877        _ => Occurrences::Many,
3878    }
3879}
3880
3881/// Whether `name` occurs free in `body` at all — stops at the first occurrence.
3882pub fn occurs_free_in_term(body: &Term, name: &str) -> bool {
3883    count_occurrences(body, name, 1) > 0
3884}
3885
3886thread_local! {
3887    /// Whether this thread is inside a SPECULATIVE ζ expansion — the probe
3888    /// [`zeta_or_share_opt`] runs to decide whether holding a shared binding back would stop
3889    /// computation.
3890    ///
3891    /// Without it the probe is EXPONENTIAL in the nesting depth of stuck shared `Let`s: each
3892    /// arm steps its body (one descent) and then steps its expansion (a second descent through
3893    /// the same nest), so `T(k) = 2·T(k−1)`. A term nesting thirty of them would hang the
3894    /// reducer inside a single step, which is a denial of service on adversarial input rather
3895    /// than a slow answer.
3896    ///
3897    /// Inside the probe a nested shared binding therefore answers with the verdict the rule
3898    /// itself gives it: a shared `Let` whose parts are all normal IS the normal form, so it is
3899    /// NOT a redex. That makes "normal form" an inductive definition — a term is a SHARED normal
3900    /// form when nothing in it steps and every surviving binding's one-step ζ expansion is
3901    /// itself one — and the probe becomes one walk per level, `T(k) = T(k−1) + O(k·size)`.
3902    ///
3903    /// The price, stated exactly: a redex that appears only when TWO nested bindings are
3904    /// expanded at once is not found, and `normalize` stops at the shared form instead of the
3905    /// fully-expanded one. That is the same bargain the whole rule makes — a surviving `Let` is
3906    /// a `Let` conversion must ζ-expand on demand — and it is the INCOMPLETE direction, never
3907    /// the wrong one. The alternative is a probe that is exponential in nesting depth, which is
3908    /// a hang inside a single reduction step on input a caller chooses.
3909    static ZETA_SPECULATING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3910}
3911
3912/// Restores the speculation flag on the way out, including on a panic.
3913struct Speculating(bool);
3914
3915impl Drop for Speculating {
3916    fn drop(&mut self) {
3917        ZETA_SPECULATING.with(|f| f.set(self.0));
3918    }
3919}
3920
3921impl Speculating {
3922    fn enter() -> Self {
3923        Speculating(ZETA_SPECULATING.with(|f| f.replace(true)))
3924    }
3925
3926    fn already() -> bool {
3927        ZETA_SPECULATING.with(|f| f.get())
3928    }
3929}
3930
3931/// ONE ζ step, OCCURRENCE-GATED, against a reducer that always returns a `Term`.
3932///
3933/// **[`zeta_or_share_opt`] is the one to wire in.** `reduction.rs` no longer has a
3934/// `Term`-returning `reduce_step_at`; its reducer is `reduce_step_opt -> Option<Term>`, and the
3935/// progress-reporting form is both simpler and stricter. This form stays because a step function
3936/// that cannot say "no redex" is still a legitimate shape — the in-file fixpoint driver uses it
3937/// — and because the two must be provably the same rule, which
3938/// `the_two_zeta_gates_are_the_same_rule` pins on every shape.
3939///
3940/// The rule, in three cases:
3941///
3942/// * The name occurs at most ONCE in the body → ζ fires exactly as it does today,
3943///   `body[name := value]`, on the UNREDUCED value. Byte-identical to the current arm, which is
3944///   what keeps every `let_zeta.rs` case and the T00 ζ-transparency cases green: all of them are
3945///   single-occurrence.
3946/// * The name occurs MORE → the binding is KEPT as the sharing node and `step` is run on the
3947///   annotation, the value and the body instead. The `Let` survives into the normal form, so a
3948///   value bound once and used n times is written ONCE rather than n times. That is
3949///   `a_shared_subterm_is_checked_once_and_not_once_per_occurrence`: the corpus's term is 329
3950///   nodes under the current arm and 92 under this one, against a ceiling of 97.
3951/// * When nothing inside can step any further the gate is RE-EVALUATED on the reduced body,
3952///   because reduction ERASES occurrences: a body with three uses of `n` before reduction and one
3953///   after must still expand, or `a_dependent_let_body_types_only_because_zeta_runs_before_
3954///   inference` goes red.
3955///
3956/// # What the caller owes in return
3957///
3958/// A `Let` that survives into a normal form is a `Let` that conversion has to see through.
3959/// `def_eq`/`types_equal`/`is_subtype` must ζ-expand a `Let` HEAD on demand —
3960/// [`zeta_expand_head_term`] is that step — or two convertible terms, one of which happens to
3961/// have kept its binding, will compare unequal. That is the negative test to write first.
3962pub fn zeta_or_share(
3963    name: &str,
3964    ty: &Term,
3965    value: &Term,
3966    body: &Term,
3967    step: &dyn Fn(&Term) -> Term,
3968) -> Term {
3969    if occurrences_in_term(body, name) != Occurrences::Many {
3970        return crate::type_checker::substitute(body, name, value);
3971    }
3972    let reduced_ty = step(ty);
3973    let reduced_value = step(value);
3974    let reduced_body = step(body);
3975    let unchanged = reduced_ty == *ty && reduced_value == *value && reduced_body == *body;
3976    if unchanged {
3977        // See [`zeta_or_share_opt`]: nothing inside stepping is NOT the same as no redex, and
3978        // the binding may be held back only when its expansion is itself a normal form.
3979        if !Speculating::already() {
3980            let expanded = crate::type_checker::substitute(body, name, value);
3981            let _probe = Speculating::enter();
3982            if step(&expanded) != expanded {
3983                return expanded;
3984            }
3985        }
3986    }
3987    Term::Let {
3988        name: name.to_string(),
3989        ty: Box::new(reduced_ty),
3990        value: Box::new(reduced_value),
3991        body: Box::new(reduced_body),
3992    }
3993}
3994
3995/// [`zeta_or_share`] against a reducer that reports PROGRESS: `step` answers `None` when there
3996/// is no redex. `reduction.rs`'s `reduce_step_opt` calls it for every `Term::Let`.
3997///
3998/// ```
3999/// use logicaffeine_kernel::term_store::zeta_or_share_opt;
4000/// use logicaffeine_kernel::Term;
4001///
4002/// let v = |n: &str| Term::Var(n.into());
4003/// let g = |n: &str| Term::Global(n.into());
4004/// let app = |f: Term, x: Term| Term::App(Box::new(f), Box::new(x));
4005/// let normal = |_: &Term| None;
4006///
4007/// // Used once: ζ fires on the unreduced value.
4008/// let once = app(g("Succ"), v("x"));
4009/// assert_eq!(
4010///     zeta_or_share_opt("x", &g("Nat"), &g("Zero"), &once, &normal),
4011///     Some(app(g("Succ"), g("Zero"))),
4012/// );
4013///
4014/// // Used twice, nothing inside steps and neither does the expansion: the shared `Let` is the
4015/// // normal form.
4016/// let twice = app(app(g("quad"), v("x")), v("x"));
4017/// assert_eq!(zeta_or_share_opt("x", &g("Nat"), &g("Zero"), &twice, &normal), None);
4018/// ```
4019///
4020/// The rule, in four cases, and note that NONE of them needs a structural comparison:
4021///
4022/// * The name occurs at most ONCE in the body → ζ fires exactly as it did before,
4023///   `Some(body[name := value])` on the UNREDUCED value. Byte-identical to the old arm, which is
4024///   what keeps every `let_zeta.rs` case and the T00 ζ-transparency cases green: all of them are
4025///   single-occurrence.
4026/// * The name occurs MORE and something inside still steps → the binding is KEPT as the sharing
4027///   node and the step is reported on whichever of the annotation, value and body moved. A value
4028///   bound once and used n times is written ONCE instead of n times, which is
4029///   `a_shared_subterm_is_checked_once_and_not_once_per_occurrence`: the corpus's term is 329
4030///   nodes under the old arm and **92** under this one, against a ceiling of 97.
4031/// * The name occurs MORE, nothing inside steps, and the one-step ζ EXPANSION still has a redex
4032///   → `Some(expansion)`. **"Nothing inside steps" is NOT "no redex".** ζ is always available,
4033///   and firing it can unblock a rule that could not see past the binding:
4034///   `let twice := 4 in plus twice twice` is stuck only because `plus`'s structural argument is
4035///   a VARIABLE, and expanding it lets ι unroll to `8`. Getting this wrong took
4036///   `lean_conformance_t02_declarations::a_let_rec_is_a_fix_bound_by_a_let_and_a_plain_let_cannot_
4037///   carry_it` — recorded `Native` — from green to a stuck `Let`, along with six other tests in
4038///   five different files; `W_perf_extra::the_zeta_gate_expands_a_shared_binding_whose_
4039///   expansion_can_still_compute` is the lock.
4040/// * The name occurs MORE, nothing inside steps, and the expansion has no redex either → `None`.
4041///   The shared `Let` IS the normal form — the corpus's `quad b b b b`, whose head is a
4042///   declaration with no body and which cannot step however `b` is spelled — said exactly rather
4043///   than recovered from `reduced == current`, so the fixpoint in `normalize_checked_at`
4044///   terminates on the nose and never pays a Θ(size) compare to find out.
4045///
4046/// The probe is run at most ONE level deep (see `ZETA_SPECULATING`): inside it, a nested shared
4047/// binding answers with the verdict the rule gives it — a shared `Let` whose parts are all
4048/// normal is not a redex — because an unguarded probe is `T(k) = 2·T(k−1)` in the nesting depth
4049/// of stuck shared bindings, which is a hang inside a single reduction step on input a caller
4050/// chooses. `W_perf_extra::a_nest_of_shared_bindings_does_not_make_the_zeta_probe_exponential`
4051/// is the lock, at sixteen levels.
4052///
4053/// The gate is RE-READ for free: reduction erases occurrences, and a body that stepped comes back
4054/// through this same arm on the next pass, where a name now used once expands. That is what keeps
4055/// `a_dependent_let_body_types_only_because_zeta_runs_before_inference` green — its body has
4056/// three uses of `n` before reduction and one after.
4057///
4058/// # What the caller owes in return
4059///
4060/// A `Let` that survives into a normal form is a `Let` that conversion has to see through.
4061/// `def_eq`/`types_equal`/`is_subtype` must ζ-expand a `Let` HEAD on demand —
4062/// [`zeta_expand_head_term`] is that step — or two convertible terms, one of which happens to
4063/// have kept its binding, will compare unequal. `def_eq(let b := v in body, body[b := v])` is the
4064/// RED test to write BEFORE touching the arm.
4065pub fn zeta_or_share_opt(
4066    name: &str,
4067    ty: &Term,
4068    value: &Term,
4069    body: &Term,
4070    step: &dyn Fn(&Term) -> Option<Term>,
4071) -> Option<Term> {
4072    if occurrences_in_term(body, name) != Occurrences::Many {
4073        return Some(crate::type_checker::substitute(body, name, value));
4074    }
4075    let reduced_ty = step(ty);
4076    let reduced_value = step(value);
4077    let reduced_body = step(body);
4078    if reduced_ty.is_none() && reduced_value.is_none() && reduced_body.is_none() {
4079        // NOTHING INSIDE STEPS IS NOT THE SAME AS NO REDEX. ζ is always available, and firing it
4080        // can unblock a rule that could not see past the binding: `let twice := 4 in plus twice
4081        // twice` is stuck only because `plus`'s structural argument is a VARIABLE, and expanding
4082        // it lets ι unroll. So the binding is kept only when its EXPANSION is itself a normal
4083        // form — which is the corpus's `quad b b b b`, whose head is a declaration with no body
4084        // and which cannot step however `b` is spelled.
4085        if Speculating::already() {
4086            // Inside an outer probe: a shared binding whose parts are all normal IS the normal
4087            // form, and answering anything else collapses the whole nest. See ZETA_SPECULATING.
4088            return None;
4089        }
4090        let expanded = crate::type_checker::substitute(body, name, value);
4091        let _probe = Speculating::enter();
4092        return step(&expanded).map(|_| expanded);
4093    }
4094    Some(Term::Let {
4095        name: name.to_string(),
4096        ty: Box::new(reduced_ty.unwrap_or_else(|| ty.clone())),
4097        value: Box::new(reduced_value.unwrap_or_else(|| value.clone())),
4098        body: Box::new(reduced_body.unwrap_or_else(|| body.clone())),
4099    })
4100}
4101
4102/// [`zeta_or_share_opt`] on a `Let` the caller owns, stepping its parts where they lie: the same
4103/// rule, in the same order, with the same answer, but a part that a step leaves alone is never
4104/// copied and nothing is rebuilt around the one that moved. `step_in_place` must be the in-place
4105/// twin of `step` (`reduce_step_in_place` of `reduce_step_opt`), which
4106/// `reduction::owned_normalize_controls` pins shape by shape. `false` for anything not `Let`.
4107pub fn zeta_or_share_in_place(
4108    term: &mut Term,
4109    step_in_place: &dyn Fn(&mut Term) -> bool,
4110    step: &dyn Fn(&Term) -> Option<Term>,
4111) -> bool {
4112    let Term::Let {
4113        name,
4114        ty,
4115        value,
4116        body,
4117    } = term
4118    else {
4119        return false;
4120    };
4121    if occurrences_in_term(body, name) != Occurrences::Many {
4122        let Term::Let {
4123            name, value, body, ..
4124        } = std::mem::replace(term, Term::Hole)
4125        else {
4126            unreachable!()
4127        };
4128        *term = crate::type_checker::substitute_owned(*body, &name, &value);
4129        return true;
4130    }
4131    let ty_stepped = step_in_place(ty);
4132    let value_stepped = step_in_place(value);
4133    let body_stepped = step_in_place(body);
4134    if ty_stepped || value_stepped || body_stepped {
4135        return true;
4136    }
4137    if Speculating::already() {
4138        return false;
4139    }
4140    let expanded = crate::type_checker::substitute(body, name, value);
4141    let _probe = Speculating::enter();
4142    if step(&expanded).is_some() {
4143        *term = expanded;
4144        return true;
4145    }
4146    false
4147}
4148
4149/// ζ-expand a `Let` at the head, whatever its occurrence count — the step a conversion check
4150/// must be able to take on demand once [`zeta_or_share`] lets a `Let` survive into a normal
4151/// form. `None` for anything that is not `Let`-headed.
4152///
4153/// The `Term`-side twin of [`TermStore::zeta_expand_head`].
4154pub fn zeta_expand_head_term(t: &Term) -> Option<Term> {
4155    match t {
4156        Term::Let {
4157            name, value, body, ..
4158        } => Some(crate::type_checker::substitute(body, name, value)),
4159        _ => None,
4160    }
4161}
4162
4163/// Whether syntax is within the kernel's existing term-depth limit.
4164///
4165/// This is a pure admission predicate, not a typing judgment or a kernel/TLS
4166/// entry. It does not establish that recursive work already has a sized stack.
4167pub fn within_kernel_depth(term: &Term) -> bool {
4168    !crate::type_checker::depth_exceeds(term, crate::type_checker::MAX_TERM_DEPTH)
4169}
4170
4171/// The nesting depth of a `Term`, counted in EDGES: a leaf is 0.
4172///
4173/// This is the DOOR's measure — `type_checker::depth_exceeds(t, n)` is exactly
4174/// `term_depth(t) > n` — and it is one less than [`TermStore::depth`], which counts a leaf as
4175/// one rung. The unit tests at the bottom of this file pin both relations, so the three
4176/// measures cannot drift apart silently.
4177pub fn term_depth(t: &Term) -> usize {
4178    let mut best = 0usize;
4179    let mut work: Vec<(&Term, usize)> = vec![(t, 0)];
4180    while let Some((node, d)) = work.pop() {
4181        if d > best {
4182            best = d;
4183        }
4184        let deeper = d + 1;
4185        term_for_each_child(node, &mut |c| work.push((c, deeper)));
4186    }
4187    best
4188}
4189
4190/// **THE PURE β/ζ DOOR.** The `Term`-level entry point a tree reducer wires in so a β-heavy
4191/// reduction stops being quadratic — and the only one it needs, because it answers `None` for
4192/// every term it is not certain about.
4193///
4194/// `Some(nf)` is a promise that `nf` is EXACTLY what a full kernel reduction would have
4195/// produced: the same term, not merely an α-equivalent one. `None` means "not mine" and the
4196/// caller falls through to the path it already has, unchanged.
4197///
4198/// # Why this is the fix and a faster loop is not
4199///
4200/// `lean_conformance_t00_terms::a_deep_spine_costs_linearly_in_its_depth_and_not_quadratically` asks
4201/// for at most 5× when a spine's depth TRIPLES. A tree reducer pays Θ(size) per step — the β
4202/// step copies the remaining spine, and the fixpoint comparison walks it — over Θ(size) steps,
4203/// so it is Θ(size²) and tripling costs about 9×. A ratio is immune to constant factors, so no
4204/// amount of tuning reaches the bar; the representation has to change. On this store every
4205/// step is `u32` work against hash-consed nodes, and the same reduction is linear.
4206/// `W_perf_extra::w_perf_measure_the_spine_in_the_tree_and_in_the_store` prints both columns at
4207/// the corpus's own two depths, and
4208/// `W_perf_extra::the_pure_door_triples_the_corpus_spine_in_under_five` asserts the bar on this
4209/// path.
4210///
4211/// # What it refuses, and why each refusal is exact
4212///
4213/// The store implements β and occurrence-gated ζ. It implements nothing else, so it must never
4214/// be handed a term another rule could touch. Six shapes are refused in O(1) from
4215/// [`TermStore::shapes`]:
4216///
4217/// * `Match` — ι lives there.
4218/// * `Fix` and `MutualFix` — unrolling asks `termination::structural_columns` which column the
4219///   recursion descends on; the store cannot call it.
4220/// * `Const` — δ with a level substitution, plus the well-formedness test that turns a
4221///   mis-levelled reference into `Term::Hole` rather than into a normal form.
4222/// * `Lit` — the arithmetic primitives fire only when the ARGUMENT is a literal, so a term with
4223///   no literal anywhere cannot reach one.
4224/// * `Hole` — not a term of the calculus; the type checker refuses it by construction.
4225///
4226/// And every `Global` name in the term must satisfy `inert`. That predicate is the CALLER's
4227/// because only the caller has the `Context`: a name is inert when δ cannot unfold it
4228/// (`ctx.get_definition_body(name).is_none()`) **and** it is not one of the names a rule is
4229/// keyed on rather than a shape — the reflection builtins `syn_*`, the native-evaluation hook
4230/// `reduceBool`, and the quotient eliminators `Quot_lift`/`Quot_ind`. The caller must also
4231/// refuse the whole door when its context has any registered `@[simp]` rule, since a rewrite
4232/// can fire on any shape at all.
4233///
4234/// # The two remaining honesty conditions
4235///
4236/// * **Budget.** `budget` counts β and ζ steps. Exhausting it is `None`, never a term passed
4237///   off as normal — the same distinction `normalize_checked` draws between a normal form and a
4238///   spent budget.
4239/// * **No invented names, on EITHER side.** Capture-avoiding substitution renames binders, and
4240///   an answer that renamed is α-equivalent to the kernel's rather than equal to it. Both
4241///   implementations rename on the same criterion — the binder's name is a free variable of the
4242///   replacement — so where that criterion never fires they are the SAME FUNCTION. Where it
4243///   does fire they can still differ, because `type_checker::substitute_avoiding` descends the
4244///   WHOLE body and renames such a binder even in a subtree the substituted variable never
4245///   reaches, while this store descends only where the substitution is live. So the door
4246///   refuses on [`TermStore::rename_risk`] — set the moment any substitution pushes a value
4247///   into a body that binds one of the value's free names — and, as a second and independent
4248///   check, on [`TermStore::binders_within`].
4249pub fn beta_zeta_normal_form(
4250    term: &Term,
4251    inert: &dyn Fn(&str) -> bool,
4252    budget: u64,
4253) -> Option<Term> {
4254    let mut store = TermStore::new();
4255    let root = store.intern(term);
4256    if !store.is_beta_zeta_pure(root, inert) {
4257        return None;
4258    }
4259    let StoreNormalForm::Reached(nf) = store.beta_zeta_normalize(root, budget) else {
4260        return None;
4261    };
4262    if store.rename_risk() || !store.binders_within(nf, root) {
4263        return None;
4264    }
4265    store.extract_bounded(nf, EXTRACT_CEILING).ok()
4266}
4267
4268/// [`beta_zeta_normal_form`] with every policy question answered from the `Context` — the form a
4269/// reducer wires in, with nothing left for the caller to get wrong.
4270///
4271/// # The inertness criterion, and why it is POSITIVE
4272///
4273/// A `Global` is admitted only when it is a CONSTRUCTOR or an INDUCTIVE and carries no
4274/// definition body. That is a closed, positive criterion, and it is chosen over the obvious
4275/// negative one — "not a definition and not one of these builtin names" — because the negative
4276/// one is a list that a future rule can silently fall off the end of.
4277///
4278/// It is sufficient, rule by rule, against `reduction::reduce_step_opt`:
4279///
4280/// * δ cannot fire: a constructor and an inductive have no body, and the belt-and-braces
4281///   `get_definition_body(name).is_none()` says so a second time.
4282/// * The arithmetic primitives cannot fire: `try_primitive_reduce` returns `None` unless the
4283///   ARGUMENT is a `Term::Lit`, and a `Lit` anywhere is refused by shape.
4284/// * ι cannot fire: it is the `Match` rule, refused by shape.
4285/// * Fixpoint unrolling cannot fire: it needs a `Fix` head, refused by shape.
4286/// * Every NAME-keyed rule — the reflection builtins `syn_*`, the tactic builtins `try_*` and
4287///   `tact_*`, `induction_*`, `concludes`, the native-evaluation hook `reduceBool`, and the
4288///   quotient eliminators `Quot_lift`/`Quot_ind` — is registered with `add_declaration` and is
4289///   therefore neither a constructor nor an inductive. `W_perf_extra::
4290///   the_conservative_inertness_criterion_refuses_every_name_keyed_rule_head` executes that
4291///   claim over the whole list rather than asserting it in prose.
4292/// * A registered `@[simp]` rewrite can fire on ANY shape, so the door is shut outright
4293///   whenever the context has one.
4294///
4295/// # Transparency does not enter
4296///
4297/// `Transparency::Excluding` only ever makes δ LESS available, and the criterion already admits
4298/// no term δ could touch. So the answer is the same at every transparency, which is why this
4299/// takes no transparency argument.
4300pub fn beta_zeta_normal_form_in_context(
4301    ctx: &crate::Context,
4302    term: &Term,
4303    budget: u64,
4304) -> Option<Term> {
4305    if !ctx.simp_rules().is_empty() {
4306        return None;
4307    }
4308    let inert = |name: &str| {
4309        (ctx.is_constructor(name) || ctx.is_inductive(name))
4310            && ctx.get_definition_body(name).is_none()
4311    };
4312    let mut store = TermStore::new();
4313    let root = store.intern(term);
4314    if !store.is_beta_zeta_pure(root, &inert) {
4315        return None;
4316    }
4317    let StoreNormalForm::Reached(nf) = store.beta_zeta_normalize(root, budget) else {
4318        return None;
4319    };
4320    // A compact DAG can grow a normal form deeper than the input. The tree
4321    // normalizer's callers still clone/drop recursively, so keep its depth door.
4322    if store.depth(nf) as usize > crate::type_checker::MAX_TERM_DEPTH + 1
4323        || store.rename_risk()
4324        || !store.binders_within(nf, root)
4325    {
4326        return None;
4327    }
4328    store.extract_bounded(nf, EXTRACT_CEILING).ok()
4329}
4330
4331/// Positive beta/zeta conversion over a shared graph, leaving every global,
4332/// constant, match, and recursive head opaque. These need not be full normal
4333/// forms: a common reduct proves equality even when other rules also apply.
4334/// No result or cache escapes this call, and no expanded tree is materialized.
4335pub(crate) fn beta_zeta_equivalent(ctx: &crate::Context, a: &Term, b: &Term, budget: u64) -> bool {
4336    if budget == 0 || !ctx.simp_rules().is_empty() {
4337        return false;
4338    }
4339    let mut store = TermStore::new();
4340    let a = store.intern(a);
4341    let b = store.intern(b);
4342    if [a, b]
4343        .into_iter()
4344        .any(|id| store.shapes(id).contains(NodeShape::Hole) || !store.eq_faithful(id))
4345    {
4346        return false;
4347    }
4348    let StoreNormalForm::Reached(a) = store.beta_zeta_normalize(a, budget) else {
4349        return false;
4350    };
4351    let StoreNormalForm::Reached(b) = store.beta_zeta_normalize(b, budget) else {
4352        return false;
4353    };
4354    store.alpha_same(a, b)
4355}
4356
4357/// A positive conversion shortcut for the beta/zeta/delta fragment.
4358///
4359/// Results stay in the shared store: a deep normal form is never extracted into a
4360/// recursively owned tree. Only equality proves anything; unsupported shapes,
4361/// cycles, budget exhaustion, and unequal results all defer to ordinary conversion.
4362/// The store and its caches live for this call and this immutable context only.
4363pub(crate) fn beta_delta_equivalent(ctx: &crate::Context, a: &Term, b: &Term, budget: u64) -> bool {
4364    if budget == 0 || !ctx.simp_rules().is_empty() {
4365        return false;
4366    }
4367    fn expand(
4368        store: &mut TermStore,
4369        ctx: &crate::Context,
4370        root: TermId,
4371        fuel: &mut u64,
4372        done: &mut HashMap<TermId, TermId>,
4373    ) -> Option<TermId> {
4374        enum Op {
4375            Visit(TermId),
4376            Build(TermId, usize),
4377            Seal(TermId),
4378        }
4379        let mut work = vec![Op::Visit(root)];
4380        let mut out = Vec::new();
4381        let mut active = std::collections::HashSet::new();
4382        while let Some(op) = work.pop() {
4383            match op {
4384                Op::Visit(id) => {
4385                    if let Some(&result) = done.get(&id) {
4386                        out.push(result);
4387                        continue;
4388                    }
4389                    if *fuel == 0 || !active.insert(id) {
4390                        return None;
4391                    }
4392                    *fuel -= 1;
4393                    #[cfg(test)]
4394                    DELTA_EXPANSION_WORK.with(|counter| {
4395                        if let Some((visits, matches)) = counter.get() {
4396                            counter.set(Some((
4397                                visits + 1,
4398                                matches
4399                                    + usize::from(matches!(
4400                                        &store.nodes[id.index()],
4401                                        Node::Match { .. }
4402                                    )),
4403                            )));
4404                        }
4405                    });
4406                    let node = store.nodes[id.index()].clone();
4407                    match &node {
4408                        Node::Global(sym) => {
4409                            let name = store.names.resolve(*sym).to_string();
4410                            // A bare global does not instantiate a universe telescope.
4411                            if ctx
4412                                .poly_declaration(&name)
4413                                .is_some_and(|(params, _)| !params.is_empty())
4414                            {
4415                                return None;
4416                            }
4417                            if let Some(body) = ctx.get_definition_body(&name) {
4418                                let body = store.intern(body);
4419                                // Delta unfolding beneath a binder requires a closed body.
4420                                if !store.free_vars(body).is_empty() {
4421                                    return None;
4422                                }
4423                                let shapes = store.shapes(body);
4424                                if shapes.contains(NodeShape::Hole) || !store.float_free(body) {
4425                                    return None;
4426                                }
4427                                let supported = ShapeMask::BETA_ZETA
4428                                    .with(NodeShape::Const)
4429                                    .with(NodeShape::Lit);
4430                                if shapes.subset_of(supported) {
4431                                    work.push(Op::Seal(id));
4432                                    work.push(Op::Visit(body));
4433                                    continue;
4434                                }
4435                                // Outside this fragment, keep the original named
4436                                // definition. Equality may need no delta step here;
4437                                // ordinary conversion handles required evaluation.
4438                            }
4439                            // No supported visible body: retain the exact opaque name. A
4440                            // shared reduct proves conversion without requiring
4441                            // this leaf to be a constructor or a full normal form.
4442                        }
4443                        // Explicit constants remain exact opaque leaves. Do not
4444                        // instantiate levels or look up a same-named Global body;
4445                        // independent typing still validates reference arity.
4446                        Node::Const { .. }
4447                        | Node::Sort(_)
4448                        | Node::Var(_)
4449                        | Node::Pi { .. }
4450                        | Node::Lambda { .. }
4451                        | Node::App(..)
4452                        | Node::Let { .. } => {}
4453                        // Retain exact literal tags and payloads without evaluating
4454                        // them. Keep floats outside expansion even when a definition
4455                        // introduces one after the original input admission.
4456                        Node::Lit(lit)
4457                            if !matches!(store.lits[lit.index()], LitKey::FloatBits(_)) => {}
4458                        _ => return None,
4459                    }
4460                    work.push(Op::Build(id, node.child_count()));
4461                    node.for_each_child(&mut |child| work.push(Op::Visit(child)));
4462                }
4463                Op::Build(id, count) => {
4464                    let mut children = out.split_off(out.len() - count);
4465                    children.reverse();
4466                    let node = store.nodes[id.index()].rebuild_with(&children);
4467                    let result = store.add(node);
4468                    done.insert(id, result);
4469                    active.remove(&id);
4470                    out.push(result);
4471                }
4472                Op::Seal(id) => {
4473                    let result = *out.last()?;
4474                    done.insert(id, result);
4475                    active.remove(&id);
4476                }
4477            }
4478        }
4479        out.pop()
4480    }
4481    let mut store = TermStore::new();
4482    let (source_a, source_b) = (a, b);
4483    let a = store.intern(source_a);
4484    let b = store.intern(source_b);
4485    if [a, b]
4486        .into_iter()
4487        .any(|id| store.shapes(id).contains(NodeShape::Hole) || !store.eq_faithful(id))
4488    {
4489        return false;
4490    }
4491    // Stop at a shared intermediate term when one closed delta step proves
4492    // equality. Expanding both remaining tails first makes every generated
4493    // equation in a long declaration chain pay for all subsequent definitions.
4494    // Reuse the bounded, capture-safe delta probe; only exact alpha equality is
4495    // evidence. An unavailable/oversized step leaves the original gateway below.
4496    for (source, target) in [(source_a, b), (source_b, a)] {
4497        if let Some((_, unfolded)) = crate::reduction::bounded_delta_head(ctx, source) {
4498            let unfolded = store.intern(&unfolded);
4499            if !store.shapes(unfolded).contains(NodeShape::Hole)
4500                && store.eq_faithful(unfolded)
4501                && store.alpha_same(unfolded, target)
4502            {
4503                return true;
4504            }
4505        }
4506    }
4507    let mut fuel = budget;
4508    // Both operands inhabit this one store and immutable context. Reuse only
4509    // completed expansions: active nodes stay local to each walk, and any
4510    // failure ends the entire call. Every memo entry required a charged visit,
4511    // so this call's existing expansion allowance also bounds the memo size.
4512    // No context-sensitive evidence survives the call or its universe policy.
4513    let mut expanded = HashMap::new();
4514    let Some(a) = expand(&mut store, ctx, a, &mut fuel, &mut expanded) else {
4515        return false;
4516    };
4517    let Some(b) = expand(&mut store, ctx, b, &mut fuel, &mut expanded) else {
4518        return false;
4519    };
4520    drop(expanded);
4521    let StoreNormalForm::Reached(a) = store.beta_zeta_normalize(a, fuel) else {
4522        return false;
4523    };
4524    let StoreNormalForm::Reached(b) = store.beta_zeta_normalize(b, fuel) else {
4525        return false;
4526    };
4527    store.alpha_same(a, b)
4528}
4529
4530/// The tree size beyond which [`beta_zeta_normal_form`] refuses to materialise its answer.
4531///
4532/// A DAG can denote a tree far larger than the machine holds, and the door's whole promise is
4533/// that it hands back a `Term` a tree reducer could have built for itself. Sixteen million nodes
4534/// is four orders of magnitude past anything the corpus builds and still bounded, so the refusal
4535/// is a `None` rather than an allocator death.
4536const EXTRACT_CEILING: u64 = 1 << 24;
4537
4538// ================================================================================================
4539// THE KERNEL'S OWN STACK
4540// ================================================================================================
4541//
4542// The depth door promises a REFUSAL — "the term is neither accepted nor rejected, because
4543// descending it would exhaust the native stack" — and delivers a dead process for every term
4544// between the ceiling it actually has and the one it advertises.
4545//
4546// MEASURED on this box, debug build, libtest's 2 MiB thread:
4547//
4548// * `infer_type`'s frame is 16,832 bytes. The corpus's sixty-deep dependent telescope dies at
4549//   125 nested frames: 125 × 16,832 = 2,104,000 B against 2,097,152 B of stack, to the byte.
4550//   So the real ceiling is a term depth of about 123.
4551// * `MAX_TERM_DEPTH` is 1600. The door is thirteen times too permissive, and every term in the
4552//   band 124..=1600 passes it and then ABORTS THE PROCESS — the exact failure it exists to
4553//   prevent. `W_perf_extra::the_kernels_real_depth_ceiling_is_far_below_the_door_it_advertises`
4554//   bisects the number in a child process rather than asserting a doc comment.
4555// * Raising `MAX_TERM_DEPTH` cannot fix it; LOWERING it to ~120 would refuse the 1500-deep
4556//   β-spine the corpus normalises successfully today. The constant is not the problem: the
4557//   kernel is walking a term of unbounded depth on a stack somebody else chose.
4558//
4559// So the kernel takes its own. [`deepen_stack`] measures the term ONCE at the outermost entry
4560// — the same linear pass the door already pays — and, when the term is deeper than a stack it
4561// was handed can be trusted for, runs the whole call on a scoped thread whose stack is SIZED
4562// FROM THAT DEPTH. Nested entries see a thread-local flag and cost one `Cell` read.
4563//
4564// This is better than Lean's answer, not merely different: Lean takes a fixed global thread
4565// stack (`--tstack`) and throws "deep recursion was detected" above it, so its ceiling is a
4566// launch-time guess that is simultaneously too small for one proof and too large for every
4567// other. Sizing per call spends the reservation only on the terms that need it, and leaves the
4568// door meaning what it says.
4569
4570/// The deepest term the kernel will walk on a stack it did not size.
4571///
4572/// Set from the measurement above with a wide margin: the deepest walk measured, `infer_type`,
4573/// fits about 123 rungs in 2 MiB, and the kernel nests other recursive walks inside it
4574/// (`normalize` → `reduce_step` → `substitute_avoiding` → `freshen`), so the inline band is set
4575/// at a quarter of the shallowest ceiling any single walk has.
4576pub const INLINE_DEPTH: usize = 32;
4577
4578/// Stack reserved per rung of nesting past [`INLINE_DEPTH`].
4579///
4580/// 48 KiB is roughly three `infer_type` frames per rung of term depth, measured against a
4581/// telescope that spends about one frame per rung. The reservation is virtual: Linux commits a
4582/// thread stack page by page, so a deep term reserves address space and touches only what it
4583/// uses.
4584pub const BYTES_PER_RUNG: usize = 48 * 1024;
4585
4586/// The floor under any stack the kernel sizes for itself, whatever the term's depth — room for
4587/// the non-recursive part of a kernel call, and for a panic to unwind and format its message.
4588pub const BASE_STACK_BYTES: usize = 2 * 1024 * 1024;
4589
4590/// How many bytes of stack a term of this depth is given.
4591pub fn stack_bytes_for_depth(depth: usize) -> usize {
4592    BASE_STACK_BYTES.saturating_add(depth.saturating_mul(BYTES_PER_RUNG))
4593}
4594
4595thread_local! {
4596    /// How many bytes of stack THIS thread was given by [`deepen_stack`], or 0 when the kernel
4597    /// did not size it.
4598    static SIZED_STACK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
4599}
4600
4601/// Restores the sized-stack record on the way out, including on a panic.
4602struct SizedGuard(usize);
4603
4604impl Drop for SizedGuard {
4605    fn drop(&mut self) {
4606        SIZED_STACK.with(|s| s.set(self.0));
4607    }
4608}
4609
4610impl SizedGuard {
4611    fn enter(bytes: usize) -> Self {
4612        SizedGuard(SIZED_STACK.with(|s| s.replace(bytes)))
4613    }
4614}
4615
4616/// How much stack the kernel call on this thread is entitled to use.
4617///
4618/// [`BASE_STACK_BYTES`] when nobody sized it — the conservative reading of "whatever the host
4619/// handed us", and what `recheck.rs`'s stack budget assumes today as a hard-coded constant.
4620/// A budget derived from this scales with the stack the call actually got, so the second kernel
4621/// stops answering `Unsupported` on a deep term the first kernel now checks happily.
4622pub fn kernel_stack_bytes() -> usize {
4623    match SIZED_STACK.with(|s| s.get()) {
4624        0 => BASE_STACK_BYTES,
4625        n => n,
4626    }
4627}
4628
4629/// The rungs ([`BYTES_PER_RUNG`] each) of the stack this thread runs on, by the kernel's own
4630/// sizing rule: at least [`INLINE_DEPTH`], at most the kernel's term-depth door.
4631pub fn kernel_stack_rungs() -> usize {
4632    let sized = kernel_stack_bytes().saturating_sub(BASE_STACK_BYTES) / BYTES_PER_RUNG;
4633    sized
4634        .max(INLINE_DEPTH)
4635        .min(crate::type_checker::MAX_TERM_DEPTH)
4636}
4637
4638/// Run `f` on a stack sized for `term` — or report that this call is already on one.
4639///
4640/// `Some(r)`: `f` ran, here or on a thread of the kernel's own, and `r` is its answer.
4641/// `None`: this call is NESTED inside one that already sized the stack, so the caller should
4642/// carry straight on with its ordinary body.
4643///
4644/// It is one line at the top of a kernel entry point (`infer_type` opens with it), with no
4645/// re-indentation of the body:
4646///
4647/// ```
4648/// use logicaffeine_kernel::term_store::deepen_stack;
4649/// use logicaffeine_kernel::Term;
4650///
4651/// fn size(term: &Term) -> usize {
4652///     if let Some(answer) = deepen_stack(term, || size(term)) {
4653///         return answer;
4654///     }
4655///     match term {
4656///         Term::App(f, x) => 1 + size(f) + size(x),
4657///         _ => 1,
4658///     }
4659/// }
4660///
4661/// let term = Term::App(Box::new(Term::Global("f".into())), Box::new(Term::Var("x".into())));
4662/// assert_eq!(size(&term), 3);
4663/// // A call already running on a sized stack is NESTED and answers `None`.
4664/// assert_eq!(deepen_stack(&term, || deepen_stack(&term, || ())), Some(None));
4665/// ```
4666///
4667/// The recursive call inside the closure re-enters the entry point, sees the flag, gets `None`,
4668/// and falls through to the real body — so the measurement is paid exactly ONCE per outermost
4669/// kernel call, which is the same budget `enter_kernel`'s own `depth_exceeds` pass already has.
4670///
4671/// A panic inside `f` is re-raised on the calling thread with its payload intact, so
4672/// `#[should_panic]` and `catch_unwind` behave exactly as they do without the switch.
4673///
4674/// The closure must be `Send`, which every kernel entry's `(&Context, &Term)` capture is.
4675/// `reduction::normalize_at` is the ONE exception: `Transparency::Excluding` holds a
4676/// `&dyn Fn(&str) -> bool` with no `Sync` bound, so it is not `Send`. Adding `+ Sync` to that
4677/// trait object is the whole fix, and `normalize`/`normalize_checked` — which build their own
4678/// `Transparency::All` — need nothing.
4679#[must_use = "`None` means this call is NESTED and the caller must fall through to its own body; \
4680              dropping the answer silently discards the kernel's verdict"]
4681pub fn deepen_stack<R: Send>(term: &Term, f: impl FnOnce() -> R + Send) -> Option<R> {
4682    deepen_stack_all(&[term], f)
4683}
4684
4685/// [`deepen_stack`] for an entry point that takes more than one term — `is_subtype` and
4686/// `def_eq` compare two, and the stack has to hold the DEEPER of them.
4687#[must_use = "`None` means this call is NESTED and the caller must fall through to its own body; \
4688              dropping the answer silently discards the kernel's verdict"]
4689pub fn deepen_stack_all<R: Send>(terms: &[&Term], f: impl FnOnce() -> R + Send) -> Option<R> {
4690    deepen_stack_all_with_depth(terms, 0, f)
4691}
4692
4693/// [`deepen_stack_all`] told about depth the TERMS DO NOT SHOW — and it is not a refinement, it
4694/// is a hole in the door that this is the only way to close.
4695///
4696/// **THE DEFECT, measured rather than argued.** The depth door and the sized stack both measure
4697/// the term they were HANDED. `infer_type`'s `Var` arm does not walk the term — it returns
4698/// `ctx.get(name).cloned()`, and `Term`'s `Clone` is derived, so it recurses over the BINDING'S
4699/// type. A term of depth 1 under a context that binds one variable at a type of depth 5000
4700/// therefore walks 5000 rungs that neither the door nor [`deepen_stack`] ever saw:
4701/// `W_perf_extra::a_shallow_term_under_a_deep_binding_walks_a_depth_nobody_measured` runs it in a
4702/// child process. Every other arm inherits it — an `App`'s domain check normalises the function's
4703/// type, a `Global`'s type is cloned out of the environment — so the input term's depth is a
4704/// LOWER BOUND on what a kernel call descends, never the bound itself.
4705///
4706/// A caller that knows the depth its walk reaches passes it as `extra_depth`: the second kernel's
4707/// open entry points pass their local plan's deepest binding. `extra_depth` of 0 is exactly
4708/// [`deepen_stack_all`].
4709///
4710/// ```
4711/// use logicaffeine_kernel::term_store::deepen_stack_all_with_depth;
4712/// use logicaffeine_kernel::Term;
4713///
4714/// let shallow = Term::Var("x".into());
4715/// // One node, but the walk will descend a binding 5000 levels deep: the stack is sized for that.
4716/// assert_eq!(deepen_stack_all_with_depth(&[&shallow], 5000, || "answered"), Some("answered"));
4717/// ```
4718#[must_use = "`None` means this call is NESTED and the caller must fall through to its own body; \
4719              dropping the answer silently discards the kernel's verdict"]
4720pub fn deepen_stack_all_with_depth<R: Send>(
4721    terms: &[&Term],
4722    extra_depth: usize,
4723    f: impl FnOnce() -> R + Send,
4724) -> Option<R> {
4725    if SIZED_STACK.with(|s| s.get()) != 0 {
4726        return None;
4727    }
4728    let term_depth_max = terms.iter().map(|t| term_depth(t)).max().unwrap_or(0);
4729    // PAST THE DOOR, SPEND NOTHING. A term deeper than `MAX_TERM_DEPTH` is REFUSED by
4730    // `enter_kernel` before the kernel descends a single rung, so sizing a stack for it would
4731    // reserve tens of megabytes and start a thread to be told "no". Running inline reaches the
4732    // same refusal — the door is what answers, not the stack — and it caps the reservation this
4733    // function can ever make at `stack_bytes_for_depth(MAX_TERM_DEPTH)`.
4734    if term_depth_max > crate::type_checker::MAX_TERM_DEPTH {
4735        let _sized = SizedGuard::enter(BASE_STACK_BYTES);
4736        return Some(f());
4737    }
4738    let depth = term_depth_max.max(extra_depth);
4739    if depth <= INLINE_DEPTH {
4740        let _sized = SizedGuard::enter(BASE_STACK_BYTES);
4741        return Some(f());
4742    }
4743    Some(run_on_stack(stack_bytes_for_depth(depth), f))
4744}
4745
4746/// [`deepen_stack`] told about depth the term does not show — see
4747/// [`deepen_stack_all_with_depth`] for the defect this closes.
4748#[must_use = "`None` means this call is NESTED and the caller must fall through to its own body; \
4749              dropping the answer silently discards the kernel's verdict"]
4750pub fn deepen_stack_with_depth<R: Send>(
4751    term: &Term,
4752    extra_depth: usize,
4753    f: impl FnOnce() -> R + Send,
4754) -> Option<R> {
4755    deepen_stack_all_with_depth(&[term], extra_depth, f)
4756}
4757
4758/// Continue a reduction whose intermediate term outgrew its entry stack.
4759/// The caller checks the existing term-depth ceiling before calling. Geometric
4760/// growth bounds the number of suspended parent threads; the largest reservation
4761/// remains the one already permitted for MAX_TERM_DEPTH.
4762pub(crate) fn grow_stack_for_depth<R: Send>(depth: usize, f: impl FnOnce() -> R + Send) -> R {
4763    assert!(depth <= crate::type_checker::MAX_TERM_DEPTH);
4764    let required = stack_bytes_for_depth(depth);
4765    let current = kernel_stack_bytes();
4766    if required <= current || depth <= INLINE_DEPTH {
4767        return f();
4768    }
4769    let ceiling = stack_bytes_for_depth(crate::type_checker::MAX_TERM_DEPTH);
4770    let bytes = required.max(current.saturating_mul(2)).min(ceiling);
4771    run_on_stack(bytes, f)
4772}
4773
4774/// Run `f` on a stack sized for `bytes`, propagating its panic.
4775///
4776/// Native: a scoped thread, so `f` may borrow the `Context` and the `Term` it was called with —
4777/// no `'static` bound, no `Arc`, no clone of either. The thread is the kernel's own, part of the
4778/// calling kernel call: a resource stop recorded on it (`reduction::observe_resource_stops`) is
4779/// carried back, so a conversion that happened to run on a sized stack cannot pass off an
4780/// undecided comparison as a decided one.
4781///
4782/// wasm32 has no threads — `Builder::spawn_scoped` is `Unsupported` there — so the browser client
4783/// instead links ONE stack big enough for [`stack_bytes_for_depth`] of
4784/// `crate::type_checker::MAX_TERM_DEPTH` up front (`apps/logicaffeine_web/build.rs`, locked
4785/// against these two constants by `wasm_link_stack_matches_kernel_worst_case`), and `f` runs
4786/// inline on it; the resource stop is observed on the calling thread instead of a spawned one.
4787fn run_on_stack<R: Send>(bytes: usize, f: impl FnOnce() -> R + Send) -> R {
4788    #[cfg(target_arch = "wasm32")]
4789    let (result, stopped) = {
4790        let _sized = SizedGuard::enter(bytes);
4791        let result = f();
4792        (result, crate::reduction::resource_stop_on_this_thread())
4793    };
4794    #[cfg(not(target_arch = "wasm32"))]
4795    let (result, stopped) = std::thread::scope(|scope| {
4796        std::thread::Builder::new()
4797            .stack_size(bytes)
4798            .spawn_scoped(scope, move || {
4799                let _sized = SizedGuard::enter(bytes);
4800                let result = f();
4801                (result, crate::reduction::resource_stop_on_this_thread())
4802            })
4803            .expect("the kernel can start a thread of its own")
4804            .join()
4805            .unwrap_or_else(|payload| std::panic::resume_unwind(payload))
4806    });
4807    if let Some(reason) = stopped {
4808        crate::reduction::record_resource_stop(reason);
4809    }
4810    result
4811}
4812
4813// ================================================================================================
4814// THE IDENTITY-KEYED CACHE
4815// ================================================================================================
4816
4817/// A cache keyed on node identity, dense: `Vec<Option<V>>` indexed by the id, so a lookup is
4818/// a bounds check and NOT a hash.
4819///
4820/// This is the other half of hash-consing. Sharing makes a repeated subterm one node; a
4821/// cache keyed on that node makes it checked ONCE.
4822pub struct IdCache<V> {
4823    slots: Vec<Option<V>>,
4824    filled: usize,
4825}
4826
4827impl<V> Default for IdCache<V> {
4828    fn default() -> Self {
4829        Self::new()
4830    }
4831}
4832
4833impl<V> IdCache<V> {
4834    /// An empty cache.
4835    pub fn new() -> Self {
4836        IdCache {
4837            slots: Vec::new(),
4838            filled: 0,
4839        }
4840    }
4841
4842    /// The cached value for `id`, if any.
4843    pub fn get(&self, id: TermId) -> Option<&V> {
4844        self.slots.get(id.index()).and_then(|s| s.as_ref())
4845    }
4846
4847    /// Whether `id` has a cached value.
4848    pub fn contains(&self, id: TermId) -> bool {
4849        self.get(id).is_some()
4850    }
4851
4852    /// Cache a value for `id`, replacing any previous one.
4853    pub fn insert(&mut self, id: TermId, value: V) {
4854        if self.slots.len() <= id.index() {
4855            self.slots.resize_with(id.index() + 1, || None);
4856        }
4857        if self.slots[id.index()].is_none() {
4858            self.filled += 1;
4859        }
4860        self.slots[id.index()] = Some(value);
4861    }
4862
4863    /// How many distinct ids are cached — the count that says "checked once, not once per
4864    /// occurrence".
4865    pub fn len(&self) -> usize {
4866        self.filled
4867    }
4868
4869    /// Whether nothing is cached.
4870    pub fn is_empty(&self) -> bool {
4871        self.filled == 0
4872    }
4873
4874    /// Forget everything.
4875    pub fn clear(&mut self) {
4876        self.slots.clear();
4877        self.filled = 0;
4878    }
4879}
4880
4881// ================================================================================================
4882// THE CONVERSION CACHE — the pair-keyed half of hash-consing
4883// ================================================================================================
4884
4885/// A cache of decided conversions, keyed on a PAIR of node identities.
4886///
4887/// [`IdCache`] answers "what is this node's type" once per node. This answers "are these two
4888/// nodes convertible" once per pair, which is the other thing Lean's kernel has and ours does
4889/// not: `is_subtype` today has exactly one shortcut (`alpha_eq`) and then normalises BOTH sides
4890/// to a FULL normal form before comparing, with nothing remembered afterwards.
4891///
4892/// MEASURED on this box: 100 repetitions of one convertible-but-not-α-equal pair cost 0.662 s
4893/// against 0.00655 s for one — 6.55, 6.57 and 6.62 ms per call at 1, 10 and 100 repeats. The
4894/// per-call cost is FLAT, which is the measurement that says there is no cache anywhere.
4895/// `tests/W_perf_extra.rs::w_perf_measure_what_the_missing_conversion_cache_costs` prints it.
4896///
4897/// Conversion is SYMMETRIC, so `(a, b)` and `(b, a)` are one entry: the key is ordered.
4898///
4899/// NEGATIVE ENTRIES ARE KEPT, deliberately. They are the expensive half — deciding that two
4900/// terms are NOT convertible is what costs a full normalisation of both sides — and Lean caches
4901/// them for the same reason.
4902///
4903/// # Two rules this cache cannot enforce for itself, and a soundness trap in the second
4904///
4905/// 1. **Identity is not `Term` equality on floats.** Settled: `Literal`'s `PartialEq` compares
4906///    `f64::to_bits()`, so the store's interning key IS the kernel's equality and the two agree
4907///    everywhere. [`eq_faithful`](TermStore::eq_faithful) remains the conservative guard, and a
4908///    caller that writes `store.same(a, b) && store.eq_faithful(a)` is correct under the old
4909///    rule as well as the new one.
4910/// 2. **A cache may not outlive the environment it decided under.** Adding a definition can only
4911///    ADD conversions, so a stale NEGATIVE is merely incomplete. A stale POSITIVE is the kernel
4912///    certifying a conversion that no longer holds.
4913///
4914///    **Wave 2 named the wrong mechanism, and the correction matters.** `Context::pop_scope`
4915///    does NOT remove definitions: it reverts the four ATTRIBUTE databases — instances,
4916///    coercions, simp rules, hints — and its own doc says "NEVER a declaration"; a `def` written
4917///    inside a `section` survives its `end`. The call that genuinely removes one is
4918///    `Context::displace`, which `add_inductive`/`declare` run on the name they are about to
4919///    bind, so **re-declaring a name deletes the previous definition of it** — and a positive
4920///    decided against that body is then stale.
4921///
4922///    The reason a per-call cache needs nothing else: every kernel entry takes `&Context`, and a
4923///    shared reference cannot be displaced, extended in place or popped. **Within one outermost
4924///    kernel call the global environment is FIXED.** Only the LOCAL bindings vary, by
4925///    `Context::extend`, which returns a new `Context` and may SHADOW — and locals are what
4926///    [`EnvId`] keys. So: hold the cache for exactly one outermost call, and let [`KernelCall`]
4927///    make that a borrow rather than a comment — see [`DefEqCache::scoped`].
4928pub struct DefEqCache {
4929    decided: HashMap<(u32, u32), bool>,
4930    hits: u64,
4931    misses: u64,
4932}
4933
4934impl Default for DefEqCache {
4935    fn default() -> Self {
4936        Self::new()
4937    }
4938}
4939
4940impl DefEqCache {
4941    /// An empty cache.
4942    pub fn new() -> Self {
4943        DefEqCache {
4944            decided: HashMap::new(),
4945            hits: 0,
4946            misses: 0,
4947        }
4948    }
4949
4950    /// Conversion is symmetric, so the pair is ordered before it is keyed.
4951    fn key(a: TermId, b: TermId) -> (u32, u32) {
4952        if a.0 <= b.0 {
4953            (a.0, b.0)
4954        } else {
4955            (b.0, a.0)
4956        }
4957    }
4958
4959    /// What was decided about this pair, in either order, or `None` if it has not been asked.
4960    pub fn get(&mut self, a: TermId, b: TermId) -> Option<bool> {
4961        match self.decided.get(&Self::key(a, b)) {
4962            Some(&v) => {
4963                self.hits += 1;
4964                Some(v)
4965            }
4966            None => {
4967                self.misses += 1;
4968                None
4969            }
4970        }
4971    }
4972
4973    /// Record a decision. Recording the same pair twice with DIFFERENT verdicts is a bug in the
4974    /// caller — one `Context`, one answer — and is caught here rather than propagated.
4975    pub fn insert(&mut self, a: TermId, b: TermId, equal: bool) {
4976        if let Some(previous) = self.decided.insert(Self::key(a, b), equal) {
4977            assert_eq!(
4978                previous, equal,
4979                "a conversion cache was told both answers for one pair under one context — the \
4980                 cache outlived the context it decided under"
4981            );
4982        }
4983    }
4984
4985    /// How many pairs have been decided.
4986    pub fn len(&self) -> usize {
4987        self.decided.len()
4988    }
4989
4990    /// Whether nothing has been decided.
4991    pub fn is_empty(&self) -> bool {
4992        self.decided.is_empty()
4993    }
4994
4995    /// Lookups that were answered from the cache.
4996    pub fn hits(&self) -> u64 {
4997        self.hits
4998    }
4999
5000    /// Lookups that had to be decided.
5001    pub fn misses(&self) -> u64 {
5002        self.misses
5003    }
5004
5005    /// Forget every decision — what a `Context` change requires, and what dropping the cache at
5006    /// the end of a kernel call does for free.
5007    pub fn clear(&mut self) {
5008        self.decided.clear();
5009        self.hits = 0;
5010        self.misses = 0;
5011    }
5012
5013    /// The same cache with a LIFETIME it cannot escape — the enforceable form of rule 2 above.
5014    ///
5015    /// ```
5016    /// use logicaffeine_kernel::term_store::{DefEqCache, KernelCall};
5017    ///
5018    /// let call = KernelCall::begin();
5019    /// let conv = DefEqCache::scoped(&call);
5020    /// assert!(conv.is_empty() && conv.hits() == 0 && conv.misses() == 0);
5021    /// ```
5022    ///
5023    /// Returning it past its call does not compile:
5024    ///
5025    /// ```compile_fail,E0515
5026    /// use logicaffeine_kernel::term_store::{DefEqCache, KernelCall, ScopedDefEqCache};
5027    ///
5028    /// fn escape() -> ScopedDefEqCache<'static> {
5029    ///     let call = KernelCall::begin();
5030    ///     DefEqCache::scoped(&call)
5031    /// }
5032    /// ```
5033    ///
5034    /// `ScopedDefEqCache<'call>` borrows the `KernelCall`, so it cannot reach a `static` (that
5035    /// needs `'static`) and cannot be stored in a `Context` without giving `Context` a lifetime
5036    /// parameter — a change every call site would see. `KernelCall` is neither `Send` nor `Sync`,
5037    /// so it cannot be smuggled into a global through a `Mutex` either.
5038    pub fn scoped(_call: &KernelCall) -> ScopedDefEqCache<'_> {
5039        ScopedDefEqCache {
5040            inner: DefEqCache::new(),
5041            _call: std::marker::PhantomData,
5042        }
5043    }
5044}
5045
5046/// [`DefEqCache`] branded with the lifetime of ONE outermost kernel call. See
5047/// [`DefEqCache::scoped`].
5048pub struct ScopedDefEqCache<'call> {
5049    inner: DefEqCache,
5050    _call: std::marker::PhantomData<&'call KernelCall>,
5051}
5052
5053impl ScopedDefEqCache<'_> {
5054    /// What was decided about this pair, in either order.
5055    pub fn get(&mut self, a: TermId, b: TermId) -> Option<bool> {
5056        self.inner.get(a, b)
5057    }
5058
5059    /// Record a decision; two answers for one pair is caught here.
5060    pub fn insert(&mut self, a: TermId, b: TermId, equal: bool) {
5061        self.inner.insert(a, b, equal)
5062    }
5063
5064    /// How many pairs have been decided.
5065    pub fn len(&self) -> usize {
5066        self.inner.len()
5067    }
5068
5069    /// Whether nothing has been decided.
5070    pub fn is_empty(&self) -> bool {
5071        self.inner.is_empty()
5072    }
5073
5074    /// Lookups answered from the cache.
5075    pub fn hits(&self) -> u64 {
5076        self.inner.hits()
5077    }
5078
5079    /// Lookups that had to be decided.
5080    pub fn misses(&self) -> u64 {
5081        self.inner.misses()
5082    }
5083}
5084
5085// ================================================================================================
5086// THE TYPE CACHE — and the lifetime rule that makes it sound
5087// ================================================================================================
5088
5089/// A token that lives exactly as long as ONE outermost kernel call, and that a cache BORROWS.
5090///
5091/// The soundness rule a conversion or type cache has to obey is not a style note, so it is not
5092/// written as one. A cache that outlives the environment it decided under is the kernel
5093/// certifying a judgement that no longer holds, and the way to make that unwritable is to give
5094/// the cache a lifetime it cannot escape:
5095///
5096/// ```
5097/// use logicaffeine_kernel::term_store::{KernelCall, TypeCache};
5098///
5099/// let call = KernelCall::begin();
5100/// assert!(call.is_outermost());
5101/// let types: TypeCache<'_, u32> = TypeCache::new(&call);
5102/// assert_eq!((types.hits(), types.misses()), (0, 0));
5103/// ```
5104///
5105/// `TypeCache<'call, _>` borrows `&'call KernelCall`, so it cannot be put in a `static` (that
5106/// needs `'static`), and it cannot be put in a `Context` without giving `Context` a lifetime
5107/// parameter — a change every call site would see and a reviewer could not miss. `KernelCall`
5108/// is neither `Send` nor `Sync`, so it cannot be smuggled into a global through a `Mutex`
5109/// either. The comment becomes a compile error:
5110///
5111/// ```compile_fail,E0515
5112/// use logicaffeine_kernel::term_store::{KernelCall, TypeCache};
5113///
5114/// fn outlive() -> TypeCache<'static, u32> {
5115///     let call = KernelCall::begin();
5116///     TypeCache::new(&call)
5117/// }
5118/// ```
5119///
5120/// ```compile_fail,E0277
5121/// fn send<T: Send>() {}
5122/// send::<logicaffeine_kernel::term_store::KernelCall>();
5123/// ```
5124///
5125/// # What actually invalidates a cached judgement, corrected
5126///
5127/// Wave 2 recorded this as "`Context::pop_scope` REMOVES definitions". **That is wrong, and the
5128/// correction matters because it is the one that says where the danger really is.** `pop_scope`
5129/// reverts the four ATTRIBUTE databases — instances, coercions, simp rules, hints — and
5130/// explicitly never a declaration; a `def` written inside a `section` survives its `end`. The
5131/// call that genuinely removes one is `Context::displace`, which every `add_inductive`,
5132/// `add_definition` and `declare` runs on the name it is about to bind, so **re-declaring a name
5133/// deletes the previous definition of it** — and a conversion decided while that body was in
5134/// force is then a stale POSITIVE.
5135///
5136/// The good news, and the reason a per-call cache needs nothing else: every kernel entry point
5137/// takes `&Context`. A shared reference cannot be displaced, extended in place, or popped. So
5138/// **within one outermost kernel call the global environment is FIXED**, and only the LOCAL
5139/// bindings vary — by `Context::extend`, which returns a NEW `Context` and may SHADOW. Locals
5140/// are what [`EnvId`] keys; globals need nothing but this lifetime.
5141pub struct KernelCall {
5142    /// Neither `Send` nor `Sync`: a raw pointer is the standard way to say so, and it is never
5143    /// dereferenced.
5144    _not_send: std::marker::PhantomData<*const ()>,
5145}
5146
5147thread_local! {
5148    /// How many [`KernelCall`] tokens are live on this thread.
5149    static KERNEL_CALLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
5150}
5151
5152impl Default for KernelCall {
5153    fn default() -> Self {
5154        Self::begin()
5155    }
5156}
5157
5158impl KernelCall {
5159    /// Open a kernel call. The token must be held on the stack for as long as any cache keyed
5160    /// under it.
5161    pub fn begin() -> Self {
5162        KERNEL_CALLS.with(|n| n.set(n.get() + 1));
5163        KernelCall {
5164            _not_send: std::marker::PhantomData,
5165        }
5166    }
5167
5168    /// Whether this is the OUTERMOST kernel call on this thread — the only place a cache should
5169    /// be created, because a cache created deeper is thrown away on every ascent and buys
5170    /// nothing.
5171    pub fn is_outermost(&self) -> bool {
5172        KERNEL_CALLS.with(|n| n.get()) == 1
5173    }
5174
5175    /// How many kernel calls are live on this thread.
5176    pub fn depth() -> usize {
5177        KERNEL_CALLS.with(|n| n.get())
5178    }
5179}
5180
5181impl Drop for KernelCall {
5182    fn drop(&mut self) {
5183        KERNEL_CALLS.with(|n| n.set(n.get().saturating_sub(1)));
5184    }
5185}
5186
5187/// `inferType`, memoised on `(environment, term)` — the cache Lean has and we do not, with the
5188/// half Lean does not need because its `Expr` is de Bruijn.
5189///
5190/// # Why the environment is in the key, and what happens to a cache without it
5191///
5192/// `Term` names its variables. `Var("x")` interns to ONE [`TermId`] whatever scope it occurs in,
5193/// and `Context::extend` SHADOWS — so under `x : Nat` that id's type is `Nat` and under
5194/// `x : Bool` it is `Bool`. A cache keyed on the term ALONE hands the second scope the first
5195/// scope's answer: the kernel certifying a type the term does not have, from an optimisation.
5196/// `W_perf_extra::a_type_cache_keyed_on_the_term_alone_would_certify_the_wrong_type` builds
5197/// exactly that and watches it happen, and the same test watches this key refuse to.
5198///
5199/// Lean is not exposed to it: its `Expr` uses de Bruijn indices for bound variables and globally
5200/// unique `FVarId`s for free ones, so two occurrences under different binders are DIFFERENT
5201/// expressions, and `inferType` may cache on the expression alone. Porting that design across
5202/// without porting the representation is the mistake this type exists to make unavailable.
5203///
5204/// # What it costs, and why it still pays
5205///
5206/// **The invariant the key does NOT carry, and why it does not have to.** A term's type also
5207/// depends on the GLOBAL environment — definitions, inductives, constructors, structures. That is
5208/// not in the key because it cannot change: every kernel entry point takes `&Context`, and a
5209/// shared reference can be neither displaced, nor extended in place, nor popped. One cache must
5210/// therefore be threaded only through calls that share one `&Context`'s globals, which is exactly
5211/// one outermost kernel call — the lifetime [`KernelCall`] gives it.
5212///
5213/// [`TermStore::env_for`] restricts the environment to a term's FREE VARIABLES, which the store
5214/// already knows in O(1) and hash-conses, so the key costs O(|free vars|) and not O(|context|).
5215/// A CLOSED subterm — every type, every literal, every global's body — keys on
5216/// [`EnvId::EMPTY`] under every scope, so it is checked once for the whole call however many
5217/// scopes it appears under. That is `a_shared_subterm_is_checked_once_and_not_once_per_occurrence`
5218/// on the CHECKING side, by construction rather than by a special case.
5219pub struct TypeCache<'call, V> {
5220    decided: HashMap<(EnvId, TermId), V>,
5221    hits: u64,
5222    misses: u64,
5223    _call: std::marker::PhantomData<&'call KernelCall>,
5224}
5225
5226impl<'call, V: Clone + std::fmt::Debug> TypeCache<'call, V> {
5227    /// An empty cache, borrowing the call it may not outlive.
5228    pub fn new(_call: &'call KernelCall) -> Self {
5229        TypeCache {
5230            decided: HashMap::new(),
5231            hits: 0,
5232            misses: 0,
5233            _call: std::marker::PhantomData,
5234        }
5235    }
5236
5237    /// The type decided for this term under this environment, if it has been asked.
5238    pub fn get(&mut self, env: EnvId, id: TermId) -> Option<&V> {
5239        match self.decided.get(&(env, id)) {
5240            Some(v) => {
5241                self.hits += 1;
5242                Some(v)
5243            }
5244            None => {
5245                self.misses += 1;
5246                None
5247            }
5248        }
5249    }
5250
5251    /// Record a decision.
5252    ///
5253    /// Recording the same key twice with a DIFFERENT answer is a bug in the caller — one
5254    /// environment, one term, one type — and it is the empirical tripwire for a cache that has
5255    /// outlived the environment it decided under. It is a `debug_assert` because it compares
5256    /// through `Debug`, which every kernel answer has and not every one has `PartialEq`;
5257    /// the test suite runs in debug, which is where a stale cache would be caught.
5258    pub fn insert(&mut self, env: EnvId, id: TermId, ty: V) {
5259        if let Some(previous) = self.decided.insert((env, id), ty) {
5260            let now = self.decided.get(&(env, id)).expect("just inserted");
5261            debug_assert_eq!(
5262                format!("{previous:?}"),
5263                format!("{now:?}"),
5264                "a type cache was told two answers for one (environment, term) — the cache \
5265                 outlived the environment it decided under"
5266            );
5267        }
5268    }
5269
5270    /// How many judgements are cached.
5271    pub fn len(&self) -> usize {
5272        self.decided.len()
5273    }
5274
5275    /// Whether nothing has been decided.
5276    pub fn is_empty(&self) -> bool {
5277        self.decided.is_empty()
5278    }
5279
5280    /// Lookups answered from the cache — the number that says a shared subterm was checked once.
5281    pub fn hits(&self) -> u64 {
5282        self.hits
5283    }
5284
5285    /// Lookups that had to be decided.
5286    pub fn misses(&self) -> u64 {
5287        self.misses
5288    }
5289
5290    /// Forget every decision.
5291    pub fn clear(&mut self) {
5292        self.decided.clear();
5293        self.hits = 0;
5294        self.misses = 0;
5295    }
5296}
5297
5298// ================================================================================================
5299// THE CROSS-LOCKS — the measures this file shares with the kernel's own, pinned against each other
5300// ================================================================================================
5301//
5302// These are unit tests rather than integration tests for one reason: `type_checker::depth_exceeds`
5303// is `pub(crate)`, so the only place the door's own measure can be compared against this file's is
5304// inside the crate. Two measures of the same tree that drift apart is precisely how a door starts
5305// admitting what it cannot walk.
5306
5307#[cfg(test)]
5308#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/tests.rs"]
5309mod tests;
5310
5311#[cfg(test)]
5312#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/conversion_gateway_tests.rs"]
5313mod conversion_gateway_tests;
5314
5315#[cfg(test)]
5316#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/beta_zeta_conversion_tests.rs"]
5317mod beta_zeta_conversion_tests;
5318
5319#[cfg(test)]
5320#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/reduction_stack_growth_controls.rs"]
5321mod reduction_stack_growth_controls;
5322
5323#[cfg(test)]
5324#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/wasm_link_stack_controls.rs"]
5325mod wasm_link_stack_controls;
5326
5327// Counts actual input-node visits, including repeated visits to equal syntax.
5328// A test-only structural-work oracle; no timing thresholds or release overhead.
5329#[cfg(test)]
5330thread_local! {
5331    static INTERN_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
5332}
5333#[cfg(test)]
5334pub(crate) fn reset_intern_visits() {
5335    INTERN_VISITS.with(|count| count.set(0));
5336}
5337#[cfg(test)]
5338pub(crate) fn intern_visits() -> usize {
5339    INTERN_VISITS.with(|count| count.get())
5340}
5341
5342#[cfg(test)]
5343#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/adjacent_delta_work_tests.rs"]
5344mod adjacent_delta_work_tests;
5345
5346// Counts actual name materialization, not calls to the emptiness accessor.
5347#[cfg(test)]
5348thread_local! {
5349    static FREE_NAME_COLLECTIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
5350    static FREE_NAME_RESOLUTIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
5351}
5352#[cfg(test)]
5353pub(crate) fn reset_free_name_work() {
5354    FREE_NAME_COLLECTIONS.with(|count| count.set(0));
5355    FREE_NAME_RESOLUTIONS.with(|count| count.set(0));
5356}
5357#[cfg(test)]
5358pub(crate) fn free_name_work() -> (usize, usize) {
5359    (
5360        FREE_NAME_COLLECTIONS.with(|count| count.get()),
5361        FREE_NAME_RESOLUTIONS.with(|count| count.get()),
5362    )
5363}
5364
5365#[cfg(test)]
5366#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/opaque_leaf_delta_controls.rs"]
5367mod opaque_leaf_delta_controls;
5368
5369#[cfg(test)]
5370#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/literal_leaf_delta_controls.rs"]
5371mod literal_leaf_delta_controls;
5372
5373// Counts actual expansion dispatches, not metadata queries or test helper calls.
5374// The optional test counter neither traverses syntax nor changes fuel.
5375#[cfg(test)]
5376thread_local! {
5377    static DELTA_EXPANSION_WORK: std::cell::Cell<Option<(usize, usize)>> =
5378        const { std::cell::Cell::new(None) };
5379}
5380
5381#[cfg(test)]
5382#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/partial_delta_boundary_controls.rs"]
5383mod partial_delta_boundary_controls;
5384
5385#[cfg(test)]
5386#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/zero_universe_header_controls.rs"]
5387mod zero_universe_header_controls;
5388
5389/// Count lexical occurrences with bounded native-stack scratch. The child and
5390/// shadow rules are the same ones used by the general heap-backed traversal.
5391pub(crate) fn bounded_occurrence_count_in_term(
5392    body: &Term,
5393    name: &str,
5394    max_depth: usize,
5395    max_nodes: usize,
5396) -> Option<u32> {
5397    fn visit(
5398        term: &Term,
5399        name: &str,
5400        depth: usize,
5401        max_depth: usize,
5402        left: &mut usize,
5403    ) -> Option<u32> {
5404        if depth >= max_depth || *left == 0 {
5405            return None;
5406        }
5407        *left -= 1;
5408        if let Term::Var(v) = term {
5409            return Some(u32::from(v == name));
5410        }
5411        let hidden = match term_shadow(term, name) {
5412            Shadow::None => None,
5413            Shadow::Child(i) => Some(i),
5414            Shadow::Every => return Some(0),
5415        };
5416        let mut index = 0;
5417        let mut total = Some(0u32);
5418        term_for_each_child(term, &mut |child| {
5419            if Some(index) != hidden {
5420                total = total
5421                    .and_then(|n| n.checked_add(visit(child, name, depth + 1, max_depth, left)?));
5422            }
5423            index += 1;
5424        });
5425        total
5426    }
5427    let mut left = max_nodes;
5428    let count = visit(body, name, 0, max_depth, &mut left)?;
5429    Some(count)
5430}
5431#[cfg(test)]
5432thread_local! {static OCCURRENCE_HEAP_WALKS:std::cell::Cell<usize>=const {std::cell::Cell::new(0)};}
5433
5434#[cfg(test)]
5435#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/bounded_occurrence_controls.rs"]
5436mod bounded_occurrence_controls;
5437
5438#[cfg(test)]
5439#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/alpha_metadata_controls.rs"]
5440mod alpha_metadata_controls;
5441
5442#[cfg(test)]
5443#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/literal_payload_controls.rs"]
5444mod literal_payload_controls;
5445
5446#[cfg(test)]
5447#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/depth_admission_controls.rs"]
5448mod depth_admission_controls;
5449
5450#[cfg(test)]
5451#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/bounded_substitution_controls.rs"]
5452mod bounded_substitution_controls;
5453
5454#[cfg(test)]
5455#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/bounded_add_controls.rs"]
5456mod bounded_add_controls;
5457
5458#[cfg(test)]
5459#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/bounded_name_set_controls.rs"]
5460mod bounded_name_set_controls;
5461
5462#[cfg(test)]
5463#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/raw_name_set_controls.rs"]
5464mod raw_name_set_controls;
5465
5466#[cfg(test)]
5467#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/shared_delta_expansion_controls.rs"]
5468mod shared_delta_expansion_controls;
5469
5470#[cfg(test)]
5471#[path = "../../logicaffeine_tests/unit/logicaffeine_kernel/term_store/borrowed_children_metadata_controls.rs"]
5472mod borrowed_children_metadata_controls;