3.4 Another Verification3 Examples3.2 A User-Defined Datatype3.3 A Program Verification

3.3 A Program Verification

Our next goal is the verification of a small program that represents the core of linear search: the program finds the first index r at which a value x occurs in an array a; r is -1, if x does not occur in a:

{ olda = a /\ oldx = x /\ n = length(a) /\ i = 0 /\ r = -1 }
while i < n /\ r = -1 do
  if a[i] = x
    then r := i
    else i := i+1
{ a = olda /\ x = oldx /\
    ((r = -1 /\ /\i: 0 <= i < length(a) => a[i] /= x) \/
    ((0 <= r < length(a) /\ a[r] = x /\ /\i: 0 <= i < r => a[i] != x))) }

Above program specification is given in the form of an Hoare triple [8] of the form {I} P {O} which states the partial correctness of P: that, if the input condition I holds before the execution of P, then the output condition shall O hold afterwards (provided that P terminates).

The Verification Conditions

By the rules of the Hoare calculus [8], we can derive from above triple four verification conditions A, B1, B2, and C that have to be proved:

Input:<=> olda = a /\ oldx = x /\ n = length(a) /\ i = 0 /\ r = -1
Output:<=> a = olda /\ x = oldx /\
  ((r = -1 /\ /\i: 0 <= i < length(a) => a[i] /= x) \/
   (0 <= r < length(a) /\ a[r] = x /\ /\i: 0 <= i < r => a[i] /= x))
Invariant:<=> olda = a /\ oldx = x /\ n = length(a) /\
  0 <= i <=n /\ /\j: 0 <= j < i => a[j] /= x /\
  (r = -1 \/ (r = i /\ i < n /\ a[r] = x))

A :<=> Input => Invariant
B1:<=> Invariant /\ i < n /\ r = -1 /\ a[i] = x  => Invariant[i/r]
B2:<=> Invariant /\ i < n /\ r = -1 /\ a[i] /= x => Invariant[i+1/i]
C :<=> Invariant /\ ~(i < n /\ r = -1) => Output

Condition A states that the input condition establishes the loop invariant (a condition that is true before and after each iteration of the loop), conditions B1 and B2 state that the invariant is preserved by both branches of the conditional statement in the loop body, condition C states that the invariant and the negation of the loop condition establish the output condition. The notation F[a/x] denotes a version of formula F where every free occurrence of variable x is replaced by term a (after a suitable renaming of bound variables in F).

We are now, based on the definition of the datatype "array" given in the previous section, describing the formulation of these conditions in our system (the software distribution includes the example presented in this section in directory examples with the declarations listed in file linsearch.pn and the proofs stored in subdirectory linsearch).

First we declare the constants occurring in the verification conditions and the predicates Input and Output in which these constants freely occur:

a: ARR; olda: ARR; x: ELEM; oldx: ELEM; 
i: NAT; n: NAT; r: INT;

Input: BOOLEAN =
  olda = a AND oldx = x AND n = length(a) 
  AND i = 0 AND r = -1;

Output: BOOLEAN =
  a = olda AND
  ((r = -1 AND 
     (FORALL(j:NAT): j < length(a) => get(a,j) /= x)) 
     OR
   (0 <= r AND r < length(a) AND get(a,r) = x AND
     (FORALL(j:NAT): j < r => get(a,j) /= x)));

Since the verification conditions use different instantiations of Invariant, this predicate does not directly refer to above constants but is provided with corresponding parameters instead:

Invariant: (ARR, ELEM, NAT, NAT, INT) -> BOOLEAN =
  LAMBDA(a: ARR, x: ELEM, i: NAT, n: NAT, r: INT):
    olda = a AND oldx = x AND n = length(a) AND 
    i <= n AND
    (FORALL(j:NAT): j < i => get(a,j) /= x) AND
    (r = -1 OR (r = i AND i < n AND get(a,r) = x));

The four verification conditions can now be defined as follows:

A: FORMULA
  Input => Invariant(a, x, i, n, r);

B1: FORMULA
  Invariant(a, x, i, n, r) AND i < n AND r = -1 
  AND get(a,i) = x => Invariant(a, x, i, n, i);

B2: FORMULA
  Invariant(a, x, i, n, r) AND i < n AND r = -1 
  AND get(a,i) /= x => Invariant(a, x, i+1, n, r);

C: FORMULA
  Invariant(a, x, i, n, r) AND 
  NOT(i < n AND r = -1) => Output;

The parameterized predicate Invariant is applied to those arguments that correspond to the instantiation values in the original definition, e.g. Invariant(a, x, i+1, n, r) represents Invariant[a/a, x/x, i+1/i, n/n, r/r].

