Concrete Semantics With Isabelle/HOL
Concrete Semantics With Isabelle/HOL
Concrete Semantics With Isabelle/HOL
Concrete Semantics
with Isabelle/HOL
Springer-Verlag
I will not allow books to prove anything.
Why?
This book is the marriage of two areas: programming languages and theo-
rem proving. Most programmers feel that they understand the programming
language they use and the programs they write. Programming language se-
mantics replaces a warm feeling with precision in the form of mathematical
definitions of the meaning of programs. Unfortunately such definitions are of-
ten still at the level of informal mathematics. They are mental tools, but their
informal nature, their size, and the amount of detail makes them error prone.
Since they are typically written in LATEX, you do not even know whether they
VI Preface
would type-check, let alone whether proofs about the semantics, e.g., compiler
correctness, are free of bugs such as missing cases.
This is where theorem proving systems (or “proof asistants”) come in, and
mathematical (im)precision is replaced by logical certainty. A proof assistant is
a software system that supports the construction of mathematical theories as
formal language texts that are checked for correctness. The beauty is that this
includes checking the logical correctness of all proof text. No more ‘proofs’
that look more like LSD trips than coherent chains of logical arguments.
Machine-checked (aka “formal”) proofs offer the degree of certainty required
for reliable software but impossible to achieve with informal methods.
In research, the marriage of programming languages and proof assistants
has led to remarkable success stories like a verified C compiler [53] and a
verified operating system kernel [47]. This book introduces students and pro-
fessionals to the foundations and applications of this marriage.
Concrete?
Exercises!
The idea for this book goes back a long way [65]. But only recently have
proof assistants become mature enough for inflicting them on students without
causing the students too much pain. Nevertheless proof assistants still require
very detailed proofs. Learning this proof style (and all the syntactic details
that come with any formal language) requires practice. Therefore the book
contains a large number of exercises of varying difficulty. If you want to learn
Isabelle, you have to work through (some of) the exercises.
Acknowledgements
This book has benefited significantly from feedback by John Backes, Harry
Butterworth, Dan Dougherty, Andrew Gacek, Florian Haftmann, Peter John-
son, Yutaka Nagashima, Andrei Popescu, René Thiemann, Andrei Sabelfeld,
David Sands, Sean Seefried, Helmut Seidl, Christian Sternagel and Carl Witty.
Ronan Nugent provided very valuable editorial scrutiny.
The material in this book has been classroom-tested for a number of years.
Sascha Böhme, Johannes Hölzl, Alex Krauss, Peter Lammich and Andrei
Popescu worked out many of the exercises in the book.
Alex Krauss suggested the title Concrete Semantics.
NICTA, Technische Universität München and the DFG Graduiertenkolleg
1480 PUMA supported the writing of this book very generously.
We are very grateful for all these contributions.
Munich TN
Sydney GK
October 2014
Contents
Part I Isabelle
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Part II Semantics
6 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
8.1 Instructions and Stack Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
8.2 Reasoning About Machine Executions . . . . . . . . . . . . . . . . . . . . . . 98
8.3 Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
8.4 Preservation of Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
8.5 Summary and Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
9 Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
9.1 Typed IMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
9.2 Security Type Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
9.3 Summary and Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
B Symbols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
C Theories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
Part I
Isabelle
It’s blatantly clear
You stupid machine, that what
I tell you is true
Michael Norrish
1
Introduction
We assume that the reader is used to logical and set-theoretic notation and
is familiar with the basic concepts of functional programming. Open-minded
readers have been known to pick up functional programming through the
wealth of examples in Chapter 2 and Chapter 3.
Chapter 2 introduces HOL as a functional programming language and ex-
plains how to write simple inductive proofs of mostly equational properties
of recursive functions. Chapter 3 contains a small case study: arithmetic and
boolean expressions, their evaluation, optimization and compilation. Chap-
ter 4 introduces the rest of HOL: the language of formulas beyond equality,
automatic proof tools, single-step proofs, and inductive definitions, an essen-
tial specification construct. Chapter 5 introduces Isar, Isabelle’s language for
writing structured proofs.
This introduction to the core of Isabelle is intentionally concrete and
example-based: we concentrate on examples that illustrate the typical cases
without explaining the general case if it can be inferred from the examples. We
cover the essentials (from a functional programming point of view) as quickly
and compactly as possible. After all, this book is primarily about semantics.
For a comprehensive treatment of all things Isabelle we recommend the
Isabelle/Isar Reference Manual [92], which comes with the Isabelle distribu-
tion. The tutorial by Nipkow, Paulson and Wenzel [68] (in its updated version
that comes with the Isabelle distribution) is still recommended for the wealth
of examples and material, but its proof style is outdated. In particular it does
not cover the structured proof language Isar.
4 1 Introduction
If you have not done so already, download and install Isabelle (this book
is compatible with Isabelle2016-1) from http://isabelle.in.tum.de. You
can start it by clicking on the application icon. This will launch Isabelle’s
user interface based on the text editor jEdit. Below you see a typical exam-
ple snapshot of a session. At this point we merely explain the layout of the
window, not its contents.
The upper part of the window shows the input typed by the user, i.e., the
gradually growing Isabelle text of definitions, theorems, proofs, etc. The inter-
face processes the user input automatically while it is typed, just like modern
Java IDEs. Isabelle’s response to the user input is shown in the lower part of
the window. You can examine the response to any input phrase by clicking
on that phrase or by hovering over underlined text.
Part I frequently refers to the proof state. You can see the proof state if you
press the “State” button. If you want to see the proof state combined with other
system output, press “Output” and tick the “Proof state” box.
This should suffice to get started with the jEdit interface. Now you need
to learn what to type into it.
2
Programming and Proving
2.1 Basics
HOL is a typed logic whose type system resembles that of functional pro-
gramming languages. Thus there are
base types, in particular bool, the type of truth values, nat, the type of
natural numbers (N), and int , the type of mathematical integers (Z).
type constructors, in particular list, the type of lists, and set, the type of
sets. Type constructors are written postfix, i.e., after their arguments. For
example, nat list is the type of lists whose elements are natural numbers.
function types, denoted by ⇒.
type variables, denoted by 0a, 0b, etc., like in ML.
Note that 0a ⇒ 0b list means 0a ⇒ ( 0b list ), not ( 0a ⇒ 0b) list : postfix type
constructors have precedence over ⇒.
Terms are formed as in functional programming by applying functions to
arguments. If f is a function of type τ1 ⇒ τ2 and t is a term of type τ1 then
f t is a term of type τ2 . We write t :: τ to mean that term t has type τ.
There are many predefined infix symbols like + and 6. The name of the cor-
responding binary function is op +, not just +. That is, x + y is nice surface
syntax (“syntactic sugar”) for op + x y.
2.1.2 Theories
where T 1 . . . T n are the names of existing theories that T is based on. The
T i are the direct parent theories of T. Everything defined in the parent
theories (and their parents, recursively) is automatically visible. Each theory
T must reside in a theory file named T .thy.
HOL contains a theory Main, the union of all the basic predefined theories like
arithmetic, lists, sets, etc. Unless you know what you are doing, always include
Main as a direct or indirect parent of all your theories.
The textual definition of a theory follows a fixed syntax with keywords like
begin and datatype. Embedded in this syntax are the types and formulas of
HOL. To distinguish the two levels, everything HOL-specific (terms and types)
must be enclosed in quotation marks: ". . . ". Quotation marks around a single
identifier can be dropped. When Isabelle prints a syntax error message, it
refers to the HOL syntax as the inner syntax and the enclosing theory
language as the outer syntax.
The free variable m has been replaced by the unknown ?m. There is no
logical difference between the two but there is an operational one: unknowns
can be instantiated, which is what you want after some lemma has been
proved.
Note that there is also a proof method induct, which behaves almost like
induction; the difference is explained in Chapter 5.
Terminology: We use lemma, theorem and rule interchangeably for proposi-
tions that have been proved.
An Informal Proof
Lemma add m 0 = m
Proof by induction on m.
Case 0 (the base case): add 0 0 = 0 holds by definition of add.
Case Suc m (the induction step): We assume add m 0 = m, the induction
hypothesis (IH), and we need to show add (Suc m) 0 = Suc m. The proof
is as follows:
add (Suc m) 0 = Suc (add m 0) by definition of add
= Suc m by IH
Throughout this book, IH will stand for “induction hypothesis”.
We have now seen three proofs of add m 0 = 0: the Isabelle one, the terse
four lines explaining the base case and the induction step, and just now a
model of a traditional inductive proof. The three proofs differ in the level of
detail given and the intended reader: the Isabelle proof is for the machine, the
informal proofs are for humans. Although this book concentrates on Isabelle
proofs, it is important to be able to rephrase those proofs as informal text com-
prehensible to a reader familiar with traditional mathematical proofs. Later
on we will introduce an Isabelle proof language that is closer to traditional
informal mathematical language and is often directly readable.
10 2 Programming and Proving
Although lists are already predefined, we define our own copy for demonstra-
tion purposes:
datatype 0a list = Nil | Cons 0a " 0a list"
Type 0a list is the type of lists over elements of type 0a. Because 0a is a
type variable, lists are in fact polymorphic: the elements of a list can be
of arbitrary type (but must all be of the same type).
Lists have two constructors: Nil, the empty list, and Cons, which puts an
element (of type 0a) in front of a list (of type 0a list ). Hence all lists are
of the form Nil, or Cons x Nil, or Cons x (Cons y Nil), etc.
datatype requires no quotation marks on the left-hand side, but on the
right-hand side each of the argument types of a constructor needs to be
enclosed in quotation marks, unless it is just an identifier (e.g., nat or 0a).
We also define two standard functions, append and reverse:
fun app :: " 0a list ⇒ 0a list ⇒ 0a list" where
"app Nil ys = ys" |
"app (Cons x xs) ys = Cons x (app xs ys)"
Just as for natural numbers, there is a proof principle of induction for lists.
Induction over a list is essentially induction over the length of the list, al-
2.2 Types bool, nat and list 11
theory MyList
imports Main
begin
(* a comment *)
end
though the length remains implicit. To prove that some property P holds for
all lists xs, i.e., P xs, you need to prove
1. the base case P Nil and
2. the inductive case P (Cons x xs) under the assumption P xs, for some
arbitrary but fixed x and xs.
This is often called structural induction for lists.
We will now demonstrate the typical proof process, which involves the for-
mulation and proof of auxiliary lemmas. Our goal is to show that reversing a
list twice produces the original list.
theorem rev_rev [simp]: "rev (rev xs) = xs"
Commands theorem and lemma are interchangeable and merely indicate the
importance we attach to a proposition. Via the bracketed attribute simp we
also tell Isabelle to make the eventual theorem a simplification rule: future
proofs involving simplification will replace occurrences of rev (rev xs) by xs.
The proof is by induction:
apply(induction xs)
12 2 Programming and Proving
As explained above, we obtain two subgoals, namely the base case (Nil) and
the induction step (Cons):
1. rev (rev Nil) = Nil
V
2. x1 xs.
rev (rev xs) = xs =⇒ rev (rev (Cons x1 xs)) = Cons x1 xs
Let us try to solve both goals automatically:
apply(auto)
Subgoal 1 is proved, and disappears; the simplified version of subgoal 2 be-
comes the new subgoal 1:
V
1. x1 xs.
rev (rev xs) = xs =⇒
rev (app (rev xs) (Cons x1 Nil)) = Cons x1 xs
In order to simplify this subgoal further, a lemma suggests itself.
A First Lemma
A Second Lemma
Thankfully, this worked. Now we can continue with our stuck proof attempt
of the first lemma:
lemma rev_app [simp]: "rev (app xs ys) = app (rev ys) (rev xs)"
apply(induction xs)
apply(auto)
We find that this time auto solves the base case, but the induction step merely
simplifies to
V
1. x1 xs.
rev (app xs ys) = app (rev ys) (rev xs) =⇒
app (app (rev ys) (rev xs)) (Cons x1 Nil) =
app (rev ys) (app (rev xs) (Cons x1 Nil))
The missing lemma is associativity of app, which we insert in front of the
failed lemma rev_app.
Associativity of app
Didn’t we say earlier that all proofs are by simplification? But in both cases,
going from left to right, the last equality step is not a simplification at all!
In the base case it is app ys zs = app Nil (app ys zs). It appears almost
mysterious because we suddenly complicate the term by appending Nil on
the left. What is really going on is this: when proving some equality s = t ,
both s and t are simplified until they “meet in the middle”. This heuristic
for equality proofs works well for a functional programming context like ours.
In the base case both app (app Nil ys) zs and app Nil (app ys zs) are
simplified to app ys zs, the term in the middle.
Isabelle’s predefined lists are the same as the ones above, but with more
syntactic sugar:
[] is Nil,
x # xs is Cons x xs,
[x 1 , . . ., x n ] is x 1 # . . . # x n # [], and
xs @ ys is app xs ys.
There is also a large library of predefined functions. The most important ones
are the length function length :: 0a list ⇒ nat (with the obvious definition),
and the map function that applies a function to all elements of a list:
fun map :: "( 0a ⇒ 0b) ⇒ 0a list ⇒ 0b list" where
"map f Nil = Nil" |
"map f (Cons x xs) = Cons (f x ) (map f xs)"
Also useful are the head of a list, its first element, and the tail, the rest
of the list:
fun hd :: 0a list ⇒ 0a
hd (x # xs) = x
Exercises
Exercise 2.1. Use the value command to evaluate the following expressions:
"1 + (2::nat )", "1 + (2::int )", "1 − (2::nat )" and "1 − (2::int )".
Exercise 2.2. Start from the definition of add given above. Prove that add
is associative and commutative. Define a recursive function double :: nat ⇒
nat and prove double m = add m m.
Exercise 2.3. Define a function count :: 0a ⇒ 0a list ⇒ nat that counts the
number of occurrences of an element in a list. Prove count x xs 6 length xs.
Exercise 2.5. Define a recursive function sum_upto :: nat ⇒ nat such that
sum_upto n = 0 + ... + n and prove sum_upto n = n ∗ (n + 1) div 2.
2.3.1 Datatypes
Distinctness: Ci . . . 6= Cj . . . if i 6= j
Injectivity: (Ci x1 . . . xni = Ci y1 . . . yni ) =
(x1 = y1 ∧ . . . ∧ xni = yni )
The fact that any value of the datatype is built from the constructors implies
the structural induction rule: to show P x for all x of type ( 0a 1 ,. . ., 0a n )t,
one needs to show P(Ci x1 . . . xni ) (for each i) assuming P(xj ) for all j where
τi,j = ( 0a 1 ,. . ., 0a n )t. Distinctness and injectivity are applied automatically
by auto and other proof methods. Induction must be applied explicitly.
Like in functional programming languages, datatype values can be taken
apart with case expressions, for example
(case xs of [] ⇒ 0 | x # _ ⇒ Suc x )
Case expressions must be enclosed in parentheses.
As an example of a datatype beyond nat and list, consider binary trees:
datatype 0a tree = Tip | Node " 0a tree" 0
a " 0a tree"
with a mirror function:
fun mirror :: " 0a tree ⇒ 0a tree" where
"mirror Tip = Tip" |
"mirror (Node l a r ) = Node (mirror r ) a (mirror l)"
The following lemma illustrates induction:
lemma "mirror (mirror t ) = t"
apply(induction t )
yields
1. mirror (mirror Tip) = Tip
V
2. t1 x2 t2.
[[mirror (mirror t1) = t1; mirror (mirror t2) = t2]]
=⇒ mirror (mirror (Node t1 x2 t2)) = Node t1 x2 t2
The induction step contains two induction hypotheses, one for each subtree.
An application of auto finishes the proof.
A very simple but also very useful datatype is the predefined
datatype 0a option = None | Some 0a
Its sole purpose is to add a new element None to an existing type 0a. To
make sure that None is distinct from all the elements of 0a, you wrap them
up in Some and call the new type 0a option. A typical application is a lookup
function on a list of key-value pairs, often called an association list:
fun lookup :: "( 0a ∗ 0b) list ⇒ 0a ⇒ 0b option" where
2.3 Type and Function Definitions 17
"lookup [] x = None" |
"lookup ((a,b) # ps) x = (if a = x then Some b else lookup ps x )"
Note that τ1 ∗ τ2 is the type of pairs, also written τ1 × τ2 . Pairs can be taken
apart either by pattern matching (as above) or with the projection functions
fst and snd: fst (x , y) = x and snd (x , y) = y. Tuples are simulated by
pairs nested to the right: (a, b, c) is short for (a, (b, c)) and τ1 × τ2 × τ3
is short for τ1 × (τ2 × τ3 ).
2.3.2 Definitions
2.3.3 Abbreviations
Recursive functions are defined with fun by pattern matching over datatype
constructors. The order of equations matters, as in functional programming
languages. However, all HOL functions must be total. This simplifies the logic
— terms are always defined — but means that recursive functions must ter-
minate. Otherwise one could define a function f n = f n + 1 and conclude
0 = 1 by subtracting f n on both sides.
18 2 Programming and Proving
This customized induction rule can simplify inductive proofs. For example,
lemma "div2(n) = n div 2"
apply(induction n rule: div2.induct )
(where the infix div is the predefined division operation) yields the subgoals
1. div2 0 = 0 div 2
2. div2 (Suc 0) = Suc 0 div 2
V
3. n. div2 n = n div 2 =⇒
div2 (Suc (Suc n)) = Suc (Suc n) div 2
An application of auto finishes the proof. Had we used ordinary structural
induction on n, the proof would have needed an additional case analysis in
the induction step.
This example leads to the following induction heuristic:
Let f be a recursive function. If the definition of f is more complicated
than having one equation for each constructor of some datatype, then
properties of f are best proved via f .induct.
The general case is often called computation induction, because the
induction follows the (terminating!) computation. For every defining equation
f (e) = . . . f (r 1 ) . . . f (r k ) . . .
2.4 Induction Heuristics 19
where f (r i ), i =1. . .k, are all the recursive calls, the induction rule f .induct
contains one premise of the form
P (r 1 ) =⇒ . . . =⇒ P (r k ) =⇒ P (e)
If f :: τ1 ⇒ . . . ⇒ τn ⇒ τ then f .induct is applied like this:
apply(induction x 1 . . . x n rule: f .induct )
where typically there is a call f x 1 . . . x n in the goal. But note that the
induction rule does not mention f at all, except in its name, and is applicable
independently of f.
Exercises
Exercise 2.6. Starting from the type 0a tree defined in the text, define a
function contents :: 0a tree ⇒ 0a list that collects all values in a tree in a list,
in any order, without removing duplicates. Then define a function sum_tree
:: nat tree ⇒ nat that sums up all values in a tree of natural numbers and
prove sum_tree t = sum_list (contents t ) (where sum_list is predefined).
Exercise 2.7. Define a new type 0a tree2 of binary trees where values are
also stored in the leaves of the tree. Also reformulate the mirror function
accordingly. Define two functions pre_order and post_order of type 0a tree2
⇒ 0a list that traverse a tree and collect all stored values in the respective
order in a list. Prove pre_order (mirror t ) = rev (post_order t ).
We have already noted that theorems about recursive functions are proved by
induction. In case the function has more than one argument, we have followed
the following heuristic in the proofs about the append function:
Perform induction on argument number i
if the function is defined by recursion on argument number i.
The key heuristic, and the main point of this section, is to generalize the
goal before induction. The reason is simple: if the goal is too specific, the
induction hypothesis is too weak to allow the induction step to go through.
Let us illustrate the idea with an example.
20 2 Programming and Proving
Function rev has quadratic worst-case running time because it calls ap-
pend for each element of the list and append is linear in its first argument.
A linear time version of rev requires an extra argument where the result is
accumulated gradually, using only #:
fun itrev :: " 0a list ⇒ 0a list ⇒ 0a list" where
"itrev [] ys = ys" |
"itrev (x #xs) ys = itrev xs (x #ys)"
The behaviour of itrev is simple: it reverses its first argument by stacking
its elements onto the second argument, and it returns that second argument
when the first one becomes empty. Note that itrev is tail-recursive: it can be
compiled into a loop; no stack is necessary for executing it.
Naturally, we would like to show that itrev does indeed reverse its first
argument provided the second one is empty:
lemma "itrev xs [] = rev xs"
There is no choice as to the induction variable:
apply(induction xs)
apply(auto)
Unfortunately, this attempt does not prove the induction step:
V
1. a xs. itrev xs [] = rev xs =⇒ itrev xs [a] = rev xs @ [a]
The induction hypothesis is too weak. The fixed argument, [], prevents it from
rewriting the conclusion. This example suggests a heuristic:
Generalize goals for induction by replacing constants by variables.
Of course one cannot do this naively: itrev xs ys = rev xs is just not true.
The correct generalization is
lemma "itrev xs ys = rev xs @ ys"
If ys is replaced by [], the right-hand side simplifies to rev xs, as required. In
this instance it was easy to guess the right generalization. Other situations
can require a good deal of creativity.
Although we now have two variables, only xs is suitable for induction, and
we repeat our proof attempt. Unfortunately, we are still not there:
V
1. a xs.
itrev xs ys = rev xs @ ys =⇒
itrev xs (a # ys) = rev xs @ a # ys
The induction hypothesis is still too weak, but this time it takes no intuition
to generalize: the problem is that the ys in the induction hypothesis is fixed,
2.5 Simplification 21
Exercises
2.5 Simplification
So far we have talked a lot about simplifying terms without explaining the
concept. Simplification means
using equations l = r from left to right (only),
as long as possible.
To emphasize the directionality, equations that have been given the simp
attribute are called simplification rules. Logically, they are still symmetric,
22 2 Programming and Proving
but proofs by simplification use them only in the left-to-right direction. The
proof tool that performs simplifications is called the simplifier. It is the basis
of auto and other related proof methods.
The idea of simplification is best explained by an example. Given the
simplification rules
0+n=n (1)
Suc m + n = Suc (m + n) (2)
(Suc m 6 Suc n) = (m 6 n) (3)
(0 6 m) = True (4)
Simplification rules can be conditional. Before applying such a rule, the sim-
plifier will first try to prove the preconditions, again by simplification. For
example, given the simplification rules
2.5 Simplification 23
p 0 = True
p x =⇒ f x = g x,
the term f 0 simplifies to g 0 but f 1 does not simplify because p 1 is not
provable.
2.5.3 Termination
So far we have only used the proof method auto. Method simp is the key
component of auto, but auto can do much more. In some cases, auto is
overeager and modifies the proof state too much. In such cases the more
predictable simp method should be used. Given a goal
1. [[ P 1 ; . . .; P m ]] =⇒ C
the command
apply(simp add: th 1 . . . th n )
simplifies the assumptions P i and the conclusion C using
all simplification rules, including the ones coming from datatype and fun,
the additional lemmas th 1 . . . th n , and
24 2 Programming and Proving
the assumptions.
In addition to or instead of add there is also del for removing simplification
rules temporarily. Both are optional. Method auto can be modified similarly:
apply(auto simp add: . . . simp del: . . .)
Here the modifiers are simp add and simp del instead of just add and del
because auto does not just perform simplification.
Note that simp acts only on subgoal 1, auto acts on all subgoals. There
is also simp_all, which applies simp to all subgoals.
Goals containing if-expressions are automatically split into two cases by simp
using the rule
P (if A then s else t ) = ((A −→ P s) ∧ (¬ A −→ P t ))
For example, simp can prove
(A ∧ B ) = (if A then B else False)
because both A −→ (A ∧ B ) = B and ¬ A −→ (A ∧ B ) = False simplify
to True.
We can split case-expressions similarly. For nat the rule looks like this:
P (case e of 0 ⇒ a | Suc n ⇒ b n) =
((e = 0 −→ P a) ∧ (∀ n. e = Suc n −→ P (b n)))
2.5 Simplification 25
Case expressions are not split automatically by simp, but simp can be in-
structed to do so:
apply(simp split : nat .split )
splits all case-expressions over natural numbers. For an arbitrary datatype t
it is t .split instead of nat .split. Method auto can be modified in exactly the
same way. The modifier split : can be followed by multiple names. Splitting
if or case-expressions in the assumptions requires split : if_splits or split :
t .splits.
Exercises
Exercise 2.10. Define a datatype tree0 of binary tree skeletons which do not
store any information, neither in the inner nodes nor in the leaves. Define a
function nodes :: tree0 ⇒ nat that counts the number of all nodes (inner
nodes and leaves) in such a tree. Consider the following recursive function:
fun explode :: "nat ⇒ tree0 ⇒ tree0" where
"explode 0 t = t" |
"explode (Suc n) t = explode n (Node t t )"
Find an equation expressing the size of a tree after exploding it (nodes
(explode n t )) as a function of nodes t and n. Prove your equation. You
may use the usual arithmetic operators, including the exponentiation opera-
tor “^”. For example, 2 ^ 2 = 4.
Hint: simplifying with the list of theorems algebra_simps takes care of
common algebraic properties of the arithmetic operators.
The methods of the previous chapter suffice to define the arithmetic and
boolean expressions of the programming language IMP that is the subject
of this book. In this chapter we define their syntax and semantics, write
little optimizers for them and show how to compile arithmetic expressions
to a simple stack machine. Of course we also prove the correctness of the
optimizers and compiler!
thy
3.1 Arithmetic Expressions
3.1.1 Syntax
The tree immediately reveals the nested structure of the object and is the
right level for analysing and manipulating expressions. Linear strings are more
compact than two-dimensional trees, which is why they are used for reading
and writing programs. But the first thing a compiler, or rather its parser,
will do is to convert the string into a tree for further processing. Now we
28 3 Case Study: IMP Expressions
are at the level of abstract syntax and these trees are abstract syntax
trees. To regain the advantages of the linear string notation we write our
abstract syntax trees as strings with parentheses to indicate the nesting (and
with identifiers instead of the symbols + and *), for example like this: Plus a
(Times 5 b). Now we have arrived at ordinary terms as we have used them
all along. More precisely, these terms are over some datatype that defines the
abstract syntax of the language. Our little language of arithmetic expressions
is defined by the datatype aexp:
where int is the predefined type of integers and vname stands for variable
name. Isabelle strings require two single quotes on both ends, for example
00
abc 0 0 . The intended meaning of the three constructors is as follows: N rep-
resents numbers, i.e., constants, V represents variables, and Plus represents
addition. The following examples illustrate the intended correspondence:
Concrete Abstract
5 N5
x V 0 0x 0 0
x + y Plus (V 0 0x 0 0 ) (V 0 0y 0 0 )
2 + (z + 3) Plus (N 2) (Plus (V 0 0z 0 0 ) (N 3))
It is important to understand that so far we have only defined syntax, not
semantics! Although the binary operation is called Plus, this is merely a
suggestive name and does not imply that it behaves like addition. For example,
Plus (N 0) (N 0) 6= N 0, although you may think of them as semantically
equivalent — but syntactically they are not.
Datatype aexp is intentionally minimal to let us concentrate on the essen-
tials. Further operators can be added as desired. However, as we shall discuss
below, not all operators are as well behaved as addition.
3.1.2 Semantics
The semantics, or meaning of an expression, is its value. But what is the value
of x+1? The value of an expression with variables depends on the values of its
variables. The value of all variables is recorded in the (program) state. The
state is a function from variable names to values.
type_synonym val = int
type_synonym state = vname ⇒ val
Function aval carries around a state and is defined by recursion over the form
of the expression. Numbers evaluate to themselves, variables to their value in
the state, and addition is evaluated recursively. Here is a simple example:
0 0 00
value "aval (Plus (N 3) (V x )) (λx . 0)"
returns 3. However, we would like to be able to write down more interesting
states than λx . 0 easily. This is where function update comes in.
To update the state, that is, change the value of some variable name, the
generic function update notation f (a := b) is used: the result is the same as
f, except that it maps a to b:
f (a := b) = (λx . if x = a then b else f x )
This operator allows us to write down concrete states in a readable fashion.
Starting from the state that is 0 everywhere, we can update it to map certain
variables to given values. For example, ((λx . 0) ( 0 0x 0 0 := 7)) ( 0 0y 0 0 := 3) maps
0 0 00
x to 7, 0 0y 0 0 to 3 and all other variable names to 0. Below we employ the
following more compact notation
< 0 0x 0 0 := 7, 0 0 00
y := 3>
which works for any number of variables, even for none: <> is syntactic sugar
for λx . 0.
It would be easy to add subtraction and multiplication to aexp and extend
aval accordingly. However, not all operators are as well behaved: division by
zero raises an exception and C’s ++ changes the state. Neither exceptions nor
side effects can be supported by an evaluation function of the simple type
aexp ⇒ state ⇒ val; the return type has to be more complicated.
"asimp_const (V x ) = V x" |
"asimp_const (Plus a 1 a 2 ) =
(case (asimp_const a 1 , asimp_const a 2 ) of
(N n 1 , N n 2 ) ⇒ N (n 1 +n 2 ) |
(b 1 ,b 2 ) ⇒ Plus b 1 b 2 )"
Neither N nor V can be simplified further. Given a Plus, first the two subex-
pressions are simplified. If both become numbers, they are added. In all other
cases, the results are recombined with Plus.
It is easy to show that asimp_const is correct. Correctness means that
asimp_const does not change the semantics, i.e., the value of its argument:
lemma "aval (asimp_const a) s = aval a s"
The proof is by induction on a. The two base cases N and V are trivial. In
the Plus a 1 a 2 case, the induction hypotheses are aval (asimp_const a i ) s
= aval a i s for i =1,2. If asimp_const a i = N n i for i =1,2, then
aval (asimp_const (Plus a 1 a 2 )) s
= aval (N (n 1 +n 2 )) s = n 1 +n 2
= aval (asimp_const a 1 ) s + aval (asimp_const a 2 ) s
= aval (Plus a 1 a 2 ) s.
Otherwise
aval (asimp_const (Plus a 1 a 2 )) s
= aval (Plus (asimp_const a 1 ) (asimp_const a 2 )) s
= aval (asimp_const a 1 ) s + aval (asimp_const a 2 ) s
= aval (Plus a 1 a 2 ) s.
This is rather a long proof for such a simple lemma, and boring to boot. In
the future we shall refrain from going through such proofs in such excessive
detail. We shall simply write “The proof is by induction on a.” We will not even
mention that there is a case distinction because that is obvious from what we
are trying to prove, which contains the corresponding case expression, in the
body of asimp_const. We can take this attitude because we merely suppress
the obvious and because Isabelle has checked these proofs for us already and
you can look at them in the files accompanying the book. The triviality of
the proof is confirmed by the size of the Isabelle text:
apply (induction a)
apply (auto split : aexp.split )
done
The split modifier is the hint to auto to perform a case split whenever it sees
a case expression over aexp. Thus we guide auto towards the case distinction
we made in our proof above.
Let us extend constant folding: Plus (N 0) a and Plus a (N 0) should be
replaced by a. Instead of extending asimp_const we split the optimization
3.1 Arithmetic Expressions 31
process into two functions: one performs the local optimizations, the other tra-
verses the term. The optimizations can be performed for each Plus separately
and we define an optimizing version of Plus:
fun plus :: "aexp ⇒ aexp ⇒ aexp" where
"plus (N i 1 ) (N i 2 ) = N (i 1 +i 2 )" |
"plus (N i ) a = (if i =0 then a else Plus (N i ) a)" |
"plus a (N i ) = (if i =0 then a else Plus a (N i ))" |
"plus a 1 a 2 = Plus a 1 a 2 "
It behaves like Plus under evaluation:
lemma aval_plus: "aval (plus a 1 a 2 ) s = aval a 1 s + aval a 2 s"
The proof is by induction on a 1 and a 2 using the computation induction rule
for plus (plus.induct ). Now we replace Plus by plus in a bottom-up manner
throughout an expression:
fun asimp :: "aexp ⇒ aexp" where
"asimp (N n) = N n" |
"asimp (V x ) = V x" |
"asimp (Plus a 1 a 2 ) = plus (asimp a 1 ) (asimp a 2 )"
Correctness is expressed exactly as for asimp_const :
lemma "aval (asimp a) s = aval a s"
The proof is by structural induction on a; the Plus case follows with the help
of Lemma aval_plus.
Exercises
Exercise 3.2. In this exercise we verify constant folding for aexp where we
sum up all constants, even if they are not next to each other. For example, Plus
(N 1) (Plus (V x ) (N 2)) becomes Plus (V x ) (N 3). This goes beyond asimp.
Define a function full_asimp :: aexp ⇒ aexp that sums up all constants and
prove its correctness: aval (full_asimp a) s = aval a s.
0 0 00 0 0 00 0 0 00 0 0 00
subst x (N 3) (Plus (V x ) (V y )) = Plus (N 3) (V y )
Prove the so-called substitution lemma that says that we can either
substitute first and evaluate afterwards or evaluate with an updated state:
aval (subst x a e) s = aval e (s(x := aval a s)). As a consequence prove
aval a 1 s = aval a 2 s =⇒ aval (subst x a 1 e) s = aval (subst x a 2 e) s.
Exercise 3.4. Take a copy of theory AExp and modify it as follows. Extend
type aexp with a binary constructor Times that represents multiplication.
Modify the definition of the functions aval and asimp accordingly. You can
remove asimp_const. Function asimp should eliminate 0 and 1 from multi-
plications as well as evaluate constant subterms. Update all proofs concerned.
Exercise 3.6. The following type adds a LET construct to arithmetic ex-
pressions:
datatype lexp = Nl int | Vl vname | Plusl lexp lexp | LET vname lexp lexp
The LET constructor introduces a local variable: the value of LET x e 1 e 2
is the value of e 2 in the state where x is bound to the value of e 1 in the
original state. Define a function lval :: lexp ⇒ state ⇒ int that evaluates
lexp expressions. Remember s(x := i ).
Define a conversion inline :: lexp ⇒ aexp. The expression LET x e 1 e 2
is inlined by substituting the converted form of e 1 for x in the converted form
of e 2 . See Exercise 3.3 for more on substitution. Prove that inline is correct
w.r.t. evaluation.
thy
3.2 Boolean Expressions
Note that there are no boolean variables in this language. Other operators
like disjunction and equality are easily expressed in terms of the basic ones.
Evaluation of boolean expressions is again by recursion over the abstract
syntax. In the Less case, we switch to aval:
Exercises
Exercise 3.7. Define functions Eq, Le :: aexp ⇒ aexp ⇒ bexp and prove
bval (Eq a 1 a 2 ) s = (aval a 1 s = aval a 2 s) and bval (Le a 1 a 2 ) s =
(aval a 1 s 6 aval a 2 s).
thy
3.3 Stack Machine and Compilation
This section describes a simple stack machine and compiler for arithmetic
expressions. The stack machine has three instructions:
datatype instr = LOADI val | LOAD vname | ADD
The semantics of the three instructions will be the following: LOADI n (load
immediate) puts n on top of the stack, LOAD x puts the value of x on top of
the stack, and ADD replaces the two topmost elements of the stack by their
sum. A stack is simply a list of values:
type_synonym stack = "val list"
The top of the stack is its first element, the head of the list (see Sec-
tion 2.2.5). We define two further abbreviations: hd2 xs ≡ hd (tl xs) and
tl2 xs ≡ tl (tl xs).
An instruction is executed in the context of a state and transforms a stack
into a new stack:
fun exec1 :: "instr ⇒ state ⇒ stack ⇒ stack" where
"exec1 (LOADI n) _ stk = n # stk" |
"exec1 (LOAD x ) s stk = s(x ) # stk" |
"exec1 ADD _ stk = (hd2 stk + hd stk ) # tl2 stk"
A list of instructions is executed one by one:
fun exec :: "instr list ⇒ state ⇒ stack ⇒ stack" where
"exec [] _ stk = stk" |
"exec (i #is) s stk = exec is s (exec1 i s stk )"
The simplicity of this definition is due to the absence of jump instructions.
Forward jumps could still be accommodated, but backward jumps would cause
a serious problem: execution might not terminate.
Compilation of arithmetic expressions is straightforward:
fun comp :: "aexp ⇒ instr list" where
"comp (N n) = [LOADI n]" |
"comp (V x ) = [LOAD x ]" |
"comp (Plus e 1 e 2 ) = comp e 1 @ comp e 2 @ [ADD]"
The correctness statement says that executing a compiled expression is the
same as putting the value of the expression on the stack:
lemma "exec (comp a) s stk = aval a s # stk "
The proof is by induction on a and relies on the lemma
exec (is 1 @ is 2 ) s stk = exec is 2 s (exec is 1 s stk )
36 3 Case Study: IMP Expressions
Exercises
Exercise 3.11. This exercise is about a register machine and compiler for
aexp. The machine instructions are
datatype instr = LDI int reg | LD vname reg | ADD reg reg
where type reg is a synonym for nat. Instruction LDI i r loads i into register
r, LD x r loads the value of x into register r, and ADD r 1 r 2 adds register
r 2 to register r 1 .
Define the execution of an instruction given a state and a register state
(= function from registers to integers); the result is the new register state:
fun exec1 :: instr ⇒ state ⇒ (reg ⇒ int ) ⇒ reg ⇒ int
Define the execution exec of a list of instructions as for the stack machine.
The compiler takes an arithmetic expression a and a register r and pro-
duces a list of instructions whose execution places the value of a into r. The
registers > r should be used in a stack-like fashion for intermediate results,
the ones < r should be left alone. Define the compiler and prove it correct:
exec (comp a r ) s rs r = aval a s.
Exercise 3.12. This is a variation on the previous exercise. Let the instruc-
tion set be
datatype instr0 = LDI0 val | LD0 vname | MV0 reg | ADD0 reg
All instructions refer implicitly to register 0 as the source (MV0) or target
(all others). Define a compiler pretty much as explained above except that
the compiled code leaves the value of the expression in register 0. Prove that
exec (comp a r ) s rs 0 = aval a s.
4
Logic and Proof Beyond Equality
4.1 Formulas
The core syntax of formulas (form below) provides the standard logical con-
structs, in decreasing order of precedence:
Terms are the ones we have seen all along, built from constants, variables,
function application and λ-abstraction, including all the syntactic sugar like
infix symbols, if, case, etc.
Remember that formulas are simply terms of type bool. Hence = also works for
formulas. Beware that = has a higher precedence than the other logical operators.
Hence s = t ∧ A means (s = t ) ∧ A, and A ∧ B = B ∧ A means A ∧ (B = B )
∧ A. Logical equivalence can also be written with ←→ instead of =, where ←→ has
the same low precedence as −→. Hence A ∧ B ←→ B ∧ A really means (A ∧ B )
←→ (B ∧ A).
The most frequent logical symbols and their ASCII representations are shown
in Fig. 4.1. The first column shows the symbols, the other columns ASCII
representations. The \<...> form is always converted into the symbolic form
by the Isabelle interfaces, the treatment of the other ASCII forms depends on
the interface. The ASCII forms /\ and \/ are special in that they are merely
keyboard shortcuts for the interface and not logical symbols by themselves.
38 4 Logic and Proof Beyond Equality
∀ \<forall> ALL
∃ \<exists> EX
λ \<lambda> %
−→ -->
←→ <->
∧ /\ &
∨ \/ |
¬ \<not> ~
6= \<noteq> ~=
4.2 Sets
Sets of elements of type 0a have type 0a set . They can be finite or infinite.
Sets come with the usual notation:
{}, {e 1 ,. . .,e n }
e ∈ A, A ⊆ B
A ∪ B , A ∩ B , A − B, − A
(where A − B and −A are set difference and complement) and much more.
UNIV is the set of all elements of some type. Set comprehension is written
{x . P } rather than {x | P }.
In {x . P } the x must be a variable. Set comprehension involving a proper term
t must be written {t | x y. P }, where x y are those free variables in t that occur
in P. This is just a shorthand for {v . ∃ x y. v = t ∧ P }, where v is a new variable.
For example, {x + y |x . x ∈ A} is short for {v . ∃ x . v = x +y ∧ x ∈ A}.
A = {x . ∃ B ∈A. x ∈ B } A = {x . ∀ B ∈A. x ∈ B }
S T
S T
The ASCII forms of are \<Union> and Union, those of are \<Inter>
and Inter. There are also indexed unions and intersections:
( x ∈A B x ) = {y. ∃ x ∈A. y ∈ B x }
S
( x ∈A B x ) = {y. ∀ x ∈A. y ∈ B x }
T
The ASCII forms are UN x:A. B and INT x:A. B where x may occur in B.
If A is UNIV you can write UN x. B and INT x. B.
Some other frequently useful functions on sets are the following:
set :: 0a list ⇒ 0a set converts a list to the set of its elements
finite :: 0a set ⇒ bool is true iff its argument is finite
card :: 0a set ⇒ nat is the cardinality of a finite set
and is 0 for all infinite sets
f ‘ A = {y. ∃ x ∈A. y = f x } is the image of a function over a set
See [64] for the wealth of further predefined functions in theory Main.
Exercises
Exercise 4.1. Start from the data type of binary trees defined earlier:
datatype 0a tree = Tip | Node " 0a tree" 0a " 0a tree"
Define a function set :: 0a tree ⇒ 0a set that returns the elements in a tree
and a function ord :: int tree ⇒ bool that tests if an int tree is ordered.
Define a function ins that inserts an element into an ordered int tree
while maintaining the order of the tree. If the element is already in the tree,
the same tree should be returned. Prove correctness of ins: set (ins x t ) =
{x } ∪ set t and ord t =⇒ ord (ins i t ).
So far we have only seen simp and auto: Both perform rewriting, both can
also prove linear arithmetic facts (no multiplication), and auto is also able to
prove simple logical or set-theoretic goals:
lemma "∀ x . ∃ y. x = y"
by auto
by proof-method
is short for
apply proof-method
done
The key characteristics of both simp and auto are
They show you where they got stuck, giving you an idea how to continue.
They perform the obvious steps but are highly incomplete.
A proof method is complete if it can prove all true formulas. There is no
complete proof method for HOL, not even in theory. Hence all our proof
methods only differ in how incomplete they are.
A proof method that is still incomplete but tries harder than auto is
fastforce. It either succeeds or fails, it acts on the first subgoal only, and it
can be modified like auto, e.g., with simp add. Here is a typical example of
what fastforce can do:
lemma "[[ ∀ xs ∈ A. ∃ ys. xs = ys @ ys; us ∈ A ]]
=⇒ ∃ n. length us = n+n"
by fastforce
This lemma is out of reach for auto because of the quantifiers. Even fastforce
fails when the quantifier structure becomes more complicated. In a few cases,
its slow version force succeeds where fastforce fails.
The method of choice for complex logical goals is blast . In the following
example, T and A are two binary predicates. It is shown that if T is total,
A is antisymmetric and T is a subset of A, then A is a subset of T :
lemma
"[[ ∀ x y. T x y ∨ T y x ;
∀ x y. A x y ∧ A y x −→ x = y;
∀ x y. T x y −→ A x y ]]
=⇒ ∀ x y. A x y −→ T x y"
by blast
We leave it to the reader to figure out why this lemma is true. Method blast
is (in principle) a complete proof procedure for first-order formulas, a
fragment of HOL. In practice there is a search bound.
does no rewriting and knows very little about equality.
covers logic, sets and relations.
either succeeds or fails.
Because of its strength in logic and sets and its weakness in equality reasoning,
it complements the earlier proof methods.
4.3 Proof Automation 41
4.3.1 Sledgehammer
4.3.2 Arithmetic
If you want to try all of the above automatic proof methods you simply type
try
There is also a lightweight variant try0 that does not call sledgehammer. If
desired, specific simplification and introduction rules can be added:
try0 simp: . . . intro: . . .
Although automation is nice, it often fails, at least initially, and you need
to find out why. When fastforce or blast simply fail, you have no clue why.
At this point, the stepwise application of proof rules may be necessary. For
example, if blast fails on A ∧ B, you want to attack the two conjuncts A and
B separately. This can be achieved by applying conjunction introduction
?P ?Q
conjI
?P ∧ ?Q
to the proof state. We will now examine the details of this process.
We had briefly mentioned earlier that after proving some theorem, Isabelle re-
places all free variables x by so called unknowns ?x. We can see this clearly in
rule conjI. These unknowns can later be instantiated explicitly or implicitly:
By hand, using of . The expression conjI [of "a=b" "False"] instantiates
the unknowns in conjI from left to right with the two formulas a=b and
False, yielding the rule
4.4 Single Step Proofs 43
a =b False
a = b ∧ False
In general, th[of string 1 . . . string n ] instantiates the unknowns in the
theorem th from left to right with the terms string 1 to string n .
By unification. Unification is the process of making two terms syntacti-
cally equal by suitable instantiations of unknowns. For example, unifying
?P ∧ ?Q with a = b ∧ False instantiates ?P with a = b and ?Q with
False.
We need not instantiate all unknowns. If we want to skip a particular one we
can write _ instead, for example conjI [of _ "False"]. Unknowns can also be
instantiated by name using where, for example conjI [where ?P = "a=b"
and ?Q = "False"].
?P =⇒ ?Q ?Q =⇒ ?P
iffI
?P = ?Q
These rules are part of the logical system of natural deduction (e.g., [44]).
Although we intentionally de-emphasize the basic rules of logic in favour of
automatic proof methods that allow you to take bigger steps, these rules are
helpful in locating where and why automation fails. When applied backwards,
these rules decompose the goal:
conjI and iffI split the goal into two subgoals,
impI moves the left-hand side of a HOL implication into the list of as-
sumptions,
and allI removes a ∀ by turning the quantified variable into a fixed local
variable of the subgoal.
Isabelle knows about these and a number of other introduction rules. The
command
apply rule
automatically selects the appropriate rule for the current subgoal.
You can also turn your own theorems into introduction rules by giving
them the intro attribute, analogous to the simp attribute. In that case blast,
fastforce and (to a limited extent) auto will automatically backchain with
those theorems. The intro attribute should be used with care because it in-
creases the search space and can lead to nontermination. Sometimes it is better
to use it only in specific calls of blast and friends. For example, le_trans, tran-
sitivity of 6 on type nat, is not an introduction rule by default because of the
disastrous effect on the search space, but can be useful in specific situations:
lemma "[[ (a::nat ) 6 b; b 6 c; c 6 d; d 6 e ]] =⇒ a 6 e"
by(blast intro: le_trans)
Of course this is just an example and could be proved by arith, too.
Forward proof means deriving new theorems from old theorems. We have
already seen a very simple form of forward proof: the of operator for instan-
tiating unknowns in a theorem. The big brother of of is OF for applying
one theorem to others. Given a theorem A =⇒ B called r and a theorem
A 0 called r 0 , the theorem r [OF r 0 ] is the result of applying r to r 0 , where
r should be viewed as a function taking a theorem A and returning B. More
precisely, A and A 0 are unified, thus instantiating the unknowns in B, and
the result is the instantiated B. Of course, unification may also fail.
4.5 Inductive Definitions 45
Application of rules to other rules operates in the forward direction: from the
premises to the conclusion of the rule; application of rules to proof states operates
in the backward direction, from the conclusion to the premises.
Inductive definitions are the third important definition facility, after datatypes
and recursive function. In fact, they are the key construct in the definition of
operational semantics in the second part of the book.
The operative word “inductive” means that these are the only even numbers.
In Isabelle we give the two rules the names ev0 and evSS and write
inductive ev :: "nat ⇒ bool" where
ev0: "ev 0" |
evSS : "ev n =⇒ ev (n + 2)"
To get used to inductive definitions, we will first prove a few properties of ev
informally before we descend to the Isabelle level.
How do we prove that some number is even, e.g., ev 4? Simply by com-
bining the defining rules for ev :
ev 0 =⇒ ev (0 + 2) =⇒ ev ((0 + 2) + 2) = ev 4
Rule Induction
Showing that all even numbers have some property is more complicated. For
example, let us prove that the inductive definition of even numbers agrees
with the following recursive one:
fun evn :: "nat ⇒ bool" where
"evn 0 = True" |
"evn (Suc 0) = False" |
"evn (Suc(Suc n)) = evn n"
We prove ev m =⇒ evn m. That is, we assume ev m and by induction on
the form of its derivation prove evn m. There are two cases corresponding to
the two rules for ev :
Case ev0: ev m was derived by rule ev 0:
=⇒ m = 0 =⇒ evn m = evn 0 = True
Case evSS : ev m was derived by rule ev n =⇒ ev (n + 2):
=⇒ m = n + 2 and by induction hypothesis evn n
=⇒ evn m = evn(n + 2) = evn n = True
What we have just seen is a special case of rule induction. Rule induction
applies to propositions of this form
ev n =⇒ P n
That is, we want to prove a property P n for all even n. But if we assume
ev n, then there must be some derivation of this assumption using the two
defining rules for ev. That is, we must prove
Case ev0: P 0
Case evSS : [[ev n; P n]] =⇒ P (n + 2)
4.5 Inductive Definitions 47
The first premise ev n enforces that this rule can only be applied in situations
where we know that n is even.
Note that in the induction step we may not just assume P n but also
ev n, which is simply the premise of rule evSS. Here is an example where the
local assumption ev n comes in handy: we prove ev m =⇒ ev (m − 2) by
induction on ev m. Case ev0 requires us to prove ev (0 − 2), which follows
from ev 0 because 0 − 2 = 0 on type nat. In case evSS we have m = n + 2
and may assume ev n, which implies ev (m − 2) because m − 2 = (n +
2) − 2 = n. We did not need the induction hypothesis at all for this proof
(it is just a case analysis of which rule was used) but having ev n at our
disposal in case evSS was essential. This case analysis of rules is also called
“rule inversion” and is discussed in more detail in Chapter 5.
In Isabelle
Let us now recast the above informal proofs in Isabelle. For a start, we use
Suc terms instead of numerals in rule evSS :
ev n =⇒ ev (Suc (Suc n))
This avoids the difficulty of unifying n+2 with some numeral, which is not
automatic.
The simplest way to prove ev (Suc (Suc (Suc (Suc 0)))) is in a forward
direction: evSS [OF evSS [OF ev0]] yields the theorem ev (Suc (Suc (Suc
(Suc 0)))). Alternatively, you can also prove it as a lemma in backwards
fashion. Although this is more verbose, it allows us to demonstrate how each
rule application changes the proof state:
lemma "ev (Suc(Suc(Suc(Suc 0))))"
apply(rule evSS )
apply(rule evSS )
1. ev 0
48 4 Logic and Proof Beyond Equality
apply(rule ev0)
done
Rule induction is applied by giving the induction rule explicitly via the
rule: modifier:
lemma "ev m =⇒ evn m"
apply(induction rule: ev .induct )
by(simp_all)
Both cases are automatic. Note that if there are multiple assumptions of the
form ev t, method induction will induct on the leftmost one.
As a bonus, we also prove the remaining direction of the equivalence of ev
and evn:
lemma "evn n =⇒ ev n"
apply(induction n rule: evn.induct )
This is a proof by computation induction on n (see Section 2.3.4) that sets
up three subgoals corresponding to the three equations for evn:
1. evn 0 =⇒ ev 0
2. evn (Suc 0) =⇒ ev (Suc 0)
V
3. n. [[evn n =⇒ ev n; evn (Suc (Suc n))]] =⇒ ev (Suc (Suc n))
The first and third subgoals follow with ev0 and evSS, and the second subgoal
is trivially true because evn (Suc 0) is False:
by (simp_all add: ev0 evSS )
The rules for ev make perfect simplification and introduction rules because
their premises are always smaller than the conclusion. It makes sense to turn
them into simplification and introduction rules permanently, to enhance proof
automation. They are named ev .intros by Isabelle:
declare ev .intros[simp,intro]
The rules of an inductive definition are not simplification rules by default be-
cause, in contrast to recursive functions, there is no termination requirement
for inductive definitions.
Exercises
S → ε | aSb | SS
T → ε | T aT b
as two inductive predicates. If you think of a and b as “(” and “)”, the
grammar defines strings of balanced parentheses. Prove T w =⇒ S w and
S w =⇒ T w separately and conclude S w = T w.
52 4 Logic and Proof Beyond Equality
Exercise 4.7. Consider the stack machine from Chapter 3 and recall the
concept of stack underflow from Exercise 3.10. Define an inductive predicate
ok :: nat ⇒ instr list ⇒ nat ⇒ bool such that ok n is n 0 means that with
any initial stack of length n the instructions is can be executed without stack
underflow and that the final stack has length n 0 . Prove that ok correctly
computes the final stack size
[[ok n is n 0 ; length stk = n]] =⇒ length (exec is s stk ) = n 0
and that instruction sequences generated by comp cannot cause stack under-
flow: ok n (comp a) ? for some suitable value of ?.
5
Isar: A Language for Structured Proofs
A proof can either be an atomic by with a single proof method which must
finish off the statement being proved, for example auto, or it can be a proof–
qed block of multiple steps. Such a block can optionally begin with a proof
method that indicates how to start off the proof, e.g., (induction xs).
A step either assumes a proposition or states a proposition together with
its proof. The optional from clause indicates which facts are to be used in the
proof. Intermediate propositions are stated with have, the overall goal is stated
with show. A step can also introduce new local variables with fix. Logically,
V
fix introduces -quantified variables, assume introduces the assumption of an
implication (=⇒) and have/show introduce the conclusion.
Propositions are optionally named formulas. These names can be referred
to in later from clauses. In the simplest case, a fact is such a name. But facts can
also be composed with OF and of as shown in Section 4.4.4 — hence the . . .
in the above grammar. Note that assumptions, intermediate have statements
and global lemmas all have the same status and are thus collectively referred
to as facts.
Fact names can stand for whole lists of facts. For example, if f is defined by
command fun, f .simps refers to the whole list of recursion equations defining
f. Individual facts can be selected by writing f .simps(2), whole sublists by
writing f .simps(2−4).
meaningful names are hard to invent and are often not necessary. Both have
steps are obvious. The second one introduces the diagonal set {x . x ∈ / f x },
the key idea in the proof. If you wonder why 2 directly implies False: from 2
it follows that (a ∈
/ f a) = (a ∈ f a).
Labels should be avoided. They interrupt the flow of the reader who has to
scan the context for the point where the label was introduced. Ideally, the
proof is a linear flow, where the output of one step becomes the input of
the next step, piping the previously proved fact into the next proof, like in
a UNIX pipe. In such cases the predefined name this can be used to refer to
the proposition proved in the previous step. This allows us to eliminate all
labels from our proof (we suppress the lemma statement):
proof
assume "surj f"
from this have "∃ a. {x . x ∈
/ f x } = f a" by(auto simp: surj_def )
from this show "False" by blast
qed
We have also taken the opportunity to compress the two have steps into one.
To compact the text further, Isar has a few convenient abbreviations:
then = from this
thus = then show
hence = then have
With the help of these abbreviations the proof becomes
proof
assume "surj f"
hence "∃ a. {x . x ∈
/ f x } = f a" by(auto simp: surj_def )
thus "False" by blast
qed
There are two further linguistic variations:
(have|show) prop using facts = from facts (have|show) prop
with facts = from facts this
The using idiom de-emphasizes the used facts by moving them behind the
proposition.
lemma
fixes f :: " 0a ⇒ 0a set"
assumes s: "surj f"
shows "False"
The optional fixes part allows you to state the types of variables up front
rather than by decorating one of their occurrences in the formula with a type
constraint. The key advantage of the structured format is the assumes part that
allows you to name each assumption; multiple assumptions can be separated
by and. The shows part gives the goal. The actual theorem that will come out
of the proof is surj f =⇒ False, but during the proof the assumption surj f
is available under the name s like any other fact.
proof −
have "∃ a. {x . x ∈
/ f x } = f a" using s
by(auto simp: surj_def )
thus "False" by blast
qed
Note the hyphen after the proof command. It is the null method that does
nothing to the goal. Leaving it out would be asking Isabelle to try some suitable
introduction rule on the goal False — but there is no such rule and proof would fail.
In the have step the assumption surj f is now referenced by its name s. The
duplication of surj f in the above proofs (once in the statement of the lemma,
once in its proof) has been eliminated.
Stating a lemma with assumes-shows implicitly introduces the name assms
that stands for the list of all assumptions. You can refer to individual assump-
tions by assms(1), assms(2), etc., thus obviating the need to name them
individually.
In the proof patterns shown above, formulas are often duplicated. This can
make the text harder to read, write and maintain. Pattern matching is an
abbreviation mechanism to avoid such duplication. Writing
show formula (is pattern)
matches the pattern against the formula, thus instantiating the unknowns in
the pattern for later use. As an example, consider the proof pattern for ←→:
show "formula 1 ←→ formula 2 " (is "?L ←→ ?R")
5.3 Streamlining Proofs 59
proof
assume "?L"
..
.
show "?R" hproof i
next
assume "?R"
..
.
show "?L" hproof i
qed
Instead of duplicating formula i in the text, we introduce the two abbrevia-
tions ?L and ?R by pattern matching. Pattern matching works wherever a
formula is stated, in particular with have and lemma.
The unknown ?thesis is implicitly matched against any goal stated by
lemma or show. Here is a typical example:
lemma "formula"
proof −
..
.
show ?thesis hproof i
qed
Unknowns can also be instantiated with let commands
let ?t = "some-big-term"
Later proof steps can refer to ?t :
have ". . . ?t . . . "
Names of facts are introduced with name: and refer to proved theorems. Un-
knowns ?X refer to terms or formulas.
5.3.2 moreover
The moreover version is no shorter but expresses the structure a bit more
clearly and avoids new names.
Sometimes one would like to prove some lemma locally within a proof, a
lemma that shares the current context of assumptions but that has its own
assumptions and is generalized over its locally fixed variables at the end. This
is what a raw proof block does:
{ fix x 1 . . . x n
assume A1 . . . Am
..
.
have B hproof i
}
proves [[ A1 ; . . . ; Am ]] =⇒ B where all x i have been replaced by unknowns
?x i .
The conclusion of a raw proof block is not indicated by show but is simply the
final have.
lemma fixes a b :: int assumes "b dvd (a+b)" shows "b dvd a"
proof −
{ fix k assume k : "a+b = b∗k"
have "∃ k 0 . a = b∗k 0 "
proof
show "a = b∗(k − 1)" using k by(simp add: algebra_simps)
qed }
then show ?thesis using assms by(auto simp add: dvd_def )
qed
Note that the result of a raw proof block has no name. In this example it was
directly piped (via then) into the final proof, but it can also be named for
later reference: you simply follow the block directly by a note command:
note name = this
This introduces a new name name that refers to this, the fact just proved, in
this case the preceding block. In general, note introduces a new name for one
or more facts.
Exercises
next
P
fix n assume " {0..n::nat } = n∗(n+1) div 2"
P
thus " {0..Suc n} = Suc n∗(Suc n+1) div 2" by simp
qed
Except for the rewrite steps, everything is explicitly given. This makes the
proof easily readable, but the duplication means it is tedious to write and
maintain. Here is how pattern matching can completely avoid any duplication:
P
lemma " {0..n::nat } = n∗(n+1) div 2" (is "?P n")
proof (induction n)
show "?P 0" by simp
next
fix n assume "?P n"
thus "?P (Suc n)" by simp
qed
The first line introduces an abbreviation ?P n for the goal. Pattern matching
P
?P n with the goal instantiates ?P to the function λn. {0..n} = n ∗ (n +
1) div 2. Now the proposition to be proved in the base case can be written as
?P 0, the induction hypothesis as ?P n, and the conclusion of the induction
step as ?P (Suc n).
Induction also provides the case idiom that abbreviates the fix-assume step.
The above proof becomes
proof (induction n)
case 0
show ?case by simp
next
case (Suc n)
thus ?case by simp
qed
The unknown ?case is set in each case to the required claim, i.e., ?P 0 and
?P (Suc n) in the above proof, without requiring the user to define a ?P. The
general pattern for induction over nat is shown on the left-hand side:
64 5 Isar: A Language for Structured Proofs
Recall the inductive and recursive definitions of even numbers in Section 4.5:
5.4 Case Analysis and Induction 65
The proof resembles structural induction, but the induction rule is given
explicitly and the names of the cases are the names of the rules in the inductive
definition. Let us examine the two assumptions named evSS : ev n is the
premise of rule evSS, which we may assume because we are in the case where
that rule was used; evn n is the induction hypothesis.
Because each case command introduces a list of assumptions named like the case
name, which is the name of a rule of the inductive definition, those rules now
need to be accessed with a qualified name, here ev .ev0 and ev .evSS.
In the case evSS of the proof above we have pretended that the system
fixes a variable n. But unless the user provides the name n, the system will
just invent its own name that cannot be referred to. In the above proof, we
do not need to refer to it, hence we do not give it a specific name. In case one
needs to refer to it one writes
case (evSS m)
like case (Suc n) in earlier structural inductions. The name m is an arbi-
trary choice. As a result, case evSS is derived from a renamed version of rule
66 5 Isar: A Language for Structured Proofs
In any induction, case name sets up a list of assumptions also called name,
which is subdivided into three parts:
name.IH contains the induction hypotheses.
name.hyps contains all the other hypotheses of this case in the induction
rule. For rule inductions these are the hypotheses of rule name, for struc-
tural inductions these are empty.
name.prems contains the (suitably instantiated) premises of the statement
being proved, i.e., the Ai when proving [[ A1 ; . . .; An ]] =⇒ A.
5.4 Case Analysis and Induction 67
Proof method induct differs from induction only in this naming policy: induct
does not distinguish IH from hyps but subsumes IH under hyps.
More complicated inductive proofs than the ones we have seen so far often
need to refer to specific assumptions — just name or even name.prems and
name.IH can be too unspecific. This is where the indexing of fact lists comes
in handy, e.g., name.IH (2) or name.prems(1−2).
Rule inversion is case analysis of which rule could have been used to de-
rive some fact. The name rule inversion emphasizes that we are reasoning
backwards: by which rules could some given fact have been proved? For the
inductive definition of ev, rule inversion can be summarized like this:
ev n =⇒ n = 0 ∨ (∃ k . n = Suc (Suc k ) ∧ ev k )
The realisation in Isabelle is a case analysis. A simple example is the proof
that ev n =⇒ ev (n − 2). We already went through the details informally
in Section 4.5.1. This is the Isar proof:
assume "ev n"
from this have "ev (n − 2)"
proof cases
case ev0 thus "ev (n − 2)" by (simp add: ev .ev0)
next
case (evSS k ) thus "ev (n − 2)" by (simp add: ev .evSS )
qed
The key point here is that a case analysis over some inductively defined pred-
icate is triggered by piping the given fact (here: from this) into a proof by
cases. Let us examine the assumptions available in each case. In case ev0 we
have n = 0 and in case evSS we have n = Suc (Suc k ) and ev k. In each
case the assumptions are available under the name of the case; there is no
fine-grained naming schema like there is for induction.
Sometimes some rules could not have been used to derive the given fact
because constructors clash. As an extreme example consider rule inversion
applied to ev (Suc 0): neither rule ev0 nor rule evSS can yield ev (Suc 0)
because Suc 0 unifies neither with 0 nor with Suc (Suc n). Impossible cases
do not have to be proved. Hence we can prove anything from ev (Suc 0):
assume "ev (Suc 0)" then have P by cases
That is, ev (Suc 0) is simply not provable:
lemma "¬ ev (Suc 0)"
68 5 Isar: A Language for Structured Proofs
proof
assume "ev (Suc 0)" then show False by cases
qed
Normally not all cases will be impossible. As a simple exercise, prove that
¬ ev (Suc (Suc (Suc 0))).
qed
qed
Remarks:
Instead of the case and ?case magic we have spelled all formulas out. This
is merely for greater clarity.
We only need to deal with one case because the ev0 case is impossible.
The form of the IH shows us that internally the lemma was expanded as
explained above: ev x =⇒ x = Suc m =⇒ ¬ ev m.
The goal ¬ ev (Suc n) may surprise. The expanded version of the lemma
would suggest that we have a fix m assume Suc (Suc n) = Suc m and need
to show ¬ ev m. What happened is that Isabelle immediately simplified
Suc (Suc n) = Suc m to Suc n = m and could then eliminate m. Beware
of such nice surprises with this advanced form of induction.
This advanced form of induction does not support the IH naming schema ex-
plained in Section 5.4.4: the induction hypotheses are instead found under the
name hyps, as they are for the simpler induct method.
Exercises
Exercise 5.4. Give a structured proof of ¬ ev (Suc (Suc (Suc 0))) by rule
inversions. If there are no cases to be proved you can close a proof immediately
with qed.
Exercise 5.5. Recall predicate star from Section 4.5.2 and iter from Exer-
cise 4.4. Prove iter r n x y =⇒ star r x y in a structured style; do not just
sledgehammer each case of the required induction.
Exercise 5.6. Define a recursive function elems :: 0a list ⇒ 0a set and prove
x ∈ elems xs =⇒ ∃ ys zs. xs = ys @ x # zs ∧ x ∈ / elems ys.
Exercise 5.7. Extend Exercise 4.5 with a function that checks if some
alpha list is a balanced string of parentheses. More precisely, define a recursive
function balanced :: nat ⇒ alpha list ⇒ bool such that balanced n w is true
iff (informally) S (a n @ w ). Formally, prove that balanced n w = S (replicate
n a @ w ) where replicate :: nat ⇒ 0a ⇒ 0a list is predefined and replicate
n x yields the list [x , . . ., x ] of length n.
Part II
Semantics
It is all very well to aim for a more “abstract” and a “cleaner”
approach to semantics, but if the plan is to be any good, the
operational aspects cannot be completely ignored.
Welcome to the second part of this book. In the first part you have mastered
the basics of interactive theorem proving and are now able to navigate the
depths of higher-order logic. In this second part, we put these skills to concrete
use in the semantics of programming languages.
Why formal semantics? Because there is no alternative when it comes
to an unambiguous foundation of what is at the heart of computer science:
programs. A formal semantics provides the much needed foundation for their
work not just to programmers who want to reason about their programs but
also to developers of tools (e.g., compilers, refactoring tools and program
analysers) and ultimately to language designers themselves.
This second part of the book is based entirely on a small imperative lan-
guage called IMP. IMP is our vehicle for showing not just how to formally
define the semantics of a programming language but also how to use the
semantics to reason about the language, about the behaviour of programs,
and about program analyses. Specifically, we examine the following topics:
operational semantics, compiler correctness, type systems, program analysis,
denotational semantics, Hoare logic, and abstract interpretation.
IMP is a minimal language, with just enough expressive power to be
Turing-complete. It does not come with the bells and whistles of a real,
mainstream programming language. This is by design: our aim is to show
the essence of the techniques, analyses, and transformations that we study
in this book, not to get bogged down in detail and sheer volume. This does
not mean that formal, machine-checked semantics cannot scale to mainstream
languages such as C or Java or more complex features such as object orienta-
tion. In fact, this is where proof assistants shine: the ability to automatically
check a large amount of error-prone detail relieves the tedium of any sizeable
formalization. At the end of each chapter we give pointers to further reading
and recent successful applications of the techniques we discuss.
74 6 Introduction
Isabelle
Although the reader will get the most out of this second part of the book if she
has studied Isabelle before, it can be read without knowledge of interactive
theorem proving. We describe all proofs in detail, but in contrast to the first
part of the book, we describe them informally. Nevertheless, everything has
been formalized and proved in Isabelle. All these theories can be found in the
directory src/HOL/IMP of the Isabelle distribution. HTML versions are avail-
able online at http://isabelle.in.tum.de/library/HOL/HOL-IMP. Many
section headings have a link to the corresponding Isabelle theory attached
that looks like this: thy . Appendix C contains a table that shows which sec-
tions are based on which theories. When building upon any of those theories,
for example when solving an exercise, the imports section needs to include
"~~/src/HOL/IMP/T" where T is the name of the required theory.
In this second part of the book we simplify the Isabelle syntax in two
minor respects to improve readability:
We no longer enclose types and terms in quotation marks.
We no longer separate clauses in function definitions or inductive defini-
tions with “|”.
Finally, a note on terminology: We call a proof “automatic” if it requires
only a single invocation of a basic Isabelle proof method like simp, auto,
blast or metis, possibly modified with specific lemmas. Inductions are not
automatic, although each case can be.
7
IMP: A Simple Imperative Language
thy
7.1 IMP Commands
Before we jump into any formalization or define the abstract syntax of com-
mands, we need to determine which constructs the language IMP should con-
tain. The basic constraints are given by our aim to formalize the semantics
of an imperative language and to keep things simple. For an imperative lan-
guage, we will want the basics such as assignments, sequential composition
(semicolon), and conditionals (IF ). To make it Turing-complete, we will want
to include WHILE loops. To be able to express other syntactic forms, such
as an IF without an ELSE branch, we also include the SKIP command
that does nothing. The right-hand side of variable assignments will be the
arithmetic expressions already defined in Chapter 3, and the conditions in IF
and WHILE will be the boolean expressions defined in the same chapter. A
program is simply a, possibly complex, command in this language.
We have already seen the formalization of expressions and their semantics
in Chapter 3. The abstract syntax of commands is:
76 7 IMP: A Simple Imperative Language
In the definitions, proofs, and examples further along in this book, we will
often want to refer to concrete program fragments. To make such fragments
more readable, we also introduce concrete infix syntax in Isabelle for the
four compound constructors of the com datatype. The term Assign x a for
instance can be written as x ::= a, the term Seq c 1 c 2 as c 1 ;; c 2 , the term
If b c 1 c 2 as IF b THEN c 1 ELSE c 2 , and the while loop While b c as
WHILE b DO c. Sequential composition is denoted by “;;” to distinguish it
from the “;” that separates assumptions in the [[. . .]] notation. Nevertheless we
still pronounce “;;” as “semicolon”.
Example 7.1. The following is an example IMP program with two assign-
ments.
0 0 00 0 0 00 0 0 00
x ::= Plus (V y ) (N 1);; y ::= N 2
We have not defined its meaning yet, but the intention is that it assigns the
value of variable y incremented by 1 to the variable x, and afterwards sets
y to 2. In a more conventional concrete programming language syntax, we
would have written
x := y + 1; y := 2
We will occasionally use this more compact style for examples in the text,
with the obvious translation into the formal form.
We write concrete variable names as strings enclosed in double quotes, just as in
the arithmetic expressions in Chapter 3. Examples are V 0 0x 0 0 or 0 0x 0 0 ::= exp.
If we write V x instead, x is a logical variable for the name of the program variable.
That is, in x ::= exp, the x stands for any concrete name 0 0x 0 0 , 0 0y 0 0 , and so on, the
same way exp stands for any arithmetic expression.
While more convenient than writing abstract syntax trees, as we have seen
in the example, even the more concrete Isabelle notation above is occasion-
ally somewhat cumbersome to use. This is not a fundamental restriction of
the theorem prover or of mechanised semantics. If one were interested in a
7.2 Big-Step Semantics 77
more traditional concrete syntax for IMP, or if one were to formalize a larger,
more realistic language, one could write separate parsing/printing ML code
that integrates with Isabelle and implements the concrete syntax of the lan-
guage. This is usually only worth the effort when the emphasis is on program
verification as opposed to meta-theorems about the programming language.
A larger language may also contain a so-called syntactic de-sugaring phase,
where more complex constructs in the language are transformed into simple
core concepts. For instance, our IMP language does not have syntax for Java
style for-loops, or repeat . . . until loops. For our purpose of analysing pro-
gramming language semantics in general these concepts add nothing new, but
for a full language formalization they would be required. De-sugaring would
take the for-loop and do . . . while syntax and translate it into the standard
WHILE loops that IMP supports. Therefore definitions and theorems about
the core language only need to worry about one type of loop, while still sup-
porting the full richness of a larger language. This significantly reduces proof
size and effort for the theorems that we discuss in this book.
thy
7.2 Big-Step Semantics
In the previous section we defined the abstract syntax of the IMP language.
In this section we show its semantics. More precisely, we will use a big-step
operational semantics to give meaning to commands.
In an operational semantics setting, the aim is to capture the meaning
of a program as a relation that describes how a program executes. Other styles
of semantics may be concerned with assigning mathematical structures as
meanings to programs, e.g., in the so-called denotational style in Chapter 11,
or they may be interested in capturing the meaning of programs by describing
how to reason about them, e.g., in the axiomatic style in Chapter 12.
7.2.1 Definition
Skip Assign
(SKIP , s) ⇒ s (x ::= a, s) ⇒ s(x := aval a s)
(c 1 , s 1 ) ⇒ s 2 (c 2 , s 2 ) ⇒ s 3
Seq
(c 1 ;; c 2 , s 1 ) ⇒ s 3
bval b s (c 1 , s) ⇒ t
IfTrue
(IF b THEN c 1 ELSE c 2 , s) ⇒ t
¬ bval b s (c 2 , s) ⇒ t
IfFalse
(IF b THEN c 1 ELSE c 2 , s) ⇒ t
¬ bval b s
WhileFalse
(WHILE b DO c, s) ⇒ s
bval b s 1 (c, s 1 ) ⇒ s 2 (WHILE b DO c, s 2 ) ⇒ s 3
WhileTrue
(WHILE b DO c, s 1 ) ⇒ s 3
inductive
big_step :: com × state ⇒ state ⇒ bool (infix ⇒ 55)
where
Skip: (SKIP ,s) ⇒ s |
Assign: (x ::= a,s) ⇒ s(x := aval a s) |
Seq: [[ (c 1 ,s 1 ) ⇒ s 2 ; (c 2 ,s 2 ) ⇒ s 3 ]] =⇒ (c 1 ;;c 2 , s 1 ) ⇒ s 3 |
IfTrue: [[ bval b s; (c 1 ,s) ⇒ t ]] =⇒ (IF b THEN c 1 ELSE c 2 , s) ⇒ t |
IfFalse: [[ ¬bval b s; (c 2 ,s) ⇒ t ]] =⇒ (IF b THEN c 1 ELSE c 2 , s) ⇒ t |
WhileFalse: ¬bval b s =⇒ (WHILE b DO c,s) ⇒ s |
WhileTrue:
[[ bval b s 1 ; (c,s 1 ) ⇒ s 2 ; (WHILE b DO c, s 2 ) ⇒ s 3 ]]
=⇒ (WHILE b DO c, s 1 ) ⇒ s 3
Figure 7.3 shows a so-called derivation tree, i.e., a composition of the rules
from Figure 7.1 that visualizes a big-step execution: we are executing the
80 7 IMP: A Simple Imperative Language
( 0 0x 0 0 ::= N 5, s) ⇒ s( 0 0x 0 0 := 5) ( 0 0y 0 0 ::= V 0 0 00
x , s( 0 0x 0 0 := 5)) ⇒ s 0
( 0 0x 0 0 ::= N 5;; 0 0 00
y ::= V 0 0 00
x , s) ⇒ s 0
where s 0 = s( 0 0x 0 0 := 5, 0 0 00
y := 5)
but this only shows us {_}, i.e., that the result is a set containing one el-
ement. Functions cannot always easily be printed, but lists can be, so we
just ask for the values of a list of variables we are interested in, using the
set-comprehension notation introduced in Section 4.2:
values {map t [ 0 0x 0 0 , y ] |t . ( 0 0x 0 0 ::= N 2, λ_. 0) ⇒ t }
0 0 00
(WHILE b DO c, s) ⇒ t =⇒
¬ bval b s ∧ t = s ∨
bval b s ∧ (∃ s 0 . (c, s) ⇒ s 0 ∧ (WHILE b DO c, s 0 ) ⇒ t )
(c 1 ;; c 2 , s 1 ) ⇒ s 3 ←→ (∃ s 2 . (c 1 , s 1 ) ⇒ s 2 ∧ (c 2 , s 2 ) ⇒ s 3 )
Every =⇒ in the inverted rules can be turned into ←→ because the ⇐=
direction follows from the original rules.
As an example of the two proof techniques in this and the previous section
consider the following lemma. It states that the syntactic associativity of
semicolon has no semantic effect. We get the same result, no matter if we
group semicolons to the left or to the right.
Lemma 7.2. (c 1 ;; c 2 ;; c 3 , s) ⇒ s 0 ←→ (c 1 ;; (c 2 ;; c 3 ), s) ⇒ s 0
Proof. We show each direction separately. Consider first the execution where
the semicolons are grouped to the left: ((c 1 ;; c 2 );; c 3 , s) ⇒ s 0 . By rule
inversion we can decompose this execution twice and obtain the intermediate
states s 1 and s 2 such that (c 1 , s) ⇒ s 1 , as well as (c 2 , s 1 ) ⇒ s 2 and (c 3 ,
s 2 ) ⇒ s 0 . From this, we can construct a derivation for (c 1 ;; (c 2 ;; c 3 ), s) ⇒
s 0 by first concluding (c 2 ;; c 3 , s 1 ) ⇒ s 0 with the Seq rule and then using
the Seq rule again, this time on c 1 , to arrive at the final result. The other
direction is analogous. t
u
In the previous section we have applied rule inversion and introduction rules
of the big-step semantics to show equivalence between two particular IMP
commands. In this section, we define semantic equivalence as a concept in its
own right.
We call two commands c and c 0 equivalent w.r.t. the big-step semantics
when c started in s terminates in t iff c 0 started in s also terminates in
t. Formally, we define it as an abbreviation:
abbreviation
equiv_c :: com ⇒ com ⇒ bool (infix ∼ 50) where
c ∼ c 0 ≡ (∀ s t . (c,s) ⇒ t = (c 0 ,s) ⇒ t )
Note that the ∼ symbol in this definition is not the standard tilde ∼ , but the
symbol \<sim> instead.
Proof. The proof is by induction on the big-step semantics. With our inver-
sion and introduction rules from above, each case is solved automatically by
Isabelle. Note that the automation in this proof is not completely obvious.
Merely using the proof method auto after the induction for instance leads
to non-termination, but the backtracking capabilities of blast manage to solve
each subgoal. Experimenting with different automated methods is encouraged
if the standard ones fail. t
u
While the above proof is nice for showing off Isabelle’s proof automation,
it does not give much insight into why the property is true. Figure 7.4 shows
an Isar proof that expands the steps of the only interesting case and omits
the boring cases using automation. This is much closer to a blackboard pre-
sentation.
So far, we have defined the big-step semantics of IMP, we have explored the
proof principles of derivation trees, rule inversion, and rule induction in the
context of the big-step semantics, and we have explored semantic equivalence
as well as determinism of the language. In the next section we will look at a
different way of defining the semantics of IMP.
7.3 Small-Step Semantics 85
theorem
(c,s) ⇒ t =⇒ (c,s) ⇒ t 0 =⇒ t 0 = t
proof (induction arbitrary: t 0 rule: big_step.induct )
— the only interesting case, WhileTrue:
fix b c s s 1 t t 0
— The assumptions of the rule:
assume bval b s and (c,s) ⇒ s 1 and (WHILE b DO c,s 1 ) ⇒ t
V
— Ind.Hyp; note the because of arbitrary:
assume IHc: t . (c,s) ⇒ t 0 =⇒ t 0 = s 1
V 0
— Premise of implication:
assume (WHILE b DO c,s) ⇒ t 0
with ‘bval b s‘ obtain s 10 where
c: (c,s) ⇒ s 10 and
w : (WHILE b DO c,s 10 ) ⇒ t 0
by auto
from c IHc have s 10 = s 1 by blast
with w IHw show t 0 = t by blast
qed blast + — prove the rest automatically
thy
7.3 Small-Step Semantics
The big-step semantics executes a program from an initial to the final state in
one big step. Short of inspecting the derivation tree of big-step introduction
rules, it does not allow us to explicitly observe intermediate execution states.
That is the purpose of a small-step semantics.
Small-step semantics lets us explicitly observe partial executions and
make formal statements about them. This enables us, for instance, to talk
about the interleaved, concurrent execution of multiple programs. The main
idea for representing a partial execution is to introduce the concept of how
far execution has progressed in the program. There are many ways of doing
this. Traditionally, for a high-level language like IMP, we modify the type of
the big-step judgement from com × state ⇒ state ⇒ bool to something
like com × state ⇒ com × state ⇒ bool. The second com × state com-
ponent of the judgement is the result state of one small, atomic execution
step together with a modified command that represents what still has to be
executed. We call a com × state pair a configuration of the program, and
use the command SKIP to indicate that execution has terminated.
The idea is easiest to understand by looking at the set of rules. They define
one atomic execution step. The execution of a command is then a sequence
of such steps.
86 7 IMP: A Simple Imperative Language
Assign
(x ::= a, s) → (SKIP , s(x := aval a s))
(c 1 , s) → (c 10 , s 0 )
Seq1 Seq2
(SKIP ;; c 2 , s) → (c 2 , s) (c 1 ;; c 2 , s) → (c 10 ;; c 2 , s 0 )
bval b s
IfTrue
(IF b THEN c 1 ELSE c 2 , s) → (c 1 , s)
¬ bval b s
IfFalse
(IF b THEN c 1 ELSE c 2 , s) → (c 2 , s)
While
(WHILE b DO c, s) → (IF b THEN c;; WHILE b DO c ELSE SKIP, s)
Proof. After induction on the first premise (the small-step semantics), the
proof is as automatic as for the big-step semantics. t
u
Recall that both sides of the small-step arrow → are configurations, that is, pairs
of commands and states. If we don’t need to refer to the individual components,
we refer to the configuration as a whole, such as cs in the lemma above.
We could conduct further tests like this, but since we already have a
semantics for IMP, we can use it to show that our new semantics defines
precisely the same behaviour. The next section does this.
Having defined an alternative semantics for the same language, the first in-
teresting question is of course if our definitions are equivalent. This section
88 7 IMP: A Simple Imperative Language
shows that this is the case. Both directions are proved separately: for any
big-step execution, there is an equivalent small-step execution and vice versa.
We start by showing that any big-step execution can be simulated by a
sequence of small steps ending in SKIP :
Lemma 7.12. cs ⇒ t =⇒ cs →∗ (SKIP , t )
This is proved in the canonical fashion by rule induction on the big-step
judgement. Most cases follow directly. As an example we look at rule IfTrue:
bval b s (c 1 , s) ⇒ t
(IF b THEN c 1 ELSE c 2 , s) ⇒ t
By IH we know that (c 1 , s) →∗ (SKIP , t ). This yields the required small-step
derivation:
bval b s
(IF b THEN c 1 ELSE c 2 , s) → (c 1 , s) (c 1 , s) →∗ (SKIP , t )
(IF b THEN c 1 ELSE c 2 , s) →∗ (SKIP , t )
Only rule Seq does not go through directly:
(c 1 , s 1 ) ⇒ s 2 (c 2 , s 2 ) ⇒ s 3
(c 1 ;; c 2 , s 1 ) ⇒ s 3
The IHs are (c 1 , s 1 ) →∗ (SKIP , s 2 ) and (c 2 , s 2 ) →∗ (SKIP , s 3 ) but we
need a reduction (c 1 ;;c 2 , s 1 ) →∗ ... . The following lemma bridges the gap:
it lifts a →∗ derivation into the context of a semicolon:
Lemma 7.13.
(c 1 , s 1 ) →∗ (c, s 2 ) =⇒ (c 1 ;; c 2 , s 1 ) →∗ (c;; c 2 , s 2 )
Proof. The proof is by induction on the reflexive transitive closure star. The
base case is trivial and the step is not much harder: If (c 1 , s 1 ) → (c 10 , s 10 )
and (c 10 , s 10 ) →∗ (c, s 2 ), we have (c 10 ;; c 2 , s 10 ) →∗ (c;; c 2 , s 2 ) by IH. Rule
Seq2 and the step rule for star do the rest:
(c 1 , s 1 ) → (c 10 , s 10 )
(c 1 ;; c 2 , s 1 ) → (c 10 ;; c 2 , s 10 ) (c 10 ;; c 2 , s 10 ) →∗ (c;; c 2 , s 2 )
(c 1 ;; c 2 , s 1 ) →∗ (c;; c 2 , s 2 )
t
u
Returning to the proof of the Seq case, Lemma 7.13 turns the first IH into
(c 1 ;; c 2 , s 1 ) →∗ (SKIP ;; c 2 , s 2 ). From rule Seq1 and the second IH we have
(SKIP ;; c 2 , s 2 ) →∗ (SKIP , s 3 ):
(SKIP ;; c 2 , s 2 ) → (c 2 , s 2 ) (c 2 , s 2 ) →∗ (SKIP , s 3 )
(SKIP ;; c 2 , s 2 ) →∗ (SKIP , s 3 )
7.3 Small-Step Semantics 89
Proof. The proof is automatic after rule induction on the small-step seman-
tics. t
u
This concludes the proof of Lemma 7.14. Both directions together (Lemma 7.12
and Lemma 7.14) let us derive the equivalence we were aiming for in the first
place:
This concludes our proof that the small-step and big-step semantics of IMP
are equivalent. Such equivalence proofs are useful whenever there are different
formal descriptions of the same artefact. The reason one might want different
descriptions of the same thing is that they differ in what they can be used
for. For instance, big-step semantics are relatively intuitive to define, while
small-step semantics allow us to make more fine-grained formal observations.
The next section exploits the fine-grained nature of the small-step semantics
to elucidate the big-step semantics.
Proof. One direction is easy: clearly, if the command c is SKIP, the configura-
tion is final. The other direction is not much harder. It is proved automatically
after inducting on c. t
u
With this we can show that ⇒ yields a final state iff → terminates:
Lemma 7.18. (∃ t . cs ⇒ t ) ←→ (∃ cs 0 . cs →∗ cs 0 ∧ final cs 0 )
Proof. Using Lemma 7.17 we can replace final with configurations that have
SKIP as the command. The rest follows from the equivalence of small and
big-step semantics. t
u
This lemma says that in IMP the absence of a big-step result is equivalent to
non-termination. This is not necessarily the case for any language. Another
reason for the absence of a big-step result may be a runtime error in the
execution of the program that leads to no rule being applicable. In the big-
step semantics this is often indistinguishable from non-termination. In the
small-step semantics the concept of final configurations neatly distinguishes
the two causes.
Since IMP is deterministic, there is no difference between “may” and “must”
termination. Consider a language with non-determinism.
In such a language, Lemma 7.18 is still valid and both sides speak about
possible (may) termination. In fact, the big-step semantics cannot speak
about necessary (must ) termination at all, whereas the small-step semantics
can: there must not be an infinite reduction cs 0 → cs 1 → . . ..
This concludes the chapter on the operational semantics for IMP. In the first
part of this chapter, we have defined the abstract syntax of IMP commands,
we have defined the semantics of IMP in terms of a big-step operational se-
mantics, and we have experimented with the concepts of semantic equivalence
and determinism. In the second part of this chapter, we have defined an al-
ternative form of operational semantics, namely small-step semantics, and
we have proved that this alternative form describes the same behaviours as
the big-step semantics. The two forms of semantics have different application
trade-offs: big-step semantics were easier to define and understand, small-
step semantics let us talk explicitly about intermediate states of execution
and about termination.
We have looked at three main proof techniques: derivation trees, rule in-
version and rule induction. These three techniques form the basic tool set
that will accompany us in the following chapters.
7.4 Summary and Further Reading 91
Syntax. For loops, do . . . while loops, the if . . . then command, etc. are just
further syntactic forms of the basic commands above. They could either
be formalized directly, or they could be transformed into equivalent basic
forms by syntactic de-sugaring.
Jumps. The goto construct, although considered harmful [27], is relatively
easy to formalize. It merely requires the introduction of some notion of
program position, be it as an explicit program counter, or a set of la-
bels for jump targets. We will see jumps as part of a machine language
formalization in Chapter 8.
Blocks and local variables. Like the other constructs they do not add
computational power but are an important tool for programmers to
achieve data hiding and encapsulation. The main formalization challenge
with local variables is their visibility scope. Nielson and Nielson [62] give
a good introduction to this topic.
Procedures. Parameters to procedures introduce issues similar to local vari-
ables, but they may have additional complexities depending on which call-
ing conventions the language implements (call by reference, call by value,
call by name, etc.). Recursion is not usually a problem to formalize. Pro-
cedures also influence the definition of what a program is: instead of a
single command, a program now usually becomes a list or collection of
procedures. Nielson and Nielson cover this topic as well [62].
Exceptions. Throwing and catching exceptions is usually reasonably easy to
integrate into a language formalization. However, exceptions may inter-
act with features like procedures and local variables, because exceptions
provide new ways to exit scopes and procedures. The Jinja [48] language
formalization is an example of such rich interactions.
Data types, structs, pointers, arrays. Additional types such as fixed size
machine words, records, and arrays are easy to include when the corre-
sponding high-level concept is available in the theorem prover, but IEEE
floating point values for instance may induce an interesting amount of
work [24]. The semantics of pointers and references is a largely orthogo-
nal issue and can be treated at various levels of detail, from raw bytes [87]
up to multiple heaps separated by type and field names [13].
Objects, classes, methods. Object-oriented features have been the target
of a large body of work in the past decade. Objects and methods lead
92 7 IMP: A Simple Imperative Language
All of the features above can be formalized in a theorem prover. Many add
interesting complications, but the song remains the same. Schirmer [78], for
instance, shows an Isabelle formalization of big-step and small-step semantics
in the style of this chapter for the generic imperative language Simpl. The
formalization includes procedures, blocks, exceptions, and further advanced
concepts. With techniques similar to those described Chapter 12, he develops
the language to a point where it can directly be used for large-scale program
verification.
Exercises
Exercise 7.1. Define a function assigned :: com ⇒ vname set that com-
putes the set of variables that are assigned to in a command. Prove that if
some variable is not assigned to in a command, then that variable is never
modified by the command: [[(c, s) ⇒ t ; x ∈ / assigned c]] =⇒ s x = t x.
Exercise 7.2. Define a recursive function skip :: com ⇒ bool that determines
if a command behaves like SKIP. Prove skip c =⇒ c ∼ SKIP.
Exercise 7.3. Define a recursive function deskip :: com ⇒ com that elimi-
nates as many SKIP s as possible from a command. For example:
deskip (SKIP ;; WHILE b DO (x ::= a;; SKIP )) = WHILE b DO x ::= a
Prove deskip c ∼ c by induction on c. Remember Lemma 7.5 for the WHILE
case.
[split_format (complete)]. Do not use the case idiom but write down explic-
itly what you assume and show in each case: fix . . . assume . . . show . . . .
For the following exercises copy theories Com, Big_Step and Small_Step
and modify them as required.
Exercise 7.10. Extend IMP with exceptions. Add two constructors THROW
and TRY c 1 CATCH c 2 to datatype com. Command THROW throws
94 7 IMP: A Simple Imperative Language
thy
8.1 Instructions and Stack Machine
We are now ready to define the machine itself. To keep things simple,
we directly reuse the concepts of values and variable names from the source
language. In a more realistic setting, we would explicitly map variable names
to memory locations, instead of using strings as addresses. We skip this step
here for clarity, adding it does not pose any fundamental difficulties.
The instructions in our machine are the following. The first three are
familiar from Section 3.3:
datatype instr =
LOADI int | LOAD vname | ADD | STORE vname |
JMP int | JMPLESS int | JMPGE int
The instruction LOADI loads an immediate value onto the stack, LOAD
loads the value of a variable, ADD adds the two topmost stack values,
STORE stores the top of stack into memory, JMP jumps by a relative value,
JMPLESS compares the two topmost stack elements and jumps if the sec-
ond one is less, and finally JMPGE compares and jumps if the second one is
greater or equal.
These few instructions are enough to compile IMP programs. A real ma-
chine would have significantly more arithmetic and comparison operators,
different addressing modes that are useful for implementing procedure stacks
and pointers, potentially a number of primitive data types that the machine
understands, and a number of instructions to deal with hardware features such
as the memory management subsystem that we ignore in this formalization.
As in the source language, we proceed by defining the state such programs
operate on, followed by the definition of the semantics itself.
8.1 Instructions and Stack Machine 97
The next level up, a single execution step selects the instruction the pro-
gram counter (pc) points to and uses iexec to execute it. For execution to
be well defined, we additionally check if the pc points to a valid location in
the list. We call this predicate exec1 and give it the notation P ` c → c 0 for
program P executes from configuration c to configuration c 0 .
definition exec1 :: instr list ⇒ config ⇒ config ⇒ bool where
P ` c → c0 =
(∃ i s stk . c = (i , s, stk ) ∧ c 0 = iexec (P !! i ) c ∧ 0 6 i < size P )
where x 6 y < z ≡ x 6 y ∧ y < z as usual in mathematics.
The last level is the lifting from single step execution to multiple steps
using the standard reflexive transitive closure definition that we already used
for the small-step semantics of the source language, that is:
98 8 Compiler
thy
8.2 Reasoning About Machine Executions
The compiler proof is more involved than the short proofs we have seen so
far. We will need a small number of technical lemmas before we get to the
compiler correctness problem itself. Our aim in this section is to execute
machine programs symbolically as far as possible using Isabelle’s proof tools.
We will then use this ability in the compiler proof to assemble multiple smaller
machine executions into larger ones.
A first lemma to this end is that execution results are preserved if we
append additional code to the left or right of a program. Appending at the
right side is easy:
Lemma 8.2. P ` c →∗ c 0 =⇒ P @ P 0 ` c →∗ c 0
Proof. The proof is by induction on the reflexive transitive closure. For the
step case, we observe after unfolding of exec1 that appending program context
on the right does not change the result of indexing into the original instruction
list. t
u
Appending code on the left side requires shifting the program counter.
Lemma 8.3.
P ` (i , s, stk ) →∗ (i 0 , s 0 , stk 0 ) =⇒
P 0 @ P ` (size P 0 + i , s, stk ) →∗ (size P 0 + i 0 , s 0 , stk 0 )
Proof. The proof is again by induction on the reflexive transitive closure and
reduction to exec1 in the step case. To show the lemma for exec1, we unfold
its definition and observe Lemma 8.4. t
u
Proof. We observe by case distinction on the instruction x that the only com-
ponent of the result configuration that is influenced by the program counter
i is the first one, and in this component only additively. For instance, in the
LOADI n instruction, we get on both sides s 0 = s and stk 0 = n # stk. The
pc field for input i on the right-hand side is i 0 = i + 1. The pc field for n +
i on the left-hand side becomes n + i + 1, which is n + i 0 as required. The
other cases are analogous. t
u
Taking these two lemmas together, we can compose separate machine ex-
ecutions into one larger one.
Lemma 8.5 (Composing machine executions).
[[P ` (0, s, stk ) →∗ (i 0 , s 0 , stk 0 ); size P 6 i 0 ;
P 0 ` (i 0 − size P , s 0 , stk 0 ) →∗ (i 0 0 , s 0 0 , stk 0 0 )]]
=⇒ P @ P 0 ` (0, s, stk ) →∗ (size P + i 0 0 , s 0 0 , stk 0 0 )
Proof. The proof is by suitably instantiating Lemma 8.2 and Lemma 8.3. t
u
thy
8.3 Compilation
We are now ready to define the compiler, and will do so in the three usual
steps: first for arithmetic expressions, then for boolean expressions, and finally
for commands. We have already seen compilation of arithmetic expressions in
Section 3.3. We define the same function for our extended machine language:
fun acomp :: aexp ⇒ instr list where
acomp (N n) = [LOADI n]
acomp (V x ) = [LOAD x ]
acomp (Plus a 1 a 2 ) = acomp a 1 @ acomp a 2 @ [ADD]
The correctness statement is not as easy any more as in Section 3.3, because
program execution now is a relation, not simply a function. For our extended
machine language a function is not suitable because we now have potentially
non-terminating executions. This is not a big obstacle: we can still express
naturally that the execution of a compiled arithmetic expression will leave
the result on top of the stack, and that the program counter will point to the
end of the compiled expression.
Lemma 8.6 (Correctness of acomp).
acomp a ` (0, s, stk ) →∗ (size (acomp a), s, aval a s # stk )
?
code for b code for c
6
The correctness statement is the following: the stack and state should
remain unchanged and the program counter should indicate if the expression
evaluated to True or False. If f = False then we end at size (bcomp b f
n) in the True case and size (bcomp b f n) + n in the False case. If f =
True it is the other way around. This statement only makes sense for forward
jumps, so we require n to be non-negative.
Proof. The proof is by induction on b. The constant and Less cases are solved
automatically. For the Not case, we instantiate the induction hypothesis man-
ually to ¬ f. For And, we get two recursive cases. The first needs the induction
hypothesis instantiated with size (bcomp b 2 f n) + (if f then 0 else n) for
n, and with False for f, and the second goes through with simply n and f. t u
With both expression compilers in place, we can now proceed to the com-
mand compiler ccomp. The idea is to compile c into a program that will
perform the same state transformation as c and that will always end with the
program counter at size (ccomp c). It may push and consume intermediate
values on the stack for expressions, but at the end, the stack will be the same
as in the beginning. Because of this modular behaviour of the compiled code,
the compiler can be defined recursively as follows:
SKIP compiles to the empty list;
for assignment, we compile the expression and store the result;
sequential composition appends the corresponding machine programs;
IF is compiled as shown in Figure 8.2;
WHILE is compiled as shown in Figure 8.1.
Figure 8.3 shows the formal definition.
Since everything in ccomp is executable, we can inspect the results of
compiler runs directly in the theorem prover. For example, let p 1 be the
command for IF u < 1 THEN u := u + 1 ELSE v := u. Then
value ccomp p 1
102 8 Compiler
?
code for b code for c 1 code for c 2
6
results in
[LOAD 0 0u 0 0 , LOADI 1, JMPGE 5, LOAD 0 0 00
u , LOADI 1, ADD,
STORE 0 0u 0 0 , JMP 2, LOAD 0 0u 0 0 , STORE 0 0 00
v ]
Similarly for loops. Let p 2 be WHILE u < 1 DO u := u + 1. Then
value ccomp p 2
results in
[LOAD 0 0u 0 0 , LOADI 1, JMPGE 5, LOAD 0 0 00
u , LOADI 1, ADD,
STORE 0 0u 0 0 , JMP (− 8)]
thy
8.4 Preservation of Semantics
This section shows the correctness proof of our small toy compiler. For IMP,
the correctness statement is fairly straightforward: the machine program
should have precisely the same behaviour as the source program. It should
cause the same state change and nothing more than that. It should terminate
if and only if the source program terminates. Since we use the same type for
states at the source level and machine level, the first part of the property is
easy to express. Similarly, it is easy to say that the stack should not change.
8.4 Preservation of Semantics 103
Finally, we can express correct termination by saying that the machine exe-
cution started at pc = 0 should stop at the end of the machine code with pc
= size (ccomp c). In total, we have
(c, s) ⇒ t ←→ ccomp c ` (0, s, stk ) →∗ (size (ccomp c), t , stk )
In other words, the compiled code executes from s to t if and only if the
big-step semantics executes the source code from s to t.
c c
s t s t
ccomp c ccomp c
s0 ∗ t0 s0 ∗ t0
The two directions of the “←→” are shown diagrammatically in Figure 8.4.
The upper level is the big-step execution (s, c) ⇒ t. The lower level is
stack machine execution. The relationship between states and configurations
is given by s 0 = (0, s, stk ) and t 0 = (size (ccomp c), t , stk ).
Such diagrams should be read as follows: the solid lines and arrows imply
the existence of the dashed ones. Thus the left diagram depicts the “−→” di-
rection (source code can be simulated by compiled code) and the right diagram
depicts the “←−” direction (compiled code can be simulated by source code).
We have already seen the analogous simulation relations between big-step and
small-step semantics in Section 7.3.1 and will see more such simulations later
in the book.
In the case of the compiler, the crucial direction is the simulation of the
compiled code by the source code: it tells us that every final state produced
by the compiled code is justified by the source code semantics. Therefore we
can trust all results produced by the compiled code, if it terminates. But it is
still possible that the compiled code does not terminate although the source
code does. That can be ruled out by proving also that the compiled code
simulates the source code.
We will now prove the two directions of our compiler correctness statement
separately. Simulation of the source code by the compiled code is compara-
tively easy.
Lemma 8.9 (Correctness of ccomp).
If the source program executes from s to t, so will the compiled program.
Formally:
104 8 Compiler
The main reason this direction is harder to show than the other one is the
lack of a suitable structural induction principle that we could apply. Since
rule induction on the semantics is not applicable, we have only two further
induction principles left in the arsenal we have learned so far: structural in-
duction on the command c or induction on the length of the →∗ execution.
Neither is strong enough on its own. The solution is to combine them: we will
use an outside structural induction on c which will take care of all cases but
WHILE, and then a nested, inner induction on the length of the execution
for the WHILE case.
This idea takes care of the general proof structure. The second problem we
encounter is the one of decomposing larger machine executions into smaller
ones. Consider the semicolon case. We will have an execution of the form
ccomp c 1 @ ccomp c 2 ` cfg →∗ cfg 0 , and our first induction hypothesis will
come with the precondition ccomp c 1 ` cfg →∗ cfg 0 0 . It may seem intuitive
that if cs 1 @ cs 2 ` cfg →∗ cfg 0 then there must be some intermediate
executions cs 1 ` cfg →∗ cfg 0 0 and cs 2 ` cfg 0 0 →∗ cfg, but this is not
true in general for arbitrary code sequences cs 1 and cs 2 or configurations
cfg and cfg 0 . For instance, the code may be jumping between cs 1 and cs 2
continuously, and neither execution may make sense in isolation.
However, the code produced from our compiler is particularly well be-
haved: execution of compiled code will never jump outside that code and
will exit at precisely pc = size (ccomp c). We merely need to formalize this
concept and prove that it is adhered to. This requires a few auxiliary notions.
First we define isuccs, the possible successor program counters of a given
instruction at position n:
definition isuccs :: instr ⇒ int ⇒ int set where
isuccs i n = (case i of
JMP j ⇒ {n + 1 + j } |
JMPLESS j ⇒ {n + 1 + j , n + 1} |
JMPGE j ⇒ {n + 1 + j , n + 1} |
_ ⇒ {n +1})
Proof. The proof is by induction on the instruction list cs. To solve each case,
we derive the equations for Nil and Cons in succs separately:
succs [] n = {}
succs (x # xs) n = isuccs x n ∪ succs xs (1 + n)
t
u
We could prove a similar lemma about exits (cs @ cs 0 ), but this lemma
would have a more complex right-hand side. For the results below it is easier
to reason about succs first and then apply the definition of exits to the result
instead of decomposing exits directly.
Before we proceed to reason about decomposing machine executions and
compiled code, we note that instead of using the reflexive transitive closure
of single-step execution, we can equivalently talk about n steps of execution.
This will give us a more flexible induction principle and allow us to talk
more precisely about sequences of execution steps. We write P ` c →^n c 0
to mean that the execution of program P starting in configuration c can reach
configuration c 0 in n steps and define:
P ` c →^0 c 0 = (c 0 = c)
P ` c →^(Suc n) c 0 0 = (∃ c 0 . P ` c → c 0 ∧ P ` c 0 →^n c 0 0 )
Our old concept of P ` c →∗ c 0 is equivalent to saying that there exists an
n such that P ` c →^n c 0 .
Lemma 8.11. (P ` c →∗ c 0 ) = (∃ n. P ` c →^n c 0 )
Proof. The proof is by first computing all successors of acomp, and then
deriving the exits from that result. For the successors of acomp, we show
succs (acomp a) n = {n + 1..n + size (acomp a)}
by induction on a. The set from n + 1 to n + size (acomp a) is not empty,
because we can show 1 6 size (acomp a) by induction on a. t
u
Compilation for boolean expressions is less well behaved. The main idea is
that bcomp has two possible exits, one for True, one for False. However, as we
have seen in the examples, compiling a boolean expression might lead to the
empty list of instructions, which has no successors or exits. More generally,
the optimization that bcomp performs may statically exclude one or both of
the possible exits. Instead of trying to precisely describe each of these cases,
we settle for providing an upper bound on the possible successors of bcomp.
We are also only interested in positive offsets i.
Lemma 8.13. If 0 6 i, then
exits (bcomp b f i ) ⊆ {size (bcomp b f i ), i + size (bcomp b f i )}
Finally, we come to the exits of ccomp. Since we are building on the lemma
for bcomp, we can again only give an upper bound: there are either no exits,
or the exit is precisely size (ccomp c). As an example that the former case
can occur as a result of ccomp, consider the compilation of an endless loop
ccomp (WHILE Bc True DO SKIP ) = [JMP (− 1)]
108 8 Compiler
That is, we get one jump instruction that jumps to itself and
exits [JMP (− 1)] = {}
We have derived these exits results about acomp, bcomp and ccomp be-
cause we wanted to show that the machine code produced by these functions
is well behaved enough that larger executions can be decomposed into smaller
separate parts. The main lemma that describes this decomposition is some-
what technical. It states that, given an n-step execution of machine instruc-
tions cs that are embedded in a larger program (P @ cs @ P 0 ), we can find
a k -step sub-execution of cs in isolation, such that this sub-execution ends at
one of the exit program counters of cs, and such that it can be continued to
end in the same state as the original execution of the larger program. For this
to be true, the initial pc of the original execution must point to somewhere
within cs, and the final pc 0 to somewhere outside cs. Formally, we get the
following.
Lemma 8.15 (Decomposition of machine executions).
[[P @ cs @ P 0 ` (size P + pc, stk , s) →^n (pc 0 , stk 0 , s 0 );
pc ∈ {0..<size cs}; pc 0 ∈
/ {size P ..<size P + size cs}]]
00 00 00
=⇒ ∃ pc stk s k m.
cs ` (pc, stk , s) →^k (pc 0 0 , stk 0 0 , s 0 0 ) ∧
pc 0 0 ∈ exits cs ∧
P @ cs @ P 0 ` (size P + pc 0 0 , stk 0 0 , s 0 0 ) →^m (pc 0 , stk 0 , s 0 ) ∧
n =k +m
Proof. The proof is by induction on n. The base case is trivial, and the step
case essentially reduces the lemma to a similar property for a single execution
step:
[[P @ cs @ P 0 ` (size P + pc, stk , s) → (pc 0 , stk 0 , s 0 );
pc ∈ {0..<size cs}]]
=⇒ cs ` (pc, stk , s) → (pc 0 − size P , stk 0 , s 0 )
This property is proved automatically after unfolding the definition of single-
step execution and case distinction on the instruction to be executed.
8.4 Preservation of Semantics 109
Considering the step case for n + 1 execution steps in our induction again,
we note that this step case consists of one single step in the larger context
and the n-step rest of the execution, also in the larger context. Additionally
we have the induction hypothesis, which gives us our property for executions
of length n.
Using the property above, we can reduce the single-step execution to cs.
To connect this up with the rest of the execution, we make use of the induction
hypothesis in the case where the execution of cs is not at an exit yet, which
means the pc is still inside cs, or we observe that the execution has left cs
and the pc must therefore be at an exit of cs. In this case k is 1 and m = n,
and we can just append the rest of the execution from our assumptions. t u
The accompanying Isabelle theories contain a number of more convenient
instantiations of this lemma. We omit these here, and directly move on to
proving the correctness of acomp, bcomp, ccomp.
As always, arithmetic expressions are the least complex case.
Lemma 8.16 (Correctness of acomp, reverse direction).
acomp a ` (0, s, stk ) →^n (size (acomp a), s 0 , stk 0 ) =⇒
s 0 = s ∧ stk 0 = aval a s # stk
Proof. The proof is by induction on the expression, and most cases are solved
automatically, symbolically executing the compilation and resulting machine
code sequence. In the Plus case, we decompose the execution manually using
Lemma 8.15, apply the induction hypothesis for the parts, and combine the
results symbolically executing the ADD instruction. t
u
The next step is the compilation of boolean expressions. Correctness here
is mostly about the pc 0 at the end of the expression evaluation. Stack and
state remain unchanged.
Lemma 8.17 (Correctness of bcomp, reverse direction).
[[bcomp b f j ` (0, s, stk ) →^n (pc 0 , s 0 , stk 0 );
size (bcomp b f j ) 6 pc 0 ; 0 6 j ]]
=⇒ pc 0 = size (bcomp b f j ) + (if f = bval b s then j else 0) ∧
s 0 = s ∧ stk 0 = stk
Proof. The proof is by induction on the expression. The And case is the only
interesting one. We first split the execution into one for the left operand b 1
and one for the right operand b 2 with a suitable instantiation of our splitting
lemma above (Lemma 8.15). We then determine by induction hypothesis that
stack and state did not change for b 1 and that the program counter will either
exit directly, in which case we are done, or it will point to the instruction
sequence of b 2 , in which case we apply the second induction hypothesis to
conclude the case and the lemma. t
u
110 8 Compiler
Proof. Follows directly from Lemma 8.18, Lemma 8.9, and Lemma 8.11. t
u
112 8 Compiler
Exercises
For the following exercises copy and adjust theory Compiler. Intrepid readers
only should attempt to adjust theory Compiler2 too.
Exercise 8.1. Modify the definition of ccomp such that it generates fewer
instructions for commands of the form IF b THEN c ELSE SKIP. Adjust
the proof of Lemma 8.9 if needed.
Exercise 8.2. Building on Exercise 7.8, extend the compiler ccomp and its
correctness theorem ccomp_bigstep to REPEAT loops. Hint: the recursion
pattern of the big-step semantics and the compiler for REPEAT should
match.
Exercise 8.3. Modify the machine language such that instead of variable
names to values, the machine state maps addresses (integers) to values. Adjust
the compiler and its proof accordingly.
In the simple version of this exercise, assume the existence of a globally
bijective function addr_of :: vname ⇒ int with bij addr_of to adjust the
compiler. Use the search facility of the interface to find applicable theorems
for bijective functions.
For the more advanced version and a slightly larger project, assume that
the function works only on a finite set of variables: those that occur in the
program. For the other, unused variables, it should return a suitable default
address. In this version, you may want to split the work into two parts: first,
update the compiler and machine language, assuming the existence of such
a function and the (partial) inverse it provides. Second, separately construct
this function from the input program, having extracted the properties needed
for it in the first part. In the end, rearrange your theory file to combine both
into a final theorem.
This chapter introduces types into IMP, first a traditional programming lan-
guage type system, then more sophisticated type systems for information flow
analysis.
Why bother with types? Because they prevent mistakes. They are a sim-
ple, automatic way to find obvious problems in programs before these pro-
grams are ever run.
There are three kinds of types.
The Good Static types that guarantee absence of certain runtime faults.
The Bad Static types that have mostly decorative value but do not guaran-
tee anything at runtime.
The Ugly Dynamic types that detect errors only when it can be too late.
Examples of the first kind are Java, ML and Haskell. In Java, for instance,
the type system enforces that there will be no memory access errors, which
in other languages manifest themselves as segmentation faults. Haskell has an
even more powerful type system, in which, for instance, it becomes visible
whether a function can perform input/output actions or not.
Famous examples of the bad kind are C and C++. These languages have
static type systems, but they can be circumvented easily. The language spec-
ification may not even allow these circumventions, but there is no way for
compilers to guarantee their absence.
Examples for dynamic types are scripting languages such as Perl and
Python. These languages are typed, but typing violations are discovered and
reported at runtime only, which leads to runtime messages such as “TypeEr-
ror: . . . ” in Python for instance.
In all of the above cases, types are useful. Even in Perl and Python, they at
least are known at runtime and can be used to conveniently convert values of
one type into another and to enable object-oriented features such as dynamic
dispatch of method calls. They just don’t provide any compile-time checking.
116 9 Types
In C and C++, the compiler can at least report some errors already at compile
time and alert the programmer to obvious problems. But only static, sound
type systems can enforce the absence of whole classes of runtime errors.
Static type systems can be seen as proof systems, type checking as proof
checking, and type inference as proof search. Every time a type checker passes
a program, it in effect proves a set of small theorems about this program.
The ideal static type system is permissive enough not to get in the pro-
grammer’s way, yet strong enough to guarantee Robin Milner’s slogan
Well-typed programs cannot go wrong [56].
It is the most influential slogan and one of the most influential papers in
programming language theory.
What could go wrong? Some examples of common runtime errors are cor-
ruption of data, null pointer exceptions, nontermination, running out of mem-
ory, and leaking secrets. There exist type systems for all of these, and more,
but in practice only the first is covered in widely used languages such as
Java, C#, Haskell, or ML. We will cover this first kind in Section 9.1, and
information leakage in Section 9.2.
As mentioned above, the ideal for a language is to be type safe. Type
safe means that the execution of a well-typed program cannot lead to certain
errors. Java and the JVM, for instance, have been proved to be type safe. An
execution of a Java program may throw legitimate language exceptions such
as NullPointer or OutOfMemory, but it can never produce data corruption or
segmentation faults other than by hardware defects or calls into native code.
In the following sections we will show how to prove such theorems for IMP.
Type safety is a feature of a programming language. Type soundness
means the same thing, but talks about the type system instead. It means
that a type system is sound or correct with respect to the semantics of the
language: If the type system says yes, the semantics does not lead to an error.
The semantics is the primary definition of behaviour, and therefore the type
system must be justified w.r.t. it.
If there is soundness, how about completeness? Remember Rice’s theorem:
Nontrivial semantic properties of programs are undecidable.
Hence there is no (decidable) type system that accepts precisely the programs
that have a certain semantic property, e.g., termination.
This applies not only to type systems but to all automatic semantic analyses
and is discussed in more detail at the beginning of the next chapter.
9.1 Typed IMP 117
thy
9.1 Typed IMP
In this section we develop a very basic static type system as a typical applica-
tion of programming language semantics. The idea is to define the type system
formally and to use the semantics for stating and proving its soundness.
The IMP language we have used so far is not well suited for this proof,
because it has only one type of value. This is not enough for even a simple
type system. To make things at least slightly non-trivial, we invent a new
language that computes on real numbers as well as integers.
To define this new language, we go through the complete exercise again,
and define new arithmetic and boolean expressions, together with their values
and semantics, as well as a new semantics for commands. In the theorem
prover we can do this by merely copying the original definitions and tweaking
them slightly. Here, we will briefly walk through the new definitions step by
step.
We begin with values occurring in the language. Our introduction of a
second kind of value means our value type now correspondingly has two al-
ternatives:
datatype val = Iv int | Rv real
This definition means we tag values with their type at runtime (the construc-
tor tells us which is which). We do this so we can observe when things go
wrong, for instance when a program is trying to add an integer to a real. This
does not mean that a compiler for this language would also need to carry this
information around at runtime. In fact, it is the type system that lets us avoid
this overhead! Since it will only admit safe programs, the compiler can opti-
mize and blindly apply the operation for the correct type. It can determine
statically what that correct type is.
Note that the type real stands for the mathematical real numbers, not floating
point numbers, just as we use mathematical integers in IMP instead of finite
machine words. For the purposes of the type system this difference does not mat-
ter. For formalizing a real programming language, one should model values more
precisely.
Plus (Ic 1) (Rc 3) tries to add an integer to a real number. Assuming for
a moment that these are fundamentally incompatible types that cannot pos-
sibly be added, this expression makes no sense. We would like to express
in our semantics that this is not an expression with well-defined behaviour.
One alternative would be to continue using a functional style of semantics
for expressions. In this style we would now return val option with the con-
structor None of the option data type introduced in Section 2.3.1 to denote
the undefined cases. It is quite possible to do so, and in later chapters we will
demonstrate that variant. However, it implies that we would have to explicitly
enumerate all undefined cases.
It is more elegant and concise to only write down the cases that make sense
and leave everything else undefined. The operational semantics judgement
already lets us do this for commands. We can use the same style for arithmetic
expressions. Since we are not interested in intermediate states at this point,
we choose the big-step style.
Our new judgement relates an expression and the state it is evaluated in
to the value it is evaluated to. We refrain from introducing additional syntax
and call this judgement taval for typed arithmetic value of an expression. In
Isabelle, this translates to an inductive definition with type aexp ⇒ state ⇒
val ⇒ bool. We show its introduction rules in Figure 9.1. The term taval a s v
means that arithmetic expression a evaluates in state s to value v.
The definition is straightforward. The first rule taval (Ic i ) s (Iv i ) for
instance says that an integer constant Ic i always evaluates to the value Iv i ,
no matter what the state is. The interesting cases are the rules that are not
there. For instance, there is no rule to add a real to an int. We only needed
to provide rules for the cases that make sense and we have implicitly defined
what the error cases are. The following is an example derivation for taval
where s 0 0x 0 0 = Iv 4.
0 0 00
taval (Ic 3) s (Iv 3) taval (V x ) s (Iv 4)
0 0 00
taval (Plus (Ic 3) (V x )) s (Iv 7)
9.1 Typed IMP 119
tbval b s bv
tbval (Bc v ) s v tbval (Not b) s (¬ bv )
taval a s v
(x ::= a, s) → (SKIP , s(x := v ))
(c 1 , s) → (c 10 , s 0 )
(SKIP ;; c, s) → (c, s) (c 1 ;; c 2 , s) → (c 10 ;; c 2 , s 0 )
tbval b s True
(IF b THEN c 1 ELSE c 2 , s) → (c 1 , s)
tbval b s False
(IF b THEN c 1 ELSE c 2 , s) → (c 2 , s)
Having defined our new language above, we can now define its type system.
The idea of such type systems is to predict statically which values will ap-
pear at runtime and to exclude programs in which unsafe values or value
combinations might be encountered.
9.1 Typed IMP 121
Γ ` a1 : τ Γ ` a2 : τ
Γ ` Plus a 1 a 2 : τ
Γ `b
Γ ` Bc v Γ ` Not b
Γ ` b1 Γ ` b2 Γ ` a1 : τ Γ ` a2 : τ
Γ ` And b 1 b 2 Γ ` Less a 1 a 2
The type system we use for this is very rudimentary, it has only two types:
int and real, written as the constructors Ity and Rty, corresponding to the
two kinds of values we have introduced. In Isabelle:
datatype ty = Ity | Rty
The purpose of the type system is to keep track of the type of each variable
and to allow only compatible combinations in expressions. For this purpose,
we define a so-called typing environment. Where a runtime state maps variable
names to values, a static typing environment maps variable names to their
static types.
type_synonym tyenv = vname ⇒ ty
For example, we could have Γ 0 0x 0 0 = Ity, telling us that variable x has type
integer and that we should therefore not use it in an expression of type real.
With this, we can give typing rules for arithmetic expressions. The idea
is simple: constants have fixed type, variables have the type the typing envi-
ronment Γ prescribes, and Plus can be typed with type τ if both operands
have the same type τ. Figure 9.4 shows the definition in Isabelle. We use the
notation Γ ` a : τ to say that expression a has type τ in context Γ .
The typing rules for booleans in Figure 9.5 are even simpler. We do not
need a result type, because it will always be bool, so the notation is just Γ ` b
for expression b is well-typed in context Γ . For the most part, we just need
to capture that boolean expressions are well-typed if their subexpressions are
well-typed. The interesting case is the connection to arithmetic expressions in
Less. Here we demand that both operands have the same type τ, i.e., either
we compare two ints or two reals, but not an int to a real.
Similarly, commands are well-typed if their subcommands and subexpres-
sions are well-typed. In addition, in an assignment the arithmetic expression
122 9 Types
Γ `a :Γ x
Γ ` SKIP Γ ` x ::= a
Γ ` c1 Γ ` c2 Γ `b Γ ` c1 Γ ` c2 Γ `b Γ `c
Γ ` c 1 ;; c 2 Γ ` IF b THEN c 1 ELSE c 2 Γ ` WHILE b DO c
must have the same type as the variable it is assigned to. The full set of
rules is shown in Figure 9.6. We re-use the syntax Γ ` c for command c is
well-typed in context Γ .
This concludes the definition of the type system itself. Type systems can be
arbitrarily complex. The one here is intentionally simple to show the structure
of a type soundness proof without getting side-tracked in interesting type
system details.
Note that there is precisely one rule per syntactic construct in our def-
inition of the type system, and the premises of each rule apply the typing
judgement only to subterms of the conclusion. We call such rule sets syntax-
directed. Syntax-directed rules are a good candidate for automatic applica-
tion and for deriving an algorithm that infers the type simply by applying
them backwards, at least if there are no side conditions in their assumptions.
Since there is exactly one rule per construct, it is always clear which rule to
pick and there is no need for back-tracking. Further, since there is always
at most one rule application per syntax node in the term or expression the
rules are applied to, this process must terminate. This idea can be extended
to allow side conditions in the assumptions of rules, as long as these side
conditions are decidable.
Given such a type system, we can now check whether a specific command
c is well-typed. To do so, we merely need to construct a derivation tree for
the judgment Γ ` c. Such a derivation tree is also called a type derivation.
Let for instance Γ 0 0x 0 0 = Ity as well as Γ 0 0y 0 0 = Ity. Then our previous
example program is well-typed, because of the following type derivation.
0 0 00 0 0 00
Γ x = Ity Γ y = Ity
0 0 00 0 0 00 0 0 00
Γ y = Ity Γ `V x : Ity Γ `V y : Ity
0 0 00 0 0 00 0 0 00
Γ `V y : Ity Γ ` Plus (V x ) (V y ) : Ity
0 0 00 0 0 00 0 0 00 0 0 00 0 0 00
Γ ` x ::= V y Γ ` y ::= Plus (V x ) (V y )
0 0 00 0 0 00 0 0 00 0 0 00 0 0 00
Γ ` x ::= V y ;; y ::= Plus (V x ) (V y )
9.1 Typed IMP 123
In this section we prove that the type system defined above is sound. As men-
tioned in the introduction to this chapter, Robert Milner coined the phrase
Well-typed programs cannot go wrong, i.e., well-typed programs will not ex-
hibit any runtime errors such as segmentation faults or undefined execution.
In our small-step semantics we have defined precisely what “go wrong” means
formally: a program exhibits a runtime error when the semantics gets stuck.
To prove type soundness we have to prove that well-typed programs never
get stuck. They either terminate successfully, or they make further progress.
Taken literally, the above sentence translates into the following property:
[[(c, s) →∗ (c 0 , s 0 ); Γ ` c]] =⇒ c 0 = SKIP ∨ (∃ cs 0 0 . (c 0 , s 0 ) → cs 0 0 )
Given an arbitrary command c, which is well-typed Γ ` c, any execution
(c, s) →∗ (c 0 , s 0 ) either has terminated successfully with c 0 = SKIP, or can
make another execution step ∃ cs 0 0 . (c 0 , s 0 ) → cs 0 0 . Clearly, this statement is
wrong, though: take c for instance to be a command that computes the sum
of two variables: z := x +y. This command is well-typed, for example, if the
variables are both of type int. However, if we start the command in a state
that disagrees with this type, e.g., where x contains an int but y contains a
real, the execution gets stuck.
Of course, we want the value of a variable to be of type int if the typing says
it should be int. This means we want not only the program to be well-typed,
but the state to be well-typed too.
We so far have the state assigning values to variables and we have the type
system statically assigning types to variables in the program. The concept of
well-typed states connects these two: we define a judgement that determines
if a runtime state is compatible with a typing environment for variables. We
call this formal judgement styping below, written Γ ` s. We equivalently also
say that a state s conforms to a typing environment Γ .
With this judgement, our full statement of type soundness is now
[[(c, s) →∗ (c 0 , s 0 ); Γ ` c; Γ ` s; c 0 6= SKIP ]] =⇒ ∃ cs 0 0 . (c 0 , s 0 ) → cs 0 0
Given a well-typed program, started in a well-typed state, any execution
that has not reached SKIP yet can make another step.
We will prove this property by induction on the reflexive transitive closure
of execution steps, which naturally decomposes this type soundness property
into two parts: preservation and progress. Preservation means that well-
typed states stay well-typed during execution. Progress means that in a
well-typed state, the program either terminates successfully or can make one
more step of execution progress.
In the following, we formalize the soundness proof for typed IMP.
124 9 Types
The proof of the progress lemma is slightly more verbose. It is almost the
only place where something interesting is concluded in the soundness proof
— there is the potential of something going wrong: if the operands of a Plus
were of incompatible type, there would be no value v the expression evaluates
to. Of course, the type system excludes precisely this case.
The progress statement is as standard as the preservation statement for
arithmetic expressions: given that a has type τ under environment Γ , and
given a conforming state s, there must exist a result value v such that a
evaluates to v in s.
Proof. The proof is again by rule induction on the typing derivation. The
interesting case is Plus a 1 a 2 . The induction hypothesis gives us two values
v 1 and v 2 for the subexpressions a 1 and a 2 . If v 1 is an integer, then, by
preservation, the type of a 1 must have been Ity. The typing rule says that
the type of a 2 must be the same. This means, by preservation, the type of v 2
must be Ity, which in turn means then v 2 must be an Iv value and we can
conclude using the taval introduction rule for Plus that the execution has
a result. Isabelle completes this reasoning chain automatically if we carefully
provide it with the right facts and rules. The case for reals is analogous, and
the other typing cases are solved automatically. t
u
For boolean expressions, there is no preservation lemma, because tbval,
by its Isabelle type, can only return boolean values. The progress statement
makes sense, though, and follows the standard progress statement schema.
Lemma 9.4 (Progress for boolean expressions).
[[Γ ` b; Γ ` s]] =⇒ ∃ v . tbval b s v
Proof. As always, the proof is by rule induction on the typing derivation. The
interesting case is where something could go wrong, namely where we execute
arithmetic expressions in Less. The proof is very similar to the one for Plus:
we obtain the values of the subexpressions; we perform a case distinction on
one of them to reason about its type; we infer the other has the same type by
typing rules and by preservation on arithmetic expressions; and we conclude
that execution can therefore progress. Again, this case is automatic if written
carefully; the other cases are trivial. t
u
For commands, there are two preservation statements, because the con-
figurations in our small-step semantics have two components: command and
state. We first show that the command remains well-typed and then that the
state does. Both proofs are by induction on the small-step semantics. They
could be proved by induction on the typing derivation as well. Often it is
preferable to try induction on the typing derivation first, because the type
system typically has fewer cases. On the other hand, depending on the com-
plexity of the language, the more fine-grained information that is available
in the operational semantics might make the more numerous cases easier to
prove in the other induction alternative. In both cases it pays off to design the
structure of the rules in both systems such that they technically fit together
nicely, for instance such that they decompose along the same syntactic lines.
Theorem 9.5 (Preservation: commands stay well-typed).
[[(c, s) → (c 0 , s 0 ); Γ ` c]] =⇒ Γ ` c 0
Proof. The preservation of program typing is fully automatic in this simple
language. The only mildly interesting case where we are not just transforming
126 9 Types
the command into a subcommand is the while loop. Here we just need to apply
the typing rules for IF and sequential composition and are done. t
u
Proof. Most cases are trivial because the state is not modified. In the second
;; rule the induction hypothesis applies. In the assignment rule the state is
updated with a new value. Type preservation on expressions gives us that the
new value has the same type as the expression, and unfolding the styping
judgement shows that it is unaffected by state updates that are type pre-
serving. In more complex languages, there are likely to be a number of such
update cases and the corresponding lemma is a central piece of type sound-
ness proofs. t
u
The next step is the progress lemma for commands. Here, we need to take
into account that the program might have fully terminated. If it has not, and
we have a well-typed program in a well-typed state, we should be able to
make at least one step.
Proof. This time the only induction alternative is on the typing derivation
again. The cases with arithmetic and boolean expressions make use of the
corresponding progress lemmas to generate the values the small-step rules de-
mand. For IF, we additionally perform a case distinction for picking the corre-
sponding introduction rule. As for the other cases: SKIP is trivial, sequential
composition applies the induction hypotheses and makes a case distinction
whether c 1 is SKIP or not, and WHILE always trivially makes progress in
the small-step semantics, because it is unfolded into an IF /WHILE. t
u
All that remains is to assemble the pieces into the final type soundness
statement: given any execution of a well-typed program started in a well-
typed state, we are not stuck; we have either terminated successfully, or the
program can perform another step.
Proof. The proof lifts the one-step preservation and progress results to a
sequence of steps by induction on the reflexive transitive closure. The base
case of zero steps is solved by the progress lemma; the step case needs our
two preservation lemmas for commands. t
u
9.1 Typed IMP 127
Exercises
Exercise 9.1. Reformulate the inductive predicates Γ ` a : τ, Γ ` b and
Γ ` c as three functions atype :: tyenv ⇒ aexp ⇒ ty option, bok :: tyenv
⇒ bexp ⇒ bool and cok :: tyenv ⇒ com ⇒ bool and prove the three
equivalences (Γ ` a : τ) = (atype Γ a = Some τ), (Γ ` b) = bok Γ b and
(Γ ` c) = cok Γ c.
Exercise 9.2. Modify the evaluation and typing of aexp by allowing int s to
be coerced to reals with the predefined coercion function real_of_int :: int
⇒ real where necessary. Now every aexp has a value and a type. Define an
evaluation function aval :: aexp ⇒ state ⇒ val and a typing function atyp
:: tyenv ⇒ aexp ⇒ ty and prove Γ ` s =⇒ atyp Γ a = type (aval a s).
For the following two exercises copy theory Types and modify it as re-
quired.
Exercise 9.3. Add a REPEAT loop (see Exercise 7.8) to the typed version
of IMP and update the type soundness proof.
Exercise 9.4. Modify the typed version of IMP as follows. Values are now
either integers or booleans. Thus variables can have boolean values too. Merge
the two expression types aexp and bexp into one new type exp of expressions
that has the constructors of both types (of course without real constants).
Combine taval and tbval into one evaluation predicate eval :: exp ⇒ state ⇒
val ⇒ bool. Similarly combine the two typing predicates into one: Γ ` e : τ
where e :: exp and the IMP-type τ can be one of Ity or Bty. Adjust the
small-step semantics and the type soundness proof.
128 9 Types
As usual in IMP, the typing for expressions was simple. We now define a
syntax-directed set of security typing rules for commands. This makes the
rules directly executable and allows us to run examples. Checking for explicit
flows, i.e., assignments from high to low variables, is easy. For implicit flows,
the main idea of the type system is to track the security level of variables
that decisions are made on, and to make sure that their level is lower than or
equal to variables assigned to in that context.
We write l ` c to mean that command c contains no information flows to
variables lower than level l, and only safe flows to variables > l.
Going through the rules of Figure 9.7 in detail, we have defined SKIP to
be safe at any level. We have defined assignment to be safe if the level of x is
higher than or equal to both the level of the information source a and the level
l. For semicolon to conform to level l, we recursively demand that both parts
conform to the same level l. As previously shown in the motivating example,
9.2 Security Type Systems 131
Example 9.11. Testing our intuition about what we have just defined, we
look at four examples for various security levels.
00
0 ` IF Less (V x1 0 0 ) (V 0 0 00
x ) THEN 00
x1 0 0 ::= N 0 ELSE SKIP
Now we get False, because we need to take the maximum of the context and
the boolean expression for evaluating the branches. The intuition is that the
context gives the minimum level to which we may reveal information.
As we can already see from these simple examples, the type system is
not complete: it will reject some safe programs as unsafe. For instance, if the
value of x in the second command was already 0 in the beginning, the context
would not have mattered, we only would have overwritten 0 with 0. As we
know by now, we should not expect otherwise. The best we can hope for is
a safe approximation such that the false alarms are hopefully programs that
rarely occur in practice or that can be rewritten easily.
It is the case that the simple type system presented here, going back to
Volpano, Irvine, and Smith [89], has been criticised as too restrictive. It ex-
cludes too many safe programs. This can be addressed by making the type
system more refined, more flexible, and more context-aware. For demonstrat-
ing the type system and its soundness proof in this book, however, we will
stick to its simplest form.
9.2.3 Soundness
An important property, which will be useful for this lemma, is the so-
called anti-monotonicity of the type system: a command that is typeable
in l is also typeable in any level smaller than l. Anti-monotonicity is also
often called the subsumption rule, to say that higher contexts subsume
lower ones. Intuitively it is clear that this property should hold: we defined
l ` c to mean that there are no flows to variables < l. If we write l 0 ` c with
an l 0 6 l, then we are only admitting more flows, i.e., we are making a weaker
statement.
Proof. The formal proof is by rule induction on the type system. Each of the
cases is then solved automatically. t
u
9.2 Security Type Systems 133
The second key lemma in the argument for the soundness of our security
type system is confinement: an execution of a command that is type correct
in context l can only change variables of level l and above, or conversely, all
variables below l will remain unchanged. In other words, the effect of c is
confined to variables of level > l.
Proof. The first instinct may be to try rule induction on the type sys-
tem again, but the WHILE case will only give us an induction hypothe-
sis about the body when we will have to show our goal for the whole loop.
Therefore, we choose rule induction on the big-step execution instead. In
the IF and WHILE cases, we make use of anti-monotonicity to instan-
tiate the induction hypothesis. In the IfTrue case, for instance, the hy-
pothesis is l ` c 1 =⇒ s = t (< l), but from the type system we only know
max (sec b) l ` c 1 . Since l 6 max (sec b) l, anti-monotonicity allows us to
conclude l ` c 1 . t
u
With these two lemmas, we can start the main noninterference proof.
Proof. The proof is again by induction on the big-step execution. The SKIP
case is easy and automatic, as it should be.
The assignment case is already somewhat interesting. First, we note that
s 0 is the usual state update s(x := aval a s) in the first big-step execution. We
perform rule inversion for the second execution to get the same update for t.
We also perform rule inversion on the typing statement to get the relationship
between security levels of x and a: sec a 6 sec x. Now we show that the two
updated states s 0 and t 0 still agree on all variables below l. For this, it is
sufficient to show that the states agree on the new value if sec x < l, and
that all other variables y with sec y < l still agree as before. In the first case,
looking at x, we know from above that sec a 6 sec x. Hence, by transitivity,
we have that sec a 6 l. This is enough for our noninterference result on
expressions to apply, given that we also know s = t (6 l) from the premises.
This means, we get aval a s = aval a t : the new values for x agree as required.
The case for all other variables y below l follows directly from s = t (6 l).
In the semicolon case, we merely need to compose the induction hypothe-
ses. This is solved automatically.
IF has two symmetric cases as usual. We will look only at the IfTrue
case in more detail. We begin the case by noting via rule inversion that both
branches are type correct to level sec b, since the maximum with 0 is the
134 9 Types
l `0 c1 l `0 c2 sec b 6 l l `0 c1 l `0 c2
l ` 0 c 1 ;; c 2 l ` 0 IF b THEN c 1 ELSE c 2
sec b 6 l l `0 c l `0 c l0 6 l
0 0 0
l ` WHILE b DO c l ` c
sec a 6 sec x ` c1 : l 1 ` c2 : l 2
` SKIP : l ` x ::= a : sec x ` c 1 ;; c 2 : min l 1 l 2
The equivalence proof goes by rule induction on the respective type system
in each direction separately. Isabelle proves each subgoal of the induction
automatically.
Lemma 9.15. l ` c =⇒ l ` 0 c
Lemma 9.16. l ` 0 c =⇒ l ` c
The type systems presented above are top-down systems: the level l is passed
from the context or the user and is checked at assignment commands. We can
also give a bottom-up formulation where we compute the greatest l consistent
with variable assignments and check this value at IF and WHILE commands.
Instead of max computations, we now get min computations in Figure 9.9.
We can read the bottom-up statement ` c : l as c has a write-effect of
l, meaning that no variable below l is written to in c.
Again, we can prove equivalence. The first direction is straightforward and
the proof is automatic.
Lemma 9.17. ` c : l =⇒ l ` 0 c
We read it as: If our type system says yes, our data is safe: there will
be no information flowing from high to low variables.
Is this correct? The formal statement is certainly true, we proved it in Isa-
belle. But: it doesn’t quite mean what the sentence above says. It means only
precisely what the formula states: given two terminating executions started
in states we can’t tell apart, we won’t be able to tell apart their final states.
What if we don’t have two terminating executions? Consider, for example,
the following typing statement.
0 0 00
0 ` WHILE Less (V x ) (N 1) DO SKIP
tions must not depend on confidential data. If they don’t, then termination
cannot leak information.
In the following, we formalize and prove this idea.
Formalizing our idea means we replace the WHILE -rule with a new one
that does not admit anything higher than level 0 in the condition:
sec b = 0 0`c
0 ` WHILE b DO c
This is already it. Figure 9.10 shows the full set of rules, putting the new
one into context.
We now need to change our noninterference statement such that it takes
termination into account. The interesting case was where one execution ter-
minated and the other didn’t. If both executions terminate, our previous
statement already applies; if both do not terminate then there is no informa-
tion leakage, because there is nothing to observe.1 So, since our statement is
symmetric, we now assume one terminating execution, a well-typed program
of level 0 as before, and two start states that agree up to level l, also as before.
We then have to show that the other execution also terminates and that the
final states still agree up to level l.
We build up the proof of this new theorem in the same way as before. The
first property is again anti-monotonicity, which still holds.
Proof. The proof is by induction on the typing derivation. Isabelle then solves
each of the cases automatically. t
u
Proof. The proof is the same as before, first by induction on the big-step
execution, then by using anti-monotonicity in the IF cases, and automation
on the rest. t
u
Before we can proceed to noninterference, we need one new fact about the
new type system: any program that is type correct, but not at level 0 (only
higher), must terminate. Intuitively that is easy to see: WHILE loops are the
only cause of potential nontermination, and they can now only be typed at
level 0. This means, if the program is type correct at some level, but not at
level 0, it does not contain WHILE loops.
Proof. The formal proof of this lemma does not directly talk about the occur-
rence of while loops, but encodes the argument in a contradiction. We start
the proof by induction on the typing derivation. The base cases all terminate
trivially, and the step cases terminate because all their branches terminate in
the induction hypothesis. In the WHILE case we have the contradiction: our
assumption says that l 6= 0, but the induction rule instantiates l with 0, and
we get 0 6= 0. t
u
Equipped with these lemmas, we can finally proceed to our new statement
of noninterference.
sec b 6 l l `0 c1 l `0 c2 sec b = 0 0 `0 c
l ` 0 IF b THEN c 1 ELSE c 2 0 ` 0 WHILE b DO c
l `0 c l0 6 l
0 0
l ` c
to make the same decision whether to terminate or not. That was the whole
point of our type system change. In the WhileFalse case that is all that is
needed; in the WhileTrue case, we can make use of this fact to access the
induction hypothesis: from the fact that the loop is type correct at level 0,
we know by rule inversion that 0 ` c. We also know, by virtue of being in
the WhileTrue case, that bval b s, (c, s) ⇒ s 0 0 , and (w , s 0 0 ) ⇒ s 0 . We now
need to construct a terminating execution of the loop starting in t, ending in
some state t 0 that agrees with s 0 below l. We start by noting bval b t using
noninterference for boolean expressions. Per induction hypothesis we conclude
that there is a t 0 0 with (c, t ) ⇒ t 0 0 that agrees with s 0 0 below l. Using the
second induction hypothesis, we repeat the process for w, and conclude that
there must be such a t 0 that agrees with s 0 below l. t
u
Proof. As with the equivalence proofs of different security type system formu-
lations in previous sections, this proof goes first by considering each direction
of the if-and-only-if separately, and then by induction on the type system in
the assumption of that implication. As before, Isabelle then proves each sub
case of the respective induction automatically. t
u
Exercises
Exercise 9.7. Define a function erase :: level ⇒ com ⇒ com that erases
those parts of a command that contain variables above some security level.
Function erase l should replace all assignments to variables with security level
> l by SKIP. It should also erase certain IF s and WHILE s, depending on
the security level of the boolean condition. Prove that c and erase l c behave
the same on the variables up to level l:
[[(c, s) ⇒ s 0 ; (erase l c, t ) ⇒ t 0 ; 0 ` c; s = t (< l)]] =⇒ s 0 = t 0 (< l)
It is recommended to start with the proof of the very similar looking nonin-
terference Theorem 9.14 and modify that.
In the theorem above we assume that both (c, s) and (erase l c, t )
terminate. How about the following two properties?
In this chapter we have analysed two kinds of type systems: a standard type
system that tracks types of values and prevents type errors at runtime, and
a security type system that prevents information flow from higher-level to
lower-level variables.
Sound, static type systems enjoy widespread application in popular pro-
gramming languages such as Java, C#, Haskell, and ML, but also on low-level
languages such as the Java Virtual Machine and its bytecode verifier [54]. Some
of these languages require types to be declared explicitly, as in Java. In other
languages, such as Haskell, these declarations can be left out, and types are
inferred automatically.
The purpose of type systems is to prevent errors. In essence, a type deriva-
tion is a proof, which means type checking performs basic automatic proofs
about programs.
9.3 Summary and Further Reading 141
Limitations
Program analyses, no matter what techniques they employ, are always limited.
This is a consequence of Rice’s Theorem from computability theory. It roughly
tells us that Nontrivial semantic properties of programs (e.g. termination)
are undecidable. That is, no nontrivial semantic property P has a magic
analyser that
terminates on every input program,
only says Yes if the input program has property P (correctness), and
only says No if the input program does not have property P (completeness).
For concreteness, let us consider definite initialization analysis of the following
program:
FOR ALL positive integers x, y, z, n DO
IF n > 2 ∧ xn + yn = zn THEN u := u ELSE SKIP
For convenience we have extended our programming language with a FOR ALL
loop and an exponentiation operation: both could be programmed in pure
IMP, although it would be painful. The program searches for a counterex-
ample to Fermat’s conjecture that no three positive integers x, y, and z can
satisfy the equation xn + yn = zn for any integer n > 2. It reads the unini-
tialized variable u (thus violating the definite initialization property) iff such
a counterexample exists. It would be asking a bit much from a humble pro-
gram analyser to determine the truth of a statement that was in the Guinness
Book of World Records for “most difficult mathematical problems” prior to
its 1995 proof by Wiles.
As a consequence, we cannot expect program analysers to terminate, be
correct and be complete. Since we do not want to sacrifice termination and
correctness, we sacrifice completeness: we allow analysers to say No although
the program has the desired semantic property but the analyser was unable
to determine that.
10.1 Definite Initialization Analysis 145
case, the program is unsafe. So, if our goal is to reject all potentially unsafe
programs, we have to reject this one.
As mentioned in the introduction, we do not analyse boolean expressions
statically to make predictions about program execution. Instead we take both
potential outcomes into account. This means, the analysis we are about to
develop will only accept the first program, but reject the other two.
Java is more discerning in this case, and will perform the optimization
of constant folding, which we discuss in Section 10.2, before definite ini-
tialization analysis. If during that pass it turns out an expression is always
True or always False, this can be taken into account. This is a nice example
of positive interaction between different kinds of optimization and program
analysis, where one enhances the precision and predictive power of the other.
As discussed, we cannot hope for completeness of any program analysis,
so there will be cases of safe programs that are rejected. For this specific
analysis, this is usually the case when the programmer is smarter than the
boolean constant folding the compiler performs. As with any restriction in a
programming language, some programmers will complain about the shackles
of definite initialization analysis, and Java developer forums certainly contain
such complaints. Completely eliminating this particularly hard-to-find class
of Heisenbugs well justifies the occasional program refactoring, though.
In the following sections, we construct our definite initialization analysis,
define a semantics where initialization failure is observable, and then proceed
to prove the analysis correct by showing that these failures will not occur.
The Java Language Specification quotes a number of rules that definite ini-
tialization analysis should implement to achieve the desired result. They have
the following form (adjusted for IMP):
Variable x is definitely initialized after SKIP
iff x is definitely initialized before SKIP.
Similar statements exist for each language construct. Our task is simply
to formalize them. Each of these rules talks about variables, or more precisely
sets of variables. For instance, to check an assignment statement, we will want
to start with a set of variables that is already initialized, we will check that
set against the set of variables that is used in the assignment expression, and
we will add the assigned variable to the initialized set after the assignment
has completed.
So, the first formal tool we need is the set of variables mentioned in an
expression. The Isabelle theory Vars provides an overloaded function vars
for this:
10.1 Definite Initialization Analysis 147
vars a ⊆ A
D A SKIP A D A (x ::= a) (insert x A)
D A1 c 1 A2 D A2 c 2 A3
D A1 (c 1 ;; c 2 ) A3
vars b ⊆ A D A c 1 A1 D A c 2 A2
D A (IF b THEN c 1 ELSE c 2 ) (A1 ∩ A2 )
vars b ⊆ A D A c A0
D A (WHILE b DO c) A
Fig. 10.1. Definite initialization D :: vname set ⇒ com ⇒ vname set ⇒ bool
With this we can define our main definite initialization analysis. The purpose
is to check whether each variable in the program is assigned to before it is
used. This means we ultimately want a predicate of type com ⇒ bool, but we
have already seen in the examples that we need a slightly more general form
for the computation itself. In particular, we carry around a set of variables
that we know are definitely initialized at the beginning of a command. The
analysis then has to do two things: check whether the command only uses
these variables, and produce a new set of variables that we know are initialized
afterwards. This leaves us with the following type signature:
D :: vname set ⇒ com ⇒ vname set ⇒ bool
We want the notation D A c A 0 to mean:
If all variables in A are initialized before c is executed, then no unini-
tialized variable is accessed during execution, and all variables in A 0 are
initialized afterwards.
Figure 10.1 shows how we can inductively define this analysis with one rule
per syntactic construct. We walk through them step by step:
The SKIP rule is obvious, and translates exactly the text rule we have
mentioned above.
148 10 Program Analysis
Similarly, the assignment rule follows our example above: the predicate
D A (x ::= a) A 0 is True if the variables of the expression a are contained
in the initial set A, and if A 0 is precisely the initial A plus the variable x
we just assigned to.
Sequential composition has the by now familiar form: we simply pass
through the result A2 of c 1 to c 2 , and the composition is definitely ini-
tialized if both commands are definitely initialized.
In the IF case, we check that the variables of the boolean expression
are all initialized, and we check that each of the branches is definitely
initialized. We pass back the intersection of the results produced by c 1
and c 2 , because we do not know which branch will be taken at runtime. If
we were to analyse boolean expression more precisely, we could introduce
further case distinctions into this rule.
Finally, the WHILE case. It also checks that the variables in the boolean
expression are all in the initialized set A, and it also checks that the com-
mand c is definitely initialized starting in the same set A, but it ignores
the result A 0 of c. Again, this must be so, because we have to be con-
servative: it is possible that the loop will never be executed at runtime,
because b may be already False before the first iteration. In this case no
additional variables will be initialized, no matter what c does. It may be
possible for specific loop structures, such as for-loops, to statically deter-
mine that their body will be executed at least once, but no mainstream
language currently does that.
We can now decide whether a command is definitely initialized, namely
exactly when we can start with the empty set of initialized variables and find
a resulting set such that our inductive predicate D is True:
D c = (∃ A 0 . D {} c A 0 )
Defining a program analysis such as definite initialization by an inductive
predicate makes the connection to type systems clear: in a sense, all program
analyses can be phrased as sufficiently complex type systems. Since our rules
are syntax-directed, they also directly suggest a recursive execution strategy.
In fact, for this analysis it is straightforward to turn the inductive predicate
into two recursive functions in Isabelle that compute our set A 0 if it exists,
and check whether all expressions mention only initialized variables. We leave
this recursive definition and proof of equivalence as an exercise to the reader
and turn our attention to proving correctness of the analysis instead.
Here, this is easy: we should observe an error when the program uses a
variable that has not been initialized. That is, we need a new, finer-grained
semantics that keeps track of which variables have been initialized and leads
to an error if the program accesses any other variable.
To that end, we enrich our set of values with an additional element that
we will read as uninitialized. As mentioned in Section 2.3.1 in the Isabelle
part in the beginning, Isabelle provides the option data type, which is useful
for precisely such situations:
datatype 0a option = None | Some 0a
We simply redefine our program state as
type_synonym state = vname ⇒ val option
and take None as the uninitialized value. The option data type comes with
additional useful notation: s(x 7→ y) means s(x := Some y), and dom s =
{a. s a 6= None}.
Now that we can distinguish initialized from uninitialized values, we can
check the evaluation of expressions. We have had a similar example of po-
tentially failing expression evaluation in type systems in Section 9.1. There
we opted for an inductive predicate, reasoning that in the functional style
where we would return None for failure, we would have to consider all failure
cases explicitly. This argument also holds here. Nevertheless, for the sake of
variety, we will this time show the functional variant with option. It is less
elegant, but not so horrible as to become unusable. It has the advantage of
being functional, and therefore easier to apply automatically in proofs.
We can reward ourselves for all these case distinctions with two concise lem-
mas that confirm that expressions indeed evaluate without failure if they only
mention initialized variables.
Both lemmas are proved automatically after structural induction on the ex-
pression.
aval a s = Some i
(x ::= a, s) → (SKIP , s(x 7→ i ))
(c 1 , s) → (c 10 , s 0 )
(SKIP ;; c, s) → (c, s) (c 1 ;; c 2 , s) → (c 10 ;; c 2 , s 0 )
result state directly into the start of the next execution without any additional
operation or case distinction for unwrapping the option type. We achieve this
by making the start type state option as well.
com × state option ⇒ state option
We can now write one rule that defines how error (None) propagates:
(c, None) ⇒ None
Consequently, in the rest of the semantics in Figure 10.3 we only have to
locally consider the case where we directly produce an error, and the case of
normal execution. An example of the latter is the assignment rule, where we
update the state as usual if the arithmetic expression evaluates normally:
aval a s = Some i
(x ::= a, Some s) ⇒ Some (s(x 7→ i ))
An example of the former is the assignment rule, where expression evaluation
leads to failure:
aval a s = None
(x ::= a, Some s) ⇒ None
The remaining rules in Figure 10.3 follow the same pattern. They only have
to worry about producing errors, not about propagating them.
If we are satisfied that this semantics encodes failure for accessing uninitial-
ized variables, we can proceed to proving correctness of our program analysis
D.
The statement we want in the end is, paraphrasing Milner, well-initialized
programs cannot go wrong.
[[D (dom s) c A 0 ; (c, Some s) ⇒ s 0 ]] =⇒ s 0 6= None
The plan is to use rule induction on the big-step semantics to prove this prop-
erty directly, without the detour over progress and preservation. Looking at
the rules for D A c A 0 , it is clear that we will not be successful with a con-
stant pattern of dom s for A, because the rules produce different patterns.
This means, both A and A 0 need to be variables in the statement to produce
suitably general induction hypotheses. Replacing dom s with a plain variable
A in turn means we have to find a suitable side condition such that our state-
ment remains true, and we have to show that this side condition is preserved.
A suitable such condition is A ⊆ dom s, i.e., it is OK if our program analysis
succeeds with fewer variables than are currently initialized in the state. Af-
ter this process of generalizing the statement for induction, we arrive at the
following lemma.
10.1 Definite Initialization Analysis 153
(c 1 , s 1 ) ⇒ s 2 (c 2 , s 2 ) ⇒ s 3
(c 1 ;; c 2 , s 1 ) ⇒ s 3
bval b s = None
(IF b THEN c 1 ELSE c 2 , Some s) ⇒ None
bval b s = None
(WHILE b DO c, Some s) ⇒ None
more concise, the soundness proof is longer, and while the big-step semantics
has a larger number of rules, its soundness proof is more direct and shorter.
As always, the trade-off depends on the particular application. With machine-
checked proofs, it is in general better to err on the side of nicer and easier-to-
understand definitions than on the side of shorter proofs.
Exercises
thy
10.2 Constant Folding and Propagation
The previous section presented an analysis that prohibits a common pro-
gramming error, uninitialized variables. This section presents an analysis that
enables program optimizations, namely constant folding and propagation.
Constant folding and constant propagation are two very common compiler
optimizations. Constant folding means computing the value of constant ex-
pressions at compile time and substituting their value for the computation.
Constant propagation means determining if a variable has constant value, and
propagating that constant value to the use-occurrences of that variable, for
instance to perform further constant folding:
x := 42 - 5;
y := x * 2
In the first line, the compiler would fold the expression 42 - 5 into its value
37, and in the second line, it would propagate this value into the expression
x * 2 to replace it with 74 and arrive at
x := 37;
y := 74
Further liveness analysis could then for instance conclude that x is not live
in the program and can therefore be eliminated, which frees up one more
register for other local variables and could thereby improve time as well as
space performance of the program.
Constant folding can be especially effective when used on boolean expres-
sions, because it allows the compiler to recognize and eliminate further dead
code. A common pattern is something like
10.2 Constant Folding and Propagation 155
10.2.1 Folding
The definitions and the proof reflect that the constant folding part of
the folding and propagation optimization is the easy part. For more complex
languages, one would have to consider further operators and cases, but nothing
fundamental changes in the structure of proof or definition.
As mentioned, in more complex languages, care must be taken in the def-
inition of constant folding to preserve the failure semantics of that language.
For some languages it is permissible for the compiler to return a valid result
for an invalid program, for others the program must fail in the right way.
10.2.2 Propagation
At this point, we have a function that will fold constants in arithmetic expres-
sions for us. To lift this to commands for full constant propagation, we apply
the same technique, defining a new function fold :: com ⇒ tab ⇒ com. The
idea is to take a command and a constant table and produce a new command.
The first interesting case in any of these analyses usually is assignment. This
is easy here, because we can use afold:
fold (x ::= a) t = x ::= afold a t
What about sequential composition? Given c 1 ;; c 2 and t, we will still need
to produce a new sequential composition, and we will obviously want to use
fold recursively. The question is, which t do we pass to the call fold c 2 for
the second command? We need to pick up any new values that might have
been assigned in the execution of c 1 . This is basically the analysis part of the
optimization, whereas fold is the code adjustment.
We define a new function for this job and call it defs :: com ⇒ tab ⇒
tab for definitions. Given a command and a constant table, it should give us
a new constant table that describes the variables with known constant values
after the execution of this command.
Figure 10.4 shows the main definition. Auxiliary function lvars computes
the set of variables on the left-hand side of assignments (see Appendix A).
Function merge computes the intersection of two tables:
merge t 1 t 2 = (λm. if t 1 m = t 2 m then t 1 m else None)
Let’s walk through the equations of defs one by one.
For SKIP there is nothing to do, as usual.
158 10 Program Analysis
With all these auxiliary definitions in place, our definition of fold is now
as expected. In the WHILE case, we fold the body recursively, but again
restrict the set of variables to those not written to in the body.
10.2 Constant Folding and Propagation 159
For any fixed predicate, our new definition is an equivalence relation, i.e., it
is reflexive, symmetric, and transitive.
Lemma 10.8 (Equivalence Relation).
P |= c ∼ c
(P |= c ∼ c 0 ) = (P |= c 0 ∼ c)
[[P |= c ∼ c 0 ; P |= c 0 ∼ c 0 0 ]] =⇒ P |= c ∼ c 0 0
It is easy to prove that, if we already know that two commands are equivalent
under a condition P, we are allowed to weaken the statement by strengthening
that precondition:
[[P |= c ∼ c 0 ; ∀ s. P 0 s −→ P s]] =⇒ P 0 |= c ∼ c 0
For the old notion of semantic equivalence we had the concept of congruence
rules, where two commands remain equivalent if equivalent sub-commands
are substituted for each other. The corresponding rules in the new setting
are slightly more interesting. Figure 10.5 gives an overview. The first rule, for
sequential composition, has three premises instead of two. The first two are
standard, i.e., equivalence of c and c 0 as well as d and d 0 . As for the sets of
initialized variables in the definite initialization analysis of Section 10.1, we
allow the precondition to change. The first premise gets the same P as the
conclusion P |= c;; d ∼ c 0 ;; d 0 , but the second premise can use a new Q. The
third premise describes the relationship between P and Q: Q must hold in
the states after execution of c, provided P held in the initial state.
The rule for IF is simpler; it just demands that the constituent expres-
sions and commands are equivalent under the same condition P. As for the
10.2 Constant Folding and Propagation 161
P |= c ∼ c 0 Q |= d ∼ d 0 ∀ s s 0 . (c, s) ⇒ s 0 −→ P s −→ Q s 0
P |= c;; d ∼ c 0 ;; d 0
P |= b <∼> b 0 P |= c ∼ c 0 P |= d ∼ d 0
P |= IF b THEN c ELSE d ∼ IF b 0 THEN c 0 ELSE d 0
P |= b <∼> b 0
P |= c ∼ c 0
∀ s s . (c, s) ⇒ s 0 −→ P s −→ bval b s −→ P s 0
0
P |= WHILE b DO c ∼ WHILE b 0 DO c 0
semicolon case, we could provide a stronger rule here that takes into account
which branch of the IF we are looking at, i.e., adding b or ¬ b to the con-
dition P. Since we do not analyse the content of boolean expressions, we will
not need the added power and prefer the weaker, but simpler rule.
The WHILE rule is similar to the semicolon case, but again in a weaker
formulation. We demand that b and b 0 be equivalent under P, as well as c and
c 0 . We additionally need to make sure that P still holds after the execution
of the body if it held before, because the loop might enter another iteration.
In other words, we need to prove as a side condition that P is an invariant
of the loop. Since we only need to know this in the iteration case, we can
additionally assume that the boolean condition b evaluates to true.
This concludes our brief interlude into conditional semantic equivalence.
As indicated in Section 7.2.4, we leave the proof of the rules in Figure 10.5
as an exercise, as well as the formulation of the strengthened rules that take
boolean expressions further into account.
10.2.4 Correctness
So far we have defined constant folding and propagation, and we have devel-
oped a tool set for reasoning about conditional equivalence of commands. In
this section, we apply this tool set to show correctness of our optimization.
As mentioned before, the eventual aim for our correctness statement is
unconditional equivalence between the original and the optimized command:
fold c empty ∼ c
To prove this statement by induction, we generalize it by replacing the
empty table with an arbitrary table t. The price we pay is that the equivalence
is now only true under the condition that the table correctly approximates
the state the commands are run from. The statement becomes
approx t |= c ∼ fold c t
162 10 Program Analysis
Note that the term approx t is partially applied. It is a function that takes a
state s and returns True iff t is an approximation of s as defined previously
in Section 10.2.1. Expanding the definition of equivalence we get the more
verbose but perhaps easier to understand form.
∀ s s 0 . approx t s −→ (c, s) ⇒ s 0 = (fold c t , s) ⇒ s 0
For the proof it is nicer not to unfold the definition equivalence and work with
the congruence lemmas of the previous section instead. Now, proceeding to
prove this property by induction on c it quickly turns out that we will need
four key lemmas about the auxiliary functions mentioned in fold.
The most direct and intuitive one of these is that our defs correctly ap-
proximates real execution. Recall that defs statically analyses which constant
values can be assigned to which variables.
Lemma 10.9 (defs approximates execution correctly).
[[(c, s) ⇒ s 0 ; approx t s]] =⇒ approx (defs c t ) s 0
The last case of our proof above rests on one lemma we have not shown
yet. It says that our restriction to variables that do not occur on the left-hand
sides of assignments is broad enough, i.e., that it appropriately masks any
new table entries we would get by running defs on the loop body.
Lemma 10.10. defs c t (− lvars c) = t (− lvars c)
Proof. This proof is by induction on c. Most cases are automatic, merely
for sequential composition and IF Isabelle needs a bit of hand-holding for
applying the induction hypotheses at the right position in the term. In the
IF case, we also make use of this property of merge:
[[t 1 S = t S ; t 2 S = t S ]] =⇒ merge t 1 t 2 S = t S
It allows us to merge the two equations we get for the two branches of the IF
into one. t
u
The final lemma we need before we can proceed to the main induction is
again a property about the restriction of t to the complement of lvars. It is
the remaining fact we need for the WHILE case of that induction and it says
that runtime execution can at most change the values of variables that are
mentioned on the left-hand side of assignments.
Lemma 10.11.
[[(c, s) ⇒ s 0 ; approx (t (− lvars c) ) s]] =⇒ approx (t (− lvars c) ) s 0
Proof. This proof is by rule induction on the big-step execution. Its cases are
very similar to those of Lemma 10.10. t
u
Putting everything together, we can now prove our main lemma.
Lemma 10.12 (Generalized correctness of constant folding).
approx t |= c ∼ fold c t
Proof. As mentioned, the proof is by induction on c. SKIP is simple, and
assignment reduces to the correctness of afold, Lemma 10.6. Sequential com-
position uses the congruence rule for semicolon and Lemma 10.9. The IF
case is automatic given the IF congruence rule. The WHILE case reduces to
Lemma 10.11, the WHILE congruence rule, and strengthening of the equiv-
alence condition. The strengthening uses the following property
[[approx t 2 s; t 1 ⊆m t 2 ]] =⇒ approx t 1 s
where (m 1 ⊆m m 2 ) = (m 1 = m 2 on dom m 1 ) and t S ⊆m t. t
u
This leads us to the final result.
Theorem 10.13 (Correctness of constant folding).
fold c empty ∼ c
Proof. Follows immediately from Lemma 10.12 after observing that approx
empty = (λ_. True). t
u
164 10 Program Analysis
Exercises
Exercise 10.2. Extend afold with simplifying addition of 0. That is, for any
expression e, e + 0 and 0 + e should be simplified to e, including the case
where the 0 is produced by knowledge of the content of variables. Re-prove
the results in this section with the extended version.
Exercise 10.3. Strengthen and re-prove the congruence rules for conditional
semantic equivalence in Figure 10.5 to take the value of boolean expressions
into account in the IF and WHILE cases.
Exercise 10.4. Extend constant folding with analysing boolean expressions
and eliminate dead IF branches as well as loops whose body is never executed.
Hint: you will need to make use of stronger congruence rules for conditional
semantic equivalence.
Exercise 10.5. This exercise builds infrastructure for Exercise 10.6, where we
will have to manipulate partial maps from variable names to variable names.
In addition to the function merge from theory Fold, implement two func-
tions remove and remove_all that remove one variable name from the range
of a map, and a set of variable names from the domain and range of a map.
Prove the following properties:
ran (remove x t ) = ran t − {x }
ran (remove_all S t ) ⊆ − S
dom (remove_all S t ) ⊆ − S
remove_all {x } (remove x t ) = remove_all {x } t
remove_all A (remove_all B t ) = remove_all (A ∪ B ) t
Reformulate the property [[t 1 S = t S ; t 2 S = t S ]] =⇒ merge t 1 t 2 S = t S
from Lemma 10.10 for remove_all and prove it.
Exercise 10.6. This is a more challenging exercise. Define and prove cor-
rect copy propagation. Copy propagation is similar to constant folding, but
propagates the right-hand side of assignments if these right-hand sides are
just variables. For instance, the program x := y; z := x + z will be trans-
formed into x := y; z := y + z. The assignment x := y can then be elim-
inated in a liveness analysis. Copy propagation is useful for cleaning up after
other optimization phases.
thy
10.3 Live Variable Analysis
This section presents another important analysis that enables program opti-
mizations, namely the elimination of assignments to a variable whose value is
not needed afterwards. Here is a simple example:
10.3 Live Variable Analysis 165
x := 0; y := 1; x := y
The first assignment to x is redundant because x is dead at this point: it is
overwritten by the second assignment to x without x having been read in
between. In contrast, the assignment to y is not redundant because y is live
at that point.
Semantically, variable x is live before command c if the initial value of
x before execution of c can influence the final state after execution of c. A
weaker but easier to check condition is the following: we call x live before c if
there is some potential execution of c where x is read for the first time before
it is overwritten. For the moment, all variables are implicitly read at the end
of c. A variable is dead if it is not live. The phrase “potential execution” refers
to the fact that we do not analyse boolean expressions.
Example 10.14.
x := rhs
Variable x is dead before this assignment unless rhs contains x.
Variable y is live before this assignment if rhs contains y.
IF b THEN x := y ELSE SKIP
Variable y is live before this command because execution could potentially
enter the THEN branch.
x := y; x := 0; y := 1
Variable y is live before this command (because of x := y) although the
value of y is semantically irrelevant because the second assignment over-
writes the first one. This example shows that the above definition of live-
ness is strictly weaker than the semantic notion. We will improve on this
under the heading of “True Liveness” in Section 10.4.
Let us now formulate liveness analysis as a recursive function. This re-
quires us to generalize the liveness notion w.r.t. a set of variables X that are
implicitly read at the end of a command. The reason is that this set changes
during the analysis. Therefore it needs to be a parameter of the analysis and
we speak of the set of variables live before a command c relative to a set of
variables X. It is computed by the function L c X defined like this:
fun L :: com ⇒ vname set ⇒ vname set where
L SKIP X =X
L (x ::= a) X = vars a ∪ (X − {x })
L (c 1 ;; c 2 ) X = L c 1 (L c 2 X )
L (IF b THEN c 1 ELSE c 2 ) X = vars b ∪ L c 1 X ∪ L c 2 X
L (WHILE b DO c) X = vars b ∪ X ∪ L c X
In a nutshell, L c X computes the set of variables that are live before c given
the set of variables X that are live after c (hence the order of arguments in
L c X ).
166 10 Program Analysis
We discuss the equations for L one by one. The one for SKIP is obvious.
The one for x ::= a expresses that before the assignment all variables in a are
live (because they are read) and that x is not live (because it is overwritten)
unless it also occurs in a. The equation for c 1 ;; c 2 expresses that the com-
putation of live variables proceeds backwards. The equation for IF expresses
that the variables of b are read, that b is not analysed and both the THEN
and the ELSE branch could be executed, and that a variable is live if it is
live on some computation path leading to some point — hence the ∪. The
situation for WHILE is similar: execution could skip the loop (hence X is
live) or it could execute the loop body once (hence L c X is live). But what
if the loop body is executed multiple times?
In the following discussion we assume this abbreviation:
w = WHILE b DO c
For a more intuitive understanding of the analysis of loops one should
think of w as the control-flow graph in Figure 10.6. A control-flow graph is
LwX
c
¬b b
X L c (L w X )
a graph whose nodes represent program points and whose edges are labelled
with boolean expressions or commands. The operational meaning is that ex-
ecution moves a state from node to node: a state s moves unchanged across
an edge labelled with b provided bval b s, and moving across an edge labelled
with c transforms s into the new state resulting from the execution of c.
In Figure 10.6 we have additionally annotated each node with the set of
variables live at that node. At the exit of the loop, X should be live, at the
beginning, L w X should be live. Let us pretend we had not defined L w X
e
yet but were looking for constraints that it must satisfy. An edge Y −→ Z
(where e is a boolean expression and Y, Z are liveness annotations) should
satisfy vars e ⊆ Y and Z ⊆ Y (because the variables in e are read but no
variable is written). Thus the graph leads to the following three constraints:
10.3 Live Variable Analysis 167
vars b ⊆LwX
X ⊆LwX (10.1)
L c (L w X ) ⊆ L w X
The first two constraints are met by our definition of L, but for the third
constraint this is not clear. To facilitate proofs about L we now rephrase its
definition as an instance of a general analysis.
This is a class of simple analyses that operate on sets. That is, each analysis
in this class is a function A :: com ⇒ τ set ⇒ τ set (for some type τ) that
can be defined as
A c S = gen c ∪ (S − kill c)
for suitable auxiliary functions gen and kill of type com ⇒ τ set that specify
what is to be added and what is to be removed from the input set. Gen/kill
analyses satisfy nice algebraic properties and many standard analyses can be
expressed in this form, in particular liveness analysis. For liveness, gen c are
the variables that may be read in c before they are written and kill c are the
variables that are definitely written in c:
fun kill :: com ⇒ vname set where
kill SKIP = {}
kill (x ::= a) = {x }
kill (c 1 ;; c 2 ) = kill c 1 ∪ kill c 2
kill (IF b THEN c 1 ELSE c 2 ) = kill c 1 ∩ kill c 2
kill (WHILE b DO c) = {}
Lemma 10.16. L c (L w X ) ⊆ L w X
Moreover, we can prove that L w X is the least solution for the constraint
system (10.1). This shows that our definition of L w X is optimal: the fewer
live variables, the better; from the perspective of program optimization, the
only good variable is a dead variable. To prove that L w X is the least solution
of (10.1), assume that P is a solution of (10.1), i.e., vars b ⊆ P, X ⊆ P and
L c P ⊆ P . Because L c P = gen c ∪ (P − kill c) we also have gen c ⊆
P. Thus L w X = vars b ∪ gen c ∪ X ⊆ P by assumptions.
10.3.2 Correctness
f = g on X ≡ ∀ x ∈X . f x = g x
With this notation we can concisely express that the value of an expression
only depends of the value of the variables in the expression:
Lemma 10.17 (Coincidence).
1. s 1 = s 2 on vars a =⇒ aval a s 1 = aval a s 2
2. s 1 = s 2 on vars b =⇒ bval b s 1 = bval b s 2
c
s s0
on L c X on X
c
t t0
Proof. The proof is by rule induction. The only interesting cases are the
assignment rule, which is correct by the Coincidence Lemma, and rule
WhileTrue. For the correctness proof of the latter we assume its hypothe-
ses bval b s 1 , (c, s 1 ) ⇒ s 2 and (w , s 2 ) ⇒ s 3 . Moreover we assume s 1 =
t 1 on L w X and therefore in particular s 1 = t 1 on L c (L w X ) because
L c (L w X ) ⊆ L w X. Thus the induction hypothesis for (c, s 1 ) ⇒ s 2
applies and we obtain t 2 such that (c, t 1 ) ⇒ t 2 and s 2 = t 2 on L w X. The
latter enables the application of the induction hypothesis for (w , s 2 ) ⇒ s 3 ,
which yields t 3 such that (w , t 2 ) ⇒ t 3 and s 3 = t 3 on X. By means of the
Coincidence Lemma, s 1 = t 1 on L w X and vars b ⊆ L w X imply bval b
s 1 = bval b t 1 . Therefore rule WhileTrue yields (w , t 1 ) ⇒ t 3 as required.
A graphical view of the skeleton of this argument:
c w
s1 s2 s3
on L c (L w X ) on L w X on X
c w
t1 t2 t3
t
u
Note that the proofs of the loop cases (WhileFalse too) do not rely on the
definition of L but merely on the constraints (10.1).
10.3.3 Optimization
With the help of the analysis we can program an optimizer bury c X that
eliminates assignments to dead variables from c where X is of course the set
of variables live at the end.
fun bury :: com ⇒ vname set ⇒ com where
bury SKIP X = SKIP
bury (x ::= a) X = (if x ∈ X then x ::= a else SKIP )
bury (c 1 ;; c 2 ) X = bury c 1 (L c 2 X );; bury c 2 X
bury (IF b THEN c 1 ELSE c 2 ) X =
170 10 Program Analysis
c c
s s0 s s0
on L c X on X on L c X on X
bury c bury c
t t0 t t0
Exercises
Exercise 10.8. Find a command c such that bury (bury c {}) {} 6= bury c
{}. For an arbitrary command, can you put a limit on the amount of burying
needed until everything that is dead is also buried?
Exercise 10.9. Let lvars c/rvars c be the set of variables that occur on
the left-hand/right-hand side of an assignment in c. Let rvars c additionally
include those variables mentioned in the conditionals of IF and WHILE. Both
functions are predefined in theory Vars. Prove the following two properties
of the small-step semantics. Variables that are not assigned to do not change
their value:
[[(c,s) →∗ (c 0 ,s 0 ); lvars c ∩ X = {}]] =⇒ s = s 0 on X
172 10 Program Analysis
thy
10.4 True Liveness
In Example 10.14 we had already seen that our definition of liveness is too
simplistic: in x := y; x := 0, variable y is read before it can be written,
but it is read in an assignment to a dead variable. Therefore we modify the
definition of L (x ::= a) X to consider vars a live only if x is live:
L (x ::= a) X = (if x ∈ X then vars a ∪ (X − {x }) else X )
As a result, our old analysis of loops is no longer correct.
Example 10.22. Consider for a moment the following specific w and c
w = WHILE Less (N 0) (V x ) DO c
c = x ::= V y;; y ::= V z
where x, y and z are distinct. Then L w {x } = {x , y} but z is live too,
semantically: the initial value of z can influence the final value of x. This is
the computation of L: L c {x } = L (x ::= V y) (L (y ::= V z ) {x }) = L (x
::= V y) {x } = {y} and therefore L w {x } = {x } ∪ {x } ∪ L c {x } = {x , y}. The
reason is that L w X = {x ,y} is no longer a solution of the last constraint of
(10.1): L c {x , y} = L (x ::= V y) (L (y ::= V z ) {x , y}) = L (x ::= V y)
{x , z } = {y, z } 6⊆ {x , y}.
10.4 True Liveness 173
Let us now abstract from this example and reconsider (10.1). We still want
L w X to be a solution of (10.1) because the proof of correctness of L depends
on it. An equivalent formulation of (10.1) is
vars b ∪ X ∪ L c (L w X ) ⊆ L w X (10.2)
{P . f P ⊆ P }
T
lfp f =
Proof. This is the same correctness lemma as in Section 10.3, proved in the
same way, but for a modified L. The proof of the WHILE case remains
unchanged because it only relied on the pre-fixpoint constraints (10.1) that
are still satisfied. The proof of correctness of the new definition of L (x ::=
a) X is just as routine as before. t
u
This gives us a way to compute the least fixpoint. In general this will not
terminate, as the sets can grow larger and larger. However, in our application
only a finite set of variables is involved, those in the program. Therefore
termination is guaranteed.
w = WHILE Less (N 0) (V x ) DO c
c = x ::= V y;; y ::= V z
To compute L w {x } we iterate f = (λY . {x } ∪ L c Y ). For compactness the
notation X 2 c 1 X 1 c 2 X 0 (where the X i are sets of variables) abbreviates
X 1 = L c 2 X 0 and X 2 = L c 1 X 1 . Figure 10.9 shows the computation of f
{} through f 4 {}. The final line confirms that {x , y, z } is a fixpoint. Of course
{} x ::= V y {} y ::= V z {}
=⇒ f {} = {x } ∪ {} = {x }
{y} x ::= V y {x } y ::= V z {x }
=⇒ f {x } = {x } ∪ {y} = {x , y}
{y, z } x ::= V y {x , z } y ::= V z {x , y}
=⇒ f {x , y} = {x } ∪ {y, z } = {x , y, z }
{y, z } x ::= V y {x , z } y ::= V z {x , y, z }
=⇒ f {x , y, z } = {x } ∪ {y, z } = {x , y, z }
this is obvious because {x , y, z } cannot get any bigger: it already contains all
the variables of the program.
Let us make the termination argument more precise and derive some con-
crete bounds. We need to compute the least fixpoint of f = (λY . vars b ∪
X ∪ L c Y ). Informally, the chain of the f n {} must stabilize because only a
finite set of variables is involved — we assume that X is finite. In the follow-
ing, let rvars c be the set of variables read in c (see Appendix A). An easy
induction on c shows that L c X ⊆ rvars c ∪ X . Therefore f is bounded
by U = vars b ∪ rvars c ∪ X in the following sense: Y ⊆ U =⇒ f Y ⊆
U. Hence f k {} is a fixpoint of f for some k 6 card U, the cardinality of U.
More precisely, k 6 card (rvars c) + 1 because already in the first step f {}
⊇ vars b ∪ X.
It remains to give an executable definition of L w X. Instead of program-
ming the required function iteration ourselves we use a combinator from the
library theory While_Combinator :
while :: ( 0a ⇒ bool) ⇒ ( 0a ⇒ 0a) ⇒ 0a ⇒ 0a
while b f x = (if b x then while b f (f x ) else x )
The equation makes while executable. Calling while b f x leads to a sequence
of (tail!) recursive calls while b f (f n x ), n = 0, 1, . . ., until b (f k x ) for
some k. The equation cannot be a definition because it may not terminate.
It is a lemma derived from the actual definition; the latter is a bit tricky and
need not concern us here.
10.4 True Liveness 177
Exercises
The proof is straightforward except for the case While b c where reasoning
about lfp is required.
Now idempotence (bury (bury c X ) X = bury c X ) should be easy.
This chapter has explored three different, widely used data-flow analyses
and associated program optimizations: definite initialization analysis, con-
stant propagation, and live variable analysis. They can be classified according
to two criteria:
Forward/backward
A forward analysis propagates information from the beginning to the
end of a program.
A backward analysis propagates information from the end to the be-
ginning of a program.
May/must
A may analysis checks if the given property is true on some path.
A must analysis checks if the given property is true on all paths.
According to this schema
Definite initialization analysis is a forward must analysis: variables must
be assigned on all paths before they are used.
Constant propagation is a forward must analysis: a variable must have the
same constant value on all paths.
Live variable analysis is a backward may analysis: a variable is live if it is
used on some path before it is overwritten.
There are also forward may and backward must analyses.
Data-flow analysis arose in the context of compiler construction and is
treated in some detail in all decent books on the subject, e.g. [2], but in
particular in the book by Muchnik [58]. The book by Nielson, Nielson and
Hankin [61] provides a comprehensive and more theoretical account of pro-
gram analysis.
In Chapter 13 we study “Abstract Interpretation”, a powerful but also com-
plex approach to program analysis that generalizes the algorithms presented
in this chapter.
11
thy
Denotational Semantics
Id :: ( 0a × 0a) set
Id = {p. ∃ x . p = (x , x )}
D :: com ⇒ com_den
D SKIP = Id
D (x ::= a) = {(s, t ). t = s(x := aval a s)}
D (c 1 ;; c 2 ) = D c 1
D c 2
D (IF b THEN c 1 ELSE c 2 )
= {(s, t ). if bval b s then (s, t ) ∈ D c 1 else (s, t ) ∈ D c 2 }
Why the least? The formal justification will be an equivalence proof between
denotational and big-step semantics. The following example provides some
intuition why leastness is what we want.
Proof. For Isabelle, the proof is automatic. The core of the argument rests
on the fact that relation composition is monotone in both arguments: if r ⊆
r 0 and s ⊆ s 0 , then r
s ⊆ r 0
s 0 , which can be seen easily from the
definition of
. If dw ⊆ dw 0 then W db dc dw ⊆ W db dc dw 0 because
dc
dw ⊆ dc
dw 0 . t
u
t = lfp f = f (lfp f ) = f t
where the step lfp f = f (lfp f ) is the consequence of the Knaster-Tarski
Fixpoint Theorem because f is monotone. Setting t = D w and f =
W (bval b) (D c) in t = f t results in (∗) by definition of W.
An immediate consequence is
D (WHILE b DO c) =
D (IF b THEN c;; WHILE b DO c ELSE SKIP )
Just expand the definition of D for IF and ;; and you obtain (∗). This is an
example of the simplicity of deriving program equivalences with the help of
denotational semantics.
Discussion
Why can’t we just define (∗) as it is but have to go through the indirection of
lfp and prove monotonicity of W ? None of this was required for the opera-
tional semantics! The reason for this discrepancy is that inductive definitions
require a fixed format to be admissible. For this fixed format, it is not hard to
prove that the inductively defined predicate actually exists. Isabelle does this
by converting the inductive definition internally into a function on sets, prov-
ing its monotonicity and defining the inductive predicate as the least fixpoint
of that function. The monotonicity proof is automatic provided we stick to
the fixed format. Once you step outside the format, in particular when using
negation, the definition will be rejected by Isabelle because least fixpoints
may cease to exist and the inductive definition may be plain contradictory:
P x =⇒ ¬ P x
¬ P x =⇒ P x
The analogous recursive ‘definition’ is P x = (¬ P x ), which is also rejected
by Isabelle, because it does not terminate.
To avoid the manual monotonicity proof required for our denotational se-
mantics one could put together a collection of basic functions that are all
monotone or preserve monotonicity. One would end up with a little program-
ming language where all functions are monotone and this could be proved
automatically. In fact, one could then even automate the translation of recur-
sion equations like (∗) into lfp format. Creating such a programming language
is at the heart of denotational semantics, but we do not go into it in our brief
introduction to the subject.
In summary: Although our treatment of denotational semantics appears
more complicated than operational semantics because of the explicit lfp, op-
erational semantics is defined as a least fixpoint too, but this is hidden inside
inductive. One can hide the lfp in denotational semantics too and allow direct
11.1 A Relational Denotational Semantics 183
We show that the denotational semantics is logically equivalent with our gold
standard, the big-step semantics. The equivalence is proved as two separate
lemmas. Both proofs are almost automatic because the denotational semantics
is relational and thus close to the operational one. Even the treatment of
WHILE is the same: D w is defined explicitly as a least fixpoint and the
operational semantics is an inductive definition which is internally defined as
a least fixpoint (see the Discussion above).
Lemma 11.4. (c, s) ⇒ t =⇒ (s, t ) ∈ D c
Proof. By rule induction. All cases are automatic. We just look at WhileTrue
where we may assume bval b s 1 and the IHs (s 1 , s 2 ) ∈ D c and (s 2 , s 3 ) ∈
D (WHILE b DO c). We have to show (s 1 , s 3 ) ∈ D (WHILE b DO c),
which follows immediately from (∗). t
u
The other direction is expressed by means of the abbreviation Big_step
introduced at the beginning of this chapter. The reason is purely technical.
Lemma 11.5. (s, t ) ∈ D c =⇒ (s, t ) ∈ Big_step c
Proof. By induction on c. All cases are proved automatically except w =
WHILE b DO c, which we look at in detail. Let B = Big_step w and f =
W (bval b) (D c). By definition of W and the big-step ⇒ it follows that
B is a pre-fixpoint of f, i.e., f B ⊆ B : given (s, t ) ∈ f B, either bval b s
and there is some s 0 such that (s, s 0 ) ∈ D c (hence (c, s) ⇒ s 0 by IH) and
(w , s 0 ) ⇒ t, or ¬ bval b s and s = t ; in either case (w , s) ⇒ t, i.e., (s, t )
∈ B. Because D w is the least fixpoint and also the least pre-fixpoint of f
(see Knaster-Tarski), D w ⊆ B and hence (s, t ) ∈ D w =⇒ (s, t ) ∈ B as
claimed. t
u
The combination of the previous two lemma yields the equivalence:
Theorem 11.6 (Equivalence of denotational and big-step semantics).
(s, t ) ∈ D c ←→ (c, s) ⇒ t
As a nice corollary we obtain that the program equivalence ∼ defined in
Section 7.2.4 is the same as denotational equality: if you replace (c i , s) ⇒ t
in the definition of c 1 ∼ c 2 by (s, t ) ∈ D c i this yields ∀ s t . (s, t ) ∈ D c 1
←→ (s, t ) ∈ D c 2 , which is equivalent with D c 1 = D c 2 because two sets
are equal iff they contain the same elements.
Corollary 11.7. c 1 ∼ c 2 ←→ D c 1 = D c 2
184 11 Denotational Semantics
11.1.2 Continuity
To understand why these notions are relevant for us, think in terms of
relations between states, or, for simplicity, input and output of some com-
putation. For example, the input-output behaviour of a function sum that
sums up the first n numbers can be expressed as this infinite relation Sum
= {(0,0), (1,1), (2,3), (3,6), (4,10), . . .} on nat. We can compute this relation
gradually by starting from {} and adding more pairs in each step: {} ⊆ {(0,0)}
⊆ {(0,0), (1,1)} ⊆ . . .. This is why chains are relevant. Each element S n in the
chain is only a finite approximation of the full semantics of the summation
S
function which is the infinite set n S n.
To understand the computational meaning of monotonicity, consider a sec-
ond summation function sum 2 with semantics Sum 2 = {(0, 0), (1, 1), (2,
3)}, i.e., sum 2 behaves like sum for inputs 6 2 and does not terminate oth-
erwise. Let P[·] be a program where we can plug in different subcomponents.
11.1 A Relational Denotational Semantics 185
but ( n T (S n)) = {}. Going back to the P[·] scenario above, continuity
S
= U
186 11 Denotational Semantics
Proof. Although the Isabelle proof is automatic, we explain the details be-
cause they may not be obvious. Let R :: nat ⇒ com_den be any sequence
of state relations — it does not even need to be a chain. We show that (s, t )
∈ W b r ( n R n) iff (s, t ) ∈ ( n W b r (R n)). If ¬ b s then (s, t ) is
S S
←→ ∃ s 0 n. (s, s 0 ) ∈ r ∧ (s 0 , t ) ∈ R n
←→ ∃ n. (s, t ) ∈ r
R n
S
←→ (s, t ) ∈ ( n r
R n)
S
←→ (s, t ) ∈ ( n W b r (R n))
Warning: such ←→ chains are an abuse of notation: A ←→ B ←→ C really
means the logically not equivalent (A ←→ B ) ∧ (B ←→ C ) which implies
A ←→ C. t
u
(because f is also monotone) the f n {} form a chain. All its elements are single-
valued because {} is single-valued and f preserves single-valuedness. The union
of a chain of single-valued relations is obviously single-valued too. t
u
Exercises
Exercise 11.1. Building on Exercise 7.8, extend the denotational semantics
and the equivalence proof with the big-step semantics with a REPEAT loop.
Exercise 11.2. Consider Example 11.14 and prove by induction on n that
f n {} = {(s, t ). 0 6 s 0 0x 0 0 ∧ s 0 0x 0 0 < int n ∧ t = s( 0 0x 0 0 := 0)}.
Exercise 11.3. Consider Example 11.14 but with the loop condition b =
Less (N 0) (V 0 0x 0 0 ). Find a closed expression M (containing n) for f n {}
and prove f n {} = M.
Exercise 11.4. Define an operator B such that you can express the equation
for D (IF b THEN c 1 ELSE c 2 ) in a point-free way. In this context, we call
a definition point free if it does not mention the state on the left-hand side.
For example:
D (IF b THEN c 1 ELSE c 2 ) = B b O D c 1 ∪ B (Not b) O D c 2
A point-wise definition would start
D (IF b THEN c 1 ELSE c 2 ) s = . . .
Similarly, find a point-free equation for W (bval b) dc and use it to write
down a point-free version of D (WHILE b DO c) (still using lfp). Prove that
your two equations are equivalent to the old ones.
11.2 Summary and Further Reading 189
Exercise 11.5. Let the ‘thin’ part of a relation be its single-valued subset:
thin R = {(a, b). (a, b) ∈ R ∧ (∀ c. (a, c) ∈ R −→ c = b)}
Prove that if f :: ( 0a ∗ 0a) set ⇒ ( 0a ∗ 0a) set is monotone and for all R,
f (thin R) ⊆ thin (f R), then single_valued (lfp f ).
and prove
lemma [[ (c,s) ⇒ s 0 ; (c,t ) ⇒ t 0 ; s = t on Deps c X ]] =⇒ s 0 = t 0 on X
Give an example that the following stronger termination-sensitive property
[[(c, s) ⇒ s 0 ; s = t on Deps c X ]] =⇒ ∃ t 0 . (c, t ) ⇒ t 0 ∧ s 0 = t 0 on X
does not hold. Hint: X = {}.
In the definition of Dep (IF b THEN c 1 ELSE c 2 ) the variables in b can
influence all variables (UNIV ). However, if a variable is not assigned to in c 1
and c 2 , it is not influenced by b (ignoring termination). Theory Vars defines
a function lvars such that lvars c is the set of variables on the left-hand side
of an assignment in c. Modify the definition of Dep as follows: replace UNIV
190 11 Denotational Semantics
thy
12.1 Proof via Operational Semantics
Before introducing the details of Hoare logic we show that in principle we can
prove properties of programs via their operational semantics. Hoare logic can
be viewed as the structured essence of such proofs.
As an example, we prove that the program
y := 0;
WHILE 0 < x DO (y := y+x; x := x-1)
sums up the numbers 1 to x in y. Formally let
0 0 00
wsum = WHILE Less (N 0) (V x ) DO csum
0 0 00 0 0 00
csum = y ::= Plus (V y ) (V 0 0x 0 0 );;
0 0 00 0 0 00
x ::= Plus (V x ) (N (−1))
192 12 Hoare Logic
sum (s 0 0x 0 0 ) = 0.
If the loop condition is true, i.e., if 0 < s 0 0x 0 0 , then we may assume
(csum, s) ⇒ u and the IH t 0 0y 0 0 = u 0 0y 0 0 + sum (u 0 0x 0 0 ) and we have
to prove the conclusion of (∗∗). From (csum, s) ⇒ u it follows by inversion
of the rules for ;; and ::= (Section 7.2.3) that u = s( 0 0y 0 0 := s 0 0y 0 0 + s 0 0x 0 0 ,
0 0 00
x := s 0 0x 0 0 − 1). Substituting this into the IH yields t 0 0y 0 0 = s 0 0y 0 0 +
s 0 0x 0 0 + sum (s 0 0x 0 0 − 1). This is equivalent with the conclusion of (∗∗)
because 0 < s 0 0x 0 0 .
Having proved (∗∗), (∗) follows easily: From ( 0 0y 0 0 ::= N 0;; wsum, s) ⇒
t it follows by rule inversion that after the assignment the intermediate state
must have been s( 0 0y 0 0 := 0) and therefore (wsum, s( 0 0y 0 0 := 0)) ⇒ t. Now
(∗∗) implies t 0 0y 0 0 = sum (s 0 0x 0 0 ), thus concluding the proof of (∗).
Hoare logic can be viewed as the structured essence of such operational
proofs. The rules of Hoare logic are (almost) syntax-directed and automate
all those aspects of the proof that are concerned with program execution.
However, there is no free lunch: you still need to be creative to find gen-
eralizations of formulas when proving properties of loops, and proofs about
arithmetic formulas are still up to you (and Isabelle).
We will now move on to the actual topic of this chapter, Hoare logic.
The formulas of Hoare logic are the Hoare triples {P } c {Q}, where P is called
the precondition and Q the postcondition. We call {P } c {Q} valid if the
following implication holds:
12.2 Hoare Logic for Partial Correctness 193
Assertions are ordinary logical formulas and include all the boolean expres-
sions of IMP. We are deliberately vague because the exact nature of assertions
is not important for understanding how Hoare logic works. Just as we did for
the simplified notation for IMP, we write concrete assertions and Hoare triples
in typewriter font. Here are some examples of valid Hoare triples:
{x = 5} x := x+5 {x = 10}
{True} x := 10 {x = 10}
{x = y} x := x+1 {x 6= y}
Note that the precondition True is always true; hence the second triple merely
says that from whatever state you start, after x := 10 the postcondition x =
10 is true.
More interesting are the following somewhat extreme examples:
{True} c 1 {True}
194 12 Hoare Logic
{True} c 2 {False}
{False} c 3 {Q}
Which c i make these triples valid? Think about it before you read on. Remem-
ber that we work with partial correctness in this section. Therefore every c 1
works because the postcondition True is always true. In the second triple, c 2
must not terminate, otherwise False would have to be true, which it certainly
is not. In the final triple, any c 3 and Q work because the meaning of the triple
is that “if False is true . . . ”, but False is not true. Note that for the first two
triples, the answer is different under a total correctness interpretation.
Proof System
So far we have spoken of Hoare triples being valid. Now we will present a set
of inference rules or proof system for deriving Hoare triples. This is a new
mechanism and here we speak of Hoare triples being derivable. Of course
being valid and being derivable should have something to do with each other.
When we look at the proof rules in a moment they will all feel very natural
(well, except for one) precisely because they follow our informal understanding
of when a triple is valid. Nevertheless it is essential not to confuse the notions
of validity (which employs the operational semantics) and derivability (which
employs an independent set of proof rules).
The proof rules for Hoare logic are shown in Figure 12.1. We go through
them one by one.
The SKIP rule is obvious, but the assignment rule needs some explana-
tion. It uses the substitution notation
P [a/x ] ≡ P with a substituted for x.
For example, (x = 5)[5/x] is 5 = 5 and (x = x)[5+x/x] is 5+x = 5+x.
The latter example shows that all occurrences of x in P are simultaneously
replaced by a, but that this happens only once: if x occurs in a, those oc-
currences are not replaced, otherwise the substitution process would go on
forever. Here are some instances of the assignment rule:
{5 = 5} x := 5 {x = 5}
{x+5 = 5} x := x+5 {x = 5}
{2*(x+5) > 20} x := 2*(x+5) {x > 20}
Simplifying the preconditions that were obtained by blind substitution yields
the more readable triples
{True} x := 5 {x = 5}
{x = 0} x := x+5 {x = 5}
{x > 5} x := 2*(x+5) {x > 20}
12.2 Hoare Logic for Partial Correctness 195
{P } SKIP {P }
{P [a/x ]} x ::= a {P }
{P 1 } c 1 {P 2 } {P 2 } c 2 {P 3 }
{P 1 } c 1 ;; c 2 {P 3 }
{P ∧ b} c 1 {Q} {P ∧ ¬ b} c 2 {Q}
{P } IF b THEN c 1 ELSE c 2 {Q}
{P ∧ b} c {P }
{P } WHILE b DO c {P ∧ ¬ b}
P 0 −→ P {P } c {Q} Q −→ Q 0
{P 0 } c {Q 0 }
The assignment rule may still puzzle you because it seems to go in the wrong
direction by modifying the precondition rather than the postcondition. After
all, the operational semantics modifies the post-state, not the pre-state. Cor-
rectness of the assignment rule can be explained as follows: if initially P [a/x ]
is true, then after the assignment x will have the value of a, and hence no
substitution is necessary anymore, i.e., P itself is true afterwards. A forward
version of this rule exists but is more complicated.
The ;; rule strongly resembles its big-step counterpart. Reading it back-
ward it decomposes the proof of c 1 ;; c 2 into two proofs involving c 1 and c 2
and a new intermediate assertion P 2 .
The IF rule is pretty obvious: you need to prove that both branches lead
from P to Q, where in each proof the appropriate b or ¬ b can be conjoined
to P. That is, each sub-proof additionally assumes the branch condition.
Now we consider the WHILE rule. Its premise says that if P and b are
true before the execution of the loop body c, then P is true again afterwards
(if the body terminates). Such a P is called an invariant of the loop: if you
start in a state where P is true then no matter how often the loop body is
iterated, as long as b is true before each iteration, P stays true too. This
explains the conclusion: if P is true initially, then it must be true at the end
because it is invariant. Moreover, if the loop terminates, then ¬ b must be
196 12 Hoare Logic
true too. Hence P ∧ ¬ b at the end (if we get there). The WHILE rule can
be viewed as an induction rule where the invariance proof is the step.
The final rule in Figure 12.1 is called the consequence rule. It is indepen-
dent of any particular IMP construct. Its purpose is to adjust the precondition
and postcondition. Going from {P } c {Q} to {P 0 } c {Q 0 } under the given
premises permits us to
strengthen the precondition: P 0 −→ P
weaken the postcondition: Q −→ Q 0
where A is called stronger than B if A −→ B. For example, from {x > 0}
c {x > 1} we can prove {x = 5} c {x > 0}. The latter is strictly weaker
than the former because it tells us less about the behaviour of c. Note that the
consequence rule is the only rule where some premises are not Hoare triples
but assertions. We do not have a formal proof system for assertions and rely on
our informal understanding of their meaning to check, for example, that x = 5
−→ x > 0. This informality will be overcome once we consider assertions as
predicates on states.
This completes the discussion of the basic proof rules. Although these
rules are sufficient for all proofs, i.e., the system is complete (which we show
later), the rules for SKIP, ::= and WHILE are inconvenient: they can only
be applied backwards if the pre- or postcondition are of a special form. For
example, for SKIP they need to be identical. Therefore we derive new rules for
those constructs that can be applied backwards irrespective of the pre- and
postcondition of the given triple. The new rules are shown in Figure 12.2.
They are easily derived by combining the old rules with the consequence rule.
P −→ Q
{P } SKIP {Q}
P −→ Q[a/x ]
{P } x ::= a {Q}
{P ∧ b} c {P } P ∧ ¬ b −→ Q
{P } WHILE b DO c {Q}
Two of the three premises are overlined because they have been proved,
namely with the original assignment rule and with the trivial logical fact
that anything implies itself.
Examples
We return to the summation program from Section 12.1. This time we prove
it correct by means of Hoare logic rather than operational semantics. In Hoare
logic, we want to prove the triple
{x = i} y := 0; wsum {y = sum i}
We cannot write y = sum x in the postcondition because x is 0 at that point.
Unfortunately the postcondition cannot refer directly to the initial state. In-
stead, the precondition x = i allows us to refer to the unchanged i and
therefore to the initial value of x in the postcondition. This is a general trick
for remembering values of variables that are modified.
The central part of the proof is to find and prove the invariant I of the
loop. Note that we have three constraints that must be satisfied:
1. It should be an invariant: {I ∧ 0 < x} csum {I}
2. It should imply the postcondition: I ∧ ¬ 0 < x −→ y = sum i
3. The invariant should be true initially: x = i ∧ y = 0 −→ I
In fact, this is a general design principle for invariants. As usual, it is a case
of generalizing the desired postcondition. During the iteration, y = sum i is
not quite true yet because the first x numbers are still missing from y. Hence
we try the following assertion:
I = (y + sum x = sum i)
It is easy to check that the constraints 2 and 3 are true. Moreover, I is indeed
invariant as the following proof tree shows:
I ∧ 0 < x −→ I[x-1/x][y+x/y]
{I ∧ 0 < x} y := y+x {I[x-1/x]} {I[x-1/x]} x := x-1 {I}
{I ∧ 0 < x} csum {I}
Although we have not given the proof rules names, it is easy to see at any
point in the proof tree which one is used. In the above tree, the left assignment
is proved with the derived rule, the right assignment with the basic rule.
In case you are wondering why I ∧ 0 < x −→ I[x-1/x][y+x/y] is true,
expand the definition of I and carry out the substitutions and you arrive at
the following easy arithmetic truth:
198 12 Hoare Logic
Now we only need to connect this result with the initialization to obtain
the correctness proof for y := 0; wsum:
x = i −→ I[0/y]
{x = i} y := 0 {I} {I} wsum {y = sum i}
{x = i} y := 0; wsum {y = sum i}
We have proved that if the loop terminates, any assertion Q is true. It sounds
like magic but is merely the consequence of nontermination. The proof is
straightforward: the invariant is True and Q is trivially implied by the nega-
tion of the loop condition.
As a final example consider swapping two variables:
{P} h := x; x := y; y := h {Q}
Both Q[h/y] and Q[h/y][y/x] are simply the result of the basic assign-
ment rule. All that is left to check is the first assignment with the derived
assignment rule, i.e., check P −→ Q[h/y][y/x][x/h]. This is true because
Q[h/y][y/x][x/h] = (y = b ∧ x = a).
It should be clear that this proof procedure works for any sequence of
assignments, thus reducing the proof to pulling back the postcondition (which
is completely mechanical) and checking an implication.
12.2 Hoare Logic for Partial Correctness 199
The Method
If we look at the proof rules and the examples it becomes apparent that there
is a method in this madness: the backward construction of Hoare logic proofs
is partly mechanical. Here are the key points:
We only need the original rules for ;; and IF together with the derived rules
for SKIP, ::= and WHILE. This is a syntax-directed proof system and
each backward rule application creates new subgoals for the subcommands.
Thus the shape of the proof tree exactly mirrors the shape of the command
in the Hoare triple we want to prove. The construction of the skeleton of
this proof tree is completely automatic.
The consequence rule is built into the derived rules and is not required any-
more. This is crucial: the consequence rule destroys syntax-directedness
because it can be applied at any point.
When applying the ;; rule backwards we need to provide the intermediate
assertion P 2 that occurs in the premises but not the conclusion. It turns
out that we can compute P 2 by pulling the final assertion P 3 back through
c 2 . The variable swapping example illustrates this principle.
There are two aspects that cannot be fully automated (or program veri-
fication would be completely automatic, which is impossible): invariants
must be supplied explicitly, and the implications between assertions in the
premises of the derived rules must be proved somehow.
In a nutshell, Hoare logic can be reduced to finding invariants and proving
assertions. We will carry out this program in full detail in Section 12.4. But
first we need to formalize our informal notion of assertions.
Skip
` {P } SKIP {P }
Assign
` {λs. P (s[a/x ])} x ::= a {P }
` {λs. P s ∧ bval b s} c {P }
While
` {P } WHILE b DO c {λs. P s ∧ ¬ bval b s}
∀ s. P 0 s −→ P s ` {P } c {Q} ∀ s. Q s −→ Q 0 s
conseq
` {P } c {Q }
0 0
of the syntactic ones, taking into account that assertions are predicates on
states. Only rule Assign requires some explanation. The notation s[a/x ] is
merely an abbreviation that mimics syntactic substitution into assertions:
s[a/x ] ≡ s(x := aval a s)
What does our earlier P[a/x] have to do with P (s[a/x ])? We have not for-
malized the syntax of assertions, but we can explain what is going on at the
level of their close relatives, boolean expressions. Assume we have a substi-
tution function bsubst such that bsubst b a x corresponds to b[a/x], i.e.,
substitutes a for x in b. Then we can prove
Lemma 12.1 (Substitution lemma).
bval (bsubst b a x ) s = bval b (s[a/x ])
We can now perform Hoare logic proofs in Isabelle. For that purpose we go
back to the apply-style because it allows us to perform such proofs without
having to type in the myriad of intermediate assertions. Instead they are
12.2 Hoare Logic for Partial Correctness 201
∀ s. P s −→ Q (s[a/x ])
Assign 0
` {P } x ::= a {Q}
A total of 4 subgoals...
202 12 Hoare Logic
Now the two assignment rules (basic and derived) do their job.
apply(rule Assign)
apply(rule Assign 0 )
The resulting subgoal is large and hard to read because of the substitutions;
therefore we do not show it. It corresponds to (12.1) and can be proved by
simp (not shown). We move on to the second premise of While 0 , the proof
that at the exit of the loop the required postcondition is true:
1. ∀ s. s 0 0y 0 0 = sum i − sum (s 0 0x 0 0 ) ∧
¬ bval (Less (N 0) (V 0 0x 0 0 )) s −→
s 0 0y 0 0 = sum i
A total of 2 subgoals...
This is proved by simp and all that is left is the initialization.
1. ` {λs. s 0 0 00
x = i } 0 0y 0 0 ::= N 0
{λs. s 0 0 00
y = sum i − sum (s 0 0 00
x )}
apply(rule Assign 0 )
The resulting subgoal shows a simple example of substitution into the state:
1. ∀ s. s 0 0x 0 0 = i −→
(s[N 0/ 0 0y 0 0 ]) 0 0y 0 0 = sum i − sum ((s[N 0/ 0 0y 0 0 ]) 0 0 00
x )
The proof is again a plain simp.
Functional assertions lead to more verbose statements. For the verification of
larger programs one would add some Isabelle syntax magic to make functional
assertions look more like syntactic ones. We have refrained from that as our
emphasis is on explaining Hoare logic rather than verifying concrete programs.
Exercises
Exercise 12.2. Define bsubst and prove the Substitution Lemma 12.1. This
may require a similar definition and proof for aexp.
Exercise 12.3. Define a command cmax that stores the maximum of the
values of the IMP variables x and y in the IMP variable z and prove
that ` {λs. True} cmax {λs. s 0 0z 0 0 = max (s 0 0x 0 0 ) (s 0 0y 0 0 )} where max
is the predefined maximum function.
12.3 Soundness and Completeness 203
Exercise 12.6. Define a command cmult that stores the product of x and y
in z (assuming 0 6 y) and prove ` {λs. s 0 0x 0 0 = x ∧ s 0 0y 0 0 = y ∧ 0 6 y}
cmult {λt . t 0 0z 0 0 = x ∗ y}.
Exercise 12.9. Design and prove a forward assignment rule of the form
` {P } x ::= a {? } where ? is some suitable postcondition that depends on
P, x and a.
thy
12.3 Soundness and Completeness
12.3.1 Completeness
Proof. By induction on c. We consider only the WHILE case, the other cases
are automatic (with the help of the wp equations). Let w = WHILE b DO c.
We show ` {wp w Q} w {Q} by an application of rule While 0 . Its first
premise is ` {λs. wp w Q s ∧ bval b s} c {wp w Q}. It follows from the IH
` {wp c R} c {R} (for any R) where we set R = wp w Q, by precondition
strengthening: the implication wp w Q s ∧ bval b s −→ wp c (wp w Q) s
follows from the wp equations for WHILE and ;;. The second premise we
need to prove is wp w Q s ∧ ¬ bval b s −→ Q s; it follows from the wp
equation for WHILE. t
u
12.3.2 Incompleteness
Exercises
Exercise 12.13. Based on Exercise 7.9, extend Hoare logic and the soundness
and completeness proofs with nondeterministic choice.
Exercise 12.14. Based on Exercise 7.8, extend Hoare logic and the soundness
and completeness proofs with a REPEAT loop.
thy
12.4 Verification Condition Generation
This section shows what we have already hinted at: Hoare logic can be au-
tomated. That is, we reduce provability in Hoare logic to provability in the
assertion language, i.e., HOL in our case. Given a triple {P } c {Q} that we
want to prove, we show how to compute an assertion A from it such that
` {P } c {Q} is provable iff A is provable.
We call A a verification condition and the function that computes A
a verification condition generator or VCG. The advantage of working
with a VCG is that no knowledge of Hoare logic is required by the person or
machine that attempts to prove the generated verification conditions. Most
systems for the verification of imperative programs are based on VCGs.
Our VCG works like The Method for Hoare logic we sketched above: it
simulates the backward application of Hoare logic rules and gathers up the
implications between assertions that arise in the process. Of course there is the
problem of loop invariants: where do they come from? We take the easy way
out and let the user provide them. In general this is the only feasible solution
because we cannot expect the machine to come up with clever invariants in all
situations. In Chapter 13 we will present a method for computing invariants
in simple situations.
Invariants are supplied to the VCG as annotations of WHILE loops. For
that purpose we introduce a type acom of annotated commands with the
same syntax as that of type com, except that WHILE is annotated with an
assertion Inv :
{Inv } WHILE b DO C
To distinguish variables of type com and acom, the latter are capitalised.
Function strip :: acom ⇒ com removes all annotations from an annotated
command, thus turning it into an ordinary command.
Verification condition generation is based on two functions: pre is similar
to wp, vc is the actual VCG.
fun pre :: acom ⇒ assn ⇒ assn where
pre SKIP Q =Q
pre (x ::= a) Q = (λs. Q (s[a/x ]))
pre (C 1 ;; C 2 ) Q = pre C 1 (pre C 2 Q)
pre (IF b THEN C 1 ELSE C 2 ) Q
= (λs. if bval b s then pre C 1 Q s else pre C 2 Q s)
pre ({I } WHILE b DO C ) Q = I
Function pre follows the recursion equations for wp except in the WHILE
case where the annotation is returned. If the annotation is an invariant then
it must also hold before the loop and thus it makes sense for pre to return it.
12.4 Verification Condition Generation 209
The same conditions arose on pages 197f. in the Hoare logic proof of the triple
{I} wsum {y = sum i} and we satisfied ourselves that they are true.
The example has demonstrated the computation of vc and pre. The gener-
ated verification condition turned out to be true, but it remains unclear what
that proves. We need to show that our VCG is sound w.r.t. Hoare logic. This
will allow us to reduce the problem of proving ` {P } c {Q} to the problem of
proving the verification condition. We will obtain the following result:
Corollary 12.8.
[[vc C Q; ∀ s. P s −→ pre C Q s]] =⇒ ` {P } strip C {Q}
This can be read as a procedure for proving ` {P } c {Q}:
1. Annotate c with invariants, yielding C such that strip C = c.
2. Prove the verification condition vc C Q and that P implies pre C Q.
The actual soundness lemma is a bit more compact than its above corollary
which follows from it by precondition strengthening.
Proof. By induction on c. The WHILE case is routine, the other cases are
automatic. t
u
True∧True −→ True
{True∧True} x:=0 {True} True∧¬True −→ False
x=1−→True {True} WHILE True DO x:=0 {False}
{x=1} WHILE True DO x := 0 {False}
Exercises
Exercise 12.17. Solve Exercises 12.4 to 12.7 using the VCG: for every Hoare
triple ` {P } c {Q} from one of those exercises define an annotated version C
of c and prove ` {P } strip C {Q} with the help of Corollary 12.8.
Exercise 12.19. Design a VCG that computes post- rather than precondi-
tions. Start by solving Exercise 12.9. Now modify theory VCG as follows.
Instead of pre define a function post :: acom ⇒ assn ⇒ assn such that
212 12 Hoare Logic
thy
12.5 Hoare Logic for Total Correctness
Example 12.12. We redo the proof of wsum from Section 12.2.2. The only
difference is that when applying rule While_fun (combined with postcon-
dition strengthening as in rule While 0 ) we need not only instantiate P as
previously but also f : f = (λs. nat (s 0 0x 0 0 )) where the predefined func-
tion nat coerces an int into a nat, coercing all negative numbers to 0. The
resulting invariance subgoal now looks like this:
The rest of the proof steps are again identical to the partial correctness
proof. After pulling back the postcondition the additional conjunct in the goal
is nat (s 0 0x 0 0 − 1) < n which follows automatically from the assumptions
0 < s 0 0x 0 0 and n = nat (s 0 0x 0 0 ).
Proof. By rule induction. All cases are automatic except rule While, which
we look at in detail. Let w = WHILE b DO c. By a (nested) induction on n
we show for arbitrary n and s
[[P s; T s n]] =⇒ ∃ t . (w , s) ⇒ t ∧ P t ∧ ¬ bval b t (∗)
from which |=t {λs. P s ∧ (∃ n. T s n)} w {λs. P s ∧ ¬ bval b s}, the goal
in the While case, follows immediately. The inductive proof assumes that (∗)
holds for all smaller n. For n itself we argue by cases. If ¬ bval b s then
(w , s) ⇒ t is equivalent with t = s and (∗) follows. If bval b s then we
assume P s and T s n. The outer IH |=t {λs. P s ∧ bval b s ∧ T s n} c
{λs 0 . P s 0 ∧ (∃ n 0 . T s 0 n 0 ∧ n 0 < n)} yields s 0 and n 0 such that (c, s)
⇒ s 0 , P s 0 , T s 0 n 0 and n 0 < n. Because n 0 < n the inner IH yields the
required t such that (w , s 0 ) ⇒ t, P t and ¬ bval b t. With rule WhileTrue
the conclusion of (∗) follows. t
u
The completeness proof proceeds like the one for partial correctness. The
weakest precondition is now defined in correspondence to |=t :
wp t c Q = (λs. ∃ t . (c, s) ⇒ t ∧ Q t )
The same recursion equations as for wp can also be proved for wp t . The
crucial lemma is again this one:
Exercises
Exercise 12.21. Modify the VCG from Section 12.4 to take termination into
account. First modify type acom by annotating WHILE with a measure
function f :: state ⇒ nat in addition to an invariant:
{I , f } WHILE b DO C
Functions strip and pre remain almost unchanged. The only significant
change is in the WHILE case for vc. Finally update the old soundness proof
to obtain vc C Q =⇒ `t {pre C Q} strip C {Q}. You may need the combined
soundness and completeness of `t : (`t {P } c {Q}) = (|=t {P } c {Q}).
This chapter was dedicated to Hoare logic and the verification of IMP pro-
grams. We covered three main topics:
A Hoare logic for partial correctness and its soundness and completeness
w.r.t. the big-step semantics.
A verification condition generator that reduces the task of verifying a
program by means of Hoare logic to the task of annotating all loops in that
program with invariants and proving that these annotations are indeed
invariants and imply the necessary postconditions.
A Hoare logic for total correctness and its soundness and completeness.
Hoare logic is a huge subject area and we have only scratched the surface.
Therefore we provide further references for the theory and applications of
Hoare logic.
12.6.1 Theory
12.6.2 Applications
Our emphasis on foundational issues and toy examples is bound to give the
reader the impression that Hoare logic is not practically useful. Luckily, that
is not the case.
Among the early practical program verification tools are the KeY [10]
and KIV [73] systems. The KeY system can be used to reason about Java
programs. One of the recent larger applications of the KIV system is for
instance the verification of file-system code in operating systems [29].
Hoare logic-based tools also exist for concurrent programs. VCC [19], for
instance, is integrated with Microsoft Visual Studio and can be used to verify
concurrent C. Cohen et al. have used VCC to demonstrate the verification of a
small operating system hypervisor [4]. Instead of interactive proof, VCC aims
for automation and uses the SMT solver Z3 [25] as back-end. The interaction
with the tool consists of annotating invariants and function pre- and postcon-
ditions in such a way that they are simple enough for the prover to succeed
automatically. The automation is stronger than in interactive proof assistants
like Isabelle, but it forces specifications to be phrased in first-order logic. The
Dafny tool [52] uses the same infrastructure. It does not support concurrency,
but is well suited for learning this paradigm of program verification.
12.6 Summary and Further Reading 217
The term “alarm” describes the situation where the analysis finds an erroneous
state, in which case it raises an alarm, typically by flagging the program
point in question. In the No Alarm situation, the analysis does not find any
erroneous states and everybody is happy. In the True Alarm situation, the
analysis finds erroneous states and some reachable states are indeed erroneous.
But due to overapproximation, there are also so-called false alarms: the
analysis finds an erroneous state that cannot arise at this program point. False
alarms are the bane of all program analyses. They force the programmer to
convince himself that the potential error found is not a real error but merely
a weakness of the analysis.
{I }
WHILE b DO {P } c
{Q}
where I (as in “invariant”) annotates the loop head, P annotates the point
before the loop body c, and Q annotates the exit of the whole loop. The
annotation points are best visualized by means of the control-flow graph in
Figure 13.1. We introduced control-flow graphs in Section 10.3. In fact, Fig-
c
¬b b
Q P
ure 13.1 is the same as Figure 10.6, except that we now label nodes not with
sets of live variables but with the annotations at the corresponding program
points. Edges labelled by compound commands stand for whole subgraphs.
Now we move from the semantics to abstract interpretation in two steps. First
we replace sets of states with single states that map variables to sets of values:
(vname ⇒ val) set becomes vname ⇒ val set.
Our first example above now looks much simpler:
x := 0 {<x := {0}>} ;
{x := {0, 2, 4}>}
WHILE x < 3
DO {<x := {0, 2}>}
x := x+2 {<x := {2, 4}>}
{<x := {4}>}
However, this simplification comes at a price: it is an overapproximation that
loses relationships between variables. For example, {<x := 0, y := 0>, <x :=
1, y := 1>} is overapproximated by <x := {0, 1}, y := {0, 1}>. The latter
also subsumes <x := 0, y := 1> and <x := 1, y := 0>.
In the second step we replace sets of values by “abstract values”. This
step is domain specific. For example, we can approximate sets of integers by
intervals. For the above example we obtain the following consistent annotation
with integer intervals (written [low , high]):
x := 0 {<x := [0, 0]>} ;
{<x := [0, 4]>}
WHILE x < 3
DO {<x := [0, 2]>}
13.1 Informal Introduction 223
0 1 2 3 4 5 6 7 8 9
A0 ⊥ [0, 0] [0, 0]
A1 ⊥ [0, 0] [0, 2] [0, 4] [0, 4]
A2 ⊥ [0, 0] [0, 2] [0, 2]
A3 ⊥ [2, 2] [2, 4] [2, 4]
A4 ⊥ [3, 4]
Instead of the full annotation <x := ivl>, the table merely shows ivl. Col-
umn 0 shows the initial annotations where ⊥ represents the empty interval.
Unchanged entries are left blank. In steps 1–3, [0, 0] is merely propagated
around. In step 4, 2 is added to it, making it [2, 2]. The crucial step is from 4
to 5: [0, 0], the invariant A1 in the making, is combined with the annotation
[2, 2] at the end of the loop body, telling us that the value of x at A1 can in
fact be any value from the union [0, 0] and [2, 2]. This is overapproximated
by the interval [0, 2]. The same thing happens again in step 8, where the
invariant becomes [0, 4]. In step 9, the final annotation A4 is reached at last:
the invariant is now [0, 4] and intersecting it with the negation of x < 3 lets
[3, 4] reach the end of the loop. The annotations reached in step 9 (which are
displayed in full) are stable: performing another step leaves them unchanged.
224 13 Abstract Interpretation
This is the end of our informal introduction and we become formal again.
First we define the type of annotated commands, then the collecting seman-
tics, and finally abstract interpretation. Most of the chapter is dedicated to
the stepwise development of a generic abstract interpreter whose precision is
gradually increased.
The rest of this chapter builds on Section 10.4.1.
thy
13.2 Annotated Commands
We exclusively use the concrete syntax and do not show the actual datatype.
Variables C, C 1 , C 0 , etc. will henceforth stand for annotated commands.
The layout of IF and WHILE is suggestive of the intended meaning of the
annotations but has no logical significance. We have already discussed the
annotations of WHILE but not yet of IF :
IF b THEN {P 1 } C 1 ELSE {P 2 } C 2
{Q}
Annotation P i refers to the state before the execution of C i . Annotation {Q}
is placed on a separate line to emphasize that it refers to the state after the
execution of the whole conditional, not just the ELSE branch. The corre-
sponding annotated control-flow graph is shown in Figure 13.2.
Our annotated commands are polymorphic because, as we have already
seen, we will want to annotate programs with different objects: with sets
of states for the collecting semantics and with abstract states, for example,
involving intervals, for abstract interpretation.
We now introduce a number of simple and repeatedly used auxiliary func-
tions. Their functionality is straightforward and explained in words. In case
of doubt, you can consult their full definitions in Appendix A.
13.3 Collecting Semantics 225
b ¬b
P1 P2
C1 C2
thy
13.3 Collecting Semantics
The aim is to annotate commands with the set of states that can occur at
each annotation point. The annotations are generated iteratively by a func-
226 13 Abstract Interpretation
fun step :: state set ⇒ state set acom ⇒ state set acom where
step S (SKIP {Q}) = SKIP {S }
step S (x ::= e {Q}) = x ::= e {{s(x := aval e s) |s. s ∈ S }}
step S (C 1 ;; C 2 ) = step S C 1 ;; step (post C 1 ) C 2
step S (IF b THEN {P 1 } C 1 ELSE {P 2 } C 2 {Q})
= IF b THEN {{s ∈ S . bval b s}} step P 1 C 1
ELSE {{s ∈ S . ¬ bval b s}} step P 2 C 2
{post C 1 ∪ post C 2 }
step S ({I } WHILE b DO {P } C {Q})
= {S ∪ post C }
WHILE b
DO {{s ∈ I . bval b s}}
step P C
{{s ∈ I . ¬ bval b s}}
In the SKIP and the assignment case, the input set S is transformed
and replaces the old post-annotation Q: for SKIP the transformation is the
identity, x ::= e transforms S by updating all of its elements (remember the
set comprehension syntax explained in Section 4.2).
In the following let S27 be the (somewhat arbitrary) state set {<x := 2>,
<x := 7>}. It is merely used to illustrate the behaviour of step on the various
constructs. Here is an example for assignment:
step S27 (x ::= Plus (V x ) (N 1) {Q}) =
x ::= Plus (V x ) (N 1) {{<x := 3>, <x := 8>}}
When applied to C 1 ;; C 2 , step executes C 1 and C 2 simultaneously: the
input to the execution of C 2 is the post-annotation of C 1 , not of step S C 1 .
For example:
13.3 Collecting Semantics 227
x := 0 {A0 } ;
{A1 }
WHILE x < 3
DO {A2 } x := x+2 {A3 }
{A4 }
In a separate table we can see how the annotations change with each iteration
of step S (where S is irrelevant as long as it is not empty).
0 1 2 3 4 5 6 7 8 9
A0 {} {0} {0}
A1 {} {0} {0,2} {0,2,4} {0,2,4}
A2 {} {0} {0,2} {0,2}
A3 {} {2} {2,4} {2,4}
A4 {} {4}
Instead of the full annotation {<x := i 1 >,<x :=i 2 >,. . .}, the table merely
shows {i 1 ,i 2 ,. . .}. Unchanged entries are left blank. In steps 1–3, {0} is merely
propagated around. In step 4, 2 is added to it, making it {2}. The crucial
step is from 4 to 5: {0}, the invariant A1 in the making, is combined with the
annotation {2} at the end of the loop body, yielding {0,2}. The same thing
happens again in step 8, where the invariant becomes {0,2,4}. In step 9, the
final annotation A4 is reached at last: intersecting the invariant {0,2,4} with
the negation of x < 3 lets {4} reach the exit of the loop. The annotations
reached in step 9 (which are displayed in full) are stable: performing another
step leaves them unchanged.
In contrast to the interval analysis in Section 13.1, the semantics is and has
to be exact. As a result, it is not computable in general. The above example
is particularly simple in a number of respects that are all interrelated: the
initial set S plays no role because x is initialized, all annotations are finite,
and we reach a fixpoint after a finite number of steps. Most of the time we
will not be so lucky.
If we only deal with finite sets of states, we can let Isabelle execute step for us.
Of course we again have to print states explicitly because they are functions.
This is encapsulated in the function
show_acom :: state set acom ⇒ (vname × val)set set acom
that turns a state into a set of variable-value pairs. We reconsider the example
program above, but now in full Isabelle syntax:
13.3 Collecting Semantics 229
definition Cex :: state set acom where Cex = annotate (λp. {}) cex
{{}}
WHILE Less (V 0 0x 0 0 ) (N 3)
DO {{}}
x ::= Plus (V 0 0x 0 0 ) (N 2) {{}}
0 0 00
{{}}
The triply nested braces are the combination of the outer annotation braces
with annotations that are sets of sets of variable-value pairs, the result of
converting sets of states into printable form.
You can iterate step by means of the function iteration operator f ^^ n
(pretty-printed as f n ). For example, executing four steps
value show_acom ((step {<>} ^^ 4) Cex )
yields
x ::= N 0 {{{( 0 0x 0 0 , 0)}}};;
0 0 00
{{{( 0 0x 0 0 , 0)}}}
WHILE Less (V 0 0x 0 0 ) (N 3)
DO {{{( 0 0x 0 0 , 0)}}}
x ::= Plus (V 0 0x 0 0 ) (N 2) {{{( 0 0x 0 0 , 2)}}}
0 0 00
{{}}
Iterating step {<>} nine times yields the fixpoint shown in the table above:
x ::= N 0 {{{( 0 0x 0 0 , 0)}}};;
0 0 00
{{{( x , 4)}}}
0 0 00
230 13 Abstract Interpretation
At the end of this section we prove that the least fixpoint is consistent with
the big-step semantics.
∀ s ∈ S. S 6 s
d
If ∀ s∈S . l 0 6 s then l 0 6 S
d
If ∀ s∈S . s 6 u then S 6 u
F
The least upper bound is the greatest lower bound of all upper bounds:
S = {u. ∀ s ∈ S . s 6 u}.
F d
The proof is left as an exercise. Thus complete lattices can be defined via the
existence of all infima or all suprema or both.
It follows that a complete lattice does not only have a least element
d F
UNIV, the infimum of all elements, but also a greatest element UNIV.
They are denoted by ⊥ and > (pronounced “bottom” and “top”). Figure 13.4
shows a typical complete lattice and motivates the name “lattice”.
The generalization of the Knaster-Tarski fixpoint theorem (Theorem 10.29)
from sets to complete lattices is straightforward:
Theorem 13.3 (Knaster-Tarski1 ). Every monotone function f on a
{p. f p 6 p}.
d
complete lattice has the least (pre-)fixpoint
1
Tarski [85] actually proved that the set of fixpoints of a monotone function on a
complete lattice is itself a complete lattice.
232 13 Abstract Interpretation
>
@
@
@ @@
@
@ @ @
@ @@ @@ @
@
@ @ @ @ @
@ @@ @@ @@ @ @
@ @
@ @@ @@ @@ @ @@
@ @ @ @ @@
@ @ @
@ @@ @@@@
@ @@ @
@ @@
@
⊥
d
The proof is the same as for the set version but with 6 and instead of ⊆
T T
and . This works because M is the greatest lower bound of M w.r.t. ⊆.
Because (in Isabelle) ⊆ is just special syntax for 6 on sets, the above defi-
nition lifts ⊆ to a partial order 6 on state set acom. Unfortunately this order
does not turn state set acom into a complete lattice: although SKIP {S } and
SKIP {T } have the infimum SKIP {S ∩ T }, the two commands SKIP {S }
and x ::= e {T } have no lower bound, let alone an infimum. The fact is:
Only structurally equal annotated commands have an infimum.
Below we show that for each c :: com the set {C :: 0a acom. strip C =
c} of all annotated commands structurally equal to c is a complete lattice,
13.3 Collecting Semantics 233
The proof is the same as before, but we have to keep track of the fact that
we always stay within L, which is ensured by f ∈ L → L and M ⊆ L =⇒
d
M ∈ L.
To apply Knaster-Tarski to state set acom we need to show that acom
transforms complete lattices into complete lattices as follows:
Theorem 13.7. Let 0a be a complete lattice and c :: com. Then the set
L = {C :: 0a acom. strip C = c} is a complete lattice.
We have made explicit the dependence on the command c from the statement
of the theorem. The infimum properties of Inf_acom follow easily from the
d
corresponding properties of . The proofs are automatic. t
u
Because type state set is a complete lattice, Theorem 13.7 implies that L =
{C :: state set acom. strip C = c} is a complete lattice too, for any c. It
is easy to prove (Lemma 13.26) that step S ∈ L → L is monotone: C 1 6
C 2 =⇒ step S C 1 6 step S C 2 . Therefore Knaster-Tarski tells us that lfp
returns the least fixpoint and we can define the collecting semantics at last:
The extra argument c of lfp comes from the fact that lfp is defined in The-
orem 13.6 in the context of a set L and our concrete L depends on c. The
UNIV argument of step expresses that execution may start with any initial
state; other choices are possible too.
Function CS is not executable because the resulting annotations are usu-
ally infinite state sets. But just as for true liveness in Section 10.4.2, we can
approximate (and sometimes reach) the lfp by iterating step. We already
showed how that works in Section 13.3.1.
In contrast to previous operational semantics that were “obviously right”,
the collecting semantics is more complicated and less intuitive. In case you
still have some nagging doubts that it is defined correctly, the following lemma
should help. Remember that post extracts the final annotation (Appendix A).
Lemma 13.8. (c, s) ⇒ t =⇒ t ∈ post (CS c)
Proof. By definition of CS the claim follows directly from [[(c, s) ⇒ t ; s ∈ S ]]
=⇒ t ∈ post (lfp c (step S )), which in turn follows easily from two auxiliary
propositions:
post (lfp c f ) = {post C |C . strip C = c ∧ f C 6 C }
T
of post and Inf_acom. The second proposition is the key. It is proved by rule
induction. Most cases are automatic, but WhileTrue needs some work. t
u
Thus we know that the collecting semantics overapproximates the big-step
semantics. Later we show that the abstract interpreter overapproximates the
collecting semantics. Therefore we can view the collecting semantics merely
as a stepping stone for proving that the abstract interpreter overapproximates
the big-step semantics, our standard point of reference.
One can in fact show that both semantics are equivalent. One can also
refine the lemma: it only talks about the post annotation but one would like
to know that all annotations are correct w.r.t. the big-step semantics. We do
not pursue this further but move on to the main topic of this chapter, abstract
interpretation.
Exercises
The exercises below are conceptual and should be done on paper, also because
many of them require Isabelle material that will only be introduced later.
Exercise 13.1. Show the iterative computation of the collecting semantics of
the following program in a table like the one on page 228.
13.3 Collecting Semantics 235
x := 0; y := 2 {A0 } ;
{A1 }
WHILE 0 < y
DO {A2 } ( x := x+y; y := y - 1 {A3 } )
{A4 }
Note that two annotations have been suppressed to make the task less tedious.
You do not need to show steps where only the suppressed annotations change.
Exercise 13.2. Extend type acom and function step with a construct OR
for the nondeterministic choice between two commands (see Exercise 7.9).
Hint: think of OR as a nondeterministic conditional without a test.
S = {u. ∀ s∈S . s 6 u}
F d
Exercise 13.3. Prove that in a complete lattice
is the least upper bound of S.
Exercise 13.4. Where is the mistake in the following argument? The natural
numbers form a complete lattice because any set of natural numbers has an
infimum, its least element.
Exercise 13.5. Show that the integers extended with ∞ and −∞ form a
complete lattice.
Exercise 13.6. Prove the following slightly generalized form of the Knaster-
Tarski pre-fixpoint theorem: If P is a set of pre-fixpoints of a monotone func-
d
tion on a complete lattice, then P is a pre-fixpoint too. In other words, the
set of pre-fixpoints of a monotone function on a complete lattice is a complete
lattice.
Exercise 13.8. The term collecting semantics suggests that the reachable
states are collected in the following sense: step should not transform {S } into
{S 0 } but into {S ∪ S 0 }. Show that this makes no difference. That is, prove
that if f is a monotone function on sets, then f has the same least fixpoint as
λS . S ∪ f S.
236 13 Abstract Interpretation
thy
13.4 Abstract Values
The topic of this section is the abstraction from the state sets in the collecting
semantics to some type of abstract values. In Section 13.1.2 we had already
discussed a first abstraction of state sets
(vname ⇒ val) set vname ⇒ val set
and that it constitutes a first overapproximation. There are so-called relational
analyses that avoid this abstraction, but they are more complicated. In a
second step, sets of values are abstracted to abstract values:
val set abstract domain
where the abstract domain is some type of abstract values that we can
compute on. What exactly that type is depends on the analysis. Interval
analysis is one example, parity analysis is another. Parity analysis determines
if the value of a variable at some point is always even, is always odd, or can
be either. It is based on the following abstract domain
datatype parity = Even | Odd | Either
that will serve as our running example.
Abstract values represent sets of concrete values val and need to come with
an ordering that is an abstraction of the subset ordering. Figure 13.5 shows
γ_parity
Either Z
⊇
⊆
6
>
Even Odd 2Z 2Z + 1
the abstract values on the left and the concrete ones on the right, where 2Z
and 2Z + 1 are the even and the odd integers. The solid lines represent the
orderings on the two types. The dashed arrows represent the concretisation
function:
13.4 Abstract Values 237
Lemma 13.10. x t y 6 z ←→ x 6 z ∧ y 6 z
The proof is left as an exercise.
In addition, we will need an abstract value that corresponds to UNIV.
Definition 13.11. A partial order has a top element > if x 6 > for all
x.
Thus we will require our abstract domain to be a semilattice with top element,
or semilattice with > for short.
Of course, type parity is a semilattice with >:
238 13 Abstract Interpretation
We will now sketch how abstract concepts like semilattices are modelled with
the help of Isabelle’s type classes. A type class has a name and is defined by
a set of required functions, the interface, and
a set of axioms about those functions.
For example, a partial order requires that there is a function 6 with certain
properties. The type classes introduced above are called order, semilattice_sup,
top, and semilattice_sup_top.
To show that a type τ belongs to some class C we have to
define all functions in the interface of C on type τ
and prove that they satisfy the axioms of C.
This process is called instantiating C with τ. For example, above we have
instantiated semilattice_sup_top with parity. Informally we say that parity
is a semilattice with >.
Note that the function definitions made when instantiating a class are
unlike normal function definitions because we define an already existing but
overloaded function (e.g., 6) for a new type (e.g., parity).
The instantiation of semilattice_sup_top with parity was unconditional.
There is also a conditional version exemplified by the following proposition:
require one. If we drop the type annotations altogether, both the definition
and the lemma statement work fine but in the definition the implicitly gener-
ated type variable 0a will be of class ord, where ord is a predefined superclass
of order that merely requires an operation 6 without any axioms. This means
that the lemma will not be provable.
Note that it suffices to constrain one occurrence of a type variable in a given
type. The class constraint automatically propagates to the other occurrences
of that type variable.
We do not describe the precise syntax for defining and instantiating classes.
The semi-formal language used so far is perfectly adequate for our pur-
pose. The interested reader is referred to the Isabelle theories (in particular
Abs_Int1_parity to see how classes are instantiated) and to the tutorial on
type classes [40]. If you are familiar with the programming language Haskell
you will recognize that Isabelle provides Haskell-style type classes extended
with axioms.
type_synonym 0a st = vname ⇒ 0a
but there is a complication: the empty set of states has no meaningful ab-
straction. The empty state set is important because it arises at unreachable
program points, and identifying the latter is of great interest for program
optimization. Hence we abstract state set by
0
a st option
where None is the abstraction of {}. That is, the abstract interpretation will
compute with and annotate programs with values of type 0a st option instead
of state set. We will now lift the semilattice structure from 0a to 0a st option.
All the proofs in this subsection are routine and we leave most of them as
exercises whose solutions can be found in the Isabelle theories.
Because states are functions we show as a first step that function spaces
preserve semilattices:
0
Lemma 13.13. If a is a semilattice with >, so is 0b ⇒ 0a, for every
type 0b.
f 6 g = ∀x. f x 6 g x
f t g = λx . f x t g x
> = λx . >
It is easily shown that the result is a semilattice with > if 0a is one. For
example, f 6 f t g holds because f x 6 f x t g x = (f t g) x for all x. t u
> >
a b Some a Some b
c Some c
None
without modifying the ordering between them and None is adjoined as the
least element.
The semilattice properties of 0a option are proved by exhaustive case
analyses. As an example consider the proof of x 6 x t y. In each of the cases
x = None, y = None and x = Some a ∧ y = Some b it holds by definition
and because a 6 a t b on type 0a. t
u
Exercises
thy
13.5 Generic Abstract Interpreter
In this section we define a first abstract interpreter which we refine in later
sections. It is generic because it is parameterized with the abstract domain.
It works like the collecting semantics but operates on the abstract domain
instead of state sets. To bring out this similarity we first abstract the collecting
semantics’ step function.
242 13 Abstract Interpretation
suppressed the parameters asem and bsem of Step in the figure for better
readability. In reality, they are there and step is defined like this:
step =
Step (λx e S . {s(x := aval e s) |s. s ∈ S }) (λb S . {s ∈ S . bval b s})
(This works because in Isabelle ∪ is just nice syntax for t on sets.) The
equations in Figure 13.3 are merely derived from this definition. This way we
avoided having to explain the more abstract Step early on.
with abstractions of all operations on the concrete type val = int. The result
of the module is an abstract interpreter that operates on the given abstract
domain.
In detail, the parameters and their required properties are the following:
Abstract domain A type 0av of abstract values.
Must be a semilattice with >.
Concretisation function γ :: 0av ⇒ val set
Must be monotone: a 1 6 a 2 =⇒ γ a 1 ⊆ γ a 2
Must preserve >: γ > = UNIV
Abstract arithmetic num 0 :: val ⇒ 0av
plus 0 :: 0av ⇒ 0av ⇒ 0av
Must establish and preserve the i ∈ γ a relationship:
i ∈ γ (num 0 i ) (num 0 )
[[i 1 ∈ γ a 1 ; i 2 ∈ γ a 2 ]] =⇒ i 1 + i 2 ∈ γ (plus 0 a 1 a 2 ) (plus 0 )
Remarks:
Every constructor of aexp (except V ) must have a counterpart on 0av.
num 0 and plus 0 abstract N and Plus.
The requirement i ∈ γ (num 0 i ) could be replaced by γ (num 0 i ) = {i }.
We have chosen the weaker formulation to emphasize that all operations
must establish or preserve i ∈ γ a.
Functions whose names end with a prime usually operate on 0av.
Abstract values are usually called a whereas arithmetic expressions are
usually called e now.
From γ we define three lifted concretisation functions:
plus 0 = plus_parity
where
num_parity i = (if i mod 2 = 0 then Even else Odd)
plus_parity Even Even = Even
plus_parity Odd Odd = Even
plus_parity Even Odd = Odd
plus_parity Odd Even = Odd
plus_parity x Either = Either
plus_parity Either y = Either
We had already discussed that parity is a semilattice with > and γ_parity is
monotone. Both γ_parity > = UNIV and (num 0 ) are trivial, as is (plus 0 )
after an exhaustive case analysis on a 1 and a 2 .
Let us call the result aval 0 of this module instantiation aval_parity. Then
the following term evaluates to Even:
aval_parity (Plus (V 0 0 00
x ) (V y )) (λ_. Odd)
0 0 00
The abstract interpreter for commands is defined in two steps. First we in-
stantiate Step to perform one interpretation step, later we iterate this step 0
until a pre-fixpoint is found.
step 0 :: 0av st option ⇒ 0av st option acom ⇒ 0av st option acom
step 0 = Step asem (λb S . S )
where
13.5 Generic Abstract Interpreter 245
asem x e S =
(case S of None ⇒ None | Some S ⇒ Some (S (x := aval 0 e S )))
Remarks:
From now on the identifier S will (almost) always be of type 0av st or
0
av st option.
Function asem updates the abstract state with the abstract value of the
expression. The rest is boiler-plate to handle the option type.
Boolean expressions are not analysed at all. That is, they have no effect
on the state. Compare λb S . S with λb S . {s ∈ S . bval b s} in the
collecting semantics. The former constitutes a gross overapproximation to
be refined later.
x := 3 {None} ; Odd
{None} Odd
WHILE ...
DO {None} Odd
x := x+2 {None} Odd
{None} Odd
In the table on the right we iterate step_parity, i.e., we see how the annota-
tions in (step_parity >)k C 0 change with increasing k (where C 0 is the
initial program with Nones, and by definition > = (λ_. Either )). Each row
of the table refers to the program annotation in the same row. For compact-
ness, a parity value p represents the state Some (>(x := p)). We only show
an entry if it differs from the previous step. After four steps, there are no
more changes; we have reached the least fixpoint.
Exercise: What happens if the 2 in the program is replaced by a 1?
The abstract interpreter iterates step 0 with the help of a library function:
while_option :: ( 0a ⇒ bool) ⇒ ( 0a ⇒ 0a) ⇒ 0a ⇒ 0a option
while_option b f x = (if b x then while_option b f (f x ) else Some x )
The equation is an executable consequence of the (omitted) definition.
This is a generalization of the while combinator used in Section 10.4.2
for the same purpose as now: iterating a function. The difference is that
while_option returns an optional value to distinguish termination (Some)
from nontermination (None). Of course the execution of while_option will
simply not terminate if the mathematical result is None.
The abstract interpreter AI is a search for a pre-fixpoint of step 0 >:
13.5.6 Monotonicity
Although we know that if pfp terminates, by construction it yields a pre-
fixpoint, we don’t yet know under what conditions it terminates and which
pre-fixpoint is found. This is where monotonicity of Step comes in:
Lemma 13.26 (Monotonicity of Step). Let 0a be a semilattice and let
f :: vname ⇒ aexp ⇒ 0a ⇒ 0a and g :: bexp ⇒ 0a ⇒ 0a be two mono-
tone functions: S 1 6 S 2 =⇒ f x e S 1 6 f x e S 2 and S 1 6 S 2 =⇒
g b S 1 6 g b S 2 . Then Step f g is also monotone, in both arguments:
[[ C 1 6 C 2 ; S 1 6 S 2 ]] =⇒ Step f g S 1 C 1 6 Step f g S 2 C 2 .
Proof. The proof is a straightforward computation induction on Step. Addi-
tionally it needs the easy lemma C 1 6 C 2 =⇒ post C 1 6 post C 2 . t
u
As a corollary we obtain the monotonicity of step (already used in the
collecting semantics to guarantee that step has a least fixed point) because
step is defined as Step f g for some monotone f and g in Section 13.5.1. Sim-
ilarly we have step 0 = Step asem (λb S . S ). Although λb S . S is obviously
monotone, asem is not necessarily monotone. The monotone framework is
the extension of the interface to our abstract interpreter with a monotonicity
assumption for abstract arithmetic:
Abstract arithmetic must be monotone:
[[a 1 6 b 1 ; a 2 6 b 2 ]] =⇒ plus 0 a 1 a 2 6 plus 0 b 1 b 2
Monotonicity of aval 0 follows by an easy induction on e:
S 1 6 S 2 =⇒ aval 0 e S 1 6 aval 0 e S 2
In the monotone framework, step 0 is therefore monotone too, in both argu-
ments. Therefore step 0 > is also monotone.
Monotonicity is not surprising and is only a means to obtain more interest-
ing results, in particular precision and termination of our abstract interpreter.
For the rest of this section we assume that we are in the context of the mono-
tone framework.
248 13 Abstract Interpretation
13.5.7 Precision
13.5.8 Termination
Assume the conditions of the lemma hold for f and x 0 . Because pfp f
x 0 computes the f i x 0 until a pre-fixpoint is found, we know that the f i x 0
form a strictly increasing chain. Therefore we can prove termination of this
loop by exhibiting a measure function m into the natural numbers such that
x < y =⇒ m x > m y. The latter property is called anti-monotonicity.
The following lemma summarizes this reasoning. Note that the relativisa-
tion to a set L is necessary because in our application the measure function
will not be be anti-monotone on the whole type.
Lemma 13.29. Let 0a be a partial order, let L :: 0a set, let f ∈ L →
L be a monotone function, let x 0 ∈ L such that x 0 6 f x 0 , and let
m :: 0a ⇒ nat be anti-monotone on L. Then ∃ p. pfp f x 0 = Some p,
i.e., pfp f x 0 terminates.
In our concrete situation f is step 0 >, x 0 is bot c, and we now construct a
measure function m c such that C 1 < C 2 =⇒ m c C 1 > m c C 2 (for certain
C i ). The construction starts from a measure function on 0av that is lifted to
0
av st option acom in several steps.
We extend the interface to the abstract interpretation module by two more
parameters that guarantee termination of the analysis:
Measure function and height: m :: 0av ⇒ nat
h :: nat
Must be anti-monotone and bounded:
a 1 < a 2 =⇒ m a 1 > m a 2
ma 6h
Under these assumptions the ordering 6 on 0av is of height at most h: every
chain a 0 < a 1 < . . . < a n has height at most h, i.e., n 6 h. That is, 6 on
0
av is of finite height.
Let us first sketch the intuition behind the termination proof. The anno-
tations in an annotated command can be viewed as a big tuple of the abstract
values of all variables at all annotation points. For example, if the program has
two annotations A1 and A2 and three variables x, y, z, then Figure 13.8 depicts
some assignment of abstract values a i to the three variables at the two anno-
tation points. The termination measure is the sum of all m a i . Lemma 13.28
A1 A2
x y z x y z
a1 a2 a3 a4 a5 a6
the ordering strictly increases, i.e., (a 1 ,a 2 ,. . .) < (b 1 ,b 2 ,. . .), which for tuples
means a i 6 b i for all i and a k < b k for some k. Anti-monotonicity implies
both m a i > m b i and m a k > m b k . Therefore the termination measure
strictly decreases. Now for the technical details.
We lift m in three stages to 0av st option acom:
into account that X are the variables in the command and that therefore the
variables not in X do not change. This “does not change” property (relating
two states) can more simply be expressed as “is top” (a property of one state):
variables not in the program can only ever have the abstract value > because
we iterate step 0 >. Therefore we define three “is top” predicates relative to
some set of variables X :
With the help of these predicates we can now formulate that all three measure
functions are anti-monotone:
[[finite X ; S 1 = S 2 on − X ; S 1 < S 2 ]] =⇒ m s S 2 X < m s S 1 X
[[finite X ; top_on o o 1 (− X ); top_on o o 2 (− X ); o 1 < o 2 ]] =⇒ m o o 2
X < m o o1 X
[[top_on c C 1 (− vars C 1 ); top_on c C 2 (− vars C 2 ); C 1 < C 2 ]] =⇒
mc C 2 < mc C 1
The proofs involve simple reasoning about sums.
Now we can apply Lemma 13.29 to AI c = pfp (step 0 >) (bot c) to
conclude that AI always delivers:
Theorem 13.30. ∃ C . AI c = Some C
Proof. In Lemma 13.29, let L be {C . top_on c C (− vars C )} and let m be
m c . Above we have stated that m c is anti-monotone on L. Now we examine
that the remaining conditions of the lemma are satisfied. We had shown al-
ready that step 0 > is monotone. Showing that it maps L to L requires a little
lemma
top_on c C (− vars C ) =⇒ top_on c (step 0 > C ) (− vars C )
together with the even simpler lemma strip (step 0 S C ) = strip C (∗).
Both follow directly from analogous lemmas about Step which are proved by
computation induction on Step. Condition bot c 6 step 0 > (bot c) follows
from the obvious strip C = c =⇒ bot c 6 C with the help of (∗) above. And
bot c ∈ L holds because top_on c (bot c) X is true for all X by definition. t
u
252 13 Abstract Interpretation
Example 13.31. Parity analysis can be shown to terminate because the parity
semilattice has height 1. Defining the measure function
m_parity a = (if a = Either then 0 else 1)
Either is given measure 0 because it is the largest and therefore least infor-
mative abstract value. Both anti-monotonicity of m_parity and m_parity a
6 1 are proved automatically.
Note that finite height of 6 is actually a bit stronger than necessary to
guarantee termination. It is sufficient that there is no infinitely ascending
chain a 0 < a 1 < . . .. But then there can be ascending chains of any finite
height and we cannot bound the number of iterations of the abstract inter-
preter, which is problematic in practice.
13.5.9 Executability
Above we have shown that for semilattices of finite height, fixpoint itera-
tion of the abstract interpreter terminates. Yet there is a problem: AI is
not executable! This is why: pfp compares programs with annotations of
type 0av st option; but S 1 6 S 2 (where S 1 , S 2 :: 0av st ) is defined as
∀ x . S 1 x 6 S 2 x where x comes from the infinite type vname. Therefore 6
on 0av st is not directly executable.
We learn two things: we need to refine type st such that 6 becomes ex-
ecutable (this is the subject to the following section), and we need to be
careful about claiming termination. We had merely proved that some term
while_option b f s is equal to Some. This implies termination only if b, f
and s are executable, which the given b is not. In general, there is no logical
notion of executability and termination in HOL and such claims are informal.
They could be formalized in principle but this would require a formalization
of the code generation process and the target language.
Exercises
Exercise 13.10. Redo Example 13.22 but replace the 2 in the program by 1.
Exercise 13.11. Take the Isabelle theories that define commands, big-step
semantics, annotated commands and the collecting semantics and extend
them with a nondeterministic choice construct as in Exercise 13.2.
The following exercises require class constraints like 0a :: order as intro-
duced in Section 13.4.1.
Exercise 13.12. Prove Lemma 13.27 and Lemma 13.28 in a detailed and
readable style. Remember that f i x is input as (f ^^ i ) x.
13.6 Executable Abstract States 253
thy
13.6 Executable Abstract States
This section is all about a clever representation of abstract states. We define
a new type 0a st that is a semilattice where 6, t and > are executable
(provided 0a is a semilattice with executable operations). Moreover it supports
two operations that behave like function application and function update:
fun :: 0a st ⇒ vname ⇒ 0a
update :: 0a st ⇒ vname ⇒ 0a ⇒ 0a st
This exercise in data refinement is independent of abstract interpretation
and a bit technical in places. Hence it can be skipped on first reading. The
key point is that our generic abstract interpreter from the previous section
only requires two modifications: S x is replaced by fun S x and S (x :=
a) is replaced by update S x a. The proofs carry over either verbatim or
they require only local modifications. We merely need one additional lemma
which reduces fun and update to function application and function update:
fun (update S x y) = (fun S )(x := y).
Before we present the new definition of 0a st, we look at two applications,
parity analysis and constant propagation.
{Some {( 0 0x 0 0 , Odd)}}
WHILE Less (V 0 0x 0 0 ) (N 100)
DO {Some {( 0 0x 0 0 , Odd)}}
x ::= Plus (V 0 0x 0 0 ) (N 3) {Some {( 0 0x 0 0 , Even)}}
0 0 00
{Some {( 0 0x 0 0 , Odd)}}
254 13 Abstract Interpretation
Now the Even feeds back into the invariant and waters it down to Either :
x ::= N 1 {Some {( 0 0x 0 0 , Odd)}};;
0 0 00
{Some {( 0 0x 0 0 , Odd)}}
A few more steps to propagate Either and we reach the least fixpoint:
x ::= N 1 {Some {( 0 0x 0 0 , Odd)}};;
0 0 00
Any
In symbols:
(x 6 y) = (y = Any ∨ x = y)
> = Any
IF Less (N 41) (V 0 0x 0 0 )
THEN {Some {( 0 0x 0 0 , Const 42)}} 0 0x 0 0 ::= N 5 {Some {( 0 0x 0 0 , Const 5)}}
ELSE {Some {( 0 0x 0 0 , Const 42)}} 0 0x 0 0 ::= N 6 {Some {( 0 0x 0 0 , Const 6)}}
{Some {( 0 0x 0 0 , Any)}}
256 13 Abstract Interpretation
This is where the analyser from Section 10.2 beats our abstract interpreter.
Section 13.8 will rectify this deficiency.
As an exercise, compute the following result step by step:
x ::= N 0 {Some {( 0 0x 0 0 , Const 0), ( 0 0y 0 0 , Any), ( 0 0z 0 0 , Any)}};;
0 0 00
less_eq_st_rep ps 1 ps 2 =
(∀ x ∈set (map fst ps 1 ) ∪ set (map fst ps 2 ).
fun_rep ps 1 x 6 fun_rep ps 2 x )
Unfortunately this is not a partial order (which is why we gave it the name
less_eq_st_rep instead of 6). For example, both [( 0 0x 0 0 , a), ( 0 0y 0 0 , b)] and
[( 0 0y 0 0 , b), ( 0 0x 0 0 , a)] represent the same function and the less_eq_st_rep
relationship between them holds in both directions, but they are distinct
association lists.
Therefore we define the actual new abstract state type as a quotient type.
In mathematics, the quotient of a set M by some equivalence relation ∼ (see
Definition 7.7) is written M /∼ and is the set of all equivalence classes [m]∼
⊆ M, m ∈ M, defined by [m]∼ = {m 0 . m ∼ m 0 }. In our case the equivalence
relation is eq_st and it identifies two association lists iff they represent the
same function:
eq_st ps 1 ps 2 = (fun_rep ps 1 = fun_rep ps 2 )
This is precisely the point of the quotient construction: identify data elements
that are distinct but abstractly the same.
In Isabelle the quotient type 0a st is introduced as follows
quotient_type 0a st = ( 0a::top) st_rep / eq_st
thereby overwriting the old definition of 0a st. The class constraint is required
because eq_st is defined in terms of fun_rep, which requires top. Instead of
the mathematical equivalence class notation [ps]eq_st we write St ps. We do
not go into further details of quotient_type: they are not relevant here and can
be found elsewhere [92].
Now we can define all the required operations on 0a st, starting with fun
and update:
> = St []
(St ps 1 6 St ps 2 ) = less_eq_st_rep ps 1 ps 2
St ps 1 t St ps 2 = St (map2_st_rep (op t) ps 1 ps 2 )
fun map2_st_rep ::
( 0a::top ⇒ 0a ⇒ 0a) ⇒ 0a st_rep ⇒ 0a st_rep ⇒ 0a st_rep
where
map2_st_rep f [] ps 2 = map (λ(x , y). (x , f > y)) ps 2
map2_st_rep f ((x , y) # ps 1 ) ps 2 =
(let y 2 = fun_rep ps 2 x in (x , f y y 2 ) # map2_st_rep f ps 1 ps 2 )
Exercises
Exercise 13.15. The previous sign analysis can be refined as follows. The
basic signs are “+”, “−” and “0”, but all combinations are also possible: e.g.,
{0,+} abstracts the set of non-negative integers. This leads to the following
semilattice (we do not need {}):
{−, 0, +}
thy
13.7 Analysis of Boolean Expressions
The analysis needs an inverse function for the arithmetic operator “<”, i.e.,
abstract arithmetic in the abstract interpreter interface needs to provide an-
other executable function:
inv_less 0 :: bool ⇒ 0av ⇒ 0av ⇒ 0av × 0av such that
[[inv_less 0 (i 1 < i 2 ) a 1 a 2 = (a 10 , a 20 ); i 1 ∈ γ a 1 ; i 2 ∈ γ a 2 ]]
=⇒ i 1 ∈ γ a 10 ∧ i 2 ∈ γ a 20
The specification follows the inv_plus 0 pattern.
Now we can define the backward analysis of boolean expresions:
fun inv_bval 0 :: bexp ⇒ bool ⇒ 0av st option ⇒ 0av st option where
inv_bval 0 (Bc v ) res S = (if v = res then S else None)
inv_bval 0 (Not b) res S = inv_bval 0 b (¬ res) S
inv_bval 0 (And b 1 b 2 ) res S =
(if res then inv_bval 0 b 1 True (inv_bval 0 b 2 True S )
else inv_bval 0 b 1 False S t inv_bval 0 b 2 False S )
inv_bval 0 (Less e 1 e 2 ) res S =
(let (a 1 , a 2 ) = inv_less 0 res (aval 0 0 e 1 S ) (aval 0 0 e 2 S )
in inv_aval 0 e 1 a 1 (inv_aval 0 e 2 a 2 S ))
The Bc and Not cases should be clear. The And case becomes symmetric
when you realise that inv_bval 0 b 1 True (inv_bval 0 b 2 True S ) is another
way of saying inv_bval 0 b 1 True S u inv_bval 0 b 2 True S without needing
u. Case Less is analogous to case Plus of inv_aval 0 .
Lemma 13.35 (Correctness of inv_bval 0 ).
s ∈ γo S =⇒ s ∈ γo (inv_bval 0 b (bval b s) S )
Proof. Just as for Lemma 13.25, with the help of Corollary 13.23, which needs
correctness of inv_bval 0 . t
u
Exercises
Exercise 13.17. Consider a simple sign analysis based on the abstract do-
main datatype sign = None | Neg | Pos0 | Any where γ :: sign ⇒ val set
is defined by γ None = {}, γ Neg = {i . i < 0}, γ Pos0 = {i . 0 6 i } and
γ Any = UNIV . Define inverse analyses for “+” and “<”
inv_plus 0 :: sign ⇒ sign ⇒ sign ⇒ sign × sign
inv_less 0 :: bool ⇒ sign ⇒ sign ⇒ sign × sign
and prove the required correctness properties inv_plus 0 a a 1 a 2 = (a 10 , a 20 )
=⇒ ... and inv_less 0 bv a 1 a 2 = (a 10 , a 20 ) =⇒ ... stated in Section 13.7.1
and 13.7.2.
Exercise 13.18. Extend the abstract interpreter from the previous section
with the simple forward analysis of boolean expressions sketched in the first
paragraph of this section. You need to add a function less 0 :: 0av ⇒ 0av
⇒ bool option (where None, Some True and Some False mean Maybe,
Yes and No) to locale Val_semilattice in theory Abs_Int0 (together with a
suitable assumption), define a function bval 0 :: bexp ⇒ 0av st ⇒ bool op-
tion in Abs_Int1, modify the definition of step 0 to make use of bval 0 , and
update the proof of correctness of AI. The remainder of theory Abs_Int1
can be discarded. Finally adapt constant propagation analysis in theory
Abs_Int1_const.
thy
13.8 Interval Analysis
13.8.1 Intervals
Let us start with the type of intervals itself. Our intervals need to include in-
finite endpoints, otherwise we could not represent any infinite sets. Therefore
we base intervals on extended integers, i.e., integers extended with ∞ and
−∞. The corresponding polymorphic data type is
datatype 0a extended = Fin 0a | ∞ | −∞
and eint are the extended integers:
type_synonym eint = int extended
Type eint comes with the usual arithmetic operations.
Constructor Fin can be dropped in front of numerals: 5 is as good as Fin 5
(except for the type constraint implied by the latter). This applies to input and
output of terms. We do not discuss the necessary Isabelle magic.
Most of the time we blur the distinction between ivl and eint2 by introducing
the abbreviation [l,h] :: ivl, where l,h :: eint, for the equivalence class of
all pairs equivalent to (l, h) w.r.t. eq_ivl. Unless γ_rep (l, h) = {}, this
equivalence class only contains (l, h). Moreover, let ⊥ be the empty interval,
i.e., [l, h] for any h < l. Note that there are two corner cases where [l, h] =
⊥ does not imply h < l: [∞, ∞] and [−∞, −∞].
We will now “define” most functions on intervals by pattern matching on
[l, h]. Here is a first example:
266 13 Abstract Interpretation
l1 h1
l2 h2
[l 1 , h 1 ] t [l 2 , h 2 ] =
(if [l 1 , h 1 ] = ⊥ then [l 2 , h 2 ]
else if [l 2 , h 2 ] = ⊥ then [l 1 , h 1 ] else [min l 1 l 2 , max h 1 h 2 ])
[l 1 , h 1 ] u [l 2 , h 2 ] = [max l 1 l 2 , min h 1 h 2 ]
> = [−∞, ∞]
[l 1 , h 1 ] t [l 2 , h 2 ]
l1 h1
l2 h2
[l 1 , h 1 ] u [l 2 , h 2 ]
[l 1 , h 1 ] t [l 2 , h 2 ]
l1 h1 l2 h2
− [l, h] = [− h, − l]
iv 1 − iv 2 = iv 1 + − iv 2
inv_less_ivl res iv 1 iv 2 =
(if res
then (iv 1 u (below iv 2 − [1, 1]),
iv 2 u (Abs_Int2_ivl.above iv 1 + [1, 1]))
else (iv 1 u Abs_Int2_ivl.above iv 2 , iv 2 u below iv 1 ))
The correctness proof follows the pattern of the one for inv_plus_ivl.
For a visualization of inv_less_ivl we focus on the “>” case, i.e., ¬ res,
and consider the following situation, where iv 1 = [l 1 , h 1 ], iv 2 = [l 2 , h 2 ] and
the thick lines indicate the result:
l1 h1
l2 h2
All those points that can definitely not lead to the first interval being > the
second have been eliminated. Of course this is just one of finitely many ways
in which the two intervals can be positioned relative to each other.
The “<” case in the definition of inv_less_ivl is dual to the “>” case but
adjusted by 1 because the ordering is strict.
Now we instantiate the abstract interpreter interface with the interval domain:
num 0 = λi . [Fin i , Fin i ], plus 0 = op +, test_num 0 = in_ivl where in_ivl
i [l, h] = (l 6 Fin i ∧ Fin i 6 h), inv_plus 0 = inv_plus_ivl, inv_less 0 =
inv_less_ivl. In the preceding subsections we have already discussed that all
requirements hold.
Let us now analyse some concrete programs. We mostly show the final
results of the analyses only. For a start, we can do better than the naive
constant propagation in Section 13.6.2:
x ::= N 42 {Some {( 0 0x 0 0 , [42, 42])}};;
0 0 00
IF Less (N 41) (V 0 0x 0 0 )
THEN {Some {( 0 0x 0 0 , [42, 42])}} 0 0x 0 0 ::= N 5 {Some {( 0 0x 0 0 , [5, 5])}}
ELSE {None} 0 0x 0 0 ::= N 6 {None}
{Some {( 0 0x 0 0 , [5, 5])}}
13.8 Interval Analysis 269
This time the analysis detects that the ELSE branch is unreachable. Of course
the constant propagation in Section 10.2 can deal with this example equally
well. Now we look at examples beyond constant propagation. Here is one that
demonstrates the analysis of boolean expressions very well:
y ::= N 7 {Some {( 0 0x 0 0 , [−∞, ∞]), ( 0 0y 0 0 , [7, 7])}};;
0 0 00
IF Less (V 0 0x 0 0 ) (V 0 0y 0 0 )
THEN {Some {( 0 0x 0 0 , [−∞, 6]), ( 0 0y 0 0 , [7, 7])}}
0 0 00
y ::= Plus (V 0 0y 0 0 ) (V 0 0x 0 0 )
{Some {( 0 0x 0 0 , [−∞, 6]), ( 0 0y 0 0 , [−∞, 13])}}
ELSE {Some {( 0 0x 0 0 , [7, ∞]), ( 0 0y 0 0 , [7, 7])}}
0 0 00
x ::= Plus (V 0 0x 0 0 ) (V 0 0y 0 0 )
{Some {( 0 0x 0 0 , [14, ∞]), ( 0 0y 0 0 , [7, 7])}}
{Some {( 0 0x 0 0 , [−∞, ∞]), ( 0 0y 0 0 , [−∞, 13])}}
Let us now analyse some WHILE loops.
{Some {( 0 0x 0 0 , [−∞, ∞])}}
WHILE Less (V 0 0x 0 0 ) (N 100)
DO {Some {( 0 0x 0 0 , [−∞, 99])}}
x ::= Plus (V 0 0x 0 0 ) (N 1) {Some {( 0 0x 0 0 , [−∞, 100])}}
0 0 00
{None}
The interval [0, 1] will increase slowly until it reaches the invariant [0, 100]:
x ::= N 0 {Some {( 0 0x 0 0 , [0, 0])}};;
0 0 00
{None}
The interval for x keeps increasing indefinitely. The problem is that ivl is not
of finite height: [0, 0] < [0, 1] < [0, 2] < . . . . The Alexandrian solution to this is
quite brutal: if the analysis sees the beginning of a possibly infinite ascending
chain, it overapproximates wildly and jumps to a much larger point, namely
[0, ∞] in this case. An even cruder solution would be to jump directly to >
to avoid nontermination.
Exercises
thy
13.9 Widening and Narrowing
13.9.1 Widening
Widening abstracts the idea sketched at the end of the previous section: if an
iteration appears to diverge, jump upwards in the ordering to avoid nonter-
mination or at least accelerate termination, possibly at the cost of precision.
This is the purpose of the overloaded widening operator:
op 5 :: 0a ⇒ 0a ⇒ 0a such that
x 6 x 5 y and y 6 x 5 y
which is equivalent to x t y 6 x 5 y. This property is only needed for the
termination proof later on. We assume that the abstract domain provides a
widening operator.
Iteration with widening means replacing x i +1 = f x i by x i +1 = x i 5 f x i .
That is, we apply widening in each step. Pre-fixpoint iteration with widening
is defined for any type 0a that provides 6 and 5:
13.9 Widening and Narrowing 271
fx
iteration of f
iteration with widening
narrowing iteration of f
[l 1 , h 1 ] 5 [l 2 , h 2 ] = [l, h]
where l = (if l 1 > l 2 then −∞ else l 1 )
h = (if h 1 < h 2 then ∞ else h 1 )
For example: [0, 1] 5 [0, 2] = [0, ∞]
[0, 2] 5 [0, 1] = [0, 2]
[1, 2] 5 [0, 5] = [−∞, ∞]
The first two examples show that although the symbol 5 looks symmetric,
the operator need not be commutative: its two arguments are the previous
and the next value in an iteration and it is important which one is which.
The termination argument for iter_widen on intervals goes roughly like
this: if we have not reached a pre-fixpoint yet, i.e., ¬ [l 2 , h 2 ] 6 [l 1 , h 1 ], then
either l 1 > l 2 or h 1 < h 2 and hence at least one of the two bounds jumps to
infinity, which can happen at most twice.
{None}
Previously, in the next step the annotation at the head of the loop would
change from [0, 0] to [0, 1]; now this is followed by widening. Because
[0, 0] 5 [0, 1] = [0, ∞] we obtain
x ::= N 0 {Some {( 0 0x 0 0 , [0, 0])}};;
0 0 00
{None}
Two more steps and we have reached a pre-fixpoint:
x ::= N 0 {Some {( 0 0x 0 0 , [0, 0])}};;
0 0 00
{None}
This is very nice because we have actually reached the least fixpoint.
The details of what happened are shown in Table 13.1, where A0 to A4
are the five annotations from top to bottom. We start with the situation after
four steps. Each iteration step is displayed as a block of two columns: first the
f 5 f 5 f 5
A0 [0, 0] [0, 0]
A1 [0, 0] [0, 1] [0, ∞] [0, 1] [0, ∞] [0, ∞] [0, ∞]
A2 [0, 0] [0, ∞] [0, ∞] [0, ∞] [0, ∞]
A3 [1, 1] [1, ∞] [1, ∞]
A4 None None
result of applying the step function to the previous column, then the result of
widening that result with the previous column. The last column is the least
pre-fixpoint. Empty entries mean that nothing has changed.
274 13 Abstract Interpretation
13.9.2 Narrowing
In contrast to widening, we are not looking for a pre-fixpoint (if we start from
a pre-fixpoint, it remains a pre-fixpoint), but we terminate as soon as we no
longer make progress, i.e., no longer strictly decrease. The narrowing operator
can enforce termination by making x 4 f x return x.
Example 13.38. The narrowing operator for intervals only changes one of the
interval bounds if it can be improved from infinity to some finite value:
[l 1 , h 1 ] 4 [l 2 , h 2 ] = [l, h]
where l = (if l 1 = −∞ then l 2 else l 1 )
h = (if h 1 = ∞ then h 2 else h 1 )
For example: [0, ∞] 4 [0, 100] = [0, 100]
[0, 100] 4 [0, 0] = [0, 100]
[0, 0] 4 [0, 100] = [0, 0]
Narrowing operators need not be commutative either.
The termination argument for iter_narrow on intervals is very intuitive:
finite interval bounds remain unchanged; therefore it takes at most two steps
until both bounds have become finite and iter_narrow terminates.
The last of the above three lemmas implies termination of iter_widen f C for
C :: 0av st option acom as follows. If you set C 2 = f C 1 and assume that f
preserves the strip and top_on c properties then each step of iter_widen f C
strictly decreases m c , which must terminate because m c returns a nat.
To apply this result to widening on intervals we merely need to provide
the right m and h:
m_ivl [l, h] =
(if [l, h] = ⊥ then 3
else (if l = −∞ then 0 else 1) + (if h = ∞ then 0 else 1))
Strictly speaking m does not need to consider the ⊥ case because we have
made sure it cannot arise in an abstract state, but it is simpler not to rely on
this and cover ⊥.
Function m_ivl satisfies the required properties: m_ivl iv 6 3 and
y 6 x =⇒ m_ivl x 6 m_ivl y.
Because bot suitably initializes and step_ivl preserves the strip and
top_on c properties, this implies termination of iter_widen (step_ivl >):
∃ C . iter_widen (step_ivl >) (bot c) = Some C
The proof is similar to that for widening. We require another measure function
n :: 0av ⇒ nat such that
[[a 2 6 a 1 ; a 1 4 a 2 < a 1 ]] =⇒ n (a 1 4 a 2 ) < n a 1 (13.5)
This property guarantees that the measure goes down with every iteration of
iter_narrow f a 0 , provided f is monotone and f a 0 6 a 0 : let a 2 = f a 1 ; then
a 2 6 a 1 is the pre-fixpoint property that iteration with narrowing preserves
(see Section 13.9.2) and a 1 4 a 2 < a 1 is the loop condition.
Now we need to lift this from 0av to other types. First we sketch how
it works for tuples. Define the termination measure n 0 (a 1 ,a 2 ,. . .) = n a 1
+ n a 2 + . . .. To show that (13.5) holds for n 0 too, assume (b 1 ,b 2 ,. . .) 6
(a 1 ,a 2 ,. . .) and (a 1 ,a 2 ,. . .) 4 (b 1 ,b 2 ,. . .) < (a 1 ,a 2 ,. . .), i.e., b i 6 a i for all i,
278 13 Abstract Interpretation
Property (13.5) carries over to 0av st option acom with suitable side condi-
tions just as (13.4) carried over in the previous subsection:
[[strip C 1 = strip C 2 ; top_on c C 1 (− vars C 1 );
top_on c C 2 (− vars C 2 ); C 2 6 C 1 ; C 1 4 C 2 < C 1 ]]
=⇒ n c (C 1 4 C 2 ) < n c C 1
This implies termination of iter_narrow f C for C :: 0av st option acom
provided f is monotone and C is a pre-fixpoint of f (which is preserved by
narrowing because f is monotone and 4 a narrowing operator) because it
guarantees that each step of iter_narrow f C strictly decreases n c .
To apply this result to narrowing on intervals we merely need to provide
the right n:
n_ivl iv = 3 − m_ivl iv
It does what it is supposed to do, namely satisfy (a strengthened version of)
(13.5): x 4 y < x =⇒ n_ivl (x 4 y) < n_ivl x. Therefore narrowing termi-
nates provided we start with a pre-fixpoint which is > outside its variables:
[[top_on c C (− vars C ); step_ivl > C 6 C ]]
=⇒ ∃ C 0 . iter_narrow (step_ivl >) C = Some C 0
Because both preconditions are guaranteed by the output of widening we ob-
tain the final termination theorem where AI_wn_ivl is AI_wn for intervals:
Exercises
{None}
tabulate the three steps that step_ivl with widening takes to reach (13.3).
Follow the format of Table 13.1.
Exercise 13.21. Starting from (13.3), tabulate both the repeated application
of step_ivl > alone and of iter_narrow (step_ivl >), the latter following the
format of Table 13.1 (with 5 replaced by 4).
This appendix contains auxiliary definitions omitted from the main text.
Variables
fun lvars :: com ⇒ vname set where
lvars SKIP = {}
lvars (x ::= e) = {x }
lvars (c 1 ;; c 2 ) = lvars c 1 ∪ lvars c 2
lvars (IF b THEN c 1 ELSE c 2 ) = lvars c 1 ∪ lvars c 2
lvars (WHILE b DO c) = lvars c
Abstract Interpretation
[[ [| \<lbrakk>
]] |] \<rbrakk>
=⇒ ==> \<Longrightarrow>
V
!! \<And>
≡ == \<equiv>
λ % \<lambda>
⇒ => \<Rightarrow>
∧ & \<and>
∨ | \<or>
−→ --> \<longrightarrow>
→ -> \<rightarrow>
¬ ~ \<not>
6= ~= \<noteq>
∀ ALL \<forall>
∃ EX \<exists>
6 <= \<le>
× * \<times>
∈ : \<in>
∈
/ ~: \<notin>
⊆ <= \<subseteq>
⊂ < \<subset>
∪ Un \<union>
∩ Int \<inter>
S
UN, Union \<Union>
T
INT, Inter \<Inter>
t sup \<squnion>
u inf \<sqinter>
F
SUP, Sup \<Squnion>
d
INF, Inf \<Sqinter>
> \<top>
⊥ \<bottom>
Table B.1. Mathematical symbols, their ascii equivalents and internal names
C
Theories
The following table shows which sections are based on which theories in the
directory src/HOL/IMP of the Isabelle distribution.
29. Gidon Ernst, Gerhard Schellhorn, Dominik Haneberg, Jörg Pfähler, and Wolf-
gang Reif. A formal model of a virtual filesystem switch. In Proc. 7th SSV,
pages 33–45, 2012.
30. Robert Floyd. Assigning meanings to programs. In J. T. Schwartz, editor, Math-
ematical Aspects of Computer Science, volume 19 of Proceedings of Symposia
in Applied Mathematics, pages 19–32. American Mathematical Society, 1967.
31. Anthony Fox. Formal specification and verification of ARM6. In David Basin
and Burkhart Wolff, editors, Proceedings of the 16th Int. Conference on Theo-
rem Proving in Higher Order Logics (TPHOLs), volume 2758 of LNCS, pages
25–40, Rome, Italy, September 2003. Springer.
32. Anthony Fox and Magnus Myreen. A trustworthy monadic formalization of
the ARMv7 instruction set architecture. In Matt Kaufmann and Lawrence C.
Paulson, editors, 1st Int. Conference on Interactive Theorem Proving (ITP),
volume 6172 of LNCS, pages 243–258, Edinburgh, UK, July 2010. Springer.
33. Thomas Gawlitza and Helmut Seidl. Precise fixpoint computation through
strategy iteration. In Rocco De Nicola, editor, Programming Languages and
Systems, ESOP 2007, volume 4421 of LNCS, pages 300–315. Springer, 2007.
34. Kurt Gödel. Über formal unentscheidbare Sätze der Principia Mathematica und
verwandter Systeme I. Monatshefte für Mathematik und Physik, 38(1):173–
198, 1931.
35. Joseph A. Goguen and José Meseguer. Security policies and security models.
In IEEE Symposium on Security and Privacy, pages 11–20, 1982.
36. Michael J.C. Gordon. HOL: A machine oriented formulation of higher-order
logic. Technical Report 68, University of Cambridge, Computer Laboratory,
1985.
37. Michael J.C. Gordon. Mechanizing programming logics in higher order logic. In
G. Birtwistle and P.A. Subrahmanyam, editors, Current Trends in Hardware
Verification and Automated Theorem Proving. Springer, 1989.
38. James Gosling, Bill Joy, Guy Steele, and Gilad Bracha. Java(TM) Language
Specification, 3rd edition. Addison-Wesley, 2005.
39. Carl Gunter. Semantics of programming languages: structures and techniques.
MIT Press, 1992.
40. Florian Haftmann. Haskell-style type classes with Isabelle/Isar. http:
//isabelle.in.tum.de/doc/classes.pdf.
41. C.A.R. Hoare. An axiomatic basis for computer programming. Communica-
tions of the ACM, 12:567–580,583, 1969.
42. John Hopcroft, Rajeev Motwani, and Jeffrey Ullman. Introduction to Automata
Theory, Languages, and Computation. Addison-Wesley, 3rd edition, 2006.
43. Brian Huffman. A purely definitional universal domain. In S. Berghofer, T. Nip-
kow, C. Urban, and M. Wenzel, editors, Theorem Proving in Higher Order
Logics (TPHOLs 2009), volume 5674 of LNCS, pages 260–275. Springer, 2009.
44. Michael Huth and Mark Ryan. Logic in Computer Science. Cambridge Uni-
versity Press, 2004.
45. Atshushi Igarashi, Benjamin Pierce, and Philip Wadler. Featherweight Java:
a minimal core calculus for Java and GJ. In Proceedings of the 14th ACM
SIGPLAN conference on Object-oriented programming, systems, languages,
and applications, OOPSLA ’99, pages 132–146. ACM, 1999.
290 References
46. Gilles Kahn. Natural semantics. In STACS 87: Symp. Theoretical Aspects of
Computer Science, volume 247 of LNCS, pages 22–39. Springer, 1987.
47. Gerwin Klein, Kevin Elphinstone, Gernot Heiser, June Andronick, David Cock,
Philip Derrin, Dhammika Elkaduwe, Kai Engelhardt, Rafal Kolanski, Michael
Norrish, Thomas Sewell, Harvey Tuch, and Simon Winwood. seL4: Formal Ver-
ification of an OS Kernel. In Jeanna Neefe Matthews and Thomas E. Anderson,
editors, Proc. 22nd ACM Symposium on Operating Systems Principles 2009,
pages 207–220. ACM, 2009.
48. Gerwin Klein and Tobias Nipkow. A machine-checked model for a Java-like
language, virtual machine and compiler. ACM Trans. Program. Lang. Syst.,
28(4):619–695, 2006.
49. Alexander Krauss. Defining Recursive Functions in Isabelle/HOL. http:
//isabelle.in.tum.de/doc/functions.pdf.
50. Alexander Krauss. Recursive definitions of monadic functions. In A. Bove,
E. Komendantskaya, and M. Niqui, editors, Proc. Workshop on Partiality and
Recursion in Interactive Theorem Provers, volume 43 of EPTCS, pages 1–13,
2010.
51. Butler W. Lampson. A note on the confinement problem. Communications of
the ACM, 16(10):613–615, October 1973.
52. K. Rustan M. Leino. Dafny: An automatic program verifier for functional cor-
rectness. In LPAR-16, volume 6355 of LNCS, pages 348–370. Springer, 2010.
53. Xavier Leroy. Formal certification of a compiler back-end or: programming a
compiler with a proof assistant. In Proc. 33rd ACM Symposium on Principles
of Programming Languages, pages 42–54. ACM, 2006.
54. Tim Lindholm, Frank Yellin, Gilad Bracha, and Alex Buckley. The Java Virtual
Machine Specification, Java SE 7 Edition. Addison-Wesley, February 2013.
55. Farhad Mehta and Tobias Nipkow. Proving pointer programs in higher-order
logic. Information and Computation, 199:200–227, 2005.
56. Robin Milner. A theory of type polymorphism in programming. Journal of
Computer and System Sciences (JCCS), 17(3):348–375, 1978.
57. Greg Morrisett, Gang Tan, Joseph Tassarotti, Jean-Baptiste Tristan, and Ed-
ward Gan. Rocksalt: better, faster, stronger SFI for the x86. In Proceedings of
the 33rd ACM SIGPLAN conference on Programming Language Design and
Implementation, PLDI ’12, pages 395–404, New York, NY, USA, 2012. ACM.
58. Steven Muchnick. Advanced Compiler Design and Implementation. Morgan
Kaufmann, 1997.
59. Olaf Müller, Tobias Nipkow, David von Oheimb, and Oskar Slotosch. HOLCF
= HOL + LCF. J. Functional Programming, 9:191–223, 1999.
60. Toby Murray, Daniel Matichuk, Matthew Brassil, Peter Gammie, Timothy
Bourke, Sean Seefried, Corey Lewis, Xin Gao, and Gerwin Klein. seL4: from
general purpose to a proof of information flow enforcement. In IEEE Sympo-
sium on Security and Privacy, pages 415–429, 2013.
61. Flemming Nielson, Hanne Riis Nielson, and Chris Hankin. Principles of Pro-
gram Analysis. Springer, 1999.
62. Hanne Riis Nielson and Flemming Nielson. Semantics With Applications: A
Formal Introduction. Wiley, 1992.
63. Hanne Riis Nielson and Flemming Nielson. Semantics with Applications. An
Appetizer. Springer, 2007.
References 291
81. Edward Schwartz, Thanassis Avgerinos, and David Brumley. All you ever
wanted to know about dynamic taint analysis and forward symbolic execution
(but might have been afraid to ask). In Proc. IEEE Symposium on Security
and Privacy, pages 317–331. IEEE Computer Society, 2010.
82. Dana Scott. Outline of a mathematical theory of computation. In Information
Sciences and Systems: Proc. 4th Annual Princeton Conference, pages 169–
176. Princeton University Press, 1970.
83. Dana Scott and Christopher Strachey. Toward a mathematical semantics for
computer languages. Programming Research Group Technical Monograph PRG-
6, Oxford University Computing Lab., 1971.
84. Thomas Sewell, Magnus Myreen, and Gerwin Klein. Translation validation for
a verified OS kernel. In PLDI, pages 471–481, Seattle, Washington, USA, June
2013. ACM.
85. Alfred Tarski. A lattice-theoretical fixpoint theorem and its applications. Pacific
J. Math., 5:285–309, 1955.
86. Robert Tennent. Denotational semantics. In S. Abramsky, D. Gabbay, and
T.S.E. Maibaum, editors, Handbook of Logic in Computer Science, volume 3,
pages 169–322. Oxford University Press, 1994.
87. Harvey Tuch. Formal Memory Models for Verifying C Systems Code. PhD
thesis, School of Computer Science and Engineering, University of NSW, Syd-
ney, Australia, August 2008.
88. Harvey Tuch, Gerwin Klein, and Michael Norrish. Types, bytes, and separation
logic. In Martin Hofmann and Matthias Felleisen, editors, Proceedings of the
34th ACM SIGPLAN-SIGACT Symposium on Principles of Programming
Languages, pages 97–108, Nice, France, January 2007. ACM.
89. D. Volpano, C. Irvine, and G. Smith. A sound type system for secure flow
analysis. Journal of computer security, 4(2/3):167–188, 1996.
90. Dennis Volpano and Geoffrey Smith. Eliminating covert flows with minimum
typings. In Proceedings of the 10th IEEE workshop on Computer Security
Foundations, CSFW ’97, pages 156–169. IEEE Computer Society, 1997.
91. Dennis M. Volpano and Geoffrey Smith. A type-based approach to program
security. In Proc. 7th Int. Joint Conference CAAP/FASE on Theory and
Practice of Software Development (TAPSOFT ’97), volume 1214 of LNCS,
pages 607–621. Springer, 1997.
92. Makarius Wenzel. The Isabelle/Isar Reference Manual. http://isabelle.in.
tum.de/doc/isar-ref.pdf.
93. Glynn Winskel. The Formal Semantics of Programming Languages. MIT
Press, 1993.
Index
JMPGE 96 Nil 14
JMPLESS 96 non-deterministic 84
join 237 None 16
judgement 78 noninterference 129
Not 32
Kleene fixpoint theorem 185 note 61
Knaster-Tarski fixpoint theorem 174, num 0 243
231, 233
obtain 58
L 165, 174 OF 44
language-based security 128 of 42
lattice 260 operational semantics 77
bounded 260 option 16
complete 231, 233 outer syntax 7
least element 173
least upper bound 231 parent theory 7
lemma 9 partial correctness 193
lemma 55 partial order 173
length 14 pfp 246
Less 32 Plus 28
let 59 plus 0 243
level 129 point free 188
lfp 174 polymorphic 10
linear arithmetic 41 post 225
list 10 postcondition 192
live variable 165 strongest 207
LOAD 96 pre 208
LOADI 96 pre-fixpoint 173
locale 242 precondition 192
lvars 281 weakest 204
weakest liberal 204
Main 7 .prems 64, 66
map 14 preservation 123
map_acom 225, 282 program counter 96
may analysis 178 exits 106
meet 260 successors 105
metis 41 progress 123
mono 174 proof 53
monotone 173
monotone framework 247 qed 53
moreover 60 quantifier 6
must analysis 178 quotient type 257