Richer Memory Models, Low*, and EverParse
… we saw how to deeply embed language and build certified tools for them
… we saw how to shallowly embed effectful languages and reason about them with refined computation types
with Lens-indexed Imperative Lenses
Program libraries to model memory, e.g., the ML or C heap
Derive effectful actions for primitive operations (e.g., !
, :=
etc.)
Write effectful programs against these libraries and verify them with refined computation types
Extract them to programs in ML or C with primitive effects
F*:
let incr (r:ref int)
: ST unit
(requires fun h0 -> h0 `contains` r)
(ensures fun h0 _ h1 -> sel h1 r = sel h0 r + 1)
= r := !r + 1
ML:
let incr (r:ref int) : unit = r := !r + 1
C:
void incr (int *r) { *r = *r + 1; }
A sketch of FStar.Heap
:
module Heap
let addr = nat
abstract let heap = {
next_addr: addr;
map: addr -> option (a:Type & v:a) {
forall a. h > next_addr ==> map a == None
}
}
abstract let ref t = addr
let contains h (r:ref t) = r < h.next_addr /\ h.map r == Some (t, _)
let sel h (r:ref t{h `contains` r}) = let Some (_, v) = h.map r in v
let upd h (r:ref t{h `contains` r}) v = ...
Reading references
let (!) #t (r:ref t)
: ST t
(requires fun h -> h `contains` r)
(ensures fun h0 x h1 -> h0 == h1 /\ x = sel h1 r) =
sel (get()) r
Writing references
let (:=) #t (r:ref t) (v:t)
: ST (ref t)
(requires fun h -> h `contains` r)
(ensures fun h0 x h1 -> h1 == upd h0 r v) =
put (upd (get()) r v); r
Allocating and freeing references …
type lens a b = {
get : a -> b;
put : b -> a -> a
}
sel
and upd
form a lens
let ref_lens : lens (heap * ref a) a = {
get = fun (h, r) -> sel h r;
put = fun v (h, r) -> upd h r v
}
A lens-indexed computation type:
type st_lens inv (l:lens (heap * a) b) = {
st_get : x:a
-> ST b
(requires fun h -> inv h x)
(ensures fun h0 y h1 -> h0==h1 /\ y == l.get (h0, x));
st_put : y:b
-> x:a
-> ST a
(requires fun h -> inv h x)
(ensures fun h0 x' h1 -> h1, x' == l.put y (h0, x))
}
(!) and (:=) are imperative lenses
let iref_lens : st_lens contains ref_lens = {
st_get = (!);
st_put = (:=);
}
Inherit the control constructs, modular structure and typing discipline, partial evaluation capabilities of F*
Verified programs are extracted as usual by F* to an ML-like language
Very similar to Coq's extraction to OCaml
Heavy use of erasure and partial evaluation
If the extracted program is first order, doesn't use unbounded inductive types (e.g., no list, tree etc.), … KreMLin emits the program to C after
translating F* types modeling C constructs to C primitives (e.g,
FStar.UInt64.t
to uint64_t
; array t
to t*
)
monomorphizing
compilation of pattern matches
bundling fragments into compilation units
EverCrypt: A Fully Verified, High-performance Cryptographic Provider
EverParse: A Verified Parser Generator for Binary Message Formats
100s of thousands of lines of verified code developed in Low*
Deployments in many places, including Windows kernel, Linux kernel, Azure, Firefox, …
HyperStack: A region-based memory model for stacks and heaps
Heap reasoning with implicit dynamic frames
Libraries with specialized extraction via Kremlin
let incr (p:pointer int)
: ST unit
(requires fun h0 -> h0 `contains` p)
(ensures fun h0 _ h1 -> modifies {p} h0 h1 /\ h1.[p] = h0.[p] + 1)
= p := !p + 1
modifies l h0 h1
, where l : loc
abstracts an aggregation of memory locations
Stateful specifications are heavily reliant on modifies
for “framing”
Later, we'll see other ways to handle framing, notably with separation logic
abstract let array t = { len:nat ; base:ref (s:seq t {lengths = len}) }
let ptr t = a:array t{a.len = 1}
let contains m (a:array t) : prop = ...
let sel m (a:array t{m `contains` a}) : seq a = ...
let upd m (a:array t{m `contains` a}) s : mem = ...
Array indexing: a.[i]
extracted to *(a + i)
let ( .[] ) #t (a:array t) (i:nat)
: ST t
(requires fun h -> h `contains` r /\ i < a.len)
(ensures fun h0 x h1 -> h0 == h1 /\ x == Seq.index (sel h1 r) i)
= index (sel (get()) a) i
Array update: a.[i] <- v
extracted to *(a + i) = v
let ( .[]<- ) #t (a:array t) (i:nat) (v:t)
: ST unit
(requires fun h -> h `contains` a /\ i < a.len)
(ensures fun h0 x h1 -> h1 == upd h0 a (Seq.update (sel h0 a) i v))
= let h0 = get () in
put (upd h0 a (Seq.update (sel h0 a) i v))
You can program and prove code in Low* directly
But, you can also metaprogram Low*
Or, build further proof-oriented DSLs on top of Low*
Write low-level code if you must
But, program it tastefully in a proof assistant, not directly in C or asm
Thoughtful structuring of imperative coding patterns can make reasoning about imperative programs similar to functional programming
And with proofs, partial evaluation and proof erasure, the resulting code can be significantly faster than hand-written C