A Basis for Formal Speci cation and Veri cation of Generic ... - CiteSeerX

0 downloads 0 Views 318KB Size Report
A software library in which most algorithms are generic can thus provide very ... Components in the ANSI/ISO C++ Standard Template Library (STL) 14] have the.
A Basis for Formal Speci cation and Veri cation of Generic Algorithms in the C++ Standard Template Library David R. Musser and Changqing Wang Computer Science Department Rensselaer Polytechnic Institute Troy, NY 12180 Abstract

Generic algorithms are algorithms designed to work with a variety of data structures. A software library in which most algorithms are generic can thus provide very extensive capabilities with a relatively small amount of source code. The high initial cost of applying formal methods to generic components becomes reasonable when amortized over the many di erent uses that can later be made. This paper is an attempt to provide a foundation for formal speci cation and veri cation of generic algorithms, paying particular attention to supporting the techniques by which generality is achieved in the C++ Standard Template Library. Axioms and inference rules are presented that support reasoning about essentially all the C++ language features and STL programming techniques used in the library's generic algorithms. Examples of applications to several simple STL generic algorithms are given. Categories and Subject Descriptors: D.2.2. [Software Engineering]: Tools and Techniques|software libraries; D.2.4 [Software Engineering]: Program Veri cation| correctness proofs; F.3.1 [Logics and Meanings of Programs]: Specifying and Verifying and Reasoning about Programs|speci cation techniques. General Terms: Speci cation, Standardization, Veri cation Additional Key Words and Phrases: Generic algorithms, software libraries, C++, templates, Standard Template Library. Partially supported by National Science Foundation Grant Number CCR{9308016 and subcontract CB0204 of SRI Contract MDA904-92-C-5186 with The Maryland Procurement Oce. 

i

1 INTRODUCTION

1

1 Introduction Formal methods of software speci cation and veri cation are not widely used, perhaps mainly because of the excessive level of expertise and e ort required to apply such methods to nontrivial programs. One type of software in which it appears the e ort is justi able is standard library routines, especially generic algorithms and data structures, which are designed to be usable in many di erent contexts. The high initial cost of applying formal methods to generic components becomes reasonable when amortized over the many di erent uses that can later be made. (Such uses may require additional veri cation e ort, but it is minimal relative to the original veri cation cost.) Components in the ANSI/ISO C++ Standard Template Library (STL) [14] have the potential to become some of the most widely used software in existence, since C++ itself has become one of the most popular programming languages and all STL components are generic. In particular, STL contains more than one hundred generic algorithms for commonly needed operations such as searching, sorting, copying, merging, and transforming of data. These algorithms are applicable to data stored in many di erent kinds of data structures. Applying formal methods to C++ programs presents formidable technical challenges. The language is complex in the sense of having many features that are dicult to treat formally|including pointers, reference parameters, expressions with side-e ects, and unspeci ed evaluation order|and in having few hard restrictions on how these features can interact. Fortunately, although STL makes use of such features, including extensive use of pointers and side-e ects, it uses them in a very restrained and technically elegant manner, making formal characterization tractable. In this paper we attempt to provide a foundation for formal speci cation and veri cation of generic software components, paying particular attention to supporting the techniques by which generality is achieved in STL. We are developing formal models of the most fundamental notions on which STL is based, including generic algorithms, generic data containers, and the components called iterators that are used to connect algorithms and containers. In this paper we concentrate on modeling generic algorithms and iterators. In order to properly characterize the way STL generic algorithms are coded in C++, we adapt and extend some of the existing techniques for formalization of programming constructs, so that the relevant C++ features can be handled. Although there has been previous work on formalizing C++ in its entirety, such as [16], we do not need such a complete and complex characterization to deal with STL generic algorithms, because of the aforementioned restrained way in which the algorithms are expressed. Although the examples in this paper are based on formal modeling of C++ code, it is important to recognize that the most fundamental ideas of STL are not speci c to C++; see for example [9], in which many of the same ideas are manifested in Ada.

1 INTRODUCTION

2

