Foundations to FrontierPart IX: The graduate coreBezhig

G01: Specifications, invariants, and finite-state checking

6 exercises, each with a complete solution. 1 figure. 2 code listings.

On this page
  1. Prerequisites and observable goals
  2. The central question: what must every execution preserve?
  3. Assertions and Hoare triples
  4. Worked proof: sum of squares
  5. Recognition: choose the right evidence
  6. An inventory transition proof
  7. Finite exploration and its boundary
  8. Independent practice
  9. Smallest useful hints
  10. Complete solutions: G01
  11. Retention and readiness

Prerequisites and observable goals#

Before

foundation C01, C04, and C05: state, implications, quantifiers, and induction.

After

Write a contract, prove a loop’s partial correctness and termination separately, and explain exactly what an exhaustive finite model establishes.

The central question: what must every execution preserve?#

A test is a particular execution. A specification describes permitted executions. An implementation can pass examples while violating an unstated assumption: that inventory never becomes negative, that a rejected transaction has no effect, or that an object remains alive until its last use. Write the objects and the contract before choosing a proof or a test.

For a sequential inventory operation, let the state be s=(a,b)s=(a,b), where a∈Na\in\N is available stock and b∈Nb\in\N is sold stock. An order requests q∈Nq\in\N. Our policy is deliberately simple: accept only when q≤aq\le a; otherwise leave state unchanged. The mathematical transition is

T((a,b),q)={(a−q,b+q),q≤a,(a,b),q>a.T((a,b),q)=\begin{cases}(a-q,b+q),&q\le a,\\(a,b),&q>a.\end{cases}

This definition contains an error-handling policy, not merely an arithmetic formula. A policy that partially fills orders is also possible, but it is a different operation. The theorem we want is: starting with a+b=Na+b=N and nonnegative components, every finite sequence of valid requests preserves a+b=Na+b=N and nonnegativity.

Assertions and Hoare triples#

An assertion is a predicate on program state: a statement that can be true or false for a particular mapping from variable names to values. The notation

{P} C {Q}\{P\}\ C\ \{Q\}

means partial correctness: for any initial state satisfying PP, if command CC terminates normally, its final state satisfies QQ. PP is the precondition and QQ the postcondition. It does not, by itself, promise termination or describe exceptions. A total-correctness claim adds termination under the precondition. [G-HOARE]

Consider assignment x=x+1 over mathematical integers. To guarantee x>5x>5 afterward, it is sufficient to require x+1>5x+1>5 beforehand, equivalently x>4x>4. This backward substitution is the assignment rule: to establish QQ after x:=Ex:=E, establish Q[E/x]Q[E/x] before. The substitution replaces the variable’s value in the assertion; it does not execute QQ as a program. With bounded machine integers, overflow behavior must be added to the model. Python’s arbitrary-precision integers avoid fixed-width wraparound, but memory/time remain finite resources.

For a sequence C1;C2C_1;C_2, find an intermediate assertion RR: prove {P}C1{R}\{P\}C_1\{R\} and {R}C2{Q}\{R\}C_2\{Q\}. For a conditional, prove each branch under PP plus its branch condition. Do not prove only the branch you expect to be common.

Worked proof: sum of squares#

Suppose nn is a fixed nonnegative integer and the program is

Python
i = 0
s = 0
while i < n:
    s = s + i*i
    i = i + 1

An invariant true at every loop head is

I:0≤i≤n,s=∑k=0i−1k2.I:\quad 0\le i\le n,\qquad s=\sum_{k=0}^{i-1}k^2.

An empty sum is zero. Initialization establishes i=0i=0 and s=0s=0. For preservation, assume II and i<ni<n before the body. The first assignment gives s′=s+i2=∑k=0ik2s'=s+i^2=\sum_{k=0}^{i}k^2. The second gives i′=i+1i'=i+1, so s′=∑k=0i′−1k2s'=\sum_{k=0}^{i'-1}k^2 and 0≤i′≤n0\le i'\le n. The same assertion is restored, expressed in new state.

At exit, II and ¬(i<n)\neg(i<n) imply i=ni=n, not merely i≥ni\ge n. Substituting into the sum yields the postcondition s=∑k=0n−1k2s=\sum_{k=0}^{n-1}k^2. A separate variant n−in-i is a nonnegative integer at loop heads and strictly decreases on every executed iteration. Thus the loop terminates in the ideal model. An invariant can be true forever in a nonterminating loop; a decreasing well-founded measure supplies the missing argument.

