Skip to main content

CSE 331: Loops and loop invariants

lecture

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
/**
 * Gives the absolute value of a number.
 *
 * @param n the number
 * @requires -2147483647 <= n
 * @return n if 0 <= n, and -n if n < 0
 */
static int abs(int n) {
    if (n < 0) {
        n = -n;
    }
    return n;
}

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
/**
 * Adds up the elements of an array.
 *
 * @param a the array
 * @return a[0] + ... + a[a.length - 1],
 *         the sum of the elements
 */
static long sum(int[] a) {
    long s = 0;
    int i = 0;
    while (i < a.length) {
        s = s + a[i];
        i = i + 1;
    }
    return s;
}

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. i starts at 0. Only line 18 changes i, by adding 1. A pass runs only when i < a.length, so after the pass i is at most a.length.
  • s = a[0] + ... + a[i - 1]. At the first condition check, i is 0 and s is 0, the sum of no terms. Suppose the claim is true at a condition check, and a pass runs. Line 17 adds a[i] to s, so s is the sum of the first i + 1 elements. Line 18 adds 1 to i, so s is again the sum of the first i elements.

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 b implies Q.

Read plainly, the three checks say:

  • (1) The precondition P implies the invariant I.
  • (2) If a pass begins with I true and b true, then I is true again after the body.
  • (3) If the loop exits with I true, then the postcondition Q is true.

A flowchart. An arrow labeled P comes in from the top, then
carries the claim I into a diamond holding the condition b. The
true edge, labeled I and b, leads down into a box labeled body. A
back edge labeled I runs from the body around the left side and
joins the arrow into the diamond above the label I. The false
edge, labeled I and not b, leads out to the right and down to an
arrow labeled Q. Circled numbers mark the three checks: 1 on the
entry arrow, 2 beside the body, and 3 on the
exit.

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 - i is an integer, since a.length and i are ints.
  • a.length - i is at least 0 at each condition check, since I holds there and says i <= a.length.
  • Each pass makes a.length - i smaller by 1, since line 18 adds 1 to i and no line changes a.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 is s = a[0] + ... + a[i - 1].
  • How far along the loop is. For sum, that is 0 <= 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
/**
 * Finds the largest element of an array.
 *
 * @param a the array
 * @requires 0 < a.length
 * @return the largest element of a
 */
static int max(int[] a) {
    int m = a[0];
    int i = 1;
    while (i < a.length) {
        if (m < a[i]) {
            m = a[i];
        }
        i = i + 1;
    }
    return m;
}

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
/**
 * Adds up the numbers 1 through n.
 *
 * @param n the last number to add
 * @requires 0 <= n
 * @return 1 + ... + n
 */
static long sumTo(int n) {
    long s = 0;
    int i = 0;
    while (i < n) {
        i = i + 1;
        s = s + i;
    }
    return s;
}

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.length from sum'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.length to sum's I. Check (2) still passes, since no line changes a.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, and 0 < a.length is 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 then s = 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 int wraps, so each method in this lecture bounds its inputs, as the exercises' factorial does with n <= 20, or computes in a long, as sum and sumTo do, 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
/**
 * Divides one number by another.
 *
 * @param x the number divided
 * @param y the number it is divided by
 * @requires 0 <= x and 0 < y
 * @return the quotient of x divided by y, rounded down
 */
static int quotient(int x, int y) {
    int q = 0;
    int r = x;
    while (y <= r) {
        q = q + 1;
        r = r - y;
    }
    return q;
}

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 q and r, which the precondition never mentions. So the claim before the loop is the precondition with q = 0 and r = x added:

    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 q at line 21 is q - 1, and the old value of r at line 22 is r + 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 sides
    

    The 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, so 1 <= y stays true. Lines 21 and 22 run only when y <= r, so r - y is between 0 and r. q starts at 0 and only grows, so at line 21, q + 1 <= (q + 1) * y <= q * y + r, which is x. The code never computes q * y.

  • The loop ends, with r as its variant. r is an integer, and I says 0 <= r at each condition check. Each pass takes y from r, and y is at least 1. r starts at x, so the loop makes at most x passes. With the three checks, that shows quotient satisfies 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
/**
 * Multiplies the numbers 1 through n.
 *
 * @param n the last number to multiply
 * @requires 0 <= n and n <= 20
 * @return n!, the product 1 * 2 * ... * n
 */
static long factorial(int n) {
    long f = 1;
    int i = 0;
    while (i != n) {
        i = i + 1;
        f = f * i;
    }
    return f;
}

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 multiplied f by the numbers 1 through i, so we put i for n in the postcondition. Line 17 starts i at 0, which gives 0 <= i. With !=, the condition alone does not give i <= n, as i < n did for sumTo. The bound holds because in the body, i != n and i <= n give i < n, so i + 1 <= n. Check (3) does not need the bound, since not (i != n) gives i = n. Check (2)'s step to i < n needs the bound. So does the variant n - 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 = 0 added:

    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 != n and i <= n give i < n. At line 19 the old value of i is i - 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 * ... * i
    

    At line 20 the forward rule writes f0 for the old value of f, and the claim before the line says what f0 is:

    f = f0 * i                      the forward rule
      = 1 * 2 * ... * (i - 1) * i   since f0 = 1 * 2 * ... * (i - 1)
      = 1 * 2 * ... * i
    

    So after line 20, 1 <= i and i <= n and f = 1 * 2 * ... * i, which implies I.

  • The bound 20 keeps f within a long. After line 20, f is 1 * 2 * ... * i with i <= n, so f is at most 20!, which is 2432902008176640000. The largest long is 9223372036854775807. 21! is 51090942171709440000, larger than the largest long, so with n = 21 line 20 would wrap on the last pass. Line 19 runs only when i < n, so i + 1 is at most 20. FactorialTest's test twenty checks that factorial(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
