imprimatur

Spike S2: stable statement hashes

Result: works, with three changes to DESIGN §7.7. With these changes the canonical form of every statement and its dependency cone in the Trust fixture is byte-identical on v4.28.0-rc1, v4.31.0, v4.34.1 and v4.35.0. Run on 2026-10-17.

Question

Does a canonical serialisation of Expr, with binder names dropped and universe parameters renamed, give the same hash for the same declaration on every toolchain? Do the dependency-cone walk and collectAxioms behave the same way (PLAN §3)?

What was built

The fixture was built on each toolchain, with both variants of Trust.Hyp. The serialiser printed every theorem’s statement, axioms and cone. The outputs were then compared.

Results

Bodies are not stable across the range. With definitions serialised by their values:

Equation lemmas are stable. Each definition in the cone is serialised by its type plus the statements of its equation lemmas (Meta.getEqnsFor?), and the walk follows the constants in those statements rather than the body. The output is then byte-identical on all four toolchains, for both variants. The walk never reaches brecOn, below, matchers or _f helpers. getEqnsFor? also gives equations for non-recursive definitions such as Trust.Hyp and Trust.isZero.

Hash properties on the fixture (SHA-256 of the canonical form, first 16 hex digits; identical on all four toolchains):

Declarations Hash As intended?
double_even and double_even_renamed (binder renamed) 7e61db2023a9c3d9 (both) yes: same
pairMap_fst and pairMap_fst_renamed (universes and binders renamed) a8ee792b7376db33 (both) yes: same
uses_hyp, real against vacuous Hyp differ, only in the cone entry for Trust.Hyp yes: drift is caught

collectAxioms agrees on all four toolchains:

For deep_sorry, whose proof is honest but whose statement mentions a definition built on sorry, collectAxioms reports sorryAx, because it follows definitions in types. The partial collatz appears in the cone as an opaque constant with no body, and collectAxioms doesn’t flag it, so the cone check must (DESIGN §7.7).

Changes to the design

  1. trusted_prefixes are module prefixes, not namespaces. Core constants such as Nat.add, HAdd.hAdd and instAddNat are declared in module Init.Prelude but live in no Init namespace. A namespace test would pull all of core into every cone. The walk uses env.getModuleIdxFor? and tests the module name. DESIGN §6.1 and §7.7 are updated.
  2. Definitions in the cone are hashed by their equation lemmas, not their bodies. For each definition: its type, then the types of getEqnsFor?, in order. Fall back to the body only when there are no equations. Matchers, brecOn, below and other auxiliary constants never enter the cone. DESIGN §7.7 is updated.
  3. Import with loadExts := true. Without it, getEqnsFor? fails on an imported environment (“failed to generate equational theorem … no progress”), and Meta.isMatcherCore returns false for matchers. With it, both work on all four toolchains. It needs enableInitializersExecution, and so an unsafe entry point (see also S1, finding 2).
  4. Normalise sorry. An elaborated sorry is sorryAx applied to a unique tag (a Lean.Name literal that encodes where the sorry was written). A canonical form that kept the tag would change with unrelated edits above the sorry. The serialiser must replace every sorryAx application by one fixed token.

Remaining risks for M3

The spike code is not kept in the repository (PLAN §3). The fixture is kept.

Revised in M3

The math catalogue’s comparator manifests, run in external mode (scripts/integration.sh), changed two of the conclusions above:

  1. Equation lemmas only for recursive definitions. Generating equation lemmas for one non-recursive definition (ProducedMarkedSeamEquations, a few lets) used more than 12 GB. That work happens where the extractor’s heartbeat limit doesn’t reach it. Equation lemmas are needed only where the compiled form differs between toolchains, which is recursion.
    • A recursive definition is now hashed by its equation lemmas.
    • Any other definition is hashed by its body, which is what the author wrote.
    • Matchers and auxiliary recursors are hashed by their type alone.
    • The fixtures’ hashes are still identical on v4.28.0-rc1 to v4.35.0.
  2. The serialisation must follow sharing. The same definition’s body is 3,813 shared nodes, but more than 10^8 as a tree. The serialisation is now a Merkle hash memoised on pointers, and is linear in the shared size. The memo table must be keyed on pointers, not on Expr equality, which ignores binder annotations that the serialisation keeps. A hash-invariance property caught that mistake.