Why a handler frame holds a body address

plan.org now says a top-level function value is a stable trampoline over the
indirection cell and never the address of a particular body, so that a stored
callback observes a redefinition. A pushed handler frame breaks that rule and
should: it is not a Fn value, nothing in the language can name it, and it is
live only for the duration of the handler-bind body - so a reload landing while
it is on the stack finds the clause it pushed still valid, which is the whole of
old code is never unloaded.

The consequence worth knowing is that a handler already on the stack does not
pick up a redefinition of its own clause; the next entry to the handler-bind
pushes the new one. Recorded at the store in emit.ml and in NEXT.md, because
when Fn values arrive this is the one place that stores a body address on
purpose and must not be swept up with the rest.
This commit is contained in:
Joseph Ferano 2026-09-11 12:25:28 +07:00
parent c322ef60bf
commit f55ec0a0b8
2 changed files with 18 additions and 0 deletions

12
NEXT.md
View File

@ -919,6 +919,18 @@ Three decisions worth keeping:
- **A clause is lifted into a function of its own.** A handler runs from
wherever the signal was, so it cannot be a branch in the function that wrote
it.
- **A pushed handler frame holds the clause's body address, not a cell.** This
is a deliberate divergence from plan.org's rule that a top-level function
value is a stable trampoline over the cell and never the address of a
particular body. A handler frame is not a `Fn` value — nothing in the
language can name it — and it is live only for the duration of the
`handler-bind` body, so a reload landing while it is on the stack finds the
clause it pushed still valid, which is exactly the "old code is never
unloaded" guarantee. The consequence to know: a handler already on the stack
does *not* observe a redefinition of its own clause; the next entry to the
`handler-bind` pushes the new one. When `Fn` values arrive, this is the one
place that stores a body address on purpose and must not be swept up with
them.
Which gives the two refusals, both by the house rule rather than by accident:

View File

@ -574,6 +574,12 @@ and emit_handled f frames body =
let fp = fresh f in
ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 2"
fp slot;
(* The clause's body address, deliberately, and not a cell load:
plan.org makes a top-level function value a stable trampoline over
its cell, but a handler frame is not one nothing can name it, and
it lives only for this body. A reload landing while it is on the
stack finds what it pushed still valid, which is what "old code is
never unloaded" means. See NEXT.md, conditions step 1. *)
ins f "store ptr %s, ptr %s" (fname h.Tast.hfn) fp;
ins f "call void @flan_handler_push(ptr %s)" slot;
slot)