With generic algorithms, formal methods can help with more than just the issue of correctness. One of the questions that may be most in doubt about a generic algorithm is just how general the algorithm is; that is, how widely applicable it is. In attempting to write a formal speci cation of a generic algorithm, we can seek to put as few conditions on its inputs as possible while still being able to obtain a useful result. Often in this process generic algorithms can be seen to be even more widely applicable than one might initially have thought. But it then becomes all the more desirable to prove the correctness of the algorithm, so that the full degree of conjectured generality is mathematically con rmed. As a simple example, and one that we take up in more detail later, consider the STL copy algorithm. This algorithm copies a sequence of values in a container to other locations in the same or another container. The range of locations to be copied is indicated in STL with objects called iterators, which are a generalization of C++ pointers. We discuss the actual STL algorithm later; for the moment, let us simplify matters somewhat by considering a version that just uses pointers. The source sequence is indicated using two pointers, f and b (\ rst" and \beyond"), pointing to the rst location and one beyond the last location to be copied. The destination is indicated with a single pointer, r (\result"), pointing to the rst location into which the values are copied. Here is how such a copy operation might be coded as a template function in C++: template T  copy (T  f, T  b, T  r ) f

while f r return r (

!=

b)

++ =

f ++;

;

g This code1 is not as general as the actual STL generic algorithm, which we discuss later, but even this version has a variety of uses, e.g., int a [100], b [100]; == . . . code to initialize array a copy (a, a +100, b ); == copy all of array a to array b copy (a +1, a +100, a ); == shift a[1],. . .,a[99] left one position copy (b +10, b +20, b ); == shift b[10],. . .,b[19] left ten positions copy (a, a +10, a +10); == copy a[0],. . .,a[9] to a[10],. . .,a[19] By closely examining the way copy is coded, we can informally determine that each of these uses of copy works (does what the comment says), though another quite similar use does not: copy (a, a +10, a +1); == shift a[0],. . .,a[9] right one position ??? 1

The code examples in this paper are identical to or very similar to the code in [15].

1 INTRODUCTION

3

This doesn't work because source positions are overwritten before they can be copied. In the previous examples of left shifting, some source positions are overwritten but only after they have been copied. (STL has another copying operation, called copy backward , that proceeds through the source locations from b ?1 to f , allowing it to be used for shifting to the right.) We thus see that a formal speci cation of copy must give the conditions under which a correct copy is obtained, and we might also like to know the conditions under which the source locations are left intact (as in the rst, third and fourth examples above). In the ANSI/ISO STL document [14], the speci cation is given informally and operationally, saying that for each non-negative integer n < b ? f , the assignment (r + n ) = (f + n ) is performed. Note that this speci cation does not explicitly state that the assignments are performed in the order n = 0; 1; : : : ; b ? f , though one could infer that this is the intention from the later discussion of copy backward . But even if that fact were added, the speci cation would still be dicult to use in most formal reasoning frameworks, which are usually axiomatic or denotational rather than operational. Moreover, this speci cation is incomplete, and [14] goes on to specify the condition under which the copy is valid. In stating this condition, the document uses a convenient notation for a range of pointers (more generally, iterators), namely [f, b ] denotes the sequence of pointers f , f +1, : : : , b . Here, as in all uses of this range notation in the STL document and in this paper, there is an implicit assumption that the range is valid, which means that there is some nonnegative integer n such that f + n == b .2 With the range [f, b ] both endpoints are included, but most STL algorithms only process up to, but not including, the endpoint b , so it is convenient to use the conventional mathematical notation for a \halfopen interval," namely [f, b ), for the sequence f , f +1, : : : , b ?1. The additional speci cation is then:  the result of copy is unde ned if r is in the range [f, b ). But this statement still does not characterize the condition under which the source locations are left intact. This condition could be stated in the same informal language as:  if, in addition, r + (b ?f ) is not in the range [f, b ), then the sequence of locations referred to by the range [f, b ) remains unchanged. If such speci cations are to be used in formal reasoning about copy , a suitable formal logic is needed, including a precise notation and semantic rules for using it for establishing that a particular implementation satis es the speci cation. For example, we should be able to prove that the implementation of copy given above satis es a formalization of the To avoid inconsistencies in notation, we use == in this paper for the equality relation, inequality relation, and = for assignment, which is the way these symbols are used in C++. 2

!=

for the

1 INTRODUCTION

4

speci cation. The main goal of this paper is to develop such notations and semantic rules for speci cation and formal veri cation of generic algorithms, with direct application to STL generic algorithms. The proposed formalization is illustrated in this paper only on copy and a couple of other relatively simple generic algorithms, but we believe it provides all the basic machinery needed to handle all of the STL generic algorithms.

1.1 Overview of STL

The C++ Standard Template Library provides a set of container classes, template algorithms and other components designed to work together to produce a wide range of useful functionality. STL contains ve major kinds of components: containers, algorithms, iterators, function objects, and adaptors. The container classes include those most widely used, such as vectors, lists, deques, sets, and maps. They de ne objects that store collections of other objects. Most algorithms in STL are generic; they can work with virtually all containers as long as the containers provide suitable iterators. Iterators are pointer-like objects that are used by STL algorithms to traverse through the objects in a container. A function object encapsulates a function in an object for use by other components. For example, di erent function objects for comparing two elements can be passed to a generic sorting algorithm to cause it to sort in ascending or descending order. Adaptors are used to change the interfaces of components so that they can be combined with other components. For example, a queue adaptor changes the general list or deque interface to a more restricted interface providing only the operations of a queue, so that users of the container cannot accidentally use non-queue operations (which would make it dicult to change the representation later). Some generic algorithms in STL can be combined with all STL containers through iterators, under certain constraints. For example, the nd algorithm for linear search in STL can be used with vectors, lists, arrays, etc. vector a ; list b ;

int c == Initialize a, b, c == Find the rst occurrence of 7 in vector a, list b, array c int i nd a begin a end int j nd b begin b end int k nd c c [100];

...

=

( .

(),

.

(), 7);

=

( .

(),

.

(), 7);

=

( ,

+ 100, 7);

A more general form of the copy algorithm considered earlier can also be combined, via iterators, with all STL containers. To be able to specify such generic algorithms formally and prove that certain implementations satisfy the speci cations, we develop in the next

2 FORMALIZATION OF STL ITERATORS

5

two sections a formal logic sucient to deal with both the C++ language issues and the key concepts by which generality is achieved in STL.

2 Formalization of STL iterators In this paper we formalize only as much of STL iterator concepts as is needed to deal with simple examples of STL generic algorithms. A fuller treatment using an axiomatic approach will be given in a separate paper (see also [10] for a speci cation of iterator properties using a model-based approach).

2.1 References and the evaluation and dereferencing operators

STL iterators generalize C++ pointers, and operations on iterators as well as pointers involve the C++ notion of references. A reference is a name for a data object. A reference can be used as an lvalue, meaning the data object itself, or an rvalue, meaning the data value stored in the data object. Such context dependence makes it dicult to use formal rules of inference such as substitution rules, so we instead use a notation that makes explicit the operation of retrieving the data value stored in a data object:  If r is a reference, let :r be the data value stored at r. The dot operator (which is not used in this way in C++) is called the evaluation operator. Thus wherever a reference r is treated in C++ code as an rvalue, we write :r, but when it is used as an lvalue, we write just r. In the formal language used in this paper a reference expression may be either  a simple identi er (a character string made up of letters, numbers, or underscores, beginning with a letter or underscore);  an expression of the form i , where i is an iterator-valued expression (where an iterator can be a pointer); or,  an expression of the form r , where r is a range expression. The use of  with pointers corresponds to the C++ dereferencing operator (or indirection operator), which converts a pointer value into a reference to the object pointed to. The type of object pointed to is called the value type of the pointer. When  is applied to an STL iterator, a reference also results, but not necessarily in such a simple way as with pointers. If i is an iterator object, then i is a reference, but it may be distinct from the object that i points to. This is the case, for example, for STL list iterators, which refer to list node objects containing previous and next pointers in addition

2 FORMALIZATION OF STL ITERATORS

6

to the space for the data; i returns a reference to the data storage, not the whole object, and this reference is a di erent address from one for the whole node if the data storage is not allocated at beginning of the node. When  is applied to an iterator, the type of object obtained is called the value type of the iterator; in the case of the list node object, the value type is the type of the data eld.

Axiom 1 If a and b are iterator objects and a

!=

b, then a

!=

b.

Note that this says that the references produced by  are di erent for di erent iterator objects. The data objects at those references might be the same: possibly .a == .b . Also, if i and j are distinct iterator variables, the iterator objects they hold could be equal: .i == .j . In this case we also obviously have .i == .j and ..i == ..j . As further illustration of these conventions, let's express a few simple examples of C++ code using explicit evaluation and dereferencing. Let p and q be pointer variables, i and j be iterator variables with value type T , and x and y be variables of type T . C ++ code x =y x = p p = x q =p x = i i = x

Formal Model x = .y x = ..p .p = .x q = .p x = ..i .i = .x

Now consider various versions of a swap operation:

void swap T x T y f (

g

&

T t = x; x = y; y = t;

,

&

)

== t = . .x; == .x = . .y; == .y = .t

void swap via iterators (iterator x, iterator y ) f T t = x ; == t = ..x x = y ; == .x = ..y y = t ; == .y = .t g

2 FORMALIZATION OF STL ITERATORS

7

void swap via references to iterators iterator x iterator y f T t x == t = .. .x x y == . .x = .. .y y t == . .y = .t g (

=

&

,

&

)

;

=

;

=

;

2.2 The ++ operator and range notation

The main purpose of iterators is to allow access in a uniform manner to values stored in containers of di erent kinds, so that algorithms can be written to work with more than one kind of container. In STL the most basic such \visitation" capability is provided via successive applications of operator ++, just as with ordinary C++ pointers. An iterator b is called reachable from an iterator a if there is a nite sequence of applications of operator ++ to a that makes a == b . If b is reachable from a , then [a, b ) is the sequence a; a + 1; a + 2; : : : ; a + (n ? 1), in which a + k is de ned by a + 0 == a and a + k == + + (a + (k ? 1)); k == 1; : : : ; n, where a + n == b. [a, b ) is called an iterator range; note that b is not included in the range. The integer n is called the distance from a to b, written distance (a, b ). For any iterator a , the range [a, a ) is the empty sequence (distance (a, a ) == 0). Note that the + operator used in this de nition is not actually supplied by all iterator classes; if it is de ned for a particular iterator class, it is assumed to be de ned as above, and otherwise we de ne it as above in terms of ++ just for use in speci cations and proofs. The dereferencing and evaluation operators are extended to ranges by:

[a, b )

==

a, (a +1), (a +2),

. . .,

(a +(n ?1))

 a b ) == .a, .(a +1), .(a +2), . . . .(a + (n ?1)) To express these properties as formal axioms, let # be an operator on nite sequences that takes a value x of type T and a sequence s of T values and pre xes x to s . Then: . [ ,

