of condition expressions (expressions evaluated for their
truth value only).
The next pass translates CminorSel to RTL, a classic register transfer language where control is represented as a
control-flow graph (CFG). Each node of the graph carries
a machine-level instruction operating over temporaries
(pseudo-registers). RTL is a convenient representation to
conduct optimizations based on dataflow analyses. Two
such optimizations are currently implemented: constant
propagation and common subexpression elimination, the
latter being performed via value numbering over extended
basic blocks. A third optimization, lazy code motion, was
developed separately and will be integrated soon. Unlike the
other two optimizations, lazy code motion is implemented
following the verified validator approach.
24
After these optimizations, register allocation is performed via coloring of an interference graph. 6 The output
of this pass is LTL, a language similar to RTL where temporaries are replaced by hardware registers or abstract stack
locations. The CFG is then “linearized,” producing a list of
instructions with explicit labels, conditional and unconditional branches. Next, spills and reloads are inserted around
instructions that reference temporaries that were allocated
to stack locations, and moves are inserted around function
calls, prologues and epilogues to enforce calling conventions. Finally, the “stacking” pass lays out the activation
records of functions, assigning offsets within this record
to abstract stack locations and to saved callee-save registers, and replacing references to abstract stack locations
by explicit memory loads and stores relative to the stack
pointer.
This brings us to the Mach intermediate language,
which is semantically close to PowerPC assembly language. Instruction scheduling by list or trace scheduling
can be performed at this point, following the verified validator approach again. 23 The final compilation pass expands
Mach instructions into canned sequences of PowerPC
instructions, dealing with special registers such as the
condition registers and with irregularities in the PowerPC
instruction set. The target language, PPC, accurately models a large subset of PowerPC assembly language, omitting
instructions and special registers that CompCert does not
generate.
From a compilation standpoint, CompCert is unremarkable: the various passes and intermediate representations
are textbook compiler technology from the early 1990s.
Perhaps the only surprise is the relatively high number of
intermediate languages, but many are small variations on
one another: for verification purposes, it was more convenient to identify each variation as a distinct language than
as different subsets of a few, more general-purpose intermediate representations.
3. 3. Proving the compiler
The added value of CompCert lies not in the compilation
technology implemented, but in the fact that each of the
source, intermediate and target languages has formally
defined semantics, and that each of the transformation and
optimization passes is proved to preserve semantics in the
sense of Section 2. 4.
These semantic preservation proofs are mechanized
using the Coq proof assistant. Coq implements the
Calculus of Inductive and Coinductive Constructions, a
powerful constructive, higher-order logic which supports
equally well three familiar styles of writing specifications:
by functions and pattern-matching, by inductive or coinductive predicates representing inference rules, and by
ordinary predicates in first-order logic. All three styles are
used in the CompCert development, resulting in specifications and statements of theorems that remain quite close
to what can be found in programming language research
papers. In particular, compilation algorithms are naturally presented as functions, and operational semantics
use mostly inductive predicates (inference rules). Coq also
features more advanced logical features such as higher-order logic, dependent types and an ML-style module system, which we use occasionally in our development. For
example, dependent types let us attach logical invariants to
data structures, and parameterized modules enable us to
reuse a generic dataflow equation solver for several static
analyses.
Proving theorems in Coq is an interactive process: some
decision procedures automate equational reasoning or
Presburger arithmetic, for example, but most of the proofs
consist in sequences of “tactics” (elementary proof steps)
entered by the user to guide Coq in resolving proof obligations. Internally, Coq builds proof terms that are later
rechecked by a small kernel verifier, thus generating very
high confidence in the validity of proofs. While developed
interactively, proof scripts can be rechecked a posteriori in
batch mode.
The whole Coq formalization and proof represents 42,000
lines of Coq (excluding comments and blank lines) and
approximately three person-years of work. Of these 42,000
lines, 14% define the compilation algorithms implemented
in CompCert, and 10% specify the semantics of the languages
involved. The remaining 76% correspond to the correctness
proof itself. Each compilation pass takes between 1,500 and
3,000 lines of Coq for its specification and correctness proof.
Likewise, each intermediate language is specified in 300 to
600 lines of Coq, while the source language Clight requires
1, 100 lines. Additional 10,000 lines correspond to infrastructure shared between all languages and passes, such as
the formalization of machine integer arithmetic and of the
memory model.
3. 4. Programming and running the compiler
We use Coq not only as a prover to conduct semantic preservation proofs, but also as a programming language to write
all verified parts of the CompCert compiler. The specification
language of Coq includes a small, pure functional language,
featuring recursive functions operating by pattern-matching
over inductive types (ML- or Haskell-style tree-shaped data
types). With some ingenuity, this language suffices to write
a compiler. The highly imperative algorithms found in compiler textbooks need to be rewritten in pure functional style.
We use persistent data structures based on balanced trees,
which support efficient updates without modifying data