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.