COMP 317: Semantics of Programming Languages

Program Verification



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 as desired. Our program really does swap the values of 'x and 'y.



Specification

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:
for all Stores S, if pre(S), then post(S ; P).
For example, if we define
    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:
for all Stores S and for all Integers X0, if pre(S, X0), then post(S ; P, X0).
Expanding the definitions of pre and post, this is equivalent to:
P satisfies the specification iff:
for all Stores S and for all Integers X0, if (S[['x]]) is X0 and 0 <= X0, then (S ; P [['y]]) is X0 div 2.
This now rules out our second, specious implementation.

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
for all Stores S and for all Integers X0, and Y0,
if S[['x]] = X0, and S[['y]] = Y0,
then 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

Exercises

  1. Specify a program that doubles the value of the variable 'x (yes, the program is trivial!).
     
  2. Specify a program that sets 'x to the sum of the values of the variables 'y and 'z (yes, the program is trivial!).
     
  3. Specify a program that adds the value of 'x to the variable 'y (yes, the program is trivial!).
     
  4. Specify a program that sets '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.)
     
  5. Explain in words what the following specification requires:
      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 .
    

     
  6. Specify a program that sets '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).



Implementation

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.

Exercise 7

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

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
for all Stores S and for all Integers X0, and Y0,
if S[['x]] = X0, and S[['y]] = Y0,
then S ; P [['x]] = Y0 and S ; P [['y]] = X0.
The Theorem of Constants tells us we can prove "for-all"-sentences by introducing new constants; in this case:
    op  s : -> Store .
    ops x0 y0 : -> Int .
and then we have to show:
if s[['x]] = x0, and s[['y]] = y0,
then s ; P [['x]] = y0 and s ; P [['y]] = x0.
How can we prove this sort of conditional ("if ... then ...") statement? Typically, we assume the "if" part, then show that the "then" part is true. In Maude, we can "assume" something to be true by adding one or more equations. In our example, we declare two equations which say that the starting value of '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:
for all Stores S and for all Integers X0 and Y0,
if S[['x]] = X0 and S[['y]] = Y0, then (S ; p [['x]]) = Y0 and (S ; p [['y]]) = X0,
where p is the program as above.


Exercise 8

Show that the following program also satisfies the "swap" specification:

  'x := 'x + 'y  ;  'y := 'x - 'y  ;  'x := 'x - 'y .


Conditionals

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.


Exercise 9

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.


While-loops

A program to compute powers of two might be specified by:
    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:

From these two observations, it follows that when the loop terminates, '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:
  1. if we start in a store 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
  2. the invariant stays true each time the body of the loop is executed; i.e., if we start in a state 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
  3. when we exit the loop, the postcondition holds; i.e., if we are in a state 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: The Maude proof score is:
    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.

Summary

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:

Also, the Fibonacci example is available here.

Exercises

  1. The following program also computes 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.
     
  2. Exercise 5 above (in the section on Specification) specifies a program that computes the results of integer division by two (the result is stored in 'p) and remainder on division by two (the result is stored in 'r). Give a program that satisfies this specification, and prove it correct.


Grant Malcolm