@polyengine/runtime 0.1.0-pre.g633468a
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/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cabi/async_values.js +162 -0
- package/esm/cabi/bulk_lists.js +198 -0
- package/esm/cabi/context.js +42 -0
- package/esm/cabi/flatten.js +145 -0
- package/esm/cabi/float.js +67 -0
- package/esm/cabi/handles.js +253 -0
- package/esm/cabi/layout.js +149 -0
- package/esm/cabi/lift.js +196 -0
- package/esm/cabi/load.js +146 -0
- package/esm/cabi/lower.js +141 -0
- package/esm/cabi/memory.js +182 -0
- package/esm/cabi/mod.js +22 -0
- package/esm/cabi/store.js +186 -0
- package/esm/cabi/strings.js +336 -0
- package/esm/cabi/trap.js +38 -0
- package/esm/cabi/types.js +264 -0
- package/esm/cabi/values.js +64 -0
- package/esm/cache/core.js +156 -0
- package/esm/cache/dir.js +170 -0
- package/esm/cache/mod.js +4 -0
- package/esm/cache/web.js +136 -0
- package/esm/digest/digest.js +332 -0
- package/esm/digest/mod.js +3 -0
- package/esm/digest/verify.js +129 -0
- package/esm/embedder/casing.js +56 -0
- package/esm/embedder/copy.js +42 -0
- package/esm/embedder/errors.js +26 -0
- package/esm/embedder/imports.js +63 -0
- package/esm/embedder/instantiate.js +978 -0
- package/esm/embedder/mod.js +40 -0
- package/esm/embedder/resources.js +406 -0
- package/esm/embedder/streams.js +770 -0
- package/esm/embedder/values.js +450 -0
- package/esm/embedder/version.js +273 -0
- package/esm/exec/boundary.js +1902 -0
- package/esm/exec/executor.js +1004 -0
- package/esm/exec/host_streams.js +818 -0
- package/esm/exec/mod.js +4 -0
- package/esm/intrinsics/async_builtins.js +510 -0
- package/esm/intrinsics/context.js +90 -0
- package/esm/intrinsics/errors.js +13 -0
- package/esm/intrinsics/fact_calls.js +865 -0
- package/esm/intrinsics/mod.js +564 -0
- package/esm/intrinsics/stream_builtins.js +578 -0
- package/esm/intrinsics/transcode.js +443 -0
- package/esm/jspi/bridge.js +579 -0
- package/esm/jspi/mechanics.js +89 -0
- package/esm/jspi/mod.js +5 -0
- package/esm/jspi/suspending.js +15 -0
- package/esm/jspi/types.js +29 -0
- package/esm/package.json +3 -0
- package/esm/plan/format.js +5 -0
- package/esm/plan/loader.js +657 -0
- package/esm/plan/mod.js +3 -0
- package/esm/shim/mod.js +2 -0
- package/esm/shim/translator.js +94 -0
- package/esm/task/mod.js +483 -0
- package/esm/task/scheduler.js +1028 -0
- package/esm/task/streams.js +786 -0
- package/esm/task/subtask.js +135 -0
- package/esm/task/thread.js +255 -0
- package/esm/task/waitable.js +144 -0
- package/package.json +91 -0
- package/types/cabi/async_values.d.ts +35 -0
- package/types/cabi/bulk_lists.d.ts +18 -0
- package/types/cabi/context.d.ts +59 -0
- package/types/cabi/flatten.d.ts +14 -0
- package/types/cabi/float.d.ts +14 -0
- package/types/cabi/handles.d.ts +70 -0
- package/types/cabi/layout.d.ts +13 -0
- package/types/cabi/lift.d.ts +25 -0
- package/types/cabi/load.d.ts +14 -0
- package/types/cabi/lower.d.ts +10 -0
- package/types/cabi/memory.d.ts +58 -0
- package/types/cabi/mod.d.ts +15 -0
- package/types/cabi/store.d.ts +12 -0
- package/types/cabi/strings.d.ts +23 -0
- package/types/cabi/trap.d.ts +11 -0
- package/types/cabi/types.d.ts +206 -0
- package/types/cabi/values.d.ts +5 -0
- package/types/cache/core.d.ts +97 -0
- package/types/cache/dir.d.ts +6 -0
- package/types/cache/mod.d.ts +3 -0
- package/types/cache/web.d.ts +10 -0
- package/types/digest/digest.d.ts +17 -0
- package/types/digest/mod.d.ts +2 -0
- package/types/digest/verify.d.ts +48 -0
- package/types/embedder/casing.d.ts +40 -0
- package/types/embedder/copy.d.ts +24 -0
- package/types/embedder/errors.d.ts +11 -0
- package/types/embedder/imports.d.ts +47 -0
- package/types/embedder/instantiate.d.ts +88 -0
- package/types/embedder/mod.d.ts +11 -0
- package/types/embedder/resources.d.ts +158 -0
- package/types/embedder/streams.d.ts +202 -0
- package/types/embedder/values.d.ts +70 -0
- package/types/embedder/version.d.ts +85 -0
- package/types/exec/boundary.d.ts +360 -0
- package/types/exec/executor.d.ts +125 -0
- package/types/exec/host_streams.d.ts +165 -0
- package/types/exec/mod.d.ts +3 -0
- package/types/intrinsics/async_builtins.d.ts +69 -0
- package/types/intrinsics/context.d.ts +28 -0
- package/types/intrinsics/errors.d.ts +5 -0
- package/types/intrinsics/fact_calls.d.ts +120 -0
- package/types/intrinsics/mod.d.ts +187 -0
- package/types/intrinsics/stream_builtins.d.ts +113 -0
- package/types/intrinsics/transcode.d.ts +21 -0
- package/types/jspi/bridge.d.ts +227 -0
- package/types/jspi/mechanics.d.ts +50 -0
- package/types/jspi/mod.d.ts +3 -0
- package/types/jspi/suspending.d.ts +1 -0
- package/types/jspi/types.d.ts +26 -0
- package/types/plan/format.d.ts +369 -0
- package/types/plan/loader.d.ts +113 -0
- package/types/plan/mod.d.ts +2 -0
- package/types/shim/mod.d.ts +1 -0
- package/types/shim/translator.d.ts +55 -0
- package/types/task/mod.d.ts +257 -0
- package/types/task/scheduler.d.ts +421 -0
- package/types/task/streams.d.ts +370 -0
- package/types/task/subtask.d.ts +96 -0
- package/types/task/thread.d.ts +73 -0
- package/types/task/waitable.d.ts +67 -0
|
@@ -0,0 +1,1902 @@
|
|
|
1
|
+
// Host-boundary wiring: lifted-export invocation (reference `canon_lift`,
|
|
2
|
+
// sync path) and lowered-import bodies (reference `canon_lower`, sync path),
|
|
3
|
+
// built on the cabi v1 interpreter (runtime/src/cabi/) driven by the plan's
|
|
4
|
+
// canonical options — docs/architecture.md §4.3 items 2 and 5, degenerate sync case.
|
|
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
|
+
import { AssertionError, assert_ } from "../cabi/trap.js";
|
|
7
|
+
import { driveSyncLift, clearResumingThread, EventCode, withActivation, hasRealHostCall, hasResumingThread, dispatchableTail, NeedsJspi, needsJspi, setResumingThread, packSubtaskResult, PendingCapability, notifyInstancePoisoned, realHostCalls, storeQuiescent, Subtask, WaitableSet, SubtaskState, Task, Thread, withPoisonCause, } from "../task/mod.js";
|
|
8
|
+
import { currentTask } from "../task/scheduler.js";
|
|
9
|
+
import { PlanError } from "../plan/loader.js";
|
|
10
|
+
import { blockCurrentActivation, enterWasm, } from "../jspi/mod.js";
|
|
11
|
+
export function newStats() {
|
|
12
|
+
return {
|
|
13
|
+
liftedCalls: 0,
|
|
14
|
+
tasksResolved: 0,
|
|
15
|
+
postReturnsRun: 0,
|
|
16
|
+
loweredCalls: 0,
|
|
17
|
+
enterSyncCalls: 0,
|
|
18
|
+
exitSyncCalls: 0,
|
|
19
|
+
callbackInvocations: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A `MemInst`-shaped view over a `WebAssembly.Memory` that never goes stale:
|
|
24
|
+
* `bytes`/`view` re-derive from `memory.buffer` whenever the buffer identity
|
|
25
|
+
* changes (memory.grow detaches the previous ArrayBuffer — a cached
|
|
26
|
+
* Uint8Array would silently drop writes). The provider indirection also
|
|
27
|
+
* covers plan-order effects: canonical options can reference a memory whose
|
|
28
|
+
* `extract-memory` initializer runs later; accesses before extraction fail
|
|
29
|
+
* with a PlanError.
|
|
30
|
+
*
|
|
31
|
+
* Structurally compatible with cabi's `MemInst` (same public surface).
|
|
32
|
+
*/
|
|
33
|
+
export class LiveMemory {
|
|
34
|
+
addrType = "i32"; // memory64 components: out of M0 scope
|
|
35
|
+
#provider;
|
|
36
|
+
#label;
|
|
37
|
+
#buffer = null;
|
|
38
|
+
#bytes = new Uint8Array(0);
|
|
39
|
+
#view = new DataView(new ArrayBuffer(0));
|
|
40
|
+
constructor(provider, label) {
|
|
41
|
+
this.#provider = provider;
|
|
42
|
+
this.#label = label;
|
|
43
|
+
}
|
|
44
|
+
#memory() {
|
|
45
|
+
const m = this.#provider();
|
|
46
|
+
if (m === undefined) {
|
|
47
|
+
throw new PlanError(`${this.#label} accessed before its extract-memory initializer ran`);
|
|
48
|
+
}
|
|
49
|
+
return m;
|
|
50
|
+
}
|
|
51
|
+
#refresh() {
|
|
52
|
+
const buffer = this.#memory().buffer;
|
|
53
|
+
if (buffer !== this.#buffer) {
|
|
54
|
+
this.#buffer = buffer;
|
|
55
|
+
this.#bytes = new Uint8Array(buffer);
|
|
56
|
+
this.#view = new DataView(buffer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
get bytes() {
|
|
60
|
+
this.#refresh();
|
|
61
|
+
return this.#bytes;
|
|
62
|
+
}
|
|
63
|
+
get view() {
|
|
64
|
+
this.#refresh();
|
|
65
|
+
return this.#view;
|
|
66
|
+
}
|
|
67
|
+
get length() {
|
|
68
|
+
return this.#memory().buffer.byteLength;
|
|
69
|
+
}
|
|
70
|
+
ptrType() {
|
|
71
|
+
return this.addrType;
|
|
72
|
+
}
|
|
73
|
+
ptrSize() {
|
|
74
|
+
return 4;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Compile-time proof that LiveMemory satisfies the MemInst surface.
|
|
78
|
+
const _memInstCheck = new LiveMemory(() => undefined, "check");
|
|
79
|
+
void _memInstCheck;
|
|
80
|
+
function require(resolver, what) {
|
|
81
|
+
if (resolver === null)
|
|
82
|
+
return null;
|
|
83
|
+
const v = resolver();
|
|
84
|
+
if (v === undefined) {
|
|
85
|
+
throw new PlanError(`${what} accessed before its extract initializer ran`);
|
|
86
|
+
}
|
|
87
|
+
return v;
|
|
88
|
+
}
|
|
89
|
+
/** cabi-facing options object (LiftLowerOptions + flatten inputs). */
|
|
90
|
+
export function cabiOptions(opts) {
|
|
91
|
+
return {
|
|
92
|
+
stringEncoding: opts.stringEncoding,
|
|
93
|
+
memory: opts.memory,
|
|
94
|
+
realloc: opts.realloc === null ? null : (o, os, a, n) => {
|
|
95
|
+
const realloc = require(opts.realloc, "realloc");
|
|
96
|
+
const p = callCore(realloc, [o, os, a, n]);
|
|
97
|
+
trapIf(p.length !== 1 || typeof p[0] !== "number", "realloc result");
|
|
98
|
+
return p[0] >>> 0;
|
|
99
|
+
},
|
|
100
|
+
postReturn: null, // post-return handled by the task layer, not cabi
|
|
101
|
+
async_: opts.async,
|
|
102
|
+
// Truthiness only: `flattenFunctype` branches on whether a callback
|
|
103
|
+
// exists (async lifts with a callback return a packed i32; stackful ones
|
|
104
|
+
// return nothing). Passing the resolver rather than `null` is what makes
|
|
105
|
+
// the callback-ABI core type come out right.
|
|
106
|
+
callback: opts.callback === null ? null : opts.callback,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Call a core function, mapping core-wasm exceptions to canonical-ABI traps
|
|
111
|
+
* (reference `call_and_trap_on_throw`). Component traps and internal errors
|
|
112
|
+
* of ours propagate unchanged.
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* Layering rule: a core-wasm trap's message is engine-specific text (V8,
|
|
116
|
+
* SpiderMonkey, JSC each word `unreachable` differently, for instance) and is
|
|
117
|
+
* passed through here UNTOUCHED — it is diagnostics only, "engine-flavored"
|
|
118
|
+
* and not normalized to any particular host's wording. The runtime never
|
|
119
|
+
* emulates another host's (e.g. wasmtime's) message text.
|
|
120
|
+
*
|
|
121
|
+
* Suite-wording normalization (matching the official test suite's
|
|
122
|
+
* `assert_trap` expectations, which are typically worded per wasmtime) lives
|
|
123
|
+
* in the harness instead: see `TRAP_MESSAGE_EQUIVALENTS` in
|
|
124
|
+
* harness/src/runner.ts, which maps engine-specific spellings to the
|
|
125
|
+
* suite-expected forms at comparison time. (The FACT *adapter* traps take a
|
|
126
|
+
* different route entirely — they arrive as numeric codes through the `trap`
|
|
127
|
+
* trampoline and are runtime-authored text, see `FACT_TRAP_MESSAGES` in
|
|
128
|
+
* intrinsics/mod.ts; that table is untouched by this layering rule.)
|
|
129
|
+
*/
|
|
130
|
+
export function callCore(fn, args) {
|
|
131
|
+
let raw;
|
|
132
|
+
try {
|
|
133
|
+
raw = fn(...args);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
throw mapCoreException(e);
|
|
137
|
+
}
|
|
138
|
+
if (raw === undefined)
|
|
139
|
+
return [];
|
|
140
|
+
if (Array.isArray(raw))
|
|
141
|
+
return raw;
|
|
142
|
+
return [raw];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The `call_and_trap_on_throw` translation, factored so BOTH routes a core
|
|
146
|
+
* trap can take reach it:
|
|
147
|
+
*
|
|
148
|
+
* * a synchronous throw out of `fn(...args)` (`callCore` above — the plain
|
|
149
|
+
* path, and jspi pre-suspension);
|
|
150
|
+
* * a **rejection of a `promising` entry's Promise** (jspi pin (e): a trap
|
|
151
|
+
* after a resumption arrives as an ordinary rejection). That rejection
|
|
152
|
+
* carries the raw `WebAssembly.RuntimeError`, and before this helper was
|
|
153
|
+
* applied on the awaited path (`awaitCore` below), a post-suspension
|
|
154
|
+
* guest trap escaped to the embedder as `RuntimeError: unreachable`
|
|
155
|
+
* instead of the wasmtime-worded `Trap` — every deliberate guest trap
|
|
156
|
+
* under detection scored as a harness failure
|
|
157
|
+
* (big-interleaving-test.wast:836's assert_trap "unreachable").
|
|
158
|
+
*/
|
|
159
|
+
function mapCoreException(e) {
|
|
160
|
+
if (e instanceof WebAssembly.RuntimeError) {
|
|
161
|
+
try {
|
|
162
|
+
trap(`guest trapped: ${e.message}`);
|
|
163
|
+
}
|
|
164
|
+
catch (t) {
|
|
165
|
+
return t;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return e;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Normalize raw JS-API core values to cabi's canonical lane representation:
|
|
172
|
+
* i32 lanes as unsigned numbers (the JS API yields signed), i64 lanes as
|
|
173
|
+
* unsigned bigints, floats as numbers.
|
|
174
|
+
*/
|
|
175
|
+
export function normalizeCoreValues(values, lanes, what) {
|
|
176
|
+
if (values.length !== lanes.length) {
|
|
177
|
+
throw new AssertionError(`${what}: expected ${lanes.length} core values, got ${values.length}`);
|
|
178
|
+
}
|
|
179
|
+
return values.map((v, i) => {
|
|
180
|
+
switch (lanes[i]) {
|
|
181
|
+
case "i32":
|
|
182
|
+
assert_(typeof v === "number", `${what}[${i}]: i32 lane`);
|
|
183
|
+
return v >>> 0;
|
|
184
|
+
case "i64":
|
|
185
|
+
assert_(typeof v === "bigint", `${what}[${i}]: i64 lane`);
|
|
186
|
+
return BigInt.asUintN(64, v);
|
|
187
|
+
case "f32":
|
|
188
|
+
case "f64":
|
|
189
|
+
assert_(typeof v === "number", `${what}[${i}]: float lane`);
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** Map a resolved result list to the host-facing return value by arity. */
|
|
195
|
+
function resultsToHost(results) {
|
|
196
|
+
if (results.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (results.length === 1)
|
|
199
|
+
return results[0];
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Driving the scheduler from the host boundary
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
//
|
|
206
|
+
// run_tests.py's `lift_and_run` (line 55) is the reference embedding:
|
|
207
|
+
//
|
|
208
|
+
// ```python
|
|
209
|
+
// func_inst = inst.store.lift(callee, ft, opts, inst)
|
|
210
|
+
// _ = inst.store.invoke(func_inst, on_start, on_resolve)
|
|
211
|
+
// while inst.store.waiting:
|
|
212
|
+
// inst.store.tick()
|
|
213
|
+
// ```
|
|
214
|
+
//
|
|
215
|
+
// i.e. enter the component, then pump the store until nothing is waiting.
|
|
216
|
+
// `drive` below is that loop, with two additions the reference does not need:
|
|
217
|
+
//
|
|
218
|
+
// 1. **A deadlock verdict.** The reference's `while store.waiting` spins
|
|
219
|
+
// forever if no waiting thread is ready, because its host functions run
|
|
220
|
+
// on real OS threads and always eventually make progress. Ours cannot
|
|
221
|
+
// spin: when no thread is ready and no host promise is outstanding, the
|
|
222
|
+
// task can never resolve, which is the same condition `canon_lift`'s
|
|
223
|
+
// sync loop traps on (`trap_if(not candidates)`), so we trap too.
|
|
224
|
+
//
|
|
225
|
+
// 2. **Host promises.** A host import implemented as an `async` JS function
|
|
226
|
+
// resolves its subtask on a *microtask turn*, not on a thread. When the
|
|
227
|
+
// only way forward is such a promise, `drive` returns a Promise and the
|
|
228
|
+
// lifted export's return value becomes a Promise. This needs no JSPI:
|
|
229
|
+
// the guest is stackless (callback ABI), so nothing is suspended mid-wasm
|
|
230
|
+
// — the guest already returned WAIT and the host merely resumes it later.
|
|
231
|
+
//
|
|
232
|
+
// Consequence for callers: a lifted export returns `T` when the whole call
|
|
233
|
+
// completed synchronously, and `Promise<T>` when a host promise was involved.
|
|
234
|
+
// The conformance harness invokes exports synchronously
|
|
235
|
+
// (harness/src/runtime-executor.ts) and the official suite has no
|
|
236
|
+
// promise-returning host imports, so it only ever sees the synchronous shape.
|
|
237
|
+
/** True for thenables, which is what "is this host call asynchronous" means. */
|
|
238
|
+
function isPromiseLike(v) {
|
|
239
|
+
return (typeof v === "object" && v !== null &&
|
|
240
|
+
typeof v.then === "function");
|
|
241
|
+
}
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Handshake probe (M2 phase 3l)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
//
|
|
246
|
+
// Env-gated tracing of the drive loops. This exists because site 1 is the
|
|
247
|
+
// first *lit* suspension site, so the `SuspensionPoint` <-> `Store.tick` <->
|
|
248
|
+
// `driveAsync` handshake had never executed before it; a pure-microtask stall
|
|
249
|
+
// there is invisible from the outside (no trap, no rejection -- just an await
|
|
250
|
+
// nothing settles). Off unless POLYENGINE_DRIVE_TRACE is set, and the getter is read
|
|
251
|
+
// once at module load so normal runs pay a boolean test.
|
|
252
|
+
const DRIVE_TRACE = (() => {
|
|
253
|
+
try {
|
|
254
|
+
return Deno.env.get("POLYENGINE_DRIVE_TRACE") === "1";
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
let traceTurn = 0;
|
|
261
|
+
function describeWaiter(t) {
|
|
262
|
+
const w = t;
|
|
263
|
+
const kind = w?.constructor?.name ?? "?";
|
|
264
|
+
let verdict = "?";
|
|
265
|
+
try {
|
|
266
|
+
verdict = w.ready?.() ? "READY" : (w.readyFunc === null ? "explicit" : "not-ready");
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
verdict = `threw:${e}`;
|
|
270
|
+
}
|
|
271
|
+
return `${kind}[${verdict}]`;
|
|
272
|
+
}
|
|
273
|
+
function traceDrive(loop, store, done, branch) {
|
|
274
|
+
if (!DRIVE_TRACE)
|
|
275
|
+
return;
|
|
276
|
+
let doneVerdict = "?";
|
|
277
|
+
try {
|
|
278
|
+
doneVerdict = String(done());
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
doneVerdict = `threw:${e}`;
|
|
282
|
+
}
|
|
283
|
+
const waiters = store.waiting.map(describeWaiter).join(",");
|
|
284
|
+
const awaiters = [...store.awaiting].map((t) => {
|
|
285
|
+
const a = t;
|
|
286
|
+
return `${a?.constructor?.name ?? "?"}`;
|
|
287
|
+
}).join(",");
|
|
288
|
+
console.error(`[drive #${traceTurn++}] ${loop} branch=${branch} ` +
|
|
289
|
+
`ready=${store.readyCandidates().length} ` +
|
|
290
|
+
`waiting=${store.waiting.length}{${waiters}} ` +
|
|
291
|
+
`awaiting=${store.awaiting.size} ` +
|
|
292
|
+
`hostCalls=${store.pendingHostCalls.size} ` +
|
|
293
|
+
`awaiters={${awaiters}} claim=${hasResumingThread()} done=${doneVerdict}`);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pump `store` until `done()` holds. Returns `undefined` if that was achieved
|
|
297
|
+
* synchronously, or a Promise that settles when it has been.
|
|
298
|
+
*/
|
|
299
|
+
function drive(store, done, what) {
|
|
300
|
+
for (;;) {
|
|
301
|
+
traceDrive("drive", store, done, "top");
|
|
302
|
+
// The synchronous drain must not run while a thread is parked on a
|
|
303
|
+
// Promise: `tick` cannot see those, so a thread that re-parks READY on
|
|
304
|
+
// every resume (a callback-ABI guest spinning YIELD) would hold this
|
|
305
|
+
// loop forever while the promise-parked thread that would stop the spin
|
|
306
|
+
// never gets serviced (drop-subtask.wast:139 under detection: the
|
|
307
|
+
// Looper spins YIELD until `return` runs, and `return`'s caller sat
|
|
308
|
+
// parked on its activation promise). In jspi mode the lifted export
|
|
309
|
+
// returns a Promise anyway, so handing off to `driveAsync` — whose
|
|
310
|
+
// drain interleaves fairly — costs nothing; in plain mode `awaiting`
|
|
311
|
+
// is always empty and this loop is bit-for-bit what it was.
|
|
312
|
+
while (store.awaiting.size === 0 && store.tick()) {
|
|
313
|
+
traceDrive("drive", store, done, "ticked");
|
|
314
|
+
if (store.hostFailure !== undefined)
|
|
315
|
+
throw takeHostFailure(store);
|
|
316
|
+
}
|
|
317
|
+
if (store.hostFailure !== undefined)
|
|
318
|
+
throw takeHostFailure(store);
|
|
319
|
+
if (done()) {
|
|
320
|
+
traceDrive("drive", store, done, "EXIT-done");
|
|
321
|
+
// Fully-synchronous completion: no `driveAsync` ran, so its exit hook
|
|
322
|
+
// will not fire — arm the settlement pump here for any host calls the
|
|
323
|
+
// guest registered fire-and-forget during this drive.
|
|
324
|
+
ensureSettlementPump(store);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// A thread parked on a Promise (jspi) can only progress after a microtask
|
|
328
|
+
// turn, exactly like an outstanding host call. So can an outstanding
|
|
329
|
+
// ambient claim: a suspension has been settled and its activation has not
|
|
330
|
+
// run yet (see `Store.tick`).
|
|
331
|
+
if (store.awaiting.size > 0 || hasResumingThread()) {
|
|
332
|
+
traceDrive("drive", store, done, "->async(awaiting/claim)");
|
|
333
|
+
return driveAsync(store, done, what);
|
|
334
|
+
}
|
|
335
|
+
if (store.pendingHostCalls.size === 0) {
|
|
336
|
+
traceDrive("drive", store, done, "DEADLOCK-TRAP");
|
|
337
|
+
trapIf(true, `wasm trap: deadlock detected: event loop cannot make further ` +
|
|
338
|
+
`progress (${what}: no thread is ready and no host call is ` +
|
|
339
|
+
`outstanding)`);
|
|
340
|
+
}
|
|
341
|
+
traceDrive("drive", store, done, "->async(hostcalls)");
|
|
342
|
+
return driveAsync(store, done, what);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Tagged promises, memoized by the *promise* (not the thread) so re-racing on
|
|
347
|
+
* every turn does not attach a fresh continuation to the same promise, and so
|
|
348
|
+
* a thread that parks again later can never pick up a stale tag.
|
|
349
|
+
*/
|
|
350
|
+
const taggedAwaits = new WeakMap();
|
|
351
|
+
function tagAwait(t) {
|
|
352
|
+
const p = t.awaiting;
|
|
353
|
+
let tag = taggedAwaits.get(p);
|
|
354
|
+
if (tag === undefined) {
|
|
355
|
+
tag = p.then((value) => ({ t, p, value, failure: undefined }), (e) => ({ t, p, value: undefined, failure: { error: e } }));
|
|
356
|
+
taggedAwaits.set(p, tag);
|
|
357
|
+
}
|
|
358
|
+
return tag;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* THE asynchronous driving loop, exported for the one other driver in the
|
|
362
|
+
* runtime: `HostActivity` in exec/host_streams.ts, which must pump the store
|
|
363
|
+
* BETWEEN export calls (when no lifted call is in flight) with exactly these
|
|
364
|
+
* semantics — service settled tails, tick to quiescence, then await the race
|
|
365
|
+
* of every outstanding promise (parked activations AND `pendingHostCalls`),
|
|
366
|
+
* repeat. Reimplementing it there diverged: that copy only drained
|
|
367
|
+
* `store.awaiting` and never awaited `pendingHostCalls`, so a guest parked on
|
|
368
|
+
* a Promise-returning host import was never resumed and the host's read of
|
|
369
|
+
* the stream it was feeding hung (C0 finding R-1).
|
|
370
|
+
*
|
|
371
|
+
* Callers that must not hit the deadlock traps below (the host pump: an
|
|
372
|
+
* embedder that never does its half is documented to hang, not trap) can
|
|
373
|
+
* exclude them entirely — BOTH trap sites require
|
|
374
|
+
* `store.pendingHostCalls.size === 0`, and both are reached only through the
|
|
375
|
+
* synchronous fall-through from `done()`, so a `done` that returns true
|
|
376
|
+
* whenever `pendingHostCalls` is empty provably never traps.
|
|
377
|
+
*/
|
|
378
|
+
export async function driveStoreAsync(store, done, what) {
|
|
379
|
+
return await driveAsync(store, done, what);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* How many `driveAsync` loops are live on a store.
|
|
383
|
+
*
|
|
384
|
+
* THE INVARIANT is not "only one loop may ever run" — concurrent export calls
|
|
385
|
+
* have always produced concurrent loops, and the host-stream pump's stand-down
|
|
386
|
+
* below is cooperative, so a *bounded overlap window* remains by construction
|
|
387
|
+
* (an export call can start while the pump is parked mid-`await`; the pump
|
|
388
|
+
* only notices at its next `done()` evaluation). The invariant is:
|
|
389
|
+
*
|
|
390
|
+
* **no activation is resumed twice for one settlement, and no activation is
|
|
391
|
+
* resumed with a value from a settlement it has already consumed.**
|
|
392
|
+
*
|
|
393
|
+
* Overlap is benign for that invariant because of three mechanisms, in
|
|
394
|
+
* decreasing order of how much weight they carry:
|
|
395
|
+
*
|
|
396
|
+
* (a) LOAD-BEARING — `resumeWith` synchronously deletes the thread from
|
|
397
|
+
* `store.awaiting` (task/thread.ts `Thread.resumeWith`), and every
|
|
398
|
+
* resumption site here is guarded by an `store.awaiting.has(...)` test
|
|
399
|
+
* evaluated synchronously immediately before the call. The loser of a
|
|
400
|
+
* race therefore sees the deletion. The ordering that makes this
|
|
401
|
+
* airtight is microtask FIFO: both loops' race continuations were
|
|
402
|
+
* queued when the *tag* settled, which is strictly before the winner's
|
|
403
|
+
* `resumeWith` can run and therefore strictly before any re-park the
|
|
404
|
+
* resumed activation performs can queue a new settlement. So the loser
|
|
405
|
+
* observes "deleted", never a re-park that restored membership.
|
|
406
|
+
* (b) `tagAwait` memoizes per PROMISE (not per thread), so overlapping loops
|
|
407
|
+
* racing the same parked thread await the *same* tag object and see one
|
|
408
|
+
* settlement, not two independent ones. This is what makes (a)'s
|
|
409
|
+
* "queued at tag settlement" premise hold across loops.
|
|
410
|
+
* (c) The ambient resume claim (`setResumingThread`) serializes the claim
|
|
411
|
+
* path: a second claimant while one is live is asserted against, and
|
|
412
|
+
* every loop yields at its top while `hasResumingThread()`.
|
|
413
|
+
*
|
|
414
|
+
* (a) is the guarantee; (b) and (c) are what make (a) apply across loops
|
|
415
|
+
* rather than only within one. The one corner (a) does NOT cover — a thread
|
|
416
|
+
* resumed by the *other* loop's `tick`, re-parked on a NEW promise, whose OLD
|
|
417
|
+
* promise then settles late — is closed separately at the resumption site
|
|
418
|
+
* below by comparing promise identity, not just membership.
|
|
419
|
+
*
|
|
420
|
+
* What overlap is NOT benign for is throughput and blame: two loops ticking
|
|
421
|
+
* the same store interleave their `serviceSettled`/`tick` phases, and the
|
|
422
|
+
* host-stream pump was observed to trip `Trap: table entry empty` out of
|
|
423
|
+
* `runCallbackLoop` when it drove unconditionally alongside an export call's
|
|
424
|
+
* loop. Export calls own their loops and cannot yield to anyone; the pumps
|
|
425
|
+
* are *fallback* drivers — the host-activity pump for embedder operations
|
|
426
|
+
* that land BETWEEN export calls, the settlement pump (below) for host-call
|
|
427
|
+
* settlements that land between them — so they are the side that stands
|
|
428
|
+
* down, using the two accessors below, narrowing the window to the
|
|
429
|
+
* cooperative residue described above. When an export call's loop is live it
|
|
430
|
+
* already races `pendingHostCalls` and `store.awaiting`, i.e. it pumps host
|
|
431
|
+
* activity on the embedder's behalf.
|
|
432
|
+
*/
|
|
433
|
+
const driverDepth = new WeakMap();
|
|
434
|
+
const driverIdle = new WeakMap();
|
|
435
|
+
export function storeDriverDepth(store) {
|
|
436
|
+
return driverDepth.get(store) ?? 0;
|
|
437
|
+
}
|
|
438
|
+
/** Resolves once no `driveAsync` loop is live on `store`. */
|
|
439
|
+
export function whenStoreDriverIdle(store) {
|
|
440
|
+
if (storeDriverDepth(store) === 0)
|
|
441
|
+
return Promise.resolve();
|
|
442
|
+
let w = driverIdle.get(store);
|
|
443
|
+
if (w === undefined) {
|
|
444
|
+
let r;
|
|
445
|
+
const p = new Promise((res) => (r = res));
|
|
446
|
+
w = { p, r };
|
|
447
|
+
driverIdle.set(store, w);
|
|
448
|
+
}
|
|
449
|
+
return w.p;
|
|
450
|
+
}
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// The settlement pump: liveness between export calls
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
//
|
|
455
|
+
// A host-import promise that settles while a driver is live is serviced by
|
|
456
|
+
// that driver (`driveAsync` races `store.pendingHostCalls`). One that settles
|
|
457
|
+
// while NO driver is live only mutates scheduler state — the registration
|
|
458
|
+
// site's continuation delivers results and readies threads, but nothing calls
|
|
459
|
+
// `serviceSettled`/`tick`, so the work sits queued until the next export call
|
|
460
|
+
// or host stream/future operation happens to drive the store. For a guest
|
|
461
|
+
// with genuinely background work — the canonical shape is a task parked WAIT
|
|
462
|
+
// on a waitable set whose pending host call is a clock (a componentize-go
|
|
463
|
+
// keep-alive ticker, a wasi:clocks `wait-for`) — that turned "the host will
|
|
464
|
+
// wake me" into "the embedder's next unrelated call will wake me": a liveness
|
|
465
|
+
// gap, not a policy (wasmtime's event loop delivers such wakeups whenever the
|
|
466
|
+
// embedder dwells in `run_concurrent`; on a JS host the event loop is always
|
|
467
|
+
// dwelling).
|
|
468
|
+
//
|
|
469
|
+
// The settlement pump closes the gap: whenever a driver exits leaving real
|
|
470
|
+
// host calls outstanding (`hasRealHostCall` — activity arms excluded, they
|
|
471
|
+
// mean "the embedder may still act", not "the host owes an event"), a
|
|
472
|
+
// detached keeper parks on `Promise.race` of those calls and, when one
|
|
473
|
+
// settles, drives the store to quiescence with the same loop and the same
|
|
474
|
+
// cooperative discipline as the host-activity pump above it in the driver
|
|
475
|
+
// hierarchy:
|
|
476
|
+
//
|
|
477
|
+
// * it stands down whenever an export call's loop is live
|
|
478
|
+
// (`storeDriverDepth` / `whenStoreDriverIdle`, plus the `> 1` clause in
|
|
479
|
+
// its `done`, exactly as `HostActivity.#pumpAsync`);
|
|
480
|
+
// * its `done` returns true whenever `pendingHostCalls` is empty, which is
|
|
481
|
+
// the precondition of BOTH deadlock traps in `driveAsync` — the pump can
|
|
482
|
+
// therefore never convert the documented embedder-never-acts hang into a
|
|
483
|
+
// trap (see the `driveStoreAsync` note above);
|
|
484
|
+
// * failures park on `store.hostFailure` for the next embedder call to
|
|
485
|
+
// surface, the channel every between-calls driver already uses.
|
|
486
|
+
//
|
|
487
|
+
// Every real `pendingHostCalls` entry is born during guest execution, i.e.
|
|
488
|
+
// inside some driver, so arming at driver exit (`driveAsync`'s finally and
|
|
489
|
+
// `drive`'s synchronous completion) observes every registration. A
|
|
490
|
+
// HOST-initiated resource dtor (embedder `drop()` between calls) is no
|
|
491
|
+
// exception since #160: it is a lifted call like any other, so it brings its
|
|
492
|
+
// own driver, and any host call its activation makes is registered inside
|
|
493
|
+
// that driver.
|
|
494
|
+
//
|
|
495
|
+
// STALE SNAPSHOTS: the keeper races the real host calls it saw when it
|
|
496
|
+
// parked. A drive it performs can register NEW calls (the keep-alive ticker
|
|
497
|
+
// re-arming is the routine case), and `ensureSettlementPump` may be called
|
|
498
|
+
// while the keeper is already parked. Both are handled by a nudge promise
|
|
499
|
+
// raced alongside the snapshot: arming an already-live pump fires the nudge,
|
|
500
|
+
// the keeper wakes, re-snapshots, and re-parks.
|
|
501
|
+
const settlementPumps = new WeakSet();
|
|
502
|
+
const settlementNudges = new WeakMap();
|
|
503
|
+
function armSettlementNudge(store) {
|
|
504
|
+
let n = settlementNudges.get(store);
|
|
505
|
+
if (n === undefined) {
|
|
506
|
+
let r;
|
|
507
|
+
const p = new Promise((res) => (r = res));
|
|
508
|
+
n = { p, r };
|
|
509
|
+
settlementNudges.set(store, n);
|
|
510
|
+
}
|
|
511
|
+
return n.p;
|
|
512
|
+
}
|
|
513
|
+
function fireSettlementNudge(store) {
|
|
514
|
+
const n = settlementNudges.get(store);
|
|
515
|
+
if (n !== undefined) {
|
|
516
|
+
settlementNudges.delete(store);
|
|
517
|
+
n.r();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Ensure a settlement pump is watching `store`'s real outstanding host calls.
|
|
522
|
+
* Idempotent and cheap; called at every driver exit. Never throws.
|
|
523
|
+
*/
|
|
524
|
+
export function ensureSettlementPump(store) {
|
|
525
|
+
if (settlementPumps.has(store)) {
|
|
526
|
+
// Already parked (or driving): wake it so it re-snapshots the race —
|
|
527
|
+
// this call may be reporting host calls registered after it parked.
|
|
528
|
+
fireSettlementNudge(store);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (store.hostFailure !== undefined)
|
|
532
|
+
return;
|
|
533
|
+
if (!hasRealHostCall(store))
|
|
534
|
+
return;
|
|
535
|
+
settlementPumps.add(store);
|
|
536
|
+
void settlementPumpLoop(store);
|
|
537
|
+
}
|
|
538
|
+
async function settlementPumpLoop(store) {
|
|
539
|
+
let failed = false;
|
|
540
|
+
try {
|
|
541
|
+
for (;;) {
|
|
542
|
+
// Stand down while any driver is live: it races `pendingHostCalls`
|
|
543
|
+
// itself and services settlements on the guest's behalf.
|
|
544
|
+
while (storeDriverDepth(store) > 0) {
|
|
545
|
+
await whenStoreDriverIdle(store);
|
|
546
|
+
}
|
|
547
|
+
// A parked failure belongs to the next embedder call (the only place
|
|
548
|
+
// it can surface); driving into it here would just consume and re-park
|
|
549
|
+
// it in a loop.
|
|
550
|
+
if (store.hostFailure !== undefined)
|
|
551
|
+
return;
|
|
552
|
+
const real = realHostCalls(store);
|
|
553
|
+
if (real.length === 0)
|
|
554
|
+
return;
|
|
555
|
+
const nudge = armSettlementNudge(store);
|
|
556
|
+
// Rejections are not this pump's to report: the registration site's
|
|
557
|
+
// own continuation parks them on `store.hostFailure`.
|
|
558
|
+
await Promise.race([
|
|
559
|
+
...real.map((p) => p.then(() => { }, () => { })),
|
|
560
|
+
nudge,
|
|
561
|
+
]);
|
|
562
|
+
if (storeDriverDepth(store) > 0)
|
|
563
|
+
continue;
|
|
564
|
+
// Drive unconditionally after a wake: `storeQuiescent` cannot see a
|
|
565
|
+
// READY waiting thread (the usual product of a settlement — the
|
|
566
|
+
// continuation readied the guest and deleted its own host call), so
|
|
567
|
+
// gating the drive on it skips exactly the work this pump exists to
|
|
568
|
+
// do. `driveAsync` drains ready threads before consulting `done`, and
|
|
569
|
+
// a vacuous round exits on its first `done` evaluation.
|
|
570
|
+
await driveStoreAsync(store,
|
|
571
|
+
// Quiescence, not completion — and the same three exit clauses as
|
|
572
|
+
// the host-activity pump: nothing only an event-loop turn could
|
|
573
|
+
// advance; `pendingHostCalls` empty (the deadlock traps'
|
|
574
|
+
// precondition, so this pump provably never traps); another driver
|
|
575
|
+
// appeared (ours is the 1).
|
|
576
|
+
() => store.pendingHostCalls.size === 0 ||
|
|
577
|
+
storeQuiescent(store) ||
|
|
578
|
+
storeDriverDepth(store) > 1, "settlement pump");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
failed = true;
|
|
583
|
+
store.hostFailure ??= e;
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
settlementPumps.delete(store);
|
|
587
|
+
// Close the exit race: an `ensureSettlementPump` that saw us live and
|
|
588
|
+
// fired the nudge after our last snapshot check must not be lost.
|
|
589
|
+
if (!failed && store.hostFailure === undefined &&
|
|
590
|
+
storeDriverDepth(store) === 0 && hasRealHostCall(store)) {
|
|
591
|
+
ensureSettlementPump(store);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function driveAsync(store, done, what) {
|
|
596
|
+
driverDepth.set(store, storeDriverDepth(store) + 1);
|
|
597
|
+
try {
|
|
598
|
+
let claimHops = 0;
|
|
599
|
+
for (;;) {
|
|
600
|
+
traceDrive("driveAsync", store, done, "top");
|
|
601
|
+
// FIRST: service every settled-but-unserviced activation tail, in settle
|
|
602
|
+
// order (`Store.settled` — armed eagerly at park time). A settled
|
|
603
|
+
// `awaitValue` is the rest of an activation that already finished its
|
|
604
|
+
// wasm; the reference runs that bookkeeping atomically inside
|
|
605
|
+
// `Thread.resume`, so nothing may be scheduled past it (`Store.tick`
|
|
606
|
+
// refuses while the queue is non-empty). Servicing after ticking let a
|
|
607
|
+
// freshly-resumed caller race into an entry gate while a finished
|
|
608
|
+
// callee's body had yet to release the exclusive slot — cancellable.wast
|
|
609
|
+
// then reported STARTING for an entry the reference admits.
|
|
610
|
+
store.serviceSettled();
|
|
611
|
+
if (store.hostFailure !== undefined)
|
|
612
|
+
throw takeHostFailure(store);
|
|
613
|
+
// A live claim is an engine-driven resumption in flight: its activation
|
|
614
|
+
// has not yet parked again or finished. It will die on its own — parking
|
|
615
|
+
// consumes it (`blockCurrentActivation`), finishing releases it
|
|
616
|
+
// (`Store.noteAwaiting`'s settle continuation) — so yield microtasks
|
|
617
|
+
// until it does. The driver must NOT blanket-clear here: the claim may
|
|
618
|
+
// have been taken by a guest built-in settling another activation's
|
|
619
|
+
// suspension (`subtask.cancel` delivering a cancellation), and clearing
|
|
620
|
+
// it before that activation runs re-opens the mis-attribution window the
|
|
621
|
+
// claim exists to close.
|
|
622
|
+
if (hasResumingThread()) {
|
|
623
|
+
traceDrive("driveAsync", store, done, "yield-claim");
|
|
624
|
+
// Bounded: a claim that never dies is an internal bug (every path out
|
|
625
|
+
// of a resumed activation releases it — park, finish, trap), and a
|
|
626
|
+
// pure-microtask wait would otherwise starve the event loop and every
|
|
627
|
+
// stall timer with it. Interleave macrotask hops so timers stay alive,
|
|
628
|
+
// and fail loudly rather than spin forever.
|
|
629
|
+
claimHops++;
|
|
630
|
+
assert_(claimHops < 10_000, "driveAsync: a resumed-activation claim was never released " +
|
|
631
|
+
"(the activation neither parked, finished, nor trapped)");
|
|
632
|
+
if (claimHops % 100 === 0) {
|
|
633
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
await Promise.resolve();
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
claimHops = 0;
|
|
641
|
+
while (store.tick()) {
|
|
642
|
+
if (store.hostFailure !== undefined)
|
|
643
|
+
throw takeHostFailure(store);
|
|
644
|
+
// FAIRNESS between tick-able threads and promise-parked ones. A thread
|
|
645
|
+
// that is READY again on every resume (the callback-ABI YIELD spin)
|
|
646
|
+
// would otherwise monopolize this drain while a parked thread's
|
|
647
|
+
// settled promise waits (the starvation that hung
|
|
648
|
+
// drop-subtask.wast:139), and the engine's own continuations (jspi
|
|
649
|
+
// pin (j)) only ever land on microtask turns. One hop per tick; bail
|
|
650
|
+
// to the top the moment an activation tail lands.
|
|
651
|
+
if (store.awaiting.size > 0) {
|
|
652
|
+
await Promise.resolve();
|
|
653
|
+
if (store.hasServiceableSettled())
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (store.hostFailure !== undefined)
|
|
658
|
+
throw takeHostFailure(store);
|
|
659
|
+
if (done()) {
|
|
660
|
+
traceDrive("driveAsync", store, done, "EXIT-done");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
// Only a SERVICEABLE tail is a reason to loop again: a queue holding
|
|
664
|
+
// only tails DEFERRED on a non-enterable instance (issue #156) would
|
|
665
|
+
// spin this loop hot — nothing in the cycle awaits.
|
|
666
|
+
if (store.hasServiceableSettled() || hasResumingThread()) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
// Service promise-parked threads (jspi).
|
|
670
|
+
//
|
|
671
|
+
// This must NOT block on one chosen thread's promise. A thread parked on a
|
|
672
|
+
// promising-wrapped nested activation only settles once that activation's
|
|
673
|
+
// own suspension points have been resumed -- and resuming those is
|
|
674
|
+
// `Store.tick`'s job, i.e. *this loop's* job. Awaiting a single promise
|
|
675
|
+
// therefore stops the scheduler while waiting for something that needs the
|
|
676
|
+
// scheduler: a pure-microtask stall with no trap and no rejection.
|
|
677
|
+
// Observed on `async/async-calls-sync.wast` the moment site 1 became the
|
|
678
|
+
// first lit suspension site (M2 phase 3l): turn N serviced a promise that
|
|
679
|
+
// never settled while three other parked threads and three ready-able
|
|
680
|
+
// suspension points went unexamined.
|
|
681
|
+
//
|
|
682
|
+
// So: race every outstanding promise (parked threads AND host calls) and
|
|
683
|
+
// service whichever settles first, re-ticking each turn. The claim is
|
|
684
|
+
// taken in the tagged continuation -- as close to settlement as we can get
|
|
685
|
+
// -- so pin (i)'s window (engine-driven wasm resumption running built-ins
|
|
686
|
+
// before our continuation) is still covered for the thread that actually
|
|
687
|
+
// resumed, without falsely claiming the ambient for threads that did not.
|
|
688
|
+
if (store.awaiting.size > 0) {
|
|
689
|
+
// Is this actually progress, or a deadlock wearing its clothes?
|
|
690
|
+
//
|
|
691
|
+
// Everything in `store.awaiting` is an INTERNAL promise: a
|
|
692
|
+
// promising-wrapped wasm activation. Such a promise settles either on
|
|
693
|
+
// its own (the activation ran to completion -- which happens within one
|
|
694
|
+
// macrotask turn, since the work is already done and only the microtask
|
|
695
|
+
// hop remains) or because WE resume a suspension point it is waiting
|
|
696
|
+
// behind. If no thread is ready, no host call is outstanding, and a full
|
|
697
|
+
// macrotask turn passes with nothing settling, then nobody can move: the
|
|
698
|
+
// awaited promises need us and we need them. That is the deadlock trap
|
|
699
|
+
// (definitions.py `canon_lift`'s empty-candidate-set `trap_if`), and
|
|
700
|
+
// without this check it presents as a silent stall instead -- which is
|
|
701
|
+
// exactly what `tests/jspi/deadlock_test.ts` caught the moment site 2
|
|
702
|
+
// was lit.
|
|
703
|
+
if (store.pendingHostCalls.size === 0 && !hasResumingThread()) {
|
|
704
|
+
traceDrive("driveAsync", store, done, "deadlock-probe");
|
|
705
|
+
// Exclude threads whose settle is already QUEUED in `store.settled`
|
|
706
|
+
// (issue #156): their promise has settled, so racing them wins
|
|
707
|
+
// instantly off the memoized `tagAwait` tag, forever, in an unbounded
|
|
708
|
+
// microtask chain — the tail is `serviceSettled`'s to run.
|
|
709
|
+
const queued = new Set(store.settled.map((s) => s.t));
|
|
710
|
+
const parked = [...store.awaiting].filter((t) => !queued.has(t));
|
|
711
|
+
const progressed = await Promise.race([
|
|
712
|
+
...parked.map((t) => tagAwait(t).then(() => true)),
|
|
713
|
+
new Promise((r) => setTimeout(() => r(false), 0)),
|
|
714
|
+
]);
|
|
715
|
+
traceDrive("driveAsync", store, done, `deadlock-probe:progressed=${progressed}`);
|
|
716
|
+
if (!progressed) {
|
|
717
|
+
// The race covered a SNAPSHOT of the awaiting set. A thread that
|
|
718
|
+
// parked during the macrotask turn (a promising callee's body
|
|
719
|
+
// yielding its awaitValue mid-hop — jspi pin (j) makes this
|
|
720
|
+
// routine) was not raced, and its promise may already be settled;
|
|
721
|
+
// trapping now would declare a deadlock one iteration before the
|
|
722
|
+
// loop would have serviced it. Membership change ⇒ re-probe.
|
|
723
|
+
//
|
|
724
|
+
// `fresh` gets the SAME queued-entry filter `parked` got (issue
|
|
725
|
+
// #156), against a RECOMPUTED queued set — the settled queue can
|
|
726
|
+
// change across the probe's await. Comparing a filtered snapshot
|
|
727
|
+
// against an unfiltered one would read "changed" on every turn in
|
|
728
|
+
// the all-deferred wedge state, so the verdict below could never
|
|
729
|
+
// be reached and the wedge would present as a silent
|
|
730
|
+
// macrotask-paced busy idle instead of a trap.
|
|
731
|
+
const freshQueued = new Set(store.settled.map((s) => s.t));
|
|
732
|
+
const fresh = [...store.awaiting].filter((t) => !freshQueued.has(t));
|
|
733
|
+
const changed = fresh.length !== parked.length ||
|
|
734
|
+
fresh.some((t, i) => t !== parked[i]);
|
|
735
|
+
if (changed)
|
|
736
|
+
continue;
|
|
737
|
+
// The probe's precondition can also expire WITHOUT the awaiting
|
|
738
|
+
// set changing: the same activation resumes off an engine
|
|
739
|
+
// continuation chunk during the probe's macrotask turn (jspi
|
|
740
|
+
// pin (j) — a sync-completing Suspending import still defers its
|
|
741
|
+
// continuation), runs, and re-parks through the A1 arm, which
|
|
742
|
+
// registers a fresh `pendingHostCalls` entry. The activation
|
|
743
|
+
// promise never settled and `awaiting` membership is unchanged,
|
|
744
|
+
// but the park is externally wakeable now — the verdict's own
|
|
745
|
+
// precondition (`pendingHostCalls.size === 0`) no longer holds.
|
|
746
|
+
// Observed on wasi-shims' A5 poll (sync fast path): probe sampled
|
|
747
|
+
// hostCalls=0 between a settled park and the next one, then
|
|
748
|
+
// trapped a live workload with hostCalls=1. Re-check ⇒ re-probe.
|
|
749
|
+
// Likewise a SERVICEABLE settled entry (issue #156): dispatching
|
|
750
|
+
// it is progress, so this is not a deadlock verdict — re-probe.
|
|
751
|
+
// A deferred-only queue deliberately does NOT re-probe: nothing
|
|
752
|
+
// can dispatch it while the lock is held, and if no host call is
|
|
753
|
+
// outstanding nothing will ever release that lock, so it falls
|
|
754
|
+
// THROUGH to the verdict below — the same loud-wedge treatment the
|
|
755
|
+
// servicing race's own all-deferred fallthrough gets. Per the #156
|
|
756
|
+
// analysis that state is unreachable (a lock spanning this loop's
|
|
757
|
+
// await always has a `pendingHostCalls` entry, which fails this
|
|
758
|
+
// probe's precondition); keeping it loud is what makes it an
|
|
759
|
+
// internal-wedge detector rather than dead code.
|
|
760
|
+
if (store.pendingHostCalls.size > 0 || hasResumingThread() ||
|
|
761
|
+
store.hasServiceableSettled()) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (store.readyCandidates().length === 0) {
|
|
765
|
+
trapIf(true, `wasm trap: deadlock detected: event loop cannot make ` +
|
|
766
|
+
`further progress (${what}: every suspended activation is ` +
|
|
767
|
+
`waiting on a suspension only this scheduler could resume, ` +
|
|
768
|
+
`and none is ready)`);
|
|
769
|
+
}
|
|
770
|
+
// No promise settled, but a thread became READY while we waited --
|
|
771
|
+
// typically a suspension point whose `readyFunc` turned true because
|
|
772
|
+
// another activation ran during the macrotask turn. The way forward
|
|
773
|
+
// is `Store.tick`, not a promise: go back to the top and resume it.
|
|
774
|
+
// Falling through to the servicing block instead would await
|
|
775
|
+
// promises that nothing will settle while a runnable thread sits
|
|
776
|
+
// there -- the `async/sync-barges-in.wast` stall exactly.
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
// Progress IS possible: fall through to the normal servicing below,
|
|
780
|
+
// which resumes the settled thread. Returning to the top instead would
|
|
781
|
+
// spin -- the memoized tag is already settled, so the race would win
|
|
782
|
+
// instantly, forever, without anyone being resumed.
|
|
783
|
+
}
|
|
784
|
+
// Re-check membership: the deadlock probe above AWAITS, and everything
|
|
785
|
+
// below reads `[...store.awaiting][0]` as if the set were still
|
|
786
|
+
// non-empty. A thread resumed during the probe (its settle continuation
|
|
787
|
+
// runs `resumeWith`, which deletes it) can empty the set, and the
|
|
788
|
+
// snapshot's `parked[0]` is then `undefined` — the exact check-then-act
|
|
789
|
+
// shape that made the host pump's copy of this loop throw
|
|
790
|
+
// `TypeError: ... (reading 'awaiting')` into `store.hostFailure`, where
|
|
791
|
+
// it poisoned a later unrelated call (C0 finding R-2). Nothing to
|
|
792
|
+
// service ⇒ go back to the top and re-evaluate `done`.
|
|
793
|
+
// Same re-check for the settled queue, and for the same reason: the
|
|
794
|
+
// probe's macrotask turn can land a fresh, SERVICEABLE activation tail
|
|
795
|
+
// (that is exactly what "progress IS possible" above usually means).
|
|
796
|
+
// The queue owns those threads — the race below deliberately excludes
|
|
797
|
+
// them (issue #156) — so the way forward is the top of the loop, where
|
|
798
|
+
// `serviceSettled` dispatches them. Without this, filtering the
|
|
799
|
+
// just-settled thread out of the race left the loop awaiting promises
|
|
800
|
+
// that only its dispatch could settle (observed: tests/jspi/
|
|
801
|
+
// handshake_test.ts stalled, then tripped the claim assert).
|
|
802
|
+
if (store.awaiting.size === 0 || store.hasServiceableSettled())
|
|
803
|
+
continue;
|
|
804
|
+
// Claim the ambient for ONE parked thread and await its promise -- as
|
|
805
|
+
// before, so pin (i)'s window is covered exactly as it was -- but race
|
|
806
|
+
// that promise against every other outstanding promise so this loop can
|
|
807
|
+
// never be held hostage by it. The claimed thread's promise may only be
|
|
808
|
+
// settleable by further scheduler progress (a promising-wrapped nested
|
|
809
|
+
// activation whose own suspension points this loop must still resume);
|
|
810
|
+
// blocking on it alone is the pure-microtask stall of M2 phase 3l.
|
|
811
|
+
// Same exclusion as the probe (issue #156): a thread whose tail is
|
|
812
|
+
// already queued in `store.settled` must not be raced — its tag is
|
|
813
|
+
// settled, so it re-wins instantly and livelocks the event loop,
|
|
814
|
+
// starving the very host-call settle that would release the lock.
|
|
815
|
+
const queued = new Set(store.settled.map((s) => s.t));
|
|
816
|
+
const parked = [...store.awaiting].filter((t) => !queued.has(t));
|
|
817
|
+
if (parked.length === 0) {
|
|
818
|
+
// Every awaiting thread's settle is deferred on a non-enterable
|
|
819
|
+
// instance. The way out is the lock holder finishing, and the only
|
|
820
|
+
// await-spanning host-entry lock is the async-dtor bracket, which
|
|
821
|
+
// registers in `pendingHostCalls` — so park on those.
|
|
822
|
+
if (store.pendingHostCalls.size > 0) {
|
|
823
|
+
await Promise.race([...store.pendingHostCalls]).catch(() => { });
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
// Per the issue #156 analysis this is unreachable (a spanning lock
|
|
827
|
+
// always has a `pendingHostCalls` entry; a synchronous lock cannot
|
|
828
|
+
// span this loop's await). An internal-wedge detector, not expected
|
|
829
|
+
// behavior.
|
|
830
|
+
traceDrive("driveAsync", store, done, "DEADLOCK-TRAP-deferred");
|
|
831
|
+
trapIf(true, `wasm trap: deadlock detected: event loop cannot make further ` +
|
|
832
|
+
`progress (${what}: every settled activation tail is deferred ` +
|
|
833
|
+
`on a non-enterable instance and no host call is outstanding)`);
|
|
834
|
+
}
|
|
835
|
+
const chosen = parked[0];
|
|
836
|
+
const chosenTag = tagAwait(chosen);
|
|
837
|
+
const others = parked.slice(1).map(tagAwait);
|
|
838
|
+
for (const h of store.pendingHostCalls) {
|
|
839
|
+
others.push(h.then(() => null, () => null));
|
|
840
|
+
}
|
|
841
|
+
// A SPECULATIVE claim: the chosen thread is a promising-wrapped
|
|
842
|
+
// activation, and the engine may run its wasm during this await (pin
|
|
843
|
+
// (i)). It is released unconditionally on the way out — if the
|
|
844
|
+
// activation is genuinely mid-resumption its own exact claim (minted by
|
|
845
|
+
// `SuspensionPoint.resume`) is what carries it, and releasing a claim
|
|
846
|
+
// that names a thread already gone from the queue is a no-op.
|
|
847
|
+
setResumingThread(chosen);
|
|
848
|
+
let winner;
|
|
849
|
+
try {
|
|
850
|
+
winner = await Promise.race([chosenTag, ...others]);
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
clearResumingThread();
|
|
854
|
+
}
|
|
855
|
+
// Resume whichever thread actually settled -- not necessarily the one we
|
|
856
|
+
// claimed. Resuming only the claimed thread would spin: its promise may
|
|
857
|
+
// never settle, the same thread would be chosen again next turn, and the
|
|
858
|
+
// already-settled tags would win the race instantly forever (observed as
|
|
859
|
+
// an OOM, not a hang). The claim is cleared above before any resumption,
|
|
860
|
+
// exactly as on the original single-promise path, so this does not widen
|
|
861
|
+
// the ambient window; it only ensures the loop always makes progress.
|
|
862
|
+
// Membership is not enough: the corner it misses is a thread the OTHER
|
|
863
|
+
// overlapping loop resumed via `tick`, which then re-parked on a NEW
|
|
864
|
+
// promise, after which its OLD promise settles late — membership is
|
|
865
|
+
// true again but the tag's value belongs to a settlement this thread
|
|
866
|
+
// has already consumed. Compare promise identity too.
|
|
867
|
+
if (winner !== null && store.awaiting.has(winner.t) &&
|
|
868
|
+
winner.t.awaiting === winner.p &&
|
|
869
|
+
// Dispatch guard, the same predicate `Store.serviceSettled` uses
|
|
870
|
+
// (issue #156): never resume into an instance that is not
|
|
871
|
+
// host-enterable. The entry is (also) queued in `store.settled` by
|
|
872
|
+
// `noteAwaiting`'s continuation, and `serviceSettled` owns it once
|
|
873
|
+
// the lock releases.
|
|
874
|
+
dispatchableTail(winner.t)) {
|
|
875
|
+
winner.t.resumeWith(winner.value, winner.failure);
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (store.pendingHostCalls.size === 0) {
|
|
880
|
+
traceDrive("driveAsync", store, done, "DEADLOCK-TRAP");
|
|
881
|
+
trapIf(true, `wasm trap: deadlock detected: event loop cannot make further ` +
|
|
882
|
+
`progress (${what}: no thread is ready and no host call is ` +
|
|
883
|
+
`outstanding)`);
|
|
884
|
+
}
|
|
885
|
+
traceDrive("driveAsync", store, done, "await-race");
|
|
886
|
+
// Settlement order among several outstanding host calls is the host's,
|
|
887
|
+
// not ours — this is genuine, unavoidable nondeterminism at the boundary
|
|
888
|
+
// (the reference has the same freedom in `Store.tick`). Everything
|
|
889
|
+
// *inside* the component stays deterministic per scheduler.ts.
|
|
890
|
+
await Promise.race([...store.pendingHostCalls]).catch(() => { });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
const left = storeDriverDepth(store) - 1;
|
|
895
|
+
driverDepth.set(store, left);
|
|
896
|
+
if (left === 0) {
|
|
897
|
+
const w = driverIdle.get(store);
|
|
898
|
+
driverIdle.delete(store);
|
|
899
|
+
w?.r();
|
|
900
|
+
// The store just went driver-idle; if real host calls remain, hand
|
|
901
|
+
// liveness to the settlement pump (which stands down again the moment
|
|
902
|
+
// any driver starts).
|
|
903
|
+
ensureSettlementPump(store);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function takeHostFailure(store) {
|
|
908
|
+
const e = store.hostFailure;
|
|
909
|
+
store.hostFailure = undefined;
|
|
910
|
+
return e;
|
|
911
|
+
}
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
// canon lift
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
/**
|
|
916
|
+
* Build the host-callable function for one lifted export (reference
|
|
917
|
+
* `Store.lift` + `canon_lift`, definitions.py lines 578 and 2154).
|
|
918
|
+
*
|
|
919
|
+
* All three lift shapes go through one `Task` + implicit `Thread`:
|
|
920
|
+
*
|
|
921
|
+
* * **sync** (`not ft.async`) — call, lift results, `task.return_`,
|
|
922
|
+
* post-return, then the sync driving loop until the task resolves;
|
|
923
|
+
* * **async + callback** (stackless) — the packed-code loop
|
|
924
|
+
* (EXIT / YIELD / WAIT), fully implemented here;
|
|
925
|
+
* * **async, no callback** (stackful) — the guest blocks mid-stack, which
|
|
926
|
+
* needs genuine wasm-frame suspension: `needsJspi`, at the precise point.
|
|
927
|
+
*/
|
|
928
|
+
/**
|
|
929
|
+
* The plain-entered variant of a `[constructor]` export in jspi mode,
|
|
930
|
+
* attached to the promising-wrapped lifted function under this symbol.
|
|
931
|
+
*
|
|
932
|
+
* A WIT constructor is surfaced as a JS class constructor
|
|
933
|
+
* (contracts/embedder-api.md §"Resources"), and a JS constructor cannot
|
|
934
|
+
* await — but in jspi mode every promising-wrapped entry returns a Promise
|
|
935
|
+
* even when the activation completes without suspending (jspi pin (e)). So
|
|
936
|
+
* constructor exports carry a second lifted function whose ENTRY is plain
|
|
937
|
+
* (unwrapped): a constructor that completes synchronously — the
|
|
938
|
+
* overwhelmingly common case; WIT constructors are always sync-typed —
|
|
939
|
+
* returns its rep synchronously through it.
|
|
940
|
+
*
|
|
941
|
+
* The cost is confined to genuinely-suspending constructors, which no JS
|
|
942
|
+
* host can surface as `new` anyway: a blocking built-in reached through the
|
|
943
|
+
* plain entry signals `NeedsJspi` (a capability error, instance left
|
|
944
|
+
* enterable), and a Suspending-wrapped host import reached from the
|
|
945
|
+
* unwrapped frame fails as a trap. Both name the constructor rather than
|
|
946
|
+
* silently deadlocking.
|
|
947
|
+
*/
|
|
948
|
+
export const CONSTRUCTOR_SYNC_ENTRY = Symbol("polyengine.constructorSyncEntry");
|
|
949
|
+
export function createLiftedFunction(input) {
|
|
950
|
+
const { name, ft, opts, core, stats, trapState, syncCallStack, allInstances, } = input;
|
|
951
|
+
const inst = opts.instance;
|
|
952
|
+
const store = inst.store;
|
|
953
|
+
const mode = input.suspensionMode ?? "plain";
|
|
954
|
+
// Entry wrapping, half of jspi/bridge.ts's invariant: a lifted export's core
|
|
955
|
+
// function is one of the three activations that can reach a blocking
|
|
956
|
+
// built-in, so it is `promising`-wrapped exactly when the imports are
|
|
957
|
+
// `Suspending`-wrapped.
|
|
958
|
+
const enteredCore = enterWasm(core, mode);
|
|
959
|
+
const taskOpts = {
|
|
960
|
+
async_: opts.async,
|
|
961
|
+
callback: opts.callback !== null,
|
|
962
|
+
stringEncoding: opts.stringEncoding,
|
|
963
|
+
memory: opts.memory,
|
|
964
|
+
};
|
|
965
|
+
// definitions.py `canon_lift` only ever sees consistent combinations; the
|
|
966
|
+
// plan could in principle carry others, so reject at instantiate time.
|
|
967
|
+
if (opts.callback !== null && !opts.async) {
|
|
968
|
+
throw new PlanError(`export '${name}': canonical options carry a callback but are not ` +
|
|
969
|
+
`async (callback is meaningless for a sync lift)`);
|
|
970
|
+
}
|
|
971
|
+
// Instantiate-time consistency check (descriptor-ir.md "Flattening"):
|
|
972
|
+
// flattening computed from the type must agree with the shim's coreType.
|
|
973
|
+
const computed = flattenFunctype(cabiOptions(opts), ft, "lift");
|
|
974
|
+
if (!coreFuncTypeEquals(computed, opts.coreType)) {
|
|
975
|
+
throw new PlanError(`export '${name}': computed flat type ${JSON.stringify(computed)} ` +
|
|
976
|
+
`!= plan coreType ${JSON.stringify(opts.coreType)}`);
|
|
977
|
+
}
|
|
978
|
+
const invokeNow = (hostArgs) => {
|
|
979
|
+
stats.liftedCalls++;
|
|
980
|
+
// A trap remembered during an earlier call must never be attributed to
|
|
981
|
+
// this one (see intrinsics `HostTrapState`).
|
|
982
|
+
if (trapState !== undefined)
|
|
983
|
+
trapState.pending = undefined;
|
|
984
|
+
// Depth of the sync-call scope stack on entry; see the `finally` below.
|
|
985
|
+
const syncCallDepth = syncCallStack?.length ?? 0;
|
|
986
|
+
// Reference `Store.lift` (line 578): the host is the caller, so the
|
|
987
|
+
// entering set is the callee's `self_and_ancestors()`.
|
|
988
|
+
// On refusal, distinguish the corpse from the crowd: a poisoned
|
|
989
|
+
// instance's refusal names the original trap (polyengine#145 ask 1).
|
|
990
|
+
if (!inst.mayEnterFrom(null)) {
|
|
991
|
+
trap(withPoisonCause(inst, `cannot enter component instance ${inst.index} (reentrance forbidden)`));
|
|
992
|
+
}
|
|
993
|
+
// The set this entry locked (definitions.py `ComponentInstance.enter_from`
|
|
994
|
+
// iterates `entering_set`). Remembered so a trap can leave exactly these
|
|
995
|
+
// locked and no others.
|
|
996
|
+
const enteredSet = inst.enteringSet(null);
|
|
997
|
+
inst.enterFrom(null);
|
|
998
|
+
let entered = true;
|
|
999
|
+
let completed = false;
|
|
1000
|
+
let resolved = null;
|
|
1001
|
+
let resolvedSeen = false;
|
|
1002
|
+
const task = new Task(ft, taskOpts, inst, () => hostArgs, (result) => {
|
|
1003
|
+
resolved = result;
|
|
1004
|
+
resolvedSeen = true;
|
|
1005
|
+
stats.tasksResolved++;
|
|
1006
|
+
});
|
|
1007
|
+
const thread = new Thread(task, liftBody({
|
|
1008
|
+
name,
|
|
1009
|
+
ft,
|
|
1010
|
+
opts,
|
|
1011
|
+
core: enteredCore,
|
|
1012
|
+
stats,
|
|
1013
|
+
task,
|
|
1014
|
+
thread: () => thread,
|
|
1015
|
+
mode,
|
|
1016
|
+
}));
|
|
1017
|
+
const finishHostEntry = () => {
|
|
1018
|
+
completed = true;
|
|
1019
|
+
trapIf(!resolvedSeen, `${name}: task finished without resolving (deadlock)`);
|
|
1020
|
+
if (resolved === null) {
|
|
1021
|
+
// definitions.py `Task.cancel`: `on_resolve(None)`. A host-initiated
|
|
1022
|
+
// call has no way to express "cancelled" in its return value, and the
|
|
1023
|
+
// host never requests cancellation, so reaching this is a bug.
|
|
1024
|
+
throw new AssertionError(`${name}: task resolved as cancelled, but the host never ` +
|
|
1025
|
+
`requested cancellation`);
|
|
1026
|
+
}
|
|
1027
|
+
return resultsToHost(resolved);
|
|
1028
|
+
};
|
|
1029
|
+
const unwind = () => {
|
|
1030
|
+
// Unwind any FACT sync-call brackets a trap escaped.
|
|
1031
|
+
//
|
|
1032
|
+
// A trap thrown inside an adapter skips that adapter's
|
|
1033
|
+
// `exit-sync-call`, so its `SyncCallScope` (and the `num_lends` it
|
|
1034
|
+
// holds on the caller's handles) would otherwise survive the call.
|
|
1035
|
+
// wasmtime does not need this: it poisons the whole store on trap
|
|
1036
|
+
// (`Store::call_hook`/panic-on-reuse semantics), so no later call can
|
|
1037
|
+
// observe the stale state. This runtime deliberately supports
|
|
1038
|
+
// post-trap re-entry — the `trapState.pending` reset above exists for
|
|
1039
|
+
// exactly that — so the state has to be unwound instead. Leaving it
|
|
1040
|
+
// would attach the next `transfer-borrow` to a dead scope and leave
|
|
1041
|
+
// lent handles permanently un-droppable ("while borrowed" forever).
|
|
1042
|
+
if (completed)
|
|
1043
|
+
return;
|
|
1044
|
+
// Per-ACTIVATION now (see `Thread.syncCallStack`): unwind the brackets
|
|
1045
|
+
// of every activation this task owns, which a trap inside a FACT adapter
|
|
1046
|
+
// skipped. A task can have several threads, so the loop is over threads.
|
|
1047
|
+
for (const t of task.threads) {
|
|
1048
|
+
while (t.syncCallStack.length > 0) {
|
|
1049
|
+
t.syncCallStack.pop().releaseLenders();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
void syncCallStack;
|
|
1053
|
+
void syncCallDepth;
|
|
1054
|
+
// FACT clears the callee's / caller's `may_leave` flag around each
|
|
1055
|
+
// lift and lower (`fact/trampoline.rs`, `set_may_leave_false`) and
|
|
1056
|
+
// restores it afterwards. A trap in between skips the restore, so an
|
|
1057
|
+
// instance can be left permanently unable to leave — every later call
|
|
1058
|
+
// through an adapter then trips FACT's own `CannotLeaveComponent`
|
|
1059
|
+
// check. With the stack unwound to the host boundary no lift or lower
|
|
1060
|
+
// is in flight, so `may_leave` is true for every instance by
|
|
1061
|
+
// definition; assert that resting state rather than leaving the
|
|
1062
|
+
// component bricked.
|
|
1063
|
+
//
|
|
1064
|
+
// The *entered* instances are excluded: they are poisoned by this trap
|
|
1065
|
+
// (see `poison` below) and must stay exactly as the trap left them.
|
|
1066
|
+
// Restoring their `may_leave` would be tidying the state of an instance
|
|
1067
|
+
// that is no longer allowed to run at all.
|
|
1068
|
+
for (const i of allInstances?.() ?? []) {
|
|
1069
|
+
if (!enteredSet.has(i)) {
|
|
1070
|
+
i.mayLeave = true;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const leave = () => {
|
|
1075
|
+
if (!entered)
|
|
1076
|
+
return;
|
|
1077
|
+
entered = false;
|
|
1078
|
+
inst.leaveTo(null);
|
|
1079
|
+
};
|
|
1080
|
+
/**
|
|
1081
|
+
* A trap escaped the task: **do not** release the reentrance lock.
|
|
1082
|
+
*
|
|
1083
|
+
* definitions.py `Store.lift` (line 578) is
|
|
1084
|
+
*
|
|
1085
|
+
* ```python
|
|
1086
|
+
* trap_if(not inst.may_enter_from(caller))
|
|
1087
|
+
* inst.enter_from(caller)
|
|
1088
|
+
* on_cancel = canon_lift(...) # <-- a Trap propagates out of here
|
|
1089
|
+
* inst.leave_to(caller) # <-- and so this never runs
|
|
1090
|
+
* ```
|
|
1091
|
+
*
|
|
1092
|
+
* so a trapping task leaves every instance it entered with
|
|
1093
|
+
* `may_enter == False` permanently. That is the Component Model's
|
|
1094
|
+
* "poisoning": a component that trapped is not in a known state, so it may
|
|
1095
|
+
* never be entered again, and the next call reports `cannot enter
|
|
1096
|
+
* component instance`. `test/async/builtin-trap-poisons-instance.wast`
|
|
1097
|
+
* asserts exactly this, twice.
|
|
1098
|
+
*
|
|
1099
|
+
* Only the entered set is affected; sibling instances stay usable, which
|
|
1100
|
+
* is why the lock is released per-instance rather than by poisoning a
|
|
1101
|
+
* whole store the way wasmtime does.
|
|
1102
|
+
*
|
|
1103
|
+
* Poisoned instances can never rendezvous again, so their handle tables'
|
|
1104
|
+
* live stream/future ends are retired here (#66): parked host operations
|
|
1105
|
+
* settle (DROPPED) instead of hanging forever, and the recorded failure
|
|
1106
|
+
* lets the embedder layer reject them loudly.
|
|
1107
|
+
*/
|
|
1108
|
+
const poison = (e) => {
|
|
1109
|
+
entered = false; // consumed: the lock is now permanent
|
|
1110
|
+
// ...for the leaf. The synthetic per-instantiation root (plan v3
|
|
1111
|
+
// amendment 4) is in `enteredSet` too, and leaving IT locked would
|
|
1112
|
+
// poison every instance of the component — exactly the store-wide
|
|
1113
|
+
// behaviour the paragraph above says this runtime deliberately does not
|
|
1114
|
+
// have. Released; see `releaseSyntheticRootOnPoison` in task/mod.ts.
|
|
1115
|
+
inst.releaseSyntheticRootOnPoison();
|
|
1116
|
+
// Through the seam (not retireInstanceAsyncEnds directly) so the
|
|
1117
|
+
// poison marker is recorded too — `Thread.resumeWith` retires this
|
|
1118
|
+
// instance's late settles against it instead of assert-cascading.
|
|
1119
|
+
for (const i of enteredSet) {
|
|
1120
|
+
if (i.isSyntheticRoot)
|
|
1121
|
+
continue;
|
|
1122
|
+
notifyInstancePoisoned(i, e);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
/**
|
|
1126
|
+
* Is `e` a *capability* signal rather than a genuine trap?
|
|
1127
|
+
*
|
|
1128
|
+
* `NeedsJspi` and `PendingCapability` mean "this runtime is incomplete",
|
|
1129
|
+
* not "the component faulted". Poisoning on them is wrong on the
|
|
1130
|
+
* reference's own terms: the operation they stand in for — a synchronous
|
|
1131
|
+
* stream copy, `waitable-set.wait`, a blocking cross-component call —
|
|
1132
|
+
* *blocks and then completes* in definitions.py. `Store.lift` reaches
|
|
1133
|
+
* `leave_to` in every one of those executions, so the instance stays
|
|
1134
|
+
* enterable. Poisoning would attribute a permanent fault to a component
|
|
1135
|
+
* that, on a complete runtime, is perfectly healthy — and it cascades:
|
|
1136
|
+
* one unsupported operation made every later call on that instance report
|
|
1137
|
+
* `cannot enter component instance`, which is neither our real behaviour
|
|
1138
|
+
* nor the reference's.
|
|
1139
|
+
*
|
|
1140
|
+
* What unwinding must still do on this path, and what it must not:
|
|
1141
|
+
*
|
|
1142
|
+
* - MUST release the reentrance lock (`leave`) — the call is over and no
|
|
1143
|
+
* activation of this instance survives it.
|
|
1144
|
+
* - MUST unwind the FACT sync-call scopes and restore `may_leave`
|
|
1145
|
+
* (`unwind`), for exactly the reasons it does after a trap: a bail-out
|
|
1146
|
+
* mid-adapter skips `exit-sync-call` and the `may_leave` restore, and
|
|
1147
|
+
* that state is shared with sibling instances.
|
|
1148
|
+
* - MUST NOT try to "finish" the abandoned operation. A stream end left
|
|
1149
|
+
* in `CopyState.COPYING` with its buffer parked in the shared object is
|
|
1150
|
+
* the honest record of "this copy never happened"; the counterpart has
|
|
1151
|
+
* not been notified and must not be, because on a complete runtime the
|
|
1152
|
+
* copy would still be pending. Likewise a `prepare-call` slot consumed
|
|
1153
|
+
* by a `*-start-call` that then bailed is already cleared by
|
|
1154
|
+
* `takePrepared`, so nothing leaks there.
|
|
1155
|
+
* - MUST NOT resolve or cancel the task: the host call fails, and the
|
|
1156
|
+
* task simply never resolved.
|
|
1157
|
+
*
|
|
1158
|
+
* In other words the instance is left exactly as a *pending* operation
|
|
1159
|
+
* would leave it, which is the truthful state, and the only thing the
|
|
1160
|
+
* embedder loses is the result of this one call.
|
|
1161
|
+
*/
|
|
1162
|
+
const isCapabilitySignal = (e) => e instanceof NeedsJspi || e instanceof PendingCapability;
|
|
1163
|
+
try {
|
|
1164
|
+
thread.resume();
|
|
1165
|
+
// definitions.py `canon_lift` (line 2213): the sync driving loop runs
|
|
1166
|
+
// *inside* the enter/leave bracket, over the callee instance's threads.
|
|
1167
|
+
//
|
|
1168
|
+
// It is skipped in jspi mode, and must be. That loop resumes *ready*
|
|
1169
|
+
// threads and traps when there are none — the reference's deadlock
|
|
1170
|
+
// trap. A thread parked on a Promise is neither ready nor waiting: only
|
|
1171
|
+
// a microtask turn can advance it, which a synchronous loop cannot give.
|
|
1172
|
+
// Running it anyway declared a bogus deadlock the moment a sync-lifted
|
|
1173
|
+
// export's activation suspended, which then trap-poisoned the instance
|
|
1174
|
+
// and abandoned the activation mid-bracket — the orphaned
|
|
1175
|
+
// `exit-sync-call` traced across phases 3h-3j.
|
|
1176
|
+
//
|
|
1177
|
+
// `drive` below is the correct driver in that mode: it knows about
|
|
1178
|
+
// `store.awaiting`, still enforces the deadlock trap (no ready thread,
|
|
1179
|
+
// no pending host call, nothing awaiting), and returns a Promise, which
|
|
1180
|
+
// a jspi-mode lifted export returns anyway.
|
|
1181
|
+
if (!ft.async && mode !== "jspi" && !input.allowAsyncCompletion) {
|
|
1182
|
+
driveSyncLift(task);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
unwind();
|
|
1187
|
+
if (isCapabilitySignal(e))
|
|
1188
|
+
leave();
|
|
1189
|
+
else
|
|
1190
|
+
poison(e);
|
|
1191
|
+
throw e;
|
|
1192
|
+
}
|
|
1193
|
+
// The reentrance gate is released here, before the store is pumped:
|
|
1194
|
+
// `Store.tick` re-enters each waiting thread's instance itself
|
|
1195
|
+
// (`enter_from(None)` / `leave_to(None)`), exactly as in the reference,
|
|
1196
|
+
// where `lift_and_run` ticks after `store.invoke` has returned.
|
|
1197
|
+
leave();
|
|
1198
|
+
let pending;
|
|
1199
|
+
try {
|
|
1200
|
+
// Completion is "the task resolved AND its threads have drained", not
|
|
1201
|
+
// merely "resolved". `task.return` resolves the task, but the activation
|
|
1202
|
+
// is not finished until its implicit thread reaches
|
|
1203
|
+
// `exit_implicit_thread` — for a callback task that means running the
|
|
1204
|
+
// loop out to EXIT, which releases `inst.exclusiveThread`.
|
|
1205
|
+
//
|
|
1206
|
+
// In plain mode the two almost always coincide, because the generator
|
|
1207
|
+
// runs to completion inside one `resume()`. Under JSPI they do not: the
|
|
1208
|
+
// guest calls `task.return` while the activation is still suspended, so
|
|
1209
|
+
// the old predicate let the driver return early and the thread was
|
|
1210
|
+
// abandoned mid-loop — leaking the exclusive thread and its table slot.
|
|
1211
|
+
// The lifted call is over when the task has resolved AND this task's
|
|
1212
|
+
// activation is no longer mid-wasm-call. Those are two different events
|
|
1213
|
+
// and both matter (M2 phase 3e):
|
|
1214
|
+
//
|
|
1215
|
+
// * "task resolved" alone abandons a still-running activation. Under
|
|
1216
|
+
// JSPI the guest calls `task.return` while suspended, so returning
|
|
1217
|
+
// there left the callback loop parked forever — leaking the
|
|
1218
|
+
// exclusive thread and its table slot.
|
|
1219
|
+
// * "activation finished" alone deadlocks a *producer* guest, which
|
|
1220
|
+
// legitimately keeps forwarding after `task.return`
|
|
1221
|
+
// (wit-bindgen `wit_stream::new()` + a spawned loop).
|
|
1222
|
+
//
|
|
1223
|
+
// The distinguishing question is *what* the thread is parked on. An
|
|
1224
|
+
// `awaitValue` park means a wasm call is in flight and will settle on
|
|
1225
|
+
// its own, so we must keep draining. A park in `store.waiting` means the
|
|
1226
|
+
// activation is waiting on a scheduler condition only the embedder can
|
|
1227
|
+
// satisfy — that is a **background activation**: we return to the host
|
|
1228
|
+
// and leave the thread live, and later `drive`/`pump` calls (host stream
|
|
1229
|
+
// writes, the next export call) go on servicing it.
|
|
1230
|
+
const midWasmCall = () => task.threads.some((t) => store.awaiting.has(t));
|
|
1231
|
+
pending = drive(store, () => resolvedSeen && !midWasmCall(), `export '${name}'`);
|
|
1232
|
+
}
|
|
1233
|
+
catch (e) {
|
|
1234
|
+
unwind();
|
|
1235
|
+
throw e;
|
|
1236
|
+
}
|
|
1237
|
+
if (pending === undefined) {
|
|
1238
|
+
try {
|
|
1239
|
+
return finishHostEntry();
|
|
1240
|
+
}
|
|
1241
|
+
catch (e) {
|
|
1242
|
+
unwind();
|
|
1243
|
+
throw e;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return pending.then(finishHostEntry, (e) => {
|
|
1247
|
+
unwind();
|
|
1248
|
+
throw e;
|
|
1249
|
+
});
|
|
1250
|
+
};
|
|
1251
|
+
return (...hostArgs) => {
|
|
1252
|
+
if (hostArgs.length !== ft.params.length) {
|
|
1253
|
+
throw new TypeError(`${name}: expected ${ft.params.length} argument(s), got ${hostArgs.length}`);
|
|
1254
|
+
}
|
|
1255
|
+
// THE HOP-QUIESCENCE GATE (jspi mode only; hop_atomicity_test.ts).
|
|
1256
|
+
//
|
|
1257
|
+
// A promising-wrapped entry settles a microtask AFTER the guest's core
|
|
1258
|
+
// call returns, even when nothing suspended (jspi pin (j)) — so there
|
|
1259
|
+
// is a hop between core return and the host-side result LIFT, and the
|
|
1260
|
+
// reentrance bracket has already been released by then (`leave()` runs
|
|
1261
|
+
// when the first segment parks). In the reference no such window
|
|
1262
|
+
// exists: `canon_lift` for sync options runs core + lift atomically
|
|
1263
|
+
// inside one entered bracket. Admitting another host call into the
|
|
1264
|
+
// window lets a full guest turn mutate the memory the pending lift
|
|
1265
|
+
// will read — observed as `Trap: list too long` lifting the wosh
|
|
1266
|
+
// engine's `tick` (`list<list<u8>>`) after a concurrent `feed-keys`
|
|
1267
|
+
// turn reused the return area.
|
|
1268
|
+
//
|
|
1269
|
+
// The gate: defer this call until the instance has no HOP-parked
|
|
1270
|
+
// activation. A hop-park is an `awaiting` thread with no owning
|
|
1271
|
+
// `SuspensionPoint` — the same discriminator `hasRunnableWork` uses;
|
|
1272
|
+
// genuinely JSPI-suspended activations (SuspensionPoint-owned) keep
|
|
1273
|
+
// today's documented interleaving (the wasmtime-tracking divergence in
|
|
1274
|
+
// jspi/bridge.ts), which host-import re-entry patterns rely on.
|
|
1275
|
+
// Plain mode has no hops and keeps its synchronous fast path exactly.
|
|
1276
|
+
if (mode === "jspi" && entryHopThreads(store, inst).length > 0) {
|
|
1277
|
+
return awaitHopQuiescence(store, inst).then(() => invokeNow(hostArgs));
|
|
1278
|
+
}
|
|
1279
|
+
return invokeNow(hostArgs);
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Threads of `inst` parked on a promising-entry hop: in `store.awaiting`
|
|
1284
|
+
* with no `SuspensionPoint` owner in `store.waiting` (that would be a
|
|
1285
|
+
* genuine JSPI suspension). Mirrors `Store.hasRunnableWork`'s (b)/(c)
|
|
1286
|
+
* split.
|
|
1287
|
+
*/
|
|
1288
|
+
function entryHopThreads(store, inst) {
|
|
1289
|
+
if (store.awaiting.size === 0)
|
|
1290
|
+
return [];
|
|
1291
|
+
const suspended = new Set();
|
|
1292
|
+
for (const w of store.waiting) {
|
|
1293
|
+
const owner = w.owner;
|
|
1294
|
+
if (owner !== undefined && owner !== null)
|
|
1295
|
+
suspended.add(owner);
|
|
1296
|
+
}
|
|
1297
|
+
const out = [];
|
|
1298
|
+
for (const t of store.awaiting) {
|
|
1299
|
+
const tt = t;
|
|
1300
|
+
if (tt.task.inst === inst && !suspended.has(t))
|
|
1301
|
+
out.push(tt);
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Wait until `inst` has no hop-parked activation. Each settled hop is
|
|
1307
|
+
* serviced synchronously (`serviceSettled` runs the lift segment), after
|
|
1308
|
+
* which the activation either completed or re-parked; re-derive and
|
|
1309
|
+
* repeat. Progress is guaranteed: a hop promise settles on the engine's
|
|
1310
|
+
* own schedule, independent of any other activation of the instance, and
|
|
1311
|
+
* a settled-but-unserviced hop resolves the race instantly. Multiple
|
|
1312
|
+
* gated callers re-derive independently (no strict FIFO; starvation-free
|
|
1313
|
+
* in practice because hops are sub-microtask).
|
|
1314
|
+
*/
|
|
1315
|
+
async function awaitHopQuiescence(store, inst) {
|
|
1316
|
+
for (;;) {
|
|
1317
|
+
const hops = entryHopThreads(store, inst);
|
|
1318
|
+
if (hops.length === 0)
|
|
1319
|
+
return;
|
|
1320
|
+
await Promise.race(hops.map((t) => (t.awaiting ?? Promise.resolve()).then(() => undefined, () => undefined)));
|
|
1321
|
+
store.serviceSettled();
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// ---------------------------------------------------------------------------
|
|
1325
|
+
// Host-initiated resource destructors (#160)
|
|
1326
|
+
// ---------------------------------------------------------------------------
|
|
1327
|
+
/**
|
|
1328
|
+
* The canonical function type of a destructor: definitions.py
|
|
1329
|
+
* `canon_resource_drop` (line 2326) — `FuncType([U32Type()], [], async_ = False)`.
|
|
1330
|
+
*/
|
|
1331
|
+
const DTOR_FT = {
|
|
1332
|
+
params: [{ kind: "u32" }],
|
|
1333
|
+
results: [],
|
|
1334
|
+
async: false,
|
|
1335
|
+
};
|
|
1336
|
+
/**
|
|
1337
|
+
* `CanonicalOptions(async_ = False)` (definitions.py line 2325): every field
|
|
1338
|
+
* at its inert default. A dtor takes one flat `i32` and returns nothing, so
|
|
1339
|
+
* no memory / realloc / post-return / callback is ever reached.
|
|
1340
|
+
*/
|
|
1341
|
+
function dtorOptions(instance) {
|
|
1342
|
+
return {
|
|
1343
|
+
stringEncoding: "utf8",
|
|
1344
|
+
memory: null,
|
|
1345
|
+
realloc: null,
|
|
1346
|
+
postReturn: null,
|
|
1347
|
+
callback: null,
|
|
1348
|
+
async: false,
|
|
1349
|
+
cancellable: false,
|
|
1350
|
+
coreType: { params: ["i32"], results: [] },
|
|
1351
|
+
instance,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Build the host-callable entry for a resource destructor — a full canonical
|
|
1356
|
+
* **lift**, exactly as definitions.py `canon_resource_drop` (line 2319) does:
|
|
1357
|
+
*
|
|
1358
|
+
* ```python
|
|
1359
|
+
* opts = CanonicalOptions(async_ = False)
|
|
1360
|
+
* ft = FuncType([U32Type()], [], async_ = False)
|
|
1361
|
+
* dtor = rt.dtor or (lambda rep: [])
|
|
1362
|
+
* callee = inst.store.lift(dtor, ft, opts, rt.impl)
|
|
1363
|
+
* ```
|
|
1364
|
+
*
|
|
1365
|
+
* Before #160 the host-initiated path (embedder `drop()`, the GC backstop,
|
|
1366
|
+
* `dropOwn`) hand-rolled the bracket in cabi/handles.ts `callDtorGated`: a
|
|
1367
|
+
* bare call to the dtor with `enterFrom(null)` HELD across the returned
|
|
1368
|
+
* promise. Three defects followed from having no Task/Thread behind the
|
|
1369
|
+
* activation:
|
|
1370
|
+
*
|
|
1371
|
+
* - **#160 itself**: the held bracket left the impl instance non-enterable,
|
|
1372
|
+
* so `Store.tick`'s enterability filter (#155) could never resume a
|
|
1373
|
+
* suspension point belonging to the dtor's own activation. The completion
|
|
1374
|
+
* promise sat in `pendingHostCalls` looking like external work, and every
|
|
1375
|
+
* driver parked on it forever.
|
|
1376
|
+
* - it was the runtime's only `enterFrom(null)` bracket spanning an await —
|
|
1377
|
+
* the macro-scale reachability window of the #156 class, through which a
|
|
1378
|
+
* sibling instance looked non-enterable from the synthetic root.
|
|
1379
|
+
* - built-ins reached inside the dtor had no ambient task (`currentTask()`
|
|
1380
|
+
* → `PendingCapability`, or a foreign-task misattribution, the #24 class).
|
|
1381
|
+
*
|
|
1382
|
+
* Under the lift harness all three go away structurally: the activation has a
|
|
1383
|
+
* real `Task` + implicit `Thread`, the entry bracket is released when the
|
|
1384
|
+
* first segment parks (`leave()` before `drive`), and settled tails flow
|
|
1385
|
+
* through `serviceSettled` like any other lifted sync call.
|
|
1386
|
+
*
|
|
1387
|
+
* The returned function takes the rep and returns either `undefined` (the
|
|
1388
|
+
* activation completed synchronously — the overwhelmingly common case) or a
|
|
1389
|
+
* Promise, exactly like any lifted sync export in jspi mode.
|
|
1390
|
+
*/
|
|
1391
|
+
export function createDtorEntry(input) {
|
|
1392
|
+
const mode = input.suspensionMode ?? "plain";
|
|
1393
|
+
const raw = input.dtor ?? (() => undefined);
|
|
1394
|
+
// A dtor's core type is `(i32) -> ()`, but the *host*-supplied dtors this
|
|
1395
|
+
// helper also serves (embedder test doubles, `ResourceTypeInfo` built
|
|
1396
|
+
// directly) are ordinary JS functions whose incidental return value would
|
|
1397
|
+
// otherwise trip `normalizeCoreValues`' arity check. Discard it — except a
|
|
1398
|
+
// thenable, which is the activation itself and must reach `awaitCore`'s
|
|
1399
|
+
// park. Not applied in jspi mode: `WebAssembly.promising` only accepts a
|
|
1400
|
+
// wasm callable, so the core must be passed through untouched there (and a
|
|
1401
|
+
// real wasm dtor returns nothing by construction).
|
|
1402
|
+
const core = mode === "jspi" ? raw : ((rep) => {
|
|
1403
|
+
const r = raw(rep);
|
|
1404
|
+
return isPromiseLike(r) ? r : undefined;
|
|
1405
|
+
});
|
|
1406
|
+
const lifted = createLiftedFunction({
|
|
1407
|
+
name: input.name ?? "[resource-dtor]",
|
|
1408
|
+
ft: DTOR_FT,
|
|
1409
|
+
opts: dtorOptions(input.instance),
|
|
1410
|
+
core,
|
|
1411
|
+
stats: input.stats ?? newStats(),
|
|
1412
|
+
suspensionMode: mode,
|
|
1413
|
+
trapState: input.trapState,
|
|
1414
|
+
syncCallStack: input.syncCallStack,
|
|
1415
|
+
allInstances: input.allInstances,
|
|
1416
|
+
// The host does not wait for a destructor: `drop(): void` is
|
|
1417
|
+
// non-blocking, and an unfinished dtor's tail is driven by the store.
|
|
1418
|
+
allowAsyncCompletion: true,
|
|
1419
|
+
});
|
|
1420
|
+
return (rep) => lifted(rep);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Run a host-initiated drop of a guest (or host-implemented) resource rep —
|
|
1424
|
+
* the observable remainder of `canon_resource_drop` for an owning handle when
|
|
1425
|
+
* the holder is the host (`caller = None`, `Store.invoke`).
|
|
1426
|
+
*
|
|
1427
|
+
* A failure that arrives asynchronously has no frame to propagate into, so it
|
|
1428
|
+
* is parked on the store's host-failure channel (first failure wins), where
|
|
1429
|
+
* the next driven call surfaces it. The completion promise is deliberately
|
|
1430
|
+
* NOT registered in `store.pendingHostCalls`: that registration was #160's
|
|
1431
|
+
* lie — it claims *external* work for a promise whose settlement may need
|
|
1432
|
+
* this very scheduler. The dtor's genuine external dependencies (its host
|
|
1433
|
+
* imports) register themselves when they park. Poisoning on a trap now
|
|
1434
|
+
* happens inside the lift harness (`poison()` in `createLiftedFunction`).
|
|
1435
|
+
*/
|
|
1436
|
+
export function hostDtorCall(rt, rep) {
|
|
1437
|
+
const impl = rt.impl;
|
|
1438
|
+
// An imported (host-implemented) resource has `impl === null` by
|
|
1439
|
+
// construction (executor `bindImportedResources`): there is no component
|
|
1440
|
+
// instance to gate entry into, so the dtor is called directly, as before.
|
|
1441
|
+
if (impl === null) {
|
|
1442
|
+
rt.dtor?.(rep);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (rt.dtorHost === null) {
|
|
1446
|
+
// The executor pre-wires `dtorHost` for every defined resource; this is
|
|
1447
|
+
// the direct-construction path (embedder test doubles, and any token that
|
|
1448
|
+
// reached the host without going through the `resource` initializer).
|
|
1449
|
+
rt.dtorHost = createDtorEntry({
|
|
1450
|
+
dtor: rt.dtor,
|
|
1451
|
+
instance: impl,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
const out = rt.dtorHost(rep);
|
|
1455
|
+
if (isPromiseLike(out)) {
|
|
1456
|
+
const store = impl.store;
|
|
1457
|
+
Promise.resolve(out).catch((e) => {
|
|
1458
|
+
if (store !== undefined && store.hostFailure === undefined) {
|
|
1459
|
+
store.hostFailure = e;
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Call into wasm and hand back the result, awaiting it only if it is a
|
|
1466
|
+
* Promise.
|
|
1467
|
+
*
|
|
1468
|
+
* This is the whole of the jspi entry seam. In **plain** mode the entry is not
|
|
1469
|
+
* `promising`-wrapped, `callCore` returns core values, and this returns them
|
|
1470
|
+
* without yielding — no await, no Promise allocation, the identical
|
|
1471
|
+
* synchronous path M1 shipped. In **jspi** mode the entry *is* wrapped, so the
|
|
1472
|
+
* call returns a Promise (jspi pin (e)) and we park the thread on it via the
|
|
1473
|
+
* `awaitValue` block request; the driving loop resumes us with the values, or
|
|
1474
|
+
* throws the rejection in (a post-resume trap).
|
|
1475
|
+
*/
|
|
1476
|
+
export function* awaitCore(fn, args,
|
|
1477
|
+
// deno-lint-ignore no-explicit-any
|
|
1478
|
+
thread) {
|
|
1479
|
+
// Enter wasm with the activation-attached ambient in scope. In jspi mode the
|
|
1480
|
+
// engine captures this context when it registers its resumption, so a
|
|
1481
|
+
// built-in called by the resumed activation can recover its thread even when
|
|
1482
|
+
// nobody is driving (see `withActivation`).
|
|
1483
|
+
const raw = withActivation(thread, () => callCore(fn, args));
|
|
1484
|
+
// `callCore` normalizes a bare value to a one-element array; a promising
|
|
1485
|
+
// entry yields `[Promise]`.
|
|
1486
|
+
if (raw.length === 1 && isPromiseLike(raw[0])) {
|
|
1487
|
+
const settled = yield {
|
|
1488
|
+
readyFunc: null,
|
|
1489
|
+
cancellable: false,
|
|
1490
|
+
// A rejection of the promising Promise is a core trap by another route
|
|
1491
|
+
// (jspi pin (e)); translate it exactly as `callCore` translates a
|
|
1492
|
+
// synchronous throw, so the embedder sees one `Trap` vocabulary in both
|
|
1493
|
+
// modes (see `mapCoreException`).
|
|
1494
|
+
awaitValue: Promise.resolve(raw[0]).then(undefined, (e) => {
|
|
1495
|
+
throw mapCoreException(e);
|
|
1496
|
+
}),
|
|
1497
|
+
};
|
|
1498
|
+
if (settled === undefined)
|
|
1499
|
+
return [];
|
|
1500
|
+
return Array.isArray(settled) ? settled : [settled];
|
|
1501
|
+
}
|
|
1502
|
+
return raw;
|
|
1503
|
+
}
|
|
1504
|
+
/** definitions.py `CallbackCode` (line 2220). */
|
|
1505
|
+
var CallbackCode;
|
|
1506
|
+
(function (CallbackCode) {
|
|
1507
|
+
CallbackCode[CallbackCode["EXIT"] = 0] = "EXIT";
|
|
1508
|
+
CallbackCode[CallbackCode["YIELD"] = 1] = "YIELD";
|
|
1509
|
+
CallbackCode[CallbackCode["WAIT"] = 2] = "WAIT";
|
|
1510
|
+
})(CallbackCode || (CallbackCode = {}));
|
|
1511
|
+
const CALLBACK_CODE_MAX = 2;
|
|
1512
|
+
/** definitions.py `unpack_callback_result` (line 2226). */
|
|
1513
|
+
export function unpackCallbackResult(packed) {
|
|
1514
|
+
// Reference parity insurance only: callers already guarantee this range via
|
|
1515
|
+
// core-result normalization before calling in.
|
|
1516
|
+
assert_(packed >= 0 && packed < 2 ** 32, `unpack-callback-result: packed out of range: ${packed}`);
|
|
1517
|
+
const code = packed & 0xf;
|
|
1518
|
+
trapIf(code > CALLBACK_CODE_MAX, `invalid callback code ${code}`);
|
|
1519
|
+
return [code, packed >>> 4];
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* The body of `canon_lift`'s implicit thread (definitions.py line 2155),
|
|
1523
|
+
* as a generator so its block points are real suspension points of the
|
|
1524
|
+
* host-side thread model (see task/scheduler.ts).
|
|
1525
|
+
*/
|
|
1526
|
+
function* liftBody(input) {
|
|
1527
|
+
const { name, ft, opts, core, stats, task } = input;
|
|
1528
|
+
const thread = input.thread();
|
|
1529
|
+
const inst = opts.instance;
|
|
1530
|
+
if (!(yield* task.enterImplicitThread(thread)))
|
|
1531
|
+
return;
|
|
1532
|
+
const cx = new LiftLowerContext(cabiOptions(opts), inst, task);
|
|
1533
|
+
const args = task.start();
|
|
1534
|
+
const flatArgs = lowerFlatValues(cx, MAX_FLAT_PARAMS, args, ft.params);
|
|
1535
|
+
if (!opts.async) {
|
|
1536
|
+
const flatResults = normalizeCoreValues(yield* awaitCore(core, flatArgs, thread), opts.coreType.results, `${name} results`);
|
|
1537
|
+
const results = liftFlatValues(cx, MAX_FLAT_RESULTS, new CoreValueIter(flatResults), ft.results);
|
|
1538
|
+
task.return_(results);
|
|
1539
|
+
// Post-return runs after the results were read out of guest memory,
|
|
1540
|
+
// with may_leave cleared (reference canon_lift).
|
|
1541
|
+
const postReturn = require(opts.postReturn, `${name} post-return`);
|
|
1542
|
+
if (postReturn !== null) {
|
|
1543
|
+
assert_(inst.mayLeave, "post-return with may_leave already false");
|
|
1544
|
+
inst.mayLeave = false;
|
|
1545
|
+
callCore(postReturn, flatResults);
|
|
1546
|
+
inst.mayLeave = true;
|
|
1547
|
+
stats.postReturnsRun++;
|
|
1548
|
+
}
|
|
1549
|
+
task.exitImplicitThread(thread);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (opts.callback === null) {
|
|
1553
|
+
// definitions.py line 2179: `[] = call_and_trap_on_throw(callee, flat_args)`
|
|
1554
|
+
// — the guest keeps running on its own stack and blocks inside wasm at
|
|
1555
|
+
// whatever built-in it chooses. There is no return-to-host between the
|
|
1556
|
+
// call and the block, so the only way to model it is genuine wasm-frame
|
|
1557
|
+
// suspension.
|
|
1558
|
+
//
|
|
1559
|
+
// In jspi mode that is exactly what happens and no special handling is
|
|
1560
|
+
// needed: the entry is `promising`-wrapped, so the activation suspends on
|
|
1561
|
+
// whichever blocking built-in it reaches and `awaitCore` parks this thread
|
|
1562
|
+
// until it finishes. Results arrive through `task.return`, so there is
|
|
1563
|
+
// nothing to lift here.
|
|
1564
|
+
if (input.mode !== "jspi") {
|
|
1565
|
+
needsJspi(`stackful async lift of export '${name}' (async canonical options ` +
|
|
1566
|
+
`without a callback)`);
|
|
1567
|
+
}
|
|
1568
|
+
yield* awaitCore(core, flatArgs, thread);
|
|
1569
|
+
task.exitImplicitThread(thread);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
// --- callback ABI (definitions.py lines 2183-2214) ----------------------
|
|
1573
|
+
//
|
|
1574
|
+
// Stackless by construction: every wasm activation *returns* a packed code,
|
|
1575
|
+
// and all waiting happens on the host side between activations. This is the
|
|
1576
|
+
// path wit-bindgen 0.60 emits for every async export, and it needs no JSPI.
|
|
1577
|
+
// The callback export is the second of the three entries that can reach a
|
|
1578
|
+
// blocking built-in (jspi/bridge.ts's invariant), so it is wrapped exactly
|
|
1579
|
+
// like the lifted core. Leaving it plain while the core was promising was a
|
|
1580
|
+
// *mixed* activation, which pin (c) punishes: the first Suspending import
|
|
1581
|
+
// it reached would trap.
|
|
1582
|
+
const callback = enterWasm(require(opts.callback, `${name} callback`), input.mode);
|
|
1583
|
+
const [packed] = normalizeCoreValues(yield* awaitCore(core, flatArgs, thread), opts.coreType.results, `${name} results`);
|
|
1584
|
+
yield* runCallbackLoop({ name, task, thread, inst, callback, packed, stats });
|
|
1585
|
+
task.exitImplicitThread(thread);
|
|
1586
|
+
}
|
|
1587
|
+
// ---------------------------------------------------------------------------
|
|
1588
|
+
// canon lower
|
|
1589
|
+
// ---------------------------------------------------------------------------
|
|
1590
|
+
/**
|
|
1591
|
+
* Build the core-callable body for one lowered host import (reference
|
|
1592
|
+
* `canon_lower`, definitions.py line 2242).
|
|
1593
|
+
*
|
|
1594
|
+
* Sync and async lowers share one `Subtask` and one pair of
|
|
1595
|
+
* `on_start`/`on_resolve` closures, exactly as the reference does; the sync
|
|
1596
|
+
* case is the degenerate one where the callee resolves before returning.
|
|
1597
|
+
*
|
|
1598
|
+
* The host callee is a plain JS function. If it returns a **Promise**, the
|
|
1599
|
+
* subtask resolves when that promise settles:
|
|
1600
|
+
*
|
|
1601
|
+
* * async lower — fully supported and JSPI-free. The guest gets a STARTED
|
|
1602
|
+
* subtask back, joins it to a waitable set, returns WAIT from its
|
|
1603
|
+
* callback, and the scheduler delivers the SUBTASK event once the promise
|
|
1604
|
+
* settles. This is the flagship capability of this phase: an ordinary
|
|
1605
|
+
* `async` JS function is a valid Component Model async import.
|
|
1606
|
+
* * sync lower — the guest's wasm frame would have to block
|
|
1607
|
+
* (`thread.wait_until(subtask.resolved)`, line 2286), so: `needsJspi`.
|
|
1608
|
+
*/
|
|
1609
|
+
export function createLoweredImport(input) {
|
|
1610
|
+
const { name, ft, opts, hostFn, stats, mode, suspendable } = input;
|
|
1611
|
+
const inst = opts.instance;
|
|
1612
|
+
const store = inst.store;
|
|
1613
|
+
const computed = flattenFunctype(cabiOptions(opts), ft, "lower");
|
|
1614
|
+
if (!coreFuncTypeEquals(computed, opts.coreType)) {
|
|
1615
|
+
throw new PlanError(`import '${name}': computed flat type ${JSON.stringify(computed)} ` +
|
|
1616
|
+
`!= plan coreType ${JSON.stringify(opts.coreType)}`);
|
|
1617
|
+
}
|
|
1618
|
+
// definitions.py lines 2250-2256.
|
|
1619
|
+
const maxFlatParams = opts.async ? MAX_FLAT_ASYNC_PARAMS : MAX_FLAT_PARAMS;
|
|
1620
|
+
const maxFlatResults = opts.async ? 0 : MAX_FLAT_RESULTS;
|
|
1621
|
+
return (...rawFlatArgs) => {
|
|
1622
|
+
stats.loweredCalls++;
|
|
1623
|
+
// Reference canon_lower: trap_if(!inst.may_leave).
|
|
1624
|
+
trapIf(!inst.mayLeave, `cannot leave component instance ${inst.index} (may_leave violation)`);
|
|
1625
|
+
const subtask = new Subtask();
|
|
1626
|
+
const cx = new LiftLowerContext(cabiOptions(opts), inst, subtask);
|
|
1627
|
+
const vi = new CoreValueIter(normalizeCoreValues(rawFlatArgs, opts.coreType.params, `${name} args`));
|
|
1628
|
+
/**
|
|
1629
|
+
* definitions.py's `maybe_on_progress`: a no-op until the subtask has been
|
|
1630
|
+
* given a handle index, then the pending-event setter. Assigning it only
|
|
1631
|
+
* after the callee returned unresolved is deliberate in the reference —
|
|
1632
|
+
* an eagerly-resolving callee must never produce an event.
|
|
1633
|
+
*/
|
|
1634
|
+
let onProgress = () => { };
|
|
1635
|
+
const onStart = () => {
|
|
1636
|
+
onProgress();
|
|
1637
|
+
assert_(subtask.state === SubtaskState.STARTING, `${name}: on_start on a started subtask`);
|
|
1638
|
+
subtask.state = SubtaskState.STARTED;
|
|
1639
|
+
return liftFlatValues(cx, maxFlatParams, vi, ft.params);
|
|
1640
|
+
};
|
|
1641
|
+
const onResolve = (result) => {
|
|
1642
|
+
onProgress();
|
|
1643
|
+
if (result === null) {
|
|
1644
|
+
assert_(subtask.cancellationRequested, `${name}: resolved as cancelled without a cancellation request`);
|
|
1645
|
+
subtask.resolve(subtask.state === SubtaskState.STARTING
|
|
1646
|
+
? SubtaskState.CANCELLED_BEFORE_STARTED
|
|
1647
|
+
: SubtaskState.CANCELLED_BEFORE_RETURNED, []);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
assert_(subtask.state === SubtaskState.STARTED, `${name}: on_resolve on a subtask that never started`);
|
|
1651
|
+
// Spilled results use the trailing retptr lane(s) of the flat args
|
|
1652
|
+
// (reference passes the same iterator as out_param).
|
|
1653
|
+
const flatResults = lowerFlatValues(cx, maxFlatResults, result, ft.results, vi);
|
|
1654
|
+
subtask.resolve(SubtaskState.RETURNED, flatResults);
|
|
1655
|
+
};
|
|
1656
|
+
// --- invoke the host callee (the reference's `callee(...)`, line 2283) --
|
|
1657
|
+
//
|
|
1658
|
+
// definitions.py assigns the callee's `OnCancel` here:
|
|
1659
|
+
// `subtask.on_cancel = callee(on_start, on_resolve, caller = ...)`
|
|
1660
|
+
//
|
|
1661
|
+
// A host import is a plain JS function and offers no cancellation
|
|
1662
|
+
// channel — there is nothing to forward a request to. The faithful model
|
|
1663
|
+
// is therefore a handler that *accepts and ignores* the request, which is
|
|
1664
|
+
// exactly what the reference permits: `canon_subtask_cancel` (line 2469)
|
|
1665
|
+
// calls `on_cancel` and then re-checks `subtask.resolved()`; a callee that
|
|
1666
|
+
// declines to cancel promptly leaves the subtask unresolved, and the async
|
|
1667
|
+
// form returns BLOCKED while the sync form waits. The subtask still
|
|
1668
|
+
// resolves normally when the promise settles — cancellation is a request,
|
|
1669
|
+
// not a guarantee.
|
|
1670
|
+
//
|
|
1671
|
+
// Leaving `on_cancel` null instead made a *legal* `subtask.cancel` crash
|
|
1672
|
+
// with an internal AssertionError, which is neither reference behaviour
|
|
1673
|
+
// nor a sanctioned incompleteness signal.
|
|
1674
|
+
subtask.onCancel = () => { };
|
|
1675
|
+
const args = onStart();
|
|
1676
|
+
const raw = hostFn(...args);
|
|
1677
|
+
const toResults = (v) => ft.results.length === 0 ? [] : [v];
|
|
1678
|
+
if (isPromiseLike(raw)) {
|
|
1679
|
+
if (!opts.async) {
|
|
1680
|
+
if (mode !== "jspi" || !suspendable) {
|
|
1681
|
+
// definitions.py line 2286: `thread.wait_until(subtask.resolved)` —
|
|
1682
|
+
// blocking the calling *wasm frame*. Parking needs BOTH jspi mode
|
|
1683
|
+
// and the embedder's per-declaration `suspending()` marker: the
|
|
1684
|
+
// Suspending wrap is applied per-declaration (`importValue`), so an
|
|
1685
|
+
// unmarked import physically cannot suspend, whatever the mode.
|
|
1686
|
+
//
|
|
1687
|
+
// A capability signal is expressly NON-poisoning (amendment 2, #91
|
|
1688
|
+
// scope clarification): the caller keeps running, so the borrows
|
|
1689
|
+
// `onStart` lifted into this subtask must be discharged here or
|
|
1690
|
+
// its lenders stay elevated forever and later `resource.drop`s
|
|
1691
|
+
// trap "handle still lent out" on a healthy instance (found
|
|
1692
|
+
// during the #106 closure; same class as the fact_calls.ts #91
|
|
1693
|
+
// sites).
|
|
1694
|
+
subtask.unwindLenders();
|
|
1695
|
+
needsJspi(suspendable
|
|
1696
|
+
? `synchronous lower of import '${name}', whose host ` +
|
|
1697
|
+
`implementation returned a Promise (the guest's wasm frame ` +
|
|
1698
|
+
`must block)`
|
|
1699
|
+
: `synchronous lower of import '${name}', whose host ` +
|
|
1700
|
+
`implementation returned a Promise; a sync-typed import may ` +
|
|
1701
|
+
`only park the frame when declared with suspending() ` +
|
|
1702
|
+
`(contracts/embedder-api.md §"Functions and async")`);
|
|
1703
|
+
}
|
|
1704
|
+
// The park (A1): the reference's plain, NON-cancellable wait — a
|
|
1705
|
+
// cancel request against the caller stays pending-cancel and is
|
|
1706
|
+
// delivered at its next cancellable wait, exactly as for any other
|
|
1707
|
+
// mid-frame block. The instance-entry gate stays HELD across the park
|
|
1708
|
+
// (the #43 hold rule; see `blockCurrentActivation`'s GATE LIFETIME
|
|
1709
|
+
// note).
|
|
1710
|
+
//
|
|
1711
|
+
// The settle handler only RECORDS the outcome. All CABI work —
|
|
1712
|
+
// `onResolve`'s result lowering (which may re-enter the guest through
|
|
1713
|
+
// realloc) and `deliverResolve` — is deferred to `produce`, which
|
|
1714
|
+
// runs at resume time under the suspension point's ambient claim.
|
|
1715
|
+
// Lowering from the bare promise continuation instead would execute
|
|
1716
|
+
// guest code in an unattributed chunk — the issue-#24 class the
|
|
1717
|
+
// attribution sentinels exist to prevent.
|
|
1718
|
+
let outcome;
|
|
1719
|
+
// The async arm runs `onResolve` — result lowering, including possible
|
|
1720
|
+
// realloc re-entry into the guest — in this bare promise continuation,
|
|
1721
|
+
// where the sync arm above defers all CABI work to `produce` (the
|
|
1722
|
+
// issue-#24 attribution note). The asymmetry is deliberate (#93): here
|
|
1723
|
+
// no wasm frame is suspended mid-call — the guest returned BLOCKED and
|
|
1724
|
+
// is between activations, which is exactly when the reference's
|
|
1725
|
+
// `on_resolve` runs (the callee's turn), so there is no activation for
|
|
1726
|
+
// the sentinels to attribute this chunk to. Lowering failures are host
|
|
1727
|
+
// failures, not guest traps: they land on `store.hostFailure` and the
|
|
1728
|
+
// driving loop raises them site-named (pinned by
|
|
1729
|
+
// tests/async_lower_onresolve_failure_test.ts).
|
|
1730
|
+
const promise = Promise.resolve(raw).then((v) => {
|
|
1731
|
+
store.pendingHostCalls.delete(promise);
|
|
1732
|
+
outcome = { value: v };
|
|
1733
|
+
}, (e) => {
|
|
1734
|
+
store.pendingHostCalls.delete(promise);
|
|
1735
|
+
outcome = { error: e };
|
|
1736
|
+
});
|
|
1737
|
+
// Registered so the driver's deadlock probe counts this park as
|
|
1738
|
+
// externally-wakeable (driveAsync: `pendingHostCalls.size === 0` is a
|
|
1739
|
+
// precondition of the deadlock verdict) and so teardown can observe
|
|
1740
|
+
// the outstanding call, mirroring the async arm below.
|
|
1741
|
+
store.pendingHostCalls.add(promise);
|
|
1742
|
+
// LENDER DISCHARGE ON EVERY SETTLE PATH (#106, the sibling of the
|
|
1743
|
+
// fact_calls.ts sync-start park's #102 enumeration):
|
|
1744
|
+
//
|
|
1745
|
+
// * produce SUCCESS -> `onResolve` + `deliverResolve` release the
|
|
1746
|
+
// lenders; the `onSettled` backstop below observes
|
|
1747
|
+
// `resolveDelivered()` and is a no-op.
|
|
1748
|
+
// * produce THROW -> exempt-by-poisoning under amendment 2
|
|
1749
|
+
// (contracts/intrinsics.md v0.2 §2: release is owed only on exits
|
|
1750
|
+
// that do NOT poison the caller). Every rejection that reaches
|
|
1751
|
+
// this park is a poisoning trap in the CALLER's own frame:
|
|
1752
|
+
// branded `ComponentException`s on fallible imports were already resolved
|
|
1753
|
+
// into err-shaped VALUES by the conventions layer
|
|
1754
|
+
// (embedder/instantiate.ts `#wrapImportFn`'s `fail` — they take
|
|
1755
|
+
// the success arm above), every other conventions-layer throw is
|
|
1756
|
+
// a `Trap`, and a raw-executor rejection is a declared host bug
|
|
1757
|
+
// that traps (empirical fact (e)). No capability signal can
|
|
1758
|
+
// originate inside `produce`: this park only exists once jspi +
|
|
1759
|
+
// `suspending()` were both granted. The backstop's unwind here is
|
|
1760
|
+
// belt-and-braces bookkeeping on a poisoned instance, not an
|
|
1761
|
+
// obligation.
|
|
1762
|
+
// * abandon -> produce never runs, and an abandoned park
|
|
1763
|
+
// does NOT poison the caller (pinned by
|
|
1764
|
+
// resource_lender_park_settle_test.ts) — without the hook the
|
|
1765
|
+
// subtask's lenders stayed elevated forever and later
|
|
1766
|
+
// `resource.drop`s trapped "handle still lent out". The hook is
|
|
1767
|
+
// the fix.
|
|
1768
|
+
return blockCurrentActivation({
|
|
1769
|
+
store,
|
|
1770
|
+
task: currentTask(),
|
|
1771
|
+
readyFunc: () => outcome !== undefined,
|
|
1772
|
+
cancellable: false,
|
|
1773
|
+
produce: () => {
|
|
1774
|
+
const done = outcome;
|
|
1775
|
+
if ("error" in done) {
|
|
1776
|
+
// A rejection of a sync-typed import is a host failure: it
|
|
1777
|
+
// reaches the guest as a rejection of the import's Promise,
|
|
1778
|
+
// which the engine turns back into a wasm trap (empirical
|
|
1779
|
+
// fact (e); `SuspensionPoint` routes a produce-throw through
|
|
1780
|
+
// exactly that path). Branded `ComponentException`s never reach the raw
|
|
1781
|
+
// boundary — the conventions layer resolves them into
|
|
1782
|
+
// err-shaped values one layer up (see the settle-path
|
|
1783
|
+
// enumeration above).
|
|
1784
|
+
throw done.error;
|
|
1785
|
+
}
|
|
1786
|
+
onResolve(toResults(done.value));
|
|
1787
|
+
subtask.deliverResolve();
|
|
1788
|
+
assert_(vi.done(), `${name}: unconsumed flat arguments`);
|
|
1789
|
+
const flatResults = subtask.flatResults;
|
|
1790
|
+
if (flatResults.length === 0)
|
|
1791
|
+
return undefined;
|
|
1792
|
+
if (flatResults.length === 1)
|
|
1793
|
+
return flatResults[0];
|
|
1794
|
+
return flatResults;
|
|
1795
|
+
},
|
|
1796
|
+
onSettled: () => subtask.unwindLenders(),
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
const promise = Promise.resolve(raw).then((v) => {
|
|
1800
|
+
store.pendingHostCalls.delete(promise);
|
|
1801
|
+
try {
|
|
1802
|
+
onResolve(toResults(v));
|
|
1803
|
+
}
|
|
1804
|
+
catch (e) {
|
|
1805
|
+
store.hostFailure = e;
|
|
1806
|
+
}
|
|
1807
|
+
}, (e) => {
|
|
1808
|
+
store.pendingHostCalls.delete(promise);
|
|
1809
|
+
store.hostFailure = e;
|
|
1810
|
+
});
|
|
1811
|
+
store.pendingHostCalls.add(promise);
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
onResolve(toResults(raw));
|
|
1815
|
+
}
|
|
1816
|
+
// definitions.py line 2284: a sync-*typed* callee must have resolved.
|
|
1817
|
+
assert_(ft.async || subtask.resolved(), `${name}: a non-async-typed import must resolve before returning`);
|
|
1818
|
+
if (!opts.async) {
|
|
1819
|
+
if (!subtask.resolved()) {
|
|
1820
|
+
needsJspi(`synchronous lower of import '${name}' on an unresolved subtask`);
|
|
1821
|
+
}
|
|
1822
|
+
subtask.deliverResolve();
|
|
1823
|
+
assert_(vi.done(), `${name}: unconsumed flat arguments`);
|
|
1824
|
+
const flatResults = subtask.flatResults;
|
|
1825
|
+
if (flatResults.length === 0)
|
|
1826
|
+
return undefined;
|
|
1827
|
+
if (flatResults.length === 1)
|
|
1828
|
+
return flatResults[0];
|
|
1829
|
+
return flatResults;
|
|
1830
|
+
}
|
|
1831
|
+
// --- async lower (definitions.py lines 2289-2309) ----------------------
|
|
1832
|
+
if (subtask.resolved()) {
|
|
1833
|
+
// Eager-resolve fast path: no handle, no event, no waitable — the guest
|
|
1834
|
+
// learns the call is done from the return value alone.
|
|
1835
|
+
subtask.deliverResolve();
|
|
1836
|
+
assert_(subtask.flatResults.length === 0, `${name}: async lower produced flat results`);
|
|
1837
|
+
return SubtaskState.RETURNED;
|
|
1838
|
+
}
|
|
1839
|
+
const subtaski = inst.handles.add(subtask);
|
|
1840
|
+
onProgress = () => subtask.setSubtaskPendingEvent(subtaski);
|
|
1841
|
+
return packSubtaskResult(subtask.state, subtaski);
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* The callback-ABI dispatch loop of `canon_lift` (definitions.py lines
|
|
1846
|
+
* 2183-2214), factored out so both entry points share one implementation:
|
|
1847
|
+
*
|
|
1848
|
+
* * a host-boundary lift (`liftBody` above), and
|
|
1849
|
+
* * a FACT cross-component call, where the host invokes an async-lifted
|
|
1850
|
+
* callee on the caller's behalf (`intrinsics/fact_calls.ts`).
|
|
1851
|
+
*
|
|
1852
|
+
* `packed` is the code the *initial* activation returned; the loop runs until
|
|
1853
|
+
* it sees EXIT, invoking the callback export with each delivered event.
|
|
1854
|
+
*/
|
|
1855
|
+
export function* runCallbackLoop(input) {
|
|
1856
|
+
const { name, task, thread, inst, callback, stats } = input;
|
|
1857
|
+
let [code, si] = unpackCallbackResult(input.packed);
|
|
1858
|
+
while (code !== CallbackCode.EXIT) {
|
|
1859
|
+
// definitions.py line 2187, verbatim shape: the implicit thread of a
|
|
1860
|
+
// needs-exclusive callback task holds the slot on every loop iteration.
|
|
1861
|
+
// (The former per-iteration `holding` check tolerated a resolved task
|
|
1862
|
+
// that had released the slot at a mid-frame block — the release-at-BLOCK
|
|
1863
|
+
// divergence removed by issue #43. Under the hold rule, which is both the
|
|
1864
|
+
// reference's and wasmtime's — `do_not_enter` is set for each callback
|
|
1865
|
+
// invocation, concurrent.rs :942/:960 — the invariant is unconditional.)
|
|
1866
|
+
assert_(task.needsExclusive() &&
|
|
1867
|
+
inst.exclusiveThread === task.implicitThread, "callback loop without holding the exclusive thread");
|
|
1868
|
+
// Releasing the exclusive thread across the wait is what lets *another*
|
|
1869
|
+
// task of the same instance enter and run while this one waits — the
|
|
1870
|
+
// whole point of the callback ABI (definitions.py line 2188). Equally,
|
|
1871
|
+
// RETAKING it below is what defers event delivery to a parked-between-
|
|
1872
|
+
// invocations task while any invocation of this instance is mid-frame:
|
|
1873
|
+
// the `() => inst.exclusiveThread === null` guard on the wait is the
|
|
1874
|
+
// reference's `wait_for_event_and(lambda: not inst.exclusive_thread)`
|
|
1875
|
+
// (line 2199) and wasmtime's `GuestCall::is_ready` DeliverEvent arm,
|
|
1876
|
+
// which requires `!do_not_enter` (concurrent.rs :765).
|
|
1877
|
+
inst.exclusiveThread = null;
|
|
1878
|
+
let event;
|
|
1879
|
+
switch (code) {
|
|
1880
|
+
case CallbackCode.YIELD: {
|
|
1881
|
+
const cancelled = yield* thread.waitUntil(() => inst.exclusiveThread === null, true);
|
|
1882
|
+
event = cancelled
|
|
1883
|
+
? [EventCode.TASK_CANCELLED, 0, 0]
|
|
1884
|
+
: [EventCode.NONE, 0, 0];
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
case CallbackCode.WAIT: {
|
|
1888
|
+
const wset = inst.handles.get(si);
|
|
1889
|
+
trapIf(!(wset instanceof WaitableSet), `callback returned WAIT with index ${si}, which is not a waitable set`);
|
|
1890
|
+
event = yield* wset.waitForEventAnd(thread, () => inst.exclusiveThread === null, true);
|
|
1891
|
+
break;
|
|
1892
|
+
}
|
|
1893
|
+
default:
|
|
1894
|
+
trap(`invalid callback code ${code}`);
|
|
1895
|
+
}
|
|
1896
|
+
assert_(inst.exclusiveThread === null, "exclusive thread taken while this task was waiting");
|
|
1897
|
+
inst.exclusiveThread = task.implicitThread;
|
|
1898
|
+
stats.callbackInvocations++;
|
|
1899
|
+
const [next] = normalizeCoreValues(yield* awaitCore(callback, [event[0], event[1], event[2]], thread), ["i32"], `${name} callback result`);
|
|
1900
|
+
[code, si] = unpackCallbackResult(next);
|
|
1901
|
+
}
|
|
1902
|
+
}
|