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.
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)?
Trust fixture (test/fixtures/Trust, kept: M3 needs it). It
covers structural and well-founded recursion, pattern matching, an
inductive predicate, a structure with an instance, universe polymorphism,
sorry in a proof and three definitions deep in a statement, a custom
axiom, a partial and an unsafe definition, and Trust.uses_hyp, whose
hypothesis Trust.Hyp has a real and a vacuous version selected by
lake build -Kweak=1. Two pairs of theorems differ only in binder or
universe names.Expr as an s-expression:
u0, u1, … by position;mdata is dropped.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.
Bodies are not stable across the range. With definitions serialised by their values:
double calls an auxiliary Trust.double._f, whereas v4.28
inlines a matcher application. Matcher bodies also differ (Eq.ndrec_symm
in later versions against Eq.ndrec with Eq.symm in v4.28).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 double_even;[propext, Quot.sound] for norm1_add;[sorryAx] for direct_sorry, uses_direct_sorry and deep_sorry;[Trust.custom_axiom] for uses_custom_axiom.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).
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.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.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).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.match with side conditions, mutual and nested recursion,
where clauses) produce equations whose shape could change between
toolchains. The fixture has only simple cases. M3 extends Trust with these
before the cross-toolchain golden test is trusted. PLAN’s fallback stands:
per-toolchain hashes with “toolchain changed, run accept”.Trust.instAddPoint) is part of the hash.
The scheme for these names has changed in Lean’s past. It was stable across
v4.28–v4.35 on the fixture.The spike code is not kept in the repository (PLAN §3). The fixture is kept.
The math catalogue’s comparator manifests, run in external mode
(scripts/integration.sh), changed two of the conclusions above:
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.
Expr equality, which
ignores binder annotations that the serialisation keeps. A
hash-invariance property caught that mistake.