Weakest Preconditions

Hoare logic gives you the rules to verify a program, but it leaves you doing the creative work: at every step you have to guess the assertion in the middle and then check it fits. For a machine that is hopeless — you cannot ask a computer to "have an insight." In the mid-1970s Edsger Dijkstra reorganised the whole logic around a single function that removes the guessing entirely. Give it a command and a goal, and it computes the precondition you need. No search, no cleverness — just substitution, run backwards through the code.

That function is the weakest precondition, written \operatorname{wp}(C, Q). It turns Hoare logic from an art you practise into a calculation you can automate — the foundation of every modern program verifier.

What "weakest" means

Fix a command C and a desired postcondition Q. Many preconditions P might make the triple \{P\}\,C\,\{Q\} valid — but they are not equally good. A precondition that is too strong (demands too much) rules out starting states that would actually have worked. The weakest precondition is the most permissive one: it accepts every starting state from which running C is guaranteed to terminate in a state satisfying Q, and no others.

\operatorname{wp}(C, Q) = \text{the weakest } P \text{ such that } \{P\}\, C\, \{Q\} \text{ holds for total correctness.}

It is a predicate transformer: feed in a predicate Q on the final state, get out a predicate on the initial state. Crucially, Dijkstra defined it for total correctness — a state satisfies \operatorname{wp}(C,Q) only if C started there is certain to halt and land in Q. Termination is baked in, not bolted on.

Think of \operatorname{wp}(C, Q) as describing the largest possible set of safe launch pads. Strengthen it and you throw away good launch pads for no reason; weaken it and you include pads from which the flight might crash or never land.

The equivalence that ties it to Hoare logic

Because \operatorname{wp} captures exactly the safe starting states, checking any Hoare triple collapses into a single implication:

\{P\}\, C\, \{Q\} \quad\Longleftrightarrow\quad P \Rightarrow \operatorname{wp}(C, Q)

This is the whole trick of automated verification: reduce "does this code satisfy this contract?" to "is this one formula valid?", and hand the formula to a solver.

The rules — one per construct

Each rule tells you how to push a postcondition backwards across one language construct. Applied repeatedly, they walk from the end of the program to the start.

Notice there is no case analysis to invent and no middle assertion to guess. The rules are purely syntactic: they manipulate the text of Q. A program built from these four constructs has its weakest precondition computed, not discovered.

A backward calculation, line by line

Let us verify that a three-line block leaves y holding twice the larger of two inputs — postcondition Q \equiv y = 2\max(a,b). We start at the bottom with Q and drag it upward, applying one rule per line.

// Goal Q: y === 2 * Math.max(a, b) let m: number; if (a >= b) { m = a; } else { m = b; } // (1) conditional y = 2 * m; // (2) assignment y := 2*m // Q here: y === 2 * Math.max(a, b)

Step 2 — assignment y := 2m. Substitute 2m for y in Q:

\operatorname{wp}(y := 2m,\; y = 2\max(a,b)) \;=\; \bigl(2m = 2\max(a,b)\bigr) \;=\; \bigl(m = \max(a,b)\bigr).

Step 1 — the conditional with this new goal R \equiv m = \max(a,b). The then branch is m := a, giving \operatorname{wp} = (a = \max(a,b)); the else branch m := b gives (b = \max(a,b)). Assemble:

\bigl(a \ge b \Rightarrow a = \max(a,b)\bigr) \wedge \bigl(a < b \Rightarrow b = \max(a,b)\bigr).

Both implications are logically true (if a \ge b then indeed a = \max(a,b), and likewise for the other), so the weakest precondition of the whole block reduces to \texttt{true}. The program works for every input — proved, without ever guessing an intermediate assertion. We simply computed.

Liberal preconditions: dropping termination

Sometimes you only care about partial correctness — "if it halts, the answer is right" — and are content to argue termination separately. For that there is the twin transformer, the weakest liberal precondition \operatorname{wlp}(C, Q).

For the loop-free constructs above, \operatorname{wp} and \operatorname{wlp} coincide — no loops, so nothing can diverge. The difference only appears once loops (and their variants) enter the story.

The magic is that \operatorname{wp} for the loop-free fragment is purely syntactic: every rule is either "leave it alone", "substitute", or "combine with \wedge / \Rightarrow." Nothing requires understanding what the program means; you push the postcondition backward through the parse tree the way an interpreter pushes values forward. The output is one formula — the verification condition — and deciding its validity is the only part that needs a theorem prover. This clean split, "compute the VC syntactically, then discharge it," is exactly how tools like Dafny, Why3, and ESC/Java work: the wp calculus is their engine room. Loops are the one place the syntactic march stalls, because you cannot substitute your way across an unknown number of iterations — there you must supply an invariant, and the tool takes over again from both sides.

The single most common blunder is treating assignment forwards: given x := x+1 and goal x > 0, students write the precondition as "x > 0 then add one, so x > 1." Wrong direction. The rule is \operatorname{wp}(x := E, Q) = Q[x := E]: substitute the right-hand side into the postcondition, giving (x + 1) > 0, i.e. x > -1. Sanity check it — starting from x = 0 (which satisfies x > -1 but not x > 1) the assignment yields x = 1 > 0. So x > 1 is far too strong; it needlessly rejects the perfectly-good launch pad x = 0. Whenever your "precondition" looks stronger than the postcondition after an increment, you have run the substitution the wrong way.