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

clambda

clambda has lambda’s shape and compiles its body against the semantics in force. it answers a floor closure under a tag:

narju> (clambda f (n) (+ n 1))
('cfun . #<closure>)

narju> ((clambda f (n) (* n 2)) 21)
42

the tag matters at a call site and nowhere else. the base environment binds the primitives to floor closures too, and a call site treats the two oppositely: a primitive handed code unfolds into it, a compiled function is called. nothing in the value says which, so clambda says it.

it is an annotation, not a restriction. the same body works either way and callers cannot tell which they got.

(define ifib (lambda fib (n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))))
(define cfib (clambda fib (n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))))

(ifib 22) takes about eight seconds here, (cfib 22) about thirty milliseconds. an interpreted call allocates a frame per level per step, and compiling removes every level between the body and the floor.

What happens to the tower

everything reflective in a compiled body resolves while staging:

(say ((clambda f (n) (+ 1 (call/cc (lambda (k) (+ 100 (k n)))))) 5))
6

the call/cc, the escape and the abandoned (+ 100 _) are gone. the residual is (+ 1 n).

the flip side: an operator that only becomes reflective at run time cannot be recovered, and static-apply throws ('no-call-site . f). that is the whole of what compiling does not preserve.

Interpreted callees get inlined

a call from a compiled region into an interpreted closure unfolds the callee into the code being emitted:

(let ((g (lambda g (x) (+ x 1))))
  (say ((clambda f (n) (g (g n))) 1)))
3

the emitted body is two additions. g is not called at run time.

which is fine until the callee recurses:

(say (attempt (lambda ()
  (let ((g (lambda g (x) (if (< x 1) 0 (g (- x 1))))))
    (clambda f (n) (g n))))))
('throw 'inlines-forever 'g)

unfolding would not stop, so it is refused rather than attempted. the check is not a depth limit. it looks for the same closure reached with statically equal arguments, which is exactly when the next unfolding cannot differ from this one, so a recursion whose static arguments do change still unfolds. that case is the one worth having: an interpreter staged against a static program is precisely it.

a cycle through several functions names them all:

('inlines-forever 'g 'h)

Compiled callees get called

(define down (clambda down (n) (if (< n 1) 0 (down (- n 1)))))
(define go (clambda go (n) (down n)))
(say (go 5))
0

a call to a compiled function is emitted, not unfolded, so its recursion is its own business. the two ways down can reach the call site emit the same thing: staged in the same region as its caller it is still code, and already run it is a cfun reached through a reference. either way go’s residual is one call.

a self-call never needs any of this. lift-fun binds the function’s own name to a code variable before the body is staged, so recursion residualizes as a call by construction.

Static data still unfolds

(let ((walk (lambda walk (xs n)
              (if (nil? xs) n (walk (cdr xs) (+ n 1))))))
  (clambda f (n) (walk '(1 2 3) n)))

the list is known while compiling, so walk runs three times at staging time and the residual is three additions with no list in it. this is the Futamura direction: a general procedure plus static data becomes a specialised one.