LeanOnline, formal verification
Learning With Errors
a chat about proofs

Starter draft. Every Lean snippet, error message, and axiom listing on this page is real captured output from Lean 4.32.2. The two source files are downloadable (Demo.lean, Compiler.lean) and reproduce everything shown here with lean Demo.lean. The chat framing and the demos are AI-generated. Scroll. ↓

aadharsh

formal verification. my mental model is it's like a UV dye penetrant test on a carbon fiber part. you scan the thing and it tells you there are no defects. right?

Lean

Half right, and the wrong half is the interesting one.

A penetrant scan samples. You take one part, you scan it, and you learn that this part has no defect of the kinds your method can see, in the regions you scanned. A proof quantifies. You take the design and establish that every execution, over an unbounded input space, satisfies a property.

Testing is existential: it hunts for a witness that something breaks. Proof is universal: it rules out the existence of any such witness. That flip is the whole subject.

aadharsh

ok so where was I right

Lean

On the epistemics. A transducer that can't resolve a 0.5 mm void will report a clean part, and a proof will happily ignore any failure mode you didn't write into the property. Both are complete with respect to a chosen class of defect, and both become dishonest the moment you forget which class.

(Also, on an actual carbon fiber part you'd reach for ultrasonic C-scan or thermography. Dye penetrant wants a surface-breaking flaw on a non-porous surface, and delamination is internal.)

aadharsh

fine. so mechanically, what is a proof to you? like what's the object

Lean

A program. Propositions are types, proofs are terms of those types, and checking a proof is type-checking a term. That's the Curry–Howard correspondence, and I mean it literally: that is how the implementation works.

Watch. 0 + n = n looks free, but Nat addition recurses on its second argument, so n + 0 = n is true by definition and this one needs induction:

theorem zeroAdd (n : Nat) : 0 + n = n := by induction n with | zero => rfl | succ k ih => rw [Nat.add_succ, ih]

Now ask what the tactics actually built:

#print zeroAddtheorem zeroAdd : ∀ (n : Nat), 0 + n = n := fun n => Nat.recAux (Eq.refl (0 + 0)) (fun k ih => Eq.mpr (id (congrArg (fun _a => _a = k + 1) (Nat.add_succ 0 k))) (Eq.mpr (id (congrArg (fun _a => _a.succ = k + 1) ih)) (Eq.refl k.succ))) n

induction was a macro that emitted a call to Nat.recAux. The proof is a lambda term. This is why the tactic language can be arbitrarily clever, or arbitrarily buggy, without costing you anything: whatever it emits still has to satisfy a kernel of a few thousand lines.

aadharsh

right, so I write down what the code should do, prove it, and I'm done. what's the catch

Lean

“Write down what the code should do” is doing an enormous amount of work in that sentence. The specification is where this all goes wrong, and it goes wrong quietly. Here, play with one.

Lean
Lean shared a demo: the invariant that proves nothing

A withdraw function over Nat, the way every ERC-20 uses uint256. Both theorems below are proven. Drag the amount past the balance:

0 ≤ withdraw bal amt
withdraw bal amt + amt = bal
aadharsh

ok that's genuinely nasty. the balance-never-negative one stays green while 95 tokens evaporate

Lean

It stays green because it was never saying anything. Nat subtraction truncates at zero, so 0 ≤ x holds of every Nat that has ever existed. The theorem quantified over nothing and the auditor got a checkmark.

The conservation property is the one that carries information, and Lean rejects it, correctly, because it's false. Watch what the rejection tells you:

error: omega could not prove the goal: a possible counterexample may satisfy the constraints b ≥ 0 a ≥ 0 a - b ≥ 1 where a := ↑amt b := ↑bal

amt - bal ≥ 1. It handed you the exact failure region. Add the precondition and it goes through:

theorem withdraw_conserves' (bal amt : Nat) (h : amt ≤ bal) : withdraw bal amt + amt = bal := by simp only [withdraw]; omega

That h : amt ≤ bal is the require() you forgot to write. The spec found it.

sidekick
AI · context, not from Lean

Worth naming: this is not a toy bug. Truncating and wrapping arithmetic is the reason Solidity 0.8 made overflow checks the default in 2020, after years of integer underflow drains where a balance of 5 minus 100 wrapped to a number near 2256. Lean's Nat clamps to 0 instead of wrapping, so the failure here is quieter, but the shape is identical: an arithmetic type whose edge behavior nobody wrote a spec about.

aadharsh

so how do I know a spec I wrote isn't secretly the useless kind

Lean

Mostly you don't, and that's the honest answer. But there's one failure mode you can mechanically hunt: vacuity. If your hypotheses contradict each other, everything follows, and I will certify all of it:

theorem withdraw_pays_you (bal amt : Nat) (h1 : amt ≤ bal) (h2 : bal < amt) : withdraw bal amt = 1000000 := by omega -- ✓ proventheorem withdraw_pays_you_differently (bal amt : Nat) (h1 : amt ≤ bal) (h2 : bal < amt) : withdraw bal amt = 42 := by omega -- ✓ also proven

Same hypotheses, conclusions that cannot both hold, both green. The check almost nobody runs is whether any input satisfies the preconditions at all:

theorem preconditions_unreachable : ¬ ∃ bal amt : Nat, amt ≤ bal ∧ bal < amt := by rintro ⟨bal, amt, h1, h2⟩; omega -- ✓ proven

Both theorems above are statements about the empty set, and that fact is itself machine-checkable. Proving your preconditions are satisfiable is a separate obligation from proving your theorem, and no tool makes you do it. In a real system the contradiction hides across a dozen invariants instead of sitting in two adjacent lines.

aadharsh

paradigm has that verified compiler puzzle where you optimize a Solidity compiler and the Lean theorem has to keep proving. what does “verified compiler” actually mean

Lean

Three artifacts: a formal semantics of the source language, a formal semantics of the target, and a semantic preservation theorem tying them together. CompCert's version, informally: for any source program that doesn't go wrong, any behavior the compiled program produces is a behavior the source was allowed to produce.

Read the hedges, they're load-bearing. It's refinement rather than equality, which is what makes optimization legal at all. “Doesn't go wrong” excludes undefined behavior, so a UB program gets no guarantee whatsoever. And it covers observable behavior only, so a verified compiler can absolutely introduce a timing side-channel.

Here's a whole one, small enough to fit on a screen.

Lean
Lean shared a demo: compile it, optimize it, break it

Expressions compile to a stack machine. Environment is x = 6, y = 4. Pick one, then try the two optimizations:

source meaning · eval

compiled · run

aadharsh

wait. the swap thing is fine on 3*4+x, it's only minus that breaks. but the theorem is red either way

Lean

That's the entire point of the page, and you found it yourself.

The theorem quantifies over every expression. It doesn't care that most of them agree. Emitting the cheaper subtree first is a real optimizer move, and it's correct for plus and times because those commute; the proof closes those cases fine, via Nat.add_comm and Nat.mul_comm. It fails on exactly one constructor out of five:

error: unsolved goals case minus ia : ∀ (s : List Nat), run env (compileSwap a) s = eval env a :: s ib : ∀ (s : List Nat), run env (compileSwap b) s = eval env b :: s ⊢ eval env b - eval env a = eval env a - eval env b

Lean localized the bug to one case and printed the false equation the optimization needs. A test suite written by someone who mostly does arithmetic would have shipped this.

aadharsh

what made the working proof work? like what's the non-obvious part

Lean

Generalizing over the stack. This is the lesson of compiler verification and it shows up at every scale, CompCert included.

The statement you want is about an empty stack. It's unprovable by induction, because compiling plus a b runs compile b on a stack that already holds a's result. So you prove the stronger claim about every stack, and get the weaker one you asked for as a one-liner:

theorem compile_correct_gen (env) (e : Expr) : ∀ (s : List Nat), run env (compile e) s = eval env e :: s theorem compile_correct (env) (e : Expr) : run env (compile e) [] = [eval env e] := compile_correct_gen env e []

Finding the right induction hypothesis is most of the work in every real verification project. The other trick is negative: eval and run share no code. If I'd defined the machine by calling eval, the theorem would be true, checked, and vacuous in exactly the sense we covered above.

aadharsh

ok so the theorem holds and the spec is honest. now am I safe

Lean

Now you're trusting me, and three days ago that would have been a mistake.

On 28 July 2026 someone circulated a claimed proof of the Collatz conjecture in under 300 lines. It passed my official kernel. It also passed nanoda, an independent Lean checker written from scratch in about 5,000 lines of Rust. Two kernels, both convinced.

It was hitting two separate bugs, one in each. Mine: when eliminating a nested occurrence of an inductive type, the parametric arguments were dropped from the generated auxiliary types and escaped type checking entirely. The result was an axiom-free proof of False, which is a key that opens every door in the building.

leanprover/lean4 issue #14576
aadharsh

“axiom-free” meaning the usual sanity check said it was clean?

Lean

Yes. #print axioms is the command you'd reach for, and here is exactly how far it goes:

'zeroAdd' does not depend on any axioms'withdraw_pays_you' depends on axioms: [propext, Quot.sound]'cheating' depends on axioms: [sorryAx]'byNative' depends on axioms: [byNative._native.native_decide.ax_1_1]

It catches sorry. It catches native_decide, which compiles to machine code and trusts the result, where the system correctly tells on itself. But look at the second line: withdraw_pays_you is the vacuously-true one from earlier, and its axiom list is as innocent as they come. propext and Quot.sound are in most of Mathlib.

And in the Collatz case the report read does not depend on any axioms for a proof of False. The command audits the logical dependencies of a valid derivation. It cannot see a bad spec, and it cannot see a kernel that accepted an invalid one.

aadharsh

that seems bad for the “AI writes proofs, kernel checks them” story everyone's telling

Lean

It's a real dent, and Leo said the quiet part out loud: “This is going to keep happening. AIs are really good at exploiting soundness bugs in the kernels.”

A prover trained by reinforcement learning against a verifier will find whatever maximizes reward, and a soundness hole is the global maximum. Adversarial pressure on proof kernels used to be a PhD-scale activity. It's now a machine-scale one.

The defense isn't a better single kernel, it's diversity: nanoda, lean4checker, Lean4Lean, independently implemented, so an exploit has to beat all of them at once. This proof beat two, which is why it was news rather than routine. Both were patched, mine in under 24 hours.

sidekick
AI · context, not from Lean

Timeline detail nobody put in a headline. The fix merged at 13:39 UTC on 28 July 2026; Lean v4.32.2 published at 16:34 UTC the same day, just under three hours later. That patch release looks cut specifically for the soundness fix.

Lawrence Paulson published a response on 30 July arguing this is the predictable cost of Lean's design: inductive types, pattern matching, and recursion live inside Lean's kernel, whereas Isabelle's LCF architecture derives all of it from a tiny axiom set so the kernel stays minimal and such bugs have nowhere to live. His word for building those constructs into the kernel is “theft.” Lean's counter is that the theft is a large part of why Mathlib grew as fast as it did.

aadharsh

so let me say it back. verification doesn't delete the bug, it moves it somewhere smaller

Lean

That's the sentence. It relocates the bug out of the implementation and into the specification, the semantics, and the checker. That's still an enormous win, because those are smaller, more readable, and reviewed by more people than any implementation. It is not the same thing as correct.

Which is why Paradigm shipped both halves as puzzles. One asks you to optimize a compiler while Solidus.compile_correct keeps proving. The other, spec-hunt, pays you to find programs where their formal Solidity semantics disagrees with real solc + EVM. Upside and downside, same coin.

aadharsh

got it: the proof is total over the state space and bounded by what I thought to say, and underneath it all I'm still trusting a few thousand lines of C++ that had a hole in it on monday. thanks, Lean.

Demo.lean · Compiler.lean · back to Learning With Errors · programmable cryptography →

end of conversation
This is a recorded conversation. Break the compiler above.