diff --git a/NEXT.md b/NEXT.md index 781a365..a18ff04 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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: diff --git a/lib/emit.ml b/lib/emit.ml index 0cfea65..80b7c1f 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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)