mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
`const visit = (node) => { ... visit(child) ... }` lowered to a closure
over the box of `visit`, and the box then held the closure: a two-object
reference cycle that only the cycle collector could free, along with
everything the closure captured. Nested function declarations already
read their own name through selfRef; arrows and function expressions
stored into a binding now do too, when the IR proves the binding holds
the closure itself whenever the closure can run.
resolveSelfCaptures runs with the other backend IR passes, before
copyStableCaptures. A capture qualifies when every creation site stores
the new closure, unadapted and with the binding's own type, into a boxed
local of the creating function (its only declaration's initializer, or
the only assignment after an empty declaration in the same statement
list), nothing else writes the binding in any body that shares its box,
and the closure body is synchronous, not a generator, and does not hand
the binding to a nested closure. The capture then leaves the body and
every creation site; its reads become selfRef. A creating binding that
no other closure captures loses its box, except dead-zone bindings.
Self-hosted compiler (native, compiling its CLI to IR): the cycle
collector frees 14.5M objects instead of 22.1M; closures that held a box
containing themselves drop from 3.5M to 54k. Of the runtime suite and the
cg-* cycle workloads, only published-graphs compiles differently
(-1.3%, 31 interleaved runs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>