![]() | ![]() | ![]() | 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).
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:
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.
The proof of verification condition A is very simple:
The root state [bca] consists of a single goal formula:
We execute expand Input, Invariant and get the proof state[fuo].
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.
The proof of verification condition B1 is trivial:
The root state [p1b] consists of a single goal [en3] with two occurrences of the predicate Invariant:
We execute expand Invariant which results in a single child state [lf6] that is closed automatically.
The proof of verification condition B1 has the following structure:
Like in the proof of condition B1, the root state [q1b] consists of a single goal [6kv] with two occurrences of the predicate Invariant:
We execute the command expand Invariant in 6ev which results in the following state:
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:
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.
The proof of condition C has a slightly more complicated structure:
The root state [dca] has goal [zfg] with occurrences of the predicates Invariant and Output.
We use the command expand Invariant, Output in zfg to replace these predicates by their definitions, which results in the following state:
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:
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:
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:
This state has an existential assumption [1bb]. To get rid of the quantifier,
we press the "Scatter" button
and get the state [lvn]:
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).
![]() | ![]() | ![]() | 3.3 A Program Verification |