Formal Verification of the Brainfuck Esolang in Lean 4.
LeanBF formalizes the Brainfuck esolang in the Lean 4 interactive theorem prover. It pins down a precise, total, deterministic formalization — the tape, the instruction set, and a small-step operational semantics are all pure Lean definitions — and proves two things about it.
Brainfuck is Turing complete: it simulates a two-counter Minsky machine
(turingCompleteness, closed by Theory.Simulate), in both directions — the
compiled program halts holding the machine's final counters, and a halting
compiled run implies the machine halts.
Brainfuck's halting problem is undecidable
(brainfuck_halting_undecidable): there is a fixed program whose halting on
a given input no computable predicate decides. The chain runs
Nat.Partrec.Code → register machine → two counters → Minsky → Brainfuck,
each link proved both ways, with the register file packed into two counters
by a Gödel encoding.
Every transition of the interpreter is a pure Lean function; nothing is executed by an external trusted interpreter.
For a detailed overview of the project's design and the current state of the verified theorems, see ARCHITECTURE.md.
The implementation is organized into Core (definitions), Theory
(verified theorems — the dispatch simulation, the completeness proof, and
the run-level tape lemmas), Examples (example programs, split into the
Brainfuck programs and the Minsky machines that exercise the compiler), and
Tests (kernel re-assertions of the definitions).
The headline results, with the supporting machinery documented in ARCHITECTURE.md rather than repeated here.
- Total, deterministic interpreter (
Core.Semantics):step/RunsTodefine a single-step transition onProgram × State, with loop unrolling and end-of-input handled inside the pure function. It is total and deterministic (Theory.Determinism), so a program's behaviour is a function of its state, not a matter of interpretation. - The Brainfuck state machine (
Core.State): an infiniteInt-indexed tape of unboundedNatcells, pointer, input stream, and output stream. - The Minsky machine model (
Core.Minsky): a two-counter machine withinc/jzdecinstructions and its ownRunsToclosure. - The compiler (
Core.Compiler): a total translation of any Minsky program into a Brainfuck dispatch loop, exercised end-to-end inTestswithdecide. - Turing completeness (
Theory.Completeness,Theory.Simulate):turingCompleteness— whenever the Minsky machine runs toms_final, the compiled program halts withms_final's counters in cells 2 and 3. The converse holds too: a halting compiled run implies the machine halts, so the simulation is faithful in both directions rather than only sound. - Undecidability of Brainfuck halting (
Theory.Undecidable):universal_brainfuckis one fixed Brainfuck program that halts on the tape encoding2 ^ Nat.pair c nexactly when codechalts on inputn. The program depends on neither the code nor the input and the starting tape is a computable function of both, so a decider for Brainfuck halting would decideNat.Partrec.Codehalting, whichComputablePred.halting_problemforbids. The chain runsNat.Partrec.Code→ register machine (Theory.Universal) → two counters (Theory.Packing, via a Gödel encoding of the register file as prime exponents) → Minsky → Brainfuck, each link proved in both directions. - Concrete syntax (
Core.Parser): a totalparse : String → Program, driven by an explicit fuel argument because a[continues on whatever its body left behind, which is not a structural subterm. Every example program is checked against the source it documents by aparsetheorem, so a transcription and its source cannot drift apart. - Brainfuck idioms (
Theory.Idioms): the loops a programmer writes, stated on the literal instruction lists rather than on compiler output.[-]clears a cell,[->+<]moves one and[->+>+<<]duplicates it, and[>]/[<]scan to the nearest zero. The scans are the interesting case: they do not terminate on every tape, sorunsTo_scanLooptakes the distance to a zero as a parameter together with the hypothesis that no cell before it is zero — without that minimality the statement is false. - Verified example programs (
LeanBF.Examples, split intoBrainfuckandMinskyby what each is written in):cat(,[.,]) is proved for every input rather than on fixed cases —catRunssays its output isinput.takeWhile (· != 0), so it echoes only inputs with no zero byte and otherwise stops early, andcat_haltsholds unconditionally because,writes0at end of input.HelloWorldis machine-checked withdecideto print exactlyHello World!and a newline and to halt. The Minsky machines (countDown,quadruple,tripler,addMachine) exercise the compiler, their compiled programs verified to halt with the right counters and to complete under the executablerunin exactlynsteps. - Kernel re-assertions (
Tests): the definitions are re-asserted on concrete inputs withrflanddecide, including compiled Minsky programs run to their halting state.
Open work only. What the project has already proved is described under Results and in ARCHITECTURE.md.
| Task | Priority | Notes |
|---|---|---|
| The scan idioms' divergence | Medium | runsTo_scanLoop covers [>] only when a zero exists to the right, and ARCHITECTURE.md asserts in prose that the loop diverges otherwise — a claim nothing in the development proves. The machinery is already here: loop_incVal_never_halts (Theory.Semantics) proves a Brainfuck program never halts, by strong induction showing stepsToHalt n = n for every fuel, and the same shape should carry over with the invariant "every cell from the pointer rightward is nonzero", which incPtr preserves. The hypothesis is the awkward part: an infinite tape that is nonzero everywhere to the right is a statement about infinitely many cells, so it is worth checking that it states cleanly before assuming the proof does. Closing this would also let the prose stop asserting what it cannot cite. |
| Idioms as composable fragments | Low | The idiom theorems are stated on a program that is exactly the idiom, so runsTo_moveLoop says what [->+<] does when it is the whole program, not when it sits inside one. The compiler layer solved this already — Theory.Transfer states its loops with Reaches and hypotheses pinning the slots a fragment occupies, precisely so fragments compose — and RunsTo_append is the tool. Restating the idioms that way would make them usable for proving things about real programs assembled from them, which is the point of having them. Whether it is worth the restatement depends on there being a program that wants them. |
| More Brainfuck programs | Low | cat established that decide is not the constraint and that the interesting statements are input-quantified. The obvious next ones are the two-cell adder ,>,[-<+>]<. (measured: 26 steps on 3, 4, so well within reach) and echo. Neither is likely to teach anything new about the semantics, which is why this is Low rather than a natural successor to the cat work — it would be more examples of a thing already demonstrated, not a new kind of result. |
Scope. LeanBF formalizes the Brainfuck language — the tape, the eight
commands, the bracketed loop as a recursive instruction list, and the
small-step semantics — together with the Minsky machine model, the compiler,
and the proof that Compiler.compileProgram simulates a two-counter Minsky
machine, so Brainfuck is Turing complete. It also formalizes the register
machine and Gödel-encoding layers needed to run the reduction the other way,
giving the undecidability of Brainfuck halting.
Design choices. Brainfuck's informal specification leaves several details
open; LeanBF makes the following choices, documented in ARCHITECTURE.md:
- Infinite tape: cells live on an
Int-indexed tape with unboundedNatvalues, so pointer arithmetic never faults and decrement never underflows. - Loops as trees:
[/]are paired at definition time;loop bodyis a recursiveList Instruction. At runtime a non-zero loop pushesbody ++ [loop body] ++ rest, so no control-flow parser is trusted. - Input/output:
,consumes from an explicitList Natinput stream (pushing0at EOF);.prepends the current cell to the output stream. - Halting: a program halts exactly when its instruction list is empty.
Limitations.
- The completeness theorem is proven:
Theory.Simulate.turingCompleteness_proofshowsCompiler.compileProgramsimulates the two-counter Minsky machine and halts holding its final counters (HaltsWith). The halting state is notSimulates ms_final: the dispatch loop clears the running flag on halting, so cell0is0whereSimulatesdemands1. - The undecidability theorem is stated of the universal program specifically: no computable predicate decides whether that program halts on a given input. It is not phrased as a statement quantifying over all Brainfuck programs and states, which would need a computable encoding of programs and tapes that the project does not define.
- The compiled two-counter programs are enormous and are never run. A
conditional on register
rcompiles to a block quadratic inr's prime — fifty-four slots for the third register — and the register file is a product of prime powers, so a machine holding a few small values already needs a counter beyond any feasible simulation. The construction is a proof device, not an implementation;compileis also noncomputable,regPrimebeing defined throughNat.nth.
Make sure you have elan installed for Lean 4 version management.
git clone --recurse-submodules https://github.com/bangyen/leanbf.git
cd leanbf
lake exe cache get # Downloads the pre-compiled Mathlib libraries
lake buildThen run the verification:
lake test # Builds the test suite
lake lint # Runs the linter
./scripts/check_all.sh # Runs the repository guard checksThis repo uses standard Mathlib naming conventions and the shared guard scripts
from lean-guards, vendored as a
submodule at scripts/. If you are interested in extending the formalization —
for example, completing the jzdec translation or taking a first step toward
the completeness proof — feel free to open a pull request.
If you use this work in your research, please cite:
@misc{pham_leanbf_2026,
author = {Pham, Bangyen},
title = {LeanBF: Formal Verification of the Brainfuck Esolang in Lean 4},
year = {2026},
url = {https://github.com/bangyen/leanbf}
}