Jump to content

Computation inside proofs

From The Invariance Itself
Revision as of 18:00, 14 October 2026 by PMA Publisher (talk | contribs) (Published from the Principia Machina source)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Some statements are proved by calculating. That 2 + 2 is 4, or that 97 is prime, can be settled by working the sum or by trying every divisor. Lean accepts such calculations as proofs. This page shows how, what has to be trusted when it does, and what the calculation costs.

A sum as a proof

example : 2 + 2 = 4 := rfl

example : 2 ^ 100 = 1267650600228229401496703205376 := rfl

The proof rfl says that the two sides are the same thing. To accept it, Lean works out both sides and compares the results. Two expressions that compute to the same value are, in Lean's terms, definitionally equal, and for them rfl is a complete proof (see two kinds of equality).

The second line raises 2 to the power 100 and is accepted at once. In the logic a natural number is zero followed by a chain of successors, and a chain of that length could never be written out. The Lean kernel does not try. It stores number literals in a fast library for arithmetic on large numbers, usually GMP, and replaces addition, multiplication and the other basic operations with routines from that library. The Lean Language Reference adds the condition that comes with the speed: these routines are part of the Lean kernel, so if they did not agree with the definitions they stand in for, soundness could suffer.[1]

Deciding a finite question

A finite question can be answered by a program, and the run of the program can be a proof. Here is a test for primality by trial division, applied to 97:

def isPrime (n : Nat) : Bool :=
  n ≥ 2 && (List.range (n - 2)).all (fun k => n % (k + 2) != 0)

#eval isPrime 97

theorem prime97 : isPrime 97 = true := by decide

#print axioms prime97

The function checks that n is at least 2 and that no number from 2 to n − 1 divides it. #eval runs it and prints true. That is a program's answer, not yet a proof. The theorem turns it into one. The tactic decide finds a procedure that decides the statement and evaluates it inside the logic; when the result is "true", that evaluation is the proof, and the Lean kernel repeats it when it checks the theorem.[2] The last line confirms that nothing extra was assumed: 'prime97' does not depend on any axioms.

Running the program instead: native_decide

The same question can be answered faster by running the program as a program and accepting its answer:

theorem prime97_native : isPrime 97 = true := by native_decide

#print axioms prime97_native
#print prime97_native._native.native_decide.ax_1_1

Lean 4.34.1 answers:

'prime97_native' depends on axioms: [prime97_native._native.native_decide.ax_1_1]
axiom prime97_native._native.native_decide.ax_1_1 : decide (isPrime 97 = true) = true

The theorem now rests on an axiom made for the occasion. native_decide evaluated the question the way #eval does, and then added an axiom stating the answer. The Lean kernel asks for no proof of an axiom; it accepts it. So the theorem is only as sound as the evaluation behind the axiom, which means the Lean compiler and every function that has been given a faster implementation for running. The reference manual says that this adds the entire Lean compiler to the trusted part of the system, and that a new axiom will show up in #print axioms for the theorem and for anything that depends on it.[2] In versions of Lean up to 4.28, such theorems were marked instead by one shared axiom, Lean.trustCompiler.[3]

That is a widening of what must be trusted, not a mistake in the answer. The prime is still prime. But a reader who wants to rely on the theorem must now trust a much larger body of code than the Lean kernel. The project this wiki records forbids native_decide in its own sources, as its proof gate states (see the transition page). Nothing is hidden either way: the axiom's name appears in the list, as on what has to be trusted.

What the calculation costs

Evaluation inside the logic is slow. It proceeds step by step through the definitions, with few of the shortcuts of a compiled program, and it has limits on how deep it may go. Asking the same question about 1009 shows the difference:

#eval isPrime 1009

example : isPrime 1009 = true := by decide

#eval prints true at once, but decide stops with maximum recursion depth has been reached. The option decide +kernel hands the evaluation directly to the Lean kernel, which evaluates the procedure once instead of twice:[2]

theorem prime1009 : isPrime 1009 = true := by decide +kernel

#print axioms prime1009

This succeeds, and 'prime1009' does not depend on any axioms. The cost grows with the size of the question. In one measurement made for this page, on one computer, checking the same theorem for the six-digit prime 100003 with decide +kernel took about fifteen seconds, while running the program with #eval took well under a second. Other computers will give other times.

Some procedures cannot be evaluated inside the logic at all. A decision procedure defined by well-founded recursion may get stuck, because evaluating it would require working through proofs as well as data.[2] The page on functions that always finish explains where such definitions come from.

The choice

Computing inside a proof is a trade between speed and trust. Evaluation by the Lean kernel is slow and bounded, but it adds nothing to what must be trusted. Evaluation by the compiler is fast, but it adds the compiler, and an axiom, to the foundation of the theorem. When a calculation is too large for the first, the alternatives include proving a general theorem that makes the calculation unnecessary, and accepting the larger trust and recording it. Lean makes the choice visible, so that a reader can see which was taken.

Next

References

  1. ↑ The Lean Language Reference, version for Lean 4.34.0 (lean-lang.org, 2026), section 20.1, "Natural Numbers", with section 20.1.2, "Run-Time Representation". https://lean-lang.org/doc/reference/4.34.0/Basic-Types/Natural-Numbers/
  2. ↑ 2.0 2.1 2.2 2.3 The Lean Language Reference, version for Lean 4.34.0 (lean-lang.org, 2026), section 14.5, "Tactic Reference": entries for decide and native_decide. https://lean-lang.org/doc/reference/4.34.0/Tactic-Proofs/Tactic-Reference/
  3. ↑ The Lean Language Reference, version for Lean 4.34.0 (lean-lang.org, 2026), "Validating a Lean Proof", with the note "On Lean.trustCompiler (up to Lean 4.28.0)". https://lean-lang.org/doc/reference/4.34.0/ValidatingProofs/