The closed form n(n−1)(2n−1)/6n(n-1)(2n-1)/6 can be proved by induction, but it is not necessary to prove that the loop computes the specified sum. Keeping the specification as a sum often makes the invariant easier to discover. Prove the closed form separately when it is useful for an oracle or optimization.

Recognition: choose the right evidence#

Use an invariant when the algorithm repeats a step while preserving a relationship. Use structural induction when it traverses a recursively built object. Use a finite-state model when transitions are small enough to enumerate and boundary behavior matters. Use tests to connect that model to the actual implementation. No one method makes the others redundant.

A good invariant is neither only a goal nor only a tautology. “The answer is correct” may be false until completion. “ss is an integer” is easy to preserve but too weak to prove the sum. Work backward from the final expression: replace the final bound nn with the evolving bound ii, then add bounds needed for safe indexing and exit reasoning.

An inventory transition proof#

For acceptance, q≤aq\le a implies a−q≥0a-q\ge0, and b+q≥0b+q\ge0 follows from nonnegative inputs. Conservation follows by cancellation:

(a−q)+(b+q)=a+b=N.(a-q)+(b+q)=a+b=N.

For rejection, the state is unchanged, so both properties remain true. Induction over the number of requests proves the invariant for every finite sequential request list. This proof is stronger than checking a few orders, but it depends on the transition definition. It does not cover concurrent unsynchronized reads, database failures, or an API allowing a negative request.

Every shown node sums to four. The self-loop is a real transition: an operation can occur without changing inventory. This matters when distinguishing “no effect” from “no request received.”

Finite exploration and its boundary#

The lab explores all reachable states for a fixed initial stock NN and request range 0,…,Q0,\ldots,Q. Breadth-first search stores a set of seen states so zero requests and rejections do not cause endless repetition. With Q≥1Q\ge1, every (N−b,b)(N-b,b) for b=0,…,Nb=0,\ldots,N is reachable by repeatedly ordering one unit. There are N+1N+1 states. Each tries Q+1Q+1 requests, so work is O((N+1)(Q+1))O((N+1)(Q+1)) transition evaluations, assuming state hashing and integer arithmetic costs are bounded for this model.

The code rejects negative inputs and Booleans, even though Python treats bool as a subclass of int. This is a deliberate contract choice. The exhaustive exploration establishes properties only of this sequential finite model. To model concurrent check-then-decrement, split the operation into read/check/write transitions and include each thread’s local state; the resulting counterexample is not erased by the sequential proof.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

G01.01FSolution

For x=x+3, derive a precondition sufficient to guarantee 2x≤142x\le14 afterward. State the arithmetic model.

G01.02CSolution

Prove partial correctness of the sum-of-squares loop for n≥0n\ge0, including initialization, preservation, and exit. Then explain why changing the guard to i<=n changes the result.

G01.03CSolution

A proposed proof says “i≤ni\le n remains true, therefore the loop terminates.” Refute it with a loop and provide the missing termination condition for the original program.

G01.04TSolution

Two buyers independently check available stock 1, then both record a sale of one before a shared decrement. Construct an interleaving that violates the intended inventory contract. Identify the atomic operation that the sequential model hid.

G01.05TSolution

Change the policy to partially fill an order: sell r=min⁡(a,q)r=\min(a,q). Derive the transition and prove conservation/nonnegativity. Explain which previous tests need revision.

G01.06LSolution

Implement reserve and explore in starters/contracts.py. Accept strict nonnegative integers; explore only bounds 0–100. Reject excessive requests without changing state. For N=4,Q=4N=4,Q=4, predict all reachable states before executing. Test invalid inputs, zero requests, rejection, and the conservation invariant.

Smallest useful hints#

For .01, replace the post-state xx by the expression used to compute it. For .04, give each buyer its own read value before choosing the writes. For .06, store seen states before enqueueing successors; do not count a repeated state as new work.

Complete solutions: G01#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

G01.01Show solution

Backward substitution

Use mathematical integers and let xoldx_{\rm old} be the starting value. After the assignment, xnew=xold+3x_{\rm new}=x_{\rm old}+3. The desired 2xnew≤142x_{\rm new}\le14 is 2(xold+3)≤142(x_{\rm old}+3)\le14, then 2xold≤82x_{\rm old}\le8, hence xold≤4x_{\rm old}\le4. This is also necessary in this model. Check the boundary: starting at 4 gives 7 and 2(7)=142(7)=14. A fixed-width implementation additionally needs representability of x+3x+3.

Back to G01.01
G01.02Show solution

The upper limit follows the guard

