Primitive Effect Refinements
Note
This chapter provides some background on Floyd-Hoare logic and weakest-precondition-based verification condition generation. This is necessary if you want to understand a bit about how F* infers the logical constraints needed to prove the correctness of a program. It is also useful background for more advanced material in subsequent chapters about defining custom effects in F*, e.g., effects to model state, exceptions, or concurrency.
Refinement types x:t{p}
refine value types t
and allow us to
make more precise assertions about the values in the program. For
example, when we have v : x:int{x >= 0}
, then not only we know
that v
is an int
, but also that v >= 0
.
In a similar manner, F* allows refining computation types with specifications that describe some aspects of a program’s computational behavior. These effect refinements can, in general, be defined by the user in a reasoning system of their choosing, e.g., the refinements may use separation logic, or they may count computation steps.
However, F* has built-in support for refining the specification of
pure programs with effect refinements that encode the standard
reasoning principles of Floyd-Hoare Logic and weakest
precondition-based calculi. Foreshadowing what’s about to come in this
chapter, we can write the following specification for the
factorial
function:
open FStar.Mul
let rec factorial (x:int)
: Pure int (requires x >= 0) (ensures fun r -> r >= 1)
= if x = 0
then 1
else x * factorial (x - 1)
Intuitively, this type states that factorial x
is a computation
defined only when x >= 0
and always terminates returning a value
r >= 1
. In a way, this type is closely related to other, more
familiar, types we have given to factorial
so far, e.g., nat ->
pos
, and, indeed, factorial
can be used at this type.
let fact (x:nat) : pos = factorial x
Actually, in all the code we’ve seen so far, what’s happening under
the covers is that F* infers a type for a pure program similar to
Pure t pre post
and then checks that that type can be subsumed to
a user-provided specification of the form Tot t'
.
In this chapter, we look into how these Pure
specifications work,
starting with a primer on Floyd-Hoare Logic and weakest precondition
calculi. If the reader is familiar with these, they may safely skip
the next subsections, though even if you are an expert, if may be of
interest to see how such program logics can be formalized in F*.
A Primer on Floyd-Hoare Logic and Weakest Preconditions
Floyd-Hoare Logic is a system of specifications and rules to reason about the logical properties of programs, introduced by Robert Floyd in a paper titled Assigning Meaning to Programs and by Tony Hoare in An axiomatic basis for computer programming. The notation used in most modern presentations (called Hoare triples) is due to Hoare. An algorithm to compute Hoare triples was developed by Edsger Dijkstra presented first in this paper , using a technique called weakest preconditions. All of them received Turing Awards for their work on these and other related topics.
For an introduction to these ideas, we’ll develop a small imperative language with global variables, presenting
An operational semantics for the language, formalized as an interpreter.
A Floyd-Hoare program logic proven sound with respect to the operational semantics.
And, finally, an algorithm to compute weakest preconditions proved sound against the Floyd-Hoare logic.
Our language has the following abstract syntax:
let var = nat
type expr =
| EConst : int -> expr // constants: ..., -1, 0, 1, ...
| EVar : var -> expr // global variables
| EAdd : expr -> expr -> expr // arithmetic: e1 + e2
type program =
| Assign : var -> expr -> program // x := e
| Seq : program -> program -> program // p1; p2
| If : expr -> program -> program -> program // if e then p1 else p2
| Repeat : expr -> program -> program // repeat n { p }
Expressions includes integer constants, global variables (represented just as natural numbers), and some other forms, e.g., arithmetic expressions like addition.
A program includes:
Assignments,
EAssign x e
, representing the assignment of the result of an expressione
to a global variablex
, i.e.,x := e
Seq
, to compose programs sequentiallyIf
to compose programs conditionallyAnd
Repeat n p
, which represents a construct similar to afor
-loop (or primitive recursion), where the programp
is repeatedn
times, wheren
evaluates to a non-negative integer.
Our language does not have while
loops, whose semantics are a bit
more subtle to develop. We will look at a semantics for while
in a
subsequent chapter.
Operational Semantics
Our first step in giving a semantics to programs is to define an interpreter for it to run a program while transforming a memory that stores the values of the global variables.
To model this memory, we use the type state
shown below:
// The state of a program is a map from global variable to integers
let state = var -> int
let read (s:state) (i:var) : int = s i
let write (s:state) (i:var) (v:int)
: state
= fun j -> if i=j then v else read s j
Writing a small evaluator for expressions is easy:
let rec eval_expr (e:expr) (s:state)
: int
= match e with
| EConst v ->
v
| EVar x ->
read s x
| EAdd e1 e2 ->
eval_expr e1 s + eval_expr e2 s
The interpreter for programs itself takes a bit more work, since
programs can both read and write the state. To structure our
interpreter, we’ll introduce a simple state monad st a
. We’ve seen
this construction before in a previous chapter—so, look there if the state monad is unfamiliar
to you. Recall that F* has support for monadic let operators: the
let!
provides syntactic sugar to convenient compose st
terms.
let st (a:Type) = state -> (a & state)
let get : st state = fun s -> (s, s)
let put (s:state) : st unit = fun _ -> ((), s)
let (let!) #a #b (f:st a) (g: a -> st b) : st b =
fun s -> let v, s' = f s in g v s'
let return #a (x:a) : st a = fun s -> (x, s)
Now, the interpreter itself is a total, recursive function run
which
interprets a program p
as a state-passing function of type st
unit
, or state -> unit & state
.
let rec run (p:program)
: Tot (st unit)
(decreases %[p;0])
= match p with
| Assign x e ->
let! s = get in
let v = eval_expr e s in
put (write s x v)
| Seq p1 p2 ->
run p1;!
run p2
| If e p1 p2 ->
let! s = get in
let n = eval_expr e s in
if n <> 0 then run p1 else run p2
| Repeat e p ->
let! s = get in
let n = eval_expr e s in
if n <= 0 then return ()
else run_repeat p n
and run_repeat (p:program) (n:nat)
: Tot (st unit)
(decreases %[p; 1; n])
= if n = 0 then return ()
else ( run p ;! run_repeat p (n - 1) )
Let’s look at its definition in detail:
Assign x e
: Evaluatee
in the current state and then update the state with a new value ofx
.
Seq p1 p2
: Simply runp1
and then runp2
, where;!
is syntactic sugar forlet! _ = run p1 in run p2
.
If e p1 p2
: Evaluatee
in the current state, branch on its result and run eitherp1
orp2
Repeat e p
: Evaluatee
ton
, and ifn
is greater than zero, call the mutually recursiverun_repeat n p
. Most of the subtlety here is in convincing F* that this mutually recursive function terminates, but this is fairly straightforward once you know how—we discussed termination proofs for mutually recursive functions earlier.
These operational semantics are the ground truth for our programming language—it defines how programs execute. Now that we have that settled, we can look at how a Floyd-Hoare logic makes it possible to reason about programs in a structured way.
Floyd-Hoare Logic
The goal of a Floyd-Hoare logic is to provide a way to reason about a
program based on the structure of its syntax, rather than reasoning
directly about its operational semantics. The unit of reasoning is
called a Hoare triple, a predicate of the form {P} c {Q}
, where
P
and Q
are predicates about the state of the program, and
c
is the program itself.
We can define Hoare triples for our language by interpreting them as
an assertion about the operational semantics, where triple p c q
represents, formally, the Hoare triple { p } c { q }
.
let triple (pre:state -> prop)
(c:program)
(post:state -> prop)
= forall s0. pre s0 ==> post (snd (run c s0))
The predicate triple p c q
is valid, if when executing c
in a
state that satisfies p
results in a state that satisfies q
.
The predicates p
and q
are also called precondition and
postcondition of c
, respectively.
For each syntactic construct of our language, we can prove a lemma
that shows how to build an instance of the triple
predicate for
that construct. Then, to build a proof of program, one stitches
together these lemmas to obtain a triple p main q
, a statement of
correctess of the main
program.
Assignment
Our first rule is for reasoning about variable assignment:
let assignment (x:var) (e:expr) (post:state -> prop)
: Lemma (triple (fun s0 -> post (write s0 x (eval_expr e s0)))
(Assign x e)
post)
= ()
This lemma says that post
holds after executing x := e
in the
initial state s0
, if post
holds on the initial state updated
at x
with the value of e
.
For example, to prove that after executing z := y + 1
in s0
,
if we expect the value of z
to be greater than zero`, then the
assignment rule says that read s0 y + 1 > 0
should hold before the
assignment, which is what we would expect.
Sequence
Our next lemma about triples stitches together triples for two programs that are sequentially composed:
let sequence (p1 p2:program)
(pre pre_mid post:state -> prop)
: Lemma
(requires
triple pre p1 pre_mid /\
triple pre_mid p2 post)
(ensures
triple pre (Seq p1 p2) post)
= ()
The lemma says that if we can derive the Hoare triples of the two
statements such that postcondition of p1
matches the precondition
of p2
, then we can compose them.
Conditional
The lemma for conditionals is next:
let conditional (e:expr)
(p1 p2:program)
(pre post:state -> prop)
: Lemma
(requires
triple (fun s -> pre s /\ eval_expr e s =!= 0) p1 post /\
triple (fun s -> pre s /\ eval_expr e s == 0) p2 post)
(ensures
triple pre (If e p1 p2) post)
= ()
It says that to derive the postcondition post
from the If e p1
p2
, we should be able to derive it from each of the branches with the
same precondition pre
. In addition, since we know that p1
executes only when e
is non-zero, we can add these facts to the
preconditions of each branch.
Repeat
In all the cases so far, these lemmas are proved automated by F* and Z3. In the case of repeats, however, we need to do a little more work, since an inductive argument is involved.
The rule for repeat
requires a loop invariant inv
. The loop
invariant is an assertion that holds before the loop starts, is
maintained by each iteration of the loop, and is provided as the
postcondition of the loop.
The lemma below states that if we can prove that triple inv p inv
,
then we can also prove triple inv (Repeat e p) inv
.
// We need an auxilary lemma to prove it by induction for repeat_n
let rec repeat_n (p:program) (inv:state -> prop) (n:nat)
: Lemma
(requires triple inv p inv)
(ensures (forall s0. inv s0 ==> inv (snd (run_repeat p n s0))))
= if n = 0 then ()
else repeat_n p inv (n - 1)
// Then, we use that auxiliary lemma to prove the main
// rule for reasoning about repeat using an invariant
let repeat (e:expr) (p:program) (inv:state -> prop)
: Lemma
(requires triple inv p inv)
(ensures triple inv (Repeat e p) inv)
= introduce forall s0. inv s0 ==> inv (snd (run (Repeat e p) s0)) with
introduce _ ==> _ with
inv_s0 . (
let n = eval_expr e s0 in
if n <= 0 then ()
else repeat_n p inv n
)
The auxiliary lemma repeat_n
proves that run_repeat p n
preserves inv
, if p
preserves inv
.
To call this lemma from the main repeat
lemma, we need to “get
our hands on” the initial state s0
, and the syntactic sugar
to manipulate logical connectives makes this
possible.
Consequence
The final lemma about our Hoare triples is called the rule of consequence. It allows strengthening the precondition and weakening the postcondition of a triple.
let consequence (p:program) (pre pre' post post':state -> prop)
: Lemma
(requires
triple pre p post /\
(forall s. pre' s ==> pre s) /\
(forall s. post s ==> post' s))
(ensures
triple pre' p post')
= ()
A precondition of a program is an obligation before the statement
is executed. So if p
requires pre
, we can always strengthen
the precondition to pre'
, provided pre' ==> pre
, i.e. it is
logically valid to require more than necessary in the
precondition. Similarly, postcondition is what a statement
guarantees. So if p
guarantees post
, we can always weaken it
to guarantee less, i.e. some post'
where post ==> post'
.
Weakest Preconditions
The rules of Floyd-Hoare logic provide an abstract way to reason about
programs. However, the rules of the logic are presented
declaratively. For example, to apply the sequence
rule, one has
derive triples for each component in a way that they prove exactly the
same assertion (pre_mid
) about the intermediate state. There may
be many ways to do this, e.g., one could apply the rule of
consequence to weaken the postcondition of the first component, or to
strengthen the precondition of the second component.
Dijkstra’s system of weakest preconditions eliminates such ambiguity and provides an algorithm for computing valid Hoare triples, provided the invariants of all loops are given. This makes weakest preconditions the basis of many program proof tools, since given a program annotated with loop invariants, one can simply compute a logical formula (called a verification condition) whose validity implies the correctness of the program.
At the core of the approach is a function WP (c, Q)
, which
computes a unique, weakest precondition P
for the program c
and postcondition Q
. The semantics of WP
is that WP (c, Q)
is the weakest precondition that should hold before executing c
for the postcondition Q
to be valid after executing c
. Thus,
the function WP
assigns meaning to programs as a transformer of
postconditions Q
to preconditions WP (s, Q)
.
The wp
function for our small imperative language is shown below:
let rec wp (c:program) (post: state -> prop)
: state -> prop
= match c with
| Assign x e ->
fun s0 -> post (write s0 x (eval_expr e s0))
| Seq p1 p2 ->
wp p1 (wp p2 post)
| If e p1 p2 ->
fun s0 ->
(eval_expr e s0 =!= 0 ==> wp p1 post s0) /\
(eval_expr e s0 == 0 ==> wp p2 post s0)
| Repeat e p ->
fun s0 ->
(exists (inv:state -> prop).
inv s0 /\
(forall s. inv s ==> post s) /\
(forall s. inv s ==> wp p inv s))
The case of
Assign
is identical to theassignment
lemma shown earlier.The case of
Seq
sequentially composes the wp’s. That is, to prove thepost
after runningp1 ;; p2
we need to provewp p2 post
after runningp1
. It may be helpful to read this case as the equivalent formfun s0 -> wp p1 (fun s1 -> wp p2 post s1) s0
, wheres0
is the initial state ands1
is the state that results after running justp1
.The
If
case computes the WPs for each branch and requires them to be proven under the suitable branch condition.The
Repeat
case is most interesting: it involves an existentially quantified invariantinv
, which is the loop invariant. That is, to reason aboutRepeat n p
, one has to somehow find an invariantinv
that is true initially, and implies both the WP of the loop body as well as the final postcondition.
The wp
function is sound in the sense that it computes a
sufficient precondition, as proven by the following lemma.
let rec wp_soundness (p:program) (post:state -> prop)
: Lemma (triple (wp p post) p post)
= match p with
| Assign x e -> ()
| Seq p1 p2 ->
wp_soundness p2 post;
wp_soundness p1 (wp p2 post)
| If e p1 p2 ->
wp_soundness p1 post;
wp_soundness p2 post
| Repeat e p ->
introduce forall s0. wp (Repeat e p) post s0 ==>
post (snd (run (Repeat e p) s0)) with
introduce _ ==> _ with
_ . (
eliminate exists (inv:state -> prop).
inv s0 /\
(forall s. inv s ==> post s) /\
(forall s. inv s ==> wp p inv s)
returns _
with inv_props. (
wp_soundness p inv;
repeat e p inv
)
)
One could also prove that wp
computes the weakest precondition,
i.e., if triple p c q
then forall s. p s ==> wp c q s
, though
we do not prove that formally here.
A Sample Program Proof
We now illustrate some sample proofs using our Hoare triples and
wp
function. To emphasize that Hoare triples provide an abstract
way of reasoning about the execution of programs, we define the
hoare p c q
an alias for triple p c q
marked with an attribute
to ensure that F* and Z3 cannot reason directly about the underlying
definition of triple
—that would allow Z3 to find proofs by
reasoning about the operational semantics directly, which we want to
avoid , since it would not scale to larger programs. For more about
the opaque_to_smt
and reveal_opaque
construct, please see
this section on opaque definitions.
[@@"opaque_to_smt"]
let hoare p c q = triple p c q
let wp_hoare (p:program) (post:state -> prop)
: squash (hoare (wp p post) p post)
= reveal_opaque (`%hoare) hoare;
wp_soundness p post
let hoare_consequence (#p:program)
(#pre #post:state -> prop)
(pre_annot:state -> prop)
(_: squash (hoare pre p post))
(_: squash (forall s. pre_annot s ==> pre s))
: squash (hoare pre_annot p post)
= reveal_opaque (`%hoare) hoare
The lemmas above are just restatements of the wp_soundness
and
consequence
lemmas that we’ve already proven. Now, these are the
only two lemmas we have to reason about the hoare p c q
predicate.
Next, we define some notation to make it a bit more convenient to write programs in our small language.
let ( := ) (x:var) (y:expr) = Assign x y
let add (e1 e2:expr) = EAdd e1 e2
let c (i:int) : expr = EConst i
let v (x:var) : expr = EVar x
let rec prog (p:list program { Cons? p })
: program
= match p with
| [c] -> c
| c::cs -> Seq c (prog cs)
let x = 0
let y = 1
let z = 2
Finally, we can build proofs of some simple, loop-free programs
automatically by computing their wp
using wp_hoare
and
applying hoare_consequence
to get F* and Z3 to prove that the
inferred WP is implied by the annotated precondition.
let swap = prog [ z := v x; x := v y; y := v z]
let proof_swap (lx ly:int)
: squash (hoare (fun s -> read s x = lx /\ read s y = ly) swap
(fun s -> read s x = ly /\ read s y = lx))
= hoare_consequence _ (wp_hoare swap _) ()
This recipe of computing verification conditions using WPs and then checking the computed WP against the annotated specification using a solver like Z3 is a very common and powerful pattern. In fact, as we’ll see below, the methodology that we’ve developed here for our small imperative language is exactly what the F* typechecker does (at a larger scale and for the whole F* language) when checking an F* program.
The PURE
Effect: A Dijkstra Monad for Pure Computations
F* provides a weakest precondition calculus for reasoning about pure computations. The calculus is based on a Dijkstra Monad, a construction first introduced in this paper. In this chapter, we will learn about Dijkstra Monad and its usage in specifying and proving pure programs in F*.
The first main difference in adapting the Hoare triples and weakest precondition computations that we saw earlier to the setting of F*’s functional language is that there are no global variables or mutable state (we’ll see about how model mutable state in F*’s effect system later). Instead, each pure expression in F*’s returns a value and the postconditions that we will manipulate are predicates about these values, rather than state predicates.
To illustrate, we sketch the definition of pure WPs below.
WP c Q = Q c
WP (let x = e1 in e2) Q = WP e1 (fun x -> WP e2 Q)
WP (if e then e1 else e2) Q = (e ==> WP e1 Q) /\ (~e ==> WP e2 Q)
The WP of a constant
c
is just the postconditionQ
applied toc
.The WP of a
let
binding is a sequential composition of WPs, applied to the values returned by each sub-expressionThe WP of a condition is the WP of each branch, weakened by the suitable branch condition, as before.
The F* type system internalizes and generalizes this WP construction
to apply it to all F* terms. The form this takes is as a computation
type in F*, PURE a wp
, where in prims.fst
, PURE
is defined
as an F* primitive effect with a signature as shown below–we’ll see
much more of the new_effect
syntax as we look at user-defined
effects in subsequent chapters; for now, just see it as a reserved
syntax in F* to introduce a computation type constructor.
new_effect PURE (a:Type) (w:wp a) { ... }
where
let pre = Type0
let post (a:Type) = a -> Type0
let wp (a:Type) = post a -> pre
A program e
of type PURE a wp
is a computation which
Is defined only when
wp (fun _ -> True)
is validIf
wp post
is valid, thene
terminates without any side effects and returns a valuev:a
satisfyingpost v
.
Notice that wp a
is the type of a function transforming
postconditions (a -> Type0
) to preconditions (Type0
). [1] The
wp
argument is also called an index of the PURE
effect. [2]
The return operator for wp a
is shown below: it is analogous to
the WP c Q
and WP x Q
rules for variables and constants that
we showed earlier:
let return_wp (#a:Type) (x:a) : wp a = fun post -> post x
The bind operator for wp a
is analogous to the rule for sequencing
WPs, i.e., the rule for WP (let x = e1 in e2) Q
above:
let bind_wp (#a #b:Type) (wp1:wp a) (wp2:a -> wp b)
: wp b
= fun post -> wp1 (fun x -> wp2 x post)
Finally, analogous to the WP rule for conditionals, one can write a
combinator for composing wp a
in a branch:
let if_then_else_wp (#a:Type) (b:bool) (wp1 wp2:wp a)
: wp a
= fun post -> if b then wp1 post else wp2 post
This is the essence of the Dijkstra monad construction for pure
programs: the rule for computing weakest preconditions for a
computation returning a value x
is return_wp
; the rule for
computing the WP of the sequential composition of terms is the
sequential composition of WPs using bind_wp
; the rule for
computing the WP of a conditional term is the conditional composition
of WPs using if_then_else_wp
.
If fact, if one thinks of pure computations as the identity monad,
tot a
as shown below:
let tot (a:Type) = a
let return_tot (#a:Type) (x:a) : tot a = x
let bind_tot (#a #b:Type) (x:tot a) (y:a -> tot b)
: tot b
= let v = x in y v
then the parallel between the tot
monad and wp
becomes even
clearer—the WP analog of return_tot
is return_wp
and of
bind_tot
is bind_wp
.
It turns out that wp a
(for monotonic weakest preconditions) is
itself a monad, as shown below by a proof of the monad laws:
(* A monotonic WP maps stronger postconditions to stronger preconditions *)
let monotonic (#a:Type) (wp:wp a) =
forall (p q:post a). (forall x. p x ==> q x) ==> (wp p ==> wp q)
let mwp (a:Type) = w:wp a { monotonic w }
(* An equivalence relation on WPs *)
let ( =~= ) (#a:Type) (wp1 wp2:wp a)
: prop
= forall post. wp1 post <==> wp2 post
(* The three monad laws *)
let left_identity (a b:Type) (x:a) (wp:a -> mwp a)
: Lemma (bind_wp (return_wp x) wp =~= wp x)
= ()
let right_identity (a b:Type) (wp:mwp a)
: Lemma (wp =~= (bind_wp wp return_wp))
= ()
let associativity (a b c:Type) (wp1:mwp a) (wp2:a -> mwp b) (wp3:b -> mwp c)
: Lemma (bind_wp wp1 (fun x -> bind_wp (wp2 x) wp3) =~=
bind_wp (bind_wp wp1 wp2) wp3)
= ()
PURE
and Tot
When typechecking a program, F* computes a weakest precondition which characterizes a necessary condition for the program to satisfy all its typing constraints. This computed weakest precondition is usually hidden from the programmer, but if you annotate your program suitably, you can get access to it, as shown in the code snippet below:
let square (n:int)
: PURE nat (as_pure_wp #nat (fun q -> n*n >= 0 /\ q (n * n)))
= n * n
The type says that square n
is a pure function, which for any
postcondition q:nat -> prop
,
Is defined only when
n * n >= 0
and whenq (n * n)
is validAnd returns a value
m:nat
satisfyingq m
Let’s look at another example:
let maybe_incr (b:bool) (x:int)
: PURE int (as_pure_wp (if_then_else_wp b
(bind_wp (return_wp (x + 1)) (fun y -> return_wp y))
(return_wp x)))
= if b
then let y = x + 1 in y
else x
Notice how the wp
index of PURE
mirrors the structure of the
computation itself—it starts with an if_then_else_wp
, then in
the first branch, uses a bind_wp
followed by a return; and in the
else branch it returns x
.
As such, the wp-index simply “lifts” the computation into a specification in a form amenable to logical reasoning, e.g., using the SMT solver. For pure programs this may seem like overkill, since the pure term itself can be reasoned about directly, but when the term contains non-trivial typing constraints, e.g., such as those that arise from refinement type checking, lifting the entire program into a single constraint structures and simplifies logical reasoning.
Of course, one often writes specifications that are more abstract than
the full logical lifting of the program, as in the example below,
which says that to prove post
of the return value, the
precondition is to prove post
on all y >= x
. This is a
valid, although weaker, characterization of the function’s return
value.
let maybe_incr2 (b:bool) (x:int)
: PURE int (as_pure_wp (fun post -> forall (y:int). y >= x ==> post y))
= if b
then let y = x + 1 in y
else x
The PURE
computation type comes with a built-in weakening rule. In
particular, if a term is computed to have type PURE a wp_a
and it is
annotated to have type PURE b wp_b
, then F* does the following:
It computes a constraint
p : a -> Type0
, which is sufficient to prove thata
is a subtype ofb
, e.g., isa = int
andb = nat
, the constraintp
isfun (x:int) -> x >= 0
.Next, it strengthens
wp_a
to assert that the returned value validates the subtyping constraintsp x
, i.e., it buildsassert_wp wp_a p
, wherelet assert_wp (#a:Type) (w:wp a) (p: a -> Type0) : wp (x:a{ p x }) = fun post -> w (fun (x:a) -> p x /\ post x)Finally, it produces the verification condition
stronger_wp #b wp_b (assert_wp wp_a p)
, wherestronger_wp
is defined as shown below:let stronger_wp (#a:Type) (wp1 wp2:wp a) : prop = forall post. wp1 post ==> wp2 postThat is, for any postcondition
post
, the preconditionwp_b post
implies the original preconditionwp_a post
as well as the subtyping constraintp x
. This matches the intuition about preconditions that we built earlier: it is always sound to require more in the precondition.
Thus, when we have e:PURE a wp
in F*, the wp
is a predicate
transformer for e
, not necessarily the weakest one.
Of course, even maybe_incr2
is not particularly idiomatic in
F*. One would usually annotate a program with a refinement type, such
as the one below:
let maybe_incr_tot (b:bool) (x:int)
: Tot (y:int { y >= x })
= if b
then let y = x + 1 in y
else x
Internally to the compiler, F* treats Tot t
as the following
instance of PURE
:
Tot t = PURE t (fun post -> forall (x:t). post x)
Once Tot t
is viewed as just an instance of PURE
, checking if
a user annotation Tot t
is stronger than the inferred
specification of a term PURE a wp
is just as explained before.
Pure
: Hoare Triples for PURE
Although specification are easier to compute using WPs, they are more natural to read and write when presented as Hoare triples, with a clear separation between precondition and postconditions. Further, when specifications written as Hoare triples naturally induce monotonic WPs.
F* provides an effect abbreviation called Pure
for writing and
typechecking Hoare-style specifications for pure programs, and is
defined as shown below in prims.fst
:
effect Pure (a:Type) (req:Type0) (ens:a -> Type0) =
PURE a (fun post -> req /\ (forall x. ens x ==> post x))
The signature of Pure
is Pure a req ens
, where req
is the
precondition and ens:a -> Type0
is the postcondition. Using
Pure
, we can write the factorial
function we saw at the top of
this chapter—F* infers a PURE a wp
type for it, and relates it
to the annotated Pure int req ens
type, proving that the latter
has a stronger precondition and weaker postcondition.
One may wonder when one should write specifications using the notation
x:a -> Pure b req ens
versus x:a{req} -> Tot (y:b { ens y
})
. The two styles are closely related and choosing between them is
mostly a matter of taste. As you have seen, until this point in the
book, we have not used Pure a req ens
at all. However, when a
function has many pre and postconditions, it is sometimes more
convenient to use the Pure a req ens
notation, rather than
stuffing all the constraints in refinement types.
GHOST
and DIV
Just as PURE
is an wp-indexed refinement of Tot
, F* provides
two more primitive wp-indexed effects:
GHOST (a:Type) (w:wp a)
is a refinement ofGTot a
DIV (a:Type) (w:wp a)
is a refinement ofDv a
That is, F* uses the GHOST
effect to infer total correctness WPs
for ghost computations, where, internally, GTot a
is equivalent to
GHOST a (fun post -> forall x. post x)
Likewise, F* uses the DIV
effect to infer partial correctness
WPs for potentially non-terminating computations, where, internally,
Dv a
is equivalent to DIV a (fun post -> forall x. post x)
.
As with Tot
and PURE
, F* automatically relates GTot
and
GHOST
computations, and Dv
and DIV
computations. Further,
the effect ordering Tot < Dv
and Tot < GTot
extends to PURE
< DIV
and PURE < GHOST
as well.
The prims.fst
library also provides Hoare-triple style
abbreviations for GHOST
and DIV
, i.e.,
effect Ghost a req ens = GHOST a (fun post -> req /\ (forall x. ens x /\ post x))
effect Div a req ens = DIV a (fun post -> req /\ (forall x. ens x /\ post x))
These Hoare-style abbreviations are more convenient to use than their more primitive WP-based counterparts.
The tradeoffs of using Ghost
vs. GTot
or Div
vs. Dv
are similar to those for Pure
vs Tot
—it’s mostly a matter of
taste. In fact, there are relatively few occurrences of Pure
,
Ghost
, and Div
in most F* codebases. However, there is one
important exception: Lemma
.
The Lemma
abbreviation
We can finally unveil the definition of the Lemma
syntax, which we
introduced as a syntactic shorthand in an early chapter. In fact, Lemma
is defined in prims.fst
as follows:
effect Lemma (a: eqtype_u)
(pre: Type)
(post: (squash pre -> Type))
(smt_pats: list pattern) =
Pure a pre (fun r -> post ())
That is, Lemma
is an instance of the Hoare-style refinement of
pure computations Pure a req ens
. So, when you write a proof term
and annotate it as e : Lemma (requires pre) (ensures post)
, F*
infers a specification for e : PURE a wp
, and then, as with all
PURE computations, F* tries to check that the annotated Lemma
specification has a stronger WP-specification than the computed
weakest precondition.
Of course, F* still includes syntactic sugar for Lemma
, e.g.,
Lemma (requires pre) (ensures post)
is desugared to Lemma unit
pre (fun _ -> post) []
. The last argument of a lemma, the
smt_pats
are used to introduce lemmas to the SMT solver for proof
automation—a later chapter covers that in detail.
Finally, notice the type of the post
, which assumes squash pre
as an argument–this is what allows the ensures
clause of a
Lemma
to assume that what was specified in the `requires
clause.