/**
 * Counts how often a value appears in an array.
 *
 * @param a the array
 * @param x the value to count
 * @return the number of indices k with a[k] == x
 */
static int count(int[] a, int x) {
    int c = 0;
    int i = 0;
    while (i < a.length) {
        if (a[i] == x) {
            c = c + 1;
        }
        i = i + 1;
    }
    return c;
}

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, c counts the indices below i + 1. The claim at the top of the body is I and i < a.length. In the true branch, a[i] = x is added, and the forward rule for line 18 writes c0 for the old value of c. Since a[i] = x, index i adds 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] = x
    

    The false branch is empty, so its claim is the claim before the if with a[i] != x added. Since a[i] != x, index i adds nothing, so there too c counts the indices k < i + 1 with a[k] = x. In both cases of the claim after the if, then, 0 <= i and i < a.length, and c counts the indices k < i + 1 with a[k] = x. At line 20 the old value of i is i - 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
    

    Putting i - 1 for i turns the indices k < i + 1 into the indices k < i. So the last claim implies I.

  • Check (1) passes. After lines 14 and 15, c = 0 and i = 0. So 0 <= i, and i <= a.length since 0 <= a.length. No index is below 0, so the count of indices k < 0 is 0, which is c.

  • Check (3) passes. not (i < a.length) gives a.length <= i, and with i <= a.length, i = a.length. So c counts the indices k < a.length with a[k] = x, which are all the indices the @return clause counts.
  • No line wraps. c counts some of the indices below i, so c <= i. In the body i < a.length, so c + 1 and i + 1 are each at most a.length, an int.

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
/**
 * Finds where a value first appears in an array.
 *
 * @param a the array
 * @param x the value to find
 * @return the smallest index i with a[i] == x, or -1 if there is
 *     none
 */
static int find(int[] a, int x) {
    int i = 0;
    while (i < a.length) {
        if (a[i] == x) {
            return i;
        }
        i = i + 1;
    }
    return -1;
}

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, i is the smallest index with a[i] = x. A run reaches line 19 having found i < a.length and a[i] == x true. So the claim there is I with i < a.length and a[i] = x added. I says that no index below i holds x. So i is the smallest such index, and the @return clause promises i. A run that returns from inside the body never reaches the body's end, so check (2) asks nothing of it. At the return, we check the claim there against what the @return clause promises, as Lecture 7 did at compare's returns.
  • At line 23, no index of a holds x. The claim there is I and not (i < a.length). not (i < a.length) and i <= a.length give i = a.length. So I says a[k] != x for every k < a.length, which is every index of a, and the clause promises -1.
  • Check (2) passes, and the claim after the if is the false branch's. As for Lecture 7's compare, a return ends the run of the method, so no run comes out of the true branch. So the claim after the if is the claim at the top of the body with a[i] != x added:

    0 <= i and i < a.length and a[i] != x and
    a[k] != x for every k < i
    

    At line 21 the old value of i is i - 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 above
    

    That claim implies I, since 1 <= i implies 0 <= i.

  • Check (1) passes. With i = 0, 0 <= i and i <= a.length. No index is below 0, so a[k] != x for every k < 0 is 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
/**
 * Adds up the numbers 1 through n.
 *
 * @param n the last number to add
 * @requires 0 <= n
 * @return 1 + ... + n
 */
static long sumTo(int n) {
    long s = 0;
    int i = 0;
    while (i < n) {
        s = s + i;
        i = i + 1;
    }
    return s;
}

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, s is 1 short of I. The claim at the top of the body is I with i < n added. At line 19 the forward rule writes s0 for the old value of s, and I says what s0 is:

    s = s0 + i              the forward rule
      = 1 + ... + i + i     since s0 = 1 + ... + i
    

    At line 20 the old value of i is i - 1:

    s = 1 + ... + (i - 1) + (i - 1)   from s = 1 + ... + i + i
      = (1 + ... + i) - 1
    

    I 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, take n = 1, i = 0, and s = 0, where I and i < n is true. The body leaves s = 0 and i = 1, where I asks for s = 1.

  • Checks (1) and (3) pass. Before the loop, s = 0 and i = 0, and the sum of no terms is 0. After the loop, not (i < n) and i <= n give i = n, so I gives s = 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 adds i before raising it, so it adds 0, 1, 2, and 3, and misses 4.

  1. 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 is false in every state, would decide whether the loop can halt. Rice's theorem (1953) extends the result to every property of what a program computes. ↩