From ff4b2bdb3d494f782d4764e16e15657b670df622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Thu, 6 Aug 2026 20:47:26 +0200 Subject: [PATCH] docs 22: the one-root constraint that orders the B+C+D wiring The scheduler fiber and libcontext's root would both adopt the main stack - two emscripten_fiber_t describing one stack, mutual corruption on first entry. Harmless in Phase A (KiCad never called drain), fatal at the flip. So dispatch must move onto a context FIRST, the libcontext root then adopts the running context rather than the main stack, and only then can jump_fcontext become a star transfer. Also records that the synchronous-Call question is answered and pinned. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS --- .../features/async/22-absorbing-libcontext.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/features/async/22-absorbing-libcontext.md b/docs/features/async/22-absorbing-libcontext.md index 34fbc5a..0a14e85 100644 --- a/docs/features/async/22-absorbing-libcontext.md +++ b/docs/features/async/22-absorbing-libcontext.md @@ -165,6 +165,38 @@ in it is **wrong** and must not come back — one dispatch context suffices once > **B + C + D land as ONE commit.** Each alone regresses. Build them on a sub-branch, > keep every guard as a tripwire, flip once, gate hard. +**Sub-branch `feature/async-star-flip` (2026-08-06): the core mechanism is built and +proven, the wiring is not.** Landed there: `fiber_transfer(from, to, value)` — +libcontext's symmetric swap expressed as a star transition (park the source, make the +target runnable, the scheduler performs every entry) — plus `fiber_start` (the lane's +entry point, since a transfer needs a running context to park) and `drain_all` (the +top-level pump, because a star transition only makes work *runnable*). + +The question that decided whether B is possible at all is answered YES and pinned by +`star_transfer_call_is_synchronous`: **parking the caller and resuming it when the +callee yields is indistinguishable, in the caller's own C++ frame, from a synchronous +return.** So `TOOL_MANAGER`'s `Call(); if( !Running() )` contract survives the flip +untouched — which is what makes a KiCad-minimal Phase B realistic. + +**The constraint that fixes the order of the remaining wiring — ONE root, not two.** +`ensure_scheduler_context()` adopts whatever stack first calls `drain()`, and +libcontext's `ensure_main_context()` adopts whatever stack first calls +`jump_fcontext`. In production both are the main stack, so the scheduler fiber and the +libcontext root would be two `emscripten_fiber_t`s describing the SAME stack — mutual +corruption the moment either is entered. In Phase A this was harmless (nothing in +KiCad ever called `drain()`); at the flip it is fatal. Therefore: + +1. **D first, inside the flip:** dispatch moves onto its own context, so the main + stack is only ever the scheduler and nothing else runs there. +2. libcontext's root then adopts **the running context** (the dispatch context), never + the main stack. +3. Only then may `jump_fcontext` become `fiber_transfer`, because every caller is now + provably on a context and can park. +4. C follows naturally: `wxWasmYieldUntil` yields the owning context, `resolveWait` + marks it ready, and the doc-19 mainstack bounce comes out. + +The tick becomes `fiber_start(dispatch, …)` + `drain_all()` from a fresh JS task. + ### Phase E — bridges on contexts (1 wk) Doc 21 §1's K1–K7 and W4/W5 through one `wasm_await_promise`-style helper. After this **no