Monadic effects in F* and Dijkstra Monads

Functional core of F*

  • Variant of dependent type theory

    • $\lambda$, $\Pi$, inductives, matches, universe polymorphism
  • Recursion and semantic termination check

    • potential non-termination is an effect
  • Refinements

    • Refined value types: x:t{p}
    • Refined computation types: Pure t pre post
    • computationally and proof irrelevant, discharged by SMT
  • Subtyping and sub-effecting

  • Extensional equality

This talk

  • Monadic effects in F*

  • Verifying effectful programs extrinsically (monadic reification)

  • Tactics as a user-defined, non-primitive effect (experimental)

  • Under the hood: Weakest pre-conditions and Dijkstra monads (for free)

Monadic effects in F*

type st (mem:Type) (a:Type) = mem -> Tot (a * mem)
total reifiable new_effect {
 STATE_m (mem:Type) : a:Type -> Effect
 with repr = st mem;
      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';
      get = fun () (m:mem) -> m, m;
      put = fun (m:mem) _ -> (), m  }
total reifiable new_effect STATE = STATE_m heap
  • this monadic definition is the model F* uses to verify stateful code
  • state can be primitively implemented under the hood or not
    • for instance by ML heap or C stack+heap

Monadic lifts in F*

State and exception monad

let stexn a = nat -> Tot ((either a string) * nat))
new_effect {
  STEXN: a:Type -> Effect with
    repr    = stexn;
    return  = fun (a:Type) (x:a) s -> Inl x, s;
    bind    = fun (a b:Type) (f:stexn a) (g:a -> stexn b) s0 ->
                 let (r,s1) = f s0 in
                 match r with
                 | Inl ret -> Inl (g ret s1), s1
                 | Inr m -> Inr m, s1
    raise   = fun (a:Type) (msg:string) s -> Inr msg, s
}
sub_effect STATE ~> STEXN {
  lift = fun (a:Type) (e:st nat a) -> (fun s -> let (x,s') = e s in Inl x, s')
}

Programming with effects, in direct style

  • In F*, the programmer writes:

    let incr () =
        let x = STATE.get() in
        STATE.put (x + 1);
        let y = STATE.get() in
        assert (y > x)
  • Made explicitly monadic via type and effect inference

    let incr () =
      STATE.bind (STATE.get ()) (fun x -> 
      STATE.bind (STATE.put (x + 1)) (fun _ ->
      STATE.bind (STATE.get ()) (fun y ->
      STATE.return (assert (y > x)))))

Programming with multiple effects

  • Programmer writes:

      ( / ) : int -> x:int{x<>0} -> Tot int
      let divide_by (x:int) : STEXN unit ...
        = if x <> 0 then put (get () / x) else raise "Divide by zero"
  • Elaborated to:

      let divide_by x = 
        if x <> 0 then STATE_STEXN.lift (STATE.bind (STATE.get())
                                          (fun n -> STATE.put (n / x)))
        else STEXN.raise "Divide by zero"
  • F* infers the least effect of each sub-term

    • automatically lifts computations to use the suitable effect
    • ensures that reasoning isn't needlessly polluted by unused effects

Verifying effectful programs extrinsically

Verifying effectful programs

New way: extrinsically (by exposing pure monadic representation)

  • Monadic reification (intuitively)
      STATE.reify : (St a) -> Ghost (nat -> Tot (a * nat))
  • Allows us to give weak specification to an effectful function

      let incr (r:ref nat) : St unit = (r := (!r + 1))
  • and prove lemmas about reification of effectful computation

      let incr_works (r:ref nat) (h:heap) :
        Lemma (sel (snd (STATE.reify (incr r) h)) r = sel h r + 1) = ()

Reification works very well

  • Reducing effectful verification to pure verification

    • for which F* already has good support (i.e. SMT automation)
  • Recent experiments using this for “relational verification”

    • Correctness of program transformations
    • Information flow control
    • Proofs of algorithmic optimizations (memoization, union-find)
    • Simple game-based cryptographic proofs

Tactics

Tactics (New, Experimental)

  • 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 normalizer or compiled to OCaml

  • user-defined, non-primitive effect: proof state + exceptions monad

      noeq type __result a =
          | Success of a * proofstate
          | Failed  of string    //error message
                    * proofstate //the proofstate at time of failure
    
      let __tac (a:Type) = proofstate -> Tot (__result a)
    
      reifiable reflectable new_effect {
        TAC : a:Type -> Effect
        with repr     = __tac   ... }
    
      let tactic (a:Type) = unit -> Tac a

Tactics can discharge SMT obligations

tactics-assert_by_tactic

Tactics can discharge SMT obligations (today's example)

tactics-multi

Tactics can massage SMT obligations

tactics-canon

Tactics can synthesize F* terms (metaprogramming)

tactics-synth_by_tactic

Early uses of tactics

  • Arithmetic expression canonizer (proof automation)

  • Bitvectors in Vale (proof automation)

  • Separation logic (proof automation, ongoing, ask Aseem)

  • Pattern matcher (proof automation, ongoing)

  • Generate code for inductive types (metaprogramming, ongoing)

  • Turn F* to Low* buffer code (metaprogramming, upcoming)

  • Metatheory of subsets of F* (interactive proofs, upcoming)

Under the hood

Weakest pre-conditions

Dijkstra monads for free

Computation types indexed by predicate transformers

Pre- and post- conditions are just syntactic sugar:

Pure t (pre : Type0) (post : t->Type0)
  = PURE t (fun k -> pre /\ forall y. post y ==> k y)
val factorial : x:int -> Pure int (requires (x >= 0)) (ensures (fun y -> y >= 0))
val factorial : x:int -> Pure (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))

Computing weakest preconditions, by example

let incr () = STATE.bind (STATE.get()) (fun x -> STATE.put (x + 1))
  • By inferring type for 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)
    we compute the weakest precondition for incr
    val incr : unit -> STATE unit
    (STATE.bind_wp (STATE.get_wp()) (fun x -> STATE.put_wp (x + 1)))
    = (fun n0 k -> k () (n0 + 1)) 
  • Generic way of computing weakest-preconditions for all effects, provided we have a monad on predicate transformers

