Correctnessimplementation

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.

The question

What does it actually mean for a compiler to be proven correct, and what is still not proven?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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

implementationThe verified perimeter has moved over time and continues to. Parts of CompCert’s frontend have been brought inside the proof since the original work, and related projects verify assemblers and linkers separately. Treat the boundary as a property of a specific version to be checked in its documentation, not as a fixed fact — and check it, because the boundary is precisely what you are relying on.

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.

Inside and outside the proofimplementation
ComponentVerified?What that means for you
Middle-end and backend passesYes — machine-checked semantic preservationThe class of bug fuzzers find in other compilers is ruled out here
Formal semantics of the sourceTrusted, not provenIf the model disagrees with ISO C, the compiler faithfully implements the wrong thing
Formal semantics of the targetTrusted, not provenIf the model of the ISA is wrong, correct-by-proof code still misbehaves on the chip
Parser and elaborationHistorically outside the proofA frontend bug produces a wrong program that the verified pipeline then faithfully preserves
Assembler and linkerOutsideThe proof ends at assembly text; encoding and relocation are somebody else’s correctness problem
Runtime library and linked codeOutsideNothing is claimed about memcpy, the startup code, or any library you link
The proof assistantTrustedA soundness bug in the kernel would invalidate the proof; small, heavily scrutinized, not zero
Your program’s definednessAssumedA 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.c compiles 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-tv from [[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.

A verified compiler makes my program correct.
It removes the compiler from the list of suspects for a well-defined program. Your own logic errors and your own undefined behavior are untouched, and the second is explicitly outside the theorem.
Verification is all-or-nothing.
CompCert verifies the middle-end and backend and trusts the rest, and the perimeter has moved over time. Partial verification of the highest-risk region is the practical shape, and it is what produced the empirical result.
The Csmith result proves CompCert has no bugs.
It reports that one fuzzing campaign found wrong-code bugs in every other compiler tested and none in CompCert’s verified middle-end. That is a bounded empirical claim, and the authors stated it as one.

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.

implementationEverything here describes CompCert specifically, and its verified perimeter has expanded over the years — parts of the frontend that were originally outside the proof have been brought in, and separate projects verify assemblers and linkers. Check the trusted-computing-base section of the version you are actually using rather than repeating a boundary from a paper, because that boundary is exactly what the guarantee is made of.
specThe theorem is conditional on the source program being well-defined under CompCert’s formal C semantics. That semantics is a model of ISO C, not the standard itself, and it deliberately defines some behaviors the standard leaves undefined. A program relying on the standard’s undefinedness in a way the model resolves differently is outside the guarantee, and no tool will tell you so.
typicalThe comparison of generated code quality — near mainstream compilers at moderate optimization, behind at the most aggressive settings — reflects published benchmarks and varies by workload and target. Vectorization-heavy floating-point code shows the largest gap; ordinary control-heavy embedded code shows very little. Measure on your own workload rather than carrying a single number.

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

Securityfuzzing
Domains that do not exist yet
  • Testing & Reliability Engineering — Assurance arguments: what evidence supports which claim, and what the assumption list is
    The 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.