Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A Metacircular Interpreter

Pink is an interpreter for λ↑↓ written in λ↑↓. On its own that is a standard construction — the metacircular interpreter of every Lisp textbook. What Part III shows is what staging does to the construction: because the interpreter is written in a language with lift, the interpreter can be made to compile the programs it interprets, and then to compile itself. This chapter reads the interpreter; the next two stage it.

The chapters in this part use the Purple session (narju at the shell, the default REPL). The session’s own mechanics — define, load, the tower it runs on — are Part IV’s subject; for now it is just the place where the Pink bindings live.

Programs as data, environments as functions

Pink’s programs are quoted s-expressions and its environments are functions from symbol to value. pink-eval takes both:

((pink-eval '(+ 1 2)) nil-env)
;=> 3
((pink-eval '(let x 5 (+ x 1))) nil-env)
;=> 6
((pink-eval '(cons 1 2)) nil-env)
;=> (1 . 2)
((pink-eval '(quote (a b))) nil-env)
;=> ('a 'b)

nil-env is the session’s name for the empty environment, (lambda _ y 0) — every lookup answers 0. The reference implementation uses the same silent zero, which makes an unbound variable indistinguishable from a bound zero:

((pink-eval 'x) nil-env)
;=> 0

Binding forms extend the environment functionally: eval-let wraps the incoming env in a new function that answers the binder’s name and defers everything else. There is no environment data structure to inspect, only a chain of closures.

The interpreted language is the staged core of λ↑↓ — the forms of Parts I and II except cells, throw/catch, and I/O other than log. Recursion needs no special treatment; a recursive function runs as written —

((pink-eval '((lambda f n (if (eq? n 0) 1 (* n (f (- n 1))))) 5)) nil-env)
;=> 120

The shape of the source

The interpreter is one closed expression in lib/pink-forms.naj: a let-chain of named handlers ending in a small dispatch function. Each handler has the same curried signature — it receives tie and eval (explained below), the stage parameter l (next chapter), the expression, and the environment. A representative pair:

(let eval-plus
  (lambda _ tie (lambda _ eval (lambda _ l (lambda _ exp (lambda _ env
    (+ (((eval l) (cadr exp)) env)
       (((eval l) (caddr exp)) env)))))))

(let eval-if
  (lambda _ tie (lambda _ eval (lambda _ l (lambda _ exp (lambda _ env
    (if (((eval l) (cadr exp)) env)
        (((eval l) (caddr exp)) env)
        (((eval l) (cadddr exp)) env)))))))

Each form of the object language is interpreted by the same form of the meta language: Pink’s + is the floor’s +, Pink’s if is the floor’s if. This is the metacircular bargain — the interpreter is short because it inherits the semantics it implements — and it is also, in Part II’s terms, exactly what makes the interpreter stageable: handlers written against ordinary values work unchanged when the values are code.

base-eval is the dispatch: numbers are constants, symbols are variable lookups, and a pair dispatches on its head through a chain of eq? tests to the matching handler. After the special forms comes application. The final expression of the chain ties the knot:

(lambda tie eval
  (lambda _ l
    (lambda _ exp
      (lambda _ env
        (((((base-eval tie) eval) l) exp) env)))))

The self-name tie is the interpreter’s own recursion — every handler’s eval is this function — and it is also the seam at which the interpreter can be replaced (below).

Booting from data

The session gets pink-eval by reading lib/pink-forms.naj as data and animating it with the floor’s trans/evalms primitives (the floor reference lists both): read-file produces the expression as a list, trans translates it to a code value, evalms evaluates that under an empty environment. The interpreter’s source exists exactly once, in that file; the session also keeps it as plain data:

(car pink-poly-src)
;=> 'let

Programs, interpreters, and the sources of interpreters are all the same kind of value — quoted lists — which is the precondition for everything in the Futamura chapter.

Extending the evaluator

delta-eval is a Pink form that runs a subprogram under a modified interpreter. Its first operand evaluates to an extension: a function that receives tie — the unmodified dispatch — and returns a replacement evaluator, deferring to tie for whatever it does not change. An extension that logs every variable lookup:

((pink-eval '(delta-eval (lambda _ tie (lambda _ eval (lambda ev l (lambda _ exp (lambda _ env (if (symbol? exp) (log 0 (((eval l) exp) env)) ((((tie ev) l) exp) env))))))) (let x 3 (+ x x)))) nil-env)
;=> [log] 3
;=> [log] 3
;=> 6

The two [log] lines are the two lookups of x; the sum still comes out. Nothing about the interpreter was anticipated for this — the extension point is just the tie argument every handler already threads. The matcher chapter’s reference implementation uses the same mechanism to build a tracing matcher.