Proving a Loop Correct

We have the pieces: a specification as pre- and postconditions, the Hoare while-rule, and the Initialise / Maintain / Conclude discipline for a loop invariant. This page assembles them into a single, complete, end-to-end proof of a non-trivial loop — the kind of argument you would write in a verification report or a viva. We will prove partial correctness: if the loop halts, its answer is right. (Termination is a separate obligation, handled with a variant later in the course.)

The example is integer division by repeated subtraction: given a dividend a \ge 0 and a divisor b > 0, compute the quotient q and remainder r with a = b\,q + r and 0 \le r < b. It is small enough to prove in full, rich enough that the invariant does real work.

The algorithm and its specification

// precondition P: a >= 0 AND b > 0 // postcondition Q: a === b*q + r AND 0 <= r AND r < b function divide(a: number, b: number): { q: number, r: number } { let q = 0; let r = a; // invariant I: a === b*q + r AND 0 <= r while (r >= b) { r = r - b; q = q + 1; } // exit: r < b, and with I this gives Q return { q, r }; }

The idea: start with q = 0 and the whole of a sitting in r; repeatedly peel off one b from r and add one to q. Every peel keeps the total b\,q + r equal to a — that conservation law is the heart of the invariant.

The chosen invariant

We take

I:\quad a = b\,q + r \;\wedge\; 0 \le r.

Where did it come from? Apply the loop-invariant heuristic: the postcondition is a = bq + r \wedge 0 \le r \wedge r < b. Drop the clause the guard will deliver at exit — the guard r \ge b is false on exit, giving exactly r < b — and what remains, a = bq + r \wedge 0 \le r, is the invariant. This is the "weaken the postcondition by the part the loop guard provides" move: the invariant is the goal minus the one fact you get for free by stopping.

Step 1 — Initialisation

Before the loop, the setup code runs q := 0 and r := a. We must show I holds. Substituting:

b \cdot 0 + a = a \;\checkmark \qquad\text{and}\qquad 0 \le a \text{ (from the precondition } a \ge 0). \;\checkmark

Both conjuncts of I hold, so \{P\}\; q := 0;\, r := a \;\{I\}. Formally this is two applications of the assignment axiom composed by the sequence rule, but the substitution above is all the content. The precondition a \ge 0 is exactly what we needed to get 0 \le r off the ground — a first hint that preconditions are not decoration.

Step 2 — Maintenance

We must prove the body preserves the invariant when the guard holds: \{I \wedge r \ge b\}\; r := r - b;\; q := q + 1 \;\{I\}. Assume I and the guard r \ge b at the top. Write the new values as r' = r - b and q' = q + 1. Check both conjuncts of I for the primed values.

Both conjuncts of I hold of q', r', so the body restores I. Maintenance is discharged. (In pure Hoare logic you would derive the precondition of the composed body by two backward applications of the assignment axiom and then use the rule of consequence to show I \wedge r \ge b implies it — the algebra above is that consequence step.)

Step 3 — Exit implies the postcondition

By the while-rule, the loop establishes I \wedge \neg(\text{guard}), i.e.

\underbrace{a = b\,q + r \;\wedge\; 0 \le r}_{I} \;\wedge\; \underbrace{r < b}_{\neg(r \ge b)}.

Line this up against the postcondition Q:\; a = bq + r \wedge 0 \le r \wedge r < b: the first two conjuncts come straight from I, and the third, r < b, is exactly the negated guard handed to us at exit. So I \wedge \neg(r \ge b) \Rightarrow Q — in fact they are identical. The proof is complete: \{P\}\; \texttt{divide} \;\{Q\}, partial correctness established for every valid input at once.

Observe how the roles divided up. The invariant carried the conservation law a = bq + r and the lower bound 0 \le r through every iteration; the guard, on failing, supplied the upper bound r < b that the invariant alone never guaranteed. Invariant plus negated guard equals postcondition — the signature shape of every loop proof.

The state trace

Concretely, a = 17, b = 5. Watch the invariant 17 = 5q + r hold at every top-of-loop, and the loop halt the instant r < 5.

Pointqr5q + rguard r \ge 5
before loop01717 ✓true — iterate
after iter 111217 ✓true — iterate
after iter 22717 ✓true — iterate
after iter 33217 ✓false — exit

On exit q = 3, r = 2: indeed 17 = 5 \cdot 3 + 2 with 0 \le 2 < 5. The invariant column never changes — that is the conservation law made visible — and the guard flips to false exactly when the remainder has been squeezed below the divisor.

A second, shorter example: array sum

The same skeleton, run at speed. Specify \texttt{sum}(A) to return \sum_{j=0}^{n-1} A[j] for an array of length n.

function sum(A: readonly number[]): number { let s = 0; let i = 0; // invariant I: s === A[0] + ... + A[i-1] AND 0 <= i <= A.length while (i < A.length) { s = s + A[i]; i = i + 1; } // exit: i === A.length, so s === A[0] + ... + A[n-1] return s; }

Initialise: s = 0 is the empty sum \sum_{j=0}^{-1} with i = 0 — holds. Maintain: if s = \sum_{j=0}^{i-1} A[j], then after s := s + A[i] and i := i+1 we have s = \sum_{j=0}^{i} A[j] = \sum_{j=0}^{i'-1} A[j] — restored (the guard i < n guarantees A[i] is a valid element, keeping the bound i \le n). Conclude: the guard fails at i = n, and I then reads s = \sum_{j=0}^{n-1} A[j] — the postcondition. Same three boxes, same invariant-plus-negated-guard finish. Once you have seen the pattern twice, every loop proof is a fill-in- the-blanks exercise: state the invariant, initialise it, maintain it, and read the postcondition off I \wedge \neg b.

Every step above assumed the loop reaches exit — we proved "if it stops, r < b and the answer is right," and never that it stops. For divide it plainly does: each iteration strictly decreases r by b > 0 while keeping r \ge 0, and a non-negative integer cannot decrease forever. That quantity — the variant r, strictly decreasing in the well-founded order of the naturals — is the extra ingredient that upgrades partial correctness to total correctness. We deliberately keep the two apart because they use different tools (invariants and consequence for the "right answer," a well-founded variant for "terminates"); the termination argument gets its own treatment later.

Try to run the proof with a weaker precondition and it collapses. If b could be 0, the invariant's conservation clause a = bq + r becomes a = r forever, the guard r \ge 0 never fails (assuming a > 0), and the loop spins without end — the postcondition's r < b = 0 is unsatisfiable for r \ge 0, so no correct answer even exists. That is exactly why the precondition demands b > 0. Symmetrically, the maintenance step's non-negativity conjunct 0 \le r' was rescued only by the guard r \ge b; delete the guard's contribution and a subtraction could push r negative, violating I. A loop proof is a tightly-wound machine: the precondition seeds the invariant, the guard powers maintenance and delivers the final conjunct at exit. When a proof won't close, the culprit is almost always a precondition you under-stated or a guard fact you forgot to use.