01-Contracts c0
01-Contracts c0
Contracts
15-122: Principles of Imperative Computation (Spring 2018)
Frank Pfenning
1 A Mysterious Program
You are a new employee in a company, and a colleague comes to you with
the following program, written by your predecessor who was summarily
fired for being a poor programmer. Your colleague claims he has tracked a
bug in a larger project to this function. It is your job to find and correct this
bug.
1 int f (int x, int y) {
2 int r = 1;
3 while (y > 1) {
4 if (y % 2 == 1) {
5 r = x * r;
6 }
7 x = x * x;
8 y = y / 2;
9 }
10 return r * x;
11 }
Before you read on, you might examine this program for a while to try
to determine what it does, or is supposed to do, and see if you can spot the
problem.
Lecture 1: Contracts 3
2 Forming a Conjecture
The first step is to execute the program on some input values to see its
results. The code is in a file called mystery.c0 so we invoke the coin in-
terpreter to let us experiment with code.
% coin mystery.c0
C0 interpreter (coin) 0.3.3 ’Nickel’
Type ‘#help’ for help or ‘#quit’ to exit.
-->
At this point we can type in statements and they will be executed. One
form of statement is an expression, in which case coin will show its value.
For example:
--> 3+8;
11 (int)
-->
We can also use the functions in the files that we loaded when we
started coin. In this case, the mystery function is called f, so we can eval-
uate it on some arguments.
--> f(2,3);
8 (int)
--> f(2,4);
16 (int)
--> f(1,7);
1 (int)
--> f(3,2);
9 (int)
-->
Can you form a conjecture from these values?
Lecture 1: Contracts 4
From these and similar examples, you might form the conjecture that
f (x, y) = xy , that is, x to the power y. One can confirm that with a few
more values, such as
--> f(-2,3);
-8 (int)
--> f(2,8);
256 (int)
--> f(2,10);
1024 (int)
-->
It seems to work out! Our next task is to see why this function actually
computes the power function. Understanding this is necessary so we can
try to find the error and correct it.
Lecture 1: Contracts 5
iteration x y r
0 2 8 1
1 4 4 1
2 16 2 1
3 256 1 1
To understand why this loop works we need to find a so-called loop in-
variant: a quantity that does not change throughout the loop. In this exam-
ple, when y is a power of 2 then r is a loop invariant. Can you find a loop
invariant involving just x and y?
Lecture 1: Contracts 7
Going back to our earlier conjecture, we are trying to show that this
function computes xy . Interestingly, after every iteration of the loop, this
quantity is exactly the same! Before the first iteration it is 28 = 256. After
the first iteration it is 44 = 256. After the second iteration it is 162 = 256.
After the third iteration, it is 2561 = 256. Let’s note it down in the table.
iteration x y r xy
0 2 8 1 256
1 4 4 1 256
2 16 2 1 256
3 256 1 1 256
r ∗ x = 1 ∗ x = x = x1 = xy
6 Termination
In this case it is easy to see that the loop always terminates. To show that
a loop always terminates, we need to define some quantity that always gets
strictly smaller during any arbitrary iteration of the loop, and that can never
become negative. This means that the loop can only run a finite number of
times.
The quantity y/2 is always less than y when y > 0, so on any arbitrary
iteration of the loop, y gets strictly smaller and it can never become nega-
tive. Therefore, we know the loop has to terminate.
(By the same token, we could identify any lower bound, not just zero,
and a quantity that strictly decreases and never passes that lower bound,
or we could identify an upper bound and a quantity that strictly increases
but never passes that upper bound!)
Lecture 1: Contracts 11
7 A Counterexample
We don’t have to look at y being a power of 2 — we already know the func-
tion works correctly there. Some of the earlier examples were not powers
of two, and the function still worked:
--> f(2,3);
8 (int)
--> f(-2,3);
-8 (int)
--> f(2,1);
2 (int)
-->
What about 0, or negative exponents?
--> f(2,0);
2 (int)
--> f(2,-1);
2 (int)
-->
It looks like we have found at least two problems. 20 = 1, so the answer
2 is definitely incorrect. 2−1 = 1/2 so one might argue it should return
0. Or one might argue in the absence of fractions (we are working with
integers), a negative exponent does not make sense. In any case, f (2, −1)
should certainly not return 2.
Lecture 1: Contracts 12
8 Imposing a Precondition
Let’s go back to a mathematical definition of the power function xy on inte-
gers x and y. We define:
0
x = 1
xy+1 = x ∗ xy for y ≥ 0
9 Promising a Postcondition
The C0 language does not have a built-in power function. So we need to
write it explicitly ourselves. But wait! Isn’t that what f is supposed to do?
The idea in this and many other examples is to capture a specification in the
simplest possible form, even if it may not be computationally efficient, and
then promise in the postcondition to satisfy this simple specification. Here,
we can transcribe the mathematical definition into a recursive function.
int POW (int x, int y)
//@requires y >= 0;
{
if (y == 0)
return 1;
else
return x * POW(x, y-1);
}
In the rest of the lecture we often silently go back and forth between xy
and POW(x,y). Now we incorporate POW into a formal postcondition for
the function. Postconditions have the form //@ensures e;, where e is a
boolean expression. They are also written before the function body, by con-
vention after the preconditions. Postconditions can use a special variable
\result to refer to the value returned by the function.
1 int f (int x, int y)
2 //@requires y >= 0;
3 //@ensures \result == POW(x,y);
4 {
5 int r = 1;
6 while (y > 1) {
7 if (y % 2 == 1) {
8 r = x * r;
9 }
10 x = x * x;
11 y = y / 2;
12 }
13 return r * x;
14 }
Note that as far as the function f is concerned, if we are considering calling
it we do not need to look at its body at all. Just looking at the pre- and
Lecture 1: Contracts 14
Let’s make a table again, this time to trace a call when the exponent is
not a power of two, say, while computing 27 by calling f (2, 7).
iteration b e r be r ∗ be
0 2 7 1 128 128
1 4 3 2 64 128
2 16 1 8 16 128
As we can see, be is not invariant, but r ∗ be = 128 is! The extra factor from
the equation on the previous page is absorbed into r.
We now express this proposed invariant formally in C0. This requires
the @loop_invariant annotation. It must come immediately before the
loop body, but it is checked just before the loop exit condition. We would
like to say that the expression r * POW(b,e) is invariant, but this is not
possible directly.
Loop invariants in C0 are boolean expressions which must be either true
or false. We can achieve this by stating that r * POW(b,e) == POW(x,y).
Observe that x and y do not change in the loop, so this guarantees that
r * POW(b,e) never changes either. But it says a little more, stating what
the invariant quantity is in terms of the original function parameters.
1 int f (int x, int y)
2 //@requires y >= 0;
3 //@ensures \result == POW(x,y);
4 {
5 int r = 1;
6 int b = x; /* base */
7 int e = y; /* exponent */
8 while (e > 1)
9 //@loop_invariant r * POW(b,e) == POW(x,y);
10 {
11 if (e % 2 == 1) {
12 r = b * r;
13 }
14 b = b * b;
15 e = e / 2;
16 }
17 return r * b;
18 }
Lecture 1: Contracts 19
Init: The invariant holds initially. When we enter the loop, e = y and y ≥ 0
by the precondition of the function. Done.
Preservation: Assume the invariant holds just before the exit condition is
checked. We have to show that it is true again when we reach the exit
condition after one iteration of the loop.
Assumption: e ≥ 0.
To show: e0 ≥ 0 where e0 = e/2, with integer division. This clearly
holds.
Init: The invariant holds initially, because when entering the loop we have
r = 1, b = x and e = y.
Assumption: r ∗ be = xy .
0
To show: r0 ∗ b0 e = xy , where r0 , b0 , and e0 are the values of r, b, and e
after one iteration.
Case e is even, so that e = 2 ∗ n for some positive n:
Then r0 = r, b0 = b ∗ b and e0 = e/2 = 2 ∗ n/2 = n, and we reason:
0
r0 ∗ b0 e = r ∗ (b ∗ b)n
= r ∗ b2∗n Since (a2 )c = a2∗c
= r∗b e Because we set e = 2 ∗ n
= xy By assumption
Lecture 1: Contracts 23
15 Termination
The previous argument for termination still holds. By loop invariant, we
know that e ≥ 0. When we enter the body of the loop, the condition must
be true so e > 0. Now we just use that e/2 < e for e > 0, so the value
of e is strictly decreasing and positive, which, as an integer, means it must
eventually become 0, upon which we exit the loop and return from the
function after one additional step.
Lecture 1: Contracts 25
16 A Surprise
Now, let’s try our function on some larger numbers, computing some pow-
ers of 2.
% coin mystery2f.c0 -d
C0 interpreter (coin) 0.3.3 ’Nickel’
Type ‘#help’ for help or ‘#quit’ to exit.
--> f(2,30);
1073741824 (int)
--> f(2,31);
-2147483648 (int)
--> f(2,32);
0 (int)
-->
230 looks plausible, but how could 231 be negative or 232 be zero? We
claimed we just proved it correct!
The reason is that the values of type int in C0 or C and many other
languages actually do not represent arbitrarily large integers, but have a
fixed-size representation. In mathematical terms, this means that we are
dealing with modular arithmetic. The fact that 232 = 0 provides a clue that
integers in C0 have 32 bits, and arithmetic operations implement arithmetic
modulo 232 .
In this light, the results above are actually correct. We examine modular
arithmetic in detail in the next lecture.
Lecture 1: Contracts 26
@requires: At the call sites we have to prove that the precondition for the
function is satisfied for the given arguments. We can then assume it
when reasoning in the body of the function.
Lecture 1: Contracts 27
@ensures: At the return sites inside a function we have to prove that the
postcondition is satisfied for the given return value. We can then as-
sume it at the call site.
Init: The loop invariant is satisfied initially, when the loop is first
encountered.
Preservation: Assuming the loop invariant is satisfied at the begin-
ning of the loop (just before the exit test), we have to show it still
holds when the beginning of the loop is reached again, after one
iteration of the loop.
We are then allowed to assume that the loop invariant holds after the
loop exits, together with the exit condition.
Contracts are crucial for reasoning since (a) they express what needs to
be proved in the first place (give the program’s specification), and (b) they
localize reasoning: from a big program to the conditions on the individual
functions, from the inside of a big function to each loop invariant or asser-
tion.
Lecture 1: Contracts 28
Exercises
Exercise 1. Rewrite first POW and then f so that it signals an error in case of
an overflow rather than silently working in modular arithmetic. You can use the
statement error("Overflow"); to signal an overflow.
Exercise 2. Find an input for f that fails our first guess of a loop invariant:
//@loop_invariant POW(b,e) == POW(x,y);