The tower
naj/tower.naj is 572 lines of narju that evaluates narju. it is CPS, so a
continuation is an ordinary value, which is what lets call/cc hand one to an
object procedure without machinery.
value = number | symbol | nil | pair | floor closure | code
| ('clo self param body env . m)
| ('rclo params body env . m)
| ('cfun . f)
| ('cont f . h)
env = list of frames, a frame an immutable assoc list
m = assoc list from handler name to handler
l = concrete-l, or (staged-l frames)
Dispatch
everything goes through one function:
(define (meta m l name e r k)
(let ((h (mget m name)))
(if (has-tag? h 'clo)
(meta (clo-m h) concrete-l 'base-eval (clo-body h)
(clo-frame h (list m l e r k))
(cont-in k (lambda (v) v)))
(h m l e r k))))
two arms. a floor closure is applied. an object closure needs interpreting,
and the interpreter it gets is (clo-m h), the one it closed over.
that is the entire tower. there is no level counter in this repository. a level exists where somebody wrote a handler in the object language, and the regress terminates where handlers stop being object closures.
Two levels, visibly
(define (shifted i)
(with-handler i 'eval-lit
(lambda h (m l e r k) (apply-cont k (+ e 10)))))
(define (bump m l e r k) (apply-cont k (+ e 100)))
(define lvl1
(with-handler (interpreter) 'eval-lit
(with-interp bump (shifted (interp-of bump)))))
(define (f) 1)
(say ((with-interp f lvl1)))
111
from the inside out: f’s body is the literal 1, evaluated by bump, which
answers (+ e 100). bump is an object closure written under shifted, so
its literals get ten added and its 100 is a 110.
Open recursion
(define (eval-if m l e r k)
(meta m l 'base-eval (cadr e) r
(cont-in k (lambda (c)
(if c
(meta m l 'base-eval (caddr e) r k)
(meta m l 'base-eval (cadddr e) r k))))))
nothing here names base-eval’s definition. it names the entry in whatever
dictionary it was handed, which is why shadowing one entry changes the meaning
of every form that reaches it.
the three simplest handlers are worth reading for what they omit:
(define (eval-lit m l e r k) (apply-cont k e))
(define (eval-var m l e r k) (apply-cont k (env-get r e)))
(define (eval-quote m l e r k) (apply-cont k (cadr e)))
no mention of staging. an evaluator written over floor operations is
stage-polymorphic without saying so, because what belongs in a residual is
decided by the values an operation meets. l is there for the handlers that
want to override that, and most do not.
What the file exports
(lambda (sel)
(cond ((eq? sel 'eval) ...)
((eq? sel 'compile) ...)
((eq? sel 'task) ...)
((eq? sel 'm) base-m)
((eq? sel 'env) base-env)
(else (throw (cons 'no-export sel)))))
eval and compile differ in one argument, concrete-l against
(staged-l '()). compiling is the same evaluator started in the other stage
state. task is that again with the task’s own address bound to self.