0% found this document useful (0 votes)
48 views

A Practical Theory of Programming

This document introduces a formalism for describing computation that divides it into logic and timing. The logic concerns what results are obtained, while timing concerns when results are obtained. Specifications are defined as predicates relating initial and final variable values. Refinement is defined as one specification implying another for all initial/final states. Programs are defined as implemented specifications written in a restricted notation.

Uploaded by

James Woodcock
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
48 views

A Practical Theory of Programming

This document introduces a formalism for describing computation that divides it into logic and timing. The logic concerns what results are obtained, while timing concerns when results are obtained. Specifications are defined as predicates relating initial and final variable values. Refinement is defined as one specification implying another for all initial/final states. Programs are defined as implemented specifications written in a restricted notation.

Uploaded by

James Woodcock
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

Science of Computer Programming 14 (1990) 133-158 133

North-Holland

Received January I990

Abstract. Programsare predicates, programming is proving, and termination is timing.

1. Intmductiub

Cur formalism for the description of computation is divided into two parts: logic
and timing. The logic is concerned with the question: What results do we get? lfhe
timing is concerned with the question: When do we get them? One possible answer
to the latter question is: never, or as we may say, at time infinity. Thus we place
termination within the timing part of our formalism. Nontermination is just the
extreme case of taking a tong time.
We shall begin with a very simple model of computation. There are some observ-
able quantities, called variables X, y, - . , each with some domain. Our input to a
l

com~utatiun is to provide initial values for the variables. After a time, the output
from the computation is the final values of the variables. Later we shall add
communi~tion during the course of a imputation, but at first we shall consider
only an initial input and a final output. Also, for exposition, we start by igno~ng
timing, and therefore also te~ination.

2, Spedbtifms

We consider a specification of computer behavior to be a predicate in the initial


values x, y, . . and fina values x’, y’, . . . of some variables (we make no typographic
l

distinction between a variable and its initict ~~~ti~), FKHFY ;ix”cz&i%ialstate CF~=1
[x, y, . . .I9 a computation satisfies a specification by delivering a fkal state cr’=
[x’, y’, . f that satisfies the predicate. Here is an example. Suppose there are two
l l

variables x and y each with domain in&Then

(xF=x+l)t\(yF=y)
specifies the behavior of a computer that increases the value of x by 1 and ieaves
y unchanged.

0~4~a~23/90/S~3.S0 @ 199~Elsevier Science Publishers B.V. (North-Holland)


134 E. C. R. Hehner

Suppose we have an implementation of specification S. We provide the input O;


the computer provides an output a’ to satisfy S. For a specification to be implement-
able, then, there must be at least one satisfactory output for each input: A
specification S is called implemenfuble iff Vu3o’.S.
In the same variables, here is a second specification:

This specification is satisfied by a computation that increases x by any amount; it


may leave y unchanged or may change it to any integer. The first specification is
deterministic, and the second nondeterministic, for each initial state.
At one extreme, we have the specification true; it is the easiest specification to
implement becaus? every implementation satisfies it. At the other extreme is the
specification false, which is not satisfied by any implementation. But $&e is not
the only unimplementable specification. Here is anothrr:

If the initial value of x is nonnegative, the specification can be satisfied by setting


variable y to 0. But if the initial va!ue of x is negative, there is no way to satisfy
the specification. Perhaps the specifier has no intention of providing a negative
input, but to the implementer, every input is a possibility. The specifier should have
written
(xaO)q(y’=O)
For nonnegative initial x, this specification still requires variable y to be assigned
0. If we never intend to provide a negative value for x, then we do not care what
would happen if we did. That is what this specification says: for negative x any
result is satisfactory.
For our specification language we will not be definitive or restrictive; we allow
any well understood predicate notations. Often this will include notations from the
application area. When it helps to make a specification clearer and more understand-
able, a new notation may be invented (and defined) on the spot. In addition, we
provide four more specification notations:
ok
=(u’=u)
= (J+ x)I\(y’=y)w l l

(x:= e)
= (substitute e for x in ok)
= (x’= e)A(y’=y)A-

(if b then S else R)


=(bnS)v(lbii R)
= (b+S) A (IbaR)

(S; R)
= 3o”.(substitute u” for u’ in S) A(substitute u” for u in R)
A practical theory of programming 135

The notation ok is the identity relation between pre- and poststate: it specifies
that the final values of all variables equal the corresponding initial values. It is
satisfied by a machine that does nothing. In the assignment notation, e is any
expression. For example,

(x:= x+y)
= W=x+yMy'=y)

specifies that the final value of x should be the sum of the initial values of x and
y, and the value of y should be unchanged. The if and semi-colon notations combine
specifications to make a new specification. They apply to all specifications, not just
implementable specifications. They are just logical connectives, like A and v. But
they have the nice property that if their operands are implementable, so is the result.
The specification if 6 thenS elseR can be implemented by a computer that behaves
according to either S or R depending on the initial value of 6. The specification
S; R is the relational composition (or relational product) of S and R It can be
implemented by a computer that first behaves according to S, then behaves according
to R, with the final values from S serving as initial values for R It is therefore
sequential composition.

Notational note
The identity relation ok is called skip by some. We have used ok because it is
shorter, less active, and not in conflict with usage in other programming languages.
Our operator precedence is as follows:

0 (juxtaposition) index or argument left-to-right


1 +-1 prefix
2 X/A infix /left-to-right
3 +-A infix
4 V3 right-to-left
5 =#c>S2e=* infix continuing
6 := ! if-then-else right-to-left
7 . M/i
9
8 II infix
9 .. .. infix

On level 5, the operators can be used in a continuing fashion. This means, for
example, that a = b = c is neither grouped to the !eft nor grouped to the right, but
means (a = b) A (b = c).
136 E.C. R. Hehner

3. Refinement

Specification S is refined by specification R iff all computer behavior satisfying


R also satisfies S. We denote this by S:: R and define it formally as
(S:: R)
= Wu.Vo’.( Se R)
Thus refinement simply means finding another specification that is everywhere equal
or stronger. Here are two examples of refinement:
x’>x::(x’=x+l)A(y’=~)
(x’ =x+l)A(y’=~)::x:=xil
In each case, the left-hand side is implied by the right-hand side for all initial and
final values of all variables.
The prcondition for S to be refined by R is V.r’.(S* R).
The postcondirion for S to be refined by R is Vo.( Se R).
For example, although x’) 5 is not refined by x := x + 1, we can calculate (in one
integer variable):
(the precondition for x’) 5 to be refined by x := x + 1)
= Vx’.((x’> 5)c-(x:= x+ 1))
=Vx’.((x’>S)c(x’=x+ 1))
=(x+1)5) One-point Law
=(x)4)
This means that a computation satisfying s := x + 1 will also satisfy x’> 5 if and
only if it starts with x > 4. If we are interested only in starting states such that x > 4,
then we should weaken our specification with that antecedent, obtaining the
refinement:

