CSE 331: Reasoning and Floyd-Hoare logic
Course motto: Your job is to deliver code that you know works.
From three tests to every run
Here is a short method with its specification, and three tests of it:
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
@Test
void allZero() {
assertEquals(0, Seconds.seconds(0, 0, 0));
}
@Test
void oneOfEach() {
assertEquals(3661, Seconds.seconds(1, 1, 1));
}
@Test
void ninetyMinutes() {
assertEquals(5400, Seconds.seconds(0, 90, 0));
}
All three pass. The method has no if and no loop, so every test
runs every line. For such a method,
Lecture 5's list
asks for at least two tests that, between them, run every
statement, so these three complete the list. Is seconds right?
The specification promises an answer for every allowed input:
every choice of non-negative h, m, and s, far more than a
loop could try in our lifetimes. The three tests tried three. As
Lecture 1 said,
testing can refute a claim about every run and never confirm it.
To know that seconds satisfies its specification, we turn to
the third of
Lecture 4's three ways,
reasoning. In reasoning, we argue from the code itself that a
method satisfies its specification on every run.
Lecture 1
argued about Sum in prose, at the level of "the loop added each
element once", and said that making such an argument precise is
the work of the lectures on reasoning. This lecture begins that
work, on code with no loops: we write down what is true after
each line, and we name the rule that justifies each step, so that
anyone can check the argument. Friday's lecture adds loops.
What is true after each line
Let's reason about seconds the way Lecture 1 reasoned about
Sum: by asking, at each point, what we know about the
variables. When the program runs, a line changes the values of
the variables. When we reason, a line changes what we know about
those values, and the claims record what we know.
A client must make the precondition true, so we may assume it at the top.
As in Lecture 1, a claim sits in double braces beside the code it
is about, and the braces are not Java: the program never executes
them. A claim is written in math, not Java. In a claim, = means
equals, != means not equal, and we combine facts with
and, or, and not. We write a Java condition in a claim in
this notation, so a == b in the code is a = b in a claim. Here is
the method again, with a claim after each line.
static long seconds(int h, int m, int s) {
{{ 0 <= h and 0 <= m and 0 <= s }}
long t = h;
{{ 0 <= h and 0 <= m and 0 <= s and t = h }}
t = t * 60 + m;
{{ 0 <= h and 0 <= m and 0 <= s and t = 60 * h + m }}
t = t * 60 + s;
{{ 0 <= h and 0 <= m and 0 <= s and
t = 3600 * h + 60 * m + s }}
return t;
}
Read the claims in order, each against the line above it. The line numbers are the first listing's.
- Line 17 sets
ttoh. A declaration that gives a value is an assignment. Nothing abouth,m, orschanged, so the precondition is still true, and nowt = his true too. - Line 18 changes
t, sot = his no longer true after it. The new value oftis the old value times 60, plusm, and the old value wash. So after line 18,t = 60 * h + m. - Line 19 does the same with
s. The old value was60 * h + m, so after line 19,t = 3600 * h + 60 * m + s.
The @return clause says which value is returned, and line 20
returns t. So at the return, the postcondition is a claim
about t, and the last claim says that t is exactly what the
postcondition promises. Every run that starts with the
precondition true returns 3600 * h + 60 * m + s. That is what it
means for seconds to satisfy its specification. Four claims
showed that for every allowed input, without a single run.
The claims treat * and + as the operations on the integers,
the whole numbers you learned in school, positive, negative, and
zero. Java's int wraps around at the top of its range, as
Lecture 1 showed,
and the seconds in 596524 hours already pass it. So seconds
returns a long. Its inputs are int values, so every value
computed on the way stays far below a long's top. So no line
wraps, and the claims may use integer arithmetic. Exercise 5
returns an int instead, with the clause its specification
then needs.
What we just did, from a claim before a line, through the line, to a claim after it, we call forward reasoning. Each step used a rule, and the rest of this lecture gives the rules. Once we have them, anyone can check each step of an argument against the rules. Such an argument is a proof, as A proof says near the end. A step that breaks a rule is a mistake in the argument.
A claim at a point
The claims above are Lecture 1's assertions. Here is Lecture 1's definition again, with its second paragraph changed.
Definition: assertion
An assertion is a true or false claim about the program's state (the values of variables and memory) at a particular point in the program.
An assertion holds if the claim is true each time a run reaches its point. If a run ever reaches the point with the claim false, the assertion does not hold.
Each time a run reaches its point says when the claim has to be true. Every assertion in Lecture 1 sat at a point that every run reaches once, so the clause made no difference there. A point inside a branch is different. Only the runs that take the branch reach that point, so a claim there is about only those runs. We write such claims in The rule for a conditional. A point inside a loop can be reached many times on one run. Friday's lecture puts claims at such points.
Lecture 2 said that a specification promises nothing on a call that breaks its precondition. So the assertions in a method's body set such calls aside too: an assertion there is about the calls that begin with the precondition true.
A triple
Each step of the argument had Lecture 1's shape: a claim before a piece of code, the code, and a claim after.
Definition: Hoare triple
A Hoare triple {{ P }} code {{ Q }} is a claim P, called
its precondition, a piece of code, and a claim Q, called
its postcondition.
The triple is valid if every terminating run of the code from a state in which P is true ends in a state in which Q is true.
A run terminates when it ends, and the definition asks nothing of a run that never does. Friday's loops raise a separate question: does the code terminate?
Lecture 1's rule was about this shape. If P holds at its point, and the triple is valid, then Q holds at its point. Robert Floyd and Tony Hoare worked out how to reason this way in the late 1960s. We call the triple, the rules below, and the arguments built from the rules Floyd-Hoare logic.1
By Lecture 2's definition,
a method satisfies its specification if every call that begins
with the precondition true ends with the postcondition true. Now
read the method's body as a triple, with the precondition before
the body and the postcondition after it. Then a method satisfies
its specification exactly when that triple is valid and every
call that begins with the precondition true terminates. Today every
method terminates, so the question is whether the triple is
valid, and the argument about seconds showed the triple for
seconds valid.
A triple can be about any piece of code, even a single line. Here are six triples about one line each, with their verdicts and the reasons for them:
{{ true }} x = 5; {{ x = 5 }}- Whatever the state was, the line makes
x5. Valid.
- Whatever the state was, the line makes
{{ x = 3 }} y = x + 1; {{ y = 4 }}- From any state where
xis 3, the line makesy4. Valid.
- From any state where
{{ x = 3 }} y = x + 1; {{ 0 < y }}- The line makes
y4, and 4 is positive. Valid too: a postcondition may say less than all that is true afterward.
- The line makes
{{ true }} y = x + 1; {{ y = 4 }}trueallows the state wherexis 0, and from that state the line makesy1. A run from that state is a counterexample, so the triple is not valid.
{{ 0 < x }} x = x - 1; {{ 0 < x }}- The precondition allows the state where
xis 1, and from that state the line makesx0. Not valid: a valid triple needs the postcondition true after a run from every state the precondition allows.
- The precondition allows the state where
{{ false }} x = 0; {{ x = 1 }}- No state makes
falsetrue, so the definition asks nothing of the postcondition. Valid. A valid triple says nothing about the runs the precondition rules out, as a specification says nothing about a call that breaks its precondition.
- No state makes
A triple's code may also be empty: {{ P }} {{ Q }}, two claims at
the same point. Running no code leaves the state as it was, so this
triple is valid exactly when Q is true in every state where P is
true. We say P implies Q.
Lecture 2's words
for the same relation were that P is stronger than Q, and Q is
weaker than P. The last step of the argument about seconds was
an empty triple: the claim after line 19 contains
t = 3600 * h + 60 * m + s, and that is the postcondition, so the
claim implies it.
Claims written one under another at a point are empty triples, one after another, so they read as a chain of implications: each claim implies the claim below it. For example, after line 17:
{{ 0 <= h and 0 <= m and 0 <= s and t = h }}
{{ 0 <= h and t = h }}
{{ 0 <= t }}
The second claim keeps two facts of the first, and the third follows from the second. A rule below, the rule of consequence, lets us write such a pair at any point.
Stronger claims say more
Many claims hold at the same point. After line 17, t = h holds.
So do 0 <= t and true, the claim that is true in every state
and so holds at every point. In the argument above, each claim
said all we knew about the variables.
Think of a claim as the set of states in which it is true. Then P implies Q means that P's set is inside Q's, as in the figure.
For example, 0 <= h and t = h implies 0 <= t: in every state
where the first claim is true, t equals a non-negative h. So
the stronger claim has the smaller set. "Stronger" sounds bigger,
so this is easy to get backward. A claim with a smaller set rules
out more states, so that claim says more about the state the
program is in. The weakest claim of all is true, which is true
in every state and says nothing. The strongest is false, which
is true in no state.
Among the claims that hold at a point, one claim implies all the others. That claim describes exactly the states that the runs reaching the point can be in. We call it the strongest claim at that point.
Why insist on the strongest? Because a weaker claim may not be
enough to reach the postcondition. Suppose that after line 18 we
had written 0 <= t. It holds there. But then we would know
nothing more about t. So after line 19 we could not say what
t is in terms of h, m, and s, and the postcondition needs
exactly that. The claim after line 18 dropped the fact
t = 60 * h + m, so this argument fails, even though the method
satisfies its specification. Forward reasoning keeps the
strongest claim at every step. We weaken a claim only where we
check it against a claim someone wrote, such as the
postcondition, which may say less.
Two directions
Forward reasoning fills in the claim after some code, given the claim before it:
{{ P }}
code
{{ ? }}
Forward reasoning finds the strongest claim that makes the triple valid. We call that claim the strongest postcondition of the code from P.
Reasoning the other way fills in the claim before some code, given the claim after it. We call it backward reasoning:
{{ ? }}
code
{{ Q }}
Backward reasoning finds the weakest claim that makes the triple valid: the least that has to be true beforehand for Q to be true afterward. We call that claim the weakest precondition of the code for Q. A stronger claim would rule out some states from which the code reaches Q. An argument that asks more of the precondition than the code needs may then fail on a method that satisfies its specification. So the weakest claim is the right choice here, for the same reason the strongest claim was the right choice going forward.
Either direction turns the question "is this triple valid?" into
an implication. Given {{ P }} code {{ Q }}:
- Reason forward from P to the strongest claim R after the code.
The triple
{{ P }} code {{ R }}is valid because we found R that way, and every state R allows is a state some run from P ends in. So the original triple is valid exactly when R implies Q. - Or reason backward from Q to the weakest claim R before the
code. The triple
{{ R }} code {{ Q }}is valid because we found R that way, and every state outside R is a state from which some run misses Q. So the original triple is valid exactly when P implies R.
The rules below say how a claim moves through an assignment, a
sequence of lines, and an if. The first is the rule for an
assignment, the only kind of line seconds has before its return.
The rules for an assignment
Each rule below is a schematic: a triple laid out as it would sit in a program, with letters for the parts that change from one use to the next. Here is the forward rule for an assignment:
Rule: assignment, forward
{{ P }}
x = e;
{{ P[x ↦ x0] and x = e[x ↦ x0] }}
x is any variable, and e is any expression that changes
no variable. P[x ↦ x0] is P with every x replaced by
x0, a fresh name for the value x had before the line: a
name that neither the program nor any claim uses. The claim after
the line is the strongest that makes the triple valid.
The rule says two things. First, everything P said is still
true, once we write x0 for the old value of x. That includes
a fact about another variable: if P says w = x + 1, the claim
after the line says w = x0 + 1. Second, the new value of x
is e, computed from the old values. Here is the rule on lines
18 and 19 of seconds, starting from t = h, the one fact in
the claim after line 17 that mentions t. The rule leaves the
other facts as they are:
{{ t = h }}
t = t * 60 + m;
{{ t0 = h and t = t0 * 60 + m }}
t = t * 60 + s;
{{ t0 = h and t1 = t0 * 60 + m and t = t1 * 60 + s }}
At line 19 the rule renames t again. Since t0 is taken, the
fresh name is t1. Each use of the rule picks a fresh name,
t0, then t1, then t2, so no two old values share a name.
The facts chain back to h. Since t0 is h, t1 is
h * 60 + m. So t is (h * 60 + m) * 60 + s, which multiplies
out to the 3600 * h + 60 * m + s we wrote after line 19.
Two habits keep the fresh names out of the claims we write:
- If P and
enever mentionx, nothing has to be renamed. The claim after the line isP and x = e. Line 17 was this case:t = hjoined the precondition. - If we can write the old value of
xwithoutx0, put that expression forx0in every other fact. Then only the fact we read the expression from still mentionsx0. That fact says nothing about the state, so we drop it. Line 18 was this case, withhfort0, which givest = h * 60 + m, thet = 60 * h + mwe wrote. Afterx = x + 1, the old value isx - 1. Exercise 1 works such a case.
The backward rule for an assignment is simpler:
Rule: assignment, backward
{{ Q[x ↦ e] }}
x = e;
{{ Q }}
x is any variable, and e is any expression that changes
no variable. Q[x ↦ e] is Q with every x replaced by
e. The claim before the line is the weakest that makes the
triple valid.
We build the claim before the line from the claim after it, so
read this rule from the bottom up. e is what x is about to
become, so whatever Q claims about x after the line had better
be true of e before it. Before the line, x still has its old
value, so the claim before the line can call that value x, and
the rule needs no new name. The rule is a substitution.
Let's reason backward through seconds from its postcondition.
Before line 19, replacing t by t * 60 + s:
{{ t * 60 + s = 3600 * h + 60 * m + s }}
t = t * 60 + s;
{{ t = 3600 * h + 60 * m + s }}
Before line 18, replacing t by t * 60 + m in that claim:
{{ (t * 60 + m) * 60 + s = 3600 * h + 60 * m + s }}
t = t * 60 + m;
{{ t * 60 + s = 3600 * h + 60 * m + s }}
And before line 17, replacing t by h:
{{ (h * 60 + m) * 60 + s = 3600 * h + 60 * m + s }}
long t = h;
{{ (t * 60 + m) * 60 + s = 3600 * h + 60 * m + s }}
Both sides of the claim at the top are the same number, so that claim is true in every state. In particular, the precondition implies it. So the method's triple is valid.
Forward reasoning gave the same verdict by a different route. Backward reasoning took less work: three substitutions and one check, with no thought about which facts stay true across a line. On the other hand, the claims grew with each substitution, and we simplified them only at the end.
Neither argument used the precondition's three facts about h,
m, and s. They say what a duration is, and nothing in the body
depends on them. A precondition can carry a fact that no argument
about the body uses. Exercises 2 and 5 use these three facts.
The two directions can also meet in the middle. Reason forward
through the first lines and backward through the rest. The
triple is valid exactly when the claim forward reasoning gives at
the meeting point implies the claim backward reasoning gives
there. On seconds, forward through line 17 gives the
precondition and t = h. Backward through lines 18 and 19 gives
the claim before line 18 above. The first implies the second. We
write such a check as a calculation, one step to a line, with the
fact a step uses beside it:
(t * 60 + m) * 60 + s = (h * 60 + m) * 60 + s since t = h
= 3600 * h + 60 * m + s
The backward claim's left side comes out as its right side, so
the claim is true wherever t = h.
The rule for a sequence
We reason about two pieces of code in a row one at a time, through a claim at the point between them:
Rule: sequence
{{ P }}
first
{{ R }}
second
{{ Q }}
The triple around both pieces of code is valid exactly when
some claim R at the point between them makes both halves
valid: {{ P }} first {{ R }} and {{ R }} second {{ Q }}.
The schematic does not say what R is. Forward reasoning chooses R
as the strongest claim after the first piece of code. In What is
true after each line, R for lines 18 and 19 was the claim after
line 18, 0 <= h and 0 <= m and 0 <= s and t = 60 * h + m, and
each half was valid by the forward rule for an assignment.
Backward reasoning chooses R as the weakest claim before the
second piece of code. Here are the same two lines backward, from
the postcondition. The claim between the lines is R, and each
half is valid by the backward rule:
{{ (t * 60 + m) * 60 + s = 3600 * h + 60 * m + s }}
t = t * 60 + m;
{{ t * 60 + s = 3600 * h + 60 * m + s }}
t = t * 60 + s;
{{ t = 3600 * h + 60 * m + s }}
Forward reasoning keeps all that is true at the point. Backward reasoning keeps only what has to be true there. So the two Rs are different claims.
Each argument about seconds, forward and backward, used this
rule twice: at the two points between its three assignments.
The rule of consequence
Forward through seconds, the rules gave the claim after
line 19. But the claim we had to reach was the postcondition.
Backward, the rules gave the claim at the top. But we had to
start from the precondition. In general, each rule for an
assignment gives one claim, the strongest after the line or the
weakest before it. A specification may promise less than the
strongest claim, or ask more than the weakest, so the claim we
have to reach is often a different claim.
To connect the two claims, the rule of consequence uses the empty triple from A triple: two claims at one point, valid exactly when the upper implies the lower. We may write such a pair at any point, so we can check a triple by writing claims of our own beside the claims we were given:
Rule: consequence
{{ P }}
{{ A }}
code
{{ B }}
{{ Q }}
The triple {{ P }} code {{ Q }} is valid if P implies A,
{{ A }} code {{ B }} is valid, and B implies Q.
To see which way the implications point, follow a run down the schematic, as the figure does:
The run starts in a state where P is true, so A is true there too. The code takes the run to a state where B is true, so Q is true there too. So to show a triple valid, it is enough to show valid a triple with a weaker precondition and a stronger postcondition, one that assumes less and promises more. The next section says who wants that.
Here is the rule twice on seconds. Forward, we stack the claim
after line 19 above the postcondition:
{{ 0 <= h and 0 <= m and 0 <= s and
t = 3600 * h + 60 * m + s }}
{{ t = 3600 * h + 60 * m + s }}
return t;
The lower claim is one fact of the upper, so the upper implies it. This is weakening by dropping facts, as the chain in A triple did. Where the directions met, we stack the claim forward reasoning gave after line 17 above the claim backward reasoning gave before line 18:
{{ 0 <= h and 0 <= m and 0 <= s and t = h }}
{{ (t * 60 + m) * 60 + s = 3600 * h + 60 * m + s }}
t = t * 60 + m;
This time the lower claim is not one of the upper claim's facts.
The upper claim says t = h. Putting h for t in the lower
claim makes its two sides the same number, as the calculation in
The rules for an assignment showed. So the upper claim implies
the lower.
The rules for an assignment and a sequence produce claims
mechanically: a substitution, a fact added, a claim carried
through. This rule's two implications are not mechanical. They
involve no code, and we check them with arithmetic and logic.
They are where an argument can go wrong, if we believe an
implication that is false. The two pairs above, and the check at
the top of the backward argument, used this rule. So did the
weakening in Stronger claims say more: writing 0 <= t after
line 18 used this rule in the middle of the argument. That step
broke no rule. It dropped a fact the argument needed later, to
reach the postcondition.
Stronger for whom
Who wants a stronger claim? The precondition and the postcondition are promises made by different parties, so the answer depends on where the claim sits:
- The precondition is the client's promise. The client must
make it true before every call, and the implementer may assume it.
- Stronger, the smaller set: the client has more to make true, and the implementer has fewer states to handle.
- Weaker, the bigger set: the client has less to make true, and the implementer has more states to handle.
- The postcondition is the implementer's promise. The
implementer must make it true at every return, and the client
may rely on it.
- Stronger, the smaller set: the implementer has more to deliver, and the client has more to rely on.
- Weaker, the bigger set: the implementer has less to deliver, and the client has less to rely on.
So each choice of strength gives one party more to do and the other less. Here is who smiles and who frowns:
| precondition | postcondition | |
|---|---|---|
| stronger, the smaller set |
🙁 the client 🙂 the implementer |
🙂 the client 🙁 the implementer |
| weaker, the bigger set |
🙂 the client 🙁 the implementer |
🙁 the client 🙂 the implementer |
Each party wants its own promise weak and the other party's strong. Lecture 2's rule calls one specification stronger than another when its precondition is weaker and its postcondition stronger. Both changes favor the client, since a stronger specification assumes less and promises more. Which strength is right is still a design choice, the question of Lecture 2's "Which is better?": a good specification rules out exactly the implementations its clients cannot use.
Across a call
The rule of consequence also checks what a method's specification asks of the code that calls it. Lecture 2 reasoned across a call with the method's specification as the call's triple: the precondition before the call, the call, and the postcondition after. When the method satisfies its specification, that triple is valid, so the argument never reads a line of the method's body. To use the triple, we show that the claim before the call implies the method's precondition. The precondition is the client's promise, and that implication is how an argument checks that the client keeps it. After the call, the postcondition is part of what we know. The rule for a sequence carries it on, for example to the precondition of the next call.
The rule of consequence is why a method with a stronger specification can stand in at a call that relied on a weaker specification. The stronger specification's precondition is weaker, so the claim before the call still implies it. Its postcondition is stronger, so that postcondition still implies what the argument after the call needed. A specification that assumes less and promises more supplies exactly the two implications the rule of consequence asks for.
The rule for a conditional
An if runs one of two pieces of code, and its condition decides
which. Here is abs, which gives the absolute value of a number.
Its precondition keeps out -2147483648, the one int whose
negation does not fit in an int, for a reason What reasoning
tells us gives.
9 10 11 12 13 14 15 16 17 18 19 20 21 | |
What is true after the if? In the true branch, n < 0, and
n = -n makes n positive. The false branch is empty, and in
it, 0 <= n. We reason without running anything, so we cannot
know which branch a run took. We know only what each branch
leaves behind. So after the if, 0 < n or 0 <= n, which is
0 <= n. That claim holds at line 20. Here is the rule that
justifies the step:
Rule: conditional
{{ P }}
if (b) {
{{ P and b }}
first
{{ A }}
} else {
{{ P and not b }}
second
{{ B }}
}
{{ A or B }}
{{ Q }}
b is the if's condition. A is the strongest claim after
the true branch, and B after the false branch, so A or B is
the strongest claim after the if. The triple
{{ P }} if ... {{ Q }} is valid exactly when A or B implies
Q, that is, when both A and B imply Q.
Every run that enters the true branch found b true, and every
run that enters the false branch found b false. So those facts
hold at the top of their branches, and the argument often needs
them. After the if, a run has taken one branch or the other. A
run that took the true branch ends with A true, and a run that
took the false branch ends with B true. So the strongest claim
after the if is A or B. Q has to be true whichever branch
ran, so we check Q against both A and B. An if with no else
still has a false branch, as
Lecture 5 said,
and that branch is empty, so B is P and not b.
Is 0 <= n enough to show the postcondition? No. It says the
returned number is non-negative, and the specification promises
more: the number returned is the number given, or its negation.
Line 18 overwrites n, so to say that, the claims need a name
for the number given. Let n0 be the value of n at the call.
When the specification says n, it means n0. The precondition's
job is to keep n = -n from wrapping, so that the claims may use
integer arithmetic. No claim needs it beyond that, so the
claim at the top is n = n0. Unlike a fresh name, n0 names one
value for the whole argument, so facts about it stay. In the
forward rule for line 18, the old value of n is n0, by the
second habit. Here is the whole argument:
static int abs(int n) {
{{ n = n0 }}
if (n < 0) {
{{ n = n0 and n < 0 }}
n = -n;
{{ n0 < 0 and n = -n0 }}
}
{{ (n0 < 0 and n = -n0) or (0 <= n0 and n = n0) }}
return n;
}
The claim after the if implies what the specification promises,
in both of its cases:
- In the first case
n0 < 0andn = -n0, and the specification promises-n0. - In the second case
0 <= n0andn = n0, and the specification promisesn0.
So abs satisfies its specification.
compare has two ifs, and the body of each if is a return:
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
A return ends the run of the method, so no run comes out of
line 15 to the point after the first if. Only the false branch
leads there, so the claim after such an if is P and not b.
No run reaches the end of the true branch, so every claim holds
there, false among them: false at a point says that no run
reaches it. So the conditional rule gives the same claim, since
false or (P and not b) is P and not b.
false works the other way too. When the claim at a point implies
false, no state makes that claim true, so no run reaches the
point: reasoning has found code that never runs. Exercise 3 finds
such a point.
At each return, we check the postcondition: the claim there must
imply that the returned value is what the @return clause
promises in that case.
static int compare(int a, int b) {
{{ true }}
if (a < b) {
{{ a < b }}
return -1;
}
{{ b <= a }}
if (a == b) {
{{ b <= a and a = b }}
return 0;
}
{{ b <= a and a != b }}
return 1;
}
- At line 15,
return -1:a < b, and the specification promises -1. - At line 18,
return 0:a = b, and the specification promises 0. - At line 20,
return 1:b <= aanda != btogether sayb < a, and the specification promises 1.
So compare satisfies its specification: all three
implications are true.
Reasoning finds the mistake
Lecture 5 showed methods whose tests passed while the method was
wrong. min is one such method. Here it is, with the one test
that gave it statement coverage:
/**
* Picks the smaller of two numbers.
*
* @param a one number
* @param b the other number
* @return the smaller of a and b
*/
static int min(int a, int b) {
int r = a;
if (a <= b) {
r = a;
}
return r;
}
@Test
void firstIsSmaller() {
assertEquals(1, Min.min(1, 2));
}
Lecture 5
found the mistake with a second test, min(2, 1), the test that
branch coverage asks for. Let's find the mistake by reasoning
instead. The specification has no precondition, so we start from
true, and reason forward:
static int min(int a, int b) {
{{ true }}
int r = a;
{{ r = a }}
if (a <= b) {
{{ r = a and a <= b }}
r = a;
{{ r = a and a <= b }}
}
{{ (r = a and a <= b) or (r = a and b < a) }}
{{ r = a and (a <= b or b < a) }}
{{ r = a }}
return r;
}
The claim after the if has two cases, and both say r = a. So
the claim after the if implies the second claim.
a <= b or b < a is true of any two numbers, so that fact says
nothing. Dropping it gives the third claim, as the rule of
consequence allows. The third claim says what the method does on
every run: it returns a. Now check that claim against the
postcondition. The postcondition promises the smaller number: a
when a <= b, and b when b < a. When b < a, the
postcondition promises b, but r = a. So the implication is
false, and the states with b < a are exactly where it fails. A
run from any such state is a counterexample, and Lecture 5's
min(2, 1) is such a run.
quadrant, another of Lecture 5's methods, has two ifs.
Lecture 5 gave it two tests that complete the list, running both
branches of both ifs, and both tests pass:
/**
* Gives the quadrant of a point that is on neither axis.
*
* y
* 2 | 1
* --------+-------- x
* 3 | 4
*
* @param x the point's x coordinate
* @param y the point's y coordinate
* @requires x != 0 and y != 0
* @return 1 if x > 0 and y > 0,
* 2 if x < 0 and y > 0,
* 3 if x < 0 and y < 0, and
* 4 if x > 0 and y < 0
*/
static int quadrant(int x, int y) {
int q = 1;
if (x < 0) {
q = 2;
}
if (y < 0) {
q = q + 1;
}
return q;
}
@Test
void upperRight() {
assertEquals(1, Quadrant.quadrant(1, 1));
}
@Test
void lowerLeft() {
assertEquals(3, Quadrant.quadrant(-1, -1));
}
Reason forward from the precondition. After the first if, a run
took its true branch or its false branch, so there are two cases.
After the second, each of those splits again, so there are four.
At the second if, the branch's fact joins each case of the
claim, and q = q + 1 changes q in each case. Since x != 0,
not (x < 0) is 0 < x, and likewise for y:
static int quadrant(int x, int y) {
{{ x != 0 and y != 0 }}
int q = 1;
{{ x != 0 and y != 0 and q = 1 }}
if (x < 0) {
{{ x < 0 and y != 0 and q = 1 }}
q = 2;
{{ x < 0 and y != 0 and q = 2 }}
}
{{ (x < 0 and y != 0 and q = 2) or
(0 < x and y != 0 and q = 1) }}
if (y < 0) {
{{ (x < 0 and y < 0 and q = 2) or
(0 < x and y < 0 and q = 1) }}
q = q + 1;
{{ (x < 0 and y < 0 and q = 3) or
(0 < x and y < 0 and q = 2) }}
}
{{ (x < 0 and y < 0 and q = 3) or
(0 < x and y < 0 and q = 2) or
(x < 0 and 0 < y and q = 2) or
(0 < x and 0 < y and q = 1) }}
return q;
}
Now check the four cases against the four lines of the method's
@return clause:
0 < xand0 < y: quadrant 1 is promised, andq = 1.x < 0and0 < y: quadrant 2 is promised, andq = 2.x < 0andy < 0: quadrant 3 is promised, andq = 3.0 < xandy < 0: quadrant 4 is promised, andq = 2.
The fourth implication is false: it fails exactly on the lower
right quadrant. Lecture 5's third test, quadrant(1, -1), is a
point there, and the method returns 2 on it. Lecture 5's list
never asked for that test, so we had to think of it in Lecture 5.
The four cases are the four paths through quadrant. A test
follows a path on one input. A case covers every input that takes
the path.
Backward through a conditional
Forward reasoning through quadrant found the case that fails.
Reasoning backward from the postcondition asks a different
question: on which inputs does quadrant return what its
specification promises? That is the weakest precondition of the
body for the postcondition. To reason backward through an if,
we need the conditional rule the other way around:
Rule: conditional, backward
{{ (b and A) or (not b and B) }}
if (b) {
{{ A }}
first
{{ Q }}
} else {
{{ B }}
second
{{ Q }}
}
{{ Q }}
b is the if's condition. A is the weakest claim before the
true branch for Q, and B the weakest before the false branch.
A run that takes the true branch needs A, and a run that takes
the false branch needs B, so the claim before the if is the
weakest that makes the triple valid.
The rule joins A with b, so inside the true branch we may drop
the cases of A that b rules out, and likewise B with not b.
An if with no else has an empty false branch, so B is Q. Here
is quadrant backward from its postcondition, the four cases of
its @return clause written as one claim. Read it from the
bottom up:
static int quadrant(int x, int y) {
{{ x != 0 and y != 0 }}
{{ (x < 0 and y != 0) or (0 < x and 0 < y) }}
int q = 1;
{{ (x < 0 and y != 0) or
(0 < x and ((y < 0 and q = 3) or (0 < y and q = 1))) }}
if (x < 0) {
{{ y != 0 }}
q = 2;
{{ (y < 0 and ((x < 0 and q = 2) or (0 < x and q = 3))) or
(0 < y and ((0 < x and q = 1) or (x < 0 and q = 2))) }}
}
{{ (y < 0 and ((x < 0 and q = 2) or (0 < x and q = 3))) or
(0 < y and ((0 < x and q = 1) or (x < 0 and q = 2))) }}
if (y < 0) {
{{ (x < 0 and q = 2) or (0 < x and q = 3) }}
q = q + 1;
{{ (x < 0 and q = 3) or (0 < x and q = 4) }}
}
{{ (0 < x and 0 < y and q = 1) or (x < 0 and 0 < y and q = 2) or
(x < 0 and y < 0 and q = 3) or (0 < x and y < 0 and q = 4) }}
return q;
}
- Inside the second
if,y < 0, so the two cases with0 < ydrop. Beforeq = q + 1, putq + 1forq: the cases ask forq = 2orq = 3. The false branch is empty, so B is the postcondition under0 <= y, which keeps the two cases with0 < y. The rule joins the two into the claim before theif. - Inside the first
if, put 2 forqin that claim, andx < 0drops the cases with0 < x. What remains asks only thaty != 0. The false branch is empty, so B is the claim after theifunder0 <= x, which keeps the two cases with0 < x. The rule joins the two again. - Before
int q = 1, put 1 forq, and the claim at the top is(x < 0 and y != 0) or (0 < x and 0 < y).
The claim at the top is the weakest precondition of the body for
the postcondition: exactly the inputs on which quadrant returns
what its specification promises. The precondition allows more. A
point with 0 < x and y < 0, in the lower right quadrant, makes
the precondition true and the claim false. So the precondition
does not imply the claim, and the triple is not valid, on exactly
the inputs forward reasoning found. Forward reasoning found the
case that fails. Backward reasoning found the inputs that work,
and the mistake is the difference between those inputs and
the precondition.
A proof
If you took CSE 311, you wrote proofs there, about numbers and
sets. An argument that goes from what is given to what is claimed
in steps, each step justified by a rule, so that anyone can check
it without trusting its author, is a proof. The arguments for
seconds, abs, and compare are proofs. In each, what is
given is the precondition, what is claimed is the postcondition,
and the steps use the rules above. A proof that a method
satisfies its specification is a proof about every run at once.
Unlike the prose argument Lecture 1 made about Sum, an argument
built from the rules says where it fails. When every claim in it
is the strongest, as in the arguments for min and quadrant, the
implication that is false says on which inputs the method is wrong.
Leslie Lamport says, in one of Reading 2's articles, that proving a whole program correct this way turned out to be very hard in practice and is rarely done.2 Floyd and Hoare, Lamport says, taught us that a program is a mathematical object. So a programmer can ask what a program is supposed to do, and whether they could prove that it does that. In the terms of this lecture, we asked two questions at every line above: what is true here, and what is true after the next line?
What reasoning tells us
Lecture 5 closed by saying that a method whose tests complete the list, all passing, has been tested enough, and that this is still less than knowing it is correct. A test confirms a claim about its own run. The claim that the method's triple is valid, with its specification around its body, is about every run, and the rules can confirm it. So a proof that the triple is valid is the knowledge Lecture 5 said tests fall short of. Lecture 1 said that reasoning can confirm a claim about every run, if the argument is right. Two things can still go wrong, and one thing the rules cannot yet do:
- A false implication. A slip in applying a mechanical rule, such as a missed substitution, shows on the page to anyone who checks the step against the rule. We check an implication with arithmetic and logic instead, and a person can believe an implication that is false. Lecture 1 said what catches such a mistake: a test that refutes the claim refutes the argument too. So we still test.
- Arithmetic that is not Java's. The claims are about the
integers. Java's
intwraps, so a claim that is true of the integers can be false of the program.abswithout its precondition is the example. The argument says thatn = -nmakes a negativenpositive. But 2147483648 does not fit in anint, so whennis -2147483648,n = -nleavesnat -2147483648:abs(-2147483648)returns -2147483648! The precondition keeps that one input out, so on every input it allows, Java's arithmetic agrees with the integers. Every method in this lecture either has such a clause, asabsdoes, or does no arithmetic that could wrap, assecondsdoes by computing in along. - A loop. On every run of today's methods, each line ran at
most once.
Sum's loops run their lines once for each number typed. The rules above do not carry a claim through a loop that may run any number of times, so Lecture 1's argument aboutSumis still in prose. Friday's lecture gives the rule for a loop. That rule needs one claim from the programmer, a claim the rule itself does not produce.
Monday's lecture discusses Reading 2. Tuesday's section is on Floyd-Hoare logic.
Exercises
Each answer is folded under its question. Write yours first.
1. Here is a precondition, one line, and a postcondition:
{{ 1 <= n and n <= 100 }}
n = n + 3;
{{ 10 <= n * n }}
Reason forward from the precondition by the rule, then write the claim without the fresh name, and check that claim against the postcondition. Then reason backward from the postcondition, and check the precondition against the claim that the backward rule gives.
Answer
-
Forward, the claim after the line implies the postcondition. The forward rule gives the first claim after the line. Each claim implies the claim below it:
{{ 1 <= n and n <= 100 }} n = n + 3; {{ 1 <= n0 and n0 <= 100 and n = n0 + 3 }} {{ 1 <= n - 3 and n - 3 <= 100 }} {{ 4 <= n and n <= 103 }} {{ 4 <= n }} {{ 16 <= n * n }} {{ 10 <= n * n }}Counting from the top: the old value of
nis the new value minus 3, so the third claim putsn - 3forn0and dropsn = n0 + 3, the fact that gaven - 3. The fourth adds 3 to each side of both facts. The fifth drops a fact. The sixth squares both sides, since4 <= nmakesn * nat least4 * 4. The last is the postcondition, since 10 is at most 16. -
Backward, the precondition implies the claim before the line. Putting
n + 3fornin the postcondition gives the claim just above the line. Above that claim, a chain from the precondition reaches it:{{ 1 <= n and n <= 100 }} {{ 1 <= n }} {{ 4 <= n + 3 }} {{ 16 <= (n + 3) * (n + 3) }} {{ 10 <= (n + 3) * (n + 3) }} n = n + 3; {{ 10 <= n * n }}The second claim drops a fact. The third adds 3 to both sides. The fourth squares both sides. The last follows, since 10 is at most 16.
-
The triple is valid. Both implications are true. Neither check used
n <= 100. That bound keepsn + 3within anint, so the line does not wrap.
2. When does seconds return a number less than 3600? Reason
backward from the claim t < 3600 after line 19, and read the
answer off the claim at the top.
Answer
-
Three substitutions reach the top.
{{ (h * 60 + m) * 60 + s < 3600 }} long t = h; {{ (t * 60 + m) * 60 + s < 3600 }} t = t * 60 + m; {{ t * 60 + s < 3600 }} t = t * 60 + s; {{ t < 3600 }} -
secondsreturns less than 3600 exactly whenh = 0and60 * m + s < 3600. The claim at the top simplifies to3600 * h + 60 * m + s < 3600. Under the precondition,hand60 * m + sare both at least 0, sohmust be 0, and then the minutes and seconds together must come to less than an hour. Backward reasoning found the exact condition, which is more than "the hours are zero":seconds(0, 90, 0)returns 5400.
3. Here is compare with its first condition changed from
a < b to a <= b. Reason forward through it and say whether it
still satisfies its specification. If not, which inputs show that
it does not?
/**
* Compares two numbers.
*
* @param a one number
* @param b the other number
* @return -1 if a < b, 0 if a == b, and 1 if b < a
*/
static int compare(int a, int b) {
if (a <= b) {
return -1;
}
if (a == b) {
return 0;
}
return 1;
}
Answer
-
Forward reasoning gives these claims for the changed method.
static int compare(int a, int b) { {{ true }} if (a <= b) { {{ a <= b }} return -1; } {{ b < a }} if (a == b) { {{ b < a and a = b }} {{ false }} return 0; } {{ b < a }} return 1; } -
The changed method fails on equal numbers. At the first
return, the claim isa <= b. The specification promises -1 whena < band 0 whena = b. Every call witha = breaches thisreturnand returns -1, where the specification promises 0. So the changed method does not satisfy its specification, and every input witha = b, such ascompare(3, 3), shows that. - No run reaches the second
if's body. After the firstif, the claim isb < a. The body of the secondifneedsa = bas well, and no state makes both true, sob < a and a = bimpliesfalse, the claim written under it. No run reaches a point wherefalseholds, so the body runs on no input at all.
4. Repair min by giving its if an else that sets r to
b. Reason forward through the repaired method, with a claim at
the top and the bottom of each branch, and show that it satisfies
its specification.
Answer
-
Forward reasoning gives these claims for the repaired method.
static int min(int a, int b) { {{ true }} int r = a; {{ r = a }} if (a <= b) { {{ r = a and a <= b }} r = a; {{ r = a and a <= b }} } else { {{ r = a and b < a }} r = b; {{ r = b and b < a }} } {{ (r = a and a <= b) or (r = b and b < a) }} return r; } -
In both cases,
ris the smaller ofaandb. In the first casea <= bandr = a, the smaller. In the second caseb < aandr = b, the smaller. Both implications are true, so the repaired method satisfies its specification. The same argument on the originalminfailed in the caseb < a, because nothing setrtob.
5. Here is seconds again, returning an int, with the
clause its specification then needs:
/**
* Converts a duration in hours, minutes, and seconds to seconds.
*
* @param h the hours
* @param m the minutes
* @param s the seconds
* @requires 0 <= h, 0 <= m, and 0 <= s
* @requires 3600 * h + 60 * m + s <= 2147483647 (fits in an int)
* @return 3600 * h + 60 * m + s
*/
static int seconds(int h, int m, int s) {
int t = h;
t = t * 60 + m;
t = t * 60 + s;
return t;
}
The three tests pass on this version too. Reason forward through it. Does the argument use the new clause? What would go wrong without the clause? Then say how a client could check the clause before a call, and what else the method could do instead of asking for the clause.
Answer
- The argument never uses the clause. The four claims are the
claims in What is true after each line, and the check at the
returnis the same implication. The claims use integer arithmetic, but Java'sintwraps. So without the clause, a claim could be true of the integers and false of the program, as forabswithout its bound. The clause and the precondition's three facts0 <= h,0 <= m, and0 <= skeep every value computed on the way at most the answer, and the answer fits, so no line wraps. - A client checks the clause in
long. Inintarithmetic the check itself could wrap. Inlongit cannot. The client can also compute the total withMath.multiplyExactandMath.addExact, which throw instead of wrapping. An exercise in Lecture 6 used the second of them. - Two other designs. Return a
long, as the version at the top does, and the clause goes away. Or keep theint, compute withMath.multiplyExactandMath.addExact, and promise anArithmeticExceptionwhen the answer does not fit, one of Lecture 6's six ways. The argument for that design needs a case the rules above do not cover: a line that throws ends the run early, and the argument has to show the@throwspromise on that run.
Further reading
- The rules. C. A. R. Hoare, "An Axiomatic Basis for Computer Programming" (Communications of the ACM, 1969), a short paper and the source of the triple, the backward rule for an assignment, the rule for a sequence, and the rule of consequence.
- Weakest preconditions. Edsger W. Dijkstra, "Guarded Commands, Nondeterminacy and Formal Derivation of Programs" (Communications of the ACM, 1975), which treats a statement as a function from a postcondition to the weakest precondition from which the statement reaches it: the view behind backward reasoning.
- A textbook. David Gries, The Science of Programming (1981), which develops programs together with their proofs, by weakest preconditions.
-
C. A. R. Hoare, "An Axiomatic Basis for Computer Programming" (Communications of the ACM, 1969), says its treatment is essentially Robert Floyd's, applied to program texts instead of flowcharts. Floyd's "Assigning Meanings to Programs" (1967) attached a claim to each arrow of a flowchart, to hold whenever that arrow is taken, and gave the forward rule for an assignment, with
x0for the old value. ↩ -
Leslie Lamport, "The Future of Computing: Logic or Biology" (2003), the text of a talk, and one of Reading 2's three articles. ↩