![]() | ![]() | ![]() | 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.
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:
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:
Selecting the command prove get2 from the menu of formula get2 yields the initial proof state6
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:
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:
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:
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).
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:
To support the understanding of the following presentation, we already show the structure of the proof that we are going to generate:
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:
We expand the constant definitions by executing command expand length, get, content yielding the state [cw2]:
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:
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:
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:
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:
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.
![]() | ![]() | ![]() | 3.2 A User-Defined Datatype |