Predicates, States & Quantification

The logic prelude of Gries’ The Science of Programming — what a predicate really is, how to quantify over ranges, and the textual-substitution machinery that turns a postcondition into a precondition.

Student edition · understand, apply, verify

Use predicates, quantifiers, and capture-free substitution to specify arrays and state.

What you should be able to do: Write bounded quantified conditions and preserve their meaning under assignment.

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.

Before you can prove a single program correct you have to be fluent in the language you reason in. In The Science of Programming, David Gries spends the opening chapters on exactly that: the predicate calculus. A specification is nothing more than two predicates — a precondition and a postcondition — and a proof is a chain of implications between predicates. If your predicate algebra is shaky, program proofs are impossible. This page distils the pieces that matter most for the programming that follows: predicates as sets of states, quantification with Gries’ linear notation, textual substitution, and the altered-function notation for reasoning about arrays.

This is an original study summary for quick reference. Notation follows Gries: R[x := e] for textual substitution, (∀ i : R : P) and (∃ i : R : P) for quantification over a range, and (b; i:e) for an array altered at one index. See the book for the full, rigorous treatment.

Contents

  1. Predicates as sets of states
  2. The predicate calculus
  3. Quantification
  4. State manipulation & arrays
  5. Textual substitution is the engine
  6. Common quantified assertions
  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.

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.

Quantifiers and implication

∀ means every value in a domain satisfies a property; ∃ means at least one does. P⇒Q says every P-state is a Q-state, so P is stronger.

Example. ∀i in 0≤i

Boundary. Negation switches ∀/∃ and negates the predicate. Preserve domains and do not reverse implication.

Substitution without capture

Replace free variable occurrences by an expression without changing quantified/bound-variable meaning. Rename a binder when necessary to keep an inserted free name free.

Example. For x:=y+1 derive Q[x←y+1]. A ∀y inside Q must not accidentally bind a free y from the replacement.

Boundary. This is expression reasoning, not blind string substitution. Simultaneous assignment evaluates right sides in the old state.

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.

Aliasing and frame conditions

Aliasing means multiple names refer to one mutable object. A frame condition specifies state the operation leaves unchanged.

Example. A sorted-copy function promises it does not mutate its input. Two swap parameters may unexpectedly name one location.

Boundary. State ownership, shared references, mutation, and exception behavior. Local-variable reasoning alone does not protect aliased global state.

Predicates as sets of states

A state is an assignment of values to the program’s variables — one point in the space of everything the variables could be. A predicate is a boolean expression over those variables; equivalently, and this is the shift in view that unlocks everything, a predicate names the set of states in which it is true.

PredicateAs a boolean expressionAs a set of states
x > 0true when x is positiveall states whose x component is positive
x = ytrue when the two agreethe “diagonal” where x and y match
Tidentically trueall states (the whole space)
Fidentically falsethe empty set of states

Under the set reading, the logical connectives become set operations: ∧ is intersection, ∨ is union, ¬ is complement. And implication becomes containment: P ⇒ Q exactly when the set of P-states is a subset of the set of Q-states. That is why a stronger predicate (fewer states, more restrictive) implies a weaker one — F is the strongest predicate of all, T the weakest.

Every assertion you sprinkle through a program — a precondition, a postcondition, a loop invariant — is one of these predicates, carving out the set of states the program is allowed to be in at that point. Nothing more exotic is going on.

Why the set view pays off: “weakest precondition” literally means the largest set of starting states from which a command still reaches its goal. Once predicates are sets, “weakest” and “strongest” stop being jargon and become “biggest” and “smallest.”

The predicate calculus

The predicate calculus extends ordinary propositional logic (the algebra of ∧, ∨, ¬, ⇒, ≡) with predicates over variables and quantifiers that bind those variables. Two notions become central the moment quantifiers appear.

Substitution notation

R[x := e] denotes the predicate R with every free occurrence of x replaced by the expression e. Bound occurrences are left untouched. This single operation is, quite literally, the engine of the assignment axiom later on.

R[x := e]  =  R, with every free x textually replaced by e

A worked example. Let R be x < y ∧ x ≥ 0. Then:

# R           :  x < y  and  x ≥ 0
# R[x := x+1] :  (x+1) < y  and  (x+1) ≥ 0
#             =  x < y-1  and  x ≥ -1

# only the free x's changed; y was untouched

Two hazards Gries insists on

Substitution looks purely mechanical, but two things can silently corrupt it.

✗ Variable capture
# R:  (∃ y : y > x)      "some y exceeds x"

# Naively compute R[x := y]:
#   (∃ y : y > y)   ← FALSE, always