Axiom 2 If a b is a valid range and a [ ,

a b)

[ ,

)

==

a

#

a

[ +1,

!=

b,

b ),

[a, b ) == a # [a +1, b ) .[a, b ) == .a # .[a +1, b )

Axiom 3 Let empty T be the empty sequence of type T; then for any iterator a with value type T, [a, a ) == empty T.

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

8

2.3 InputIterators and OutputIterators

The STL copy function is more general than the version given earlier; instead of working only with pointers, it works with two very general classes of iterators called InputIterators and OutputIterators . These types are assumed only to have ++ and  operators de ned, for which the range notation and axioms we have previously discussed hold.3 Note that these axioms can be satis ed not only by pointer types T , as indicated in the earlier discussion of copy , but also by linked lists, by de ning ++ to be the operation of advancing to the next node in the list using the address stored in a link eld of the node. Our axioms about iterator ranges generalize some of the reachability properties of linked structures discussed in [11], but they do not have the complexity of other axioms of that paper that are speci c to linked structures.

3 Axioms and inference rules for imperative programs We adopt a notation and set of axioms and inference rules similar in spirit to Hoare's or Dijkstra's axiomatic systems [3, 1], but substantially di erent in details. As in Hoare's logic, we use triples fQgS fRg, where Q and R are predicates and S is a program statement, as the primary notation for specifying the meaning of program statements and reducing claims about them to predicate logic. Q is called a precondition and R a postcondition for S , and the meaning is that if a program state satis es the precondition and an execution of S starting in that state terminates, then the resulting program state satis es the postcondition. The axioms and rules of inference of Hoare's logic generally favor reduction of a Hoare triple fQgS1 ; S2 ; : : : ; S fRg concerning a sequence of statements by rst eliminating the last statement S , principally because Hoare's rule for assignment statements is a backward substitution rule. Our rules are based instead on forward symbolic execution. Although these rules are somewhat more complicated than backward substitution rules, they are no more dicult to use in practice; they appear in fact to be easier to use in the presence of variable aliasing, which is, as we shall see, a major issue with C++ programs.4 Another major di erence from most formulations of Hoare logic is that we allow expressions with side-e ects. This requires a more detailed treatment of memory states than in traditional versions of the logic. We formally model the state of memory as a set of (reference, value) associations. We characterize memory states with predicates (as in all n

n

