Introduction to Axiomatic Semantics: Introduced by Floyd (on state charts) and generalized by Hoare as a logical or algebraic way of constructing and reasoning about programs. Gries, Dijkstra, and others really pushed on this idea, not just for simple imperative programming languages but for other purposes. Today, the ideas show up in projects such as proof-carrying code, SPLint, model checkers, etc. Basic idea: we want to show that a given program c computes something. We can describe the something using a logical formula or assertion. For instance, we might write: c =def= (x := 0; s := 0; while (s <= p) do ( x := x + s; s := s + 1 )) Assuming that p is a positive integer, then what do we get as an output? A state where s = p+1 and x = \Sum_{i=1}^{p+1} i. We might write this as: { p >= 0 } c { s = p+1 ^ x = \Sum_{i=1}^{p+1} i } Here, the assertion {p >= 0} is a pre-condition describing which states are valid for running the command. The assertion { s = p+1 ^ x = \Sum_{i=1}^{p+1} i } is a post-condition specifying what the state looks like after the command completes. In general, when we have: { Pre } c { Post } then we read this as: "If we start in a state s such that Pre is true, and if we run command c in state s and get an output state s', then Post will be true for s'." Note that this is a *partial correctness* statement, as it doesn't say anything about a non-terminating command. In particular: { true } while true do skip { Post } is a valid statement for any assertion Post, because the command does not terminate. A *total correctness assertion*, sometimes written: [ Pre ] c [ Post ] says: "If we start in a state s satisfying Pre, then if we run c in state s, we will get an output state s' satisfying Post." Pre/post-conditions are often written (informally) on interfaces as the contract between a client and a library designer. For instance, we might have a lookup procedure that expects its data to be in sorted order. That could be specified as a pre-condition. We might also have a sorting routine which would have as a post-condition that it leaves data in sorted order. What we're going to do with axiomatic semantics: * Specify language of assertions carefully. * Define what we mean by a state satisfying an assertion. * Specify pre/post-condition rules for IMP that are as general as possible. * These are built using something called weakest pre-conditions (wp). Weakest pre-conditions are a very powerful tool. They let us take an arbitrary post condition Post (a specification) and a command c, and automatically calculate a pre-condition Pre such that: { Pre } c { Post } Furthermore, Pre will be the weakest possible pre-condition, meaning it will put as few requirements on the input state as is possible to run the command and get a state satisfying the post-condition. Note that the ideal pre-condition is "true". e in Exp ::= ... | n <---- a logical variable A in Assn ::= true | false | e1 <= e2 | A1 and A2 | A1 or A2 | A1 implies A2 | not(A) | All n.A | Exists n.A Assertions are like boolean expressions, except that we've added in for-all and there-exists quantifiers for integers. These are useful for expressing facts at the logical level that correspond to loops. For instance, if we want to specify that all of the elements in an array M are sorted, we might write: All i.0 <= i < size(M). All j.i < j < size(M).M[i] <= M[j] Or if we wanted to assert that x divides y evenly, we might have something like: Exists z.x * z = y We now need to define when a state satisfies a given assertion. We write: s |=I A to mean that state s satisfies assertion A, under interpretation I for the logical variables. Here, I is just a value for the logical variables in the assertion A. Its role will become apparent when we see what happens with quantifiers. We can define satisfaction as follows: s |=I true (always) s |=I e1 <= e2 if E[subst(I,e1)]s <= E[subst(I,e2)]s s |=I A1 and A2 if s |=I A1 and s |=I A2 s |=I A1 or A2 if s |=I A1 or s |=I A2 s |=I A1 implies A1 if not(s |=I A1) or s |=I A2 s |=I not(A) if not(s |=I A) s |=I All n.A if for all i, s |=I[i/n] A s |=I Exists n.A if there exists an i such that s |=I[i/n] A {} |=I A Note that the definition relies upon an auxiliary function subst(I,e) which is meant to substitute away the logical variables in an expression. In particular: subst(I,n) = I(n) subst(I,i) = i subst(I,x) = x subst(I,e1 op e2) = subst(I,e1) op subst(I,e2) Now we can define: s |=I { A1 } c { A2 } as meaning: If s |=I A1 and (s,s') in C[c], then s' |=I A2. We write: |=I { A1} c { A2} and say that this is a valid partial correctness assertion with respect to I, if for all states s, s |=I A1, if (s,s') in C[c], then s' |=I A2. Finally, we write: |= {A1} c {A2} and say the partial correctness assertion is valid if for all interpretations I, |=I {A1} c {A2}. Proof rules for determining partial correctness assertions: [skip] {A}skip{A} [assign] {A[e/x]} x := e {A} {A1}c1{A2} {A2}c2{A3} [seq] ------------------------ {A1}c1;c2{A3} {A1 and b}c1{A2} {A1 and not(b)}c2{A2} [if] ------------------------------------------ {A1}if B then c1 else c2{A2} {A and b}c{A} [while] ----------------------------- {A}while b do c{A and not(b)} |= A1 implies A2 {A2}c{A3} |= A3 implies A4 [consequence] ------------------------------------------------ {A1}c{A4} ------------------------------------------------------------------