diff --git a/docs/features/async/17-mailbox-scheduler-plan.md b/docs/features/async/17-mailbox-scheduler-plan.md index cee0cc0..f1c49a8 100644 --- a/docs/features/async/17-mailbox-scheduler-plan.md +++ b/docs/features/async/17-mailbox-scheduler-plan.md @@ -235,6 +235,24 @@ rollback = the `WX_SCHEDULER=0` build + a per-step tag. 3. modal dialog (`startModal` + `_wxModalResolvers` die), 4. clipboard + font enum, 5. **open** last — the biggest semantic flip (gates → ordering; N2/N3/N6 are the gate). + > **Work log 2026-08-05 — S4 LANDED and gated (waits 1–3; 4–5 resolved by scoping).** + > The doc-13 §2 API exists as `wx/wasm/private/yieldwait.h` (`wxWasmBeginWait` / + > `wxWasmYieldUntil` / `wxWasmResolveWait`/`ResolveTopWait`) backed by the shim's wait + > registry (per-kind LIFO stacks, resolve-before-yield safe). On scheduler builds: + > **nested loops, popups, and modals are registered WAITS with NO pumps** — the top-level + > tick is the sole dispatcher at any DoRun depth (the s_wxRunDepth gate is scheduler- + > bypassed; the June §6e double-driver hazard needed two awaited pumps, which no longer + > exist). Dead on scheduler builds: `startModal`, `wxWasmRunNestedLoop`, the wx-dom popup + > pump, `_wxModalResolvers`, `_wxNestedLoopExit`, `_pendingModalResult` (waits begin + > before Show(), so a racing EndModal pre-resolves). Tick error containment now releases + > the innermost nested wait AND cancels the top modal (the dead pumps' catch role). + > Waits 4–5 by scoping: clipboard/font enum keep their EM_ASYNC_JS promise-waits — they + > have no pumps and are already S2-managed sleeps (migrating them to tokens buys + > uniformity, not behavior; revisit at S5 if the token registry should own ALL waits); + > open keeps the C++ gate as second line under the JS embind lane's queueing (N2-gated + > since S1). **Gates:** asyncify 9/9 (triple-modal LIFO now on wait stacks), coroutine + > 39/39 (quasi-modal with zero pumps), wx modal-heavy 45/45, kicad 6/6 incl. + > modal-stack + contextmenu-scrollbar on a fresh warm-cache build. - **S5 · Guard demolition (few d).** Delete: dispatch interlock + save/zero/restore sites, timer retry, `ProcessEvents` Paint-only gate, `s_wxRunDepth` gate, open-settle/fiber-busy/ enumerate gates. Downgrade libcontext refusals to dev-build assertions; keep counters-only diff --git a/scripts/common/shims/asyncify-scheduler.js b/scripts/common/shims/asyncify-scheduler.js index f71bdc0..eb71d06 100644 --- a/scripts/common/shims/asyncify-scheduler.js +++ b/scripts/common/shims/asyncify-scheduler.js @@ -148,6 +148,64 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { }, 16); }, + // --- S4 wait registry --------------------------------------------------- + // Token-based waits (doc 13 §2: wasm_begin_async_wait / wasm_yield_until / + // wasm_resolve_wait). A wait is begun BEFORE the C++ side parks, so a + // resolve that races ahead of the park (EndModal during Show()) simply + // pre-resolves the promise — yieldUntil then returns immediately. Per-kind + // LIFO stacks give wx modal/nested semantics ("innermost first") without + // the legacy per-wait resolver stacks (_wxModalResolvers / + // _wxNestedLoopExit), which die on scheduler builds. Resolution flows + // through the S2 deferred-wake law automatically: resolving a wait wakes + // its parked sleep via the wrapped handleSleep path. + waits: new Map(), // token → {kind, promise, resolve, resolved} + waitSeq: 0, + waitStacks: {}, // kind → [unresolved tokens], LIFO + waitsBegun: 0, + waitsResolved: 0, + beginWait: function (kind) { + var token = ++this.waitSeq; + var entry = { kind: kind, resolved: false, resolve: null, promise: null }; + var self = this; + entry.promise = new Promise(function (resolve) { entry.resolve = resolve; }); + this.waits.set(token, entry); + (this.waitStacks[kind] = this.waitStacks[kind] || []).push(token); + this.waitsBegun++; + return token; + }, + waitPromise: function (token) { + var entry = this.waits.get(token); + if (!entry) { + console.warn("[wx-scheduler] waitPromise(" + token + "): unknown token"); + return Promise.resolve(0); + } + return entry.promise; + }, + resolveWait: function (token, result) { + var entry = this.waits.get(token); + if (!entry || entry.resolved) return false; + entry.resolved = true; + this.waitsResolved++; + var stack = this.waitStacks[entry.kind]; + if (stack) { + var idx = stack.indexOf(token); + if (idx !== -1) stack.splice(idx, 1); + } + this.waits.delete(token); + entry.resolve(result | 0); + return true; + }, + // Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics). + resolveTopWait: function (kind, result) { + var stack = this.waitStacks[kind]; + if (!stack || stack.length === 0) return false; + return this.resolveWait(stack[stack.length - 1], result); + }, + pendingWaits: function (kind) { + var stack = this.waitStacks[kind]; + return stack ? stack.length : 0; + }, + // --- S2 scheduler core state ------------------------------------------- // Deferred sleep wakes: {deliver, result} queued because a transition was // in flight when the wake arrived. Delivered FIFO from a clean macrotask. @@ -175,7 +233,10 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) { + " readyWakes=" + this.readyWakes.length + " deferredWakes=" + this.deferredWakes + " drainedWakes=" + this.drainedWakes - + " strayWrites=" + this.strayWrites; + + " strayWrites=" + this.strayWrites + + " waits=" + this.waits.size + + " waitsBegun=" + this.waitsBegun + + " waitsResolved=" + this.waitsResolved; }, }; diff --git a/wxwidgets b/wxwidgets index 6c33caf..1953c18 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit 6c33cafa9c31fba99d4dae4b0d300183751d05dd +Subproject commit 1953c18527e92204e1bbc957a90922f211937613