You are on page 1of 59

Programming and verifying a compiler with Coq: an experience report

Xavier Leroy
INRIA Paris-Rocquencourt

Dependently Typed Programming 2008

X. Leroy (INRIA)

Compiler verication

DTP 2008

1 / 55

Can you trust your compiler?

Source program

Compiler

Executable machine code Bugs in the compiler can lead to incorrect machine code being generated from a correct source program.

X. Leroy (INRIA)

Compiler verication

DTP 2008

2 / 55

Can you trust your compiler?

Source program

Compiler

Executable machine code Non-critical sofware: Compiler bugs are negligible compared with those of the program itself.

X. Leroy (INRIA)

Compiler verication

DTP 2008

2 / 55

Can you trust your compiler?

Source program

Compiler

Executable machine code

Test

Critical software certied by systematic testing: What is tested: the executable code generated by the compiler. Compiler bugs are detected along with those of the program.
X. Leroy (INRIA) Compiler verication DTP 2008 2 / 55

Can you trust your compiler?

Source program

Formal verication

Compiler

Executable machine code Critical software certied by formal methods: What is formally veried: the source code, not the executable code. Compiler bugs can invalidate the guarantees obtained by formal methods.
X. Leroy (INRIA) Compiler verication DTP 2008 2 / 55

Can you trust your compiler?

Source program observational equivalence

Formal verication

Compiler

Executable machine code Formally veried compiler: Guarantees that the generated executable code behaves as prescribed by the semantics of the source program.
X. Leroy (INRIA) Compiler verication DTP 2008 2 / 55

In reality. . .
A great many tools are involved in the production and verication of critical software: Model checker Program prover Static analyzers Compiler Code reviews Test

Matlab Code gen. Hand-written Assembly C

Scade Code gen.

Executable

X. Leroy (INRIA)

Compiler verication

DTP 2008

3 / 55

Guidelines for certication


(E.g. DO178-B in avionics.) Code generation tools: Can introduce bugs in programs either the generated code is certied as if hand-written or the generator is certied at the same level of assurance as the application. Verication tools used for bug-nding: Cannot introduce bugs, just fail to notice their presence. can be certied at lower levels of assurance. Verication tools used to establish the absence of certain bugs: Should be certied at the same level of assurance as the application?

X. Leroy (INRIA)

Compiler verication

DTP 2008

4 / 55

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

5 / 55

Formal verication of compilers

Apply formal methods to the compiler itself to prove a semantic preservation property:

Theorem
For all source codes S, if the compiler generates machine code C from source S, without reporting a compilation error, then C behaves like S. Note: compilers are allowed to fail (ill-formed source code, or capacity exceeded).

X. Leroy (INRIA)

Compiler verication

DTP 2008

6 / 55

Some preservation properties of interest


Preservation of all behaviors

The observable behaviors of the source and compiled programs are identical: b, S b C b

X. Leroy (INRIA)

Compiler verication

DTP 2008

7 / 55

Some preservation properties of interest


Preservation of not going wrong behaviors

Compilers are allowed to rene behaviors, and replace undened behaviors by more specic behaviors. b, S b = (b, C b) (b , C b = S b ) If the target language is deterministic, this is implied by b, S b = C b

X. Leroy (INRIA)

Compiler verication

DTP 2008

8 / 55

Some preservation properties of interest


Preservation of specications

Let Spec : behavior Prop be a functional specication for the program. If the source satises Spec, so does the compiled code. (b, S b b, S b = Spec(b)) = (b, C b b, C b = Spec(b)) Implied by the previous property (preservation of not going wrong behaviors). Special case: preservation of type and memory safety.

X. Leroy (INRIA)

Compiler verication

DTP 2008

9 / 55

Approach 1: proving the compiler

Model the compiler as a function Comp : Source Code + Error and prove that S, C , b, Comp(S) = C = S C (observational equivalence) using a proof assistant. Note: complex data structures + recursive algorithms interactive program proof is a necessity.