# The free x was CAPTURED by the
# bound y. Meaning changed entirely.
✓ Rename the bound variable first
# Rename bound y to a fresh k:
#   R:  (∃ k : k > x)

# Now substitute safely:
#   R[x := y] = (∃ k : k > y)   ✓

# Same meaning, no capture.

The rule: if e mentions a variable that is bound inside R, rename that bound variable to something fresh before substituting. Otherwise the substitution “captures” a free variable of e and changes the predicate’s meaning.

The second hazard is definedness. Substitution is only meaningful when e is actually defined in the states you care about — no division by zero, no array index out of bounds. Gries carries a domain condition alongside a substitution: R[x := e] is only trustworthy where domain(e) holds (e.g. b[i] requires 0 ≤ i < n). This resurfaces as the definedness proviso in the assignment axiom.

Quantification

Gries uses a clean linear notation with three explicit parts — a bound variable, a range, and a term — which avoids the ambiguity of the classic ∀ i . … form.

(∀ i : R : P)  —  for all i in range R, P holds
(∃ i : R : P)  —  there exists an i in range R with P

Read (∀ i : R : P) as “for every i such that R, we have P” and (∃ i : R : P) as “for some i such that R, we have P.” The same skeleton generalises to sum (∑ i : R : t) and product (∏ i : R : t), where the term is a numeric expression rather than a predicate.

The empty-range rules

What is the value of a quantification whose range admits no i? These conventions are not arbitrary — they are the identity elements that make range-splitting work, and they matter constantly at loop initialisation (an empty prefix).

QuantifierEmpty rangeWhy (identity element)
(∀ i : F : P)= Tvacuously true — T is the identity of ∧
(∃ i : F : P)= Fnothing to witness — F is the identity of ∨
(∑ i : F : t)= 00 is the identity of +
(∏ i : F : t)= 11 is the identity of ×

Laws for manipulating quantifiers

These identities are the workhorses of every array proof. You will reach for range-splitting and the one-point rule in nearly every loop invariant.

LawStatementUse
De Morgan (∀)¬(∀ i : R : P) ≡ (∃ i : R : ¬P)Push negation through a universal — “not all” is “some not.”
De Morgan (∃)¬(∃ i : R : P) ≡ (∀ i : R : ¬P)“none” is “all not” — used to state “x not in b[0..i-1].”
Split the range (∀)(∀ i : R∨S : P) ≡ (∀ i : R : P) ∧ (∀ i : S : P)Peel one element off a loop range to extend an invariant.
Split the range (∃)(∃ i : R∨S : P) ≡ (∃ i : R : P) ∨ (∃ i : S : P)Same, for existentials and sums.
One-point rule(∀ i : i=c : P) ≡ P[i := c]  (same for ∃)Collapse a singleton range to a single substitution.
Move term outif Q has no free i: (∀ i : R : Q ∧ P) ≡ Q ∧ (∀ i : R : P)Factor an i-independent condition outside the quantifier.

The range-split law is the one that makes loops tick. Extending (∀ i : 0 ≤ i < k : P) to k+1 is just splitting off the singleton i = k, then collapsing it with the one-point rule to P[i := k] — exactly the extra fact one loop iteration must establish.

State manipulation & arrays

Arrays force one more idea. An array b is best treated as a function from indices to values: b[i] is function application. So what does the assignment b[i] := e do to a predicate? It cannot just textually replace b[i] — other subscripts like b[j] might refer to the same element when j = i, and naive replacement would miss that aliasing.

Gries’ answer is the altered-function notation. Write (b; i:e) for “the array b everywhere the same, except that index i now maps to e.” Formally it is the function:

(b; i:e)[j] = e  if j = i,   otherwise b[j]

With this, the assignment b[i] := e is treated exactly like an ordinary assignment to the whole array variable b: it replaces b by (b; i:e). Substitution into a predicate then handles all the aliasing correctly, because evaluating (b; i:e)[j] forces you to compare j with i.

✗ Naive textual swap of b[i]
# post R:  b[j] = 5
# command:  b[i] := 7

# Naive: "b[i] does not appear in R,
# so R is unchanged" → precond b[j]=5

# WRONG when i = j: after b[i]:=7
# b[j] is 7, not 5. Aliasing ignored.
✓ Substitute the altered array
# Replace b by (b; i:7) throughout R:
#   R[b := (b; i:7)]  =  (b; i:7)[j] = 5

# Expand the application by cases:
#   if j = i :  7 = 5   → F
#   if j ≠ i :  b[j] = 5

