3.3 A Program Verification3 Examples3.1 An Induction Proof3.2 A User-Defined Datatype

3.2 A User-Defined Datatype

This section deals with a constructive definition of the datatype "array" and the proof that this definition is adequate with respect to the fundamental properties that we expect of arrays. Such a datatype is useful for verifications of programs operating on arrays; one such verification is shown in the next section. The definition of the datatype also illustrates more features of the specification language of our system.

This specification language already includes a type constructor ARRAY such that we can build, given arbitrary types INDEX and ELEM, the type ARRAY INDEX OF ELEM; the elements of this type map every value of type INDEX to a value of type ELEM. For our purpose, we may define INDEX as NAT but then encounter the problem that a programming language array has a finite length which needs to be properly represented, too.

The Declarations

These considerations lead us to the following type declarations:

INDEX: TYPE = NAT;
ELEM:  TYPE;
ARR:   TYPE = [INDEX, ARRAY INDEX OF ELEM];

These declarations introduce a type constant INDEX (which is identified with NAT), a type constant ELEM (which remains undefined and is thus assumed to denote a type different from all other types), and a type constant ARR. This type is defined in the declaration as the domain of all binary tuples whose first component is of type INDEX (representing the length of the array, i.e., the first index that is not in use by the array) and whose second component is of type ARRAY INDEX OF ELEM (representing the actual content of the array, i.e. the mapping of array indices to array elements).

In subsequent value declarations, we introduce various auxiliary constants whose values remain undefined and that serve as error signals:

any:      ARRAY INDEX OF ELEM;
anyelem:  ELEM;
anyarray: ARR;

We also define the following auxiliary function constant:

content: ARR -> (ARRAY INDEX OF ELEM) = 
  LAMBDA(a:ARR): a.1;

The declaration of a function constant is just the declaration of a value constant of function type, in our case a constant content of type ARR -> (ARRAY INDEX OF ELEM). The value of this function is defined by the function value expression LAMBDA(a:ARR): a.1 which denotes the function that, given an argument a of type ARR, returns its second component a.1, i.e. the content of the array (the notation a.i denotes component i of tuple a; components are numbered starting with 0).

With the help of these auxiliary notions, we define our core functions on arrays:

length: ARR -> INDEX = 
  LAMBDA(a:ARR): a.0;
new: INDEX -> ARR = 
  LAMBDA(n:INDEX): (n, any);
put: (ARR, INDEX, ELEM) -> ARR =
 LAMBDA(a:ARR, i:INDEX, e:ELEM):
   IF i < length(a) 
     THEN (length(a), content(a) WITH [i]:=e)
     ELSE anyarray
   ENDIF;
get: (ARR, INDEX) -> ELEM =
  LAMBDA(a:ARR, i:INDEX):
    IF i < length(a) 
      THEN content(a)[i] 
      ELSE anyelem 
    ENDIF;

The meaning of these definitions is as follows:

Proving the Array Axioms

The adequacy of these definitions is stated by the following formula declarations:

length1: FORMULA
  FORALL(n:INDEX): length(new(n)) = n;

length2: FORMULA
  FORALL(a:ARR, i:INDEX, e:ELEM):
    i < length(a) => 
      length(put(a, i, e)) = length(a);

get1: FORMULA
  FORALL(a:ARR, i:INDEX, e:ELEM):
    i < length(a) => get(put(a, i, e), i) = e;

get2: FORMULA
  FORALL(a:ARR, i, j:INDEX, e:ELEM):
    i < length(a) AND j < length(a) AND i /= j =>
      get(put(a, i, e), j) = get(a, j);

These declarations state that the functions defined above obey the laws expected from arrays: length1 says that the request to allocate an array of length n indeed yields an array of this length; length2 states that putting an element into an array at a valid index does not change the length of the array; get1 says that consequently looking up this array at that index yields the element put there; get2 says that looking up this array at any other valid index yields the original element there.

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

Array Declarations

The strategy for proving formulas length1, length2, get1, and get2 is in all cases the same: we expand the constants by their definitions and apply the usual decomposition rules to get rid of the universal quantifier (FORALL) and of the logical connectives implication (=>) and conjunction (AND). The resulting proof states have only atomic formulas and the system can close these states automatically. We demonstrate this strategy in detail by the proof of the most complex formula get2 (the other proofs are analogous).