X. Leroy (INRIA)

Compiler verication

DTP 2008

10 / 55

Approach 2: translation validation


(A. Pnueli et al; G. Necula; X. Rival)

Validate a posteriori the results of compilation: Comp : Source Code + Error Validator : Source Code bool

If Comp(S) = C and Validator (S, C ) = true, success. Otherwise, error. It suces to prove that the validator is correct: S, C , Validator (S, C ) = true = S C The compiler itself needs not be proved.

X. Leroy (INRIA)

Compiler verication

DTP 2008

11 / 55

Approach 3: proof-carrying code


(G. Necula and P. Lee)

Comp : Source Prop Proof Code Certicate + Error Checker : Prop Code Certicate bool

If Comp(S, P) = (C , ) and Validator (P, C , ) = true, success. Otherwise, error. Assume that the checker is proved correct: P, C , , Checker (P, C , ) = true C |= P Enables the code consumer to check the validity of the compiled code without trusting the code producer and without having access to the source code. (Think mobile code.)
X. Leroy (INRIA) Compiler verication DTP 2008 12 / 55

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

13 / 55

The Compcert experiment


(X.Leroy, Y.Bertot, S.Blazy, Z.Dargaye, B.Grgoire, P.Letouzey, T.Moniot, L.Rideau, e B.Serpette, J.B.-Tristan)

Develop and prove correct a realistic compiler, usable for critical embedded software. Source language: a subset of C. Target language: PowerPC assembly. Generates reasonably compact and fast code some optimizations. This is software-proof codesign (as opposed to proving an existing compiler). The proof of semantic preservation is mechanized using the Coq proof assistant.
X. Leroy (INRIA) Compiler verication DTP 2008 14 / 55

The subset of C supported


Supported: Types: integers, oats, arrays, pointers, struct, union. Operators: arithmetic, pointer arithmetic. Structured control: if/then/else, loops, simple switch. Functions, recursive functions, function pointers. Not supported: The long long and long double types. Dynamic memory allocation malloc/free. goto, unstructured switch, longjmp/setjmp. Variable-arity functions.

X. Leroy (INRIA)

Compiler verication

DTP 2008

15 / 55

The formally veried part of the compiler


initial translation stack allocation instruction selection

Clight

C#minor

Cminor

CFG construction decomposition of expressions linearization of the CFG register allocation

LTLin

LTL

RTL
Optimizations: constant propagation, common subexpressions

spilling, reloading calling conventions layout of of stack frames PowerPC code generation

Linear

Mach

PPC

X. Leroy (INRIA)

Compiler verication

DTP 2008

16 / 55

The whole Compcert compiler


parsing

C source

construct AST type-checking (CIL)

AST C

simplications

AST Clight

Type reconstruction Graph coloring

Executable

assembling linking

Assembly

printing of asm syntax

AST PPC Proved in Coq

Not proved

X. Leroy (INRIA)

Compiler verication

DTP 2008

17 / 55

Formally veried using Coq


The correctness proof (semantic preservation) for the compiler is entirely machine-checked, using the Coq proof assistant. Theorem transf_c_program_correct: forall prog tprog behavior, transf_c_program prog = Some tprog -> Csem.exec_program prog behavior -> PPC.exec_program tprog behavior. Observable behaviors are either Termination, with a nite trace of input-output events (system calls) and the integer returned by the main function (exit code). Divergence, with a nite or innite trace of input-output events.

X. Leroy (INRIA)

Compiler verication

DTP 2008

18 / 55

The Coq proof

Approximately 2.5 man.years and 40000 lines of Coq: 13% 8% 22% 50% Proof scripts 7% Misc

Code Sem. Statements

X. Leroy (INRIA)

Compiler verication

DTP 2008

19 / 55

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

20 / 55

Using a conventional programming language


