@polyengine/runtime 0.5.0 → 0.6.0
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/cabi/async_values.js +6 -5
- package/esm/cabi/bulk_lists.js +0 -5
- package/esm/cabi/context.js +13 -3
- package/esm/cabi/flatten.js +41 -9
- package/esm/cabi/handles.js +57 -54
- package/esm/cabi/layout.js +113 -52
- package/esm/cabi/load.js +31 -23
- package/esm/cabi/store.js +33 -26
- package/esm/cabi/trap.js +2 -2
- package/esm/cabi/types.js +138 -25
- package/esm/cabi/values.js +25 -7
- package/esm/cache/core.js +2 -11
- package/esm/digest/digest.js +10 -8
- package/esm/digest/mod.js +1 -1
- package/esm/digest/verify.js +6 -86
- package/esm/embedder/casing.js +24 -9
- package/esm/embedder/copy.js +6 -6
- package/esm/embedder/errors.js +2 -2
- package/esm/embedder/imports.js +3 -3
- package/esm/embedder/instantiate.js +170 -41
- package/esm/embedder/mod.js +9 -8
- package/esm/embedder/resources.js +39 -16
- package/esm/embedder/streams.js +36 -37
- package/esm/embedder/sync.js +242 -0
- package/esm/embedder/values.js +84 -22
- package/esm/embedder/version.js +9 -9
- package/esm/exec/boundary.js +318 -165
- package/esm/exec/executor.js +51 -24
- package/esm/exec/host_streams.js +31 -31
- package/esm/intrinsics/async_builtins.js +19 -8
- package/esm/intrinsics/context.js +1 -1
- package/esm/intrinsics/errors.js +9 -9
- package/esm/intrinsics/fact_calls.js +37 -49
- package/esm/intrinsics/mod.js +54 -117
- package/esm/intrinsics/stream_builtins.js +2 -2
- package/esm/intrinsics/transcode.js +1 -1
- package/esm/jspi/bridge.js +4 -3
- package/esm/jspi/suspending.js +10 -5
- package/esm/plan/loader.js +5 -5
- package/esm/shim/translator.js +2 -2
- package/esm/task/mod.js +45 -182
- package/esm/task/scheduler.js +154 -185
- package/esm/task/streams.js +39 -54
- package/esm/task/subtask.js +2 -2
- package/esm/task/thread.js +20 -41
- package/esm/task/waitable.js +0 -1
- package/package.json +2 -2
- package/types/cabi/async_values.d.ts +3 -2
- package/types/cabi/bulk_lists.d.ts +0 -2
- package/types/cabi/context.d.ts +15 -5
- package/types/cabi/flatten.d.ts +2 -2
- package/types/cabi/handles.d.ts +15 -26
- package/types/cabi/layout.d.ts +22 -1
- package/types/cabi/load.d.ts +10 -2
- package/types/cabi/store.d.ts +4 -2
- package/types/cabi/types.d.ts +22 -3
- package/types/digest/mod.d.ts +1 -1
- package/types/digest/verify.d.ts +3 -19
- package/types/embedder/casing.d.ts +9 -1
- package/types/embedder/copy.d.ts +4 -4
- package/types/embedder/instantiate.d.ts +4 -4
- package/types/embedder/mod.d.ts +3 -2
- package/types/embedder/resources.d.ts +20 -7
- package/types/embedder/streams.d.ts +5 -6
- package/types/embedder/sync.d.ts +81 -0
- package/types/embedder/values.d.ts +2 -2
- package/types/exec/boundary.d.ts +66 -42
- package/types/exec/executor.d.ts +3 -2
- package/types/exec/host_streams.d.ts +8 -8
- package/types/intrinsics/errors.d.ts +3 -3
- package/types/intrinsics/mod.d.ts +1 -1
- package/types/intrinsics/stream_builtins.d.ts +2 -2
- package/types/jspi/bridge.d.ts +6 -5
- package/types/jspi/suspending.d.ts +1 -1
- package/types/plan/format.d.ts +11 -10
- package/types/plan/loader.d.ts +2 -2
- package/types/shim/translator.d.ts +2 -2
- package/types/task/mod.d.ts +26 -97
- package/types/task/scheduler.d.ts +81 -62
- package/types/task/streams.d.ts +23 -38
- package/types/task/subtask.d.ts +2 -2
- package/types/task/waitable.d.ts +0 -1
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, EventCode, withActivation, hasRealHostCall,
|
|
7
|
+
import { driveSyncLift, EventCode, withActivation, hasRealHostCall, NeedsJspi, needsJspi, packSubtaskResult, PendingCapability, notifyInstancePoisoned, realHostCalls, storeQuiescent, Subtask, SyncEntryBusy, WaitableSet, SubtaskState, Task, Thread, entryRefusal, } 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";
|
|
@@ -31,7 +31,7 @@ export function newStats() {
|
|
|
31
31
|
* Structurally compatible with cabi's `MemInst` (same public surface).
|
|
32
32
|
*/
|
|
33
33
|
export class LiveMemory {
|
|
34
|
-
addrType = "i32"; // memory64 components:
|
|
34
|
+
addrType = "i32"; // memory64 components: not yet implemented
|
|
35
35
|
#provider;
|
|
36
36
|
#label;
|
|
37
37
|
#buffer = null;
|
|
@@ -240,7 +240,7 @@ function isPromiseLike(v) {
|
|
|
240
240
|
typeof v.then === "function");
|
|
241
241
|
}
|
|
242
242
|
// ---------------------------------------------------------------------------
|
|
243
|
-
// Handshake probe
|
|
243
|
+
// Handshake probe
|
|
244
244
|
// ---------------------------------------------------------------------------
|
|
245
245
|
//
|
|
246
246
|
// Env-gated tracing of the drive loops. This exists because site 1 is the
|
|
@@ -366,7 +366,7 @@ function tagAwait(t) {
|
|
|
366
366
|
* repeat. Reimplementing it there diverged: that copy only drained
|
|
367
367
|
* `store.awaiting` and never awaited `pendingHostCalls`, so a guest parked on
|
|
368
368
|
* a Promise-returning host import was never resumed and the host's read of
|
|
369
|
-
* the stream it was feeding hung (
|
|
369
|
+
* the stream it was feeding hung (host-pump starvation of `pendingHostCalls`).
|
|
370
370
|
*
|
|
371
371
|
* Callers that must not hit the deadlock traps below (the host pump: an
|
|
372
372
|
* embedder that never does its half is documented to hang, not trap) can
|
|
@@ -385,7 +385,9 @@ export async function driveStoreAsync(store, done, what) {
|
|
|
385
385
|
* have always produced concurrent loops, and the host-stream pump's stand-down
|
|
386
386
|
* below is cooperative, so a *bounded overlap window* remains by construction
|
|
387
387
|
* (an export call can start while the pump is parked mid-`await`; the pump
|
|
388
|
-
*
|
|
388
|
+
* notices at its next `done()` evaluation, which the driver-arrival one-shot
|
|
389
|
+
* below now makes prompt — before issue #239 it was "whenever the host happens
|
|
390
|
+
* to answer", i.e. not bounded at all). The invariant is:
|
|
389
391
|
*
|
|
390
392
|
* **no activation is resumed twice for one settlement, and no activation is
|
|
391
393
|
* resumed with a value from a settlement it has already consumed.**
|
|
@@ -455,6 +457,51 @@ export function whenStoreDriverIdle(store) {
|
|
|
455
457
|
return w.p;
|
|
456
458
|
}
|
|
457
459
|
// ---------------------------------------------------------------------------
|
|
460
|
+
// Driver arrival: closing the overlap window (issue #239)
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
//
|
|
463
|
+
// The stand-down above ("the pumps are *fallback* drivers") is evaluated only
|
|
464
|
+
// at a driver's next `done()`, so the doc's "bounded overlap window" is really
|
|
465
|
+
// bounded by whatever the incumbent driver is parked on — and its longest park
|
|
466
|
+
// is `Promise.race([...parked tags, ...pendingHostCalls])`, i.e. HOST-CONTROLLED
|
|
467
|
+
// time. That is a stall in its own right, and it is fatal in combination with
|
|
468
|
+
// the SPECULATIVE resume entry the race holds: `Store.pendingResumptions` is a
|
|
469
|
+
// store-wide scheduling gate, so a second driver on the same store spins at
|
|
470
|
+
// `driveAsync`'s top and dies at the 10,000-hop internal-bug assert in ~311ms
|
|
471
|
+
// (issue #239 — the same-store half of the cross-store stall #210 fixed; see
|
|
472
|
+
// `tests/cross_store_driver_test.ts`, whose header describes this gate being
|
|
473
|
+
// "held for the entire duration of a guest's wait on a slow host import").
|
|
474
|
+
//
|
|
475
|
+
// So drivers announce themselves: every `driveAsync` that finds itself the
|
|
476
|
+
// second (or later) loop on a store fires this one-shot, which every driver
|
|
477
|
+
// races alongside its parked tags. The incumbent wakes within a microtask,
|
|
478
|
+
// drops the speculative entry on its way out of the race, and re-evaluates
|
|
479
|
+
// `done()` — which is exactly the stand-down the pumps were always supposed to
|
|
480
|
+
// perform, now prompt instead of "whenever the host happens to answer".
|
|
481
|
+
const driverArrivals = new WeakMap();
|
|
482
|
+
/** A one-shot that resolves (to `null`, the race's "nothing settled" value)
|
|
483
|
+
* when another driver starts on `store`. */
|
|
484
|
+
function armDriverArrival(store) {
|
|
485
|
+
let n = driverArrivals.get(store);
|
|
486
|
+
if (n === undefined) {
|
|
487
|
+
let r;
|
|
488
|
+
const p = new Promise((res) => (r = () => res(null)));
|
|
489
|
+
n = { p, r };
|
|
490
|
+
driverArrivals.set(store, n);
|
|
491
|
+
}
|
|
492
|
+
return n.p;
|
|
493
|
+
}
|
|
494
|
+
function fireDriverArrival(store) {
|
|
495
|
+
const n = driverArrivals.get(store);
|
|
496
|
+
if (n === undefined)
|
|
497
|
+
return;
|
|
498
|
+
// Deleted before resolving so the next `armDriverArrival` mints a fresh,
|
|
499
|
+
// unresolved one-shot: a driver that wakes on this and re-parks must not
|
|
500
|
+
// pick the settled promise back up and spin.
|
|
501
|
+
driverArrivals.delete(store);
|
|
502
|
+
n.r();
|
|
503
|
+
}
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
458
505
|
// The settlement pump: liveness between export calls
|
|
459
506
|
// ---------------------------------------------------------------------------
|
|
460
507
|
//
|
|
@@ -599,7 +646,14 @@ async function settlementPumpLoop(store) {
|
|
|
599
646
|
}
|
|
600
647
|
}
|
|
601
648
|
async function driveAsync(store, done, what) {
|
|
602
|
-
|
|
649
|
+
const depth = storeDriverDepth(store) + 1;
|
|
650
|
+
driverDepth.set(store, depth);
|
|
651
|
+
// An incumbent driver may be parked in the awaiting-race holding the
|
|
652
|
+
// speculative resume entry — a store-wide gate this loop would otherwise
|
|
653
|
+
// spin on until the 10,000-hop assert (issue #239). Announce ourselves so it
|
|
654
|
+
// stands down within a microtask.
|
|
655
|
+
if (depth > 1)
|
|
656
|
+
fireDriverArrival(store);
|
|
603
657
|
try {
|
|
604
658
|
let claimHops = 0;
|
|
605
659
|
for (;;) {
|
|
@@ -626,11 +680,12 @@ async function driveAsync(store, done, what) {
|
|
|
626
680
|
// and clearing it before that activation runs re-opens the
|
|
627
681
|
// mis-attribution window the entry exists to close.
|
|
628
682
|
//
|
|
629
|
-
// PER-STORE (issue #210):
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
683
|
+
// PER-STORE (issue #210): read only THIS store's entries. Activations
|
|
684
|
+
// never cross stores, so another store's pending resumption is none of
|
|
685
|
+
// this loop's business — and a gate shared across stores would spin an
|
|
686
|
+
// idle store's driver here, to its death at the hop bound below in
|
|
687
|
+
// ~311ms, merely because ANOTHER store's guest was dwelling on a slow
|
|
688
|
+
// host import.
|
|
634
689
|
if (store.hasPendingResumptions()) {
|
|
635
690
|
traceDrive("driveAsync", store, done, "yield-pending");
|
|
636
691
|
// Bounded: a pending entry that never dies is an internal bug (every
|
|
@@ -688,7 +743,7 @@ async function driveAsync(store, done, what) {
|
|
|
688
743
|
// therefore stops the scheduler while waiting for something that needs the
|
|
689
744
|
// scheduler: a pure-microtask stall with no trap and no rejection.
|
|
690
745
|
// Observed on `async/async-calls-sync.wast` the moment site 1 became the
|
|
691
|
-
// first lit suspension site
|
|
746
|
+
// first lit suspension site: turn N serviced a promise that
|
|
692
747
|
// never settled while three other parked threads and three ready-able
|
|
693
748
|
// suspension points went unexamined.
|
|
694
749
|
//
|
|
@@ -751,12 +806,12 @@ async function driveAsync(store, done, what) {
|
|
|
751
806
|
// set changing: the same activation resumes off an engine
|
|
752
807
|
// continuation chunk during the probe's macrotask turn (jspi
|
|
753
808
|
// pin (j) — a sync-completing Suspending import still defers its
|
|
754
|
-
// continuation), runs, and re-parks through the
|
|
809
|
+
// continuation), runs, and re-parks through the suspending mark arm, which
|
|
755
810
|
// registers a fresh `pendingHostCalls` entry. The activation
|
|
756
811
|
// promise never settled and `awaiting` membership is unchanged,
|
|
757
812
|
// but the park is externally wakeable now — the verdict's own
|
|
758
813
|
// precondition (`pendingHostCalls.size === 0`) no longer holds.
|
|
759
|
-
// Observed on wasi-shims'
|
|
814
|
+
// Observed on wasi-shims' stream/future round-trip poll (sync fast path): probe sampled
|
|
760
815
|
// hostCalls=0 between a settled park and the next one, then
|
|
761
816
|
// trapped a live workload with hostCalls=1. Re-check ⇒ re-probe.
|
|
762
817
|
// Likewise a SERVICEABLE settled entry (issue #156): dispatching
|
|
@@ -801,7 +856,7 @@ async function driveAsync(store, done, what) {
|
|
|
801
856
|
// snapshot's `parked[0]` is then `undefined` — the exact check-then-act
|
|
802
857
|
// shape that made the host pump's copy of this loop throw
|
|
803
858
|
// `TypeError: ... (reading 'awaiting')` into `store.hostFailure`, where
|
|
804
|
-
// it poisoned a later unrelated call
|
|
859
|
+
// it poisoned a later unrelated call via check-then-act on `store.hostFailure`. Nothing to
|
|
805
860
|
// service ⇒ go back to the top and re-evaluate `done`.
|
|
806
861
|
// Same re-check for the settled queue, and for the same reason: the
|
|
807
862
|
// probe's macrotask turn can land a fresh, SERVICEABLE activation tail
|
|
@@ -820,7 +875,7 @@ async function driveAsync(store, done, what) {
|
|
|
820
875
|
// never be held hostage by it. The claimed thread's promise may only be
|
|
821
876
|
// settleable by further scheduler progress (a promising-wrapped nested
|
|
822
877
|
// activation whose own suspension points this loop must still resume);
|
|
823
|
-
// blocking on it alone is the pure-microtask stall
|
|
878
|
+
// blocking on it alone is the pure-microtask stall described above.
|
|
824
879
|
// Same exclusion as the probe (issue #156): a thread whose tail is
|
|
825
880
|
// already queued in `store.settled` must not be raced — its tag is
|
|
826
881
|
// settled, so it re-wins instantly and livelocks the event loop,
|
|
@@ -828,12 +883,17 @@ async function driveAsync(store, done, what) {
|
|
|
828
883
|
const queued = new Set(store.settled.map((s) => s.t));
|
|
829
884
|
const parked = [...store.awaiting].filter((t) => !queued.has(t));
|
|
830
885
|
if (parked.length === 0) {
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
886
|
+
// UNREACHABLE BY CONSTRUCTION. `parked` is `store.awaiting` minus
|
|
887
|
+
// the threads whose tails are already queued in `store.settled`, and
|
|
888
|
+
// we only get here with `awaiting` non-empty and
|
|
889
|
+
// `hasServiceableSettled()` false — which means the settled queue is
|
|
890
|
+
// EMPTY, so nothing was excluded. Retained as a wedge detector, not
|
|
891
|
+
// as expected behavior.
|
|
835
892
|
if (store.pendingHostCalls.size > 0) {
|
|
836
|
-
await Promise.race([
|
|
893
|
+
await Promise.race([
|
|
894
|
+
...store.pendingHostCalls,
|
|
895
|
+
armDriverArrival(store),
|
|
896
|
+
]).catch(() => { });
|
|
837
897
|
continue;
|
|
838
898
|
}
|
|
839
899
|
// Per the issue #156 analysis this is unreachable (a spanning lock
|
|
@@ -858,18 +918,55 @@ async function driveAsync(store, done, what) {
|
|
|
858
918
|
// `SuspensionPoint.resume`) is what carries it, and dropping an entry
|
|
859
919
|
// that names a thread already gone from the set is a no-op.
|
|
860
920
|
//
|
|
861
|
-
// ONLY ITS OWN ENTRY (issue #158): the `finally`
|
|
862
|
-
//
|
|
863
|
-
// await
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
|
|
921
|
+
// ONLY ITS OWN ENTRY (issue #158): the `finally` must drop the entry
|
|
922
|
+
// THIS loop added and nothing else. A guest-synchronous delivery during
|
|
923
|
+
// the await takes a fresh entry of its own, and clearing that one here
|
|
924
|
+
// would re-open early the very window it exists to close — which is why
|
|
925
|
+
// the gate is a set of entries rather than a single slot.
|
|
926
|
+
//
|
|
927
|
+
// SOLE DRIVER ONLY, AND ONLY UNTIL ONE ARRIVES (issue #239). The entry
|
|
928
|
+
// is a claim over a window this loop cannot bound: the race settles when
|
|
929
|
+
// the HOST answers, which may be never. As a store-wide scheduling gate
|
|
930
|
+
// (`Store.tick` refuses; every driver yields at its top) that is a wedge
|
|
931
|
+
// the moment a second driver exists — it spins at the top of its own
|
|
932
|
+
// loop and dies at the 10,000-hop assert in ~311ms, an internal-bug
|
|
933
|
+
// detector firing on a perfectly ordinary suspended guest. Two concurrent
|
|
934
|
+
// export calls with one slow suspending import were enough; the reported
|
|
935
|
+
// shape was a detached guest task cancelling an in-flight import, which
|
|
936
|
+
// parks mid-frame with no export call outstanding and leaves the
|
|
937
|
+
// settlement pump holding this entry.
|
|
938
|
+
//
|
|
939
|
+
// What the entry protects — "the engine may run `chosen`'s wasm during
|
|
940
|
+
// this await" — it protects by refusing OTHER `Store.tick` callers, and
|
|
941
|
+
// this loop is not one of them while it awaits. The tick callers that
|
|
942
|
+
// can reach a store mid-race are another `driveAsync` loop and
|
|
943
|
+
// `HostActivity.pump`'s synchronous drain (exec/host_streams.ts) — the
|
|
944
|
+
// latter is not gated by driver depth, so scoping the entry to "sole
|
|
945
|
+
// driver" does hand it a window an unscoped entry would close at
|
|
946
|
+
// depth >= 2.
|
|
947
|
+
// What holds regardless is the invariant the `driverDepth` note names:
|
|
948
|
+
// a genuine resumption is preceded by `SuspensionPoint.resume`'s OWN
|
|
949
|
+
// entry (jspi/bridge.ts, minted before the settle), and every
|
|
950
|
+
// resumption site here re-checks membership and promise identity
|
|
951
|
+
// synchronously — mechanisms (a) and (b), which is where that note
|
|
952
|
+
// already puts the weight.
|
|
953
|
+
const sole = storeDriverDepth(store) === 1;
|
|
954
|
+
if (sole)
|
|
955
|
+
store.addPendingResumption(chosen);
|
|
867
956
|
let winner;
|
|
868
957
|
try {
|
|
869
|
-
|
|
958
|
+
// `armDriverArrival` rides the race for every driver, not just the one
|
|
959
|
+
// holding the entry: waking on a new arrival is also how a fallback
|
|
960
|
+
// pump reaches its next `done()` — i.e. its stand-down — promptly.
|
|
961
|
+
winner = await Promise.race([
|
|
962
|
+
chosenTag,
|
|
963
|
+
...others,
|
|
964
|
+
armDriverArrival(store),
|
|
965
|
+
]);
|
|
870
966
|
}
|
|
871
967
|
finally {
|
|
872
|
-
|
|
968
|
+
if (sole)
|
|
969
|
+
store.removePendingResumption(chosen);
|
|
873
970
|
}
|
|
874
971
|
// Resume whichever thread actually settled -- not necessarily the one we
|
|
875
972
|
// claimed. Resuming only the claimed thread would spin: its promise may
|
|
@@ -884,13 +981,7 @@ async function driveAsync(store, done, what) {
|
|
|
884
981
|
// true again but the tag's value belongs to a settlement this thread
|
|
885
982
|
// has already consumed. Compare promise identity too.
|
|
886
983
|
if (winner !== null && store.awaiting.has(winner.t) &&
|
|
887
|
-
winner.t.awaiting === winner.p
|
|
888
|
-
// Dispatch guard, the same predicate `Store.serviceSettled` uses
|
|
889
|
-
// (issue #156): never resume into an instance that is not
|
|
890
|
-
// host-enterable. The entry is (also) queued in `store.settled` by
|
|
891
|
-
// `noteAwaiting`'s continuation, and `serviceSettled` owns it once
|
|
892
|
-
// the lock releases.
|
|
893
|
-
dispatchableTail(winner.t)) {
|
|
984
|
+
winner.t.awaiting === winner.p) {
|
|
894
985
|
winner.t.resumeWith(winner.value, winner.failure);
|
|
895
986
|
}
|
|
896
987
|
continue;
|
|
@@ -906,7 +997,18 @@ async function driveAsync(store, done, what) {
|
|
|
906
997
|
// not ours — this is genuine, unavoidable nondeterminism at the boundary
|
|
907
998
|
// (the reference has the same freedom in `Store.tick`). Everything
|
|
908
999
|
// *inside* the component stays deterministic per scheduler.ts.
|
|
909
|
-
|
|
1000
|
+
//
|
|
1001
|
+
// The driver-arrival one-shot rides here too. This is the routine park of
|
|
1002
|
+
// a quiet guest with a real host call outstanding — no speculative entry
|
|
1003
|
+
// is held, so there is no wedge to break, but a fallback pump parked here
|
|
1004
|
+
// would otherwise not reach its `done()` (i.e. its stand-down) until the
|
|
1005
|
+
// HOST answered, leaving two loops interleaving `serviceSettled`/`tick`
|
|
1006
|
+
// for that whole window. That interleaving is what the `driverDepth` note
|
|
1007
|
+
// above calls out as bad for throughput and blame.
|
|
1008
|
+
await Promise.race([
|
|
1009
|
+
...store.pendingHostCalls,
|
|
1010
|
+
armDriverArrival(store),
|
|
1011
|
+
]).catch(() => { });
|
|
910
1012
|
}
|
|
911
1013
|
}
|
|
912
1014
|
finally {
|
|
@@ -945,26 +1047,35 @@ function takeHostFailure(store) {
|
|
|
945
1047
|
* needs genuine wasm-frame suspension: `needsJspi`, at the precise point.
|
|
946
1048
|
*/
|
|
947
1049
|
/**
|
|
948
|
-
* The plain-entered variant of a
|
|
949
|
-
* attached to the promising-wrapped lifted function under this symbol
|
|
1050
|
+
* The plain-entered variant of a **sync-typed** lifted export in jspi mode,
|
|
1051
|
+
* attached to the promising-wrapped lifted function under this symbol
|
|
1052
|
+
* (contracts/embedder-api.md §"Functions and async", §"Functions and async").
|
|
1053
|
+
*
|
|
1054
|
+
* In jspi mode every promising-wrapped entry returns a Promise even when the
|
|
1055
|
+
* activation completes without suspending (jspi pin (e)). Some host contexts
|
|
1056
|
+
* cannot use a Promise no matter how promptly it resolves, so each sync-typed
|
|
1057
|
+
* export carries a second lifted function whose ENTRY is plain (unwrapped):
|
|
1058
|
+
* a guest activation that completes synchronously — the overwhelmingly common
|
|
1059
|
+
* case for sync-typed WIT — delivers its results synchronously through it.
|
|
950
1060
|
*
|
|
951
|
-
*
|
|
952
|
-
* (contracts/embedder-api.md §"Resources"), and a JS constructor cannot
|
|
953
|
-
* await — but in jspi mode every promising-wrapped entry returns a Promise
|
|
954
|
-
* even when the activation completes without suspending (jspi pin (e)). So
|
|
955
|
-
* constructor exports carry a second lifted function whose ENTRY is plain
|
|
956
|
-
* (unwrapped): a constructor that completes synchronously — the
|
|
957
|
-
* overwhelmingly common case; WIT constructors are always sync-typed —
|
|
958
|
-
* returns its rep synchronously through it.
|
|
1061
|
+
* Two consumers, one mechanism:
|
|
959
1062
|
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
1063
|
+
* * **resource constructors** — a WIT constructor is surfaced as a JS class
|
|
1064
|
+
* constructor (§"Resources") and a JS constructor cannot await, so the
|
|
1065
|
+
* embedder layer reads this symbol unconditionally for `[constructor]`
|
|
1066
|
+
* exports;
|
|
1067
|
+
* * **the embedder `sync()` adapter** — the explicit per-use
|
|
1068
|
+
* synchronous view of any sync-typed export.
|
|
1069
|
+
*
|
|
1070
|
+
* The cost is confined to genuinely-suspending activations: a blocking
|
|
1071
|
+
* built-in reached through the plain entry signals `NeedsJspi` (a capability
|
|
1072
|
+
* error, instance left enterable), and a `Suspending`-wrapped host import
|
|
1073
|
+
* reached from the unwrapped frame fails as a trap. Both name the export
|
|
1074
|
+
* rather than silently deadlocking. A call made while the instance has
|
|
1075
|
+
* hop-parked activations refuses with `SyncEntryBusy` before entering
|
|
1076
|
+
* (`refuseOnEntryHops` below).
|
|
966
1077
|
*/
|
|
967
|
-
export const
|
|
1078
|
+
export const SYNC_ENTRY = Symbol("polyengine.syncEntry");
|
|
968
1079
|
export function createLiftedFunction(input) {
|
|
969
1080
|
const { name, ft, opts, core, stats, trapState, syncCallStack, allInstances, } = input;
|
|
970
1081
|
const inst = opts.instance;
|
|
@@ -1002,19 +1113,16 @@ export function createLiftedFunction(input) {
|
|
|
1002
1113
|
trapState.pending = undefined;
|
|
1003
1114
|
// Depth of the sync-call scope stack on entry; see the `finally` below.
|
|
1004
1115
|
const syncCallDepth = syncCallStack?.length ?? 0;
|
|
1005
|
-
// Reference `Store.lift` (
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1116
|
+
// Reference `Store.lift` (@ 2f13265) runs `canon_lift` with NO gate
|
|
1117
|
+
// (CM#705), so host entry into a live instance is valid. What this adds
|
|
1118
|
+
// is polyengine's per-instance
|
|
1119
|
+
// poisoning divergence — a poisoned instance is a corpse, and its refusal
|
|
1120
|
+
// names the original trap (polyengine#145 ask 1).
|
|
1121
|
+
{
|
|
1122
|
+
const refusal = entryRefusal(inst, null, `cannot enter component instance ${inst.index}`);
|
|
1123
|
+
if (refusal !== null)
|
|
1124
|
+
trap(refusal);
|
|
1011
1125
|
}
|
|
1012
|
-
// The set this entry locked (definitions.py `ComponentInstance.enter_from`
|
|
1013
|
-
// iterates `entering_set`). Remembered so a trap can leave exactly these
|
|
1014
|
-
// locked and no others.
|
|
1015
|
-
const enteredSet = inst.enteringSet(null);
|
|
1016
|
-
inst.enterFrom(null);
|
|
1017
|
-
let entered = true;
|
|
1018
1126
|
let completed = false;
|
|
1019
1127
|
let resolved = null;
|
|
1020
1128
|
let resolvedSeen = false;
|
|
@@ -1080,44 +1188,30 @@ export function createLiftedFunction(input) {
|
|
|
1080
1188
|
// definition; assert that resting state rather than leaving the
|
|
1081
1189
|
// component bricked.
|
|
1082
1190
|
//
|
|
1083
|
-
// The
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1191
|
+
// The ENTERED instance is excluded: it is poisoned by this trap (see
|
|
1192
|
+
// `poison` below) and must stay exactly as the trap left it. Restoring
|
|
1193
|
+
// its `may_leave` would be tidying the state of an instance that is no
|
|
1194
|
+
// longer allowed to run at all.
|
|
1087
1195
|
for (const i of allInstances?.() ?? []) {
|
|
1088
|
-
if (
|
|
1196
|
+
if (i !== inst) {
|
|
1089
1197
|
i.mayLeave = true;
|
|
1090
1198
|
}
|
|
1091
1199
|
}
|
|
1092
1200
|
};
|
|
1093
|
-
const leave = () => {
|
|
1094
|
-
if (!entered)
|
|
1095
|
-
return;
|
|
1096
|
-
entered = false;
|
|
1097
|
-
inst.leaveTo(null);
|
|
1098
|
-
};
|
|
1099
1201
|
/**
|
|
1100
|
-
* A trap escaped the task:
|
|
1101
|
-
*
|
|
1102
|
-
* definitions.py `Store.lift` (line 578) is
|
|
1103
|
-
*
|
|
1104
|
-
* ```python
|
|
1105
|
-
* trap_if(not inst.may_enter_from(caller))
|
|
1106
|
-
* inst.enter_from(caller)
|
|
1107
|
-
* on_cancel = canon_lift(...) # <-- a Trap propagates out of here
|
|
1108
|
-
* inst.leave_to(caller) # <-- and so this never runs
|
|
1109
|
-
* ```
|
|
1202
|
+
* A trap escaped the task: mark the instance poisoned.
|
|
1110
1203
|
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1204
|
+
* polyengine's NAMED DIVERGENCE. definitions.py has no notion of a
|
|
1205
|
+
* post-trap instance at all — a Trap is the end of the world — and
|
|
1206
|
+
* wasmtime's answer is to poison the whole store. This runtime keeps the
|
|
1207
|
+
* component graph alive and buries only the instance that trapped: it is
|
|
1208
|
+
* not in a known state, so it may never be entered again, and the next
|
|
1209
|
+
* call reports `cannot enter component instance` with the recorded cause
|
|
1210
|
+
* appended (polyengine#145 ask 1).
|
|
1211
|
+
* `test/async/builtin-trap-poisons-instance.wast` asserts exactly this,
|
|
1212
|
+
* twice; the marker (`notifyInstancePoisoned`) is the whole mechanism.
|
|
1117
1213
|
*
|
|
1118
|
-
* Only
|
|
1119
|
-
* is why the lock is released per-instance rather than by poisoning a
|
|
1120
|
-
* whole store the way wasmtime does.
|
|
1214
|
+
* Only `inst` is affected; sibling instances stay usable.
|
|
1121
1215
|
*
|
|
1122
1216
|
* Poisoned instances can never rendezvous again, so their handle tables'
|
|
1123
1217
|
* live stream/future ends are retired here (#66): parked host operations
|
|
@@ -1125,21 +1219,10 @@ export function createLiftedFunction(input) {
|
|
|
1125
1219
|
* lets the embedder layer reject them loudly.
|
|
1126
1220
|
*/
|
|
1127
1221
|
const poison = (e) => {
|
|
1128
|
-
entered = false; // consumed: the lock is now permanent
|
|
1129
|
-
// ...for the leaf. The synthetic per-instantiation root (plan v3
|
|
1130
|
-
// amendment 4) is in `enteredSet` too, and leaving IT locked would
|
|
1131
|
-
// poison every instance of the component — exactly the store-wide
|
|
1132
|
-
// behaviour the paragraph above says this runtime deliberately does not
|
|
1133
|
-
// have. Released; see `releaseSyntheticRootOnPoison` in task/mod.ts.
|
|
1134
|
-
inst.releaseSyntheticRootOnPoison();
|
|
1135
1222
|
// Through the seam (not retireInstanceAsyncEnds directly) so the
|
|
1136
1223
|
// poison marker is recorded too — `Thread.resumeWith` retires this
|
|
1137
1224
|
// instance's late settles against it instead of assert-cascading.
|
|
1138
|
-
|
|
1139
|
-
if (i.isSyntheticRoot)
|
|
1140
|
-
continue;
|
|
1141
|
-
notifyInstancePoisoned(i, e);
|
|
1142
|
-
}
|
|
1225
|
+
notifyInstancePoisoned(inst, e);
|
|
1143
1226
|
};
|
|
1144
1227
|
/**
|
|
1145
1228
|
* Is `e` a *capability* signal rather than a genuine trap?
|
|
@@ -1148,9 +1231,9 @@ export function createLiftedFunction(input) {
|
|
|
1148
1231
|
* not "the component faulted". Poisoning on them is wrong on the
|
|
1149
1232
|
* reference's own terms: the operation they stand in for — a synchronous
|
|
1150
1233
|
* stream copy, `waitable-set.wait`, a blocking cross-component call —
|
|
1151
|
-
* *blocks and then completes* in definitions.py.
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1234
|
+
* *blocks and then completes* in definitions.py. Every one of those
|
|
1235
|
+
* executions returns normally there, so the instance stays healthy.
|
|
1236
|
+
* Poisoning would attribute a permanent fault to a component
|
|
1154
1237
|
* that, on a complete runtime, is perfectly healthy — and it cascades:
|
|
1155
1238
|
* one unsupported operation made every later call on that instance report
|
|
1156
1239
|
* `cannot enter component instance`, which is neither our real behaviour
|
|
@@ -1158,8 +1241,6 @@ export function createLiftedFunction(input) {
|
|
|
1158
1241
|
*
|
|
1159
1242
|
* What unwinding must still do on this path, and what it must not:
|
|
1160
1243
|
*
|
|
1161
|
-
* - MUST release the reentrance lock (`leave`) — the call is over and no
|
|
1162
|
-
* activation of this instance survives it.
|
|
1163
1244
|
* - MUST unwind the FACT sync-call scopes and restore `may_leave`
|
|
1164
1245
|
* (`unwind`), for exactly the reasons it does after a trap: a bail-out
|
|
1165
1246
|
* mid-adapter skips `exit-sync-call` and the `may_leave` restore, and
|
|
@@ -1203,17 +1284,10 @@ export function createLiftedFunction(input) {
|
|
|
1203
1284
|
}
|
|
1204
1285
|
catch (e) {
|
|
1205
1286
|
unwind();
|
|
1206
|
-
if (isCapabilitySignal(e))
|
|
1207
|
-
leave();
|
|
1208
|
-
else
|
|
1287
|
+
if (!isCapabilitySignal(e))
|
|
1209
1288
|
poison(e);
|
|
1210
1289
|
throw e;
|
|
1211
1290
|
}
|
|
1212
|
-
// The reentrance gate is released here, before the store is pumped:
|
|
1213
|
-
// `Store.tick` re-enters each waiting thread's instance itself
|
|
1214
|
-
// (`enter_from(None)` / `leave_to(None)`), exactly as in the reference,
|
|
1215
|
-
// where `lift_and_run` ticks after `store.invoke` has returned.
|
|
1216
|
-
leave();
|
|
1217
1291
|
let pending;
|
|
1218
1292
|
try {
|
|
1219
1293
|
// Completion is "the task resolved AND its threads have drained", not
|
|
@@ -1229,7 +1303,7 @@ export function createLiftedFunction(input) {
|
|
|
1229
1303
|
// abandoned mid-loop — leaking the exclusive thread and its table slot.
|
|
1230
1304
|
// The lifted call is over when the task has resolved AND this task's
|
|
1231
1305
|
// activation is no longer mid-wasm-call. Those are two different events
|
|
1232
|
-
// and both matter
|
|
1306
|
+
// and both matter:
|
|
1233
1307
|
//
|
|
1234
1308
|
// * "task resolved" alone abandons a still-running activation. Under
|
|
1235
1309
|
// JSPI the guest calls `task.return` while suspended, so returning
|
|
@@ -1268,6 +1342,14 @@ export function createLiftedFunction(input) {
|
|
|
1268
1342
|
});
|
|
1269
1343
|
};
|
|
1270
1344
|
return (...hostArgs) => {
|
|
1345
|
+
// sync() arm 2: the synchronous variant refuses rather than deferring, and
|
|
1346
|
+
// does so FIRST — before the arity check's sibling logic reaches
|
|
1347
|
+
// `invokeNow` — because the refusal must be pre-enter to stay
|
|
1348
|
+
// non-poisoning. See `refuseOnEntryHops` above for why the mode-keyed
|
|
1349
|
+
// gate below cannot cover this variant.
|
|
1350
|
+
if (input.refuseOnEntryHops && entryHopThreads(store, inst).length > 0) {
|
|
1351
|
+
throw new SyncEntryBusy(name);
|
|
1352
|
+
}
|
|
1271
1353
|
if (hostArgs.length !== ft.params.length) {
|
|
1272
1354
|
throw new TypeError(`${name}: expected ${ft.params.length} argument(s), got ${hostArgs.length}`);
|
|
1273
1355
|
}
|
|
@@ -1275,11 +1357,10 @@ export function createLiftedFunction(input) {
|
|
|
1275
1357
|
//
|
|
1276
1358
|
// A promising-wrapped entry settles a microtask AFTER the guest's core
|
|
1277
1359
|
// call returns, even when nothing suspended (jspi pin (j)) — so there
|
|
1278
|
-
// is a hop between core return and the host-side result LIFT,
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
// inside one entered bracket. Admitting another host call into the
|
|
1360
|
+
// is a hop between core return and the host-side result LIFT, with
|
|
1361
|
+
// nothing holding the instance against another host entry. In the
|
|
1362
|
+
// reference no such window exists: `canon_lift` for sync options runs
|
|
1363
|
+
// core + lift atomically. Admitting another host call into the
|
|
1283
1364
|
// window lets a full guest turn mutate the memory the pending lift
|
|
1284
1365
|
// will read — observed as `Trap: list too long` lifting the wosh
|
|
1285
1366
|
// engine's `tick` (`list<list<u8>>`) after a concurrent `feed-keys`
|
|
@@ -1381,27 +1462,13 @@ function dtorOptions(instance) {
|
|
|
1381
1462
|
* callee = inst.store.lift(dtor, ft, opts, rt.impl)
|
|
1382
1463
|
* ```
|
|
1383
1464
|
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
* so `Store.tick`'s enterability filter (#155) could never resume a
|
|
1392
|
-
* suspension point belonging to the dtor's own activation. The completion
|
|
1393
|
-
* promise sat in `pendingHostCalls` looking like external work, and every
|
|
1394
|
-
* driver parked on it forever.
|
|
1395
|
-
* - it was the runtime's only `enterFrom(null)` bracket spanning an await —
|
|
1396
|
-
* the macro-scale reachability window of the #156 class, through which a
|
|
1397
|
-
* sibling instance looked non-enterable from the synthetic root.
|
|
1398
|
-
* - built-ins reached inside the dtor had no ambient task (`currentTask()`
|
|
1399
|
-
* → `PendingCapability`, or a foreign-task misattribution, the #24 class).
|
|
1400
|
-
*
|
|
1401
|
-
* Under the lift harness all three go away structurally: the activation has a
|
|
1402
|
-
* real `Task` + implicit `Thread`, the entry bracket is released when the
|
|
1403
|
-
* first segment parks (`leave()` before `drive`), and settled tails flow
|
|
1404
|
-
* through `serviceSettled` like any other lifted sync call.
|
|
1465
|
+
* The host-initiated paths (embedder `drop()`, the GC backstop, `dropOwn`)
|
|
1466
|
+
* route through this harness rather than calling the dtor bare, because the
|
|
1467
|
+
* activation then has a real `Task` + implicit `Thread`: built-ins reached
|
|
1468
|
+
* inside the dtor are well-attributed (a bare call leaves `currentTask()`
|
|
1469
|
+
* with no ambient task — `PendingCapability`, or a foreign-task
|
|
1470
|
+
* misattribution, the #24 class), and settled tails flow through
|
|
1471
|
+
* `serviceSettled` like any other lifted sync call.
|
|
1405
1472
|
*
|
|
1406
1473
|
* The returned function takes the rep and returns either `undefined` (the
|
|
1407
1474
|
* activation completed synchronously — the overwhelmingly common case) or a
|
|
@@ -1487,7 +1554,7 @@ export function hostDtorCall(rt, rep) {
|
|
|
1487
1554
|
* This is the whole of the jspi entry seam. In **plain** mode the entry is not
|
|
1488
1555
|
* `promising`-wrapped, `callCore` returns core values, and this returns them
|
|
1489
1556
|
* without yielding — no await, no Promise allocation, the identical
|
|
1490
|
-
* synchronous path
|
|
1557
|
+
* synchronous path plain mode always used. In **jspi** mode the entry *is* wrapped, so the
|
|
1491
1558
|
* call returns a Promise (jspi pin (e)) and we park the thread on it via the
|
|
1492
1559
|
* `awaitValue` block request; the driving loop resumes us with the values, or
|
|
1493
1560
|
* throws the rejection in (a post-resume trap).
|
|
@@ -1626,7 +1693,7 @@ function* liftBody(input) {
|
|
|
1626
1693
|
* (`thread.wait_until(subtask.resolved)`, line 2286), so: `needsJspi`.
|
|
1627
1694
|
*/
|
|
1628
1695
|
export function createLoweredImport(input) {
|
|
1629
|
-
const { name, ft, opts, hostFn, stats, mode, suspendable } = input;
|
|
1696
|
+
const { name, ft, opts, hostFn, stats, mode, suspendable, deferCancel, abortable, } = input;
|
|
1630
1697
|
const inst = opts.instance;
|
|
1631
1698
|
const store = inst.store;
|
|
1632
1699
|
const computed = flattenFunctype(cabiOptions(opts), ft, "lower");
|
|
@@ -1677,22 +1744,42 @@ export function createLoweredImport(input) {
|
|
|
1677
1744
|
// definitions.py assigns the callee's `OnCancel` here:
|
|
1678
1745
|
// `subtask.on_cancel = callee(on_start, on_resolve, caller = ...)`
|
|
1679
1746
|
//
|
|
1680
|
-
//
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1747
|
+
// The `OnCancel` is the CALLEE's to supply: `Store.invoke` takes it back
|
|
1748
|
+
// from the callee it invoked (`on_cancel = f(on_start, on_resolve, caller
|
|
1749
|
+
// = None)`, definitions.py line 572), i.e. the reference expects the
|
|
1750
|
+
// embedding to hand back the cancellation behaviour of whatever it is
|
|
1751
|
+
// hosting. A wasmtime host gets a real one for free — dropping a Rust
|
|
1752
|
+
// future IS cancellation. A JS Promise has no such channel, so polyengine
|
|
1753
|
+
// answers on the host's behalf; §"Functions and async" makes the DEFAULT answer the
|
|
1754
|
+
// reference's prompt-cancel host (`on_cancel = () => on_resolve(None)`),
|
|
1755
|
+
// installed by the async arm below.
|
|
1756
|
+
//
|
|
1757
|
+
// The no-op assigned HERE is only the placeholder for paths where
|
|
1758
|
+
// `subtask.cancel` is unreachable, so no answer can ever be demanded of
|
|
1759
|
+
// it: an eagerly-resolving callee never mints a subtask handle (the
|
|
1760
|
+
// fast-path return below is a bare state), and a sync-typed import's suspending mark
|
|
1761
|
+
// park never mints one either. It is also the FINAL handler for a
|
|
1762
|
+
// `deferCancel()`-branded import — accept and ignore, the pre-cancellation discard
|
|
1763
|
+
// behaviour, now per-declaration.
|
|
1689
1764
|
//
|
|
1690
1765
|
// Leaving `on_cancel` null instead made a *legal* `subtask.cancel` crash
|
|
1691
1766
|
// with an internal AssertionError, which is neither reference behaviour
|
|
1692
1767
|
// nor a sanctioned incompleteness signal.
|
|
1693
1768
|
subtask.onCancel = () => { };
|
|
1769
|
+
// abortable() (contracts/embedder-api.md §"Functions and async"): a marked import
|
|
1770
|
+
// is handed a fresh `AbortSignal` after its WIT-declared parameters. The
|
|
1771
|
+
// mark controls the SIGNATURE UNCONDITIONALLY — a marked function receives
|
|
1772
|
+
// a signal on every call, including the paths where it can never fire
|
|
1773
|
+
// (sync-typed, eager resolve, `deferCancel`) — so the host's arity is a
|
|
1774
|
+
// property of its declaration, not of how a particular call happened to
|
|
1775
|
+
// go. `new AbortController()` is evaluated only for marked imports, which
|
|
1776
|
+
// keeps bare engine shells with no `AbortController` off this path for the
|
|
1777
|
+
// whole unmarked corpus.
|
|
1778
|
+
const controller = abortable ? new AbortController() : null;
|
|
1694
1779
|
const args = onStart();
|
|
1695
|
-
const raw =
|
|
1780
|
+
const raw = controller === null
|
|
1781
|
+
? hostFn(...args)
|
|
1782
|
+
: hostFn(...args, controller.signal);
|
|
1696
1783
|
const toResults = (v) => ft.results.length === 0 ? [] : [v];
|
|
1697
1784
|
if (isPromiseLike(raw)) {
|
|
1698
1785
|
if (!opts.async) {
|
|
@@ -1703,8 +1790,9 @@ export function createLoweredImport(input) {
|
|
|
1703
1790
|
// Suspending wrap is applied per-declaration (`importValue`), so an
|
|
1704
1791
|
// unmarked import physically cannot suspend, whatever the mode.
|
|
1705
1792
|
//
|
|
1706
|
-
// A capability signal is expressly NON-poisoning (
|
|
1707
|
-
//
|
|
1793
|
+
// A capability signal is expressly NON-poisoning (the
|
|
1794
|
+
// trap-unwind/lender-release obligation, contracts/intrinsics.md §A):
|
|
1795
|
+
// the caller keeps running, so the borrows
|
|
1708
1796
|
// `onStart` lifted into this subtask must be discharged here or
|
|
1709
1797
|
// its lenders stay elevated forever and later `resource.drop`s
|
|
1710
1798
|
// trap "handle still lent out" on a healthy instance (found
|
|
@@ -1720,7 +1808,7 @@ export function createLoweredImport(input) {
|
|
|
1720
1808
|
`only park the frame when declared with suspending() ` +
|
|
1721
1809
|
`(contracts/embedder-api.md §"Functions and async")`);
|
|
1722
1810
|
}
|
|
1723
|
-
// The park
|
|
1811
|
+
// The park: the reference's plain, NON-cancellable wait — a
|
|
1724
1812
|
// cancel request against the caller stays pending-cancel and is
|
|
1725
1813
|
// delivered at its next cancellable wait, exactly as for any other
|
|
1726
1814
|
// mid-frame block. The instance-entry gate stays HELD across the park
|
|
@@ -1764,8 +1852,8 @@ export function createLoweredImport(input) {
|
|
|
1764
1852
|
// * produce SUCCESS -> `onResolve` + `deliverResolve` release the
|
|
1765
1853
|
// lenders; the `onSettled` backstop below observes
|
|
1766
1854
|
// `resolveDelivered()` and is a no-op.
|
|
1767
|
-
// * produce THROW -> exempt
|
|
1768
|
-
// (contracts/intrinsics.md
|
|
1855
|
+
// * produce THROW -> exempt under the trap-unwind/lender-release
|
|
1856
|
+
// obligation (contracts/intrinsics.md §A: release is owed only on exits
|
|
1769
1857
|
// that do NOT poison the caller). Every rejection that reaches
|
|
1770
1858
|
// this park is a poisoning trap in the CALLER's own frame:
|
|
1771
1859
|
// branded `ComponentException`s on fallible imports were already resolved
|
|
@@ -1817,6 +1905,16 @@ export function createLoweredImport(input) {
|
|
|
1817
1905
|
}
|
|
1818
1906
|
const promise = Promise.resolve(raw).then((v) => {
|
|
1819
1907
|
store.pendingHostCalls.delete(promise);
|
|
1908
|
+
// cancellation discard: the subtask may already be resolved when the host promise
|
|
1909
|
+
// settles — the discard `onCancel` below resolved it
|
|
1910
|
+
// CANCELLED_BEFORE_RETURNED (the only pre-settle resolver on this
|
|
1911
|
+
// arm). The value has no addressee, and `onResolve` would run
|
|
1912
|
+
// straight into its `state === STARTED` assert ("on_resolve on a
|
|
1913
|
+
// subtask that never started") and park that AssertionError on
|
|
1914
|
+
// `store.hostFailure`, poisoning whatever unrelated embedder call
|
|
1915
|
+
// came next.
|
|
1916
|
+
if (subtask.resolved())
|
|
1917
|
+
return;
|
|
1820
1918
|
try {
|
|
1821
1919
|
onResolve(toResults(v));
|
|
1822
1920
|
}
|
|
@@ -1825,9 +1923,64 @@ export function createLoweredImport(input) {
|
|
|
1825
1923
|
}
|
|
1826
1924
|
}, (e) => {
|
|
1827
1925
|
store.pendingHostCalls.delete(promise);
|
|
1926
|
+
// Same guard, different reason: a rejection of a RENOUNCED call is
|
|
1927
|
+
// not a host failure. The guest cancelled and was told so; surfacing
|
|
1928
|
+
// the rejection would fail an unrelated later call with the error of
|
|
1929
|
+
// an operation nobody is waiting for.
|
|
1930
|
+
if (subtask.resolved())
|
|
1931
|
+
return;
|
|
1828
1932
|
store.hostFailure = e;
|
|
1829
1933
|
});
|
|
1830
1934
|
store.pendingHostCalls.add(promise);
|
|
1935
|
+
if (!deferCancel) {
|
|
1936
|
+
// cancellation discard DISCARD (contracts/embedder-api.md §"Functions and async";
|
|
1937
|
+
// polyengine#241) — the reference's prompt-cancel host,
|
|
1938
|
+
// `on_cancel = () => on_resolve(None)` (definitions.py canon_lower's
|
|
1939
|
+
// null branch, line ~2267).
|
|
1940
|
+
//
|
|
1941
|
+
// This runs synchronously inside `canon_subtask_cancel`, which already
|
|
1942
|
+
// set `cancellationRequested` before calling us (the assert in
|
|
1943
|
+
// `onResolve`'s null branch relies on that ordering). `onResolve(null)`
|
|
1944
|
+
// arms the SUBTASK event — a delivery-time thunk — and resolves
|
|
1945
|
+
// CANCELLED_BEFORE_RETURNED, so the built-in's `finish()` tail consumes
|
|
1946
|
+
// the event, `deliverResolve` releases the lenders (the #106 class,
|
|
1947
|
+
// discharged exactly as a RETURNED delivery would), and BOTH cancel
|
|
1948
|
+
// forms return the state without blocking. The null path lowers
|
|
1949
|
+
// nothing, so there is no realloc re-entry from inside a built-in.
|
|
1950
|
+
//
|
|
1951
|
+
// The renounced call can no longer wake the guest, so it must stop
|
|
1952
|
+
// counting as externally-wakeable for the driver's deadlock probe:
|
|
1953
|
+
// deregister it NOW. (The settle continuation above also deletes;
|
|
1954
|
+
// `Set.delete` is idempotent.)
|
|
1955
|
+
subtask.onCancel = () => {
|
|
1956
|
+
store.pendingHostCalls.delete(promise);
|
|
1957
|
+
onResolve(null);
|
|
1958
|
+
if (controller !== null) {
|
|
1959
|
+
// abortable(): tell the host its result was discarded, so it can stop the
|
|
1960
|
+
// underlying operation — clear a timer, abort a fetch, close a
|
|
1961
|
+
// dial. Reachable only from this arm by construction: a
|
|
1962
|
+
// `deferCancel()` import never discards, so its signal never
|
|
1963
|
+
// fires.
|
|
1964
|
+
//
|
|
1965
|
+
// Deferred one microtask. This closure runs SYNCHRONOUSLY inside
|
|
1966
|
+
// `canon_subtask_cancel`, i.e. inside a live guest activation, and
|
|
1967
|
+
// host abort listeners must not execute there — that is the
|
|
1968
|
+
// issue-#24 attribution class, plus arbitrary re-entrancy into a
|
|
1969
|
+
// guest mid-built-in. `Promise.resolve().then`, not
|
|
1970
|
+
// `queueMicrotask`: the latter does not exist in bare engine
|
|
1971
|
+
// shells (see jspi/bridge.ts's SENTINEL_TICK note).
|
|
1972
|
+
//
|
|
1973
|
+
// The resulting order is: the guest observes
|
|
1974
|
+
// CANCELLED_BEFORE_RETURNED first, the host observes the abort a
|
|
1975
|
+
// tick later. Any settlement the abort provokes (typically an
|
|
1976
|
+
// `AbortError` rejection) arrives at the settle continuation above
|
|
1977
|
+
// with the subtask already resolved, so it lands on the cancellation discard
|
|
1978
|
+
// resolved-subtask guards and is discarded like any other late
|
|
1979
|
+
// settlement — never a `store.hostFailure`.
|
|
1980
|
+
Promise.resolve().then(() => controller.abort());
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1831
1984
|
}
|
|
1832
1985
|
else {
|
|
1833
1986
|
onResolve(toResults(raw));
|