Joseph Ferano 9f2f0b1635 Roots, pushed where the addresses are stable and popped where the frame leaves
A precise collector has to be told where the live dyn words are, and the shadow
stack next door is the precedent for where that goes: set up in the entry block,
undone in ret, which is the one funnel all five exits pass through -- the tail,
both returns, the none arm of (some x), and the landing block a handled
condition unwinds through. A pop written only on the normal path would leave a
frame's roots on the stack after every handled error.

It differs from the shadow stack in two ways, and both are forced. It is not
gated on dev: a backtrace is a convenience and a collector that cannot find its
roots frees live values. And it is a count rather than a saved head pointer,
because the ABI offers root_pop(n) and no way to read the stack's height -- so
the number has to be known before the body is emitted, since ret runs during
emission and a tally accumulated as roots were discovered would be short at
every early return. dyn_roots works it out up front by walking the same nodes
the emission will visit, the slots are minted from that count at entry, and
dyn_tmp only hands them out. The pushes and the pops balance by construction
rather than by two walks agreeing.

Every dyn-producing call is spilled into a rooted slot the moment it exists. An
SSA value is invisible to a collector that finds roots by address, and the next
allocation could be the one that frees what it holds. Rooting all of them rather
than only those that outlive a call is conservative and is the only thing
available here: this file has no liveness and no lexical scope, the checker
having resolved both into flat slot indices long before. The cost is a stack
slot and a store per dyn value at every optimisation level, because a rooted
alloca has its address escape and mem2reg cannot promote it. That is the price
of an address-registration ABI rather than stack maps.

A function with no dyn emits nothing at all -- no push, no pop, not a pop of
zero -- which is what makes an annotated program's IR identical to what it was
before any of this existed.

Globals are rooted in main, before the startup function that fills them and
before any other push, because every pop takes the top of the stack and these
are the ones that must never be at the top. They are never popped, which is what
a global's extent means. A dyn global needed no new machinery otherwise: a call
is not a constant, so it is a computed global, and that already existed.
2026-09-19 06:12:41 +07:00
..