Lecture 10:
Verifying the Correctness of Programs

Today's dominant practice in the software industry (and when writing up assignments) is to demonstrate the correctness of programs empirically. The simplest form of this technique consists of feeding various inputs to the tested program and verifying the correctness of the output. In some cases exhaustive testing is possible, but generally it is not. 

More sophisticated testing tries to choose the inputs so that all, or at least the majority of the possible execution paths are examined, and to test modules or units as well as the overall software behavior. This is generally referred to as the degree of code coverage. Independent of how sophisticated such testing is, empirical methods do not actually prove that a respective program is correct.

The only thing we can actually prove with an  empirical approach is that the program is incorrect - as a single example of incorrect behavior suffices. Absent an observation of incorrect behavior, however, we cannot know - in general - whether the program is correct, or whether we have just not tested it with an input that would trigger an error.

As we all know, incorrect program behavior is pervasive. Some program errors are only irritating, but some can endanger life and limb. Next time you fly spend a minute pondering the importance of correct program behavior for your airplane's navigational system. Would you like the manufacturer to "think" that the program is correct based on a number of empirical tests, or would you prefer an unambiguous and definitive proof?

Today we are going to discuss some simple program correctness proofs that use the substitution model and induction.

Induction Proofs

In other classes such as CS211 you have seen induction over the natural numbers.  Induction is a technique that we can use to prove that certain properties hold for each element of a suitably chosen infinite set. The most common form of induction is that of mathematical induction, whereby we establish the truth of statement for all natural numbers, or - more generally - for all elements of a sequence of numbers. Induction can also be performed on more complicated sets, like pairs of non-negative integers, or binary trees (see below).

An inductive argument can be thought of as being not a proof per se, but a recipe for generating proofs. First, the relevant property P(n) is proven for the base case, which for the natural numbers often corresponds to n = 0 or n = 1. For more general inductively-defined sets the base case is an initial element of the set.  For instance for lists, [] is a natural initial element. For the natural numbers we assume that P(n) is true, and we prove P(n+1).  More general ly we assume P(n) is true and prove P(Succ(n)). The proof for the base case(s) and the proof from P(n) to P(Succ(n)) provide a method to prove the property for any m in the set, by successively proving P(0), P(1), ..., P(m), or more generally P(b), P(Succ(b)), ..., P(Succ...(Succ(b))) for some base element b.

We can't explicitly perform the infinite number of proofs necessary for all choices of m, but the recipe that we provided assures us that such a proof exists for all choices of m.

To reduce the possibility of error, we will structure all our induction proofs rigidly, always highlighting the following four parts:

  1. The general statement of what we want to prove;
  2. The specification of the set we will perform induction on;
  3. The statement and proof of the base case(s);
  4. The statement of the induction hypothesis (generally, we will assume that P(n) holds, but sometimes we need stronger assumptions, see below), the statement of P(n+1) or more generally P(Succ(n)) and proof of the induction step (or case).

We prefer that you use precise notation for all the statements that you make (see our examples below), however we also accept semi-informal statements in plain English, assuming that they are correct, unambiguous, and complete. For example, the statement "given a natural number n >= 0, the sum of the first n natural numbers is equal to n(n + 1) / 2" is acceptable. However, the statement "the sum of the first n numbers is n(n + 1) / 2" is incomplete: we are not given enough information about n. Of course, we can infer some things about n - it can not be a general real number, or a negative number, because then the phrase "the first n numbers" (by the way, what kind of numbers?) would not make sense. Don't let us guess, however, fill in all the necessary details!

Aside

Sometimes we can not prove P(n+1) based solely on P(n), but we can succeed with the proof if we assume that the property holds for all natural numbers m <= n. Formally: given a natural number n >= 0 we want to prove that (for all m s.t. 0 <= m <= n: P(m) holds) => P(m+1) holds. This form of induction is called strong induction.

The Substitution Model and Correctness Proofs

Induction on the Set of Natural Numbers

Consider the following simple function:

(* Computes the product of an integer a and a natural number b (i.e., b>=0) *)
fun times (a, b) =
  if (b = 0) then 0
  else a + times(a,b-1)

