Axiomatic Semantics:
We're going to be using the following assertion language to describe
program states:
e ::= i | x | X | e1 bop e2
A ::= true | false | e1 < e2 | e1 = e2 | A1 && A2 | A1 || A2 |
A1 => A2 | !A | All X.A | Exists X.A
Here, x ranges over program-level variables, whereas X ranges over
logical (meta-level) variables. Notice that our quantifiers range
over meta-variables. Notice also that the expressions used within
the assertion language are a sub-set of the expressions used in the
object language -- this is mostly a happy coincidence. The key thing
is that the expression language here is purely functional (i.e.,
has no effects.) Note that we consider X bound in A for assertions
All X.A and Exists X.A, and that we identify assertions up to
alpha-conversion of bound variables (more on this later.) Also
note that I'll occasionally use derived forms. For instance,
I'll write e1 != e2 as shorthand for (e1 < e2) || (e2 < e1).
Next, we're going to define when a program state s satisfies an
assertion (written s |= A). However, to do so, we need to provide
an interpretation for logic variables that may show up in the
assertion. An interpretation I is a mapping from logic variables
to integers:
I in Interp = LogicVar -> Z
So technically, we write s |=I A to mean that under the interpretation I,
the store s satisfies the assertion A. This can be defined inductively
based on the structure of A. But before we do that, we're going to
need to define a valuation function for expressions that is relative to
a store and an interpretation. We'll define the function as follows:
E[i] s I = i
E[x] s I = s(x)
E[X] s I = I(x)
E[e1 bop e2] s I = (E[e1] s I) bop (E[e2] s I)
Now we can define the satisfaction relation:
s |=I true (every store satisfies true)
s |/=I false (no store satisfies false)
s |=I e1 < e2 iff (E[e1] s I) < (E[e2] s I)
s |=I e1 < e2 iff (E[e1] s I) = (E[e2] s I)
s |=I A1 && A2 iff s |=I A1 and s |=I A2
s |=I A1 || A2 iff s |=I A1 or s |=I A2
s |=I A1 => A2 iff s |=I !A1 or s |=I A2
s |=I !A1 iff s |/=I I
s |=I All X.A iff for all i, s |=I[X|->i] A
s |=I Exists X.A iff there exists an i s.t. s |=I[X|->i] A
Finally, we say that s |= A iff for all interpretations I, s |=I A.
Formally, a Hoare partial-correctness triple:
{A1}c{A2}
is valid, written |= {A1}c{A2}, if for all states s such that s |= A1,
if => s' then s' |= A2.
In contrast, a total-correctness triple is valid, written |= [A1]c[A2]
if for all states s such that s |= A1, there exists an s' such that
=> s' and s' |= A2.
Note that what we've effectively done is define an interpretation of
commands as a set of pairs of assertions. That is, we've effectively
defined a new C[c] = { (A1,A2) | |= {A1}c{A2} }. In this respect, our
definitions are similar to our denotational semantics and our dataflow
analysis.
Hoare formulated a set of inference rules that let us prove (rather
mechanically) when a Hoare-triple is valid. The rules look like this:
(R1) |- {A}skip{A}
(R2) |- {A[e/x]}x:=e{A}
|- {A1}c1{A} |- {A}c2{A2}
(R3) --------------------------
|- {A1}c1;c2{A2}
|- {A && e != 0}c1{B} |- {A && e = 0}c2{B}
(R4) --------------------------------------------
|- {A}if e then c1 else c2{B}
|- {A && e != 0}c{A}
(R5) ------------------------------
|- {A}while e do c{A && e = 0}
|- A1 => B1 |- {B1}c{B2} |- B2 => A2
(R6) ----------------------------------------
|- {A1}c{A2}
Most of these rules are straightforward, but rules R2, R5, and R6
deserve a bit of explanation. R2 is a sort of "backwards" rule. It
says that to show executing x:=e ends up in a state satisfying A, it's
sufficient to start in a state satisfying A[e/x] (i.e., A where we
substitute e for x). For instance, suppose we have x := y+1 as our
command and we have as a post- condition (x = y+1). Then our
pre-condition is (y+1) = (y+1) which is logically equivalent to
"true". Now supose we execute the same command and want as a
post-condition that x > z. Then our pre-condition is (y+1) > z.
An alternative to rule R2 is the following:
(R2') |- {A}x:=e{exists X.(A[X/x] && x = (e[X/x]))}
Try rule R2' out on some example pre-conditions and commands to
see how it works out. In some sense, R2 is a "backwards" flow
analysis, whereas R2' is a forwards flow analysis. We'll talk
more about this later.
Rule R6 is called the Rule of Consequence. It basically says
that if {B1}c{B2} holds, and A1 is a stronger pre-condition than
B1 (A1 => B1) and A2 is a weaker post-condition than B2 (B2=>A2),
then we can conclude that {A1}c{A2} holds. This is the usual
contra-/co-variant relation that we see in subtyping.
Note that the antecdents for the Rule of Consequence involve
derivations in 1st-order logic to show |- A1 => B1 and |- B2 => A2.
Those aren't Hoare-triples, so we need to prove them using the
axioms and inference rules of 1st-order logic (over integer
variables.) I'll remark that (a) proving or disproving A => B
in this logic is generally undecidable, (b) any (sound) set of
inference rules for concluding A => B is necessarily incomplete.
That is, there will be some assertions A such that |= A, but
that aren't provable (|/- A).
Finally, R5 concerns while loops. In essence, the rule says that
we need to find an invariant A which is true before execution of
the loop, and remains true each time we execute the command c.
Anyone who's ever proven something about programs knows that this
is where the real magic is. In general, we'll use the Rule of
Consequence to weaken our known pre-condition to a good loop
invariant, and then push that through the R5 rule. Then we'll
use Consequence again to weaken the post-condition.
You can use the Hoare rules to somewhat mechancially reason about
programs. Let's do an example:
----------------------------------------------------------------
Let c be the command:
c = while x do { y := x*y; x := x-1 }
We wish to prove that c computes n factorial (n!) where
we start off in an initial state s such that s(x) = n,
n is a positive number, and s(y) = 1. More precisely,
we want to prove:
|- { x=n && n>=0 && y=1} c { y=n! }
(Note that we define 0! = 1.) We'll need an invariant for the while loop.
Let I be the assertion:
I = (y * x! = n!) && (x >= 0)
This captures the intermediate states -- for each iteration of
the while loop, we still have to multiply the accumulator y
by x, x-1, x-2, ... 1 which is the same as n!. We're going
to need the other condition (x >= 0) to ensure that we get
out of the loop.
Our first step is to show that I is indeed an invariant. That is,
|- { I && x <> 0 } y := x*y; x := x-1 { I }
or expanding out:
|- {y*x!=n! && x >= 0 && x>0} y := x*y; x := x-1 {y*x!=n! && x>=0}
To prove this, we will first use the assignment rule applied to
the sub-command x := x-1:
|- {y*(x-1)!=n! && (x-1) <> 0} x := x-1 {y*x!=n! && x <> 0}
Again, by the assignment rule applied to the sub-command
y := x*y we have:
|- {(x*y)*(x-1)!=n! && (x-1) <> 0} y := x*y {y*(x-1)!=n! && (x-1) <> 0}
So from the rule for ";" we have:
|- {(x*y)*(x-1)!=n! && (x-1) <> 0} y := x*y; x := x-1 { I }
Now:
I && x<>0 == y*x!=n! && x >= 0 && x <> 0
=> y*x!=n! && x >= 1
=> y*x*(x-1)!=n! && (x-1) >= 0
=> x*y*(x-1)!=n! && (x-1) >= 0
So from the rule of consequence, we can conclude that:
|- {I && x <> 0} y := x*y; x := x-1 {I}
Then from the while-rule, we can conclude:
|- {I} c {I ^ x=0}
We now need to establish (x=n && n>=0 && y=1) => I and
(I && x=0) => y=n! to use the rule of consequence again
and finally establish that the desired pre/post-conditions
hold.
To show:
(x=n && n>= 0 && y=1) => ((y*x! = n!) && (x >= 0))
we need to show:
(x=n && n>= 0 && y=1) => (y*x! = n!) and
(x=n && n>= 0 && y=1) => (x >= 0)
The latter is straightforward since x=n && n >= 0. The former
is also straightforward since y=1 and x=n.
To show:
((y*x! = n!) && (x >= 0) && (x=0)) => y=n!
recall that 0! = 1, so we have:
x = 0 && y*x! = n! => y*0! = n! => y*1 = n! => y = n!.
----------------------------------------------------------------
Of course, this reasoning is only valid if we can show that the
rules are sound. So, we'd like to prove that whenever
|- {A1}c{A2}, it follows that |= {A1}c{A2}. That is, whatever
we can prove using the rules is in fact true.
Theorem: If |- {A1}c{A2} then |= {A1}c{A2}.
Proof: by induction on the derivation D concluding with
|- {A1}c{A2}.
case R1: |- {A}skip{A}. Let s |= A. Then => s and
s |= A.
case R2: |- {A[e/x]}x:=e{A}. Let s |= A[e/x]. Then
=> s[x|->e]. We now need to argue:
Lemma 1: s |=I A[e/x] implies s[x|->e] |=I A. By induction on A.
case A=true: s[x|->e] trivially satisfies A.
case A=false: A[e/x] = false[e/x] = false. Thus, there is no s |=I A.
case A=(e1 < e2): A[e/x] = e1[e/x] < e2[e/x]. We need to argue:
Lemma 2: E[e'[e/x]] s I = E[e'] s[x->(E[e] s I)] I.
Proof:
case A=(e1 = e2). Follows from Lemma 2.
case A=(A1 && A2). s |=I (A1 && A2)[e/x] = A1[e/x] && A2[e/x] =>
s |=I A1[e/x] and s |=I A2[e/x]. Applying the induction hypothesis,
this tells us s[x->e] |=I A1 and s[x->e] |=I A2 and thus
s[x->e] |=I A1 && A2.
[The other cases follow in a similar fashion.]
|- {A1}c1{A} |- {A}c2{A2}
case R3: --------------------------
|- {A1}c1;c2{A2}
Applying our induction hypothesis, we know (a) |= {A1}c1{A} and
(b) |= {A}c2{A2}. Let (c) s |= A1. If => s' then by
inversion of the evaluation rules, we know there exists an s1
such that (d) =>s1 and (e) =>s'. From (a) and (c)
and (d) we can conclude (f) s1 |= A. From (f), (b), and (e),
we can conclude s' |= A2. Thus |= {A1}c1;c2{A2}.
The other cases follow in a similar fashion.