@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,564 @@
|
|
|
1
|
+
// Host trampolines (contracts/intrinsics.md §B) and FACT-adapter intrinsic
|
|
2
|
+
// obligations (§A) — the M0 subset, with instantiate-time (never call-time)
|
|
3
|
+
// milestone-aware failures for everything else.
|
|
4
|
+
//
|
|
5
|
+
// Implemented in M0:
|
|
6
|
+
// lower-import host function call through descriptor-IR lift/lower
|
|
7
|
+
// trap FACT `Trap` import -> ComponentTrap
|
|
8
|
+
// enter/exit-sync-call degenerate sync-call bookkeeping (assert-and-count)
|
|
9
|
+
// resource-new/rep/drop sync resource paths over cabi handle tables
|
|
10
|
+
// (M1 schedule, implemented early: the resources
|
|
11
|
+
// fixture references them at instantiation)
|
|
12
|
+
//
|
|
13
|
+
// Everything else fails at instantiate time with the milestone at which
|
|
14
|
+
// intrinsics.md §B schedules it — "this component needs the M2 task core"
|
|
15
|
+
// is a feature, not a crash.
|
|
16
|
+
import { canonResourceDrop, canonResourceNew, canonResourceRep, trap, } from "../cabi/mod.js";
|
|
17
|
+
import { ResourceHandle } from "../cabi/handles.js";
|
|
18
|
+
import { trapIf } from "../cabi/trap.js";
|
|
19
|
+
import { assert_ } from "../cabi/trap.js";
|
|
20
|
+
import { maybeCurrentThread, PendingCapability, withPoisonCause } from "../task/mod.js";
|
|
21
|
+
import { UnsupportedFeatureError } from "./errors.js";
|
|
22
|
+
import { createBackpressureDec, createBackpressureInc, createSubtaskCancel, createSubtaskDrop, createTaskCancel, createTaskReturn, createThreadYield, createWaitableJoin, createWaitableSetDrop, createWaitableSetNew, createWaitableSetPoll, createWaitableSetWait, } from "./async_builtins.js";
|
|
23
|
+
import { createAsyncStartCall, createPrepareCall, createSyncStartCall, } from "./fact_calls.js";
|
|
24
|
+
import { createErrorContextDebugMessage, createErrorContextDrop, createErrorContextNew, createFutureCancelRead, createFutureCancelWrite, createFutureDropReadable, createFutureDropWritable, createFutureNew, createFutureRead, createFutureWrite, createStreamCancelRead, createStreamCancelWrite, createStreamDropReadable, createStreamDropWritable, createStreamNew, createStreamRead, createStreamWrite, createStreamTransfer, createFutureTransfer, createErrorContextTransfer, } from "./stream_builtins.js";
|
|
25
|
+
import { createTranscoder, TRANSCODE_OPS, } from "./transcode.js";
|
|
26
|
+
export * from "./transcode.js";
|
|
27
|
+
export * from "./context.js";
|
|
28
|
+
export * from "./async_builtins.js";
|
|
29
|
+
export * from "./fact_calls.js";
|
|
30
|
+
export * from "./stream_builtins.js";
|
|
31
|
+
/**
|
|
32
|
+
* Trap-code → message, from wasmtime-environ 47.0.3 `trap_encoding.rs`
|
|
33
|
+
* (`generate_trap_type!`), whose ordinals are what FACT passes to the
|
|
34
|
+
* `runtime.trap` import. Only the codes a sync FACT adapter can raise are
|
|
35
|
+
* listed; anything else falls back to the numeric code.
|
|
36
|
+
*
|
|
37
|
+
* Rendered with wasmtime's `"wasm trap: "` prefix (its `impl Display for
|
|
38
|
+
* Trap`), because that is the text the official suite's `assert_trap`
|
|
39
|
+
* commands expect for adapter-raised traps
|
|
40
|
+
* (e.g. `values/realloc.wast:67,94`).
|
|
41
|
+
*/
|
|
42
|
+
const FACT_TRAP_MESSAGES = {
|
|
43
|
+
9: "wasm `unreachable` instruction executed",
|
|
44
|
+
17: "cannot enter component instance",
|
|
45
|
+
23: "cannot leave component instance",
|
|
46
|
+
24: "cannot block a synchronous task before returning",
|
|
47
|
+
25: "invalid `char` bit pattern",
|
|
48
|
+
30: "string content out-of-bounds",
|
|
49
|
+
31: "list content out-of-bounds",
|
|
50
|
+
32: "invalid variant discriminant",
|
|
51
|
+
33: "unaligned pointer",
|
|
52
|
+
46: "reference count overflow",
|
|
53
|
+
49: "uncaught exception propagated out of component",
|
|
54
|
+
};
|
|
55
|
+
/** Ordinal of `Trap::UncaughtException` in wasmtime's trap encoding. */
|
|
56
|
+
const TRAP_UNCAUGHT_EXCEPTION = 49;
|
|
57
|
+
export { UnsupportedFeatureError } from "./errors.js";
|
|
58
|
+
/** Milestone at which each trampoline kind stops instantiate-failing. */
|
|
59
|
+
const TRAMPOLINE_MILESTONE = {
|
|
60
|
+
"lower-import": "M0",
|
|
61
|
+
"trap": "M0",
|
|
62
|
+
"enter-sync-call": "M0",
|
|
63
|
+
"exit-sync-call": "M0",
|
|
64
|
+
"resource-new": "M1",
|
|
65
|
+
"resource-rep": "M1",
|
|
66
|
+
"resource-drop": "M1",
|
|
67
|
+
"transcoder": "M1",
|
|
68
|
+
"resource-transfer-own": "M1",
|
|
69
|
+
"resource-transfer-borrow": "M1",
|
|
70
|
+
};
|
|
71
|
+
function milestoneOf(kind) {
|
|
72
|
+
return TRAMPOLINE_MILESTONE[kind] ?? "M2";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The borrow bookkeeping of one in-flight synchronous cross-component call,
|
|
76
|
+
* bracketed by the FACT adapter's `enter-sync-call` / `exit-sync-call`
|
|
77
|
+
* imports (wasmtime-environ 47.0.3 `fact/trampoline.rs:810,904`: enter is
|
|
78
|
+
* emitted *before* argument translation and exit *after* the callee returns,
|
|
79
|
+
* so every resource transfer for the call happens inside the bracket).
|
|
80
|
+
*
|
|
81
|
+
* It plays the role definitions.py gives the callee `Subtask`/`Task`:
|
|
82
|
+
*
|
|
83
|
+
* - `lenders` — handles lent to the callee (`Subtask.lenders`); each
|
|
84
|
+
* `num_lends` is dropped again when the call returns, which is what makes
|
|
85
|
+
* a lender's own handle liftable again afterwards.
|
|
86
|
+
* - `numBorrows` — borrow handles lowered into the callee's table
|
|
87
|
+
* (`Task.num_borrows`); the callee must drop them all before returning
|
|
88
|
+
* (definitions.py `Task.return_`: `trap_if(self.num_borrows > 0)`).
|
|
89
|
+
*
|
|
90
|
+
* Structurally satisfies cabi's `TaskBorrowScope` and `SubtaskBorrowScope`.
|
|
91
|
+
*/
|
|
92
|
+
export class SyncCallScope {
|
|
93
|
+
numBorrows = 0;
|
|
94
|
+
lenders = [];
|
|
95
|
+
/**
|
|
96
|
+
* definitions.py `Subtask.add_lender` (line 890) — note there is **no**
|
|
97
|
+
* `own` check, and `lift_borrow` (line 1516) calls it unconditionally: a
|
|
98
|
+
* component that received a borrow may lend it onward, and the borrow
|
|
99
|
+
* handle's own `num_lends` is what blocks `resource.drop` on it until the
|
|
100
|
+
* onward call returns (`canon_resource_drop`, line 2325, traps on
|
|
101
|
+
* `num_lends != 0` for owning *and* borrowed handles alike).
|
|
102
|
+
* wasmtime 47.0.3 `vm/component/resources.rs:285` (`resource_lift_borrow`)
|
|
103
|
+
* agrees.
|
|
104
|
+
*/
|
|
105
|
+
addLender(h) {
|
|
106
|
+
h.numLends += 1;
|
|
107
|
+
this.lenders.push(h);
|
|
108
|
+
}
|
|
109
|
+
/** definitions.py `Subtask.deliver_resolve` (lines 902-906): releases lenders at delivery time. */
|
|
110
|
+
releaseLenders() {
|
|
111
|
+
for (const h of this.lenders)
|
|
112
|
+
h.numLends -= 1;
|
|
113
|
+
this.lenders.length = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create the JS function backing one plan trampoline. Called during
|
|
118
|
+
* initializer/arg/export resolution — i.e. at instantiate time — so an
|
|
119
|
+
* unsupported kind fails instantiation, not the first call
|
|
120
|
+
* (plan-format.md "Executor obligations"). Unreferenced trampolines are
|
|
121
|
+
* never created and therefore never fail (intrinsics.md §B tolerates e.g.
|
|
122
|
+
* an unreferenced task-return until M2).
|
|
123
|
+
*/
|
|
124
|
+
export function createTrampoline(decl, ctx) {
|
|
125
|
+
const fn = createTrampolineBody(decl, ctx);
|
|
126
|
+
// Remember host traps so the FACT exception barrier cannot swallow them
|
|
127
|
+
// (see `HostTrapState`). This wraps the `trap` trampoline too, which is
|
|
128
|
+
// what keeps a specific trap specific across *nested* adapters: the inner
|
|
129
|
+
// barrier's `trap` trampoline restores and rethrows the real trap, this
|
|
130
|
+
// wrapper re-records it, and the outer barrier restores it again instead
|
|
131
|
+
// of reporting the generic `UncaughtException`.
|
|
132
|
+
return (...args) => {
|
|
133
|
+
try {
|
|
134
|
+
return fn(...args);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
ctx.trapState.pending = e;
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A trampoline that instantiates fine but fails at its first call, naming the
|
|
144
|
+
* phase that will implement it. See the CONTRACT note at the stream/future
|
|
145
|
+
* cases for why these are not instantiate-time failures.
|
|
146
|
+
*/
|
|
147
|
+
function deferredCapability(kind, capability) {
|
|
148
|
+
return () => {
|
|
149
|
+
throw new PendingCapability(`built-in '${kind}' is not implemented yet: ${capability}`);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The component instance a trampoline is declared in (wasmtime names it in
|
|
154
|
+
* every instance-scoped `Trampoline` variant). This is the static answer to
|
|
155
|
+
* definitions.py's `current_instance()`, and unlike it, it is defined during
|
|
156
|
+
* instantiation — when a core module's start function may already be calling
|
|
157
|
+
* these built-ins. See the header of ./async_builtins.ts.
|
|
158
|
+
*/
|
|
159
|
+
/** Narrow the trampoline context to what the stream built-ins need. */
|
|
160
|
+
function sctx(ctx) {
|
|
161
|
+
return ctx;
|
|
162
|
+
}
|
|
163
|
+
function declaredInstance(decl, ctx) {
|
|
164
|
+
const instance = decl.instance;
|
|
165
|
+
assert_(typeof instance === "number", `trampoline '${decl.kind}' has no declared component instance`);
|
|
166
|
+
return ctx.componentInstance(instance);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The FACT sync-call bracket stack in force right now: the running task's
|
|
170
|
+
* (activations interleave since background activations exist, so a single
|
|
171
|
+
* executor-wide stack is not a stack), or the executor's when no task is
|
|
172
|
+
* running — instantiation-time start functions.
|
|
173
|
+
*/
|
|
174
|
+
// deno-lint-ignore no-explicit-any
|
|
175
|
+
const SCOPE_TRACE = (() => {
|
|
176
|
+
try {
|
|
177
|
+
return Deno.env.get("CE_SCOPE_TRACE") === "1";
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
const taskIds = new WeakMap();
|
|
184
|
+
let nextTaskId = 1;
|
|
185
|
+
function taskId(t) {
|
|
186
|
+
if (t === undefined || t === null)
|
|
187
|
+
return "NONE(->ctx fallback)";
|
|
188
|
+
let id = taskIds.get(t);
|
|
189
|
+
if (id === undefined) {
|
|
190
|
+
id = nextTaskId++;
|
|
191
|
+
taskIds.set(t, id);
|
|
192
|
+
}
|
|
193
|
+
return `T${id}`;
|
|
194
|
+
}
|
|
195
|
+
function syncScopes(ctx, site = "?") {
|
|
196
|
+
const thread = maybeCurrentThread();
|
|
197
|
+
const scopes = thread?.syncCallStack ?? ctx.syncCallStack;
|
|
198
|
+
if (SCOPE_TRACE) {
|
|
199
|
+
console.error(`[scope] ${site} act=${taskId(thread)} depth=${scopes.length}`);
|
|
200
|
+
}
|
|
201
|
+
return scopes;
|
|
202
|
+
}
|
|
203
|
+
function createTrampolineBody(decl, ctx) {
|
|
204
|
+
switch (decl.kind) {
|
|
205
|
+
case "lower-import": {
|
|
206
|
+
const d = decl;
|
|
207
|
+
return ctx.loweredImport(d);
|
|
208
|
+
}
|
|
209
|
+
case "trap":
|
|
210
|
+
// FACT `runtime.trap` import: `(code: i32) -> ()` followed by
|
|
211
|
+
// `unreachable` (wasmtime-environ `fact/trampoline.rs:3932`).
|
|
212
|
+
return (code) => {
|
|
213
|
+
if (code === TRAP_UNCAUGHT_EXCEPTION) {
|
|
214
|
+
const pending = ctx.trapState.pending;
|
|
215
|
+
if (pending !== undefined) {
|
|
216
|
+
// Deliberately *not* cleared: an enclosing adapter's barrier will
|
|
217
|
+
// catch this rethrow and needs to restore the same trap. The slot
|
|
218
|
+
// is reset per lifted-export call (exec/boundary.ts), which is
|
|
219
|
+
// what bounds its lifetime.
|
|
220
|
+
throw pending;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const message = code === undefined
|
|
224
|
+
? undefined
|
|
225
|
+
: FACT_TRAP_MESSAGES[code];
|
|
226
|
+
trap(message === undefined
|
|
227
|
+
? `FACT adapter trap (code ${code ?? "?"})`
|
|
228
|
+
: `wasm trap: ${message}`);
|
|
229
|
+
};
|
|
230
|
+
// Sync-call task bookkeeping (intrinsics.md §A: "degenerate-case
|
|
231
|
+
// implementation in M0: assert-and-count"). wasmtime 47 signatures:
|
|
232
|
+
// enter-sync-call carries the caller/callee instance pair, which is what
|
|
233
|
+
// the reentrance gate below needs; balance of the bracket is asserted at
|
|
234
|
+
// component teardown by tests.
|
|
235
|
+
// Signatures (wasmtime-environ 47.0.3 `fact.rs:743,754`):
|
|
236
|
+
// async.enter-sync-call(caller_instance: i32, async: i32,
|
|
237
|
+
// callee_instance: i32) -> ()
|
|
238
|
+
// async.exit-sync-call() -> ()
|
|
239
|
+
case "enter-sync-call":
|
|
240
|
+
return (callerInstance, async_, calleeInstance) => {
|
|
241
|
+
// Reference reentrance gate. Every guest->guest call in
|
|
242
|
+
// definitions.py routes through the callee's lift wrapper:
|
|
243
|
+
// canon_lower (line 2312) calls
|
|
244
|
+
// `callee(on_start, on_resolve, caller = thread.task.inst)`,
|
|
245
|
+
// and `callee` is `Store.lift`'s `func_inst` (lines 578-585), whose
|
|
246
|
+
// first act is
|
|
247
|
+
// `trap_if(not inst.may_enter_from(caller))` (line 581)
|
|
248
|
+
// with `entering_set(caller) = callee.self_and_ancestors()
|
|
249
|
+
// - caller.self_and_ancestors()`
|
|
250
|
+
// (lines 230-234).
|
|
251
|
+
// A sync fused adapter is an *optimization* of that path, so the gate
|
|
252
|
+
// belongs here (issue #99).
|
|
253
|
+
//
|
|
254
|
+
// Note on the shape of the entering set, which is what makes this
|
|
255
|
+
// check safe for the legal shapes:
|
|
256
|
+
// * caller == callee, or either an ancestor of the other -> the
|
|
257
|
+
// entering set is empty and this never traps. Those pairs never
|
|
258
|
+
// reach this trampoline anyway: FACT emits an unconditional
|
|
259
|
+
// `CannotEnterComponent` trap for them at compile time
|
|
260
|
+
// (wasmtime-environ 47.0.3 `fact/trampoline.rs:120-127`), which
|
|
261
|
+
// is what `test/async/trap-on-reenter.wast` cases 2 and 3 pin.
|
|
262
|
+
// * an *idle* sibling -> `mayEnter` is true, no trap. This is what
|
|
263
|
+
// `test/async/sync-barges-in.wast` needs: an async callee that is
|
|
264
|
+
// merely blocked has already run `leave_to` (its `canon_lift`
|
|
265
|
+
// returned), so a sync sibling may barge in.
|
|
266
|
+
// * an *entered* sibling -> trap, which is the A -> C -> A cycle.
|
|
267
|
+
//
|
|
268
|
+
// CONTRACT / reachability: a pure guest-to-guest sibling cycle is
|
|
269
|
+
// unreachable by construction, because component instance imports
|
|
270
|
+
// form a DAG (a callee must be instantiated before its caller, so it
|
|
271
|
+
// cannot hold an import of its caller; `wasm-tools` rejects the
|
|
272
|
+
// mutual-import composition outright). wasmtime relies on exactly
|
|
273
|
+
// that to elide the runtime check in fused adapters -- see the
|
|
274
|
+
// comment in `may_enter`, wasmtime 47.0.3
|
|
275
|
+
// `runtime/component/concurrent.rs:1876-1886`, and
|
|
276
|
+
// `enter_guest_sync_call` (concurrent.rs:1723) which performs no
|
|
277
|
+
// reentrance check at all. The gate is kept anyway because the
|
|
278
|
+
// reference mandates it and no corpus test pins the permissive
|
|
279
|
+
// behaviour; it is cheap, and it is the honest place for the
|
|
280
|
+
// invariant to be asserted rather than assumed.
|
|
281
|
+
//
|
|
282
|
+
// Deliberately *not* done here: `enter_from` / `leave_to` around the
|
|
283
|
+
// bracket. The reference locks the callee for the duration, which
|
|
284
|
+
// would additionally trap host-mediated reentrance (host -> A.f ->
|
|
285
|
+
// C.g -> host import -> host invokes C.g).
|
|
286
|
+
//
|
|
287
|
+
// ADJUDICATED 2026-08-20 (polyengine#165; named divergence in
|
|
288
|
+
// docs/architecture.md section 6): accepted — the bracket stays
|
|
289
|
+
// omitted. Three grounds: wasmtime parity (`enter_guest_sync_call`
|
|
290
|
+
// checks nothing); taking the bracket would create a guest-to-guest
|
|
291
|
+
// lock spanning suspension points, reintroducing the await-spanning
|
|
292
|
+
// -lock class removed by #156/#160; and upstream is deleting the
|
|
293
|
+
// trap outright — CM PR #705 ("CABI: remove the may_enter
|
|
294
|
+
// flag/trap") makes previously-trapping reentrance valid, so this
|
|
295
|
+
// divergence is a trailing indicator of the removal and
|
|
296
|
+
// self-resolves at the submodule pin advance (migration map:
|
|
297
|
+
// polyengine#173). Until that pin advance the reference's checks polyengine
|
|
298
|
+
// DOES enforce stay in force.
|
|
299
|
+
if (typeof callerInstance === "number" &&
|
|
300
|
+
typeof calleeInstance === "number") {
|
|
301
|
+
const callerInst = ctx.componentInstance(callerInstance >>> 0);
|
|
302
|
+
const calleeInst = ctx.componentInstance(calleeInstance >>> 0);
|
|
303
|
+
// A poisoned callee's refusal names the original trap (polyengine#145).
|
|
304
|
+
if (!calleeInst.mayEnterFrom(callerInst)) {
|
|
305
|
+
trap(withPoisonCause(calleeInst, "cannot enter component instance"));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// `async_` records whether the callee is *async-lifted*. wasmtime
|
|
309
|
+
// stores it on the guest task it creates here
|
|
310
|
+
// (`concurrent.rs:1723` `enter_guest_sync_call`, whose `callee_async`
|
|
311
|
+
// parameter flows into `GuestTask::new`) and never traps on it.
|
|
312
|
+
//
|
|
313
|
+
// Before the task core existed this trampoline refused `async_ == 1`
|
|
314
|
+
// rather than silently treating an async callee as sync. That guard is
|
|
315
|
+
// now stale and actively wrong: a sync-lowered caller reaching an
|
|
316
|
+
// async-lifted export is the `sync-start-call` path
|
|
317
|
+
// (intrinsics/fact_calls.ts), and the task it needs is created by
|
|
318
|
+
// `prepare-call`, not here. What remains of this bracket for us is the
|
|
319
|
+
// borrow bookkeeping (`SyncCallScope`), which applies either way.
|
|
320
|
+
void async_;
|
|
321
|
+
ctx.stats.enterSyncCalls++;
|
|
322
|
+
// Per task where there is one; the executor-wide stack is the
|
|
323
|
+
// fallback for a start function running at instantiation time, which
|
|
324
|
+
// has no task (see `maybeCurrentTask`).
|
|
325
|
+
// Invariant, per ACTIVATION: every `enter` is matched by exactly one
|
|
326
|
+
// `exit` on the same stack. Recorded here so the `exit` side can
|
|
327
|
+
// assert it structurally rather than only by depth (CE_SCOPE_TRACE
|
|
328
|
+
// proved this is where the interesting failures live).
|
|
329
|
+
const scopes = syncScopes(ctx, "enter");
|
|
330
|
+
scopes.push(new SyncCallScope());
|
|
331
|
+
};
|
|
332
|
+
case "exit-sync-call":
|
|
333
|
+
return (..._args) => {
|
|
334
|
+
ctx.stats.exitSyncCalls++;
|
|
335
|
+
assert_(ctx.stats.exitSyncCalls <= ctx.stats.enterSyncCalls, "exit-sync-call without matching enter-sync-call");
|
|
336
|
+
const scope = syncScopes(ctx, "exit").pop();
|
|
337
|
+
assert_(scope !== undefined,
|
|
338
|
+
// If this fires, an `exit` reached an activation that never ran the
|
|
339
|
+
// matching `enter` -- the bracket is attached to the wrong unit
|
|
340
|
+
// again. See `Thread.syncCallStack`.
|
|
341
|
+
"exit-sync-call with an empty sync-call stack");
|
|
342
|
+
// definitions.py `Task.return_`: the callee may not return while it
|
|
343
|
+
// still holds borrow handles. Wording parity with wasmtime's
|
|
344
|
+
// exit-time check (drop-cross-task-borrow.wast:309 pins the async
|
|
345
|
+
// path; the sync bracket is the same check).
|
|
346
|
+
trapIf(scope.numBorrows > 0, "borrow handles still remain at the end of the call");
|
|
347
|
+
scope.releaseLenders();
|
|
348
|
+
};
|
|
349
|
+
// Guest-side resource built-ins (sync paths of docs/architecture.md §7 over the cabi
|
|
350
|
+
// handle tables). rep is always i32 in current wasmtime.
|
|
351
|
+
case "resource-new": {
|
|
352
|
+
const d = decl;
|
|
353
|
+
const inst = ctx.componentInstance(d.instance);
|
|
354
|
+
const rt = ctx.resourceToken(d.resource);
|
|
355
|
+
return (rep) => canonResourceNew(inst, rt, rep >>> 0);
|
|
356
|
+
}
|
|
357
|
+
case "resource-rep": {
|
|
358
|
+
const d = decl;
|
|
359
|
+
const inst = ctx.componentInstance(d.instance);
|
|
360
|
+
const rt = ctx.resourceToken(d.resource);
|
|
361
|
+
return (handle) => canonResourceRep(inst, rt, handle >>> 0);
|
|
362
|
+
}
|
|
363
|
+
case "resource-drop": {
|
|
364
|
+
const d = decl;
|
|
365
|
+
const inst = ctx.componentInstance(d.instance);
|
|
366
|
+
const rt = ctx.resourceToken(d.resource);
|
|
367
|
+
return (handle) => {
|
|
368
|
+
canonResourceDrop(inst, rt, handle >>> 0);
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// FACT resource transfer (contracts/intrinsics.md §A, wasmtime-environ
|
|
372
|
+
// 47.0.3 `fact.rs:721` — signature `(i32 src_handle, i32 src_table,
|
|
373
|
+
// i32 dst_table) -> i32 dst_handle`). These are the fused-adapter form of
|
|
374
|
+
// `lift_own`/`lower_own` and `lift_borrow`/`lower_borrow`
|
|
375
|
+
// (definitions.py) with the src/dst tables named by index rather than
|
|
376
|
+
// implied by the running instance.
|
|
377
|
+
// FACT string transcoders (contracts/intrinsics.md §B "M1"). The plan
|
|
378
|
+
// carries the op name plus the source/destination `RuntimeMemoryIndex`es;
|
|
379
|
+
// `./transcode.ts` holds the twelve operations.
|
|
380
|
+
case "transcoder": {
|
|
381
|
+
const d = decl;
|
|
382
|
+
if (d.from64 || d.to64) {
|
|
383
|
+
// 64-bit linear memories are out of scope (https://github.com/polymorph-components/polyengine/issues/12); refusing at
|
|
384
|
+
// instantiate time keeps "instantiate-time, never call-time".
|
|
385
|
+
throw new UnsupportedFeatureError("M2", `transcoder '${d.op}' over a 64-bit linear memory`);
|
|
386
|
+
}
|
|
387
|
+
if (!TRANSCODE_OPS.includes(d.op)) {
|
|
388
|
+
throw new UnsupportedFeatureError("M2", `unknown string transcode operation '${d.op}'`);
|
|
389
|
+
}
|
|
390
|
+
return createTranscoder(d.op, ctx.runtimeMemory(d.from), ctx.runtimeMemory(d.to));
|
|
391
|
+
}
|
|
392
|
+
// --- 0.3 async built-ins (contracts/intrinsics.md §B "M2") -------------
|
|
393
|
+
// All ported in ./async_builtins.ts; the ones that would have to block a
|
|
394
|
+
// wasm frame fail there, at the call site, with a JSPI-shaped message.
|
|
395
|
+
case "task-return":
|
|
396
|
+
return createTaskReturn(decl, ctx);
|
|
397
|
+
case "task-cancel":
|
|
398
|
+
return createTaskCancel();
|
|
399
|
+
// No `backpressure-set` case on purpose: wasmtime-environ 47.0.3 has only
|
|
400
|
+
// `Trampoline::BackpressureInc` / `BackpressureDec`
|
|
401
|
+
// (`component/info.rs:775,781`) — there is no `BackpressureSet` variant to
|
|
402
|
+
// dispatch, so a case for it would be unreachable code implying a wire
|
|
403
|
+
// shape that cannot occur. definitions.py's own dead
|
|
404
|
+
// `canon_backpressure_set` was removed upstream (CM PR #690); see
|
|
405
|
+
// upstream-component-model-repo-findings.md CM-2, RESOLVED.
|
|
406
|
+
case "backpressure-inc":
|
|
407
|
+
return createBackpressureInc(declaredInstance(decl, ctx));
|
|
408
|
+
case "backpressure-dec":
|
|
409
|
+
return createBackpressureDec(declaredInstance(decl, ctx));
|
|
410
|
+
case "waitable-set-new":
|
|
411
|
+
return createWaitableSetNew(declaredInstance(decl, ctx));
|
|
412
|
+
case "waitable-set-wait":
|
|
413
|
+
return createWaitableSetWait(decl, ctx, declaredInstance(decl, ctx), ctx.suspensionMode);
|
|
414
|
+
case "waitable-set-poll":
|
|
415
|
+
return createWaitableSetPoll(decl, ctx, declaredInstance(decl, ctx));
|
|
416
|
+
case "waitable-set-drop":
|
|
417
|
+
return createWaitableSetDrop(declaredInstance(decl, ctx));
|
|
418
|
+
case "waitable-join":
|
|
419
|
+
return createWaitableJoin(declaredInstance(decl, ctx));
|
|
420
|
+
case "subtask-drop":
|
|
421
|
+
return createSubtaskDrop(declaredInstance(decl, ctx));
|
|
422
|
+
case "subtask-cancel":
|
|
423
|
+
return createSubtaskCancel(decl, declaredInstance(decl, ctx), ctx.suspensionMode);
|
|
424
|
+
case "thread-yield":
|
|
425
|
+
return createThreadYield(decl, ctx.suspensionMode);
|
|
426
|
+
// --- FACT cross-component calls (see ./fact_calls.ts) -----------------
|
|
427
|
+
case "prepare-call":
|
|
428
|
+
return createPrepareCall(decl, ctx);
|
|
429
|
+
case "sync-start-call":
|
|
430
|
+
return createSyncStartCall(decl, ctx);
|
|
431
|
+
case "async-start-call":
|
|
432
|
+
return createAsyncStartCall(decl, ctx);
|
|
433
|
+
// --- stream / future / error-context (see ./stream_builtins.ts) -------
|
|
434
|
+
case "stream-new":
|
|
435
|
+
return createStreamNew(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
436
|
+
case "future-new":
|
|
437
|
+
return createFutureNew(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
438
|
+
case "stream-read":
|
|
439
|
+
return createStreamRead(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
440
|
+
case "stream-write":
|
|
441
|
+
return createStreamWrite(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
442
|
+
case "future-read":
|
|
443
|
+
return createFutureRead(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
444
|
+
case "future-write":
|
|
445
|
+
return createFutureWrite(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
446
|
+
case "stream-cancel-read":
|
|
447
|
+
return createStreamCancelRead(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
448
|
+
case "stream-cancel-write":
|
|
449
|
+
return createStreamCancelWrite(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
450
|
+
case "future-cancel-read":
|
|
451
|
+
return createFutureCancelRead(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
452
|
+
case "future-cancel-write":
|
|
453
|
+
return createFutureCancelWrite(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
454
|
+
case "stream-drop-readable":
|
|
455
|
+
return createStreamDropReadable(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
456
|
+
case "stream-drop-writable":
|
|
457
|
+
return createStreamDropWritable(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
458
|
+
case "future-drop-readable":
|
|
459
|
+
return createFutureDropReadable(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
460
|
+
case "future-drop-writable":
|
|
461
|
+
return createFutureDropWritable(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
462
|
+
case "error-context-new":
|
|
463
|
+
return createErrorContextNew(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
464
|
+
case "error-context-debug-message":
|
|
465
|
+
return createErrorContextDebugMessage(decl, sctx(ctx), declaredInstance(decl, ctx));
|
|
466
|
+
case "error-context-drop":
|
|
467
|
+
return createErrorContextDrop(declaredInstance(decl, ctx));
|
|
468
|
+
case "stream-transfer":
|
|
469
|
+
return createStreamTransfer(ctx);
|
|
470
|
+
case "future-transfer":
|
|
471
|
+
return createFutureTransfer(ctx);
|
|
472
|
+
case "error-context-transfer":
|
|
473
|
+
return createErrorContextTransfer(ctx,
|
|
474
|
+
// plan v3: the transfer's table arguments are
|
|
475
|
+
// `TypeComponentLocalErrorContextTableIndex`es (fact/trampoline.rs:
|
|
476
|
+
// 3526-3539), resolved through the plan's own `errorContextTables`
|
|
477
|
+
// section. Before v3 this went through `resourceTableInstance` — a
|
|
478
|
+
// different index space, which mis-routed silently whenever a
|
|
479
|
+
// concrete resource table happened to exist at the colliding slot
|
|
480
|
+
// (polyengine#89).
|
|
481
|
+
(t) => ctx.errorContextTableInstance(t));
|
|
482
|
+
case "resource-transfer-own":
|
|
483
|
+
return (handle, srcTable, dstTable) => transferOwn(ctx, handle >>> 0, srcTable, dstTable);
|
|
484
|
+
case "resource-transfer-borrow":
|
|
485
|
+
return (handle, srcTable, dstTable) => transferBorrow(ctx, handle >>> 0, srcTable, dstTable);
|
|
486
|
+
default:
|
|
487
|
+
throw new UnsupportedFeatureError(milestoneOf(decl.kind) === "M1" ? "M1" : "M2", `component requires host trampoline '${decl.kind}'`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// Resource transfer (FACT `resource.transfer-own` / `transfer-borrow`)
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
/**
|
|
494
|
+
* `lift_own` out of the source table followed by `lower_own` into the
|
|
495
|
+
* destination table (definitions.py `lift_own` / `lower_own`): the source
|
|
496
|
+
* handle is *removed* (ownership moves), must be owning, and must not be
|
|
497
|
+
* lent out.
|
|
498
|
+
*/
|
|
499
|
+
function transferOwn(ctx, handle, srcTable, dstTable) {
|
|
500
|
+
const src = ctx.resourceTableInstance(srcTable);
|
|
501
|
+
const dst = ctx.resourceTableInstance(dstTable);
|
|
502
|
+
const srcRt = ctx.resourceToken(srcTable);
|
|
503
|
+
const dstRt = ctx.resourceToken(dstTable);
|
|
504
|
+
const h = src.handles.remove(handle);
|
|
505
|
+
trapIf(!(h instanceof ResourceHandle), "transfer-own: not a resource handle");
|
|
506
|
+
const rh = h;
|
|
507
|
+
trapIf(rh.rt !== srcRt, "transfer-own: resource type mismatch");
|
|
508
|
+
// definitions.py `lift_own`: `trap_if(h.num_lends != 0)`.
|
|
509
|
+
trapIf(rh.numLends !== 0, "cannot remove owned resource while borrowed (handle still lent out)");
|
|
510
|
+
trapIf(!rh.own, "transfer-own: expected an owning handle");
|
|
511
|
+
return dst.handles.add(new ResourceHandle(dstRt, rh.rep, true));
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* `lift_borrow` from the source table followed by `lower_borrow` into the
|
|
515
|
+
* destination table. The source handle stays in place; the destination gets a
|
|
516
|
+
* non-owning handle.
|
|
517
|
+
*
|
|
518
|
+
* Two deviations from the plain lift/lower pair, both taken from
|
|
519
|
+
* definitions.py:
|
|
520
|
+
*
|
|
521
|
+
* - `lower_borrow` returns the *rep* directly when the destination instance
|
|
522
|
+
* is the one that implements the resource ("own the resource" fast path),
|
|
523
|
+
* since a component always has direct access to its own reps.
|
|
524
|
+
* - lender / `num_borrows` bookkeeping is attached to the enclosing
|
|
525
|
+
* `SyncCallScope` (the `enter-sync-call` / `exit-sync-call` bracket),
|
|
526
|
+
* which is this path's stand-in for the callee `Subtask`/`Task` of
|
|
527
|
+
* definitions.py.
|
|
528
|
+
*/
|
|
529
|
+
// CONTRACT: contracts/intrinsics.md §A schedules ResourceTransfer* at "M1,
|
|
530
|
+
// resources milestone" and describes them only as "handle-table moves between
|
|
531
|
+
// component instances" — the borrow-scope interaction is unspecified there.
|
|
532
|
+
// The reading implemented here is taken from definitions.py
|
|
533
|
+
// (`lift_borrow`/`lower_borrow` + `Subtask.lenders`/`Task.num_borrows`) and
|
|
534
|
+
// is what makes `test/resources/borrows.wast:162` (`lend-trap`) trap.
|
|
535
|
+
function transferBorrow(ctx, handle, srcTable, dstTable) {
|
|
536
|
+
const src = ctx.resourceTableInstance(srcTable);
|
|
537
|
+
const dst = ctx.resourceTableInstance(dstTable);
|
|
538
|
+
const srcRt = ctx.resourceToken(srcTable);
|
|
539
|
+
const dstRt = ctx.resourceToken(dstTable);
|
|
540
|
+
// Innermost-scope resolution. A FACT `[async-start]` copy window is
|
|
541
|
+
// strictly synchronous and innermost when present (the copy adapters
|
|
542
|
+
// cannot make nested calls), so it wins over any enclosing sync bracket.
|
|
543
|
+
const fact = ctx.factStartScopes[ctx.factStartScopes.length - 1];
|
|
544
|
+
const stack = syncScopes(ctx);
|
|
545
|
+
const scope = stack[stack.length - 1];
|
|
546
|
+
assert_(fact !== undefined || scope !== undefined, "transfer-borrow outside an enter-sync-call/exit-sync-call bracket " +
|
|
547
|
+
"or FACT start window");
|
|
548
|
+
const h = src.handles.get(handle);
|
|
549
|
+
trapIf(!(h instanceof ResourceHandle), "transfer-borrow: not a resource handle");
|
|
550
|
+
const rh = h;
|
|
551
|
+
trapIf(rh.rt !== srcRt, "transfer-borrow: resource type mismatch");
|
|
552
|
+
// definitions.py `lift_borrow`: the source handle becomes a lender of the
|
|
553
|
+
// callee's activation, which is what makes lifting it as an `own` trap for
|
|
554
|
+
// the duration of the call.
|
|
555
|
+
(fact?.lenders ?? scope).addLender(rh);
|
|
556
|
+
// definitions.py `lower_borrow`: `if inst is t.rt.impl: return rep` — a
|
|
557
|
+
// component that implements the resource is handed the rep directly and
|
|
558
|
+
// gets no handle (and therefore no `num_borrows` obligation).
|
|
559
|
+
if (dstRt.impl !== null && dstRt.impl === dst)
|
|
560
|
+
return rh.rep;
|
|
561
|
+
const borrowScope = fact !== undefined ? fact.taskScope : scope;
|
|
562
|
+
borrowScope.numBorrows += 1;
|
|
563
|
+
return dst.handles.add(new ResourceHandle(dstRt, rh.rep, false, borrowScope));
|
|
564
|
+
}
|