pub struct DefEqCache { /* private fields */ }Expand description
A cache of decided conversions, keyed on a PAIR of node identities.
IdCache answers “what is this node’s type” once per node. This answers “are these two
nodes convertible” once per pair, which is the other thing Lean’s kernel has and ours does
not: is_subtype today has exactly one shortcut (alpha_eq) and then normalises BOTH sides
to a FULL normal form before comparing, with nothing remembered afterwards.
MEASURED on this box: 100 repetitions of one convertible-but-not-α-equal pair cost 0.662 s
against 0.00655 s for one — 6.55, 6.57 and 6.62 ms per call at 1, 10 and 100 repeats. The
per-call cost is FLAT, which is the measurement that says there is no cache anywhere.
tests/W_perf_extra.rs::w_perf_measure_what_the_missing_conversion_cache_costs prints it.
Conversion is SYMMETRIC, so (a, b) and (b, a) are one entry: the key is ordered.
NEGATIVE ENTRIES ARE KEPT, deliberately. They are the expensive half — deciding that two terms are NOT convertible is what costs a full normalisation of both sides — and Lean caches them for the same reason.
§Two rules this cache cannot enforce for itself, and a soundness trap in the second
-
Identity is not
Termequality on floats. Settled:Literal’sPartialEqcomparesf64::to_bits(), so the store’s interning key IS the kernel’s equality and the two agree everywhere.eq_faithfulremains the conservative guard, and a caller that writesstore.same(a, b) && store.eq_faithful(a)is correct under the old rule as well as the new one. -
A cache may not outlive the environment it decided under. Adding a definition can only ADD conversions, so a stale NEGATIVE is merely incomplete. A stale POSITIVE is the kernel certifying a conversion that no longer holds.
Wave 2 named the wrong mechanism, and the correction matters.
Context::pop_scopedoes NOT remove definitions: it reverts the four ATTRIBUTE databases — instances, coercions, simp rules, hints — and its own doc says “NEVER a declaration”; adefwritten inside asectionsurvives itsend. The call that genuinely removes one isContext::displace, whichadd_inductive/declarerun on the name they are about to bind, so re-declaring a name deletes the previous definition of it — and a positive decided against that body is then stale.The reason a per-call cache needs nothing else: every kernel entry takes
&Context, and a shared reference cannot be displaced, extended in place or popped. Within one outermost kernel call the global environment is FIXED. Only the LOCAL bindings vary, byContext::extend, which returns a newContextand may SHADOW — and locals are whatEnvIdkeys. So: hold the cache for exactly one outermost call, and letKernelCallmake that a borrow rather than a comment — seeDefEqCache::scoped.
Implementations§
Source§impl DefEqCache
impl DefEqCache
Sourcepub fn get(&mut self, a: TermId, b: TermId) -> Option<bool>
pub fn get(&mut self, a: TermId, b: TermId) -> Option<bool>
What was decided about this pair, in either order, or None if it has not been asked.
Sourcepub fn insert(&mut self, a: TermId, b: TermId, equal: bool)
pub fn insert(&mut self, a: TermId, b: TermId, equal: bool)
Record a decision. Recording the same pair twice with DIFFERENT verdicts is a bug in the
caller — one Context, one answer — and is caught here rather than propagated.
Sourcepub fn clear(&mut self)
pub fn clear(&mut self)
Forget every decision — what a Context change requires, and what dropping the cache at
the end of a kernel call does for free.
Sourcepub fn scoped(_call: &KernelCall) -> ScopedDefEqCache<'_>
pub fn scoped(_call: &KernelCall) -> ScopedDefEqCache<'_>
The same cache with a LIFETIME it cannot escape — the enforceable form of rule 2 above.
use logicaffeine_kernel::term_store::{DefEqCache, KernelCall};
let call = KernelCall::begin();
let conv = DefEqCache::scoped(&call);
assert!(conv.is_empty() && conv.hits() == 0 && conv.misses() == 0);Returning it past its call does not compile:
use logicaffeine_kernel::term_store::{DefEqCache, KernelCall, ScopedDefEqCache};
fn escape() -> ScopedDefEqCache<'static> {
let call = KernelCall::begin();
DefEqCache::scoped(&call)
}ScopedDefEqCache<'call> borrows the KernelCall, so it cannot reach a static (that
needs 'static) and cannot be stored in a Context without giving Context a lifetime
parameter — a change every call site would see. KernelCall is neither Send nor Sync,
so it cannot be smuggled into a global through a Mutex either.