Precondition Law. if A is the precondition for S to be refined by R, then AaS:: R.

There is a similar story for postconditions. For example, although x > 4 is unim-
plementable in general:
(the postcondition for x > 4 to be refined by x := x + 1)
=Vx.((x>4)e(x:= x+ 1))
=vx.((x-,4)~(x’=x+~l))
= (x’-1>4) One-point Law
= (x3 5)
This means that a computation satisfying x := x + 1 will also satisfy x > 4 if and only
if it ends with x’) 5. If we are interested only in final states such that x’) 5, then
we should weaken our specification with that antecedent, obtaining the refinement:
(x’>5)*(04)::x:=x+l
A practical theory of programming t37

For easier understanding, it might be better to use the contrapositive law to rewrite
the specification (x’ > 5 )*(x > 4) as the logically equivalent specification (x s 4) *
(x” s 5).

Postcondition Law. If A'is the postcondition for S to be refined by R, then A'+


S::R.

Let P be the specification “make x be further from zero”. Formally,

P=((x>o)*(x’>x))A
((X=O)*(x’#O))h
((x<O)*(x’cx))

or equivalently,

Then
(the precondition for P to be refined by x := x + 1)
=Vx'.(Pe(x'=x+1))
=~x+1>x>o)v(x+1#x=o)v(x+1cxco)
=(x20)

We find that x := x + 1 specifies that x becomes further from zero if and only if x
starts nonnegative. Also,

(the postcondition for P to be refined by x := x + 1)


=Vx(Pe(x'=x+1))
=(x’>x’-l>O)v(x’#x’-l=O)v(x’<x’-1cO)
=(x%1)

Therefore x := x + 1 specifies that x becomes further from zero if and only if x ends
positive.

4. Programs

A program is a specification of computer brkav~~t; 2 ia G:‘;&we a predkxe in


the initial and final state. Not every specafication is a program A program is an
“implemented” specification, one that a computef can execute. To be SO, it must
be written in a restricted notation. Let us take the following as our programming
notations.
(a) ok is a program.
(b) If x is any variable and e is an “implemented” expression, then x := e is a
program. -
138 E.C. R. Hchner

(c) If 6 is an “implemented” boolean expression and P and Q are programs,


then if h thee P else Q is a program..
(d) If P and Q are programs then P; Q is a program.
(e) If P is an implementable specification and Q is a program such that P:: Q,
then P is a program.

In (b) and (c) we have not stated which expressions are “implemented”; that set
may vary from one implementation to another, and from time to time. Likewise the
programming notations (a) to (d) are not to be considered definitive, but only an
example. Part (e) states that any implementable specification P is a program if a
program Q is provided such that P:: Q. To execute P, just execute Q. The refinement
acts as a procedure declaration; P acts as the procedure name, and Q as the
procedure body; use of the name P acts as a call. Recursion is allowed; in part (e)
we may use P as a program in order to obtain program O_
Here is an example. Let x be an integer variable. The specification x’ = 0 says
that the final value of variable x is zero. It becomes a program by refining it, which
can be done in many ways. Here is one:

x’=O::if x=0 then ok else (x:=x-l;x’=O)

In standard predicate notations, this refinement is

which is easily proven.

5. Common laws

A law is a sentence form such that all sentences of the form are theorems. In
other words, it is a theorem schema. The laws we present are not axioms (postulates);
they can be proved from the definitions we have given. It is essential for programming
that the laws hold for all specifications, not just for programs. That way we can use
them for program construction, not just for verification.

Transformation laws

The following laws hold for all specifications P, Q N&L 8 and F+~“:%~~n
expressions 6:

(ok;P)=(P;ok)= P
(P;W;R))=UP;Q);R)
(if 6 then P else P) = P
(if 6 tbea P else Q) = (if 16 tben Q else P)
((if !I then P else 0); R) = (if 6 then (P; R) else (Q;R))
A practical theory of programming 139

Reformation laws

The following laws hold for all specifications P, Q and R, variables X,expressions
e and boolean expressions b:

(x:= e ; P) = (for x substitute e in P) Substitution Law


P =(if 6 then b+P else 76-P) Case Law
(PvQ;RvS)=(P;R)v(P;S)v(Q;R)v(Q;S)
(ifbthenYelseQ)vR=(if6theaPvRelseQvR)
(ifbtheaPelseQ)AR=(ifbthenP~RekseQ~R)

In the Substitution Law, P must use only standard predicate notations, not program-
ming notations. Here are some examples of the Substitution Law:
(x:=y+z;y’>x’)=(y’>x’)
(X:=X+i;(y’>X)A(X’>X))=(J”>X+l)A(X’~X+1)

(x:= 1 ;(x = 1)*3x.(y’= 2 xx)) = (( 1 = 1)=33x.(y’= 2 xx))


(x:=y;(x= 1)*3y.(y’=xxy))=((y=1)33k(y’=yxk))
(x:= y+z;y’=2xx)=(y’=2x(y+2))

Statta laws

Let P and Q be any specifications, and let A be a predicate of the prestate, and
let A’ be the corresponding predicate of the poststate. Then:

AA(P;Q)::AA P;Q
(P;Q)AA’::P;QAA’
A+P;Q)::A+P;Q
(P; Q)*A’:: P; Q*A’
P;AAQ::PAA’;Q
P;Q::PAA’;A*Q