The pretty-printed versions of the declarations are shown below:

Verification Conditions

We are now going to discuss the proof of each condition in turn, starting for better understanding with a display of the the overall structure of each proof.

Verification Condition A

The proof of verification condition A is very simple:

Verification of Condition A

The root state [bca] consists of a single goal formula:

Root State of Condition A

We execute expand Input, Invariant and get the proof state[fuo].

After Expansion

Rather than investigating this state, we simply press the "Scatter" button which generates a single state [bxg] which is automatically closed by the decision procedure.

Verification Condition B1

The proof of verification condition B1 is trivial:

Verification of Condition B1

The root state [p1b] consists of a single goal [en3] with two occurrences of the predicate Invariant:

Root State of Proof of B1

We execute expand Invariant which results in a single child state [lf6] that is closed automatically.

Verification Condition B2

The proof of verification condition B1 has the following structure:

Verification of Condition B2

Like in the proof of condition B1, the root state [q1b] consists of a single goal [6kv] with two occurrences of the predicate Invariant:

Root State of Proof of B2

We execute the command expand Invariant in 6ev which results in the following state:

Proof State After Expansion

Rather than investigating this state further, we press the "Scatter" button which generates five children states of which four are closed automatically. Only the state [a1y] requires our attention:

Proof State After Scattering

This state contains a universally quantified assumption [564]. We guess that this assumption needs to be appropriately instantiated and press the "Auto" button which generates a single child state [cch] that is automatically closed; thus the proof is complete.

Verification Condition C

The proof of condition C has a slightly more complicated structure:

Verification of Condition C

The root state [dca] has goal [zfg] with occurrences of the predicates Invariant and Output.

Initial Proof State

We use the command expand Invariant, Output in zfg to replace these predicates by their definitions, which results in the following state:

Proof State after Expansion

As usual, we do not bother to investigate the structure of this state further but immediately press the "Scatter" button which generates four children states of which one is closed automatically. Of the three remaining states [dcu], [ecu], and [fcu], the first one is as follows:

Proof State after Scattering

The state has an universally quantified assumption [564]; thus it looks like as if we need to use a proper instantiation of this formula. Before trying the "Auto" button we remember the other two open states and ponder that they may have similar structures. Rather than applying individually on each state, we press the button which applies the auto command not only to the current state but also to all its sibling states. Our boldness is rewarded by the fact that both [dcu] and [fcu] are automatically closed such that we only need to investigate state [ecu] further:

Proof State after Automatic Instantiations

This state has three assumptions [gkr], [orv] and [pkg] that start with the atomic formula r=-1 (which, as we remember, denotes "element not found" in the program). If r=-1, we can derive additional knowledge from the implications [orv] and [pkg] (where r=-1 appears in the hypothesis of the implication such that we can deduce its conclusion part); if r != -1, we may derive additional knowledge from the disjunction [gkr] (because then the remaining clauses of the disjunction must be true). Thus our further reasoning depends on the fact, which of the two possibilities r=-1 or r != -1 is true and we have to split our our proof correspondingly into two branches.

One way to proceed is thus to execute the command case r=-1 which generates two child states, one with the additional assumption r=-1, one with the additional assumption r != -1. However, there also exists another possibility: moving the mouse cursor over the labels of [gkr], [orv], and [pkg] reveal popup menus that list applications of the command split to these formulas. This command "splits" the current state into several child states each of which receives as an additional assumption one of the components of the disjunctive formula to which the command is applied (also an implication F => G can be seen as a disjunction  F G).

Being lazy, rather than typing in case r=-1 on the command line, we select from the menu of [pkg] the command split pkg. This yields two child states of which one is automatically closed while the other with label [lel] still requires our attention7:

Proof State after Splitting

This state has an existential assumption [1bb]. To get rid of the quantifier, we press the "Scatter" button and get the state [lvn]:

Proof State after Scattering

This state contains a universally quantified assumption [564]; before investigating the state any further, we try whether the automatic instantiation of this formula with the "Auto" button does any good: indeed, a proof state [lap] is generated which is automatically closed such that the proof is completed.

Having proved all verification conditions, the partial correctness of the initially stated program is verified.

As shown in this proof, splitting proof states by case distinctions belongs to those activities (apart from expanding definitions, finding instantiations of quantified formulas, and introducing lemmas, see the next section) where human intervention is required. Since such splits may have drastic consequences on the size of the proof tree (and thus the complexity of the proof), the system does not try on its own to split a proof state by disjunctive assumptions (the "Scatter" button splits proof states by conjunctive goals only).


Wolfgang Schreiner

3.4 Another Verification3 Examples3.2 A User-Defined Datatype3.3 A Program Verification