@polyengine/runtime 0.5.1 → 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 +132 -37
- 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 +123 -161
- package/esm/exec/executor.js +37 -25
- package/esm/exec/host_streams.js +31 -31
- package/esm/intrinsics/async_builtins.js +15 -7
- 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 +5 -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 +55 -44
- 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/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
|
|
@@ -680,11 +680,12 @@ async function driveAsync(store, done, what) {
|
|
|
680
680
|
// and clearing it before that activation runs re-opens the
|
|
681
681
|
// mis-attribution window the entry exists to close.
|
|
682
682
|
//
|
|
683
|
-
// PER-STORE (issue #210):
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
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.
|
|
688
689
|
if (store.hasPendingResumptions()) {
|
|
689
690
|
traceDrive("driveAsync", store, done, "yield-pending");
|
|
690
691
|
// Bounded: a pending entry that never dies is an internal bug (every
|
|
@@ -742,7 +743,7 @@ async function driveAsync(store, done, what) {
|
|
|
742
743
|
// therefore stops the scheduler while waiting for something that needs the
|
|
743
744
|
// scheduler: a pure-microtask stall with no trap and no rejection.
|
|
744
745
|
// Observed on `async/async-calls-sync.wast` the moment site 1 became the
|
|
745
|
-
// first lit suspension site
|
|
746
|
+
// first lit suspension site: turn N serviced a promise that
|
|
746
747
|
// never settled while three other parked threads and three ready-able
|
|
747
748
|
// suspension points went unexamined.
|
|
748
749
|
//
|
|
@@ -805,12 +806,12 @@ async function driveAsync(store, done, what) {
|
|
|
805
806
|
// set changing: the same activation resumes off an engine
|
|
806
807
|
// continuation chunk during the probe's macrotask turn (jspi
|
|
807
808
|
// pin (j) — a sync-completing Suspending import still defers its
|
|
808
|
-
// continuation), runs, and re-parks through the
|
|
809
|
+
// continuation), runs, and re-parks through the suspending mark arm, which
|
|
809
810
|
// registers a fresh `pendingHostCalls` entry. The activation
|
|
810
811
|
// promise never settled and `awaiting` membership is unchanged,
|
|
811
812
|
// but the park is externally wakeable now — the verdict's own
|
|
812
813
|
// precondition (`pendingHostCalls.size === 0`) no longer holds.
|
|
813
|
-
// Observed on wasi-shims'
|
|
814
|
+
// Observed on wasi-shims' stream/future round-trip poll (sync fast path): probe sampled
|
|
814
815
|
// hostCalls=0 between a settled park and the next one, then
|
|
815
816
|
// trapped a live workload with hostCalls=1. Re-check ⇒ re-probe.
|
|
816
817
|
// Likewise a SERVICEABLE settled entry (issue #156): dispatching
|
|
@@ -855,7 +856,7 @@ async function driveAsync(store, done, what) {
|
|
|
855
856
|
// snapshot's `parked[0]` is then `undefined` — the exact check-then-act
|
|
856
857
|
// shape that made the host pump's copy of this loop throw
|
|
857
858
|
// `TypeError: ... (reading 'awaiting')` into `store.hostFailure`, where
|
|
858
|
-
// it poisoned a later unrelated call
|
|
859
|
+
// it poisoned a later unrelated call via check-then-act on `store.hostFailure`. Nothing to
|
|
859
860
|
// service ⇒ go back to the top and re-evaluate `done`.
|
|
860
861
|
// Same re-check for the settled queue, and for the same reason: the
|
|
861
862
|
// probe's macrotask turn can land a fresh, SERVICEABLE activation tail
|
|
@@ -874,7 +875,7 @@ async function driveAsync(store, done, what) {
|
|
|
874
875
|
// never be held hostage by it. The claimed thread's promise may only be
|
|
875
876
|
// settleable by further scheduler progress (a promising-wrapped nested
|
|
876
877
|
// activation whose own suspension points this loop must still resume);
|
|
877
|
-
// blocking on it alone is the pure-microtask stall
|
|
878
|
+
// blocking on it alone is the pure-microtask stall described above.
|
|
878
879
|
// Same exclusion as the probe (issue #156): a thread whose tail is
|
|
879
880
|
// already queued in `store.settled` must not be raced — its tag is
|
|
880
881
|
// settled, so it re-wins instantly and livelocks the event loop,
|
|
@@ -882,12 +883,12 @@ async function driveAsync(store, done, what) {
|
|
|
882
883
|
const queued = new Set(store.settled.map((s) => s.t));
|
|
883
884
|
const parked = [...store.awaiting].filter((t) => !queued.has(t));
|
|
884
885
|
if (parked.length === 0) {
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
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.
|
|
891
892
|
if (store.pendingHostCalls.size > 0) {
|
|
892
893
|
await Promise.race([
|
|
893
894
|
...store.pendingHostCalls,
|
|
@@ -917,11 +918,11 @@ async function driveAsync(store, done, what) {
|
|
|
917
918
|
// `SuspensionPoint.resume`) is what carries it, and dropping an entry
|
|
918
919
|
// that names a thread already gone from the set is a no-op.
|
|
919
920
|
//
|
|
920
|
-
// ONLY ITS OWN ENTRY (issue #158): the `finally`
|
|
921
|
-
//
|
|
922
|
-
// await
|
|
923
|
-
//
|
|
924
|
-
//
|
|
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.
|
|
925
926
|
//
|
|
926
927
|
// SOLE DRIVER ONLY, AND ONLY UNTIL ONE ARRIVES (issue #239). The entry
|
|
927
928
|
// is a claim over a window this loop cannot bound: the race settles when
|
|
@@ -941,13 +942,14 @@ async function driveAsync(store, done, what) {
|
|
|
941
942
|
// can reach a store mid-race are another `driveAsync` loop and
|
|
942
943
|
// `HostActivity.pump`'s synchronous drain (exec/host_streams.ts) — the
|
|
943
944
|
// latter is not gated by driver depth, so scoping the entry to "sole
|
|
944
|
-
// driver" does hand it a window
|
|
945
|
+
// driver" does hand it a window an unscoped entry would close at
|
|
946
|
+
// depth >= 2.
|
|
945
947
|
// What holds regardless is the invariant the `driverDepth` note names:
|
|
946
948
|
// a genuine resumption is preceded by `SuspensionPoint.resume`'s OWN
|
|
947
949
|
// entry (jspi/bridge.ts, minted before the settle), and every
|
|
948
|
-
// resumption site here re-checks membership
|
|
949
|
-
//
|
|
950
|
-
//
|
|
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.
|
|
951
953
|
const sole = storeDriverDepth(store) === 1;
|
|
952
954
|
if (sole)
|
|
953
955
|
store.addPendingResumption(chosen);
|
|
@@ -979,13 +981,7 @@ async function driveAsync(store, done, what) {
|
|
|
979
981
|
// true again but the tag's value belongs to a settlement this thread
|
|
980
982
|
// has already consumed. Compare promise identity too.
|
|
981
983
|
if (winner !== null && store.awaiting.has(winner.t) &&
|
|
982
|
-
winner.t.awaiting === winner.p
|
|
983
|
-
// Dispatch guard, the same predicate `Store.serviceSettled` uses
|
|
984
|
-
// (issue #156): never resume into an instance that is not
|
|
985
|
-
// host-enterable. The entry is (also) queued in `store.settled` by
|
|
986
|
-
// `noteAwaiting`'s continuation, and `serviceSettled` owns it once
|
|
987
|
-
// the lock releases.
|
|
988
|
-
dispatchableTail(winner.t)) {
|
|
984
|
+
winner.t.awaiting === winner.p) {
|
|
989
985
|
winner.t.resumeWith(winner.value, winner.failure);
|
|
990
986
|
}
|
|
991
987
|
continue;
|
|
@@ -1051,26 +1047,35 @@ function takeHostFailure(store) {
|
|
|
1051
1047
|
* needs genuine wasm-frame suspension: `needsJspi`, at the precise point.
|
|
1052
1048
|
*/
|
|
1053
1049
|
/**
|
|
1054
|
-
* The plain-entered variant of a
|
|
1055
|
-
* 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").
|
|
1056
1053
|
*
|
|
1057
|
-
*
|
|
1058
|
-
* (
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
* overwhelmingly common case; WIT constructors are always sync-typed —
|
|
1064
|
-
* returns its rep synchronously through it.
|
|
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.
|
|
1065
1060
|
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1061
|
+
* Two consumers, one mechanism:
|
|
1062
|
+
*
|
|
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).
|
|
1072
1077
|
*/
|
|
1073
|
-
export const
|
|
1078
|
+
export const SYNC_ENTRY = Symbol("polyengine.syncEntry");
|
|
1074
1079
|
export function createLiftedFunction(input) {
|
|
1075
1080
|
const { name, ft, opts, core, stats, trapState, syncCallStack, allInstances, } = input;
|
|
1076
1081
|
const inst = opts.instance;
|
|
@@ -1108,19 +1113,16 @@ export function createLiftedFunction(input) {
|
|
|
1108
1113
|
trapState.pending = undefined;
|
|
1109
1114
|
// Depth of the sync-call scope stack on entry; see the `finally` below.
|
|
1110
1115
|
const syncCallDepth = syncCallStack?.length ?? 0;
|
|
1111
|
-
// Reference `Store.lift` (
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
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);
|
|
1117
1125
|
}
|
|
1118
|
-
// The set this entry locked (definitions.py `ComponentInstance.enter_from`
|
|
1119
|
-
// iterates `entering_set`). Remembered so a trap can leave exactly these
|
|
1120
|
-
// locked and no others.
|
|
1121
|
-
const enteredSet = inst.enteringSet(null);
|
|
1122
|
-
inst.enterFrom(null);
|
|
1123
|
-
let entered = true;
|
|
1124
1126
|
let completed = false;
|
|
1125
1127
|
let resolved = null;
|
|
1126
1128
|
let resolvedSeen = false;
|
|
@@ -1186,44 +1188,30 @@ export function createLiftedFunction(input) {
|
|
|
1186
1188
|
// definition; assert that resting state rather than leaving the
|
|
1187
1189
|
// component bricked.
|
|
1188
1190
|
//
|
|
1189
|
-
// The
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
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.
|
|
1193
1195
|
for (const i of allInstances?.() ?? []) {
|
|
1194
|
-
if (
|
|
1196
|
+
if (i !== inst) {
|
|
1195
1197
|
i.mayLeave = true;
|
|
1196
1198
|
}
|
|
1197
1199
|
}
|
|
1198
1200
|
};
|
|
1199
|
-
const leave = () => {
|
|
1200
|
-
if (!entered)
|
|
1201
|
-
return;
|
|
1202
|
-
entered = false;
|
|
1203
|
-
inst.leaveTo(null);
|
|
1204
|
-
};
|
|
1205
1201
|
/**
|
|
1206
|
-
* A trap escaped the task:
|
|
1207
|
-
*
|
|
1208
|
-
* definitions.py `Store.lift` (line 578) is
|
|
1209
|
-
*
|
|
1210
|
-
* ```python
|
|
1211
|
-
* trap_if(not inst.may_enter_from(caller))
|
|
1212
|
-
* inst.enter_from(caller)
|
|
1213
|
-
* on_cancel = canon_lift(...) # <-- a Trap propagates out of here
|
|
1214
|
-
* inst.leave_to(caller) # <-- and so this never runs
|
|
1215
|
-
* ```
|
|
1202
|
+
* A trap escaped the task: mark the instance poisoned.
|
|
1216
1203
|
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
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.
|
|
1223
1213
|
*
|
|
1224
|
-
* Only
|
|
1225
|
-
* is why the lock is released per-instance rather than by poisoning a
|
|
1226
|
-
* whole store the way wasmtime does.
|
|
1214
|
+
* Only `inst` is affected; sibling instances stay usable.
|
|
1227
1215
|
*
|
|
1228
1216
|
* Poisoned instances can never rendezvous again, so their handle tables'
|
|
1229
1217
|
* live stream/future ends are retired here (#66): parked host operations
|
|
@@ -1231,21 +1219,10 @@ export function createLiftedFunction(input) {
|
|
|
1231
1219
|
* lets the embedder layer reject them loudly.
|
|
1232
1220
|
*/
|
|
1233
1221
|
const poison = (e) => {
|
|
1234
|
-
entered = false; // consumed: the lock is now permanent
|
|
1235
|
-
// ...for the leaf. The synthetic per-instantiation root (plan v3
|
|
1236
|
-
// amendment 4) is in `enteredSet` too, and leaving IT locked would
|
|
1237
|
-
// poison every instance of the component — exactly the store-wide
|
|
1238
|
-
// behaviour the paragraph above says this runtime deliberately does not
|
|
1239
|
-
// have. Released; see `releaseSyntheticRootOnPoison` in task/mod.ts.
|
|
1240
|
-
inst.releaseSyntheticRootOnPoison();
|
|
1241
1222
|
// Through the seam (not retireInstanceAsyncEnds directly) so the
|
|
1242
1223
|
// poison marker is recorded too — `Thread.resumeWith` retires this
|
|
1243
1224
|
// instance's late settles against it instead of assert-cascading.
|
|
1244
|
-
|
|
1245
|
-
if (i.isSyntheticRoot)
|
|
1246
|
-
continue;
|
|
1247
|
-
notifyInstancePoisoned(i, e);
|
|
1248
|
-
}
|
|
1225
|
+
notifyInstancePoisoned(inst, e);
|
|
1249
1226
|
};
|
|
1250
1227
|
/**
|
|
1251
1228
|
* Is `e` a *capability* signal rather than a genuine trap?
|
|
@@ -1254,9 +1231,9 @@ export function createLiftedFunction(input) {
|
|
|
1254
1231
|
* not "the component faulted". Poisoning on them is wrong on the
|
|
1255
1232
|
* reference's own terms: the operation they stand in for — a synchronous
|
|
1256
1233
|
* stream copy, `waitable-set.wait`, a blocking cross-component call —
|
|
1257
|
-
* *blocks and then completes* in definitions.py.
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
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
|
|
1260
1237
|
* that, on a complete runtime, is perfectly healthy — and it cascades:
|
|
1261
1238
|
* one unsupported operation made every later call on that instance report
|
|
1262
1239
|
* `cannot enter component instance`, which is neither our real behaviour
|
|
@@ -1264,8 +1241,6 @@ export function createLiftedFunction(input) {
|
|
|
1264
1241
|
*
|
|
1265
1242
|
* What unwinding must still do on this path, and what it must not:
|
|
1266
1243
|
*
|
|
1267
|
-
* - MUST release the reentrance lock (`leave`) — the call is over and no
|
|
1268
|
-
* activation of this instance survives it.
|
|
1269
1244
|
* - MUST unwind the FACT sync-call scopes and restore `may_leave`
|
|
1270
1245
|
* (`unwind`), for exactly the reasons it does after a trap: a bail-out
|
|
1271
1246
|
* mid-adapter skips `exit-sync-call` and the `may_leave` restore, and
|
|
@@ -1309,17 +1284,10 @@ export function createLiftedFunction(input) {
|
|
|
1309
1284
|
}
|
|
1310
1285
|
catch (e) {
|
|
1311
1286
|
unwind();
|
|
1312
|
-
if (isCapabilitySignal(e))
|
|
1313
|
-
leave();
|
|
1314
|
-
else
|
|
1287
|
+
if (!isCapabilitySignal(e))
|
|
1315
1288
|
poison(e);
|
|
1316
1289
|
throw e;
|
|
1317
1290
|
}
|
|
1318
|
-
// The reentrance gate is released here, before the store is pumped:
|
|
1319
|
-
// `Store.tick` re-enters each waiting thread's instance itself
|
|
1320
|
-
// (`enter_from(None)` / `leave_to(None)`), exactly as in the reference,
|
|
1321
|
-
// where `lift_and_run` ticks after `store.invoke` has returned.
|
|
1322
|
-
leave();
|
|
1323
1291
|
let pending;
|
|
1324
1292
|
try {
|
|
1325
1293
|
// Completion is "the task resolved AND its threads have drained", not
|
|
@@ -1335,7 +1303,7 @@ export function createLiftedFunction(input) {
|
|
|
1335
1303
|
// abandoned mid-loop — leaking the exclusive thread and its table slot.
|
|
1336
1304
|
// The lifted call is over when the task has resolved AND this task's
|
|
1337
1305
|
// activation is no longer mid-wasm-call. Those are two different events
|
|
1338
|
-
// and both matter
|
|
1306
|
+
// and both matter:
|
|
1339
1307
|
//
|
|
1340
1308
|
// * "task resolved" alone abandons a still-running activation. Under
|
|
1341
1309
|
// JSPI the guest calls `task.return` while suspended, so returning
|
|
@@ -1374,6 +1342,14 @@ export function createLiftedFunction(input) {
|
|
|
1374
1342
|
});
|
|
1375
1343
|
};
|
|
1376
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
|
+
}
|
|
1377
1353
|
if (hostArgs.length !== ft.params.length) {
|
|
1378
1354
|
throw new TypeError(`${name}: expected ${ft.params.length} argument(s), got ${hostArgs.length}`);
|
|
1379
1355
|
}
|
|
@@ -1381,11 +1357,10 @@ export function createLiftedFunction(input) {
|
|
|
1381
1357
|
//
|
|
1382
1358
|
// A promising-wrapped entry settles a microtask AFTER the guest's core
|
|
1383
1359
|
// call returns, even when nothing suspended (jspi pin (j)) — so there
|
|
1384
|
-
// is a hop between core return and the host-side result LIFT,
|
|
1385
|
-
//
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1388
|
-
// 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
|
|
1389
1364
|
// window lets a full guest turn mutate the memory the pending lift
|
|
1390
1365
|
// will read — observed as `Trap: list too long` lifting the wosh
|
|
1391
1366
|
// engine's `tick` (`list<list<u8>>`) after a concurrent `feed-keys`
|
|
@@ -1487,27 +1462,13 @@ function dtorOptions(instance) {
|
|
|
1487
1462
|
* callee = inst.store.lift(dtor, ft, opts, rt.impl)
|
|
1488
1463
|
* ```
|
|
1489
1464
|
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
* so `Store.tick`'s enterability filter (#155) could never resume a
|
|
1498
|
-
* suspension point belonging to the dtor's own activation. The completion
|
|
1499
|
-
* promise sat in `pendingHostCalls` looking like external work, and every
|
|
1500
|
-
* driver parked on it forever.
|
|
1501
|
-
* - it was the runtime's only `enterFrom(null)` bracket spanning an await —
|
|
1502
|
-
* the macro-scale reachability window of the #156 class, through which a
|
|
1503
|
-
* sibling instance looked non-enterable from the synthetic root.
|
|
1504
|
-
* - built-ins reached inside the dtor had no ambient task (`currentTask()`
|
|
1505
|
-
* → `PendingCapability`, or a foreign-task misattribution, the #24 class).
|
|
1506
|
-
*
|
|
1507
|
-
* Under the lift harness all three go away structurally: the activation has a
|
|
1508
|
-
* real `Task` + implicit `Thread`, the entry bracket is released when the
|
|
1509
|
-
* first segment parks (`leave()` before `drive`), and settled tails flow
|
|
1510
|
-
* 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.
|
|
1511
1472
|
*
|
|
1512
1473
|
* The returned function takes the rep and returns either `undefined` (the
|
|
1513
1474
|
* activation completed synchronously — the overwhelmingly common case) or a
|
|
@@ -1593,7 +1554,7 @@ export function hostDtorCall(rt, rep) {
|
|
|
1593
1554
|
* This is the whole of the jspi entry seam. In **plain** mode the entry is not
|
|
1594
1555
|
* `promising`-wrapped, `callCore` returns core values, and this returns them
|
|
1595
1556
|
* without yielding — no await, no Promise allocation, the identical
|
|
1596
|
-
* synchronous path
|
|
1557
|
+
* synchronous path plain mode always used. In **jspi** mode the entry *is* wrapped, so the
|
|
1597
1558
|
* call returns a Promise (jspi pin (e)) and we park the thread on it via the
|
|
1598
1559
|
* `awaitValue` block request; the driving loop resumes us with the values, or
|
|
1599
1560
|
* throws the rejection in (a post-resume trap).
|
|
@@ -1789,23 +1750,23 @@ export function createLoweredImport(input) {
|
|
|
1789
1750
|
// embedding to hand back the cancellation behaviour of whatever it is
|
|
1790
1751
|
// hosting. A wasmtime host gets a real one for free — dropping a Rust
|
|
1791
1752
|
// future IS cancellation. A JS Promise has no such channel, so polyengine
|
|
1792
|
-
// answers on the host's behalf;
|
|
1753
|
+
// answers on the host's behalf; §"Functions and async" makes the DEFAULT answer the
|
|
1793
1754
|
// reference's prompt-cancel host (`on_cancel = () => on_resolve(None)`),
|
|
1794
1755
|
// installed by the async arm below.
|
|
1795
1756
|
//
|
|
1796
1757
|
// The no-op assigned HERE is only the placeholder for paths where
|
|
1797
1758
|
// `subtask.cancel` is unreachable, so no answer can ever be demanded of
|
|
1798
1759
|
// it: an eagerly-resolving callee never mints a subtask handle (the
|
|
1799
|
-
// fast-path return below is a bare state), and a sync-typed import's
|
|
1760
|
+
// fast-path return below is a bare state), and a sync-typed import's suspending mark
|
|
1800
1761
|
// park never mints one either. It is also the FINAL handler for a
|
|
1801
|
-
// `deferCancel()`-branded import — accept and ignore, the pre-
|
|
1762
|
+
// `deferCancel()`-branded import — accept and ignore, the pre-cancellation discard
|
|
1802
1763
|
// behaviour, now per-declaration.
|
|
1803
1764
|
//
|
|
1804
1765
|
// Leaving `on_cancel` null instead made a *legal* `subtask.cancel` crash
|
|
1805
1766
|
// with an internal AssertionError, which is neither reference behaviour
|
|
1806
1767
|
// nor a sanctioned incompleteness signal.
|
|
1807
1768
|
subtask.onCancel = () => { };
|
|
1808
|
-
//
|
|
1769
|
+
// abortable() (contracts/embedder-api.md §"Functions and async"): a marked import
|
|
1809
1770
|
// is handed a fresh `AbortSignal` after its WIT-declared parameters. The
|
|
1810
1771
|
// mark controls the SIGNATURE UNCONDITIONALLY — a marked function receives
|
|
1811
1772
|
// a signal on every call, including the paths where it can never fire
|
|
@@ -1829,8 +1790,9 @@ export function createLoweredImport(input) {
|
|
|
1829
1790
|
// Suspending wrap is applied per-declaration (`importValue`), so an
|
|
1830
1791
|
// unmarked import physically cannot suspend, whatever the mode.
|
|
1831
1792
|
//
|
|
1832
|
-
// A capability signal is expressly NON-poisoning (
|
|
1833
|
-
//
|
|
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
|
|
1834
1796
|
// `onStart` lifted into this subtask must be discharged here or
|
|
1835
1797
|
// its lenders stay elevated forever and later `resource.drop`s
|
|
1836
1798
|
// trap "handle still lent out" on a healthy instance (found
|
|
@@ -1846,7 +1808,7 @@ export function createLoweredImport(input) {
|
|
|
1846
1808
|
`only park the frame when declared with suspending() ` +
|
|
1847
1809
|
`(contracts/embedder-api.md §"Functions and async")`);
|
|
1848
1810
|
}
|
|
1849
|
-
// The park
|
|
1811
|
+
// The park: the reference's plain, NON-cancellable wait — a
|
|
1850
1812
|
// cancel request against the caller stays pending-cancel and is
|
|
1851
1813
|
// delivered at its next cancellable wait, exactly as for any other
|
|
1852
1814
|
// mid-frame block. The instance-entry gate stays HELD across the park
|
|
@@ -1890,8 +1852,8 @@ export function createLoweredImport(input) {
|
|
|
1890
1852
|
// * produce SUCCESS -> `onResolve` + `deliverResolve` release the
|
|
1891
1853
|
// lenders; the `onSettled` backstop below observes
|
|
1892
1854
|
// `resolveDelivered()` and is a no-op.
|
|
1893
|
-
// * produce THROW -> exempt
|
|
1894
|
-
// (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
|
|
1895
1857
|
// that do NOT poison the caller). Every rejection that reaches
|
|
1896
1858
|
// this park is a poisoning trap in the CALLER's own frame:
|
|
1897
1859
|
// branded `ComponentException`s on fallible imports were already resolved
|
|
@@ -1943,7 +1905,7 @@ export function createLoweredImport(input) {
|
|
|
1943
1905
|
}
|
|
1944
1906
|
const promise = Promise.resolve(raw).then((v) => {
|
|
1945
1907
|
store.pendingHostCalls.delete(promise);
|
|
1946
|
-
//
|
|
1908
|
+
// cancellation discard: the subtask may already be resolved when the host promise
|
|
1947
1909
|
// settles — the discard `onCancel` below resolved it
|
|
1948
1910
|
// CANCELLED_BEFORE_RETURNED (the only pre-settle resolver on this
|
|
1949
1911
|
// arm). The value has no addressee, and `onResolve` would run
|
|
@@ -1971,7 +1933,7 @@ export function createLoweredImport(input) {
|
|
|
1971
1933
|
});
|
|
1972
1934
|
store.pendingHostCalls.add(promise);
|
|
1973
1935
|
if (!deferCancel) {
|
|
1974
|
-
//
|
|
1936
|
+
// cancellation discard DISCARD (contracts/embedder-api.md §"Functions and async";
|
|
1975
1937
|
// polyengine#241) — the reference's prompt-cancel host,
|
|
1976
1938
|
// `on_cancel = () => on_resolve(None)` (definitions.py canon_lower's
|
|
1977
1939
|
// null branch, line ~2267).
|
|
@@ -1994,7 +1956,7 @@ export function createLoweredImport(input) {
|
|
|
1994
1956
|
store.pendingHostCalls.delete(promise);
|
|
1995
1957
|
onResolve(null);
|
|
1996
1958
|
if (controller !== null) {
|
|
1997
|
-
//
|
|
1959
|
+
// abortable(): tell the host its result was discarded, so it can stop the
|
|
1998
1960
|
// underlying operation — clear a timer, abort a fetch, close a
|
|
1999
1961
|
// dial. Reachable only from this arm by construction: a
|
|
2000
1962
|
// `deferCancel()` import never discards, so its signal never
|
|
@@ -2012,7 +1974,7 @@ export function createLoweredImport(input) {
|
|
|
2012
1974
|
// CANCELLED_BEFORE_RETURNED first, the host observes the abort a
|
|
2013
1975
|
// tick later. Any settlement the abort provokes (typically an
|
|
2014
1976
|
// `AbortError` rejection) arrives at the settle continuation above
|
|
2015
|
-
// with the subtask already resolved, so it lands on the
|
|
1977
|
+
// with the subtask already resolved, so it lands on the cancellation discard
|
|
2016
1978
|
// resolved-subtask guards and is discarded like any other late
|
|
2017
1979
|
// settlement — never a `store.hostFailure`.
|
|
2018
1980
|
Promise.resolve().then(() => controller.abort());
|