Refinement is a partial ordering of specifications. A function from specifications


to specifications is called monotonic if it respects the refinement ordering. Thus,
trivially, we have:

Stepwise Refinement Law (refinement by steps). If f is a monotonic function on


specifications, and P:: Q, then fp::fQ.

Another simple and very useful law is the following:

Partwise Refinement Law (refinement by parts). If S is a monotonic function on


specifications, and f ::p and Q ::fQ, then P A Q ::$( P A Q).

These two laws are so important for programming that we shall require all our
program constructors to be monotonic. So far we have only two, if and sequential
140 E.C. R. Hehner

composition, and they are both monotonic in both their specification operands. (In
fact, they are pointwise monotonic.)

6. Program developamt

Here is an example of program development. It is not expected to convince the


reader that program development is aided by a good theory; that has been demon-
strated many times in the literature using Dijkstra’s wp theory and Jones’ VDM
theory. The example is intended only :o help the reader become familiar with the
theory presented in this paper.
Let n be a natural variable. The problem is to reduce n modulo 2 using addition
and subtraction. The first step is to express the desired result as clearly and as simply
as possible:
n’= modn2

We refine it using the Case Law.

n’= modn 2::


if n<2dhen(n<2)*(n’=modn2)
else (n32)*(n’=modn 2)

We consider that we have solved the original problem, but now we have two new
problems to solve. One is trivial:

(nC2)*(n’=mdn 2)::ok

The other can be solved using the Substitution Law:

The final specification has already been refined, so we have finished programming
(except for timing). Each refinement is a theorem; programming is proving.
The last refinement could have been written as

(n 32)*(n’= modn 2)::


(even nf =evenn);n’=modn2

in order to leave room for improvement. Proceeding at a faster pace,

evenn’=evenn::
p:=2;evenp*evenph(evenn’=evenn)
evenp*evenp’A(evenn’=evenn)::
n :=n-p;p:=p+p;
if n<p then ok else evenp+evenp’n (even n’= even n)
A pa&al theory t$ programming 141

When a compiler translates a program into machine language, it treats each refined
specification as just an identifier. Our program looks like:

A::r’f n<2 then B else C


B::ok
C:: D;A
D::p:= 2; E

to a compiler. Because if and sequential composition are monotonic, a compiler


can compile the calls to B, C and D inline (macro-expansion) creating:
A::if nc2 then ok else (p:=2;E;A)

There is no need for the programmer to perform this mechanical assembly, and we
need not worry about the number of refinements used. Also, the recursive calls to
A and E can be translated as just branches (as can 99% of calls), so we need not
worry about recursion being inefficient.

7. Timiog

So far, we have talked only about the result of a computation, not about how
long it takes. To talk about time, we just add a time variable. We do not change
the theory at all; the time variable is treated just like any other variable, as part of
the state. The state Q = [ t, X,y, . . .] now consists of a time variable t and some space
variables X, y, . . . . The interpretation of I as time is justified by the way we use it.
We use t for the initial time, i.e. the time at the start of execution, and I’ for the
final time, i.e. the time at the end of execution. To allow for nontermination we
take the domain of time to be a number system extended with 00. The number
system we extend can be the naturals, or the integers, or the rationals, or the reals.
The number 00 is maximum, i.e.,

and it absorbs any addition:


uo+1=00

-+ time is m&;se~table
Time cannot decrease, therefore a specification s yficS iff

Vu*3a’.S n ( t’a t)

And for every program P,

Time increases as a program is executed. There are many ways to measure time.
We present just two: real time and recursive time.
142 E. C. R. Hehner

7.1. Real time


Real time has the advantage of measuring the real execution time, ‘and is useful
in real-time programming. It has the disadvantage of requiring intimate knowledge
of the implementation (machine and compiler).
To obtain the real execution time of a program, modify the program as follows:
Step 1. Replace each assignment x := e by
t := t+u;x:= e

where u is the time required to evaluate and store e.


Step 2. Replace each conditional if 6 then P else Q by
I:= t+u;if 6 then P else Q
where v is the time required to evaluate 6 and branch.
Step 3. Replace each call P by
t := t+w;P

where w is the time required for the call and return. For a call that is implemented
“inline”, this time will be zero. For a call that is executed last in a refinement, it
may be just the time for a branch. Sometimes it will be the time required to push
a return address onto a stack and branch, plus the time to pop the return address
and branch back.
Step 4. Each refined specification can include time. For example, let f be a
function of the initial state m. Then
t’= t +fu
specifies that fo is the execution time,
t’s t +fu
specifies that fo is an upper bound on the execution time, and
t’a t+fu
specifies that fo is a lower bound on the execution time.
We could place the time increase after each of the programming notations instead
of before. By placing it before, we make it easier to use the Substitution IAVX
In an earlier section, we considered the example
x’=O::if x=Otbea ok eke (x:=x-l;x’=O)

Suppose that the conditional, the assignment, and the call each take time 1. Using
P for the specification, the refinement becomes

P:: t:= t+l;


