SE 504
Development of a program to compute Fibonacci numbers:
An example of employing the "strengthening the loop invariant" heuristic

The Fibonacci function is often defined as follows:

fib.0  =  0
fib.1  =  1
fib.k  =  fib.(k−2) + fib.(k−1)   (for k>1)

Viewing this function as defining the sequence

< fib.0, fib.1, fib.2, fib.3, ... >

gives rise to the so-called Fibonacci sequence

< 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... >

in which, beginning at the third element, each element is the sum of the preceding two.

Let's develop a program that, given as input a positive integer N, calculates fib.N. Here is a specification:

|[ con N : int;  { N > 0 }
   var m : int;

   m := ?

   { Q : m = fib.N }
]|

It seems likely that we will need to use repetition to solve this problem. Applying the replace a constant by a variable heuristic, we can rewrite the postcondition Q as the slightly stronger

Q' : m = fib.n  ∧  n = N

Applying the remove a conjunct heuristic, we take the first conjunct of Q' as a loop invariant and the second as the negation of the loop's guard. We obtain the following refinement of the specification:

|[ con N : int;  { N > 0 }
   var m : int;
   var n : int;

   n, m := ?, ?;
   { I : m = fib.n }
   do n ≠ N  --->  n, m := ?, ?;
   od
   { Q': m = fib.n  ∧  n = N }
   { Q : m = fib.N }
]|

First we consider how to initialize n and m so as to "truthify" the proposed loop invariant. That is, we wish to find expressions to replace the question marks so as to truthify the Hoare Triple

{N>0} n,m := ?,? {I}

(The post-condition of initialization is the loop invariant; the pre-condition of this program is N > 0, which, because it involves only constants, we view as a "global invariant".) The obvious possibilities for initialization are n,m := 0,0 (reflecting the fact that fib.0 = 0) and n,m := 1,1 (reflecting the fact that fib.1 = 1). We choose the latter for reasons that will become clear later.

Either way, this suggests that n should be increased within the body of the loop so as to eventually reach value N. The simplest way to do that is by incrementing n, so that's what we propose. These refinements lead to the program

|[ con N : int;  { N > 0 }
   var m : int;
   var n : int;

   n,m := 1,1;
   { I : m = fib.n }
   do n ≠ N  --->  n,m := n+1,?;
   od
   { Q' : m = fib.n   ∧   n = N }
   { Q : m = fib.N }
]|

It remains to determine how to update m inside the body of the loop. We attempt to calculate the appropriate expression E by proving item (ii) among the five proof obligations for a loop:

(ii) {I ∧ B} n,m := n+1,E {I}

Assume I and B.

     wp.(n,m := n+1,E).I

  =    < wp assignment law >

     I(n,m := n+1,E)

  =    < defn of I; textual sub. >

     E = fib.(n+1)

At this point, our only manipulative opportunity arises from the third equation in the definition of fib. However, for that equation to apply requires that n+1>1, i.e., n>0. As we initialized n to 1 and thereafter all changes to n are due to incrementing it, it is clear that n>0 will be true. (Note: This explains why we initialized n to 1 rather than to 0! End of note.) Hence, we incorporate this condition into the loop invariant so that we can use it here as an assumption. Starting the proof over again with the stronger version of I (namely, I = I0 ∧ I1, where I0 : m = fib.n and I1 : n > 0), we get

(ii) {I ∧ B} n,m := n+1,E {I}

Assume I (i.e., m = fib.n and n > 0) and B (i.e., n ≠ N).

     wp.(n,m := n+1,E).I

  =    < wp assignment law >

     I(n,m := n+1,E)

  =    < defn of I; textual sub. >

     E = fib.(n+1)  ∧  n+1 > 0

  =    < assumption n > 0 implies 2nd conjunct; (Gries 3.39) >

     E = fib.(n+1)

  =    < assumption n > 0 implies n+1 > 1,
         justifying use of 3rd equation in defn of fib >

     E = fib.(n-1) + fib.n

  =    < assumption m = fib.n >

     E = fib.(n-1) + m

At this point, we observe that it would be nice to have a program variable, say r, guaranteed to have value fib.(n−1) at this point in execution, because then we could finish the derivation of E:


  =    < assumption r = fib.(n-1) >

     E = r + m

As we are free to introduce such a variable, we do so! That is, we strengthen the loop invariant by adding I2 : r = fib.(n-1) as a new conjunct. Of course, doing this brings with it the responsibility to introduce code to initialize r so as to truthify I2 and to update r during each loop iteration so as to preserve the truth of I2. The refined program is as follows:

|[ con N : int;  { N > 0 }
   var m, r : int;
   var n : int;

   n, m, r := 1, 1, ?;
   { I : I0 ∧ I1 ∧ I2, where
     I0 : m = fib.n, I1 : n > 0, and I2 : r = fib.(n-1) }
   do n ≠ N  --->  n, m, r := n+1, r+m, ?;
   od
   { Q' : m = fib.n   ∧   n = N }
   { Q : m = fib.N }
]|

Let's calculate the proper initialization to r, by deriving an expression F satisfying {N>0} n,m,r := 1,1,F {I}.

By the Hoare Triple Assignment Law), this is equivalent to [N>0 ===> I(n,m,r := 1,1,F)]. As assuming the antecedant provides no value, we simply prove the consequent:

     I(n,m,r := 1,1,F)

  =    < defn of I; text. sub. >

     1 = fib.1  ∧  1 > 0  ∧  F = fib.(1-1)

  =    < defn of fib; arithmetic, (Gries 3.39) >

     F = fib.0

  =    < defn of fib >

     F = 0 

Next we determine how to modify r during each iteration. As our previous work has demonstrated that execution of the loop body preserves the truth of I0 and I1 (neither of which mentions r), we use as a postcondition only I2.


     {I ∧ B} n,m,r := n+1, r+m, G {I2}

Assume I and B.

     wp.(n,m,r := n+1,r+m,G).I2

  =    < defn of I2; text. sub >

     G = fib.(n+1-1)

  =    < arithmetic >

     G = fib.n

  =    < assumption m = fib.n >

     G = m
We obtain the following program:

|[ con N : int;  { N > 0 }
   var m, r : int;
   var n : int;

   n,m,r := 1,1,0;
   { I : I0 ∧ I1 ∧ I2, where I0 : m = fib.n, I1 : n > 0, and I2 : r = fib.(n-1) }
   do n ≠ N  --->  n, m, r := n+1, r+m, m;
   od
   { Q' : m = fib.n   ∧   n = N }
   { Q : m = fib.N }
]|

All that remains is to provide a bound function. Here, the obvious choice is t : N-n. Technically, in order to prove item (iv), we need to include the condition n≤N within the loop invariant. (We can incorporate this into I1.) Clearly, this condition is truthified by initializing n to 1 (given that N>0 is a global invariant) and preserved by incrementing n when it is known that n<N, as is guaranteed by I ∧ B.

Here is the final, annotated program:

|[ con N : int;  { N > 0 }
   var m, r : int;
   var n : int;

   n,m,r := 1,1,0;
   { I : I0 ∧ I1 ∧ I2, where I0 : m = fib.n, I1 : 0<n≤N, and I2 : r = fib.(n-1) }
   { t : N-n }
   do n ≠ N  --->  n, m, r := n+1, r+m, m;
   od
   { Q': m = fib.n   ∧   n = N }
   { Q : m = fib.N }
]|

Given the roles played by variables m and r, one could make a reasonable argument that fibThis and fibPrev, respectively, would be better names.

Suppose that the precondition had been the weaker N≥0 rather than N>0. Since the program we developed above works for any N strictly greater than zero, all we need to do is to introduce a selection command, one branch of which handles the case N=0 and the other of which handles the case N>0. Of course, the body of the latter branch would be the loop (preceded by initialization code) that we developed above. The body of the former branch would simply set m to zero, as fib.0 = 0. Here is that program:

|[ con N : int;  { N ≥ 0 }
   var m, r : int;
   var n : int;

   if N = 0 ---> m := 0;
   [] N > 0 ---> 
      { N > 0 }
      n,m,r := 1,1,0;
      { I : I0 ∧ I1 ∧ I2, where I0 : m = fib.n, I1 : 0<n≤N, and I2 : r = fib.(n-1) }
      { t : N-n }
      do n ≠ N  --->  n, m, r := n+1, r+m, m;
      od
      { Q' : m = fib.n   ∧   n = N }
   fi
   { Q : m = fib.N }
]|

But this solution lacks some of the elegance of the original. Can we somehow avoid having to introduce the selection command in order to handle the case N = 0? It turns out that we can! The only place where our reasoning relies upon n>0 is in the proof of loop checklist item (ii), as there we made use of the fact that, by definition, n>0 implies fib.(n+1) = fib.(n−1) + fib.n. (But this equation fails to hold for n = 0, because fib.(−1) is not even defined.)

But there is no reason why we cannot modify the definition of fib to enlarge its domain, as long as we change nothing else about the function. So what we seek is a value for fib.(-1) that is consistent with fib.1 = fib.(-1) + fib.0 (the recursive case of the function instantiated with k=1). But this works out to

fib.(-1) = fib.1 − fib.0 = 1 − 0 = 1.

All we have to change to make our original solution work is to make the loop initialization code be

n,m,r := 0,0,1;

The loop invariant is weakened insofar as I2 becomes

I2 : 0≤n≤N

Another generalization that could be made is to define fib.0 = A and fib.1 = B, where A and B are "input constants". (Thus our domain-enlarged function has fib.(-1) = B−A.) Then all we have to change in the program is to make the initialization assignment be

n,m,r := 0,A,B-A;