Write the compiler in a conventional programming language, e.g. functional + imperative. Add invariants, preconditions, postconditions, . . . Use a program logic (e.g. Hoare logic) to generate proof obligations. Pros: + The compiler is directly executable + Can use a civilized programming language such as Caml. + Compilation algorithms found in textbooks are imperative in nature. Cons: Nobody likes Hoare logic. At the beginning of the project, there was no good program logic for higher-order functional languages.
X. Leroy (INRIA) Compiler verication DTP 2008 21 / 55

Using a proof assistant as the programming language


Write the compiler directly in the specication language of a proof assistant (Gallina in the case of Coq, in purely functional style). Use program extraction to obtain an executable compiler.
(Note: this is extraction from functional specs, not from proofs.)

Pros: + No need for a program logic; can reason directly over the program. + Can use rich types, incl. dependent types. . . Apparent cons that are pros if you believe in functional programming: Must get rid of errors and state, e.g. by using monads. Must use purely functional (persistent) data structures. Cons: Must prove that all functions terminate. . . . or hack around possible non-termination
(e.g. force termination on a compile-time error after 10N iterations)
X. Leroy (INRIA) Compiler verication DTP 2008 22 / 55

The untrusted oracle trick


When the need for imperative programming becomes too strong, we can exploit Coqs ability to mix extracted code with hand-written Caml code. The Caml code is untrusted, but its results are checked for correctness by a verier written and proved correct in Coq. Untrusted Caml code Type reconstruction via unication Coloring of an interference graph (for register allocation) Trace picking for code linearization Instruction scheduling pass Veried verier in Coq Checking of a type assignment Check that a given mapping node color is a valid coloring Check that a list of nodes contains all reachable nodes only once Veried translation validator
(J. B. Tristan, POPL 2008)
X. Leroy (INRIA) Compiler verication DTP 2008 23 / 55

Compcert: the program


4500 lines of Coq specications (pure functions) 1500 lines of non-veried Caml code

Coqs extraction mechanism produces executable Caml code from the specications. Execution speed is reasonable despite the overhead of purely functional data structure, Coqs binary integers, etc. Probably the biggest program ever extracted from a Coq development. Most of the Coq code is programmed in pedestrian ML style (HM types). A few uses of richer types (see later).

X. Leroy (INRIA)

Compiler verication

DTP 2008

24 / 55

Performances of the generated code


gcc -O0 Compcert gcc -O1

Execution time

Nbody

Almabench

Mandelbrot

SHA1

Btree

Qsort

AES

X. Leroy (INRIA)

Compiler verication

DTP 2008

Spectral
25 / 55

FFT

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

26 / 55

From Cminor to RTL

The Cminor language: a low-level, much simplied C. Structured in functions, statements, and expressions. The RTL language: Machine-level instructions operating over variables and temporaries. Organized in a control-ow graph:
Nodes = instructions. Edge from I to J = J is a successor of I (J can execute just after I ).

X. Leroy (INRIA)

Compiler verication

DTP 2008

27 / 55

Example: some Cminor code


average(tbl, size) { var s, i; s = 0.0; i = 0; block { loop { if (i >= size) exit; s = s +f floatofint(load(int32, tbl + i * 4)); i = i + 1; } } return s /f floatofint(size); }

X. Leroy (INRIA)

Compiler verication

DTP 2008

28 / 55

Example: the corresponding RTL graph


s = 0.0 i = 0 if (i >= size) d = float(size) e = s /f d return(e) a = i << 2 b = load(tbl, a) c = float(b) s = s +f c i = i + 1

X. Leroy (INRIA)

Compiler verication

DTP 2008

29 / 55

Formalizing the Cminor/RTL correspondence

Idea: every expression or statement occurring in the Cminor source should correspond to a region of the control-ow graph.

Start node i1

in

End node

The instructions i1 , . . . , in in this region should compute the corresponding expression, leaving its value in a given temporary t, or execute the statement. A recursive decomposition of the control-ow graph.

X. Leroy (INRIA)

Compiler verication

DTP 2008

30 / 55

Examples of correspondences
e1 + e2 in r . . . s1 ; s2 . . . if (e) s1 else s2 s1 . . . while (e) s . . .

