flan/spike/x86/p13-dyn-collect.flan
Joseph Ferano c2d378957e The x86 backend and dyn finally meet, which is where the dev loop is
x86 is what flan dev takes by default and dyn is the iteration feature, so
a backend that refused dyn meant the two halves of the dev loop could not
be in the same program. The refusal was one arm of is_agg, and it said the
true thing: it was never the representation that was missing. A dyn is
uint64_t, a scalar in both calling conventions, classified by every rule
this file already had; every operation on one is a Tast.Rt primitive and
call_rt has always known how to make one of those. What the lane actually
cost was the collector's root discipline.

Which is emit.ml's, reused rather than rewritten: Emit.dyn_roots counts the
roots for both backends now, so the pushes and the pops balance because one
counter decides both ends, and the two backends root the same nodes because
there is one counter and not two. A zeroed frame slot per dyn slot and per
dyn-producing call, minted beside the channel and outside every scoped --
the bump allocator reclaims at the end of a statement and a slot minted in
the body would be handed out again while the collector still held its
address. Pushed from the body buffer, not the prologue's, because a call
clobbers the registers the prologue is still spilling from. And one pop in
the epilogue, which is the whole of why this backend needed no landing-pad
work for it: there is exactly one epilogue, and the return, the fall-through
and the transfer exit all arrive at it. emit.ml needs the same pop at five
separate rets.

The ABI point the dyn handoff left open for the integrator is settled by
reading the other side rather than by agreeing: flan_dyn.c's mark follows a
value only when the quiet-NaN prefix is set, and the zero word does not have
it, so a zeroed root decodes as the double 0.0 and is never an address
anything dereferences. Zero is safe for a reason. The header says so now.

And one line in dev.ml that was never x86's: the merged dev host resets the
condition stacks and the frame chain between runs, because main is
re-entered by longjmp and pops no frame -- and it never reset the root
stack, so every root a finished run pushed still named stack the next run
was about to write over. That gap was an LLVM dev build's too.

Verification, and one of the numbers is new. @x86: MATCH 129 -> 135, DIFFER
0, REFUSED 0 -- the five dyn programs off survey.sh's llvmonly list, which
is gone rather than empty, plus p13. dune test --force green, with --x86
acceptance rows beside the LLVM ones for all five dyn programs, dyn-boundary
asserted on the same exit 134 and the same sentence on both.

p13-dyn-collect.flan is the one that is not a formality. Nothing else in
this repository allocates past flan_dyn.c's one-megabyte floor, so nothing
else collects even once, so a program whose roots are entirely wrong passes
every output test there is -- the handoff wrote that about the stub and it
outlived the stub. p13 allocates several megabytes of garbage while holding
live values across it: at forty times the corpus size it peaks at 4MB of
RSS, which is the collector running many times over, and both backends
still print the same four lines.
2026-09-19 14:22:55 +07:00

56 lines
2.3 KiB
Plaintext

;;;; Enough allocation that the collector actually runs, with live dyn values
;;;; held across it.
;;;;
;;;; Every other dyn program in this repository allocates a handful of objects
;;;; and stops. runtime/flan_dyn.c's trigger has a one-megabyte floor, so none
;;;; of them ever crosses it and none of them collects even once — which means
;;;; that until this file existed, a program whose root discipline was entirely
;;;; wrong printed the right answer on both backends. The dyn handoff said that
;;;; about the stub that never collected; the stub is gone and the observation
;;;; outlived it, because a heap that never fills is a collector that never
;;;; runs.
;;;;
;;;; So this one allocates well past the floor while holding values the
;;;; collector must not free: a vector that grows for the whole run, a text
;;;; allocated before the loop and read after it, and a running total. The
;;;; garbage is the per-iteration vector that nothing keeps, and there is a lot
;;;; of it.
;;;;
;;;; What a lost root looks like here is not a wrong number. It is a use of
;;;; freed memory — a crash, or a word that decodes as some other tag and traps
;;;; with a sentence about the wrong type. Either way the two backends stop
;;;; saying the same thing, which is what the sweep asks.
;;; Held in locals across every allocation the loop makes, which are the slots
;;; the entry block roots. Returned, so the vector is live to the last line.
(defn build [n dyn] dyn
(let [xs (vec-new dyn)
i 0]
(while (< i n)
;; Fresh and unreferenced: this is the garbage. Four pushes each, so the
;; items array is allocated too and the heap moves quickly.
(let [junk (vec-new dyn)]
(push junk i)
(push junk "row")
(push junk 2.5)
(push junk true))
;; Every sixteenth iteration keeps one, so the live vector grows *through*
;; the collections rather than only between them.
(if (= 0 (% i 16)) (push xs i))
(set i (+ i 1)))
xs))
(defn main [] ()
;; Allocated before the loop runs and read after it, which is the check that
;; main's own root outlived every collection build triggered.
(let [keep "kept"
xs (build 40000)]
(print (len xs))
(print "\n")
(print (at xs 0))
(print "\n")
(print (at xs (- (len xs) 1)))
(print "\n")
(print keep)
(print "\n")))