Joseph Ferano 4c667cf062 A defer the text never reached is not cleanup the unwind owes
The report was that handler-case segfaults in a top-level global
initialiser. It does not, and never did. What crashes is one frame
further down, in a position that has nothing to do with startup:

    (defn read-file [path string] dyn
      (let [src (slurp path (heap-allocator))]
        (defer (free src))
        (read (as-slice src))))

slurp signals FileError, a handler further out unwinds, and the
transfer leaves this frame through its defers -- all of them. But src
was never written: the form that would have written it is the form
that transferred. free then reads whatever the stack held under that
slot, which at -O0 in a small program is zero and at -O2 is a live
pointer, which is why the same program looked like an optimiser bug
from one direction and a startup bug from the other.

return never had this. The checker splices the defers registered above
it and no others, and says so where it does it. The transfer exit took
the whole list, because it is one landing block per function and
nothing in the IR said where each defer had come into being.

So the count is kept. The first defer in a function mints an i64 slot
zeroed at the top of the body; each defer leaves a store of its own
number where it was written; and fdefers -- the transfer path's copy,
and only that copy -- tests the count before running each one. The
normal paths are untouched and still need no test. It is all in the
checker: what reaches a backend is a slot, a store and an if, so
neither emitter learned anything and the x86 one needed no frame of
its own.

test/programs/init-conditions.flan is the survey. The top half is the
part of the report that was never true: handler-case with its
condition firing and with its body completing, handler-bind, and a
restart-case, all four in a global initialiser, all four answering
what they answer anywhere. The bottom half is the part that was: a
defer below the signalling form, which must not run, beside one above
it, which must -- a fix that took the unregistered one off by taking
them all off would have traded the crash for a leak, and 302/2 is the
line that would catch it. Three acceptance rows, LLVM, -O0 and --x86.

The detector was valgrind, not ASan: reading a stack slot nobody wrote
is not ASan's bug class and it reported nothing on the broken binary,
while memcheck named the conditional jump in flan_vec_free with the
unwinding frame directly above it. The program is in both lists --
test_valgrind.ml because that is what saw it, test_sanitize.ml because
that is where the transfer exit's frames are already watched.

spec-conditions.md section 5 now says which defers a transfer runs.
The author's (defvar game-data dyn (handler-case (edn/read-file ...)
[(FileError [c] nil)])) works.
2026-09-19 21:57:00 +07:00
..
2026-09-11 20:11:01 +07:00
2026-09-11 20:11:01 +07:00
2026-09-11 20:11:01 +07:00