Use I:0≤i≤nI:0\le i\le n and s=∑k=0i−1k2s=\sum_{k=0}^{i-1}k^2. Initialization gives the empty sum. Preservation adds the previously excluded term i2i^2, then changes the bound to i+1i+1. Exit combines i≥ni\ge n with i≤ni\le n, giving i=ni=n. With i<=n, an iteration at i=ni=n executes and adds n2n^2; the appropriate invariant bound is i≤n+1i\le n+1 and final value is ∑k=0nk2\sum_{k=0}^{n}k^2. For n=0n=0, the numerical totals happen to agree because 02=00^2=0, so that single test would miss the specification change.

Back to G01.02
G01.03Show solution

Safety is not progress

For n=3n=3, i=0; while i<n: pass preserves i≤ni\le n and never terminates. In the original loop, n−in-i is an integer bounded below by zero and decreases by one on every iteration. A strictly decreasing sequence in the nonnegative integers cannot be infinite. Decreasing real numbers alone are insufficient: 1,1/2,1/4,…1,1/2,1/4,\ldots stays positive forever.

Back to G01.03
G01.04Show solution

The check and effect must agree on one state

Both buyers read a=1a=1 and pass the check. If each increments a separately maintained sold total and both overwrite availability with its locally computed zero, the final record can be (a,b)=(0,2)(a,b)=(0,2) starting from total one. If they decrement a shared value independently under a different implementation, it may become negative. The exact bad outcome depends on the program; the common defect is that the acceptance decision and inventory change are not one serialized transition. A mutex around the full sequential operation or an appropriate atomic database transaction can enforce that boundary. This is a model-level interleaving, not permission to reason about undefined C++ races as deterministic behavior.

Back to G01.04
G01.05Show solution

A different policy, the same invariant

Set r=min⁡(a,q)r=\min(a,q) and T((a,b),q)=(a−r,b+r)T((a,b),q)=(a-r,b+r). For nonnegative a,qa,q, 0≤r≤a0\le r\le a, so the new available value is nonnegative; sold is nonnegative because b,r≥0b,r\ge0. Their sum is still a+ba+b. A request larger than availability now changes state whenever a>0a>0, so the old excessive-request unchanged-state test is wrong for this new specification. Reuse conservation tests, add the partial-fill acceptance criterion, and do not change tests merely to silence a failure without changing the written contract.

Back to G01.05
G01.06Show solution

Complete reference and evidence boundary

The expected states are (4,0),(3,1),(2,2),(1,3),(0,4)(4,0),(3,1),(2,2),(1,3),(0,4). The reference follows the mathematical transition, validates the domain before arithmetic, and explores every allowed outgoing transition exactly once per discovered state. The queue may contain several states but never needs to contain duplicates. The code below is complete; the archive’s tests also check N=0N=0 through 20 and Q=0Q=0.

graduate_code/solutions/contracts.pyPython
"""G01: a finite state model. This is sequential, not a thread model."""
from collections import deque
from dataclasses import dataclass


@dataclass(frozen=True)
class Stock:
    available: int
    sold: int = 0


def reserve(state: Stock, amount: int) -> Stock:
    if any(type(x) is not int or x < 0
           for x in (state.available, state.sold, amount)):
        raise ValueError("nonnegative mathematical integers required")
    if amount > state.available:
        return state
    return Stock(state.available - amount, state.sold + amount)


def explore(initial: int, max_amount: int = 4) -> set[Stock]:
    if (type(initial) is not int or not 0 <= initial <= 100
            or type(max_amount) is not int or not 0 <= max_amount <= 100):
        raise ValueError("finite model bounds must be integers in [0,100]")
    start = Stock(initial)
    seen, todo = {start}, deque([start])
    while todo:
        state = todo.popleft()
        for amount in range(max_amount + 1):
            nxt = reserve(state, amount)
            if nxt.available < 0 or nxt.available + nxt.sold != initial:
                raise AssertionError((state, amount, nxt))
            if nxt not in seen:
                seen.add(nxt)
                todo.append(nxt)
    return seen


if __name__ == "__main__":
    print(sorted(explore(4), key=lambda s: s.sold))
Back to G01.06

Retention and readiness#

Explain why the proof needs both branches, why zero requests do not break exploration, and why this is not a concurrency proof. Revisit implication and induction if those explanations fail. Advance when you can modify the policy, repair the proof, and change tests consistently without viewing the solution. Continue with Hoare logic in Software Foundations for mechanized proof practice; this chapter’s handwritten arguments are not machine-checked proofs. [G-HOARE]