@polyengine/runtime 0.3.0 → 0.3.1-pre.g23b4970
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/embedder/copy.js +1 -1
- package/esm/exec/boundary.js +55 -36
- package/esm/exec/executor.js +35 -4
- package/esm/intrinsics/context.js +19 -9
- package/esm/jspi/bridge.js +29 -18
- package/esm/task/scheduler.js +237 -98
- package/package.json +2 -2
- package/types/embedder/copy.d.ts +1 -1
- package/types/intrinsics/context.d.ts +13 -3
- package/types/task/scheduler.d.ts +130 -36
package/esm/embedder/copy.js
CHANGED
|
@@ -29,7 +29,7 @@ export const COPY_URL = import.meta.url;
|
|
|
29
29
|
* @internal — copy-identity constant for the A9 multi-copy diagnostics; not
|
|
30
30
|
* host-facing.
|
|
31
31
|
*/
|
|
32
|
-
export const RUNTIME_VERSION = "0.3.
|
|
32
|
+
export const RUNTIME_VERSION = "0.3.1";
|
|
33
33
|
/**
|
|
34
34
|
* Compose a cross-copy diagnostic: what was foreign, which copy is speaking,
|
|
35
35
|
* the census of every copy in the graph, and the by-value remediation.
|
package/esm/exec/boundary.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// canonical options — docs/architecture.md §4.3 items 2 and 5, degenerate sync case.
|
|
5
5
|
import { coreFuncTypeEquals, CoreValueIter, flattenFunctype, liftFlatValues, LiftLowerContext, lowerFlatValues, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, trap, trapIf, } from "../cabi/mod.js";
|
|
6
6
|
import { AssertionError, assert_ } from "../cabi/trap.js";
|
|
7
|
-
import { driveSyncLift,
|
|
7
|
+
import { driveSyncLift, EventCode, withActivation, hasRealHostCall, dispatchableTail, NeedsJspi, needsJspi, packSubtaskResult, PendingCapability, notifyInstancePoisoned, realHostCalls, storeQuiescent, Subtask, WaitableSet, SubtaskState, Task, Thread, withPoisonCause, } from "../task/mod.js";
|
|
8
8
|
import { currentTask } from "../task/scheduler.js";
|
|
9
9
|
import { PlanError } from "../plan/loader.js";
|
|
10
10
|
import { blockCurrentActivation, enterWasm, } from "../jspi/mod.js";
|
|
@@ -290,7 +290,7 @@ function traceDrive(loop, store, done, branch) {
|
|
|
290
290
|
`waiting=${store.waiting.length}{${waiters}} ` +
|
|
291
291
|
`awaiting=${store.awaiting.size} ` +
|
|
292
292
|
`hostCalls=${store.pendingHostCalls.size} ` +
|
|
293
|
-
`awaiters={${awaiters}}
|
|
293
|
+
`awaiters={${awaiters}} pending=${store.pendingResumptions.size} done=${doneVerdict}`);
|
|
294
294
|
}
|
|
295
295
|
/**
|
|
296
296
|
* Pump `store` until `done()` holds. Returns `undefined` if that was achieved
|
|
@@ -326,10 +326,10 @@ function drive(store, done, what) {
|
|
|
326
326
|
}
|
|
327
327
|
// A thread parked on a Promise (jspi) can only progress after a microtask
|
|
328
328
|
// turn, exactly like an outstanding host call. So can an outstanding
|
|
329
|
-
//
|
|
330
|
-
// run yet (see `Store.tick`).
|
|
331
|
-
if (store.awaiting.size > 0 ||
|
|
332
|
-
traceDrive("drive", store, done, "->async(awaiting/
|
|
329
|
+
// pending resumption of THIS store: a suspension has been settled and its
|
|
330
|
+
// activation has not run yet (see `Store.tick`).
|
|
331
|
+
if (store.awaiting.size > 0 || store.hasPendingResumptions()) {
|
|
332
|
+
traceDrive("drive", store, done, "->async(awaiting/pending)");
|
|
333
333
|
return driveAsync(store, done, what);
|
|
334
334
|
}
|
|
335
335
|
if (store.pendingHostCalls.size === 0) {
|
|
@@ -407,9 +407,15 @@ export async function driveStoreAsync(store, done, what) {
|
|
|
407
407
|
* racing the same parked thread await the *same* tag object and see one
|
|
408
408
|
* settlement, not two independent ones. This is what makes (a)'s
|
|
409
409
|
* "queued at tag settlement" premise hold across loops.
|
|
410
|
-
* (c) The
|
|
411
|
-
*
|
|
412
|
-
*
|
|
410
|
+
* (c) The store's pending-resumption set (`Store.pendingResumptions`)
|
|
411
|
+
* serializes the resumption path WITHIN a store: every loop driving
|
|
412
|
+
* that store yields at its top while `store.hasPendingResumptions()`,
|
|
413
|
+
* so a settled activation runs before anything else is scheduled.
|
|
414
|
+
* (Until 2026-08-22 this was a module-global single slot with a
|
|
415
|
+
* one-claimant assert; per-store multi-entry replaced it — issues #158
|
|
416
|
+
* mechanism B and #210. Overlapping loops in the sense meant here are
|
|
417
|
+
* loops on the SAME store, which is exactly what (c) still covers;
|
|
418
|
+
* loops on different stores never shared a settlement to race for.)
|
|
413
419
|
*
|
|
414
420
|
* (a) is the guarantee; (b) and (c) are what make (a) apply across loops
|
|
415
421
|
* rather than only within one. The one corner (a) does NOT cover — a thread
|
|
@@ -610,22 +616,29 @@ async function driveAsync(store, done, what) {
|
|
|
610
616
|
store.serviceSettled();
|
|
611
617
|
if (store.hostFailure !== undefined)
|
|
612
618
|
throw takeHostFailure(store);
|
|
613
|
-
// A
|
|
614
|
-
// has not yet parked again or finished. It will
|
|
615
|
-
// consumes it (`blockCurrentActivation`),
|
|
616
|
-
// (`Store.noteAwaiting`'s settle continuation) — so
|
|
617
|
-
// until it does. The driver must NOT blanket-clear here:
|
|
618
|
-
// have been taken by a guest built-in settling another
|
|
619
|
-
// suspension (`subtask.cancel` delivering a cancellation),
|
|
620
|
-
// it before that activation runs re-opens the
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
619
|
+
// A pending resumption of THIS store is an engine-driven resumption in
|
|
620
|
+
// flight: its activation has not yet parked again or finished. It will
|
|
621
|
+
// die on its own — parking consumes it (`blockCurrentActivation`),
|
|
622
|
+
// finishing releases it (`Store.noteAwaiting`'s settle continuation) — so
|
|
623
|
+
// yield microtasks until it does. The driver must NOT blanket-clear here:
|
|
624
|
+
// an entry may have been taken by a guest built-in settling another
|
|
625
|
+
// activation's suspension (`subtask.cancel` delivering a cancellation),
|
|
626
|
+
// and clearing it before that activation runs re-opens the
|
|
627
|
+
// mis-attribution window the entry exists to close.
|
|
628
|
+
//
|
|
629
|
+
// PER-STORE (issue #210): this gate used to read a module-global slot, so
|
|
630
|
+
// an idle store's driver spun here — and died at the hop bound below in
|
|
631
|
+
// ~311ms — merely because ANOTHER store's guest was dwelling on a slow
|
|
632
|
+
// host import. Activations never cross stores; another store's pending
|
|
633
|
+
// resumption is none of this loop's business.
|
|
634
|
+
if (store.hasPendingResumptions()) {
|
|
635
|
+
traceDrive("driveAsync", store, done, "yield-pending");
|
|
636
|
+
// Bounded: a pending entry that never dies is an internal bug (every
|
|
637
|
+
// path out of a resumed activation releases it — park, finish, trap),
|
|
638
|
+
// and a pure-microtask wait would otherwise starve the event loop and
|
|
639
|
+
// every stall timer with it. Interleave macrotask hops so timers stay
|
|
640
|
+
// alive, and fail loudly rather than spin forever. Scoped per store,
|
|
641
|
+
// this is again the internal-bug detector it was meant to be.
|
|
629
642
|
claimHops++;
|
|
630
643
|
assert_(claimHops < 10_000, "driveAsync: a resumed-activation claim was never released " +
|
|
631
644
|
"(the activation neither parked, finished, nor trapped)");
|
|
@@ -663,7 +676,7 @@ async function driveAsync(store, done, what) {
|
|
|
663
676
|
// Only a SERVICEABLE tail is a reason to loop again: a queue holding
|
|
664
677
|
// only tails DEFERRED on a non-enterable instance (issue #156) would
|
|
665
678
|
// spin this loop hot — nothing in the cycle awaits.
|
|
666
|
-
if (store.hasServiceableSettled() ||
|
|
679
|
+
if (store.hasServiceableSettled() || store.hasPendingResumptions()) {
|
|
667
680
|
continue;
|
|
668
681
|
}
|
|
669
682
|
// Service promise-parked threads (jspi).
|
|
@@ -700,7 +713,7 @@ async function driveAsync(store, done, what) {
|
|
|
700
713
|
// without this check it presents as a silent stall instead -- which is
|
|
701
714
|
// exactly what `tests/jspi/deadlock_test.ts` caught the moment site 2
|
|
702
715
|
// was lit.
|
|
703
|
-
if (store.pendingHostCalls.size === 0 && !
|
|
716
|
+
if (store.pendingHostCalls.size === 0 && !store.hasPendingResumptions()) {
|
|
704
717
|
traceDrive("driveAsync", store, done, "deadlock-probe");
|
|
705
718
|
// Exclude threads whose settle is already QUEUED in `store.settled`
|
|
706
719
|
// (issue #156): their promise has settled, so racing them wins
|
|
@@ -757,7 +770,7 @@ async function driveAsync(store, done, what) {
|
|
|
757
770
|
// await always has a `pendingHostCalls` entry, which fails this
|
|
758
771
|
// probe's precondition); keeping it loud is what makes it an
|
|
759
772
|
// internal-wedge detector rather than dead code.
|
|
760
|
-
if (store.pendingHostCalls.size > 0 ||
|
|
773
|
+
if (store.pendingHostCalls.size > 0 || store.hasPendingResumptions() ||
|
|
761
774
|
store.hasServiceableSettled()) {
|
|
762
775
|
continue;
|
|
763
776
|
}
|
|
@@ -838,25 +851,31 @@ async function driveAsync(store, done, what) {
|
|
|
838
851
|
for (const h of store.pendingHostCalls) {
|
|
839
852
|
others.push(h.then(() => null, () => null));
|
|
840
853
|
}
|
|
841
|
-
// A SPECULATIVE
|
|
854
|
+
// A SPECULATIVE entry: the chosen thread is a promising-wrapped
|
|
842
855
|
// activation, and the engine may run its wasm during this await (pin
|
|
843
|
-
// (i)). It is
|
|
844
|
-
//
|
|
845
|
-
// `SuspensionPoint.resume`) is what carries it, and
|
|
846
|
-
// that names a thread already gone from the
|
|
847
|
-
|
|
856
|
+
// (i)). It is dropped on the way out — if the activation is genuinely
|
|
857
|
+
// mid-resumption its own exact entry (minted by
|
|
858
|
+
// `SuspensionPoint.resume`) is what carries it, and dropping an entry
|
|
859
|
+
// that names a thread already gone from the set is a no-op.
|
|
860
|
+
//
|
|
861
|
+
// ONLY ITS OWN ENTRY (issue #158): the `finally` used to blanket-clear
|
|
862
|
+
// the single global slot, so a guest-synchronous delivery during the
|
|
863
|
+
// await — which takes a fresh entry of its own — had that entry
|
|
864
|
+
// clobbered early, re-opening the window it exists to close. With a set
|
|
865
|
+
// we can name exactly what we added.
|
|
866
|
+
store.addPendingResumption(chosen);
|
|
848
867
|
let winner;
|
|
849
868
|
try {
|
|
850
869
|
winner = await Promise.race([chosenTag, ...others]);
|
|
851
870
|
}
|
|
852
871
|
finally {
|
|
853
|
-
|
|
872
|
+
store.removePendingResumption(chosen);
|
|
854
873
|
}
|
|
855
874
|
// Resume whichever thread actually settled -- not necessarily the one we
|
|
856
875
|
// claimed. Resuming only the claimed thread would spin: its promise may
|
|
857
876
|
// never settle, the same thread would be chosen again next turn, and the
|
|
858
877
|
// already-settled tags would win the race instantly forever (observed as
|
|
859
|
-
// an OOM, not a hang).
|
|
878
|
+
// an OOM, not a hang). Our own entry is dropped above before any resumption,
|
|
860
879
|
// exactly as on the original single-promise path, so this does not widen
|
|
861
880
|
// the ambient window; it only ensures the loop always makes progress.
|
|
862
881
|
// Membership is not enough: the corner it misses is a thread the OTHER
|
package/esm/exec/executor.js
CHANGED
|
@@ -82,8 +82,26 @@ class Executor {
|
|
|
82
82
|
* another.
|
|
83
83
|
*/
|
|
84
84
|
store = new Store();
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Memoized `unsafe-intrinsic` core functions, by DECLARING COMPONENT
|
|
87
|
+
* INSTANCE and symbol.
|
|
88
|
+
*
|
|
89
|
+
* The instance is part of the key because `context.{get,set}` resolve their
|
|
90
|
+
* thread against it (`currentThreadForInstance`, task/scheduler.ts): the
|
|
91
|
+
* declaring instance is the one whose core frame is executing when the
|
|
92
|
+
* intrinsic is called, which is what keeps a JSPI continuation chunk's
|
|
93
|
+
* `context.set` out of a sibling task's slots. `null` keys the shared
|
|
94
|
+
* adapter/instance-less flavour (the plan records `instance: null` for FACT
|
|
95
|
+
* adapter modules).
|
|
96
|
+
*/
|
|
86
97
|
unsafeIntrinsics = new Map();
|
|
98
|
+
/**
|
|
99
|
+
* The component instance of the core module currently being instantiated
|
|
100
|
+
* (`instantiate-module`'s `instance` field; null for a FACT adapter). Read
|
|
101
|
+
* by `unsafeIntrinsic` while its import list is being resolved — the one
|
|
102
|
+
* place a core instance's owning component instance is stated by the plan.
|
|
103
|
+
*/
|
|
104
|
+
#declaringInstance = null;
|
|
87
105
|
/** The single in-flight FACT `prepare-call` state (intrinsics/fact_calls.ts). */
|
|
88
106
|
preparedCall = { current: null };
|
|
89
107
|
/**
|
|
@@ -353,6 +371,13 @@ class Executor {
|
|
|
353
371
|
// suspension point; every function it exports is therefore
|
|
354
372
|
// potentially-blocking, and everything else is not.
|
|
355
373
|
this.sawBlockingImport = false;
|
|
374
|
+
// Which component instance this core module belongs to — the plan
|
|
375
|
+
// states it here and nowhere else (`instance: null` = FACT adapter,
|
|
376
|
+
// contracts/plan-format.md). `unsafeIntrinsic` reads it while the
|
|
377
|
+
// import list below is resolved.
|
|
378
|
+
this.#declaringInstance = init.instance === null
|
|
379
|
+
? null
|
|
380
|
+
: this.componentInstance(init.instance);
|
|
356
381
|
// ISSUE #88: core wasm permits two imports with the same
|
|
357
382
|
// (module, field) pair (trusted wasmtime-environ 47.0.3 info.rs
|
|
358
383
|
// :438-445 gives one flat positional CoreDef per import slot, but
|
|
@@ -390,6 +415,10 @@ class Executor {
|
|
|
390
415
|
{})[imp.name] =
|
|
391
416
|
value;
|
|
392
417
|
});
|
|
418
|
+
// Scoped strictly to the import list above: a CoreDef resolved by
|
|
419
|
+
// any other initializer (extract-*, resource dtors) names no core
|
|
420
|
+
// module, so it must not inherit this one's instance.
|
|
421
|
+
this.#declaringInstance = null;
|
|
393
422
|
let instance;
|
|
394
423
|
try {
|
|
395
424
|
instance = await WebAssembly.instantiate(module, importObject);
|
|
@@ -670,10 +699,12 @@ class Executor {
|
|
|
670
699
|
return m;
|
|
671
700
|
}
|
|
672
701
|
unsafeIntrinsic(symbol) {
|
|
673
|
-
|
|
702
|
+
const inst = this.#declaringInstance;
|
|
703
|
+
const key = `${inst === null ? "-" : inst.index}\0${symbol}`;
|
|
704
|
+
let fn = this.unsafeIntrinsics.get(key);
|
|
674
705
|
if (fn === undefined) {
|
|
675
|
-
fn = createUnsafeIntrinsic(symbol);
|
|
676
|
-
this.unsafeIntrinsics.set(
|
|
706
|
+
fn = createUnsafeIntrinsic(symbol, inst);
|
|
707
|
+
this.unsafeIntrinsics.set(key, fn);
|
|
677
708
|
}
|
|
678
709
|
return fn;
|
|
679
710
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// memory access for wasmtime's own internals; they have no portable meaning
|
|
11
11
|
// in a JS host and are refused at instantiate time.
|
|
12
12
|
import { assert_, trapIf } from "../cabi/trap.js";
|
|
13
|
-
import {
|
|
13
|
+
import { currentThreadForInstance } from "../task/mod.js";
|
|
14
14
|
import { ambientDebug, dbgId } from "../task/scheduler.js";
|
|
15
15
|
import { UnsupportedFeatureError } from "./errors.js";
|
|
16
16
|
/**
|
|
@@ -28,8 +28,8 @@ export const NUM_CONTEXT_SLOTS = 2;
|
|
|
28
28
|
* in slot 0, which is why this intrinsic is the entry blocker for async
|
|
29
29
|
* guests.
|
|
30
30
|
*/
|
|
31
|
-
export function canonContextGet(i) {
|
|
32
|
-
const thread =
|
|
31
|
+
export function canonContextGet(i, inst) {
|
|
32
|
+
const thread = currentThreadForInstance(inst);
|
|
33
33
|
assert_(i < NUM_CONTEXT_SLOTS, `context.get slot ${i} out of range`);
|
|
34
34
|
const result = thread.storage[i];
|
|
35
35
|
assert_(result < 2 ** 32, "context.get value out of i32 range");
|
|
@@ -53,11 +53,11 @@ export function ctxThreadId(t) {
|
|
|
53
53
|
}
|
|
54
54
|
function trace(msg, thread) {
|
|
55
55
|
const a = ambientDebug();
|
|
56
|
-
console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${JSON.stringify(thread.storage)} | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${a.claims.map(ctxThreadId).join(",")}]
|
|
56
|
+
console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${JSON.stringify(thread.storage)} | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${a.claims.map(ctxThreadId).join(",")}]`);
|
|
57
57
|
}
|
|
58
58
|
/** definitions.py `canon_context_set` (line 2358). */
|
|
59
|
-
export function canonContextSet(i, v) {
|
|
60
|
-
const thread =
|
|
59
|
+
export function canonContextSet(i, v, inst) {
|
|
60
|
+
const thread = currentThreadForInstance(inst);
|
|
61
61
|
assert_(i < NUM_CONTEXT_SLOTS, `context.set slot ${i} out of range`);
|
|
62
62
|
if (CTX_TRACE)
|
|
63
63
|
trace(`set[${i}] = ${v >>> 0}`, thread);
|
|
@@ -70,7 +70,17 @@ export function canonContextSet(i, v) {
|
|
|
70
70
|
* unimplementable symbol fails instantiation rather than the first call
|
|
71
71
|
* (contracts/plan-format.md "Executor obligations").
|
|
72
72
|
*/
|
|
73
|
-
export function createUnsafeIntrinsic(symbol
|
|
73
|
+
export function createUnsafeIntrinsic(symbol,
|
|
74
|
+
/**
|
|
75
|
+
* The component instance whose core module declares this import — the
|
|
76
|
+
* instance whose frame is, by construction, the one executing when it is
|
|
77
|
+
* called. `undefined`/`null` (a FACT adapter module, which the plan records
|
|
78
|
+
* with `instance: null`) falls back to the unscoped ambient. See
|
|
79
|
+
* `currentThreadForInstance` (task/scheduler.ts) for why this discriminator
|
|
80
|
+
* is what makes a JSPI continuation chunk's `context.set` land in its own
|
|
81
|
+
* thread's slots.
|
|
82
|
+
*/
|
|
83
|
+
inst) {
|
|
74
84
|
const match = /^context-(get|set)-i32-(\d+)$/.exec(symbol);
|
|
75
85
|
if (match === null) {
|
|
76
86
|
throw new UnsupportedFeatureError("M2", `component imports the unsafe intrinsic '${symbol}', which has no ` +
|
|
@@ -83,8 +93,8 @@ export function createUnsafeIntrinsic(symbol) {
|
|
|
83
93
|
trapIf(slot >= NUM_CONTEXT_SLOTS, `unsafe intrinsic '${symbol}' addresses context slot ${slot}, but a ` +
|
|
84
94
|
`thread has ${NUM_CONTEXT_SLOTS}`);
|
|
85
95
|
if (match[1] === "get")
|
|
86
|
-
return () => canonContextGet(slot);
|
|
96
|
+
return () => canonContextGet(slot, inst);
|
|
87
97
|
return (v) => {
|
|
88
|
-
canonContextSet(slot, (v ?? 0) >>> 0);
|
|
98
|
+
canonContextSet(slot, (v ?? 0) >>> 0, inst);
|
|
89
99
|
};
|
|
90
100
|
}
|
package/esm/jspi/bridge.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
// results to become Promises where cabi needs a number synchronously.
|
|
43
43
|
import { assert_ } from "../cabi/trap.js";
|
|
44
44
|
import { isSupported, makePromising, makeSuspending } from "./mechanics.js";
|
|
45
|
-
import { withActivation, claimActivationAmbient, dbgId,
|
|
45
|
+
import { withActivation, claimActivationAmbient, dbgId, maybeCurrentThread, releaseActivationAmbient, } from "../task/mod.js";
|
|
46
46
|
/**
|
|
47
47
|
* Decide the mode for one instantiation.
|
|
48
48
|
*
|
|
@@ -473,9 +473,20 @@ export class SuspensionPoint {
|
|
|
473
473
|
// like the value path. Missing it here is what `trap-if-done.wast:448`
|
|
474
474
|
// and the `assert_trap` rows of `big-interleaving-test.wast` detect
|
|
475
475
|
// ("exit-sync-call with an empty sync-call stack").
|
|
476
|
+
//
|
|
477
|
+
// Symmetry with the success arm below (issue #158): if a pending
|
|
478
|
+
// resumption of this store names the activation currently executing,
|
|
479
|
+
// that activation is the code delivering this resume (a running guest's
|
|
480
|
+
// `subtask.cancel`, whose `produce` computes a trap), so its window has
|
|
481
|
+
// closed — retire that entry here. Historically this also avoided a
|
|
482
|
+
// false-positive one-claimant assert that preempted `#fail(e)`, handing
|
|
483
|
+
// the parked guest an AssertionError in place of its trap; the assert is
|
|
484
|
+
// gone with the single-slot claim (#158 mechanism B), but the entry must
|
|
485
|
+
// still be retired here or the store stays gated on a finished window.
|
|
486
|
+
this.#store.consumePendingIfRunning();
|
|
476
487
|
if (maybeCurrentThread() === undefined)
|
|
477
488
|
claimActivationAmbient(this.owner);
|
|
478
|
-
|
|
489
|
+
this.#store.addPendingResumption(this.task?.implicitThread ?? null);
|
|
479
490
|
this.#fail(e);
|
|
480
491
|
return;
|
|
481
492
|
}
|
|
@@ -483,25 +494,25 @@ export class SuspensionPoint {
|
|
|
483
494
|
// settling the import's Promise hands control to wasm, which will call
|
|
484
495
|
// built-ins with an empty bracket stack. The claim names `owner` — the
|
|
485
496
|
// activation captured when this point was minted — not a guess derived
|
|
486
|
-
// now; see `owner` and `
|
|
497
|
+
// now; see `owner` and `Store.pendingResumptions`.
|
|
487
498
|
//
|
|
488
|
-
// If
|
|
489
|
-
// is the code that called us — a running guest's
|
|
490
|
-
// delivering a cancellation settles the callee's
|
|
491
|
-
// its own frame), that
|
|
492
|
-
//
|
|
493
|
-
|
|
499
|
+
// If a pending resumption of this store names the activation currently
|
|
500
|
+
// executing (it is the code that called us — a running guest's
|
|
501
|
+
// `subtask.cancel` delivering a cancellation settles the callee's
|
|
502
|
+
// suspension from inside its own frame), that entry has served its
|
|
503
|
+
// purpose; retire it.
|
|
504
|
+
this.#store.consumePendingIfRunning();
|
|
494
505
|
// The activation-ambient claim (site (i) in scheduler.ts). Taken only when
|
|
495
506
|
// NOBODY is running right now: if a guest activation is executing, `owner`
|
|
496
507
|
// does not run until that activation yields, and pushing onto a
|
|
497
508
|
// LAST-IN-FIRST-OUT stack now would make `owner` the ambient for the
|
|
498
|
-
// caller's remaining frame. In that shape `owner` is picked up
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
509
|
+
// caller's remaining frame. In that shape `owner` is picked up by its own
|
|
510
|
+
// first `Suspending` call (site (ii)); the retired tier-3 slot used to
|
|
511
|
+
// cover the window before that, and the measurement behind its retirement
|
|
512
|
+
// (#158, see `resolveAmbient`) says nothing ever read it there.
|
|
502
513
|
if (maybeCurrentThread() === undefined)
|
|
503
514
|
claimActivationAmbient(this.owner);
|
|
504
|
-
|
|
515
|
+
this.#store.addPendingResumption(this.task?.implicitThread ?? null);
|
|
505
516
|
this.#settle(value);
|
|
506
517
|
}
|
|
507
518
|
/** Abandon this suspension without resuming the guest (teardown paths). */
|
|
@@ -568,11 +579,11 @@ export function blockCurrentActivation(input) {
|
|
|
568
579
|
// and strands the point with no owner (measured: `cancellable.wast:322`
|
|
569
580
|
// then reported `pending-capability: instantiation-time task context`).
|
|
570
581
|
const owner = maybeCurrentThread() ?? input.task?.implicitThread ?? null;
|
|
571
|
-
// The activation is parking: if it still carried the
|
|
572
|
-
// from the settle that resumed it, that
|
|
573
|
-
// other closing edge — the activation FINISHING — is handled by
|
|
582
|
+
// The activation is parking: if it still carried the pending-resumption
|
|
583
|
+
// entry from the settle that resumed it, that entry's window closes here
|
|
584
|
+
// (the other closing edge — the activation FINISHING — is handled by
|
|
574
585
|
// `Store.noteAwaiting`'s settle continuation).
|
|
575
|
-
|
|
586
|
+
input.store.consumePendingIfRunning();
|
|
576
587
|
releaseActivationAmbient(owner);
|
|
577
588
|
const point = new SuspensionPoint(input.store, input.task, input.readyFunc, input.cancellable, input.produce, owner, input.onSettled);
|
|
578
589
|
return point.promise;
|
package/esm/task/scheduler.js
CHANGED
|
@@ -269,7 +269,7 @@ export function withActivation(t, fn) {
|
|
|
269
269
|
* async-context store held: the store was written by `withActivation` and by
|
|
270
270
|
* nothing else, so a built-in reached under a scheduler `resume()` bracket
|
|
271
271
|
* that had not (yet) entered wasm saw NO store, even though `threadStack`
|
|
272
|
-
* named a thread. `
|
|
272
|
+
* named a thread. `Store.consumePendingIfRunning` — the driver-gate
|
|
273
273
|
* release whose scheduling effects the corpus pins precisely — asked exactly
|
|
274
274
|
* that question, so it must keep asking exactly that question — measured:
|
|
275
275
|
* routing it through the full `threadStack` instead moved 64 conformance
|
|
@@ -280,7 +280,8 @@ export function withActivation(t, fn) {
|
|
|
280
280
|
const entryStack = [];
|
|
281
281
|
/**
|
|
282
282
|
* "Whose wasm frame are we lexically inside, or running on behalf of?" — the
|
|
283
|
-
* async-context store's replacement, used only by
|
|
283
|
+
* async-context store's replacement, used only by
|
|
284
|
+
* `Store.consumePendingIfRunning`.
|
|
284
285
|
*/
|
|
285
286
|
// deno-lint-ignore no-explicit-any
|
|
286
287
|
function activationOf() {
|
|
@@ -346,8 +347,9 @@ function activationOf() {
|
|
|
346
347
|
* The opposite shape — A settles B's suspension so B runs AFTER A — is
|
|
347
348
|
* deliberately NOT represented here: `SuspensionPoint.resume` pushes only when
|
|
348
349
|
* nothing is currently running, so B never shadows A. B is picked up by its
|
|
349
|
-
* own first `Suspending` call
|
|
350
|
-
* slot
|
|
350
|
+
* own first `Suspending` call. (Until 2026-08-22 a third ambient tier — the
|
|
351
|
+
* driver's `resumingThread` slot — also named B here; it was retired with the
|
|
352
|
+
* slot, see `resolveAmbient` and `Store.pendingResumptions`.)
|
|
351
353
|
*
|
|
352
354
|
* An activation leaves this stack when it parks again
|
|
353
355
|
* (`blockCurrentActivation`) or finishes (its `awaitValue` promise settles —
|
|
@@ -364,7 +366,7 @@ const activationClaims = [];
|
|
|
364
366
|
* direct evidence that `t` is running RIGHT NOW (its Suspending import just
|
|
365
367
|
* returned into its wasm). The previous early-return kept stale order: a
|
|
366
368
|
* nested callee's claim whose release edge is a promise reaction
|
|
367
|
-
* (`Store.noteAwaiting` -> `
|
|
369
|
+
* (`Store.noteAwaiting` -> `Store.releasePendingOf`) outlives the callee by a
|
|
368
370
|
* microtask, and an outer activation's continuation chunk that resumed in
|
|
369
371
|
* that window re-claimed itself as a NOOP — leaving the finished callee on
|
|
370
372
|
* top, so every ambient read in the rest of the chunk (the next hop's
|
|
@@ -395,7 +397,8 @@ export function claimActivationAmbient(t) {
|
|
|
395
397
|
function traceAmbient(what, t) {
|
|
396
398
|
// Lazy import avoidance: reuse context.ts's ids via a local map.
|
|
397
399
|
console.error(`[amb] ${what} ${dbgId(t)} | stack=[${threadStack.map(dbgId).join(",")}] ` +
|
|
398
|
-
`claims=[${activationClaims.map(dbgId).join(",")}]
|
|
400
|
+
`claims=[${activationClaims.map(dbgId).join(",")}]` +
|
|
401
|
+
`\n${(new Error().stack ?? "").split("\n").slice(2, 6).join("\n")}`);
|
|
399
402
|
}
|
|
400
403
|
const dbgIds = new WeakMap();
|
|
401
404
|
let nextDbgId = 1;
|
|
@@ -437,78 +440,17 @@ export function releaseActivationAmbient(t) {
|
|
|
437
440
|
activationClaims.splice(i, 1);
|
|
438
441
|
}
|
|
439
442
|
// ---------------------------------------------------------------------------
|
|
440
|
-
// The
|
|
443
|
+
// The resumed-but-not-yet-run gate (a SEPARATE concern from the ambient above)
|
|
441
444
|
// ---------------------------------------------------------------------------
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
* because an activation that merely hopped (case (ii) above) legitimately
|
|
452
|
-
* holds an ambient while the scheduler is free to proceed.
|
|
453
|
-
*/
|
|
454
|
-
// deno-lint-ignore no-explicit-any
|
|
455
|
-
let resumingThread = null;
|
|
456
|
-
/** Claim the ambient for `t` across an engine-driven resumption. */
|
|
457
|
-
// deno-lint-ignore no-explicit-any
|
|
458
|
-
export function setResumingThread(t) {
|
|
459
|
-
if (AMBIENT_TRACE)
|
|
460
|
-
traceAmbient("set-resuming", t);
|
|
461
|
-
assert_(resumingThread === null || resumingThread === t, "two activations claim the resumed ambient at once — the " +
|
|
462
|
-
"resolve-one-per-turn discipline was violated");
|
|
463
|
-
resumingThread = t;
|
|
464
|
-
}
|
|
465
|
-
/** Is a settled-but-not-yet-run activation holding the ambient? */
|
|
466
|
-
export function hasResumingThread() {
|
|
467
|
-
return resumingThread !== null;
|
|
468
|
-
}
|
|
469
|
-
/** Release the claim; called once we are back in our own continuation. */
|
|
470
|
-
export function clearResumingThread() {
|
|
471
|
-
resumingThread = null;
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Release the driver's claim iff its activation is demonstrably RUNNING —
|
|
475
|
-
* i.e. the claim names the same thread the ACTIVATION AMBIENT names for the
|
|
476
|
-
* code calling us. The claim exists to cover the window between settling a
|
|
477
|
-
* suspension and the resumed activation running; once that activation's own
|
|
478
|
-
* code is on the stack the window is closed, and holding the claim would
|
|
479
|
-
* falsely trip the one-claimant assert when the running activation's built-in
|
|
480
|
-
* settles ANOTHER activation's suspension — `subtask.cancel` delivering a
|
|
481
|
-
* cancellation to a parked callee (cancellable.wast) is exactly that shape.
|
|
482
|
-
* When the two disagree (or no ambient is present) the claim stays, and the
|
|
483
|
-
* assert keeps guarding the genuine two-unrun-claimants bug it was built for.
|
|
484
|
-
*
|
|
485
|
-
* The comparison used to be against the async-context store; it is now
|
|
486
|
-
* against `activationOf()`, which is the same statement made explicitly.
|
|
487
|
-
*/
|
|
488
|
-
export function consumeClaimIfRunning() {
|
|
489
|
-
if (resumingThread !== null && activationOf() === resumingThread) {
|
|
490
|
-
resumingThread = null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Release the claim iff it names `t` — the settle-side half of the claim
|
|
495
|
-
* discipline: a claim taken when `t`'s suspension was settled dies when `t`'s
|
|
496
|
-
* activation finishes (its `awaitValue` promise settles; `Store.noteAwaiting`
|
|
497
|
-
* calls this from the eager settle continuation) or parks again
|
|
498
|
-
* (`blockCurrentActivation` consumes via `consumeClaimIfRunning`).
|
|
499
|
-
*
|
|
500
|
-
* `t` FINISHING also ends its activation ambient, so both are dropped here.
|
|
501
|
-
*/
|
|
502
|
-
// deno-lint-ignore no-explicit-any
|
|
503
|
-
export function releaseClaimOf(t) {
|
|
504
|
-
releaseActivationAmbient(t);
|
|
505
|
-
if (resumingThread !== null &&
|
|
506
|
-
(resumingThread === t ||
|
|
507
|
-
t?.task?.implicitThread ===
|
|
508
|
-
resumingThread)) {
|
|
509
|
-
resumingThread = null;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
445
|
+
//
|
|
446
|
+
// This used to be a module-global single slot, `resumingThread`, doing two
|
|
447
|
+
// jobs: (1) the DRIVER's scheduling gate ("a suspension was settled and its
|
|
448
|
+
// activation has not run yet — do not schedule anything else"), and (2) tier 3
|
|
449
|
+
// of ambient resolution. Job (2) was retired on 2026-08-22 (issue #158) after
|
|
450
|
+
// measurement showed it never decided a read; job (1) is real, but it is
|
|
451
|
+
// per-Store SET semantics, not a global identity slot — see
|
|
452
|
+
// `Store.pendingResumptions` below, and `resolveAmbient` for the retirement
|
|
453
|
+
// evidence.
|
|
512
454
|
const AMBIENT_TRACE = (() => {
|
|
513
455
|
try {
|
|
514
456
|
return Deno.env.get("CE_AMBIENT_TRACE") === "1";
|
|
@@ -522,14 +464,17 @@ export function ambientDebug() {
|
|
|
522
464
|
return {
|
|
523
465
|
stack: [...threadStack],
|
|
524
466
|
claims: [...activationClaims],
|
|
525
|
-
resuming: resumingThread,
|
|
526
467
|
};
|
|
527
468
|
}
|
|
528
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Diagnostic: module-scope AMBIENT state that must NOT survive a completed
|
|
471
|
+
* call. The scheduling gate is no longer module-scope — a store's
|
|
472
|
+
* `pendingResumptions` set is the per-Store analogue and is checked there.
|
|
473
|
+
*/
|
|
529
474
|
export function ambientResidue() {
|
|
530
475
|
return {
|
|
531
476
|
stack: threadStack.length,
|
|
532
|
-
claim:
|
|
477
|
+
claim: activationClaims.length > 0,
|
|
533
478
|
};
|
|
534
479
|
}
|
|
535
480
|
/**
|
|
@@ -543,9 +488,9 @@ export function ambientResidue() {
|
|
|
543
488
|
* is running outside our frames (a `Suspending` hop or a resumption).
|
|
544
489
|
* LIFO, because activations nest: an outer activation's built-in can
|
|
545
490
|
* synchronously enter an inner one's wasm.
|
|
546
|
-
* 3. `resumingThread
|
|
547
|
-
*
|
|
548
|
-
*
|
|
491
|
+
* (There is no tier 3. A third tier — `resumingThread`, the driver's
|
|
492
|
+
* settle-time claim — existed from M3A-1 until 2026-08-22 and was
|
|
493
|
+
* RETIRED, see below.)
|
|
549
494
|
*
|
|
550
495
|
* Tier 2 replaced an async-context store (M3A-1). The store held
|
|
551
496
|
* precisely "the innermost wasm activation currently executing, across the
|
|
@@ -565,18 +510,36 @@ export function ambientResidue() {
|
|
|
565
510
|
* one reader measured as "no change" because the failing sites used the other.
|
|
566
511
|
* Do not add a third reader; extend this one. (`activationOf` above is not a
|
|
567
512
|
* second reader -- it answers a different question, "whose wasm frame are we
|
|
568
|
-
* running on behalf of", and is used only by
|
|
513
|
+
* running on behalf of", and is used only by
|
|
514
|
+
* `Store.consumePendingIfRunning`.)
|
|
515
|
+
*
|
|
516
|
+
* TIER 3 RETIRED, 2026-08-22 (issue #158). The bottom tier used to be
|
|
517
|
+
* `resumingThread`, the driver's settle-time claim -- a last resort that named
|
|
518
|
+
* whichever activation was settled or claimed across an await, right for that
|
|
519
|
+
* one and wrong for every other in-flight activation. It was removed on the
|
|
520
|
+
* strength of a re-run of the M3A-1 differential methodology: an instrumented
|
|
521
|
+
* build counted every read where tiers 1-2 were empty and the slot was live,
|
|
522
|
+
* and measured ZERO deciding reads across the conformance corpus (FIFO,
|
|
523
|
+
* 1257/0), both seeded shuffles (`POLYENGINE_SCHED_SEED` 1 and 4242),
|
|
524
|
+
* test-runtime (all jspi pins), and the smoke-tls three-async-component #24
|
|
525
|
+
* corpus. A removal build then ran green on every engine lane we have:
|
|
526
|
+
* test-runtime, test-protocol, conformance (1257/0, no expectation changes),
|
|
527
|
+
* sched-seeds, the shells (sm + node + jsc + bun, all "OK, matches
|
|
528
|
+
* expectation"), the browsers (chromium + firefox), smoke-tls and smoke-c0.
|
|
529
|
+
* The reading: post-#24 the sentinel discipline (tier 2's claim/release edges)
|
|
530
|
+
* always answers first, so the slot's attribution role was vestigial. Its
|
|
531
|
+
* other, live role -- the scheduling gate -- survives as the per-Store
|
|
532
|
+
* `Store.pendingResumptions` set.
|
|
569
533
|
*/
|
|
570
534
|
function resolveAmbient() {
|
|
571
535
|
return threadStack[threadStack.length - 1] ??
|
|
572
|
-
activationClaims[activationClaims.length - 1] ??
|
|
536
|
+
activationClaims[activationClaims.length - 1] ??
|
|
573
537
|
undefined;
|
|
574
538
|
}
|
|
575
539
|
export function currentThread() {
|
|
576
540
|
if (AMBIENT_TRACE && threadStack.length === 0) {
|
|
577
541
|
console.error(`[ambient] bracket empty; claims=${activationClaims.length} ` +
|
|
578
|
-
`head=${activationClaims[0]?.constructor?.name ?? "none"}
|
|
579
|
-
`resuming=${resumingThread?.constructor?.name ?? "none"}`);
|
|
542
|
+
`head=${activationClaims[0]?.constructor?.name ?? "none"}`);
|
|
580
543
|
}
|
|
581
544
|
const t = resolveAmbient();
|
|
582
545
|
if (t === undefined) {
|
|
@@ -604,6 +567,76 @@ export function currentThread() {
|
|
|
604
567
|
export function maybeCurrentThread() {
|
|
605
568
|
return resolveAmbient();
|
|
606
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* THE ambient, NARROWED BY THE INSTANCE WHOSE CORE FRAME IS EXECUTING.
|
|
572
|
+
*
|
|
573
|
+
* For a built-in whose declaration names a component instance, "who is
|
|
574
|
+
* running" is not an open question about the whole store: the call arrived
|
|
575
|
+
* from a core frame OF THAT INSTANCE, so the running activation is one of
|
|
576
|
+
* that instance's. This narrows `resolveAmbient` accordingly — same tiers,
|
|
577
|
+
* same order, candidates filtered — and falls back to the unscoped answer
|
|
578
|
+
* when the instance has no candidate at all (the instantiation-time shape,
|
|
579
|
+
* and any built-in reached before its instance has a task).
|
|
580
|
+
*
|
|
581
|
+
* WHY IT IS NEEDED (polyengine#24's residue; polyvisor#49 trap 1,
|
|
582
|
+
* `runtime/tests/context_attribution_test.ts`). A JSPI continuation chunk —
|
|
583
|
+
* the tail of a suspended activation, e.g. wit-bindgen's callback epilogue
|
|
584
|
+
* restoring its task pointer with `context.set` (rt/async_support.rs:592) —
|
|
585
|
+
* runs with an EMPTY `threadStack` and, unlike a hop, has no re-anchoring
|
|
586
|
+
* edge of its own. Tier 2 then answers the newest claim, which is whichever
|
|
587
|
+
* SIBLING activation suspended most recently. The attribution sentinels
|
|
588
|
+
* (jspi/bridge.ts) plant that claim one microtask ahead of the chunk, which
|
|
589
|
+
* is exact when the engine queues the resumption while the settle reaction
|
|
590
|
+
* returns (measured so in Deno's V8) — and NOT exact in Chromium, where a
|
|
591
|
+
* wider gap lets a sibling's sentinel land in between. Measured there 3/3:
|
|
592
|
+
* one task's epilogue wrote its state pointer into another task's slots, and
|
|
593
|
+
* the starved task's next callback entry hit `assert!(!state.is_null())`
|
|
594
|
+
* (async_support.rs:578) -> unreachable.
|
|
595
|
+
*
|
|
596
|
+
* Ordering discipline cannot fix that class — engine chunk boundaries are not
|
|
597
|
+
* observable, so every microtask-ordering scheme is a hope. Instance identity
|
|
598
|
+
* is not a hope: it is static (the declaration), and it is decisive because
|
|
599
|
+
* ONE INSTANCE CAN ONLY HAVE ONE ACTIVATION MID-FRAME AT A TIME — a callback
|
|
600
|
+
* invocation holds `inst.exclusiveThread` for its whole extent, suspensions
|
|
601
|
+
* included (definitions.py line 2187 / `runCallbackLoop`), and a sync or
|
|
602
|
+
* stackful-async lift holds the entry gate. Two activations that can race for
|
|
603
|
+
* an unbracketed read are therefore necessarily of different instances, which
|
|
604
|
+
* is exactly what this discriminates.
|
|
605
|
+
*
|
|
606
|
+
* SPEC BASIS. `canon_context_get`/`canon_context_set` (definitions.py 2348 /
|
|
607
|
+
* 2358) read `current_thread().storage`, and in the reference a built-in is
|
|
608
|
+
* only ever reached from inside the activation that called it — the identity
|
|
609
|
+
* is exact by construction, never inferred. This runtime has to reconstruct
|
|
610
|
+
* it; narrowing the reconstruction to the declaring instance moves it TOWARD
|
|
611
|
+
* the reference (it can only ever remove candidates the reference would never
|
|
612
|
+
* have named), never away.
|
|
613
|
+
*/
|
|
614
|
+
// deno-lint-ignore no-explicit-any
|
|
615
|
+
export function currentThreadForInstance(inst) {
|
|
616
|
+
const t = resolveAmbientForInstance(inst);
|
|
617
|
+
if (t !== undefined)
|
|
618
|
+
return t;
|
|
619
|
+
// No candidate of this instance: the unscoped ladder, including its
|
|
620
|
+
// `PendingCapability` for the instantiation-time shape.
|
|
621
|
+
return currentThread();
|
|
622
|
+
}
|
|
623
|
+
function resolveAmbientForInstance(inst) {
|
|
624
|
+
if (inst === null || inst === undefined)
|
|
625
|
+
return resolveAmbient();
|
|
626
|
+
const top = threadStack[threadStack.length - 1];
|
|
627
|
+
if (top !== undefined && instOf(top) === inst)
|
|
628
|
+
return top;
|
|
629
|
+
for (let i = activationClaims.length - 1; i >= 0; i--) {
|
|
630
|
+
const c = activationClaims[i];
|
|
631
|
+
if (instOf(c) === inst)
|
|
632
|
+
return c;
|
|
633
|
+
}
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
// deno-lint-ignore no-explicit-any
|
|
637
|
+
function instOf(t) {
|
|
638
|
+
return t?.task?.inst;
|
|
639
|
+
}
|
|
607
640
|
/** definitions.py `current_task()` (line 309). */
|
|
608
641
|
// deno-lint-ignore no-explicit-any
|
|
609
642
|
export function currentTask() {
|
|
@@ -648,6 +681,109 @@ export class Store {
|
|
|
648
681
|
* is driving the store — which is the call the guest is blocked in.
|
|
649
682
|
*/
|
|
650
683
|
hostFailure = undefined;
|
|
684
|
+
/**
|
|
685
|
+
* Resumed-but-not-yet-run activations of THIS store — the driver's
|
|
686
|
+
* scheduling gate, not an ambient.
|
|
687
|
+
*
|
|
688
|
+
* Keeping this distinct from `activationClaims` matters. This set answers
|
|
689
|
+
* "may I schedule something else right now?" (`Store.tick` and both driving
|
|
690
|
+
* loops refuse while it is non-empty, which is what forces a microtask yield
|
|
691
|
+
* so the resumed activation actually runs). `activationClaims` answers
|
|
692
|
+
* "whose code is this?". Conflating them — driving off the ambient queue —
|
|
693
|
+
* wedges the loops, because an activation that merely hopped legitimately
|
|
694
|
+
* holds an ambient while the scheduler is free to proceed.
|
|
695
|
+
*
|
|
696
|
+
* PER-STORE and MULTI-ENTRY since 2026-08-22 (issues #158 mechanism B,
|
|
697
|
+
* #210). It was one module-global slot with a one-claimant assert, which
|
|
698
|
+
* (a) could not represent two legitimately-pending engine resumptions — a
|
|
699
|
+
* running activation X delivering a resume to Z while Y's resumption was
|
|
700
|
+
* still pending crashed on the assert — and (b) made every driver on every
|
|
701
|
+
* store yield while ANY store held a claim, so an idle store's
|
|
702
|
+
* `driveStoreAsync` died at the 10,000-hop assert (~311ms) while another
|
|
703
|
+
* store merely dwelt on a slow host import. The assert's invariant was
|
|
704
|
+
* tier-3 attribution unambiguity, which no longer exists (see
|
|
705
|
+
* `resolveAmbient`), so it is gone with the slot; the entries and their
|
|
706
|
+
* release edges are otherwise unchanged, per entry.
|
|
707
|
+
*
|
|
708
|
+
* Cross-store de-serialization is safe by disjointness: an activation
|
|
709
|
+
* belongs to exactly one store. Same-store it is strictly more conservative
|
|
710
|
+
* than the old slot — the gate keeps refusing until EVERY pending entry has
|
|
711
|
+
* died, rather than crashing on the second.
|
|
712
|
+
*
|
|
713
|
+
* Release edges, per entry: the activation PARKS again
|
|
714
|
+
* (`blockCurrentActivation` -> `consumePendingIfRunning`), it FINISHES (its
|
|
715
|
+
* `awaitValue` promise settles -> `noteAwaiting` -> `releasePendingOf`), or
|
|
716
|
+
* the driver drops its own speculative entry (`removePendingResumption`).
|
|
717
|
+
*/
|
|
718
|
+
pendingResumptions = new Set();
|
|
719
|
+
/**
|
|
720
|
+
* Record that a suspension of this store has been settled and its
|
|
721
|
+
* activation has not run yet. Idempotent; a null/undefined activation is
|
|
722
|
+
* "no entry" (the instantiation-time shape that has no thread at all).
|
|
723
|
+
*
|
|
724
|
+
* No one-claimant assert: two entries are legitimate (see
|
|
725
|
+
* `pendingResumptions`). Two SuspensionPoints of ONE task cannot be pending
|
|
726
|
+
* simultaneously — a task's single activation suspends at one point at a
|
|
727
|
+
* time — so collapsing entries by identity loses nothing.
|
|
728
|
+
*/
|
|
729
|
+
addPendingResumption(t) {
|
|
730
|
+
if (t === null || t === undefined)
|
|
731
|
+
return;
|
|
732
|
+
if (AMBIENT_TRACE)
|
|
733
|
+
traceAmbient("pending+", t);
|
|
734
|
+
this.pendingResumptions.add(t);
|
|
735
|
+
}
|
|
736
|
+
/** Is some settled-but-not-yet-run activation of this store pending? */
|
|
737
|
+
hasPendingResumptions() {
|
|
738
|
+
return this.pendingResumptions.size > 0;
|
|
739
|
+
}
|
|
740
|
+
/** Drop exactly `t` (the driver's own speculative entry). */
|
|
741
|
+
removePendingResumption(t) {
|
|
742
|
+
if (AMBIENT_TRACE)
|
|
743
|
+
traceAmbient("pending-", t);
|
|
744
|
+
this.pendingResumptions.delete(t);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Drop the pending entry iff its activation is demonstrably RUNNING — i.e.
|
|
748
|
+
* the entry names the same thread the ACTIVATION AMBIENT names for the code
|
|
749
|
+
* calling us. An entry exists to cover the window between settling a
|
|
750
|
+
* suspension and the resumed activation running; once that activation's own
|
|
751
|
+
* code is on the stack the window is closed, and holding the entry would
|
|
752
|
+
* gate the store on an activation that has already had its turn — while a
|
|
753
|
+
* running activation's built-in settles ANOTHER activation's suspension
|
|
754
|
+
* (`subtask.cancel` delivering a cancellation to a parked callee,
|
|
755
|
+
* cancellable.wast) that other entry must legitimately stay.
|
|
756
|
+
*
|
|
757
|
+
* The comparison is against `activationOf()` — the wasm-ENTRY brackets,
|
|
758
|
+
* deliberately not the full `threadStack` (see `entryStack`: routing it
|
|
759
|
+
* through the full stack moved 64 conformance commands).
|
|
760
|
+
*/
|
|
761
|
+
consumePendingIfRunning() {
|
|
762
|
+
const a = activationOf();
|
|
763
|
+
if (a !== null && a !== undefined)
|
|
764
|
+
this.pendingResumptions.delete(a);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Drop the pending entry naming `t` — the settle-side half: an entry taken
|
|
768
|
+
* when `t`'s suspension was settled dies when `t`'s activation finishes (its
|
|
769
|
+
* `awaitValue` promise settles; `noteAwaiting` calls this from the eager
|
|
770
|
+
* settle continuation) or parks again (`blockCurrentActivation` consumes via
|
|
771
|
+
* `consumePendingIfRunning`).
|
|
772
|
+
*
|
|
773
|
+
* The `task.implicitThread` indirection covers entries taken against a
|
|
774
|
+
* task's implicit thread. `t` FINISHING also ends its activation ambient,
|
|
775
|
+
* so both are dropped here.
|
|
776
|
+
*/
|
|
777
|
+
// deno-lint-ignore no-explicit-any
|
|
778
|
+
releasePendingOf(t) {
|
|
779
|
+
releaseActivationAmbient(t);
|
|
780
|
+
this.pendingResumptions.delete(t);
|
|
781
|
+
const implicit = t?.task
|
|
782
|
+
?.implicitThread;
|
|
783
|
+
if (implicit !== undefined && implicit !== null) {
|
|
784
|
+
this.pendingResumptions.delete(implicit);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
651
787
|
startWaiting(t) {
|
|
652
788
|
assert_(!this.waiting.includes(t), "thread already in the waiting list");
|
|
653
789
|
this.waiting.push(t);
|
|
@@ -693,17 +829,17 @@ export class Store {
|
|
|
693
829
|
* another activation's suspension — `subtask.cancel` delivering a
|
|
694
830
|
* cancellation): the claim taken at settle time must survive until the
|
|
695
831
|
* resumed activation parks again or finishes, and "finished" is exactly
|
|
696
|
-
* this continuation firing. See `
|
|
832
|
+
* this continuation firing. See `releasePendingOf`.
|
|
697
833
|
*/
|
|
698
834
|
// deno-lint-ignore no-explicit-any
|
|
699
835
|
noteAwaiting(t, promise) {
|
|
700
836
|
this.awaiting.add(t);
|
|
701
837
|
promise.then((value) => {
|
|
702
838
|
this.settled.push({ t, value, failure: undefined });
|
|
703
|
-
|
|
839
|
+
this.releasePendingOf(t);
|
|
704
840
|
}, (e) => {
|
|
705
841
|
this.settled.push({ t, value: undefined, failure: { error: e } });
|
|
706
|
-
|
|
842
|
+
this.releasePendingOf(t);
|
|
707
843
|
});
|
|
708
844
|
}
|
|
709
845
|
/**
|
|
@@ -863,14 +999,17 @@ export class Store {
|
|
|
863
999
|
//
|
|
864
1000
|
// Settling a suspension hands control to wasm in a *microtask*, not
|
|
865
1001
|
// synchronously — so `tick` returns with the resumed activation not yet
|
|
866
|
-
// run and its
|
|
867
|
-
// before that happens would
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
|
|
1002
|
+
// run and its pending entry still outstanding. Resolving a second one
|
|
1003
|
+
// before that happens would let the first activation's built-ins
|
|
1004
|
+
// attribute themselves to the wrong task (observed as `exit-sync-call`
|
|
1005
|
+
// popping another task's bracket). Refusing to make progress while an
|
|
1006
|
+
// entry is pending forces the caller to yield to the microtask queue
|
|
1007
|
+
// first, which is exactly what `driveAsync` does.
|
|
1008
|
+
//
|
|
1009
|
+
// THIS STORE's entries only (issue #210): activations never cross stores,
|
|
1010
|
+
// so another store's pending resumption says nothing about what this one
|
|
1011
|
+
// may schedule.
|
|
1012
|
+
if (this.pendingResumptions.size > 0)
|
|
874
1013
|
return false;
|
|
875
1014
|
// Same discipline, other edge: a settled-but-unserviced activation tail
|
|
876
1015
|
// (see `settled`) is mid-"atomic resume" from the reference's point of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polyengine/runtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1-pre.g23b4970",
|
|
4
4
|
"description": "A WebAssembly Component Model host for JavaScript engines: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, and embedder API.",
|
|
5
5
|
"homepage": "https://github.com/polymorph-components/polyengine#readme",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@polyengine/protocol": "0.
|
|
58
|
+
"@polyengine/protocol": "^0.1.0"
|
|
59
59
|
},
|
|
60
60
|
"_generatedBy": "dnt@0.43.2"
|
|
61
61
|
}
|
package/types/embedder/copy.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const COPY_URL: string;
|
|
|
17
17
|
* @internal — copy-identity constant for the A9 multi-copy diagnostics; not
|
|
18
18
|
* host-facing.
|
|
19
19
|
*/
|
|
20
|
-
export declare const RUNTIME_VERSION = "0.3.
|
|
20
|
+
export declare const RUNTIME_VERSION = "0.3.1";
|
|
21
21
|
/**
|
|
22
22
|
* Compose a cross-copy diagnostic: what was foreign, which copy is speaking,
|
|
23
23
|
* the census of every copy in the graph, and the by-value remediation.
|
|
@@ -14,10 +14,10 @@ export declare const NUM_CONTEXT_SLOTS = 2;
|
|
|
14
14
|
* in slot 0, which is why this intrinsic is the entry blocker for async
|
|
15
15
|
* guests.
|
|
16
16
|
*/
|
|
17
|
-
export declare function canonContextGet(i: number): number;
|
|
17
|
+
export declare function canonContextGet(i: number, inst?: unknown): number;
|
|
18
18
|
export declare function ctxThreadId(t: unknown): string;
|
|
19
19
|
/** definitions.py `canon_context_set` (line 2358). */
|
|
20
|
-
export declare function canonContextSet(i: number, v: number): void;
|
|
20
|
+
export declare function canonContextSet(i: number, v: number, inst?: unknown): void;
|
|
21
21
|
/**
|
|
22
22
|
* Materialize one `unsafe-intrinsic` CoreDef as a core function.
|
|
23
23
|
*
|
|
@@ -25,4 +25,14 @@ export declare function canonContextSet(i: number, v: number): void;
|
|
|
25
25
|
* unimplementable symbol fails instantiation rather than the first call
|
|
26
26
|
* (contracts/plan-format.md "Executor obligations").
|
|
27
27
|
*/
|
|
28
|
-
export declare function createUnsafeIntrinsic(symbol: string
|
|
28
|
+
export declare function createUnsafeIntrinsic(symbol: string,
|
|
29
|
+
/**
|
|
30
|
+
* The component instance whose core module declares this import — the
|
|
31
|
+
* instance whose frame is, by construction, the one executing when it is
|
|
32
|
+
* called. `undefined`/`null` (a FACT adapter module, which the plan records
|
|
33
|
+
* with `instance: null`) falls back to the unscoped ambient. See
|
|
34
|
+
* `currentThreadForInstance` (task/scheduler.ts) for why this discriminator
|
|
35
|
+
* is what makes a JSPI continuation chunk's `context.set` land in its own
|
|
36
|
+
* thread's slots.
|
|
37
|
+
*/
|
|
38
|
+
inst?: unknown): CoreFn;
|
|
@@ -129,7 +129,7 @@ export declare function withActivation<T>(t: any, fn: () => T): T;
|
|
|
129
129
|
* direct evidence that `t` is running RIGHT NOW (its Suspending import just
|
|
130
130
|
* returned into its wasm). The previous early-return kept stale order: a
|
|
131
131
|
* nested callee's claim whose release edge is a promise reaction
|
|
132
|
-
* (`Store.noteAwaiting` -> `
|
|
132
|
+
* (`Store.noteAwaiting` -> `Store.releasePendingOf`) outlives the callee by a
|
|
133
133
|
* microtask, and an outer activation's continuation chunk that resumed in
|
|
134
134
|
* that window re-claimed itself as a NOOP — leaving the finished callee on
|
|
135
135
|
* top, so every ambient read in the rest of the chunk (the next hop's
|
|
@@ -154,51 +154,67 @@ export declare function dbgId(t: unknown): string;
|
|
|
154
154
|
* second edge for claims taken against a task's implicit thread.
|
|
155
155
|
*/
|
|
156
156
|
export declare function releaseActivationAmbient(t: any): void;
|
|
157
|
-
/** Claim the ambient for `t` across an engine-driven resumption. */
|
|
158
|
-
export declare function setResumingThread(t: any): void;
|
|
159
|
-
/** Is a settled-but-not-yet-run activation holding the ambient? */
|
|
160
|
-
export declare function hasResumingThread(): boolean;
|
|
161
|
-
/** Release the claim; called once we are back in our own continuation. */
|
|
162
|
-
export declare function clearResumingThread(): void;
|
|
163
|
-
/**
|
|
164
|
-
* Release the driver's claim iff its activation is demonstrably RUNNING —
|
|
165
|
-
* i.e. the claim names the same thread the ACTIVATION AMBIENT names for the
|
|
166
|
-
* code calling us. The claim exists to cover the window between settling a
|
|
167
|
-
* suspension and the resumed activation running; once that activation's own
|
|
168
|
-
* code is on the stack the window is closed, and holding the claim would
|
|
169
|
-
* falsely trip the one-claimant assert when the running activation's built-in
|
|
170
|
-
* settles ANOTHER activation's suspension — `subtask.cancel` delivering a
|
|
171
|
-
* cancellation to a parked callee (cancellable.wast) is exactly that shape.
|
|
172
|
-
* When the two disagree (or no ambient is present) the claim stays, and the
|
|
173
|
-
* assert keeps guarding the genuine two-unrun-claimants bug it was built for.
|
|
174
|
-
*
|
|
175
|
-
* The comparison used to be against the async-context store; it is now
|
|
176
|
-
* against `activationOf()`, which is the same statement made explicitly.
|
|
177
|
-
*/
|
|
178
|
-
export declare function consumeClaimIfRunning(): void;
|
|
179
|
-
/**
|
|
180
|
-
* Release the claim iff it names `t` — the settle-side half of the claim
|
|
181
|
-
* discipline: a claim taken when `t`'s suspension was settled dies when `t`'s
|
|
182
|
-
* activation finishes (its `awaitValue` promise settles; `Store.noteAwaiting`
|
|
183
|
-
* calls this from the eager settle continuation) or parks again
|
|
184
|
-
* (`blockCurrentActivation` consumes via `consumeClaimIfRunning`).
|
|
185
|
-
*
|
|
186
|
-
* `t` FINISHING also ends its activation ambient, so both are dropped here.
|
|
187
|
-
*/
|
|
188
|
-
export declare function releaseClaimOf(t: any): void;
|
|
189
157
|
/** Diagnostic (#24 probe): the full ambient state, for tracing. */
|
|
190
158
|
export declare function ambientDebug(): {
|
|
191
159
|
stack: unknown[];
|
|
192
160
|
claims: unknown[];
|
|
193
|
-
resuming: unknown;
|
|
194
161
|
};
|
|
195
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* Diagnostic: module-scope AMBIENT state that must NOT survive a completed
|
|
164
|
+
* call. The scheduling gate is no longer module-scope — a store's
|
|
165
|
+
* `pendingResumptions` set is the per-Store analogue and is checked there.
|
|
166
|
+
*/
|
|
196
167
|
export declare function ambientResidue(): {
|
|
197
168
|
stack: number;
|
|
198
169
|
claim: boolean;
|
|
199
170
|
};
|
|
200
171
|
export declare function currentThread<T = CurrentThreadLike>(): T;
|
|
201
172
|
export declare function maybeCurrentThread(): CurrentThreadLike | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* THE ambient, NARROWED BY THE INSTANCE WHOSE CORE FRAME IS EXECUTING.
|
|
175
|
+
*
|
|
176
|
+
* For a built-in whose declaration names a component instance, "who is
|
|
177
|
+
* running" is not an open question about the whole store: the call arrived
|
|
178
|
+
* from a core frame OF THAT INSTANCE, so the running activation is one of
|
|
179
|
+
* that instance's. This narrows `resolveAmbient` accordingly — same tiers,
|
|
180
|
+
* same order, candidates filtered — and falls back to the unscoped answer
|
|
181
|
+
* when the instance has no candidate at all (the instantiation-time shape,
|
|
182
|
+
* and any built-in reached before its instance has a task).
|
|
183
|
+
*
|
|
184
|
+
* WHY IT IS NEEDED (polyengine#24's residue; polyvisor#49 trap 1,
|
|
185
|
+
* `runtime/tests/context_attribution_test.ts`). A JSPI continuation chunk —
|
|
186
|
+
* the tail of a suspended activation, e.g. wit-bindgen's callback epilogue
|
|
187
|
+
* restoring its task pointer with `context.set` (rt/async_support.rs:592) —
|
|
188
|
+
* runs with an EMPTY `threadStack` and, unlike a hop, has no re-anchoring
|
|
189
|
+
* edge of its own. Tier 2 then answers the newest claim, which is whichever
|
|
190
|
+
* SIBLING activation suspended most recently. The attribution sentinels
|
|
191
|
+
* (jspi/bridge.ts) plant that claim one microtask ahead of the chunk, which
|
|
192
|
+
* is exact when the engine queues the resumption while the settle reaction
|
|
193
|
+
* returns (measured so in Deno's V8) — and NOT exact in Chromium, where a
|
|
194
|
+
* wider gap lets a sibling's sentinel land in between. Measured there 3/3:
|
|
195
|
+
* one task's epilogue wrote its state pointer into another task's slots, and
|
|
196
|
+
* the starved task's next callback entry hit `assert!(!state.is_null())`
|
|
197
|
+
* (async_support.rs:578) -> unreachable.
|
|
198
|
+
*
|
|
199
|
+
* Ordering discipline cannot fix that class — engine chunk boundaries are not
|
|
200
|
+
* observable, so every microtask-ordering scheme is a hope. Instance identity
|
|
201
|
+
* is not a hope: it is static (the declaration), and it is decisive because
|
|
202
|
+
* ONE INSTANCE CAN ONLY HAVE ONE ACTIVATION MID-FRAME AT A TIME — a callback
|
|
203
|
+
* invocation holds `inst.exclusiveThread` for its whole extent, suspensions
|
|
204
|
+
* included (definitions.py line 2187 / `runCallbackLoop`), and a sync or
|
|
205
|
+
* stackful-async lift holds the entry gate. Two activations that can race for
|
|
206
|
+
* an unbracketed read are therefore necessarily of different instances, which
|
|
207
|
+
* is exactly what this discriminates.
|
|
208
|
+
*
|
|
209
|
+
* SPEC BASIS. `canon_context_get`/`canon_context_set` (definitions.py 2348 /
|
|
210
|
+
* 2358) read `current_thread().storage`, and in the reference a built-in is
|
|
211
|
+
* only ever reached from inside the activation that called it — the identity
|
|
212
|
+
* is exact by construction, never inferred. This runtime has to reconstruct
|
|
213
|
+
* it; narrowing the reconstruction to the declaring instance moves it TOWARD
|
|
214
|
+
* the reference (it can only ever remove candidates the reference would never
|
|
215
|
+
* have named), never away.
|
|
216
|
+
*/
|
|
217
|
+
export declare function currentThreadForInstance<T = CurrentThreadLike>(inst: unknown): T;
|
|
202
218
|
/** definitions.py `current_task()` (line 309). */
|
|
203
219
|
export declare function currentTask(): any;
|
|
204
220
|
/**
|
|
@@ -241,6 +257,84 @@ export declare class Store {
|
|
|
241
257
|
* is driving the store — which is the call the guest is blocked in.
|
|
242
258
|
*/
|
|
243
259
|
hostFailure: unknown;
|
|
260
|
+
/**
|
|
261
|
+
* Resumed-but-not-yet-run activations of THIS store — the driver's
|
|
262
|
+
* scheduling gate, not an ambient.
|
|
263
|
+
*
|
|
264
|
+
* Keeping this distinct from `activationClaims` matters. This set answers
|
|
265
|
+
* "may I schedule something else right now?" (`Store.tick` and both driving
|
|
266
|
+
* loops refuse while it is non-empty, which is what forces a microtask yield
|
|
267
|
+
* so the resumed activation actually runs). `activationClaims` answers
|
|
268
|
+
* "whose code is this?". Conflating them — driving off the ambient queue —
|
|
269
|
+
* wedges the loops, because an activation that merely hopped legitimately
|
|
270
|
+
* holds an ambient while the scheduler is free to proceed.
|
|
271
|
+
*
|
|
272
|
+
* PER-STORE and MULTI-ENTRY since 2026-08-22 (issues #158 mechanism B,
|
|
273
|
+
* #210). It was one module-global slot with a one-claimant assert, which
|
|
274
|
+
* (a) could not represent two legitimately-pending engine resumptions — a
|
|
275
|
+
* running activation X delivering a resume to Z while Y's resumption was
|
|
276
|
+
* still pending crashed on the assert — and (b) made every driver on every
|
|
277
|
+
* store yield while ANY store held a claim, so an idle store's
|
|
278
|
+
* `driveStoreAsync` died at the 10,000-hop assert (~311ms) while another
|
|
279
|
+
* store merely dwelt on a slow host import. The assert's invariant was
|
|
280
|
+
* tier-3 attribution unambiguity, which no longer exists (see
|
|
281
|
+
* `resolveAmbient`), so it is gone with the slot; the entries and their
|
|
282
|
+
* release edges are otherwise unchanged, per entry.
|
|
283
|
+
*
|
|
284
|
+
* Cross-store de-serialization is safe by disjointness: an activation
|
|
285
|
+
* belongs to exactly one store. Same-store it is strictly more conservative
|
|
286
|
+
* than the old slot — the gate keeps refusing until EVERY pending entry has
|
|
287
|
+
* died, rather than crashing on the second.
|
|
288
|
+
*
|
|
289
|
+
* Release edges, per entry: the activation PARKS again
|
|
290
|
+
* (`blockCurrentActivation` -> `consumePendingIfRunning`), it FINISHES (its
|
|
291
|
+
* `awaitValue` promise settles -> `noteAwaiting` -> `releasePendingOf`), or
|
|
292
|
+
* the driver drops its own speculative entry (`removePendingResumption`).
|
|
293
|
+
*/
|
|
294
|
+
readonly pendingResumptions: Set<unknown>;
|
|
295
|
+
/**
|
|
296
|
+
* Record that a suspension of this store has been settled and its
|
|
297
|
+
* activation has not run yet. Idempotent; a null/undefined activation is
|
|
298
|
+
* "no entry" (the instantiation-time shape that has no thread at all).
|
|
299
|
+
*
|
|
300
|
+
* No one-claimant assert: two entries are legitimate (see
|
|
301
|
+
* `pendingResumptions`). Two SuspensionPoints of ONE task cannot be pending
|
|
302
|
+
* simultaneously — a task's single activation suspends at one point at a
|
|
303
|
+
* time — so collapsing entries by identity loses nothing.
|
|
304
|
+
*/
|
|
305
|
+
addPendingResumption(t: unknown): void;
|
|
306
|
+
/** Is some settled-but-not-yet-run activation of this store pending? */
|
|
307
|
+
hasPendingResumptions(): boolean;
|
|
308
|
+
/** Drop exactly `t` (the driver's own speculative entry). */
|
|
309
|
+
removePendingResumption(t: unknown): void;
|
|
310
|
+
/**
|
|
311
|
+
* Drop the pending entry iff its activation is demonstrably RUNNING — i.e.
|
|
312
|
+
* the entry names the same thread the ACTIVATION AMBIENT names for the code
|
|
313
|
+
* calling us. An entry exists to cover the window between settling a
|
|
314
|
+
* suspension and the resumed activation running; once that activation's own
|
|
315
|
+
* code is on the stack the window is closed, and holding the entry would
|
|
316
|
+
* gate the store on an activation that has already had its turn — while a
|
|
317
|
+
* running activation's built-in settles ANOTHER activation's suspension
|
|
318
|
+
* (`subtask.cancel` delivering a cancellation to a parked callee,
|
|
319
|
+
* cancellable.wast) that other entry must legitimately stay.
|
|
320
|
+
*
|
|
321
|
+
* The comparison is against `activationOf()` — the wasm-ENTRY brackets,
|
|
322
|
+
* deliberately not the full `threadStack` (see `entryStack`: routing it
|
|
323
|
+
* through the full stack moved 64 conformance commands).
|
|
324
|
+
*/
|
|
325
|
+
consumePendingIfRunning(): void;
|
|
326
|
+
/**
|
|
327
|
+
* Drop the pending entry naming `t` — the settle-side half: an entry taken
|
|
328
|
+
* when `t`'s suspension was settled dies when `t`'s activation finishes (its
|
|
329
|
+
* `awaitValue` promise settles; `noteAwaiting` calls this from the eager
|
|
330
|
+
* settle continuation) or parks again (`blockCurrentActivation` consumes via
|
|
331
|
+
* `consumePendingIfRunning`).
|
|
332
|
+
*
|
|
333
|
+
* The `task.implicitThread` indirection covers entries taken against a
|
|
334
|
+
* task's implicit thread. `t` FINISHING also ends its activation ambient,
|
|
335
|
+
* so both are dropped here.
|
|
336
|
+
*/
|
|
337
|
+
releasePendingOf(t: any): void;
|
|
244
338
|
startWaiting(t: SchedulableThread): void;
|
|
245
339
|
stopWaiting(t: SchedulableThread): void;
|
|
246
340
|
/** Ready waiting threads, in wait order (the FIFO of the default policy). */
|
|
@@ -282,7 +376,7 @@ export declare class Store {
|
|
|
282
376
|
* another activation's suspension — `subtask.cancel` delivering a
|
|
283
377
|
* cancellation): the claim taken at settle time must survive until the
|
|
284
378
|
* resumed activation parks again or finishes, and "finished" is exactly
|
|
285
|
-
* this continuation firing. See `
|
|
379
|
+
* this continuation firing. See `releasePendingOf`.
|
|
286
380
|
*/
|
|
287
381
|
noteAwaiting(t: any, promise: Promise<unknown>): void;
|
|
288
382
|
/**
|