01

Weakest Preconditions

One simple question, asked about your code:
“What must be true before this runs, so that what I want is true after?”

Plain idea

Figure out the exact input condition that makes code do the right thing

The trick

Read the code backwards, from the goal to the start

Why bother

Catch bugs by thinking, not just by testing & hoping

Lots of concrete code and real numbers ahead — no heavy math needed. (Idea: Dijkstra; teaching style: Gries.)

02 / Why

“Will this actually work?” THE EVERYDAY PROBLEM

Here's a tiny function. When is it safe to call? Testing a few inputs gives you some confidence. We want certainty.

A withdrawal

def withdraw(balance):
    balance = balance - 100
    return balance

# We want one thing to always hold:
# the balance must never go negative.
#
# So AFTER running:  balance >= 0

Testing only samples

  • Try balance = 500 → ends at 400. Fine.
  • Try balance = 100 → ends at 0. Fine.
  • Try balance = 50 → ends at −50. Bug!

Tests found one bad case by luck. What we really want is the exact rule for when it's safe — for every input, not just the ones we tried.

That exact rule is the weakest precondition.

03 / Vocabulary

Two Words: Before & After THE CONTRACT

Every piece of code has an implied contract: “if X is true when you call me, then Y will be true when I'm done.”

Precondition — the “before”

What must be true when the code starts. The caller's responsibility.

e.g. balance >= 100

Postcondition — the “after”

What you want to be true when the code finishes. The goal.

e.g. balance >= 0

Written as a contract

# { before:  balance >= 100 }
balance = balance - 100
# { after:   balance >= 0   }

Read it as a promise:

If balance >= 100 going in, then balance >= 0 coming out — guaranteed.

The curly-brace notation {before} code {after} is the standard way to write this. That's all it means.

04 / The idea

What wp Actually Means ONE QUESTION

wp(code, goal) answers:

“What's the condition on the input that guarantees the goal afterward?”

wp is short for weakest precondition. Give it two things:

  • the code that will run,
  • the goal you want true afterward,

and it hands back the before-condition you need.

Picture it as a machine

wp( code , goal ) work it out backwards goal (after) before-condition

“Guarantees” also means the code actually finishes — no infinite loops. We'll see how loops earn that on slides 11–13.

05 / The idea

Why “Weakest”? = MOST PERMISSIVE

Many before-conditions would be “safe.” We want the one that rules out the fewest inputs — the most generous rule that still works.

For withdraw, goal balance >= 0

Before-conditionSafe?Verdict
balance == 100yestoo strict — forbids 500
balance >= 1000yesway too strict
balance >= 100yesjust right — weakest
balance >= 50notoo loose — 50 breaks it

Weakest = the exact boundary

50 100 500 safe: balance >= 100 unsafe

The weakest precondition is the line in the sand: everything on the safe side works, and it doesn't exclude a single input that would have been fine. That makes it the most reusable rule.

06 / The trick

The Whole Trick: Read It Backwards GOAL → START

Start from the goal (the “after”) and push it back through each line until you reach the start. Let's do our withdrawal, with real numbers.

Push the goal through the line

# goal (after):  balance >= 0

# the line that runs:
balance = balance - 100

# Ask: for the RESULT to be >= 0,
# what must balance be BEFORE?
# The result is (balance - 100), so:
       balance - 100 >= 0
       balance >= 100   # answer!

Check it with numbers

beforeafter −100>= 0 ?
1000yes
15050yes
99−1no

balance >= 100 is exactly the cut-off. We didn't guess-and-test — we computed it by substituting the new value back into the goal.

Notice we replaced balance in the goal with what it becomes: balance - 100. That's the one rule for assignments →

07 / Rule 1

Rule for Assignment: Copy & Replace x = e

To find the before-condition of x = e:
take the goal, and replace every x with e.

Example A

# goal: x > 5
x = x + 1
# replace x with x+1
→ x + 1 > 5x > 4

To end above 5 after adding 1, start above 4.

Example B

# goal: y == 10
y = y * 2
# replace y with y*2
→ y * 2 == 10y == 5

To land on 10 after doubling, start at 5.

Example C

# goal: total <= 50
total = total + item
→ total+item <= 50total <= 50 - item

Room left before adding = 50 minus the item.

Common trap: people substitute into the before picture (“x becomes 5, so…”). Don't. You always substitute into the goal (the after), because that's the thing you're trying to make true.

08 / Rule 2

Several Lines: Go Bottom to Top ONE LINE AT A TIME

For a sequence of statements, push the goal up through the last line first, then the line above, and so on.

The program

x = x + 1
y = x * 2
# goal (after): y > 10

Work upward

Step 1 — through y = x*2
   goal y > 10, replace y with x*2
   → x*2 > 10x > 5

Step 2 — through x = x+1
   need x > 5, replace x with x+1
   → x+1 > 5x > 4   # final answer

Sanity check with numbers

start xx+1y = 2×y>10?
5612yes
4510no
101122yes

x > 4 is exactly right — x = 4 just misses (y lands on 10, not above it), x = 5 just makes it.

Rule in one line: wp(A; B, goal) = wp(A, wp(B, goal)). Do the inner one (B) first.

09 / Rule 3

if / else: Both Branches Must Work CHECK EACH PATH

For an if, the before-condition is: whichever branch runs, it must reach the goal. So check every path and require them all.

Absolute value

if x >= 0:
    r = x
else:
    r = -x
# goal: r == abs(x)  (r is |x|)

