CSE 331: Loops and loop invariants
Course motto: Your job is to deliver code that you know works.
Last time: Hoare logic for assignment and if
Lecture 7 reasoned about code with no loops. Its rules for that code are mechanical. That is, given the claim on one side of a line and the line, a rule writes the claim on the other side, with no decision to make. Applying such a rule is "turning the crank."
Let's review a few key ideas.
A Hoare triple
{{ P }} code {{ Q }} is an assertion P, a piece of code, and an
assertion Q. The triple is valid if every terminating run of the code
from a state in which P is true ends in a state in which Q is true. A
method satisfies its specification exactly when the triple of its
specification around its body is valid and every call that begins
with the precondition true terminates. Every method in Lecture 7
terminates, so there the question was only whether the triple is
valid. A loop raises the other question: does the code terminate?
The rules for an assignment statement are mechanical. The forward
rule
writes x0 for the old value of x in the claim before the line.
Then it adds the fact that the new value of x is e. The backward
rule is a substitution: every x in the claim after the line becomes
e. The rule for a
sequence
passes the claim after one line to the next line. Reasoning
forward from the
claim before some code, the rules give the strongest postcondition.
Backward from the claim after the code, the rules give the weakest
precondition.
One step in Lecture 7's arguments is not mechanical. A specification
may promise less than the strongest postcondition, or ask more than
the weakest precondition. So the claim the rules give is often not
the claim we have to reach. The rule of
consequence
connects the two claims: the triple {{ P }} code {{ Q }} is valid
if P implies A, {{ A }} code {{ B }} is valid, and B implies Q. The
rule's two implications involve no code. We check them with
arithmetic and logic. They are where an argument can go wrong, if we
believe an implication that is false.
The rule for a
conditional
is mechanical too. It adds the condition b at the top of the true
branch, and not b at the top of the false branch. The rules then
carry each claim through its branch. Finally, the rule joins the two
branches' claims with or. Wednesday's lecture stopped at the
rule's schematic, before any argument used it. Lecture 7's notes work
the rule on abs:
9 10 11 12 13 14 15 16 17 18 19 20 21 | |
In lecture, the half sheet's first question asked for the claim just after line 18. Here is the whole argument:
static int abs(int n) {
{{ n = n0 }}
if (n < 0) {
{{ n = n0 and n < 0 }}
n = -n;
{{ n0 < 0 and n = -n0 }}
}
{{ (n0 < 0 and n = -n0) or (0 <= n0 and n = n0) }}
return n;
}
Line 18 overwrites n, so the claims write n0 for the value of
n at the call. The if's condition b is n < 0. Every run
that enters the true branch found b true, so the rule adds
n < 0 at the top of that branch. The forward rule for line 18
substitutes n0 for the old value of n and adds n = -n0. That
gives n0 < 0 and n = -n0 at the end of the true branch. The
false branch is empty, so the claim at its end is the claim before
the if with not b added: n = n0 and 0 <= n, which is
0 <= n0 and n = n0.
After the if, a run has taken one branch or the other. So the
rule joins the claims at the ends of the two branches with or.
In the first case n = -n0 with n0 < 0, and in the second
n = n0 with 0 <= n0. The last step is the one that is not
mechanical. Each case implies what the @return clause promises
in that case, so abs satisfies its specification.
Today's rule for a loop adds b and not b for the same reason:
a run enters the loop's body having found b true, and leaves
the loop having found b false.
Finally, Lecture 7's notes closed by saying that the rules do not carry a claim through a loop that may run any number of times. So no mechanical rule writes the claim after a loop, and the next section says why. The rule for a loop needs one claim from the programmer, a claim the rule itself does not produce. This lecture gives that rule.
A loop
Here is a method with a loop, and its specification:
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
sum is the second loop of Lecture 1's Sum
, with a
while in place of the for, an array in place of the list, and a
long in place of the int, among other changes. a[i] is the
element of a at index i, counting from 0. a.length is the
number of elements, so the last index is a.length - 1. Our claims
write a sum as the @return clause does: a[0] + ... + a[i - 1] is
the sum of the first i elements, from index 0 through index i - 1
. When i is 0, that sum has no terms. The sum of no terms is 0, so
on an empty array, sum promises 0.
An array holds at most 2147483647 elements, each at most 2147483648
from zero. So s, a sum of some of those elements, is at most
2147483647 * 2147483648 from zero, which is below
4611686018427387904. The largest long is 9223372036854775807, so
s stays inside a long's range. Line 18 runs only when
i < a.length, so i never passes a.length. So no line wraps,
and the claims may use integer arithmetic, as Lecture 7's did for
seconds
.
The specification has no precondition, so we start from
{{ true }}. Can Wednesday's rules reach the postcondition? Forward
through lines 14 and 15, they give s = 0 and i = 0. Each pass
through the loop runs lines 17 and 18, so we can reason forward
through those two lines once per pass. After one pass, with
i < a.length added at the top of the body, the rules give
0 < a.length and i = 1 and s = a[0]. After two passes, they give
0 < a.length and 1 < a.length and i = 2 and s = a[0] + a[1]. A
run may make any number of passes. So a claim at line 20 built
pass by pass would need one case for each number of passes. No
finite list of cases has one for every number. So Wednesday's
rules give no one claim at line 20.
In general, no rule can compute the strongest postcondition or the weakest precondition of a loop.1 So instead, we write a claim at the loop by hand. That claim is a loop invariant, which the next section defines.
A loop invariant
To see what such a claim looks like, let's watch one run. Here is
the run of sum on the array {3, 4, 5}, with one row for each
time the loop checks its condition on line 16:
| condition check | i |
s |
i < a.length |
s = a[0] + ... + a[i - 1] |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 < 3: true |
true: 0 is the sum of no terms |
| 2 | 1 | 3 | 1 < 3: true |
true: 3 = 3 |
| 3 | 2 | 7 | 2 < 3: true |
true: 7 = 3 + 4 |
| 4 | 3 | 12 | 3 < 3: false |
true: 12 = 3 + 4 + 5 |
At the fourth condition check the condition is false, so the loop ends. Line 20 returns 12. Two facts are true at every condition check of this run:
0 <= i and i <= a.length.istarts at 0. Only line 18 changesi, by adding 1. A pass runs only wheni < a.length, so after the passiis at mosta.length.s = a[0] + ... + a[i - 1]. At the first condition check,iis 0 andsis 0, the sum of no terms. Suppose the claim is true at a condition check, and a pass runs. Line 17 addsa[i]tos, sosis the sum of the firsti + 1elements. Line 18 adds 1 toi, sosis again the sum of the firstielements.
Neither reason depends on the array we picked. A claim that holds where the loop checks its condition is what we call a loop invariant:
Definition: loop invariant
A loop invariant of while (b) { body } is a claim that
holds at the point where the loop checks its condition b.
The claim is true each time a run reaches that point: before
the first pass, between passes, and after the last pass.
We write a loop invariant I as {{ Inv: I }} on the line above the
while. A plain claim on that line would sit at the point before
the loop, which a run reaches once. Inv: puts I at the condition
check instead, a point one run can reach many times. Lecture 7's
definition of
holds applies
there unchanged. Here is sum with its invariant written in:
static long sum(int[] a) {
long s = 0;
int i = 0;
{{ Inv: 0 <= i and i <= a.length and
s = a[0] + ... + a[i - 1] }}
while (i < a.length) {
s = s + a[i];
i = i + 1;
}
return s;
}
The rule for a while loop
The table shows the invariant true at every condition check of
one run. The reasons in the list argue that it holds, but in
prose, as Lecture 1 argued about Sum. To check such an argument
step by step, and to reach the postcondition at line 20, we need a
rule for a loop. Here is the rule, laid out as the conditional's
rule was:
Rule: while loop
{{ P }}
{{ Inv: I }}
while (b) {
{{ I and b }}
body
{{ I }}
}
{{ I and not b }}
{{ Q }}
b is the loop's condition. I is a claim the programmer
supplies. The triple {{ P }} while ... {{ Q }} is valid if
three checks pass:
- (1) P implies I.
- (2) The triple
{{ I and b }} body {{ I }}is valid. - (3)
I and not bimplies Q.
Read plainly, the three checks say:
- (1) The precondition P implies the invariant I.
- (2) If a pass begins with I true and
btrue, then I is true again after the body. - (3) If the loop exits with I true, then the postcondition Q is true.
Why do three checks make I true at every condition check? In the
figure, every way into the diamond crosses the arrow labeled I. A
run reaches the first condition check from the top, with P true,
so check (1) makes I true there. Every later condition check
comes at the end of a pass. That pass began at the condition
check before, with b true. If I was true at that earlier
condition check, the pass began with I and b true. So by
check (2), the pass ended with I true, at the next condition
check. Starting from the first, then, I is true at every
condition check. The last condition check finds b false, so
I and not b is true after the loop. By check (3), Q is true
there.
Checks (1) and (3) are implications between two claims with no
code between them, so each is a use of
the rule of consequence.
Check (2) is a triple about the body. When the body has no loop,
as in sum, Wednesday's rules check that triple, forward or
backward. So each check is a step we already know how to take.
What is new is I. For now, I is given. The section
Finding the invariant says where I comes from.
Checking a loop against its invariant
The rule for a while loop turns the question "is the triple for
sum valid?" into three checks. In the prose below, as in the
rule's box, I is sum's invariant,
0 <= i and i <= a.length and s = a[0] + ... + a[i - 1]. P, the
claim at the top, is true. Here is the method with every claim
of the argument written in, as Lecture 7 wrote the argument for
abs:
static long sum(int[] a) {
{{ true }}
long s = 0;
{{ s = 0 }}
int i = 0;
{{ s = 0 and i = 0 }}
{{ Inv: 0 <= i and i <= a.length and
s = a[0] + ... + a[i - 1] }}
while (i < a.length) {
{{ 0 <= i and i <= a.length and
s = a[0] + ... + a[i - 1] and i < a.length }}
s = s + a[i];
{{ 0 <= i and i < a.length and s = a[0] + ... + a[i] }}
i = i + 1;
{{ 1 <= i and i <= a.length and
s = a[0] + ... + a[i - 1] }}
}
{{ 0 <= i and i <= a.length and
s = a[0] + ... + a[i - 1] and not (i < a.length) }}
{{ s = a[0] + ... + a[a.length - 1] }}
return s;
}
The line numbers below are those of the first listing of sum.
Lines 14 and 15 each set a variable that the claim above never
mentions. So the forward rule adds one fact for each line, by the
first habit in Lecture 7's rules for an
assignment.
As the rule's box says, the claim at the top of the body is I with
the condition i < a.length added. The claim after the loop is I
with not (i < a.length) added. As A loop showed, no line of sum
wraps, so the claims use the arithmetic of the integers. Each check
below is written as a calculation, one step to a line, with the fact
a step uses beside it.
Check (1) asks that the claim before the loop imply I. After lines 14
and 15, the claim is s = 0 and i = 0. In the listing, that claim
sits above {{ Inv: ... }}. Two claims stacked at a point are
Lecture 7's empty
triple, an implication.
Check (1) is that implication. Each of I's three facts follows from
s = 0 and i = 0:
0 <= i since i = 0
i <= a.length since i = 0 and 0 <= a.length
a[0] + ... + a[i - 1] = 0 since i = 0: the sum of no terms
= s since s = 0
The second line uses a fact about arrays, not one from the claim: no array has a negative length. So check (1) passes.
Check (2) asks that the triple
{{ I and i < a.length }} s = s + a[i]; i = i + 1; {{ I }} be valid.
The body is two assignments in a row, so the rule for a
sequence
applies. We reason forward through line 17 to a claim at the point
between the two lines. Then we start from that claim at line 18. At
line 17, the forward rule writes s0 for the old value of s. The
claim before the line says that this old value is
a[0] + ... + a[i - 1]. By Lecture 7's second habit, we put that
sum for s0:
s = s0 + a[i] the forward rule
= a[0] + ... + a[i - 1] + a[i] since s0 = a[0] + ... + a[i - 1]
= a[0] + ... + a[i]
Line 17 does not change i, so the facts about i stay as they
were. i < a.length implies i <= a.length, so we drop
i <= a.length. The claim between the lines is then
0 <= i and i < a.length and s = a[0] + ... + a[i].
At line 18, the old value of i is i - 1, by the second habit
again. So we put i - 1 for i in each fact of the claim between
the lines:
0 <= i - 1 from 0 <= i
1 <= i adding 1 to both sides
i - 1 < a.length from i < a.length
i <= a.length since i and a.length are integers
s = a[0] + ... + a[i - 1] from s = a[0] + ... + a[i]
That gives the claim after line 18,
1 <= i and i <= a.length and s = a[0] + ... + a[i - 1]. That
claim implies I, since 1 <= i implies 0 <= i and the other two
facts are I's. So check (2) passes. (Backward works too. Two
substitutions into I give the facts 0 <= i + 1,
i + 1 <= a.length, and s + a[i] = a[0] + ... + a[i].
I and i < a.length implies all three.)
Check (3) asks that the claim after the loop,
I and not (i < a.length), imply the postcondition:
a.length <= i since not (i < a.length)
i = a.length since i <= a.length
s = a[0] + ... + a[i - 1] from I
= a[0] + ... + a[a.length - 1] since i = a.length
The calculation ends at the postcondition, the sum of all of
a's elements. So check (3) passes.
The step to i = a.length in check (3) used the bound
i <= a.length in I. Suppose we left the bound out, and took only
the claim 0 <= i and s = a[0] + ... + a[i - 1]. Checks (1) and
(2) still pass with that claim. But after the loop,
not (i < a.length) gives only a.length <= i, not
i = a.length. Without i = a.length, the calculation for
check (3) cannot put a.length for i in the sum, so it cannot
reach the postcondition. The loop has not changed, so the trouble
is that the claim is too weak. The bound is part of I so that
check (3) can conclude i = a.length.
All three checks pass. So the triple with true before the body
of sum and the postcondition after it is valid.
Does the loop end?
Lecture 7's definition
of a valid triple asks only about terminating runs. So the three
checks say nothing about a run that never ends. As
Last time recalled, a method satisfies its
specification only if every call that begins with the
precondition true also terminates. So we still have to show that
every call of sum terminates.
To show that a loop ends, we name an integer quantity that is an
upper bound on the number of passes the loop has left. Such a
quantity is a variant of the loop. A quantity is a variant if
it has three properties: it is an integer, it is at least 0 at
each condition check, and each pass makes it smaller. For sum,
the variant is a.length - i, the number of elements not yet
added:
a.length - iis an integer, sincea.lengthandiareints.a.length - iis at least 0 at each condition check, since I holds there and saysi <= a.length.- Each pass makes
a.length - ismaller by 1, since line 18 adds 1 toiand no line changesa.length.
Why do the three properties make the loop end? The variant is an
integer, so each pass makes it smaller by at least 1. It is never
below 0 at a condition check. So the loop makes at most as many
passes as the variant's value at the first condition check. For
sum, that value is a.length, so the loop makes at most
a.length passes. The other lines of sum are assignments and a
return, and each of them ends. So every call of sum
terminates. The triple of sum's specification around its body
is valid as well. So
by Lecture 7's definition, sum satisfies its specification.
Why does termination need a separate argument? Take
while (true) { }, a loop that never ends, with true for I.
Checks (1) and (2) pass, since every claim implies true.
Check (3) asks that true and not true imply Q. The claim
true and not true is false in every state, so it implies every
Q. So check (3) passes, whatever Q is. Take false for Q. Then
check (3) asks that true and not true imply false, and it
does. So the three checks would "prove" the postcondition false
for a loop that never ends.
Finding the invariant
So far, the invariant of sum has been given to us. Where does an
invariant come from? In general, finding an invariant takes some
creativity. In simple cases, such as the loops in this lecture, we
read the invariant off the postcondition, by saying how the loop
makes progress toward the postcondition. The postcondition says
what is true when the loop is done. The invariant says what is
true when the loop is part way done.
Here is that reading on sum. Its postcondition Q is
s = a[0] + ... + a[a.length - 1], the sum of all a.length
elements. At each condition check, i is the count of elements
added so far. So we put i for a.length in Q, which gives
s = a[0] + ... + a[i - 1], the main fact of I. At the exit,
i = a.length, so the main fact turns back into Q.
The bounds on i come from where i starts and where it stops.
Line 15 starts i at 0, which gives 0 <= i. A pass runs only
when i < a.length, and adds 1 to i, which gives
i <= a.length. Checking a loop against its invariant showed
that check (3) needs that bound. So an invariant has two kinds of
facts:
- What has been done so far. For
sum, that iss = a[0] + ... + a[i - 1]. - How far along the loop is. For
sum, that is0 <= i and i <= a.length.
We knew sum's invariant before we read it off the postcondition.
Here is a loop whose invariant we have not yet seen:
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
The precondition says that a has at least one element. Line 15
starts m at a[0]. Each pass compares m with the next element,
a[i], on line 18. When a[i] is larger, line 19 sets m to
a[i]. Line 21 then moves i on to the next index.
To read the invariant off the postcondition, we first write the
postcondition as a claim. The @return clause promises that m,
which line 23 returns, is the largest element of a. So Q is
m is the largest of a[0], ..., a[a.length - 1]. A claim of this
form says two things: m is one of the elements listed, and no
element listed is larger than m. The claim is in words, as the
... in a sum is. A claim needs only to be true or false in each
state, so it need not be Java.
At each condition check, the loop has looked at the elements
a[0] through a[i - 1]. So we put i for a.length in Q, as
for sum. That gives the main fact,
m is the largest of a[0], ..., a[i - 1]. The bounds come from
where i starts and where it stops. Line 16 starts i at 1,
which gives 1 <= i. A pass runs only when i < a.length, and
adds 1 to i, which gives i <= a.length. So we take I to be:
1 <= i and i <= a.length and
m is the largest of a[0], ..., a[i - 1]
Now we check max against I, as we checked sum. Here is the
method with every claim of the argument written in:
static int max(int[] a) {
{{ 0 < a.length }}
int m = a[0];
{{ 0 < a.length and m = a[0] }}
int i = 1;
{{ 0 < a.length and m = a[0] and i = 1 }}
{{ Inv: 1 <= i and i <= a.length and
m is the largest of a[0], ..., a[i - 1] }}
while (i < a.length) {
{{ 1 <= i and i <= a.length and
m is the largest of a[0], ..., a[i - 1] and
i < a.length }}
if (m < a[i]) {
{{ 1 <= i and i < a.length and
m is the largest of a[0], ..., a[i - 1] and
m < a[i] }}
m = a[i];
{{ 1 <= i and i < a.length and
m0 is the largest of a[0], ..., a[i - 1] and
m0 < a[i] and m = a[i] }}
}
{{ (1 <= i and i < a.length and
m0 is the largest of a[0], ..., a[i - 1] and
m0 < a[i] and m = a[i]) or
(1 <= i and i < a.length and
m is the largest of a[0], ..., a[i - 1] and
not (m < a[i])) }}
{{ 1 <= i and i < a.length and
m is the largest of a[0], ..., a[i] }}
i = i + 1;
{{ 2 <= i and i <= a.length and
m is the largest of a[0], ..., a[i - 1] }}
}
{{ 1 <= i and i <= a.length and
m is the largest of a[0], ..., a[i - 1] and
not (i < a.length) }}
{{ m is the largest of a[0], ..., a[a.length - 1] }}
return m;
}
The line numbers below are those of the first listing of max. As
for sum, lines 15 and 16 each add one fact, by the first habit.
Check (1) asks that the claim after line 16 imply I:
1 <= i since i = 1
i <= a.length since i = 1 and 0 < a.length
m is the largest of a[0] since m = a[0]
The third fact of I, at i = 1, is
m is the largest of a[0], ..., a[0], a list of one element,
a[0]. Since m = a[0], m is one of the elements listed, and
no element listed is larger than m. The second line used the
precondition: 0 < a.length gives 1 <= a.length, since both are
integers. The precondition also makes a[0] an element of a, so
line 15 reads an element that exists. So check (1) passes.
Check (2) asks that the triple
{{ I and i < a.length }} body {{ I }} be valid. The body of max
is an if followed by an assignment. So the rule for a sequence puts
a claim at the point after the if, and Lecture 7's rule for a
conditional
gives that claim. At the top of the body, 1 <= i and i < a.length,
so a[i] is an element of a and line 18 reads an element that
exists.
In the true branch, the rule adds the condition m < a[i] to the
claim at the top of the body. i < a.length implies
i <= a.length, so we drop i <= a.length. Line 19 sets m, and
the forward rule writes m0 for the old value of m. No
expression in the program gives that old value, so m0 stays in
the claim. Let j be any index from 0 to i - 1:
a[j] <= m0 since m0 is the largest of a[0], ..., a[i - 1]
< a[i] since m0 < a[i]
= m since m = a[i]
So no element from a[0] to a[i - 1] is larger than m. m is
a[i] itself, so m is one of a[0] through a[i], and a[i]
is not larger than m. So m is the largest of
a[0], ..., a[i].
The if has no else, so its false branch is empty. The claim at
the end of the false branch is the claim at the top of the body
with not (m < a[i]) added, and i <= a.length dropped as
before. Again let j be any index from 0 to i - 1:
a[i] <= m since not (m < a[i])
a[j] <= m since m is the largest of a[0], ..., a[i - 1]
So no element from a[0] to a[i] is larger than m. m is one
of a[0] through a[i - 1], so m is one of a[0] through
a[i] too. So m is the largest of a[0], ..., a[i].
After the if, the conditional rule gives the claim with or in
the listing, one case for each branch. Both cases say
1 <= i and i < a.length. By the two calculations, each case
implies m is the largest of a[0], ..., a[i]. So the claim with
or implies the shorter claim below it, which leaves out m0,
since no later step needs it. By the rule of consequence, we go on
from the shorter claim.
At line 21, the old value of i is i - 1, by the second habit.
So we put i - 1 for i in each fact of the shorter claim:
1 <= i - 1 from 1 <= i
2 <= i adding 1 to both sides
i - 1 < a.length from i < a.length
i <= a.length since i and a.length are integers
The fact about m becomes
m is the largest of a[0], ..., a[i - 1]. That gives the claim
after line 21 in the listing. It implies I, since 2 <= i
implies 1 <= i. So check (2) passes.
Check (3) asks that the claim after the loop,
I and not (i < a.length), imply the postcondition:
a.length <= i since not (i < a.length)
i = a.length since i <= a.length
With i = a.length, the fact
m is the largest of a[0], ..., a[i - 1] in I is Q. So check (3)
passes. The only arithmetic in max is i + 1 at line 21, which
runs only when i < a.length, so no line wraps. All three checks
pass, so the triple for max is valid. As for sum, the variant
a.length - i shows that the loop ends, so max satisfies its
specification.
Here is one more loop, a method that adds up the numbers 1 through
n. In lecture, the half sheet asked for its invariant. Write yours
before reading on.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
In a claim, 1 + ... + i is the sum of the numbers 1 through i.
When i is 0, that sum has no terms, so it is 0. The postcondition
Q is s = 1 + ... + n. At each condition check, the loop has added
the numbers 1 through i, so we put i for n in Q. The bounds
come, as before, from where i starts and where it stops. So the
invariant is 0 <= i and i <= n and s = 1 + ... + i.
Check (1) starts from the claim after line 15,
0 <= n and s = 0 and i = 0, and uses the precondition for
i <= n:
0 <= i since i = 0
i <= n since i = 0 and 0 <= n
1 + ... + i = 0 since i = 0, so the sum has no terms
= s since s = 0
Check (2) starts from I and i < n. This time line 17 sets i
first, so its old value i - 1 goes into each fact. Then line 18
adds the new i to s:
0 <= i - 1 and i - 1 < n and after line 17
s = 1 + ... + (i - 1)
1 <= i and i <= n since i and n are integers
s = 1 + ... + (i - 1) + i after line 18
= 1 + ... + i
With 1 <= i and i <= n, that claim implies I. Check (3) starts
from I and not (i < n):
n <= i since not (i < n)
i = n since i <= n
s = 1 + ... + n since s = 1 + ... + i and i = n
Line 17 runs only when i < n, so i + 1 is at most n, an
int. s is a long, and is at most 1 + ... + 2147483647,
which is 2305843008139952128. The largest long is
9223372036854775807, so line 18 never wraps.
s = i * (i + 1) / 2 is another way to write the fact
s = 1 + ... + i. One of i and i + 1 is even, so the division
is exact. A reader who wrote that has the same invariant.
Not every invariant comes from putting i for a bound in Q. The
invariant of exercise 1's quotient says what the loop keeps
equal, q * y + r = x, and later lectures meet more such claims.
Finally, even when the loop is right, a claim we choose for I may not pass all three checks, in two ways:
- Too weak. Drop the bound
i <= a.lengthfromsum's I. Checks (1) and (2) still pass. But as Checking a loop against its invariant showed, the calculation for check (3) then cannot reach the postcondition. - Too strong. Add
0 < a.lengthtosum's I. Check (2) still passes, since no line changesa.length. Check (3) still passes, since the claim after the loop only gains a fact. But on an empty array, the claim before the loop,s = 0 and i = 0, is true, and0 < a.lengthis false. So check (1) fails.
The too weak claim holds at the condition check, so it is a loop
invariant. Check (3) needs more than the too weak claim says. The
too strong claim is false at the first condition check on an
empty array, so it is not a loop invariant at all. In general, a
failed check means that the code is wrong, or that the claim we
chose for I is. Here sum is right, so both times the claim is
what has to change. The next section keeps the claim I and
changes the code instead, in three ways.
The checks find the mistake
Here are three copies of sum, each with one change. The line
numbers are those of the first listing of sum:
- Wrong start. Line 15 becomes
int i = 1;. - Swapped body. Lines 17 and 18 trade places, so a pass runs
i = i + 1;and thens = s + a[i];. - Wrong exit. Line 16 becomes
while (i < a.length - 1) {.
We check each copy against the same claim I, sum's invariant.
For each copy, which of the three checks fails? Try each one
before reading on.
The wrong start changes only the claim before the loop, so checks
(2) and (3) pass as they did for sum. That claim is now
s = 0 and i = 1, and check (1) asks that it imply I. On the
empty array, I's bound i <= a.length says 1 <= 0, which is
false. On a nonempty array, I's sum has one term:
a[0] + ... + a[i - 1] = a[0] since i = 1, so the sum has one term
!= s unless a[0] = 0, since s = 0
So on the empty array, and on every array whose first element is
not 0, the claim before the loop is true and I is false. Check (1)
fails. The wrong start never adds a[0], so a run on {3, 4, 5}
returns 9, which is 4 + 5.
The swapped body starts and stops as sum does, so checks (1) and
(3) pass. Check (2) reasons forward from I and i < a.length. The
body's first line is now i = i + 1, so by Lecture 7's second
habit we put i - 1 for i in each fact:
1 <= i and i <= a.length from 0 <= i - 1 and i - 1 < a.length
s = a[0] + ... + a[i - 2] from s = a[0] + ... + a[i - 1]
The body's second line is s = s + a[i], and the claim above it
says what the old value s0 is:
s = s0 + a[i] the forward rule
= a[0] + ... + a[i - 2] + a[i] since s0 = a[0] + ... + a[i - 2]
I asks for s = a[0] + ... + a[i - 2] + a[i - 1]. The claim the
body leaves has a[i] in place of a[i - 1], so that claim does
not imply I. Check (2) fails. On the first pass, s gets a[1]
where a[0] was due. In lecture, the half sheet's question 2
asked for this check. A run on {3, 4, 5} sets i to 3 on its
third pass and reads a[3], past the end, so the call throws
ArrayIndexOutOfBoundsException.
The wrong exit changes only the condition. Check (1) is the one
sum passed. Check (2) starts from I and i < a.length - 1,
which implies I and i < a.length, so check (2) passes by the
calculation for sum. Check (3) asks that
I and not (i < a.length - 1) imply Q:
a.length - 1 <= i since not (i < a.length - 1)
i = a.length - 1 or i = a.length since i <= a.length
On a nonempty array, I allows i = a.length - 1. There I gives
s = a[0] + ... + a[a.length - 2], the sum without the last
element. So when i = a.length - 1 and the last element is not
0, the claim after the loop is true and Q is false. Check (3)
fails. A run on {3, 4, 5} returns 7, which is 3 + 4.
Would tests have found these mistakes? The tests beside sum, in
ArraySumTest.java, call sum on {}, {7}, and {3, 4, 5}. All
three tests pass for sum. Each copy returns 0 on {}, the right
sum, so a test on the empty array alone misses all three mistakes.
On {7}, the wrong start and the wrong exit return 0, and the
swapped body throws. So the tests on {7} and on {3, 4, 5} each
fail for all three copies.
Each check that failed above came with a run that goes wrong.
Still, a state on which a check fails need not be the state of a
run that goes wrong. The wrong start fails check (1) on the empty
array, and a run on the empty array returns 0, the right sum.
Check (2) starts from every state in which I and b is true, so
it may fail on a state that no run reaches. In Lecture 7, the
arguments for min and quadrant wrote the strongest claim at
every point. As
Lecture 7's A proof
says, the false implication then says on which inputs the method
is wrong. We choose the claim I, so it need not be the strongest
claim at the condition check.
What reasoning tells us
Lecture
1
argued about Sum in prose, at the level of "the loop added each
element once." For sum, the second loop of Sum, that sentence
is now one claim, the invariant, and three checks that Lecture 7's
rules settle.
Lecture 7's notes closed with two things that can still go wrong, and one thing the rules could not yet do, a loop. Here is where each stands for loops:
- A false implication. Checks (1) and (3) are implications, and check (2) ends in one. A person can believe an implication that is false. A test that refutes the claim refutes the argument too, so we still test.
- Arithmetic that is not Java's. The claims are about the
integers. Java's
intwraps, so each method in this lecture bounds its inputs, as the exercises'factorialdoes withn <= 20, or computes in along, assumandsumTodo, or does no arithmetic that could wrap. - A claim the rules do not produce. The rule for a while loop asks for I, and none of Lecture 7's rules gives I. So finding the invariant is the programmer's work. For the loops in this lecture, the section Finding the invariant reads I off the postcondition.
Monday's lecture discusses Reading 2. Tuesday's section is on Floyd-Hoare logic. Homework 2 is due Friday, October 23.
Exercises
Each answer is folded under its question. Write yours first.
1. Here is a method that divides one number by another:
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
The quotient of x divided by y, rounded down, is the integer
q with q * y <= x and x < (q + 1) * y. So that claim is the
postcondition. The loop's invariant is q * y + r = x and 0 <= r.
Do the three checks of the rule for a while loop, each as a
calculation. Then give a variant to show that the loop ends.
Answer
-
Check (1) passes. Lines 18 and 19 set
qandr, which the precondition never mentions. So the claim before the loop is the precondition withq = 0 and r = xadded:q * y + r = 0 * y + x since q = 0 and r = x = x 0 <= r since r = x and 0 <= x -
Check (2) passes. By Lecture 7's second habit, the old value of
qat line 21 isq - 1, and the old value ofrat line 22 isr + y. We start from the claim at the top of the body,I and y <= r:(q - 1) * y + r = x from q * y + r = x, at line 21 (q - 1) * y + r + y = x from (q - 1) * y + r = x, at line 22 q * y + r = x since (q - 1) * y + y = q * y y <= r + y from y <= r, at line 22 0 <= r taking y from both sidesThe third and last lines are I's two facts.
-
Check (3) passes. The claim after the loop is
I and not (y <= r):r < y since not (y <= r) q * y = x - r from q * y + r = x <= x since 0 <= r x = q * y + r from q * y + r = x < q * y + y since r < y = (q + 1) * y -
No line wraps. No line changes
y, so1 <= ystays true. Lines 21 and 22 run only wheny <= r, sor - yis between 0 andr.qstarts at 0 and only grows, so at line 21,q + 1 <= (q + 1) * y <= q * y + r, which isx. The code never computesq * y. -
The loop ends, with
ras its variant.ris an integer, and I says0 <= rat each condition check. Each pass takesyfromr, andyis at least 1.rstarts atx, so the loop makes at mostxpasses. With the three checks, that showsquotientsatisfies its specification.
2. Here is a method that multiplies the numbers 1 through n:
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
When n is 0, the product 1 * 2 * ... * n has no factors, and
the product of no factors is 1. Find an invariant for the loop.
Then do checks (1) and (3), and do check (2) forward through
lines 19 and 20. Why does the precondition say n <= 20?
Answer
- The invariant is
0 <= i and i <= n and f = 1 * 2 * ... * i. At each condition check, the loop has multipliedfby the numbers 1 throughi, so we putifornin the postcondition. Line 17 startsiat 0, which gives0 <= i. With!=, the condition alone does not givei <= n, asi < ndid forsumTo. The bound holds because in the body,i != nandi <= ngivei < n, soi + 1 <= n. Check (3) does not need the bound, sincenot (i != n)givesi = n. Check (2)'s step toi < nneeds the bound. So does the variantn - i, which must be at least 0 at each condition check. -
Check (1) passes. After lines 16 and 17, the claim is the precondition with
f = 1 and i = 0added:0 <= i since i = 0 i <= n since i = 0 and 0 <= n 1 * 2 * ... * i = 1 since i = 0, so the product has no factors = f since f = 1 -
Check (3) passes.
i = n since not (i != n) f = 1 * 2 * ... * i from I = 1 * 2 * ... * n since i = n -
Check (2) passes. At the top of the body,
i != nandi <= ngivei < n. At line 19 the old value ofiisi - 1:0 <= i - 1 from 0 <= i 1 <= i adding 1 to both sides i - 1 < n from i < n i <= n since i and n are integers f = 1 * 2 * ... * (i - 1) from f = 1 * 2 * ... * iAt line 20 the forward rule writes
f0for the old value off, and the claim before the line says whatf0is:f = f0 * i the forward rule = 1 * 2 * ... * (i - 1) * i since f0 = 1 * 2 * ... * (i - 1) = 1 * 2 * ... * iSo after line 20,
1 <= i and i <= n and f = 1 * 2 * ... * i, which implies I. -
The bound 20 keeps
fwithin along. After line 20,fis1 * 2 * ... * iwithi <= n, sofis at most 20!, which is 2432902008176640000. The largestlongis 9223372036854775807. 21! is 51090942171709440000, larger than the largestlong, so withn = 21line 20 would wrap on the last pass. Line 19 runs only wheni < n, soi + 1is at most 20. FactorialTest's testtwentychecks thatfactorial(20)returns 2432902008176640000, and it passes.
3. Here is a method that counts how often a value appears in an array:
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
The specification has no precondition. The loop's invariant is
this claim, where the indices k < i are 0 through i - 1:
0 <= i and i <= a.length and
c = the number of indices k < i with a[k] = x
The body holds an if, so do check (2) with the rule for a
conditional,
through both branches. Then do checks (1) and (3).
Answer
-
Check (2) passes: after the
if,ccounts the indices belowi + 1. The claim at the top of the body isI and i < a.length. In the true branch,a[i] = xis added, and the forward rule for line 18 writesc0for the old value ofc. Sincea[i] = x, indexiadds one to the count:c = c0 + 1 = (the number of indices k < i with a[k] = x) + 1 = the number of indices k < i + 1 with a[k] = xThe false branch is empty, so its claim is the claim before the
ifwitha[i] != xadded. Sincea[i] != x, indexiadds nothing, so there tooccounts the indicesk < i + 1witha[k] = x. In both cases of the claim after theif, then,0 <= i and i < a.length, andccounts the indicesk < i + 1witha[k] = x. At line 20 the old value ofiisi - 1:0 <= i - 1 from 0 <= i 1 <= i adding 1 to both sides i - 1 < a.length from i < a.length i <= a.length since i and a.length are integersPutting
i - 1foriturns the indicesk < i + 1into the indicesk < i. So the last claim implies I. -
Check (1) passes. After lines 14 and 15,
c = 0 and i = 0. So0 <= i, andi <= a.lengthsince0 <= a.length. No index is below 0, so the count of indicesk < 0is 0, which isc. - Check (3) passes.
not (i < a.length)givesa.length <= i, and withi <= a.length,i = a.length. Soccounts the indicesk < a.lengthwitha[k] = x, which are all the indices the@returnclause counts. - No line wraps.
ccounts some of the indices belowi, soc <= i. In the bodyi < a.length, soc + 1andi + 1are each at mosta.length, anint.
4. Here is a method that searches an array for a value, with a
return inside its loop:
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
The specification has no precondition. The loop's invariant is
0 <= i and i <= a.length and a[k] != x for every k < i. Write
the claim at each return, and check each claim against the
@return clause. Then do check (2). What is the claim after the
if?
Answer
- At line 19,
iis the smallest index witha[i] = x. A run reaches line 19 having foundi < a.lengthanda[i] == xtrue. So the claim there is I withi < a.lengthanda[i] = xadded. I says that no index belowiholdsx. Soiis the smallest such index, and the@returnclause promisesi. A run that returns from inside the body never reaches the body's end, so check (2) asks nothing of it. At thereturn, we check the claim there against what the@returnclause promises, as Lecture 7 did atcompare's returns. - At line 23, no index of
aholdsx. The claim there isI and not (i < a.length).not (i < a.length)andi <= a.lengthgivei = a.length. So I saysa[k] != xfor everyk < a.length, which is every index ofa, and the clause promises -1. -
Check (2) passes, and the claim after the
ifis the false branch's. As for Lecture 7'scompare, areturnends the run of the method, so no run comes out of the true branch. So the claim after theifis the claim at the top of the body witha[i] != xadded:0 <= i and i < a.length and a[i] != x and a[k] != x for every k < iAt line 21 the old value of
iisi - 1:0 <= i - 1 from 0 <= i 1 <= i adding 1 to both sides i - 1 < a.length from i < a.length i <= a.length since i and a.length are integers a[i - 1] != x from a[i] != x a[k] != x for every k < i - 1 from a[k] != x for every k < i a[k] != x for every k < i from the two lines aboveThat claim implies I, since
1 <= iimplies0 <= i. -
Check (1) passes. With
i = 0,0 <= iandi <= a.length. No index is below 0, soa[k] != x for every k < 0is true.
5. Here is sumTo, which adds the numbers 1 through n, with
the two lines of its loop's body in the other order:
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
Take for I the claim 0 <= i and i <= n and s = 1 + ... + i, the
invariant of the right sumTo in Finding the invariant, where
1 + ... + i is 0 when i is 0. Which of the three checks fails?
What does sumTo(4) return?
Answer
-
Check (2) fails: after a pass,
sis 1 short of I. The claim at the top of the body is I withi < nadded. At line 19 the forward rule writess0for the old value ofs, and I says whats0is:s = s0 + i the forward rule = 1 + ... + i + i since s0 = 1 + ... + iAt line 20 the old value of
iisi - 1:s = 1 + ... + (i - 1) + (i - 1) from s = 1 + ... + i + i = (1 + ... + i) - 1I asks for
s = 1 + ... + i, and no state makes both claims true. So the triple for the body is not valid. For one state that shows it, taken = 1,i = 0, ands = 0, whereI and i < nis true. The body leavess = 0andi = 1, where I asks fors = 1. -
Checks (1) and (3) pass. Before the loop,
s = 0andi = 0, and the sum of no terms is 0. After the loop,not (i < n)andi <= ngivei = n, so I givess = 1 + ... + n. sumTo(4)returns 6, where the specification promises 10. A failed check means that the code is wrong, or that the claim we chose for I is. This run shows that the code is wrong. The method addsibefore raising it, so it adds 0, 1, 2, and 3, and misses 4.
-
This follows from the undecidability of the halting problem, which CSE 311 covers: Alan Turing, "On Computable Numbers, with an Application to the Entscheidungsproblem" (1936). A procedure that gave the strongest postcondition of every loop from
{{ true }}, and could tell when that claim isfalsein every state, would decide whether the loop can halt. Rice's theorem (1953) extends the result to every property of what a program computes. ↩