The Iterative Command — Loops, Invariants & Bounds

The centrepiece of Gries’ calculus: how the guarded do…od loop is given meaning, why its weakest precondition has no closed form, and how a loop invariant plus a bound function let you prove — and design — a loop that is totally correct.

Every other command in Gries’ tiny language has a weakest precondition you can write down and compute directly. The loop does not. A loop can iterate any number of times depending on its data, so its wp is a fixed point over unbounded repetition — you cannot evaluate it in your head or on paper. Gries’ answer is to stop trying. Instead of computing the loop’s wp, you prove the loop correct against a specification using two objects you invent yourself: a loop invariant that captures what stays true across iterations, and a bound function that forces termination. This page is the heart of the method — the same invariant that proves a loop is what lets you write it in the first place.

This is an original study summary for quick reference. Notation follows Gries and Dijkstra: wp for weakest precondition, {P} S {Q} for a Hoare triple, guarded commands (if…fi, do…od) for the programming language. See the book for the full, rigorous treatment.

Contents

  1. The guarded loop
  2. Why wp has no closed form
  3. The loop invariant P
  4. The bound function t
  5. The five-part checklist
  6. Reading the checklist as a design tool
  7. Fully worked example: integer division
  8. Partial vs total correctness
  9. Summary

The guarded loop

The iterative command is Dijkstra’s guarded repetition. In its simplest single-guard form it repeats one body while a condition holds:

do B → S od     # repeat S as long as B is true; stop when ¬B

The general form has several guarded commands, exactly like if…fi but repeated:

do B1 → S1
[] B2 → S2
   …
[] Bn → Sn
od

Semantics. On each pass the loop looks at all the guards. If at least one is true, it chooses one true guard (nondeterministically, if several are true) and executes its body; then it repeats. The loop terminates only when all guards are false. The crucial payoff is what you know at that moment: on normal termination the loop has established ¬B for the single-guard form, or ¬B1 ∧ ¬B2 ∧ … ∧ ¬Bn for the general form. Unlike if…fi, an empty set of true guards does not abort the loop — it is precisely how the loop ends normally.

On termination of do B1→S1 [] … [] Bn→Sn od:
¬B1 ∧ ¬B2 ∧ … ∧ ¬Bn holds.

Why wp has no closed form

For every other command, wp(S, R) is a finite formula you can build mechanically. The loop breaks this, because the number of iterations is not fixed — it depends on the starting state. Formally, wp(DO, R) is defined as a limit over “terminates in at most k iterations” predicates. Write IF for the selection command with the loop’s guards and bodies. Then:

H0(R) = ¬B ∧ R
Hk(R) = wp(IF, Hk−1(R)) ∨ H0(R)

wp(DO, R) = (∃ k ≥ 0 : Hk(R))

Read it informally. H0(R) says “the loop is already done — the guard is false and R already holds, so zero iterations suffice.” Hk(R) says “either the loop finishes in at most k−1 more iterations after one step, or it was already done.” The full wp is then: “there exists some number of iterations k after which the loop has terminated in a state satisfying R.” That existential over an unbounded k is the fixed point — and there is no way to collapse it into a single computable expression for an arbitrary loop.

The practical consequence: because you cannot compute wp(DO, R) directly, you never try. Instead you supply an invariant P and a bound function t and discharge a fixed checklist of implications. The checklist is sound — passing it guarantees the loop meets its specification — and, unlike the fixed point, it is finite and mechanical.

The loop invariant P

The invariant P is a predicate that is true before the loop begins and true again after every complete iteration of the body. It may be temporarily broken in the middle of the body, but the body must always restore it before the next guard test. Intuitively, P asserts that “the work done so far is consistent and correct” — it is the loop’s running promise about its own partial results.

P holds initially  ∧  each iteration re-establishes P
∴ on exit you know  P ∧ ¬B

That exit condition, P ∧ ¬B, is the entire reason the invariant is worth inventing. The invariant captures the correct-so-far part; the negated guard captures the done part; together they must add up to the postcondition you want. In a real sense the invariant is the meaning of the loop: it says what the loop is doing, independent of how many times it runs. A loop without a stated invariant is a loop nobody — including its author — can be sure is correct.

The bound function t

The invariant guarantees that if the loop stops, it stops in a good state — but it says nothing about whether the loop stops at all. Termination is the job of the bound function t: an integer-valued expression over the program variables with two properties.

1. t > 0 while the loop is still running (P ∧ B ⇒ t > 0)
2. every iteration strictly decreases t

The termination argument is then a simple appeal to the well-ordering of the integers: t starts at some finite value, drops by at least one each pass, and cannot fall below zero while the loop runs. An integer cannot decrease forever while staying positive, so after finitely many iterations the guard must become false and the loop halts. The bound function is the loop’s clock, counting down to a guaranteed stop.

The five-part checklist for total correctness

To prove that {Q} init; do B → S od {R} is totally correct — it terminates and establishes R — using invariant P and bound function t, discharge all five obligations below. Passing every one is sufficient; there is nothing left to check.

#ObligationWhat it guarantees
1Q ⇒ wp(init, P)The initialisation, run from any state satisfying the precondition, establishes the invariant — the loop starts in a valid state.
2{P ∧ B} S {P}  (i.e. P ∧ B ⇒ wp(S, P))The body, run when the invariant holds and a guard is true, re-establishes the invariant — iterations preserve correctness-so-far.
3P ∧ ¬B ⇒ ROn exit, the invariant together with the false guard is strong enough to imply the desired postcondition — stopping means finishing.
4P ∧ B ⇒ t > 0Whenever the loop is about to iterate, the bound is strictly positive — there is still room to count down.
5{P ∧ B} t0 := t; S {t < t0}Each iteration strictly decreases the bound (t0 is a fresh variable snapshotting t before the body) — so the loop cannot run forever.

