The term “metacircular evaluator” is a rather opaque way of saying “Lisp written in the Lisp you just wrote”, which is what we’re going to test out right now.
It’s a fun demonstration that you’ve build enough of an interpreter in OCaml (we’ll call it Lisp0) that you can build a shorter version of that same language, and extend it from there (we’ll call that Lisp1).
I’ve taken Paul Graham’s (henceforth written as “pg”) implementation of the metacircular evaluator and modified it slightly for our use case. It’s easy to read, and short enough to be understood fully in a sitting. I’ll walk through it function by function.
(define null. (x) (eq x '()))
This seems to be a reasonable way to check if a thing is
nil — check
equality using the built-in
(define and. (x y) (cond (x (cond (y #t) (#t #f))) (#t #f)))
pg’s Lisp uses
cond to write
and., but we ended up building that into
Lisp0 as a special form.
(define not. (x) (cond (x #f) (#t #t)))
pg’s Lisp uses
cond to invert the truthiness of
(val cons pair) (define append. (x y) (cond ((null. x) y) (#t (cons (car x) (append. (cdr x) y)))))
In order to give
list access to
pair, we have to make an alias and call it
cons. This is the simple non-tail-recursive version of
(define list. (x y) (cons x (cons y '())))
We could also just dispatch to the built-in
list function instead of using
(define pair. (x y) (cond ((and. (null. x) (null. y)) '()) ((and. (not. (atom? x)) (not. (atom? y))) (cons (list. (car x) (car y)) (pair. (cdr x) (cdr y))))))
Confusingly enough, pg’s Lisp defines a function named
pair. that zips two
lists together, like so:
> (pair. '(1 2 3) '(a b c)) ((1 a) (2 b) (3 c))
Note that the elements of the parent list are themselves lists, not just pairs.
I think it’ll be easier if we rename
(define o (f g) (lambda (x) (f (g x)))) (val caar (o car car)) (val cadar (o car (o cdr car)))
pg’s Lisp requires some functions like
caddr, that are defined to
(car (car x)) and
(car (cdr (cdr x))), respectively. I’ve added in a
o that does function composition, so that we can support
(define lookup. (key alist) (cond ((eq (caar alist) key) (cadar alist)) (#t (lookup. key (cdr alist)))))
lookup. looks up a variable in an environment. pg calls it
assoc., but we
might as well call it
lookup., since that’s what we use internally (in
OCaml). His environment uses a list of lists instead of a list of pairs and
(due to our implementation of
key is not found
The key to the interpreter is the
eval function. It’s definitely rather long
for a Lisp function, so I’m going to walk through it in comments. The only
thing, though, is that our Lisp reader does not understand comments — so
you can’t just copy and paste it into the Lisp0 shell.
; eval takes two parameters: an expression and an environment. It's like our ; evalexp. (define eval. (e env) ; There are a lot of cases to consider. This is like our large match ; expression. (cond ; If it's a symbol, look it up. This is different from pg's Lisp in that ; he *only* has symbols to work with. ((sym? e) (lookup. e env)) ; If it's some other type of atom, just leave it be. Let it self-evaluate. ((atom? e) e) ; If it's a list (the only alternative to being an atom), check if the ; first item is an atom. ((atom? (car e)) ; What kind of form is it? (cond ; Quote accepts one argument, so just return that argument as an ; unevaluated expression (note the lack of a recursive call to eval.). ((eq (car e) 'quote) (cadr e)) ; For atom?, eq, car, cdr, and cons, just evaluate the expression then ; pass it through to the built-in form. ((eq (car e) 'atom?) (atom? (eval. (cadr e) env))) ((eq (car e) 'eq) (eq (eval. (cadr e) env) (eval. (caddr e) env))) ((eq (car e) 'car) (car (eval. (cadr e) env))) ((eq (car e) 'cdr) (cdr (eval. (cadr e) env))) ((eq (car e) 'cons) (cons (eval. (cadr e) env) (eval. (caddr e) env))) ; For cond, it's a wee bit tricker. We get to this function a bit later. ((eq (car e) 'cond) (eval-cond. (cdr e) env)) ; ...else, try and evaluate the function as a user-defined function, ; applying it to the arguments. (#t (eval. (cons (lookup. (car e) env) (cdr e)) env)))) ; If it's a compound expression in which the first element is a ; label-expression, ((eq (caar e) 'label) ; ...evaluate the expression in environment with a new recursive binding. (eval. (cons (caddar e) (cdr e)) (cons (list. (cadar e) (car e)) env))) ; If it's a compound expression in which the first element is a ; lambda-expresison, ((eq (caar e) 'lambda) ; ...evaluate the application of the lambda to the given arguments, ; evaluating them. (eval. (caddar e) (append. (zip. (cadar e) (map-eval. (cdr e) env)) env))))) ; Some helpers... ; cond works by evaluating each of the conditions in order until it encounters ; a truthy one. (define eval-cond. (c env) ; If we have no more conditions left, there's an error. (cond ((null. c) 'error) ; If the current condition is true, evaluate that branch. (eval. (caar c) env) (eval. (cadar c) env)) ; Otherwise, keep going. (#t (eval-cond. (cdr c) env)))) ; This is a manually curried form of map. It runs eval over every element in a ; list using the given environment. (define map-eval. (exps env) (cond ((null. exps) '()) (#t (cons (eval. (car exps) env) (map-eval. (cdr exps) env)))))
And there we have it! Now, it took me a bit to understand exactly how to use
this so-called “metacircular evaluator”. It’s not obviously well-documented.
However, there is an excellent pdf
that helped me understand the finer points of
label is used much like our built-in
define, except that it requires manual
lambda. Here’s what I mean:
; Our Lisp (define fact (x) (cond ((< x 2) 1) (#t (* x (fact (- x 1)))))) ; pg's Lisp (label fact (lambda (x) (cond ((< x 2) 1) (#t (* x (fact (- x 1)))))))
While we could build in the nice syntactic transform that adds the
us, effectively turning
define, that’s apparently too much
effort. Oh well!
In any case, let’s try running this thing. A couple of notes:
loadfunction yet, so there’s no easy way to bring this in as a sort of library.
sym?primitive and use actual booleans instead of the symbol
So I’ve added the pass-throughs for
< so we can write
factorial. Here’s what it looks like:
$ cat 12_metacircular.lsp [...] (eval. '((label fact (lambda (x) (cond ((< x 2) 1) (#t (* x (fact (- x 1))))))) 5) '()) $ ocaml 12_metacircular.ml < 12_metacircular.lsp > #<closure> > #<closure> > #<closure> > #<closure> > #<primitive:pair> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > #<closure> > 120 > Exception: End_of_file. $
Yeah, that looks weird. Turns out
eval. can only evaluate one expression at a
time. Understandable. So
label is less of a
define and more of a “recursive
lambda”. It needs to be applied immediately — there’s no
val, there’s no
define, and it can’t stand alone and evaluate into a closure.
begin statement that allows for imperative programs would be another
feature, but we’ve chosen not to include it for simplicity’s sake.
And there you have it — reasonable proof that our Lisp implemementation can be considered feature complete. Now we can move on to features that make the language easier to use, like let.