e1 in r1

e in r

e in r

if(r ) . . . e2 in r2 . . . s2 . . . r = r1 + r2 s1 . . . s2 . . .

if(r )

nop
X. Leroy (INRIA) Compiler verication DTP 2008 31 / 55

Specication of the Cminor-RTL correspondence


As non-deterministic, inductive predicates. For expressions: G , , P expr in reg node1 , node2 .

G : control-ow graph. : mapping Cminor variable RTL temporary. P: set of temporaries to preserve. G , , P e1 in r1 n, n1 G , , P {r1 } e2 in r2 n1 , n2 G (n2 ) = add(r , r1 , r2 , n ) r P / G , , P e1 + e2 in r n, n

G (n) = move(r , r , n ) (x) = r r P / G , , P x in r n, n

X. Leroy (INRIA)

Compiler verication

DTP 2008

32 / 55

Specication of the Cminor-RTL correspondence

For statements: G ,

stmt node1 , node2 . G , , Codom() e in r n, n1 G (n1 ) = if(r , n2 , n ) G , s n2 , n3 G (n3 ) = nop(n) G, (while (e) s) n, n

G, G, G,

s1 n, n1 s2 n1 , n (s1 ; s2 ) n, n

X. Leroy (INRIA)

Compiler verication

DTP 2008

33 / 55

Proving semantic preservation


Assuming G , , P e in r n, n , prove the simulation diagram: Inv

n, R, M . . . . . . t .t . . . .? . Inv Post .................................... n , R , M v, E , M Hypotheses: Left, a big-step evaluation of expression e to value v in Cminor. Top, invariant R(r ) = E (x) whenever (x) = r . Conclusions: Right, a sequence of transitions in the RTL semantics, from node n to n . Bottom, invariant plus postcondition: R (r ) = v r0 P, R (r0 ) = R(r0 ).
X. Leroy (INRIA) Compiler verication DTP 2008 34 / 55

e, E , M

Programming the translation

The graph is built incrementally by recursive functions such as transl expr e r n Eect: add to the graph the instructions that evaluate expression e, deposit its result in temporary r , and branch to node n . Return rst node n of this sequence of instructions. If we were to write this function in ML, we would use: Mutable state to represent the current state of the CFG, the generators for fresh CFG nodes and fresh temporaries. Exceptions to report errors (e.g. unbound Cminor variables).

X. Leroy (INRIA)

Compiler verication

DTP 2008

35 / 55

Making the state explicit

CFG construction functions as state transformers state result state + Error


Record state: Set := { st_nextreg: positive; st_nextnode: positive; st_code: positive -> option instr }.

X. Leroy (INRIA)

Compiler verication

DTP 2008

36 / 55

The state and error monad to the rescue


Encapsulate threading of the state in a monad. An imperative computation returning type A is a Coq term of type mon A.
Inductive res (A: Set) : Set := | Error: res A | OK: A -> state -> res A. Definition mon (A: Set) : Set := state -> res A. Definition ret (A: Set) (x: A) : mon A := fun (s: state) => OK x s. Definition bind (A B: Set) (f: mon A) (g: A -> mon B) : mon B := fun (s: state) => match f s with Error => Error | OK a s => g a s end. Notation "do X <- A ; B" := (bind A (fun X => B)) (at level 200, X ident, A at level 100, B at level 200).
X. Leroy (INRIA) Compiler verication DTP 2008 37 / 55

Excerpt from the translation

Fixpoint transl_expr (map: mapping) (a: expr) (rd: reg) (nd: node) {struct a}: mon node := match a with | Evar v => do r <- find_var map v; add_move r rd nd | Eop op al => do rl <- alloc_regs map al; do no <- add_instr (Iop op rl rd nd); transl_exprlist map al rl no ...

Reads almost like (imperative) ML code!

X. Leroy (INRIA)

Compiler verication

DTP 2008

38 / 55

