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.
Student edition · understand, apply, verify
Prove a loop by establishing and preserving its invariant and showing a decreasing well-founded measure.
What you should be able to do: Separate partial correctness from progress and verify the first/last iteration.
Assumes basic programming, not formal-methods notation. This guide defines the needed notation locally. Scalar proofs use mathematical integers unless stated otherwise; finite arrays require integer valid bounds. Guarded-command notation is not Python syntax. Machine overflow, floats, exceptions, aliasing, and concurrency need additional contracts.
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.
Boundary. A positive real can halve forever. Use discrete domains or another well-founded order; nested loops need a correct combined or lexicographic bound.
Hoare triples and total correctness
{P} S {Q} relates starting states P, program S, and resulting states Q. Partial correctness permits nontermination; total correctness also proves termination.
Example. {x=2} x:=x+1 {x=3} holds. A never-ending loop can satisfy some partial triples but not a total-correctness promise.
Boundary. Exceptions, concurrency, machine arithmetic, and nondeterminism require a stated model. Finite tests do not prove a universal triple.
Preconditions and postconditions
A precondition states required inputs/state before an operation. A postcondition states the result promised when those requirements hold and execution terminates normally. Together they make requirements testable.
Example. For sum_first(b,n), require integer 0≤n≤len(b) and numeric elements; promise the sum of b[0:n].
Boundary. State exceptions, mutation, numeric domains, and empty cases. A mathematical array range does not automatically validate a runtime input.
Guarded commands and nondeterminism
Guarded selection can choose any enabled branch. Every enabled choice must preserve correctness. No enabled selection guard aborts; an iteration stops when all guards are false.
Example. Choosing max(x,y) with guards x≥y and y≥x permits either branch on equality.
Boundary. This differs from Python's first-matching if/elif. Check coverage under the precondition/invariant; do not assume fairness unless specified.
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 invariantP 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 functiont: 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.
#
Obligation
What it guarantees
1
Q ⇒ 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.
3
P ∧ ¬B ⇒ R
On exit, the invariant together with the false guard is strong enough to imply the desired postcondition — stopping means finishing.
4
P ∧ B ⇒ t > 0
Whenever 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:
Conditions 1–3 pin down the frame. Condition 3 (P ∧ ¬B ⇒ R) forces the guard to be exactly what is missing between the invariant and the postcondition — so choosing P essentially determines B. Condition 1 tells you what the initialisation must accomplish (make P true cheaply). Condition 3 also confirms that the postcondition really is reachable this way.
Conditions 4–5 constrain the body to make progress. Once P and B are fixed, the body S is whatever preserves P (condition 2) while strictly shrinking a positive bound (conditions 4–5). That is a tight box — often only one reasonable body fits.
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: rdo 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.
Level
Conditions
Guarantee
Partial correctness
1, 2, 3 (invariant only)
Correct answer if the loop halts. Says nothing about halting.
Total correctness
1, 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.”
Worked example: sum a three-element array
Work through the state changes and the exact claim being checked.
Sequence for this example. The table explains the evidence and decision at each step.
Add b[i], then increment i. States become (1,2),(2,7),(3,8).
Measure progress
Variant n−i takes 3,2,1,0, decreasing each iteration.
Exit safely
i=n implies s=8. For n=0, the initialized empty sum already meets the goal.
Check your understanding: Can an invariant hold in a loop that never stops?
Yes. A separate well-founded progress argument is needed.
Run and change the example
Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:
python3 guide_labs.py invariants
python3 guide_labs.py test
The complete standard-library lab checks the stated finite cases and boundary inputs. Its invariants explain the reasoning; tests alone do not prove all possible inputs.
The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.
do B → S od runs while some guard is true; on termination all guards are false, so you know ¬B.
No closed-form wp
wp(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 P
True before the loop and after every iteration; on exit you have P ∧ ¬B. The invariant is the meaning of the loop.
Bound function t
A positive integer expression that strictly decreases each iteration — it cannot fall forever, so the loop halts.
Five-part checklist
init establishes P; body preserves P; P ∧ ¬B ⇒ R; P ∧ B ⇒ t > 0; body decreases t.
Checklist as design
Choose the invariant first; the guard, init, body, and termination argument follow almost mechanically.
Worked division
P: a=q*b+r ∧ 0≤r, B: r≥b, t: r — all five conditions discharged with plain algebra.
Partial vs total
Invariant 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.