From 0fea9717718b73dfe0b806f9c4a69cd3e5841037 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 07:52:02 +0700 Subject: [PATCH] Say why transfer is lowered explicitly, and pick the channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stated reason was that native and wasm32 must behave identically, which is misleading: we do not develop on wasm32 and only ever export to it. The reasons that hold are all release-side. wasm32 cannot unwind without the exceptions proposal, so the export would not work at all. Native unwinding is not cheaper and is much less legible - every call becomes an invoke with a landing pad, where a cmp/jne after a call reads like ordinary code, which will matter once there is a disassembler. And one mechanism is one thing to get right, since the acceptance table runs the same programs on both targets and compares a hash. The channel is an out-parameter rather than a discriminated return value, which §6 had left open. The return type then stays what the source says; a discriminated return would repack every ret, turn an aggregate return into an sret call, and nest inside the discriminated return (Option T) already is. One pointer threads down the chain, so a callee writes the target into its caller's own slot and each frame only checks and returns early - reusing the existing return path and therefore §5's defers. A single global would be more legible still, with no signature change at all, but it is not re-entrant: §5 runs defers during a transfer, so a defer that signals and invokes a restart would start a second transfer over the first. --- spec-conditions.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/spec-conditions.md b/spec-conditions.md index 9c6902e..d0bcc3a 100644 --- a/spec-conditions.md +++ b/spec-conditions.md @@ -91,10 +91,37 @@ Invoking a restart transfers control outward past zero or more frames. ## 6. Crossing compiler-generated frames Transfer is lowered **explicitly** — result propagation plus branch targets — not -via platform unwinding, so that native and wasm32 behave identically. That means +via platform unwinding. Three reasons, none of them about dev builds: + +- **wasm32 cannot unwind** without the exceptions proposal, so a release export + would not work at all. +- **Native unwinding is not cheaper and is much less legible.** Every call + becomes an `invoke` with a landing pad, plus a personality function and an + exception table; a `cmp`/`jne` after a call reads like ordinary code and that + matters once there is a disassembler. +- **One mechanism is one thing to get right.** The acceptance table runs the + same programs on both targets and compares a hash, and that hash is the only + tripwire two implementations would have. + +That means every function on the path between the invoke and the target must be -transfer-aware: it returns a discriminated "normal value / transferring to frame -N" result, checks it after each call, and forwards. +transfer-aware: it carries a "normal / transferring to frame N" channel, checks +it after each call, and forwards. + +**The channel is an out-parameter**, a `ptr` appended to the signature, and not +a discriminated return value. The return type then stays what the source says, +which keeps a function's disassembly readable as the release one plus a guard; +a discriminated return would repack every `ret`, turn an aggregate return into +an `sret` call, and nest awkwardly inside the discriminated return `(Option T)` +already is. One pointer threads down the whole chain, so a callee writes the +target into its caller's own slot and each frame only has to check and return +early — which reuses the existing `return` path, and therefore §5's defers, for +free. + +A single global slot would be more legible still — no signature change at all — +but it is not re-entrant: §5 runs defers *during* a transfer, so a defer that +signals and invokes a restart would start a second transfer over the first. A +per-frame slot nests correctly with no threads involved. - The compiler marks a function transfer-transparent if it can call, directly or indirectly, anything that may invoke a restart. Escape analysis narrows this