Reasoning over monadic computations


Monadic laws arent much useful. Can dene (in Ltac) a useful monadic inversion tactic:

H: bind f (fun x => g) s = OK y s ====================================== ...

After monadic inversion on H:


x: B s1: state H1: f s = OK x s1 H2: g s1 = OK y s ======================================= ...
X. Leroy (INRIA) Compiler verication DTP 2008 39 / 55

Monotone evolution of the state


A crucial property of the CFG construction functions is that the state evolves in a monotone way: New nodes are added to the CFG, but old nodes are never changed. Temporaries are not reused. Consequence: if, at some state S, we have st code(S), , P expr in reg node1 , node2

then at any later state S (and in particular in the nal state), we still have st code(S ), , P expr in reg node1 , node2

X. Leroy (INRIA)

Compiler verication

DTP 2008

40 / 55

Monotone evolution of the state

We must maintain an invariant over states:


state_wf s = forall pc, pc >= s.(st_nextnode) -> s.(st_code) pc = None

and guarantee that if the state evolves from s1 to s2, we have


state_incr s1 s2 = s1.(st_nextnode) <= s2.(st_nextnode) /\ s1.(st_nextreg) <= s2.(st_nextreg) /\ forall pc, pc < s1.(st_nextnode) -> s2.(st_code) pc = s1.(st_code) pc

X. Leroy (INRIA)

Compiler verication

DTP 2008

41 / 55

Proving monotonicity
The properties on the state are guaranteed by the basic state operations (add a new node, etc), but we still have to show that all CFG construction operations, built on these basic operations, also guarantee these properties.
Lemma alloc_regs_ok: forall s map al rl s, state_wf s -> alloc_regs map al s = OK rl s -> state_wf s /\ state_incr s s. Lemma transl_expr_ok: forall s map a rd nd s ns s, state_wf s -> transl_expr map a rd nd s = OK ns s -> state_wf s /\ state_incr s s.

Long, boring proofs where Coqs proof automation doesnt work well. Dependent types can help!

X. Leroy (INRIA)

Compiler verication

DTP 2008

42 / 55

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

43 / 55

Invariants over a single state


The state_wf property of a single state can trivially be attached to all states we manipulate:
Record state: Set := { st_nextreg: positive; st_nextnode: positive; st_code: positive -> option instr; st_wf: forall pc, pc >= st_nextnode -> st_code pc = None }.

This property needs to be shown once for each basic function over the state (add a node, etc), but is then propagated for free through all RTL generation functions.

X. Leroy (INRIA)

Compiler verication

DTP 2008

44 / 55

Other uses of this pattern


The same programming pattern occurs often in Compcert and in many Coq developments. Example: nite sets over type A (Fillitre & Letouzey): a set(A) = { s : binarytree(A) | searchtree(s, order) AVLbalanced(s) } Example: machine 32-bit integers: int = { n : Z | 0 n < 232 } More generally: many uses of abstract types in ML programs correspond naturally to subset types {x : implem-type | Invariant(x)}.

X. Leroy (INRIA)

Compiler verication

DTP 2008

45 / 55

Invariant over two states


Less trivially, the state_incr property of two states can be neatly hidden in the monad.
Inductive res (A: Set) (s: state): Set := | Error: res A s | OK: A -> forall (s: state), state_incr s s -> res A s. Definition mon (A: Set) : Set := forall (s: state), res A s. Definition ret (A: Set) (x: A) : mon A := fun (s: state) => OK x s (state_incr_refl s). Definition bind (A B: Set) (f: mon A) (g: A -> mon B) : mon B := fun (s: state) => match f s with | Error => Error | OK a s i => match g a s with | Error => Error | OK b s i => OK b s (state_incr_trans s s s i i) end X. Leroy (INRIA) Compiler verication DTP 2008 end.

46 / 55

Reasoning over dependently-typed monadic computations


The monadic inversion tactic becomes even more powerful:
i: state_incr s s H: bind f (fun x => g) s = OK y s i ====================================== ...