We want to prove formally that times(a,b) = a*b for all natural numbers b.  Note that we induct over b, why?  We write down the proof by following precisely the four steps specified above:

  1. We want to prove that, for all natural numbers b, times(a,b) = a*b
  2. We perform induction on the set of natural numbers N.
  3. Base case (corresponds to n = 0): we need to prove that times(a,0) = 0.

    Our task is to prove the equivalence of an SML expression and a mathematical one.  Thus in the proof we will need to determine the value of SML expressions.  We already know how to do this using the substitution model. To reduce the size of the proof we will skip all non-essential steps in the application of the substitution model.

    eval(times(a,0))
    
    = eval(if 0 = 0 then 0 else a + times(a, 0 - 1) 
       by the substitution rule for function application
    
    = 0 by the substitution rule for if: the condition is true so the then branch is evaluated
    

    This proves the base case.
     

  4. Assuming that, for any given natural number b, times(a,b) = a*b, we will prove that times(a,b+1) = a (b + 1)

    Note: here b+1 is not an SML expression but a notation for the successor of b in the set of natural numbers. Because this is not an SML expression, you should not not try to evaluate it in the context of the substitution model. If this confuses you, you can introduce the notation m = b+1, and just use m whenever we would have used b+1.

    We should point out that you should not confuse the two uses of b: we used the same symbol for the formal argument of function times, and for the natural number that appears in the induction hypothesis. If you are confused by this, you can rewrite the definition of times to use another identifier for its argument, say, k.

    eval((fun times(a,b) = BODY) a,b+1)
    
    = eval(if b+1 = 0 then 0 else a + times(a, (b+1 - 1))
      by substitution rule for function application 
    
    = eval(a + times(a, (b+1 - 1))
      by substitution rule for if-then-else, note that b>=0 because it is a natural number, 
       hence b+1 > 0, and this implies that the condition in 'if' evaluates to false
    

    Now we note that b+1 - 1 is simple b, and thus the expression involving times represents times(a,b), which we know from the induction hypothesis to be equal to a*b.

    eval(a + times(a, b)
    = [using the induction hypothesis]
      a + a*b
    = a(b+1) as was to be proved
    

    This proves the induction step and completes the induction proof.

Induction on Lists

There are two different approaches to induction over lists (or sets, or trees or other structures). One is to map from the structure to the natural numbers.  For instance, for lists the length provides such a mapping (similarly for sets, whereas for trees the depth is generally the most natural mapping).  The other is to induct directly over the set, because the set itself is defined inductively.  This is commonly referred to as structural induction.

For example 'a lists of are defined inductively:

The principle of (weak) induction that for 'a lists is as follows.  To show that P holds for all 'a lists we need only show:

  1. P([]) is true
  2. P(x) => P(a::x)

Note that this second condition requires us to show that if P holds for an 'a list x then it holds for all 'a lists of the form a::x (i.e., the argument cannot depend on a).

Consider the following simple function for computing the length of a list.  We know from the ML type system that this function always returns an integer.  However, it would be useful to be able to prove that it always returns a natural number.


fun length (l) =
  if null(l) then 0
  else 1 + length(tl(l))

We start by assuming that null and tl are built-in operators (ie. we do not need to use the substitution model to determine their values), where null(x) is true when x=[] and for any other x is false, and tl(a::x) is x.

1) State what is to be proven: P: For all 'a lists x, length(x) returns a natural number.

2) By induction over the list x

3) Base case: Show P([]): length([]) is a natural number.

length([])
if null([]) then 0 else 1 + length(tl([]))
0
0 is by definition a natural number

4) Inductive case.
Show P(x) =>P(a::x).  That is, length(x) is a natural number => length(a::x) is a natural number

length(a::x)
if null(a::x) then 0 else 1 + length(tl(a::x))
1+length(tl(a::x))
1+length(x)
By IH length(x) is a natural number, and by definition of natural numbers if x is a natural number so is x+1, thus we have shown length(a::x) is a natural number.

Since P([]) holds and P(x)=>P(a::x) holds we have shown length(x) always returns a natural number.

Why does this structural form of induction work?  In essence we can prove by induction over the natural numbers that correspond to the length of a list that a given property holds for all lists (a mapping from lists to their length).

Thus you can either use structural induction directly on lists, or natural induction over their length.

Similar forms of argument hold for sets, and closely related arguments for trees (inducting over depth).

 

 


 

 

 

 

In the more general setting, there are limits as to what we can do in proving properties of programs. It can be proven, for example, that there are no general algorithms that check (or prove) that a program correctly implements a given specification. We cannot even give general solutions for certain much simpler problems. For example, there is no algorithm that would be able to decide whether an arbitrary program will terminate execution for a given input. Wouldn't it be nice to have a tool that would prevent a program from even starting because the tool could determine a priori that the program would get into an infinite loop?  The lack of general solutions does not mean that program correctness cannot be proven in certain particular cases, or in a context that is restricted in some sense.  For critical pieces of software, particularly where lives are at stake, proof techniques can play an important role in software verification.