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;