# precond:  j ≠ i  and  b[j] = 5   ✓

This is not a curiosity — it is exactly the reasoning you need for any program that mutates an array, and every array-loop derivation in the book leans on it.

Textual substitution is the engine

Everything above converges on one payoff. The assignment axiom — the rule that gives the weakest precondition of an assignment — is pure textual substitution. There is no separate theory of assignment; there is only the substitution you just learned.

wp(“x := e”, R)  =  domain(e) ∧ R[x := e]
wp(“b[i] := e”, R)  =  domain(i,e) ∧ R[b := (b; i:e)]

Read it plainly: to find what must hold before an assignment so that R holds after, take R and substitute backward. Scalar assignment substitutes the expression for the variable; array assignment substitutes the altered function for the array. The capture and definedness cautions from the substitution section are precisely why the axiom carries a domain(e) conjunct and why bound variables must be renamed. Master substitution here and the command semantics come almost for free — the full treatment of the commands and their weakest preconditions is in the commands and their wp.

Common quantified assertions in programs

A handful of quantified predicates recur so often in loop invariants that they are worth memorising as idioms. Each says something precise about a prefix or a slice of an array b. These are exactly the building blocks of the invariants in developing loops from invariants.

Informal claimFormal predicate
sum of b[0..i-1] equals ss = (∑ k : 0 ≤ k < i : b[k])
x does not occur in b[0..i-1](∀ k : 0 ≤ k < i : b[k] ≠ x)
x occurs somewhere in b[0..n-1](∃ k : 0 ≤ k < n : b[k] = x)
b[0..i-1] is sorted (ascending)(∀ k : 0 ≤ k < i-1 : b[k] ≤ b[k+1])
b[i] is the maximum of b[0..n-1]0 ≤ i < n ∧ (∀ k : 0 ≤ k < n : b[k] ≤ b[i])
every element of b[0..n-1] is positive(∀ k : 0 ≤ k < n : b[k] > 0)

Notice how “x not in b[0..i-1]” is the De Morgan dual of an existential, and how “sorted” ranges over adjacent pairs so the singleton and empty prefixes are trivially sorted (empty range ⇒ T). When a loop advances i to i+1, you extend each of these by range-splitting off the new index and discharging the one extra fact — the whole reason the quantifier laws earned their place above.

Worked example: an empty universal and a bounded array claim

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

an empty universal and a bounded array claimSpecify the range: All b[i] for integer 0≤i<n are nonnegative, with 0≤n≤len(b).. Handle n=0: The universal over no elements is true; an existential claim that one element exists is false.. Negate correctly: Not(all nonnegative) becomes some valid i with b[i]<0.. Substitute safely: Replacing a free variable cannot accidentally bind it under a quantifier; rename bound variables when needed.1Specify the range2Handle n=03Negate correctly4Substitute safely
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
Specify the rangeAll b[i] for integer 0≤i
Handle n=0The universal over no elements is true; an existential claim that one element exists is false.
Negate correctlyNot(all nonnegative) becomes some valid i with b[i]<0.
Substitute safelyReplacing a free variable cannot accidentally bind it under a quantifier; rename bound variables when needed.
Check your understanding: Can a predicate index b[n] when n=len(b)?

No. Valid indices are 0 through len(b)−1. Definedness is part of the specification.

Run and change the example

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

python3 guide_labs.py quantifiers
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
Predicate as a set of statesA boolean over variables is the set of states where it holds; T = all, F = none.
Stronger / weakerP ⇒ Q is subset containment; stronger = fewer states, weakest = most permissive.
Free vs boundFree variables reach into the state; bound variables are renameable local placeholders.
Substitution R[x := e]Replace every free x with e — rename bound vars to avoid capture; require e defined.
Quantifier notation(∀ i : R : P) / (∃ i : R : P) — bound var, range, term, made explicit.
Empty-range rules∀ = T, ∃ = F, ∑ = 0, ∏ = 1 — the identity elements.
Quantifier lawsDe Morgan, range-splitting, one-point, and moving i-independent terms out drive array proofs.
Array altered-functionb[i] := e replaces b by (b; i:e); substitution handles aliasing correctly.
Substitution is the engineThe assignment axiom is nothing but backward textual substitution.
Quantified idiomssum, membership, sortedness, max — the raw material of loop invariants.
The recurring theme: predicates are the medium, substitution is the mechanism. Get comfortable seeing a predicate as a set of states and treating an array as a function you can alter one point at a time, and the weakest-precondition calculus — assignments, sequences, loops over arrays — becomes a matter of pushing symbols correctly rather than guessing.

⌂