mailbox S4: wait registry in the shim + work log; bump wxwidgets

Per-kind LIFO wait stacks (beginWait/waitPromise/resolveWait/
resolveTopWait), resolve-before-yield safe, S2 deferred-wake compliant.
Gates: asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45, kicad 6/6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
Gergő Törcsvári 2026-08-05 16:54:29 +02:00
commit 5a8b0a3279
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
3 changed files with 81 additions and 2 deletions

View file

@ -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 13; 45 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 45 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

View file

@ -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;
},
};

@ -1 +1 @@
Subproject commit 6c33cafa9c31fba99d4dae4b0d300183751d05dd
Subproject commit 1953c18527e92204e1bbc957a90922f211937613