Predicate transformers monad for state

aka Dijkstra monad

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)
  • we need to implement this Dijkstra monad:
    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;
    but this is quite tricky and comes with strong proof obligations
    (correctness with respect to effect definition, monad laws, )

Dijkstra monads for free

STATE.wp t  = (t -> nat -> Type0) -> (nat -> Type0)
           ~= nat -> (t * nat -> Type0) -> Type0
  • This can be automatically derived from the state monad

    STATE.repr t = nat -> t * nat

    by selective continuation-passing style (CPS) returning Type0

  • This works well for large class of monadic effects:
    state, exceptions, continuations, etc.

  • From monadic effect definition we can derive a correct-by-construction weakest-precondition calculus for this effect.

Formalization

  • Two calculi

    • DMF: simply-typed with an abstract base monad, and restricted;
      used to define monads, actions, lifts
    • EMF*: dependently-typed, user-defined effects, reifying/reflecting
  • Two translations from well-typed DMF terms to EMF*

    • *-translation: gives specification (selective CPS)
    • elaboration: gives implementation (essentially an identity)
  • *-trans gives correct Dijkstra monad for elaborated terms

Graphically

dm4free

Correct reasoning about PURE

  • PURE is the only primitive EMF* effect (F* also has DIV)

  • A WP for PURE is of type

    PURE.wp t = (t -> Type0) -> Type0
    • Dijkstra monad for PURE is exactly the continuation monad
  • Total Correctness of PURE:
    If ⊢ e : PURE t wp   and   ⊢ wp p   then   e ↝* v   s.t.   ⊨ p v

Correct reasoning about STATE

  • Say we have a term e : nat -> t × nat

  • From logical relation, we get

    • e : s₀ : nat -> PURE (t × nat) (e* s₀)
  • From previous and correctness of PURE, we get
    Correctness of STATE
    If ⊢ e : nat -> t × nat   and  e* s₀ p
    then   e s₀ ↝* (v,s)   s.t.   ⊨ p (v,s)

Extra properties of the translations

  • *-translation preserves equality

    • Monads mapped to Dijkstra monads
    • Lifts mapped to Dijkstra lifts
    • Laws about actions preserved
  • e* is monotonic: maps weaker post's to weaker pre's

    • (∀x. p₁ ⇒ p₂) ⇒ e* p₁ ⇒ e* p₂
  • e* is conjunctive: distributes over ∧ and ∀

    • e* (fun x -> p₁ x ∧ p₂ x) ⇔ e* p₁ ∧ e* p₂
  • so for any DMF monad we produce correct Dijkstra monad,
    that's usable within the F* verification system

Some dreams and ongoing work on F*

  • Improve tactics, balance automation and control

  • Better treatment of effects: Dijkstra monads for free v2

  • Studying more effects: probabilities, concurrency,

  • Effect masking: hiding exceptions, state, divergence(!?),

  • Verified interoperability: F* (OCaml) + Low* (C) + Vale (ASM)

  • Further work out metatheory and self-certify core type-checker

    • monadic reification + monotonic state and relation to modal logic
    • soundly allowing depth subtyping for datatypes
  • Join the team at Inria Paris and Microsoft Research