07. The formal engine: Lean 4, Mathlib, proof checking
The formal engine is the middle box of the top row of the diagram, with the label "Lean 4, Mathlib, Proof Checker". It is the only component that can produce E5. Thus its specification is stricter than that of any other component. The content side is in tamnd/math2/lean/, the Math2 Lake package. The toolchain side is in tamnd/math2-agent/lean and in the lean/M2Extract package.
The job of the engine is one sentence: given the content at a commit, produce for every formal binding a checked declaration, its elaborated-type hash, its axiom profile, its formal dependencies, and the results of independent replay, all pinned to one Lean toolchain and one Mathlib version.
7.1 What is pinned
Each release pins three things. Its manifest records them (06.2):
| Item | Pinned as | Source of truth |
|---|---|---|
| Lean toolchain | a stable release, e.g. leanprover/lean4:v4.35.0 |
lean/lean-toolchain |
| Mathlib | the matching Mathlib tag v4.35.0, plus the commit it names |
lean/lake-manifest.json |
| Checkers | nanoda, lean4lean, con-leche, con-ron and comparator versions, as bundled with the toolchain or pinned separately |
manifests/releases/<r>.yaml → checkers: |
A release never pins a Lean release candidate or Mathlib master. Mathlib has no semantic versions. It tags each toolchain bump v4.X.Y, and its stable branch follows those tags (document 01). math2 pins those tags. This gives monthly releases a natural rhythm: each release takes the newest tag that the bump branch (7.9) has built green.
When a node binds to other formal libraries, we pin them in the same way. These libraries are Tau Ceti, the textbook libraries of Formal Frontier, the formal-conjectures repository, the Imperial FLT project, Carleson, PFR, and the AFP for Isabelle bindings. Each one is an entry in manifests/sources.yaml with a commit and a toolchain. Some libraries have a toolchain that is different from the toolchain of the release. We check a binding to such a library in the environment of that library, and the manifest marks it.
7.2 The Math2 package
lean/ is an ordinary Lake package. Its contents follow the 2026 community consensus on how to grow a formal library (the "How to formalize all of mathematics" statement by the Mathlib maintainers; document 01): defer to Mathlib, avoid duplication, keep quality high, and link to the literature.
- Mathlib first. If the result of a node is in Mathlib, the node binds to the Mathlib declaration and has no Lean code in math2.
Math2/holds only what Mathlib does not have. That includes statements of research-level results (so we can bind and review them before anyone proves them), proofs that are not upstream yet, conjecture statements, hypotheses for conditional results, and certificate checkers. - Module system only. Each file in
Math2/is a Leanmodule, with explicitpublic imports. Mathlib has moved to the module system. It will stop support for downstream projects that have not moved. If we start there, it costs nothing. - Mathlib style. Names, formats and linters follow the Mathlib conventions, and
lake lint --code-qualityruns in CI. This is a condition for upstreaming, and the plan is to upstream all general material (16.5). - Hypotheses are the only axioms.
Math2/Hypotheses/holds named axioms such asGRHorABC. Each one is bound to its conjecture node. Then the axiom profile of a conditional theorem names exactly the hypotheses that it depends on. Nothing else inMath2/may containaxiom,sorry,admit,@[implemented_by]orunsafe(ruleF06). - Conjectures are definitions, not axioms.
Math2/Conjectures/Collatz.leandefinesdef Collatz : Prop := …. A proof of the conjecture is a term of typeCollatz. A refutation is a term of type¬ Collatz. If the formal-conjectures repository has conventions for a statement of an open problem, follow them, and bind to its entries withsame_as. - A statement with no proof is a definition too. Rule
F06refusessorry, thus a result that is stated and not yet proved isdef lagrange_statement : Prop := …, and the node binds to it. The degenerate-statement checks and the F1 battery read the value of such a definition as the statement, as they read the type of a theorem (lean.Record.Statement). A definition of a proposition is never a proof, thus the binding is not E5. A proof addstheorem lagrange : lagrange_statement, moves the@[m2 "TAG"]attribute to it, and changes thedeclof the binding to the theorem. The value of the definition does not change.
The binding attribute
module
public import Mathlib.GroupTheory.Coset.Card
public import Math2.Meta.Attr
namespace Math2
/-- m2://9TQ4WA: every finite subgroup of the multiplicative group of a field is cyclic. -/
@[m2 "9TQ4WA"]
theorem finite_subgroup_units_cyclic {K : Type*} [Field K] (G : Subgroup Kˣ) [Finite G] :
IsCyclic G :=
inferInstance -- Mathlib's instance; the binding is to a math2 restatement
end Math2
@[m2 "TAG"] records the binding on the Lean side. Rule F01 checks that it agrees with the front matter of the node. A node can also bind to a Mathlib declaration that has no attribute, because math2 cannot add attributes to Mathlib. Only the front matter and registers/bindings.reg list those bindings.
7.3 Binding kinds and adapters
A binding states how the formal declaration relates to the informal statement of the node (04.5). The match field takes one of five values:
match |
Meaning | Can reach E5 for the node? |
|---|---|---|
exact |
the declaration states exactly the node's statement | yes |
stronger |
the declaration implies the statement; an adapter in Math2/Meta/Match.lean derives the exact form |
yes, through the adapter, which is itself a checked declaration |
weaker |
the declaration is a special case | no; contributes evidence for the special case only |
variant |
same mathematics, different formal packaging (e.g. Finset against Set, bundled against unbundled) |
yes, if the adapter proves the equivalence |
partial |
a statement-only binding for an unproved result, or one that is still sorry-free only in its statement |
never (rule F07) |
Mathlib often states a classical theorem in a more general form than a textbook does, or packages it differently. Adapters are how math2 handles this. The adapter is small and checked, and we review it together with the binding. The F2 review covers the composite: the conclusion of the adapter against the informal statement.
7.4 Extraction
After a build, M2Extract walks the environment. It writes one record for each bound declaration, and one record for each declaration in the closure of each bound declaration:
decl: Math2.finite_subgroup_units_cyclic
module: Math2.Algebra.Field.Units
tag: 9TQ4WA
type_hash: blake3:5f1c… # hash of the elaborated type, after instantiating universe params canonically
value_hash: blake3:a903…
statement_pp: "∀ {K : Type u_1} [inst : Field K] (G : Subgroup Kˣ) [inst_1 : Finite ↥G], IsCyclic ↥G"
axioms: [propext, Classical.choice, Quot.sound]
native_decide_axioms: []
uses_decls: [IsCyclic, Subgroup, Units, Field, Finite, instIsCyclicSubtypeMemSubgroupUnitsOfFinite, …]
uses_constants_in_statement: [IsCyclic, Subgroup, Units, Field, Finite]
deprecated: null # or {new_name, since, text} from the @[deprecated] attribute
checks: {} # or {vacuous, trivial, refuted}, each the tactic that closed the goal
heartbeats: 1840
- Type hash. This is the key hash in math2 (06.3). We compute it over the elaborated type in a canonical serialization: universe parameters get names by position, binder names are erased, and instance arguments stay. If two declarations have the same type hash, they state the same thing. Because of this, a review stays valid after a rename (Flow C, 03.4), and align can propose
same_aswithout judgement. - Dependencies.
uses_declscomes from the value of the declaration. It is each constant that the proof term mentions, closed transitively at the boundary of other bound declarations. Formal dependencies between bound declarations becomeusesevidence in the graph. RuleG04reports each formal dependency that theusesof the node does not show. - Statement constants.
uses_constants_in_statementdrives the review tool. A reviewer must see each non-trivial definition that the statement relies on (16.3). - Pretty-printing. We print the statement normally, and also with full elaboration and with notation off. Reviewers see both forms.
- Deprecation.
deprecatedcopies the entry of the@[deprecated]attribute. RuleF08reports each bound declaration that has one, with the new name. The bump agent (07.9) uses the same data to propose the rename. - Degenerate statements. For each theorem,
checksgives the result of the checks 4, 5 and 6 of 10.4. Automation tries to proveFalsefrom the hypotheses (vacuous), to prove the statement without the theorem itself (trivial) and to prove the negation (refuted). Each tactic has a limit of 10 million heartbeats, which is 5 percent of the default limit of Lean, and the record keeps the first tactic that closed the goal. RuleF09fails onvacuousand onrefuted. Atrivialresult does not fail, because a trivial statement can be the correct one.
Extraction runs in the same pinned environment as the build. Its output is a build artefact, with its hash as its address. The extraction code is part of the trusted base (15.1). Thus it is small, and we test it against golden files in M2Extract/test/.
7.5 Axioms and native_decide
- The allowed set for E5 is
{propext, Classical.choice, Quot.sound}. A node can allow fewer: front matteraxioms: constructivelimits it topropextandQuot.sound. Then status reports the node asnoac(04.7). - Hypotheses from
Math2/Hypotheses/can be in the axiom profile of a conditional result. Status reports them by name. The evidence of the node is then "E5 conditional on GRH", and the workspace shows this in words. native_decide. Since Lean 4.35, each use ofnative_decideadds a new auxiliary axiom, andLean.ofReduceBoolis removed. Thus the axiom audit seesnative_decidewithout a special case. It is not in the allowed set. A node that depends on it can be at most E4, with the certificate kindcompiled-evaluation, and ruleF04lists it. Ifdecide,decide_cbvorcbvcan do the same job with kernel-only reduction, we tell the prove role to use them.- The checker sees the exported environment, not the source. Axiom profiles come from the environment as the kernel checked it. A search of the source for
axiomis not sufficient, because metaprograms can add declarations.
7.6 Building
m2 build # nodes → lean → extract → replay → battery → status → graph → store → search
m2 build -only lean # lake build of Math2, with the Mathlib cache
- Cache. Lake's cache tools fetch the Mathlib
.oleancache, and we check it against the hash in the release manifest. The.oleanfiles are a build cache, not evidence. Evidence comes from the replay (7.7), which checks the exported environment again from the start. A compromised cache can make the build lie, but it cannot make the replay lie. - Incremental. PR CI builds only the modules that a change touches, plus their reverse dependencies inside
Math2. CI onmainbuilds everything. The release build runs on a clean runner with no cache from earlier PR runs. - Budgets. We record the heartbeat count of each declaration. If a PR makes the build time of
Math2more than 5% longer, its description must say why.
7.7 Replay and the checker stack
A proof is E5 only after it passes independent replay (rule F05). The toolchain provides this directly since 2026. This came after the kernel soundness incidents of that summer (document 01):
| Layer | Tool | What it adds |
|---|---|---|
| 1 | lake build |
elaboration and the official kernel, in the build process |
| 2 | lake check |
re-checks the exported environment with the official kernel, in a separate sandboxed process |
| 3 | lake check --paranoid |
adds the independent kernels nanoda (Rust), lean4lean (Lean), con-leche (Lean, proved consistent) and con-ron (Rust, proved to refine con-leche) |
| 4 | comparator | checks that the replayed declaration's statement matches the pinned statement (a challenge module built from the binding's recorded type, against Mathlib and Math2 only), and that its axioms are permitted |
At release time, math2 runs all four layers for each E5 candidate. On each PR, it runs layers 1, 2 and 4. The release manifest records the version of each checker. In 2026, a checker that was one week out of date accepted an exploit that the current checker rejected. The release audit fails if a checker is older than the version bundled with the pinned toolchain.
When this document was written, comparator had known open issues (document 01). Thus math2 also compares type hashes from its own extraction against the pinned binding (7.4). Two independent statement checks are cheap, and each one covers the gaps of the other.
For the largest proofs, a full paranoid replay is expensive. The 13M-line Fermat formalization needed 230 GB of RAM and 15 hours under comparator. math2 does not hold proofs of that size. It binds to the declaration in the external library. It records the replay results of that library with their checker versions, at E5 with the flag external_replay (04.7). It runs those replays again at the pinned commit of the library only when a checker changes.
7.8 Interfaces for agents and people
The formal engine has three front doors. All three use the pinned toolchain.
- Lean sessions for agents (11.3). These are REPL processes with Mathlib and
Math2loaded from a snapshot. They expose the state at tactic level. Implementations to build on are the community REPL, Pantograph, and lean-beam, which is the agent interface of the Lean FRO and is in beta now (document 01). math2 uses the one that gives the best control at goal level under its sandbox. The protocol in 11.3 belongs to math2, so the implementation can change and the agents do not see a change. - An MCP server.
m2 serveexposeslean_check,lean_goaland the session calls as MCP tools. With the publiclean-lsp-mcp, any MCP agent, Claude Code included, can work onMath2from a local checkout. - Workbench and editors for people. A contributor works in VS Code with the Lean extension on a local checkout. Or the contributor works in the zero-install Workbench of the Lean FRO in the browser (beta in 2026). math2 adds nothing to the editor experience other than the binding attribute. This is intentional.
7.9 The bump
The bump role and m2 bump keep math2 current. The process:
- Weekly, the bump branch moves to the newest Mathlib tag. It also tries Mathlib
masteras an early-warning build. We never merge that build. - Breaks are classified. Each failure is one of four kinds:
- a rename with a deprecation alias (mechanical: take the new name from the
@[deprecated]attribute); - a rename without one (find the new declaration by type hash);
- a refactor (the type hash changed);
- a real break in
Math2code (needs a fix).
- a rename with a deprecation alias (mechanical: take the new name from the
- Mechanical renames auto-merge (12.1). Migrate deprecated names before Mathlib deletes them. Mathlib deletes them in batches, about six months after deprecation.
- Refactors make reviews stale (rule
F02). Each affected node drops to F0 and goes into the fidelity queue, ranked byunlock. It gets a review-assist packet that shows the old and new types side by side. Math2breaks become prove jobs to repair the proof, at high priority. The release cannot pin the new Mathlib until they are fixed.- Upstream catch-up. A new Mathlib declaration can have the same type hash as a
Math2declaration. Then the bump proposes to rebind the node to Mathlib and to deprecate theMath2copy.Math2gets smaller as Mathlib gets larger. That is the intended outcome. - Toolchain bumps follow Mathlib, because each Mathlib tag names its toolchain. A toolchain bump also runs the full paranoid replay again, because the checkers change with it.
The bump report goes into reports/bump/<date>.md. It counts breaks by kind, time to green, and the nodes whose fidelity reviews became stale. Over months, it measures the cost to stay current. Each downstream Lean project pays this cost, and few measure it.
7.10 Other proof assistants
math2 is Lean-first, not Lean-only. Bindings have a system: field. Two other systems have support from the start:
- Isabelle/AFP. The AFP is the second-largest library, with more than a thousand entries. Some results exist only there. For example, the 2026 Jacobian counterexample was formalised in the AFP first. To check an AFP binding, build the entry at its pinned AFP and Isabelle release. It counts as E5 with
system: isabelle. The trusted base for such nodes is the Isabelle kernel, and the workspace says so. - Rocq/MathComp, Metamath, Mizar, HOL Light are crosswalk targets (
same_as, 05.3). In v1 they are not bindings for E5. Metamath's set.mm is the exception. It is a candidate for a v2 binding, because its verifier is very small and there are several independent implementations of it.
Translation between systems (lean2dk, lean2agda and similar tools) was still experimental in 2026, and it is not on the critical path. We record agreement between systems as same_as between bindings. It counts towards F3, as an independent formalization of the same statement, only when an equivalence is proved or both formalizations have independent F2 reviews (04.7).