if x=0 tbea ok else (t := t+l;x:=x-l;t:= t+l;P)
A practical theory of programming 143

This is still a theorem when P = (x’= 0) as before, but we can now include time in
the specification. The refinement with time is a theorem when

P=(x’=O)h
((X~0)3(t’=t+3xx+1))h
((xcO)*(t’=oo))
Execution of this program always sets x to 0; when x starts with a nonegative value,
it takes time 3 x x + 1 to do so; when x starts with a negative value, it takes infinite
time. It is strange to say that a result such as x’= 0 is obtained at time infinity. This
is really just a way of saying that a result is never obtained. We could change our
theory of programming to prevent any mention of results at time infinity, but we
do not for two reasons: it would make the theory more complicated, and we will
need to distinguish among infinite loops later when we introduce communications.

7.2. Recursive time


Tr, free ourselves from having to know implementation details, we allow any
arbitraryscheme for inserting time increments t := t + u into programs. Each scheme
defines a new measure of time. In the recursive time measure:

- Each recursive call costs time 1.


- All else is free.
This measure neglects the time for “straight-line” and “branching” programs,
charging only for loops.
In the recursive measure, our earlier example becomes:

P::if x=0 then ok else (x:=x-l;t:= t+l; P)

which is a theorem when

P=(x’=o)A
((xaO)*(t’= t+x))ll
((xcO)*(t’=aD))

As a second example, consider the refinement


Q::if x= 1 then ok else (x:=divx2;t:= t+l;Q)

where
Q=(x’=l)~
((X>,l)+t-f++IbX))A
((xcl)*(t’=@)

( HJ= binary logarithm; div - division and round down). Execution of this program
always sets x to 1; when x starts with a positive value, it takes logarithmic time;
when x starts nonpositive, it takes infinite time. The proof breaks into six pieces:
144 E. C. R. Hchncr

thanks to the Partwise Refinement Law, it is sufficient to verify the three conjuncts
of Q separately; and for each there are two cases in the refinement. In detail,
x’=l::(x=l)n(x’=x)A~r’=r)
x8=I::(xf r)h(x’= 1)

(xa 1)*(r’s r+lbx)::(x= l)n(x’=x)n(f= 1)


(x2 l)_(f’S t+lbx)::

(xc l)*(r’=a\)::(x= l)~(.d=x)~(t'=f)


(x<l)*(f’=a:)::(x# l)h((ditrX2C1)~(1’=~))

Each is an easy exercise, which we leave to the reader.


Our examples were of direct recursion, but recursions can also be indirect. For
example, the refinement of a specification A may contain a call to B, whose refinement
contains a call to C, whose refinement contains a call to A. The general rule is that
in every loop of calls, at least one call must be charged at least one time unit.

A standard way to prove terrriiination is to find a variant (or bound function) for
each loop. A variant is an expression over a well-founded set whose value decreases
with each iteration. When the well-founded set is the natural numbers (a common
choice), a variant is exactly a time bound according to the recursive time measure.
We shall see in the next section why it is essential to recognize that a variant is a
time bound, and to conclude that a computation terminates within some bound,
rather than to conclude merely that it terminates.
Another common choice of well-founded set is based on lexicographic ordering.
For any given program, the lexicographic ordering used to prove termination can
be translated into a numeric ordering, and be seen as a time bound. To illustrate,
suppose, for some program, that the pair [n, m] of naturals is decreased as follows:
[n, m+ I] decreases to [n, m], and [n+ 1, 0] decreases to [ n,fi]. Then we consider
[n, m] as expressing a natural number, defined as follows.
[O,O]=O

[n,m+l]=[n,m]+l
[n+l,O]=[n,fn]+1
Well-founded sets are unnecessary in our theory; time can be an e~te~&d ;?“qtfl;
rational, or real. Consider the following infinite loop:
R::x:=x+l; R

We can, if we wish, use a measure of time in which each iteration costs half the
time of the previous iteration:

R::x:=x-tl; t:= t+2-“; R


A practical theov of programming 145

is a theorem when R = (t’ = t +2-“). The theory correctly tells us that the infinite
iteration takes finite time. We are not advocating this time measure; we are simply
showing the generality of the theory.

7.4. Termination
A customer arrives with the specification

(1)
He evidently wants a computation in which variable x has final value 2. I provide
it in the usual way: I refine his specification by a program and execute it. The
refinement I choose, including recursive time, is:

and execution begins. The customer waits for his result, and after a time becomes
impatient. 1 tell him to wait longer. After more time has passed, the customer sees
the weakness in his specification and decides to strengthen it. He want a computation
that fir,ishes at a finite time, and specifies it thus:
(x’=2)n(t’<al) (2)
We reject (2) because it is unimplementable: (2) A(1’a t) is unsatisfiable for t = 00.
It may seem strange to reject a specification just because it cannot be satisfied with
nondecreasing time when started at time ~0.After all, we never want to start at time
43. But consider the sequential composition P; Q with P taking infinite time. Then
Q starts at time 00 (in other words, it never starts), and the theory must be good
for this case too. An implementable specification must be satisfiable with nondecreas-
ing time for all initial states, even for initial time 00, So the customer weakens his
specification a little:
(x’=2)n((t<m),(t’<m)) (3)
He says he does not care how long it takes, except that it must not take forever- I
can refine (3) with exactly the same construction as (l)! Including recursive time,
my refinement is:
(X’=2)h((tcao)*(t’c~))::
t:= t+l;(X’=2)A((t<a)+t’<(X3))
Execution begins. When the customer becomes restless I again tell him to wait
longer. After a while he decides to change hia sp ;%c~&G +h:
(X’=2)A(t’=t) (4)

He not only wants his result in finite time, he wants it instantly. Now I must abandon
my recursive construction, and I refine:
(x’=z)A(t’= t)::X:=?

Execution provides the customer with the desired result at the desired time.
146 E. C. R. Hehner

Under specification (I), the customer is entitled to complain about a computation


if and only if it terminates in a state in which x’ ;c 2. Under specification (3), he
can complain about a computation if it delivers a final state in which x’ # 2 or if it
takes forever. But of course there is never a time when he can complain that a
computation has taken forever, so the circumstances in which he can complain that
specification (3) has not been met are exactly the same as for specification (1). It
is therefore entirely appropriate that our theory allows the same refinement construc-
tions for (3) as for (1). Specification (4) gives a time bound, therefore more
circumstances in which to complain, therefore fewer refinements.
As this example shows, it is meaningless to request or to promise results in a
“finite but unbounded” time. One might suggest that a time bound of a million
years is also untestable in practice, but the distinction is one of principle, not
practicality. A time bound is a line that divides shorter computations from longer
ones. There is no line dividing finite computations from infinite ones.

8. Local variable

The ability to declare a NW variable within a local scope is so useful that it is


provided by every decent programming language. A declaration may look something
like this:
varx: T
where x is the variable being declared, and T, called the type, indicates what values
x can be assigned. There must be a rule to say what scope the declaration has, that
is, what it applies to. We shall consider that a variable declaration applies to what
follows it, up to the next enclosing right parenthesis. In program theory, it is essential
that each of our notations apply to all specifications, not just to programs. That
way we can introduce a local variable as part of the programming process, before
its scope is refined.
We can express a variable declaration together with the specification to which it
applies as a predicate in the initial and final state:
(var x: T; P) =3x: 73x’: T.P
Specification P is a predicate in the initial and final values of all global (already
declared) variables plus the newly declared local variable. According to this pre&
cate, the initial value of the variable is an arbitrary value of the type-
global variables are integer variables y and z. Then
(var x: int;y:= x)
= 3x : int.3x’: int.(x’ = x) A (y’ = x) n (2’ = 2)
= (z’=z)

which says that z is unchanged. Variable x is not mentioned because it is not one
of the global variables, and variable y is not mentioned because its final value is
A practical theory of programming 147

unknown. However
(vatx:int;y:=x-x)
=(y’=O)/+‘=z)
In some languages, a newly declared variable has a special value called “the
undefined value” which cannot participate in any expressions. To write such
declarations as predicates, we introduce the elementary expression urrdejinedbut
we do not give any axioms about it, so nothing but trivialities like undefined =
undefined can be proved about it. Then
(vat x: T; P)
= 3x : undefined3x’: T, undefined.P

9. la-place refioemeot

We have not introduced a loop programming notation; instead we have used


recursive refinement. When convenient, we can refine “in-place”. For example,
x:= 5;
(y’>x::y:=x+l);
2:= Y

The middle line means what its left-hand side says, hence the three lines together
are equal to:
y'= 235

We are not allowed to use the way y’> x is refined and conc!ude
(x’ = 5) A(y’ = 2’ = 6).
In-place refinement can be recursive, and thus serve as a loop. For example,
(x~o)*(y’=x!)A(z’=t+x)::
y:=l; _-
((x~0)3(y’=yxx!)A(t’=t+x)::
if x=0 theo ok
else (y:=yxx;x:=x-1;2:= t+l;
(X3O)+y’=yXX!)A(t’=t+x)))

