Weakest preconditions

Weakest preconditions generate a verification condition for checking the validity of Floyd-Hoare triples by starting at the postcondition and moving backward through the program.

Definition

The weakest precondition of a program statement Sand a postcondition Q is the predicate WP describing the largest possible set of states such that the Floyd-Hoare triple

{ WP } S { Q }

is valid for total correctness. In other words, whenever { P } S { Q } is valid, then P implies WP.

Intuition

The weakest precondition is a predicate, which describes all possible initial states (think: inputs) on which we can run program such that it terminates without runtime errors in final states (think: outputs) specified by the postcondition .

When determining the weakest precondition of a program and a postcondition , we thus aim to modify in a way that reverts changes made to the state by . The result is a predicate that is true for a given initial state whenever running on that initial state leads to a final state satisfying again. Thus, one can think about the weakest precondition as the predicate that anticipates the truth value of postcondition but before (that is, evaluated in initial states) rather than after (that is, evaluated in final states) running program .

Computation

We typically do not need to apply the above (semantic) definition, since we can compute weakest preconditions (syntactically) on the program structure using the following recursive definition:

S                       WP(S, Q)
===============================================
var x                   forall x :: Q
assert R                R && Q
assume R                R ==> Q
x := a                  Q[x / a]
S1; S2                  WP(S1, WP(S2, Q))
S1 [] S2                WP(S1, Q) && WP(S2, Q)

Here, Q[x / a] is the predicate Q in which every (free) appearance of x has been replaced by a. A formal definition is given below.

We briefly go over the above rules for computing weakest preconditions.

Variable declarations

The variable declaration var x declares x as an unitialized (integer-valued) variable named x. All information about the value of some other variables named x that might have existed before are forgotten. This is reflected by the operational rule

which nondeterministically assigns any integer to x.

Now, assume that postcondition Q holds for the final state . What are the possible initial states such that executing var x could lead to such a state?

In fact, such states must coincide with for all variables except x, since var x does not change any other variable. Moreover, the value of x does not matter at all, since x will be set to an arbitrary integer by executing var x.

Hence, we can (but, due to nondeterminism do not have to) reach from any state for all initial values of x.

As a predicate, we can use a universal quantifier over x to express that the value of x does not matter. This yields WP(var x, Q) ::= forall x :: Q.

Assignments

The assignment x := a evaluates expression a in the current (initial) program state and assigns the resulting value to variable x. This is reflected by the operational rule

Similarly to the variable declaration, only variable x is potentially changed by the assignment. However, the original value of x now does matter, since we may need it to evaluate expression a. For example, the assignment x := x + 1 depends on the value of x in the initial state.

Now, assume that postcondition Q holds for the final state . What are the possible initial states such that executing x := a leads to such a state?

Intuitively, we would like to undo the change made by the assignment. For example, if x >= 17 holds after the assignment x := x + 1, then x >= 16 should hold before the assignment. One way to do this is to observe that the expression x + 1 is evaluated in initial states, just like the weakest precondition. We can thus use it to refer to the value v. To undo the assignment, we then replace every occurrence of x by x+1 and obtain x + 1 >= 17.

The effect of such a replacement is that the effect of setting x to the value of x+1 now appears on both sides of every constraint involving x. For the arithmetic operations in our language, they will thus cancel each other out. More concretely, x + 1 >= 17 is equivalent to x >= 16, our desired precondition.

Formally, the weakest precondition of assignments is defined as WP(x:=a, Q) ::= Q[x/a], that is we formally substitute x by a in Q.

The following lemma justifies the above intuition, that is, we can substitute x by a to "undo" the effect of executing the assignment x := a:

Substitution Lemma Consider a predicate , an arithmetic expression , and a state . Moreover, assume that evaluates to value for state , that is, .

Then, if and only if .

The proof is by structural induction on the syntax of arithmetic expressions and left as an exercise.

Assertions

The assertion assert R checks whether the predicate R holds in the current (initial) state and causes a runtime error if this is not the case; otherwise, it has no effect. This is reflected by two operational rules, one for the case that R holds and one for the case that it does not hold:

Now, assume that postcondition Q holds for the final state . We do not consider the configuration error because reason about total correctness; a triple is not valid if we can reach error from an initial state that satisfies the precondition.

