You are on page 1of 34

Certification of high-level and low-level programs, IHP, Paris, 2014

CakeML
A verified implementation of ML

Ramana
umar, Ramana
Magnus Kumar
Myreen,
Ramana
Ramana Kumar,
Kumar, Magnus
Michael
Kumar,
Magnus
Magnus Myreen
Norrish,
Magnus Scott
Myreen,
Myreen, Michael
Owens
Myreen, Norrish
Michael
Michael
Michael Norrish,
Norrish, Scott
Norrish, ScottOwens
Owens
Scott Owens
Background
From my PhD (2009):
Verified Lisp interpreter
in ARM, x86 and PowerPC machine code

Collaboration with Jared Davis (2011):


Verified Lisp read-eval-print loop
in 64-bit x86 machine code, with dynamic compilation
(plus verification of an ACL2-like theorem prover)

Can we do the same for ML?


A verified implementation of ML
(plus verification of a HOL-like theorem prover?)

Other HOL4 hackers also have relevant interests…


People involved
operational semantics

verified compilation from


CakeML to bytecode
Ramana Kumar verified type inferencer Scott Owens
(Uni. Cambridge) (Uni. Kent)
verified parsing (syntax is
compatible with SML)

verified x86 implementations

proof-producing code
Michael Norrish generation from HOL Magnus Myreen
(NICTA, ANU) (Uni. Cambridge)
Overall aim
to make proof assistants into trustworthy and
practical program development platforms

Trustworthy code extraction:


functions in HOL (shallow embedding)
proof-producing translation [ICFP’12, JFP’14]
CakeML program (deep embedding)
verified compilation of CakeML [POPL’14]

x86-64 machine code (deep embedding)


This talk
Part 1: verified implementation of CakeML

Part 2: current status, HOL light, future


Part 1: verified implementation of CakeML

POPL’14
i o n o f M L
p le m e n t at
Ve r ifie d Im
e M L : A
Cak
3
t O w e ns
rr i s h 2 Scot
a e l N o † 1 Mich
us O . M yreen UK
⇤ 1 Ma g n o f C ambridge, ‡
Ku m a r nive r s it y
Ram a n a
p u te r L a boratory, U b , N I C TA , Australia
1
Com esearch La f Kent, UK
2
Canberra R puting, University o
3 chool of Com
S