Check each path:

# when x >= 0, we run r = x
#   is x == |x|?  yes ✓ (x is non-neg)

# when x < 0, we run r = -x
#   is -x == |x|? yes ✓ (x is neg)

Result

Both branches reach the goal, so the before-condition is true — it works for every x. No precondition needed.

The rule

wp(if B: S1 else: S2, goal)
  =  (B      and wp(S1, goal))
     or
     (not B  and wp(S2, goal))

In words: either the test is true and the if-branch reaches the goal, or the test is false and the else-branch does. Miss a case (e.g. no else) and you must prove the goal already holds there.

10 / Loops

Loops Are the Hard Part — Here's the Idea DON'T UNROLL

You can't “substitute backward” through a loop — it might run 3 times or 3 million. Instead we describe the loop with two things we invent, then check a short list.

1 · The invariant

A fact that stays true every time around the loop — before it starts and after each pass.

Think: “what's my progress-so-far statement?” It captures the work done up to now.

This is the creative part — and the single most useful idea for real code.

2 · The bound (why it stops)

A number that counts down — it stays > 0 while looping and shrinks every pass.

If something keeps getting smaller and can't go below zero, the loop must end. That's your termination proof.

Usually “how far left to go” — like n - i.

Slogan: the invariant says the loop is doing the right thing; the bound says it won't do it forever.

11 / Loops

Finding the Invariant — a Real Loop SUM 1..n

Add up 1 + 2 + … + n. Watch what stays true after every pass — that's the invariant.

i = 0
s = 0
while i != n:
    i = i + 1
    s = s + i
# goal: s == 1+2+...+n

Trace it for n = 4 and watch s vs 1..i:

after passis1+..+i
start000
1111
2233
3366
441010

The invariant jumps out

s == 1 + 2 + … + i

s always holds the sum up to i so far.” True at the start (0 = empty sum) and true after every single pass — look at the table, the last two columns always match.

Why it gives the answer

The loop stops when i == n. Plug that into the invariant:

s == 1 + 2 + … + n  ✓

invariant (still true) + loop-finished (i == n) = the goal. That's the whole point of picking a good invariant.

12 / Loops

Proving It Stops — the Bound A COUNTDOWN

Pick something that shrinks

For the sum loop, how much work is left? n - i. Watch it fall for n = 4:

ibound = n − i
04
13
22
31
40 → loop stops

Two things to confirm

  • While looping (i != n), the bound is > 0. ✓
  • Each pass makes it strictly smaller (i goes up by 1, so n - i drops by 1). ✓

A whole number that keeps dropping and never goes below 0 cannot drop forever — so the loop must end. Termination proven, no guesswork.

If you can't find such a countdown, that's a red flag your loop might hang.

13 / Loops

The Loop Checklist (All Together) 4 QUICK CHECKS

Once you have an invariant P and a bound t, a correct loop is just these four checks. Here they are for the sum loop.

CheckIn plain wordsSum loop
1 · Starts trueThe setup makes the invariant hold before the first pass.i=0, s=0s = empty sum = 0
2 · Stays trueIf it's true and we loop once more, it's still true.add i+1 to both i and s → still “s = 1..i” ✓
3 · Gives the goalInvariant + loop-finished ⇒ the postcondition.s=1..i and i=ns=1..n
4 · It endsThe bound is > 0 while looping and shrinks each pass.n-i > 0, drops by 1 each time ✓

All four tick → the loop is correct and terminates, for every valid input. Checks 1–3 are the invariant's job; check 4 is the bound's job.

14 / Good to know

Three Sanity Facts About wp QUICK INTUITION

You don't need these to use the method, but they explain why it behaves so predictably.

No miracles

wp(code, false) = false

No input can make an impossible goal come true. Code can't perform magic.

Easier goal, easier input

If goal A is easier than goal B, then A needs a looser before-condition.

Asking for less never demands more of the caller.

Split & conquer

wp(S, A and B)
= wp(S, A) and wp(S, B)

To hit two goals at once, hit each — then combine. Handy for proving compound goals piece by piece.

These are just consistency guarantees. If some rule you wrote ever broke one of them, the rule would be wrong — they're the guardrails that keep the whole calculus honest.

15 / In practice

You Already Half-Do This EVERYDAY PAYOFF

You don't have to write formal proofs to benefit. The habits show up directly in code you write today.

Preconditions → guards & asserts

def withdraw(balance, amt):
    assert balance >= amt   # the wp!
    return balance - amt
# the assert IS the weakest
# precondition, written down.

Invariants → fewer off-by-ones

Just naming “what's true each pass” catches most loop bugs — boundaries, empty inputs, the last element — before you ever run the code.

Backward reasoning → debugging

“This line needs x > 0… so the line above must guarantee it…” is exactly wp, done by hand. It's how you localise a bug fast.

It's the engine of real tools

Verifiers like Dafny, Why3, and ESC/Java compute weakest preconditions under the hood to check code automatically. Same idea, scaled up.

16 / Recap

The Cheat Sheet ONE PAGE

The rules

CodeBefore-condition
x = egoal with x replaced by e
A ; Bdo B first, then A (bottom–up)
if / elseeach branch must reach the goal
whileinvariant + bound (4 checks)

The loop checks

1 starts true · 2 stays true · 3 gives the goal · 4 it ends.

Remember three things

  • Read backwards — from the goal to the start.
  • Weakest = most generous input rule that still works.
  • Loops = invariant + bound — “doing right” + “won't run forever.”