(not finished … but should be readable … based on notes by Paula Severi)
In Programming Languages there is an important circle of ideas including Hoare logic and Dijkstra's predicate transformer semantics, Kozen's Kleene algebra and Pratt's Dynamic logic, O'Hearn and Reynolds's Separation logic and many more methods, techniques, tools, etc.
This line of research grew out of the methodology of structured programming, which is something that every programmer learns today right from the start, but was a novel concept around 1970. I postpone a discussion of this to the conclusions.
In this lecture, we will just give some of the basic ideas and illustrate them with an important technique of proving the correctness of while programs. Therefore, an alternative title of the lecture would be
The technique is based on a refinement of the method of invariants discussed in a previous lecture, so another possible title is
These ideas have various ramifications in programming and software engineering.
Preconditions, Postconditions, and Invariants are useful at all levels of programming and software development: Every function, procedure, method, component, API, etc has
Design by contract is a software engineering method build on these ideas.
Check out the list of programming languages supporting these ideas.
Assume/guarantee or rely/guarantee is an important technique in the development of concurrent programs
I am planning to add more in the conclusions, but now let us jump into a concrete
Consider the following program.
while (x != 0 ) do z:=z+y; x:= x-1 done
Does this program terminate?
What does the program calculate?
Does this program calculate x*y
in the variable z
?
It will depend on the value of x
and z
before executing the program.
This program will terminate and yield z = x*y
at the end if x
is positive and z==0
prior to it.
But how do we establish this conclusion?
And is it even correct to say that "z == x*y
at the end". After all, at the end, we have x==0
.
Let us think about it.
Each time the program goes through the loop x
will be decremented. How often does it happen? This again depends on x
.
The aim is to find a reasoning principle that frees us from tracking how often we go through a loop.
Here it is actually easy to see that we go x
times through the loop (if x
is not negative). But in more complicated examples there may not be a formula that allows us to caclulate the number of times we go through a loop from the available data.
(This follows from the halting problem.)
So we would like to decouple the question "how often do we go through the loop?" (termination) from the question of correctness … which is then called partial correctness, or more precisely, correctness under the assumption of termination.
Does this sound impossible?
Remember our lecture about invariants?
Can you spot an invariant? A property that remains unchanged while going throuth the loop?
Hint: Invent a notation that allows to distinguish the value of a program variable before and after the execution of some code. (There are different ways of doing this.)
Can you conclude from the invariant what the result of the computation is?
After answering the questions yourself, you can check with my solution in the footnote.[1]
As the example shows part of the difficulty lies in having a reliable notation that allows us to track change through a program and making statements (such as z == x*y
) about its properties.
The first important idea is that we want express that certain assumptions are satisfied before executing the program (preconditions) and properties that we then know after the execution of the program (postconditions)
If
A predicate that holds before the execution of the program is called a {\em precondition} and a predicate that holds after a {\em postcondition}. If the precondition is met before execution, the program establishes the postcondition if it terminates.
For example, we can write
to express what we said above, namely that the we want to execute the program above only in case
Notation: We use mathematical notation in pre and postconditions. For example, we write "=" in
What should be the post condition?
Exercise: A first idea for a postcondition is
What do you think about this? Does it look right? Consider how
Answer.[2]
Our difficulty in finding the correct postcondition can be remedied using the following trick.
While we allow program variables to change during the execution of a program
we also allow, not in programs, but in the properties
the mathematical variable
Exercise: What does
express about
Do you think it should say that the program variable
Exercise: Do the following pre and postconditions
capture our informal discusions above?
Exercise: How do you have to change the post and precondition if you want to drop the precondition
See the footnote for a solution.[3]
Hoare logic (also known as Floyd-Hoare logic) is a formal system developed by the British computer scientist C. A. R. Hoare, and subsequently refined by Hoare and other researchers. The purpose of the system is to provide a set of logical rules in order to reason about the correctness of computer programs with the rigour of mathematical logic and, therefore, the possibility of delegating such reasoning to a compiler or other verification software tools.
We will describe Hoare Logic for a minimal programming language containg only assignments, conditionals and while-loops. In addition to the rules for the simple language in Hoare's original paper, rules for other language constructs have been developed since then by Hoare and many other researchers. There are rules for concurrency, procedures, jumps, and pointers and much more. For example, Separation logic is used by Facebook in verification tools.
Let us go back to our leading example.
So let us try to formalise this using pre and postcondition, or, as on says, Hoare triples.
Exercise: Prove the Hoare triple
Question: Why is
Activity: Recall that the intended meaning of the while loop is to compute
What reasoning steps are needed to obtain this Hoare triple from the one of the previous exercise?
To carefully explain the answer to this question takes a little effort. So let us first give a short answer:
Show that the precondition implies the invariant and
that the invariant implies the postcondition
First, to show that the precondition implies the invariant, is to show that
This is easy: Just replace in the conclusion
Second, to show that the invariant implies the postcondition, is to show that
and if you go back to the program you see that this is possible since we know that
is a postcondition that must always be true after termination of the while-loop.
Exercise: Explain why. [5]
So summarise in symbolic notation what we have done so far, we started out with a rule stating that
and then modified it to take into account that after termination of the loop we also know "not
And then we used this rule by showing that the precondition implies
This last step, which is important to bring into the picture the pre and postconditions that are actually needed for our (partial) correctness assertion, can also be formalised as a rule as follows.
This rule is our first official rule of Hoare logic. We will see more later and they are all highlighted in red for reference.
Question: Why does this reasoning not require the precondition
This question has an important answer, so do look up the footnote after thinking about it yourself. [6]
Before we formulate the official rule for how to reason with a loop invariant, we need one more adjustment.
Question: Given the loop
what could we use as loop invariant?
It is easy to write down some irrelevant invariants, but which invariant would help us to prove the postcondition
A little thought shows that while an
is obviously an invariant for while B do S done
, this is more than we need. After all, the invariant only needs to hold if we enter the loop, so it is enough for a loop invariant
To illustrate this idea think about the following.
Exercise: Consider again
and find a loop invariant. Can you conclude from that invariant the postcondition
For a solution see the footnote.[7]
We are now ready to state the Hoare rule for a while loop.
The while rule is as follows.
Here
Exercise: Use the red rules above to show
and
From a practical programming point of view we already learned some important lessons from studying Hoare logic.
For example, we have seen
how to separate termination from partial correctness and
how to prove partial correctness of loops using invariants.
But there is more to Hoare logic.
We have seen two examples of symbolic rules (see the red rules above) that formalise the way we reasoned about our example programs.
Two big ideas of programming languages are now the following.
If we can formalise enough rules needed to reason about programs, then we can build software tools that do reason for us automatically.
There is hope to find all rules as long as we proceed in a compositional way, that is, "by induction" on the rules that define the programming language.
In our case, we have a programming language that has assignments, sequential composition, conditionals, and while-loops.
So far we have only seen the rule for while-loops.
In what follows, we will look at rules for the other programming constructions.
The rule of composition applies to sequentially-executed programs
For example, consider the following two instances of the assignment axiom:
and
By putting these together, we get:
The rule for composition of programs is as follows.
The assignment instruction is written as
and it assigns the value
which assigns
Now, suppose we know that
The predicate
Our predicates could involve more variables. For instance,
The assignment axiom in Hoare Logic states that after the assignment any predicate holds for the variable that was previously true for the right-hand side of the assignment:
In other words, the post-condition
Suppose now that the program is the statement
and we want that
Using the assignment axiom we get that
The precondition
Suppose now that our program is the statement
and we want that
Hence,
Remark: The assignment axiom proposed by Hoare does not apply when more than one name can refer to the same stored value. For example,
The above statement is not true if
Consider the following program.
We want to show that the above program calculates the absolute value of
denoted by
If
and then
Consider the following program.
We want to show that the above program calculates the maximum of the
two values
If
The conditional rule is as follows.
I will first summarise some of the lessons that we learned from studying Hoare logic, lessons that are relevant to programmers and software engineers far beyond the particularities of this specific approach to program correctness.
Then I will briefely look at the history of ideas around "structured programming" which gave rise to Hoare logic and other approaches to program verification.
… the following summary is currently just a collection of random notes …
The following habits can improve the practice of programming
These programming techniques have formal counterparts, some of which are
Hoare logic is compositional
We have seen that each programming construct corresponds to a rule in Hoare logic for a simple programming language consisting only of assignments, conditionals, sequential composition and while-loops. In principle there is no reason why this approach could not be extended to cover any construct available in any programming language. Not surprisingly, the literature on this topic is vast. For us here, the upshot is that the rules of Hoare logic are compositional in the definition of the syntax of the language.
Hoare logic is a formal system
While this lecture emphasised how to use invariants to reason about the correctness of while loops, we also hinted at how this reasoning can be formalised.
Hoare logic can be implemented by software tools that support program design and development
This is a consequence of the previous item. Formal systems can be implemented and then turned into tools.
Hoare logic as many ramifications for various special purpose situations such as concurrency, memory management, etc
(this section is being written as an after-thought and was not part of the semester)
Early programming close to the machine relies on jumps as a basic mechanism to control flow. Accordingly, early higher level languages such as Fortran had a corresponding
In 1966, an article by Bohm and … showed that jumps are not needed when … tbc …
We have seen that the rules of Hoare logic can be formalised in symbolic notation. It should therefore should be possible to implement software engineering tools that automate reasoning about programs.
To get a taste of how verification is done in real world programming applications, have a look at the blogs by David Crocker about Verifying loops in C and C++, part 1 and part 2 and part 3. There is also a very short introduction to software verification.
What about
The first trick here is to use two different variables: program variables
The second trick is that we can conclude the value of (x != 0)
that allows the entry into the loop. After all, we can only finish the loop if we are not allowed entry. But now see what happens. This conjunction implies
The problem with
The rule of logic that allows to replace equals by equals is one of the most basic rules that we typically use without feeling the need to formulate it explicitely, as we did in the example of equational reasoning under the name of "congruence rule". ↩︎
The condition x!=0
, that is,
Our first observation was that the precondition
Note that this goes in the opposite direction of our usual thinking about code, which is operational and proceeds from an initial state and works forward from there. ↩︎