n compila tion;
d u c ti o r ifi e d
1. Intro in ve
a s tr o ng interest s u lt s, ma ny based
seen r e
ecade has high-profile interest is
ed The last d ve been significant, 1, 14, 16, 29]. This nverified
system call . ha [
M L and there o m p iler for C ram verification, an u puting
Abstract rifi ed a n ML mpC er t c rog com
n d m e c h anically ve ubset of Standard on the Co y: in the context of p x part of the trusted work on
eveloped a stantial s print loop tif le g
easy to jus ms a large and comp none of the existin essed all
We have d hich supports a sub teractive read-eval- ensures o r e , dr
CakeML,
w
m e n te d as an in correctness theorem mitted compiler f er, to our knowledg ose languages has ad pilation
ple ev purp e com
keML is
im
ne code. O
ur
e results p
er base. How or general- ns: one, th of
Dimensions of Compiler Verification

source code
how far compiler goes
abstract syntax
intermediate language
bytecode Our verification covers the full
spectrum of both dimensions.
machine code

compiler implementation implementation interactive call in read-


algorithm in ML in machine code eval-print loop runtime

the thing that is verified


The CakeML language
was originally
Design: “The CakeML language is designed to be both easy
to program in and easy to reason about formally”
It is still clean, but not always simple.
Reality: CakeML, the language
= Standard ML without I/O or functors

i.e. with almost everything else:


✓ higher-order functions
✓ mutual recursion and polymorphism
✓ datatypes and (nested) pattern matching
✓ references and (user-defined) exceptions
✓ modules, signatures, abstract types
Contributions of POPL’14 paper
Artefacts Proof techniques
Specifications Divergence Preservation
Verified Algorithms Bootstrapping

light-weight approach to main new technique: use


divergence preservation verified compiler to produce
with big-step op. sem. verified implementation

Proof development where everything fits together.


Approach

Proof by refinement:

Step 1: specification of CakeML language


‣ big-step and small-step operational semantics
Step 2: functional implementation in logic
‣ read-eval-print-loop as verified function in logic
Step 3: production of verified x86-64 code
‣ produced mostly by bootstrapping the compiler
Operational semantics

Big-step semantics:

‣ big-step evaluation relation


‣ environment semantics (cf. substitution sem.)
‣ produces TypeError for badly typed evaluations (e.g. 1+nil)
‣ stuck = divergence
Equivalent small-step semantics:

‣ used for type soundness proof and definition of divergence


Read-eval-print-loop semantics.

Semantics written in Lem, see Mulligan et al. [ICFP’14]


Functional implementation
Read-eval-print loop defined as rec. function in the logic:

read-eval-print-loop

lexing, parsing

type inference

compilation

bytecode execution
lexing, parsing

Specification:
Context-free grammar (CFG) for significant subset of SML
Executable lexer.

Implementation:
Parsing-Expression-Grammar (PEG) Parser
‣ inductive evaluation relation
‣ executable interpreter for PEGs
Correctness:
Soundness and completeness
‣ induction on length of token list/parse tree and
non-terminal rank
type inference

Specification:
Declarative type system.

Implementation:

Based on Milner’s Algorithm W


Purely functional (uses state-exception monad)

Correctness:
Proved sound w.r.t. declarative type system
Re-use of previous work on verified unification
compilation

Purpose:
Translates (typechecked) CakeML into CakeML Bytecode.

Implementation:

Translation via an intermediate language (IL).


‣ de Bruijn indices
‣ big-step operational semantics
CakeML to IL: makes language more uniform
IL to IL: removes pattern-matching, lightweight opt.
IL to Bytecode: closure conversion, data refinement, tail-call opt.
Semantics of bytecode execution

Instructions:

bc inst ::= Stack bc stack op | PushExc | PopExc fetch(bs) = Sta


| Return | CallPtr | Call loc bs ! (bu
| PushPtr loc | Jump loc | JumpIf loc
bc inst |
::= Ref | Deref
Stack | Update
bc stack | Print | PrintC
op | PushExc | PopExc char fetch(bs) = Sta
= Ret
|| fetch(bs)
Returnn||CallPtr
Label Tick | Stop
| Call loc !!
bs bs (bub
bc stack op ::=| Pop | Pops
PushPtr locn| |Jump
Shift n locn || JumpIf
PushIntloc int
|| Cons n n | El
Ref | Deref n | TagEq
| Update n | IsBlock
| Print | PrintCnchar
|| Labelnn||Store
Load | LoadRev n
Tick |nStop fetch(bs) = Re
fetch(bs) =
bc stack op |
::= Equal | Lessn| |Add
Pop | Pops Shift | Sub
n n || Mult
PushInt| Div
int| Mod bs !
bs ! bs{stack
loc ::=| Consnn| Addr
Lab n | El nn | TagEq n | IsBlock n
n = | num
Load n | Store n | LoadRev n
bc value ::= Number int | RefPtr n | Block n bc value ⇤ fetch(bs)
fetch(bs) ==
| Equal | Less | Add | Sub | Mult | Div | Mod
Small-step
loc semantics;
|
::= Lab nvalues
CodePtr | Addr n ⇤andnstate:
n | StackPtr ! bs
bs !
bs bs{stack
0
{stack
bc state
n = { stack
::= num : bc value ⇤
; refs : n 7! bc value;
code : bc inst
Number int | RefPtr ; pc :nn;| handler
Block n :bcn;value ⇤ fetch(bs)
bc value ::=
output namesn: n 7! string; fetch(bs)=
| CodePtr: ?string;
n | StackPtr bs 0 {stack
bs !bs.stack
bc state ::= { clock
stack :: n } ⇤ ; refs : n 7! bc value;
bc value =
code : bc inst ⇤ ; pc : n; handler : n; bs ! (bump bs
Figure 2. CakeML Bytecode
output :syntax,
string; values,
names :andn 7!machine
string; state fetch(bs)
clock : n? } Figure bs.stack
3. CakeM=
function (bumpcal
bs ! fetch bs
Semantics of bytecode execution

Sample rules:

pExc fetch(bs) = Stack (Cons t n) bs.stack = vs @ xs |vs| = n


bs ! (bump bs){stack = Block t (rev vs) :: xs}
c
C char
fetch(bs) = Return bs.stack = x :: CodePtr ptr :: xs
int bs ! bs{stack = x :: xs; pc = ptr }
kn
fetch(bs) = CallPtr bs.stack = x :: CodePtr ptr :: xs
iv | Mod
bs ! bs{stack = x :: CodePtr (bump bs).pc :: xs; pc = ptr }

c value ⇤ fetch(bs) = PushExc bs.stack = xs bs 0 = bump bs


bs ! bs 0 {stack = StackPtr (bs.handler) :: xs; handler = |xs|}
alue;
n;
ng; fetch(bs) = PopExc bs.handler = |ys|
bs.stack = x :: xs @ StackPtr h :: ys
compilation

Correctness:

Proved in the direction of compilation.

Shape of correctness theorem:


(exp, env ) +ev val =)
“the code for exp is installed in bs etc.” =)
0 ⇤ 0 0
9bs . bs ! bs ^ “bs contains val ”

Bytecode semantics step relation


What about divergence?
We want: generated code diverges
if and only if source code diverges
Idea: add logical clock

Big-step semantics:
• has an optional clock component
• clock ‘ticks’ decrements every time a function is applied
• once clock hits zero, execution stops with a TimeOut
Why do this?
• because now big-step semantics describes both
terminating and non-terminating evaluations

either: Result
for every exp env clock there is some result or TimeOut

8exp env clock . 9res. (exp, env , Some clock) +ev res

produced by the semantics


Divergence
Evaluation diverges if
8clock . (exp, env , Some clock ) +ev TimeOut

for all clock values TimeOut happens

Compiler correctness proved in conventional forward direction:

(exp, env ) +ev val =)


“the code for exp is installed in bs etc.” =)
0 ⇤ 0 0
9bs . bs ! bs ^ “bs contains val ”

Bytecode has clock … that stays in sync with CakeML clock

Theorem: bytecode diverges if and only if CakeML eval diverges


Step 3: production of verified x86-64 code
Verified x86-64 Implementation
lexer
read-eval-print-loop

parsing verified x86-64 code


generated using
type inference bootstrapping of the
verified compiler.
compilation
JIT: translates Bytecode to
bytecode execution machine code; jumps to
generated machine code.
garbage collector

bignum library hand-crafted verified


machine code based
on previous work.

Real executable also has 30-line unverified C wrapper.


representation of bytecode states, we define a function that maps
REPLs l for some list l of type in
CakeML Bytecode instructions into concrete x86-64 machine in-
structions (i.e. lists of bytes). Here i is the index of the instruction temporal entire machine cod
that is to be translated (i is used for the translation of branch in- (now (init inp aux ) )
Translation into x86-64
structions, such as Jump).
x64 i (Stack Pop)
= [0x48, 0x58]
later ((9l res. repl retur
(REPLs l
x64 i (Stack Add)
= [0x48, . . .] _
.. (9l res. repl diver
Extract of definition: . each bytecode inst. maps to some
(REPLs l
x86
_
Entire bytecode programs are translated by x64 code: now (out of mem
x64x64i Pop
code i =
[ ] [0x48,
= [ ] 0x58] Here repl returns states that cont
x64 code i (x :: xs) = let c = x64 i x in of aux , and out and terminates a
x64_code i @ x64
(x::xs)c = x64code
i x(i@+ length c) xs
x64_code (i + …) xs
out Terminate = ""
We prove a few key lemmas about the execution of the gener- out Diverge = ""
ated x86-64 machine code. out (Result str rest) =
Correctness:
Theorem 21 (x64 code Implements Bytecode Steps). The code terminates Terminate =
generated by x64 code is faithful to the execution of each of terminates Diverge =
the CakeML Bytecode instructions. Each instruction executes terminates (Result str r
Each
at least one Bytecode instruction
x86-64 instruction (hence later).is correctly
Note that exe-executed by
cutiongenerated
must either reach the target state or resort to an error Proof sketch. The execution of
x86-64 code. above. The other parts of the x
(out of memory error).
code, the lexer and the code tha
bs ! bs 0 =) was verified, again, using a com
temporal {(base, x64 code 0 bs.code)} reasoning and proof-producing sy
(now (bc heap bs (base, aux )) ) replace REPLi by the top-level s
0
later (now (bc heap bs (base, aux ))
_ now (out of memory error aux )))
11. Small Benchmarks
Proof sketch. For simple cases of the bytecode step relation
(!), the proof washeap invariant
manual using the /programming
memory abstraction
logic from To run the verified x86-64 machi
Myreen [19]. More complex instruction snippets (such as the sup- 30-line C program, which essent
porting routines) were produced using a combination of manual execute permissions enabled) th
pointers for getc and putc).
Bootstrapping the verified compiler
Production of verified x86-64

parsing function in logic: compile

type inference by proof-producing synthesis [ICFP’12]


compilation
CakeML program (COMPILE) such that:
⊢ COMPILE implements compile

Proof by evaluation inside the logic:


⊢ compile-to-x64 COMPILE = x64-code
Compiler correctness theorem:
⊢ ∀prog. compile-to-x64 prog implements prog
Combination of theorems:
⊢ x64-code implements compile
c. (8bs . bs ! bs =) bs.output = bs .output) =)
ral entire machine code {(base,
temporal implementation
x64 code 0 bs.code)}
w roots
(init inp aux ) )(now (bc heap bs (base, aux )) )
stack
ter
ween
((9l res. repl returns Top-level theorem
later (repl
(out diverged
res) aux ^
bs.output aux )
(REPL l inp _res
now^(out of memory
terminates res))error aux )))
must s
inte-_ Proof sketch.
Top-level Theorem 21 and temporal logic.
specification:
least(9l res. repl diverged (out res) aux ^
Top-level
(REPL Correctness
l inp res ^ Theoremresult
¬terminates The res))
top-level
output: list of theoremthat
strings forends
the
on is s
entire x86-64 implementation is stated as follows.
_ in either Terminate or Diverge
now (out of input
Theorem 25string
memory (x86-64
errorImplementation
aux ))) of REPLs ). If the state starts
d the
from a good initial state (init), then execution behaves according to
maps states
returns that control
Correctness is returned
theorem: to the return pointer
REPL l for some list l of type inference failures.
ne out
nd in- and terminates are defined as follows.
s

uction temporal entire machine code implementation


ut
ch Terminate
in- =(now"" (init inp aux ) )
ut Diverge = "" later ((9l res. repl returns (out res) aux ^
ut (Result str rest) = str @ out rests l inp res ^ terminates res))
(REPL
rminates Terminate = _true
(9l res. repl diverged (out res) aux ^
rminates Diverge = false
(REPLs l inp res ^ ¬terminates res))
rminates (Result str rest) _ = terminates rest
tch. The execution of bytecode now (outisofverified
memoryaserrorsketched
aux )))
e other parts ofrepl
Here the returns
x86-64states
implementation
that control is (the setup
returned to the return pointer
lexer and the code that ties together the top-level loop)
Numbers
Performance:
Slow: interpreted OCaml is 1x faster (… future work!)

Effort:
~70k lines of proof script in HOL4
< 5 man-years, but builds on a lot of previous work

Size:
875,812 instructions of verified x86-64 machine code

implementation generates
more instructions at runtime

large due to bootstrapping


This talk
Part 1: verified implementation of CakeML

Part 2: current status, HOL light, future


Current status

Current compiler:

string tokens AST IL bytecode x86

huge step huge step

Bytecode simplified proofs of


read-eval-print loop, but made
optimisation impossible.
Future plans
Refactored compiler: split into more conventional compiler phases

string tokens AST IL-1

module compilation
IL-2
closure compilation
pattern-match compilation …

removal of memory abstraction


IL-N ARM
register allocation
x86-64
… as separate phases. ASM
MIPS-64

Anthony Fox joins project and helps with final phases asm.js
Verified examples on CakeML

Verification infrastructure:
• have: synthesis tool that maps HOL into CakeML [ICFP’12]
• future: integration with Arthur Charguéraud’s characteristic
formulae technology [ICFP’10, ICFP’11]
for developing cool verified examples.
Big example: verified HOL light

ML was originally developed to host theorem provers.

Aim: verified HOL theorem prover.


We have:
• syntax, semantics and soundness of HOL (stateful, stateless)
• verified implementation of the HOL light kernel in CakeML
(produced through synthesis)
Still to do:
• soundness of kernel soundness of entire HOL light
• run HOL light standard library on top of CakeML

Freek Wiedijk is translating HOL light sources to CakeML


Summary

Contributions so far:
First(?) bootstrapping of a formally verified compiler.
New lightweight method for divergence preservation.

Current work:
Formally verified implementation of HOL light.
Verified I/O (foreign-function interface). seL4.
Compiler improvements (new ILs, opt, targets).

Long-term aim:
An ecosystem of tools and proofs around CakeML lang.

Questions? Suggestions?

You might also like