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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
This commit is contained in:
Gergő Törcsvári 2026-08-06 20:47:26 +02:00
commit ff4b2bdb3d
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322

View file

@ -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 K1K7 and W4/W5 through one `wasm_await_promise`-style helper. After this **no