@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,865 @@
|
|
|
1
|
+
// FACT cross-component call intrinsics: `prepare-call`, `sync-start-call` and
|
|
2
|
+
// `async-start-call` (contracts/intrinsics.md §A).
|
|
3
|
+
//
|
|
4
|
+
// These are how one component calls another's *async-lifted* export, or how an
|
|
5
|
+
// async-lowered import reaches any export. They have no direct analogue in
|
|
6
|
+
// definitions.py, because the reference has no fused adapters: there,
|
|
7
|
+
// `canon_lower` calls the callee's `FuncInst` directly and the host performs
|
|
8
|
+
// every copy. wasmtime instead compiles a FACT adapter that hoists the copying
|
|
9
|
+
// into wasm and asks the host to do only the task bookkeeping. The *semantics*
|
|
10
|
+
// are the reference's; only the division of labour differs.
|
|
11
|
+
//
|
|
12
|
+
// ===========================================================================
|
|
13
|
+
// THE PROTOCOL (wasmtime-environ 47.0.3)
|
|
14
|
+
// ===========================================================================
|
|
15
|
+
//
|
|
16
|
+
// Emission sites: `fact/trampoline.rs` — `call_prepare` (line 513),
|
|
17
|
+
// `compile_async_to_async_adapter` (474), `compile_sync_to_async_adapter`
|
|
18
|
+
// (607), `compile_async_to_sync_adapter` (643). Signatures: `fact.rs`
|
|
19
|
+
// `import_prepare_call` (584), `import_sync_start_call` (620),
|
|
20
|
+
// `import_async_start_call` (643), with `PREPARE_CALL_FIXED_PARAMS` at
|
|
21
|
+
// `fact.rs:47`.
|
|
22
|
+
//
|
|
23
|
+
// prepare-call(start: funcref, return: funcref,
|
|
24
|
+
// caller_instance: i32, callee_instance: i32,
|
|
25
|
+
// task_return_type: i32, callee_async: i32,
|
|
26
|
+
// string_encoding: i32, result_count_or_max_if_async: i32,
|
|
27
|
+
// ...caller's own flat params) -> ()
|
|
28
|
+
//
|
|
29
|
+
// sync-start-call (callee: funcref, lift_param_count: i32)
|
|
30
|
+
// -> the caller's flat results
|
|
31
|
+
// async-start-call(callee: funcref, param_count: i32,
|
|
32
|
+
// result_count: i32, flags: i32) -> i32 (packed subtask)
|
|
33
|
+
//
|
|
34
|
+
// **The `start` / `return` funcrefs are the reference's `on_start` /
|
|
35
|
+
// `on_resolve`.** That is the load-bearing finding, and their signatures
|
|
36
|
+
// (`fact/signature.rs`) say so exactly:
|
|
37
|
+
//
|
|
38
|
+
// `[async-start]` (async_start_signature, line 61)
|
|
39
|
+
// params = the *caller's* flattened params (what prepare-call stashed)
|
|
40
|
+
// results = the *callee's* flattened params (hand straight to the callee)
|
|
41
|
+
// i.e. "given the caller's arguments, produce the callee's" — `on_start`.
|
|
42
|
+
//
|
|
43
|
+
// `[async-return]` (async_return_signature, line 145)
|
|
44
|
+
// params = the *callee's* flattened results (+ a retptr when the caller
|
|
45
|
+
// is async-with-results, or when the caller's results spill)
|
|
46
|
+
// results = the *caller's* flattened results (empty if async/spilled)
|
|
47
|
+
// i.e. "given the callee's results, produce the caller's" — `on_resolve`.
|
|
48
|
+
//
|
|
49
|
+
// So the host never inspects a value: it calls `start` to get the callee's
|
|
50
|
+
// arguments, and calls `return` with whatever the callee produced. This is why
|
|
51
|
+
// a FACT task's payload is flat core values (`Task.factPassthrough`).
|
|
52
|
+
//
|
|
53
|
+
// Two details that fall out of the emission sites:
|
|
54
|
+
//
|
|
55
|
+
// * `prepare-call` must NOT run the callee. The callee may be exerting
|
|
56
|
+
// backpressure, and the whole point of splitting prepare from start is to
|
|
57
|
+
// let the host stash the parameters until it clears (fact.rs:580-583).
|
|
58
|
+
// The stashed state feeds `Task.enterImplicitThread`, which is exactly
|
|
59
|
+
// where the reference's backpressure gate lives.
|
|
60
|
+
// * Reentrance between *related* instances is resolved statically:
|
|
61
|
+
// `trampoline.rs:116-127` emits an unconditional
|
|
62
|
+
// `trap(Trap::CannotEnterComponent)` when the lower and lift instances are
|
|
63
|
+
// the same or are ancestors of one another. So the flat-instance-tree gap
|
|
64
|
+
// recorded in task/mod.ts is NOT load-bearing here — wasmtime has already
|
|
65
|
+
// decided those cases at translation time, and the remaining runtime check
|
|
66
|
+
// is the ordinary "is the callee instance currently executing" one, which
|
|
67
|
+
// a flat tree answers correctly.
|
|
68
|
+
import { assert_, trap } from "../cabi/trap.js";
|
|
69
|
+
import { MAX_FLAT_RESULTS } from "../cabi/mod.js";
|
|
70
|
+
import { NeedsJspi, currentTask, maybeCurrentTask, needsJspi, notifyInstancePoisoned, packSubtaskResult, PendingCapability, Subtask, SubtaskState, Task, Thread, withPoisonCause, } from "../task/mod.js";
|
|
71
|
+
import { blockCurrentActivation, enterWasm } from "../jspi/mod.js";
|
|
72
|
+
import { awaitCore, callCore, normalizeCoreValues, runCallbackLoop, } from "../exec/boundary.js";
|
|
73
|
+
import { traceCopy } from "./stream_builtins.js";
|
|
74
|
+
/** `PREPARE_ASYNC_NO_RESULT` (wasmtime-environ `component.rs:39`). */
|
|
75
|
+
const PREPARE_ASYNC_NO_RESULT = 0xffff_ffff;
|
|
76
|
+
/** `PREPARE_ASYNC_WITH_RESULT` (`component.rs:45`). */
|
|
77
|
+
const PREPARE_ASYNC_WITH_RESULT = 0xffff_fffe;
|
|
78
|
+
/** `START_FLAG_ASYNC_CALLEE` (`component.rs:52`). */
|
|
79
|
+
export const START_FLAG_ASYNC_CALLEE = 1;
|
|
80
|
+
/** Number of fixed leading parameters of `prepare-call` (`fact.rs:47`). */
|
|
81
|
+
const PREPARE_FIXED = 8;
|
|
82
|
+
/** definitions.py-shaped canonical options a FACT task must remember. */
|
|
83
|
+
function taskOptionsFor(prepared, callback, memory, calleeUsesAsyncAbi) {
|
|
84
|
+
return {
|
|
85
|
+
// NOTE the distinction definitions.py draws and this code initially got
|
|
86
|
+
// wrong: `Task.ft.async` is the *function type*'s asyncness (what
|
|
87
|
+
// `prepare-call` passes as `callee_async`), while `Task.opts.async_` is
|
|
88
|
+
// the *canonical options*' asyncness — and `canon_lift` branches on the
|
|
89
|
+
// latter (`if not opts.async_:` at line 2168). A function can be
|
|
90
|
+
// async-*typed* yet lifted with sync options, in which case the reference
|
|
91
|
+
// takes its plain synchronous path. Branching on the type instead sent
|
|
92
|
+
// those callees down the stackful path and reported a bogus JSPI
|
|
93
|
+
// requirement (`test/async/cross-abi-calls.wast`'s `async-calls-sync-*`).
|
|
94
|
+
async_: calleeUsesAsyncAbi,
|
|
95
|
+
callback: callback !== null,
|
|
96
|
+
stringEncoding: stringEncodingName(prepared.stringEncoding),
|
|
97
|
+
memory,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* wasmtime's `StringEncoding` discriminant (`component/types.rs`), as passed
|
|
102
|
+
* through `prepare-call`.
|
|
103
|
+
*/
|
|
104
|
+
function stringEncodingName(v) {
|
|
105
|
+
switch (v) {
|
|
106
|
+
case 0:
|
|
107
|
+
return "utf8";
|
|
108
|
+
case 1:
|
|
109
|
+
return "utf16";
|
|
110
|
+
case 2:
|
|
111
|
+
return "latin1+utf16";
|
|
112
|
+
default:
|
|
113
|
+
return "utf8";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// prepare-call
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
export function createPrepareCall(decl, ctx) {
|
|
120
|
+
return (...args) => {
|
|
121
|
+
assert_(args.length >= PREPARE_FIXED, `prepare-call: expected at least ${PREPARE_FIXED} arguments`);
|
|
122
|
+
const [start, return_, callerI, calleeI, taskReturnType, calleeAsync, enc, rc_] = args;
|
|
123
|
+
assert_(typeof start === "function" && typeof return_ === "function", "prepare-call: start/return must be funcrefs");
|
|
124
|
+
assert_(ctx.prepared.current === null, "prepare-call with a preparation already outstanding");
|
|
125
|
+
// GAP (tracked): wasmtime performs a `check_blocking` here —
|
|
126
|
+
// if let (CallerInfo::Sync { .. }, true) = (&caller_info, callee_async) {
|
|
127
|
+
// store.0.check_blocking()?; // concurrent.rs:2802-2807
|
|
128
|
+
// }
|
|
129
|
+
// i.e. a *sync-lowered* caller reaching an *async-typed* callee must
|
|
130
|
+
// itself have been created by an async export, else it traps: only a task
|
|
131
|
+
// that is allowed to block may make a blocking call. We cannot evaluate it
|
|
132
|
+
// yet — it needs a "may this task block" bit on `Task`, which the reference
|
|
133
|
+
// models through its thread/task structure rather than a flag. Its absence
|
|
134
|
+
// means we accept some components wasmtime rejects; it never causes a
|
|
135
|
+
// wrong answer for an accepted one. `test/async/trap-if-block-and-sync.wast`
|
|
136
|
+
// is the file that exercises it, and that file is independently blocked on
|
|
137
|
+
// the wasmparser pin drift, so nothing observable depends on it today.
|
|
138
|
+
const params = args.slice(PREPARE_FIXED);
|
|
139
|
+
const rc = Number(rc_) >>> 0;
|
|
140
|
+
// wasmtime `ResultInfo` (concurrent.rs:2815-2836).
|
|
141
|
+
const lastParam = () => {
|
|
142
|
+
assert_(params.length > 0, "prepare-call: retptr missing");
|
|
143
|
+
return params[params.length - 1];
|
|
144
|
+
};
|
|
145
|
+
let resultInfo;
|
|
146
|
+
let asyncCallerWithResult = false;
|
|
147
|
+
if (rc === PREPARE_ASYNC_WITH_RESULT) {
|
|
148
|
+
resultInfo = { kind: "heap", retptr: lastParam() };
|
|
149
|
+
asyncCallerWithResult = true;
|
|
150
|
+
}
|
|
151
|
+
else if (rc === PREPARE_ASYNC_NO_RESULT) {
|
|
152
|
+
resultInfo = { kind: "stack" };
|
|
153
|
+
}
|
|
154
|
+
else if (rc > MAX_FLAT_RESULTS) {
|
|
155
|
+
// Sync caller whose results spilled: the adapter appended a retptr to
|
|
156
|
+
// its own parameters (`flatten_functype` lower/spill path).
|
|
157
|
+
resultInfo = { kind: "heap", retptr: lastParam() };
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
resultInfo = { kind: "stack" };
|
|
161
|
+
}
|
|
162
|
+
ctx.prepared.current = {
|
|
163
|
+
start: start,
|
|
164
|
+
return_: return_,
|
|
165
|
+
callerInst: ctx.componentInstance(Number(callerI) >>> 0),
|
|
166
|
+
calleeInst: ctx.componentInstance(Number(calleeI) >>> 0),
|
|
167
|
+
taskReturnType: Number(taskReturnType) >>> 0,
|
|
168
|
+
calleeAsync: Number(calleeAsync) !== 0,
|
|
169
|
+
stringEncoding: Number(enc) >>> 0,
|
|
170
|
+
resultCountOrMax: rc,
|
|
171
|
+
params,
|
|
172
|
+
memory: decl.memory === null ? null : ctx.memoryToken(decl.memory),
|
|
173
|
+
resultInfo,
|
|
174
|
+
asyncCallerWithResult,
|
|
175
|
+
};
|
|
176
|
+
// Deliberately does not touch the callee: see the header. The callee may
|
|
177
|
+
// be under backpressure, and `*-start-call` is what runs it.
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// The shared callee activation
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Build the `Task` for a prepared call and the generator body that runs the
|
|
185
|
+
* callee on it. Shared by both `*-start-call` forms; they differ only in how
|
|
186
|
+
* they *wait* for the result.
|
|
187
|
+
*/
|
|
188
|
+
function mkCalleeTask(input) {
|
|
189
|
+
const { prepared, callee, callback, postReturn, ctx, calleeUsesAsyncAbi } = input;
|
|
190
|
+
// CONTRACT: default to `plain` when the context predates this field. Only
|
|
191
|
+
// `jspi` may wrap, and wrapping a non-wasm callee throws outright, so the
|
|
192
|
+
// conservative reading of an absent mode is "no suspension discipline".
|
|
193
|
+
const mode = input.mode ?? "plain";
|
|
194
|
+
// CONTRACT: default false -- a context that cannot answer the question gets
|
|
195
|
+
// the non-wrapping (plain-shaped) behaviour, which is the conservative one:
|
|
196
|
+
// it never forces asynchrony that the ABI forbids.
|
|
197
|
+
const canBlock = input.canBlock ?? false;
|
|
198
|
+
const memory = prepared.memory;
|
|
199
|
+
const inst = prepared.calleeInst;
|
|
200
|
+
// `ft` for the task: only `async` and `results` are consulted —
|
|
201
|
+
// `Task.needsExclusive` reads the former, `canon_task_return`'s result-type
|
|
202
|
+
// check reads the latter (which is why `prepare-call` carries
|
|
203
|
+
// `task_return_type` at all: fact.rs's comment on `PrepareCall.memory` says
|
|
204
|
+
// the same for the memory check).
|
|
205
|
+
// `task_return_type` arrives as wasmtime's *own* `TypeTupleIndex` — a
|
|
206
|
+
// runtime argument, not a plan field. Plan v3 (contracts/plan-format.md v3
|
|
207
|
+
// amendment 3) supplies the dictionary for it: every `task-return`
|
|
208
|
+
// trampoline decl carries that raw index alongside its interned
|
|
209
|
+
// `plan.types` entry, so the callee task CAN now carry its declared result
|
|
210
|
+
// types and `canon_task_return`'s `trap_if(result_type != task.ft.result)`
|
|
211
|
+
// applies to FACT tasks too (async_builtins.ts).
|
|
212
|
+
//
|
|
213
|
+
// `null` = the plan has no `task.return` trampoline for this tuple, i.e.
|
|
214
|
+
// the callee cannot call `task.return` at all (a sync-lifted callee reached
|
|
215
|
+
// through an async-to-sync adapter). Then the check has nothing to compare
|
|
216
|
+
// against and stays skipped — flagged by `factResultTypesKnown` rather than
|
|
217
|
+
// by an empty-results coincidence.
|
|
218
|
+
const declaredResults = ctx.resultTypesForTuple(prepared.taskReturnType);
|
|
219
|
+
const ft = {
|
|
220
|
+
params: [],
|
|
221
|
+
results: declaredResults ?? [],
|
|
222
|
+
async: prepared.calleeAsync,
|
|
223
|
+
};
|
|
224
|
+
const task = new Task(ft, taskOptionsFor(prepared, callback, memory, calleeUsesAsyncAbi), inst,
|
|
225
|
+
// on_start: the adapter's `[async-start]` turns the caller's flat params
|
|
226
|
+
// into the callee's flat params (fact/signature.rs:61).
|
|
227
|
+
//
|
|
228
|
+
// An async caller that has a result passes its retptr as the *last* flat
|
|
229
|
+
// parameter; `[async-start]` does not declare it, so it is chopped off
|
|
230
|
+
// here exactly as wasmtime does (concurrent.rs:2869-2876, "Async callers,
|
|
231
|
+
// if they have a result, use the last parameter as a return pointer so
|
|
232
|
+
// chop that off"). Sync callers forward everything directly.
|
|
233
|
+
() => {
|
|
234
|
+
// Open the FACT borrow window for the duration of the copy adapter:
|
|
235
|
+
// `[async-start]` is where argument resource transfers run, and it
|
|
236
|
+
// cannot block (see the WASM-ENTRY note below), so push/pop brackets a
|
|
237
|
+
// strictly synchronous window. Borrow bookkeeping lands on this
|
|
238
|
+
// (callee) task's `numBorrows` and the caller's lender scope — see
|
|
239
|
+
// intrinsics/mod.ts `FactStartScope`.
|
|
240
|
+
ctx.factStartScopes.push({ taskScope: task, lenders: input.lenderScope });
|
|
241
|
+
let calleeArgs;
|
|
242
|
+
try {
|
|
243
|
+
calleeArgs = callCore(prepared.start, prepared.asyncCallerWithResult
|
|
244
|
+
? prepared.params.slice(0, -1)
|
|
245
|
+
: prepared.params);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
ctx.factStartScopes.pop();
|
|
249
|
+
}
|
|
250
|
+
input.onStarted?.();
|
|
251
|
+
return calleeArgs;
|
|
252
|
+
},
|
|
253
|
+
// on_resolve: the adapter's `[async-return]` turns the callee's flat
|
|
254
|
+
// results into the caller's (fact/signature.rs:145).
|
|
255
|
+
(result) => {
|
|
256
|
+
if (result === null) {
|
|
257
|
+
// Cancelled before returning: there is nothing for `[async-return]`
|
|
258
|
+
// to copy. The subtask's CANCELLED_BEFORE_* state carries the news,
|
|
259
|
+
// so signal it rather than a normal empty result.
|
|
260
|
+
input.onCallerResults(null);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// `[async-return]` takes the callee's flat results and, when the
|
|
264
|
+
// caller's results live in linear memory, the caller-supplied return
|
|
265
|
+
// pointer as a trailing argument (fact/signature.rs:166,178; appended by
|
|
266
|
+
// wasmtime at concurrent.rs:2916-2919). Omitting it made the adapter
|
|
267
|
+
// read `undefined` for that parameter, which coerces to 0 — every
|
|
268
|
+
// spilled result was written to linear-memory address 0.
|
|
269
|
+
const args = result;
|
|
270
|
+
const withRetptr = prepared.resultInfo.kind === "heap"
|
|
271
|
+
? [...args, prepared.resultInfo.retptr]
|
|
272
|
+
: args;
|
|
273
|
+
input.onCallerResults(callCore(prepared.return_, withRetptr));
|
|
274
|
+
});
|
|
275
|
+
task.factPassthrough = true;
|
|
276
|
+
task.factResultTypesKnown = declaredResults !== null;
|
|
277
|
+
const body = function* (thread) {
|
|
278
|
+
if (!(yield* task.enterImplicitThread(thread)))
|
|
279
|
+
return;
|
|
280
|
+
const calleeArgs = task.start();
|
|
281
|
+
traceCopy(`mkCalleeTask callee canBlock=${canBlock} mode=${mode}`);
|
|
282
|
+
// WASM ENTRY (3 of 3 that can reach a blocking built-in).
|
|
283
|
+
//
|
|
284
|
+
// The other two — a lifted export's core function and a callback export —
|
|
285
|
+
// are entered through `awaitCore`, which establishes the
|
|
286
|
+
// activation-attached ambient. This one was not, and it is precisely the
|
|
287
|
+
// entry that owns a FACT sync-call bracket: `enter-sync-call` runs here
|
|
288
|
+
// under this task, and if the callee suspends, the engine resumes it later
|
|
289
|
+
// with no driver. Without the ambient travelling with the activation the
|
|
290
|
+
// matching `exit-sync-call` had no task in scope at all (traced in M2
|
|
291
|
+
// phase 3h as `ENTER-SYNC owner=K26` / `EXIT-SYNC owner=EXECUTOR`).
|
|
292
|
+
//
|
|
293
|
+
// Entries deliberately NOT wrapped: `realloc`, `post-return`, resource
|
|
294
|
+
// destructors and the `[async-start]`/`[async-return]` copy adapters.
|
|
295
|
+
// None of them may block — they cannot reach a canonical built-in that
|
|
296
|
+
// suspends — so the engine can never resume them, and wrapping would only
|
|
297
|
+
// cost an ALS frame on the hot copy path.
|
|
298
|
+
// The callee is its own activation and must get its own `promising`
|
|
299
|
+
// entry, not merely an ambient scope: otherwise it runs *inside* whatever
|
|
300
|
+
// `Suspending` trampoline invoked us, putting our JS frame between the
|
|
301
|
+
// caller's promising entry and any suspension the callee reaches --
|
|
302
|
+
// `SuspendError: trying to suspend JS frames` (jspi pin (b), mechanics.ts
|
|
303
|
+
// line 12). This is only coherent together with site 1 below blocking
|
|
304
|
+
// rather than raising `NeedsJspi`, since a promising callee resolves on a
|
|
305
|
+
// later turn by construction.
|
|
306
|
+
// NOTE (M2 stackful round): this wrap is RIGHT for a callee that blocks and
|
|
307
|
+
// Wrap ONLY a callee that can actually reach a suspension point.
|
|
308
|
+
//
|
|
309
|
+
// The wrap is required when the callee blocks: without its own `promising`
|
|
310
|
+
// entry it would suspend inside whatever `Suspending` trampoline invoked
|
|
311
|
+
// us, with our JS frame in between (`SuspendError: trying to suspend JS
|
|
312
|
+
// frames`, jspi pin (b)). But `enterWasm` returns a Promise
|
|
313
|
+
// unconditionally, so wrapping a callee that CANNOT block forces
|
|
314
|
+
// asynchrony the ABI forbids: an eagerly-completing callee must report its
|
|
315
|
+
// subtask RETURNED, and a wrapped one reports STARTED. That broke all six
|
|
316
|
+
// `async-calls-sync-*` cases of cross-abi-calls.wast.
|
|
317
|
+
//
|
|
318
|
+
// There is no per-CALL discriminator -- the same call site serves both --
|
|
319
|
+
// so the answer is per-callee, derived from whether the callee's core
|
|
320
|
+
// instance imports any blocking trampoline (`Executor.suspendableFuncs`).
|
|
321
|
+
const raw = yield* awaitCore(canBlock ? enterWasm(callee, mode) : callee, calleeArgs, thread);
|
|
322
|
+
if (!calleeUsesAsyncAbi) {
|
|
323
|
+
// Sync canonical options (definitions.py `canon_lift` line 2168, `if not
|
|
324
|
+
// opts.async_`): the callee returns its results directly and resolves
|
|
325
|
+
// before returning. Reached via `compile_async_to_sync_adapter`, which
|
|
326
|
+
// passes flags without `START_FLAG_ASYNC_CALLEE`.
|
|
327
|
+
task.return_(raw);
|
|
328
|
+
if (postReturn !== null) {
|
|
329
|
+
assert_(inst.mayLeave, "post-return with may_leave already false");
|
|
330
|
+
inst.mayLeave = false;
|
|
331
|
+
// NO local try/finally here, deliberately (#91, verified rather than
|
|
332
|
+
// assumed). definitions.py `canon_lift` (lines 2170-2174) has the
|
|
333
|
+
// same bare bracket: a trapping post-return skips `may_leave = True`
|
|
334
|
+
// and, since `Store.lift`'s `leave_to` is also skipped, leaves the
|
|
335
|
+
// instance poisoned — restoring `may_leave` locally would contradict
|
|
336
|
+
// both. What this runtime additionally needs, because it supports
|
|
337
|
+
// post-trap re-entry, is that no *live* instance is stranded with
|
|
338
|
+
// `may_leave === false`; exec/boundary.ts `unwind` covers exactly
|
|
339
|
+
// that: at the host boundary no lift or lower is in flight, so it
|
|
340
|
+
// asserts that resting state for every instance outside the poisoned
|
|
341
|
+
// entered set. This instance is either in that set (poisoned, left
|
|
342
|
+
// as the trap left it) or restored there.
|
|
343
|
+
callCore(postReturn, raw);
|
|
344
|
+
inst.mayLeave = true;
|
|
345
|
+
ctx.stats.postReturnsRun++;
|
|
346
|
+
}
|
|
347
|
+
task.exitImplicitThread(thread);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (callback === null) {
|
|
351
|
+
// Stackful async lift -- definitions.py `canon_lift` line 2178:
|
|
352
|
+
//
|
|
353
|
+
// if not opts.callback:
|
|
354
|
+
// [] = call_and_trap_on_throw(callee, flat_args)
|
|
355
|
+
// task.exit_implicit_thread()
|
|
356
|
+
// return
|
|
357
|
+
//
|
|
358
|
+
// That is the whole path. The callee runs to completion on its own
|
|
359
|
+
// stack, returning NO results and calling `task.return` itself; any
|
|
360
|
+
// blocking happened *inside* it, through the canonical built-ins. Which
|
|
361
|
+
// is exactly what the callee's own `promising` entry provides when it
|
|
362
|
+
// can block -- the `awaitCore` above parks the CALLEE's thread, not the
|
|
363
|
+
// caller's, so nothing here parks an async-lowered caller (the mistake
|
|
364
|
+
// the cross-abi differential caught).
|
|
365
|
+
normalizeCoreValues(raw, [], "stackful callee result");
|
|
366
|
+
task.exitImplicitThread(thread);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const [packed] = normalizeCoreValues(raw, ["i32"], "callee result");
|
|
370
|
+
yield* runCallbackLoop({
|
|
371
|
+
name: "fact-callee",
|
|
372
|
+
task,
|
|
373
|
+
thread,
|
|
374
|
+
inst,
|
|
375
|
+
// The callback re-entry is the second of the three entries that can
|
|
376
|
+
// reach a blocking built-in (jspi/bridge.ts's invariant) and gets the
|
|
377
|
+
// same per-callee treatment as the initial entry above: a callee that
|
|
378
|
+
// parks (WAIT) and then, on a later callback activation, reaches a
|
|
379
|
+
// *synchronous* blocking built-in (wit-bindgen's `block_on` shape —
|
|
380
|
+
// e.g. a composed iroh endpoint signing a CertificateVerify via
|
|
381
|
+
// `waitable-set.wait` mid-handshake) suspends inside the plain
|
|
382
|
+
// callback frame otherwise: `SuspendError` (jspi pin (c)). Caught by
|
|
383
|
+
// the first composed consumer workload (wosh client), not by
|
|
384
|
+
// cross-abi-calls.wast, whose callees only ever block via WAIT codes.
|
|
385
|
+
callback: canBlock ? enterWasm(callback, mode) : callback,
|
|
386
|
+
packed,
|
|
387
|
+
stats: ctx.stats,
|
|
388
|
+
});
|
|
389
|
+
task.exitImplicitThread(thread);
|
|
390
|
+
};
|
|
391
|
+
return { task, body };
|
|
392
|
+
}
|
|
393
|
+
/** Take the outstanding preparation, or trap if the adapter skipped it. */
|
|
394
|
+
function takePrepared(ctx, what) {
|
|
395
|
+
const p = ctx.prepared.current;
|
|
396
|
+
assert_(p !== null, `${what} without a preceding prepare-call`);
|
|
397
|
+
ctx.prepared.current = null;
|
|
398
|
+
return p;
|
|
399
|
+
}
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
// sync-start-call
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
/**
|
|
404
|
+
* A sync-lowered import calling an async-lifted export
|
|
405
|
+
* (`compile_sync_to_async_adapter`, trampoline.rs:607). The caller's wasm frame
|
|
406
|
+
* is blocked for the duration, so this must produce the results *now*.
|
|
407
|
+
*/
|
|
408
|
+
export function createSyncStartCall(decl, ctx) {
|
|
409
|
+
return (callee, _liftParamCount) => {
|
|
410
|
+
const prepared = takePrepared(ctx, "sync-start-call");
|
|
411
|
+
assert_(typeof callee === "function", "sync-start-call: callee funcref");
|
|
412
|
+
const callback = decl.callback === null
|
|
413
|
+
? null
|
|
414
|
+
: ctx.callback(decl.callback);
|
|
415
|
+
let callerResults = null;
|
|
416
|
+
// The caller's frame is blocked for the whole call, so resolution
|
|
417
|
+
// delivery = this intrinsic returning results: release lenders then
|
|
418
|
+
// (the sync analogue of `Subtask.deliver_resolve`, definitions.py 904).
|
|
419
|
+
// Inlined rather than reusing `SyncCallScope` to keep this module free
|
|
420
|
+
// of a value-level import cycle with intrinsics/mod.ts.
|
|
421
|
+
const lentHandles = [];
|
|
422
|
+
const lenderScope = {
|
|
423
|
+
addLender(h) {
|
|
424
|
+
h.numLends += 1;
|
|
425
|
+
lentHandles.push(h);
|
|
426
|
+
},
|
|
427
|
+
releaseLenders() {
|
|
428
|
+
for (const h of lentHandles)
|
|
429
|
+
h.numLends -= 1;
|
|
430
|
+
lentHandles.length = 0;
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
const { task, body } = mkCalleeTask({
|
|
434
|
+
prepared,
|
|
435
|
+
callee: callee,
|
|
436
|
+
callback,
|
|
437
|
+
postReturn: null,
|
|
438
|
+
ctx,
|
|
439
|
+
// `sync-start-call` exists only for "sync-lowered import to async-lifted
|
|
440
|
+
// export" (fact.rs:608), so the callee always uses the async ABI.
|
|
441
|
+
calleeUsesAsyncAbi: true,
|
|
442
|
+
mode: ctx.suspensionMode,
|
|
443
|
+
canBlock: ctx.calleeCanBlock?.(callee) ?? false,
|
|
444
|
+
onCallerResults: (r) => {
|
|
445
|
+
// sync-start-call's callee always uses the async ABI (comment above),
|
|
446
|
+
// but the *caller* side here is the sync `canon_lower` path: the
|
|
447
|
+
// reference's on_resolve(None) case is reached only when a
|
|
448
|
+
// cancellation was requested, and a sync-lowered subtask has no
|
|
449
|
+
// handle and hence no cancel channel — so `r` can never be null here.
|
|
450
|
+
assert_(r !== null, "sync-start-call: caller results missing");
|
|
451
|
+
callerResults = r;
|
|
452
|
+
},
|
|
453
|
+
lenderScope,
|
|
454
|
+
});
|
|
455
|
+
// Reference `Store.lift`: the reentrance gate, with the *caller* as the
|
|
456
|
+
// entering context (definitions.py `entering_set(caller)`).
|
|
457
|
+
// A poisoned callee's refusal names the original trap (polyengine#145).
|
|
458
|
+
if (!prepared.calleeInst.mayEnterFrom(prepared.callerInst)) {
|
|
459
|
+
trap(withPoisonCause(prepared.calleeInst, "cannot enter component instance"));
|
|
460
|
+
}
|
|
461
|
+
prepared.calleeInst.enterFrom(prepared.callerInst);
|
|
462
|
+
let ok = false;
|
|
463
|
+
try {
|
|
464
|
+
const thread = spawn(task, body);
|
|
465
|
+
thread.resume();
|
|
466
|
+
ok = true;
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
// A trap leaves the instance poisoned: `leave_to` is not reached
|
|
470
|
+
// (definitions.py `Store.lift`, line 578). A *capability signal* does
|
|
471
|
+
// not — see the `isCapabilitySignal` note in exec/boundary.ts.
|
|
472
|
+
if (e instanceof NeedsJspi || e instanceof PendingCapability) {
|
|
473
|
+
prepared.calleeInst.leaveTo(prepared.callerInst);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// Retire the poisoned CALLEE's stream/future ends (#66): this is a
|
|
477
|
+
// bracket-break site like `Store.tick`'s, and the trap unwinds to a
|
|
478
|
+
// hooked site that walks only the CALLER's chain — a composed
|
|
479
|
+
// component's callee would otherwise strand its host peers.
|
|
480
|
+
notifyInstancePoisoned(prepared.calleeInst, e);
|
|
481
|
+
}
|
|
482
|
+
// The lent handles are the CALLER's, and the caller is not poisoned by
|
|
483
|
+
// either exit (contracts/intrinsics.md v0.2 amendment 2: this runtime
|
|
484
|
+
// deliberately supports post-trap re-entry on the caller side, where
|
|
485
|
+
// the reference kills the whole store, so the sync-call scopes it
|
|
486
|
+
// skipped have to be unwound explicitly). Leaving `numLends` elevated
|
|
487
|
+
// would make every later `lift_own`/`resource.drop` of those handles
|
|
488
|
+
// trap "handle still lent out" (#91). Release is idempotent, and the
|
|
489
|
+
// success path below is unchanged.
|
|
490
|
+
lenderScope.releaseLenders();
|
|
491
|
+
throw e;
|
|
492
|
+
}
|
|
493
|
+
if (ok)
|
|
494
|
+
prepared.calleeInst.leaveTo(prepared.callerInst);
|
|
495
|
+
if (callerResults === null) {
|
|
496
|
+
// The callee did not resolve within its first activation. definitions.py
|
|
497
|
+
// `canon_lower`'s sync path blocks here — `thread.wait_until(
|
|
498
|
+
// subtask.resolved)` (line 2286) — suspending the *caller's* wasm frame
|
|
499
|
+
// while the scheduler runs other threads. That is JSPI role 2 (docs/architecture.md
|
|
500
|
+
// §6), and it is the first place a purely stackless runtime genuinely
|
|
501
|
+
// cannot proceed.
|
|
502
|
+
//
|
|
503
|
+
// Note this is NOT the sync driving loop of `canon_lift`: that loop
|
|
504
|
+
// drives the callee instance's own threads and is only correct when the
|
|
505
|
+
// callee can finish without anything from the caller. Here the caller is
|
|
506
|
+
// mid-frame and may be exactly what the callee is waiting for, so
|
|
507
|
+
// pumping the callee alone would spin rather than make progress.
|
|
508
|
+
// Note: the callee's thread stays parked in `store.waiting` when we bail
|
|
509
|
+
// here. That is deliberate — unwinding it would run callee cleanup the
|
|
510
|
+
// guest never asked for — but it does mean the store keeps a thread that
|
|
511
|
+
// will never be resumed. Harmless today (the enclosing host call is
|
|
512
|
+
// failing anyway, and the instance is not poisoned because no trap
|
|
513
|
+
// escaped a task), and it disappears once JSPI lets this path actually
|
|
514
|
+
// block instead of bailing.
|
|
515
|
+
if (ctx.suspensionMode === "jspi") {
|
|
516
|
+
// JSPI role 2 (docs/architecture.md §6): park the *caller's* wasm activation until
|
|
517
|
+
// the callee resolves, exactly as definitions.py `canon_lower`'s sync
|
|
518
|
+
// path does with `thread.wait_until(subtask.resolved)` (line 2286).
|
|
519
|
+
// The scheduler keeps ticking the callee meanwhile; when it produces
|
|
520
|
+
// results our `readyFunc` goes true and the engine resumes the caller.
|
|
521
|
+
//
|
|
522
|
+
// Not cancellable: a sync-lowered caller has no way to observe or
|
|
523
|
+
// request cancellation mid-call -- the reference's wait here carries
|
|
524
|
+
// no cancellation branch.
|
|
525
|
+
// LENDER RELEASE ON EVERY SETTLE PATH (#102).
|
|
526
|
+
//
|
|
527
|
+
// Enumeration of how this `SuspensionPoint` can reach a terminal
|
|
528
|
+
// state (jspi/bridge.ts `SuspensionPoint`), and whether `produce`
|
|
529
|
+
// runs on each:
|
|
530
|
+
//
|
|
531
|
+
// 1. `resume(false)` -> `produce` returns the packed result.
|
|
532
|
+
// RUNS. This is the success path; release stays INSIDE `produce`,
|
|
533
|
+
// before the results are shaped, so its ordering relative to the
|
|
534
|
+
// produced value is unchanged by this fix.
|
|
535
|
+
// 2. `resume(false)` -> `produce` throws (a trap computed at resume
|
|
536
|
+
// time). PARTIALLY RUNS. Release is `produce`'s first statement
|
|
537
|
+
// so it is already discharged here, but the `onSettled` backstop
|
|
538
|
+
// makes that independent of statement order.
|
|
539
|
+
// 3. `resume(true)` — a CANCELLED resume. Unreachable by
|
|
540
|
+
// construction: this park is `cancellable: false` and
|
|
541
|
+
// `SuspensionPoint.resume` asserts `cancellable || !cancelled`
|
|
542
|
+
// (#93). Note the assert fires BEFORE `#done` is set, so such a
|
|
543
|
+
// call leaves the point still parked and never settles it — a
|
|
544
|
+
// scheduler bug, not a guest-reachable exit; there is no
|
|
545
|
+
// non-poisoning continuation to release into.
|
|
546
|
+
// 4. `abandon(reason)` — store teardown / abandonment: fails the
|
|
547
|
+
// import's Promise WITHOUT calling `produce`. DOES NOT RUN. This
|
|
548
|
+
// is the #102 hole; `onSettled` covers it.
|
|
549
|
+
// 5. Never settled at all (the store is dropped while this point
|
|
550
|
+
// sits in `store.waiting`, e.g. the caller's whole host call was
|
|
551
|
+
// abandoned). No JS runs, so nothing can release; the lent
|
|
552
|
+
// handles die with the store, which is the reference's own
|
|
553
|
+
// outcome. Out of scope for amendment 2 (no non-poisoning exit).
|
|
554
|
+
// 6. Trap-poisoning of the parked instance: does not settle this
|
|
555
|
+
// point by itself — it reaches the guest either as (2) (a
|
|
556
|
+
// produce-time trap) or as (4) (teardown abandons the park), so
|
|
557
|
+
// it is covered by those two rows, not a third mechanism.
|
|
558
|
+
//
|
|
559
|
+
// `releaseLenders` is idempotent (#91), so the backstop is a no-op
|
|
560
|
+
// whenever `produce` already ran.
|
|
561
|
+
return blockCurrentActivation({
|
|
562
|
+
store: prepared.callerInst.store,
|
|
563
|
+
task: currentTask(),
|
|
564
|
+
readyFunc: () => callerResults !== null,
|
|
565
|
+
cancellable: false,
|
|
566
|
+
produce: () => {
|
|
567
|
+
lenderScope.releaseLenders();
|
|
568
|
+
return shapeResults(callerResults);
|
|
569
|
+
},
|
|
570
|
+
onSettled: () => lenderScope.releaseLenders(),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
// A capability signal is expressly NON-poisoning (see above), so
|
|
574
|
+
// stranding the caller's lenders here is strictly worse than on the
|
|
575
|
+
// trap path: the caller is guaranteed to keep running (#91).
|
|
576
|
+
lenderScope.releaseLenders();
|
|
577
|
+
needsJspi("sync-start-call whose async-lifted callee did not resolve in its " +
|
|
578
|
+
"first activation (the caller's wasm frame must block)");
|
|
579
|
+
}
|
|
580
|
+
lenderScope.releaseLenders();
|
|
581
|
+
return shapeResults(callerResults);
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Release a never-delivered subtask's lenders after a trap or capability bail
|
|
586
|
+
* broke the `[async-start-call]` bracket (#91).
|
|
587
|
+
*
|
|
588
|
+
* Now a thin alias of `Subtask.unwindLenders` — the same unwind serves the
|
|
589
|
+
* host-import parks (exec/boundary.ts, #106) — kept for the local name the
|
|
590
|
+
* `[async-start-call]` comments reference.
|
|
591
|
+
*/
|
|
592
|
+
function unwindSubtaskLenders(subtask) {
|
|
593
|
+
subtask.unwindLenders();
|
|
594
|
+
}
|
|
595
|
+
/** The core-ABI shape of a returned results vector (0 / 1 / many). */
|
|
596
|
+
function shapeResults(out) {
|
|
597
|
+
if (out === null || out.length === 0)
|
|
598
|
+
return undefined;
|
|
599
|
+
if (out.length === 1)
|
|
600
|
+
return out[0];
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// async-start-call
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
/**
|
|
607
|
+
* An async-lowered import calling any export (`compile_async_to_async_adapter`
|
|
608
|
+
* / `compile_async_to_sync_adapter`). Returns the packed subtask status the
|
|
609
|
+
* guest already knows how to interpret — the same
|
|
610
|
+
* `state | (subtaski << 4)` encoding `canon_lower` produces
|
|
611
|
+
* (definitions.py line 2308), so the caller's callback loop and waitable sets
|
|
612
|
+
* work unchanged.
|
|
613
|
+
*/
|
|
614
|
+
export function createAsyncStartCall(decl, ctx) {
|
|
615
|
+
return (callee, _paramCount, _resultCount, flags) => {
|
|
616
|
+
const prepared = takePrepared(ctx, "async-start-call");
|
|
617
|
+
assert_(typeof callee === "function", "async-start-call: callee funcref");
|
|
618
|
+
const callback = decl.callback === null
|
|
619
|
+
? null
|
|
620
|
+
: ctx.callback(decl.callback);
|
|
621
|
+
// The caller-side view of this call. Everything downstream — waitable
|
|
622
|
+
// sets, `subtask.drop`, the SUBTASK event — is the machinery already built
|
|
623
|
+
// for host-import subtasks in exec/boundary.ts.
|
|
624
|
+
// Starts STARTING and becomes STARTED only when `[async-start]` runs (see
|
|
625
|
+
// `onStarted`). A callee held at the backpressure gate is therefore
|
|
626
|
+
// reported as STARTING, and the STARTED transition delivers its own event
|
|
627
|
+
// if the guest has already been handed a subtask index.
|
|
628
|
+
const subtask = new Subtask();
|
|
629
|
+
let onProgress = () => { };
|
|
630
|
+
const { task, body } = mkCalleeTask({
|
|
631
|
+
prepared,
|
|
632
|
+
callee: callee,
|
|
633
|
+
callback,
|
|
634
|
+
postReturn: decl.postReturn === null
|
|
635
|
+
? null
|
|
636
|
+
: ctx.callback(decl.postReturn),
|
|
637
|
+
ctx,
|
|
638
|
+
// `compile_async_to_async_adapter` sets START_FLAG_ASYNC_CALLEE;
|
|
639
|
+
// `compile_async_to_sync_adapter` passes 0 (trampoline.rs:508 and :764).
|
|
640
|
+
calleeUsesAsyncAbi: ((flags ?? 0) & START_FLAG_ASYNC_CALLEE) !== 0,
|
|
641
|
+
mode: ctx.suspensionMode,
|
|
642
|
+
canBlock: ctx.calleeCanBlock?.(callee) ?? false,
|
|
643
|
+
onStarted: () => {
|
|
644
|
+
if (subtask.state === SubtaskState.STARTING) {
|
|
645
|
+
subtask.state = SubtaskState.STARTED;
|
|
646
|
+
// `onProgress` is a no-op until the guest has a handle for this
|
|
647
|
+
// subtask, mirroring `canon_lower`'s `maybe_on_progress`
|
|
648
|
+
// (definitions.py line 2296): a call that starts before
|
|
649
|
+
// `async-start-call` returns reports STARTED in its packed result
|
|
650
|
+
// instead, with no event.
|
|
651
|
+
onProgress();
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
onCallerResults: (r) => {
|
|
655
|
+
// `[async-return]` already wrote the caller's results (through the
|
|
656
|
+
// retptr the caller supplied), so there is nothing to carry here: the
|
|
657
|
+
// guest learns of completion from the SUBTASK event.
|
|
658
|
+
if (!subtask.resolved()) {
|
|
659
|
+
subtask.resolve(r === null
|
|
660
|
+
// definitions.py `canon_lower`'s `on_resolve` (line 2267): a
|
|
661
|
+
// cancelled callee resolves CANCELLED_BEFORE_{STARTED,RETURNED}
|
|
662
|
+
// depending on how far it got.
|
|
663
|
+
? (subtask.state === SubtaskState.STARTING
|
|
664
|
+
? SubtaskState.CANCELLED_BEFORE_STARTED
|
|
665
|
+
: SubtaskState.CANCELLED_BEFORE_RETURNED)
|
|
666
|
+
: SubtaskState.RETURNED, []);
|
|
667
|
+
}
|
|
668
|
+
onProgress();
|
|
669
|
+
},
|
|
670
|
+
// Borrow lenders attach to the caller-side subtask, released by its
|
|
671
|
+
// `deliverResolve` (definitions.py `Subtask.deliver_resolve`, line 904).
|
|
672
|
+
lenderScope: subtask,
|
|
673
|
+
});
|
|
674
|
+
// Cross-component cancellation: `subtask.cancel` forwards to the callee
|
|
675
|
+
// task's `request_cancellation` (definitions.py line 519), which delivers
|
|
676
|
+
// TASK_CANCELLED to a cancellable block point — for a callback-ABI callee
|
|
677
|
+
// that is its WAIT/YIELD, so the guest observes the cancellation and calls
|
|
678
|
+
// `task.cancel`, resolving this subtask CANCELLED_BEFORE_RETURNED.
|
|
679
|
+
subtask.onCancel = (callerInst) => task.requestCancellation(callerInst);
|
|
680
|
+
subtask.calleeTask = task;
|
|
681
|
+
// A poisoned callee's refusal names the original trap (polyengine#145).
|
|
682
|
+
if (!prepared.calleeInst.mayEnterFrom(prepared.callerInst)) {
|
|
683
|
+
trap(withPoisonCause(prepared.calleeInst, "cannot enter component instance"));
|
|
684
|
+
}
|
|
685
|
+
prepared.calleeInst.enterFrom(prepared.callerInst);
|
|
686
|
+
let ok = false;
|
|
687
|
+
let thread;
|
|
688
|
+
try {
|
|
689
|
+
thread = spawn(task, body);
|
|
690
|
+
thread.resume();
|
|
691
|
+
ok = true;
|
|
692
|
+
}
|
|
693
|
+
catch (e) {
|
|
694
|
+
// See the sync form above and `isCapabilitySignal` in exec/boundary.ts.
|
|
695
|
+
if (e instanceof NeedsJspi || e instanceof PendingCapability) {
|
|
696
|
+
prepared.calleeInst.leaveTo(prepared.callerInst);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
// Bracket-break site — retire the poisoned callee's ends (#66),
|
|
700
|
+
// as in the sync form above.
|
|
701
|
+
notifyInstancePoisoned(prepared.calleeInst, e);
|
|
702
|
+
}
|
|
703
|
+
// The subtask never reached `report()`, so it has no handle in the
|
|
704
|
+
// caller's table and nothing will ever deliver its resolution — but it
|
|
705
|
+
// holds `num_lends` on the caller's handles. Resolve it as cancelled
|
|
706
|
+
// (the state the reference's `on_resolve(None)` would give a call that
|
|
707
|
+
// never started/returned) and deliver, which is what releases the
|
|
708
|
+
// lenders (definitions.py `Subtask.deliver_resolve`, line 902). See the
|
|
709
|
+
// sync form above for why the caller must not be left holding them.
|
|
710
|
+
unwindSubtaskLenders(subtask);
|
|
711
|
+
throw e;
|
|
712
|
+
}
|
|
713
|
+
if (ok)
|
|
714
|
+
prepared.calleeInst.leaveTo(prepared.callerInst);
|
|
715
|
+
const report = () => {
|
|
716
|
+
if (subtask.resolved()) {
|
|
717
|
+
// Eager completion: no handle, no event (definitions.py line 2293).
|
|
718
|
+
subtask.deliverResolve();
|
|
719
|
+
traceCopy(`async-start-call -> RETURNED (eager)`);
|
|
720
|
+
return SubtaskState.RETURNED;
|
|
721
|
+
}
|
|
722
|
+
const subtaski = prepared.callerInst.handles.add(subtask);
|
|
723
|
+
onProgress = () => subtask.setSubtaskPendingEvent(subtaski);
|
|
724
|
+
const packed = packSubtaskResult(subtask.state, subtaski);
|
|
725
|
+
traceCopy(`async-start-call -> state=${subtask.state} i=${subtaski} ` +
|
|
726
|
+
`packed=0x${packed.toString(16)}`);
|
|
727
|
+
return packed;
|
|
728
|
+
};
|
|
729
|
+
// NO WAIT FOR RESOLUTION HERE, deliberately. An async-lowered caller must
|
|
730
|
+
// not block on its callee's *completion* -- that is the entire point of
|
|
731
|
+
// async lowering: it takes a subtask handle and learns of completion
|
|
732
|
+
// through events. An earlier attempt (M2 "Fix 1") parked the caller here
|
|
733
|
+
// until the callee resolved. It made cross-abi-calls agree in both modes,
|
|
734
|
+
// and it broke the thing it had no business touching: the caller's
|
|
735
|
+
// activation was now suspended, so the sync-lowered parked caller of
|
|
736
|
+
// `handshake_test.ts` was never resumed and the run hung. Correct-looking,
|
|
737
|
+
// semantically wrong.
|
|
738
|
+
//
|
|
739
|
+
// What jspi mode DOES need is a wait for **determinacy** (jspi pin (j),
|
|
740
|
+
// `fastpath_hop_test.ts`): the engine defers a promising callee's
|
|
741
|
+
// continuation to a microtask at EVERY Suspending call -- even one whose
|
|
742
|
+
// value was available synchronously -- so a callee the reference would
|
|
743
|
+
// run to completion inside this call (`canon_lift` drives the thread to
|
|
744
|
+
// its first real block point before `canon_lower` returns) is still
|
|
745
|
+
// mid-hop when `report()` runs. Reporting then is reporting a state the
|
|
746
|
+
// reference can never observe: STARTED for a call that eagerly RETURNED
|
|
747
|
+
// (big-interleaving's `call-import` scripts), or a missed synchronous
|
|
748
|
+
// cancellation (its `subtask-cancel` scripts).
|
|
749
|
+
//
|
|
750
|
+
// "Determinate" is exactly one of:
|
|
751
|
+
// * the subtask resolved (task.return ran mid-activation), or
|
|
752
|
+
// * the callee's thread finished (results flow through the body), or
|
|
753
|
+
// * the callee genuinely parked on a scheduler condition -- its
|
|
754
|
+
// SuspensionPoint (or its body's own wait) sits in `store.waiting`.
|
|
755
|
+
// A genuinely-blocking callee reaches its first real block point without
|
|
756
|
+
// anything from the caller, so unlike Fix 1 this wait cannot deadlock:
|
|
757
|
+
// it is the reference's atomic run-to-first-block, reconstructed across
|
|
758
|
+
// the engine's microtask hops.
|
|
759
|
+
//
|
|
760
|
+
// THE DEFERRED ENTRY DECISION (issue #43; wasmtime's model — source
|
|
761
|
+
// walkthrough distilled on the issue, exam kit archived at
|
|
762
|
+
// 4f3351f:exams/wasmtime-exclusivity/). The determinacy wait above is
|
|
763
|
+
// also where the initial *status* is decided, so it is where the
|
|
764
|
+
// deferral lives.
|
|
765
|
+
//
|
|
766
|
+
// In wasmtime a guest->guest call queues the callee's `StartImplicit`
|
|
767
|
+
// and the caller suspends until the first subtask status event
|
|
768
|
+
// (concurrent.rs :3040-3160); the executor first drains the work queued
|
|
769
|
+
// ahead of it, so a ready gate holder runs to invocation exit and
|
|
770
|
+
// releases `do_not_enter` BEFORE the new call's readiness is evaluated
|
|
771
|
+
// (:1497-1522). polyengine's callee thread is likewise already spawned and
|
|
772
|
+
// parked at `enter_implicit_thread`'s gate wait at this point; what
|
|
773
|
+
// changes here is only WHEN the caller reads `subtask.state`.
|
|
774
|
+
//
|
|
775
|
+
// Order-robust formulation (issue #43; a non-normative scheduler
|
|
776
|
+
// policy — entry-status timing is not normative — chosen over wasmtime's
|
|
777
|
+
// FIFO-dependent one so the seeded-shuffle reruns stay green): while the
|
|
778
|
+
// callee is still parked at the entry gate, the caller waits until the
|
|
779
|
+
// callee instance's runnable work is exhausted
|
|
780
|
+
// (`Store.hasRunnableWork`). Then:
|
|
781
|
+
// * the holder was ready -> it ran to `exit_implicit_thread`, released
|
|
782
|
+
// the gate, the callee entered: `subtask.state` is STARTED (or the
|
|
783
|
+
// callee already RETURNED) -- test/async/sync-streams.wast:145;
|
|
784
|
+
// * the holder was NOT ready (parked mid-frame on an un-rendezvous'd
|
|
785
|
+
// operation), or the holder IS this caller (a nested lower, excluded
|
|
786
|
+
// from the scan): quiescence is immediate and the caller reports
|
|
787
|
+
// STARTING -- hold semantics, observably.
|
|
788
|
+
//
|
|
789
|
+
// Backpressure-queue admission is untouched: the callee registered in
|
|
790
|
+
// `num_waiting_to_enter` synchronously at `thread.resume()` above,
|
|
791
|
+
// before any draining, so the deterministic-profile ordering pins
|
|
792
|
+
// (async-calls-sync.wast) see the same admission order as before.
|
|
793
|
+
//
|
|
794
|
+
// PLAIN MODE IS DELIBERATELY UNTOUCHED, and provably needs no drain: a
|
|
795
|
+
// needs-exclusive task holds `exclusiveThread` only across a core
|
|
796
|
+
// invocation (the callback loop releases it across every wait), and
|
|
797
|
+
// without JSPI a wasm frame cannot park mid-invocation at all. So in
|
|
798
|
+
// plain mode the gate, when held, is held by the *currently running*
|
|
799
|
+
// activation -- the one obstacle a drain can never remove. Zero cost for
|
|
800
|
+
// sync-only components, and no suspendability reclassification:
|
|
801
|
+
// `async-start-call` was already `Suspending`-wrapped for the
|
|
802
|
+
// determinacy park (exec/executor.ts, "async-start-call is wrapped").
|
|
803
|
+
if (ctx.suspensionMode === "jspi") {
|
|
804
|
+
const store = prepared.callerInst.store;
|
|
805
|
+
const calleeInst = prepared.calleeInst;
|
|
806
|
+
// The caller's task: excluded from the drain scan (it is the asker).
|
|
807
|
+
// `maybeCurrentTask` rather than `currentTask` because a host-driven
|
|
808
|
+
// entry can reach here with no ambient task at all.
|
|
809
|
+
const callerTask = maybeCurrentTask();
|
|
810
|
+
// STARTING + parked == parked at the entry gate: `[async-start]` runs
|
|
811
|
+
// immediately after `enter_implicit_thread` succeeds, so any callee
|
|
812
|
+
// that got past the gate has already left STARTING.
|
|
813
|
+
const gatedAtEntry = () => subtask.state === SubtaskState.STARTING &&
|
|
814
|
+
!subtask.resolved() &&
|
|
815
|
+
!thread.done() &&
|
|
816
|
+
thread.waiting();
|
|
817
|
+
const determinate = () => gatedAtEntry()
|
|
818
|
+
? !store.hasRunnableWork(calleeInst, callerTask)
|
|
819
|
+
: subtask.resolved() ||
|
|
820
|
+
thread.done() ||
|
|
821
|
+
store.waiting.some((w) => w.task === task);
|
|
822
|
+
if (!determinate()) {
|
|
823
|
+
// Same settle-path enumeration as the sync form above (#102). Here
|
|
824
|
+
// the lender scope is the `Subtask` itself, discharged by
|
|
825
|
+
// `deliverResolve`, and `report()` is what eventually delivers it —
|
|
826
|
+
// either eagerly (the resolved branch) or, for a live subtask, via
|
|
827
|
+
// the handle it hands the guest. So the backstop must fire ONLY when
|
|
828
|
+
// `report()` did not complete: on the success path the subtask is
|
|
829
|
+
// typically still live and in the caller's table, and unwinding it
|
|
830
|
+
// there would cancel a perfectly good call.
|
|
831
|
+
//
|
|
832
|
+
// `report()` not completing means the guest never received the
|
|
833
|
+
// subtask index (it either threw before `handles.add`, or after it
|
|
834
|
+
// with the index lost), so nothing will ever deliver this subtask's
|
|
835
|
+
// resolution — exactly the state `unwindSubtaskLenders` exists for
|
|
836
|
+
// (contracts/intrinsics.md v0.2 amendment 2).
|
|
837
|
+
let produced = false;
|
|
838
|
+
return blockCurrentActivation({
|
|
839
|
+
store: prepared.callerInst.store,
|
|
840
|
+
task: currentTask(),
|
|
841
|
+
readyFunc: determinate,
|
|
842
|
+
cancellable: false,
|
|
843
|
+
produce: () => {
|
|
844
|
+
const r = report();
|
|
845
|
+
produced = true;
|
|
846
|
+
return r;
|
|
847
|
+
},
|
|
848
|
+
onSettled: () => {
|
|
849
|
+
if (!produced)
|
|
850
|
+
unwindSubtaskLenders(subtask);
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return report();
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
/** Create a thread whose body needs a reference to the thread itself. */
|
|
859
|
+
function spawn(task, body) {
|
|
860
|
+
let thread;
|
|
861
|
+
thread = new Thread(task, (function* () {
|
|
862
|
+
yield* body(thread);
|
|
863
|
+
})());
|
|
864
|
+
return thread;
|
|
865
|
+
}
|