HOL Light Tutorial
HOL Light Tutorial
John Harrison
Intel Corporation, RA2-406
2501 NW 229th Avenue
Hillsboro, OR 97124
USA
johnh@ichips.intel.com
January 14, 2017
Abstract
The HOL Light theorem prover can be difficult to get started with. While
the manual is fairly detailed and comprehensive, the large amount of background
information that has to be absorbed before the user can do anything interesting is
intimidating. Here we give an alternative ‘quick start’ guide, aimed at teaching
basic use of the system quickly by means of a graded set of examples. Some
readers may find it easier to absorb; those who do not are referred after all to the
standard manual.
“Shouldn’t we read the instructions?”
“Do I look like a sissy?”
Calvin & Hobbes, 19th April 1988
1
Contents
1 Installation 5
1.1 Cygwin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 OCaml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 HOL Light . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Checkpointing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Other versions of HOL . . . . . . . . . . . . . . . . . . . . . . . . . 9
3 HOL basics 12
3.1 Terms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2 Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3 Theorems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.4 Derived rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4 Propositional logic 17
4.1 Proving tautologies . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.2 Low-level logical rules . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.3 Logic design and verification . . . . . . . . . . . . . . . . . . . . . . 22
2
10 Inductive definitions 62
10.1 The bug puzzle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
10.2 Verification of concurrent programs . . . . . . . . . . . . . . . . . . 68
11 Wellfounded induction√ 71
11.1 Irrationality of 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
11.2 Wellfoundedness . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
13 Recursive definitions 82
13.1 Binomial coefficients . . . . . . . . . . . . . . . . . . . . . . . . . . 85
13.2 The binomial theorem . . . . . . . . . . . . . . . . . . . . . . . . . . 87
3
19 Real analysis 143
19.1 Chebyshev polynomials . . . . . . . . . . . . . . . . . . . . . . . . . 143
19.2 A trivial part of Sarkovskii’s theorem . . . . . . . . . . . . . . . . . . 148
19.3 Derivatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
22 Vectors 178
22.1 3-dimensional vectors . . . . . . . . . . . . . . . . . . . . . . . . . . 182
22.2 Cross products . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
4
1 Installation
HOL Light can fairly easily be made to work on most modern computers. Since the
first version (Harrison 1996a), the build process has been simplified considerably. In
what follows, we will sometimes assume a Unix-like environment such as Linux. If
the reader has access to a Linux machine and feels comfortable with it, its use is rec-
ommended. However, users of Windows need not despair, because all the Unix tools
needed, and many more useful ones besides, are freely available as part of Cygwin.
Non-Windows users, or Windows users determined to work “natively”, can skip the
next subsection.
1.1 Cygwin
Cygwin is a Linux-like environment that can be run within Windows, without interfer-
ing with normal Windows usage. Among other things, it provides a traditional shell
from which the usual Unix/Linux software tools are available. Cygwin can be freely
downloaded from http://www.cygwin.com/. It is a large system, particularly
if you select all the package installation options, so the download and installation can
take some time. However it usually seems to be straightforward and unproblematic.
After installing Cygwin, simply start a ’Bash shell’. On my Windows machine,
for example, I follow the menu sequence Start → All Programs → Cygwin
→ Cygwin bash shell. This application is a ‘shell’ (Unix jargon for something
analogous to a Windows command prompt) from which the later commands below can
be invoked as if you were within Linux. We will hereinafter say ‘Linux’ when we mean
Linux, some other version of Unix, or Cygwin inside Windows.
1.2 OCaml
HOL Light is built on top of the functional programming language Objective CAML
(‘OCaml’). To be more precise, HOL Light is written in OCaml and the OCaml read-
eval-print loop is the usual means of interacting with HOL Light. So installing OCaml
is a prerequisite for using HOL Light. Besides, it is a powerful modern programming
language with much to recommend it for a wide range of other applications.
OCaml can be installed on a wide range of architectures by following the instruc-
tions on the Web site http://caml.inria.fr/ocaml/english.en.html.
I normally rebuild the system from sources, even under Cygwin (it only requires a few
short commands) but precompiled binaries are available for many platforms.
5
tar xvfz hol_light.tar.gz
This will create a directory (folder) called hol_light containing the constituent
source files.
In a pure Windows environment, when you download the hol_light.tar.gz
file, or click on it after first saving it, you will automatically be confronted with the
Windows WinZip utility, which will display the list of constituent files. By selecting
Extract you can save them all to a folder of your choice, say hol_light.
Either at the shell prompt (Linux) or the command prompt (in Windows, usually
available via the Accessories menu), move into the appropriate directory by:
cd hol_light
The first step is to create a special file used by HOL Light to handle parsing and printing
within OCaml. In Linux you can just do:
make
In Windows, you need to issue by hand some commands contained in the Makefile.
First check the version of OCaml that you have (e.g. by typing ocaml and observing
the version number in the startup banner, then exiting it). Copy the appropriately
numbered file pa_j_XXX.ml into the bare pa_j.ml. For example, if running Ocaml
version 3.08.2, do
If there isn’t a pa_j_XXX.ml that precisely matches your OCaml version number,
pick the closest one; it will probably work. However, versions older than 3.04 have
not been tried. Next, issue this command:
There should now be a file called pa_j.cmo in the current directory. (You can
check that it’s there by a directory listing, ls in Linux or dir in Windows.) Now start
up an interactive OCaml session by:
ocaml
or under Cygwin and any other systems where the OCaml num library for arbitrary-
precision rational cannot be dynamically loaded:
ocamlnum
You should see something like this (though the precise OCaml version number may
well be a little different).
/home/johnh/hol_light$ ocaml
Objective Caml version 3.08.2
6
This is what it feels like to be within the OCaml read-eval-print loop. OCaml waits
for the user to type in an expression, then evaluates it and prints out the result. OCaml
will only start evaluation when the user types two successive semicolons and a newline.
For example we can evaluate 2 + 2 by typing
2 + 2;;
val it : int = 4
#
the hash indicating that it’s waiting for the next expression. We’ll consider the OCaml
toplevel in more detail later, but it might be worth noting now that to get out of it, you
can type control-D at the prompt (i.e. hold down the control key and press D).
Within the OCaml toplevel, load in HOL Light by issuing the following OCaml
directive, which as usual includes two terminating semicolons (note that the hash is
part of the ‘#use’ directive, not a representation of the prompt):
#use "hol.ml";;
You should now see a large amount of output as HOL Light is loaded, which in
particular involves proving many theorems and giving them names. After about two
minutes, depending on how fast your computer is, you should see something like the
last few lines of output below and the OCaml prompt:
You are now ready to start using the system and may want to skip to the next main
section. However, assuming you are using a real Linux system, it is worth creating
standalone images to avoid waiting for the system to load into OCaml every time you
want to use it. This is explained in the next section.
It is not obligatory to stay within the initial hol_light directory. However, if
you want to be able to load it from anywhere, you should first edit the very first line of
the file hol.ml and change it from:
let hol_dir = ref (try Sys.getenv "HOLDIR" with Not_found -> Sys.getcwd());;
#use "/home/johnh/hol_light/hol.ml";;
or whatever, and HOL will automatically be able to find all the other files.
7
1.4 Checkpointing
It’s more convenient to be able to save the state of the OCaml toplevel with HOL Light
pre-installed, rather than wait the two minutes for it to load each time you want to
use it. Although there are no special OCaml or HOL Light facilities for doing this,
there are ‘checkpointing’ tools available for many operating systems which can save
the state of any user process, OCaml included. A list of such systems can be found
at http://www.checkpointing.org (unfortunately, I don’t know of one for
Windows). Under Linux, I have been very happy with ckpt, which is available from
http://www.cs.wisc.edu/˜zandy/ckpt/.
If you download and install ckpt, you should find that the HOL Light Makefile
conveniently automates the creation of a standalone image. Using similar checkpoint-
ing tools, you should be able to modify the Makefile appropriately. If ckpt is installed,
you can create a standalone HOL image simply by typing the following into the shell:
make hol
This should create a file hol in the current directory, which can then be moved to
any chosen location (e.g. /usr/local/bin) and thereafter invoked directly from
the shell.
Moreover, the standalone image is itself capable of being checkpointed. An ad-
ditional OCaml function self destruct is provided, which takes as its argument
a string to print when the image is later restarted. The effect of this is to terminate
the current HOL session and save the state as a file hol.snapshot in the current
directory. For example, in the following session we make an assignment to variable z,
checkpoint and restart.
/home/johnh$ hol
HOL Light, built 4 April 2005 on OCaml 3.08.1
val it : unit = ()
# let z = 12345;;
val z : int = 12345
# self_destruct "With definition of z";;
/home/johnh$ ./hol.snapshot
HOL Light, built 4 April 2005 on OCaml 3.08.1
With definition of z
val it : unit = ()
# z;;
val it : int = 12345
#
When developing large proofs in HOL, you should always keep the proof script as
an OCaml file ready to reload, rather than relying on ckpt. This will allow the proofs
to be later modified, used by others etc. However, it can be very convenient to make
intermediate snapshots so you do not have to load large files to work further on a proof.
This is analogous to the usual situation in programming: you should always keep your
complete source code, but don’t want to recompile all the sources each time you use
the code.
8
Sometimes, it seems that the checkpointed image can give an error message when
restarted rather than the appropriate introductory banner, e.g.
This error message is harmless and the image can still be used; moreover I usually
find that doing something trivial (like ‘let it = 1;;’) then checkpointing again
fixes the problem. It’s probably connected with the slightly “hacky” way in which the
OCaml process sends a signal to itself to force checkpointing. If this really becomes
a persistent problem on your machine, you can avoid it by issuing the checkpointing
exception externally rather than calling self destruct from within the session, e.g.
HOL88
HH
@
@HHH
@ HH
@ H
j
H
R
@ Isabelle/HOL
hol90 ProofPower
@
@
@@
R ?
HOL Light
?
hol98
?
HOL 4
9
pendix, we describe the evolution of HOL and its place in the world of theorem provers
in more detail.
#use "filename";;
which loads the OCaml source from a file called filename as if it had been typed
into the toplevel — this is exactly what we did to load in HOL. Moreover, the example
of 2 + 2;; was an example of evaluating an expression. Let us look more closely at
the output:
val it : int = 4
OCaml responds with the result of evaluating the expression (4), but also allocates
it a type int (meaning that it is an integer, or whole number) and introduces a name
it for the result. We can now use it as an abbreviation for the result of the evaluation,
namely 4. For example:
# it * it;;
val it : int = 16
Now it denotes the result of that expression, namely 16. However, it is just the
default OCaml gives the result of the last expression. A user can give it any chosen
name by using a definition of the form:
1 For those used to OCaml: HOL Light uses a camlp4 syntax extension, which modifies the usual OCaml
toplevel in a few ways: uppercase names are acceptable as identifiers, some new infixes such as ‘o’ (function
composition) are added, backquotes are used for quotations, and the last expression evaluated is bound to
‘it’. The usual special treatment of uppercase identifiers is reserved for those with an uppercase first letter
and some lowercase letters thereafter, which seems to be largely consistent with established OCaml usage.
2 Maple is a registered trademark of Waterloo Maple.
10
You can then use that name in subsequent expressions, and bind composite expres-
sions to other names, for example:
# let a = 741;;
val a : int = 741
# let b = 147;;
val b : int = 147
# let c = a - b;;
val c : int = 594
# let d = 495 + c;;
val d : int = 1089
As well as integers (whole numbers) CAML lets you evaluate expressions involving
other types. For example strings (finite sequences of characters) can be entered within
double-quotes, and operated on using functions such as ˆ, which concatenates (sticks
together) two strings just as + adds two numbers.
# let x = "no";;
val x : string = "no"
# let y = "body";;
val y : string = "body"
# let z = xˆy;;
val z : string = "nobody"
The reader is encouraged to try a few other examples. One of the nice things
about sitting in an interactive loop is that it’s easy to experiment and see the results
immediately.
As well as basic values, OCaml also lets you define names for functions, which take
one or more parameters, or arguments, and compute a corresponding result. To define
such a function, simply add the arguments after the name when making a definition.
For example the following is the definition of a function that squares its argument:
The type int -> int means that square is a function from integers to in-
gegers. The function can then be applied to some particular arguments by writing them
after the function name:
# square 0;;
val it : int = 0
# square 8;;
val it : int = 64
Note that while in normal mathematical notation it’s compulsory to use parentheses
round function arguments (we write f (x) not f x in informal mathematics), they are
optional in OCaml and most people don’t use them. However, they can always be used
to establish precedence just as in any other situation, or merely used for familiarity’s
sake:
# square(2 + 2);;
val it : int = 16
# square(2) + 2;;
val it : int = 6
11
Functions can have multiple arguments written one after the other. We will explain
the type OCaml prints more carefully later, but for now simply think of it as meaning a
function that takes two integers and returns another:
3 HOL basics
In the previous section we evaluated expressions containing numbers and strings. HOL
Light — hereinafter just ‘HOL’ — is a suite of tools for evaluating expressions involv-
ing terms (representing mathematical expressions or logical assertions) and theorems
(representing assertions that have been proved).
3.1 Terms
To enter terms into the system, you can type them between backquotes:
# ‘x + 1‘;;
val it : term = ‘x + 1‘
# "x + 1";;
val it : string = "x + 1"
Terms are like strings in that they are manipulated purely as symbolic expres-
sions. However, terms are not simply represented as sequences of characters, but
using a richer tree-structured representation, something like a ‘abstract syntax tree’.
The OCaml toplevel automatically attempts to parse anything in backquotes into the
internal representation, and it prints it in a similar fashion, but this is just for human
convenience. For example, several superficial variants of the input get mapped to the
same internal representation and are printed in the same way, while some malformed
expressions will not be parsed at all:
# ‘(x) + 1‘;;
val it : term = ‘x + 1‘
# ‘(x + (1))‘;;
val it : term = ‘x + 1‘
# ‘x + +‘;;
Exception: Failure "term after + expected".
The internal form is usually rather unpalatable for humans, as you can see by dis-
abling the automatic prettyprinting using the following Ocaml directive:
# #remove_printer print_qterm;;
# ‘x + 1‘;;
val it : term =
Comb (Comb (Const ("+", ‘:num->num->num‘), Var ("x", ‘:num‘)),
Comb (Const ("NUMERAL", ‘:num->num‘),
Comb (Const ("BIT1", ‘:num->num‘), Const ("_0", ‘:num‘))))
12
We will look in more detail at the internal representation later, since it is important
for advanced use of the system, but for now we will ignore it and restore the usual
behavior with:
#install_printer print_qterm;;
HOL provides a number of operations for manipulating terms. For example subst
will replace one term by another at all its occurrences in another term, e.g. replace ‘1’
by ‘2’ in the term ‘x + 1’. The syntax is analogous to the logical notation [2/1](x + 1)’
or (x + 1)[2/1] that one often sees:
The reason for entering ‘x:num’ rather than just ‘x’ lies in HOL’s type system,
explained next.
3.2 Types
A key feature of HOL is that every term has a well-defined type. Roughly speaking,
the type indicates what kind of mathematical object the term represents (a number, a
set, a function, etc.) The possible types of terms are represented using another sym-
bolic datatype hol type, and these will similarly be automatically parsed and printed
within backquotes with a colon as the first character.3
# ‘:num‘;;
val it : hol_type = ‘:num‘
You can find the type of a term by applying the type of operator to it:
# type_of ‘1‘;;
val it : hol_type = ‘:num‘
# type_of ‘x + 1‘;;
val it : hol_type = ‘:num‘
# type_of ‘x + 1 < x + 2‘;;
val it : hol_type = ‘:bool‘
The type of the terms ‘1’ and ‘x + 1’ is :num, meaning that they represent natural
numbers, i.e. nonnegative whole numbers. (In more conventional mathematical terms
we would write 1 ∈ N and x + 1 ∈ N to capture the information in HOL’s type
assignment.) On the other hand, the term ‘x + 1 < x + 2’ is of type bool (Boolean),
meaning that it is an assertion that may be true or false (in this case it happens to be
true). If HOL is able to assign a type to a term, but it is not determined uniquely, a
general type will be assigned automatically:
3 It is apt to be confusing that we have quite separate notions of ‘type’ in the HOL logic and in OCaml
itself. Indeed, a recurrent theme in what follows will be the close similarity between certain concepts at the
OCaml and logical levels. In the customary jargon, we can think of OCaml as the ‘meta-language’ used to
reason about the logic (this explains the last two letters of ‘OCaml’). Once you get used to distinguishing
these levels, the similarities can begin to be helpful rather than confusing.
13
# ‘x‘;;
Warning: inventing type variables
val it : term = ‘x‘
# type_of it;;
val it : hol_type = ‘:?48538‘
but you can impose a chosen type on any term by writing ‘:<type>‘ after it:
# ‘x:num‘;;
val it : term = ‘x‘
# ‘x:bool‘;;
val it : term = ‘x‘
(Variables like this that share the same name yet have different types are considered
completely different.) No annotations were needed in the composite term ‘x + 1’
because HOL automatically allocates type ‘num’ to the constant 1, and infers the same
type for x because the two operands to the addition operator must have the same type.
But you can attach type annotations to subterms of composite terms where necessary
or simply desired for emphasis:
# ‘(x:num) = y‘;;
val it : term = ‘x = y‘
# ‘(x:num) + 1‘;;
val it : term = ‘x + 1‘
Because of typing, some terms that are syntactically well-formed will nevertheless
be rejected by the quotation parser because they cannot be typed. For example here we
attempt to add something of type ‘bool’ to something of type ‘num’:
The value of types is that they can filter out such ‘nonsensical’ terms from the start,
and keep track of certain intuitive constraints (‘n represents a number’) without special
user guidance. On the negative side, they can sometimes be inflexible. For instance you
cannot directly add a natural number and a real number, since HOL considers these as
distinct types, even though intuitively one might imagine N ⊆ R:
# ‘(x:num) + (y:real)‘;;
Exception: Failure "unify: types cannot be unified".
3.3 Theorems
We noted that a term of type bool, which we will often call a formula, may be true or
false. For example, intuitively speaking the first term below is true (whatever value x
may have) and the second is false:
# ‘x + 1 < x + 2‘;;
val it : term = ‘x + 1 < x + 2‘
# ‘2 + 2 = 5‘;;
val it : term = ‘2 + 2 = 5‘
14
HOL does not directly use any concept of ‘truth’ or ‘falsity’. It does however have
a notion of when a formula has been proved using the accepted methods of proof, and
these methods have, needless to say, been chosen so that anything provable is also
true.4 The usual aim when using HOL is to state an assertion precisely in its formal
logic and then to prove it.
In traditional formal logic, a formula is proved by applying a well-defined set of
syntactic rules to some initial axioms; one writes ` p to mean that p is provable, and
more generally p1 , . . . , pn ` p to mean that p is provable starting from assumptions
p1 , . . . , pn . In HOL, a similar notion is put in a more computational form. A special
type thm (‘theorem’) is used for formulas that have been — actually have been, not
merely can be — proved. Initially, the only OCaml objects of type thm are the HOL
axioms, and the only way of creating new objects of type thm is to apply a limited set
of primitive rules of inference. (The complete list of axioms and primitive rules is quite
short.) What we call an ‘inference rule’ in HOL is no more and no less than an OCaml
function returning something of type thm (or some composite type thereof, e.g. a pair
or a list of theorems).
For example, perhaps the simplest inference rule of the HOL logic is the reflexivity
of equality. In HOL this rule is implemented by a function REFL, which takes a term
t and returns a theorem ` t = t. (As you can see from the output, theorems are
prettyprinted using an ASCII approximation to the usual ‘turnstile’ notation.)
# REFL ‘x:real‘;;
val it : thm = |- x = x
# let th1 = REFL ‘x + 1‘;;
val th1 : thm = |- x + 1 = x + 1
# ASSUME ‘2 + 2 = 5‘;;
val it : thm = 2 + 2 = 5 |- 2 + 2 = 5
# let th2 = ASSUME ‘2 * n = n + n‘;;
val th2 : thm = 2 * n = n + n |- 2 * n = n + n
# ASSUME ‘1‘;;
Exception: Failure "ASSUME: not a proposition".
4 There are fundamental results in logic implying that the converse cannot hold — see Smullyan (1992) for
a nice presentation. That is, for any given proof system that could be mechanized on a computer, including
HOL’s, there must be truths that are unprovable in it. However, this theoretical incompleteness does not
affect any everyday mathematical results, as far as we know.
15
Note that it will also instantiate variables in the assumptions in the same way, which
is necessary for the step to be logically sound in general:
# INST [‘2‘,‘n:num‘] th2;;
val it : thm = 2 * 2 = 2 + 2 |- 2 * 2 = 2 + 2
Moreover INST, unlike subst, will refuse to substitute for non-variables, which
in general is not a logically valid step. For example, the fact that 2 · n = n + n does
not imply that we can substitute n for 2 · n while remaining valid:5
# INST [‘2‘,‘2 * n‘] th2;;
Exception: Failure "dest_var: not a variable".
Although a theorem can only be constructed by proving it, you are always free to
break it down into its conclusion and hypotheses. For example, the concl function
returns the conclusion of a theorem as a term (which will always have Boolean type):
# concl;;
val it : thm -> term = <fun>
# concl th1;;
val it : term = ‘x + 1 = x + 1‘
In its usual form, while HOL generates all theorems by proof, the proofs are not
constructed as concrete objects. However, in the subdirectory Proofrecording
there is a system due to Steven Obua so that proofs are explicitly constructed and can
be dumped to a file in an XML-based format suitable for input to other systems, e.g. a
separate proof checker.
5 In the particular case that follows, it would be valid because the assumption is exactly the same as the
sumsofpowers/sop3.htm.
16
However, the crucial point to note is that under the surface, these are still being
proved by low-level rules at the level of REFL and INST. It is for this reason that even
these complex derived rules can be considered highly reliable: they cannot just ‘make’
something of type thm, but must prove it. Of course, doing so is not entirely trivial, but
has all been encapsulated in ARITH RULE so that from the user perspective, it looks
like an atomic operation. And in advanced use of the system, it’s invaluable to be able
to write custom derived rules for special situations.
Typically a proof in HOL proceeds as follows. The user employs special insight
into the problem to break it down into a series of relatively simple subproblems, and
once the subproblems are simple enough or fall within a limited enough domain, they
can be dealt with automatically by HOL. Generally speaking, the level of detail needed
before HOL can fill in the gaps is greater than most people are used to. On the other
hand, there are pleasant exceptions where one can replace a fairly long manual proof
with a single automated HOL step.
4 Propositional logic
Except for minor syntactic details, such as the fact that exponentiation is called EXP,
the notation HOL’s parser supports for arithmetic on natural numbers should look fairly
familiar. You can use named variables, numeric constants and various infix operators
with the usual rules of precedence, and expressions can be put in parentheses for em-
phasis or to override the rules of precedence.
Now, to manipulate formulas with a richer logical structure, it is important to mas-
ter the analogous notation HOL uses for building composite logical expressions out
of basic formulas using ‘logical connectives’. Readers are no doubt used to writing
symbols like ‘+’ rather than the word ‘plus’, and one needs similarly to get used to
using symbols in place of special logical words like ‘and’, ‘or’ and ‘not’ when stating
mathematical results. Here is a table showing conventional notation for the so-called
propositional (or Boolean) connectives, together with HOL’s ASCII approximations
and their approximate English reading.
⊥ F Falsity
> T Truth
¬ ˜ Not
∧ /\ And
∨ \/ Or
⇒ ==> Implies (‘if . . . then . . . ’)
⇔ <=> Iff (‘. . . if and only if . . . ’)
The analogy with ordinary algebraic notation is worth re-emphasizing. Truth and
falsity are logical constants to denote the true and false propositions, analogous to par-
ticular numbers like 0 and 42. Logical negation is a unary operation just like arithmeti-
cal negation of numbers. The other connectives are all binary operations analogous to
addition, multiplication, etc. Unlike arithmetic operators, there are only a finite num-
ber of possible arguments (all of them must be either true or false) so we can explicitly
17
display the meanings of the connectives using truth-tables showing the result corre-
sponding to each combination of arguments. The negation operator has the following
rather trivial truth-table:
p ¬p
false true
true false
For the binary connectives, we need four rows, for the 22 = 4 possible combina-
tions of two truth values. To save space we will put all the connectives in different
columns of the same table.
# ‘p \/ ˜p‘;;
val it : term = ‘p \/ ˜p‘
# # ASSUME ‘p /\ q‘;;
val it : thm = p /\ q |- p /\ q
18
# get_infix_status "==>";;
val it : int * string = (4, "right")
# get_infix_status "-";;
val it : int * string = (18, "left")
You can also make any other symbol you choose infix, or change the precedence of
existing infixes using ‘parse_as_infix’ as follows:
# parse_as_infix("<>",(12,"right"));;
val it : unit = ()
# parse_as_infix("+",(1,"left"));;
val it : unit = ()
# ‘x < x + 1‘;;
Exception: Failure "unify: types cannot be unified".
# parse_as_infix("+",(16,"right"));;
val it : unit = ()
Note that HOL does not specially interpret “chained” binary operators like x < y < z
to mean ‘x < y and y < z’, as mathematical notation often does. That attempt fails at
typechecking, while p ==> q ==> r is accepted but means p ⇒ (q ⇒ r), logically
equivalent to (p ∧ q) ⇒ r, and not (p ⇒ q) ∧ (q ⇒ r).
# TAUT ‘p \/ ˜p‘;;
val it : thm = |- p \/ ˜p
The following says that ‘p if and only if q’ is equivalent to ‘if p then q’ and ‘if q
then p’ together:
19
while the following, commonly known as the ‘de Morgan laws’, show an interesting
duality between ‘and’ and ‘or’. For example ‘I cannot speak Swedish and I cannot
speak Finnish’ is equivalent to ‘I cannot speak either Swedish or Finnish’:
Some tautologies may look a little surprising if you’re not used to them, but you can
always convince yourself by exhaustively considering all the possible cases according
as each propositional variable takes the value ‘true’ or ‘false’, using the truth-tables to
compute the result in each case. For example, the ‘iff’ operator is “associative”:
# TAUT ‘(p <=> (q <=> r)) <=> ((p <=> q) <=> r)‘;;
val it : thm = |- (p <=> q <=> r) <=> (p <=> q) <=> r
Since most people find the truth-table definition of implication unnatural, it’s not so
surprising that many of the strangest-looking tautologies involve implication, e.g. the
following which says that for two propositions p and q, one always implies the other:
Here’s another that may hit users who mix up precedence rules:
If the user supplies a non-tautology to TAUT then it will simply fail to return a
theorem at all:
It would not be very difficult to modify TAUT so that it gave an explicit counterex-
ample in such cases (‘that fails if p is true and q is false’). And while TAUT is generally
happy to accept composite terms instead of primitive Boolean formulas:
it just treats the atomic formulas (here x < 1 and y > 0) as separate and primitive, and
so won’t be able to exploit linkages between them:
20
That goal can be solved automatically by ARITH RULE, which actually analyzes
the arithmetical content of the formulas:
but in general, one can need arbitrarily complicated reasoning to establish validity in
such cases. For example, the following term is a statement of Fermat’s Last Theorem,
which we can hardly expect HOL to prove automatically given how much trouble it’s
given the human race:
while dually the rules CONJUNCT1 and CONJUNCT2 allow us to deduce p and q,
respectively, from p ∧ q:
# CONJUNCT1 thpq;;
val it : thm = p, q |- p
# CONJUNCT2 thpq;;
val it : thm = p, q |- q
Another quite important low-level rule is MP, which allows one to pass from p ⇒ q
and p to q, which is often an extremely useful way of linking theorems together. The
name is an abbreviation for ‘modus ponens’, the traditional if obscure name (Latin for
‘method for affirming’):
21
4.3 Logic design and verification
One reason for practical interest in propositional logic is that it corresponds quite
closely with the structures used in digital circuits, such as the computer on which you
are running HOL. To a reasonable approximation, each wire in a digital circuit at a
given time has one of two possible voltage levels which we can think of as ‘false’ (also
0 or ‘low’) and ‘true’ (also 1 or ‘high’). The logical building-blocks are circuit ele-
ments that have behavior mimicking the basic connectives of propositional logic. For
example, a (2-input) ‘AND gate’ is a circuit with two inputs and one output such that
the output is ‘true’ precisely if both inputs are ‘true’, corresponding to the propositional
connective ‘∧’.
The gates themselves in most modern microprocessors are themselves constructed
from even more basic components called transistors. A transistor is a kind of voltage-
controlled switch; more precisely, it is a 3-terminal device where the current flow be-
tween two of the terminals called ‘source’ and ‘drain’ is controlled by the voltage level
on the ‘gate’ input. In an n-type transistor, current flows between source and drain,
and hence source and drain voltages are equalized, when the gate voltage is high, but
otherwise there is a high resistance between them and so the source and drain voltages
can differ arbitrarily. We can model this behavior in HOL by the propositional formula:
Dually, in a p-type transistor, current flows and so the source and drain voltages are
equalized only when the gate voltage is low, which we can model as:
Let us see how a logic gate is normally built up from transistors using a CMOS7
arrangement. For example figure 1 shows a CMOS NOR gate, intended to realize the
logic function ‘not or’, i.e. output ⇔ ¬(inputa ∨ inputb ). The two transistors at the
top with the little circle on the gate are p-type, and the two at the bottom are n-type.
Let us use the name internal for the internal wire between the top two transistors.
The wires marked Vdd and Vss represent fixed high and low voltage wires respectively
(crudely, the positive and negative terminals of the power supply), corresponding to
‘true’ (T) and ‘false’ (F) in our Boolean model. Now we can write down a HOL formula
asserting that all the constraints implied by the circuit connections together imply that
we do indeed get the correct ‘NOR’ relationship between inputs and outputs, and verify
it using TAUT:
7 Complementary metal oxide semiconductor. The key word is complementary: there are complementary
pairs of transistors to connect the output to the high or low voltage supply, but never both at the same time.
22
V dd
Internal
Output
Input A
Input B
V ss
# TAUT
‘(˜input_a ==> (internal <=> T)) /\
(˜input_b ==> (output <=> internal)) /\
(input_a ==> (output <=> F)) /\
(input_b ==> (output <=> F))
==> (output <=> ˜(input_a \/ input_b))‘;;
val it : thm =
|- (˜input_a ==> (internal <=> T)) /\
(˜input_b ==> (output <=> internal)) /\
(input_a ==> (output <=> F)) /\
(input_b ==> (output <=> F))
==> (output <=> ˜(input_a \/ input_b))
This example wasn’t very difficult to verify by hand. On the other hand, HOL’s
tautology-checker can cope with somewhat larger examples where humans are apt to
be confused. For example, consider the following puzzle in circuit design (Wos 1998;
Wos and Pieper 1999):8
Show how to construct a digital circuit with three inputs i1 , i2 and i3 and
three outputs that are the respective negations o1 = ¬i1 , o2 = ¬i2 and
o3 = ¬i3 , using an arbitrary number of ‘AND’ and ‘OR’ gates but at most
two ‘NOT’ gates (inverters).
Be warned, this puzzle is surprisingly difficult, and readers might want to avert their
gaze from our solution below in order to enjoy thinking about it themselves. (Not that
8 Wos’s treatment of the puzzle is somewhat more impressive because he uses an automated reasoning
program to find the solution, not merely to check it. But we’re not quite ready for that yet. Wos attributes
this puzzle to one E. Snow from Intel.
23
a quick glance is likely to give you any strong intuition.) I came up with the following
after a considerable effort, and it’s sufficiently complicated that the correctness isn’t at
all obvious. But we can verify it in HOL quite easily. It takes a couple of seconds since
the algorithm used by TAUT is quite naive, but it certainly beats doing it by hand.
# TAUT
‘(i1 /\ i2 <=> a) /\
(i1 /\ i3 <=> b) /\
(i2 /\ i3 <=> c) /\
(i1 /\ c <=> d) /\
(m /\ r <=> e) /\
(m /\ w <=> f) /\
(n /\ w <=> g) /\
(p /\ w <=> h) /\
(q /\ w <=> i) /\
(s /\ x <=> j) /\
(t /\ x <=> k) /\
(v /\ x <=> l) /\
(i1 \/ i2 <=> m) /\
(i1 \/ i3 <=> n) /\
(i1 \/ q <=> p) /\
(i2 \/ i3 <=> q) /\
(i3 \/ a <=> r) /\
(a \/ w <=> s) /\
(b \/ w <=> t) /\
(d \/ h <=> u) /\
(c \/ w <=> v) /\
(˜e <=> w) /\
(˜u <=> x) /\
(i \/ l <=> o1) /\
(g \/ k <=> o2) /\
(f \/ j <=> o3)
==> (o1 <=> ˜i1) /\ (o2 <=> ˜i2) /\ (o3 <=> ˜i3)‘;;
Doing digital circuit verification in this way is open to the criticism that there’s no
checking that the assignments of internal wires are actually consistent. For example, if
we assert that an inverter has its output connected to its input, we can deduce anything.
(What connecting two wires with different voltages means in electrical terms depends
on physical details: the component may lock at one value, oscillate or burn out.)
However, we will see later how to show that the claimed assignments of the internal
wires are consistent.
• Variables
24
• Constants
• Applications
• Abstractions
Variables such as ‘x:bool‘ and ‘n:num‘ and constants like ‘true’ (‘T‘) and
‘false’ (‘F‘) are probably roughly what the reader expects.9 By an application, we
mean the application of a function to an argument, which is a composite term built from
subterms for the function and argument. For example, the term for ‘not p’, entered as
‘˜p‘, is an application of the constant denoting the negation operator to a Boolean
variable. You can enter the negation operator on its own as a term, but since it has a
special parse status, you need to enclose it in parentheses when it stands alone:
# ‘˜‘;;
Exception: Noparse.
# ‘(˜)‘;;
val it : term = ‘(˜)‘
We often use the customary jargon rator (ope-rator = function) and rand (ope-rand
= argument) for the two components of an application (which we will also sometimes
call a combination). There are correspondingly named functions on terms that will
break a combination apart into its operator and operand, or will fail if applied to a
non-combination:
# rator ‘˜p‘;;
val it : term = ‘(˜)‘
# rand ‘˜p‘;;
val it : term = ‘p‘
# rand ‘n:num‘;;
Exception: Failure "rand: Not a combination".
# ‘˜(p)‘;;
val it : term = ‘˜p‘
The critical point about applications in HOL, again as in OCaml, is that applications
can only be formed when the operator has some type α → β (a function mapping
arguments of type α to results of type β) and the operand the corresponding type α, in
which case the combination as a whole has type β. For example, as one might expect,
the negation operator has type bool → bool:
# type_of ‘(˜)‘;;
val it : hol_type = ‘:bool->bool‘
9 Note that numerals like 2 are not in fact constants, but composite expressions built from constants,
25
5.1 Curried functions
HOL’s term representation makes no special provision for functions of more than one
argument. However, functions may take one argument and yield another function, and
this can be exploited to get the same effect, a trick known as currying, after the logician
Haskell Curry. For example, rather than considering addition of natural numbers as a
binary function N × N → N, we consider it as a function N → (N → N). It accepts
a single argument a, and yields a new function of one argument that adds a to its
argument. This intermediate function is applied to the second argument, say b, and
yields the final result a + b. In other words, what we write as a + b is represented by
HOL as ((+) a)(b). And we can indeed enter that string explicitly; the underlying term
is exactly the same whether this or the usual infix surface syntax is used.
# ‘((+) x) 1‘;;
val it : term = ‘x + 1‘
# ‘((+) x) 1‘ = ‘x + 1‘;;
val it : bool = true
Note that precisely the same curried representation for binary functions is common
in OCaml itself too:
# (+);;
val it : int -> int -> int = <fun>
# (+) 1;;
val it : int -> int = <fun>
Although currying might seem like an obscure representational trick, it can some-
times be useful to consider in its own right the intermediate function arising from par-
tially applying a curried function. For example, we can define a successor operation
and use it separately.
Because currying is such a common operation, both OCaml and HOL adopt the
same conventions to reduce the number of parentheses needed when dealing with cur-
ried functions. Function application associates to the left, so f a b means (f a) b
not f(a(b)). Many beginners find this takes some getting used to:
# (+) 1 2;;
val it : int = 3
# ‘(+) 1 2‘;;
val it : term = ‘1 + 2‘
Also, iterated function types are right-associative, so the type of a binary curried
function α → β → γ means α → (β → γ).
26
5.2 Pairing
We can now start to see how more complex terms are represented internally, just using
constants, variables and applications. For example, the way this term is entered shows
more explicitly how it is built by applications from the constants for implication, equal-
ity and addition and the variables x, y, z and P . HOL prints it in a way closer to the
usual mathematical notation that the user would normally prefer:
Although currying is used for most infix operators in HOL and OCaml, they do
each have a type of ordered pairs. The type α#β in HOL, or α*β in OCaml, represents
the type of pairs whose first element has type α and whose second has type β. In other
words, if we think of the types as sets, the pair type is the Cartesian product of the
constituent sets. In both OCaml and HOL, an ordered pair is constructed by applying
a binary infix operator ‘,’:
# ‘1,2‘;;
val it : term = ‘1,2‘
# type_of it;;
val it : hol_type = ‘:num#num‘
# 1,2;;
val it : int * int = (1, 2)
As with function applications, the customary surrounding parentheses are not nec-
essary, but many users prefer to include them anyway for conformance with usual
mathematical notation — note that OCaml, unlike HOL, even prints them. Although
ordered pairs without the surrounding parentheses may seem unfamiliar, there is a con-
ceptual economy in regarding the comma as just another binary infix operator, not a
‘magical’ piece of syntax.10 Of course, in HOL at least, the pairing operation must be
curried, since it cannot itself be defined in terms of pairing. In fact all HOL’s prede-
fined binary infix operators, and most of OCaml’s, are curried. Partly this is because
currying is more logically fundamental, and partly because the ability to partially apply
functions can be quite useful. But some of the HOL syntax operations in OCaml are de-
fined using pairs. For example mk_comb, which builds an application out of function
and argument, takes a pair of arguments. Note that it will refuse to put together terms
whose types are incompatible — just like theorems, terms themselves are an abstract
type from which ill-typed ‘terms’ are excluded:
# mk_comb(‘(+) x‘,‘y:num‘);;
val it : term = ‘x + y‘
# mk_comb(‘(+) 1‘,‘T‘);;
Exception: Failure "mk_comb: types do not agree".
Similarly, the inference rule CONJ PAIR breaks a conjunctive theorem into a pair
of theorems:
# CONJ_PAIR(ASSUME ‘p /\ q‘);;
val it : thm * thm = (p /\ q |- p, p /\ q |- q)
10 In OCaml it is still slightly magical; for example there are explicit types of triples, quadruples and so
on, whereas in HOL ‘1,2,3‘ is just an iterated ordered pair (1, (2, 3)).
27
while MK COMB takes a pair of theorems Γ ` f = g and ∆ ` x = y and returns
Γ ∪ ∆ ` f (x) = g(y):
In OCaml there are functions fst (‘first’) and snd (‘second’) to select the two
components of an ordered pair. Similarly HOL has the same thing but in uppercase
(for no particular reason): FST and SND.
# fst(1,2);;
val it : int = 1
# snd(1,2);;
val it : int = 2
# ‘p /\ x = 1 <=> q‘;;
val it : term = ‘p /\ x = 1 <=> q‘
# ‘(p /\ x = 1) = q‘;;
val it : term = ‘p /\ x = 1 <=> q‘
# ‘p /\ x = 1 = q‘;;
val it : term = ‘p /\ (x <=> 1 = q)‘
It will probably be some time before the reader can appreciate the other definitions
of logical connectives in terms of equality, but to give one rough example of how this
can be done, observe that we can define the conjunction p ∧ q as (p, q) = (>, >) — in
other words, p ∧ q is true iff the pair (p, q) is equal to the pair (>, >), that is, if p and
q are both (equal to) true.
Even though, as we now know, equations are just terms of the form ((=)s)t, they
are sufficiently important that special derived syntax operations are defined for con-
structing and breaking apart equations. I hope from their names and the examples
below the reader will get the idea for the four operations mk_eq, dest_eq, lhs
(left-hand-side) and rhs (right-hand-side):
# mk_eq(‘1‘,‘2‘);;
val it : term = ‘1 = 2‘
# dest_eq it;;
val it : term * term = (‘1‘, ‘2‘)
# lhs ‘1 = 2‘;;
val it : term = ‘1‘
# rhs ‘1 = 2‘;;
val it : term = ‘2‘
28
Three fundamental properties of the equality relation are that it is reflexive (t = t
always holds), symmetric (if s = t then t = s) and transitive (if s = t and t = u then
s = u). Each of these properties has a corresponding HOL inference rule. We have
already seen the one for reflexivity (REFL); here also are the ones for symmetry (SYM)
and transitivity (TRANS) in action:
# REFL ‘1‘;;
val it : thm = |- 1 = 1
# SYM (ARITH_RULE ‘1 + 1 = 2‘);;
val it : thm = |- 2 = 1 + 1
# SYM (ARITH_RULE ‘2 + 2 = 4‘);;
val it : thm = |- 4 = 2 + 2
# TRANS (ARITH_RULE ‘1 + 3 = 4‘) it;;
val it : thm = |- 1 + 3 = 2 + 2
We have just seen the congruence rule MK_COMB. Another similar simple rule is
EQ_MP, which takes a theorem Γ ` p ⇔ q (in other words Γ ` p = q for Boolean p
and q) and another theorem ∆ ` p and returns Γ ∪ ∆ ` q. It might now be instructive
to see how SYM, which is not primitive, is defined. First we need to explain a couple
of features of OCaml that we haven’t mentioned yet. OCaml definitions can be local
to a particular expression, so expressions and definitions can be intertwined arbitrarily
deeply. This can be understood in terms of the following abstract syntax:
For example, we may define x and y at the top level, in which case they are usable
afterwards:
Another useful feature is that the left-hand side of a binding need not simply be a
variable, but can be a more general pattern. For example, after
29
# let pair = (1,2);;
val pair : int * int = (1, 2)
Now let us consider the definition of the derived inference rule SYM:
let SYM th =
let tm = concl th in
let l,r = dest_eq tm in
let lth = REFL l in
EQ_MP (MK_COMB(AP_TERM (rator (rator tm)) th,lth)) lth;;
To understand this and similar definitions of derived rules, it’s worth tracing through
a simple ‘generic’ instance step-by-step. So let’s create a theorem with conclusion
l = r and give it the name th, to match the argument:
Now we can trace through the workings of the inference rule step-by-step and see
how we manage to get the final theorem with conclusion r = l. As well as mimicking
the local definitions, we break up the large expression in the last line using it to hold
intermediate values:
That was a bit obscure, but now that it’s wrapped up as a general rule SYM, appli-
cable to any equation, we no longer need to worry about its internal workings.
30
5.4 Definitions
HOL allows you to define new constants. If you want to define a new constant c as a
shorthand for a term t, simply apply the function new_definition to an equation
v = t where v is a variable with the desired name. The function defines a constant c
and returns the corresponding theorem ` c = t with c now a constant.
since that would lead to logical inconsistency: from the two theorems ` c = 232 and
` c = 264 you could deduce ` 232 = 264 and so ` ⊥. Just as with OCaml, you can
define functions that take arguments:11
Much more general forms of definition using recursion and sophisticated pattern-
matching are possible, and will be considered later.
11 The exclamation mark you see in the returned theorem will be explained in the next section.
31
However, variables in mathematics are often used in a different way. In the sum
P∞ 2
R 1 −x2
n=1 1/n , n is not a free variable; similarly for x in the integral 0 e dx and
k in the set comprehension {k 2 | k ∈ N}. Rather, in such cases the variable is used
internally to indicate a correspondence between different parts of the expression. Such
a variable is said to be bound. If we consider a subexpression in isolation, such as 1/n2 ,
then a variable may P∞ be free in the usual sense, but when it is wrapped inside a binding
construct such as n=1 it is, so to speak, ‘captured’, and has no independent meaning
outside. A bound variable is somewhat analogous to a pronoun in ordinary language,
which is used locally to refer back to some noun established earlier but has no meaning
outside. For example in the sentence ‘He closed the book and put it in his bag’, the
meaning of ‘it’ is only local to this sentence and established by the earlier use of the
noun ‘the book’. In the next sentence the word ‘it’ may refer to something different:
‘He closed the book and put it in his bag. The weather was fine and he wanted to go
outdoors and enjoy it.’
There are many variable-binding constructs in mathematics. HOL does not have a
profusion of variable-binding notions as primitive, but expresses them all in terms of
just one, abstraction, which is is a converse operation to function application. Given
a variable x and a term t, which may or may not contain x, one can construct the so-
called lambda-abstraction λx.t, which means ‘the function of x that yields t’. In HOL’s
ASCII concrete syntax the backslash is used, e.g. \x. t. For example, λx. x + 1 is
the function that adds one to its argument, and we can write it as a HOL term thus:
# ‘\x. x + 1‘;;
val it : term = ‘\x. x + 1‘
# type_of it;;
val it : hol_type = ‘:num->num‘
The ‘lambda’ symbol is a standard piece of logical jargon that one just has to get
used to. In OCaml, the analogous construct is written using the fun keyword rather
than ‘λ’, and with -> instead of the dot:
The precise sense in which abstraction and application are inverse operations in
HOL is that there is a primitive inference rule BETA which tells us that ` (λx.t) x = t.
For example:
32
More interestingly, note what happens when we apply instantiation to the theorem
th above:
Note that the bound instances of x were left alone but the two free ones have been
instantiated to y. Thus in two steps we can get a more general notion of applying an
abstraction to an arbitrary argument and getting an appropriately substituted instance
of the abstraction’s body.
In both OCaml and HOL, definitions with arguments can just be considered as
shorthand for basic definitions of lambda-expressions. For example in OCaml these
are completely equivalent:
6.1 Quantifiers
Suppose P is a term of type α → bool for some α; we will often refer to a term with
such a type as a predicate. Note that we can equally well think of a predicate as a set, or
more precisely a subset of the type α. In fact, HOL does not define any separate notion
of set but defines an infix set membership symbol IN (corresponding to the usual ‘∈’)
simply by x IN s <=> s x. It is thus normal and often convenient to slip between
thinking of functions into bool as predicates or as sets, even within the same term.
It is often useful to be able to say of a predicate that it is true (= yields the value
>) for all values of its argument, or that there exists an argument for which it is true,
or even that there exists a unique argument for which it is true.12 In traditional ‘first-
order logic’ — see Enderton (1972) or Mendelson (1987) for good introductions —
predicates are considered separately from other functions, and special ‘quantifiers’ to
say things like ‘for all’ are a primitive notion of the language. We don’t need any
such special measures but can define all kinds of quantification using more primitive
notions.
For example, to say that P holds for all arguments is just to say that it is (equal
to) the constant function with value ‘true’, that is, P = λx. >.13 So we can define the
universal quantifier, normally written ∀ and pronounced ‘for all’, such that for any P ,
(∀)P ⇔ (P = λx. >), simply by:
are infinitely many, or that the set of arguments for which it holds have measure zero. We can indeed say all
these things, but the three we noted in the main text are so fundamental that they deserve special treatment.
13 This claim perhaps presupposes that equality on functions is extensional, but this is the case in HOL.
33
# FORALL_DEF;;
val it : thm = |- (!) = (\P. P = (\x. T))
In practice, the predicate to which one wishes to apply the quantifier is usually a
lambda-expression. For example, to say that ‘for all n, n < n + 1’, we would use
∀(λn. n < n + 1). Since this is such a common construction, it’s convenient to write
it simply as ∀n. n < n + 1, which is close to the traditional notation in first-order
logic. HOL will parse and print this form, but note that the underlying term consists of
a constant applied to an abstraction:
Constants such as ‘!’ that are parsed and printed in this abbreviated form when
applied to abstractions are called binders, because they appear to bind variables. (But
strictly speaking they don’t: it’s the underlying abstraction that does that.) For the
moment the three important ones are the three quantifiers:
∀ ! For all
∃ ? There exists
∃! ?! There exists a unique
Another convenient syntactic abbreviation that applies to lambda-abstractions and
all other binder constructs is that iterated applications can be condensed. For example,
instead of writing λx. λy. x + y, we can just write λx y. x + y, and similarly for
multiple quantifiers of the same kind. But as usual, this is just a piece of convenient
surface syntax and the underlying term does have the core construct iterated:
# ‘!m n. m + n = m + m‘;;
val it : term = ‘!m n. m + n = m + m‘
# dest_comb it;;
val it : term * term = (‘(!)‘, ‘\m. !n. m + n = m + m‘)
All the quantifiers have introduction and elimination rules derived from their def-
initions. For the universal quantifier, the two main rules are GEN (generalize) which
universally quantifies one of the free variables, and the converse operation SPEC (spe-
cialize) which instantiates a universally quantified variable. For example, here we gen-
eralize a variable and then specialize it again; this is somewhat pointless here since the
same effect could be had directly using INST:
Note that GEN is not applicable if the variable you’re trying to generalize over
occurs (free) in the hypotheses. This is necessary to ensure logical consistency, and
arises naturally from GEN’s implementation as a derived rule:
34
# ASSUME ‘x + 1 = 1 + x‘;;
val it : thm = x + 1 = 1 + x |- x + 1 = 1 + x
# GEN ‘x:num‘ it;;
Exception: Failure "GEN".
There are also ‘list’ versions SPECL and GENL which allow one to specialize and
generalize multiple variables together, but they can just be considered a shorthand for
iterated application of the more basic functions in the appropriate order:
• ∀x. ∃y. loves(x, y) means that everyone has someone they love
• ∀y. ∃x. loves(x, y) means that everyone has someone who loves them
• ∃y. ∀x. loves(x, y) means that some (fixed) person is loved by everyone.
∀. > 0 ⇒ ∀x. ∃δ. δ > 0 ∧ ∀x0 . |x0 − x| < δ ⇒ |f (x0 ) − f (x)| < ε
Uniform continuity, on the other hand asserts that given > 0 there is a δ > 0
independent of x such that for any x and x0 , whenever |x0 − x| < δ, we also have
|f (x0 ) − f (x)| < ε. Note how the changed order of quantification radically changes
the asserted property.
∀. > 0 ⇒ ∃δ. δ > 0 ∧ ∀x. ∀x0 . |x0 − x| < δ ⇒ |f (x0 ) − f (x)| < ε
The tautology-prover TAUT cannot handle non-trivial quantifier reasoning, but there
is a more powerful automated tool called MESON that can be quite convenient. Note
that deciding validity in quantification theory is an undecidable problem, but MESON
uses an automated proof search method called ‘model elimination’ (Loveland 1968;
Stickel 1988) that often succeeds on valid formulas. (It usually fails by looping indef-
initely or hitting a depth limit rather than showing that the formula is not valid.) For
example, we can prove classic “syllogisms”:
35
# MESON[] ‘(!x. man(x) ==> mortal(x)) /\ man(Socrates) ==> mortal(Socrates)‘;;
Warning: inventing type variables
0 inferences so far. Searching with maximum size 0.
0 inferences so far. Searching with maximum size 1.
1 inferences so far. Searching with maximum size 2.
Goal solved with 4 inferences.
CPU time (user): 0.
val it : thm =
|- (!x. man x ==> mortal x) /\ man Socrates ==> mortal Socrates
# MESON[]
‘((?x. !y. P(x) <=> P(y)) <=> ((?x. Q(x)) <=> (!y. Q(y)))) <=>
((?x. !y. Q(x) <=> Q(y)) <=> ((?x. P(x)) <=> (!y. P(y))))‘;;
Sometimes, indeed, MESON can automatically prove things that people don’t find
so obvious, such as the following example due to Łoś (Rudnicki 1987). This asserts
that for two binary relations P and Q on a set (type) A, both transitive and at least one
symmetric, if their union covers the whole set A × A then one or other of the relations
is already all of A × A. The machine proves this a lot faster than I could:
# MESON[]
‘(!x y z. P x y /\ P y z ==> P x z) /\
(!x y z. Q x y /\ Q y z ==> Q x z) /\
(!x y. P x y ==> P y x) /\
(!x y. P x y \/ Q x y)
==> (!x y. P x y) \/ (!x y. Q x y)‘;;
But MESON is only capable of proving purely logical facts, that is, those that hold
whatever the interpretation of the constants involved may be. For example, if we try to
use it to prove that ‘6’ is reflexive or that 2 + 2 = 4 we will fail:
The reason is that the fact relies on special properties of the inequality relation that
do not hold of a general binary relation. On the other hand, the following much less
obvious problem — attributed by Dijkstra (1989) to Hoare — is solved instantly.
The point is that even though this also uses the normal inequality relation, this is
inessential, and it’s equally valid if we replace it with an arbitrary binary relation:
36
# let ewd1062 = MESON[]
‘(!x. R x x) /\
(!x y z. R x y /\ R y z ==> R x z) /\
(!x y. R (f x) y <=> R x (g y))
==> (!x y. R x y ==> R (f x) (f y)) /\
(!x y. R x y ==> R (g x) (g y))‘;;
The main constants that MESON treats specially are the logical connectives and the
equality relation — the latter is anyway needed in order to handle ‘unique existence’
quantifiers as in the following problem14
# MESON[] ‘(?!x. g(f x) = x) <=> (?!y. f(g y) = y)‘;;
This doesn’t mean that you can’t use MESON to reason about other constants, but
just that you need to supply it with the necessary properties of those constants to use.
These properties might simply be the definitions, or more likely some higher-level
results about it. For example, if we tell MESON that addition is associative and com-
mutative, it can deduce other theorems in consequence:
# MESON [ADD_ASSOC; ADD_SYM] ‘m + (n + p) = n + (m + p)‘;;
1515.html
37
7.1 Conversionals
The advantage of the uniform approach of conversions is that there are generic func-
tions, often called ‘conversionals’, for putting together basic conversions into more
complex ones. For example, THENC, used infix, takes two conversions and returns the
new conversion that results from applying the first conversion and then applying the
second to the result. Given that we need to maintain an equational theorem, this is
more complicated than simply composition of functions, but the implementation is not
so difficult, essentially:
# let (THENC) =
fun conv1 conv2 t ->
let th1 = conv1 t in
let th2 = conv2 (rand(concl th1)) in
TRANS th1 th2;;
For example, we can construct a conversion that will first do beta-conversion and
then perform addition on the result:
Another useful building-block is the infix conversional ORELSEC which tries the
first conversional, but if it fails (generates an exception), tries the second instead. For
example, we can construct a conversion that will evaluate either addition, multiplication
or exponentiation as follows:
In cases where they can’t do anything, many conversions will naturally generate an
exception. Others will just return a reflexive theorem. It’s easy to swap one behavior for
another by using TRY_CONV, which will try a conversion but happily return a reflexive
theorem if it fails:
and CHANGED_CONV, which will generate a failure if its argument conversion re-
turned a reflexive theorem. One can also apply a conversion repeatedly until it fails
by REPEATC, e.g:
38
7.2 Depth conversions
Other useful conversionals allow one to apply conversions to subterms of a given term.
The basic ones are RAND_CONV, which applies a conversion to the rand of a combina-
tion:
# RAND_CONV NUM_ADD_CONV ‘1 + 2 + 3‘;;
val it : thm = |- 1 + 2 + 3 = 1 + 5
From those basic ones, some others are defined, including LAND_CONV (apply to
the left-hand argument of a binary operator), BINDER_CONV (apply to the body of a
binder such as a quantifiers) and BINOP_CONV (apply to both arguments of a binary
operator. In order to direct a conversion to a particular part of a term you can just
compose these primitives, for example:
Figuring out the right series of conversionals can be painful; one perhaps more
convenient alternative is PAT_CONV, which takes an abstraction term indicating where
to apply the conversion. The particular names and types in the pattern are irrelevant so
long as it identifies the structure:
39
# DEPTH_CONV NUM_AME_CONV ‘(2 EXP 8 + 1) * (3 * 2 EXP 5 + 1)‘;;
val it : thm = |- (2 EXP 8 + 1) * (3 * 2 EXP 5 + 1) = 24929
However, DEPTH_CONV isn’t always what’s needed; sometimes the act of apply-
ing a conversion at one level can create new applicable terms lower down; in this case
DEPTH_CONV will not reexamine them. Two other conversionals, TOP_DEPTH_CONV
and REDEPTH_CONV, will keep applying conversions as long as possible all over the
term.
The difference between these two depth conversions is that the main sweeps are
respectively top-down and bottom-up, which can lead to one or the other being prefer-
able, mainly for efficiency reasons, in some situations.
7.3 Matching
The reader may recall the ‘modus ponens’ inference rule MP, which allows one to pass
from ` p ⇒ q and ` p to ` q. However, the theorems must match contain exactly the
same p:15
# LT_IMP_LE;;
val it : thm = |- !m n. m < n ==> m <= n
# MP LT_IMP_LE (ARITH_RULE ‘1 < 2‘);;
Exception: Failure "dest_binary".
# MP (SPECL [‘1‘; ‘2‘] LT_IMP_LE) (ARITH_RULE ‘1 < 2‘);;
val it : thm = |- 1 <= 2
In cases like this, it’s more convenient to delegate to HOL the task of choosing the
correct instantiation for free and universally quantified variables (using SPECL, INST
etc.) to make one theorem match another. The more powerful rule MATCH_MP does
just that: the second argument theorem is considered fixed, and the first theorem is
instantiated to match it:
15 Well, that’s almost true. Strictly speaking they can have different bound variable names.
40
There is also a matching rule REWR_CONV (‘rewriting conversion’), which takes
an equational theorem s = t and produces a conversion that when applied to a term
s0 which s can be matched to, returns the corresponding theorem ` s0 = t0 , and fails
otherwise:
# REWR_CONV ADD_SYM ‘7 + 8‘;;
val it : thm = |- 7 + 8 = 8 + 7
Because ‘if and only if’ in HOL is simply equality too, this can also be used for
theorems of the form ` p ⇔ q. This means that a lot of logical transformations can
also be implemented by rewriting. For example:
# LE_LT;;
val it : thm = |- !m n. m <= n <=> m < n \/ m = n
# REWR_CONV LE_LT ‘1 <= x‘;;
val it : thm = |- 1 <= x <=> 1 < x \/ 1 = x
Matching is slightly more general than simply instantiating variables; it can also
shuffle terms around a little using beta-conversion to make more things match. (This is
commonly called ‘higher-order matching’.) For example consider using the following
theorem, a kind of infinitary de Morgan’s law, saying that it is not the case that all x
satisfy P if and only if there exists some x that does not satisfy P :
# NOT_FORALL_THM;;
val it : thm = |- !P. ˜(!x. P x) <=> (?x. ˜P x)
# let NOT_FORALL_CONV = REWR_CONV NOT_FORALL_THM;;
val ( NOT_FORALL_CONV ) : term -> thm = <fun>
That all works as expected. Perhaps slightly more surprising is the following:
Using naive matching, this just doesn’t work: we need to match P x against
n * n = n + n, which if you think of the underlying term structure means set-
ting P to (=) (n * n) and x to n + n. But the second assignment is impossible
since x is a bound variable, and the first actually doesn’t work either.16 What HOL
actually does is a kind of reverse beta-reduction to make the terms matchable, followed
by a targeted beta-conversion afterwards to get back to the initial form, rather as we
might do manually like this:
41
7.4 Rewriting
Using REWR_CONV in association with depth conversions one can rewrite a term as
much as possible with a set of equational theorems. For example, if we rewrite a term
repeatedly using associativity of addition we can get a fully left-associated form:
# TOP_DEPTH_CONV(REWR_CONV ADD_ASSOC)
‘(a + b) + ((c + d) + e) + f + g + h‘;;
val it : thm =
|- (a + b) + ((c + d) + e) + f + g + h =
((((((a + b) + c) + d) + e) + f) + g) + h
Actually, if we want the result to look nicest, it’s better to rewrite the other way
since addition is right-associative and we’ll then get minimal bracketing. We can’t quite
use SYM on the theorem ADD_ASSOC because it’s universally quantified, but a slight
generalization GSYM (generalized symmetry) will descend under universal quantifiers:
# ADD_ASSOC;;
val it : thm = |- !m n p. m + n + p = (m + n) + p
# GSYM it;;
val it : thm = |- !m n p. (m + n) + p = m + n + p
# TOP_DEPTH_CONV(REWR_CONV it)
‘(a + b) + (((c + d) + e) + (f + g + h))‘;;
val it : thm =
|- (a + b) + ((c + d) + e) + f + g + h = a + b + c + d + e + f + g + h
Except for some optimizations in the successive matching, this is basically the same
as:
There is one significant generalization, however: the list of theorems can include
conjunctions of equations which will be broken down into individual rewrites, which
are not handled by REWR_CONV:
# ADD_CLAUSES;;
val it : thm =
|- (!n. 0 + n = n) /\
(!m. m + 0 = m) /\
(!m n. SUC m + n = SUC (m + n)) /\
(!m n. m + SUC n = SUC (m + n))
# REWR_CONV ADD_CLAUSES;;
Exception: Failure "dest_eq".
# GEN_REWRITE_CONV REDEPTH_CONV [ADD_CLAUSES];;
val it : conv = <fun>
# it ‘SUC(0 + SUC(SUC 0 + 0))‘;;
val it : thm = |- SUC (0 + SUC (SUC 0 + 0)) = SUC (SUC (SUC 0))
# GEN_REWRITE_CONV(PAT_CONV ‘\x. f(c,x) + g(x,x)‘) [ADD_SYM]
‘f(1 + 2,3 + 4) + g(5 + 6,7 + 8)‘;;
Warning: inventing type variables
val it : thm =
|- f (1 + 2,3 + 4) + g (5 + 6,7 + 8) = f (1 + 2,4 + 3) + g (6 + 5,8 + 7)
42
The most common traversal strategy is TOP_DEPTH_CONV, and this is hardwired
into PURE_REWRITE_CONV, essentially the same as GEN_REWRITE_CONV TOP_DEPTH_CONV.
The rewriting conversion with the shortest name, REWRITE_CONV, has the same basic
strategy as PURE_REWRITE_CONV, except that a suite of standard rewrite rules are
always included in the rewrites in addition to the list supplied to it. You can get a list
of these by doing the following:
# basic_rewrites();;
In fact, HOL’s syntactic ordering is defined in such a way that rewriting with the
following suite of properties (AC = associative and commutative) will rewrite sums
into an entirely canonical form:
# ADD_AC;;
val it : thm =
|- m + n = n + m /\ (m + n) + p = m + n + p /\ m + n + p = n + m + p
# REWRITE_CONV[ADD_AC] ‘(a + b) + (b + a) + (c + d) + (b + a) + (d + c)‘;;
val it : thm =
|- (a + b) + (b + a) + (c + d) + (b + a) + d + c =
a + a + a + b + b + b + c + c + d + d
In particular, this together with the built-in reflexivity rewrites will dispose of the
task of proving equivalence under associative and commutative laws:
# REWRITE_CONV[ADD_AC]
‘(a + b) + (b + a) + (c + d) + (b + a) + d + c =
c + a + d + b + b + c + a + a + b + d‘;;
val it : thm =
|- (a + b) + (b + a) + (c + d) + (b + a) + d + c =
c + a + d + b + b + c + a + a + b + d <=>
T
17 This approach to rewriting originates with Boyer and Moore; see Martin and Nipkow (1990) for more
information.
43
For a more sophisticated example, here we normalize arithmetic expressions built
using addition and multiplication by first applying distributivity to ‘multiply out’ terms,
then normalizing products, and finally normalizing sums, giving a canonical ‘sum of
products’ (SOP) form. We use THENC to plug together all the steps. It would be
possible to simply throw all these steps into a single rewrite, but it might be a lot
slower because interactions between the rules can cause redundant steps. For example,
in a sum s + t that is normalized with respect to addition, changes within s and t may
cause the order to change and necessitate a swap to t + s, and so on.
# let SOP_CONV =
REWRITE_CONV[LEFT_ADD_DISTRIB; RIGHT_ADD_DISTRIB] THENC
REWRITE_CONV[MULT_AC] THENC
REWRITE_CONV[ADD_AC];;
val ( SOP_CONV ) : conv = <fun>
# SOP_CONV ‘(a + b) * (c + d) + (a + c) * (b + d)‘;;
val it : thm =
|- (a + b) * (c + d) + (a + c) * (b + d) =
a * b + a * c + a * d + a * d + b * c + b * c + b * d + c * d
Similar ‘AC’ theorems are proved for many associative and commutative operators;
for example there are INT_ADD_AC for integer addition and REAL_MUL_AC for real
multiplication. Some operators ‘⊗’ are also idempotent, meaning x ⊗ x = x. In this
case there is a correspondingly stronger suite whose name ends in _ACI, e.g.
# CONJ_ACI;;
val it : thm =
|- (p /\ q <=> q /\ p) /\
((p /\ q) /\ r <=> p /\ q /\ r) /\
(p /\ q /\ r <=> q /\ p /\ r) /\
(p /\ p <=> p) /\
(p /\ p /\ q <=> p /\ q)
44
assumptions that you are allowed to make. HOL provides a ‘goalstack’ of the goals
that need to be proved to solve the original goal.
A tactic takes a goal and reduces it to a list of subgoals. But it also keeps track of
how to construct a proof of the main goal if the user succeeds in proving the subgoal;
this is simply an ML function. So the user can keep applying tactics, and the forward
proof is reconstructed by HOL. It’s rather as if the machine automatically reverses the
user’s proof and converts it to the standard primitive inferences. The user can perform
the proof via a mixture of forward and backward steps, as desired.
and then reduces it to subgoals using tactics via the e (‘expand’) function. One simple
tactic is DISCH_TAC, which will put the antecedent of an implicational goal into the
list of hypotheses. We now have to prove not p ∧ q ⇒ p, but rather the subtly different
goal of p under the assumption p ∧ q.
# e DISCH_TAC;;
val it : goalstack = 1 subgoal (1 total)
If you change your mind or realize you’ve made a mistake, you can ‘undo’ a step with
b() (‘back up’), and restore the previous goalstack:
# b();;
val it : goalstack = 1 subgoal (1 total)
In order to understand better what goes on inside a tactic, and indeed to appreciate
the naming convention, it’s worth considering an analogous inference rule DISCH.
This takes a theorem of the form Γ ` q and produces Γ − {p} ` p ⇒ q (where p
is usually in Γ, but need not be). It is important to understand why DISCH_TAC is
considered as the natural partner to the inference rule DISCH, hence the name, even
though they seem to make the opposite transformation.
In applying an inference rule to a theorem th to get another one th0 , we show that
0
th follows from th. On the other hand, in applying a tactic to move from a goal g to
a subgoal g 0 , the corresponding claim is that g follows from g 0 , though not necessarily
conversely. That is, the truth of the subgoals arising from applying a tactic are sufficient
for the initial goal to hold, but may not be necessary. One does sometimes reduce goals
45
to goals that are logically stronger, because they are still “simpler”. For example, one
might reduce the goal E + E 0 = E 0 + E for some very large expressions E and E 0
to a stronger but simpler goal ∀x y. x + y = y + x. Of course, one must beware of
reducing a provable goal to one that is unprovable!
One very convenient way of getting a tactic is to apply the function CONV_TAC
to a conversion, which gives a tactic that applies the corresponding conversion to the
conclusion of the goal. This has a particularly appealing feature that you then know
the original goal and subgoal to be equivalent, so you can’t possibly have replaced a
provable goal by an unprovable one. For example:
# CONV_TAC;;
val it : conv -> tactic = <fun>
# CONV_TAC(REWRITE_CONV[LE_ANTISYM]);;
val it : tactic = <fun>
# e it;;
val it : goalstack = 1 subgoal (1 total)
# e(SIMP_TAC[]);;
val it : goalstack = 1 subgoal (1 total)
# e(ONCE_REWRITE_TAC[EQ_SYM_EQ]);;
val it : goalstack = 1 subgoal (1 total)
All the rewriting tactics have variants starting ASM_ which also use all the assump-
tions of the goal to rewrite. For example instead of using SIMP_TAC now we could
do:
# e DISCH_TAC;;
val it : goalstack = 1 subgoal (1 total)
0 [‘n = 2‘]
# e(ASM_REWRITE_TAC[]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘n = 2‘]
When a goal is reduced to just ‘true’ (T), HOL considers it solved. For example,
here we can use ARITH_RULE. It might seem that we need EQT_INTRO o ARITH_RULE
46
so that we get ` p = > rather than ` p, since only the former is the behavior of a proper
conversion. But CONV_TAC is set up so that it will take a solution of the goal without
this nuance. (In fact ARITH_TAC is defined to be exactly this, but it’s worth noting
that you can always turn a conversion or proof procedure into a tactic with CONV_TAC.
# e(CONV_TAC ARITH_RULE);;
val it : goalstack = No subgoals
So we’re done; there are no more subgoals left to prove. And as we said, from a
proof of the remaining subgoals (in this case none), HOL is able to reconstruct a proof
of the original goal by applying primitive inferences. To get the final theorem once all
subgoals have been proved, use top_thm(); you can then bind a theorem to a name
in the usual way:
So much for the interactive construction of the proof. If we then want to store it in
a file to be replayed later, we could simply faithfully copy down the sequence of steps:
but of course the first step was a mistake and b() its correction, so we might ex-
cise them (we might indeed excise the conjunction-swapping too, but let’s pretend we
wanted to do that for some reason) and get:
Instead of just doing the steps one after another like this, we can use THEN to plug
tactics together. This function is called a tactical: tac1 THEN tac2 applies a tactic
tac1 to a goal and then tac2 to all the resulting subgoals. (In our case there was
exactly one.) Thus the following also works:
47
Moreover, if we’re not generating the proof interactively, we don’t need to bother
setting up the goalstack and applying tactics there. We can just use the function
prove(tm,tac) to prove a goal tm using a tactic tac. In fact, when I’m develop-
ing a proof in HOL I usually construct this kind of tactic script explicitly in an editor,
and copy parts into the goalstack as I proceed to make sure I’m on the right track.
We have said that ASM_ variants of the rewriting and simplification tactics rewrite
with all assumptions. Similarly MESON_TAC is a tactic version of MESON, and ASM_MESON_TAC
is a variant that uses all the assumptions. But what about making more delicate use of
assumptions? Let’s set up another goal to illustrate this and a few other tactics along
the way.
# g ‘!x y:real. &0 < x * y ==> (&0 < x <=> &0 < y)‘;;
The tactic GEN_TAC strips off a universal quantifier, and the tactical REPEAT will
apply it as many times as possible:
# e(REPEAT GEN_TAC);;
val it : goalstack = 1 subgoal (1 total)
The tactic STRIP_TAC has the effect of either GEN_TAC (removing an outer univer-
sal quantifier), CONJ_TAC (splitting a conjunction into two goals) or an elaborated
version of DISCH_TAC (turning the antecedent of an implication into assumptions
of the goal after breaking it down using STRIP_ASSUME_TAC), depending on the
outermost connective in the goal:
# e STRIP_TAC;;
val it : goalstack = 1 subgoal (1 total)
# e EQ_TAC;;
val it : goalstack = 2 subgoals (2 total)
Remember that the assumptions are really just theorems. If you have any existing
theorem you can add it to the assumptions with ASSUME_TAC.
48
# e(ASSUME_TAC(SPECL [‘x:real‘; ‘--y‘] REAL_LE_MUL));;
val it : goalstack = 1 subgoal (2 total)
Conversely, you can always use one of the assumptions by explicitly using ASSUME;
the tactic mechanism will automatically make sure that this works.
but, reasonably enough, you can’t assume anything that isn’t there:
49
# e(ASM_REWRITE_TAC[]);;
val it : goalstack = 1 subgoal (2 total)
It’s often convenient to skip putting something on the assumptions at all. Many
tactics have _THEN variants that, instead of putting their result as an assumption, do
something else with it. Indeed DISCH_TAC is just an abbreviation for
# e(DISCH_THEN ASSUME_TAC);;
val it : goalstack = 1 subgoal (2 total)
Other convenient methods for dealing with assumptions are FIRST_ASSUM, which
applies a theorem-tactic to the first possible assumption, and FIRST_X_ASSUM, which
does the same while also removing the assumption. This can be somewhat brittle if it
depends on the order of assumptions. For example, here we apply MP_TAC to the first
assumption whose conclusion is an implication, while removing it from the assump-
tions:
‘(&0 <= x /\ &0 <= --y ==> &0 <= x * --y) ==> &0 < x ==> &0 < y‘
Anyway, let’s stop messing around and get this goal finished
# e REAL_ARITH_TAC;;
val it : goalstack = 1 subgoal (1 total)
50
A more delicate approach is not to use ASSUME_TAC to put things on the assump-
tions, but rather LABEL_TAC, which allows you to give them a name:
# e(DISCH_THEN(LABEL_TAC "ypos"));;
val it : goalstack = 1 subgoal (1 total)
‘&0 < x‘
You can then get at it by name using REMOVE_THEN, which also removes it, and
USE_THEN, which doesn’t.
# e(REMOVE_THEN "ypos" MP_TAC);;
val it : goalstack = 1 subgoal (1 total)
Of course we’re making heavy weather of a rather easy goal. We can easily finish it
with:
# e(FIRST_X_ASSUM MP_TAC THEN MP_TAC(SPECL [‘--x‘; ‘y:real‘] REAL_LE_MUL) THEN
REAL_ARITH_TAC);;
val it : goalstack = No subgoals
and indeed we could have just done this for the whole proof, just providing enough
lemmas about the nonlinear part:
# let trivial = prove
(‘!x y:real. &0 < x * y ==> (&0 < x <=> &0 < y)‘,
REPEAT GEN_TAC THEN MP_TAC(SPECL [‘--x‘; ‘y:real‘] REAL_LE_MUL) THEN
MP_TAC(SPECL [‘x:real‘; ‘--y‘] REAL_LE_MUL) THEN REAL_ARITH_TAC);;
This embodies the common mathematical practice of saying that ‘without loss of
generality (WLOG) we may assume . . . ’. In this case, if a universal goal is symmetric
between two real variables x and y, we may assume WLOG that x 6 y. Applying that
key lemma using MATCH_MP_TAC, the backwards analog for MATCH_MP, we now
only need to provide one piece of nonlinear information:
# let trivial = prove
(‘!x y:real. &0 < x * y ==> (&0 < x <=> &0 < y)‘,
MATCH_MP_TAC REAL_WLOG_LE THEN CONJ_TAC THEN REPEAT GEN_TAC THEN
MP_TAC(SPECL [‘--x‘; ‘y:real‘] REAL_LE_MUL) THEN REAL_ARITH_TAC);;
Of course, for such a trivial goal, the saving is small or nonexistent. But if the goal
is more complex, it can be a great relief not to repeat almost identical reasoning with
minor changes, so it’s worth being on the lookout for such opportunities.
51
8.2 Inductive proofs about summations
The goal-directed style of proof seems particularly natural for induction, where we
break the original goal down into ‘base’ and ‘step’ cases. Consider for example proving
the formula for the sum of the first n natural numbers
n(n + 1)
1 + 2 + 3 + ··· + n =
2
or using summation notation
n
X n(n + 1)
i=
i=1
2
This and related formulas are straightforward to prove by induction on n, so let’s
try doing it in HOL. First we have to state the problem. HOL has a somewhat more
general summation operation predefined: it can sum a function into natural numbers
over a finite set18 whose elements have any type. There is also a function, written as an
infix ‘..’, to define a segment of the natural numbers: ‘m .. n’ means {m, . . . , n}.
Combining these, we can state our goal:
# e(MATCH_MP_TAC num_INDUCTION);;
val it : goalstack = 1 subgoal (1 total)
# NSUM_CLAUSES_NUMSEG;;
val it : thm =
|- (!m. nsum (m .. 0) f = (if m = 0 then f 0 else 0)) /\
(!m n.
nsum (m .. SUC n) f =
(if m <= SUC n
then nsum (m .. n) f + f (SUC n)
else nsum (m .. n) f))
52
# e(SIMP_TAC[NSUM_CLAUSES_NUMSEG]);;
val it : goalstack = 1 subgoal (1 total)
The resulting goal may look like a mess, but the remaining reasoning is so rou-
tine that we can leave it to ARITH_TAC. It disposes of the two conditions 1 = 0
(false) and 1 <= SUC n (true since n is a natural number), then does the algebraic
rearrangement to show the equations in the conclusion hold.
# e ARITH_TAC;;
val it : goalstack = No subgoals
If we wanted to package the entire proof up as a tactic script for later re-use we
could do this:
# let th = prove
(‘!n. nsum(1..n) (\i. i) = (n * (n + 1)) DIV 2‘,
MATCH_MP_TAC num_INDUCTION THEN
SIMP_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC);;
val th : thm = |- !n. nsum (1 .. n) (\i. i) = (n * (n + 1)) DIV 2
# e INDUCT_TAC;;
val it : goalstack = 2 subgoals (2 total)
We can then work on the two goals separately in much the same way. The base
case proof is similar; after proving it we get presented with the step case:
# e(REWRITE_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC);;
val it : goalstack = 1 subgoal (1 total)
53
Once again, almost the same proof works, except that because the inductive hy-
pothesis is now in our conclusion list we need to use the ‘ASM ’ variants of the rewrite
tactics, either ASM_SIMP_TAC or ASM_REWRITE_TAC. Since we now don’t need to
get the rewrite from the context, it’s slightly more efficient to use the latter:
# e(ASM_REWRITE_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC);;
val it : goalstack = No subgoals
Now to package up the overall proof, we could do the following. Note a subtle point
here. When we split a goal into two or more subgoals, the goalstack presents them to us
one at a time. However, in the tactic script, using THEN will apply the same tactics to
all the goals. In such a situation, when tac creates multiple subgoals g1,...gn, one
can use tac THENL [tac1;...;tacn] to apply different tactics to the different
subgoals. This means that the final tactic script has a bit more structure than the linear
sequence of steps, another reason for preferring it.
let th = prove
(‘!n. nsum(1..n) (\i. i) = (n * (n + 1)) DIV 2‘,
INDUCT_TAC THENL
[REWRITE_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC;
ASM_REWRITE_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC]);;
Moreover, exactly the same proof works for some more complicated examples like
summing squares and cubes, because the overall structure is the same, and the parts
that are different are automated anyway:
# let SUM_OF_SQUARES = prove
(‘!n. nsum(1..n) (\i. i * i) = (n * (n + 1) * (2 * n + 1)) DIV 6‘,
INDUCT_TAC THEN ASM_REWRITE_TAC[NSUM_CLAUSES_NUMSEG] THEN ARITH_TAC);;
val ( SUM_OF_SQUARES ) : thm =
|- !n. nsum (1 .. n) (\i. i * i) = (n * (n + 1) * (2 * n + 1)) DIV 6
54
• The natural numbers N = {0, 1, 2, . . .}, HOL type num
• The integers Z = {. . . , −2, −1, 0, 1, 2, . . .}, HOL type int
• The real numbers R, HOL type real
If one is used to thinking of these sets as living inside each other N ⊆ Z ⊆ R, the
fact that in HOL these are quite distinct types takes some getting used to.
We have already used the natural numbers num in many examples. In some sense
this is HOL’s most fundamental number system, and when you write a numeral the
quotation parser considers it as an element of type num. Many interesting problems
require just the natural numbers. Moreover they are useful for indexing sequences of
objects and so on, and there is a standard principle of mathematical induction for them
— we used it in the last section. However, it can sometimes be quite inconvenient that
there are no negative numbers in N. In fact, HOL’s subtraction on type num is defined
as ‘cutoff’ subtraction so that m − n = 0 whenever m 6 n. This can lead to some
surprises until you get used to it. For example:
Why didn’t that work? It’s not true because of the cutoff:
but it is true if we add an inequality constraint to rule out the degenerate cases:
If we work over integers or reals, we no longer need to worry about such peculiari-
ties of subtraction. The main thing to get used to here is that ‘numerals’ of these types
must be preceded by an ampersand: &0, &1 etc. (Actually, the ampersand is simply
the injection from natural numbers and can be applied to composite expressions too.)
But while unadorned numerals always have type num, all the arithmetic operations are
overloaded, so all the following are equally acceptable:19
If there are no explicit type annotations or constants with fixed type (such as un-
adorned numerals) to resolve which number system is intended, HOL will apply a
default. You can set which number system has priority in such cases (choose the one
you’re mainly working in) by using prioritize_xxx where xxx is the number
system to give priority. It’s often worth doing this at the start of a piece of work to
avoid surprises later.
19 Nevertheless, the underlying constants are different; the overloading is just in the surface syntax.
55
# prioritize_num();;
val it : unit = ()
# type_of ‘x + y‘;;
val it : hol_type = ‘:num‘
# prioritize_int();;
val it : unit = ()
# type_of ‘x + y‘;;
val it : hol_type = ‘:int‘
# prioritize_real();;
val it : unit = ()
# type_of ‘x + y‘;;
val it : hol_type = ‘:real‘
# ADD_ASSOC;;
val it : thm = |- !m n p. m + n + p = (m + n) + p
# INT_ADD_ASSOC;;
val it : thm = |- !x y z. x + y + z = (x + y) + z
# REAL_ADD_ASSOC;;
val it : thm = |- !x y z. x + y + z = (x + y) + z
However, this consistency is far from perfect. In any case, each number system has
its own suite of properties that may not hold in others. For example the following is
not true over N or Z where only truncating division is possible:
# REAL_LE_RDIV_EQ;;
val it : thm = |- !x y z. &0 < z ==> (x <= y / z <=> x * z <= y)
# INT_LT_DISCRETE;;
val it : thm = |- !x y. x < y <=> x + &1 <= y
and the following theorem over N relies essentially on the fact that the structure is
discrete and has no negative numbers:
# MULT_EQ_1;;
val it : thm = |- !m n. m * n = 1 <=> m = 1 /\ n = 1
It takes a lot of time to get used to the suite of theorems available. Sometimes
it’s much easier to use automated decision procedures to recreate a theorem rather than
remember what it’s called in the standard library. We have already used ARITH_RULE
over N, and similar things are available over the other number systems. INT_ARITH
is for the integers (max and min choose the maximum and minimum of their two
arguments):
56
while REAL_ARITH works correspondingly over the reals (abs(x) is the absolute
value function |x|):
These three procedures are all capable of handling simple algebraic rearrangement,
even if it is highly nonlinear — though keep in mind that any formula involving subtrac-
tion need special attention over N. For example here we derive Brahmagupta’s formula,
which shows that the set of integers with a representation in {a2 − Db2 | a, b ∈ Z} is
closed under multiplication. (Over Z and R the exponentiation operator is pow rather
than EXP.)
# INT_ARITH
‘!a b a’ b’ D:int.
(a pow 2 - D * b pow 2) * (a’ pow 2 - D * b’ pow 2) =
(a * a’ + D * b * b’) pow 2 - D * (a * b’ + a’ * b) pow 2‘;;
They can also handle linear inequality reasoning, such as the following:
Sometimes you can actually prove some unobvious things even with simple linear
reasoning. For example, a nice puzzle (Colmerauer 1990) is to consider a sequence
of integers or real numbers defined by the recurrence xn+2 = |xn+1 | − xn . (This
doesn’t look so different from the usual Fibonacci numbers Fn+2 = Fn+1 + Fn .) The
claim is that this sequence is actually periodic with period 9. We can check the key fact
automatically:
REAL_ARITH
‘!x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11:real.
x3 = abs(x2) - x1 /\
x4 = abs(x3) - x2 /\
x5 = abs(x4) - x3 /\
x6 = abs(x5) - x4 /\
x7 = abs(x6) - x5 /\
x8 = abs(x7) - x6 /\
x9 = abs(x8) - x7 /\
x10 = abs(x9) - x8 /\
x11 = abs(x10) - x9
==> x1 = x10 /\ x2 = x11‘;;
However, HOL just naively does a large case-split to deal with the absolute value
function, and so this is very slow. You might like to see if you can prove it faster than
HOL.
The reals decision procedure can deal with arbitrary rational constants. (These con-
stants are explicitly manipulated by proof internally, so there are no worries about ex-
ceeding the range of machine integers.) For pure arithmetic on rational constants, there
is a conversion REAL_RAT_REDUCE_CONV exactly analogous to NUM_REDUCE_CONV
over N.
57
# REAL_ARITH ‘!x y:real. x < y ==> x < (x + y) / &2 /\ (x + y) / &2 < y‘;;
val it : thm = |- !x y. x < y ==> x < (x + y) / &2 /\ (x + y) / &2 < y
# REAL_ARITH
‘((x1 pow 2 + x2 pow 2 + x3 pow 2 + x4 pow 2) pow 2) =
((&1 / &6) * ((x1 + x2) pow 4 + (x1 + x3) pow 4 + (x1 + x4) pow 4 +
(x2 + x3) pow 4 + (x2 + x4) pow 4 + (x3 + x4) pow 4) +
(&1 / &6) * ((x1 - x2) pow 4 + (x1 - x3) pow 4 + (x1 - x4) pow 4 +
(x2 - x3) pow 4 + (x2 - x4) pow 4 + (x3 - x4) pow 4))‘;;
...
In the case of N and Z, the discreteness is not always exploited as much as it could
be in the linear decision procedures. So sometimes, results that require discreteness
or divisibility properties are not proved automatically. For example, the first of these
works, but not the second, even though they are both valid:
# ARITH_RULE ‘x < 2 EXP 30 ==> (429496730 * x) DIV (2 EXP 32) = x DIV 10‘;;
val it : thm = |- x < 2 EXP 30 ==> (429496730 * x) DIV 2 EXP 32 = x DIV 10
But if we only specify x 6 230 , the analogous call fails, even though it is still valid,
because of the incomplete exploitation of discreteness.
# ARITH_RULE ‘x <= 2 EXP 30 ==> (429496730 * x) DIV (2 EXP 32) = x DIV 10‘;;
Exception: Failure "linear_ineqs: no contradiction".
Such limitations are inevitable to some extent. Nonlinear equational and inequality
reasoning over discrete structures like N and Z is undecidable, and even over R the
theoretical complexity is discouraging. So if you want nonlinear results like those, you
may just have to grit your teeth and either search for them in the standard library, or
prove them yourself from related results found there. However, there are some classes
of nonlinear reasoning that can be automated by other inbuilt tools. In particular, if
the reasoning involves equational (not inequality) reasoning and doesn’t depend on
58
the particular structure,20 there is a suite of ‘ring’ tools that can often cope. Over the
naturals this tool is NUM_RING and over the reals REAL_RING. (There is currently
no version for the integers, but it’s relatively easy to instantiate the generic procedure
RING to this and other ring structures.) For example, here we derive a formula related
to areas of triangles:
# prioritize_real();;
val it : unit = ()
# REAL_RING
‘s = (a + b + c) / &2
==> s * (s - b) * (s - c) + s * (s - c) * (s - a) +
s * (s - a) * (s - b) - (s - a) * (s - b) * (s - c) =
a * b * c‘;;
That wasn’t a big advance over REAL_ARITH, because after substituting with the
antecedent, the consequent just requires algebraic rearrangement. But we can also
handle cases that require deeper reasoning than simple
√ substitution. For example, one
can understand the following by observing that x2 + 2x + 1 divides into x4 + 1:
# REAL_RING ‘a pow 2 = &2 /\ x pow 2 + a * x + &1 = &0 ==> x pow 4 + &1 = &0‘;;
and the following gives the customary formulas for the sum and product of the two
roots of a quadratic equation.
# REAL_RING
‘(a * x pow 2 + b * x + c = &0) /\
(a * y pow 2 + b * y + c = &0) /\
˜(x = y)
==> (a * x * y = c) /\ (a * (x + y) + b = &0)‘;;
This one verifies “Vieta’s substitution”, which can be used to reduce solution of a
general cubic equation in z to cube root extractions and solutions of quadratics. (This
formula holds over any field, including the complex numbers, so can be used to arrive
at an explicit formula for all the roots if we’re prepared to use the imaginary unit.)
REAL_RING
‘p = (&3 * a1 - a2 pow 2) / &3 /\
q = (&9 * a1 * a2 - &27 * a0 - &2 * a2 pow 3) / &27 /\
x = z + a2 / &3 /\
x * w = w pow 2 - p / &3
==> (z pow 3 + a2 * z pow 2 + a1 * z + a0 = &0 <=>
if p = &0 then x pow 3 = q
else (w pow 3) pow 2 - q * (w pow 3) - p pow 3 / &27 = &0)‘;;
In this formula we use the construct ‘if P then x else y’, which is de-
fined to be equal to x if P holds and y otherwise. Underneath the mixfix surface
syntax, this is COND P x y where COND is a constant with the appropriate behav-
ior. While REAL_RING has little ability to handle division, there is a more powerful
REAL_FIELD that is capable of handling equations involving inverses, provided it can
establish that the expressions being inverted are nonzero. In ensuring this nonzeroness,
it can use linear inequality reasoning, e.g:
20 More formally, if the equational reasoning works in all integral domains.
59
# REAL_FIELD ‘&0 < x ==> &1 / x - &1 / (&1 + x) = &1 / (x * (&1 + x))‘;;
...
REAL_FIELD ‘&0 < x ==> &1 / x - &1 / (&1 + x) = &1 / (x * (&1 + x))‘;;
A less trivial example is the following formula for roots of a quadratic, which is
2 2
√ dealt with quite easily. But note that the hypothesis is s = b − 4ac not s =
also
b2 − 4ac: over the reals, there may be no square root and this result holds vacuously
when b2 − 4ac is negative.
# REAL_FIELD
‘s pow 2 = b pow 2 - &4 * a * c
==> (a * x pow 2 + b * x + c = &0 <=>
if a = &0 then
if b = &0 then
if c = &0 then T else F
else x = --c / b
else x = (--b + s) / (&2 * a) \/ x = (--b + --s) / (&2 * a))‘;;
The ring and field procedures still don’t help with reasoning about nonlinear in-
equalities. All I can offer here are some experimental tools, not part of the core build
but found in Examples/sos.ml, based on ideas from Parrilo (2003). In order to use
these you need to have a semidefinite programming package such as CSDP installed,
since this is used to search for the proof that HOL uses.21 This provides SOS_RULE,
INT_SOS and REAL_SOS for the respective number systems. The discrete procedures
are incomplete but still quite useful for elementary nonlinear reasoning:
Less trivial examples can often be handled automatically too. For example, this
special case of Hardy’s inequality is dispatched quite easily:
# REAL_SOS
‘!a1 a2 a3 a4:real.
&0 <= a1 /\ &0 <= a2 /\ &0 <= a3 /\ &0 <= a4
==> a1 pow 2 +
((a1 + a2) / &2) pow 2 +
((a1 + a2 + a3) / &3) pow 2 +
((a1 + a2 + a3 + a4) / &4) pow 2
<= &4 * (a1 pow 2 + a2 pow 2 + a3 pow 2 + a4 pow 2)‘;;
# REAL_SOS
‘!a b c:real.
a >= &0 /\ b >= &0 /\ c >= &0
==> &3 / &2 * (b + c) * (a + c) * (a + b) <=
a * (a + c) * (a + b) +
b * (b + c) * (a + b) +
c * (b + c) * (a + c)‘;;
60
There are some more direct ‘sum of squares’ functions provided that are sometimes
useful. For example, SOS_CONV will attempt to express a polynomial as a sum of
squares:
# SOS_CONV ‘&2 * x pow 4 + &2 * x pow 3 * y - x pow 2 * y pow 2 + &5 * y pow 4‘;;
val it : thm =
|- &2 * x pow 4 + &2 * x pow 3 * y - x pow 2 * y pow 2 + &5 * y pow 4 =
&1 / &2 * (&2 * x pow 2 + x * y + -- &1 * y pow 2) pow 2 +
&1 / &2 * (x * y + y pow 2) pow 2 +
&4 * y pow 2 pow 2
and PURE_SOS is a more restricted version of REAL_SOS that attempts to prove non-
negativity of a polynomial just by expressing it as a sum of squares. (The more general
REAL_SOS searches for real Nullstellensatz certificates involving sums of squares.)
For example, we can handle all but one of the examples in Guangxing and Xiaoning
(2004) without much difficulty, including the following:
# PURE_SOS
‘x pow 4 + &2 * x pow 2 * z + x pow 2 - &2 * x * y * z +
&2 * y pow 2 * z pow 2 + &2 * y * z pow 2 + &2 * z pow 2 - &2 * x +
&2 * y * z + &1 >= &0‘;;
val it : thm =
|- x pow 4 +
&2 * x pow 2 * z +
x pow 2 - &2 * x * y * z +
&2 * y pow 2 * z pow 2 +
&2 * y * z pow 2 +
&2 * z pow 2 - &2 * x +
&2 * y * z +
&1 >=
&0
Actually a more general ‘quantifier elimination’ function is provided over the in-
tegers. The conversion COOPER_CONV will actually produce a quantifier-free equiva-
lent for a formula with non-quantified variables. A similar function is available for the
complex numbers in the Complex subdirectory, and a similar procedure for the reals,
written by Sean McLaughlin (McLaughlin and Harrison 2005) is available in the Rqe
61
subdirectory. For example the latter can solve the following formula with a non-trivial
quantifier alternation:
and, given enough time, many much more interesting ones (we will see an example
later).
10 Inductive definitions
HOL’s only primitive way of defining new constants is to make them equal by defini-
tion to some pre-existing term. (As we noted, function definitions with parameters are
actually implemented as more basic definitions of lambda terms.) But it’s often conve-
nient to be able to define things less explicitly. In particular it’s common, particularly
in formal logic and computer science, to define a predicate P inductively by a series of
rules of the form ‘if . . . then P (t)’ where the hypothesis of the rule may make assertions
involving P . These rules are customarily written with a horizontal line separating the
hypotheses (if any) from the conclusion. For example, the even-ness predicate (equiv-
alently, the set of even numbers) E might be defined as a subset of the natural numbers
by these two rules, read respectively ‘0 is an even number’ and ‘if n is an even number,
n + 2 is an even number’.
0∈E
n∈E
(n + 2) ∈ E
These closure properties do not in themselves determine E uniquely: the set of all
natural numbers also satisfies them. But the special meaning of an inductive definition
is that E is the smallest set closed under the rules — something is only in E if the rules
compel it to be so.
This begs the question: how do we know in general that there is indeed a unique
smallest set that satisfies the rules? In this case it’s not too hard to see that the even
numbers are indeed such a set, but the form of the rules might be such that it is less
clear. For example, there are infinitely many minimal sets closed under the rule:
n 6∈ E
n+1∈E
including the odd numbers, the even numbers and the set of numbers not divisible by
3, while there are no minimal sets at all closed under:
62
\
E= {s|0 ∈ s ∧ ∀n. n ∈ s ⇒ (n + 2) ∈ s}
If there is any set s closed under the rules, then E ⊆ s, simply because s is then one
of the sets appearing in the intersection. So if E is itself closed under the rules, it is the
least such set and therefore the inductively defined set we want. The only remaining
burden of proof is to show that E is closed under the rules.
In general this cannot be done (as the examples above show), but a simple and
useful syntactic restriction is enough to ensure that it can be: the hypotheses of rules
only make ‘positive’ assertions about membership in the set being defined. That is,
rules may assert that some terms (perhaps infinitely many) are contained in the set
being defined, but must never assert that anything is not. Our ‘even numbers’ example
satisfies this: it is OK to say 0 ∈ E and n ∈ E, but the impossibles example violates it
because the rules involve a negative assertion n 6∈ E.22
Subject to this constraint, HOL is able to prove the necessary properties to convert
the inductive definition into an explicit one without user intervention; the user never
needs to look explicitly at the underlying primitive definition. Let us see how this
works. The set of rules is put together into a conjunction, and each rule is universally
quantified over all variables other than the predicate being defined. One then applies
new_inductive_definition to this term. (Note that the inductively defined
object is always thought of as a predicate rather than a set: we must write E(t) not
t ∈ E before passing it to new_inductive_definition. However it is easy to
massage the theorem afterwards if one wants to think of it as a set.)
For each definition, three theorems are returned. The first is the ‘rule’ theorem
asserting that indeed the predicate E is closed under the given rules. The second is an
‘induction’ theorem, which effectively states that this is the least such set: it says that
if any other predicate E 0 satisfies the rules, then E ⊆ E 0 (unravelled into ∀a. E(a) ⇒
E 0 (a). The third theorem is a ‘cases’ theorem which allows one to conduct a case
analysis on an object a such that E(a): in such a case, either a = 0 or there is some n
satisfying E(n) such that a = n + 2. Let us capture these theorems by binding them
to names:
To illustrate how the induction theorem may be used, let us prove the following
goal:
22 One way of justifying closure under the rules is via the Knaster-Tarski fixed-point theorem (Knaster
1927; Tarski 1955) applied to the lattice of sets under inclusion. For an explicit HOL proof of this theorem,
see TARSKI SET in the file Library/card.ml. The key hypothesis is monotonicity, and the positivity
restriction on rules is sufficient to guarantee it.
63
we start by applying the induction theorem in much the same way as any other:
# e(MATCH_MP_TAC E_INDUCT THEN REPEAT STRIP_TAC);;
val it : goalstack = 2 subgoals (2 total)
0 [‘n = 2 * m‘]
‘?m. n + 2 = 2 * m‘
‘?m. 0 = 2 * m‘
The first goal is pretty easy: we just supply the witness 0 and do arithmetic:
# e(EXISTS_TAC ‘0‘ THEN ARITH_TAC);;
For the other case, let us rewrite the goal using the inductive hypothesis:
# e(ASM_REWRITE_TAC[]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘n = 2 * m‘]
‘?m’. 2 * m + 2 = 2 * m’‘
Again it’s not too hard to come up with the witness: just set m0 = m + 1 and do a
bit of arithmetic:
# e(EXISTS_TAC ‘m + 1‘ THEN ARITH_TAC);;
val it : goalstack = No subgoals
64
Although the points mentioned are all integers, the problem does not state any
restriction to integer or rational intermediate points, so we will work over the reals:
prioritize_real();;
We will represent the position of the three bugs (say A, B and C) by an element of
type (real#real)#(real#real)#(real#real), with the three pairs giving
the positions of the three bugs in terms of their x and y coordinates. Since only one
bug moves at a time, we can appropriately formalize what can happen in one ‘move’
as follows:
# let move = new_definition
‘move ((ax,ay),(bx,by),(cx,cy)) ((ax’,ay’),(bx’,by’),(cx’,cy’)) <=>
(?a. ax’ = ax + a * (cx - bx) /\ ay’ = ay + a * (cy - by) /\
bx’ = bx /\ by’ = by /\ cx’ = cx /\ cy’ = cy) \/
(?b. bx’ = bx + b * (ax - cx) /\ by’ = by + b * (ay - cy) /\
ax’ = ax /\ ay’ = ay /\ cx’ = cx /\ cy’ = cy) \/
(?c. ax’ = ax /\ ay’ = ay /\ bx’ = bx /\ by’ = by /\
cx’ = cx + c * (bx - ax) /\ cy’ = cy + c * (by - ay))‘;;
This defines a binary relation on positions, which is true precisely when the second
position is reachable from the first in one move. Note that the parallelism requirement
is encoded by the fact that the change in coordinates of the moving bug are the same
multiple of the differences in the coordinates of the other bug.
However, the problem considers what can happen after an unlimited number of
moves. How to we formalize that? One idea would be to explicitly say that there is a
finite sequence of positions s0 ,. . . ,sn with the appropriate initial and starting positions
such that all the adjacent pairs between s0 and sn in the sequence are in the relation
move. In HOL the idea of a sequence can be rendered by making s a function from
natural numbers into positions, and we might make a definition like this:
Although this works perfectly well, manipulating the kinds of expressions that arise
on the right-hand-side is a bit tedious, especially for the HOL novice. We can do things
much more simply by using an inductive definition:
# let reachable_RULES,reachable_INDUCT,reachable_CASES =
new_inductive_definition
‘(!p. reachable p p) /\
(!p q r. move p q /\ reachable q r ==> reachable p r)‘;;
So much for the formalization. How are we going to solve the puzzle? At this
point we inject some human intelligence and make the observation that the permissible
moves do not change the area of the triangle formed by the three bugs. (Think of
the two currently stationary bugs as the base of a triangle and the moving bug as the
top vertex: the base is fixed since those two bugs don’t move, and the height is fixed
because of the parallelism restriction.) We can define the area as follows (think of the
vector cross-product):
65
Actually, it serves our purposes better to leave out the absolute-value function, so
that we actually get the oriented area, positive or negative according to whether the
bugs are oriented in a clockwise or anticlockwise direction. (We could also leave out
the factor of two, since it doesn’t matter for our purposes, but then the name seems a
bit misleading.)
Now we want to formally prove what we claimed above, that the oriented area is
not changed by one bug’s permitted moves:
Since the positions p and p0 are sextuples and the defining equations for move and
oriented_area require these to be expanded into their components, we rewrite
with the following which expands out universal quantification over a pair:
# FORALL_PAIR_THM;;
val it : thm = |- (!p. P p) <=> (!p1 p2. P (p1,p2))
Rewriting with this and the definitions we get a goal that may look somewhat fright-
ening, especially thanks to the liberally primed names HOL has introduced for the
components:
# e(CONV_TAC REAL_RING);;
...
val it : goalstack = No subgoals
66
This proof can be packaged up into a script as:
Now we need to prove the same thing for any sequence of moves. We see now the
elegance and simplicity of defining reachable inductively: we just need to apply
the induction theorem and do basic first-order reasoning with the lemma just proved.23
Now we can actually prove that the answer to both parts of the puzzle is ‘no’,
because the claimed configurations violate the invariance of the oriented area. First of
all part (a):
As so often, we start with STRIP_TAC; note that this also deals with a negated
goal ¬p as if it were p ⇒ ⊥ and puts p in the assumptions.
# e STRIP_TAC;;
val it : goalstack = 1 subgoal (1 total)
‘F‘
Now we apply the invariance lemma and put the result back into the conclusion of the
goal:
‘oriented_area ((&0,&0),(&3,&0),&0,&3) =
oriented_area ((&0,&0),(&0,&3),&3,&0)
==> F‘
We can now prove the result just by expanding the definition of oriented_area
and doing arithmetic. Note that for this part the use of the oriented area was actually
crucial: the scalar area is unchanged, but the orientation of the bugs has been flipped.
Precisely the same sequence of steps solves the other part, and we may as well
package it up. Note that for the sake of completeness we show that none of the potential
6 arrangements of the bugs as the vertices of the triangle are possible.
23 A courageous reader might like to try to perform the same proof for the explicitly sequential definition
67
# let IMPOSSIBILITY_B = prove
(‘˜(reachable ((&0,&0),(&3,&0),(&0,&3)) ((&1,&2),(&2,&5),(-- &2,&3)) \/
reachable ((&0,&0),(&3,&0),(&0,&3)) ((&1,&2),(-- &2,&3),(&2,&5)) \/
reachable ((&0,&0),(&3,&0),(&0,&3)) ((&2,&5),(&1,&2),(-- &2,&3)) \/
reachable ((&0,&0),(&3,&0),(&0,&3)) ((&2,&5),(-- &2,&3),(&1,&2)) \/
reachable ((&0,&0),(&3,&0),(&0,&3)) ((-- &2,&3),(&1,&2),(&2,&5)) \/
reachable ((&0,&0),(&3,&0),(&0,&3)) ((-- &2,&3),(&2,&5),(&1,&2)))‘,
STRIP_TAC THEN FIRST_ASSUM(MP_TAC o MATCH_MP REACHABLE_INVARIANT) THEN
REWRITE_TAC[oriented_area] THEN REAL_ARITH_TAC);;
A slightly different alternative to the first line would be the following, which the
reader may like to decipher:
• A set Σ of states
• A binary transition relation τ : Σ × Σ → bool on that set.
For the bug puzzle, the set of states is R2 ×R2 ×R2 , the ‘state’ being in this case the
position of the three bugs. The transition relation is move, and tells us whether from
one state we can pass ‘immediately’ to another state. The system can then evolve by
starting in some initial state σ0 and passing through a sequence of states σ0 → σ2 →
σ2 → · · · according to the transition relation (τ σi σi+1 for each i). We verified that a
certain property I (having a specific oriented area) was an ‘invariant’, i.e. when I(σ)
and τ σ σ 0 then also I(σ 0 ), and deduced from this the property we wanted.
A similar methodology is often useful in practical applications. The behaviour of a
computer system can be modelled as a state transition system, where the state is roughly
the complete contents of all storage cells (at least those relevant to the problem being
considered) and the transition relation determined by the program. We will discuss
more thoroughly the interpretation of a program as a transition relation in section 16,
but an example should give the general idea. An assignment statement x := e in a
programming language defines a transition from σ to σ 0 precisely when state σ 0 has
these properties: (i) in state σ 0 , the variable x has the value resulting from evaluating
e in state σ, and (ii) all other variables have the same values in σ 0 and σ. Although
for an assignment statement the final state is completely determined by the initial state,
it’s convenient to keep the generality of a relation since this allows us to consider
nondeterministic behaviour.
68
We will give here a simple example in concurrent programming lifted from Lam-
port (1994), consisting of two parallel instances of essentially the same program. Each
program has three atomic instructions labelled 10, 20 and 30, and they use a shared
semaphore variable sem to ensure that at most one of them can be in the ‘critical sec-
tion’ (20 and 30) at any time. The critical sections just increment variables x and y
respectively and restore the semaphore, but one could imagine something more inter-
esting here such that the mutual exclusion property is important:
PROGRAM1 PROGRAM2
10: sem > 0 -> sem := sem - 1 10: sem > 0 -> sem := sem - 1
20: x := x + 1 20: y := y + 1
30: sem := sem + 1 30: sem := sem + 1
69
let mutex = new_definition
‘mutex (x,y,pc1,pc2,sem) <=> pc1 = 10 \/ pc2 = 10‘;;
As with the bug puzzle, we verify the property we actually want by showing that it’s
implied by an ‘inductive invariant’, which is true in the initial state(s) and preserved by
transitions. As before, coming up with a suitable invariant is the main creative step:24
We could once again define a notion of ‘reachable’. But in one of the libraries, HOL
contains a general definition of ‘reflexive transitive closure’ RSTC R of an arbitrary
relation R, so we can just re-use this for our particular relation.
needs "Library/rstc.ml";;
Let us prove a similarly general theorem to justify our use of an invariant invariant
to establish a desired property P of all reachable states in a transition system with ini-
tial states init and transition relation transition. The proof can be derived by
straightforward logical reasoning from a carefully chosen special case of the pre-proved
theorem RTC_INDUCT:
# RTC_INDUCT;;
val it : thm =
|- !R P.
(!x y. R x y ==> P x y) /\
(!x. P x x) /\
(!x y z. P x y /\ P y z ==> P x z)
==> (!x y. RTC R x y ==> P x y)
namely:
70
# e(MATCH_MP_TAC INDUCTIVE_INVARIANT THEN EXISTS_TAC ‘indinv‘);;
val it : goalstack = 1 subgoal (1 total)
We now expand out all the quantifiers over tuples into quantifiers over individual
components, expand with all the definitions and use the fact that (x, y) = (x0 , y 0 ) ⇔
x = x0 ∧ y = y 0 . The result is somewhat large and complicated:
# e ARITH_TAC;;
val it : goalstack = No subgoals
11 Wellfounded induction
By now we’ve seen several proofs by induction formalized in HOL. The most familiar
induction principle is probably traditional ‘mathematical induction’, as formalized in
num_INDUCTION. But sometimes the ‘step-by-step’ nature of this induction doesn’t
contribute to the argument as we want. A more ”powerful” induction principle is well-
founded induction (also variously known as ‘complete induction’, ‘noetherian induc-
tion’ and ‘course-of-values induction’). It allows us to establish the required result
P (n) by assuming that P (m) holds for all m < n, not just for m = n − 1. At first
sight it might seem that we need a base case, but note that for n = 0 there are no m < n
so in this case we simply need to prove P (0) with no assumptions as before.
# num_WF;;
val it : thm = |- !P. (!n. (!m. m < n ==> P m) ==> P n) ==> (!n. P n)
We put “powerful” in quotes because it’s not actually hard to derive wellfounded
induction from mathematical induction. Here’s how we could do it if it weren’t already
pre-proved:
71
# g(concl num_WF);;
val it : goalstack = 1 subgoal (1 total)
We start with a couple of routine steps to break down the goal and put the antecedent
into the hypotheses:
‘!n. P n‘
The trick now is to prove a more general fact that implies what we want. The tactic
SUBGOAL_THEN allows us to split this off as a lemma and assume it in our main goal.
(Instead of ASSUME_TAC, adding the lemma to the assumptions, we could choose to
do something else with it, e.g. rewrite with it.)
‘!n. P n‘
# LT;;
val it : thm = |- (!m. m < 0 <=> F) /\ (!m n. m < SUC n <=> m = n \/ m < n)
# e(INDUCT_TAC THEN ASM_MESON_TAC[LT]);;
The same use of MESON will also deduce our original goal from the lemma (since
it shows that n < SUC(n)).
# e(ASM_MESON_TAC[LT]);;
...
val it : goalstack = No subgoals
72
√
11.1 Irrationality of 2
√
√ is the irrationality of 2, i.e. the fact
One famous ‘impossibility’ proof in mathematics
that there are no integers p and q such that 2 = p/q. The key part of this can be
expressed as a lemma about natural numbers:
# e(MATCH_MP_TAC num_WF);;
val it : goalstack = 1 subgoal (1 total)
We slightly rewrite the goal to move the universal quantifiers over q outwards, then
break it down:
# e(REWRITE_TAC[RIGHT_IMP_FORALL_THM] THEN REPEAT STRIP_TAC);;
val it : goalstack = 1 subgoal (1 total)
‘q = 0‘
Now comes the key idea of the proof. From the fact that p2 = 2q 2 we deduce that
p is even. We apply the EVEN predicate to the (only) equational assumption and put
it in the goal. Note that AP_TERM is a degenerate case of MK_COMB, which takes us
from ` x = y to ` f (x) = f (y):
We now use the pre-proved theorem showing how the even-ness of a product can be
expressed in terms of the even-ness of the two terms, as well as ARITH, which is a set
of rewrite rules that can evaluate arithmetical constant expressions. (It’s less efficient
than the custom conversion NUM_REDUCE_CONV but since it’s just a theorem it’s easy
to throw into a set of rewrites.)
# e(REWRITE_TAC[EVEN_MULT; ARITH]);;
val it : goalstack = 1 subgoal (1 total)
‘EVEN p ==> q = 0‘
We’ve got the fact that p is even. If we rewrite with the theorem that ` EVEN(p) ⇔
∃m. p = 2m we get this:
73
# e(REWRITE_TAC[EVEN_EXISTS]);;
val it : goalstack = 1 subgoal (1 total)
‘(?m. p = 2 * m) ==> q = 0‘
So we can fix such an m and replace p with 2m everywhere in the goal. Note that
HOL automatically renames the bound variable in assumption 0 to avoid clashing with
m:
# e(DISCH_THEN(X_CHOOSE_THEN ‘m:num‘ SUBST_ALL_TAC));;
val it : goalstack = 1 subgoal (1 total)
‘q = 0‘
Now we’re ready to use the inductive hypothesis, and we set the variables in it as
follows. Note that the reduction in the induction variable is from 2m to q, far from a
simple transition from n + 1 to n, so the generality of wellfounded induction was really
needed.
# e(FIRST_X_ASSUM(MP_TAC o SPECL [‘q:num‘; ‘m:num‘]));;
val it : goalstack = 1 subgoal (1 total)
0 [‘(2 * m) * 2 * m = 2 * q * q‘]
The remaining steps are straightforward but need manual intervention, since there’s
a bit of nonlinear reasoning required.25 First note that q 2 = 2m2 in the goal is equiva-
lent to (2m)2 = 2q 2 which we have as an assumption. Moreover q < 2m ⇒ m = 0 is
just equivalent to ¬(q < 2m) since q < 0 is impossible. Using ARITH_RULE we can
fold both these transformations together:
# e(ASM_REWRITE_TAC[ARITH_RULE
‘q < 2 * m ==> q * q = 2 * m * m ==> m = 0 <=>
(2 * m) * 2 * m = 2 * q * q ==> 2 * m <= q‘]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘(2 * m) * 2 * m = 2 * q * q‘]
‘2 * m <= q ==> q = 0‘
Things are pretty easy now: if 2m 6 q then (2m)2 6 q 2 ; but from the assumption
(2m)2 = 2q 2 this gives 2q 2 6 q 2 , so q 2 = 0, so q = 0 as required. If we furnish
it with the various arithmetical assumptions underlying the last sentence, MESON can
finish the job:
25 If you have the SOS tools installed, you can just solve this automatically.
74
This entire proof can be packaged as follows:
11.2 Wellfoundedness
The usual ordering on natural numbers is not the only one with the property expressed
by num_WF. (Note that besides being prima facie stronger than ordinary mathematical
induction, wellfounded induction makes sense more generally since it only depends
on the ordering, not addition etc.) Any binary relation with this property is said to
be wellfounded. The HOL definition of this concept is actually slightly different: a
relation is wellfounded if each nonempty set/predicate has a -minimal element:
# WF;;
val it : thm =
|- !(<<). WF (<<) <=>
(!P. (?x. P x) ==> (?x. P x /\ (!y. y << x ==> ˜P y)))
# WF_IND;;
val it : thm =
|- WF (<<) <=> (!P. (!x. (!y. y << x ==> P y) ==> P x) ==> (!x. P x))
as well as the admissibility of definitions by recursion. We will not consider the under-
lying theory of wellfoundedness here, but in the next section we will see how HOL can
use it to automate some classes of recursive definitions.
75
to another (strip off a universal quantifier, rewrite with this theorem, chain backwards
using that lemma). However, a typical hand proof is more declarative, generally spec-
ifying intermediate steps explicitly but not the method by which one √ passes between
them. For example, take the proof in Hardy and Wright (1979) that 2 is irrational:
√
The traditional proof ascribed to Pythagoras runs as follows. If 2 is
rational, then the equation
a2 = 2b2 (4.3.1)
is soluble in integers a, b with (a, b) = 1. Hence a2 is even, and therefore
a is even. If a = 2c, then 4c2 = 2b2 , 2c2 = b2 , and b is also even, contrary
to the hypothesis that (a, b) = 1.
Contrast this with the proof we conducted in HOL, which is structured slightly
differently but formalizes essentially the same pattern of reasoning:
The hand proof explicitly indicates the sequence of deductions (a2 is even . . . a
is even . . . b is even . . . ), but doesn’t expand on how one gets from one to the next,
which is considered “obvious”. In complete contrast, the HOL proof does not give the
intermediate steps explicitly but does specify the procedure for getting from one to the
other. One practical consequence is that even knowing in great detail (as I do and the
reader might by now) how all the key things like REWRITE_TAC behave, it is for most
of us almost impossible to trace through the proof mentally without running through
it step-by-step in a HOL session. It’s very much like trying to visualize a chess game
from the sequence of moves given in a newspaper without using a board, but even more
difficult because the basic steps in HOL are more complicated.
76
# let fix ts = MAP_EVERY X_GEN_TAC ts;;
We want to keep the basic DISCH_TAC function, but oblige the user to explicitly
give the term being transferred to the assumptions and give it a label. And of course,
we want the tactic to check consistency of the given term with the goal:
# let assume lab t =
DISCH_THEN(fun th -> if concl th = t then LABEL_TAC lab th
else failwith "assume");;
‘!x y. x + y = 1 ==> x * y = 0‘
We can eliminate the universal quantifier by fixing variables. One might in fact
want to restrict the user to the variables in the goal, since the changing names can also
be difficult to visualize. Still, at the moment we make no such restriction:
# e(fix [‘m:num‘; ‘n:num‘]);;
val it : goalstack = 1 subgoal (1 total)
‘m + n = 1 ==> m * n = 0‘
‘m * n = 0‘
Right at the end, we expect to solve a goal by applying the last tactic. The following
is just a little piece of syntactic sugar around this:
# let we’re finished tac = tac;;
val we’re : ’a -> ’b -> ’b = <fun>
Note that the first argument is ignored, so you can equally well write we’re complete,
and if done weren’t a reserved word in OCaml you could write we’re done. But
assuming we won’t be able to finish off the goal in one step, we need to be able to
reduce the goal to another one that implies it:
# let suffices_to_prove q tac = SUBGOAL_THEN q (fun th -> MP_TAC th THEN tac);;
This simply sets up the given subgoal q and uses the tactic tac to prove that it im-
plies the original goal. We will see that this pattern of constructs based on a justifying
tactic is repeated a few times. The dual process, of deducing a new assumption from
what we have already, is implemented as two constructs note and have. The only
difference is that the former requires an explicit label for the new assumption. Once
again, it requires a tactic that justifies why the new assumption follows from what we
already have:
77
# let note(lab,t) tac =
SUBGOAL_THEN t MP_TAC THENL [tac; ALL_TAC] THEN
DISCH_THEN(fun th -> LABEL_TAC lab th);;
val note : string * term -> tactic -> tactic = <fun>
# let have t = note("",t);;
val have : term -> tactic -> tactic = <fun>
Instead of just adding a new fact to the assumptions, we may want to first (i)
perform a case analysis over it if it’s a disjunction, or (ii) eliminate the existentially
quantified variable. Again these are quite simple to code. Think of the construct
consider(x,lab,t) as: consider an x such that (call this ‘lab’) t:
These constructs certainly seem more readable than the conventional tactic scripts.
But then we still need ordinary HOL tactics to be plugged in as the justification. At
least we can introduce some more readable notation for some of the common ‘big gun’
tactics:
These tactics are atomic, and don’t have any parameters. But since in general we
will want to appeal to particular assumptions by label name, we include the following
construct. Note that it is applied to the core tactic, rather than vice versa.
# let by labs tac = MAP_EVERY (fun l -> USE_THEN l MP_TAC) labs THEN tac;;
Sometimes, we’ll also want to use an existing HOL theorem. Since theorems and
label names are different types, we need a different construct, though it’s conceptually
doing the same thing:
It’s particularly common to have chains of reasoning where we always use the
previous item in the chain. It gets a bit tedious always needing to label each stage and
explicitly invoke it in the next stage, so we set up the following to do it implicitly:
# let so constr arg tac = constr arg (FIRST_ASSUM MP_TAC THEN tac);;
12.2 Example
Let us return to the proof we were discussing:
78
Our original proof started out with MATCH_MP_TAC num_WF. While admirably
consise, this simply replaced the existing goal with one that is not quite trivial to visu-
alize. In our goal to be more declarative, we use the following instead:
# e(suffices_to_prove
‘!p. (!m. m < p ==> (!q. m * m = 2 * q * q ==> q = 0))
==> (!q. p * p = 2 * q * q ==> q = 0)‘
(MATCH_ACCEPT_TAC num_WF));;
val it : goalstack = 1 subgoal (1 total)
The effect is exactly the same. But the difference is that we didn’t need to run the
tactic to see the new goal. Now we proceed with breaking down the goal:
‘q = 0‘
Now we construct a little chain of inferences leading from our current assumptions
and culminating in ∃m. p = 2m. Note that we use the so construct to link to the
previous fact deduced, currently B:
‘q = 0‘
For the next step we need to invoke some external arithmetical lemmas about even-
ness:
‘q = 0‘
Now we finally get ∃m. p = 2m and pick a variable m, labelling the new assump-
tion p = 2m:
79
# e(so consider (‘m:num‘,"C",‘p = 2 * m‘) (using [EVEN_EXISTS] trivial));;
...
val it : goalstack = 1 subgoal (1 total)
‘q = 0‘
We’ll handle the rest of the proof in a somewhat more explicit way that previously,
by performing a case analysis over q < p ∨ p 6 q.
...
‘q = 0‘
Now we use the assumption ‘A’ by specializing the first quantifier to q and using
the new assumption ‘D’. We don’t have to be too explicit:
‘q = 0‘
‘q = 0‘
For the other goal, we use nonlinear inequality reasoning, so we need to explicitly
indicate the lemmas rather than relying on a standard decision procedure:
80
# e(so have ‘p * p <= q * q‘ (using [LE_MULT2] trivial));;
...
val it : goalstack = 1 subgoal (1 total)
‘q = 0‘
We would like to claim that this proof can be read in isolation, without running it in
HOL. For each step, every fact we used is clearly labelled somewhere else in the proof,
and every assumption is given explicitly. The reader might like to study the proof to
see how easy it is to read it without running it. If some bits still seem obscure, how
about adding new intermediate steps so the ‘jumps’ are smaller?
It seems that procedural and declarative styles of proof both have their merits (Har-
rison 1996c). Procedural scripts can be obscure and hard to understand, but declara-
tive scripts can be unduly verbose and lacking in programmability. Our present setup
just makes ‘declarative’ steps into ordinary tactics, following Wiedijk (2001), so parts
in the declarative style can be mixed with arbitrary tactic steps. For example, you
could replace the first step with MATCH_MP_TAC num_WF after all. Moreover, since
81
we’re still inside the OCaml read-eval-print loop, you don’t have to stop with the par-
ticular constructs we’ve defined, but can implement your own. In the subdirectory
Mizarlight you can find Wiedijk’s original “Mizar Light” system together with
two proofs using this style of duality in projective geometry, one using existing auto-
mated tools like MESON to fill the gaps, the other using a custom prover modelled on
Mizar’s own.
So while in what follows we will mostly do proofs by ‘traditional HOL’ tactic
scripts, you might like to do something different. Perhaps even the new proof steps are
still too stylized for your taste, because they’re forced to fit into a consistent OCaml
typing scheme. For example, it might be nice if labels could just be omitted when
not wanted, and you might even want to parse a more ‘English-like’ formal text rather
that directly plugging together OCaml constructs. The point is that HOL’s arbitrary
programmability means that when you don’t like a particular feature, you don’t have to
take it lying down.
13 Recursive definitions
It’s often convenient to define a function f recursively, where a value f (x) may be
defined in terms of f itself. A classic example is the sequence of Fibonacci numbers
1, 1, 2, 3, 5, 8, 13, 21 . . ., where each number in the sequence is defined to be the sum
of the previous two. Here the recursive definition is Fn+2 = Fn+1 + Fn (as well as the
base cases F0 = F1 = 1). In OCaml we can define a function recursively by adding
the keyword rec after the let:
Moving from OCaml to the HOL analog, the basic new_definition does not
permit recursive definitions:
but there is a more powerful principle called simply define, which can often au-
tomatically justify simple recursive definitions by reducing them to more primitive
principles:
82
# let fib2 = define
‘(fib2 0 = 1) /\
(fib2 1 = 1) /\
(fib2 (n + 2) = fib2(n) + fib2(n + 1))‘;;
val fib2 : thm =
|- fib2 0 = 1 /\ fib2 1 = 1 /\ fib2 (n + 2) = fib2 n + fib2 (n + 1)
The similarities are clear, but so are the differences. First, OCaml does not allow
the user to pattern-match against n + 2, so the analog of the HOL definition will be
rejected there. The difficulty is in proving that there is a unique n corresponding to
each value of n + 2. In this case it is easy, and HOL has no trouble justifying it, but in
general it can be false or arbitrarily difficult to prove, so OCaml places strong syntactic
restrictions on patterns. HOL will also fail if more difficult reasoning is required or if
the uniqueness property fails:
Indeed, if HOL could make such a definition, we would be able to derive incon-
sistencies from it. Note that setting n = 0 in the last clause would give fib3(0) =
fib3(0 − 1) + fib3(0 − 2). Taking cutoff subtraction into account, this would give
fib3(0) = fib3(0) + fib3(0) and so fib3(0) = 0, contradicting the first clause.
83
# let th = ASSUME
‘(fib3 0 = 1) /\
(fib3 1 = 1) /\
(!n. fib3 n = fib3(n - 1) + fib3(n - 2))‘;;
val th : thm =
fib3 0 = 1 /\ fib3 1 = 1 /\ (!n. fib3 n = fib3 (n - 1) + fib3 (n - 2))
|- fib3 0 = 1 /\ fib3 1 = 1 /\ (!n. fib3 n = fib3 (n - 1) + fib3 (n - 2))
# CONJ (CONJUNCT1 th) (SPEC ‘0‘ (CONJUNCT2(CONJUNCT2 th)));;
val it : thm =
fib3 0 = 1 /\ fib3 1 = 1 /\ (!n. fib3 n = fib3 (n - 1) + fib3 (n - 2))
|- fib3 0 = 1 /\ fib3 0 = fib3 (0 - 1) + fib3 (0 - 2)
# EQ_MP(ARITH_RULE(mk_eq(concl it,‘F‘))) it;;
val it : thm =
fib3 0 = 1 /\ fib3 1 = 1 /\ (!n. fib3 n = fib3 (n - 1) + fib3 (n - 2))
|- F
Even without using pattern-matching and multiple cases, recursive definitions can-
not always be made consistently in HOL, and will be rejected unless HOL is able to
justify them. For example, it is even more straightforward to derive a contradiction
from a definition f (n) = f (n) + 1:
The contrast with OCaml is interesting. The analogous definition there is accepted,
but when applied will go into an infinite loop until memory is exhausted. We might say
that OCaml avoids an analogous formal inconsistency because expressions may result
in exceptions or nontermination, and both sides of the recursion equation are equal in
the sense ‘both are nonterminating’.
On the other hand, there are situations where HOL can justify a definition when the
analogous OCaml program would loop or fail. Consider the following slightly different
definition:
This is accepted by HOL because the definition is trivially logically consistent (any
constant function would work) and yet the analogous function would loop indefinitely
in OCaml when applied. For similar reasons HOL will happily swallow the follow-
ing recursive definition, even though it’s a significant open problem in mathematics
whether the analogous program always terminates — the Collatz conjecture (Lagarias
1985).26
26 Functional programming cognoscenti might observe that every tail recursive function is automatically
logically admissible. This observation — which I owe to J Moore — is used internally by HOL in such
cases.
84
define
‘!n. collatz(n) = if n <= 1 then n
else if EVEN(n) then collatz(n DIV 2)
else collatz(3 * n + 1)‘;;
Although useful in common simple cases, the power of define is limited, and it
often rejects functions that to a human are obviously consistent. We will later see in
more detail how to help HOL in such a situation. One simple method that sometimes
works is to slightly reformulate the definition and derive the “definition” that was really
wanted as a consequence. For example, the following is rejected because HOL is
unable to prove automatically that the first and last clauses are consistent with each
other: 27
and a pretty straightforward case-split lets us prove the original desideratum as a theo-
rem; it also follows from either one that fusc 0 = 0:
85
# let binom = define
‘(!n. binom(n,0) = 1) /\
(!k. binom(0,SUC(k)) = 0) /\
(!n k. binom(SUC(n),SUC(k)) = binom(n,SUC(k)) + binom(n,k))‘;;
At least HOL accepts the definition without complaint, and we have established
from the start that it’s a natural number. And as we will see, we can prove the bino-
mial theorem directly from this definition without once mentioning factorials. How-
ever, it does
no harm to sanity-check our definition by deriving the expected property
n!k! n+kk = (n + k)!. First of all it’s handy
to have lemmas covering a couple of
degenerate cases. We want to know that nk = 0 for n < k, and then that nn = 1.
The proofs amount to little more than performing induction and expanding with the
definition of binom:
# let BINOM_LT = prove
(‘!n k. n < k ==> (binom(n,k) = 0)‘,
INDUCT_TAC THEN INDUCT_TAC THEN REWRITE_TAC[binom; ARITH; LT_SUC; LT] THEN
ASM_SIMP_TAC[ARITH_RULE ‘n < k ==> n < SUC(k)‘; ARITH]);;
For the base case we perform another induction, this time on k, so the overall proof is by
nested induction. Once again we follow the induction by expanding all the definitions
and this is enough to eliminate the base case:
# e(INDUCT_TAC THEN REWRITE_TAC[ADD_CLAUSES; FACT; MULT_CLAUSES; binom]);;
val it : goalstack = 1 subgoal (1 total)
86
This is starting to look a bit bewildering, but we only need one more injection of
human intelligence, to apply the (outer) inductive hypothesis to the case SUC(k):
at which point, after again rewriting with the recursion equations, the result is just
algebra:
The whole proof script, though quite a lot goes on, is fairly compact:
87
The base case is, as often, a case of rewriting with various elementary lemmas; you
just have to know what some of them are called.
We now use the recursion for binomial coefficients to rewrite the sum on the right:
and now apply distributivity as well as simplifying the non-sum term on the right:
Now, the first sum on the left and the second sum on the right are actually equal
when a few straightforward normalizations are made. In order to avoid disturbing other
subterms with these normalizations, we split it off into a separate subgoal and solve it;
we grab the chance to slightly rearrange the other subgoal too:
88
Pn Pn+1
We can now reindex the sum i=0 f (i + 1) = i=1 f (i); note that we need to
instantiate the appropriate lemma manually since HOL’s higher-order matching will
not find the instantiation on its own:
# e(SIMP_TAC[ADD1; SYM(REWRITE_CONV[NSUM_OFFSET]‘nsum(m+1..n+1) (\i. f i)‘)]);;
Now we separate out the last term of the sum on the right:
and then separate the first term of the sum on the left:
# e(SIMP_TAC[NSUM_CLAUSES_LEFT; LE_0]);;
val it : goalstack = 1 subgoal (1 total)
‘y * y EXP n +
nsum (1 .. n) (\x’. y * binom (n,x’) * x EXP x’ * y EXP (n - x’)) =
y * y EXP n +
nsum (SUC 0 .. n) (\i. binom (n,i) * x EXP i * y EXP (SUC n - i))‘
‘y * y EXP n +
nsum (1 .. n) (\x’. y * binom (n,x’) * x EXP x’ * y EXP (n - x’)) =
y * y EXP n + nsum (1 .. n) (\i. binom (n,i) * x EXP i * y * y EXP (n - i))‘
# e(REWRITE_TAC[MULT_AC]);;
val it : goalstack = No subgoals
For the proof script, we don’t bother to throw away the inductive hypothesis, and
we apply the rewrite that disposes of the base case at both subgoals, since it just has no
effect on the latter:
89
let BINOMIAL_THEOREM = prove
(‘!n. (x + y) EXP n = nsum(0..n) (\k. binom(n,k) * x EXP k * y EXP (n - k))‘,
INDUCT_TAC THEN ASM_REWRITE_TAC[EXP] THEN
REWRITE_TAC[NSUM_SING_NUMSEG; binom; SUB_REFL; EXP; MULT_CLAUSES] THEN
SIMP_TAC[NSUM_CLAUSES_LEFT; ADD1; ARITH_RULE ‘0 <= n + 1‘; NSUM_OFFSET] THEN
ASM_REWRITE_TAC[EXP; binom; GSYM ADD1; GSYM NSUM_LMUL] THEN
REWRITE_TAC[RIGHT_ADD_DISTRIB; NSUM_ADD_NUMSEG; MULT_CLAUSES; SUB_0] THEN
MATCH_MP_TAC(ARITH_RULE ‘a = e /\ b = c + d ==> a + b = c + d + e‘) THEN
CONJ_TAC THENL [REWRITE_TAC[MULT_AC; SUB_SUC]; REWRITE_TAC[GSYM EXP]] THEN
SIMP_TAC[ADD1; SYM(REWRITE_CONV[NSUM_OFFSET]‘nsum(m+1..n+1) (\i. f i)‘)] THEN
REWRITE_TAC[NSUM_CLAUSES_NUMSEG; GSYM ADD1; LE_SUC; LE_0] THEN
SIMP_TAC[NSUM_CLAUSES_LEFT; LE_0] THEN
SIMP_TAC[BINOM_LT; LT; MULT_CLAUSES; ADD_CLAUSES; SUB_0; EXP; binom] THEN
SIMP_TAC[ARITH; ARITH_RULE ‘k <= n ==> SUC n - k = SUC(n - k)‘; EXP] THEN
REWRITE_TAC[MULT_AC]);;
# IN;;
val it : thm = |- !P x. x IN P <=> P x
All the usual set operations are defined, as well as one or two others that may be
unfamiliar. (For example UNIV, the universe set, only really makes sense when there
is a largest set of a given type, which is not true in most conventional set theories.)
x∈s x IN s x is in set s
s⊆t s SUBSET t s is a subset of t
s⊂t s PSUBSET t s is a proper subset of t
∅ or {} EMPTY or {} Empty set
s∪t s UNION t Union of s and t
s∩t s INTER t Intersection of s and t
s − t or s \ t s DIFF t Difference of s and t
V UNIV Universe set
{x} ∪ t x INSERT t Insertion of x into s
tS− {x} t DELETE x Deletion of x from s
Ts UNIONS s Union of all members of s
s INTERS s Intersection of all members of s
If you know what the standard definitions of all these concepts are in set theory, the
formal HOL definitions should hold few surprises, e.g.
# SUBSET;;
val it : thm = |- !s t. s SUBSET t <=> (!x. x IN s ==> x IN t)
90
# NOT_IN_EMPTY;;
val it : thm = |- !x. ˜(x IN {})
# IN_INSERT;;
val it : thm = |- !x y s. x IN y INSERT s <=> x = y \/ x IN s
# IN_INTER;;
val it : thm = |- !s t x. x IN s INTER t <=> x IN s /\ x IN t
# IN_DIFF;;
val it : thm = |- !s t x. x IN s DIFF t <=> x IN s /\ ˜(x IN t)
In the case of set abstractions, HOL also supports the extension to non-variables:
# ‘{n * n | ODD(n)}‘;;
val it : term = ‘{n * n | ODD n}‘
Note that the latter is printed without the double bars since it matches the defaults
in this case. HOL’s rule for simple abstractions is that all variables appearing free
on both sides of the bar are considered to range arbitrarily, and others are considered
fixed parameters, except in the trivial case that there are no variables free on the right
or only one free on the left. If you don’t want to use the ‘double bar’ notation to
disambiguate, you can always introduce additional ranging variables right of the bar by
stating membership in the universe, which is perhaps no bad thing as documentation
anyway.
Note that HOL prints the universal set UNIV over a type ty as (: ty), since this
often makes formulas more understandable. The same notation is also accepted on
parsing, as in the following example. In order to eliminate the set abstraction from a
term of the form x ∈ {t | p}, rewrite with the theorem IN_ELIM_THM, which will
nicely eliminate the internal representation of set abstractions, e.g.
28 See Dijkstra (2000) for a discussion of this and other issues in formal mathematical notation.
91
# REWRITE_CONV[IN_ELIM_THM]
‘z IN {3 * x + 5 * y | x IN (:num) /\ y IN (:num)}‘;;
val it : thm =
|- z IN {3 * x + 5 * y | x IN (:num) /\ y IN (:num)} <=>
(?x y. (x IN (:num) /\ y IN (:num)) /\ z = 3 * x + 5 * y)
HOL also supports set enumeration syntax, like set abstractions but with an explicit
comma-separated list of elements rather than an indexed expression. This is just a
parsing and printing layer on top of iterated application of INSERT to the empty set,
precisely analogous to the special parsing and printing of lists. For example:
# ‘{1,2,3}‘;;
val it : term = ‘{1, 2, 3}‘
# ‘1 INSERT 2 INSERT 3 INSERT EMPTY‘;;
val it : term = ‘{1, 2, 3}‘
One critical property of sets is extensionality; this is just a special case of exten-
sionality of functions reformulated using IN:
# FUN_EQ_THM;;
val it : thm = |- !f g. f = g <=> (!x. f x = g x)
# EXTENSION;;
val it : thm = |- !s t. s = t <=> (!x. x IN s <=> x IN t)
# SELECT_AX;;
val it : thm = |- !P x. P x ==> P ((@) P)
The operator is a binder in HOL (hence the parentheses round it above), and is
most commonly used together with a lambda. The usual English reading of εx. P [x],
92
reflecting the intended semantics above, is ‘some x such that P [x]’. If we happen to
know that there is exactly one x satisfying P , then the reading ‘the x such that P [x]’ is
justified. For example, it is used to define another binder for ‘the minimal n satisfying
P ’:
# minimal;;
val it : thm = |- !P. (minimal) P = (@n. P n /\ (!m. m < n ==> ˜P m))
Occasionally it can be useful to use the ε operator directly in proofs. MESON has
some ability to prove simple things about it, e.g.
The implication the other way holds trivially, and the equivalence is actually em-
bedded in the following theorem:29
# SKOLEM_THM;;
val it : thm = |- !P. (!x. ?y. P x y) <=> (?y. !x. P x (y x))
# CARD_UNION;;
val it : thm =
|- !s t.
FINITE s /\ FINITE t /\ s INTER t = {}
==> CARD (s UNION t) = CARD s + CARD t
29 The name is explained by the fact that this is a kind of non-metatheoretic variant of Skolemization for
first-order logic.
93
Note that the value returned by CARD is unspecified for infinite sets. It’s often
useful to combine a cardinality and finiteness assumption using the infix HAS_SIZE
operator:
# HAS_SIZE;;
val it : thm = |- !s n. s HAS_SIZE n <=> FINITE s /\ CARD s = n
Moving on to infinite sets, we will consider here only the idea of cardinal com-
parison, by defining what it means for a set S to be ‘6 in size’ than set T , whether
the sets are finite or infinite. As is customary, we will informally write this relation
as |S| 6 |T |, but there is no need for our purposes to assume that |S| independently
means anything.30 Two plausible definitions of |S| 6 |T | are:
We’ll start by proving that these two definitions are indeed equivalent. For sim-
plicity we will assume that the ‘sets’ are simply the universal sets of the corresponding
types A and B. Our first step will be to prove that a function is surjective iff it has a
‘right inverse’ under composition; here I is the identity function:
Rewriting with the definitions of composition and the identity map and applying
extensionality we get:
It might seem far from obvious how to prove this. But amusingly enough it’s just
an instance of SKOLEM_THM:
# e(REWRITE_TAC[SKOLEM_THM]);;
val it : goalstack = No subgoals
That wasn’t too bad. Now we’ll try an analogous thing with injectivity, showing its
equivalence to the existence of a left inverse:
30 Our use of | − | is a kind of contextual definition, only considered meaningful in special contexts. In
set theory it’s normal to use actual sets for cardinal numbers — for example minimal von Neumann ordinals
— so that there really is an independent notion corresponding to |S|. This doesn’t really work in HOL
because the types restrict how large sets over a given base type can be. However, for many elementary uses
of cardinal arithmetic, no independent cardinal ‘objects’ are needed, and in some contexts they can even
be harmful, arguably lying at the root of the Burali-Forti paradox (Forster 2003). Analogously, one can
always see which of two finite sets is bigger by pairing up the elements against each other with no need to
assign numbers, and one can see which body is hotter by seeing which way heat flows, without measuring a
‘temperature’.
94
# g ‘(!x x’. f(x) = f(x’) ==> x = x’) <=> (?g:B->A. g o f = I)‘;;
val it : goalstack = 1 subgoal (1 total)
Things aren’t quite as easy this time; we can’t yet simply apply SKOLEM_THM as
a rewrite. But by a devious trick we can massage the goal into a form where we can.
We split off a basic first-order equivalence as a lemma, and rewrite our goal with it:
# let lemma = MESON[]
‘(!x x’. f x = f x’ ==> x = x’) <=> (!y:B. ?u:A. !x. f x = y ==> u = x)‘;;
...
# e(REWRITE_TAC[lemma]);;
val it : goalstack = 1 subgoal (1 total)
Don’t be thrown by HOL’s choice of u as the variable name; this is a function with
the same type as the g on the right. If one ignores the outer existential quantifiers, both
sides are first-order equivalent, so MESON can finish the job:
# e(MESON_TAC[]);;
...
val it : goalstack = No subgoals
Now we can prove that our two characterizations of cardinal comparison are indeed
equivalent. Rewriting with the two lemmas almost finishes the job:
# g‘(?f:A->B. !x x’. f(x) = f(x’) ==> x = x’) <=> (?g:B->A. !x. ?y. g y = x)‘;;
val it : goalstack = 1 subgoal (1 total)
# e(REWRITE_TAC[INJECTIVE_IFF_LEFT_INVERSE; SURJECTIVE_IFF_RIGHT_INVERSE]);;
val it : goalstack = 1 subgoal (1 total)
95
The two sides are the same except for the order of the existential quantifiers, which
makes no logical difference. The fact that it doesn’t is embodied in a theorem SWAP_EXISTS_THM,
so while we can just use MESON to finish things off, we might more explicitly do this:
# SWAP_EXISTS_THM;;
val it : thm = |- !P. (?x y. P x y) <=> (?y x. P x y)
# e(MATCH_ACCEPT_TAC SWAP_EXISTS_THM);;
val it : goalstack = No subgoals
If you think of the way this enumerates elements one at a time, it becomes clear that
it is indeed injective. To formalize this fact algebraically, we might start by showing
that cantor(x, y) = cantor(x0 , y 0 ) ⇒ x + y = x0 + y 0 . To see this, observe that we can
write cantor(x, y) as ((x + y)2 + (x + y) + 2x)/2. Now if x0 + y 0 > x + y, since
everything is an integer, we have x0 + y 0 > x + y + 1 and so
But let us consider a more explicit proof without relying on such ‘big guns’. The
critical reasoning above was that x + y < x0 + y 0 ⇒ cantor(x, y) < cantor(x0 , y 0 ), so
let’s tackle that:
96
First we use discreteness to change x + y < x0 + y 0 into x + y + 1 6 x0 + y 0 and
expand out the definition of the Cantor function, rewriting the body into the (x + y)2 +
(x + y) + 2x form:
Integer division is usually a bit painful to deal with. We could explicitly show that
the division is always exact because the body is even, e.g.
But the present inequality is not tight enough that we need to be so careful. We can
just use the following:
Following the informal proof, we’ll now use some transitivity reasoning, chaining
inequalities through an intermediate term:
The first goal is not nonlinear in any interesting way, so ARITH can deal with it:
# e ARITH_TAC;;
val it : goalstack = 1 subgoal (1 total)
This goal does involve a little nonlinear reasoning, albeit fairly trivial. We’ll use a
lemma to break it down into its subcomponents:
This can be finished off by simplification using the assumptions and a couple of pre-
proved theorems, one that n2 = n · n and the other the monotonicity of multiplication:
97
# e(ASM_SIMP_TAC[EXP_2; LE_MULT2]);;
val it : goalstack = No subgoals
Now we’ve got the key lemma, one way or another. For the final theorem, we try
to conform exactly to our definition of injectivity by quantifying over an element of
N × N rather than the two components separately:
We want to use the lemma, but not to throw away the antecedent here because it
will be used again:
We repeat the expansion and tweaking of the cantor pairing from the proof of the
lemma:
98
We can eliminate the division by 2 from the last term:
We can explicitly cancel the identical terms from the equation in the antecedent by
rewriting with EQ_ADD_LCANCEL. But now we’re already at the stage where we can
leave things to the linear arithmetic tactic anyway:
These examples may be so surprising that one might swing to the opposite conclu-
sion of assuming cardinal inequality holds between any pair of infinite sets. (Galileo
said something of that sort.) However, an even more fundamental theorem of Cantor
shows that this is not true; there are (infinitely!) many distinct levels of infinity. In
particular, there is no injective map from a function space α → bool back into α. (In
the light of the equivalence of sets and predicates, we can think of α → bool as the set
of all subsets of α, often called its power set.)
For convenience, we exploit the function calculus equivalent we used above; we expand
away composition and identity as usual, but for clarity we only apply extensionality
once:
# e(REWRITE_TAC[INJECTIVE_IFF_LEFT_INVERSE] THEN
ONCE_REWRITE_TAC[FUN_EQ_THM] THEN
REWRITE_TAC[I_DEF; o_DEF] THEN STRIP_TAC);;
val it : goalstack = 1 subgoal (1 total)
0 [‘!x. g (f x) = x‘]
‘F‘
Now comes the clever ‘diagonalization’ step — this proof is much shorter than the
pairing function proof above, but subtler. We consider the set of elements that are not
in their g-image, i.e. {x | x 6∈ g(x)}:
31 Again, a very short proof is to be had using the SOS tools, though the main lemma is still needed.
99
# e(FIRST_X_ASSUM(MP_TAC o SPEC ‘{x:A | ˜(x IN g(x))}‘) THEN
REWRITE_TAC[EXTENSION; NOT_FORALL_THM]);;
val it : goalstack = 1 subgoal (1 total)
Here is a slightly shorter variant of this proof, using predicate rather than set no-
tation. Since MESON’s handling of lambdas is a bit better than its handling of set
abstractions, it can figure out the last witness for itself, though not the first diagonal
trick:
# let CANTOR_THM = prove
(‘˜(?f:(A->bool)->A. (!x y. f(x) = f(y) ==> x = y))‘,
REWRITE_TAC[INJECTIVE_IFF_LEFT_INVERSE; FUN_EQ_THM; I_DEF; o_DEF] THEN
STRIP_TAC THEN FIRST_ASSUM(MP_TAC o SPEC ‘\x:A. ˜(g x x)‘) THEN
MESON_TAC[]);;
These two results are enough to show that cardinal comparison is a non-trivial
subject. A more comprehensive HOL theory of cardinality, including comparisons of
arbitrary subsets and addition and multiplication operations on infinite sets, can be
found in Library/card.ml.
15 Inductive datatypes
Sometimes one wants to define new types in addition to the ones HOL has built in.
HOL does support a mechanism for defining new types, but although it’s technically
simple and guaranteed to be conservative, it’s not very convenient to use. So we’ll
defer a full explanation for a while, and just show how to use a more convenient higher-
level function define_type, which works in many situations. This is also a good
opportunity to discuss the analogy with similar facilities at the OCaml level.
100
# type ternary = Zero | One | Unknown;;
type ternary = Zero | One | Unknown
The new constants are ‘constructors’, so you can use them to pattern-match against,
e.g. in the following lifting of a ‘and’ gate to the information-theoretic level:
The induction theorem states that to demonstrate that a property P holds for all
objects of type ‘ternary’, it suffices to prove that it holds for the three values Zero,
One and Unknown. The recursion theorem states that given any three values f 0, f 1
and f 2, you can always define a function mapping the three values Zero, One and
Unknown to those values respectively. If you think about it, induction is just saying
that Zero, One and Unknown are the only values of the type, while recursion is saying
that these values are all distinct. But stating them in this form admits generalization
to the situations we will consider next. HOL’s define will automatically be able to
exploit the recursion theorem to make pattern-matching definitions work much as in
OCaml, e.g.
and such structured types can be made general, so they apply to any suitable base types:
101
# type (’a,’b)sum = Inl of ’a | Inr of ’b;;
type (’a, ’b) sum = Inl of ’a | Inr of ’b
# Inl 1;;
val it : (int, ’a) sum = Inl 1
# Inr (fun x -> x + 1);;
val it : (’a, int -> int) sum = Inr <fun>
(α, β)sum
α XXX
XXX inl
XXX
XXX
z
X
:
inr
β
More interestingly still, we can parametrize by the very type being defined, leading
to recursive types. For example, this declares a type of lists (finite ordered sequences)
of elements of type ’a.
# Nil;;
val it : ’a seq = Nil
# Cons(false,Nil);;
val it : bool seq = Cons (false, Nil)
# Cons(1,Cons(2,Nil));;
val it : int seq = Cons (1, Cons (2, Nil))
# Cons(Cons(1,Nil),Nil);;
val it : int seq seq = Cons (Cons (1, Nil), Nil)
Note that in the last example the type is different, because the head is itself a se-
quence. Such type constructors can be composed with themselves and with others
32 This is similar to a union in C, but in CAML the copies of the component types are kept apart and one
always knows which of these an element of the union belongs to. By contrast, in C the component types are
overlapped, and the programmer is responsible for this book-keeping.
102
(sums, pairs, functions etc.) arbitrarily. Actually, OCaml already has a built-in type
of lists, with the empty list written [] and the recursive constructor ::, having infix
status. The analogous lists to the above are:
# [];;
val it : ’a list = []
# false::[];;
val it : bool list = [false]
# 1::2::[];;
val it : int list = [1; 2]
# (1::[])::[];;
val it : int list list = [[1]]
Note the abbreviated way in which lists are printed by OCaml. This is also accepted
on input. And pretty much everything we’ve said for OCaml applies for HOL too.
There is a type constructor for lists, the empty list is NIL, the constructor is CONS and
the same sort of abbreviated format is supported on parsing and printing:
We can even, if we wish, support the same infix consing operation to make HOL look
even more like OCaml:
# parse_as_infix("::",(23,"right"));;
val it : unit = ()
# override_interface("::",‘CONS‘);;
Warning: inventing type variables
val it : unit = ()
# ‘1::2::[]‘;;
val it : term = ‘[1; 2]‘
103
# REWRITE_CONV[length] ‘length [1;2;3;4]‘;;
val it : thm = |- length [1; 2; 3; 4] = 1 + 1 + 1 + 1 + 0
# REWRITE_CONV[length; ARITH] ‘length [1;2;3;4]‘;;
val it : thm = |- length [1; 2; 3; 4] = 4
It is often helpful to visualize the elements of recursive types as tree structures, with
the recursive constructors at the branch nodes and the other datatypes at the leaves. The
recursiveness merely says that plugging subtrees together gives another tree. In the case
of lists the ‘trees’ are all rather spindly and one-sided, with the list [1;2;3;4] being
represented as:
@
@
@
1 @
@
@
2 @
@
@
3 @
@
@
4 []
A particularly natural use of recursive types is to represent the abstract syntax trees
(ASTs) of programming languages, logics and other formalized languages. For exam-
ple, consider a language of arithmetic expressions built up by addition and multiplica-
tion from numeric constants:
Indeed, it is exactly in this way that HOL’s own types and terms are defined in
OCaml. And later we will use similar constructs at the HOL level to reason about
programming languages and other logics.
104
Figure 2: The Fano plane
It’s a bit laborious entering the exact incidence relation (which points are on which
lines). First, we express it as a list of pairs, where a pair (i, j) is included precisely if
point i is supposed to be on line j:
let fano_incidence =
[1,1; 1,2; 1,3; 2,1; 2,4; 2,5; 3,1; 3,6; 3,7; 4,2; 4,4;
4,6; 5,2; 5,5; 5,7; 6,3; 6,4; 6,7; 7,3; 7,5; 7,6];;
Now we can do a little bit of ad-hoc programming to map this incidence relation
into an appropriate HOL definition. First we define functions to map an index i into the
corresponding constants Pointi and Linei , and also define a couple of generic variables
of the appropriate types:
Hence we can define a function to map a pair (i, j) into the assertion p = Pointi ∧
l = Linej :
and hence define the incidence relation, for which we use an infix ON:
105
# parse_as_infix("ON",(11,"right"));;
val it : unit = ()
# let ON = new_definition
(mk_eq(‘((ON):point->line->bool) p l‘,
list_mk_disj(map fano_clause fano_incidence)));;
val ( ON ) : thm =
|- !p l.
p ON l <=>
p = Point_1 /\ l = Line_1 \/
p = Point_1 /\ l = Line_2 \/
p = Point_1 /\ l = Line_3 \/
p = Point_2 /\ l = Line_1 \/
p = Point_2 /\ l = Line_4 \/
p = Point_2 /\ l = Line_5 \/
p = Point_3 /\ l = Line_1 \/
p = Point_3 /\ l = Line_6 \/
p = Point_3 /\ l = Line_7 \/
p = Point_4 /\ l = Line_2 \/
p = Point_4 /\ l = Line_4 \/
p = Point_4 /\ l = Line_6 \/
p = Point_5 /\ l = Line_2 \/
p = Point_5 /\ l = Line_5 \/
p = Point_5 /\ l = Line_7 \/
p = Point_6 /\ l = Line_3 \/
p = Point_6 /\ l = Line_4 \/
p = Point_6 /\ l = Line_7 \/
p = Point_7 /\ l = Line_3 \/
p = Point_7 /\ l = Line_5 \/
p = Point_7 /\ l = Line_6
For later proofs, it’s more convenient to have a comprehensive list of theorems
telling us which of the 49 possible incidence relationships are true. We can easily do
this using the built-in function allpairs, which maps the first argument function
over all pairs from the two lists in the next two arguments:
Now, since the underlying types are finite, any universal quantifier over those types
is equivalent to a conjunction. If we split this into two implications, one of them is
already there in the induction theorem, and the other is a triviality handled by simplifi-
cation.
106
let FORALL_POINT = prove
(‘(!p. P p) <=> P Point_1 /\ P Point_2 /\ P Point_3 /\ P Point_4 /\
P Point_5 /\ P Point_6 /\ P Point_7‘,
EQ_TAC THENL [SIMP_TAC[]; REWRITE_TAC[point_INDUCT]]);;
‘(?p. P p) <=>
P Point_1 \/
P Point_2 \/
P Point_3 \/
P Point_4 \/
P Point_5 \/
P Point_6 \/
P Point_7‘
‘˜(?p. P p) <=>
˜(P Point_1 \/
P Point_2 \/
P Point_3 \/
P Point_4 \/
P Point_5 \/
P Point_6 \/
P Point_7)‘
and use the de Morgan laws, finite (DE_MORGAN_THM) and infinite (NOT_EXISTS_THM)
to convert the problem back to the universal case for a negated predicate:
# e(REWRITE_TAC[DE_MORGAN_THM; NOT_EXISTS_THM]);;
val it : goalstack = 1 subgoal (1 total)
‘(!p. ˜P p) <=>
˜P Point_1 /\
˜P Point_2 /\
˜P Point_3 /\
˜P Point_4 /\
˜P Point_5 /\
˜P Point_6 /\
˜P Point_7‘
Throwing the universal theorem FORALL_POINT into the rewrite will now solve
the goal. We can package up these proofs as:
107
let EXISTS_POINT = prove
(‘(?p. P p) <=> P Point_1 \/ P Point_2 \/ P Point_3 \/ P Point_4 \/
P Point_5 \/ P Point_6 \/ P Point_7‘,
MATCH_MP_TAC(TAUT ‘(˜p <=> ˜q) ==> (p <=> q)‘) THEN
REWRITE_TAC[DE_MORGAN_THM; NOT_EXISTS_THM; FORALL_POINT]);;
Now, we can in principle test the truth or falsity of any formula where all variables
are of type Point or Line and are bound by quantifiers, and the only relation is the in-
cidence relation ON. We just need to expand all the quantified formulas into cases, then
apply the clauses of the incidence relation as a rewrite. Of course, this approach is a lit-
tle crude and vulnerable to exponential blowup. But for the relatively simple formulas,
it works fast enough, and we can improve things by carefully choosing the appropri-
ate depth conversions to expand inner quantifiers first (otherwise the outer quantifiers
will blow up the number of instance of the inner quantifiers) and similarly apply the
remaining evaluation bottom-up:
let FANO_TAC =
GEN_REWRITE_TAC DEPTH_CONV
[FORALL_POINT; EXISTS_LINE; EXISTS_POINT; FORALL_LINE] THEN
GEN_REWRITE_TAC DEPTH_CONV
(basic_rewrites() @
[ON_CLAUSES; distinctness "point"; distinctness "line"]);;
Our hard work is done; we now just have to postulate the axioms and sit back and
wait (not for too long). Any two distinct points determine a line, and that line is unique:
108
and every line contains at least three distinct points:
• Denotational
• Operational
• Axiomatic
# let string_INDUCT,string_RECURSION =
define_type "string = String (int list)";;
Now we move on to the abstract syntax of the language; this is just a HOL for-
malization of the kind of thing you typically find in a programming language reference
manual. First we have a type of expressions: an expression may be built from variables
and numeric literals by addition and multiplication (note that in our toy language, all
variables will be considered as having type num):
109
# let expression_INDUCT,expression_RECURSION = define_type
"expression = Literal num
| Variable string
| Plus expression expression
| Times expression expression";;
# parse_as_infix(";;",(18,"right"));;
# parse_as_infix(":=",(20,"right"));;
# override_interface(";;",‘Sequence‘);;
# override_interface(":=",‘Assign‘);;
# overload_interface("+",‘Plus‘);;
# overload_interface("*",‘Times‘);;
We made the expression the first argument of value, and the state the second. This
is mostly a question of taste, but it’s interesting to observe that if we just apply value
to an expression, we get what we can think of as its semantics: a function from expres-
sions to values. Such a semantics is said to be denotational: it maps a programming
language construct to a mathematical object that is considered its meaning.
What about the meaning of a command? The key point about a command is that it
can not only read but also modify the state (by assigning variables). So we might want
110
to consider a command as a function from (initial) states to (final) states. However,
this raises some problems since it presupposes that all commands are terminating (the
final state always exists) and deterministic (the final state is determined uniquely by the
initial state). Instead we will use a somewhat more general notion: the semantics of
a command will be a binary relation on states, which holds for two states precisely if
the command can start in the first state and terminate in the second state. The style of
our definition will be operational, because we can think of it as an abstraction of how
a program ‘operates’ (is executed) step-by-step on a machine. This is naturally defined
inductively. For example, if a command c1 can achieve a transition from state s to s0
and a command c2 can achieve a transition from state s0 to s00 , the sequence c1 ; c2 can
achieve a transition from state s to state s00 . This is the second clause in the definition
below, and the other clauses can be understood similarly.
Although we have called our semantics operational, the cases theorem returned
looks very much like a denotational semantics:
The difficulty in taking this as the definition is that the semantics for the while loop
is recursive. Indeed, we might have tried to be clever and define it like this, but HOL
is not capable of proving that such a definition is consistent:
# define
‘sem(While e c) s s’ <=> if value e s = 0 then (s’ = s)
else ?s’’. sem c s s’’ /\ sem(While e c) s’’ s’‘;;
Exception: Failure "new_specification: Assumptions not allowed in theorem".
Even if it were, it’s not the case that this recursion equation fully defines the se-
mantics of a while loop. For example, if the expression e is always true (say just the
literal 1), the intended semantics is the empty relation, since the loop will never ter-
minate. But provided the command c in the body always terminates, there are many
111
other solutions to the recursion equation, including the everywhere-true relation. More
generally, we can satisfy the recursion equation for a while-loop by making the loop
terminate in any places where it ‘really’ doesn’t. Thus, the appropriate ‘denotational’
approach is more subtle. One may consider an explicitly iterative definition: there is
some sequence of states σ = σ0 , σ1 , . . . , σn = σ 0 such that the expression is true
in all states except σn and each pair of σi and σi+1 are connected by the command.
But this definition is a bit awkward to work with. We can use the recursive definition
provided we specify the minimal solution of the recursion equation — essentially the
recursion equation in conjunction with the induction theorem. An important aspect of
denotational semantics for less trivial languages (Scott and Strachey 1971) is the care-
ful study of when least fixed points exists for certain classes of recursion equation —
this is significant not only in defining the semantics of commands but also the meaning
of recursive types like OCaml’s.
16.2 Determinism
As our first deduction from the semantics, we will show that all programs are deter-
ministic.
As one might expect, this is proved by induction over the semantic rules. We start
by massaging the goal into the precise form to match the induction theorem:
# e(REWRITE_TAC[IMP_CONJ; RIGHT_FORALL_IMP_THM]);;
val it : goalstack = 1 subgoal (1 total)
Now we can match against the induction theorem and break down the resulting
subgoals a little:
‘˜(value e s = 0) /\
(!s’’. sem c s s’’ ==> s’ = s’’) /\
(!s’’’. sem (While e c) s’ s’’’ ==> s’’ = s’’’)
==> (!s’’’. sem (While e c) s s’’’ ==> s’’ = s’’’)‘
‘(!s’’. sem c1 s s’’ ==> s’ = s’’) /\ (!s’’’. sem c2 s’ s’’’ ==> s’’ = s’’’)
==> (!s’’’. sem (c1 ;; c2) s s’’’ ==> s’’ = s’’’)‘
112
Although this mass of cases may look a bit intimidating, they’re all pretty easy, and
can all be solved by a similar sequence of steps. We start by stripping down the first
goal further and performing a case analysis over sem (only rewriting once to avoid
infinite looping):
This seems to give many cases, but many hypothesize impossible equivalences be-
tween different program syntax constructors, and can be scrubbed easily:
By extensionality, to prove s0 = s00 it suffices to prove that for any variable name
x we have s0 (x) = s00 (x). After that, the goal is just first order and equality reasoning,
so MESON can finish the job, though it takes a few seconds because its handling of the
equations is a bit naive:
113
The reasoning for all the other cases is similar, and in fact the proofs are easier. So
we can get a complete script just by using the same sequence for all subgoals arising
from the initial induction theorem, and the final proof script is not as long as one might
have feared:
let DETERMINISM = prove
(‘!c s s’ s’’. sem c s s’ /\ sem c s s’’ ==> (s’ = s’’)‘,
REWRITE_TAC[IMP_CONJ; RIGHT_FORALL_IMP_THM] THEN
MATCH_MP_TAC sem_INDUCT THEN REPEAT CONJ_TAC THEN REPEAT GEN_TAC THEN
DISCH_TAC THEN ONCE_REWRITE_TAC[sem_CASES] THEN
REWRITE_TAC[distinctness "command"; injectivity "command"] THEN
REWRITE_TAC[FUN_EQ_THM] THEN ASM_MESON_TAC[]);;
This has the defect that it is vacuously true if the program fails to terminate in
state s, so one more commonly uses the plain ‘weakest precondition’ which adds a
hypothesis of termination:
# let wp = new_definition
‘wp c q s <=> terminates c s /\ wlp c q s‘;;
Note that if the program state space (type) is Σ, then wp c has type (Σ → bool) →
(Σ → bool), mapping output predicates to corresponding input predicates. For this
reason, wp c is frequently referred to as a predicate transformer. Predicate transform-
ers for real programs necessarily obey certain ‘healthiness’ conditions identified by
Dijkstra. For example, they must map empty sets to empty sets and must be monotonic
with respect to inclusion. We can prove these in HOL simply by rewriting with the
definitions and performing trivial first-order reasoning.
114
In addition, wp must distribute over conjunction and disjunction. The former must
hold even for a nondeterministic programming language, but for the latter determinism
is essential:
let WP_DISJUNCTIVE = prove
(‘(wp c p) UNION (wp c q) = wp c (p UNION q)‘,
REWRITE_TAC[FUN_EQ_THM; IN; wp; wlp; IN_ELIM_THM; UNION; terminates] THEN
MESON_TAC[DETERMINISM]);;
One can obtain explicit expressions for the weakest preconditions of commands.
For example, attractively, the weakest precondition of a sequence of commands is just
the composition of the two weakest preconditions of the components:
let WP_SEQ = prove
(‘!c1 c2 q. wp (c1 ;; c2) = wp c1 o wp c2‘,
REWRITE_TAC[wp; wlp; terminates; FUN_EQ_THM; o_THM] THEN REPEAT GEN_TAC THEN
GEN_REWRITE_TAC (LAND_CONV o ONCE_DEPTH_CONV) [sem_CASES] THEN
REWRITE_TAC[injectivity "command"; distinctness "command"] THEN
MESON_TAC[DETERMINISM]);;
Similarly, we can quite easily derive the usual Hoare rule for sequences:
let CORRECT_SEQ = prove
(‘!p q r c1 c2.
correct p c1 r /\ correct r c2 q ==> correct p (c1 ;; c2) q‘,
REWRITE_TAC[correct; WP_SEQ; o_THM] THEN
MESON_TAC[WP_MONOTONIC; SUBSET_TRANS]);;
115
We will develop these ideas more in the next section in a slightly different context.
17 Shallow embedding
In the previous section we explicitly defined the syntax of a programming language
and its semantic map within HOL. The syntactic constructs and their meanings were
quite different things, with the semantic map mediating between them. A contrasting
approach is to regard a programming language as merely a notation for describing that
semantics directly. For example, an assignment statement x := e does not denote
a syntactic object, but rather is a shorthand for, say, a state mapping, state transition
relation or weakest precondition that we associate with it.
This approach, and indeed the embedding of programming language semantics in
theorem provers generally, was pioneered by Gordon (1989), the inventor of the orig-
inal HOL. For some time it was known as semantic embedding, and the fullblown
formalization of the semantic map as syntactic embedding. However, since Boulton,
Gordon, Gordon, Harrison, Herbert, and Van Tassel (1993) the customary terminology
has been to describe the formalization of the syntax and the semantic map (as in the
previous section) as deep embedding and the direct semantic embedding as shallow
embedding.
It is worth noting that shallow embedding provides a natural formalization of the at-
titudes expressed by Dijkstra (1976), where a “programming language” (he deprecates
the very term) is viewed as an algorithmically-oriented system of mathematical nota-
tion first and foremost. Such a notation is meaningful in isolation from any particular
implementation, and indeed the role of a real programming language compiler on a real
machine is to provide a sufficiently close approximation to the abstract mathematical
counterpart. (Perhaps, for example, imposing some limitations on sizes of integers.)
116
code. But we will see when we consider refinement below that this can be considered
a strength as well as a weakness.
If we are planning to use variables of unrestricted types, extensible at will, we
cannot simply have the state as a function from variables to some type of ‘values’,
since we cannot decide on any fixed set of values (say, just integers and booleans) in
advance. A natural alternative is to represent the state as an iterated cartesian product
of the types of whatever variables we need (there will, presumably, only be a fairly
small finite number). For example, if we have two variables b and c of Boolean type
and three integer variables x, y and z, we can consider the state as
17.2 Commands
Recall that we are planning to regard commands as their weakest preconditions. An
assignment is basically determined by a state transition function, which is nothing
but another expression on states whose value happens to be of the same type. The
corresponding weakest precondition is just the composition of this transition with the
postcondition, because we test the postcondition after making the state transition, so:33
This notion of assignment is quite general. Among the more minor generalizations
of ordinary assignment we may encode a parallel assignment of multiple variables, e.g.
x, y := y, x to swap x and y:
33 The type restrictions are not essential but can help to infer more types later. Usually we make the
ML identifier storing the definitional theorem match the name of the constant. Here we would fall foul of
OCaml’s case sensitivity.
117
# ‘Assign (\(b:bool,c:bool,x:int,y:int,z:int). (b,c,y,x,z))‘;;
As we recall from the deep embedding, sequencing effectively composes the weak-
est preconditions, so that becomes our definition in the shallow embedding:
# parse_as_infix(";;",(18,"right"));;
# let sequence = new_definition
‘(c1:(S->bool)->(S->bool)) ;; (c2:(S->bool)->(S->bool)) = c1 o c2‘;;
It is also not difficult to see that the following is an appropriate definition for the ‘if’
constructs. We have two versions, If which has no ‘else’ clause and Ite (if-then-else)
that does:
The most challenging definition is the loop construct. Once again, we may be
tempted to define it inductively as the least predicate with the basic closure property,
and this almost works:
This theorem and the induction and cases theorem all have a hypothesis. The rea-
son is that, as mentioned earlier, HOL needs an appropriate monotonicity property to
justify the inductive definitions. While HOL does quite a good job of proving this
in obvious cases, here we actually have no guarantee because c can be any predi-
cate transformer, for instance complementation. Here is a less desirable contrast with
deep embedding. There, we proved that the wp of any command is monotonic, one
of Dijkstra’s healthiness conditions. However, here in the shallow embedding we are
not restricted to a fixed repertoire of commands so we need to add monotonicity as an
assumption in certain cases. We will therefore make a ‘manual’ definition of While
as the smallest predicate satisfying the basic closure property, i.e. the intersection of
all such predicates. Note that we have expanded out the auxiliary notions like If:
We will return to the proof of the critical properties later, but first we formalize the
notion of monotonicity for a predicate transformer and prove some key facts about it.
The definition is as one might expect:
118
# let monotonic = new_definition
‘monotonic c <=> !q q’. q SUBSET q’ ==> (c q) SUBSET (c q’)‘;;
It’s easy to prove that the basic assignment command is monotonic, basically by
just expanding the definitions:
and not much harder to show that the composite structures built from monotonic com-
ponents are themselves monotonic:
The proofs once again just really need us to expand the definitions, and are there-
after straightforward first-order reasoning. This applies equally to our While con-
struct, since our explicit definition is monotonic:
But let us return to verifying that our While has the properties we expect. If it had
been accepted as a normal inductive definition, we would have expected the usual trio
of theorems, provided the underlying command is monotonic:
# g ‘!e c q:S->bool.
monotonic c
==> (!s. If e (c ;; While e c) q s ==> While e c q s) /\
(!w’. (!s. If e (c ;; (\q. w’)) q s ==> w’ s)
==> (!a. While e c q a ==> w’ a)) /\
(!s. While e c q s <=> If e (c ;; While e c) q s)‘;;
We can at least use one of the less well-known functions in the inductive definition
package to verify that these properties hold.34
34 The reader probably won’t want to understand this proof in great detail. Essentially, we would other-
wise need to prove an instance of the Knaster-Tarski fixpoint theorem, and we are relying on the inductive
definitions package to perform the corresponding reasoning.
119
# e(REPEAT GEN_TAC THEN DISCH_TAC THEN
(MP_TAC o GEN_ALL o DISCH_ALL o derive_nonschematic_inductive_relations)
‘!s:S. (if e s then c w s else q s) ==> w s‘);;
val it : goalstack = 1 subgoal (1 total)
0 [‘monotonic c‘]
‘(!e c w q.
w = (\a. !w’. (!a. (if e a then c w’ a else q a) ==> w’ a) ==> w’ a)
==> (!w w’.
(!a. w a ==> w’ a)
==> (!a. (if e a then c w a else q a)
==> (if e a then c w’ a else q a)))
==> (!s. (if e s then c w s else q s) ==> w s) /\
(!w’. (!s. (if e s then c w’ s else q s) ==> w’ s)
==> (!a. w a ==> w’ a)) /\
(!a. w a <=> (if e a then c w a else q a)))
==> (!s. If e (c ;; While e c) q s ==> While e c q s) /\
(!w’. (!s. If e (c ;; (\q. w’)) q s ==> w’ s)
==> (!a. While e c q a ==> w’ a)) /\
(!s. While e c q s <=> If e (c ;; While e c) q s)‘
If we expand out all the definitions, the conclusion of the newly added antecedent
matches our desired conclusion:
# e(REWRITE_TAC[if_def; sequence; o_THM; IN_ELIM_THM; IMP_IMP]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘monotonic c‘]
‘(!e c w q.
w = (\a. !w’. (!a. (if e a then c w’ a else q a) ==> w’ a) ==> w’ a) /\
(!w w’.
(!a. w a ==> w’ a)
==> (!a. (if e a then c w a else q a)
==> (if e a then c w’ a else q a)))
==> (!s. (if e s then c w s else q s) ==> w s) /\
(!w’. (!s. (if e s then c w’ s else q s) ==> w’ s)
==> (!a. w a ==> w’ a)) /\
(!a. w a <=> (if e a then c w a else q a)))
==> (!s. (if e s then c (While e c q) s else q s) ==> While e c q s) /\
(!w’. (!s. (if e s then c w’ s else q s) ==> w’ s)
==> (!a. While e c q a ==> w’ a)) /\
(!s. While e c q s <=> (if e s then c (While e c q) s else q s))‘
and then we can eliminate the equational hypothesis since it will then just be an instance
of the definition of While:
# e(DISCH_THEN MATCH_MP_TAC THEN
REWRITE_TAC[FUN_EQ_THM; while_def; IN_ELIM_THM]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘monotonic c‘]
‘!w w’.
(!s. w s ==> w’ s)
==> (!s. (if e s then c w s else q s) ==> (if e s then c w’ s else q s))‘
120
giving the following as the overall proof:
In fact, if all we’re interested in is proving programs totally correct (which includes
proving termination), all we really need is the fixpoint property, so it’s worth separating
this out:35
Deriving the Hoare rules from the weakest preconditions is generally straightfor-
ward (as one might expect since weakest preconditions were developed with Hoare
rules as an inspiration). The basic ‘precondition strengthening’ rule can be proved in
exactly the same easy way as before:
121
# let CORRECT_ASSIGN = prove
(‘!p f q. (p SUBSET (q o f)) ==> correct p (Assign f) q‘,
REWRITE_TAC[correct; assign]);;
but sequencing once again needs monotonicity, at least for the first command:
The only real challenge is the While construct. The usual Hoare rule for partial
correctness is based on the existence of a ‘loop invariant’, a state predicate that is
preserved by each iteration. And to ensure termination, we need to guarantee that
the sequence of states reached after each execution is limited, which we can do by
postulating a wellfounded ordering for them. Thus we aim to prove the following:
assume a wellfounded order , a loop guard e and a monotonic command c. Suppose
also that there is an invariant that (i) is implied by the precondition p and (ii) together
with the failure of the loop guard e, implies the postcondition. Then the loop satisfies
the corresponding total correctness condition provided that in one iteration of the loop
(where we can assume that e also holds at the beginning) preserves the invariant and
makes the state decrease in the wellfounded ordering. Note that the invariance clause
uses an extra variable X to record the ‘previous’ value of the state for comparison at the
end of the loop body.
# g ‘!(<<) p c q e invariant.
monotonic c /\
WF(<<) /\
p SUBSET invariant /\
(UNIV DIFF e) INTER invariant SUBSET q /\
(!X:S. correct (invariant INTER e INTER (\s. X = s)) c
(invariant INTER (\s. s << X)))
==> correct p (While e c) q‘;;
122
# e(REWRITE_TAC[correct; SUBSET; IN_INTER; IN_UNIV; IN_DIFF; IN] THEN
REPEAT GEN_TAC THEN STRIP_TAC);;
val it : goalstack = 1 subgoal (1 total)
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!X x.
invariant x /\ e x /\ X = x ==> c (invariant INTER (\s. s << X)) x‘]
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!X x.
invariant x /\ e x /\ X = x ==> c (invariant INTER (\s. s << X)) x‘]
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!X x.
invariant x /\ e x /\ X = x ==> c (invariant INTER (\s. s << X)) x‘]
5 [‘!y. y << s ==> invariant y ==> While e c q y‘]
6 [‘invariant s‘]
‘While e c q s‘
We proceed by applying the fixpoint property of the while loop once; note that as
claimed we use nothing else about the while loop in this proof. We then eliminate the
defined notions of conditional and sequencing:
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!X x.
invariant x /\ e x /\ X = x ==> c (invariant INTER (\s. s << X)) x‘]
5 [‘!y. y << s ==> invariant y ==> While e c q y‘]
6 [‘invariant s‘]
123
We now proceed by a case analysis on the tested expression; one case is a trivial
consequence of the assumption [3]:
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!X x.
invariant x /\ e x /\ X = x ==> c (invariant INTER (\s. s << X)) x‘]
5 [‘!y. y << s ==> invariant y ==> While e c q y‘]
6 [‘invariant s‘]
7 [‘e s‘]
‘c (While e c q) s‘
We want to appeal to the core property of each loop iteration, applying it to the
initial state s:
0 [‘monotonic c‘]
1 [‘WF (<<)‘]
2 [‘!x. p x ==> invariant x‘]
3 [‘!x. ˜e x /\ invariant x ==> q x‘]
4 [‘!y. y << s ==> invariant y ==> While e c q y‘]
5 [‘invariant s‘]
6 [‘e s‘]
0 [‘WF (<<)‘]
1 [‘!x. p x ==> invariant x‘]
2 [‘!x. ˜e x /\ invariant x ==> q x‘]
3 [‘!y. y << s ==> invariant y ==> While e c q y‘]
4 [‘invariant s‘]
5 [‘e s‘]
124
# let CORRECT_WHILE = prove
(‘!(<<) p c q e invariant.
monotonic c /\
WF(<<) /\
p SUBSET invariant /\
(UNIV DIFF e) INTER invariant SUBSET q /\
(!X:S. correct (invariant INTER e INTER (\s. X = s)) c
(invariant INTER (\s. s << X)))
==> correct p (While e c) q‘,
REWRITE_TAC[correct; SUBSET; IN_INTER; IN_UNIV; IN_DIFF; IN] THEN
REPEAT GEN_TAC THEN STRIP_TAC THEN
SUBGOAL_THEN ‘!s:S. invariant s ==> While e c q s‘ MP_TAC THENL
[ALL_TAC; ASM_MESON_TAC[]] THEN
FIRST_ASSUM(MATCH_MP_TAC o REWRITE_RULE[WF_IND]) THEN
X_GEN_TAC ‘s:S‘ THEN REPEAT DISCH_TAC THEN
FIRST_ASSUM(fun th -> ONCE_REWRITE_TAC[MATCH_MP WHILE_FIX th]) THEN
REWRITE_TAC[if_def; sequence; o_THM; IN_ELIM_THM] THEN
COND_CASES_TAC THENL [ALL_TAC; ASM_MESON_TAC[]] THEN
FIRST_X_ASSUM(MP_TAC o SPECL [‘s:S‘; ‘s:S‘]) THEN ASM_REWRITE_TAC[] THEN
FIRST_X_ASSUM(MP_TAC o GEN_REWRITE_RULE I [monotonic]) THEN
REWRITE_TAC[SUBSET; IN; RIGHT_IMP_FORALL_THM] THEN
DISCH_THEN MATCH_MP_TAC THEN ASM_SIMP_TAC[INTER; IN_ELIM_THM; IN]);;
Now we can modify the basic correctness theorems to take the additional informa-
tion from the annotations:
125
# let CORRECT_SEQ_VC = prove
(‘!p q r c1 c2.
monotonic c1 /\ correct p c1 r /\ correct r c2 q
==> correct p (c1 ;; assert r ;; c2) q‘,
REWRITE_TAC[correct; sequence; monotonic; assert_def; o_THM] THEN SET_TAC[]);;
then we can arrive at such a tactic by repeatedly applying ‘composition’ theorems back-
wards and then appealing to monotonicity for the basic commands:
# let MONO_TAC =
REPEAT(MATCH_MP_TAC MONOTONIC_WHILE ORELSE
(MAP_FIRST MATCH_MP_TAC
[MONOTONIC_SEQ; MONOTONIC_IF; MONOTONIC_ITE] THEN CONJ_TAC)) THEN
MAP_FIRST MATCH_ACCEPT_TAC
[MONOTONIC_ASSIGN; MONOTONIC_ASSERT; MONOTONIC_VARIANT];;
Now, to apply the correctness rules, we just backchain through the various correct-
ness theorems, sometimes breaking apart and processing the resulting subgoals ready
for the next step. If the program is completely annotated, the resulting subgoals will be
free of programming constructs, consisting of purely mathematical assertions. These
are usually called verification conditions, because their truth is a sufficient condition
for the overall correctness goal.
# let VC_TAC =
FIRST
[MATCH_MP_TAC CORRECT_SEQ_VC THEN CONJ_TAC THENL [MONO_TAC; CONJ_TAC];
MATCH_MP_TAC CORRECT_ITE THEN CONJ_TAC;
MATCH_MP_TAC CORRECT_IF THEN CONJ_TAC;
MATCH_MP_TAC CORRECT_WHILE_VC THEN REPEAT CONJ_TAC THENL
[MONO_TAC; TRY(MATCH_ACCEPT_TAC WF_MEASURE); ALL_TAC; ALL_TAC;
REWRITE_TAC[FORALL_PAIR_THM; MEASURE] THEN REPEAT GEN_TAC];
MATCH_MP_TAC CORRECT_ASSIGN];;
126
As a concrete example, we will consider a simple program for computing gcds
(greatest common divisors). First we load in some basic results about gcds:
# needs "Library/prime.ml";;
x = m, y = n;
while (!(x == 0 || y == 0))
{ if (x < y) y = y - x;
else x = x - y;
}
if (x == 0) x = y;
If we wrap this in the lambda abstractions and add assertions as needed, we come
up with the following for the correctness claim:
# g ‘correct
(\(m,n,x,y). T)
(Assign (\(m,n,x,y). m,n,m,n) ;; // x,y := m,n
assert (\(m,n,x,y). x = m /\ y = n) ;;
While (\(m,n,x,y). ˜(x = 0 \/ y = 0))
(assert (\(m,n,x,y). gcd(x,y) = gcd(m,n)) ;;
variant(MEASURE(\(m,n,x,y). x + y)) ;;
Ite (\(m,n,x,y). x < y)
(Assign (\(m,n,x,y). m,n,x,y - x))
(Assign (\(m,n,x,y). m,n,x - y,y))) ;;
assert (\(m,n,x,y). (x = 0 \/ y = 0) /\ gcd(x,y) = gcd(m,n)) ;;
If (\(m,n,x,y). x = 0) (Assign (\(m,n,x,y). (m,n,y,y))))
(\(m,n,x,y). gcd(m,n) = x)‘;;
# e(REPEAT VC_TAC);;
However, they become more readable if we expand out the state variables and per-
form some beta-reduction and simplification, so let’s back up and apply this to all the
VCs:
127
# b();;
# e(REPEAT VC_TAC THEN REWRITE_TAC[SUBSET; FORALL_PAIR_THM] THEN
MAP_EVERY X_GEN_TAC [‘m:num‘; ‘n:num‘; ‘x:num‘; ‘y:num‘] THEN
REWRITE_TAC[IN; INTER; UNIV; DIFF; o_DEF; IN_ELIM_THM; PAIR_EQ] THEN
CONV_TAC(TOP_DEPTH_CONV GEN_BETA_CONV) THEN SIMP_TAC[]);;
val it : goalstack = 4 subgoals (4 total)
As expected, these VCs are purely arithmetical assertions. The first two are show-
ing that the loop invariant and variant properties hold for each arm of the nested con-
ditional. The next two originate in the final step. We can simplify the first VC using a
basic lemma about gcds:
# e(SIMP_TAC[GCD_SUB; LT_IMP_LE]);;
val it : goalstack = 1 subgoal (4 total)
# e ARITH_TAC;;
val it : goalstack = 1 subgoal (3 total)
128
# e(SIMP_TAC[GCD_SUB; NOT_LT] THEN ARITH_TAC);;
val it : goalstack = 1 subgoal (2 total)
# e(MESON_TAC[GCD_0]);;
...
val it : goalstack = 1 subgoal (1 total)
and this is similarly easy if we throw in symmetry, so the correctness of our program
has been proved.
# e(MESON_TAC[GCD_0; GCD_SYM]);;
...
val it : goalstack = No subgoals
17.5 Refinement
Verification condition generation is, in a sense, the ultimate in ‘post hoc’ verification,
where during the verification process the program itself is forgotten (at least in a for-
mal sense), leaving only its verification conditions behind. The opposite approach is to
integrate the process of proof with the original development of the program. One in-
fluential approach to this is ‘stepwise refinement’, and this is amenable to a convenient
formalization in the framework of weakest precondition semantics. Roughly speaking,
we say that a program c2 refines another program c1 (often written c1 v c2 ) if any
precondition-postcondition behaviour guaranteed by c1 is also guaranteed by c2 (Back
1980), or more precisely, that for a given postcondition the allowable preconditions for
c2 include at least those of c1 :
# parse_as_infix("refines",(12,"right"));;
129
To be useful in program development, we need two other properties. Most obvi-
ously, we want to show that refinement preserves correctness:
# let REFINES_CORRECT = prove
(‘correct p c1 q /\ c2 refines c1 ==> correct p c2 q‘,
REWRITE_TAC[correct; refines] THEN MESON_TAC[SUBSET_TRANS]);;
130
18 Number theory
So far our use of the natural numbers has been limited to straightforward algebraic and
inequality reasoning, except for one use of the ‘odd’/‘even’ distinction. However, it’s
often useful to use more number-theoretic notions such as primality and divisibility. If
you load in two files from the Library directory:
needs "Library/prime.ml";;
needs "Library/pocklington.ml";;
you will get some definitions of such notions, as well as a reasonable suite of back-
ground lemmas and evaluation conversions. Let us first run through the main defini-
tions. We say that ‘m divides n’, and write m|n, if n is an integer multiple of m. In
HOL there is an infix predicate divides defined by:
# divides;;
val it : thm = |- !b a. a divides b <=> (?x. b = a * x)
Two numbers are said to be coprime if they have no common factor besides 1:
# coprime;;
val it : thm =
|- !a b. coprime (a,b) <=> (!d. d divides a /\ d divides b ==> d = 1)
And a number is prime if it is not 1 and its only divisors are 1 and itself:
# prime;;
val it : thm =
|- !p. prime p <=> ˜(p = 1) /\ (!x. x divides p ==> x = 1 \/ x = p)
A number of key lemmas are proved, for example that if a prime number divides a
product it must divide one of the factors:
# PRIME_DIVPROD;;
val it : thm =
|- !p a b. prime p /\ p divides a * b ==> p divides a \/ p divides b
A theorem with the opposite flavor is that if two coprime numbers divide a third,
so does their product:
# DIVIDES_MUL;;
val it : thm =
|- !m n r. m divides r /\ n divides r /\ coprime (m,n) ==> m * n divides r
131
In the case of the prime predicate, HOL needs to factor related numbers to con-
struct the primality proof. (Essentially, to prove p is prime it needs to factor p − 1,
recursively proving that all those factors are prime.) For numbers > 225 , HOL at-
tempts to call on the external PARI/GP system to do the factoring. (PARI/GP can be
freely downloaded from http://pari.math.u-bordeaux.fr/.) However, if
this is not installed, a naive algorithm is used instead. Users interested in substitut-
ing other factoring software for PARI/GP should see the later example of linking the
external tool Maxima into HOL.
n
For example, Fermat once conjectured that all numbers of the form 22 + 1 are
prime. We can put this to the test for reasonably small n; let us set up a derived
n
rule that takes the given n as a parameter and resolves the primality of 22 + 1. We
apply mk_small_numeral36 to convert the OCaml integer into the correspond-
x
ing HOL numeral term, and substitute it for x in the pattern prime(22 + 1). We
use RAND_CONV NUM_REDUCE_CONV to evaluate the operand to prime and then
PRIME_CONV to resolve the primality.
# let FERMAT_PRIME_CONV n =
let tm = subst [mk_small_numeral n,‘x:num‘] ‘prime(2 EXP (2 EXP x) + 1)‘ in
(RAND_CONV NUM_REDUCE_CONV THENC PRIME_CONV) tm;;
val ( FERMAT_PRIME_CONV ) : int -> thm = <fun>
We can confirm that while primality holds for n = 0, . . . , 4, it fails for the next
n
few numbers. (In fact, no n > 5 is known such that 22 + 1 is prime, and on fairly
naive heuristic grounds based on the known density of primes, you might expect the
total number to be finite. So maybe there actually aren’t any more.)
FERMAT_PRIME_CONV 0;;
proving that 3 is prime
val it : thm = |- prime (2 EXP (2 EXP 0) + 1) <=> T
# FERMAT_PRIME_CONV 1;;
proving that 5 is prime
val it : thm = |- prime (2 EXP (2 EXP 1) + 1) <=> T
# FERMAT_PRIME_CONV 2;;
proving that 17 is prime
val it : thm = |- prime (2 EXP (2 EXP 2) + 1) <=> T
# FERMAT_PRIME_CONV 3;;
proving that 257 is prime
val it : thm = |- prime (2 EXP (2 EXP 3) + 1) <=> T
# FERMAT_PRIME_CONV 4;;
proving that 65537 is prime
val it : thm = |- prime (2 EXP (2 EXP 4) + 1) <=> T
# FERMAT_PRIME_CONV 5;;
proving that 4294967297 is composite
val it : thm = |- prime (2 EXP (2 EXP 5) + 1) <=> F
# FERMAT_PRIME_CONV 6;;
proving that 18446744073709551617 is composite
val it : thm = |- prime (2 EXP (2 EXP 6) + 1) <=> F
# FERMAT_PRIME_CONV 7;;
proving that 340282366920938463463374607431768211457 is composite
val it : thm = |- prime (2 EXP (2 EXP 7) + 1) <=> F
# FERMAT_PRIME_CONV 8;;
proving that
115792089237316195423570985008687907853269984665640564039457584007913129639937
is composite
val it : thm = |- prime (2 EXP (2 EXP 8) + 1) <=> F
36 The simple mk numeral takes an element of OCaml’s unlimited-range integer type num.
132
18.1 Congruences
It’s very common in number theory to use the notation x ≡ y (mod n), meaning that
n|(x − y), i.e. that the difference x − y is exactly divisible by n. For example we have
familiar examples from the clock, 15 ≡ 3 (mod 12) and 18 ≡ 6 (mod 12), or more
exotically that 11 ≡ −1 (mod 12) and 1000 ≡ 4000 (mod 12). These notions are
defined in HOL in two parts. The basic ‘congruence’ notion itself is simply a syntactic
sugar for applying a third (relational) argument to the first two arguments:
# cong;;
val it : thm = |- !rel x y. (x == y) rel <=> rel x y
Then we have definitions for mod on the natural numbers and integers that give
the right relation (the underlying constants are different but the interface uses mod for
both. The one over integers is defined as one might expect:
# mod_int;;
val it : thm = |- !x y n. mod n x y <=> (?q. x - y = q * n)
But the one over natural numbers — which is what we’ll be using in the examples
that follow — is slightly more intricate because naive use of subtraction will hit the
‘cutoff’ problem:
# mod_nat;;
val it : thm = |- !x y n. mod n x y <=> (?q1 q2. x + n * q1 = y + n * q2)
This allows one to express congruences in something like the conventional notation.
And as usual, there is a conversion for evaluating congruences just involving particular
numerals:
One of the convenient features of the congruence relation is that it shares many of
the same properties that make equality so useful. For example it is reflexive, symmetric
and transitive:
# CONG_REFL;;
val it : thm = |- !x n. (x == x) (mod n)
# CONG_SYM;;
val it : thm = |- !x y n. (x == y) (mod n) <=> (y == x) (mod n)
# CONG_TRANS;;
val it : thm =
|- !x y z n. (x == y) (mod n) /\ (y == z) (mod n) ==> (x == z) (mod n)
133
# CONG_ADD;;
val it : thm =
|- !x x’ y y’.
(x == x’) (mod n) /\ (y == y’) (mod n)
==> (x + y == x’ + y’) (mod n)
# CONG_MULT;;
val it : thm =
|- !x x’ y y’.
(x == x’) (mod n) /\ (y == y’) (mod n)
==> (x * y == x’ * y’) (mod n)
# CONG_EXP;;
val it : thm = |- !n k x y. (x == y) (mod n) ==> (x EXP k == y EXP k) (mod n)
Most of the time, when reasoning about congruences one can work at the level of
these abstract properties without delving into the rather messy definition. For example,
suppose we want to prove that if n|x and n|y then x ≡ y (mod n). We can appeal to
the pre-proved theorem CONG_0 and the basic properties of congruences; the proof is
so easy that MESON can handle it easily with the right lemmas:
# CONG_0;;
val it : thm = |- !x n. (x == 0) (mod n) <=> n divides x
# let CONG_TRIVIAL = prove
(‘!x y. n divides x /\ n divides y ==> (x == y) (mod n)‘,
MESON_TAC[CONG_0; CONG_SYM; CONG_TRANS]);;
An example of a theorem that ‘unpacks’ the definition when we know the relative
order of the congruent numbers is:
# CONG_TO_1;;
val it : thm =
|- !a n. (a == 1) (mod n) <=> a = 0 /\ n = 1 \/ (?m. a = 1 + m * n)
# CONG_CHINESE;;
val it : thm =
|- coprime (a,b) /\ (x == y) (mod a) /\ (x == y) (mod b)
==> (x == y) (mod (a * b))
134
# let LITTLE_CHECK_CONV tm =
EQT_ELIM((RATOR_CONV(LAND_CONV NUM_EXP_CONV) THENC CONG_CONV) tm);;
val ( LITTLE_CHECK_CONV ) : term -> thm = <fun>
# LITTLE_CHECK_CONV ‘(9 EXP 8 == 9) (mod 3)‘;;
val it : thm = |- (9 EXP 8 == 9) (mod 3)
# LITTLE_CHECK_CONV ‘(9 EXP 3 == 9) (mod 3)‘;;
val it : thm = |- (9 EXP 3 == 9) (mod 3)
# LITTLE_CHECK_CONV ‘(10 EXP 7 == 10) (mod 7)‘;;
val it : thm = |- (10 EXP 7 == 10) (mod 7)
# LITTLE_CHECK_CONV ‘(2 EXP 7 == 2) (mod 7)‘;;
val it : thm = |- (2 EXP 7 == 2) (mod 7)
# LITTLE_CHECK_CONV ‘(777 EXP 13 == 777) (mod 13)‘;;
val it : thm = |- (777 EXP 13 == 777) (mod 13)
We need a bit more work to prove the general result. In fact, it is a direct conse-
quence of a more general result called the Fermat-Euler theorem, which is pre-proved
in the HOL primality theories. However, we will prove the result from first principles
since the proof is not too long and is quite instructive.
The key to our proof is the lemma that (a + 1)p ≡ ap + 1 (mod p), from which
the result follows by induction on a. And to prove the lemma, we expand (a + 1)p by
the binomial theorem and observe that all the coefficients kp except the first and the
last are divisible by p. To prove this, we start with a simplifying lemma about p|n!:
we break down the goal, apply induction and rewrite with induction clauses for the
factorial on the left and the ordering on the right:
0 [‘prime p‘]
1 [‘p divides FACT n <=> p <= n‘]
0 [‘prime p‘]
‘p divides 1 <=> p = 0‘
For the first goal, observe that p|1 implies p = 1, and so both sides are false because
neither 0 nor 1 is a prime number:
0 [‘prime p‘]
1 [‘p divides FACT n <=> p <= n‘]
For this goal, we use the key fact that for a prime, p|ab ⇔ p|a ∨ p|b to simplify the
goal.37 The simplification also uses the inductive hypothesis:
37 In fact, in more general ring structures, this property the usual definition of ‘prime’, with the idea of no
135
# e(ASM_SIMP_TAC[PRIME_DIVPROD_EQ]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘prime p‘]
1 [‘p divides FACT n <=> p <= n‘]
When putting the proof together, we can easily fold the simplification into the MESON
step without making it very hard:
0 [‘prime p‘]
1 [‘0 < n‘]
2 [‘n < p‘]
We can now once again use the p|ab ⇔ p|a ∨ p|b property, and apply our previous
lemma and a couple of other rewrites to simplify p − n + n = p etc.
0 [‘prime p‘]
1 [‘0 < n‘]
2 [‘n < p‘]
136
Using basic inequality reasoning, we can further simplify this using the n < p
hypothesis:
0 [‘prime p‘]
1 [‘0 < n‘]
2 [‘n < p‘]
and one more custom arithmetical lemma reduces the goal to triviality:
Another lemma we want is that if all elements of a sum are divisible by p (despite
the letter, we make no assumption here that p is a prime) so is the whole sum; the proof
is a fairly straightforward induction:
Now we come to the main lemma, that (a + b)p ≡ ap + bp (mod p) (we only need
the special case b = 1 but it doesn’t seem any harder to prove the general case).
We begin by breaking the goal down a bit and applying the binomial expansion to
(a + b)p :
0 [‘prime p‘]
137
# e(SUBGOAL_THEN ‘1 <= p /\ 0 < p‘ STRIP_ASSUME_TAC THENL
[FIRST_ASSUM(MP_TAC o MATCH_MP PRIME_IMP_NZ) THEN ARITH_TAC; ALL_TAC]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
This is looking a bit better. But since we’re dealing with congruence and not equal-
ity, we need to carefully massage it into a form we can handle with pro-proved the-
orems. First, we massage the right-hand side with a careful rewriting (although it’s
normally used for the LHS of binary operators, LAND_CONV will apply to the right-
hand side of the congruence here because of the additional modulus part):
# e(GEN_REWRITE_TAC LAND_CONV [ARITH_RULE ‘a + b = (b + 0) + a‘]);;
val it : goalstack = 1 subgoal (1 total)
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
Now we can match with the pre-proved congruence properties and so eliminate the
common terms:
138
# e(REPEAT(MATCH_MP_TAC CONG_ADD THEN REWRITE_TAC[CONG_REFL]));;
val it : goalstack = 1 subgoal (1 total)
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
Now we just need to rewrite x ≡ 0 (mod p) into p|x and apply the previous
lemma:
0 [‘prime p‘]
1 [‘1 <= p‘]
2 [‘0 < p‘]
This is now fairly trivialby stringing together some basic lemmas about divisibility
and our lemma that p| kp :
Now we can prove the main result. We just massage the goal, apply induction on
a, prove the base case 0p ≡ 0 (mod p) via an accumulation of trivialities, and use our
key lemma above to dispose of the step case:
139
18.3 RSA encryption
For most of the long history of encryption, it was implicitly assumed that coded com-
munication between two parties is only really possible if they share some sort of ‘key’
that no other parties have access to. (Say, a particular offset for scrambling letters, or
a book on which to base a 1-time pad cipher.) However, quite recently several ‘public
key’ or ‘asymmetric’ methods have been developed that do not depend on any such
assumption. The best-known and probably most widely used is the RSA algorithm,
named after the initials of those who were first to publish it (Rivest, Shamir, and Adel-
man 1978).38 The key idea is that each party has a ‘public key’ that is widely known (it
might, for example, be published in a directory) and a ‘private key’ that only they know
(or their computer knows). A party A can send a message to B coded using only B’s
public key. To decode it, knowledge of B’s private key is required, and so it cannot be
understood by other parties, including A if they manage to lose the original message.
In RSA, the transformations are based on a variant of Fermat’s Little Theorem, and we
will prove that variant first before explaining the magic behind RSA.
The first observation is that provided a and p are coprime (which since p is a prime
means that a is not divisible by p), we can cancel a from both sides of ap ≡ a (mod p)
to obtain ap−1 ≡ 1 (mod p), by virtue of the following lemma:
# CONG_MULT_LCANCEL;;
val it : thm =
|- !a n x y. coprime (a,n) /\ (a * x == a * y) (mod n) ==> (x == y) (mod n)
38 The general idea of public-key encryption was apparently first invented by James Ellis, and the RSA
algorithm was discovered well before the RSA trio by Clifford Cocks. However, since both of these worked
for GCHQ, a secret Government agency, this remained unknown for many years.
140
Now for RSA. We can assume that the messages we wish to encode are just num-
bers of a limited size, since any message can be broken into blocks and encoded nu-
merically. To generate private and public keys, a party chooses two large distinct prime
numbers p and q and numbers d and e such that de ≡ 1 (mod (p − 1)(q − 1)). (We
will not consider the pragmatics about how these are generated, but it is not compu-
tationally difficult.) The encoding operation takes a plaintext message x and maps it
to y = xe mod N , where N = pq. The corresponding decoding operation is just
x = y d mod N . The public key consists of N and e — all that is needed to encode
it — and the private key consists of N and d. We will leave aside the detailed secu-
rity issues, which depends on the difficulty of recovering p and q given only N , and
just confirm that the encoding and decoding operations are indeed mutually inverse,
provided x < N .
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘(d * e == 1) (mod ((p - 1) * (q - 1)))‘]
4 [‘plaintext < p * q‘]
5 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
and then reduce the goal to a corresponding congruence relation to replace the applica-
tion of the modulus function:
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘(d * e == 1) (mod ((p - 1) * (q - 1)))‘]
4 [‘plaintext < p * q‘]
5 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
We swap e · d into d · e and match an earlier lemma against the congruence in the
assumption, doing a case analysis over the disjunction in the conclusion:
141
# e(FIRST_X_ASSUM(DISJ_CASES_TAC o GEN_REWRITE_RULE I [CONG_TO_1]));;
val it : goalstack = 2 subgoals (2 total)
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘plaintext < p * q‘]
4 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
5 [‘?m. d * e = 1 + m * (p - 1) * (q - 1)‘]
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘plaintext < p * q‘]
4 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
5 [‘d * e = 0 /\ (p - 1) * (q - 1) = 1‘]
It’s quite easy to handle the first case, since (p − 1)(q − 1) = 1 implies p = 2 and
q = 2, contradicting the hypothesis p 6= q:
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘plaintext < p * q‘]
4 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
5 [‘?m. d * e = 1 + m * (p - 1) * (q - 1)‘]
We can apply the Chinese Remainder Theorem to break this into separate congru-
ences modulo p and q; since p and q are distinct primes they are coprime, by a lemma
DISTINCT_PRIME_COPRIME:
0 [‘prime p‘]
1 [‘prime q‘]
2 [‘˜(p = q)‘]
3 [‘plaintext < p * q‘]
4 [‘ciphertext = plaintext EXP e MOD (p * q)‘]
5 [‘?m. d * e = 1 + m * (p - 1) * (q - 1)‘]
142
let RSA = prove
(‘prime p /\ prime q /\ ˜(p = q) /\
(d * e == 1) (mod ((p - 1) * (q - 1))) /\
plaintext < p * q /\ (ciphertext = (plaintext EXP e) MOD (p * q))
==> (plaintext = (ciphertext EXP d) MOD (p * q))‘,
REPEAT STRIP_TAC THEN ASM_REWRITE_TAC[] THEN
ASM_SIMP_TAC[MOD_EXP_MOD; MULT_EQ_0; PRIME_IMP_NZ; EXP_EXP] THEN
SUBGOAL_THEN ‘(plaintext == plaintext EXP (e * d)) (mod (p * q))‘ MP_TAC THENL
[ALL_TAC; ASM_SIMP_TAC[CONG; MULT_EQ_0; PRIME_IMP_NZ; MOD_LT]] THEN
GEN_REWRITE_TAC (LAND_CONV o ONCE_DEPTH_CONV) [MULT_SYM] THEN
FIRST_X_ASSUM(DISJ_CASES_TAC o GEN_REWRITE_RULE I [CONG_TO_1]) THENL
[ASM_MESON_TAC[MULT_EQ_1; ARITH_RULE ‘p - 1 = 1 <=> p = 2‘]; ALL_TAC] THEN
MATCH_MP_TAC CONG_CHINESE THEN ASM_SIMP_TAC[DISTINCT_PRIME_COPRIME] THEN
ASM_MESON_TAC[FERMAT_LITTLE_VARIANT; MULT_AC; CONG_SYM]);;
19 Real analysis
HOL’s core system includes a basic theory of real numbers with the basic algebraic
operations and summations, but doesn’t define square roots or transcendental functions,
and nor does it prove any deeper analytical properties. For that, you need to load in a
couple of library files. We will show a couple of examples of how these theories can
be used.
# needs "Library/analysis.ml";;
# needs "Library/transc.ml";;
T0 (x) = 1
T1 (x) = x
T2 (x) = 2x2 − 1
T3 (x) = 4x3 − 3x
T4 (x) = 8x4 − 8x2 + 1
T5 (x) = 16x5 − 20x3 + 5x
···
The simplest approach to formalizing them in HOL is to define them by the follow-
ing recurrence relation, and then show that the key property holds. (Strictly speaking,
we are defining the polynomial functions, not the polynomials themselves as syntactic
objects.)
143
# let cheb = define
‘(!x. cheb 0 x = &1) /\
(!x. cheb 1 x = x) /\
(!n x. cheb (n + 2) x = &2 * x * cheb (n + 1) x - cheb n x)‘;;
We can prove the things we want about the Chebyshev polynomials using well-
founded induction on n. It’s more convenient to package up the special case we’ll be
using, which naturally parallels the form of the definition. The proof simply proves the
stronger hypothesis ∀n. P (n) ∧ P (n + 1) by ordinary induction:
# MATCH_MP_TAC CHEB_INDUCT;;
val it : tactic = <fun>
# e it;;
val it : goalstack = 1 subgoal (1 total)
The base cases are easily eliminated by rewriting with basic results like 1 · x = x
and 0 · x = 0 as well as the cosine-specific theorem cos(0) = 1 and the defining
recurrence for the Chebyshev polynomials:
The two base cases have been solved, and the remaining case has been expanded
out. It’s now easier to strip down the goal to put the two hypotheses in the assumptions,
then rewrite with them:
144
We are now faced with proving this trigonometric identity. First we make the trivial
step of pushing the N → R injection down through addition and distributing x over the
sum:
‘&2 * cos x * cos (&n * x + x) - cos (&n * x) = cos (&n * x + &2 * x)‘
Let us (optimistically) just rewrite with the addition and double-angle formulas for
cosines, in the hope that this identity will be resolved. (Since the addition formula for
cosine introduces sines, we also need to throw the double-angle formula for sin into
the mix.):
‘&2 * cos x * (cos (&n * x) * cos x - sin (&n * x) * sin x) - cos (&n * x) =
cos (&n * x) * (cos x pow 2 - sin x pow 2) -
sin (&n * x) * &2 * sin x * cos x‘
# e(CONV_TAC REAL_RING);;
Exception: Failure "EQ_MP".
But of course sin(x) and cos(x) are not algebraically independent; we have sin(x)2 +
cos(x)2 = 1 (SIN_CIRCLE) and likewise for sin(nx) and cos(nx). The first of these
turns out to be all we need:
# SIN_CIRCLE;;
val it : thm = |- !x. sin x pow 2 + cos x pow 2 = &1
# e(MP_TAC(SPEC ‘x:real‘ SIN_CIRCLE) THEN CONV_TAC REAL_RING);;
2 basis elements and 1 critical pairs
3 basis elements and 0 critical pairs
Translating certificate to HOL inferences
val it : goalstack = No subgoals
145
If we imagine instantiating x to cos−1 (x) in that theorem, we see that Tn (x) =
cos(ncos−1 (x)), and hence, since −1 6 cos(y) 6 1 for any y, we also have −1 6
Tn (x) 6 1, at least if x itself is in the same range so that the key inverse property holds:
# ACS_COS;;
val it : thm = |- !y. -- &1 <= y /\ y <= &1 ==> cos (acs y) = y
We can therefore prove the key property quite easily with this and a few additional
facts. Note that while trigonometric functions were used as a convenient device in the
proof, neither our definition of the Chebyshev polynomials nor this result mentions
them.
let CHEB_RIPPLE = prove
(‘!x. abs(x) <= &1 ==> abs(cheb n x) <= &1‘,
REWRITE_TAC[GSYM REAL_BOUNDS_LE] THEN
MESON_TAC[CHEB_COS; ACS_COS; COS_BOUNDS]);;
The fact that Tn (x) stays in the range [−1, 1] if its argument does is quite im-
pressive given that the leading coefficient is 2n−1 . In fact, one can show that the
Chebyshev polynomials are unique in having this property, which gives them a num-
ber of important roles in finding approximations to functions. To take the simplest
example, the best (n − 1)-degree polynomial approximation to a degree-n polyno-
mial p(x) = a0 + a1 x + · · · + an xn is p(x) − a2nn Tn (x), in the ‘minimax’ sense
of having the smallest maximum error on the interval [−1, 1]. Repeatedly applying
such ‘economization’ typically gives much better minimax error than just truncating
the polynomial, often very close to the best possible approximation (Markstein 2000;
Muller 2006).
We can quite easily compute the Chebyshev polynomials explicitly by using the
recurrence. The following converts an integer into an expansion theorem 6 = 4 + 2,
100 = 98 + 2 etc.:
let NUM_ADD2_CONV =
let add_tm = ‘(+):num->num->num‘
and two_tm = ‘2‘ in
fun tm ->
let m = mk_numeral(dest_numeral tm -/ Int 2) in
let tm’ = mk_comb(mk_comb(add_tm,m),two_tm) in
SYM(NUM_ADD_CONV tm’);;
and the following keeps applying this and the basic recurrence, then uses REAL_POLY_CONV
to put the polynomial into a canonical format:
let CHEB_CONV =
let [pth0;pth1;pth2] = CONJUNCTS cheb in
let rec conv tm =
(GEN_REWRITE_CONV I [pth0; pth1] ORELSEC
(LAND_CONV NUM_ADD2_CONV THENC
GEN_REWRITE_CONV I [pth2] THENC
COMB2_CONV
(funpow 3 RAND_CONV ((LAND_CONV NUM_ADD_CONV) THENC conv))
conv THENC
REAL_POLY_CONV)) tm in
conv;;
for example:
146
# CHEB_CONV ‘cheb 8 x‘;;
val it : thm =
|- cheb 8 x =
&128 * x pow 8 +
-- &256 * x pow 6 +
&160 * x pow 4 +
-- &32 * x pow 2 +
&1
For large n this is very inefficient, because the recurrence only proceeds one num-
ber at a time, and the same subexpressions are recomputed many times. Just as you
wouldn’t actually want to compute the Fibonacci numbers by the defining recurrence
because fib n gets recalculated about 2n times:
# let rec fib2 n = if n <= 1 then (1,1) else let a,b = fib2(n-1) in b,a + b;;
val fib2 : int -> int * int = <fun>
# let fib n = fst(fib2 n);;
val fib : int -> int = <fun>
# fib 40;;
val it : int = 102334155
it would be preferable here to recursively produce theorems for the nth and (n + 1)st
Chebyshev polynomial at once. An even more refined approach would be to use a
binary recurrence, the proof of which we present without comment:
cheb])) THEN
CONV_TAC REAL_RING);;
This could be used as the basis for a considerably more efficient proof procedure,
though because of the factors of x − 1 and x2 − 1 in the recurrence, one would also
need to establish some equivalences for x = ±1 by explicit calculation. When the
reader feels sufficiently confident with HOL, this would be an instructive and non-
trivial exercise.
147
19.2 A trivial part of Sarkovskii’s theorem
We were perhaps stretching the definition of ‘analysis’ in calling the last theorem an-
alytical. Although analysis underlies some of the theorems about cosines, the proofs
we conducted on that basis were essentially algebraic. We might call a proof properly
analytical when it depends on ideas like limits and continuity. So let us prove a simple
theorem about continuous functions. Suppose f : R → R is continuous and there is
some point x such that f 3 (x) = x, where f 3 (x) means f (f (f (x))). We will show that
there is also a point x (not in general the same one) such that f (x) = x.
The HOL formalization of ‘f is continuous at the point x’ is f contl x with
contl (‘locally continuous’) an infix binary operator. The suitably expanded defi-
nition is the usual ‘epsilon-delta’ formulation that readers familiar with mathematical
analysis may recognize:39
# REWRITE_RULE[LIM; REAL_SUB_RZERO] contl;;
val it : thm =
|- !f x.
f contl x <=>
(!e. &0 < e
==> (?d. &0 < d /\
(!x’. &0 < abs x’ /\ abs x’ < d
==> abs (f (x + x’) - f x) < e)))
One of the most fundamental facts about continuous functions is the intermediate
value property: if a function takes two values then it also somewhere takes any value
in between. The HOL formulation is that if f is continuous at all points of an interval
[a, b], and f (a) 6 y 6 f (b) then there is some point x in the interval where f (x) = y.
# IVT;;
val it : thm =
|- !f a b y.
a <= b /\
(f a <= y /\ y <= f b) /\
(!x. a <= x /\ x <= b ==> f contl x)
==> (?x. a <= x /\ x <= b /\ f x = y)
We would prefer to ignore whether f (a) 6 f (b) or vice versa. In fact there is a
dual theorem proved where the reverse is assumed, IVT2. Our first step is to combine
them and specialize to the case y = 0 and continuity on the whole real line, using
REAL_LE_TOTAL to induce a case-split:
# let IVT_LEMMA1 = prove
(‘!f. (!x. f contl x)
==> !x y. f(x) <= &0 /\ &0 <= f(y) ==> ?x. f(x) = &0‘,
ASM_MESON_TAC[IVT; IVT2; REAL_LE_TOTAL]);;
‘!f. (!x. f contl x) /\ (?x. f x <= x) /\ (?y. y <= f y) ==> (?x. f x = x)‘
39 The hypothesis 0 < |x0 | is redundant because the conclusion trivially holds in this case anyway. It
148
exactly by applying the previous lemma to the function λx. f (x) − x:
If we rearrange some of the inequalities and the equation to bring terms to the other
side:
# e(ASM_MESON_TAC[]);;
0 inferences so far. Searching with maximum size 0.
0 inferences so far. Searching with maximum size 1.
1 inferences so far. Searching with maximum size 2.
2 inferences so far. Searching with maximum size 3.
Goal solved with 6 inferences.
CPU time (user): 0.
val it : goalstack = No subgoals
149
Now we are ready to tackle the main result:
The first step is to strip down the goal and apply the lemma:
Since we know f (f (f (x)) = x it’s not hard to show that each inequality is satisfied
by at least one of the three points in {x, f (x), f (f (x))}, so let’s direct HOL’s search in
that direction:
# e(CONJ_TAC THEN MATCH_MP_TAC
(MESON[] ‘P x \/ P (f x) \/ P (f(f x)) ==> ?x:real. P x‘));;
...
val it : goalstack = 2 subgoals (2 total)
Now it’s just linear arithmetic, but we do need to use the equational assumption:
The other subgoal falls the same way. So we can package the whole proof up as follows:
The name is chosen because this is a trivial subcase of a remarkably general result
called Sarkovskii’s theorem (Devaney 1987). For our purposes, there was nothing spe-
cial about the number 3, and essentially the same proof would work for f 2 (x) = x,
f 4 (x) = x etc. But a more refined notion is to study fixed points of minimal period
150
n, i.e. points such that f n (x) = x yet for no m < n is f m (x) = x. In this case the
number 3 is quite special: Sarkovskii’s theorem, as a special case of its more general
conclusion, shows that if there is a point with minimal period 3, there is a point with
minimal period n for every integer n 6 1.
19.3 Derivatives
Another quintessentially analytic property is the derivative of a function. One might
expect to find a higher-order ‘derivative’ function deriv : (R → R) → (R → R)
mapping a function f to its derivative f 0 . Actually, no such thing is defined in HOL,
but rather a ternary operator diffl where (f diffl a) x means ‘the function f
has a derivative with value a at the point x’. Expanding out the definition, we see that
the definition is much as an analyst might expect:
The usual kinds of combining theorems are proved, for example that the derivative
of a sum is the sum of the derivatives, and the chain rule for the derivative of a function
composition:
# DIFF_ADD;;
val it : thm =
|- !f g l m x.
(f diffl l) x /\ (g diffl m) x ==> ((\x. f x + g x) diffl l + m) x
# DIFF_CHAIN;;
val it : thm =
|- !f g l m x.
(f diffl l) (g x) /\ (g diffl m) x ==> ((\x. f (g x)) diffl l * m) x
By considering such theorems, we can see why a relational form was chosen. When
we say that the derivative of a sum is the sum of the derivatives, we are implicitly
assuming that the derivatives in question do actually exist. So in a rigorous statement
we need additional hypotheses that the functions in question are in fact differentiable.
By using the relational form, we avoid this, though the reformulations are not always as
natural as here. There is nothing to stop us defining a derivative, perhaps by an ε-term:
However the fact that the derivative in this sense is equal to something does not
imply that the function is actually differentiable, for even if it weren’t the derivative
would be equal to something. (This is exactly analogous to our use of HAS_SIZE
instead of simple equations based on CARD.)
There is really only one automated theorem proving routine for analytical prop-
erties. A conversion DIFF_CONV (not actually a true conversion) will automatically
151
return theorems about derivatives of particular expressions, using known derivatives
for various transcendental functions, e.g.
Note that the result is not reduced to its ‘simplest’ form. In general, the theorem
returned will include side-conditions that HOL thinks may be needed:
20 Embedding of logics
As with programming languages, so with other formal constructs including other log-
ics: we can formalize them inside HOL in ‘deep’ and ‘shallow’ styles. To illustrate
this, we will consider propositional modal logic.
What these phrases were actually supposed to mean by Lewis seems quite obscure
to most people, and it was only really clarified when Kripke (1963) gave the precise
semantics that we will shortly formalize in HOL. And one of the nice features is that
the semantics is naturally compatible with less philosophically abstruse and more ob-
viously useful readings of p such as ‘p holds at some time in the future’, ‘p holds
when the program terminates’ or ‘p is provable from the axioms of Peano Arithmetic’.
and then set up infix and prefix parsing status for the logical connectives:
152
# parse_as_infix("&&",(16,"right"));;
# parse_as_infix("||",(15,"right"));;
# parse_as_infix("-->",(14,"right"));;
# parse_as_infix("<->",(13,"right"));;
# parse_as_prefix "Not";;
# parse_as_prefix "Box";;
# parse_as_prefix "Diamond";;
The usual notion of a model for propositional logic is a mapping from variable
names (here strings) to the 2-element set of truth values. In modal logic this is gener-
alized by introducing a set of ‘worlds’ and making a truth-assignment dependent not
only on the variable name but also in the ‘world’, so that any given atomic formula
may be true in some worlds but false in others. Then, roughly speaking, p is defined
to hold if p holds in ‘all possible worlds’.
But there is an important nuance that prevents this from being simply a trivial vari-
ant of first-order quantification. More precisely, p is defined to hold if p holds in
‘all worlds accessible from the current one’, where the question of which worlds are
accessible from others is also part of the model. In attempting to formally capture
some important philosophical notions of possibility or necessity, we can appropriately
restrict the notion of accessibility. For example, one can argue that fewer things are
physically possible than logically possible. For the latter we might permit any world to
be accessible from any other, while for the former, limit accessibility between worlds
to those satisfying the same physical laws. The upshot of this discussion is that the
heavily parametrized definition of the semantics of modal logic is as follows. Here W
is a set of worlds, R is the accessibility relation between works, V is the valuation map-
ping that determines which atomic formulas hold in which world, and w is the current
world. The pair (W, R) taken together is usually referred to as a frame.
153
We say that a formula holds in a frame if it holds in that frame for any valuation
and in any world:
# parse_as_infix("|=",(11,"right"));;
Numerous modal logics have been studied over the years. Three contrasting ones
are the following. Modal logic S4, felt by many philosophers to formalize the tradi-
tional philosophical notions of necessity and possibility, allows all frames where the
accessibility relation is reflexive and transitive. (And the set of worlds is nonempty and
accessibility is a relation on the set of worlds — this will be assumed in the other logics
without comment.)
# let S4 = new_definition
‘S4(W,R) <=> ˜(W = {}) /\ (!x y. R x y ==> x IN W /\ y IN W) /\
(!x. x IN W ==> R x x) /\
(!x y z. R x y /\ R y z ==> R x z)‘;;
Linear temporal logic (LTL) is interpreted over a very specific model, where the
worlds are the natural numbers, thought of as units of time, and a world is accessible
precisely from earlier ones, including itself. (There are other formulations where we
assume an arbitrary structure isomorphic to that one, and there are variants with past
times based on Z and continuous time based on R, but this is what we will mean by
LTL.)
Finally, the logic GL is restricted to accessibility relations that are transitive and
also converse well-founded. (And so in particular irreflexive — no world is accessible
from itself.) This has a quite different character from the modal logics traditionally
studied by philosophers, but has a particular importance since it formalizes the logic of
provability (Boolos 1995). We will expand a little on this below.
# let GL = new_definition
‘GL(W,R) <=> ˜(W = {}) /\ (!x y. R x y ==> x IN W /\ y IN W) /\
WF(\x y. R y x) /\ (!x y z:num. R x y /\ R y z ==> R x z)‘;;
154
is simply to expand all the definitions:
‘!p1 p2.
L (p1,p2)
==> (!V w.
w IN p1
==> (!w’. w’ IN p1 /\ p2 w w’
==> holds (p1,p2) V A w’
==> holds (p1,p2) V B w’)
==> (!w’. w’ IN p1 /\ p2 w w’ ==> holds (p1,p2) V A w’)
==> (!w’. w’ IN p1 /\ p2 w w’ ==> holds (p1,p2) V B w’))‘
# let MODAL_TAC =
REWRITE_TAC[valid; FORALL_PAIR_THM; holds_in; holds] THEN MESON_TAC[];;
and it disposes of many general laws of modal logic without much difficulty:
However, many other properties hold only in certain modal logics. In fact, an
appealing feature is that many formulas of modal logic precisely characterize a certain
restriction on the class of frames. For example, we will see shortly that A ⇒ A
holds precisely in logics where the accessibility relation is assumed to be transitive (as
it is in the three examples S4, LTL and GL above), while A ⇒ A holds precisely in
logics with a reflexive accessibility relation (like S4 and LTL but not GL).
In order to prove a number of results like this in a uniform style, it’s convenient
to prove a general lemma that allows us to eliminate the semantics from formulas of a
certain kind. This lemma asserts that some property P holds of holds (W,R) V A
for all V and A if and only if P simply holds for all predicates on worlds:
If we split the problem into two implications, the right-to-left one is trivial since
the left is just a special case of the right, and we are left with the other direction:
‘P p‘
155
The idea is that we can in particular apply the hypothesis to an atomic formula A
where the valuation is chosen to be agree with predicate p on all atomic formulas:
We can’t quite rewrite with the definition of holds because in this goal it is not
applied to all its parameters. We need to manually introduce the extra parameter:
# e(REWRITE_TAC[holds]);;
val it : goalstack = 1 subgoal (1 total)
‘P (\x. p x) ==> P p‘
# e(REWRITE_TAC[ETA_AX]);;
val it : goalstack = No subgoals
Now let us try applying this to a specific modal schema equivalence, the one for
reflexivity of the accessibility relation that we mentioned above:
# g ‘!W R. (!w:W. w IN W ==> R w w) <=> !A. holds_in (W,R) (Box A --> A)‘;;
# e(REWRITE_TAC[holds_in; holds]);;
val it : goalstack = 1 subgoal (1 total)
‘!W R.
(!w. w IN W ==> R w w) <=>
(!A V w.
w IN W
==> (!w’. w’ IN W /\ R w w’ ==> holds (W,R) V A w’)
==> holds (W,R) V A w)‘
156
However, just rewriting with HOLDS_FORALL_LEMMA won’t work, because HOL’s
limited higher-order matching doesn’t figure out all the instantiations; the trouble is the
two free variables W and R in the pattern. However, we can carefully force them to
match exactly the variables in the goal:
The remaining goal is free of formulas and semantics, but it does involve some
non-trivial higher-order logic. So at first sight we might not expect to solve it with
MESON, but we are lucky:
# e(MESON_TAC[]);;
...
val it : goalstack = No subgoals
# let MODAL_SCHEMA_TAC =
REWRITE_TAC[holds_in; holds] THEN MP_TAC HOLDS_FORALL_LEMMA THEN
REPEAT(MATCH_MP_TAC MONO_FORALL THEN GEN_TAC) THEN
DISCH_THEN(fun th -> REWRITE_TAC[th]);;
157
Our luck finally runs out with the schema for symmetry, where MESON won’t finish
things off for us.
# e MODAL_SCHEMA_TAC;;
val it : goalstack = 1 subgoal (1 total)
0 [‘!p w.
w IN W
==> p w
==> (!w’. w’ IN W /\ R w w’
==> (?w’’. w’’ IN W /\ R w’ w’’ /\ p w’’))‘]
1 [‘w IN W‘]
2 [‘w’ IN W‘]
3 [‘R w w’‘]
‘R w’ w‘
We need to come up with the right instantiation for the predicate p, and then it’s easy:
Still harder is the following modal schema proof, where the validity of (A ⇒
A) ⇒ A is equivalent to transitivity and converse wellfoundedness of the accessibil-
ity relation, as assumed in the logic GL. Perhaps a few more words about the logic of
provability would help to motivate the interest of this schema.
In proving limitative results such as Gödel’s incompleteness theorems — see for
example Smullyan (1992) for an introduction — one defines inside a logic a concrete
representation ppq, say as a number, for each of its formulas p, and defines within
the logic a predicate P r where the truth of P r(ppq) means that p is provable in the
logic. By rather complicated syntactic arguments one can establish some interesting
158
properties of this notion, such as the fact that if ` p, then ` P rov(ppq), i.e. that if p
is provable, then it’s provable that it’s provable(!) One can take this still further and
prove
` (⊥ ⇒ ⊥) ⇒ ⊥
in fact, there is a generalization due to Löb with an arbitrary formula p replacing falsity:
` (p ⇒ p) ⇒ p
It is precisely this schema that characterizes provability logic. By virtue of a diffi-
cult completeness theorem (Solovay 1976), any properties of provability expressible in
these modal terms can also be proved by directly reasoning in GL.
We will content ourselves with a much simpler task, establishing the equivalence of
the Löb schema with the transitivity and converse wellfoundedness of the frames. By
the earlier result on transitivity, it follows that any logic in which (A ⇒ A) ⇒ A
holds also has p ⇒ p, as was mentioned earlier for provability. In purely modal
terms, one can see this by setting A to p ∧ p followed by some transformations that
hold in any modal logic; this clever trick is indeed the motivation for an otherwise
obscure instantiation below. Let’s set about the proof:
starting in the usual way, expressing wellfoundedness as induction, and splitting the
problem into three subgoals:
...
‘!p w.
w IN W
==> (!w’. w’ IN W /\ R w w’
==> (!w’’. w’’ IN W /\ R w’ w’’ ==> p w’’)
==> p w’)
==> (!w’. w’ IN W /\ R w w’ ==> p w’)‘
All these goals require considerable thought (for most of us). Still, the first is the
easiest of the three because we can more or less follow our noses. We break down the
goal a bit:
159
# e(REPEAT GEN_TAC THEN REPEAT DISCH_TAC);;
val it : goalstack = 1 subgoal (3 total)
# e(FIRST_X_ASSUM MATCH_MP_TAC);;
val it : goalstack = 1 subgoal (3 total)
# e(ASM_MESON_TAC[]);;
...
val it : goalstack = 1 subgoal (2 total)
0 [‘!p w.
w IN W
==> (!w’. w’ IN W /\ R w w’
==> (!w’’. w’’ IN W /\ R w’ w’’ ==> p w’’)
==> p w’)
==> (!w’. w’ IN W /\ R w w’ ==> p w’)‘]
‘!x y z. x IN W /\ y IN W /\ z IN W /\ R x y /\ R y z ==> R x z‘
This is where we have to prove transitivity from the semantics of the Löb schema.
The clever bit is finding the right instantiation, which is just the semantic translation of
the instantiation p ∧ p we mentioned above:
160
# e(X_GEN_TAC ‘w:W‘ THEN FIRST_X_ASSUM(MP_TAC o SPECL
[‘\v:W. v IN W /\ R w v /\ !w’’. w’’ IN W /\ R v w’’ ==> R w w’’‘; ‘w:W‘]));;
val it : goalstack = 1 subgoal (2 total)
‘(w IN W
==> (!w’’. w’’ IN W /\ R w w’’
==> (!w’’’. w’’’ IN W /\ R w’’ w’’’
==> (\v. v IN W /\
R w v /\
(!w’’. w’’ IN W /\ R v w’’ ==> R w w’’))
w’’’)
==> (\v. v IN W /\
R w v /\
(!w’’. w’’ IN W /\ R v w’’ ==> R w w’’))
w’’)
==> (!w’’. w’’ IN W /\ R w w’’
==> (\v. v IN W /\
R w v /\
(!w’’. w’’ IN W /\ R v w’’ ==> R w w’’))
w’’))
==> (!y z. w IN W /\ y IN W /\ z IN W /\ R w y /\ R y z ==> R w z)‘
0 [‘!p w.
w IN W
==> (!w’. w’ IN W /\ R w w’
==> (!w’’. w’’ IN W /\ R w’ w’’ ==> p w’’)
==> p w’)
==> (!w’. w’ IN W /\ R w w’ ==> p w’)‘]
For the last goal, we again provide an appropriate instantiation for the predicate in
the assumptions:
# e(X_GEN_TAC ‘P:W->bool‘ THEN DISCH_TAC THEN
FIRST_X_ASSUM(MP_TAC o SPEC ‘\x:W. !w:W. x IN W /\ R w x ==> P x‘));;
val it : goalstack = 1 subgoal (1 total)
‘(!w. w IN W
==> (!w’. w’ IN W /\ R w w’
==> (!w’’. w’’ IN W /\ R w’ w’’
==> (\x. !w. x IN W /\ R w x ==> P x) w’’)
==> (\x. !w. x IN W /\ R w x ==> P x) w’)
==> (!w’. w’ IN W /\ R w w’ ==> (\x. !w. x IN W /\ R w x ==> P x) w’))
==> (!x. P x)‘
Given enough time, MESON should solve this, but the search space is narrowed
dramatically if we tell it to instantiate the first order variable in the antecedent in the
same way as in the consequent:
# e(MATCH_MP_TAC MONO_FORALL THEN ASM_MESON_TAC);;
val it : goalstack = No subgoals
We can package this up into the following script, where in a somewhat quixotic bid
to reduce the total number of lines, we separate out the common call to MESON instead
of repeating it for each subgoal:
161
let MODAL_WFTRANS = prove
(‘!W R. (!x y z:W. x IN W /\ y IN W /\ z IN W /\ R x y /\ R y z ==> R x z) /\
WF(\x y. x IN W /\ y IN W /\ R y x) <=>
(!A. holds_in (W,R) (Box(Box A --> A) --> Box A))‘,
MODAL_SCHEMA_TAC THEN REWRITE_TAC[WF_IND] THEN EQ_TAC THEN
STRIP_TAC THEN REPEAT CONJ_TAC THENL
[REPEAT GEN_TAC THEN REPEAT DISCH_TAC THEN FIRST_X_ASSUM MATCH_MP_TAC;
X_GEN_TAC ‘w:W‘ THEN FIRST_X_ASSUM(MP_TAC o SPECL
[‘\v:W. v IN W /\ R w v /\ !w’’. w’’ IN W /\ R v w’’ ==> R w w’’‘; ‘w:W‘]);
X_GEN_TAC ‘P:W->bool‘ THEN DISCH_TAC THEN
FIRST_X_ASSUM(MP_TAC o SPEC ‘\x:W. !w:W. x IN W /\ R w x ==> P x‘) THEN
MATCH_MP_TAC MONO_FORALL] THEN
ASM_MESON_TAC[]);;
For a more extensive example of deep embedding of a logic, you may like to look
in the subdirectory Arithmetic, which contains a formalization of first-order arith-
metic and the proof of some classic limitative theorems such as Tarski’s theorem on the
undefinability of truth and Gödel’s incompleteness theorem.
# parse_as_prefix "Not";;
# parse_as_infix("&&",(16,"right"));;
# parse_as_infix("||",(15,"right"));;
# parse_as_infix("-->",(14,"right"));;
# parse_as_infix("<->",(13,"right"));;
Note that these are mostly just special type instances of set operations; for example
&& is a special case of set intersection, INTER. They are all just a ‘lifting’ to predi-
cates of the HOL propositional connectives. Slightly more interesting are the modal
operators, which we may as well give names reflecting the particular model:
Indeed, since we’re in a shallow embedding, we can define more temporal operators
at will. Here are two common ones: ‘next p’ says that p holds in the next state:
162
# let next = define ‘next p = \t:num. p(t + 1)‘;;
while ‘p until q’ says that there is a time in the future where q holds and until
(possibly not including) that time, p holds:
# parse_as_infix("until",(17,"right"));;
163
21.1 Normalizing if-then-else expressions
Consider a simple symbolic algorithm that takes expressions built up from Boolean
conditional expressions like HOL’s ‘if . . . then . . . else’ construct and normalizes them
so that the tested expression is not itself a conditional:
The rendition in HOL is quite mechanical, but the recursive definition of norm is
not accepted:
In such situations, try to prove the existence of a suitable recursive function by exis-
tentially quantifying the definition and replacing define with prove_general_recursive_function_exists
This should justify the existence of the function, retaining any unproven conditions in
the hypotheses. This will give you an idea what HOL was unable to prove for itself: the
existence of some suitable wellfounded ordering, or the compatibility of the clauses, or
both:
164
# let eth = prove_general_recursive_function_exists
‘?norm. (norm False = False) /\
(norm True = True) /\
(!i. norm (Atomic i) = Atomic i) /\
(!y z. norm (Ite False y z) = norm z) /\
(!y z. norm (Ite True y z) = norm y) /\
(!i y z. norm (Ite (Atomic i) y z) =
Ite (Atomic i) (norm y) (norm z)) /\
(!u v w y z. norm (Ite (Ite u v w) y z) =
norm (Ite u (Ite v y z) (Ite w y z)))‘;;
val eth : thm =
?(<<). WF (<<) /\
(!y z. T ==> (!y’. y’ << z ==> y’ << Ite False y z)) /\
(!y z. T ==> (!y’. y’ << y ==> y’ << Ite True y z)) /\
(!i y z. T ==> y << Ite (Atomic i) y z) /\
(!i y z. T ==> z << Ite (Atomic i) y z) /\
(!u v w y z.
T
==> (!y’. y’ << Ite u (Ite v y z) (Ite w y z)
==> y’ << Ite (Ite u v w) y z))
|- ?norm. norm False = False /\
norm True = True /\
(!i. norm (Atomic i) = Atomic i) /\
(!y z. norm (Ite False y z) = norm z) /\
(!y z. norm (Ite True y z) = norm y) /\
(!i y z.
norm (Ite (Atomic i) y z) = Ite (Atomic i) (norm y) (norm z)) /\
(!u v w y z.
norm (Ite (Ite u v w) y z) =
norm (Ite u (Ite v y z) (Ite w y z)))
In this case, clause compatibility was not a problem, but we need to come up with
a wellfounded order with the three properties given in the hypothesis. Let’s set that
hypothesis as a goal and proceed to prove it:
# g(hd(hyp eth));;
val it : goalstack = 1 subgoal (1 total)
‘?(<<). WF (<<) /\
(!y z. T ==> (!y’. y’ << z ==> y’ << Ite False y z)) /\
(!y z. T ==> (!y’. y’ << y ==> y’ << Ite True y z)) /\
(!i y z. T ==> y << Ite (Atomic i) y z) /\
(!i y z. T ==> z << Ite (Atomic i) y z) /\
(!u v w y z.
T
==> (!y’. y’ << Ite u (Ite v y z) (Ite w y z)
==> y’ << Ite (Ite u v w) y z))‘
Coming up with the right ordering in such situations can be quite tricky, and of-
ten makes an enjoyable puzzle. Quite often (not always), one can arrive at a suitable
measure of the ‘size’ of the objects in question so that the required ordering is sim-
ply ‘decreasing size’. For this example, a suitable definition of size (due, I believe, to
Robert Shostak) is:
Since wellfounded orderings based on numeric size measures are so common, there
is a predefined constant MEASURE:
165
# MEASURE;;
val it : thm = |- !m. MEASURE m = (\x y. m x < m y)
and so:
# e(EXISTS_TAC ‘MEASURE sizeof‘);;
val it : goalstack = 1 subgoal (1 total)
We can simplify this using two useful theorems. First of all, every order defined
by a measure is wellfounded (because the usual ordering on N is). Second, the more
intricate quantified expression in the right clause just collapses to a non-strict ordering.
Otherwise, we just expand with the definition of MEASURE:40
# WF_MEASURE;;
val it : thm = |- !m. WF (MEASURE m)
# MEASURE_LE;;
val it : thm = |- (!y. MEASURE m y a ==> MEASURE m y b) <=> m a <= m b
# e(REWRITE_TAC[WF_MEASURE; MEASURE_LE; MEASURE]);;
val it : goalstack = 1 subgoal (1 total)
Now the expression is starting to look more comprehensible. Each clause is saying
that in the original definition, the left-hand-side is defined in terms of norm applied
to ‘smaller’ instances only.41 If we expand with the definition, this can be handled
automatically:
# e(REWRITE_TAC[sizeof]);;
val it : goalstack = 1 subgoal (1 total)
# e ARITH_TAC;;
val it : goalstack = No subgoals
40 Since the default rewriting is top-down, the first two theorems will get applied in preference to the
expansion of MEASURE.
41 We only need to prove ‘6’ rather than ‘<’ in the last line is because that clause is tail recursive.
166
Now we can get this as a theorem and eliminate the hypothesis from the original
existence assertion:
let eth’ =
let th = prove
(hd(hyp eth),
EXISTS_TAC ‘MEASURE sizeof‘ THEN
REWRITE_TAC[WF_MEASURE; MEASURE_LE; MEASURE; sizeof] THEN ARITH_TAC) in
PROVE_HYP th eth;;
Now that we have an existence assertion without any hypotheses, we can make
a definition using new_specification. This simply makes a regular definition
using an ε-term derived from the existence theorem:
# g ‘!P. P False /\
P True /\
(!i. P(Atomic i)) /\
(!y z. P z ==> P(Ite False y z)) /\
(!y z. P y ==> P(Ite True y z)) /\
(!i y z. P y /\ P z ==> P (Ite (Atomic i) y z)) /\
(!u v w x y z. P(Ite u (Ite v y z) (Ite w y z))
==> P(Ite (Ite u v w) y z))
==> !e. P e‘;;
Since this involves somewhat messy reasoning, it might be better if as part of the
machinery underlying define, HOL would prove it for itself. Indeed, in the work
that acted as the inspiration for define (Slind 1997), this is done. However, in the
general case, proving the induction theorem can be harder than proving admissibility
of the definition, or the obvious induction theorem can even be false. First, it is clear
that the induction principle must cover all cases in order to be sound, whereas the
clauses handed to define may leave several cases unspecified. Second, the distinction
between termination and admissibility is important, and it will appear in this example.
Anyway, our proof is going to proceed by wellfounded induction on the same ordering,
so we specialize the principle of wellfounded induction to this case:
167
# let SIZE_INDUCT = REWRITE_RULE[WF_IND; MEASURE] (ISPEC ‘sizeof‘ WF_MEASURE);;
val ( SIZE_INDUCT ) : thm =
|- !P. (!x. (!y. sizeof y < sizeof x ==> P y) ==> P x) ==> (!x. P x)
0 [‘P False‘]
1 [‘P True‘]
2 [‘!i. P (Atomic i)‘]
3 [‘!y z. P z ==> P (Ite False y z)‘]
4 [‘!y z. P y ==> P (Ite True y z)‘]
5 [‘!i y z. P y /\ P z ==> P (Ite (Atomic i) y z)‘]
6 [‘!u v w x y z.
P (Ite u (Ite v y z) (Ite w y z)) ==> P (Ite (Ite u v w) y z)‘]
0 [‘P False‘]
1 [‘P True‘]
2 [‘!i. P (Atomic i)‘]
3 [‘!y z. P z ==> P (Ite False y z)‘]
4 [‘!y z. P y ==> P (Ite True y z)‘]
5 [‘!i y z. P y /\ P z ==> P (Ite (Atomic i) y z)‘]
6 [‘!u v w x y z.
P (Ite u (Ite v y z) (Ite w y z)) ==> P (Ite (Ite u v w) y z)‘]
‘!a0 a1 a2.
((!y. sizeof y < sizeof a0 ==> P y) ==> P a0) /\
((!y. sizeof y < sizeof a1 ==> P y) ==> P a1) /\
((!y. sizeof y < sizeof a2 ==> P y) ==> P a2)
==> (!y. sizeof y < sizeof (Ite a0 a1 a2) ==> P y)
==> P (Ite a0 a1 a2)‘
The first few cases have been disposed of; we now need a further case analysis
over the conditional a0, which again we drive by induction. We need a bit of mess-
ing around to use the right theorems to break down the problem while ignoring the
inductive hypotheses that will be unused.
168
# e(MATCH_MP_TAC ite_INDUCT THEN POP_ASSUM_LIST
(fun ths -> REPEAT STRIP_TAC THEN FIRST(mapfilter MATCH_MP_TAC ths)) THEN
REPEAT CONJ_TAC THEN FIRST_X_ASSUM MATCH_MP_TAC THEN
POP_ASSUM_LIST(K ALL_TAC));;
val it : goalstack = 5 subgoals (5 total)
Things are now looking pretty similar to the proof obligations that arose in proving
admissibility of define: we need to prove that for each clause in the definition, the
right is smaller than the left. Most of these can be solved in the same way:
# e(REWRITE_TAC[sizeof] THEN ARITH_TAC);;
val it : goalstack = 1 subgoal (4 total)
But the last one fails. If we compare it with the proof obligation arising in the
admissibility of define, we see that a non-strict inequality has been replaced by a
strict one, and this makes the problem markedly harder. Let us expand the problem:
# e(REWRITE_TAC[sizeof; LEFT_ADD_DISTRIB; RIGHT_ADD_DISTRIB; MULT_CLAUSES]);;
val it : goalstack = 1 subgoal (1 total)
‘sizeof a0 +
(sizeof a0 * sizeof a1 +
sizeof a0 * sizeof a1 * sizeof a1’ +
sizeof a0 * sizeof a1 * sizeof a2’) +
sizeof a0 * sizeof a2 +
sizeof a0 * sizeof a2 * sizeof a1’ +
sizeof a0 * sizeof a2 * sizeof a2’ <
(sizeof a0 + sizeof a0 * sizeof a1 + sizeof a0 * sizeof a2) +
(sizeof a0 * sizeof a1’ +
(sizeof a0 * sizeof a1) * sizeof a1’ +
(sizeof a0 * sizeof a2) * sizeof a1’) +
sizeof a0 * sizeof a2’ +
(sizeof a0 * sizeof a1) * sizeof a2’ +
(sizeof a0 * sizeof a2) * sizeof a2’‘
169
We can simplify things a bit by normalizing the sum and product terms and can-
celling common terms from the left:
‘sizeof a0 * sizeof a2 +
sizeof a0 * sizeof a1 * sizeof a1’ +
sizeof a0 * sizeof a1 * sizeof a2’ +
sizeof a0 * sizeof a1’ * sizeof a2 +
sizeof a0 * sizeof a2 * sizeof a2’ +
sizeof a0 <
sizeof a0 * sizeof a1’ +
sizeof a0 * sizeof a2 +
sizeof a0 * sizeof a2’ +
sizeof a0 * sizeof a1 * sizeof a1’ +
sizeof a0 * sizeof a1 * sizeof a2’ +
sizeof a0 * sizeof a1’ * sizeof a2 +
sizeof a0 * sizeof a2 * sizeof a2’ +
sizeof a0‘
and reassociating and cancelling from the right gives a more significant simplification:
# e(REWRITE_TAC[ADD_ASSOC; LT_ADD_RCANCEL]);;
val it : goalstack = 1 subgoal (1 total)
We see now why ARITH_TAC was unable to prove the goal: it needs to know that
the product of two sizeof terms is positive. For the earlier non-strict inequality it
only needed to show that it was nonnegative, which holds automatically for the natural
numbers. Still, the lemma it needs is quite easy to prove by another structural induction:
170
let SIZEOF_INDUCT = REWRITE_RULE[WF_IND; MEASURE] (ISPEC‘sizeof‘ WF_MEASURE);;
We can now use this as a quite convenient tool in proving properties. The first
thing we might want to prove is that norm does indeed normalize, which we might
state formally as satisfying the following predicate:
We can also define a still more specialized induction scheme for the class of all
normalized expressions, which will reduce the number of cases in the antecedent:
171
21.3 A theorem prover
In the original source (Boyer and Moore 1979), the conditional normalizer is used as
a component in a tautology-checker for the restricted language, and we will follow
this procedure with a few minor changes. First we define the semantics of the little
embedded logic, which should present no difficulty after the more complex semantics
of modal logic in an earlier section; v is supposed to be a valuation of the atomic
propositions.
# g‘!e. normalized e
==> !f t. (!a. ˜(MEM a t /\ MEM a f))
==> (taut (t,f) e <=>
!v. (!a. MEM a t ==> v(a)) /\ (!a. MEM a f ==> ˜v(a))
==> holds v e)‘;;
172
# e(MATCH_MP_TAC NORMALIZED_INDUCT THEN REWRITE_TAC[holds; taut] THEN
REWRITE_TAC[NOT_FORALL_THM] THEN REPEAT CONJ_TAC);;
val it : goalstack = 3 subgoals (3 total)
...
...
‘!f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (?v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a))‘
For the first goal we need to show that for two disjoint lists, we can provide a valuation
that assigns ‘true’ to all elements of one, and ‘false’ to all elements of the other:
‘!i f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (MEM i t <=>
(!v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a) ==> v i))‘
This goal is slightly more complicated because in one direction the bi-implication re-
quires higher-order reasoning, but it’s sufficiently simple that the proof is not hard:
We can simplify the remaining goal by performing a few case splits; the cases where the
atomic expression i is in one of the lists are handled automatically by simplification:
0 [‘!f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (taut (t,f) x <=>
(!v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a)
==> holds v x))‘]
1 [‘!f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (taut (t,f) y <=>
(!v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a)
==> holds v y))‘]
2 [‘!a. ˜(MEM a t /\ MEM a f)‘]
3 [‘˜MEM i t‘]
4 [‘˜MEM i f‘]
We want to use the hypotheses to rewrite the antecedents, and so we provide addi-
tional lemmas that will dispose of the instantiated conditions that the augmented lists
still have no overlap:
173
# e(ASM_SIMP_TAC[MEM; RIGHT_OR_DISTRIB; LEFT_OR_DISTRIB;
MESON[] ‘(!a. ˜(MEM a t /\ a = i)) <=> ˜(MEM i t)‘;
MESON[] ‘(!a. ˜(a = i /\ MEM a f)) <=> ˜(MEM i f)‘]);;
...
val it : goalstack = 1 subgoal (1 total)
0 [‘!f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (taut (t,f) x <=>
(!v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a)
==> holds v x))‘]
1 [‘!f t.
(!a. ˜(MEM a t /\ MEM a f))
==> (taut (t,f) y <=>
(!v. (!a. MEM a t ==> v a) /\ (!a. MEM a f ==> ˜v a)
==> holds v y))‘]
2 [‘!a. ˜(MEM a t /\ MEM a f)‘]
3 [‘˜MEM i t‘]
4 [‘˜MEM i f‘]
We’re quite close to success. The goal doesn’t quite fall to MESON because there
is some (trivial) higher-order reasoning about v involved, but it we explicitly prove the
equivalence (∀v. P [v]) ⇔ (∀v. Q[v]) by proving P [v] ⇔ Q[v] casewise the remainder
is easily handled:
and we get the main result we wanted by just plugging together the earlier results:
174
21.4 Execution
In what sense do definitions like taut constitute programs? As theorems, they merely
assert that two expressions are equal. But the idea is to apply them left-to-right as
rewrite rules to “reduce” a starting expression e to another expression v, which we
hope will be a simple ‘value’, in the case of taut just a Boolean constant.
What about the order of the rewrite rules when they could potentially be applied
to several different subterms? Well, we will be guided by the fact that we are trying
to model OCaml, and imitate OCaml’s evaluation order quite closely. But note that in
any case, different reduction orders cannot result in unequal results. For since every
reduction e → v just strings together proven equational theorems, we do know that if
e → v and e → v 0 , that we could prove ` v = v 0 . So provided we get a value at the
end, it is welldefined, and so we can’t for example get T from one approach and F from
another. Indeed, we really know that ` e = v, a strong form of program correctness.
It is true that choosing the wrong order may result in nontermination. But ex-
actly the same is true in functional languages. For example, in a function application
OCaml evaluates the arguments first before plugging them into the body of the defini-
tion. (Other non-strict or lazy languages such as Haskell have a different policy.) On
the other hand, the if construct is special, and evaluates the test before deciding which
branch to evaluate. So this definition of the factorial terminates:
whereas the same thing with our own custom if-then-else construct does not:
175
let HOLDS_BACK = prove
(‘!v. (F <=> holds v False) /\
(T <=> holds v True) /\
(!i. v i <=> holds v (Atomic i)) /\
(!p. ˜holds v p <=> holds v (Ite p False True)) /\
(!p q. (holds v p /\ holds v q) <=> holds v (Ite p q False)) /\
(!p q. (holds v p \/ holds v q) <=> holds v (Ite p True q)) /\
(!p q. (holds v p <=> holds v q) <=>
holds v (Ite p q (Ite q False True))) /\
(!p q. holds v p ==> holds v q <=> holds v (Ite p q True))‘,
REWRITE_TAC[holds] THEN CONV_TAC TAUT);;
Provided the variables in a HOL propositional formula are uniformly named v k for
various k, we can just rewrite with this theorem, with v suitably instantiated, to map
it into a formula of the form !v. hold v p. For instance let us prove a simply
contraposition tautology:
‘holds v
(Ite (Ite (Atomic 0) (Atomic 1) True)
(Ite (Ite (Atomic 1) False True) (Ite (Atomic 0) False True) True)
True)‘
‘tautology
(Ite (Ite (Atomic 0) (Atomic 1) True)
(Ite (Ite (Atomic 1) False True) (Ite (Atomic 0) False True) True)
True)‘
Let us first ‘execute’ in the crudest way, simply rewriting with the definitions of the
various functions. Since the order of their application is naturally separated, let’s do it
in two stages, first expanding tautology and applying normalization:
# e(REWRITE_TAC[tautology; norm]);;
val it : goalstack = 1 subgoal (1 total)
‘taut ([],[])
(Ite (Atomic 0)
(Ite (Atomic 1) (Ite (Atomic 1) True (Ite (Atomic 0) False True)) True)
(Ite (Atomic 1) True (Ite (Atomic 0) False True)))‘
and then likewise expanding the recursion equations for taut, remembering to include
additional rewrites to resolve the list membership assertions:
176
So far so good. But the approach of just using the default rewriting strategy is
crude. The main problem is that conditionals are not treated in a lazy way: the rewrites
are applied to both arms, whereas by evaluating the test first, a large amount of work
could be avoided. The slowdown is not huge for such a small example but it can grow
dramatically with the problem size. So let’s try to implement a more delicate traversal
strategy that will more closely mimic that of OCaml. Roughly, the strategy should
be: normally when encountering a combination, apply the conversion to both rator and
rand first before trying the toplevel reduction (this corresponds to the basic policy of
strictness), but when encountering a condition, evaluate the test first and use that to
determine which branch to expand, and similarly optimize ‘and’ and ‘or’ by evaluating
the left-hand part first and only looking at the right if the left did not resolve it. We
start with some conversions to simplify the special constructs once the left-hand part is
evaluated to a Boolean constant:
# let COND_CONV = GEN_REWRITE_CONV I [COND_CLAUSES];;
# let AND_CONV = GEN_REWRITE_CONV I [TAUT ‘(F /\ a <=> F) /\ (T /\ a <=> a)‘];;
# let OR_CONV = GEN_REWRITE_CONV I [TAUT ‘(F \/ a <=> a) /\ (T \/ a <=> T)‘];;
Let’s set up a slightly larger tautology to see how much difference this makes:
g ‘!v. v 1 \/ v 2 \/ v 3 \/ v 4 \/ v 5 \/ v 6 \/
˜v 1 \/ ˜v 2 \/ ˜v 3 \/ ˜v 4 \/ ˜v 5 \/ ˜v 6‘;;
...
# e(MP_TAC HOLDS_BACK THEN MATCH_MP_TAC MONO_FORALL THEN
GEN_TAC THEN DISCH_THEN(fun th -> REWRITE_TAC[th]) THEN
SPEC_TAC(‘v:num->bool‘,‘v:num->bool‘) THEN
REWRITE_TAC[GSYM TAUTOLOGY; tautology]);;
val it : goalstack = 1 subgoal (1 total)
‘taut ([],[])
(norm
(Ite (Atomic 1) True
(Ite (Atomic 2) True
(Ite (Atomic 3) True
(Ite (Atomic 4) True
(Ite (Atomic 5) True
(Ite (Atomic 6) True
(Ite (Ite (Atomic 1) False True) True
(Ite (Ite (Atomic 2) False True) True
(Ite (Ite (Atomic 3) False True) True
(Ite (Ite (Atomic 4) False True) True
(Ite (Ite (Atomic 5) False True) True (Ite (Atomic 6) False True)))))))))))))‘
177
using our new depth-conversion, the proof takes a fraction of a second:
whereas the same reduction using the default rewrite strategy takes close to 100 times
as long. On larger examples the difference is still more spectacular.
While adequate for the present example, the above conversional isn’t a completely
faithful translation of OCaml’s evaluation strategy. In particular, let expressions
should be dealt with correctly and more generally evaluation should stop at every
lambda-expression until it is applied to an argument and beta-reduced. For a con-
siderably more sophisticated implementation of such a scheme, see Barras (2000).
22 Vectors
It’s sometimes convenient in mathematics to be able to talk about arbitrary Cartesian
products S n for some set S and natural number n. One can always formalize this in
HOL as a set/predicate; for example one can use the set of lists whose elements are all
in S and whose length is exactly n:
However, when S is just the universe on some type — for example when consider-
ing RN — it’s convenient to have a corresponding type for S n . If n is fixed, one can
easily use Cartesian products, for example representing R3 by R × R × R. However,
if n is large this becomes unwieldy, and it does not extend to the case where n is a
variable because in HOL’s type theory, a type may only be parametrized by another
type, not by a term. HOL therefore provides an indexed Cartesian product constructor,
written as an infix ‘ˆ’. But the second argument is, as always, a type, and it is the size
of the universe set of that type that represents the parameter n:
# ‘x:realˆN‘;;
val it : term = ‘x‘
# type_vars_in_term it;;
val it : hol_type list = [‘:N‘]
The type is designed for finite Cartesian products. When the (universe of) type N
is finite, the type AˆN is essentially isomorphic to the function space N->A. When N
is infinite, it just collapses to being equivalent to A. The dimension of the indexing set
with this interpretation is defined by the theorem dimindex, which when annotated
with types looks like this:
178
Since the Cartesian product is not quite the standard function space, we need the
analogous notion to function application. Really, the actual indexing type itself is
of no interest, only its size. When we use it as for indexing, we would prefer, for
conformance with informal convention, to use natural number indices from 1 to N .
So HOL provides an indexing operator, written as an infix $ symbol, and with type
AN → N → A, which internally picks some canonical way of mapping {1, . . . , N }
bijectively into the indexing type and then applies it. This gives quite a natural rendi-
tion of the informal indexing
PN notation. For example, the informal definition of the dot
(inner) product x · y = i=0 xi yi maps into:
# dot;;
val it : thm =
|- !x y. x dot y = sum (1 .. dimindex(:N)) (\i. x $ i * y $ i)
For the Cartesian product type, there is also a corresponding notion of function
abstraction (lambda written with binder syntax), and these satisfy the property exactly
analogous to BETA_THM for ordinary functions:
# LAMBDA_BETA;;
val it : thm = |- !i. 1 <= i /\ i <= dimindex(:B) ==> (lambda) g $ i = g i
# CART_EQ;;
val it : thm =
|- !x y. x = y <=> (!i. 1 <= i /\ i <= dimindex(:B) ==> x $ i = y $ i)
The usual arithmetic operations on vectors are defined, with overloading allowing
one to use the conventional symbols:
# vector_add;;
val it : thm = |- !x y. x + y = (lambda i. x $ i + y $ i)
# vector_neg;;
val it : thm = |- !x. --x = (lambda i. --(x $ i))
# vector_mul;;
val it : thm = |- !c x. c % x = (lambda i. c * x $ i)
As with the basic arithmetic theories, there is a simple decision procedure that can
prove many simple algebraic properties of vectors. These work generically whatever
the indexing type:
179
# VECTOR_ARITH ‘(x:realˆ1 = vec 0) <=> (x$1 = &0)‘;;
Exception: Failure "GEN_TAC".
In general, the reduction needs some nonlinear inequality reasoning. Here is the
Cauchy-Schwarz inequality:
180
We first apply SOLOVAY_VECTOR_TAC to reduce the problem to the reals:
# e SOLOVAY_VECTOR_TAC;;
val it : goalstack = 1 subgoal (1 total)
‘&0 <= c’ /\
&0 <= c /\
&0 <= x’’ /\
x’’ pow 2 = h * h * c’ + c /\
&0 <= x’ /\
x’ pow 2 = c’
==> h * c’ <= x’ * x’’‘
# e(CONV_TAC REAL_SOS);;
Searching with depth limit 0
Searching with depth limit 1
Searching with depth limit 2
Searching with depth limit 3
Searching with depth limit 4
Translating proof certificate to HOL
val it : goalstack = No subgoals
Finally, let’s look an example where the reduct has a richer quantifier structure and
we need to use general real quantifier elimination. This example takes several minutes!
# needs "Rqe/make.ml";;
# let EXAMPLE_10 = prove
(‘!x:realˆN y.
x dot y > &0
==> ?u. &0 < u /\
!v. &0 < v /\ v <= u ==> norm(v % y - x) < norm x‘,
SOLOVAY_VECTOR_TAC THEN
W(fun (asl,w) -> MAP_EVERY (fun v -> SPEC_TAC(v,v)) (frees w)) THEN
CONV_TAC REAL_QELIM_CONV);;
In addition to the basic algebraic properties of vectors, the various files in the
Multivariate subdirectory formalize some results of topology and analysis in Eu-
clidean space, e.g. Brouwer’s fixed-point theorem. In many cases these generalize
univariate results in Library/analysis.ml, but as yet they are somewhat orthog-
onal. Another slightly different formalization of Euclidean space written by Tom Hales,
culminating in a proof of the Jordan Curve Theorem, can be found in the subdirectory
Jordan. It is clearly desirable to draw all these together into a unified theory.
181
22.1 3-dimensional vectors
As an illustration, let us show how to apply the general theory to vectors in 3-dimensional
space. There is already a 3-element enumerated type 3 defined in HOL, and there is
already a theorem DIMINDEX_3:
# DIMINDEX_3;
val it : thm = |- dimindex (:3) = 3
P3
Now, since quantification over all 1 6 i 6 3 and summations i=1 are going to
arise a lot in what follows, it’s handy to prove some other specialized lemmas:
We will want a handy notation for denoting vectors by enumerating their elements.
HOL provides a general notion vector:
# vector;;
val it : thm =
|- !l. vector l = (lambda i. if i <= LENGTH l then EL (i - 1) l else &0)
and hence provide specialized expansion theorems for dot products of explicitly enu-
merated vectors:
# let DOT_VECTOR = prove
(‘(vector [x1;y1;z1] :realˆ3) dot (vector [x2;y2;z2]) =
x1 * x2 + y1 * y2 + z1 * z2‘,
REWRITE_TAC[dot; DIMINDEX_3; SUM_3; VECTOR_3]);;
182
22.2 Cross products
The existing HOL theory does not define the notion of ‘cross product’; the infrastruc-
ture is supposed to be general, whereas the cross product is something special to 3
dimensions.42 We can write its definition quite directly, though, making it a new infix:
# parse_as_infix("cross",(20,"right"));;
val it : unit = ()
# let cross = new_definition
‘(a:realˆ3) cross (b:realˆ3) =
vector [a$2 * b$3 - a$3 * b$2;
a$3 * b$1 - a$1 * b$3;
a$1 * b$2 - a$2 * b$1] :realˆ3‘;;
For proving more or less routine consequences of the definitions, it’s useful to
define an automated proof rule not unlike a cut-down version of VECTOR_ARITH,
but specialized to 3-vectors. The core tactic just applies various rewrites to remove
vector notions and then tries the ring rule:
# let VEC3_TAC =
SIMP_TAC[CART_EQ; LAMBDA_BETA; FORALL_3; SUM_3; DIMINDEX_3; VECTOR_3;
vector_add; vec; dot; cross; orthogonal; basis; ARITH] THEN
CONV_TAC REAL_RING;;
val ( VEC3_TAC ) : tactic = <fun>
# let VEC3_RULE tm = prove(tm,VEC3_TAC);;
val ( VEC3_RULE ) : term -> thm = <fun>
We can immediately use this to prove one of the most fundamental properties of
the cross product: it gives a vector orthogonal to its two argument vectors.
However, note that if x and y are parallel, x × y is only orthogonal to them in the
trivial sense of being zero. It will later be of some interest to know that for any two
3-vectors, there is a nonzero vector orthogonal to both of them. If the two vectors are
not parallel, then the cross product suffices; otherwise we need to work a bit harder.
Actually the proof seems surprisingly involved, with quite a few case splits, and we
build up to it with a sequence of lemmas, all proved automatically. First, the basis
vectors are all nonzero:
# basis;;
val it : thm = |- !k. basis k = (lambda i. if i = k then &1 else &0)
# let LEMMA_0 = VEC3_RULE
‘˜(basis 1 :realˆ3 = vec 0) /\
˜(basis 2 :realˆ3 = vec 0) /\
˜(basis 3 :realˆ3 = vec 0)‘;;
1 basis elements and 0 critical pairs
Translating certificate to HOL inferences
val ( LEMMA_0 ) : thm =
|- ˜(basis 1 = vec 0) /\ ˜(basis 2 = vec 0) /\ ˜(basis 3 = vec 0)
42 More precisely, the appropriately general outer product is really a skew-symmetric matrix. It’s an acci-
dent of 3 dimensions that such a matrix can be represented as a vector, since it is determined by its proper
upper (or lower) diagonal.
183
and we reprise some special cases of the earlier result and note some other straightfor-
ward consequences of dot products, specialized to 3-vectors:
We also note that if a vector is nonzero, its cross product with one or other of the
basis vectors must be nonzero:
# let LEMMA_6 = VEC3_RULE
‘!u. ˜(u = vec 0)
==> ˜(u cross basis 1 = vec 0) \/
˜(u cross basis 2 = vec 0) \/
˜(u cross basis 3 = vec 0)‘;;
and that if two vectors u and v are parallel (in the precise sense of having zero cross
product), so are u and v × w for any w:
∀u v : R3 . ∃w : R3 . w 6= 0 ∧ u · w = 0 ∧ v · w = 0
The proof will proceed by a painfully messy case split. If u and v are both zero,
then we can just take any nonzero vector, and it will trivially be orthogonal to both u
and v by Lemmas 3 and 4; by Lemma 0 the basis vectors are all nonzero so we can
use one of them. Otherwise, if u is zero but v isn’t (the case where v is zero and u
isn’t is symmetrical) then we just need to find a nonzero vector orthogonal to v; but
by Lemma 6 we know that one of the cross products with the basis will do. So from
now on we may assume that u and v are nonzero. Moreover, if u × v 6= 0 we can just
take that cross product and the result follows by Lemma 1 and Lemma 2. The trickier
case is when u × v = 0. But then by Lemma 7 we know that u is orthogonal to the
cross product of v and anything else, and once again the cross product of v with one of
the basis vectors is nonzero. If we throw in exactly the right case-splits and lemmas,
MESON can handle the overall proof structure. However, it’s clear that you need to
have a pretty precise notion of what the proof should be in order to come up with the
lemmas.
# let NORMAL_EXISTS = prove
(‘!u v:realˆ3. ?w. ˜(w = vec 0) /\ orthogonal u w /\ orthogonal v w‘,
REPEAT GEN_TAC THEN MAP_EVERY ASM_CASES_TAC
[‘u:realˆ3 = vec 0‘; ‘v:realˆ3 = vec 0‘; ‘u cross v = vec 0‘] THEN
ASM_REWRITE_TAC[orthogonal] THEN
ASM_MESON_TAC[LEMMA_0; LEMMA_1; LEMMA_2; LEMMA_3; LEMMA_4;
LEMMA_5; LEMMA_6; LEMMA_7]);;
184
23 Custom tactics
Although our tactic scripts can, in principle, be arbitrary OCaml programs, they’ve
mostly been fairly stereotyped so far, just plugging together components in fixed ways
without any (non-trivial) flow control. But there are natural cases where we can pro-
gram the tactic to perform search.
185
Figure 3: Points on a cube
# let points =
[((0, -1), (0, -1), (2, 0)); ((0, -1), (0, 0), (2, 0));
((0, -1), (0, 1), (2, 0)); ((0, -1), (2, 0), (0, -1));
((0, -1), (2, 0), (0, 0)); ((0, -1), (2, 0), (0, 1));
((0, 0), (0, -1), (2, 0)); ((0, 0), (0, 0), (2, 0));
((0, 0), (0, 1), (2, 0)); ((0, 0), (2, 0), (-2, 0));
((0, 0), (2, 0), (0, -1)); ((0, 0), (2, 0), (0, 0));
((0, 0), (2, 0), (0, 1)); ((0, 0), (2, 0), (2, 0));
((0, 1), (0, -1), (2, 0)); ((0, 1), (0, 0), (2, 0));
((0, 1), (0, 1), (2, 0)); ((0, 1), (2, 0), (0, -1));
((0, 1), (2, 0), (0, 0)); ((0, 1), (2, 0), (0, 1));
((2, 0), (-2, 0), (0, 0)); ((2, 0), (0, -1), (0, -1));
((2, 0), (0, -1), (0, 0)); ((2, 0), (0, -1), (0, 1));
((2, 0), (0, 0), (-2, 0)); ((2, 0), (0, 0), (0, -1));
((2, 0), (0, 0), (0, 0)); ((2, 0), (0, 0), (0, 1));
((2, 0), (0, 0), (2, 0)); ((2, 0), (0, 1), (0, -1));
((2, 0), (0, 1), (0, 0)); ((2, 0), (0, 1), (0, 1));
((2, 0), (2, 0), (0, 0))];;
Although we’ll be creating a HOL theorem, it’s useful to be able to make some
manipulations on such numbers at the OCaml level to decide on the proof steps. So we
define various arithmetic operations on the encoded
√ algebraic
√ numbers such as addition
and multiplication,
√ the latter being (x1 + y 1 2)(x 2 + y2 2) = (x1 x2 + 2y1 y2 ) +
(x1 y2 + x2 y1 ) 2. In the end, we just need to be able to test orthogonality so we hide
the definitions of addition, dot product etc. inside this function:
186
# let ortho =
let mult (x1,y1) (x2,y2) = (x1 * x2 + 2 * y1 * y2,x1 * y2 + y1 * x2)
and add (x1,y1) (x2,y2) = (x1 + x2,y1 + y2) in
let dot (x1,y1,z1) (x2,y2,z2) =
end_itlist add [mult x1 x2; mult y1 y2; mult z1 z2] in
fun (v1,v2) -> dot v1 v2 = (0,0);;
Now it’s helpful to create, once and for all, a list of all possible pairs of orthogonal
points:
# let opairs = filter ortho (allpairs (fun a b -> a,b) points points);;
Now we want to be able to convert points from their OCaml representation into
HOL values. Since there are so few cases,√we can just do it by casewise pattern match-
ing for the coordinates in the form a + b 2:
# let hol_of_value =
let tm0 = ‘&0‘ and tm1 = ‘&2‘ and tm2 = ‘-- &2‘
and tm3 = ‘sqrt(&2)‘ and tm4 = ‘--sqrt(&2)‘ in
function 0,0 -> tm0 | 2,0 -> tm1 | -2,0 -> tm2 | 0,1 -> tm3 | 0,-1 -> tm4;;
That applied to each coordinate, and all we need to do now is map a triple of such
coordinates into a HOL vector, applying hol_of_value to each one:
# let hol_of_point =
let ptm = ‘vector:(real)list->realˆ3‘ in
fun (x,y,z) -> mk_comb(ptm,mk_flist(map hol_of_value [x;y;z]));;
√ HOL has no inbuilt smarts for simplifying surds, so we isolate the key lemma about
2 manually:
Now we have two key conversions to drive the HOL proof, one which proves that
a vector is nonzero:
# let PROVE_NONTRIVIAL =
let ptm = ‘˜(x :realˆ3 = vec 0)‘ and xtm = ‘x:realˆ3‘ in
fun x -> prove(vsubst [hol_of_point x,xtm] ptm,
GEN_REWRITE_TAC RAND_CONV [VECTOR_ZERO] THEN
MP_TAC SQRT_2_POW THEN CONV_TAC REAL_RING);;
let PROVE_ORTHOGONAL =
let ptm = ‘orthogonal:realˆ3->realˆ3->bool‘ in
fun (x,y) ->
prove(list_mk_comb(ptm,[hol_of_point x;hol_of_point y]),
ONCE_REWRITE_TAC[ORTHOGONAL_VECTOR] THEN
MP_TAC SQRT_2_POW THEN CONV_TAC REAL_RING);;
187
For example:
Now we come to the main lemma in the Kochen-Specker paradox. To simplify the
logic behind the case splits we just use a predicate P on vectors, which eventually will
be true in directions where spin2 = 1, and false in directions where spin2 = 0. The
quantum mechanical assumptions and desired conclusion are then as follows:
First of all, we throw into the assumptions all valid nontriviality and orthogonality
hypotheses between pairs of points; it simplifies the later proof if we can just do this
once and for all:
‘F‘
188
The logic behind the proof is to enumerate possible assignments to the P k for
each point k, using the assumptions to deduce consequential hypotheses about other
points once something is decided about a given point. The following auxiliary function
produces the HOL assertion P k for a point k:
Thus, we first try to deduce as much as possible from the current assumptions, and
if no contradiction has been reached, pick some point k and case-split over P k and
¬P k, then proceed by recursion. Eventually we will derive a contradiction in all cases
and the theorem will be proved. Rather than drive all this case-splitting manually (quite
feasible, but tedious without some careful symmetry arguments), we will program HOL
to perform it with a custom tactic. We start with a simple auxiliary tactic that attempts
to deduce a new assertion P k or ¬(P k) by backchaining through the hypothesis and
using the specified points as witnesses:
The main tactic is defined recursively, and takes two parameters, set_1, the set of
points for which we know that P holds, and set_0, the set of points where we assume
P does not. The tactic, though quite long, is conceptually quite straightforward:
We start by seeing if we already have an overlap between the sets set_0 and
set_1, in which case we construct a contradiction in HOL quite straightforwardly.
Otherwise we see at the OCaml level what new facts we should be able to deduce:
del_0 holds the set of new points for which we can deduce ¬(P k) and del_1 holds
189
those for which we can deduce P k. The body of the tactic just uses DEDUCE_POINT_TAC
to make all these deductions at the HOL level; at this point we recurse with enlarged
sets set_0’ and set_1’. Now, if no new points can be deduced we pick some point
v for which we have no existing assumption and perform a case split, recursing in each
half. And indeed, our goal gets solved, though all those case splits take a minute:
# e(KOCHEN_SPECKER_TAC [] []);;
val it : goalstack = No subgoals
We’re almost done now, but we’ve got ahead of ourself in one respect. The quantum
mechanical assumption was that the spins are 1, 1 and 0 in some order when measured
in any three orthogonal directions. This ‘obviously’ implies that if the spin is 0 in one
direction, it is 1 in any orthogonal direction, which is effectively what we used as the
hypothesis in the lemma. However, strictly speaking, we need to justify the fact that
there is a nontrivial direction orthogonal to two other orthogonal directions, otherwise
one doesn’t follow logically from another. Still, the above lemma is easy enough:
Still, note that we did need to rewrite x = 0 into x · x = 0 for the simple proof to
work, indicating that this depends to some extent on the fact that we are considering
real vectors. Anyway, now the main proof is relatively straightforward, putting together
the pieces:
190
We mentioned earlier the use of the trivial symmetry reduction theorems like WLOG_LE.
When I saw Conway prove the above theorem, he repeatedly used symmetry argu-
ments: ‘the three coordinate axes are all orthogonal, so we have a combination 0,1,1
in some order, so let’s assume the zero is in the x direction...’. It would be an interest-
ing contrast with the brutal machine-oriented case analysis above to formalize a more
delicate proof exploiting symmetry as much as possible.
' $
' $ ' $
existing
new type
type bijections P
α - ρ
& % & %
& %
It is not hard to show that this definitional mechanism, just like the one for (term)
constants, is consistency-preserving, and indeed conservative in the sense that any-
thing provable using a new type could also be proved without it. Crudely speaking, any
proofs involving the new type could equally well be carried out in the representative
types with quantifiers relativized to the subset P ; it’s just that having a new type makes
this kind of restriction unnecessary. When one defines a type by define_type, HOL
constructs a representative within an existing type and makes a primitive definition au-
tomatically using the primitive definitional mechanism (Melham 1989). But occasion-
ally it is necessary, or at least convenient, to use the underlying method directly. We
will see how this works.
191
# MESON[LEMMA_0] ‘?x:realˆ3. ˜(x = vec 0)‘;;
0 inferences so far. Searching with maximum size 0.
0 inferences so far. Searching with maximum size 1.
Goal solved with 2 inferences.
CPU time (user): 0.01
val it : thm = |- ?x. ˜(x = vec 0)
To make a new type definition, one simply provides this nonemptiness theorem
to the function new_type_definition as well as three other pieces of informa-
tion: the desired name for the new type and the desired names of the two bijections,
often called ‘abstraction’ and ‘representation’ functions. We will call the new type
direction:
# let direction_tybij = new_type_definition "direction" ("mk_dir","dest_dir")
(MESON[LEMMA_0] ‘?x:realˆ3. ˜(x = vec 0)‘);;
...
val direction_tybij : thm =
|- (!a. mk_dir (dest_dir a) = a) /\
(!r. ˜(r = vec 0) <=> dest_dir (mk_dir r) = r)
The theorem returned tells us exactly that the abstraction and representation func-
tions set up a bijection between the new type and the proffered subset of the existing
type. But they are formally completely different types, so any concepts that we want to
map over to the new type have to be defined explicitly using the bijections. For exam-
ple, we can define what it means for two directions to be perpendicular and parallel:
# parse_as_infix("||",(11,"right"));;
val it : unit = ()
# parse_as_infix("_|_",(11,"right"));;
val it : unit = ()
# let perpdir = new_definition
‘x _|_ y <=> orthogonal (dest_dir x) (dest_dir y)‘;;
val perpdir : thm = |- !x y. x _|_ y <=> orthogonal (dest_dir x) (dest_dir y)
# let pardir = new_definition
‘x || y <=> (dest_dir x) cross (dest_dir y) = vec 0‘;;
val pardir : thm = |- !x y. x || y <=> dest_dir x cross dest_dir y = vec 0
In order to transfer theorems from one type to the other, the following rewrite rules
are convenient. They map quantifiers over the new type back onto the representing type
with the appropriate relativization:
# e(REWRITE_TAC[pardir]);;
val it : goalstack = 1 subgoal (1 total)
‘!x y.
dest_dir x cross dest_dir y = vec 0 /\
dest_dir y cross dest_dir z = vec 0
==> dest_dir x cross dest_dir z = vec 0‘
192
Now all quantified variables have dest_dir applied to them inside the body of
the formula, so we can use our transferring rewrite to get an equivalent formula on the
representing type:
# e(REWRITE_TAC[DIRECTION_CLAUSES]);;
val it : goalstack = 1 subgoal (1 total)
This is easily solved by our existing 3-vector tactic. Note that the nonzero hypoth-
esis for y is actually necessary here; if y is zero then trivially x × y = 0 and y × z = 0
whatever x and z may be.
# e VEC3_TAC;;
...
val it : goalstack = No subgoals
Using this approach, it’s routine to prove that parallelism on directions is in fact an
equivalence relation:
# let [PARDIR_REFL; PARDIR_SYM; PARDIR_TRANS] = (CONJUNCTS o prove)
(‘(!x. x || x) /\
(!x y. x || y <=> y || x) /\
(!x y z. x || y /\ y || z ==> x || z)‘,
REWRITE_TAC[pardir; DIRECTION_CLAUSES] THEN VEC3_TAC);;
193
This follows immediately from NORMAL_EXISTS except that we need to use sym-
metry of the orthogonality relation:
# e(MESON_TAC[NORMAL_EXISTS; ORTHOGONAL_SYM]);;
...
val it : goalstack = No subgoals
The first axiom is a bit more complicated but is a relatively easy consequence of
the same orthogonality property. This proof takes a while to run because the last line
invokes a large case-split. Note that since we consider all parallel directions equivalent,
we use parallelism where we formerly used equality:
and the third axiom is pretty easy thanks to our earlier Lemma 0:
The final axiom is slightly more work. Recall that we need to show that for any line
there are three distinct points on it, or in terms of the model, that given any nonzero
vector there are three vectors perpendicular to it, no two out of the three being parallel.
We start with a trivial lemma:
# let CROSS_0 = VEC3_RULE ‘x cross vec 0 = vec 0 /\ vec 0 cross x = vec 0‘;;
The next step is to prove a cut-down version of the fourth axiom, asserting that
there are two non-parallel directions perpendicular to any given direction. After the
usual expansion into basic 3-vectors, the proof is simply to observe that if l 6= 0 then
some two of the cross products of l and the basis vectors will serve. We use the lemma
CROSS_0 to deduce from the non-parallelism of the two vectors that both are nonzero:
194
# let DIRECTION_AXIOM_4_WEAK = prove
(‘!l. ?p p’. ˜(p || p’) /\ p _|_ l /\ p’ _|_ l‘,
REWRITE_TAC[DIRECTION_CLAUSES; pardir; perpdir] THEN REPEAT STRIP_TAC THEN
SUBGOAL_THEN
‘orthogonal (l cross basis 1) l /\ orthogonal (l cross basis 2) l /\
˜((l cross basis 1) cross (l cross basis 2) = vec 0) \/
orthogonal (l cross basis 1) l /\ orthogonal (l cross basis 3) l /\
˜((l cross basis 1) cross (l cross basis 3) = vec 0) \/
orthogonal (l cross basis 2) l /\ orthogonal (l cross basis 3) l /\
˜((l cross basis 2) cross (l cross basis 3) = vec 0)‘
MP_TAC THENL [POP_ASSUM MP_TAC THEN VEC3_TAC; MESON_TAC[CROSS_0]]);;
Now if we have two such vectors, we can always create a third by a non-trivial
linear combination of them, most simply their sum:
Putting all the pieces together, we now get the fourth axiom at once:
One generally doesn’t need to do anything manually with the resulting type bi-
jections, which are just plugged into functions we will describe later. However it’s
instructive to ‘reverse-engineer’ from the theorem how the new type is constructed.
It is in bijection with the set of equivalence classes of the original type of directions:
note that by extensionality, (—) x— is precisely {y | x || y}, in other words the
equivalence class determined by x (the set of all directions parallel to it). The new type
is in bijection with the set of sets of directions that are an equivalence class of some
element.
195
Once again, the new type is formally completely separate from the representing
type and all definitions and theorems need to be mapped over. Fortunately there are
also tools to help with this. However, in order for lifting a function to equivalence
classes to be meaningful (or at least useful), it must respect the equivalence relation
R in the sense that equivalent arguments yield equivalent results. For arguments and
results of the type being lifted, ‘equivalent’ means related by R; for other types it
means equality. In our case, the only function over the base type we need, apart from
parallelism itself, is perpendicularity. Since the arguments are of the type being lifted
but the result is Boolean, we need to show that parallel directions satisfy the same
perpendicularity relations:
# let perpl,perpl_th =
lift_function (snd line_tybij) (PARDIR_REFL,PARDIR_TRANS)
"perpl" PERPDIR_WELLDEF;;
val perpl : thm =
|- perpl x y <=>
(@u. ?x y. (x _|_ y <=> u) /\ dest_line x x /\ dest_line y y)
val perpl_th : thm =
|- x _|_ y <=> perpl (mk_line ((||) x)) (mk_line ((||) y))
Two theorems are returned, one the actual definition of the new function and one
a consequential theorem. Once again, the precise form of these theorems need not
normally concern the user, since they will be used only in other automated routines.
Specifically, the final component of the automated support for equivalence relations
is a function that will automatically lift theorems to the equivalence classes.44 The
necessary parameters are the reflexivity, symmetry and transitivity of the equivalence
relation and the set of consequential theorems (as returned by lift_function) for
all the functions involved. In our case we can use the following for all the axioms:
Now, mapping over the axioms is simply a matter of applying this function:
44 This has some restrictions; in particular we need any quantifiers over the underlying type to be first-order.
196
It’s instructive to examine the original and lifted theorems side-by-side to see how
they correspond, with the new lifted functions replacing the old ones and the equiva-
lence relation replaced by equality, e.g.
# DIRECTION_AXIOM_4;;
val it : thm =
|- !l. ?p p’ p’’.
˜(p || p’) /\
˜(p’ || p’’) /\
˜(p || p’’) /\
p _|_ l /\
p’ _|_ l /\
p’’ _|_ l
# LINE_AXIOM_4;;
val it : thm =
|- !l. ?p p’ p’’.
˜(p = p’) /\
˜(p’ = p’’) /\
˜(p = p’’) /\
perpl p l /\
perpl p’ l /\
perpl p’’ l
and introduce the more intuitive name for incidence of points and lines:
# parse_as_infix("on",(11,"right"));;
Much as before, we define a set of rewrites to help with mapping over theorems:
Now the proofs of the final versions of the axioms are all routine:
197
# let AXIOM_1 = prove
(‘!p p’. ˜(p = p’) ==> ?l. p on l /\ p’ on l /\
!l’. p on l’ /\ p’ on l’ ==> (l’ = l)‘,
POINT_TAC LINE_AXIOM_1);;
For another example of defining a quotient type, see the construction of the real
numbers in HOL Light’s realax.ml source file.
0+x = x
−x + x = 0
(x + y) + z = x + (y + z)
(x · y) · z = x · (y · z)
(x + y) · z = x·z+y·z
For the sake of familiarity we will just state these axioms for the usual operators
on natural numbers, but we will perform only logical reasoning and not use any special
known properties of N.
let near_ring_axioms =
‘(!x. 0 + x = x) /\
(!x. neg x + x = 0) /\
(!x y z. (x + y) + z = x + y + z) /\
(!x y z. (x * y) * z = x * y * z) /\
(!x y z. (x + y) * z = (x * z) + (y * z))‘;;
45 See http://www.algebra.uni-linz.ac.at/˜erhard/Diplomarbeit/main/
node58.html.
198
Suppose we want to show that these axioms imply certain other properties, for
example that 0 · x = 0 or x + −x = 0 (or prove that they do not). It’s always worth
giving MESON a shot in such situations, because in principle it will always prove any
valid consequence of the axioms. For example, it can do this one if you’re patient:
# MESON[]
‘(!x. 0 + x = x) /\
(!x. neg x + x = 0) /\
(!x y z. (x + y) + z = x + y + z) /\
(!x y z. (x * y) * z = x * y * z) /\
(!x y z. (x + y) * z = (x * z) + (y * z))
==> !a. 0 * a = 0‘;;
0 inferences so far. Searching with maximum size 1.
1 inferences so far. Searching with maximum size 2.
2 inferences so far. Searching with maximum size 3.
6 inferences so far. Searching with maximum size 4.
...
204037 inferences so far. Searching with maximum size 16.
Goal solved with 226059 inferences.
CPU time (user): 27.5
val it : thm =
|- (!x. 0 + x = x) /\
(!x. neg x + x = 0) /\
(!x y z. (x + y) + z = x + y + z) /\
(!x y z. (x * y) * z = x * y * z) /\
(!x y z. (x + y) * z = x * z + y * z)
==> (!a. 0 * a = 0)
However, its proof search does not have any specially efficient techniques for equa-
tions, and for deeper consequences of the axioms it is unlikely to succeed in a practical
period of time. (I’ve just tried !a. neg(neg a) = a, which is also a consequence
of the axioms as we shall see, and waited an hour or two without any result.) We
turn instead to a more specialized technique for equational reasoning called comple-
tion (Knuth and Bendix 1970). As usual, our HOL implementation of completion will
be sound by construction. However, we will not give a very detailed discussion of
completeness issues, i.e. when it is always guaranteed to prove any valid equational
consequences; for more on this see Baader and Nipkow (1998).
while the following is an analog of strip_comb that will not break up anything in
the constant list, even if it’s actually composite in HOL:
199
# let rec real_strip w tm =
if mem tm w then tm,[] else
let l,r = dest_comb tm in
let f,args = real_strip w l in f,args@[r];;
The first key idea underlying completion is to use equations as rewrite rules in an
appropriate direction to make terms ‘simpler’ according to some ordering; for example
we would always use 0 + x = x left-to-right, never right-to-left. This is not unlike
the HOL idea of ordered rewriting that we’ve already discussed. However, HOL’s
ordering is not, in general, wellfounded, and it is applied to the terms after the matching
instantiation. For completion, the ordering is chosen to be wellfounded, and to respect
congruences and instantiation, so that if a rewrite rule l = r has l above r in the order
we can guarantee that rewriting any subterm of a term with l = r will give a ‘simpler’
term, without any checking at each application. Coming up with useful orders that
satisfy this property is not entirely trivial. We will use a so-called ‘lexicographic path
order’ (LPO). The starting point is to assign a ‘weight’ to a function according to its
place in a list, breaking ties between identically-named functions according to their
arity:
The most basic way the LPO orders terms is by use of the weighting function on
the top-level function symbols. For example, if we give multiplication a higher weight
than addition (by putting it later in the list lis above), this will make the distributive
law (x + y) · z = x · z + y · z applicable left-to-right, since it brings a ‘simpler’ operator
to the top, even though it actually increases the actual size of the term. In cases where
the top operators are the same, the LPO assigns preferences lexicographically left-to-
right, so that the ordering of the leftmost arguments are considered first, and only in
the event of a tie are the others considered one-by-one. This, for example, will tend to
drive associative laws like (x + y) + z = x + (y + z) in the left-to-right direction since
x is simpler than x + y. We use a generic function lexord to apply a basic ordering
ord lexicographically, returning false if the lists are of different lengths:
While the above description gives a good intuitive idea about how the LPO works,
the definition is a bit elaborate because it also needs to ensure certain other properties,
for example that any term is greater than a proper subterm (we said above that x is
simpler than x + y). Readers should feel free to pass over the definition that follows,
since it’s a bit involved and largely independent of whether things are being done in a
theorem prover like HOL or not. The key points to note are that we define an irreflexive
(>) form lpo_gt and a reflexive (>) form lpo_ge, and that both are parametrized
by the weighting list.
200
let rec lpo_gt w s t =
if is_realvar w t then not(s = t) & mem t (frees s)
else if is_realvar w s or is_abs s or is_abs t then false else
let f,fargs = real_strip w s and g,gargs = real_strip w t in
exists (fun si -> lpo_ge w si t) fargs or
forall (lpo_gt w s) gargs &&
(f = g & lexord (lpo_gt w) fargs gargs or
weight w (f,length fargs) (g,length gargs))
and lpo_ge w s t = (s = t) or lpo_gt w s t;;
# ADD_CLAUSES;;
val it : thm =
|- (!n. 0 + n = n) /\
(!m. m + 0 = m) /\
(!m n. SUC m + n = SUC (m + n)) /\
(!m n. m + SUC n = SUC (m + n))
# REWRITE_CONV[ADD_CLAUSES] ‘SUC(0 + SUC(0)) + SUC(SUC(SUC 0))‘;;
val it : thm =
|- SUC (0 + SUC 0) + SUC (SUC (SUC 0)) = SUC (SUC (SUC (SUC (SUC 0))))
# REWRITE_CONV[ADD_CLAUSES] ‘0 + SUC(SUC(0 + 0)) + SUC(SUC 0 + SUC 0)‘;;
val it : thm =
|- 0 + SUC (SUC (0 + 0)) + SUC (SUC 0 + SUC 0) =
SUC (SUC (SUC (SUC (SUC 0))))
The core of completion is the isolation and analysis of those terms that are rewritable
in more than one way. If, whenever a term is rewritable in more than one way, the
two alternatives are identical or can be brought back together by further rewrites,
then this does not pose any danger to completeness. For example we can use either
the third or fourth rewrite of ADD_CLAUSES to rewrite the term SUC a + SUC b
into either SUC(a + SUC b) or SUC(SUC a + b), but in either case exactly
one further rewrite is possible and both terms will be brought into the common form
SUC(SUC(a + b)). Similarly, if the two rewrites are applied to disjoint subterms,
the other one can always be used afterwards to bring them back together.
The difficult situations are where this joinability no longer holds, and a term t
rewrites to both t1 and t2 that cannot be brought together by further rewrites. In such a
case, (t1 , t2 ) is called a critical pair. For example, using the near-ring axioms we can
rewrite (0 + x) · y in two different ways, yielding x · y and 0 · y + x · y respectively, and
those two terms are not reducible by any further rewrites. So the set of axioms is not
complete. The idea of completion is to handle such situations by taking the equation
made by the critical pair as a new rewrite, either t1 = t2 or t2 = t1 , in our case
0 · y + x · y = x · y. (Note that by construction t1 = t2 is always a valid consequence
of the existing rewrites since t = t1 and t = t2 both are.)
The core of completion is a systematic algorithm for finding the critical pairs of a
set of rewrites. We are essentially looking for a term t to which two rewrites l1 = r1
and l2 = r2 are applicable (assume we are applying both to exactly the same subterm
for now), so we need to look for a term t to which both l1 and l2 are matchable. We
201
can find such a term, indeed the most general such term in a precise sense, by unifying
the two left-hand sides l1 and l2 , i.e. finding the most general instantiations that make
them identical. (Matching can be considered as a special case of unification where only
the variables in one of the terms can be instantiated.)
Unification of first-order terms is not built into HOL, but we can implement it quite
easily. It will be defined recursively, and we will record the instantiations discovered
so far in a finite partial function env (for ‘environment’) mapping variables to the
appropriate unifying assignment. However, for efficiency, this mapping is designed
so that in general it may need to be applied many times to get the final answer; for
example it may map just x 7→ y + z and y 7→ z in which case the effective mapping
of x is x 7→ z + z. Organizing things this way makes the core unification function a
little simpler and in some cases much more efficient. But we need to check that any
assignment is acyclic, or it is meaningless. So we start with a function that checks a
proposed assignment x 7→ t for triviality and acyclicity. If it is cyclic in the trivial sense
that it maps x to itself (even perhaps via circuitous route like x 7→ y 7→ x 7→ x) the
function returns true indicating that the assignment can be ignored. If it would cause a
non-trivial cycle, the function fails:
For use in HOL, it’s more convenient to unwrap the multi-stage finite partial func-
tions into conventional association list assignments. We just convert to an association
list the raw function and then apply it repeatedly until no further change occurs.
202
Now we are ready to compute overlaps. The programming here is a little tricky,
because when considering two subterms, we need to keep track of how to record the
overall overlap. The following function defines all ways of overlapping an equation l
= r with another term tm, where the additional argument rfn is used to create each
overall critical pair from an instantiation i. The function simply recursively traverses
the term, trying to unify l with each non-variable subterm and applying rfn to any
resulting instantiations to give the critical pair arising from that overlap. During recur-
sive descent, the function rfn is itself modified correspondingly. For updating rfn
across the list of arguments we define the auxiliary function listcases.
let rec listcases fn rfn lis acc =
match lis with
[] -> acc
| h::t -> fn h (fun i h’ -> rfn i (h’::map REFL t)) @
listcases fn (fun i t’ -> rfn i (REFL h::t’)) t acc;;
let LIST_MK_COMB f ths = rev_itlist (fun s t -> MK_COMB(t,s)) ths (REFL f);;
To understand all that rather subtle code, it’s worth tracing through some examples.
But first we want to start out by making sure there are no coincidental clashes of vari-
able names that will rule out possible unifications; remember that all our variables are
implicitly universally quantified, so we are always at liberty to do this:
let fixvariables s th =
let fvs = subtract (frees(concl th)) (freesl(hyp th)) in
let gvs = map2 (fun v n -> mk_var(sˆstring_of_int n,type_of v))
fvs (1--length fvs) in
INST (zip gvs fvs) th;;
Now we can produce the overall critical pairs function: it simply renames the two
input theorems and considers all overlaps of one inside the other and vice versa:
let critical_pairs w tha thb =
let th1,th2 = renamepair (tha,thb) in crit1 w th1 th2 @ crit1 w th2 th1;;
To emphasize the importance of renaming the variables, note that a theorem can
have non-trivial critical pairs with itself, by applying it to ‘overlapping’ subterms. For
example, the following two critical pairs, effectively the same except for variable re-
naming, result from two different ways of rewriting SUC(SUC(x)):
# let th = SPEC ‘SUC x‘ ADD1;;
val th : thm = |- SUC (SUC x) = SUC x + 1
# critical_pairs [‘SUC‘; ‘(+)‘] th th;;
val it : thm list =
[|- SUC (SUC x1 + 1) = SUC (SUC x1) + 1;
|- SUC (SUC y1 + 1) = SUC (SUC y1) + 1]
203
So much for finding the critical pairs; what do we do with them? First, given a
critical pair ` s = t we simplify both sides with the existing set of rewrite rules, giving
` s0 = t0 , and attempt to choose an orientation ` s0 = t0 or ` t0 = s0 that respects the
ordering. (Note that we don’t rule out the possibility that s0 and t0 are the same, so we
use the reflexive ordering.)
let normalize_and_orient w eqs th =
let th’ = GEN_REWRITE_RULE TOP_DEPTH_CONV eqs th in
let s’,t’ = dest_eq(concl th’) in
if lpo_ge w s’ t’ then th’ else if lpo_ge w t’ s’ then SYM th’
else failwith "Can’t orient equation";;
It helps to have some feedback on what’s going on inside the completion loop, so
we define a function that reports on current status. If the critical pairs are being steadily
depleted we only print something per thousand, but we always report if they’re getting
augmented.
let status(eqs,crs) eqs0 =
if eqs = eqs0 & (length crs) mod 1000 <> 0 then () else
(print_string(string_of_int(length eqs)ˆ" equations and "ˆ
string_of_int(length crs)ˆ" pending critical pairs");
print_newline());;
We also use one function to check whether the LHS of an equation is reducible by
a set of rewrite rules:
let left_reducible eqs eq =
can (CHANGED_CONV(GEN_REWRITE_CONV (LAND_CONV o ONCE_DEPTH_CONV) eqs))
(concl eq);;
Finally, here’s the main loop of completion. We have a set of established rewrite
rules eqs and a set of critical pairs to handle. The main loop picks the first critical pair
and attempts to normalize and orient it into a new rewrite. If this works, we are going
to add it to our set of rewrites. But we now remove any equations that are left-reducible
and put them back in the critical pair list for potential further simplification. (It is not
trivial to show that this is a complete strategy — see Baader and Nipkow (1998) for
more on this.) If the current critical pair is not orientable, we look for any other one
that is, in the hope that the resulting rewrites will allow further simplification of the
current one. If even this is impossible, we fail. Before going round the loop again we
print a status report:
let rec complete w (eqs,crits) =
match crits with
(eq::ocrits) ->
let trip =
try let eq’ = normalize_and_orient w eqs eq in
let s’,t’ = dest_eq(concl eq’) in
if s’ = t’ then (eqs,ocrits) else
let crits’,eqs’ = partition(left_reducible [eq’]) eqs in
let eqs’’ = eq’::eqs’ in
eqs’’,
ocrits @ crits’ @ itlist ((@) o critical_pairs w eq’) eqs’’ []
with Failure _ ->
if exists (can (normalize_and_orient w eqs)) ocrits
then (eqs,ocrits@[eq])
else failwith "complete: no orientable equations" in
status trip eqs; complete w trip
| [] -> eqs;;
204
To start off, we put all the equations in the critical pair list, but we just check that
at least they are orientable under the ordering chosen:
A classic example is group theory. For readability, we throw away the assumptions
in the result. Without waiting too long, we obtain a 10-equation canonical rewrite
system:
205
# let eqs = map SPEC_ALL (CONJUNCTS (ASSUME
‘(!x. 1 * x = x) /\ (!x. i(x) * x = 1) /\
(!x y z. (x * y) * z = x * y * z)‘)) in
map concl (complete_equations [‘1‘; ‘( * ):num->num->num‘; ‘i:num->num‘] eqs);;
1 equations and 4 pending critical pairs
2 equations and 5 pending critical pairs
3 equations and 10 pending critical pairs
...
15 equations and 569 pending critical pairs
16 equations and 599 pending critical pairs
16 equations and 547 pending critical pairs
15 equations and 463 pending critical pairs
10 equations and 478 pending critical pairs
10 equations and 0 pending critical pairs
val it : term list =
[‘i (x1 * x2) = i x2 * i x1‘; ‘i 1 = 1‘; ‘i (i y1) = y1‘; ‘x1 * i x1 = 1‘;
‘x1 * i x1 * y2 = y2‘; ‘x2 * 1 = x2‘; ‘i x2 * x2 * x3 = x3‘;
‘(x * y) * z = x * y * z‘; ‘i x * x = 1‘; ‘1 * x = x‘]
We can use completion to settle whether certain other equations follow from a set
of axioms such as those for groups. The following tactic completes a set of equational
theorems and adds them as new assumptions to the goal:
# let COMPLETE_TAC w th =
let eqs = map SPEC_ALL (CONJUNCTS(SPEC_ALL th)) in
let eqs’ = complete_equations w eqs in
MAP_EVERY (ASSUME_TAC o GEN_ALL) eqs’;;
# g ‘(!x. 1 * x = x) /\
(!x. i(x) * x = 1) /\
(!x y z. (x * y) * z = x * y * z)
==> !x y. i(y) * i(i(i(x * i(y)))) * x = 1‘;;
by applying completion:
‘!x y. i y * i (i (i (x * i y))) * x = 1‘
# e(ASM_REWRITE_TAC[]);;
val it : goalstack = No subgoals
46 There is no danger that HOL will reject or restrict any of these rewrites, because respecting the well-
founded order excludes the possibility of looping at all, let alone the simple kinds that HOL checks for.
206
Similarly we can handle the near-rings example that we started with.
# g ‘(!x. 0 + x = x) /\
(!x. neg x + x = 0) /\
(!x y z. (x + y) + z = x + y + z) /\
(!x y z. (x * y) * z = x * y * z) /\
(!x y z. (x + y) * z = (x * z) + (y * z))
==> (neg 0 * (x * y + z + neg(neg(w + z))) + neg(neg b + neg a) =
a + b)‘;;
# e (DISCH_THEN(COMPLETE_TAC
[‘0‘; ‘(+):num->num->num‘; ‘neg:num->num‘; ‘( * ):num->num->num‘]));;
...
val it : goalstack = 1 subgoal (1 total)
We finally see for sure that many of the consequences we might have wondered
about are indeed consequences of the axioms. And the chosen goal follows from them:
# e(ASM_REWRITE_TAC[]);;
val it : goalstack = No subgoals
The following variant with a different weighting works too, though it takes a bit
longer and the resulting set is larger. This helps to emphasize that the canonicality
property depends critically on the ordering chosen.
# e (DISCH_THEN(COMPLETE_TAC
[‘0‘; ‘(+):num->num->num‘; ‘( * ):num->num->num‘; ‘neg:num->num‘]));;
207
26 Linking external tools
HOL’s insistence on proving everything from first principles is quite rigid, and might
seem somewhat puritanical. However, it’s important to note that provided the proof can
be finally constructed (‘checked’ if you like) by HOL’s inference engine so that you get
a theorem, you can arrive at it however you like. We have already mentioned two cases
where some HOL extensions rely on external tools to assist in its proof construction:
PRIME_CONV’s use of a factorization engine and SOS_RULE and REAL_SOS’s use
of a semidefinite programming package. In this section we will illustrate how users can
set up a similar kind of interaction to use an external tool in proof construction.
26.1 Maxima
There are many potentially interesting external tools one might want to link up to
HOL, including for example more powerful first-order theorem provers than the in-
built MESON (Hurd 1999) or linear programming packages that can handle bigger
problems than REAL_ARITH and friends (Obua 2005). I’ve chosen to link up the
computer algebra system Maxima (an open-source version of the seminal Macsyma),
along the lines of the experiments reported in Harrison and Théry (1998). Everything
we do is applicable with fairly minor changes to many other computer algebra sys-
tems such as Maple47 and Mathematica48 . If you have access to some such system,
you’re encouraged to try it. However, these are (fairly expensive) commercial prod-
ucts, which readers will hardly be able to justify purchasing just to try out the lit-
tle experiment here. Maxima, by contrast, can be freely downloaded from http:
//maxima.sourceforge.net/. I downloaded the source package and found
that it built out of the box under clisp with no problems. On starting it up I get
the banner and a command prompt:
/home/johnh$ maxima
i i i i i i i ooooo o ooooooo ooooo ooooo
I I I I I I I 8 8 8 8 8 o 8 8
I \ ‘+’ / I 8 8 8 8 8 8
\ ‘-+-’ / 8 8 8 ooooo 8oooo
‘-__|__-’ 8 8 8 8 8
| 8 o 8 8 o 8 8
------+------ ooooo 8oooooo ooo8ooo ooooo 8
208
RIt’s appropriate to orient ourselves by trying to solve a problem ‘manually’. So let’s
try sin4 (x) dx:
(%i1) integrate(sin(x)ˆ4,x);
sin(4 x)
-------- + 2 x
2 sin(2 x) x
-------------- - -------- + -
8 2 2
(%o1) -----------------------------
2
While this 2D output is nice for immediate human consumption, it’s more difficult
to translate into HOL, either manually or automatically, so we disable the 2D form and
try again. In fact, I found the expanded form a bit more congenial:
(%i2) display2d:false;
(%o2) FALSE
(%i3) integrate(sin(x)ˆ4,x);
(%o3) ((sin(4*x)/2+2*x)/8-sin(2*x)/2+x/2)/2
(%i4) expand(integrate(sin(x)ˆ4,x));
(%o4) sin(4*x)/32-sin(2*x)/4+3*x/8
What we’ve got out of Maxima here may just be an answer, not a proof, but this an-
swer is a valuable “certificate” that we can turn into a proof with relatively little effort.
For by the Fundamental Theorem of Calculus, we can verify the answer by differenti-
ating and checking we get the original formula (basically). And differentiation is much
easier than integration, with an existing HOL conversion to do it.
let maximas e =
let filename = Filename.temp_file "maxima" ".out" in
let s =
"echo ’linel:10000; display2d:false;" ˆ e ˆ
";’ | maxima | grep ’ˆ(%o3)’ | sed -e ’s/ˆ(%o3) //’ >" ˆ
filename in
if Sys.command s <> 0 then failwith "maxima" else
let fd = open_in filename in
let data = input_line fd in
close_in fd; Sys.remove filename; data;;
209
We would really prefer to work at a more structured level where we pass and receive
a HOL term, not an unstructured string. So we need to resign ourselves to the slightly
tedious job of converting from a HOL term to a string understandable by Maxima, and
vice versa. We start with a couple of variables giving the correspondences between
Maxima string names and HOL functions:
# prioritize_real();;
# let maxima_ops = ["+",‘(+)‘; "-",‘(-)‘; "*",‘( * )‘; "/",‘(/)‘; "ˆ",‘(pow)‘];;
# let maxima_funs = ["sin",‘sin‘; "cos",‘cos‘];;
We’ll also need a few extra syntax functions for making and breaking apart unary
negations:
# let dest_uneg =
let ntm = ‘(--)‘ in
fun tm -> let op,t = dest_comb tm in
if op = ntm then t else failwith "dest_uneg";;
We also want a function for making a named variable of real type, and a slight twist
on making a binary operator xn . In the main recursion we’ll always create expressions
of real type, so to create xn we need to strip off the ‘&’ natural to real conversion first:
Now it’s fairly straightforward to map a HOL term to a string. We deal with nu-
meric constants (real and natural) and variables in one step, and recursively break apart
unary negations and other binary operators; if all else fails we break apart a combina-
tion and assume that it’s a unary operator applied to a single argument. We can easily
imagine some generalizations, but this will be adequate for what we want to do.
Note that we always put brackets round the string(s) resulting from recursive calls
inside negations and binary operators, and even around constants to avoid the danger
of 22/7 · x ending up as 22/(7 · x) etc. This is a bit ugly, but we take the position that
it’s harmless since all we’re going to do is feed the string to another program:
Going the other way, from a string to a HOL term, involves a bit more work. (The
reader might like to ignore the remainder of this subsection, since parsing is somewhat
210
far removed from our main theme, just a means to an end for us.) It’s conventional
to divide this kind of syntactic analysis into lexical analysis (splitting the input into
“words”) and parsing (converting a list of words into a structured object). For the
first part, the lexical conventions we need are close enough to HOL’s own quotation
parser that we can re-use its lexical analyzer, except that we throw away the distinction
between reserved words and ordinary identifiers:
# let lexe s = map (function Resword s -> s | Ident s -> s) (lex(explode s));;
val lexe : string -> string list = <fun>
Parsing functions will, in general, take a list of strings and return a pair, the first
being the HOL term constructed from it, and the second being the strings that were not
used. Although we expect no strings left over from the overall parsing, it’s useful to
allow sub-parsers to stop at some intermediate point. The parser will use various ‘parser
combinators’ which can be used to build parser out of simpler ones. For example, the
following applies a sub-arser prs and checks that the strings left over start with a
closing bracket; if so this is absorbed.
# let parse_bracketed prs inp =
match prs inp with
ast,")"::rst -> ast,rst
| _ -> failwith "Closing bracket expected";;
The trickiest bit of parsing is handling infix operators. The following highly parametrized
parser combinator is supposed to parse expressions built by an iterated infix operator
where the operator is called op and the subcomponents are parsed by a parser prs.
The two additional arguments opup and sof are used because we may want to build
up the term assuming either left or right associativity. The argument sof is a function
that is applied to the subexpression currently being parsed and builds up the overall
term, while opup determines how this is updated for each new operator.
# let rec parse_ginfix op opup sof prs inp =
match prs inp with
e1,hop::rst when hop = op -> parse_ginfix op opup (opup sof e1) prs rst
| e1,rest -> sof e1,rest;;
For our purposes, we determine the operator by its (Maxima) name op and de-
duce the corresponding HOL syntax constructor opcon. The opup function passed
to parse_ginfix is determined to apply this left-associatively or right-associatively
depending on the HOL associativity, and we start off with sof just the identity func-
tion.
let parse_general_infix op =
let opcon = if op = "ˆ" then mk_pow else mk_binop (assoc op maxima_ops) in
let constr = if op <> "ˆ" & snd(get_infix_status op) = "right"
then fun f e1 e2 -> f(opcon e1 e2)
else fun f e1 e2 -> opcon(f e1) e2 in
parse_ginfix op constr (fun x -> x);;
Now we are ready for the overall expression parser parse_expression. This is
defined by mutual recursion with several other more limited expression-parsing func-
tions. The most basic, parse_atomic_expression, handles variables and nu-
meric constants, as well as a function applied to a bracketed argument and any expres-
sion at all enclosed in brackets (note the use of the parse_bracketed combinator
211
in both cases. The next function parse_exp parses an ‘exponential expression’,
i.e. an expression of the form a or an where a is an atomic expression. The next
function parse_neg parses an expression built from exponential expressions using
unary negation. Finally, the overall function parse_expression is defined by re-
cursively applying infix parsing for all the operators. (It won’t catch the exponential,
which was dealt with lower down.) Observe that the order of the operators in the
list maxima_ops determines the precedences, because the first will be the outermost
layer of recursion.
Finally, this function simply maps a string to a term. Perhaps we should check there
are no ignored words, but we just hope for the best.
For example:
26.3 Factoring
One natural case where Maxima can help is factoring. All we need to do is (i) feed
an expression to Maxima to factor, and (ii) check using REAL_RING that the factored
and original expressions are equal:
# let FACTOR_CONV tm =
let s = "factor("ˆstring_of_hol tmˆ")" in
let tm’ = hol_of_string(maximas s) in
REAL_RING(mk_eq(tm,tm’));;
# FACTOR_CONV ‘&1234567890‘;;
val it : thm = |- &1234567890 = &2 * &3 pow 2 * &5 * &3607 * &3803
univariate polynomials:
212
# FACTOR_CONV ‘x pow 6 - &1‘;;
val it : thm =
|- x pow 6 - &1 =
(x - &1) * (x + &1) * (x pow 2 - x + &1) * (x pow 2 + x + &1)
# let ANTIDERIV_CONV tm =
let x,bod = dest_abs tm in
let s = "integrate("ˆstring_of_hol bodˆ","ˆfst(dest_var x)ˆ")" in
let tm’ = mk_abs(x,hol_of_string(maximas s)) in
let th1 = CONV_RULE (NUM_REDUCE_CONV THENC REAL_RAT_REDUCE_CONV)
(SPEC x (DIFF_CONV tm’)) in
let th2 = REAL_RING(mk_eq(lhand(concl th1),bod)) in
GEN x (GEN_REWRITE_RULE LAND_CONV [th2] th1);;
However, once we start using functions with potential singularities, like ln(x) or
1/(x + 2), we need a bit more sophistication because in general side-conditions will
be needed. Even without singularities, we can still encounter problems, e.g.
If we trace through the steps of the function we can see where the problem arises:
213
# let tm = ‘\x. sin(x) pow 3‘;;
val tm : term = ‘\x. sin x pow 3‘
# let x,bod = dest_abs tm;;
val x : term = ‘x‘
val bod : term = ‘sin x pow 3‘
# let s = "integrate("ˆstring_of_hol bodˆ","ˆfst(dest_var x)ˆ")";;
val s : string = "integrate((sin(x) ˆ 3),x)"
# let tm’ = mk_abs(x,hol_of_string(maximas s));;
val tm’ : term = ‘\x. cos x pow 3 / &3 - cos x‘
# let th1 = CONV_RULE (NUM_REDUCE_CONV THENC REAL_RAT_REDUCE_CONV)
(SPEC x (DIFF_CONV tm’));;
val th1 : thm =
|- ((\x. cos x pow 3 / &3 - cos x) diffl
(((&3 * cos x pow 2) * --sin x * &1) * &3 - &0 * cos x pow 3) / &9 -
--sin x * &1)
x
# let th2 = REAL_RING(mk_eq(lhand(concl th1),bod));;
Exception: Failure "EQ_MP".
# mk_eq(lhand(concl th1),bod);;
val it : term =
‘(((&3 * cos x pow 2) * --sin x * &1) * &3 - &0 * cos x pow 3) / &9 -
--sin x * &1 =
sin x pow 3‘
The problem is that this equation isn’t simply an algebraic identity. As with the ear-
lier example involving Chebyshev polynomials, we need to use the algebraic relation
between sin(x) and cos(x), and in more general cases we also need to use multiple-
angle formulas to relate sin(2x) to sin(x) and cos(x). These multiple-angle formulas
can be expressed as a rewrite rule if we slightly trickily use the internal representation
of numerals as binary numbers, where BIT0(n) = 2n and BIT1(n) = 2n + 1 are the
internal constants used:
Now we can produce a more general equation-prover that will take into account all
this and apply other routine simplifications then use sin(x)2 + cos(x)2 = 1 in the final
proof:
# let TRIG_IDENT_TAC x =
REWRITE_TAC[SIN_N_CLAUSES; SIN_ADD; COS_ADD] THEN
REWRITE_TAC[REAL_MUL_LZERO; SIN_0; COS_0; REAL_MUL_RZERO] THEN
MP_TAC(SPEC x SIN_CIRCLE) THEN CONV_TAC REAL_RING;;
214
Inserting this into our function we get:
# let ANTIDERIV_CONV tm =
let x,bod = dest_abs tm in
let s = "expand(integrate("ˆstring_of_hol bodˆ","ˆfst(dest_var x)ˆ"))" in
let tm’ = mk_abs(x,hol_of_string(maximas s)) in
let th1 = CONV_RULE (NUM_REDUCE_CONV THENC REAL_RAT_REDUCE_CONV)
(SPEC x (DIFF_CONV tm’)) in
let th2 = prove(mk_eq(lhand(concl th1),bod),TRIG_IDENT_TAC x) in
GEN x (GEN_REWRITE_RULE LAND_CONV [th2] th1);;
and now we find we can handle most elementary cases of trigonometric polynomials:
Those were still, properly speaking, antiderivatives rather than integrals. But we
can solve integrals using only a bit more code, by using the Fundamental Theorem of
Rb
Calculus a f 0 (x) dx = f (b) − f (a):
# FTC1;;
val it : thm =
|- !f f’ a b.
a <= b /\ (!x. a <= x /\ x <= b ==> (f diffl f’ x) x)
==> defint (a,b) f’ (f b - f a)
Note that the precise form of this theorem depends on the particular notion of inte-
gration. In the HOL analysis theory, the Kurzweil-Henstock integral is defined (DePree
and Swartz 1988) and this obeys the above theorem. For the Riemann or Lebesgue in-
tegrals some additional hypothesis is needed, for example continuity of the integrand.
Anyway, we’ll just use the special case a = 0:
and so we can elaborate the antiderivative conversion into one for definite integrals
essentially by matching against this theorem:
# let INTEGRAL_CONV tm =
let th1 = MATCH_MP FCT1_WEAK (ANTIDERIV_CONV tm) in
(CONV_RULE REAL_RAT_REDUCE_CONV o
REWRITE_RULE[SIN_0; COS_0; REAL_MUL_LZERO; REAL_MUL_RZERO] o
CONV_RULE REAL_RAT_REDUCE_CONV o BETA_RULE) th1;;
for example:
215
# INTEGRAL_CONV ‘\x. sin(x) pow 13‘;;
...
val it : thm =
|- !x. &0 <= x
==> defint (&0,x) (\x. sin x pow 13)
((--(cos x pow 13) / &13 +
&6 * cos x pow 11 / &11 - &5 * cos x pow 9 / &3 +
&20 * cos x pow 7 / &7 - &3 * cos x pow 5 +
&2 * cos x pow 3 - cos x) -
-- &1024 / &3003)
A.1 LCF
Yet another proof checking system was designed by Robin Milner (1972) and his assis-
tants at Stanford. This one was distinguished by its focus on computer science appli-
cations, rather than mainstream pure mathematics, being based on what Milner called
the ‘Logic of Computable Functions’. This is a formalism devised by Dana Scott in
196949 for reasoning about recursively defined functions on CPOs, such as typically
occur in the Scott-Strachey approach to denotational semantics. The system, known as
‘Stanford LCF’, was merely a proof checker, though by employing a backward, sub-
goaling style of proof it helped to organize the user’s work; it was also equipped with
a powerful automatic simplifier.
There were two major problems with Stanford LCF. First, the storage of proofs
tended to fill up memory very quickly. Second, the repertoire of proof commands was
fixed and could not be customized. When he moved to Edinburgh, Milner set about
fixing these defects. With the aid of his research assistants, he designed a new system
called Edinburgh LCF (Gordon, Milner, and Wadsworth 1979), which began a new
era in interactive theorem proving. To allow full customizability, Edinburgh LCF was
embedded in a general programming language, ML.50 This language was invented as
49 Though only published much later (Scott 1993).
50 ML for metalanguage; following Tarski (1936) and Carnap (1937), it has become usual to enforce a
strict separation between the object logic and the metalogic (which is used to reason about the object logic).
216
part of the LCF project specifically for the purpose of writing proof procedures, and
OCaml is one of its most successful descendants.
In LCF, recursive (tree-structured) types are defined in the ML metalanguage to
represent the (object) logical entities such as types, terms, formulas and theorems. Then
an underlying ‘machine code’ of logical inference rules is defined. Since all theorems
have type thm, these inference rules are simply functions that return something of this
type. For example, the rule for introducing a conjunction might have type ‘thm×thm →
thm’, and given a pair of theorems Γ1 ` φ1 and Γ2 ` φ2 , return a new theorem
Γ 1 ∪ Γ 2 ` φ1 ∧ φ2 .
Now one can, if one wishes, create arbitrary proofs using these logical inference
rules, simply by composing the ML functions appropriately. Although a proof is always
performed, the proof itself exists only ephemerally, and therefore no longer fills up
memory. Gordon makes a nice analogy with writing out a proof on a blackboard, and
rubbing out early steps to make room for more. In order to retain a guarantee that
objects of type thm really were created by application of the primitive rules, Milner
had the ingenious idea of making thm an abstract type, with the primitive rules as
its only constructors. After this, one simply needs to have confidence in the fairly
small amount of code underlying the primitive inference rules to be quite sure that all
theorems must have been properly deduced simply because of their type.
Performing proofs at this level is usually too tedious for the user. However because
of the availability of a flexible programming language, one can write arbitrary custom
procedures on top that implement much higher-level proof procedures while decom-
posing internally to the primitive inferences. (Notably, one can implement backward
proof in the style of Stanford LCF, by using higher order function closures to ‘remem-
ber’ the sequence of forward proof steps necessary to bring the user from the current
subgoals to the original goal.) This gives LCF a unique combination of reliability and
extensibility. In most theorem proving systems, in order to install new facilities it is
necessary to modify the basic code of the prover, or at best to use some rather com-
plicated and difficult techniques relying on code verification (Boyer and Moore 1981).
But in LCF an ordinary user can write an arbitrary ML program to automate a useful
inference pattern, while all the time being assured that even if the program has bugs, no
false ‘theorems’ will arise (though the program may fail in this case). As Slind (1991)
puts it ‘the user controls the means of (theorem) production’.
LCF was employed in several applications at Edinburgh, and this motivated certain
developments in the system. For example Sokołowski (1983) incorporated a scheme for
‘logic variables’, allowing the instantiation of existential goals to be deferred. By now,
the system had attracted attention elsewhere. Edinburgh LCF was ported to LeLisp and
MacLisp by Gérard Huet, and this formed the basis for a rationalization and redevel-
opment of the system by Paulson (1987) at Cambridge, resulting in Cambridge LCF.
First, Huet and Paulson made LCF compilable, which greatly improved performance.
Paulson (1983) replaced Edinburgh LCF’s complicated and monolithic simplifier with
an elegant scheme using higher order functions to build up theorems by applying the
basic congruence rules. Among many other improvements, Paulson introduced dis-
crimination nets to handle lookup in large sets of rewrite rules in an efficient way.
217
A.2 HOL
As emphasized by Gordon (1982), despite the name ‘LCF’, nothing in the Edinburgh
LCF methodology is tied to the Logic of Computable Functions. In the early 80s Gor-
don, in Cambridge, as well as supervising the development of LCF, was interested
in the formal verification of hardware. For this purpose, classical higher order logic
seemed a natural vehicle, since it allows a rather direct rendering of notions like sig-
nals as functions from time to values. The case was first put by Hanna and Daeche
(1986) and, after a brief experiment with an ad hoc formalism ‘LSM’ based on Milner’s
Calculus of Communicating Systems, Gordon (1985) also became a strong advocate.
Gordon modified Cambridge LCF to support classical higher order logic, and so
HOL (for Higher Order Logic) was born. Following Church (1940), the system is based
on simply typed λ-calculus, so all terms are either variables, constants, applications
or abstractions; there is no distinguished class of formulas, merely terms of boolean
type. The main difference from Church’s system is that polymorphism is an object-
level, rather than a meta-level, notion. Using defined constants and a layer of parser
and prettyprinter support, many standard syntactic conventions are broken down to λ-
calculus. For example, the universal quantifier, following Church, is simply a higher
order function, but the conventional notation ∀x. P [x] is supported, mapping down to
∀(λx. P [x]). Similarly there is a constant LET, which is semantically the identity and
is used only as a tag for the prettyprinter, and following Landin (1966), the construct
‘let x = t in s’ is broken down to ‘LET (λx. s) t’.51 The advantage of keeping the
internal representation simple is that the underlying proof procedures, e.g. those that
do term traversal during simplification, need only consider a few cases.
The exact axiomatization of the logic was partly influenced by Church, partly by
the way things were done in LCF, and partly through consultation with the logicians
Mike Fourman and Martin Hyland in Cambridge. I will discuss the axiomatization in
more detail when I contrast the HOL Light version. HOL originally included a simple
constant definitional mechanism, allowing new equational axioms of the form ` c = t
to be added, where t is a closed term and c a new constant symbol. A mechanism
for defining new types, due to Mike Fourman, was also included. Roughly speaking
one may introduce a new type in bijection with any nonempty subset of an existing
type (identified by its characteristic predicate). An important feature of these defini-
tional mechanisms bears emphasizing: they are not metalogical translations, but means
of extending the signature of the object logic, while guaranteeing that such extension
preserves consistency. HOL emphasizes this approach to theory development. It is
consonant with the LCF philosophy, since it entails pushing back the burden of con-
sistency proofs or whatever to the beginning, once and for all, such that all extensions,
whether of the theory hierarchy or proof mechanisms, are correct per construction. But
in LCF, it was common to axiomatize new types, leading to the possibility of incon-
sistent axioms; the advantages of postulation over definition were likened by Russell
(1919) to those of theft over honest toil.
51 Landin, by the way, is credited with inventing the term ‘syntactic sugar’, as well as this notable example
of it.
218
A.3 Development and applications
Development of HOL continued at Cambridge. Notably Melham (1989) introduced
a new facility for automating recursive type definitions. Although from the user’s
perspective, it provided the convenience of a direct axiomatization, it automatically
constructed a model for the new type and defined it using the standard definitional
mechanisms. By pre-storing a rather general theorem about finitely branching trees,
and instantiating it on each call, the internal model-building was made quite fast. This
was the first of a number of derived definitional principles that would follow over the
years. It was practically useful for modelling hardware structures and language syn-
taxes, and also methodologically valuable in showing that fairly sophisticated derived
rules in LCF really could be written, and efficiently at that. Melham continued to add
new facilities to HOL, and to improve the existing ones.
The HOL system was also used for hardware verification research. Prominent were
Gordon’s research students in Cambridge, with John Herbert submitting the first HOL-
based PhD thesis in 1986 (‘Application of Formal Methods to Digital System Design’);
he was followed by Inder Dhingra (‘Formalising an Integrated Circuit Design Style
in Higher Order Logic’) and Albert Camilleri (‘Executing Behavioural Definitions in
Higher Order Logic’) in 1988, and by Tom Melham (‘Formalizing Abstraction Mecha-
nisms for Hardware Verification in Higher Order Logic’) and Jeff Joyce (‘Multi-Level
Verification of Microprocessor-Based Systems’) in 1990. Research at the same level
was also taking place elsewhere in the world. In 1990 Phil Windley at UC Davis got
his PhD (‘The Formal Verification of Generic Interpreters’), and Brian Graham at the
University of Calgary his Master’s degree, ‘SECD: The Design and Verification of a
Functional Microprocessor’ (Graham 1992).
The system was consolidated and rationalized in a major release in late 1988, which
was called, accordingly, HOL88. HOL had now acquired a small but enthusiastic and
talented following around the world. In 1988, at the end of August, Phil Windley
started the info-hol email list, which helped to engender a sense of community
spirit. There was an international Users Meeting in 1988, held in Cambridge (UK);
these became an annual fixture, with the 1989 meeting also being held in Cambridge
and the 1990 meeting in Aarhus in Denmark. These gradually became transformed into
major conferences, with all contributions refereed and published through mainstream
publishers (the IEEE, Kluwer, and thereafter Springer-Verlag in the LNCS series).
By this time, HOL had attracted more international attention, especially from mil-
itary and government agencies concerned with safety-critical systems. The Australian
DSTO (Defence Science and Technology Organization) provided funding for important
developments, including a port to Common Lisp, research on improved user interfaces,
and a major documentation effort. This last project (1990/91), resulted in three high-
quality manuals, including a tutorial, detailed set-theoretic semantics of the logic, and
a reference entry for each of the hundreds of ML identifiers in the core system. HOL
could at last be seen as a usable off-the-shelf theorem prover, rather than an invitation
to an interesting collaborative programming exercise.
At the same time, there was funding for several other major new HOL projects.
There was a particular flurry of interest in the formal semantics of hardware description
languages. The US Air Force sponsored John Van Tassel’s PhD, studying the seman-
219
tics of VHDL. A UK-funded project (Richard Boulton, Mike Gordon, John Herbert
and later the present author were involved) aimed at a semantics for ELLA, a hard-
ware description language from the UK Defence Research Agency, then called RSRE.
European-funded projects looked at the SILAGE hardware description language (Andy
Gordon) and the Cathedral silicon compiler (Catia Angelo).
The major technique for embedding the semantics of simple programming lan-
guages and hardware description languages is to carry to an extreme the process of
building up layers of syntactic sugar. That is, program constructs are associated di-
rectly with logical entities, and the notation is merely a convenience. This fits naturally
with the view, expressed for example by Dijkstra (1976), that a programming language
should be thought of first and foremost as an algorithm-oriented system of mathemati-
cal notation, and only secondarily as something to be run on a machine. In a classic pa-
per, Gordon (1989) showed how a simple imperative programming language could be
semantically embedded in higher order logic in such a way that the classic Floyd-Hoare
rules simply become derivable theorems. The same approach was used to mechanize
a far more advanced theory of program refinement by Wright, Hekanaho, Luostari-
nen, and Langbacka (1993), and for many of the hardware description languages just
cited. However for some purposes, it seems better to embed the syntax of the language
and the denotation function itself formally within the logic. Boulton, Gordon, Gordon,
Harrison, Herbert, and Van Tassel (1993) describe some of the research on hardware
description languages using both these different techniques.
There is no space here to discuss all the fields to which HOL was applied in this
period.52 Some of them are mentioned in passing, and sometimes illustrated, in what
follows. The list includes pure mathematics (group theory, real analysis, theory of
wellorderings), and the semantics of programming languages, logical and computer
science formalisms (CCS, CSP, TLA, UNITY etc.) and hardware description lan-
guages. HOL has been augmented with various automated facilities, e.g. for Knuth-
Bendix completion (Slind 1991), Boyer-Moore style automation of induction proofs
(Boulton 1992), general first order logic (Kumar, Kropf, and Schneider 1991) and lin-
ear arithmetic (Boulton 1993), and new tools for inductively defined relations (Ander-
sen and Petersen 1991; Melham 1991).
220
by Konrad Slind (1991), under the supervision of Graham Birtwistle at the University
of Calgary. This was intended to be a public-domain replacement for HOL88. The
system was finally released in 1991, and steadily became the main version of HOL
worldwide. The set of ML functions and theorems was almost identical to that of
HOL88, so the main difference is in ML syntax. The entire system is coded in Standard
ML, which made all the prelogic operations such as substitution accessible.
Still, I felt that in the desire for close HOL88 compatibility, some opportunity had
been lost to improve the general organization of the system and correct some of the
design mistakes while keeping the substantial architectural improvements hol90 had
introduced. I discussed this with Konrad in Calgary in 1993, and we both agreed that
it would be interesting to start from scratch and isolate the essential core of the system.
Konrad pointed out that much of the complexity of the code came from supporting
theory files. Theory files stored new constants, definitions and the theorems derived
on disk, to avoid rerunning proofs. But they were a messy structure, trying to do
two separate jobs and doing neither of them well: provide internal organization of
theorems, and provide persistent storage of the proof state (theorems and signature)
between sessions. I had long harboured a desire to abolish them, and this swept the
problem away.
Both Konrad and I had plenty of preoccupations to keep us from actually doing
anything until early 1994. When Konrad was visiting Cambridge in early March, he
distilled the essential logical core of HOL, which was just a few hundred lines of Stan-
dard ML. I was delighted with how small and simple Konrad’s new version was. Ad-
mittedly this was just the core, without even parsing and printing support, but for the
first time I felt I was seeing the simplicity and elegance that an LCF system ought to
exhibit. As well as deleting inessentials, Konrad had made a few other neat simpli-
fications. Notably, the primitive syntax constructor for constants, mk_const, now
took a type substitution to apply to the generic type, rather than a desired type which
it would attempt to achieve by matching. This made the function much more efficient
and eliminated any use of type matching (admittedly not a very complex operation) in
the core.
I had meanwhile become interested in CAML Light. This is an interpreter for
another ML descendant, written at INRIA Rocquencourt in Paris. I found its syntax
to be an improvement on Classic ML and certainly Standard ML, and it provided all
the important extra features of later MLs, e.g. arrays. In addition, the system was a
remarkably lightweight bytecode interpreter, with many people running the system on
small PCs and the like.
I ported Konrad’s ‘core HOL’ to CAML Light, and wrote a crude parser and type-
checker and an even cruder printer to make the system usable. The typechecker was
ported directly from hol90, but with the important improvement that it was kept prop-
erly separate from the abstract type of HOL types. Similarly, I was determined that any
replacement for theory files that we might contemplate in the future would not interfere
with the basic core. I wanted to keep the logical core, the interface and the persistent
storage completely orthogonal.
Konrad dubbed the system ‘gtt’ for Gordon’s Type Theory. This name stuck for
some time, though Mike Gordon has persuaded us to call the system HOL Light. We
described it in our paper (Harrison and Slind 1994) in the 1994 HOL conference (sup-
221
plementary proceedings). This paper emphasized how GTT was an excellent platform
for experimentation with new ideas. Many HOL users, myself included, were critical
of certain features of the HOL88 and hol90 systems, and it was exciting to have a small,
simple version of HOL which one could easily modify and try to do better.
Over the next year, I occasionally worked on building up the logical system and
theory development of GTT. I also devoted a lot of thought to improving the axiomati-
zation of the system, and arrived at a scheme which I considered a big improvement. I
also thought of a way to rationalize the constant definition mechanism. It even proved
possible to delete one of the HOL axioms: the Law of Excluded Middle could be de-
duced from the Axiom of Choice in its HOL form. I reimplemented all the prelogic
completely, abandoning de Bruijn terms in favour of a return to name-carrying syntax.
(I experimented for a while with avoiding renaming altogether, but this proved unsuc-
cessful in practice.) I experimented with new ideas, just as my paper with Konrad
had anticipated, including simple higher order matching and a new representation for
numerals.
On the system front, I wrote a trivial filter to avoid explicit parser invocations (a
crude but simple solution to the lack of quotations in CAML Light), and Malcolm
Newey managed to run GTT on an Apple Macintosh with 2M of memory, fully justi-
fying my hopes of making a lightweight system. In a burst of enthusiasm for what I
had achieved, I wrote a draft called ‘HOL Done Right’53 for local circulation. At Mike
Gordon’s suggestion, I also sent this to the hol2000 mailing list devoted to future de-
velopments of HOL. The system was now developed enough to become my own main
vehicle. I ported to GTT, or redeveloped in GTT, much of my PhD work, used it to
explore Mizar-style proofs (Harrison 1996b), Stålmarck’s algorithm (Harrison 1996d)
and automated theorem proving. My use of HOL88 and hol90 gradually wound down.
I was happy to keep GTT as my own private project. My research did not rely on
any extensive collaboration with other HOL users, or on any theory developments that
were difficult to do myself. And it was good to be able to make radical changes without
worrying about upsetting a user base. My paper had stimulated some interest, but not
to the extent of making people insist to be allowed to use the system.
At this time, Don Syme had just started a PhD at Cambridge. After working on
hol90 improvements for a while, he also started to feel it was better just to start again
with something like GTT. Don created his own version of GTT in Standard ML, and
made numerous improvements that were fed back into my version. It is mainly thanks
to him that GTT attracted more attention in Cambridge. In view of the fact that several
people wanted to use it, if only to play with, I decided to release a preliminary version
in 1998.
Soon afterwards, I joined Intel, where I used HOL Light as the main tool in a num-
ber of industrial verification projects connected with floating-point algorithms (Harri-
son 2000b; Harrison 2000a; Harrison 2003). At this time, I stopped making radical
changes to the system, though I wrote many specialized HOL Light proof tools and
developed theories of floating-point numbers (neither in the public domain at present).
But in response to a few enthusiastic users, I ported the system from CAML Light to
Objective CAML and have continued to make a number of significant improvements.
53 Available on the Web from http://www.cl.cam.ac.uk/ jrh13/papers/holright.html.
222
The current version reflects these changes. I am very grateful to all those who have
made suggestions over the years, and to those who have contributed some of the code
and theories in the current version.
References
Aagaard, M. and Harrison, J. (eds.) (2000) Theorem Proving in Higher Order Logics:
13th International Conference, TPHOLs 2000, Volume 1869 of Lecture Notes in
Computer Science. Springer-Verlag.
Aichinger, E. (1994) Interpolation with Near-rings of Polynomial Functions. Ph. D.
thesis, Johannes Kepler Universität Linz. Author’s Diplomarbeit.
Andersen, F. and Petersen, K. D. (1991) Recursive boolean functions in HOL. See
Archer, Joyce, Levitt, and Windley (1991), pp. 367–377.
Archer, M., Joyce, J. J., Levitt, K. N., and Windley, P. J. (eds.) (1991) Proceedings
of the 1991 International Workshop on the HOL theorem proving system and its
Applications, University of California at Davis, Davis CA, USA. IEEE Computer
Society Press.
Baader, F. and Nipkow, T. (1998) Term Rewriting and All That. Cambridge University
Press.
Back, R. (1980) Correctness Preserving Program Transformations: Proof Theory and
Applications, Volume 131 of Mathematical Centre Tracts. Mathematical Centre,
Amsterdam.
Back, R.-J. and von Wright, J. (1998) Refinement Calculus: A Systematic Introduction.
Graduate texts in computer science. Springer-Verlag.
Barras, B. (2000) Programming and computing in HOL. See Aagaard and Harrison
(2000), pp. 17–37.
Birtwistle, G. and Subrahmanyam, P. A. (eds.) (1989) Current Trends in Hardware
Verification and Automated Theorem Proving. Springer-Verlag.
Boolos, G. S. (1995) The Logic of Provability. Cambridge University Press.
Boulton, R. (1992) Boyer-Moore automation for the HOL system. In Claesen, L. J. M.
and Gordon, M. J. C. (eds.), Proceedings of the IFIP TC10/WG10.2 International
Workshop on Higher Order Logic Theorem Proving and its Applications, Volume
A-20 of IFIP Transactions A: Computer Science and Technology, IMEC, Leuven,
Belgium, pp. 133–142. North-Holland.
Boulton, R., Gordon, A., Gordon, M., Harrison, J., Herbert, J., and Van Tassel, J.
(1993) Experience with embedding hardware description languages in HOL. In
Stavridou, V., Melham, T. F., and Boute, R. T. (eds.), Proceedings of the IFIP
TC10/WG 10.2 International Conference on Theorem Provers in Circuit Design:
223
Theory, Practice and Experience, Volume A-10 of IFIP Transactions A: Computer
Science and Technology, Nijmegen, The Netherlands, pp. 129–156. North-Holland.
Boulton, R. J. (1993) Efficiency in a fully-expansive theorem prover. Technical Report
337, University of Cambridge Computer Laboratory, New Museums Site, Pembroke
Street, Cambridge, CB2 3QG, UK. Author’s PhD thesis.
Clarke, E., Grumberg, O., and Hamaguchi, K. (1994) Another look at LTL model
checking. In Dill, D. L. (ed.), Proceedings of the Sixth International Conference on
Computer-Aided Verification (CAV’94), Volume 818 of Lecture Notes in Computer
Science, pp. 415–427. Springer-Verlag.
Colmerauer, A. (1990) An introduction to Prolog III. Communications of the ACM,
33(7), 69–90.
Cooper, D. C. (1972) Theorem proving in arithmetic without multiplication. In Melzer,
B. and Michie, D. (eds.), Machine Intelligence 7, pp. 91–99. Elsevier.
de Bruijn, N. G. (1980) A survey of the project AUTOMATH. In Seldin, J. P. and Hind-
ley, J. R. (eds.), To H. B. Curry: Essays in Combinatory Logic, Lambda Calculus,
and Formalism, pp. 589–606. Academic Press.
DePree, J. and Swartz, C. (1988) Introduction to Real Analysis. Wiley.
Devaney, R. L. (1987) An Introduction to Chaotic Dynamical Systems. Addison-
Wesley.
224
Dijkstra, E. W. (2000) The notational conventions I adopted, and why (EWD 1300).
Available on the Web as www.cs.utexas.edu/users/EWD/ewd13xx/
EWD1300.PDF.
Dijkstra, E. W. and Scholten, C. S. (1990) Predicate Calculus and Program Semantics.
Springer-Verlag.
225
Harrison, J. (1996a) HOL Light: A tutorial introduction. In Srivas, M. and Camilleri,
A. (eds.), Proceedings of the First International Conference on Formal Methods in
Computer-Aided Design (FMCAD’96), Volume 1166 of Lecture Notes in Computer
Science, pp. 265–269. Springer-Verlag.
Harrison, J. (1996b) A Mizar mode for HOL. See Wright, Grundy, and Harrison
(1996), pp. 203–220.
Harrison, J. (1996c) Proof style. In Giménez, E. and Paulin-Mohring, C. (eds.), Types
for Proofs and Programs: International Workshop TYPES’96, Volume 1512 of Lec-
ture Notes in Computer Science, Aussois, France, pp. 154–172. Springer-Verlag.
Harrison, J. (1996d) Stålmarck’s algorithm as a HOL derived rule. See Wright, Grundy,
and Harrison (1996), pp. 221–234.
Harrison, J. (2000a) Formal verification of floating point trigonometric functions. In
Hunt, W. A. and Johnson, S. D. (eds.), Formal Methods in Computer-Aided Design:
Third International Conference FMCAD 2000, Volume 1954 of Lecture Notes in
Computer Science, pp. 217–233. Springer-Verlag.
Harrison, J. (2000b) Formal verification of IA-64 division algorithms. See Aagaard
and Harrison (2000), pp. 234–251.
Harrison, J. (2003) Formal verification of square root algorithms. Formal Methods in
System Design, 22, 143–153.
Harrison, J. and Théry, L. (1998) A sceptic’s approach to combining HOL and Maple.
Journal of Automated Reasoning, 21, 279–294.
Henson, M. C. (1987) Elements of functional languages. Blackwell Scientific.
Hoare, C. A. R. (1969) An axiomatic basis for computer programming. Communica-
tions of the ACM, 12, 576–580, 583.
Homeier, P. V. (2005) A design structure for higher order quotients. In Hurd, J. and
Melham, T. (eds.), Theorem Proving in Higher Order Logics, 18th International
Conference, TPHOLs 2005, Volume 3603 of Lecture Notes in Computer Science,
Oxford, UK, pp. 130–146. Springer-Verlag.
Hurd, J. (1999) Integrating Gandalf and HOL. In Bertot, Y., Dowek, G., Hirschowitz,
A., Paulin, C., and Théry, L. (eds.), Theorem Proving in Higher Order Logics: 12th
International Conference, TPHOLs’99, Volume 1690 of Lecture Notes in Computer
Science, Nice, France, pp. 311–321. Springer-Verlag.
226
Johnson, S. D., Miner, P. S., and Camilleri, A. (1994) Studies of the single pulser in
various reasoning systems. In Kumar, R. and Kropf, T. (eds.), Theorem Provers in
Circuit Design — Theory, Practice and Experience, Second International Confer-
ence, TPCD ’94, Volume 901 of Lecture Notes in Computer Science, pp. 126–145.
Springer-Verlag.
Knaster, B. (1927) Un théorème sur les fonctions d’ensembles. Annales de la Société
Polonaise de Mathématique, 6, 133–134. Volume published in 1928.
Knuth, D. and Bendix, P. (1970) Simple word problems in universal algebras. In Leech,
J. (ed.), Computational Problems in Abstract Algebra. Pergamon Press.
Kochen, S. and Specker, E. (1967) The problem of hidden variables in quantum me-
chanics. Journal of Mathematics and Mechanics, 17, 59–87.
Kripke, S. (1963) Semantical considerations on modal logics. Acta Philosophica Fen-
nica, 16, 83–94.
Kumar, R., Kropf, T., and Schneider, K. (1991) Integrating a first-order automatic
prover in the HOL environment. See Archer, Joyce, Levitt, and Windley (1991),
pp. 170–176.
Lagarias, J. (1985) The 3x + 1 problem and its generalizations. The American Mathe-
matical Monthly, 92, 3–23. Available on the Web as http://www.cecm.sfu.
ca/organics/papers/lagarias/index.html.
Lamport, L. (1994) The temporal logic of actions. ACM Transactions on Programming
Languages and Systems, 16, 872–923.
Landin, P. J. (1966) The next 700 programming languages. Communications of the
ACM, 9, 157–166.
Lewis, C. I. and Langford, C. H. (1932) Symbolic Logic (2nd ed.). The Century Co.
Loveland, D. W. (1968) Mechanical theorem-proving by model elimination. Journal
of the ACM, 15, 236–251.
Loveland, D. W. (1984) Automated theorem-proving: A quarter-century review. In
Bledsoe, W. W. and Loveland, D. W. (eds.), Automated Theorem Proving: After 25
Years, Volume 29 of Contemporary Mathematics, pp. 1–45. American Mathematical
Society.
MacKenzie, D. (1995) The automation of proof: A historical and sociological explo-
ration. IEEE Annals of the History of Computing, 17(3), 7–29.
Markstein, P. (2000) IA-64 and Elementary Functions: Speed and Precision. Prentice-
Hall.
Martin, U. and Nipkow, T. (1990) Ordered rewriting and confluence. In Stickel, M. E.
(ed.), 10th International Conference on Automated Deduction, Volume 449 of Lec-
ture Notes in Computer Science, Kaiserslautern, Federal Republic of Germany, pp.
366–380. Springer-Verlag.
227
McLaughlin, S. and Harrison, J. (2005) A proof-producing decision procedure for real
arithmetic. In Nieuwenhuis, R. (ed.), CADE-20: 20th International Conference
on Automated Deduction, proceedings, Volume 3632 of Lecture Notes in Computer
Science, Tallinn, Estonia, pp. 295–314. Springer-Verlag.
Melham, T. F. (1989) Automating recursive type definitions in higher order logic. See
Birtwistle and Subrahmanyam (1989), pp. 341–386.
Melham, T. F. (1991) A package for inductive relation definitions in HOL. See Archer,
Joyce, Levitt, and Windley (1991), pp. 350–357.
Mendelson, E. (1987) Introduction to Mathematical Logic (Third ed.). Mathematics
series. Wadsworth and Brooks Cole.
Milner, R. (1972) Implementation and applications of Scott’s logic for computable
functions. ACM SIGPLAN Notices, 7(1), 1–6.
Moore, G. H. (1982) Zermelo’s axiom of choice: its origins, development, and in-
fluence, Volume 8 of Studies in the history of mathematics and physical sciences.
Springer-Verlag.
Morgan, C. (1988) The specification statement. ACM Transactions on Programming
Languages and Systems, 10, 403–419.
Muller, J.-M. (2006) Elementary functions: Algorithms and Implementation (2nd ed.).
Birkhäuser.
Nipkow, T. (1996) Winskel is (almost) right: Towards a mechanized semantics text-
book. In Chandru, V. and Vinay, V. (eds.), Foundations of Software Technology
and Theoretical Computer Science, 16th conference, proceedings, Lecture Notes in
Computer Science, pp. 180–192. Springer-Verlag.
Obua, S. (2005) Proving bounds for real linear programs in Isabelle/HOL. TPHOLs
2005, to appear.
Parrilo, P. A. (2003) Semidefinite programming relaxations for semialgebraic prob-
lems. Mathematical Programming, 96, 293–320.
Paulson, L. C. (1983) A higher-order implementation of rewriting. Science of Com-
puter Programming, 3, 119–149.
Paulson, L. C. (1987) Logic and computation: interactive proof with Cambridge LCF,
Volume 2 of Cambridge Tracts in Theoretical Computer Science. Cambridge Uni-
versity Press.
Peres, A. (1991) Two simple proofs of the Kochen-Specker theorem. Journal of
Physics, A24, 175–178.
Ridge, T. (2005) A mechanically verified, efficient, sound and complete theorem
prover for first order logic. Available via http://homepages.inf.ed.ac.
uk/s0128214/.
228
Rivest, R. L., Shamir, A., and Adelman, L. M. (1978) A method for obtaining digital
signatures and public-key cryptosystems. Journal of the ACM, 21, 120–126.
Rudnicki, P. (1987) Obvious inferences. Journal of Automated Reasoning, 3, 383–393.
Rudnicki, P. (1992) An overview of the MIZAR project. Available on
the Web as http://web.cs.ualberta.ca/˜piotr/Mizar/Doc/
MizarOverview.ps.
Russell, B. (1919) Introduction to mathematical philosophy. Allen & Unwin.
Scott, D. (1993) A type-theoretical alternative to ISWIM, CUCH, OWHY. Theoretical
Computer Science, 121, 411–440. Annotated version of a 1969 manuscript.
Scott, D. and Strachey, C. (1971) Toward a mathematical semantics for computer lan-
guages. Programming Research Group Technical Monograph PRG-6, Oxford Univ.
Computing Lab.
Slind, K. (1991) An implementation of higher order logic. Technical Report 91-419-
03, University of Calgary Computer Science Department, 2500 University Drive N.
W., Calgary, Alberta, Canada, TN2 1N4. Author’s Masters thesis.
Slind, K. (1997) Derivation and use of induction schemes in higher-order logic. In
Gunter, E. L. and Felty, A. (eds.), Theorem Proving in Higher Order Logics: 10th
International Conference, TPHOLs’97, Volume 1275 of Lecture Notes in Computer
Science, Murray Hill, NJ, pp. ?–? Springer-Verlag.
Smullyan, R. M. (1992) Gödel’s Incompleteness Theorems, Volume 19 of Oxford Logic
Guides. Oxford University Press.
Sokołowski, S. (1983) A note on tactics in LCF. Technical Report CSR-140-83, Uni-
versity of Edinburgh, Department of Computer Science.
229
Wiedijk, F. (2001) Mizar light for HOL Light. In Boulton, R. J. and Jackson, P. B.
(eds.), 14th International Conference on Theorem Proving in Higher Order Logics:
TPHOLs 2001, Volume 2152 of Lecture Notes in Computer Science, pp. 378–394.
Springer-Verlag.
Winskel, G. (1993) The formal semantics of programming languages: an introduction.
Foundations of computing. MIT Press.
Wos, L. (1998) Programs that offer fast, flawless, logical reasoning. Communications
of the ACM, 41(6), 87–102.
Wos, L. and Pieper, G. W. (1999) A Fascinating Country in the World of Computing:
Your Guide to Automated Reasoning. World Scientific.
Wright, J. v., Grundy, J., and Harrison, J. (eds.) (1996) Theorem Proving in Higher
Order Logics: 9th International Conference, TPHOLs’96, Volume 1125 of Lecture
Notes in Computer Science, Turku, Finland. Springer-Verlag.
Wright, J. v., Hekanaho, J., Luostarinen, P., and Langbacka, T. (1993) Mechanizing
some advanced refinement concepts. Formal Methods in System Design, 3, 49–82.
230