The main distinction between the two categories of iterators is that if i is an InputIterator then it must be possible to use i as an rvalue but not necessarily as an lvalue, whereas the opposite is true of an OutputIterator . 4 Although the use of symbolic execution in software veri cation goes back at least to one of the earliest program veri cation systems [7], and forward assignment axioms are sometimes mentioned in the literature (e.g., see [2], we are not aware of any axiomatic system with this approach as its basis. 3

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

9

other formulations of Hoare logic), but we use both unevaluated and evaluated references in the predicates. For example, using the evaluation operator, the predicate :a == a0 ^ :b == b0 ^ a0 < b0 represents all states in which reference a has the associated value a0 , reference b has the associated value b0 , and a0 < b0 , where < is an operator de ned on the type of values a0 and b0. Those clauses of predicates that are equations giving associations between references and values, such as .a = a0 , are called memory clauses.

3.1 Forward assignment axiom

The following triple speci es that an assignment statement x = .x + .y (which corresponds to a statement that would be written in C++ as x = x + y ) changes the memory association of reference x by adding to its current value the value associated with y : f.x == x0, .y == y0 g x

x

= .

y

+ . ;

f.x == x0 + y0, .y == y0 g As is done here, we sometimes separated conjuncts of predicates with commas instead of ^. In general, assignment statements are characterized in this paper by \forward" axioms and rules of inference. The simplest case is an assignment statement of the form x = E , where x is a reference and E is a side-e ect free expression, i.e., evaluation of E involves no change of state (we remove this restriction later). The forward axiom of assignment for this case is f:x == u0 ^ Qg x = E ; f:x == E 0 ^ Qg where Q is a predicate that contains no occurrence of :x, E 0 is an expression free any occurrence of the evaluation operator, and :x == u0 ^ Q  E == E 0 . This rule places a strong requirement on the precondition Q: it must contain memory clauses sucient to eliminate all occurrences of the evaluation operator in E . The expression E 0 thus obtained is said to be fully-evaluated. If this requirement on Q is not met, the rule cannot be applied, and the goal must be reformulated with a stronger precondition. If the goal was itself derived by reduction from another goal, that goal has to be reformulated, and so on. Note the presence of Q in the postcondition, which means that no other reference besides x can have its value changed.

3.2 Declaration axiom

In C++ functions, as in procedures or functions of most procedural languages, it is possible to declare local variables, so we must provide axioms for dealing with such declarations.

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

10

We also make explicit some uses of declarations that are implicit in C++, such as treating function parameters as locally declared variables initialized with the actual parameter values. In all cases in this paper we use a form of declaration that includes initialization, for which the axiom is similar to the assignment axiom:

fQg T x = E ; fQ+ ; :x == E 0 g where Q+ is Q with all occurrences of x replaced by x1 and all occurrences of x (which may have been introduced by previous applications of this rule) replaced by x +1 , with all replacements being made simultaneously; and E 0 is an expression free of any occurrence of the evaluation operator, such that Q+  E == E 0 . This axiom again assumes E is side-e ect free. T is a type and it is assumed that E has the same type, T .5 i

i

3.3 Release axiom

In C++, locally declared variables are implicitly destroyed upon exit from a function. In this paper we express this action explicitly using a release statement:

fx1 == u1 ; : : : ; x == u ; Qg release x1; : : : ; x ; fQ? g where Q? is Q with x1 replaced by x and x replaced by x ?1 ; j == 2; 3; : : :; i == 1; : : : ; n; n

i

n

i

n

j i

with all replacements being made simultaneously.

j i

3.4 Generalization of the assignment axiom

A slightly more general form of the assignment rule allows the left-hand side of the assignment statement to be an expression V that evaluates, without side-e ects, to a reference x, f:x == u0; Qg V = E ; f:x == E 0 ; Qg where Q contains no occurrence of :x, and Q  V == x ^ E == E 0 , where E 0 is an expression free of any occurrence of the evaluation operator. Note that x itself may be an expression of the form i where i is an iterator. In general we do not make explicit all of the requirements of type checking that must be included in a complete formulation of the axiomatic system; but we do assume that all code that we formalize obeys the standard C++ type checking rules. Other statically checkable legality rules such as the restriction against multiple declarations of the same variable in the same block are also assumed to be observed. One way to put it is that \we do not try to verify any code that doesn't compile." 5

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

11

3.5 Application to a swap operation

As an example of the use of the axioms given so far, consider the rst version given earlier of a swap operation: template

void swap T x T y f T t x x y y t g (

=

&

;

,

&

)

=

;

=

;

In the formal notation of this paper, this can be expressed as the following de nition: swap (x0, y0 ) == T & x = x0 ; T & y = y0 ; T t = . .x ; .x = . .y ; .y = .t ; release x, y, t ;

Lemma 1 For any references a and b to type T, any values p0 and q0 of type T, and any predicate Q that is free of occurrences of .a and .b, f.a == p0, .b swap (a, b ); f.a == q0, .b

==

q0, Q g

==

p0, Q g

Proof. We take the lemma statement to include the possibility that a == b (aliasing), and break the proof into two cases accordingly. In each case we assume that a, b, p0 , q0 and Q are free of occurrences of x , y , and t , so that there is no need to rename variables in the precondition, but note that if there were such occurrences the declaration and release rules would perform the necessary renaming, so the proof can be extended to those cases also. Case 1: a != b .

f.a

== p0, .b swap (a, b ); f.a == q0, .b

==

q0, a

==

p0, Q g

!=

b, Q g

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

12

reduces, using the de nition of swap , to f.a == p0, .b == q0, a != b, Q g T & x = a; T & y = b; T t = . .x ; .x = . .y ; .y = .t ; release x, y, t ; f.a == q0, .b == p0, Q g

The declaration rule reduces this goal to f.a == p0, .b == q0, a != b, .x T t = . .x ; .x = . .y ; .y = .t ; release x, y, t ; f.a == q0, .b == p0, Q g

==

a, .y

==

b, Q g

