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

Interpreters are values

an interpreter is an association list from handler name to handler:

narju> (map car (interpreter))
('base-eval 'eval-lit 'eval-var 'eval-quote 'eval-if 'eval-lambda 'eval-rlambda 'eval-clambda 'eval-let 'eval-import 'eval-args 'eval-app)

twelve entries, and that is the whole language. base-eval dispatches on the shape of a form and calls one of the others. every handler has the same arity:

(lambda h (m l e r k) ...)

m is the interpreter itself, l the stage dictionary, e the form, r the environment, k the continuation. m being an argument is the entire trick: a handler does not look up its evaluator in a global, it was handed one.

interp-of and with-interp

a closure carries the interpreter in force where it was written. interp-of reads that field, with-interp rebuilds the closure with a different one:

(define (inc n) (+ n 1))

(define (shifted i)
  (with-handler i 'eval-lit
    (lambda h (m l e r k) (apply-cont k ((l 'lift) (+ e 10))))))

(say (inc 0))
(say ((with-interp inc (shifted (interp-of inc))) 0))
(say (inc 0))
1
11
1

inc did not move. a change of semantics is a new closure, so everything else in scope still means what it meant.

with-handler conses the new entry onto the front rather than replacing, so the shadowed one is still reachable through handler-of. that is how to wrap instead of replace:

(define (traced i)
  (with-handler i 'eval-app
    (lambda h (m l e r k)
      (begin (say (car e))
             ((handler-of i 'eval-app) m l e r k)))))

(define (add a b) (+ (* a 2) b))

(say ((with-interp add (traced (interp-of add))) 3 4))
'+
'*
10

The boundary is the closure

(define (helper x) (* x 2))
(define (add a b) (+ (helper a) b))

(say ((with-interp add (traced (interp-of add))) 3 4))
'+
'helper
10

the trace names helper and stops. helper was written under the base interpreter and carries it, so applying it switches back. an alteration covers exactly the closure that was rebuilt, and spreading it means rebuilding the callees too.

interpreter and environment

both are reflective procedures, so they take no arguments and answer something about the call site:

narju> (length (interpreter))
12

narju> (length (environment))
3

narju> (eval-in (environment) '(+ 1 2))
3

eval without a magic global environment. the environment is a value obtained from somewhere, and without one there is nothing to evaluate in.

What is built on this

(define (with-handler i name h) (cons (cons name h) i))
(define (spawn-with mut f) (spawn (with-interp f (mut (interp-of f)))))
(define (become mut state) `(become ,mut ,state))

spawn-with starts a task whose semantics is (mut I) for the I that f was written under, so the child forks rather than shares. become does the same to a running task between turns. neither needed a primitive.