What are the possible initial states such that executing assert R leads to ? We first observe that the assertion does not change : if Q holds after execution of assert R, then it also holds before its execution. We then reach the final state if and only if initial state additionally satisfies the predicate R.

Hence, the weakest precondition of assertions is defined as WP(assert R, Q) ::= R && Q.

Assumptions

The assumption assume R is a somewhat strange statement because we cannot execute it on an actual machine; it is a verification-specific statement, similar to making an assumption in a mathematical proof. Intuitively, assume R checks whether the predicate R holds in the current (initial) state. Similar to an assertion, there is no effect if the predicate holds. However, in contrast to an assertion, we reach a "magical state" in which everything goes if the predicate does not hold. This corresponds to the case that we have made a wrong assumption in a proof: from a wrong assumption we can conclude whatever we want, so the result becomes worthless. This is reflected by two operational rules, one for the case that R holds and one for the case that it does not hold:

Now, assume that postcondition Q holds for the final state . As for assertions, we do not consider the configuration magic; executions that move to magic cannot invalidate a triple anyway.

What are the possible initial states such that executing assume R leads to ? We first observe that the assertion does not change : if Q holds after execution of assume R, then it also holds before its execution. There are now two cases:

  1. satisfies R and postcondition Q, or
  2. does not satisfy R and we thus do not care about the result due to a wrong assumption; the weakest precondition should be true in this case, since everything follows from a wrong assumption.

Formally, this can be expressed as the predicate (R && Q) || !R or, equivalently, WP(assume R) ::= R ==> Q.

Sequential composition

The sequential composition S1;S2 first executes program S1 and then program S2. This is reflected by four operational rules

Here, the first rule keeps executing S1; termination of S1 is handled by the second rule, which moves on with executing S2. The last two rules propagate the case that we already encountered a runtime error or a wrong assumption.

What are the possible initial states such that executing S1;S2 leads to final states that satisfy postcondition Q?

By the inductive definition of weakest preconditions, we can assume that we already know how to construct weakest preconditions for S1 and S2, namely WP(S2, Q) and WP(S1, WP(S2, Q)). The set of all initial states on which we can run S2 to end up in a state that satisfies Q is given by WP(S2, Q). We can use this predicate as the postcondition of program S1. By the same argument, WP(S1, WP(S2, Q)) is the set of all initial states on which we can run S1 to end up in a state that satisfies WP(S2, Q).

Putting both arguments together, WP(S1, WP(S2, Q)) is the set of all initial states such that running S1 followed by S2 leads us to states that satisfy Q. Hence, the weakest precondition of sequential composition is defined as WP(S1;S2, Q) ::= WP(S1, WP(S2, Q)).

Nondeterministic choice

The nondeterministic choice S1 [] S2 executes either program S1 or program S2; we do not know which one. This is reflected by two operational rules, one for running S1 and one for running S2:

What are the possible initial states such that executing S1 [] S2 leads to final states that satisfy postcondition Q? By the inductive definition of weakest preconditions, we can assume that we already know how to construct weakest preconditions for S1 and S2, namely WP(S1, Q) and WP(S2, Q). Since we do not know whether S1 or S2 is executed, we can only guarantee that we will certainly reach a state that satisfies Q if we start in a state from which it does not matter whether we run S1 or S2. Our desired initial states must thus satisfy both WP(S1, Q) and WP(S2, Q).

Hence, the weakest precondition of nondeterministic choice is defined as WP(S1 [] S2, Q) ::= WP(S1, Q) && WP(S2, Q).

Variable substitution

F[x/a] denotes the substitution of every (free) occurence of variable x by expression a in F. It is defined recursively as follows:

F                      F[x/a]
============================================
true                    true
false                   false
x                       a
y                       y    (where x != y)
v                       v
a1 <> a2                a1[x/a] <> a2[x/a]    
                            for <> in { ==, !=, <=, >=, <, >, +, *, - /, % }
Q1 && Q2                Q1[x/a] && Q2[x/a]
Q2 || Q2                Q1[x/a] || Q2[x/a]
!Q1                     !(Q1[x/a])
Q1 ==> Q2               Q1[x/a] ==> Q2[x/a]
exists y :: Q1          exists y :: Q1[x/a]
forall y :: Q1          forall y :: Q1[x/a]