The Alternative Command — Guarded Selection (IF)

Dijkstra’s guarded if…fi as Gries develops it: a nondeterministic choice among guarded commands whose weakest precondition makes case-coverage a proof obligation, not a courtesy.

Every conditional you have ever written picks a branch. Dijkstra’s alternative command — the guarded if…fi — does the same, but with two deliberate design choices that a conventional if/else hides from you. First, when several conditions are true it chooses any of them (nondeterminism). Second, when none is true it does not silently fall through — it aborts. Both choices exist to serve the same master the rest of Gries’ book serves: a clean weakest-precondition semantics that turns “did I handle every case?” into a formula you can actually check. This page covers the syntax, the wp rule, why coverage is a proof obligation, how to derive an IF from a specification, and the pitfalls.

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, and the guarded alternative if B1 → S1 [] … [] Bn → Sn fi. See The Science of Programming for the full, rigorous treatment.

Contents

  1. Dijkstra’s guarded commands
  2. Weakest precondition of IF
  3. Coverage is a proof obligation
  4. Deriving an IF from a specification
  5. Common patterns & pitfalls
  6. Total correctness note
  7. Summary

Dijkstra’s guarded commands

The alternative command is a list of guarded commands separated by the fat bar [] and bracketed by if and fi. Each guarded command is a pair Bi → Si: a boolean guard Bi and the statement Si that runs when it is chosen.

if B1 → S1
[] B2 → S2
[][] Bn → Sn
fi

The operational semantics has three steps:

Why nondeterminism is a feature, not a bug

Beginners read “the machine may pick either branch” as a liability. It is the opposite. Nondeterminism lets you avoid over-specifying: when two cases genuinely lead to an equally correct result, forcing an arbitrary tie-break (as a sequential if/else does — the first clause always wins) bakes an accident of ordering into the program’s meaning. A guarded IF with overlapping guards says exactly what is true: “in the overlap, either arm is acceptable.”

Weakest precondition of IF

Write IF for the whole alternative command. Its weakest precondition for a postcondition R is:

wp(IF, R) = (B1 ∨ B2 ∨ … ∨ Bn)
    ∧ (B1 ⇒ wp(S1, R))
    ∧ (B2 ⇒ wp(S2, R))
    ∧ …
    ∧ (Bn ⇒ wp(Sn, R))

The rule splits cleanly into two parts, and each carries a distinct obligation:

Contrast with an ordinary if-then-else

In most languages, if B then S with no else is sugar for if B then S else skip. When B is false, the construct silently does nothing — a skip, whose wp is just R. That is permissive: a forgotten case slides by as a no-op and the bug surfaces far downstream. The guarded IF makes the opposite choice: a missing case is an abort, whose wp is F, which immediately poisons the whole wp. Stricter, yes — and safer, because the proof fails at the point of the omission instead of hiding it.

✗ Conventional if — missing else is a silent skip
# if B then S   ==   if B then S else skip
# wp = (B => wp(S,R)) ∧ (¬B => R)
# when B is false: nothing happens, R
# must already hold. A forgotten case
# passes through unnoticed.
✓ Guarded IF — missing case is an abort
if B → S fi
# wp = B ∧ (B => wp(S,R))
#    = B ∧ wp(S,R)
# if B can be false, coverage fails
# and wp collapses to F. The proof
# forces you to state every case.

Coverage is a proof obligation

The first conjunct is not decoration — it is the term most designs get wrong. Consider the maximum of two values. The natural first draft uses strict inequalities, and it has a hole exactly at equality.

✗ Guards leave a gap at x = y
# goal R:  m = max(x, y)
if x > y → m := x
[] x < y → m := y
fi
# coverage: (x>y) ∨ (x<y)  ==  x ≠ y
# at x = y neither guard holds → ABORT
# first conjunct is NOT T, so wp fails
✓ Overlapping guards cover x = y
# goal R:  m = max(x, y)
if x ≥ y → m := x
[] y ≥ x → m := y
fi
# coverage: (x≥y) ∨ (y≥x)  ==  T   ✓
# at x = y BOTH guards hold — overlap.
# either arm sets m to the common value,
# so both establish R. Nondeterminism
# is harmless here — by design.

Two lessons sit inside this small example:

Deriving an IF from a specification

The wp rule is not just a checker — run it backward and it becomes a constructor. To build an IF that establishes postcondition R from precondition Q:

  1. Choose guards that cover Q. Pick B1, …, Bn so that Q ⇒ (B1 ∨ … ∨ Bn). This guarantees the coverage conjunct under any state allowed by the precondition — no abort is reachable.
  2. Design each branch to establish R under its guard. For every i, build Si so that the triple {Q ∧ Bi} Si {R} holds — equivalently Q ∧ Bi ⇒ wp(Si, R). The guard hands each branch an extra assumption Bi, which is usually exactly what makes the branch easy to write.

Notice how the labor divides: coverage is a property of the guard set as a whole, correctness is a property of each arm in isolation. You never reason about interactions between branches.

Worked example 1 — maximum of two

Establish R: m = max(x, y) from Q: T (works for all states).

# Step 1 — guards must cover Q = T.
#   pick x ≥ y and y ≥ x; their
#   disjunction is T, so coverage holds.
# Step 2 — design each arm:
#   {x ≥ y} m := x {m = max(x,y)}   ✓
#   {y ≥ x} m := y {m = max(x,y)}   ✓
if x ≥ y → m := x
[] y ≥ x → m := y
fi
# {m = max(x, y)}

