VerifyCompilerA Verified Compiler for CS 3110
Source Language
e ::= c | e + e
type expr =
| Const of int
| Plus of expr × expr
| Const of int
| Plus of expr × expr
Inductive expr : Type :=
| Const : nat → expr
| Plus : expr → expr → expr.
In fact, if we extract that Coq expr to OCaml,
we get essentially what we expect.
Extraction expr.
(* type expr =
| Const of nat
| Plus of expr * expr
*)
The one mismatch is that Coq uses nat, whereas in
OCaml we'd normally use int.
The dynamic semantics of expressions is something
we can easily implement. Here's a simple interpreter
that evaluates expressions:
- nat is (theoretically) unbounded and non-negative
- int is definitely bounded and can be negative.
Source: Semantics
Fixpoint evalExpr (e : expr) : nat :=
match e with
| Const n ⇒ n
| Plus e1 e2 ⇒ plus (evalExpr e1) (evalExpr e2)
end.
Again, this extracts to OCaml as we would expect:
Extraction evalExpr.
(*
let rec evalExpr = function
| Const n -> n
| Plus (e1, e2) -> plus (evalExpr e1) (evalExpr e2)
*)
Example source_test_1 : evalExpr (Const 42) = 42.
Proof. reflexivity. Qed.
Example source_test_2 : evalExpr (Plus (Const 2) (Const 2)) = 4.
Proof. reflexivity. Qed.
Target Language
- The Java compiler translates from Java to JVM bytecode.
- The OCaml compiler translates from OCaml to Zinc machine bytecode. [http://cadmium.x9c.fr/distrib/caml-instructions.pdf]
Target: Syntax
inst ::= PUSH c | ADD
Inductive inst : Type :=
| PUSH : nat → inst
| ADD : inst.
Definition prog := list inst.
These extact to OCaml as we would expect.
Extraction inst.
(*
type inst =
| PUSH of nat
| ADD
*)
Extraction prog.
(*
type prog = inst list
*)
Target: Semantics
Definition stack := list nat.
Now it's time to write an interpreter for the target language.
Evaluation of a program takes in an initial stack,
and returns the final stack. But since evaluation
could fail (if we try to ADD when there aren't at
least two values on the stack), we wrap the return
in an option, and return None if an error occurs.
Fixpoint evalProg (p : prog) (s : stack) : option stack :=
match p,s with
| (PUSH n)::p', s ⇒ evalProg p' (n::s)
| ADD::p', x::y::s' ⇒ evalProg p' ((x+y)::s')
| [], s ⇒ Some s
| _, _ ⇒ None
end.
DOES THAT LOOK FAMILIAR? It should...
Extraction of the deep pattern matching doesn't turn out
quite so nicely:
Extraction evalProg.
(*
let rec evalProg p s =
match p with
| Nil -> Some s
| Cons (i, p') ->
(match i with
| PUSH n -> evalProg p' (Cons (n, s))
| ADD ->
(match s with
| Nil -> None
| Cons (x, l) ->
(match l with
| Nil -> None
| Cons (y, s') -> evalProg p' (Cons ((plus x y), s')))))
*)
Example target_test_1 : evalProg [PUSH 42] [] = Some [42].
Proof. reflexivity. Qed.
Example target_test_2 : evalProg [PUSH 2; PUSH 2; ADD] [] = Some [4].
Proof. reflexivity. Qed.
Compiler
- To translate a constant c, we just push c onto the stack.
- To translate an addition e1 + e2, we translate e2, translate e1, then append the instructions together, followed by an ADD instruction.
(* returns: compile e produces a program p, such that
evaluation of p leaves a single new value at the top
of the stack, and that value would be the result of
evaluating e. *)
Fixpoint compile (e : expr) : prog :=
match e with
| Const n ⇒ [PUSH n]
| Plus e1 e2 ⇒ compile e2 ++ compile e1 ++ [ADD]
end.
Note that ++ is the Coq append operator, analogous to
OCaml's @.
We can extract the compiler to its own file:
Extraction "compiler.ml" compile.
Try using that file in the OCaml REPL!
Here are a couple unit tests for our compiler:
Compiler: Unit tests
Example compile_test_1 : compile (Const 42) = [PUSH 42].
Proof. reflexivity. Qed.
Example compile_test_2 : compile (Plus (Const 2) (Const 2))
= [PUSH 2; PUSH 2; ADD].
Proof. reflexivity. Qed.
These tests demonstrate that the compiler produces
some programs that do seem to correspond to the
input expression. But we haven't really tested
the postcondition of compile: we want to know
whether both side of the = above evaluate to the same
value.
Example post_test_1 : evalProg (compile (Const 42)) [] = Some [evalExpr (Const 42)].
Proof. reflexivity. Qed.
Example post_test_2 : evalProg (compile (Plus (Const 2) (Const 2))) []
= Some [evalExpr (Plus (Const 2) (Const 2))].
Proof. reflexivity. Qed.
So far, so good.
But as we know from Dijkstra, "testing can only prove the
presence of bugs, never their absence." How could we show
that the compiler is correct for every input expression?
WE PROVE IT...
The following theorem is a specification that says
what it means for compile to be correct.
Compiler Verification
Theorem compile_correct : ∀ e,
evalProg (compile e) [] = Some [evalExpr e].
Proof.
intros; rewrite (app_nil_end (compile e));
assert (lemma : ∀ e' s p,
evalProg (compile e' ++ p) s = evalProg p (evalExpr e' :: s)) by
(induction e'; crush);
crush.
Qed.
intros; rewrite (app_nil_end (compile e));
assert (lemma : ∀ e' s p,
evalProg (compile e' ++ p) s = evalProg p (evalExpr e' :: s)) by
(induction e'; crush);
crush.
Qed.
Now we have a verified compiler: we have evidence that there
cannot be any bugs in the translation. The code we extracted
is certified as correct!
CompCert is a certified C compiler.
The main theorem from the CompCert Coq source code:
CompCert
- Source language: ISO C 99, mostly.
- Target language: PowerPC, ARM, x86.
- Specified, programmed, proved correct in Coq.
- Not verified: parser, assembler, linker
- Performance: about 10 percent slowdown compared to gcc -O1.
(*
Theorem transf_c_program_correct:
forall p tp,
transf_c_program p = OK tp ->
backward_simulation (Csem.semantics p) (Asm.semantics tp).
*)
Acknowledgment
http://adam.chlipala.net/cpdt/