Conditions 1–3 establish partial correctness (correct answer if it halts); conditions 4–5 add termination. Together they give total correctness.

Reading the checklist as a design tool

The five conditions are usually presented as a proof burden, but Gries’ deeper point is that they are a design procedure. Read the roles they play:

The order matters: the invariant is chosen first, and everything else follows. You do not write a loop and then hunt for its invariant; you decide what must stay true, and the guard, the initialisation, the body, and the termination argument fall out of it almost mechanically. That inversion — invariant leads, code follows — is the whole transition into program development, covered in inventing invariants & developing loops.

Fully worked example: integer division

Compute the quotient q and remainder r of a ÷ b using only repeated subtraction. This is the canonical Gries derivation, worked here through every one of the five checklist conditions.

# Specification
# pre  Q:  a ≥ 0  and  b > 0
# post R:  a = q*b + r   and   0 ≤ r < b

The design choices, made invariant-first:

invariant P:  a = q*b + r  ∧  0 ≤ r
guard     B:  r ≥ b
bound     t:  r
init      :  q := 0 ; r := a
body     S:  r := r - b ; q := q + 1

The invariant is the postcondition with its hardest conjunct, r < b, deleted; the guard is exactly that deleted conjunct negated. Now walk the checklist.

Condition 1 — Q ⇒ wp(init, P) (init establishes P). Compute wp("q:=0; r:=a", a = q*b + r ∧ 0 ≤ r) by backward substitution: replace q by 0 and r by a, giving a = 0*b + a ∧ 0 ≤ a, i.e. a = a ∧ a ≥ 0. The first term is trivially true; the second is exactly the precondition a ≥ 0. So Q ⇒ wp(init, P). ✓

Condition 2 — {P ∧ B} S {P} (body preserves P). Assume a = q*b + r, 0 ≤ r, and r ≥ b. After the body sets r to r−b and q to q+1, check the invariant on the new values:

(q+1)*b + (r−b) = q*b + b + r − b = q*b + r = a

so a = q*b + r still holds; and since r ≥ b we have r − b ≥ 0, so 0 ≤ r still holds. The invariant is preserved. ✓

Condition 3 — P ∧ ¬B ⇒ R (exit gives the result). On exit the guard is false: ¬(r ≥ b), i.e. r < b. Combined with the invariant a = q*b + r ∧ 0 ≤ r, we get a = q*b + r ∧ 0 ≤ r < b — exactly the postcondition R. ✓

Condition 4 — P ∧ B ⇒ t > 0 (bound positive while looping). The bound is t = r. While looping, the guard B gives r ≥ b, and the precondition established b > 0, so r ≥ b > 0, hence t = r > 0. ✓

Condition 5 — {P ∧ B} t0 := t; S {t < t0} (bound strictly decreases). Snapshot t0 = r before the body. The body sets r to r − b, and since b > 0 we have r − b < r = t0. So the new bound t = r − b satisfies t < t0. The loop must terminate. ✓

All five hold, so the program below is totally correct by construction.

✓ Derived program — correct by construction
# {a ≥ 0 and b > 0}
q := 0 ;  r := a ;
# invariant P: a = q*b + r and 0 ≤ r
# bound     t: r
do r ≥ b →
     r := r - b ;
     q := q + 1
od
# {a = q*b + r and 0 ≤ r < b}

Every line was forced by the specification and the chosen invariant: nothing was guessed and later patched by testing.

Partial vs total correctness

It is worth naming exactly what each half of the checklist buys you. Conditions 1–3 alone establish partial correctness: if the loop terminates, its result satisfies R. They say nothing about whether it terminates — a loop that runs forever is vacuously partially correct, because it never produces a wrong answer, it simply never produces one.

LevelConditionsGuarantee
Partial correctness1, 2, 3 (invariant only)Correct answer if the loop halts. Says nothing about halting.
Total correctness1, 2, 3 + 4, 5 (invariant + bound)The loop halts and its result is correct.

Dropping the bound function t throws away conditions 4 and 5, leaving only partial correctness. Gries insists on total correctness — a program that might not terminate is not a program you have proven anything useful about. The bound function is not optional bookkeeping; it is what turns “correct if it stops” into “correct.”

Summary

IdeaThe one-line takeaway
Guarded loopdo B → S od runs while some guard is true; on termination all guards are false, so you know ¬B.
No closed-form wpwp(DO,R) = (∃k≥0 : Hk(R)) is a fixed point over unbounded iteration — uncomputable in general, so you prove with an invariant instead.
Loop invariant PTrue before the loop and after every iteration; on exit you have P ∧ ¬B. The invariant is the meaning of the loop.
Bound function tA positive integer expression that strictly decreases each iteration — it cannot fall forever, so the loop halts.
Five-part checklistinit establishes P; body preserves P; P ∧ ¬B ⇒ R; P ∧ B ⇒ t > 0; body decreases t.
Checklist as designChoose the invariant first; the guard, init, body, and termination argument follow almost mechanically.
Worked divisionP: a=q*b+r ∧ 0≤r, B: r≥b, t: r — all five conditions discharged with plain algebra.
Partial vs totalInvariant alone gives “correct if it halts”; add the bound for “correct.” Gries insists on total.
The recurring theme: the invariant is the design. Once you can state what stays true across every iteration and pick a bound that counts down to termination, the loop’s guard, initialisation, and body are almost forced. See weakest preconditions for the wp machinery underneath, the alternative command (IF) for the selection whose wp the loop’s fixed point reuses, developing programs for how to invent invariants, and worked examples for more derivations.