Documenting Programs & Historical Notes

Gries’ closing chapters make a quiet but radical point: the assertions you invented to prove a program correct are its best documentation — and they sit at the end of a lineage that runs Floyd → Hoare → Dijkstra → Gries, from “verify after” to “derive correct by construction.”

Student edition · understand, apply, verify

Document the specification, invariant, frame, and progress argument that readers need to maintain correct code.

What you should be able to do: Explain why assertions complement a proof but do not validate public inputs when disabled.

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 is an original study summary for quick reference, covering the documentation and historical-notes material near the end of David Gries’ The Science of Programming (1981). Notation follows Gries and Dijkstra: {P} S {Q} for a Hoare triple, wp for weakest precondition, invariants and bound functions for loops. See the book for the full, rigorous treatment.

Contents

  1. Assertions ARE the documentation
  2. How to document a loop
  3. Documentation guidelines
  4. Assertions as executable checks
  5. Historical notes — the lineage
  6. What to take away
  7. Summary
  8. Concepts explained
  9. Worked example
  10. Practice and verification

Concepts explained in this guide

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.

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.

Example. An array scan uses n−i; repeated subtraction division uses remaining dividend.

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.

Refinement and observational equivalence

Replace an implementation while preserving the required observable contract, including outputs/effects. Optimize by adding maintained relations, not discarding correctness.

Example. A running sum adds invariant s=sum(prefix) instead of recomputing that prefix each time.

Boundary. Floating-point reassociation, exceptions, mutation, and concurrency can make apparent mathematical equivalence fail at runtime.

Assertions ARE the documentation

Ask what a comment is worth and you get an uncomfortable answer: a comment records what someone hoped the code did at the moment they wrote it. It is prose, unchecked, and it rots the instant the code beneath it changes. Gries’ whole method hands you something categorically better. The precondition, the postcondition, and above all the loop invariant are not decoration bolted on afterward — they are the very predicates the program was derived from. They state intent precisely, in the same logic the code obeys, and they are checkable.

A comment says what someone hoped is true.
An assertion says what must be true — or the program is wrong.

That is the shift in status. The pre/postcondition pair is the contract: it tells a reader exactly what the code assumes and exactly what it promises, without their having to read the body. The invariant tells them the idea of the loop — the single fact that stays true across every iteration — which is the one thing raw code never reveals. These assertions are the most valuable documentation a program can carry precisely because they are the documentation the compiler, the test suite, and a proof can all hold you to.

The reframe: in Gries’ discipline documentation is not something you add to finished code. It is the material the code was built from. Keeping the assertions is keeping the design.

How to document a loop

A loop is the hardest construct to read cold, because its meaning lives in what accumulates over many iterations, not in any single line. Gries’ prescription is exact: next to every loop, record four things — the precondition that holds on entry, the invariant that holds before and after each iteration, the bound function that proves it terminates, and the postcondition it establishes on exit. Those four are not optional annotations; they are the loop’s specification.

✗ A comment that says nothing
q = 0
r = a
# loop through items
while r >= b:
    r = r - b
    q = q + 1
# why does this compute a / b ?
# what stays true each pass?
# why does it stop? the reader
# must reverse-engineer all of it.
✓ Annotated with invariant + bound
# pre  Q: a ≥ 0 and b > 0
q = 0
r = a
# invariant P: a = q*b + r  and  0 ≤ r
# bound     t: r        (t ≥ 0, t decreases)
while r >= b:          # guard B
    r = r - b
    q = q + 1
# post R: a = q*b + r  and  0 ≤ r < b

Read the two versions side by side. The left tells you the shape of the code you can already see. The right tells you the four things the code cannot tell you: what it needs (a ≥ 0, b > 0), the exact relationship it maintains (a = q*b + r ∧ 0 ≤ r), why it halts (r is a non-negative integer that strictly drops each pass), and what it delivers (0 ≤ r < b). Anyone can now verify the loop without running it — on exit, invariant plus negated guard (0 ≤ r and r < b) yields the postcondition directly. The same skeleton documents a summation loop: invariant s = (sum of A[0..i-1]) ∧ 0 ≤ i ≤ n, bound n - i, post s = sum of A[0..n-1].

