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

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.