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*
Monadic effects in F*
Verifying effectful programs extrinsically (monadic reification)
Under the hood: Weakest preconditions (and Dijkstra monads)
Tactics and Metaprogramming as a user-defined, non-primitive effect
type st (mem:Type) (a:Type) = mem -> Tot (a * mem)
total reifiable new_effect {
STATE_m (mem:Type) : a:Type -> Effect
with (* functional representation of the global state effect *)
repr = st mem;
(* standard monadic return and bind for the state monad *)
return = fun (a:Type) (x:a) (m:mem) -> (x, m);
bind = fun (a b:Type) (f:st mem a) (g:a -> st mem b) (m:mem) ->
let (z, m') = f m in
g z m';
(* standard get and put actions for the state monad *)
get = fun () (m:mem) -> (m, m);
put = fun (m:mem) _ -> ((), m)
}
total reifiable new_effect STATE = STATE_m heap
In F* the programmer writes:
let incr_and_assert () : STATE unit user_spec =
let x = get() in
put (x + 1);
assert (get() > x)
Which is then made explicitly monadic via type and effect inference:
let incr_and_assert () : STATE unit inferred_spec =
STATE.bind (STATE.get ()) (fun x ->
STATE.bind (STATE.put (x + 1)) (fun _ ->
STATE.bind (STATE.get ()) (fun y ->
STATE.return (assert (y > x)))))
And the SMT-solver is asked to discharge the VC to typecheck it
forall s0 k. user_spec s0 k ==> inferred_spec s0 k
let stexn a = nat -> Tot ((either a string) * nat))
new_effect {
STEXN: a:Type -> Effect
with (* functional representation of the sum of state and exceptions monads *)
repr = stexn;
(* standard monadic return and bind *)
return = fun (a:Type) (x:a) s0 -> (Inl x, s0);
bind = fun (a b:Type) (f:stexn a) (g:a -> stexn b) s0 ->
let (r,s1) = f s0 in
match r with
| Inl ret -> g ret s1
| Inr m -> (Inr m, s1)
(* action of raising exceptions *)
raise = fun (a:Type) (msg:string) s0 -> (Inr msg, s0);
}
sub_effect STATE ~> STEXN {
lift = fun (a:Type)
(e:st nat a) (* st comp. *)
->
fun s -> let (x,s1) = e s0 in (Inl x, s1) (* stexn comp. *) }
In F* the programmer writes:
( / ) : int -> x:int{x<>0} -> Tot int
let divide_by (x:int) : STEXN unit user_spec
= if x <> 0 then put (get () / x)
else raise "Divide by zero"
Which is then elaborated to:
let divide_by (x:int) : STEXN unit inferred_spec
= if x <> 0 then STATE_STEXN.lift (STATE.bind (STATE.get()) (fun n ->
STATE.put (n / x)))
else STEXN.raise "Divide by zero"
And the SMT-solver is asked to discharge the VC to typecheck it
forall s0 k. user_spec s0 k ==> inferred_spec s0 k
Pre- and postconditions are just syntactic sugar:
Pure t (pre : Type0) (post : t -> Type0)
= PURE t (fun k -> pre /\ (forall y. post y ==> k y))
(* where k is the "true" postcondition,
for which we compute the weakest precondition *)
val factorial : x:int -> Pure int (requires (x >= 0))
(ensures (fun y -> y >= 0))
val factorial : x:int -> PURE int (fun k -> x>=0 /\ (forall y. y>=0 ==> k y))
Same for user-defined effects, like STATE
:
ST t (pre : nat -> Type0) (post : nat -> t -> nat -> Type0)
= STATE t (fun n0 k -> pre n0 /\ (forall x n1. post n0 x n1 ==> k x n1))
val incr : unit -> St unit (requires (fun n0 -> True))
(ensures (fun n0 _ n1 -> n1 = n0 + 1))
val incr : unit -> STATE unit (fun n0 k -> k () (n0 + 1))
let incr () = STATE.bind (STATE.get()) (fun x -> STATE.put (x + 1))
incr
against following interface:
STATE.get : unit -> STATE nat (STATE.get_wp ())
STATE.put : n:nat -> STATE unit (STATE.put_wp n)
STATE.bind : STATE 'a 'wa -> (x:'a -> STATE 'b ('wb x)) ->
STATE 'b (STATE.bind_wp 'wa 'wb)
… F* computes the weakest precondition for incr
val incr : unit -> STATE unit inferred_wp
inferred_wp = STATE.bind_wp (STATE.get_wp()) (fun x -> STATE.put_wp (x+1))
= fun n0 k -> k () (n0 + 1)
let STATE.wp t = (t -> nat -> Type0) -> (nat -> Type0)
val STATE.return_wp : 'a -> Tot (STATE.wp 'a)
val STATE.bind_wp : (STATE.wp 'a) -> ('a -> Tot (STATE.wp 'b)) ->
Tot (STATE.wp 'b)
val STATE.get_wp : unit -> Tot (STATE.wp nat)
val STATE.put_wp : nat -> Tot (STATE.wp unit)
whose implementation is given by:
let STATE.return_wp v = fun p -> p v
let STATE.bind_wp wp f = fun p -> wp (fun v -> f v p)
let STATE.get_wp () = fun p n0 -> p n0 n0
let STATE.put_wp n = fun p _ -> p () n
and for a while we wrote such things by hand for each new effect;
but this is quite tricky and comes with strong proof obligations
(correctness with respect to effect definition, monad laws, …)
STATE.wp t = (t -> nat -> Type0) -> (nat -> Type0)
~= nat -> (t * nat -> Type0) -> Type0
This can be automatically derived from the state monad transformer
STATE.repr t = nat -> M (t * nat)
by selective continuation-passing style (CPS) translation
M
is the monad-argument of the monad transformer
M
can appear
This works well for many natural examples of monadic effects:
STATE
, EXN
, STEXN
, CONT
, etc. (explicitly definable monad transformers)
Summary: From a monadic effect definition we can derive a
correct-by-construction weakest-precondition calculus for this effect.
Monadic reification
STATE.reify : (St a) -> (nat -> Tot (a * nat))
Monadic reflection takes us in the other direction
STATE.reflect : (nat -> Tot (a * nat)) -> (St a)
reify
sends a STATE
comp. with a WP to a PURE
comp. with a WP
reflect
sends a PURE
comp. with a WP to a STATE
comp. with a WP
Reification allows us to give weak specification to a program
let incr () : St unit = STATE?.put (STATE?.get() + 1)
and give an extrinsic proof of their correctness
let lemma_incrs (s0:state)
: Lemma (let (_,s1) = reify (incr ()) s0 in
s1 = s0 + 1)
= ()
Reflection allows us to write some code as pure
let incr' (): St unit = STATE?.put (STATE?.reflect (fun s0 -> (s0,s0)) + 1)
and it cancels out reification in verification
let lemma_incrs' (s0:state)
: Lemma (let (_,s1) = reify (incr' ()) s0 in
s1 = s0 + 1)
= ()
It also allows us to reason about different runs of the same program
let state = int * int (* (high - low) values *)
let st (a:Type) = state -> M (a * state)
total reifiable reflectable new_effect {
STATE : a:Type -> Effect
with repr = st; ...
}
let incr (n:int) : St unit =
STATE?.put_low (STATE?.get_low() + n)
let incr2 () : St unit =
if (STATE?.get_high() = 42) then (incr 2) else (incr 1; incr 1)
let non_interference ()
: Lemma (forall h0 h1 n. let (_,(h0',n' )) = reify (incr2 ()) (h0,n) in
let (_,(h1',n'')) = reify (incr2 ()) (h1,n) in
n' = n'')
= ()
or even about different runs of different programs
Reducing effectful verification to pure verification
Recent experiments using this for “relational verification”
Downside: reification doesn't play well with monotonic state
STATE
and Stack
effects
recall
F* tactics written as effectful F* code (inspired by Lean, Idris)
have access to F*'s proof state (and can efficiently roll it back)
can introspect on F* terms (deep embedding, simply typed)
can be interpreted by F*'s normaliser(s)
or compiled to OCaml and used as native plugins
user-defined, non-primitive effect: proof state + exceptions monad
noeq type __result a =
| Success of a * proofstate
| Failed of string * proofstate
let __tac (a:Type) = proofstate -> Tot (__result a)
(* reifiable *) new_effect {
TAC : a:Type -> Effect
with repr = __tac; ...
}
let tactic (a:Type) = unit -> Tac a
Reflective tactics for arithmetic (proof automation)
Bitvectors in Vale (proof automation)
Separation logic (proof automation)
Pattern matcher (metaprogramming)
Efficient low-level parsers and printers (metaprogramming)
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*