A lot of people have written little interpreters or emulators, but automated theorem provers have been conspicously neglected. In this series of
The phrase automated theorem prover itself is intimidating. All three of the words are scary! Let’s break it down.
- Automated. This part is a bit nebulous. An automated theorem prover cannot exist in a vacuum. We have to start with something. The idea is to write a computer program that can take some specification of initial facts and rules and then be able to answer queries (questions) we give it based on the data the program is able to extract from the specification.
-
Theorem. This is the easiest. A theorem is just a mathematical fact for which we have a proof. That last part is important. If we don’t have a proof, we don’t (yet) have a theorem. Indeed, without a proof we can even question whether or not we can even Call it a fact. Mathematicians Call “facts” without proofs conjectures. Normal people Call them guesses. Theorems cannot exist in a vacuum, either. We have to know the initial facts and the rules we are allow to use to manipulate those facts. In informal settings, the facts and rules are often known from context. Here is a typical theorem: “If
$x$ and$y$ are both odd integers, then$x+y$ is an even integer." - Proof. A proof is a step-by-step application of rules to known facts, each of which is irrefutable, to establish some mathematical fact. It is analogous to a recipe for a chocolate cake. The ingredients are the facts you start with, what mathematicians Call axioms, and the activities like mixing and preheating the oven are the rules. At the end of the process of following the recipe, we get our theorem: a chocolate cake. An important feature of this process is that new facts are generated along the way. In our cake analogy we get new ingredients that result from acting on existing ingredients, the cake batter and frosting for example.
There are a few big ideas to wrap your mind around before we set out. Let’s get on the same page with these ideas. We will primarily be dealing with types and formal logic.
From now on, when we use the word type, we will mean what every programmer means when they talk about data types. Functions also have types, though in some languages the type of a function is instead called the function’s (type) signature. For example, the type of the (pseudocode) function
int f(int x){
return x + 7;
}
is
List<int> g(List<int> mylist, int (*h)(int) ){
return mylist.map(h);
}
This function takes both a list of integers and a function from integers to integers and produces another list of integers. It’s type is
As every object-oriented programmer knows, types can have subtypes and supertypes. In the last example, the type
I assume a basic knowledge of mathematical logic. Briefly, formal logic is a mathematical system formalizing operations on statements that can either be true or false. If
This is how we will state theorems in our theorem prover. Thus, our theorem prover will prove statements of formal logic. Specifically, our program will take as input a statement of the form
called a Horn clause, and attempt to prove the statement. In fact, it will do much more, but let’s not get too carried away right at the beginning.
There is a vast literature describing how such-and-such a mathematical system is equivalent in some way to another seemingly different system. Among the most famous and startling of these results is the Curry-Howard correspondence, which says roughly that proving a theorem in intuitionist logic is equivalent to constructing an object of a specified type.
Now that we know what we are attempting, we need to figure out how. Our strategy has the very fancy sounding name selective linear definite clause resolution with negation as failure. The algorithm that allows us to do this efficiently is called unification. It is not just a coincidence that unification is also how types can be inferred in some programming languages.
We will encode our initial facts in such a way that our conclusions, or goal clause, can be compiled into a program to compute, using the initial facts, a proof of the goal clause. We are trying to answer the question, “Given this set of facts, is this other fact true?” We Call this question, encoded into a statement of logic, the query. The computation that the compiled program does computes the answer to the query.
This is why we are actually building an interpreter—a virtual machine, actually—for a programming language. It turns out that the programming language will be Turing complete.
The program is a primitive interpreter running on a register based virtual machine. Most of the effort is spent on the basic parsing and tokenization facilities, as simple as they are. The compilation pipeline is:
text -> [`parser::parse`] -> `Term`s ->⋯
┌───────────────[`token::flatten_term`]────────────────┐
⋯->*│*-> [`TermIter`] -> `Cell`s -> [`order_registers`] ->*│*->⋯
└──────────────────────────────────────────────────────┘
⋯-> `Token`s -> [`compile_tokens`] -> `Cell`s/instructions ->⋯
⋯-> [`unify`] -> Success/Fail
The [unify]
step is actually part of [compile_tokens]
and, at first, interprets the instructions to build the in-memory Cell
s. The apparently redundant conversion to Cell
s, then Token
s, and back to Cell
s again is for two reasons:
- Because the term needs to be flattened and reordered, we would have the "conversion" step no matter what.
- Conversion from AST to
Cell
s allows us to build in-memory representations directly "by hand.”
Other reasonable designs are certainly possible. Indeed, I would love to see how you improve on this code. The concerns of tokenization and compilation/interpretation are separated: The token
module houses the code to generate the token stream, while the compiler/interpreter are functions of the virtual machine housed in wvm.rs
. The Term
, Cell
, and Token
intermediate representations are defined in their corresponding module source files, term.rs
, cell.rs
, and token.rs
respectively.
This whole project was inspired by Hassan Aït-kaci's charming book Warren's Abstract Machine: A Tutorial Reconstruction. The Wikipedia articles on these and related topics are generally very good and well cited.