There are IWOtheorems to be proved here. The first is:


(xz=O)*(y’=x!)A(r’=t+x)::
y:=I;(X~O)~(y’=yXX!)A(r’=f+x)

Note that the body of the loop is not used in this theorem. The other is:
(xkO)~(y’=yxx!)A(t’=z+x)::
if x=0 theo ok
else (y:=yxx;x:=x-l;t:= t+l;
(XLO)+(y’=yXX!)A(t’=t+X))
148 E. C. R. Hehner

With in-place refinement we can indent the inner refinements and make our programs
look more traditional. This is not a good reason for using them. The only advantage
they offer is to save us from writing a specification one extra time.

10. Concurresrcy

We shall define the parallel composition 11of specifications P and Q so that P 11Q
is satisfied by a computer that behaves according to P and the same time, in parallel,
according to Q. The operands of 11are called processes. The connective IIis similar
to A (conjunction), but weaker, so that
PIIQ::PitQ

Conjunction is not always implementable, even when both conjuncts are. For
example, in variables x and y,
(x:= x+l)n(y:=y+l)
= (x’= X+~)A(f=J’)A(X’=.X)A(J7’=~~+~)
= I1as4
We shall define parallel composition so that P IIQ is implementable whenever P
and Q are. In particular,
(X:=X+~~~_V:=~V+~)=(X’=X+~)A(J”=_V+l)

The program ok will be the identity for parallel composition


(ok11 P)=(Pllok)= P

just as for sequential composition, unlike conjunction.


If we ignore time, or if we use the recursive time measure, we have:
(x:= x+ l;x:= x-l)=ok

Therefore, according to the Law of Transparency (substitution of equals),

(X :=x+l;x:=x-lIly:=x)=(okIly:=x)
According to the right-hand side of this equation, the final value of y is the initial
value of x. According to the left-hand side of the equaiton, it may seem that y’ = x + 1
should also be a possibility: the right-hand process y := x may be executed in b~*we
the two assignments x:= x + 1 and x:= x - 1 in the left-hand pro&ss. %%‘e faw a
simple choice: give up the Law of Transparency and with it aii familiar mathematics,
or give up the possibility that one process may use the intermediate states of another
process. We choose the latter; our processes will not communicate with each other
through shared variables, but through communication channels that do not violate
the Law of Transparency. We postpone communication to the next section, and in
this section consider non-interacting processes.
A practical themy of pogratnming 149

