A working reference to David Gries’ classic on proving programs correct — predicate logic, weakest preconditions, and the discipline of developing a program and its proof hand in hand rather than coding first and testing later.
Student edition · understand, apply, verify
Connect a specification to an algorithm through predicates, weakest preconditions, invariants, and termination.
What you should be able to do: Explain both what a program computes and why it terminates under its input contract.
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.
This page is the overview. For the full treatment, read the eight in-depth pages below — each takes one part of the book and works through the definitions, laws, and derivations in detail.
Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.
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.
Predicates and state
A predicate is a true/false statement about values; a state assigns values to variables. A state satisfies P when P evaluates true in it.
Example. P:x≥0 holds in state x=3, not x=−1. 'Array sorted' is a predicate about a sequence.
Boundary. Specify integer/real/array domains. Division and indexing must be defined before reasoning about truth.
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.
Weakest preconditions
wp(S,Q) is the least restrictive entry predicate guaranteeing S terminates normally and establishes Q in this guide's model. Derive it backward from the result.
Example. For x:=x+3 and Q:x>10, wp is x>7. For division, also require the denominator nonzero.
Boundary. Weakest liberal precondition (wlp) omits termination. Undefined expressions need definedness checks, not just textual substitution.
Loop invariants
A predicate true after initialization and after each iteration. Together with the false exit guard it establishes the required result.
Example. While summing, preserve 0≤i≤n and s=sum(b[0:i]); add b[i] then advance i.
Boundary. An invariant alone does not prove termination. Validate array/domain bounds and establish the invariant before iteration.
Variants and well-founded orders
A variant strictly decreases each iteration in a well-founded order. A natural-number variant cannot decrease forever while remaining nonnegative.
Boundary. A positive real can halve forever. Use discrete domains or another well-founded order; nested loops need a correct combined or lexicographic bound.
David Gries’ The Science of Programming (1981) makes one radical claim: a correct program and its correctness proof should grow together. You do not write code and then hope tests catch the bugs; you let the specification — a precondition and a postcondition — drive the code, so that the finished program is correct by construction. The mathematics behind this is Dijkstra’s calculus of weakest preconditions, and the everyday tool it hands you is the loop invariant. This guide walks the logic prelude, the semantics of a tiny language, and the program-development strategies, with worked derivations.
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.
The prevailing habit — then and, honestly, now — is to write the program first and reason about it (if at all) afterward. Gries inverts this. The specification comes first as a pair of predicates, and each construct you add is chosen to satisfy the proof obligation it creates. Debugging is thereby replaced by derivation.
✗ Code-first, test-later
# write it, run it, patch what breaks
def divide(a, b):
q = 0
r = a
while r >= b:
r = r - b
q = q + 1
return q, r
# is it right for a=0? b>a? negatives?# you find out from failing tests
✓ Spec-first, correct by construction
# Start from the contract, derive the body# pre: a ≥ 0 and b > 0# post: a = q*b + r and 0 ≤ r < b# invariant: a = q*b + r and 0 ≤ r# the loop guard, init, and body all# fall out of that invariant (see below)
Gries’ famous line captures the spirit: “A program and its proof should be developed hand in hand, with the proof usually leading the way.” Testing shows the presence of bugs, never their absence; a proof addresses the absence directly.
Part I: The logic prelude
Before any programming, the book spends real time on the mathematics you will reason in. Skipping it is why most people bounce off formal methods.
State. A state is an assignment of values to the program’s variables — a single point in the space of all possible variable values.
Predicate. A predicate is a boolean expression over variables; equivalently, it names the set of states in which it is true. x > 0 denotes every state where x is positive. T (true) denotes all states; F denotes none.
Stronger / weaker.P is stronger than Q if P ⇒ Q — a stronger predicate is more restrictive (a smaller set of states). F is the strongest predicate; T is the weakest. “Weakest” will matter enormously in a moment: it means “the most permissive condition that still works.”
Quantifiers. The book fixes a clean notation for ∀ and ∃ over ranges, plus the identities you need to manipulate them — De Morgan, distribution, range splitting — which reappear constantly when reasoning about arrays.
Why this is the foundation: a specification is just two predicates (a precondition and a postcondition), and a proof is a chain of implications between predicates. If predicate manipulation is shaky, program proofs are impossible. Master the algebra first.
Part II: Weakest preconditions
The heart of the book is Dijkstra’s predicate transformerwp. For a command S and a desired postcondition R:
wp(S, R) = the set of all states such that executing S is guaranteed to terminate in a state satisfying R.
Read it as: “the weakest precondition under which S is guaranteed to establish R.” It is the exact, most permissive requirement on the starting state. Note the word guaranteed — wp encodes total correctness (the command must terminate), not merely partial correctness.
Hoare triples vs. wp
The Hoare triple {P} S {Q} means: if P holds and S is executed, then Q holds afterward. It relates to wp by a single implication:
{P} S {Q} ⇔ P ⇒ wp(S, Q)
So wp(S, Q) is the weakest such P — every valid precondition for reaching Q implies it. This is why deriving wp backward from the postcondition is the engine of program construction.
Healthiness properties of wp
Every well-defined command’s wp obeys four laws. They are both a sanity check and a proof toolkit.
Law
Statement
Meaning
Excluded miracle
wp(S, F) = F
No command can reach the impossible postcondition from any state.
Distributivity of ∧
wp(S, Q) ∧ wp(S, R) = wp(S, Q ∧ R)
Establishing both is establishing each.
Monotonicity
Q ⇒ R implies wp(S,Q) ⇒ wp(S,R)
A weaker goal has a weaker requirement.
Distributivity of ∨
wp(S,Q) ∨ wp(S,R) ⇒ wp(S, Q∨R) (equality if S deterministic)
Nondeterministic commands may weaken the disjunction.
The assignment axiom
The single most surprising rule in the book. The weakest precondition of an assignment is the postcondition with the variable textually replaced by the expression — and the substitution runs backward, which trips up everyone the first time.
wp(“x := e”, R) = R with every free x replaced by e (written R[x := e])
✗ The intuitive-but-wrong direction
# "x := 5, so afterward x = 5"# people try to push forward and# substitute into the PREcondition.# That is not what the axiom says.# Want post: x > 10 after x := x + 1# Wrong guess: pre is x > 10
✓ Backward substitution
# wp("x := x + 1", x > 10)# = (x > 10)[x := x + 1]# = (x + 1 > 10)# = x > 9# So: {x > 9} x := x + 1 {x > 10} ✓# Substitute into the POSTcondition.
A subtlety Gries is careful about: e must be defined in the starting state (no division by zero, no array index out of bounds). The full rule carries a domain condition: wp(“x := e”, R) = domain(e) ∧ R[x := e].
The commands & their wp
The language is deliberately tiny — five constructs — because a small language means few axioms to trust. Everything reduces to these.
Command
Meaning
wp(S, R)
skip
do nothing
R
abort
fail / never establish anything
F
x := e
assignment
R[x := e] (with e defined)
S1 ; S2
sequential composition
wp(S1, wp(S2, R))
if … fi
guarded selection
see below
do … od
guarded iteration
see below
Composition is the key insight to internalise: to find the precondition of a sequence, push the postcondition right-to-left through the statements. You reason from the goal backward to the start — the opposite of how the machine runs, and exactly how you should design.
# {?} t := x ; x := y ; y := t {x = Y0 and y = X0} (swap)
wp("y := t", x=Y0 ∧ y=X0) = (x=Y0 ∧ t=X0)
wp("x := y", x=Y0 ∧ t=X0) = (y=Y0 ∧ t=X0)
wp("t := x", y=Y0 ∧ t=X0) = (y=Y0 ∧ x=X0)
# precondition x=X0 and y=Y0 → the swap is proven correct
Guarded selection (IF)
Selection is Dijkstra’s guarded command: a set of guard → statement pairs. At runtime one guard whose condition is true is chosen (nondeterministically if several are), and its statement runs. If no guard is true, the command aborts — so covering all cases is a proof obligation, not an afterthought.
Read the two halves: the first conjunct (B1 ∨ B2 ∨ B3) says at least one guard must hold — this is what forces you to handle every case. Each remaining conjunct says whichever branch is taken must establish R. The classic max-of-two, correct by covering both guards:
✗ Gap in the guards
if x > y → m := x
[] x < y → m := y
fi# x = y is uncovered → aborts.# (B1 ∨ B2) is not T, so wp fails.
✓ Total, overlap allowed
if x ≥ y → m := x
[] y ≥ x → m := y
fi# x = y satisfies both; either arm# establishes m = max(x,y). Overlap# is fine — both give a correct R.
The loop and its invariant (DO)
The loop is where all the machinery pays off. A guarded loop repeats: while any guard is true, pick one and run its body; stop when all guards are false.
do B → S od# repeat S while B holds; stop when ¬B
wp of a loop cannot be written as a neat closed form — it is a fixed point over unbounded iteration. So instead of computing it, you prove the loop against a specification using two inventions of your own: a loop invariantP and a bound functiont.
Invariant P: a predicate true before the loop and after every iteration. It is the “work done so far is consistent” assertion — the heart of the loop’s meaning. On exit, you know P ∧ ¬B.
Bound function t: an integer expression, bounded below by 0 while the loop runs and strictly decreased by every iteration. It is what proves termination.
To prove {Q} init; do B → S od {R} totally correct, discharge this five-part checklist:
#
Obligation
What it guarantees
1
Q ⇒ wp(init, P)
Initialisation establishes the invariant.
2
{P ∧ B} S {P}
Each iteration preserves the invariant.
3
P ∧ ¬B ⇒ R
On exit, the invariant plus the false guard give the result.
4
P ∧ B ⇒ t > 0
While looping, the bound stays positive.
5
{P ∧ B} t0 := t; S {t < t0}
Each iteration strictly decreases the bound ⇒ termination.
The whole game: pick the right invariant and the loop writes itself. Conditions 1–3 tell you the initialisation, the guard, and confirm the postcondition; conditions 4–5 pin down the body so it makes progress. The invariant is not documentation added afterward — it is the design.
Developing loops from invariants
Where does the invariant come from? Gries’ most practical contribution: a small set of heuristics for manufacturing an invariant by weakening the postcondition. A good invariant is often the postcondition made reachable early — something true at the start, that the loop drives toward the full result.
Strategy
How
When it fits
Delete a conjunct
Drop one term of a conjunctive postcondition; that dropped term becomes the guard’s job.
Postcondition is A ∧ B. Most common by far.
Replace a constant by a variable
Generalise a fixed bound (e.g. n) into a fresh variable that the loop grows toward it.
Postcondition mentions a constant limit, like summing 0..n.
Enlarge the range of a variable
Let a variable range over a superset of its final value, narrowing each step.
Searching or shrinking an interval.
Combine pre- and postconditions
Take the parts of both that must always hold and conjoin them.
When state must stay valid throughout.
The dominant one, “delete a conjunct,” is worth burning in: your postcondition is usually “the answer is computed and we’ve processed everything.” Keep the first part as the invariant, and “we’ve processed everything” becomes ¬B — the reason the loop stops.
Worked example: integer division
Compute quotient q and remainder r of a ÷ b using only subtraction. This is the canonical Gries derivation.
# Specification# pre Q: a ≥ 0 and b > 0# post R: a = q*b + r and 0 ≤ r < b
Step 1 — invent the invariant by deleting a conjunct. The postcondition is (a = q*b + r) ∧ (0 ≤ r) ∧ (r < b). Drop the hardest-to-establish conjunct, r < b. What remains is the invariant:
P: a = q*b + r ∧ 0 ≤ r
Step 2 — the guard is the deleted conjunct, negated. We loop while r < b is not yet true, i.e. while r ≥ b. So B: r ≥ b, and on exit P ∧ ¬B gives exactly the full postcondition (checklist #3 ✓).
Step 3 — initialise to make P trivially true. Set q := 0; r := a. Then a = 0*b + a and a ≥ 0 from the precondition, so P holds (checklist #1 ✓).
Step 4 — the body must preserve P while shrinking a bound. Take bound t = r. When r ≥ b, subtract b from r and bump q — a = q*b + r is preserved (one b moves from r into the q*b term) and r strictly drops by b > 0 (checklist #2, #4, #5 ✓).
✓ Derived program
# {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}
✓ The invariant preserved (body check)
# before body: a = q*b + r, r ≥ b# after r:=r-b, q:=q+1 :# (q+1)*b + (r-b)# = q*b + b + r - b# = q*b + r = a ✓ invariant held# and r-b ≥ 0 since r ≥ b ✓ 0 ≤ r held# and r-b < r ✓ bound decreased
Every line of the program was forced by the specification and the invariant. Nothing was guessed and patched.
Worked example: linear search
Find the first index i in b[0..n-1] where b[i] = x, assuming x is present. This shows the replace-a-constant / enlarge-the-range flavour.
# pre Q: x occurs in b[0..n-1]# post R: 0 ≤ i < n and b[i] = x and x not in b[0..i-1]
Invariant — enlarge the range: let i range over the scanned prefix, asserting x has not been seen yet and x is still somewhere from i onward:
P: 0 ≤ i ≤ n ∧ x not in b[0..i-1] ∧ x occurs in b[i..n-1]
✓ Derived program
i := 0 ;
# invariant P (above); bound t: n - ido b[i] ≠ x →
i := i + 1
od# {b[i] = x and x not in b[0..i-1]}
✓ Why it terminates & is correct
# init: i=0 → prefix b[0..-1] empty,# x in b[0..n-1] from precond ✓ P# guard false: b[i] = x ✓ post# P says x is in b[i..n-1], so if# b[i] ≠ x it must be further right# → i+1 keeps P, and never runs off ✓# bound n - i drops each step > 0 ✓ halts
Notice the precondition (“x is present”) is exactly what keeps the invariant’s last conjunct alive and guarantees the loop never indexes past n-1. Drop that precondition and the derivation immediately tells you what breaks — you would need a compound guard i < n cand b[i] ≠ x. The proof is doing design work for you.
Worked example: derive nonnegative integer division
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.
Step
Evidence, decision, and expected result
State the contract
Inputs are integers a≥0,b>0. Return q,r with a=q×b+r and 0≤r
Initialize an invariant
q=0,r=a establishes a=q×b+r and r≥0.
Subtract one divisor
While r≥b, set r:=r−b and q:=q+1. The relation stays true and integer r strictly decreases.
Conclude on exit
r
Check your understanding: Does correct output on ten examples prove total correctness?
No. Total correctness needs a model-based argument for all permitted inputs and termination; finite tests validate implementation cases.
Run and change the example
Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:
python3 guide_labs.py division
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.
Weaken the postcondition — delete a conjunct, replace a constant by a variable, enlarge a range.
The recurring theme: the invariant is the design. Once you can state what stays true across every iteration, the initialisation, the guard, the body, and the termination argument all follow almost mechanically. Gries’ calculus is less about proving finished programs and more about a discipline of thought that produces correct ones on the first try.