Verified Compilers
CompCert’s middle-end and backend carry a machine-checked proof that the compiled code refines the source semantics. The honest evidence is the Yang et al. fuzzing result: every other compiler tested had wrong-code bugs found, and the verified part of CompCert had none. What is *not* proven matters just as much.
What does it actually mean for a compiler to be proven correct, and what is still not proven?
Three artifacts held together by a theorem: a formal semantics for the source language, a formal semantics for the target, and the compiler as a function between them written in a language a proof assistant can reason about. The program is never just source or just machine code — at every intermediate language it is a mathematical object with a defined behavior, and the proof is a chain of refinement lemmas linking one to the next.
The theorem states a precondition and a conclusion, and both matter. Precondition: the source program is well-defined — it does not go wrong under the source semantics. Conclusion: every observable behavior of the compiled program is a permitted behavior of the source program. Nothing is claimed for a source program that has undefined behavior, which means a verified compiler gives you exactly nothing about a C program with a buffer overflow in it. The proof rules out compiler-introduced misbehavior; it cannot rule out program-introduced misbehavior.
Key points
- A verified compiler carries a machine-checked theorem that the compiled program refines the source program’s semantics.
- The precondition is that the source program is well-defined; a program with undefined behavior gets no guarantee at all.
- The trusted computing base includes both formal semantics, the proof assistant, and historically the parser, assembler, linker and runtime.
- The Yang et al. Csmith campaign found wrong-code bugs in every other compiler tested and none in CompCert’s verified middle-end — that is the honest evidence.
- Bugs were still found in CompCert’s unverified perimeter, which is the correct way to read the result.
- The costs are a language subset, code quality behind the most aggressive optimizers, and research-scale effort per pass.
What is proven
CompCert is a C compiler whose passes are written in Coq (now Rocq) and accompanied by machine-checked proofs. The proofs are then composed: each pass is shown to preserve semantics, and semantic preservation composes, so the whole chain from a formalized C subset down to assembly for several targets carries a single theorem.
The theorem is a refinement statement, and its shape is the same one [[translation-validation]] checks per compilation: if the source program is well-defined and has some observable behavior, then the compiled program has a behavior that the source was permitted to have. It says nothing about performance, nothing about code size, and nothing about programs the source semantics does not define.
It is worth being precise about the granularity of "observable". CompCert's semantics defines observable behavior as the trace of external function calls, volatile accesses and the final exit status. Ordinary memory writes to non-volatile locations are not observable, which is exactly what licenses the optimizations that exist — the same [[observable-behaviour]] boundary every compiler works within, only written down formally rather than argued about informally.
What is not proven, which is the part people skip
The trusted computing base of a verified compiler is much larger than the verified part, and every item in it is a place a bug can live. Naming them is not a criticism of the project — the CompCert authors name them themselves — it is the difference between using the tool correctly and holding a talisman.
The specification itself is trusted. The proof says the compiler implements *this formal semantics of C*; whether that semantics matches the ISO C standard, and whether the standard matches what you believed, is human judgement. Likewise the target semantics: the proof says the output means something specific in a formal model of the assembly language, and whether the actual processor implements that model is not a theorem.
And the practical edges are unverified for ordinary engineering reasons. Historically the parser and elaboration front end, the assembler and linker, the runtime library, and any code you link against are all outside the proof. Bugs have been found in exactly those places, which is the strongest evidence for taking the list seriously.
| Component | Verified? | What that means for you |
|---|---|---|
| Middle-end and backend passes | Yes — machine-checked semantic preservation | The class of bug fuzzers find in other compilers is ruled out here |
| Formal semantics of the source | Trusted, not proven | If the model disagrees with ISO C, the compiler faithfully implements the wrong thing |
| Formal semantics of the target | Trusted, not proven | If the model of the ISA is wrong, correct-by-proof code still misbehaves on the chip |
| Parser and elaboration | Historically outside the proof | A frontend bug produces a wrong program that the verified pipeline then faithfully preserves |
| Assembler and linker | Outside | The proof ends at assembly text; encoding and relocation are somebody else’s correctness problem |
| Runtime library and linked code | Outside | Nothing is claimed about memcpy, the startup code, or any library you link |
| The proof assistant | Trusted | A soundness bug in the kernel would invalidate the proof; small, heavily scrutinized, not zero |
| Your program’s definedness | Assumed | A program with undefined behavior gets no guarantee whatsoever |
The honest evidence
The reason to believe verification works here is not the existence of the proof — it is an empirical result. In the Csmith work, Yang, Chen, Eide and Regehr ran their generator against every C compiler they could obtain and found wrong-code bugs in all of them: GCC, Clang, and a long list of commercial compilers. Against CompCert they found crash and rejection bugs, and bugs in the unverified frontend — and not one wrong-code bug in the verified middle-end.
That is a much stronger statement than a proof on its own, because it was produced by the same adversarial process that found bugs everywhere else. It is also carefully bounded, and worth quoting in its bounded form: the middle-end held up under a fuzzing campaign that broke every other compiler tested. It is not a claim that CompCert has no bugs, and the authors did not make one.
The practical reading: verification eliminated a bug class, and it did so exactly where the proof applied and nowhere else. Bugs continued to appear in the unverified perimeter. That is the shape of the result, and it is also the shape of the advice — verification is a tool for removing one class of failure from one region, not a state a project reaches.
What it costs, and when it is worth it
The costs are substantial and specific. The language handled is a subset — CompCert targets a large fragment of C99 rather than everything a mainstream compiler accepts, and the extensions your codebase uses may not be in it. The generated code is competitive but not state of the art: it is typically in the neighbourhood of GCC at moderate optimization, well behind a modern optimizer at its most aggressive, because every optimization must be proven and the aggressive ones are the hardest to prove. Adding a pass is a research-scale effort rather than a sprint.
So it is worth it where a miscompilation is a safety or certification problem rather than an inconvenience: avionics, rail signalling, nuclear instrumentation, medical devices. In those settings the alternative is not "a faster compiler" — it is an enormous manual review and testing burden imposed by a certification standard, and a verified compiler can displace part of that burden, which is where the economics actually come from.
For everyone else, the useful takeaway is not "use CompCert". It is that the verified middle-end result tells you where the bug class lives and that it is eliminable, and that the cheaper techniques in this module — validation, differential testing, fuzzing — are attempts to buy a fraction of the same assurance without the same cost.
- Verified region: middle-end and backend. That is where the empirical result applies.
- Language subset: a large fragment of C99, not every extension a mainstream compiler accepts.
- Code quality: broadly comparable to a mainstream compiler at moderate optimization, behind the most aggressive settings.
- Adding a pass costs a proof, so the optimizer grows slowly and deliberately.
- The economics come from certification burden displaced, not from developer convenience.
How it works
The steps, in the order the compiler takes them.
- Write a formal operational semantics for the source language, defining exactly which behaviors each program is permitted to have.
- Write a formal semantics for each intermediate language and for the target assembly.
- Implement each pass as a function in the proof assistant’s language, over those formal representations.
- Prove for each pass that any behavior of its output is a permitted behavior of its input, given that the input is well-defined.
- Compose the per-pass lemmas into a single end-to-end semantic-preservation theorem.
- Extract executable code from the verified definitions, and accept the extraction mechanism, the proof kernel and both semantics into the trusted base.
- Test the unverified perimeter — parser, assembler, runtime — the ordinary way, because the proof does not reach it.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A team treats "verified compiler" as "our software is now correct", and ships a program whose own undefined behavior the proof explicitly disclaims.
- The formal source semantics differs from the standard in a corner the codebase relies on, and the compiler faithfully implements the wrong meaning with a proof attached.
- A bug in the unverified parser produces an internal program that does not match the source, and the verified pipeline preserves it perfectly all the way to the binary.
- The required optimization is not in the verified compiler, the team enables an unverified pass to get the performance, and the guarantee silently no longer covers the build.
- The certification argument cites the theorem without citing its precondition, and the auditor who reads the precondition rejects the argument late and expensively.
- Code links against a runtime routine or an assembly stub outside the proof, and the failure occurs there while everyone searches the verified region.
When it helps
- Safety-critical and certified software, where the cost of a miscompilation is measured in lives or in regulatory approval rather than in an incident review.
- Settings where certification standards demand evidence about the object code, and a proof can displace part of a manual review burden that is far more expensive.
- As a reference implementation: a verified compiler is an oracle other compilers can be differentially tested against, with unusually high confidence in the reference.
- As an existence proof for the rest of us — it establishes that middle-end wrong-code bugs are eliminable, which is what justifies investing in the cheaper approximations.
When it hurts
- Performance-critical work at the top of the optimization ladder, where the verified optimizer is meaningfully behind and the gap cannot be closed without unverified passes.
- Codebases relying on language extensions, inline assembly or dialect features outside the verified subset.
- As a substitute for reasoning about your own program. The proof’s precondition is your program’s definedness, and nothing verifies that for you.
- Fast-moving language development, where a new feature must be specified and proven rather than implemented and iterated.
What it costs
Every one of these is paid by something.
- Verification buys the elimination of an entire bug class in the proven region and costs a language subset, slower generated code at the top end, and a per-pass effort measured in person-years.
- A formal semantics buys a precise statement of what the compiler guarantees and costs the ongoing obligation to keep the model, the standard and the implementation in agreement — a three-way consistency problem with no automated check.
- Extracting executable code from proofs buys a direct correspondence between the proven artifact and the running one, and costs performance in the compiler itself plus trust in the extraction mechanism.
- Restricting to a verified subset buys the theorem and costs portability of your existing code, which typically must be modified to compile at all.
- Relying on the theorem in a certification argument buys evidence and costs the discipline of stating the precondition every time, since an argument that omits it is worse than no argument.
What else you could do
What a different compiler or language does instead, and when that is better.
- Translation validation gets a per-compilation version of the same guarantee for a fraction of the cost, on the compiler you already use —
[[translation-validation]]. - Differential testing and fuzzing find the same bug class empirically, with no guarantee and no language subset —
[[differential-testing]]and[[compiler-fuzzing]]. - Verify the program instead of the compiler: seL4, and tools such as Frama-C or a Rust-based rewrite, attack the far larger problem of your own code’s correctness, which is where most bugs actually are.
- Use a memory-safe language and accept an unverified compiler. It removes a different and statistically much larger class of failure — your own undefined behavior — which for most projects is the better trade.
See it for yourself
The flag, dump or tool that shows you this directly.
- CompCert ships as
ccomp;ccomp -O -S file.ccompiles through the verified pipeline, and the manual’s section on the trusted computing base states the boundary explicitly. Read that section before relying on anything. - The proofs are readable artifacts: the Coq/Rocq sources define the semantics of Clight, Cminor and the target assembly, and the per-pass preservation lemmas are stated in full.
- The empirical result: the Csmith paper "Finding and Understanding Bugs in C Compilers" (Yang, Chen, Eide, Regehr) reports the campaign and the CompCert comparison, and its bounded phrasing is worth reading in the original.
- For a related project in a different area, seL4’s verified microkernel documents its own assumption list in the same style, and comparing the two lists is instructive about what "verified" is doing in each case.
- Contrast at the level you can run:
alive-tvfrom[[translation-validation]]gives per-compilation refinement checking on LLVM, which is the same relation with a much smaller quantifier.
Plausible wrong readings
Stated the way a confident engineer states them.
- "A verified compiler cannot produce wrong code." It cannot produce wrong code in the verified region for a well-defined source program under a trusted semantics. Every clause of that sentence is doing work.
- "Verification means bug-free." Bugs were found in CompCert — in the parser, in unverified support code, and as rejections and crashes. What was not found is a wrong-code bug in the verified middle-end.
- "If it is proven, testing is unnecessary." The unverified perimeter still needs the ordinary test suite, and the semantics needs validating against the standard by human review, which is testing by another name.
- "Verified compilers are slow, so the technique is impractical." The generated code is competitive at moderate optimization; the cost is at the aggressive end. And in the settings where this is used, the alternative is not a faster compiler but a much larger review burden.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
A verified compiler comes with a mathematical proof, checked by a machine, that the code it produces does what the source program was supposed to do. CompCert is the example. The proof covers the optimizer and code generator; it does not cover the parser, the assembler, the runtime, or the assumption that the formal description of C matches the real standard. And it assumes your program is well-defined in the first place — a program with a buffer overflow gets nothing from it.
practical
You will use this if you work in certified safety-critical software and almost certainly not otherwise. If you do, the practical discipline is to read the trusted-computing-base section of the manual and treat it as the actual specification of what you are buying, to keep the unverified perimeter under ordinary test, and never to state the theorem in a certification argument without its precondition. If you do not, the useful transfer is knowing which bug class the result eliminated and that the cheaper tools in this module are aimed at the same class.
advanced
The design insight that made CompCert tractable is that semantic preservation *composes*. Prove each pass preserves behavior between two formal languages, and the composition is a proof for the whole chain — which means the intermediate languages are chosen for provability as much as for optimizability, and the number of them is higher than an unverified compiler would tolerate. That is a real design pressure with a real cost: aggressive optimizations that blur the boundaries between representations, which is where much modern performance comes from, are precisely the ones hardest to fit into a compositional proof. The gap between verified and aggressive optimizers is not incidental; it is the shape of the proof technique showing through the compiler’s architecture.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
If you were asked this in an interview
- What exactly does CompCert’s theorem say, and what is in its trusted computing base?
- Why is the Csmith result stronger evidence than the existence of the proof?
- Your program has a buffer overflow. What does a verified compiler guarantee you?
Connections
- Testing & Reliability Engineering — Assurance arguments: what evidence supports which claim, and what the assumption list isThe discipline of stating a guarantee together with its precondition and its trusted base is general safety-engineering practice, and it is the part people drop when they repeat "verified compiler". The compiler-specific half — what the theorem says about IR refinement — is ours.