Introduction to axiomatic semantics This approach to reasoning about programs and expressing program semantics has been originally proposed by Floyd and Hoare, and then pushed further by Dijkstra and Gries. The idea in axiomatic semantics is to give specifications for what programs are supposed to compute, as opposed to operational models that show how program executes. Such program specifications can be expressed using pre-conditions and post-conditions: {Pre} c {Post} where c is a program, and Pre and Post are logical formulas that describe properties of the program state (usually referred to as assertions). Such a triple is referred to as a partial correctness statement (or partial correctness assertion triple) and has the following meaning: ``If Pre holds before c, then Post holds after c, provided that c terminates'' In other words, if I start with a store s where Pre holds, and the execution of c in store s terminates and yields store s', then Post holds in store s'. Pre- and post-conditions can be regarded as interfaces or contracts between the program and its clients. They help users to understand what the program is supposed to yield without having to understand how the program execute. Typically, programmers write them as comments for procedures and functions, for better program understanding and to make it easier to maintain programs. Such specifications are especially useful for library functions, for which the source code is, in many cases, unavailable to the users. In this case, pre- and post-conditions serve as contracts between the library developers and the library clients, i.e. users. However, there is no guarantee that pre- and post-conditions written as informal code comments are actually correct -- they specify the intent, but give no correctness guarantees. In the following lectures we will look at how to rigorously describe partial correctness statements and how to prove and reason about program correctness. Note that partial correctness doesn't ensure that the given program will terminate -- this is why it is called ``partial correctness''. In contrast, total correctness statements ensure that the program terminates whenever the precondition holds. Such statements are denoted using square brackets: [Pre] c [Post] with the meaning that ``if Pre holds before c then c will terminate and Post will hold after c''. In general pre-conditions state what the program expect before execution; and post conditions state what guarantees they give when the program finishes. For instance, if c is a array sorting program, then an appropriate post-condition would be that the array is sorted and holds a permutation of the original array elements. Here's a simpler example: {x = 0 /\ y = i} z := 0; while (x != y) do (z := z - 2; x := x + 1) {z = -2i} The assertions use a logical variable i (which doesn't occur in the program) to capture the initial value of y. Note that this is a partial correctness statement. A total correctness statement for this program would be: [x = 0 /\ y = i /\ i >= 0] z := 0; while (x != y) do (z := z - 2; x := x + 1) [z = -2i] In the rest of our presentation of axiomatic semantics we will exclusively focus on partial correctness. In the rest of this lecture and in the following lectures we will formalize and address the following: - What is the language that we use for writing assertions? - What does it mean that an assertion is valid? What does it mean that a partial correctness statement {Pre} c {Post} is valid? - How can we prove that a partial correctness statement is valid? The Language of Assertions The language that we will use for writing assertion is the set of logical formulas that include comparisons of arithmetic expressions, standard logical operators (and, or, implication, negation), as well as quantifiers (universal and existential). Assertions may use additional logical variables, different than the variables that occur in the program. P, Q in Assn P::= true | false | a1 < a2 | P1 /\ P2 | P1 \/ P2 | P1 => P2 | neg P | forall i. P | exists i. Q a in Aexp a :: = ... | i i in LVar (the domain of logical variables) One can notice that the domain of boolean expressions Bexp is a subset of the domain of assertions. Notable additions over the syntax of boolean expression are quantifiers (forall and exists). For instance, one can express the fact that variable x divides variables y using existential quantification: exists i. x * i = y Validity of assertions Now we would like to describe what we mean by ``assertion P holds in store s''. But to determine whether P holds or not, we need more than just the store s (which maps program variables to their values); we also need to know the values of the logical variables. We describe those values using an interpretation I: I : LVar -> Int Now we can express the validity (or satisfiability) of assertion as a relation s |=_I P read as ``P is valid in store s under interpretation I'', or ``store s satisfies assertion P under interpretation I''. We will write s not |=_I P whenever s |=_I P doesn't hold. We can proceed to define the validity relation inductively: s |=_I true (always) s |=_I a1 < a2 if A_i[[a1]](s,I) < A_i[[a2]](s,I) s |=_I P1 /\ P2 if s |=_I P1 /\ s |=_I P2 s |=_I P1 \/ P2 if s |=_I P1 \/ s |=_I P2 s |=_I P1 => P2 if s not |=_I P1 \/ s |=_I P2 s |=_I neg P if s not |=_I P s |=_I forall i.P if forall k in Int . s |=_I[i->k] P s |=_I exists i.P if exists k in Int . s |=_I[i->k] P The evaluation function A_i[[a]](s,I) is similar to the denotation of expressions, but also deals with logical variables: A_i[[n]](s,I) = n A_i[[x]](s,I) = s(x) A_i[[i]](s,I) = I(i) A_i[[a1+a2]](s,I) = A_i[[a1]](s,I) + A_i[[a2]](s,I) We can now say that an assertion P is valid (written |= P) if it is valid in any store, under any interpretation: forall s, I. s |=_I P. Having defined validity for assertions, we can now define the validity of partial correctness triples. We say that the triple {P} c {Q} is valid in state s and interpretation I, written s |=_I {P} c {Q}, if: forall s',I: s |=_I P and C[[c]] s = s' implies s' |=_I Q Note that this definition talks about the execution of program c in the initial state s, described using the denotation C[[c]]. Finally, we can say that a partial correctness triple is valid |= {P} c {Q}, if it is valid in any store and interpretation: forall s, I. s |=_I {P} c {Q}. Now we know what we mean when we say `` assertion P holds'' or ``triple {P} c {Q} is valid''.