Using the precondition we can deduce . .x == .a == p0 , so the next subgoal is f.a == p0, .b == q0, a != b, .x == a, .y == b, .t == p0, Q g x = . .y ; y = .t ; release x, y, t ; f.a == q0, .b == p0, Q g .

.

Applying the generalized inference rule for assignment, and noting that .x == a and . .y the memory clause .a == p0 is replaced by .a == q0 to obtain f.a == q0, .b == q0, a != b, .x == a, .y == b, .t == p0, Q g .y = .t ; release x, y, t ; f.a == q0, .b == q0, Q g

and similarly, we obtain f.a == q0, .b

== p0, a != b, .x release x, y, t ; f.a == q0, .b == p0, Q g

which, by the release rule, reduces to .a == q0, .b implies .a == q0, .b

==

p0, a

==

p0, Q

!=

b, Q

==

a, .y

==

b, .t

==

p0, Q g

b == q0 ,

== .

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

13

which is a valid formula of rst order logic. Case 2: a == b . It follows that p0 == q0 , and we simplify the precondition and postcondition accordingly: f.a == p0, a == b, Q g swap (a, b ); f.a == p0, Q g

Using the de nition of swap , we obtain

f.a

== p0, a == b, Q g T & x = a; T & y = b; T t = . .x ; .x = . .y ; .y = .t ; release x, y, t ; f.a == p0, Q g

which becomes f.a == p0, a

b, .t

==

p0, Q g

and using the assignment rule this reduces to f.a == p0, a == b, .x == a, .y == a, .t

==

p0, Q g

==

x = . .y ; .y = .t ; release x, y, t ; f.a == p0, Q g

b, .x

==

a, .y

==

.

release x, y, t ; f.a == p0, Q g

Using the release rule, this becomes .a == p0, a == b, Q implies .a == p0, Q

which again is a trivially valid formula. This completes the proof of the lemma.

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

14

3.6 Subexpression evaluation rule

C++ expressions can have side e ects; for example, what we have referred to as an \assignment statement" is actually an expression that can be embedded in a larger expression, as in

if x ((

=

y

+

z) >

u

7)

=

v ? x;

in which the assignment operator both changes the value associated in memory with x and returns the sum of y and z for use as the left-operand of >. If more than one side-e ecting subexpression is present, as in

if i ((

=

y

+

z ) > (j

u

= 7))

=

v ? i

the results might depend on the order in which subexpressions are evaluated. In this example, if i == j there is such a dependence, otherwise it doesn't matter. But the C++ language standard leaves the order of evaluation unspeci ed. Under such circumstances, proving correctness of a program requires showing that the speci ed result is achieved no matter what evaluation order is used. For this purpose we introduce the following inference rule, which requires that all possible orders of subexpression evaluation be considered:

fQg E fRg reduces to a set of subgoals consisting of all formulas

fQg T  = E1; E 0 fRg such that E1 is a proper subexpression of E , all of the proper subexpressions of E1 are side-e ect free, and E 0 is the result of replacing one occurrence of E1 in E by : . Here 

is a reference introduced by the inference rule into the subgoal; the chosen name must be distinct from any other references in the goal formula. The type T is the type of E1 . For example

fQ g if ((.i

y

= .

reduces to the pair of goals fQ g int t1 = (.i

fQ g int t1

j

= ( .

z > (.j

+ . )

y

= .

= 7);

= 7))

u

v ? .i ; fRg

= .

z if (.t1 > (.j

if  i (( .

y

= .

v ? .i ; fRg

u

= .

z > .t1 ) u

= .

+ . );

= 7))

+ . )

where t1 is a reference name that does not appear in Q or R .

v ? .i ; fRg

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

15

Such a rule of inference can of course be a source of combinatorial explosion in the number of proof cases to be dealt with, so it is wise to avoid expressing programs using expressions with many side-e ecting subexpressions. Fortunately in STL code expressions typically have at most two side-e ecting subexpressions, such as in

j ++

=

i ++;

which, using our notational conventions, is

(j ++)

i

= . ( ++);

Like the assignment operator, the ++ operator has side-e ects that must be taken into account in proofs. We can specify these side-e ects and the return value of a side-e ecting function by writing a Hoare-triple about a declaration in which a reference is initialized using a call of the function. For example, the C++ post x ++ operator on ints or pointers can be speci ed with

f.i

==

u, P g T i1

i ++; f.i

=

==

u

i1

+ 1, .

==

u, P g

and the pre x ++ operator obeys

f.i

==

u, P g T & r1

i f.i

= ++ ;

==

u

r1

+ 1, .

==

i, P g

where T is the type of i . For calls in which the returned value is not used, the following simpler speci cations can be used: f.i == u, P g i ++; f.i == u + 1, P g

f.i

==

u, P g

i f.i

++ ;

==

u

+ 1,

Pg

We adopt these speci cations as axioms for iterators; that is, all iterator classes must de ne pre x and post x ++ consistently with these axioms. These axioms (though stated less formally) are a key part of the requirements placed on iterators in the STL standard [14], and all iterator classes de ned in STL satisfy these axioms. We illustrate the use of the subexpression evaluation rule in proving the following lemma (which in turn is applied later in the proof of correctness of a copy algorithm):

Lemma 2 For any predicate P that is free of any occurrence of f f0 and r, . , .

f.f == f0, .f0 == x0, .r == r0, P g (r ++) = .(f ++); f.f == f0 + 1, .f0 == x0, .r == r0

r0

+ 1, .

==

x0, P g

.

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

16

Proof: Applying the subexpression evaluation rule produces two subgoals that di er de-

