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.