Variable x‘is independent of specification P if

x v=x::P

According to P, the final value of x will be its initial value. Variable x is dep&ent
on specification P if it is not independent:

1(x’ =x:: P)

This says that x might change (is not guaranteed not to change) its value under
specification I? The precondition for independence

Vo’.((x’= x)e P)

tells us in which initial states K does not change its value under I? Its negation

1Vo’.((x’=x)e=P)
= 3a’.(x’ f x) h P

tells us in which initial states x might change its value under l?


For any initial state, the dependent space of a specification consists of all space
variables that might change value. Let of denote the dependent space of P, and let
o. denote the dependent space of Q. Then

(PII Q) =(3&P) A (3a'p.Q)

Parallel composition is just conjunction, except that each conjunct makes no promise
about the final values of variables that might be changed by the other process.
A parallel composition can be executed by executing the processes in parallel,
but each process makes its assignments to private copies of variables. Then, when
both processes are finished, the final value of a variable is determined as follows:
if both processes left it unchanged, it is unchanged; if one process changed it and
the other left it unchanged, its final value is the changed one; if both processes
changed it, its final value is arbitrary. This final rewriting of variables does not
require coordination or communication between the processes; each process rewrites
those variables it has changed. In the case when both processes have changed a
variable, we do not even require that the final value be one of the t!vo changed
values; the rewriting may mix the bits.
In a commonly occurring special case, there is no need to copy vz++.bles. When
both processes are expressed as programs, and neither assigns to any variable
appearing in the other, copying and rewriting are unnecessary. For the sake of
efficiency, it is tempting to make this case the definition of parallel composition.
But we must define our connectives for all specifications, not just for programs. We
must be able to divide a specification into processes before deciding how each
process is refined by a program.
150 E. C. R. Hehner

Common laws
(P::Q)*(P llR::QIIR) monotonic
(P::Q)*(RIlP:AqQ) monotonic
vvIQ)=wIl~) symmetric
~~ll(QIl~~)=~(~llQ)ll~) associative
(PIIok)=(okllP)=P identity
(~llQv~)=(~IIQ~v~~ll~) distributive
(PIIN 6 tbea Q else RI
= (if 6 then (P 11Q) else (P 11RI) distributive

Examples
Let the state consist of three integer variables x, y and z. Here are two unsurprising
examples:

Here is a less obvious example:


(x:=yIIx:=z)
=((X=y)*(X’=t))A((X=t)~(X’=y))A(y’=y)A(z’=t)
To see this, we must find the dependent spaces. Variable x is in the dependent space
of x:= y when:

3a’.(x’# x) A (x:= y)
= 3Xf.3_V'.3Z'.( X' # X) A (X’ = _I’)A (?” = JT)A (Z’ = Z)
=(xzy)
Variable y is in the dependent space of x := y when
&f.(_V’ # _V)A (X := y )
=3X'.3y'.3t'.(),'#y)A(X'=y)A(~'=y)A(Z'=Z)
= /las4

Variable z is in the dependent space of x:= y when

3U’.(Z’# Z)A (X:=y)


= 3X’.3yg.3t’.( 2’ # 2) A (X’ = V) A (f = y) A (2’ = 2)

= flas4
In other words, when x = y the dependent space of x:= y is empty, i.e.,

QX=y)*(a,,,, = [nil])
and when x # y the dependent space of x := y is x, i.e.,

(x f y)*(o,:,, = [xl)
A ptacrical theory of ptvgtamming

Similarly for x:= z. Thus we have four cases:


(x:=y/Ix:= 2)
=((X=y=t)*(x:=y)A(X:=z))A
((Xfy)A(X=r)3(X:=y)A(3x'.(x:=2)))A
((X=y)A(Xf 2)*(3x’.(x:=y))h(X:= 2))h
((X # J’) A (X # 2)+(3X'.(X:= A (3X’.@ := 2)))
J'))
Simplifying, we obtain the answer stated previously.

IO.1. Parallel timing


The definition of parallel composition uses dependent spaces to say which conjunct
determines the final value of each variable. For each initial state, we place the time
variable t in the dependent space of the process that takes longer. If neither process
takes longer than the other (they take the same time), then it is in neither dependent
space. With this placement of the time variable, the time for a parallel composition
is the maximum of the individual process times.

11. Couunuaic8tion

Until now, the only input to a computation has been the initial state of the
variables, and its only output has been the final state. Now we consider input and
output during the course of a computation. We allow any number of named
communication channels through which a computation communicates with its
environment, which may be people or other computations running in parallel.
Communication on channel c is described by a list s, called the channel script,
and two extended natural variables r, and w, called the read and write cursors. The
script is the sequence of all messages-past, present and future-that pass along
the channel. The script is a constant, not a variable. At any time, the future meassages
on a channel may be unknown, but they can be referred to as items in the script.
The read cursor is a variable saying how many messages have been read, or input,
on the channel. The write cursor is a variable saying how many messages have been
written, or output, on the channel. Neither the script nor the cursors are programming
notations, but they allow us to specify any desired communications. If there is only
one channel, or if the channel is known from context, we may Gmit the subscripts
on s, r and w.
Here is an example specification. It says that if the next iapat e,;;:channel c is
even, then the next output on channel d will be 0, and otherwise it will be 1.
Formally, we may write:
if eilen (s,r,) then sdwd = 0 else sdwd= 1
or more briefly
mod ( SJ,) 2 = sdwd
152 EC. R. Hehner

We provide three programming notations far communication. Let c be a channel.


Then c? describes a computation that reads one input on that channel. We use the
channel name c to denote the message that was last previously read on the channel.
The notation c !e describes a computation that writes the output message e on
channel c. Here are the formal definitions (omitting the obvious subscripts):
C9. =(r:=r+l)

c-sqr-I)

c!e=(sw=e)h(w:= w+l)

The predicate computution is the strongest predicate that describes all computa-
tions. If we are considering time, and there is one channel c, then
computation=(t’3t)n(r+rC)n(w:3H:)
A computation cannot take less than zero time, it cannot unread, and it cannot
unwrite. There are similar conjuncts for other channels. If there are no channels
and we are not considering time, then computution is the empty conjunction true.
In general, specification S is implementu6le iff
Vu3o’S h computation
For every program P
computation :: P

Examples
Input numbers from channel c, and output their doubles on channel d. Formally,
the specification is:
S=Wn:nat.(sd(wd+n)=2xsC(rC+n))
We cannot assume that the input and output are the first input and output ever on
channels c and d. We can only ask that from now on, starting at the initial read
cursor r, and initial write cursor w,,, the outputs will be double the inputs. This
specification can be refined as follows:
S::c?;d!2xc;S
The proof is as follows:
(c?;d!2xc;S)
=(r$= rC+l;(sdwd=2xsC(r,-l))h(wd:= wd+l);S)
= (sdw&#=2XsCrC)r\Vn:nat.(s,(w~+l+n)=2xs,(rC+l+n))
=S
For reasons stated earlier, parallel processes cannot communicate and cooperate
through variables. They must communicate through channels. Here is a first example,
though not a convincing one:
c!2(1c?;x:=c
A practical theory qfprogramming 153

The dependent space of the left-hand process is w. The dependent space of the
right-hand process is r, X. This example is equal to
(SH’=2)h(H”=H’+l)h
(P’= r + 1) A(x’ = sr) A(other variables unchanged)
We do not know that initially w = r, so we cannot conclude that finally X’= 2. The
parallel composition may be part of a sequential composition, for example,
c!l;(c!211c?;x:=c);c?
and the final value of x may be the 1 from the earlier output, with the 2 going to
the later input. In order to achieve useful communication between processes, we
shall have to introduce a local channel.

I 1.1. Local channel

Channel declaration is similar to variable declaration; it defines a new channel


within some local portion of a program or specification. Each language must have
rules to say what the scope of the declaration is. We shall consider that a channel
declaration applies to what follows it, up to the next enclosing right parenthesis.
Here is a syntax and equivalent predicate.
(chaac: T;P)
= 3s, : *T.(var rC: mat := 0;var w, : mat := 0; P)
The type T says what communications are possible on this new channel. The
declaration introduces a script, which is a list of items from T; it is not a variable
(in the programmer’s sense), but a constant of unknown value. It also introduces
a read cursor with initial value 0 to say that initially there has been no input on
this channel, and a write cursor with initial value 0 to say that initially there has
been no output on this channel.
A local channel can be used without concurrency as a queue, or buffer. For
example,
(chaac:inr;c!3;c!S;c?;x:=c;c?;x:=x+c)
= (x:= 8)

Here are two processes with a communication between them:


(chm c: int;(c!211 c?;x:= c))
==3s: *int.(var r : xnat :=O;var w:xnat:=O;
(SW=2)h(W’=W+1)A(r’=1.+fjA(Xt=St)A

(other variables unchanged))


= 3~ : *int.(s0 = 2) A(x’ = SO)A (other variables unchanged)
= (x’ = 2) A (other variables unchanged)
=(x:=2)
Replacing 2 by an arbitrary expression in this example, we have a general theorem
equating communication on a local channel with assignment.
154 E. C. R. Hehnet

11.2. Communication timing


Here is an example of two communicating processes:

The first process begins with a computation described by P, and then outputs a 0
on channel c. The second process begins with an input on channel c. Even if we
decide that communication is free, we must still account for the time spent waiting
for input. We therefore modify the program by placing a time increment before
each input. For example, if

I’ =t+p::P
t’ = t+q::Q
then we modify the program as follows:

cban c : int ; cban d : int ;


(P;c!O; t:= t+G;d?;RII t:= t+p;c?;Q;d!l)
In general, the time increments in each process depend on the computation in the
others.
It is sometimes reasonable to neglect the time required for a communication, as
in the previous example. But it is not reasonable to neglect communication time
when there are communication loops. Here is a simple deadlock to illustrate the
point:

chaac:int;chand:int;(c?;d!211d?;c!3)
In the two processes, inserting a wait before each input yields

t := t+u;c?;d !2((t:= t+v;d?;c!3


On the left, input is awaited for time u; then input is received and output is sent.
We assign no time to the acts of input and output, so the output is sent at time
t + u. But we charge one time unit for transit, hence the output is available as input
on the right after waiting time v = u+l. By symmetry, we can also say u=v+l.
This pair of equations has a unique solution: u = v = 0~. The theory tells us that the
wait is infinite.
If we had not charged for communication transit time, we would be solving F = v,
and we would conclude that the eommunicatio2 ..-ould happen at anv time &SSV~C
that, after waiting time 100, an input is received on the I&
be sent at that same time (since we charge zero for the acts of input and output),
and received on the right at that same time. And then the right-hand side can send
its output to the left at that same time, which justifies our original assumption. Of
course, u = v = 00 is also a solution. Spontaneous simultaneous communication is
unphysical, so communication loops, like refinement loops, should always include
a charge for time.
A practical theory of programming 155

Livelock is an infinite communication loop on a local (hidden) channel. Without


a time variable, our logic treats livelock as though it were ok, With a time variable
we find that livelock leaves all variables unchanged (like ok) but takes infinite time
(like deadlock). The infinite time results from the infinite loop (recursive measure),
even if communication is free.

11.3. Synchronous communication


The communication we have described is asynchronous. For synchronous com-
munication, we need the same script as for asynchronous communication, but only
one cursor. The programming notations are defined the same way, but variables r
and w become one variable. Synchronous communication timing requires a time
increment before input and output because execution of an output must wait until
the corresponding input can be executed simultaneously. The theory of synchronous
communication is not very different from the theory of asynchronous communica-
tion, but in practice the extra delays for output mean more simultaneous equations
to solve.

11.4. Time dependence


Our examples have used the time variable as a ghost, or auxiliary variable, never
affecting the course of a computation. It was used as part of the theory, to prove
something about the execution time. Used for that purpose only, it did not need
representation in a computer. But if there is a readable clock available as a time
source during a computation, it can be used to affect the computation. Both

x := t

and

if t < 1989;06:26:09:30.000 then.. . else.. .

are allowed. We can look at the clock, but not reset it arbitrarily; all clock changes
must correspond to the passage of time.
We may occasionally want to specify the passage of time. For example, we may
want the computation to “wait until time w”. Let us invent a notation for it, and
define it formally as

wait until w
=(t:=mww)

This specification is easily refined. Including recursive time,

wait until w ::
if ta w them ok else (t:= t+l;wait until w)

and we obtain a busy-wait loop. (For simplicity we have considered time to be


156 E. C. R. Hehner

integer-valued, and used the recursive time measure. For practicality we should use
real values and the real time measure. The change is an easy exercise.)

12. Recursive constructioa

A popular way to define the formal semantics of a loop construct is as a least


fixed-point. An appropriate syntax would be
identijier = program

where the iderhfler can be used within the program as a recursive call. (A fixed-point
is a solution to such an equation, considering the identifier as the unknown. The
least fixed-point is the weakest solution.) There are two difficulties.
We defined sequential composition as a connective between arbitrary
specifications, not just programs. This is essential for programming in steps: we
must be able to verify that P:: Q; R without referring to the refinements of Q and
I?.Similarly if, vat, ff and than apply to arbitrary specifications. For the same reason,
we must define a loop construct, if we choose to have one, for all specifications:
identifipr = specifka tion

Unfortunately, such equations do not always have solutions. Fortunately, we are


not really interested in equality, but only in refinement. The syntax
identi@er : : specification

would be appropriate. A solution to a refinement is called a pre-fixed-point. Since


tnre is always a solution to a refinement, a pre-fixed-point always exists. But obviously
we cannot be satisfied with the least pre-fixed-point as the semantics. The greatest
(strongest) pre-fixed-point cannot be used either because it is often unimplementable,
even when the specification is a program. How about the greatest implementable
pre-fixed-point? Unfortunately, greatest implementable pre-fixed-points are not
unique, even when the specification is a program.
The second difficulty is that a least fixed-point, or greatest implementable pre-fixed-
point, is not always constructible. (With a constructive bias,, I would complain that
it does not always exist.) Even when it is constructible, the construction process is
problematic. One forms a (Kleene) sequence of predicates, then one takes the limit
of the sequence as the solution. (The sequence starts with PO= computation. Thee
Pk+$ is formed by substituting Pk into the loop body in place ar’ the in
identifier.) Finding the “general member” requires an educated guess (induction
hypothesis) and induction. The limit may not be expressible in the specification
language. Due to discontinuity, the limit may not be a solution to the original
problem.
In our approach, we ask a programmer to specify what she intends her loop to
accomplish, and then to provide a refinement. We neatly avoid the Kleene sequence,
A practical theory of programming 157

the induction, the limit, and the continuity problem. Of course, these problems do
not disappear, but we prefer to avoid them when possible.

13. Conclusion

In our theory, a program is a specification whose implementation is provided


automatically. Thus programming notations can be freely mixed with other
specification notations to allow a smooth transition in small steps from specification
to program.
A specification is a predicate, hence a program is a predicate. This is simpler
than Dijkstra’s wp, in which a program is (or can be mapped to) a function from
predicates to predicates. It is simpler than Jones’ VDM in which a program is (or
can be specified by) a pair of predicates. And we have a simpler notion of refinement:
universally quantified implication.
Our theory is also more general than its competitors, applying to sequential and
concurrent computations, communicating processes, and infinite interactive compu-
tations. For the latter purposes, it is much simpler than theories involving sets of
interleaved sequences, or those involving temporal logic.
The decision to include time was made when the following facts became evident.
Time is just an ordinary variable in the theory, distinguished only in that its changes
of value correspond to the passage of time. Thanks to the Partwise Refinement Law,
time can be ignored at first, and conjoined later. Proof of termination is essentially
the discovery of a time bound, so we may as well call it that. Without a time variable
(or other termination indicator), our logic would be more complicated. For example,
sequential composition would be
P;Q = if P requires termination
tben relational composition of P and Q
else P
in order to ensure “strictness” (we cannot have an infinite computation first, and
then something more). But with a time variable, P; Q is just relational composition,
and strictness is just the fact that m+ t = 00. Without time, input would be
c? = if there is or will be a communication on channel c
then advance the cursor
else loop forever

With time, input is just a cursor advance, perhaps at time a~


The mathematics we need is just simple logic and arithmetic, with no special
ordering olr induction rules. In effect, our computational inductions are buried in
the ordinary arithmetic on the time variable. We find it simpler to treat termination
as a part of timing, and we find that timing is more easily understood and more
useful than termination alone.
E. C. R. Hehnet

Acknowledgement

Je veux remercier mes amis 5 Grenoble, Nicolas Halbwachs, Daniel Pilaud, et


John Plaice, qui n’ont enseigni que la recursion peut ctre controlee par le tic-tat
d’une hsrloge. J’apprends lentement, mais j’apprends. I also thank IFIP Working
Group 2.3, Theo Norvell, and Andrew Malton for criticism. Support is provided
by a grant from the Natural Sciences and Engineering Research Council of Canada.

You might also like