Reflective procedures
a reflective procedure is written (lambda reflect (m l e r k) body). it is
applied to its own call site rather than to values: the five things a handler
gets, except that e is the list of operand forms, unevaluated.
(define twice
(lambda reflect (m l e r k)
(meta m l 'base-eval (car e) r
(cont-in k (lambda (v) (apply-cont k (+ v v)))))))
(say (twice (+ 1 2)))
6
meta evaluates something: interpreter, stage, handler name, form,
environment, continuation. cont-in k f builds a continuation whose normal arm
is f and whose raise arm is k’s. apply-cont k v hands v to the call
site.
never evaluating the operand gives a macro, near enough:
(define quoted (lambda reflect (m l e r k) (apply-cont k (car e))))
(say (quoted (+ 1 2)))
('+ 1 2)
except that this is not a rewriting pass. it runs at the meta level when the call is reached, with the environment and continuation in hand, neither of which a macro can see.
Not calling k
the normal arm of a reflective body’s continuation is the identity, not k. a
body that returns without going through apply-cont returns to the nearest
prompt instead of to the call site:
(define bail (lambda reflect (m l e r k) (car e)))
(say (prompt (lambda () (+ 1 (bail this-is-syntax)))))
'this-is-syntax
the (+ 1 _) is gone. that is abort, which in the prelude is exactly this:
(define abort
(lambda reflect (m l e r k)
(meta m l 'base-eval (car e) r id-cont)))
What is built this way
none of the following is a special form. all are naj/prelude.naj.
(define call/cc
(lambda reflect (m l e r k)
(meta m l 'base-eval (car e) r
(cont-in k (lambda (f) (apply l f (list k) k))))))
narju> (call/cc (lambda (k) (+ 1 (k 41))))
41
narju> (+ 1 (call/cc (lambda (k) 41)))
42
prompt delimits by applying its thunk under id-cont rather than the
caller’s k. it must be reflective for that: an ordinary function would
already be running under the continuation it means to delimit.
attempt runs a thunk under a continuation whose two arms tag their answers:
narju> (attempt (lambda () (+ 1 (throw 'boom))))
('throw . 'boom)
narju> (attempt (lambda () (+ 1 2)))
('ok . 3)
await reifies the call site and hands it to whoever runs the prompt, which is
how an RPC suspends a task without blocking a thread. see
Calls and replies.
(define interpreter (lambda reflect (m l e r k) (apply-cont k m)))
(define environment (lambda reflect (m l e r k) (apply-cont k r)))
Second class
a reflective procedure handles a call site, so it needs one. in operator
position eval-app sees the tag and routes to apply-reflective before
evaluating operands. reached any other way, by a floor application or from
compiled code, there is no syntax to hand it and static-apply throws
('no-call-site . f).
this is the one thing compiling does not preserve. by the time compiled code
applies an operator the operands are values and the tag is gone, so a
reflective call site inside a clambda resolves while staging or not at all.