pending on which use of ++ is assumed to be executed rst: f.f == f0, .f0 == x0, .r == r0, P g T r1 = r ++; .r1 = .(f ++); f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r0 == x0, P g or f.f == f0, .f0 == x0, .r == r0, P g T f1 = f ++; (r ++) = .f1 ; f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r0 == x0, P g The rst of these subgoals can be reduced by making use of the previously given speci cation of post x ++, f.i == u, P g T i1 = i ++; f.i == u + 1, .i1 == u, P g which can be adapted by substitution to f.r == r0, P g T r1 = r ++; f.r == r0 + 1, .r1 == r0, P g Applying the adaptation to the rst subgoal, we obtain f.f == f0, .f0 == x0, .r == r0 + 1, .r1 == r0, P g .r1 = .(f ++); f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r0 == x0, P g The subexpression evaluation rule then applies again to produce f.f == f0, .f0 == x0, .r == r0 + 1, .r1 == r0, P g T f1 = f ++; .r1 = ..f1 ; f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r0 == x0, P g which reduces to f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r1 == r0, .f1 == f0, P g .r1 = ..f1 ; f.f == f0 + 1, .f0 == x0, .r == r0 + 1, .r0 == x0, P g and nally to .f == f0 + 1, .f0 == x0, .r == r0 + 1, .r1 == r0, .f1 == f0, .r0 == x0, P implies .f == f0

f0 == x0, .r == r0 + 1, .r0 == x0, P which is a valid formula of rst order logic. A similar reduction of the other subgoal completes the proof of the lemma. + 1, .

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

17

3.7 General form of speci cations of functions with side-e ects

In general, if a function f has side-e ects, the e ect on memory of a call of the function can be speci ed with a formula of the form, f:x1 == u1; : : : ; :x == u ; A0 ; Qg T r = f (a1; : : : ; a ); f:x1 == v1 ; : : : ; :x == v ; C0 ; Qg where A0 is a speci c predicate that, together with the equations :x1 == u1 ; : : : ; :x == u , serves as a precondition for the function call, and C0 is another speci c predicate that, together with the equations :x1 == v1 ; : : : ; :x == v , serves as a postcondition. Here a1; : : : ; a are references; x1 ; : : : ; x are reference expressions, possibly including one or more of the arguments a1 ; : : : ; a of f ; u1 ; : : : ; u are logical variables; v1 ; : : : ; v are logical variables or expressions; and C0 is a predicate that constrains v1 ; : : : ; v and :r. Q is a predicate variable for which any predicate free of occurrences of :x1 ; : : : ; :x or r may be substituted; its presence in both the precondition and the postcondition ensures that no variables other than x1 ; : : : ; x can be changed. v1 ; : : : ; v can include logical variables that are not present in the precondition; these are considered to be existentially quanti ed, and their purpose is to leave some values unspeci ed. A simpler form of the rule can be used if f does not return a value, or if a call is being considered for which the return value is not used: f:x1 == u1 ; : : : ; :x == u ; A0 ; Qg f (a1; : : : ; a ); f:x1 == v1; : : : ; :x == v ; C0 ; Qg n

n

n

k

n

n

n

n

n

n

k

n

k

n

n

n

n

n

n

n

n

k

n

3.8 Adaptation rule

A speci cation of a function with side-e ects f:x1 == u1; : : : ; :x == u ; A0 ; Qg T r = f (a1 ; : : : ; a ) f:x1 == v1; : : : ; :x == v ; C0; Qg can be adapted to be a speci cation of a particular substitution instance Tr = (f (a1 ; : : : ; a )), where  is a substitution fb1 =a1 ; : : : ; b =a ; r1 =rg: f:x01 == u01; : : : ; :x0 == u0 ; A00 ; Qg T r1 = f (b1; : : : ; b ); f:x01 == v10 ; : : : ; :x0 == v0 ; C00 ; Qg where the primed expressions are the result of applying  to the corresponding unprimed expressions. For example, the speci cation f.i == u, P g T i1 = i ++; f.i == u + 1, .i1 == u, P g can be adapted, using the substitution fk=i; a=ug, to f.k == a, P g T i1 = k ++; f.k == a + 1, .i1 == a, P g n

n

k

n

n

k

k

n

n

k

k

n

n

3 AXIOMS AND INFERENCE RULES FOR IMPERATIVE PROGRAMS

18

3.9 Inference rules for if-statements The inference rule for an if-statement

fQg if (B ) S 1; else S 2; S fRg has two subgoals

fQ ^ B 0gS 1; S fRg

and

fQ; not(B 0)gS 2; S fRg where B 0 is a fully-evaluated expression such that Q  B == B 0 . A similar rule applies to one-branch if-statements.

3.10 Inference rules for while-statements

A while-statement cannot be eliminated by automatic application of axioms or inference rules. Essentially, we handle a while-statement the same way that we handle a function with side-e ects, by stating and proving a lemma about its e ect on memory. The lemma is proved by induction, just as we would prove a lemma about a recursively-de ned function. Note that this approach is di erent from the more common method of inventing an invariant assertion for the while-loop, which asserts a relationship among memory references that is maintained by the loop as the iteration progresses. Our approach is more akin to the subgoal induction method [8]. In analogy to the general rule for functions with side-e ects, lemmas about whilestatements have the general form

f:x1 == u1; : : : ; :x == u ; A0 ; Qg while (B ) S ; S 1 f:x1 == v1 ; : : : ; :x == v ; C0; Qg where A0 is a speci c predicate that, together with the equations :x1 == u1 ; : : : ; :x == u , serves as a precondition for the while-statement, and C0 is another speci c predicate that, together with the equations :x1 == v1 ; : : : ; :x == v , serves as a postcondition. Here x1; : : : ; x are reference expressions; u1; : : : ; u are logical variables; v1 ; : : : ; v are logical variables or expressions (possibly including some variables that are not present in A0 or Q or among u1 ; : : : ; u ); and C0 is a predicate that constrains v1 ; : : : ; v . n

n

n

n

n

n

n

n

n

n

In proving the lemma, the following inference rule is used:

fQg while (B ) S ; S 1 fRg reduces to two subgoals

fQ ^ not(B 0 )g S 1 fRg

n

n