Selecting the command prove get2 from the menu of formula get2 yields the initial proof state6

Root State

This state labelled [adu] consists of a single goal [vv6] representing the content of formula get2. To expand in this proof state all occurrences of the defined constants to their values, we apply the command

expand length, get, put, content;

The command may be typed in the input area; alternatively, we may press the "Command List" button [, select from the popup menu the command template expand [], instantiate the template parameter and hit the "Enter" key. Even quicker, moving the mouse cursor over the label [vv6] reveals a formula menu from which the more special template expand [] in vv6 can be selected and instantiated; the resulting command performs instantiations only in this formula (which has in the current state with a single formula the same net effect).

In any case, the expansion yields a single child state [c3b] of the following form:

Scatter State

The state has a single goal [d5q] which is the result of the expansion of all constants in the parent's goal [vv6]. The formula is now a bit clumsy; we become unsure whether we have applied the right strategy and press the "Undo" button to undo the effect of the expansion and return to the parent state [adu] after discarding the generated state [c3b]. On the other hand, we were perhaps too anxious and should unperturbedly proceed our path; pressing the "Redo" button undoes the "Undo" and restores state [c3b]. In real-life proofs, is perhaps the most often pressed button to investigate different proving strategies; the existence of reassures us that the inadvertent use of this button cannot cause any harm.

Our next goal is to simplify the proof state by getting rid of the universal quantifier and of the logical connectives in the goal. The simplest way to achieve this is pressing the "Scatter State" button which applies various proving rules in order to scatter the current state to a number of simpler ones. A less aggressive strategy is pursued when pressing the "Decompose State" button which applies fewer rules in order to decompose the formulas in the current state yielding a single child state only. These two buttons are frequently applied in the initial stages of a proof; using has the advantage of quickly reducing a proof to interesting proof situations at the price of giving up explicit control of the proving strategy; it is safer to apply first to get a simplified version of the current state that can be investigated before scattering.

For our example the choice does not make any difference; pressing any of these buttons yields the dialog

Proof state [qid] is closed by decision procedure.
Formula get2 is proved. QED.
Save this proof and overwrite the previous one 
  (y/n)? y
Proof saved (browse file get2_index.xhtml).
Quit proof of formula get2 
  (use 'proof get2' to see proof).

This indicates that the remainder of the proof has been automatically completed and that the system has returned to declaration mode.

By selecting the command proof get2 from the menu of formula get2, we see the structure of the generated proof:

Proved State

From state [c3b] a single child state was generated that was automatically proved by the external decision procedure CVCL. Clicking on the corresponding node in the tree displays the state as follows:

Proved State

The state has four new constants a0, i0, j0, and e0 that replace the bound variables of the universally quantified goal [d5q] in the parent state. The resulting goal without quantifier was decomposed into three atomic formulas as assumptions and a single atomic formula (the equality of two conditional expressions) as a goal. This proof state was automatically closed by the decision procedure; for a human, the corresponding reasoning steps are (although not really difficult) tedious and error-prone.

This small example already illustrates a general strategy of how to work with the system: to decompose a proof and get rid of quantifiers until sufficiently much low-level knowledge in the form of atomic predicates is available such that a decision procedure can automatically close the proof state. The task of the human (and the difficulty in real-world proofs) is to expose this low-level knowledge by guiding the overall proof construction; the task of the system is to make this process as painless as possible and to take over (via an external decision procedure) low-level reasoning on builtin datatypes (such as NAT or ARRAY).

Proving the Extensionality Principle

We now turn our attention to another interesting property that we expect of arrays and that requires some more work from the user: we would like to prove the "extensionality principle" that two arrays are identical if and only if they have the same length and hold the same elements. For this purpose, we have first to introduce two axioms:

extensionality: AXIOM
  FORALL(a, b:ARRAY INDEX OF ELEM):
    a=b <=> (FORALL(i:INDEX):a[i]=b[i]);

unassigned: AXIOM
  FORALL(a:ARR, i:INT): 
    (i >= length(a)) => content(a)[i] = anyelem;

The first axiom states the extensionality principle on the type constructor ARRAY (this principle is not builtin into the system). The second axiom states that we may assume that the content of an array outside the valid index range is the definite but unspecified value anyelem such that content of two arrays of same length outside their index range is the same.

With the help of these axioms we are going now to prove the following formula:

equality: FORMULA
  FORALL(a:ARR, b:ARR):
    a = b <=>
      length(a) = length(b) AND
      (FORALL(i:INDEX): i < length(a) => 
        get(a,i) = get(b,i));

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

Formula Declarations

To support the understanding of the following presentation, we already show the structure of the proof that we are going to generate:

Proof Skeleton

By selecting "Prove equality" from the menu of formula equality, we encounter the initial state [adt] with two assumptions [1fm] and [gca] representing the axioms and the goal [hwd] representing the formula to be proved:

Proof Root State

We expand the constant definitions by executing command expand length, get, content yielding the state [cw2]:

Proof after Constant Expansion

Rather than investigating this state in depth, we aim to quickly push forward to the actual core problem by pressing the "Scatter" button which yields two child states [qey] and [rey]. While the state [qey] (corresponding to the "left to right" direction => of the proof) is automatically closed, we have to analyze state [rey] (corresponding to the "right to left"direction <=) in more detail:

Proof after Scattering

The state has a single goal [o4i] stating the equality of two arrays a0 and b0 (two new constants that were introduced for the universally bound variables in the original goal). The assumption [3sb] states that their first components (the array lengths) are equal. So what is missing to close the state is apparently the knowledge that also their second components (the array contents) are equal. By executing the command assume b_0.1 = a_0.1 we can introduce this knowledge as an assumption yielding a new state [zpt] (which is correspondingly automatically closed) and another state [1pt] with a goal [5bh] that represents the obligation is to prove this assumption:

Proof after Assumption

The proof of this goal apparently depends on the knowledge contained in the universally quantified assumptions [1fm], [3p3], and [ruq] such that we may be attempted to ask for an automatic instantiation of these formulas. Actually, the "Auto" button (respectively the command auto) implements this feature. However, when we press this button, the system executes for a while (if we get impatient, we may abort the execution by pressing the "Abort" button ) and finally terminates leaving the current state unchanged, which means that the system could not find the right instantiation to close the proof state.

Thus we have to to rely on our own wit and investigate the assumptions further. We decide that the knowledge expressed in assumption [1fm] for two ARRAY INDEX OF ELEM variables a and b actually applies to the two values a0.1 and b0.1 in our goal. We thus select from the menu of [1fm] the template instantiate [] in 1fm which we complete to the command instantiate a_0.1, b_0.1 in 1fm whose execution yields the following state:

Proof after Instantiation

This state contains an existentially quantified assumption [2sq]; by pressing the "Scatter" button (or just the more predictable "Decompose" ) we get the following state with assumption [lhm] that represents the version of the previous assumption [2sq] where a new constant i0 replaces the variable i:

Proof after Scattering

Lazy as can be, we again try automatic instantiation with the "Auto" button and this time get the now already familiar termination dialog

Proof state [iub] is closed by decision procedure.
Formula equality is proved. QED.
Save this proof and overwrite the previous one 
  (y/n)? y
Proof saved (browse file equality_index.xhtml).
Quit proof of formula equality
  (use 'proof equality' to see proof).

with the system returning to declaration mode. Selecting "Proof equality" from the menu of formula equality shows the proof skeleton already depicted above.

As we can see from this proof, the automatic instantiation of universally quantified assumptions (or, dually, existentially quantified goals) is not a "cure for all" strategy. The system applies a very simple strategy to instantiate such formulas by a limited number of suitable terms and then attempts to close the resulting proof state by a decision procedure that takes into account the formulas without quantifiers only; if the right combination of instantiations cannot be found (which gets more and more unlikely, the larger the number of quantified formulas in the proof state is), the user must provide (at least some of) the "right instantiations" on her own, which requires creativity and insight into the proof.

In the next section, we will investigate several proofs that require more such insight from the user.


Wolfgang Schreiner

3.3 A Program Verification3 Examples3.1 An Induction Proof3.2 A User-Defined Datatype