Verification of Program Correctness..

 

When writing a program, we often make mistakes and have to take time to debug it.  Syntax errors are caught and flagged by a good compiler.  Run-time errors that abort the program or that produce wrong answers are usually found by testing the program using a test data set.  A good test data set should include examples of all possible data items.  For example, test data for the postfix stack evaluation algorithm should contain expressions using all possible operators and operands.  In addition all possible ways of making an expression invalid should be illustrated.

 

The larger a program gets, the more varied are the paths through it and the more complicated the test data becomes.  It is then easy to overlook some section of the program or some possible input to it.  While a number of computer errors publicized in the media are actually human data entry errors, many are due to unresolved bugs in programs.  These are usually in sections only entered occasionally.  Computer vendors such as Microsoft simply assume that their very large system programs contain a lot of bugs.  As these systems are used, bugs are discovered and (hopefully) corrected.  However, usually before they are all found, a new system is released with the potential for many new bugs.  Software development is a continuing process.  Except for very short programs, no one claims their work is error-free. We can prove the correctness of short programs.  The method is well understood, but it is long and tedious.  Furthermore, if the program is of moderate size, the proof becomes so long and complicated, that a person can't follow it.  It requires a computer program to check the proof, and this program itself is then subject to error.  Currently, there is no way out of this corner, but scientists continue to work on the problem. We will only look briefly at proofs of correctness, just enough so that you know what they are.

 

For non-iterative code, we use assertions.  These are of the form

{precondition}  program  {postcondition}

where we assert:  If the precondition is true before execution of the program, then the postcondition will be true afterwards.

 

Examples:

// precondition: count = 1

sum = count ++;

// postcondition: sum = 2

 

// precondition: count = number

number ++;

// postcondition: count = number - 1

 

// no precondition

if first < second

{

       // first < second

low = first;

high = second;

}

 else

       {      // second <= first   

low = second;

               high = first;

       }

       // postcondition: low <= high

 

       int temp;

       // precondition: (x = a) and (y = b)

       {

               temp = x;        // temp = a

               x = y;             // x = b

               y = temp;        // y = a

       }

       // postcondition: (x = b) and (y = a)

 

       // no precondition

       int temp;

       if (mid < low)

       {

               temp = mid; mid = low; low = temp;

       }

       // low <= mid

 

       if (high < mid)

       {

               temp = high; high = mid; mid = temp;

       }

       // (mid <= high) and (low <= high)

 

       if (mid < low)

       {

               temp = mid; mid = low; low = temp;

       }

       // postcondition (low <= mid) and (mid <= high)

 

For iteration, we use conditions called loop invariants.  These are more complicated.  They must express what the loop is supposed to be doing.  Consider a while loop:

 

// Loop invariant true, B true (or loop won't be entered)

while (B)

{

       // Loop invariant true, B true

       S1;

// Loop invariant and B may be false

       S2;  

       // Loop invariant true, B may be true or false

}

// Loop invariant true, B false (or loop won’t be left)


Example: Method that computes the nth power of 2.

 

// precondition: (n >= 0)

public int powerOfTwo ( int n)

{

int count = 0, product = 1;

 

// product = 2 count

while (count < n) do

       {

               // (count < n) and (product = 2 count)

 product = 2 * product;

               // (count < n) and (product = 2 (count+1))

               count ++;

// (count <= n) and (product = 2 count)

       }

       // (count = n) and (product = 2 count)

return product;

} // method powerOfTwo

// postcondition: 2 n  returned

 

 

The loop invariant is

(product = 2 count).

Note that the loop condition, (count < n), is true when we enter the loop and false on exit.  Also note that if the precondition, (n >= 0), is false, the loop won't be entered.  In that case, the postcondition, (count = n), will not be true.  Finally, such a proof makes the loop condition clear.  It should be (count < n) not (count <= n).  The latter condition causes an off-by-one error or boundary error.  These are common with beginning programmers.