n

4 SPECIFICATION AND PROOF OF THE COPY GENERIC ALGORITHM

19

and

fQ ^ B 0g S ; while (B ) S ; S 1 fRg where B 0 is a fully-evaluated expression such that Q  B == B 0 .

The rst of these subgoals is the basis case of a proof by induction, and the second is the inductive step. After other rules or axioms have been used to eliminate the statement S , the induction hypothesis is used to eliminate the while statement and the following statement S 1. An example of such a proof is given in the next section.

4 Speci cation and proof of the copy generic algorithm Speci cation and proof of even a simple generic algorithm, such as the STL copy algorithm, illustrates most of the concepts, axioms and inference rules developed in the previous section. In STL the function prototype of copy is template OutputIterator copy (InputIterator f, InputIterator b, OutputIterator r );

and its speci cation can be written formally as: for some s1 ,

f.[f0, b0 )

== s, not (r0 in [f0, b0 )), Q g OutputIterator r1 = copy (f0, b0, r0 ); f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, [f0, b0 ) == s1, Q g

Note that s1 appears only in the postcondition as an existentially quanti ed variable; there is no information about it in the precondition. This is one way of formalizing our earlier informal statement that the e ect on the source range is unde ned. If we want the source range to be unchanged by the copy operation, we must add to the precondition that b0 is not in [r0, r0 + distance (f0, b0 )), which, in conjunction with the other preconditions, means the source and destination ranges do not overlap. Both of these speci cations can be shown to be theorems about the following implementation, from [15], template OutputIterator copy (InputIterator rst, InputIterator beyond, OutputIterator result ) f while ( rst != beyond ) result ++ =  rst ++; return result ;

g

4 SPECIFICATION AND PROOF OF THE COPY GENERIC ALGORITHM

20

which we model with the following de nition: OutputIterator r1 = copy (f0, b0, r0 )) InputIterator f = f0 ; InputIterator b = b0 ; OutputIterator r = r0 ; while (f != b ) (r ++) = .(f ++); r1 = .r ; release f, b, r ;

(

==

We show the proof here only of the theorem in which the minimal precondition is assumed: Theorem 1 For some s1 , f.[f0, b0 ) == s, not (r0 in [f0, b0 )), Q g OutputIterator r1 = copy (f0, b0, r0 ); f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, b0 ) == s1, Q g

Proof. Substituting the de nition, the goal reduces, using the declaration rule, to the following goal, which we state as a lemma and prove by induction: Lemma 3 For some s1 , f.[f0, b0 ) == s, not (r0 in [f0, b0 )), .f == f0, .b == b0, .r == r0, Q g while f r (

!=

b)

f r1 = .r ; release f, b, r ; f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, b0 ) ( ++) = . ( ++);

==

s1, Q g

Proof. By the while-rule, this goal reduces to two subgoals Basis case: For some s1 , f.[f0, b0 )

== s, not (r0 in [f0, b0 )), .f not (f0 != b0 ), Q g r1 = .r ; release f, b, r ; f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, b0 ) == s1, Q g

==

f0, .b

==

b0, .r

==

r0,

4 SPECIFICATION AND PROOF OF THE COPY GENERIC ALGORITHM Using the assignment axiom and the release axiom, we obtain .[f0, f0 ) == s, not (r0 in [f0, b0 )), not (f0 != b0 ), .r1 implies .r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, f0 ) == s1, Q

Using the equality property not (f0 != b0 ) == (f0 distance (f0, f0 ) == 0, we next obtain

==

==

21

r0, Q

b0 ) and the distance property

.[f0, f0 ) == s, not (r0 in [f0, b0 )), f0 == b0, .r1 == r0, Q implies .r1 == r0 + 0, .[r0, r0 + 0) == s, .[f0, f0 ) == s1, Q

This follows from r0 + 0 == r0 and the fact that [f0, f0 ) and [r0, r0 ) are the empty sequence. (In this case, s1 is determined; it is the same as s , the empty sequence.)

Inductive step: For some s1 , f.[f0, b0 ) f0

!=

==

b0, Q g

s, not (r0 in [f0, b0 )), .f

==

f0, .b

==

b0, .r

==

r0,

(r ++) = .(f ++); while (f != b ) (r ++) = .(f ++);

r1 = .r ; release f, b, r ; f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, b0 ) == s1, Q g

10

Using f0 != b0 , we deduce that s is nonempty and so can be written s == x0 # s0 for some x0 and s0 , and .[f0, b0 ) == s can be written .f0 == x0 and .[f0 +1, b0 ) == s1 . Also note that not (r0 in [f0, b0 )) implies not (r0 in [f0 +1, b0 )). By Lemma 2, the goal then reduces to: f.f0 == x0, .[f0 +1, b0 ) == s0, s == x # s0, not (r0 in [f0, b0 )), .f == f0 + 1, .r == r0 + 1, .r0 == x0, f0 != b0, Q g

while f r (

!=

b)

f r1 = .r ; release f, b, r ; f.r1 == r0 + distance (f0, b0 ), .[r0, r0 .[f0, b0 ) == s1, Q g ( ++) = . ( ++);

+

distance (f0, b0 ))

==

s,

5 SPECIFICATION OF THE COUNT GENERIC ALGORITHM

22

and by the induction hypothesis we obtain, for some s2 , f.f0 == x0, .[f0 +1, b0 ) == s0, s == x # s0, not (r0 in [f0, b0 )), .f == f0 + 1, .r0 == x0, f0 != b0, r1

.

r0 +1)