Documentation guidelines

The discipline distills into a handful of rules. None are about volume — more comments is not better documentation. They are about recording the things the code genuinely cannot say for itself, and never the things it already says plainly.

RuleWhySmell it replaces
Document the WHAT / the intent, not the obvious HOW.The mechanics are already in the code. The intent — the relationship being maintained, the contract being met — is not.i = i + 1 # increment i
Keep assertions honest; update them with the code.A stale assertion is worse than none — it actively misleads. When the code changes, the invariant changes with it, or the derivation no longer holds.A postcondition that describes last quarter’s behaviour.
Name variables for their role in the invariant.If i is “number of elements already processed,” the invariant reads itself. Good names make the assertions almost redundant — the best outcome.x, tmp, flag carrying real meaning.
A procedure is documented by its spec, not its body.A caller should need only the pre/postcondition to use it correctly. If they must read the implementation, the specification has failed.“See the code for what it returns.”
Avoid comments that merely restate code.They double the maintenance surface and add zero information. Every comment must earn its place by saying something the code cannot.# set total to zero above total = 0.
The through-line: comment the reasoning, assert the truth. A comment explains why a non-obvious choice was made; an assertion states what must hold. Neither should ever narrate the how — the code owns that.

Assertions as executable checks

Here the documentation stops being passive. The very predicate that proves a loop correct can be dropped into the running program as an assert — a runtime check that fails loudly the moment reality diverges from the proof. The invariant that lived in a comment becomes an executable guard against regression.

✓ The invariant, made executable
def divide(a, b):
    assert a >= 0 and b > 0        # pre Q
    q, r = 0, a
    while r >= b:
        assert a == q*b + r and r >= 0  # inv P
        r -= b
        q += 1
    assert a == q*b + r and 0 <= r < b  # post R
    return q, r
✓ The same predicate as a test oracle
# property-based test: for ANY valid input
# the postcondition must hold. The oracle is
# the specification itself, not a table of
# hand-picked expected outputs.
def test_divide(a, b):        # a ≥ 0, b > 0
    q, r = divide(a, b)
    assert a == q*b + r
    assert 0 <= r < b
    # one predicate certifies every case

This is the bridge to modern practice. Design by contract (Eiffel, and the require/ensure/invariant constructs it inspired) is Gries’ pre/post/invariant triad promoted to first-class language features that execute. Property-based testing (QuickCheck, Hypothesis) uses the postcondition as an oracle across machine-generated inputs, so the specification checks thousands of cases you would never enumerate by hand. Neither replaces a proof — an assertion that never fires proves nothing about the paths it did not take, and tests still show only the presence of bugs. But the same predicate serves in three roles at once: it proves the code, documents the code, and guards the code against future change. That triple duty is the payoff.

Historical notes — the lineage

Gries did not invent this calculus; he synthesized and taught it. The closing notes trace a fifteen-year arc in which the field’s ambition shifted from checking a finished program to growing a correct one. Four papers carry the story.

YearWho & whatThe contribution
1967Robert W. Floyd
“Assigning Meanings to Programs”
Attaches assertions to the edges of a flowchart and derives verification conditions: if each assertion implies the next across every node, the program is correct. Introduces the well-ordered / bound idea — a quantity that strictly decreases in a well-founded order — to prove termination. The first rigorous framework for what a program means.
1969C.A.R. Hoare
“An Axiomatic Basis for Computer Programming”
Turns Floyd’s idea into a formal logic. The {P} S {Q} triple becomes the central object, and each language construct gets an axiom or inference rule (assignment, composition, conditional, the while-rule with its invariant). This is Hoare logic — correctness as formal deduction.
1975–76Edsger W. Dijkstra
“Guarded Commands, Nondeterminacy and Formal Derivation of Programs” (1975) & A Discipline of Programming (1976)
Replaces verification-after with derivation. The weakest precondition wp(S,R) is a predicate transformer computed backward from the goal; guarded commands (if…fi, do…od) build nondeterminacy in cleanly. Now you calculate the program from its specification rather than guessing and checking.
1981David Gries
The Science of Programming
Synthesizes and teaches the method to working programmers. Supplies the logic prelude, makes the calculational, predicate-transformer discipline learnable, and adds the practical heuristics — inventing invariants by weakening the postcondition — that turn theory into a repeatable way of writing code.