After monadic inversion on H:


x: B s1: state i1: state_incr s s1 H1: f s = OK x s1 i1 i2: state_incr s1 s H2: g s1 = OK y s i2 ======================================= ...
X. Leroy (INRIA) Compiler verication DTP 2008 47 / 55

Dependently typed monads: a winning combination?


Dependent function types x : A, Pre x {y : B | Post x y } are in general hard to use in Coq. A monadic encapsulation hides them entirely from the user of the monad. Dep. typed abstract type Properties need to be proved Properties are propagated
X. Leroy (INRIA)

Dep. typed monad for each operation of the monad + ret and bind through any computation monadic

for each operation of the abstract type through any well-typed computation
Compiler verication

DTP 2008

48 / 55

A problem with extraction


The only downside is an ineciency in the extracted Caml code for bind: instead of the normal code
let bind f g = fun s -> match f s with | Error -> Error | OK a s -> g a s

we get
let bind f g = fun s -> match f s with | Error -> Error | OK a s -> match g a s with | Error -> Error | OK b s -> OK b s

(* useless match *)

In particular, the call to g is not a tail-call.


X. Leroy (INRIA) Compiler verication DTP 2008 49 / 55

An open question

Are there other examples of monads that would benet from this kind of dependent typing?

X. Leroy (INRIA)

Compiler verication

DTP 2008

50 / 55

Outline
1 2 3 4 5 6 7

Introduction: Can you trust your compiler? Formally veried compilers The Compcert experiment Programming the veried compiler Case study: construction of a control-ow graph Dependent types to the rescue? Perspectives

X. Leroy (INRIA)

Compiler verication

DTP 2008

51 / 55

Preliminary conclusions
At this stage of the Compcert experiment, the initial goal proving correct a realistic compiler appears feasible. Moreover, proof assistants such as Coq are adequate (but barely) for this task. Quite a bit remains to be done: Handle a larger subset of C. (E.g. with goto.) Deploy and prove correct more optimizations. (Loop optimizations, instruction scheduling, . . . ) Target other processors beyond the PowerPC. Test usability on real-world embedded codes.

X. Leroy (INRIA)

Compiler verication

DTP 2008

52 / 55

Was Coq up to the task?


On the positive side: Powerful, expressive and intuitive specication/programming language (functions, inductive and coinductive predicates, dependent types). Robust code extraction facility. On the negative side: No support for general recursive functions. type t = Leaf of int | Node of op * t list doesnt quite work. Not much proof automation, e.g. for reasoning by cases. Equality is not built-in, e.g. in denitions and proofs by cases. Typechecking sometimes takes forever, without apparent reason.

X. Leroy (INRIA)

Compiler verication

DTP 2008

53 / 55

Were dependent types useful?


95% of the development is just ML code + separate proofs in higher-order logic. No use yet for dependent types of the list(A, n) kind. No use for type-indexed denotational semantics term( ) value( ). (The languages we manipulate are essentially untyped.) (But see Adam Chlipalas work.) Dependent types of the {x : A | P x} or (x : A), {x : B | P x y } kinds were useful in a few places to automate the propagation of some invariants. But better proof automation in Coq would have achieved the same purposes. (Cf. Isabelle/HOL.)

X. Leroy (INRIA)

Compiler verication

DTP 2008

54 / 55

Work in progress: front-end for mini-ML and Coq


Coq specs Mini-ML Cminor Clight Almost done: a veried translator mini-ML Cminor.
(Z. Dargaye)

PPC

In progress: proof of a memory allocator and GC, written in Cminor, and interfaced with the compiler proof.
(T. Ramananandro, A. Tolmach)

In progress: a veried extraction Coq mini-ML.


(P. Letouzey, S. Glondu)

Grand challenge: a bootstrap of Compcert and of the critical parts of Coq (kernel proof-checker + extraction) ?
X. Leroy (INRIA) Compiler verication DTP 2008 55 / 55

You might also like