The Procedure Call

How Gries brings the named procedure into the calculus: a procedure is a command with a specification, a call is checked against that specification alone, and parameter passing is nothing more than disciplined substitution — provided you never let the actuals alias.

Up to this point Gries’ language has been a handful of primitive commands whose weakest preconditions you compute directly. The procedure adds the one thing every real program depends on: abstraction. A procedure packages a command behind a name and a specification, so that a caller can use it without reading its body. The whole payoff — and the whole discipline — is that you reason about a call using only the procedure’s pre/postcondition, treating the body as a sealed box. This page works through what a procedure specification is, how the three parameter modes reduce to substitution, the proof rule for a call, the aliasing restriction that keeps the substitution honest, and how recursion is proved with the same bound-function idea that terminates a loop.

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, guarded commands (if…fi, do…od) for the programming language. See the book for the full, rigorous treatment.

Contents

  1. Procedures and abstraction
  2. Parameters and passing modes
  3. The proof rule for a call
  4. Aliasing — the classic pitfall
  5. Recursive procedures
  6. Procedures vs inlining
  7. Summary

Procedures and abstraction

A procedure is a named command equipped with a specification — a precondition pre and a postcondition post. The declaration ties the name to a body, but the body is deliberately hidden from anyone who calls it. What the caller sees is the contract:

proc P(value x; result y):
    # pre:  pre(x)
    # post: post(x, y)
    body       # hidden from the caller

This is the essence of modularity. The author of P proves once that the body meets the specification — {pre(x)} body {post(x,y)} — and from then on every caller reasons from the spec, never from the body. A hundred call sites do not require a hundred re-readings of the implementation; each is checked against two predicates. Change the body later, keep the spec, and every caller stays correct without being touched. Abstraction is precisely this decoupling of “what is guaranteed” from “how it is achieved.”

A call of P is correct at a site iff the state there satisfies pre.
The caller must ESTABLISH pre before the call,
and may then ASSUME post immediately after it.

Read that as a two-sided bargain. The caller owes the precondition; the procedure owes the postcondition. Neither side needs to know how the other keeps its half. That is what makes proofs compositional: the correctness of a large program is assembled from the specifications of its parts, not from their internals.

Parameters and passing modes

Parameters connect the fixed names inside a procedure to the varying data at a call site. Two vocabularies must never be confused:

Gries treats three passing modes, and the point of each is how it maps to substitution when you reason about a call.

ModeDirectionMeaningSubstitution at the call
value (in)caller → procedureThe actual’s value is copied into the formal on entry. The procedure may modify the formal locally; the caller’s variable is untouched.The formal in pre/post is replaced by the actual expression.
result (out)procedure → callerNothing flows in; on exit the formal’s final value is copied out into the actual (which must be a variable).The actual variable receives the formal’s post-value; post is read with the formal bound to that variable.
value-result (in-out)caller ↔ procedureCopy in on entry, copy out on exit. Both a value flows in and a result flows back.Formal replaced by actual on entry; actual updated from formal on exit — both substitutions apply.

The critical thing Gries insists on is that these are copy semantics, not reference (pointer/aliasing) semantics. Because a value comes in as a copy and a result goes out as a copy, the effect of a call can be described purely by textual substitution of actuals for formals — the same backward-substitution machinery used for assignment. That is the whole reason the proof rule below is so clean, and also the whole reason aliasing (Section 4) is forbidden: aliasing is exactly what breaks the copy model.

The proof rule for a call

Take a procedure with a value parameter and a result parameter:

proc P(value x; result y):   # {pre(x)} body {post(x, y)}

To reason about the call P(a, z) — passing expression a for the value formal x, and variable z for the result formal y — you substitute the actuals for the formals throughout the specification. The obligation splits into two halves, matching the bargain of Section 1:

To use call P(a, z):
1. ESTABLISH the precondition with x↔a:  pre(a)
2. Afterward you may ASSUME the postcondition with x↔a, y↔z:  post(a, z)

In weakest-precondition form, a call must guarantee the caller’s postcondition R no matter what result the procedure legitimately produces. So wp requires the precondition to hold now, and that every result value permitted by post would establish R once written back into z:

wp("P(a, z)", R) = pre(a) ∧ (∀ res : post(a, res) ⇒ R[z := res])

The universal quantifier is doing real work: it says whatever value the (possibly nondeterministic) procedure returns, as long as it is one post allows, the caller’s goal R must follow. If the procedure is deterministic and post(a, res) pins res to a single value, the quantifier collapses to a plain substitution — you just plug that value in for z. A worked call:

✓ Reasoning about a call from the spec alone
proc Double(value x; result y):
    # pre:  x ≥ 0
    # post: y = 2*x

# Caller wants:  R = (z > 6)   after   Double(n, z)

# wp("Double(n, z)", z > 6)
#   = pre(n) ∧ (∀ res : post(n,res) ⇒ (z>6)[z:=res])
#   = (n ≥ 0) ∧ (∀ res : res = 2*n ⇒ res > 6)
#   = (n ≥ 0) ∧ (2*n > 6)
#   = (n ≥ 0) ∧ (n > 3)
#   = n > 3

# So {n > 3} Double(n, z) {z > 6}   — proven without opening the body.

Notice that the body of Double never appeared. The call was discharged entirely from pre, post, and substitution — which is exactly the modularity claim of Section 1 made mechanical.

Aliasing — the classic pitfall

The substitution rule is sound only if each actual names a distinct piece of state. Aliasing — when two names refer to the same variable — silently invalidates the copy model, because a “copy out” to one name is unexpectedly visible through the other. Two aliasing situations bite:

Consider a swap procedure specified to exchange its two value-result arguments. The spec is proven for distinct actuals; alias them and the guarantee evaporates:

✗ Aliased call — substitution reasoning breaks
proc Swap(value-result u, v):
    # post: u = V0 and v = U0   (U0,V0 = entry values)
    t := u ;  u := v ;  v := t

# Aliased call: pass the SAME variable twice
# Swap(a, a)

# Spec, naively substituted u,v := a,a, claims:
#   post: a = A0 and a = A0     (looks fine!)
# But copy-in gives u=A0, v=A0; the body swaps
# copies, then BOTH copy-outs write back to a.
# Result depends on copy-out order — surprise, and
# the two formals were never independent. Unsound.
✓ Distinct actuals — the proof holds
proc Swap(value-result u, v):
    # post: u = V0 and v = U0
    t := u ;  u := v ;  v := t

# Distinct call: two different variables
# Swap(a, b)

# Substituting u,v := a,b in post:
#   a = B0 and b = A0
# copy-in: u=A0, v=B0; body swaps copies;
# copy-out: a := (new u)=B0, b := (new v)=A0.
# Exactly the postcondition. Sound. ✓

Gries’ response is a blunt restriction rather than a repair: the actual parameters of a call must not alias one another, and must not alias any global the procedure accesses. Under that restriction the substitution rule of Section 3 is sound; violate it and the proof rule simply does not apply, so no guarantee is inherited from the specification. It is the caller’s obligation to supply non-aliasing actuals — a proof obligation, not a runtime hope.

Why forbid rather than fix? A rule that stayed sound under arbitrary aliasing would have to describe the body’s internal ordering of copy-outs and every global it touches — destroying the very abstraction that makes the spec-only reasoning of Section 1 possible. Banning aliasing keeps the call rule a pure substitution.

Recursive procedures

A recursive procedure calls itself, so its correctness proof appears circular: to prove the body correct you must already know that the recursive call is correct. Gries breaks the circle with induction, and rules out infinite regress with a bound function — exactly the device that terminates a loop.

This is the same well-ordering argument used for the loop: the bound function of a loop and the variant of a recursion are the identical idea — a value counting down to a guaranteed stop. (See bound functions for the general treatment.) A worked example on factorial:

✓ Recursive factorial with its variant
proc Fact(value n; result f):
    # pre:  n ≥ 0
    # post: f = n!
    # bound (variant) t: n         # ≥ 0 and drops by 1 per call
    if n = 0 → f := 1
    [] n > 0 →
         Fact(n - 1, g) ;   # recursive call on smaller n
         f := n * g
    fi
✓ Why the recursion is correct & terminates
# TERMINATION (variant t = n):
#   pre gives n ≥ 0, so t ≥ 0.
#   recursive call uses n-1 < n, so t strictly
#   decreases → finitely many calls. ✓

# CORRECTNESS (induction on n):
#   base  n = 0: f := 1 = 0!            ✓ post
#   step  n > 0: assume spec holds for n-1,
#         so after Fact(n-1, g): g = (n-1)!
#         then f := n*g = n*(n-1)! = n!  ✓ post

# Note: actuals (n-1) and g are distinct — no
# aliasing, so the call rule of §3 applies.

The inductive hypothesis (“assume the spec for n-1”) is legitimate precisely because the variant guarantees the assumed-about case is strictly smaller; the induction is well-founded. A recursion with no decreasing bound is the analogue of a loop with no bound function — possibly non-terminating, and therefore unproven.

Procedures vs inlining

One could always inline a procedure — textually paste its body at every call site and reason about the expanded program. It would even be sound. But it discards the one property that makes large proofs feasible: compositionality. Reasoning through the spec, as in Section 3, keeps each call’s proof local and bounded by two predicates; inlining forces you to re-read and re-prove the body at every site, and re-do all of it whenever the body changes.

Reason via specificationInline the body
What the caller readspre and post onlyThe entire body, every time
Proof effort for N callsN substitutions against a fixed specN full re-proofs of the body
Effect of changing the bodyNothing, as long as the spec is preservedEvery call site must be re-verified
RecursionHandled by induction on a variantCannot be inlined finitely

The governing principle: a body may be replaced freely as long as it still meets the specification. Any two bodies satisfying the same {pre} body {post} are interchangeable to every caller — that substitutability is abstraction, and it is the reason to prefer spec-based reasoning over inlining even though both are sound. The specification, not the code, is the interface.

Summary

IdeaThe one-line takeaway
Procedure = command + specA named command with a pre/post; callers reason from the spec, never the body — the essence of modularity.
The call bargainCaller must establish pre before the call and may assume post after it.
Formal vs actualFormals are the placeholder names in the declaration; actuals are the expressions/variables supplied at a call.
Passing modesvalue (copy in), result (copy out), value-result (both) — all copy semantics, so a call reduces to substitution.
Call proof ruleSubstitute actuals for formals: establish pre(a); assume post(a,z).
wp of a callwp("P(a,z)", R) = pre(a) ∧ (∀ res : post(a,res) ⇒ R[z:=res]).
Aliasing restrictionActuals must not alias each other or a global — otherwise the copy model, and the substitution rule, are unsound.
RecursionAssume the spec for smaller arguments (induction) + a variant that strictly decreases — the loop-bound idea, guaranteeing termination.
Spec over inliningReasoning through the spec is compositional; changing a body is safe as long as the spec is preserved.
The recurring theme: the specification is the interface. A procedure earns its keep by letting every caller reason from two predicates instead of a body, parameter passing is just substitution as long as the actuals do not alias, and recursion terminates for the same reason a loop does — a bound that counts down. See weakest preconditions for the wp machinery underneath the call rule, the iterative command for the loop bound the variant mirrors, and bound functions for termination in general.