What a residual carries
the prompt will not show a residual. clambda runs the code it emitted, so
what comes back is ('cfun . #<closure>). to see the code, use the rust API
Tower::compile, which stops one step earlier. the shapes below are the
assertions in src/tower.rs.
(clambda f (n) (+ n 1))
=> (let (lambda (let (+ x1 1) x2)) x0)
floor code in A-normal form. every intermediate is let-bound and named
positionally: x0 is the lambda, x1 its parameter, x2 the sum.
arity is preserved rather than curried:
(clambda g (a b) (+ (* a b) 1))
=> (let (lambda/2 (let (* x1 x2) (let (+ x3 1) x4))) x0)
recursion comes out as a call to x0, the function’s own residual variable:
(clambda fib (n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))
=> (let (lambda (let (< x1 2)
(let (if x2 x1
(let (- x1 1) (let (x0 x3)
(let (- x1 2) (let (x0 x5)
(let (+ x4 x6) x7)))))) x3))) x0)
It carries its semantics
a residual is the meaning of the body under the interpreter in force when it was compiled, and that meaning is baked in:
(define (mk) (clambda f (n) (+ n 1)))
(define (marked i)
(with-handler i 'eval-lit (lambda h (m l e r k) (apply-cont k (+ e 100)))))
(say ((mk) 1))
(say (((with-interp mk (marked (interp-of mk)))) 1))
(say ((mk) 1))
2
102
2
under the marked interpreter the literal 1 was worth 101 while compiling,
and the addition that made 101 left nothing behind:
compiled under base (let (lambda (let (+ x1 1) x2)) x0)
compiled under marked (let (lambda (let (+ x2 101) x3)) x1)
changing the interpreter afterwards does nothing to code already emitted.
Putting something in on purpose
(l 'lift) forces a value into the residual that nothing at the site would
have put there. under concrete-l it is the identity, under (staged-l _) it
lifts:
(lambda h (m l e r k) (apply-cont k (+ e ((l 'lift) 100))))
(let (lambda (let (+ 1 100) (let (+ x1 x2) x3))) x0)
the 100 is now in the emitted code, so (+ 1 100) is a run-time addition.
the same handler without the lift folded it away.
Cross-stage persistence
a value that exists at staging time and is needed at run time crosses by
reference, never by structure. that is lift-ref, and it is the only crossing
that works for everything: a closure crosses intact and keeps its identity,
where lifting would try to expand it, and an interpreter cannot be expanded at
all.
it shows up in the residual for a call whose operator is unknown:
(clambda f (g n) (g n))
=> (let (lambda/2 (let (cons x2 ())
(let (#<closure> #<closure> x1 x3 #<('cont #<closure> . #<closure>)>) x4))) x0)
code? said the operator is unknown, not that it is applicable, so the
dispatch itself is emitted. those #<closure> operands are static-apply,
concrete-l and id-cont, persisted by reference. the site then works for any
representation the operator turns out to have:
(say ((clambda f (g n) (g n)) (lambda h (x) (+ x 1)) 5))
(say ((clambda f (g n) (g n)) (clambda h (x) (+ x 1)) 5))
(say ((clambda f (g n) (g n)) car (cons 1 2)))
6
6
1
interpreted, compiled, floor primitive. one call site, and the answer does not
depend on which arrives. deciding at run time is what makes clambda an
annotation: the callee names its own semantics in clo-m, so an alteration
made after the caller was compiled is still seen.
What cannot cross out
a compiled region builds structure fine:
(say ((clambda f (n) (cons (cons n n) n)) 1))
(say ((clambda f (n) "hi") 1))
((1 . 1) . 1)
hi
a closure it made is another matter:
(say (attempt (lambda () (clambda f (n) (lambda g (x) (+ x n))))))
('throw 'unreturnable . 'closure)
the closure captured a code variable, so it means something only inside the
region that emitted it. letting it out would let a residual variable escape its
binder. ('unpersistable . 'pair) is the same refusal for structure built the
wrong side of the boundary.