Now that we've expressed the semantics of a programming language in Maude, we can use Maude to prove things about how programs behave. This will involve specification, saying what the desired behaviour of a program is, and verification, showing that a program does indeed behave in the desired way.
For example,
suppose we have a program P that swaps the values held in the
variables 'x and 'y.
How can we express this behaviour in Maude's notation (with a view,
eventually, to proving that the program does in fact do this)?
When we talk of the values of the variables 'x and
'y, this has to be relative to some Store
(after all, Stores associate values with variables).
Given any store S, the Store
S ; P
is the Store that results from running the program P in the
Store S.
Thus, we think of S as the "initial state", and
S ; P as being the "final state".
To say that P swaps the values in the variables 'x
and 'y means that in the final state
(S ; P), 'x has the value
that 'y had in the initial state (S):
S ; P [[ 'x ]] is S[[ 'y ]]
and similarly:
S ; P [[ 'y ]] is S[[ 'x ]]
says that the final value of 'y is the initial value of
'x.
The specification of the program P is the statement that the
above equations hold for all Stores S.
A program that satisfies this specification is:
't := 'x ; 'x := 'y ; 'y := 't
that is, the equations above will be true (for all Stores S)
if we replace P by the given program.
We can verify this with the following Maude code:
th SWAP-PROOF is
including SEMANTICS .
op s : -> Store .
endth
red s ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'x ]] is s[['y]] .
red s ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'y ]] is s[['x]] .
Both reductions give true as result, so by the Theorem of
Constants, we can conclude
S : Store) S ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'x ]] = S[['y]] S : Store) S ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'y ]] = S[['x]] 'x and 'y.
The general goal of specification is to give some more or less formal description of how a program behaves. Here, we are interested in giving a formal statement in Maude about the behaviour of a program; then we can use Maude to prove that a program really does behave in the specified way. The specification of the "swap" program above is an example, but it is often useful to split a specification up into a "precondition" and a "postcondition", where the precondition is a statement about the initial state, and the postcondition is a statement about the final state. This also has the advantage that we can describe the desired behaviour without saying what the program is; this means that we can treat specification as completely independent of implementation (after all, there may be many different ways of implementing the desired behaviour). In Maude notation:
ops pre post : Store -> Bool .
says that pre and post are both statements
about Stores. The idea is that the pre- and post-conditions specify
a program: a program P satisfies the specification
if whenever
P is run in a state that satisfies the precondition,
then the
postcondition holds in the final state. That is,
P satisfies the specification iff:S, if pre(S), then
post(S ; P).
var S : Store .
eq pre(S) = 0 <= (S[['x]]) .
eq post(S) = (S[['y]]) is (S[['x]]) div 2 .
then this says that if the value of 'x in the initial state is
at least 0, then in the final state, the value of 'y
is the value of 'x divided by 2
(integer division).
A program that satisfies this specification is one that computes
integer division by 2; for example:
'y := 0 ; 'r := 'x ;
while 2 <= 'r
do
'y := 'y + 1 ;
'r := 'r - 2
od
(we'll see how to verify this later).
However, another program that satisfies this specification is:
'x := 0 ; 'y := 0
because for all Stores S,
post(S ; 'x := 0 ; 'y := 0) =
(S ; 'x := 0 ; 'y := 0 [['y]]) is (S ; 'x := 0 ; 'y := 0 [['y]]) div 2
which is certainly true (0 = 0 div 2).
How can we rule out such "cheating" implementations?
What we want to say is that in the final state, the value of 'y
is equal to the initial value of 'x on integer division
by 2. That is, we want to define
var S : Store .
eq post(S) = (S[['y]]) is X0 div 2 .
where X0 is the initial value of 'x.
We can stipulate this in Maude notation by "fixing" the value of X0
in the precondition; our whole specification now becomes:
ops pre post : Store Int -> Bool .
var S : Store .
var X0 : Int .
eq pre(S, X0) = (S[['x]]) is X0 and 0 <= X0 .
eq post(S, X0) = (S[['y]]) is X0 div 2 .
Our notion of correct implementation now universally quantifies over X0:
P satisfies the specification iff:S and for all Integers X0,
if pre(S, X0), then
post(S ; P, X0). pre and post,
this is equivalent to:
P satisfies the specification iff:S and for all Integers X0,
if (S[['x]]) is X0 and 0 <= X0, then
(S ; P [['y]]) is X0 div 2.
The above definition of satisfaction could be generalised by allowing
any number of variables X0. For example, a specification of a
program to swap the values of 'x and 'y is
ops pre post : Store Int Int -> Bool .
var S : Store .
vars X0 Y0 : Int .
eq pre(S, X0, Y0) = (S[['x]]) is X0 and (S[['y]]) is Y0 .
eq post(S, X0, Y0) = (S[['x]]) is Y0 and (S[['y]]) is X0 .
which says that 'x ends up with the initial value of
'y, and vice-versa.
More precisely, a program P satisfies the program iff
S and for all Integers X0, and Y0,
S[['x]] = X0,
and S[['y]] = Y0,
S ; P [['x]] = Y0 and
S ; P [['y]] = X0.
Sometimes we need to define one or more functions in order to write a
specification.
For example, suppose we wanted to specify a program that computes the
factorial of (the value of) 'x.
Maude doesn't have factorial as a "built-in" operation on Integers,
so we need to define it:
fmod FACTORIAL is pr ZZ .
op fac : Int -> Int .
var I : Int .
cq fac(I) = 1 if I <= 1 .
cq fac(I) = fac(I - 1) * I if 1 < I .
endfm
Now we can specify a program that computes the factorial of 'x
and stores it in a variable 'f by:
fmod FACTORIAL-SPEC is pr FACTORIAL .
ops pre post : Store Int -> Bool .
var S : Store .
var X0 : Int .
eq pre(S, X0) = (S[['x]]) is X0 .
eq post(S, X0) = (S[['f]]) is fac(X0) .
endfm
If we want to specify a program that requires 'x to start
off with a non-negative value, we could change the precondition to:
eq pre(S, X0) = (S[['x]]) is X0 and 0 <= X0 .
Similarly, a program to compute the absolute value of 'i
and store it in 'n is specified as follows:
fmod ABS-SPEC is pr ZZ .
op abs : Int -> Int .
var I : Int .
cq abs(I) = - I if I < 0 .
cq abs(I) = I if 0 <= I .
ops pre post : Store Int -> Bool .
var S : Store .
var X0 : Int .
eq pre(S, X0) = (S[['i]]) is X0 .
eq post(S, X0) = (S[['n]]) is abs(X0) .
endfm
'x
(yes, the program is trivial!).'x to the sum of the values
of the variables 'y and 'z
(yes, the program is trivial!).'x to the variable
'y
(yes, the program is trivial!).'x to the maximum of the
values of 'a and 'b.
(Note that you will need to specify the operation
op max : Int Int -> Int .
that returns the maximum of the two given Integers.) ops pre post : Store Int -> Bool . var S : Store . var X0 : Int . eq pre(S,X0) = (S[['x]]) is X0 and 0 <= X0 . eq post(S,X0) = 2 * (S[['p]]) + (S[['r]]) is X0 and 0 <= (S[['r]]) and (S[['r]]) < 2 .
'p to 2 to the power of
the (initial) value of 'e, where (the initial
value of) 'e is at least 0 (i.e., this requirement should
be stated in the precondition). From the above, it's clear that a specification lays down the requirements for a program, but doesn't give (or use) any actual program code. This is exactly as it should be: the specification describes the required behaviour; the task of writing the program is left to the programmer.
Give implementations for each of the specifications in the Exercises above. Wherever possible, try to give more than one program that meets the requirements laid down in the specification.
Verification involves showing that a given program satisfies a given
specification. This typically involves showing that, after running the
program, certain variables have certain values. This is where Maude comes
in handy: as part of the process of showing that the program satisfies
the specification, we can "run" the program, or parts of it, by using
the equations in SEMANTICS, and show that after "running"
the program, the variables do indeed have the values expected.
For example, recall the example of swapping the values of the variables
'x and 'y. One implementation of this
given by the program
't := 'x ; 'x := 'y ; 'y := 't
We can verify that this program does indeed swap the values of 'x
and 'y by doing the following reduction in Maude:
red s ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'x ]] .
The result is s[['y]]; similarly,
red s ; 't := 'x ; 'x := 'y ; 'y := 't [[ 'y ]] .
gives the result s[['x]].
From these two reductions, we can conclude that the program does indeed
swap the values of 'x and 'y.
The nice thing here is that Maude has done all the tedious work for us.
(As an exercise, you might like to go through the tedious details
yourself....)
We argued above that it was useful to use pre- and post-conditions, so that a desired behaviour can be specified without having to give an implementation (i.e., a particular program). In this format, the specification of swapping variables was
ops pre post : Store Int Int -> Bool .
var S : Store .
vars X0 Y0 : Int .
eq pre(S, X0, Y0) = (S[['x]]) is X0 and (S[['y]]) is Y0 .
eq post(S, X0, Y0) = (S[['x]]) is Y0 and (S[['y]]) is X0 .
A program P (e.g., the program given above)
satisfies this specification iff
S and for all Integers X0, and Y0,
S[['x]] = X0,
and S[['y]] = Y0,
S ; P [['x]] = Y0 and
S ; P [['y]] = X0.
op s : -> Store .
ops x0 y0 : -> Int .
and then we have to show:
s[['x]] = x0,
and s[['y]] = y0,
s ; P [['x]] = y0 and
s ; P [['y]] = x0. 'x is x0 and that the starting value
of 'y is y0:
eq s[['x]] = x0 .
eq s[['y]] = y0 .
We now show the "then" part by reducing:
red s ; P [[ 'x ]] is y0 . ***> should be: true
red s ; P [[ 'y ]] is x0 . ***> should be: true
Alternatively, we could reduce:
red s ; P [[ 'x ]] . ***> should be: y0
red s ; P [[ 'y ]] . ***> should be: x0
Either will do, as both alternatives show that the values of 'x
and 'y have been swapped by our program P.
To summarise, the following theory and reduction verify that the "swap" program satisfies its specification:
th SWAP-PROOF is
including SEMANTICS .
ops pre post : Store Int Int -> Bool .
var S : Store .
vars X0 Y0 : Int .
eq pre(S, X0, Y0) = (S[['x]]) is X0 and (S[['y]]) is Y0 .
eq post(S, X0, Y0) = (S[['x]]) is Y0 and (S[['y]]) is X0 .
*** for the Theorem of Constants:
op s : -> Store .
ops x0 y0 : -> Int .
*** assume pre(s,x0,y0):
eq s[['x]] = x0 .
eq s[['y]] = y0 .
*** an abbreviation for the program:
let p = 't := 'x ; 'x := 'y ; 'y := 't .
endth
*** show post(s ; p, x0, y0):
*** should be: true
***
red post(s ; p, x0,y0) .
The final reduction here gives true as result,
so by the Theorem of Constants, we conclude the program is correct:
S and for all Integers X0 and Y0,S[['x]] = X0
and S[['y]] = Y0,
then
(S ; p [['x]]) = Y0 and
(S ; p [['y]]) = X0, p is the program as above.
Show that the following program also satisfies the "swap" specification:
'x := 'x + 'y ; 'y := 'x - 'y ; 'x := 'x - 'y .
We've seen an example of verifying a program by assuming the precondition
holds in the initial state (s)
and then showing the postcondition holds in the
final state (s ; p).
This simple way of verifying programs really only works for simple programs
that consist of sequences of assignments.
More complex programs require more complex proof strategies.
Recall the specification of a program to set 'n
to the absolute value of 'i:
fmod ABS-SPEC is pr ZZ .
op abs : Int -> Int .
var I : Int .
cq abs(I) = - I if I < 0 .
cq abs(I) = I if 0 <= I .
ops pre post : Store Int -> Bool .
var S : Store .
var X0 : Int .
eq pre(S, X0) = (S[['i]]) is X0 .
eq post(S, X0) = (S[['n]]) is abs(X0) .
endfm
A program that satisfies this specification is
if 'i < 0 then 'n := - 'i else 'n := 'i endif .
We might try to prove this correct as follows:
th ABS-PROOF is protecting ABS-SPEC .
including SEMANTICS .
let p = if 'i < 0 then 'n := - 'i else 'n := 'i endif .
op s : -> Store .
op x0 : -> Int .
*** assume pre(s,x0):
eq s[['i]] = x0 .
endth
*** show post(s ; p, x0):
red post(s ; p, x0) .
Maude will start this reduction as follows:
post(s ; p, x0)
=
(s ; p [['n]]) is abs(x0)
=
(s ; if 'i < 0 then 'n := - 'i else 'n := 'i endif [[ 'n ]]) is abs(x0)
and then stop; the last line of the above is what Maude returns as the result
of the reduction.
Maude stops a reduction when no equations can be applied; before stopping,
Maude will try to apply the equation (from SEMANTICS):
var S : Store .
var T : Tst .
vars P1 P2 : Pgm .
cq S ; if T then P1 else P2 endif = S ; P1 if S[[T]] .
In this case, the condition on this equation is:
s[[ 'i < 0 ]]
Maude will only apply the conditional equation if this condition can be reduced
to true:
s[[ 'i < 0 ]]
=
(s[['i]]) < (s[[0]])
=
x0 < 0
this cannot be reduced any further, so Maude doesn't apply the conditional
equation.
Maude will also try to apply the other conditional equation:
var S : Store .
var T : Tst .
vars P1 P2 : Pgm .
cq S ; if T then P1 else P2 endif = S ; P2 if not(S[[T]]) .
In this case, the condition on this equation is:
not(s[[ 'i < 0 ]])
Again,
Maude will only apply the conditional equation if this condition can be reduced
to true:
not(s[[ 'i < 0 ]])
=
not((s[['i]]) < (s[[0]]))
=
not(x0 < 0)
=
0 <= x0
Once again, this doesn't reduce to true, so Maude won't apply
the equation.
Our simple attempt at verifying the program has failed.
This is exactly as it should be; Maude can't reduce the term containing
the if_then_else_endif because we don't know whether x0
is less than 0, or greater than or equal to 0
(x0 is just a name we've introduced to represent the starting
value of 'i, which might be less than 0, or it might not be).
Of course, we do know that either x0 < 0 or
0 <= x0; one of these options must be the case.
Suppose x0 < 0. Then our program sets 'n
to - 'i. That is, it sets 'n to - x0,
which is abs(x0) (because, by assumption, x0 < 0),
which is exactly what the specification says should happen.
On the other hand, suppose 0 <= x0. Then our program sets
'n to 'i; that is, it sets 'n to
x0, which is abs(x0) (since we're assuming that
0 <= x0).
Again, this is exactly what the specification says should happen.
Our informal reasoning here is making use of case analysis:
we know that one of two or more cases must hold (x0 < 0 or
0 <= x0), and we go through each case separately to verify
that the program is correct in each case.
Note that in each case, we assume either x0 < 0 or
0 <= x0, and our reasoning makes use of that assumption.
We've seen that we can assume something in Maude by declaring equations.
For the first of our cases above, we can assume x0 < 0 by
adding the equation
eq x0 < 0 = true .
(observe that this is exactly what is needed to allow Maude to apply the
first conditional equation we looked at above, in our failed attempt at
verification!).
For example, the first case becomes, in Maude notation:
*** case: x0 < 0:
***
th CASE1 is
including ABS-PROOF .
*** assume x0 < 0:
eq x0 < 0 = true .
endth
*** show post(s ; p, x0):
***
red post(s ; p, x0) . ***> should be: true
This time, the reduction succeeds:
post(s ; p, x0)
=
(s ; p [['n]]) is abs(x0)
=
(s ; if 'i < 0 then 'n := - 'i else 'n := 'i endif [[ 'n ]]) is abs(x0)
=
(s ; 'n := - 'i [[ 'n ]]) is abs(x0)
=
(s[[ - 'i ]]) is abs(x0)
=
- (s[['i]]) is abs(x0)
=
- x0 is abs(x0)
=
- x0 is - x0
=
true
Hooray!
The second case proceeds similarly:
*** case: 0 <= x0:
***
th CASE2 is
including ABS-PROOF .
*** assume 0 <= x0:
eq 0 <= x0 = true .
endth
*** show post(s ; p, x0):
***
red post(s ; p, x0) . ***> should be: true
(Guess what the exercise is?)
Thus, our program is correct in both cases.
Our verification has succeeded: the program does compute the absolute
value of 'i.
Give a program that sets 'x to the maximum of the
values of 'a and 'b (cf. Exercise 4 above).
Give a Maude proof score that shows the program is correct.
th POWERS is including SEMANTICS .
ops pre post : Store Int -> Bool .
var S : Store .
var x0 : Int .
eq pre(S, x0) = (S[['x]]) is x0 and 0 <= x0 .
eq post(S, x0) = (S[['p]]) is 2 ** x0 .
endth
where _**_ is Maude notation for exponentiation.
A program to implement this is
'p := 1 ; 'c := 0 ;
while 'c < 'x
do
'p := 'p * 2 ;
'c := 'c + 1
od
This program works by keeping the value of 'p to be the
equal to 2 to the power of 'c; this is achieved initially by
'p := 1 ; 'c := 0
then each time the body of the loop is iterated, the value of 'c
is increased by one and the value of 'p is doubled:
'p := 'p * 2 ;
'c := 'c + 1
so that 'p remains equal to 2 to the power of 'c.
The loop is exited when 'c is equal to 'x;
therefore, when execution of the program ends, the value of 'p
is 2 to the power of 'x.
The above paragraph gives an informal but persuasive proof that the program satisfies the specification; we will make the reasoning precise, and use it to structure Maude proofs of correctness for while-loops.
The statement that 'p is equal to
2 to the power of 'c is an invariant of the loop;
that is:
'p
and 'c), and 'p is equal to 2 to the power of 'c.
Of course, the statement that 'p is equal to
2 to the power of 'c has to be made relative to a given store.
A more precise form of the statement is that the value of
'p is equal to
2 to the power of the value of 'c.
An invariant is therefore a statement about a store, which we might write
as inv(S), where
inv : Store -> Bool .
In our example, for any store S,
inv(S) = (S[['p]]) is 2 ** (S[['c]]) .
As far as our proof is concerned, what we need to show is:
s that satisfies the
precondition (i.e., if pre(s)), then the initial
assignments to 'p and 'c make the invariant true;
i.e.,
inv(s ; 'p := 1 ; 'c := 0)
and s such that inv(s)
and s[['c < 'x]] (we only execute the body of the loop if
the guard is true), then the invariant remains true after the body is
executed; i.e.,
inv(s ; 'p := 2 * 'p ; 'c := 'c + 1)
and s (which we think of as the final state on exiting the loop)
such that inv(s) (this will hold of the final state because of
the first two points above) and not(s[['c < 'x]]) (because
we exit the loop when the guard is false), then post(s) holds.
The final point says that if we are in a state s in which
the invariant holds (and we know it will hold in the final state of our loop
if we can show that it starts true and stays true: the first two points above),
i.e.,
(s[['p]]) is 2 ** (s[['c]])
and the guard is false, i.e.,
(s[['x]]) <= (s[['c]])
then we must show that the postcondition holds; i.e.,
(s[['p]]) is 2 ** x0
But this latter doesn't follow from the previous two assertions!
We need a stronger invariant.
A little thought suggests we need to say that the value of 'c
is never greater than that of 'x, and that the value of
'x remains unchanged;
an invariant for our program
could therefore be:
inv(S, x0) = (S[['p]]) is 2 ** (S[['c]]) and (S[['c]]) <= (S[['x]]) and (S[['x]]) is x0 .
We need to show that:
inv holds before the loop begins; inv stays true each time the loop iterates; and
inv and the guard being false imply the postcondition holds.
th POWER-PROOF is including POWER .
op init : -> Program .
eq init = 'p := 1 ; 'c := 0 .
op guard : -> BooleanExpression .
eq guard = 'c < 'x .
op body : -> Program .
eq body = 'p := 'p * 2 ; 'c := 'c + 1 .
*** program = init ; while guard do body od
op inv : Store Int -> Bool .
var S : Store .
var X0 : Int .
eq inv(S, X0) = (S[['p]]) is 2 ** (S[['c]]) and (S[['c]]) <= (S[['x]]) and (S[['x]]) is X0 .
op s : -> Store .
op x0 : -> Int .
endth
*** show init establishes invariant:
th INIT is
including POWER-PROOF .
*** assume pre(s,x0):
eq s[['x]] = x0 .
eq 0 <= x0 = true .
endth
red inv(s ; init, x0) .
*** show inv is kept true:
th INV is
including POWER-PROOF .
*** assume inv(s,x0):
eq s[['p]] = 2 ** (s[['c]]) .
eq (s[['c]]) <= x0 = true .
eq s[['x]] = x0 .
*** assume s[[guard]]:
eq (s[['c]]) < x0 = true .
endth
red inv(s ; body, x0) .
*** show postcondition holds after loop ends:
th POST is
including POWER-PROOF .
*** assume inv(s,x0):
eq s[['p]] = 2 ** (s[['c]]) .
eq (s[['c]]) <= x0 = true .
eq s[['x]] = x0 .
*** assume s[[guard]] is false:
eq x0 <= (s[['c]]) = true .
*** therefore:
eq s[['c]] = x0 .
endth
red post(s, x0) .
If all the reductions return true, then
this shows that the program satisfies the specification.
As an exercise, run this proof score in Maude.
In general, suppose we have a program of the form
init ; while guard do body od
and we want to show that this program satisfies a specification
given by pre- and postconditions pre and post.
We can do this by finding an invariant inv (this is where
intuition, understanding and possibly ingenuity come into play)
and constructing a proof score of exactly the form above
(since we introduced the abbreviations init,
guard and body).
A further example of a proof score that shows the correctness of a program to compute factorials (as in the lectures) is available in three stages:
2 ** 'x:
'p := 1 ;
while 0 < 'x
do
'p := 'p * 2 ;
'x := 'x - 1
od
Give a Maude proof score that verifies this.
Note that the invariant for this program is different from the invariant
in the example above.
'p) and remainder on division by two (the result is stored
in 'r).
Give a program that satisfies this specification, and prove it correct.