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.
22 lines
778 B
Plaintext
22 lines
778 B
Plaintext
;;;; A dyn global, which is the case that needs the startup function.
|
|
;;;;
|
|
;;;; A dyn value is made by a call into the runtime, and a call is not a
|
|
;;;; constant, so the initialiser cannot be a constant image the way a typed
|
|
;;;; global's is. It runs in flan..init-globals, which main calls after
|
|
;;;; flan_gc_init and before anything the programmer wrote — the same machinery
|
|
;;;; the computed globals already use, which is the point: a dyn global is a
|
|
;;;; computed global and needed no new mechanism.
|
|
|
|
(defvar counter dyn 0)
|
|
(defvar label dyn "start")
|
|
|
|
(defn bump [] ()
|
|
(set counter (+ counter 1)))
|
|
|
|
(defn main [] ()
|
|
(print counter) (print " ") (print label) (print "\n")
|
|
(bump)
|
|
(bump)
|
|
(set label "done")
|
|
(print counter) (print " ") (print label) (print "\n"))
|