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

Stage Polymorphism

The previous chapter passed a parameter l through every handler without explaining it. l is a pair: its cdr is a stage level, and its car is a function the reference calls maybe-lift. The whole difference between interpreting a program and compiling it is what sits in that car.

maybe-lift

Handlers apply (car l) at exactly the points where the interpreter creates a value rather than passing one through: constants (eval-cst), closures (eval-lambda), pairs (eval-cons), and quoted data (eval-quote). Everywhere else — arithmetic, if, application, environment lookup — values flow through the handler untouched.

The session’s two evaluators are the same dispatch closed over two choices of l, built in lib/purple.naj:

(let pink-eval  (pink-tie (cons (lambda _ e e)        0))
(let pink-evalc (pink-tie (cons (lambda _ e (lift e)) 0))

With the identity, created values are ordinary values and the evaluator is the interpreter of the last chapter. With lift, every created value is code — and Part II says what happens next: an operation applied to code residualizes. The handlers do not know which mode they are in. They are stage-polymorphic: one source, two readings, and the reading is picked by a first-class argument.

This is where Part II’s auto-lift convenience earns its place. A handler like eval-plus calls the floor’s + on whatever its subterms produced; under pink-evalc those are code values mixed with the occasional plain number, and the scalar auto-lift keeps the stage-oblivious handler from erroring.

Compiling by interpreting

Under pink-evalc, evaluating a program generates it. The block below boots Pink directly on the floor — the same three moves lib/purple.naj makes at session boot (read the source, trans it, evalms it) — and then evaluates factorial’s source with lift as maybe-lift:

(let pink-poly-src (car (read-file 'lib/pink-forms.naj))
(let pink-tie-src `(let pink-poly ,pink-poly-src
                     (lambda eval l (lambda _ e (((pink-poly eval) l) e))))
(let pink-tie (evalms nil (trans pink-tie-src nil))
(let pink-evalc (pink-tie (cons (lambda _ e (lift e)) 0))
(let fac '(lambda f n (if (eq? n 0) 1 (* n (f (- n 1)))))
  ((pink-evalc fac) (lambda _ y 0)))))))
;=> #<code 25 nodes>

Twenty-five nodes. Part II compiled the same factorial by writing lift into it by hand — explicit (lift 0) and (lift 1) at the constants — and got twenty-five nodes:

(lift (lambda fact n
  (if (eq? n (lift 0))
      (lift 1)
      (* n (fact (- n (lift 1)))))))
;=> #<code 25 nodes>

That agreement is the paper’s Proposition 4.4: compiling a Pink program yields exactly the program itself, in ANF. The interpreter contributes nothing to the output. Its dispatch chain ran — every eq? test on 'lambda, 'if, '+ — but those tests consumed only the program text, which is ordinary data, so they computed away at generation time. What residualized is only what maybe-lift touched: the program’s own constants, closures, and structure. Interpretive overhead is exactly the part of the interpreter that does not pass through maybe-lift, and it vanishes. (narju’s test suite checks the stronger structural claim — the residual contains no symbol nodes, i.e. no fragment of quoted Pink source survives.)

Running the residual confirms it is factorial:

(let pink-poly-src (car (read-file 'lib/pink-forms.naj))
(let pink-tie-src `(let pink-poly ,pink-poly-src
                     (lambda eval l (lambda _ e (((pink-poly eval) l) e))))
(let pink-tie (evalms nil (trans pink-tie-src nil))
(let pink-evalc (pink-tie (cons (lambda _ e (lift e)) 0))
(let fac '(lambda f n (if (eq? n 0) 1 (* n (f (- n 1)))))
  ((run 0 ((pink-evalc fac) (lambda _ y 0))) 5))))))
;=> 120

(Floor forms are closed, so the boot preamble repeats in each block; in the session it happens once. The run 0 wraps the whole generation, for the reason Part II ended on: code values are meaningful only inside the staging context that made them, and the next chapter leans on this.)

At the session prompt

The session offers both readings of any program. interpret is pink-eval behind a convenience signature, and compile runs the generated code immediately (its mechanics are the next chapter’s):

(define fact-src '(lambda f n (if (eq? n 0) 1 (* n (f (- n 1))))))
(define f-int ((interpret fact-src) nil-env))
(define f-com (compile fact-src))
(f-int 5)
;=> 120
(f-com 5)
;=> 120

Same answers; different objects. f-int is a closure that walks fact-src on every call — each recursion re-enters base-eval, re-tests the head symbols, re-extends the environment function. f-com is the twenty-five-node residual, compiled once; the source is gone.