Friday: A Gentle Introduction to F* (Purely Functional Programs)
Yesterday: Verifying Stateful Programs in F*
Today: Monotonic State in F*
Today: F*'s Extensible Effect System and Metaprogramming in F*
So far we have seen how to use F* to verify simple stateful programs
val sum_st : n:nat -> ST nat (requires (fun _ -> True))
(ensures (fun h0 x h1 -> x == sum_rec n /\
modifies !{} h0 h1))
But recall that standard references alone are often unsatisfactory
val sum_st : n:nat -> ST nat (*(requires (fun _ -> True))*)
(ensures (fun h0 x h1 -> x == sum_rec n /\
modifies !{} h0 h1))
A simple interface for monotonic counters
val create: i:int ->
ST counter (ensures (fun h c h' -> fresh c h h' /\ ...))
val read: c:counter ->
ST int (ensures (fun h i h' -> sel h' c = i /\ h == h'))
val incr: c:counter ->
ST unit (ensures (fun h _ h' -> sel h' c = sel h c + 1 /\ ...))
An example program using monotonic counters
let main() : St unit =
let c = create 0 in
incr c;
let i = read c in assert (i > 0);
complex_procedure c; (* complex_procedure : counter -> St unit *)
let j = read c in assert (j > 0) (* (Error) assertion failed *)
For implementing a range of memory models
Reasoning about various kinds of counters
Reasoning about idealized monotonic logs
Initializing and freezing references and arrays
Modelling ghost state of protocols
…
The main user-facing interface is monotonic references
type mref (a:Type0) (rel:preorder a) = ... (* defined using monotonicity *)
(* where *)
type relation (a:Type) = a -> a -> Type0
type preorder (a:Type) = rel:relation a{
(forall (x:a). rel x x) /\
(forall (x y z:a). rel x y /\ rel y z ==> rel x z)}
Accompanied by stateful actions (again defined using monotonicity)
val alloc : #a:Type0 -> #rel:preorder a -> init:a ->
ST (mref a rel) (ensures (fun h0 r h1 -> fresh r h0 h1 /\
sel h1 r == init /\ ...))
val (!) : #a:Type0 -> #rel:preorder a -> r:mref a rel ->
ST a (ensures (fun h0 x h1 -> h0 == h1 /\ sel h1 r == x))
val (:=) : #a:Type0 -> #rel:preorder a -> r:mref a rel -> v:a ->
ST unit (requires (fun h0 -> rel (sel h0 r) v)) (* note this *)
(ensures (fun h0 _ h1 -> modifies !{r} h0 h1 /\ sel h1 r == v))
We can now implement monotonic counters as follows
let counter = mref int (fun n m -> n <= m)
let create i = alloc i (* fresh c h h' *)
let read c = !c (* sel h' c = i *)
let incr c = c := (!c + 1) (* sel h' c = sel h c + 1 *)
By themselves these definitions are not enough to verify the example
let main() : St unit =
let c = create 0 in
incr c;
let i = read c in assert (i > 0);
complex_procedure c; (* c_p : counter -> St unit *)
let j = read c in assert (j > 0) (* (Error) assertion failed *)
Idea: observe that (fun j -> j > 0)
is a stable predicate wrt. <=
let stable (#a:Type) (p:predicate a) (rel:preorder a) =
forall (x y:a). p x /\ rel x y ==> p y
To make use of monotonicity in verification, F* defines the following:
A new pure proposition witnessing the validity of a stable predicate
let token #a #rel (r:mref a rel) (p:a -> prop) : prop = ... (* modality *)
Two stateful actions to witness and recall such tokens (intro/elim)
let witness_token #a #rel (r:mref a rel) (p:(a -> prop))
: ST unit (requires (fun h0 -> p (sel h0 r) /\ stable p rel))
(ensures (fun h0 _ h1 -> h0 == h1 /\ token r p))
= ...
let recall_token #a #rel (r:mref a rel) (p:(a -> prop))
: ST unit (requires (fun _ -> token r p))
(ensures (fun h0 _ h1 -> h0 == h1 /\ p (sel h1 r)))
= ...
And as a bonus, recalling the containment of GCd references, “for free”
let recall_contains #a (#rel:preorder a) (r:mref a rel)
: ST unit (ensures (fun h0 _ h1 -> h0 == h1 /\ h1 `contains` r))
= ...
Recall that we defined monotonic counters as follows
let counter = mref int (fun n m -> n <= m)
let create i = alloc i
let read c = !c
let incr c = c := (!c + 1)
Using witness_token
and recall_token
, we can verify our example
let main() : St unit =
let c = create 0 in incr c;
let i = read c in assert (i > 0);
witness_token c (fun i -> i > 0);
complex_procedure c; (* c_p : counter -> St unit *)
recall_token c (fun i -> i > 0);
let j = read c in assert (j > 0) (* success *)
Typed references from yesterday's lecture are just an instance of mrefs
type ref a = mref a (fun _ _ -> True)
We can't make use of tokens based monotonicity any more
ref a
are now related
But we can still get heap-containment “for free” for GCd refs
let recall_contains #a (r:ref a)
: ST unit (ensures (fun h0 _ h1 -> h0 == h1 /\ h1 `contains` r))
= MonotonicReferencesSlide.recall_contains r
We model monotonic logs using monotonic references to lists of ints
let subset (l1 l2:list int) = forall x . x `mem` l1 ==> x `mem` l2
let lref = mref (list int) subset
An action for adding new elements to the log
let add_to_log (r:lref) (v:int)
: ST unit (ensures (fun _ _ h -> v `mem` (sel h r)))
= r := (v :: !r)
Can then use witness_token
and recall_token
again
let main() : St unit =
let r = alloc subset [] in add_to_log r 42;
witness_token r (fun xs -> 42 `mem` xs);
complex_procedure r; (* c_p : lref -> St unit *)
recall_token r (fun xs -> 42 `mem` xs);
let xs = !r in assert (42 `mem` xs) (* success *)
Contents of initializable and freezable references (value + ghost state)
type rstate (a:Type0) =
| Empty : rstate a
| Mutable : v:a -> rstate a
| Frozen : v:a -> rstate a
Defining a preorder for initialization and freezing
let evolve' (a:Type0) = fun (r1 r2:rstate a) -> match r1 , r2 with
| Empty , Mutable _
| Mutable _ , Mutable _ -> True
| Mutable v1 , Frozen v2 -> v1 == v2
| _ , _ -> False
let evolve (a:Type0) : preorder (rstate a) (* (Error) Subtyping failed *)
= evolve' a (* Quiz: Why does it fail? *)
| Empty , _
| Mutable _ , Mutable _
| Mutable _ , Frozen _ -> True
| Frozen v1 , Frozen v2 -> v1 == v2
| _ , _ -> False
let eref (a:Type0) : Type = mref (rstate a) (evolve a)
let alloc a : ST (eref a) (ensures (fun _ r h -> Empty? (sel h r)))
= alloc (evolve a) Empty
let read #a (r:eref a)
: ST a (requires (fun h -> ~(Empty? (sel h r))))
(ensures (fun h v h' -> h == h' /\
(sel h r == Mutable v \/
sel h r == Frozen v )))
= match (!r) with | Mutable v | Frozen v -> v
(* Quiz: Can you give read a heap-independent precondition? *)
let write #a (r:eref a) (v:a)
: ST unit (requires (fun h -> ~(Frozen? (sel h r))))
(ensures (fun _ _ h -> sel h r == Mutable v))
= r := Mutable v
let freeze #a (r:eref a) : St unit (* Exercise: Give precise type to freeze *)
= r := Frozen (Mutable?.v !r) (* so that main() typechecks *)
let main() : St unit =
let r = alloc int in
(* ignore (read r) -- fails like it should *)
write r 42;
ignore (read r);
write r 0;
witness_token r (fun rs -> ~(Empty? rs));
freeze r;
(* write r 7; -- fails like it should *)
ignore (read r);
witness_token r (fun rs -> rs == Frozen 0);
complex_procedure r;
(* ignore (read r); -- fails like it should *)
recall_token r (fun rs -> ~(Empty? rs));
let x = read r in
(* assert (x == 0); -- fails like it should *)
recall_token r (fun rs -> rs == Frozen 0);
assert (x == 0)
Arrays are created uninitialized
Only initialized elements can be read from arrays
Arrays can be mutated until they are frozen
Only fully initialized arrays can be frozen
Frozen arrays can only be read
no mutation of array elements
no initialization preconditions for reading
MST
)F* standard library provides a global monotonic state effect (almost)
MST #state #rel t (requires pre) (ensures post) (* pre-post on state *)
and a witnessed
modality giving us a state-independent proposition
val witnessed : #st:Type -> #rel:(preorder st) -> (st -> prop) -> prop
together with corresponding get
, put
, witness
, and recall
actions
val put : #state:Type -> #rel:(preorder state) -> s:state ->
MST #state #rel unit (requires (fun s0 -> rel s0 s)) (* note this *)
(ensures (fun _ _ s1 -> s1 == s))
val witness : #state:Type -> #rel:(preorder state) -> p:(state -> prop) ->
MST #state #rel unit (requires (fun s0 -> p s0 /\ stable p rel))
(ensures (fun s0 _ s1 -> s0 == s1 /\
witnessed #_ #rel p))
val recall : #state:Type -> #rel:(preorder state) -> p:(state -> prop) ->
MST #state #rel unit (requires (fun _ -> witnessed #_ #rel p))
(ensures (fun s0 _ s1 -> s0 == s1 /\ p s1))
ST
, much simplified) ST t (requires pre) (ensures post) = MST #heap #heap_rel (requires pre)
(ensures post)
For (monotonic) heaps we use the natural functional representation
type heap_rec = {
next_addr: nat;
memory : x:nat -> Tot (option (a:Type0 & rel:(option (preorder a)) & a))
}
let heap = h:heap_rec{(forall n. n >= h.next_addr ==> None? (h.memory n))}
heap_rel
is heap-inclusion + relatedness by the individual preorders
(Monotonic) references are represented by their addresses
abstract type mref' (a:Type0) (rel:preorder a) = { addr:nat; init:a }
type mref (a:Type0) (rel:preorder a)
= mref' a rel{witnessed (fun h -> h `contains` r)}
alloc
, !
, :=
, tokens
are derived from MST
actions and witnessed
Friday: A Gentle Introduction to F* (Purely Functional Programs)
Yesterday: Verifying Stateful Programs in F*
Today: Monotonic State in F*
Today: F*'s Extensible Effect System and Metaprogramming in F*
(see Snapshots.fst)