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

Cross-Stage Persistence

lift turns a value into syntax. Some values should not become syntax — they are too big to copy, or their identity matters, or they exist only in the running system. Cross-stage persistence is the other way for a value to enter generated code: as a reference to the live value, carried inside the residual program.

lift-ref

(lift-ref name v) produces code that, when run, yields the live value of v itself — not a syntactic reconstruction:

(lift-ref 'p (cons 1 2))
;=> #<code 1 nodes>
(run 0 (lift-ref 'p (cons 1 2)))
;=> (1 . 2)

The name operand doubles as the stage dispatch (code there residualizes the lift-ref itself, one stage up). The difference from lift is identity: the persisted value crosses the stage boundary as itself. A pair containing a cell demonstrates it — lift refuses such a value outright, while lift-ref carries the live cell through, still connected to its store:

(let c (cell-new 5)
  (lift (cons c nil)))
;! staging error: lift Tup: car must be Code, got Cell(0)
(let c (cell-new 5)
  (let p (cons c nil)
    (cell-read (car (run 0 (lift-ref 'p p))))))
;=> 5

Inside the expression tree the persisted value sits in the one node with no surface syntax — the reference’s proc (Part I’s reference chapter). Residual programs are otherwise printable source; a persisted value is the exception, a live object riding in the syntax.

Staged log

log persists rather than lifts for the same reason. When log residualizes — code stage dispatch — its value operand may be any value at all, and demanding code there would make the debugging tool unusable in exactly the stage-oblivious code it exists to observe. So a non-code operand is embedded by reference, and the generated program prints it when it runs:

(run 0 (log (lift 0) (cons 1 2)))
;; prints: [log] (1 . 2)
;=> (1 . 2)

The [log] line is printed by the generated program during the run, not by the generator.

What cannot cross: cells

Cells refuse both crossings, in opposite directions, and both refusals protect the same invariant.

A cell cannot be lifted — Part I stated the rule; the reason is that folding a cell’s identity into generated syntax would freeze a reference whose whole point is to be written later:

(lift (cell-new 0))
;! staging error: cannot lift a cell: cells never fold under staging

Staged cell operations residualize instead, so generated code reads and writes cells when it runs, against whatever the store holds then:

(let f (run 0 (lift (lambda _ c (cell-read c))))
  (let c (cell-new 9)
    (f c)))
;=> 9

The compiled function reads the cell at call time — the 9 was written after the function was generated.

In the other direction, code cannot be written into a cell:

(let c (cell-new 0)
  (cell-set! c (lift 3)))
;! type error in cell-set!: expected non-Code value (cells never contain Code), got (Cell, Code(1 nodes))

This is a scope-extrusion guard. A code value is a fragment of a program under construction, full of residual variables that mean something only inside their reify scope; a cell would let it outlive that scope and surface in a program where those variables bind nothing. Cells hold values, never code — which, with “cells never fold”, gives the two-sided rule the tower of Part IV is built on: mutable state stays live, staging works around it, and a compiled tower still sees every write.