== (

+

distance (f0 +1, b0 ),

 r0 +1, (r0 +1) + distance (f0 +1, b0 )) .r0 == x0, .[f0 +1, b0 ) == s2, Q g . [

==

s0,

f.r1 == r0 + distance (f0, b0 ), .[r0, r0 + distance (f0, b0 )) == s, .[f0, b0 ) == s1, Q g which becomes a rst order logic formula that follows using properties of #, distance and ranges, by letting s1 be x # s2 . This completes the proof of the lemma.

5 Speci cation of the count generic algorithm Another simple generic algorithm in STL is count , which has this interface: template

void count InputIterator f InputIterator b const T v Size n (

,

,

&

,

&

);

Informally, count adds to n the number of iterators i in the range [f, b ) for which i Size can be any type for which ++ is de ned. count can be implemented as:

==

v.

template void count (InputIterator f, InputIterator b, const T & v, Size & n ) f while (f != b ) if (f ++ == v ) n ++;

g We give a formal speci cation of count in the following lemma:

Lemma 4

f.n0

== i, .v0 == v1, .[f0 b0 ) == s, Q g count (f0, b0, v0, n0 ) f.n0 = i + occurrences (s, v1 ), .v0 == v1, .[f0 b0 )

==

s, Q g

where occurrences is a function from sequences of T and T to the natural numbers, de ned by the following axioms: occurrences (empty T, v ) = 0 occurrences (x #s, v ) = if x == v then 1 + occurrences (s, v ) else occurrences (s, v )

6 CONCLUDING REMARKS

23

The lemma can be proved using the following model of the implementation: count (f0, b0, v0, n0 ) == InputIterator f = f0 ; InputIterator b = b0 ; T & v = v0 ; Size & n = n0 ; while (.f != .b ) if (.(f ++) == . .v ) (.n )++; release f, b, v, n ;

We omit the proof as it involves no new techniques.

6 Concluding remarks We have presented the foundations of a formal approach to speci cation and veri cation of generic algorithms, one which is particularly applicable to the generic algorithms of the C++ Standard Template Library. Although the examples in this paper are only simple cases of generic algorithms, many of the algorithms of STL are just as simple, and we believe that our approach has all the machinery needed to handle the ones that are more complicated. One such STL algorithm that we have studied in detail is unguarded partition , a particularly ecient form of partitioning used in implementing quick sort . In [15] the body of unguarded partition is a while loop that contains two nested while loops, certainly a more complex structure than we have dealt with in the examples of this paper. However, that complexity can be dealt with by rst stating and proving a lemma about each of nested while loops. Then as the outer loop is unfolded, the lemmas about each of the inner loops can be used to eliminate them. The real diculty of the speci cation and proof of this algorithm comes from the nonobvious way it maintains conditions that ensure termination, without using explicit checks for reaching the end of the sequence being partitioned. We plan to report on this example in a separate paper. We are also experimenting with partial automation of proofs about generic algorithms, using the PVS speci cation language [12] and prover [13], and the Tecton language [4, 5] and proof system [6]. Even if such proofs require extensive human direction and attention to detail, the fact that the algorithms are generic means that the necessary investments of e ort can be amortized over the many subsequent uses of the algorithms. These e orts will be the subject of future papers.

REFERENCES

24

References [1] E.W. Dijkstra, A Discipline of Programming, Prentice-Hall, 1976. [2] D. Gries, The Science of Programming, Springer-Verlag, 1981, p. 120. [3] C.A.R. Hoare, \An Axiomatic Basis for Computer Programming," Comm. ACM, Vol. 12, No. 10, October 1969, 576{583. [4] D. Kapur and D. R. Musser, Tecton: a framework for specifying and verifying generic system components, Rensselaer Polytechnic Institute Computer Science Technical Report 92-20, July, 1992.6 [5] D. Kapur, D. R. Musser, and A. A. Stepanov, \Tecton, A Language for Manipulating Generic Objects" Proc. of Workshop on Program Speci cation, Aarhus, Denmark, August 1981, LNCS, vol. 134, 1982. [6] D. Kapur, D. R. Musser, and X. Nie, \An Overview of the Tecton Proof System," Theoretical Computer Science 133 (1994) 307{339. [7] J. C. King, A Program Veri er, Ph.D. thesis, Carnegie-Mellon University, 1969. [8] J.H. Morris and B. Wegbreit, \Program Veri cation by Subgoal Induction," in Current Trends in Programming Methodology, R. T. Yeh, ed., Vol. II, Ch. 8, Prentice-Hall, 1977. [9] D. R. Musser and A. A. Stepanov, The Ada Generic Library: Linear List Processing Packages, Spring-Verlag, 1989. [10] D. R. Musser and A. A. Stepanov, \Algorithm-Oriented Generic Libraries," Software Practice and Experience, Vol. 24(7), 623{642 (July 1994) [11] G. Nelson, \Verifying Reachability Invariants of Linked Structures," Tenth Annual ACM Symposium on Principles of Programming Languages, Austin, Texas, Jan. 2426, 1983, pp. 38{47. [12] N. Shankar, S. Owre and J.M. Rushby, The PVS Speci cation Language, Computer Science Laboratory, SRI International, Menlo Park, CA, March 1, 1993. [13] N. Shankar, S. Owre and J. M. Rushby, The PVS Proof Checker: A Reference Manual (Draft), Computer Science Laboratory, SRI International, Menlo Park, CA, March 1, 1993. 6

Available as a Postscript le by anonymous ftp from ftp.cs.rpi.edu (do

tpcd-tecton.ps).

cd adv-prog

then

get

REFERENCES

25

[14] A. Stepanov and M. Lee, The Standard Template Library, Technical Report, HewlettPackard Laboratories, May 31, 1994, revised February 7, 1995; incorporated into the ANSI/ISO Draft Standard C++ Library (to be available for public comment approximately May 1, 1995). [15] A. Stepanov, M. Lee, and D. Musser, Hewlett-Packard Laboratories reference implementation of the Standard Template Library, source les available via anonymous ftp from butler.hpl.hp.com in =stl =shar le.Z . [16] C. Wallace, The Semantics of the C++ Programming Language, in E. Boerger, ed., Speci cation and Validation Methods, Oxford University Press, 1994.