Worked example 2 — the “if a < b swap” style

A classic use of a single-armed IF paired with a skip arm: order two variables so that afterward a ≥ b. Here you must supply the second guard explicitly — the guarded IF gives you no implicit else, so the “do nothing” case is written out as a guard whose statement is skip.

# R:  a ≥ b   (values of {a,b} unchanged as a set)
# coverage:  (a < b) ∨ (a ≥ b)  ==  T   ✓
if a < b → a, b := b, a   # swap into order
[] a ≥ b → skip          # already ordered
fi
# {a ≥ b}
# NOTE: omitting the a ≥ b arm would abort
# whenever a is already ≥ b — the opposite
# of a conventional guardless "if a < b then swap".

This is the deterministic special case: the two guards a < b and a ≥ b are exactly B and ¬B, so they partition the state space, there is no overlap, and the IF behaves like an ordinary if-then-else — except that the “else” is stated, not assumed.

Worked example 3 — sign of a number (n > 2 guards)

Guarded IF is not limited to two arms. To classify the sign of a into s ∈ {-1, 0, +1}, three guards partition the domain. This shows the rule generalizing to any n.

# R:  (a < 0 ∧ s = -1)
#  ∨ (a = 0 ∧ s =  0)
#  ∨ (a > 0 ∧ s = +1)
# coverage: (a<0) ∨ (a=0) ∨ (a>0) == T  ✓
if a < 0 → s := -1
[] a = 0 → s :=  0
[] a > 0 → s := +1
fi
# {s = sign(a)}

The three guards are mutually exclusive and exhaustive, so the disjunction is T (coverage ✓) and each arm trivially establishes its slice of R. If you had written only a < 0 and a > 0, coverage would reduce to a ≠ 0 and the command would abort at a = 0 — the proof would refuse to close, pointing straight at the omission.

Common patterns & pitfalls

Pattern / pitfallWhat happensGuidance
Uncovered caseNo guard true ⇒ the command aborts; the coverage conjunct (B1 ∨ … ∨ Bn) is not T, so wp collapses to F.Always verify Q ⇒ (B1 ∨ … ∨ Bn). Add a skip arm for the intended “do nothing” case.
Deterministic if-else as a special caseTwo guards B and ¬B partition the state; no overlap, exactly one arm ever fires.This is the familiar if-then-else. Perfectly fine — just remember the ¬B arm must be written explicitly.
Overlapping guards for symmetryBoth arms are declared correct in the overlap; the machine may pick either.Use it deliberately (e.g. x ≥ y / y ≥ x) to expose symmetry and avoid an arbitrary tie-break. Ensure both arms establish R.
Side effects in guardsGuards are evaluated (all of them) before selection; a side-effecting guard would mutate state unpredictably and break the wp reasoning, which assumes pure boolean tests.Guards must be side-effect-free boolean expressions. Move any effect into the statement Si, never the guard.

The side-effect rule deserves emphasis: the entire wp derivation treats Bi as a predicate over the starting state that can be evaluated freely and in any order. If evaluating a guard could change the state, “evaluate all guards, then choose” is no longer well defined, and none of the conjuncts in the wp rule mean what they say. Keep guards pure.

Total correctness note

wp encodes total correctness — guaranteed termination in a state satisfying R — so it is worth asking whether the alternative command can fail to terminate. It cannot, on its own account. Given that some guard holds (coverage), the IF evaluates the finite list of guards, selects one true arm, and runs its statement exactly once. It introduces no repetition. So the IF itself always terminates, provided the chosen branch Si terminates.

The only way an alternative command “fails to finish” is by aborting — and that is a coverage failure, already caught by the first conjunct, not a nontermination in the looping sense. The genuinely interesting termination question belongs to the iterative command do…od, where a bound function is needed to prove progress. See the loop and its invariant (DO) for that argument.

Summary

IdeaThe one-line takeaway
Syntaxif B1 → S1 [] … [] Bn → Sn fi — a list of guarded commands, fat-bar separated.
Selection semanticsPick any one true guard’s statement; nondeterministic when several are true.
No true guardThe command aborts — not a silent skip.
NondeterminismA feature: avoids over-specifying, exposes symmetry, keeps proofs honest.
wp(IF, R)(B1 ∨ … ∨ Bn) ∧ ⋀i (Bi ⇒ wp(Si, R)).
Coverage conjunct(B1 ∨ … ∨ Bn) forces every case to be handled or wp collapses to F.
vs. ordinary if-elseA missing case is an abort, not a skip — stricter and safer.
Overlapping guardsAllowed and often desirable; each overlapping arm must still establish R.
Deriving an IFChoose guards so Q ⇒ (B1 ∨ … ∨ Bn); design each Si for {Q ∧ Bi} Si {R}.
Guards are pureSide-effect-free boolean expressions only — effects go in the statements.
TerminationIF always terminates if the chosen branch does; the loop is where termination gets interesting.
The recurring theme: the guard set is a specification. Coverage and per-branch correctness are exactly the two things the wp rule checks, so writing a guarded IF is writing down what must be true before you choose — and letting the calculus confirm you left no case behind. Related: weakest preconditions, the assignment axiom & commands, and the loop (DO).