@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,579 @@
|
|
|
1
|
+
// The JSPI ↔ scheduler bridge: turning a blocking canonical built-in into a
|
|
2
|
+
// genuinely suspended wasm activation.
|
|
3
|
+
//
|
|
4
|
+
// This is the phase-1 seam swap. Nothing in the task model changes: `Thread`,
|
|
5
|
+
// `BlockRequest` and `waitUntil` are exactly as they were. What changes is
|
|
6
|
+
// *who* the parked thing is. For the stackless (callback-ABI) path a parked
|
|
7
|
+
// thread is a JS generator; for a stackful one it is a suspended wasm
|
|
8
|
+
// activation, and this module is the adapter that makes the two look the same
|
|
9
|
+
// to `Store.tick`.
|
|
10
|
+
//
|
|
11
|
+
// ===========================================================================
|
|
12
|
+
// THE INVARIANT
|
|
13
|
+
// ===========================================================================
|
|
14
|
+
//
|
|
15
|
+
// A component instantiation is EITHER "plain" — no JS→wasm entry is
|
|
16
|
+
// `promising`-wrapped and no import is `Suspending`-wrapped — OR
|
|
17
|
+
// "suspension-capable": every entry that can reach a blocking built-in is
|
|
18
|
+
// `promising`-wrapped, and every blocking built-in is `Suspending`-wrapped.
|
|
19
|
+
// Never a mixture.
|
|
20
|
+
//
|
|
21
|
+
// This is forced by empirical fact (c), pinned in
|
|
22
|
+
// `runtime/tests/jspi/suspending_import_test.ts`: **a `Suspending`-wrapped
|
|
23
|
+
// import called outside a promising activation traps unconditionally — even
|
|
24
|
+
// on the plain-value fast path.** So `Suspending`-wrapping an intrinsic is not
|
|
25
|
+
// a free upgrade; it is only safe if every activation that can reach it is
|
|
26
|
+
// promising. Hence one decision per instantiation, not per call site.
|
|
27
|
+
//
|
|
28
|
+
// It is enforced structurally rather than by convention: the mode is a single
|
|
29
|
+
// value computed once by the `Executor` and threaded to both wrapping sites
|
|
30
|
+
// (`enterWasm` here, and the trampoline factory in intrinsics/mod.ts). Both
|
|
31
|
+
// assert it, and `assertModeConsistent` is called from the instantiation path
|
|
32
|
+
// so a mismatch fails loudly at instantiate time rather than as a mystery
|
|
33
|
+
// trap much later.
|
|
34
|
+
//
|
|
35
|
+
// Which entries "can reach a blocking built-in"? Exactly three:
|
|
36
|
+
// * a lifted export's core function,
|
|
37
|
+
// * a callback export (the callback-ABI loop re-enters wasm),
|
|
38
|
+
// * a FACT adapter callee invoked by `{sync,async}-start-call`.
|
|
39
|
+
// `realloc`, `post-return` and resource destructors are deliberately NOT
|
|
40
|
+
// promising-wrapped: they are guest-internal or spec-forbidden from blocking,
|
|
41
|
+
// they never call a canonical built-in, and wrapping them would force their
|
|
42
|
+
// results to become Promises where cabi needs a number synchronously.
|
|
43
|
+
import { assert_ } from "../cabi/trap.js";
|
|
44
|
+
import { isSupported, makePromising, makeSuspending } from "./mechanics.js";
|
|
45
|
+
import { withActivation, claimActivationAmbient, dbgId, consumeClaimIfRunning, maybeCurrentThread, releaseActivationAmbient, setResumingThread, } from "../task/mod.js";
|
|
46
|
+
/**
|
|
47
|
+
* Decide the mode for one instantiation.
|
|
48
|
+
*
|
|
49
|
+
* `requested` is the embedder's opt-in. We additionally require the engine to
|
|
50
|
+
* actually implement JSPI: on an engine without it every blocking site falls
|
|
51
|
+
* back to the precise `NeedsJspi` it raised before this module existed, which
|
|
52
|
+
* is the M3 browser-matrix degradation path (docs/milestones.md M3).
|
|
53
|
+
*/
|
|
54
|
+
export function chooseMode(requested, needed) {
|
|
55
|
+
if (requested === false)
|
|
56
|
+
return "plain";
|
|
57
|
+
const want = requested === true || needed === true;
|
|
58
|
+
return want && isSupported() ? "jspi" : "plain";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Does a call through this trampoline declaration genuinely block — i.e. is
|
|
62
|
+
* it a reason a component NEEDS suspension support at all?
|
|
63
|
+
*
|
|
64
|
+
* Sharper than a kind list, and it must be: the async *form* of every copy /
|
|
65
|
+
* cancel built-in NEVER blocks — it returns `BLOCKED` and delivers the result
|
|
66
|
+
* through an event (definitions.py `stream_copy` line 2530 / `cancel_copy`
|
|
67
|
+
* 2636 / `canon_subtask_cancel` 2469, each `if not async_: ... else return
|
|
68
|
+
* BLOCKED`). Classifying by kind alone marked every instance that imports an
|
|
69
|
+
* async-form built-in as suspension-capable, which promising-wrapped its
|
|
70
|
+
* eagerly-completing FACT callees — and a wrapped eager callee reports
|
|
71
|
+
* STARTED where the reference reports RETURNED, and parks on a
|
|
72
|
+
* non-cancellable `awaitValue` where the reference delivers a synchronous
|
|
73
|
+
* cancellation (both asserted by big-interleaving-test.wast's expect-codes).
|
|
74
|
+
*
|
|
75
|
+
* Where the async-ness lives varies by kind, following wasmtime's trampoline
|
|
76
|
+
* layout (component/info.rs): copy built-ins carry an options index (the
|
|
77
|
+
* flag is `canonicalOptions[i].async`); the cancel forms and `subtask.cancel`
|
|
78
|
+
* carry `async` on the declaration itself; `waitable-set.wait`,
|
|
79
|
+
* `thread.yield` and `sync-start-call` are unconditionally block-capable
|
|
80
|
+
* (their non-blocking counterparts are separate kinds: poll, the YIELD
|
|
81
|
+
* callback code, async-start-call).
|
|
82
|
+
*/
|
|
83
|
+
export function trampolineNeedsSuspension(t, optionsAsync) {
|
|
84
|
+
switch (t.kind) {
|
|
85
|
+
case "sync-start-call":
|
|
86
|
+
case "waitable-set-wait":
|
|
87
|
+
case "thread-yield":
|
|
88
|
+
return true;
|
|
89
|
+
case "subtask-cancel":
|
|
90
|
+
case "stream-cancel-read":
|
|
91
|
+
case "stream-cancel-write":
|
|
92
|
+
case "future-cancel-read":
|
|
93
|
+
case "future-cancel-write":
|
|
94
|
+
return t.async !== true;
|
|
95
|
+
case "stream-read":
|
|
96
|
+
case "stream-write":
|
|
97
|
+
case "future-read":
|
|
98
|
+
case "future-write":
|
|
99
|
+
return typeof t.options === "number" ? !optionsAsync(t.options) : true;
|
|
100
|
+
default:
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Must the executor hand this trampoline to wasm as a `Suspending` import in
|
|
106
|
+
* jspi mode?
|
|
107
|
+
*
|
|
108
|
+
* A superset of `trampolineNeedsSuspension` by exactly two kinds, both for
|
|
109
|
+
* the same reason: they never block in the reference or in plain mode, but
|
|
110
|
+
* under jspi they may park the CALLER until a callee's state is determinate,
|
|
111
|
+
* because the engine defers a resumed activation's continuation to a
|
|
112
|
+
* microtask (jspi pin (j), `fastpath_hop_test.ts`):
|
|
113
|
+
*
|
|
114
|
+
* * `async-start-call` — parks until the freshly-started callee reaches
|
|
115
|
+
* resolution / completion / a genuine block (fact_calls.ts);
|
|
116
|
+
* * the async form of `subtask.cancel` — parks until a cancellation
|
|
117
|
+
* delivered by settling the callee's suspension has actually landed
|
|
118
|
+
* (async_builtins.ts; cancellable.wast asserts the reference's
|
|
119
|
+
* synchronous-delivery answers).
|
|
120
|
+
*
|
|
121
|
+
* Neither is a *reason* to choose jspi mode, and neither marks its importer
|
|
122
|
+
* suspendable (`Executor.suspendableFuncs`): their parks only ever trigger
|
|
123
|
+
* when the nested callee is itself promising-wrapped, i.e. when a genuine
|
|
124
|
+
* blocker has already contaminated the adapter through the transitive import
|
|
125
|
+
* rule. Marking on these kinds is not only unnecessary — it is wrong: the
|
|
126
|
+
* FACT adapter's `[adapter-callee]*` pass-through exports are what get
|
|
127
|
+
* passed to `*-start-call` as lift callees, and marking the whole adapter
|
|
128
|
+
* instance promoted every eagerly-completing callee to promising, recreating
|
|
129
|
+
* the STARTED-vs-RETURNED divergence one level up.
|
|
130
|
+
*/
|
|
131
|
+
export function trampolineCanBlock(t, optionsAsync) {
|
|
132
|
+
return t.kind === "async-start-call" || t.kind === "subtask-cancel" ||
|
|
133
|
+
trampolineNeedsSuspension(t, optionsAsync);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Does this component contain anything that can block a wasm frame?
|
|
137
|
+
*
|
|
138
|
+
* Computed from the plan, on the runtime side, so embedders (and the
|
|
139
|
+
* conformance harness) stay dumb — no plan v3 field, no flag to thread
|
|
140
|
+
* through. Deliberately a slight over-approximation: it asks "could this
|
|
141
|
+
* component ever reach a blocking built-in", not "will this call". A false
|
|
142
|
+
* positive costs a promising-wrapped entry (the export returns a Promise); a
|
|
143
|
+
* false negative would be a hard trap at the blocking site, so the bias is the
|
|
144
|
+
* safe one.
|
|
145
|
+
*
|
|
146
|
+
* The two sources of blocking, both straight from the reference:
|
|
147
|
+
*
|
|
148
|
+
* * a **stackful async lift** — async canonical options with no callback
|
|
149
|
+
* (`canon_lift` line 2179 runs the callee to completion on its own stack);
|
|
150
|
+
* * a **blocking built-in** reached synchronously — see
|
|
151
|
+
* `trampolineNeedsSuspension`.
|
|
152
|
+
*/
|
|
153
|
+
export function planNeedsSuspension(plan) {
|
|
154
|
+
for (const o of plan.canonicalOptions) {
|
|
155
|
+
if (o.async && o.callback === null)
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
const optionsAsync = (i) => plan.canonicalOptions[i]?.async === true;
|
|
159
|
+
for (const t of plan.trampolines) {
|
|
160
|
+
if (trampolineNeedsSuspension(t, optionsAsync))
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Wrap a JS→wasm entry according to the mode.
|
|
167
|
+
*
|
|
168
|
+
* In `plain` mode this is the identity. In `jspi` mode the returned function
|
|
169
|
+
* always yields a Promise (empirical fact (e)), which is why the mode is an
|
|
170
|
+
* embedder opt-in: it changes the shape of every lifted export.
|
|
171
|
+
*/
|
|
172
|
+
export function enterWasm(fn, mode) {
|
|
173
|
+
if (mode === "plain")
|
|
174
|
+
return fn;
|
|
175
|
+
assert_(isSupported(), "jspi mode selected on an engine without JSPI");
|
|
176
|
+
return makePromising(fn);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wrap a blocking-capable trampoline so that returning a Promise suspends the
|
|
180
|
+
* calling wasm activation. Only legal in `jspi` mode — see the invariant.
|
|
181
|
+
*
|
|
182
|
+
* The wrapper around `fn` is ambient claim site (ii) (scheduler.ts), and the
|
|
183
|
+
* one that is easy to miss: **every** call through a `Suspending` import returns to
|
|
184
|
+
* wasm through a microtask hop, even when the import produced its value
|
|
185
|
+
* synchronously and nothing suspended (jspi pin (j),
|
|
186
|
+
* `tests/jspi/fastpath_hop_test.ts`). The rest of the guest's frame therefore
|
|
187
|
+
* runs after our JS frames — `awaitCore`'s `withActivation` bracket included —
|
|
188
|
+
* have unwound, with no `SuspensionPoint` anywhere in sight to have claimed
|
|
189
|
+
* it. Measured signature when this is missing: a FACT adapter's
|
|
190
|
+
* `enter-sync-call` runs bracketed and its `exit-sync-call` runs with no
|
|
191
|
+
* ambient at all ("exit-sync-call with an empty sync-call stack",
|
|
192
|
+
* `trap-if-done.wast:448`, `big-interleaving-test.wast`). This is the site the
|
|
193
|
+
* async-context store used to cover for free, because the engine
|
|
194
|
+
* captured the context when it registered the hop.
|
|
195
|
+
*
|
|
196
|
+
* The claim is only taken on the NON-suspending outcomes. A returned Promise
|
|
197
|
+
* is a genuine suspension whose resumption `SuspensionPoint.resume` claims
|
|
198
|
+
* exactly, and `blockCurrentActivation` has just released this activation's
|
|
199
|
+
* claim on the way in — re-adding it here would strand it.
|
|
200
|
+
*/
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Continuation-chunk attribution sentinels (issue #24)
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
//
|
|
205
|
+
// PROBLEM. Engine continuation chunks — the segments of a promising wasm
|
|
206
|
+
// activation between suspension/hop points — begin as promise REACTIONS,
|
|
207
|
+
// with no synchronous signal to this runtime. When several activations have
|
|
208
|
+
// pending continuations (a settled real suspension racing a fast-path hop,
|
|
209
|
+
// or two fast-path hops from nested entries), the chunks interleave at an
|
|
210
|
+
// empty bracket stack, and every ambient read in a later chunk — a hop's
|
|
211
|
+
// `owner` capture at `claimingFn` entry, or an unsafe intrinsic like
|
|
212
|
+
// `context.set`, which has no hop at all — inherits whatever claim the
|
|
213
|
+
// previous chunk left on top. Claim-stack ordering alone cannot repair
|
|
214
|
+
// this: the release edges are themselves promise reactions. Measured
|
|
215
|
+
// consequence (issue #24): wit-bindgen's callback epilogue restored one
|
|
216
|
+
// task's state pointer into another thread's context slots, and the next
|
|
217
|
+
// invocation of the starved thread's callback hit
|
|
218
|
+
// `assert!(!state.is_null())` (async_support.rs:578) -> unreachable.
|
|
219
|
+
// Reachable only with enough concurrently-suspended sibling activations
|
|
220
|
+
// (first corpus: polymorph-tls' webcrypto-composed suite, three async
|
|
221
|
+
// wit-bindgen components deep).
|
|
222
|
+
//
|
|
223
|
+
// FIX. Exploit the one ordering guarantee the platform does give us:
|
|
224
|
+
// microtasks run FIFO, and between our code queueing a microtask and the
|
|
225
|
+
// engine queueing the continuation reaction there is only synchronous
|
|
226
|
+
// engine-internal promise machinery. So at EVERY point where an engine
|
|
227
|
+
// continuation is about to be queued, queue a SENTINEL first that claims
|
|
228
|
+
// the chunk's owner (move-to-top):
|
|
229
|
+
//
|
|
230
|
+
// * fast-path hop: sentinel queued synchronously in `claimingFn` before
|
|
231
|
+
// returning the plain value — the engine queues the hop reaction while
|
|
232
|
+
// processing that return, so the queue reads [sentinel, chunk].
|
|
233
|
+
// * genuine suspension: the wrapper attached to the import's thenable
|
|
234
|
+
// queues the sentinel inside the settle reaction, before returning the
|
|
235
|
+
// value — the engine (attached to the WRAPPED promise) queues the
|
|
236
|
+
// resumption when that wrapper returns, so again [sentinel, chunk].
|
|
237
|
+
// This holds even when several promises settle in one drain: each
|
|
238
|
+
// pair is queued contiguously from within its own settle reaction.
|
|
239
|
+
//
|
|
240
|
+
// Nothing is delayed or reordered — unlike a serializing gate, which
|
|
241
|
+
// measurably shifted the deterministic-profile backpressure-admission
|
|
242
|
+
// order (async-calls-sync.wast caught it). This is the JSPI substitute for
|
|
243
|
+
// what fibers give wasmtime for free: identity travels with the
|
|
244
|
+
// resumption, here as a claim planted one microtask ahead of it.
|
|
245
|
+
function sentinelFor(owner) {
|
|
246
|
+
if (owner === null || owner === undefined)
|
|
247
|
+
return;
|
|
248
|
+
// `Promise.resolve().then`, not `queueMicrotask`: identical FIFO
|
|
249
|
+
// placement, but the latter does not exist in bare engine shells
|
|
250
|
+
// (SpiderMonkey jsshell; sm-pinned lane caught it).
|
|
251
|
+
SENTINEL_TICK.then(() => claimActivationAmbient(owner));
|
|
252
|
+
}
|
|
253
|
+
const SENTINEL_TICK = Promise.resolve();
|
|
254
|
+
/** Wrap a suspending import's thenable so the eventual resumption chunk is
|
|
255
|
+
* preceded contiguously by its attribution sentinel. */
|
|
256
|
+
function attributeContinuation(owner, r) {
|
|
257
|
+
return Promise.resolve(r).then((v) => {
|
|
258
|
+
sentinelFor(owner);
|
|
259
|
+
return v;
|
|
260
|
+
}, (e) => {
|
|
261
|
+
sentinelFor(owner);
|
|
262
|
+
throw e;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
export function suspendingImport(fn, mode) {
|
|
266
|
+
if (mode === "plain")
|
|
267
|
+
return fn;
|
|
268
|
+
assert_(isSupported(), "jspi mode selected on an engine without JSPI");
|
|
269
|
+
const claimingFn = (...args) => {
|
|
270
|
+
// The activation calling us — read while its bracket (or its hop claim)
|
|
271
|
+
// is still the ambient.
|
|
272
|
+
const owner = maybeCurrentThread() ?? null;
|
|
273
|
+
const invoke = () => fn(...args);
|
|
274
|
+
let r;
|
|
275
|
+
try {
|
|
276
|
+
// Bracket our own JS frame with the caller. Without this, a built-in
|
|
277
|
+
// that synchronously enters ANOTHER activation's wasm (`async-start-call`
|
|
278
|
+
// running its callee through `awaitCore`) leaves that callee's hop claim
|
|
279
|
+
// on top when the callee suspends, and the rest of OUR frame — still the
|
|
280
|
+
// caller's — then reads the callee as the ambient (measured on
|
|
281
|
+
// `fact_calls.ts:820`'s determinacy wait). The nesting is a stack, and
|
|
282
|
+
// this is the frame that owns it.
|
|
283
|
+
r = owner === null ? invoke() : withActivation(owner, invoke);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
// A synchronous trap out of a built-in also unwinds the guest through
|
|
287
|
+
// the hop, and the guest's trap-path built-ins run there.
|
|
288
|
+
claimActivationAmbient(owner);
|
|
289
|
+
throw e;
|
|
290
|
+
}
|
|
291
|
+
if (r === null || typeof r?.then !== "function") {
|
|
292
|
+
// Fast path (jspi pin (j)): the value still returns to wasm through an
|
|
293
|
+
// engine microtask hop, so the rest of the caller's frame is an engine
|
|
294
|
+
// continuation chunk like any other. The synchronous claim covers any
|
|
295
|
+
// reads before the hop; the sentinel re-claims contiguously ahead of
|
|
296
|
+
// the hop reaction (see the header above — issue #24's second shape
|
|
297
|
+
// was exactly a fast-path hop chunk misattributed after a sibling's
|
|
298
|
+
// claim intervened).
|
|
299
|
+
claimActivationAmbient(owner);
|
|
300
|
+
sentinelFor(owner);
|
|
301
|
+
return r;
|
|
302
|
+
}
|
|
303
|
+
if (SP_TRACE) {
|
|
304
|
+
console.error(`[sp] hop-suspend owner=${dbgId(owner)} promise=${dbgId(r)}`);
|
|
305
|
+
}
|
|
306
|
+
return attributeContinuation(owner, r);
|
|
307
|
+
};
|
|
308
|
+
return makeSuspending(claimingFn);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Fail loudly at instantiate time if the two wrapping sites disagree.
|
|
312
|
+
*
|
|
313
|
+
* The dangerous direction, per jspi pin (c), is a `Suspending` import
|
|
314
|
+
* reachable from a non-`promising` activation — that traps unconditionally,
|
|
315
|
+
* even on the plain-value path. Entry wrapping in jspi mode is unconditional
|
|
316
|
+
* (every lifted export, callback, and block-capable FACT callee), so the
|
|
317
|
+
* structural invariant is `importsWrapped ⇒ entriesWrapped`, per mode.
|
|
318
|
+
*
|
|
319
|
+
* Entries-without-imports is legitimate: per-declaration classification
|
|
320
|
+
* (`trampolineNeedsSuspension`) wraps no imports in a component whose
|
|
321
|
+
* built-ins are all non-blocking async forms, while the mode can still be
|
|
322
|
+
* jspi via the lift-shape over-approximation in `planNeedsSuspension`
|
|
323
|
+
* (canonical options carrying `async` with no callback are counted whether
|
|
324
|
+
* they belong to a lift or to a copy built-in — the plan does not say which,
|
|
325
|
+
* and the false positive only costs Promise-shaped exports).
|
|
326
|
+
*/
|
|
327
|
+
export function assertModeConsistent(mode, entriesWrapped, importsWrapped) {
|
|
328
|
+
if (mode === "plain") {
|
|
329
|
+
assert_(!entriesWrapped && !importsWrapped, `plain mode with wrapped entries=${entriesWrapped} / ` +
|
|
330
|
+
`imports=${importsWrapped} — wrapping ran under the wrong mode`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
assert_(entriesWrapped || !importsWrapped, `suspension mode jspi wrapped imports without wrapping any entry ` +
|
|
334
|
+
`(entries=${entriesWrapped}, imports=${importsWrapped}) — a ` +
|
|
335
|
+
`Suspending import reached from a non-promising activation traps ` +
|
|
336
|
+
`unconditionally (jspi pin (c))`);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A suspended wasm activation, presented to the scheduler as an ordinary
|
|
340
|
+
* parked thread.
|
|
341
|
+
*
|
|
342
|
+
* `Store.tick` only ever asks a thread three things — is it waiting, is it
|
|
343
|
+
* ready, please resume — so a suspension point that answers those is
|
|
344
|
+
* indistinguishable from a generator-backed `Thread`. Resuming means settling
|
|
345
|
+
* the Promise the `Suspending` import returned, which is what the engine is
|
|
346
|
+
* awaiting; the value handed to `resume` is whatever the reference would have
|
|
347
|
+
* produced at that block point (an event triple's code, a subtask state, a
|
|
348
|
+
* packed copy result).
|
|
349
|
+
*/
|
|
350
|
+
const SP_TRACE = (() => {
|
|
351
|
+
try {
|
|
352
|
+
return Deno.env.get("CE_SP_TRACE") === "1";
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
})();
|
|
358
|
+
export class SuspensionPoint {
|
|
359
|
+
task;
|
|
360
|
+
readyFunc;
|
|
361
|
+
cancellable;
|
|
362
|
+
produce;
|
|
363
|
+
onSettled;
|
|
364
|
+
promise;
|
|
365
|
+
#settle;
|
|
366
|
+
#fail;
|
|
367
|
+
#done = false;
|
|
368
|
+
#finished = false;
|
|
369
|
+
#store;
|
|
370
|
+
/**
|
|
371
|
+
* WHO the engine will resume when this point's promise settles.
|
|
372
|
+
*
|
|
373
|
+
* Captured HERE, at construction, and not derived at resume time: the
|
|
374
|
+
* blocking built-in that mints this point is running under the suspending
|
|
375
|
+
* activation's own ambient, so the ambient names that activation exactly.
|
|
376
|
+
* This is the replacement for the async-context store the scheduler
|
|
377
|
+
* used to rely on (M3A-1): same value, obtained by construction instead of
|
|
378
|
+
* by asking the platform to carry a context across the engine's resumption.
|
|
379
|
+
* `task.implicitThread` is the fallback for the one shape that has no
|
|
380
|
+
* ambient at all — a built-in reached during instantiation.
|
|
381
|
+
*/
|
|
382
|
+
// deno-lint-ignore no-explicit-any
|
|
383
|
+
owner;
|
|
384
|
+
constructor(store,
|
|
385
|
+
// deno-lint-ignore no-explicit-any
|
|
386
|
+
task,
|
|
387
|
+
/** Resumable once this holds; `null` = only an explicit resume. */
|
|
388
|
+
readyFunc, cancellable,
|
|
389
|
+
/** Produces the value to hand back to wasm at resume time. */
|
|
390
|
+
produce,
|
|
391
|
+
// deno-lint-ignore no-explicit-any
|
|
392
|
+
owner,
|
|
393
|
+
/**
|
|
394
|
+
* `finally`-style hook: runs EXACTLY ONCE, on whichever terminal
|
|
395
|
+
* transition this point takes — produce-success, produce-throw, or
|
|
396
|
+
* `abandon` (issue #102). It is the seam a blocking built-in uses to
|
|
397
|
+
* discharge state it owns for the duration of the park (the FACT
|
|
398
|
+
* start-calls' borrow-lender scopes, contracts/intrinsics.md v0.2
|
|
399
|
+
* amendment 2) without having to trust that `produce` runs.
|
|
400
|
+
*
|
|
401
|
+
* INVARIANTS this hook must respect, so bridge.ts's own contracts are
|
|
402
|
+
* not disturbed:
|
|
403
|
+
* * it must not throw (a throw here would escape `resume` *after* the
|
|
404
|
+
* import's Promise was settled, i.e. into whatever drained the
|
|
405
|
+
* scheduler); it is called inside a `try`/`catch` that reports such
|
|
406
|
+
* a throw rather than propagating it;
|
|
407
|
+
* * it must be idempotent-safe by construction anyway, because it runs
|
|
408
|
+
* AFTER `produce` on the success path — a built-in that already did
|
|
409
|
+
* its cleanup inside `produce` (to pin cleanup ordering relative to
|
|
410
|
+
* the produced value) sees this as a no-op backstop;
|
|
411
|
+
* * it must not resume/abandon this or any other suspension point.
|
|
412
|
+
*/
|
|
413
|
+
onSettled) {
|
|
414
|
+
this.task = task;
|
|
415
|
+
this.readyFunc = readyFunc;
|
|
416
|
+
this.cancellable = cancellable;
|
|
417
|
+
this.produce = produce;
|
|
418
|
+
this.onSettled = onSettled;
|
|
419
|
+
this.#store = store;
|
|
420
|
+
this.owner = owner ?? maybeCurrentThread() ?? task?.implicitThread ?? null;
|
|
421
|
+
if (SP_TRACE) {
|
|
422
|
+
console.error(`[sp] mint ${dbgId(this)} owner=${dbgId(this.owner)} task=${dbgId(this.task)}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`);
|
|
423
|
+
}
|
|
424
|
+
this.promise = new Promise((res, rej) => {
|
|
425
|
+
this.#settle = res;
|
|
426
|
+
this.#fail = rej;
|
|
427
|
+
});
|
|
428
|
+
store.startWaiting(this);
|
|
429
|
+
}
|
|
430
|
+
waiting() {
|
|
431
|
+
return !this.#done;
|
|
432
|
+
}
|
|
433
|
+
ready() {
|
|
434
|
+
return !this.#done && this.readyFunc !== null && this.readyFunc();
|
|
435
|
+
}
|
|
436
|
+
/** Settle the import's Promise; the engine resumes the wasm activation. */
|
|
437
|
+
resume(cancelled = false) {
|
|
438
|
+
assert_(!this.#done, "resume of an already-resumed suspension point");
|
|
439
|
+
// Mirrors task/thread.ts:187-190 (definitions.py:367 `Thread.resume`):
|
|
440
|
+
// a cancelled resume is only legal at a cancellable block point (#93).
|
|
441
|
+
assert_(this.cancellable || !cancelled, "cancelled resume of a non-cancellable suspension point");
|
|
442
|
+
if (SP_TRACE) {
|
|
443
|
+
console.error(`[sp] resume ${dbgId(this)} owner=${dbgId(this.owner)}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`);
|
|
444
|
+
}
|
|
445
|
+
this.#done = true;
|
|
446
|
+
this.#store.stopWaiting(this);
|
|
447
|
+
try {
|
|
448
|
+
this.#resumeInner(cancelled);
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
// Terminal state reached, by whichever of the two paths below. See
|
|
452
|
+
// `onSettled`: this is the backstop, not the primary cleanup site, so
|
|
453
|
+
// it runs after `produce` and after the settle — on the success path it
|
|
454
|
+
// observes cleanup `produce` already did, and changes nothing.
|
|
455
|
+
this.#finish();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
#resumeInner(cancelled) {
|
|
459
|
+
let value;
|
|
460
|
+
try {
|
|
461
|
+
value = this.produce(cancelled);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
// A trap computed at resume time (e.g. the event turned out to be a
|
|
465
|
+
// trapping one) must reach the guest as a rejection of the import's
|
|
466
|
+
// Promise, which the engine turns back into a wasm trap — empirical
|
|
467
|
+
// fact (e): post-resume traps arrive as ordinary rejections.
|
|
468
|
+
//
|
|
469
|
+
// This is a RESUMPTION too: the engine hands control back to the wasm
|
|
470
|
+
// activation (to unwind it), and the guest's FACT adapter runs its
|
|
471
|
+
// trap-path built-ins — `exit-sync-call` among them — before our own
|
|
472
|
+
// continuation regains control. So it takes the ambient claim exactly
|
|
473
|
+
// like the value path. Missing it here is what `trap-if-done.wast:448`
|
|
474
|
+
// and the `assert_trap` rows of `big-interleaving-test.wast` detect
|
|
475
|
+
// ("exit-sync-call with an empty sync-call stack").
|
|
476
|
+
if (maybeCurrentThread() === undefined)
|
|
477
|
+
claimActivationAmbient(this.owner);
|
|
478
|
+
setResumingThread(this.task?.implicitThread ?? null);
|
|
479
|
+
this.#fail(e);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// Claim the ambient for this activation across the engine's resumption:
|
|
483
|
+
// settling the import's Promise hands control to wasm, which will call
|
|
484
|
+
// built-ins with an empty bracket stack. The claim names `owner` — the
|
|
485
|
+
// activation captured when this point was minted — not a guess derived
|
|
486
|
+
// now; see `owner` and `setResumingThread`.
|
|
487
|
+
//
|
|
488
|
+
// If the DRIVER's slot is live for the activation currently executing (it
|
|
489
|
+
// is the code that called us — a running guest's `subtask.cancel`
|
|
490
|
+
// delivering a cancellation settles the callee's suspension from inside
|
|
491
|
+
// its own frame), that claim has served its purpose; consume it rather
|
|
492
|
+
// than false-positive the one-claimant assert.
|
|
493
|
+
consumeClaimIfRunning();
|
|
494
|
+
// The activation-ambient claim (site (i) in scheduler.ts). Taken only when
|
|
495
|
+
// NOBODY is running right now: if a guest activation is executing, `owner`
|
|
496
|
+
// does not run until that activation yields, and pushing onto a
|
|
497
|
+
// LAST-IN-FIRST-OUT stack now would make `owner` the ambient for the
|
|
498
|
+
// caller's remaining frame. In that shape `owner` is picked up either by
|
|
499
|
+
// its own first `Suspending` call (site (ii)) or, before that, by the
|
|
500
|
+
// driver's `resumingThread` slot at the bottom tier — exactly as it always
|
|
501
|
+
// was.
|
|
502
|
+
if (maybeCurrentThread() === undefined)
|
|
503
|
+
claimActivationAmbient(this.owner);
|
|
504
|
+
setResumingThread(this.task?.implicitThread ?? null);
|
|
505
|
+
this.#settle(value);
|
|
506
|
+
}
|
|
507
|
+
/** Abandon this suspension without resuming the guest (teardown paths). */
|
|
508
|
+
abandon(reason) {
|
|
509
|
+
if (this.#done)
|
|
510
|
+
return;
|
|
511
|
+
this.#done = true;
|
|
512
|
+
this.#store.stopWaiting(this);
|
|
513
|
+
try {
|
|
514
|
+
this.#fail(reason);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
// The settle path that never runs `produce` at all — the one issue #102
|
|
518
|
+
// is about.
|
|
519
|
+
this.#finish();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/** Run `onSettled` at most once. Never throws (see the field's doc). */
|
|
523
|
+
#finish() {
|
|
524
|
+
if (this.#finished)
|
|
525
|
+
return;
|
|
526
|
+
this.#finished = true;
|
|
527
|
+
if (this.onSettled === undefined)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
this.onSettled();
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
// Swallowing is the conservative reading: we are past the point where
|
|
534
|
+
// the guest's Promise was settled, so there is no frame left that could
|
|
535
|
+
// meaningfully receive this. Report loudly instead of corrupting an
|
|
536
|
+
// unrelated drain.
|
|
537
|
+
console.error(`[sp] onSettled threw for ${dbgId(this)}:`, e);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The single shape every blocking built-in needs: "park the calling wasm frame
|
|
543
|
+
* until `readyFunc` holds, then hand it `produce()`".
|
|
544
|
+
*
|
|
545
|
+
* In `plain` mode the caller must not reach here — it raises `NeedsJspi` at
|
|
546
|
+
* its own site, with a message naming the operation, exactly as before. In
|
|
547
|
+
* `jspi` mode this returns a Promise, and because the built-in was
|
|
548
|
+
* `Suspending`-wrapped the engine suspends the activation on it.
|
|
549
|
+
*/
|
|
550
|
+
export function blockCurrentActivation(input) {
|
|
551
|
+
// GATE LIFETIME: pristine reference semantics (definitions.py
|
|
552
|
+
// `block_internal` line 378 does NOT touch `inst.exclusive_thread`). A
|
|
553
|
+
// RESOLVED task that parks mid-frame in a synchronous built-in KEEPS
|
|
554
|
+
// gating its instance — identical to wasmtime, whose
|
|
555
|
+
// `ConcurrentInstanceState.do_not_enter` is set/cleared only by
|
|
556
|
+
// `enter_instance`/`exit_instance`, i.e. bracketed on the whole core
|
|
557
|
+
// invocation (source refs distilled on issue #43; exam kit archived at
|
|
558
|
+
// 4f3351f:exams/wasmtime-exclusivity/). polyengine's former release-at-BLOCK
|
|
559
|
+
// divergence was removed by issue #43; `test/async/sync-streams.wast` is
|
|
560
|
+
// now green via the DEFERRED ENTRY DECISION in intrinsics/fact_calls.ts
|
|
561
|
+
// (`createAsyncStartCall`), not via gate release.
|
|
562
|
+
//
|
|
563
|
+
// WHO is parking — read BEFORE anything below disturbs the ambient. This
|
|
564
|
+
// one value serves both purposes: it is the activation the engine will
|
|
565
|
+
// resume when this point settles (`SuspensionPoint.owner`, the replacement
|
|
566
|
+
// for the retired async-context store), and it is the activation whose
|
|
567
|
+
// ambient claim ends here. Reading it after the release yields `undefined`
|
|
568
|
+
// and strands the point with no owner (measured: `cancellable.wast:322`
|
|
569
|
+
// then reported `pending-capability: instantiation-time task context`).
|
|
570
|
+
const owner = maybeCurrentThread() ?? input.task?.implicitThread ?? null;
|
|
571
|
+
// The activation is parking: if it still carried the resumed-ambient claim
|
|
572
|
+
// from the settle that resumed it, that claim's window closes here (the
|
|
573
|
+
// other closing edge — the activation FINISHING — is handled by
|
|
574
|
+
// `Store.noteAwaiting`'s settle continuation).
|
|
575
|
+
consumeClaimIfRunning();
|
|
576
|
+
releaseActivationAmbient(owner);
|
|
577
|
+
const point = new SuspensionPoint(input.store, input.task, input.readyFunc, input.cancellable, input.produce, owner, input.onSettled);
|
|
578
|
+
return point.promise;
|
|
579
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// JSPI mechanics — a small typed wrapper over `WebAssembly.promising` and
|
|
2
|
+
// `WebAssembly.Suspending`. This module is intentionally standalone: it has
|
|
3
|
+
// no knowledge of the task/scheduler model (runtime/src/task,exec,intrinsics,
|
|
4
|
+
// plan) and must not import from those directories. It is the mechanics
|
|
5
|
+
// layer that the upcoming JSPI scheduler phase (docs/architecture.md §6) will consume —
|
|
6
|
+
// not the scheduler itself.
|
|
7
|
+
//
|
|
8
|
+
// # The frame rule (docs/architecture.md §5)
|
|
9
|
+
//
|
|
10
|
+
// From the js-promise-integration proposal Overview: only WebAssembly
|
|
11
|
+
// computations may be suspended — only wasm frames may be active between the
|
|
12
|
+
// call to a `promising`-wrapped export and any call to a `Suspending`-wrapped
|
|
13
|
+
// import. A JS frame anywhere in between traps.
|
|
14
|
+
//
|
|
15
|
+
// This has been empirically pinned in `runtime/tests/jspi/frame_rule_test.ts`
|
|
16
|
+
// against Deno 2.9.5 / V8 15.0.245.2-rusty: see that file for the exact
|
|
17
|
+
// error constructor, message, and timing observed. Consequences (also
|
|
18
|
+
// findings, not just theory, per that test):
|
|
19
|
+
//
|
|
20
|
+
// - Host-boundary JS glue is safe: a `Suspending`-wrapped import's JS body
|
|
21
|
+
// runs to completion and returns a Promise; the actual suspension
|
|
22
|
+
// happens only after control returns to wasm, so glue code itself never
|
|
23
|
+
// sits on the suspended stack (pinned in
|
|
24
|
+
// `suspending_import_test.ts::pure_wasm_stack_suspends_and_resumes`).
|
|
25
|
+
// - Cross-component JS glue between two wasm activations traps the moment
|
|
26
|
+
// anything below it suspends — cross-component adapters must be wasm
|
|
27
|
+
// (FACT), not JS (docs/architecture.md §4.1, §5).
|
|
28
|
+
//
|
|
29
|
+
// # Reentrancy and concurrency (empirical, not mechanics-layer policy)
|
|
30
|
+
//
|
|
31
|
+
// The engine permits things the Component Model forbids (e.g. reentering an
|
|
32
|
+
// instance while one of its exports is suspended) — docs/architecture.md §6 flags this as
|
|
33
|
+
// the scheduler's job to gate, not the engine's. See
|
|
34
|
+
// `reentry_test.ts`/`concurrent_activations_test.ts` for what the engine
|
|
35
|
+
// actually allows; this module does not enforce CM invariants.
|
|
36
|
+
import { jspiApi } from "./types.js";
|
|
37
|
+
/** True if the current engine implements `WebAssembly.promising` and
|
|
38
|
+
* `WebAssembly.Suspending`. Both are phase-4 API surface (docs/architecture.md §3): no
|
|
39
|
+
* fallback path exists or is planned for engines without them. */
|
|
40
|
+
export function isSupported() {
|
|
41
|
+
return (typeof globalThis.WebAssembly ===
|
|
42
|
+
"object" && jspiApi() !== null);
|
|
43
|
+
}
|
|
44
|
+
/** Throws if JSPI is not available in the current engine. Call this before
|
|
45
|
+
* using anything else in this module if you want a clear error instead of a
|
|
46
|
+
* `TypeError: WebAssembly.promising is not a function`. */
|
|
47
|
+
export function assertSupported() {
|
|
48
|
+
if (!isSupported()) {
|
|
49
|
+
throw new Error("JSPI (WebAssembly.promising / WebAssembly.Suspending) is not " +
|
|
50
|
+
"available in this engine; see docs/architecture.md §3 for the compatibility " +
|
|
51
|
+
"floor (no fallback path exists).");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Wrap a wasm-exported function (as retrieved from
|
|
56
|
+
* `instance.exports.someExport`) so that calling it:
|
|
57
|
+
* - always returns a Promise,
|
|
58
|
+
* - suspends the underlying wasm activation (rather than trapping) the
|
|
59
|
+
* first time it calls a `Suspending`-wrapped import that itself returns
|
|
60
|
+
* a genuine Promise,
|
|
61
|
+
* - resolves that Promise with the export's return value once the wasm
|
|
62
|
+
* activation runs to completion (after zero or more suspend/resume
|
|
63
|
+
* cycles).
|
|
64
|
+
*
|
|
65
|
+
* This is a direct type-safe pass-through of `WebAssembly.promising`; it
|
|
66
|
+
* does no extra bookkeeping. `TArgs`/`TReturn` are the caller's own
|
|
67
|
+
* annotation of the underlying export's signature (not reflected — see
|
|
68
|
+
* docs/architecture.md §3's note on js-types being flagged/phase-3).
|
|
69
|
+
*/
|
|
70
|
+
export function makePromising(wasmExport) {
|
|
71
|
+
assertSupported();
|
|
72
|
+
return jspiApi().promising(wasmExport);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wrap a JS function as a `Suspending` import: when a `promising`-suspendable
|
|
76
|
+
* wasm activation calls it (per the frame rule above) and it returns a
|
|
77
|
+
* genuine Promise, the wasm activation suspends until that Promise settles;
|
|
78
|
+
* if it returns a non-Promise value (or the call site is not suspension-
|
|
79
|
+
* eligible), the value passes straight through — the "fast path", pinned in
|
|
80
|
+
* `suspending_import_test.ts::non_promise_return_is_fast_path`.
|
|
81
|
+
*
|
|
82
|
+
* The returned value is an opaque `WebAssembly.Suspending` instance; hand it
|
|
83
|
+
* directly to the instantiation `imports` object in the slot the wasm module
|
|
84
|
+
* expects a function import.
|
|
85
|
+
*/
|
|
86
|
+
export function makeSuspending(fn) {
|
|
87
|
+
assertSupported();
|
|
88
|
+
return new (jspiApi().Suspending)(fn);
|
|
89
|
+
}
|