10.1 Program Correctness: Preconditions, Postconditions, Invariants, and Testing vs. Proof
Key Takeaways
- A precondition states what must be true before a procedure is called, such as "n is a positive integer" or "the array is sorted"; the caller is responsible for meeting it.
- A postcondition states what a procedure guarantees when it finishes, provided its preconditions held, such as "returns the largest value in the array."
- A loop invariant is a condition that is true before and after every iteration; in a find-the-maximum loop, max is always the largest value examined so far.
- Testing (empirical evidence) can reveal bugs but cannot prove their absence, because it checks only some inputs; a proof establishes correctness for all valid inputs.
- To compare two code segments, check whether they produce the same result for every valid input, especially boundary cases, and then compare efficiency and clarity.
What this competency asks
ETS asks you to understand how to analyze computer programs in terms of correctness:
- Trace code and indicate the output printed or the value of variables after execution.
- Indicate the inputs that produce given outputs for a code segment.
- Describe what a program does, or choose the code segment that correctly implements an intended purpose.
- Identify valid preconditions and postconditions.
- Compare two code segments or algorithms.
- Identify the type of error produced, and errors and fixes (Section 10.2).
The debugging competency adds: differentiate between empirical testing and proof.
What "correct" means
A program is correct if it meets its specification for every valid input. Passing one example does not make it correct. The specification is often written as a precondition and a postcondition.
Preconditions and postconditions
// precondition: n ≥ 1 and a[0] … a[n - 1] contain integers
// postcondition: returns the largest value among a[0] … a[n - 1]
int findMax ( int[ ] a, int n )
int max ← a[0]
for ( int i ← 1; i < n; i ← i + 1 )
if ( a[i] > max )
max ← a[i]
end if
end for
return max
end findMax
| Term | Meaning | In findMax |
|---|---|---|
| Precondition | Must be true before the call; the caller's responsibility | n ≥ 1, because a[0] must exist |
| Postcondition | Guaranteed after the call, if the precondition held | The return value is the largest element |
ETS's sample procedures state preconditions in comments, such as // precondition: n is a positive integer.
Identifying valid preconditions
Ask: what could make this code fail or give a meaningless answer? The precondition should rule out exactly those inputs.
| Procedure | Valid precondition | Why |
|---|---|---|
| Binary search | The array is sorted in the order the code assumes | Otherwise halving can discard the target |
average ( a, n ) | n > 0 | Division by n |
findMax ( a, n ) | n ≥ 1 | Reads a[0] |
sqrtApprox ( x ) | x ≥ 0 | No real square root for negative numbers |
substring ( b, e ) | 0 ≤ b ≤ e ≤ length | Positions must exist |
A precondition that is too strong is also a poor choice. "n = 10" for findMax is true of some valid calls, but the code works for any n ≥ 1.
Identifying valid postconditions
A postcondition describes the result or final state, not the steps taken:
- Good: "returns the index of the first occurrence of target, or −1 if target is not in the array."
- Good: "the elements of arr are in nondecreasing order and are a rearrangement of the original elements."
- Poor: "loops through the array." That describes steps, not a guarantee.
For a sort, "the array is sorted" is not enough on its own. A procedure that overwrote everything with zeros would satisfy it. The postcondition must also say the array holds the same values.
Loop invariants
A loop invariant is a condition that is true before the loop starts and remains true after every iteration. When the loop ends, the invariant plus the exit condition gives the postcondition.
For findMax, the invariant at the top of each iteration is: max is the largest value in a[0] … a[i − 1].
- Initially (i = 1): max = a[0], the largest of a[0] … a[0]. True.
- Maintained: if a[i] > max, max becomes a[i]; otherwise max stays. Either way max is the largest of a[0] … a[i].
- At exit (i = n): max is the largest of a[0] … a[n − 1], which is the postcondition.
ETS's own explanation of its find-the-maximum sample uses exactly this reasoning. An invariant is also a quick way to judge a missing condition: the choice that keeps the invariant true is the correct one.
Finding inputs that produce a given output
Work backward from the output, or test each candidate input:
int mystery ( int x )
if ( x % 3 == 0 )
return x / 3
else
return x + 1
end if
end mystery
Which inputs make mystery return 4? Working backward: if x is a multiple of 3, then x / 3 = 4 gives x = 12. Otherwise x + 1 = 4 gives x = 3, but 3 is a multiple of 3, so that case never uses x + 1 (mystery(3) returns 1). The only input is 12. Checking the "else" case for consistency is exactly the step that separates the right answer from a tempting wrong one.
Comparing two code segments or algorithms
When asked whether two segments are equivalent, or which is better:
- Same results? Test ordinary values and boundary values: 0, 1, negative numbers, empty arrays, equal elements, and the first and last positions. One disagreement proves they are not equivalent.
- Same side effects? Do both modify the array, print the same output, or change the same variables?
- Efficiency: compare growth rates (Section 5.1). For example, both segments may be correct, but one is O(n²) and the other O(n).
- Clarity and maintainability: fewer duplicated statements, meaningful names, and no hard-coded values (Section 10.4).
Empirical testing vs. proof
| Empirical testing | Proof | |
|---|---|---|
| Method | Run the program on selected inputs and compare with expected outputs | Logical argument (for example, loop invariants or induction) that the code meets its specification |
| Covers | Only the inputs tried | All inputs that satisfy the precondition |
| Can show | That a bug exists (a failing test) | That no bug exists relative to the specification |
| Cost | Cheap, automatable, repeatable | Demanding; used for critical code |
Edsger Dijkstra put the limitation of testing this way: testing can show the presence of bugs, but never their absence. A function that returns the correct result for 50 test inputs can still fail on the 51st, for example on an empty array or a negative number that no test included. In practice, teams combine thorough testing (Section 10.3) with reasoning such as invariants, and reserve formal proof for the most critical software.
Which is the most appropriate precondition for a binary search procedure that returns the index of target in array a?
A procedure should sort an integer array arr in increasing order. Which is the best postcondition?
A student's procedure passes 200 automated test cases. The student concludes that the procedure is correct for all inputs. What is the best response?
Given this procedure, which input returns 4?
int mystery ( int x )
if ( x % 3 == 0 )
return x / 3
else
return x + 1
end if
end mystery