The single most important movement across this table is the change in when correctness enters. Floyd and Hoare give you tools to verify after: write the program, then prove it meets its spec. Dijkstra and Gries invert this into derive correct by construction: let the spec and the emerging proof drive the code so it is right the first time. The bound function that Floyd introduced for termination survives untouched all the way to Gries — a reminder that the ideas accreted rather than replaced one another.

Floyd, Hoare  →  verify AFTER the fact
Dijkstra, Gries  →  DERIVE correct by construction

What to take away

Notation dates; the discipline does not. Few programmers today write do…od or compute a wp by hand, yet the habits of mind the calculus instills are exactly the ones that separate reliable code from hopeful code.

The discipline outlives the specific notation. Whether you write a formal wp derivation or just a one-line invariant comment above a for loop, you are practising the same craft Floyd, Hoare, Dijkstra, and Gries built: making a program’s meaning explicit, checkable, and correct by design.

Worked example: a boundary check versus a debug assertion

Work through the state changes and the exact claim being checked.

a boundary check versus a debug assertionValidate at the public boundary: Use an explicit exception when n is not an integer valid prefix length.. Document the loop: State s=sum(b[0:i]), index bounds, and variant n−i beside the implementation.. Use internal checks thoughtfully: Assertions can detect invariant mistakes in tests, but python -O removes them.. Maintain the contract: When changing code, review output, mutation, errors, numeric model, and termination rather than leaving stale comments.1Validate at the public boundary2Document the loop3Use internal checks thoughtfully4Maintain the contract
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
Validate at the public boundaryUse an explicit exception when n is not an integer valid prefix length.
Document the loopState s=sum(b[0:i]), index bounds, and variant n−i beside the implementation.
Use internal checks thoughtfullyAssertions can detect invariant mistakes in tests, but python -O removes them.
Maintain the contractWhen changing code, review output, mutation, errors, numeric model, and termination rather than leaving stale comments.
Check your understanding: Should a public input validator rely on assert?

No. Use explicit exceptions for required runtime validation; reserve assert for internal/debug assumptions where disabling is acceptable.

Run and change the example

Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:

python3 guide_labs.py documentation
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.

Sources and further verification

Summary

IdeaThe one-line takeaway
Assertions as documentationA comment says what someone hoped; an invariant says what must be true — precise and checkable.
Documenting a loopRecord four things beside every loop: precondition, invariant, bound function, postcondition.
Guideline: intent over mechanicsDocument the WHAT, never the obvious HOW; the code already owns the how.
Guideline: honest assertionsA stale assertion misleads — update it with the code or delete it.
Guideline: spec over bodyA procedure is documented by its specification; a caller should never need to read the body.
Executable assertionsThe predicate that proves a loop can run as an assert and serve as a test oracle.
Bridge to modern practiceDesign by contract and property-based testing are Gries’ triad promoted to first-class tooling.
Floyd (1967)Flowchart assertions, verification conditions, and the bound idea for termination.
Hoare (1969)The {P} S {Q} triple and an axiom per construct — correctness as formal deduction.
Dijkstra (1975–76)Weakest preconditions and guarded commands — derive the program, do not just check it.
Gries (1981)Synthesizes and teaches the calculational discipline, with heuristics for inventing invariants.
The historical shiftFrom “verify after” (Floyd, Hoare) to “derive correct by construction” (Dijkstra, Gries).
What outlives the notationThink in invariants, let the spec lead, prove termination, keep assertions living.
Related pages in this series: Foundations for the logic these assertions are written in, The iterative command for the invariant-and-bound machinery, and Developing programs for inventing the invariants that become your documentation.

⌂