@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,978 @@
|
|
|
1
|
+
// The conventions facade: `instantiate(artifacts, imports, opts)`.
|
|
2
|
+
//
|
|
3
|
+
// DESIGN (orchestrator ruling, C2): the facade is **runtime-driven**. Every
|
|
4
|
+
// camelCase name, every resource class and every import wrapper is built here,
|
|
5
|
+
// at instantiate time, from the loaded plan's type tables — the plan already
|
|
6
|
+
// carries names, kinds and function types. Bindgen emits compile-time *types*
|
|
7
|
+
// that cast this facade; no generated code participates, so everything works
|
|
8
|
+
// fully untyped.
|
|
9
|
+
//
|
|
10
|
+
// Governing contract: contracts/embedder-api.md (all sections). Secondary:
|
|
11
|
+
// contracts/plan-format.md for the wire shapes read here.
|
|
12
|
+
import { loadEnvelope, loadPlan, PlanError } from "../plan/loader.js";
|
|
13
|
+
import { Trap } from "../cabi/trap.js";
|
|
14
|
+
import { CONSTRUCTOR_SYNC_ENTRY, hostResourceType, instantiateComponent, } from "../exec/mod.js";
|
|
15
|
+
import { camelCase, parseLeafName, pascalCase } from "./casing.js";
|
|
16
|
+
import { isSuspending, suspending } from "../jspi/suspending.js";
|
|
17
|
+
import { Translator } from "../shim/mod.js";
|
|
18
|
+
import { copyCensus, isTrap, isComponentException } from "@polyengine/protocol";
|
|
19
|
+
import { NameCollisionError, ComponentException } from "./errors.js";
|
|
20
|
+
import { requiredImports } from "./imports.js";
|
|
21
|
+
import { hostDtorCall } from "../exec/boundary.js";
|
|
22
|
+
import { buildGuestResourceClass, HostResourceRegistry, invalidateWrapper, lendWrapper, makeWrapper, takeRep, } from "./resources.js";
|
|
23
|
+
import { BorrowScope, describe, fromHost, toHost, } from "./values.js";
|
|
24
|
+
import { ImportResolver } from "./version.js";
|
|
25
|
+
import { Future, Stream } from "./streams.js";
|
|
26
|
+
/** Per-element codec for a `future<T>` returned in function-result position. */
|
|
27
|
+
function elementCodec(element, o) {
|
|
28
|
+
return {
|
|
29
|
+
element,
|
|
30
|
+
where: o.where,
|
|
31
|
+
toHost: (v) => element === null ? undefined : toHost(v, element, o),
|
|
32
|
+
fromHost: (v) => element === null ? null : fromHost(v, element, o),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reconstitute `ComponentArtifacts` from a translation ENVELOPE — the
|
|
37
|
+
* single-file JSON emitted by build-time translation (`tools/translate`,
|
|
38
|
+
* or `Translator.translateRaw`), carrying the plan and the FACT adapter
|
|
39
|
+
* modules. The production deploy set is `component.wasm` + its envelope +
|
|
40
|
+
* the runtime: no translator ships (embedder-api.md amendment A4).
|
|
41
|
+
*
|
|
42
|
+
* Pure and fetch-agnostic: acquire the two blobs however the platform
|
|
43
|
+
* likes (HTTP, fs, bundler asset) and hand them over. The envelope embeds
|
|
44
|
+
* the component's sha-256, which `instantiate` verifies — a mismatched
|
|
45
|
+
* pair fails loudly at instantiation, never subtly at runtime.
|
|
46
|
+
*/
|
|
47
|
+
export function artifactsFromEnvelope(envelopeJson, componentBytes) {
|
|
48
|
+
const { wire, adapters } = loadEnvelope(envelopeJson);
|
|
49
|
+
return { plan: wire, componentBytes, adapters };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalize either accepted input form to `ComponentArtifacts` — i.e. make
|
|
53
|
+
* the PLAN available without instantiating anything. Exported because the
|
|
54
|
+
* world-digest handshake (contracts/digest.md) must complete before any
|
|
55
|
+
* guest code runs: generated `instantiate` wrappers call this, verify the
|
|
56
|
+
* plan, and only then delegate to `instantiate` below.
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveArtifacts(src) {
|
|
59
|
+
if ("plan" in src)
|
|
60
|
+
return src;
|
|
61
|
+
const translator = src.translator instanceof Translator
|
|
62
|
+
? src.translator
|
|
63
|
+
: await Translator.create(src.translator);
|
|
64
|
+
const { plan, adapters } = translator.translate(src.componentBytes);
|
|
65
|
+
return { plan, componentBytes: src.componentBytes, adapters };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Instantiate a component behind the embedder conventions.
|
|
69
|
+
*
|
|
70
|
+
* `imports` is the canonical nested record of
|
|
71
|
+
* contracts/embedder-api.md §"Module wiring and instantiation": keys are
|
|
72
|
+
* verbatim WIT interface ids (version included) or world-level camelCase
|
|
73
|
+
* names; interface-id keys additionally participate in compatibility-track
|
|
74
|
+
* resolution (see `version.ts`).
|
|
75
|
+
*/
|
|
76
|
+
export async function instantiate(source, imports = {}, opts = {}) {
|
|
77
|
+
const artifacts = await resolveArtifacts(source);
|
|
78
|
+
const facade = new Facade(artifacts, imports);
|
|
79
|
+
const handle = await instantiateComponent({
|
|
80
|
+
plan: artifacts.plan,
|
|
81
|
+
componentBytes: artifacts.componentBytes,
|
|
82
|
+
adapters: artifacts.adapters,
|
|
83
|
+
imports: facade.rawImports,
|
|
84
|
+
jspi: opts.jspi,
|
|
85
|
+
verifyHash: opts.verifyHash,
|
|
86
|
+
// THE ordering fix: the facade converted this plan in its constructor and
|
|
87
|
+
// wired its import wrappers against those very `ResourceTypeInfo` tokens.
|
|
88
|
+
// Host imports fire DURING instantiation (a core module's `start`
|
|
89
|
+
// function runs inside `runInitializers`), so the facade cannot wait for
|
|
90
|
+
// the handle to learn its own types.
|
|
91
|
+
loadedPlan: facade.loaded,
|
|
92
|
+
});
|
|
93
|
+
facade.bind(handle);
|
|
94
|
+
const instance = {
|
|
95
|
+
exports: facade.buildExports(handle),
|
|
96
|
+
handle,
|
|
97
|
+
imports: facade.leaves,
|
|
98
|
+
};
|
|
99
|
+
Object.defineProperty(instance, INTERNAL_HOST_REGISTRIES, {
|
|
100
|
+
value: facade.hostRegistries,
|
|
101
|
+
enumerable: false,
|
|
102
|
+
});
|
|
103
|
+
return instance;
|
|
104
|
+
}
|
|
105
|
+
/** Alias matching the C2 dispatch's spelling. */
|
|
106
|
+
export const instantiateEmbedder = instantiate;
|
|
107
|
+
/**
|
|
108
|
+
* Symbol-keyed, deliberately NOT re-exported from `mod.ts`: the
|
|
109
|
+
* host-resource registries of an instance, by `ResourceIndex`. Diagnostics and
|
|
110
|
+
* white-box tests only — it is not part of the embedder API surface and no
|
|
111
|
+
* generated code may depend on it.
|
|
112
|
+
*/
|
|
113
|
+
export const INTERNAL_HOST_REGISTRIES = Symbol("polyengine.embedder.hostRegistries");
|
|
114
|
+
class Facade {
|
|
115
|
+
artifacts;
|
|
116
|
+
leaves;
|
|
117
|
+
rawImports = {};
|
|
118
|
+
#resolver;
|
|
119
|
+
#bindings = new Map();
|
|
120
|
+
/** ResourceTypeInfo identity -> ResourceIndex (one index, many tokens). */
|
|
121
|
+
#tokenIndex = new Map();
|
|
122
|
+
/**
|
|
123
|
+
* The converted plan — owned by the facade and handed to the executor, so
|
|
124
|
+
* both sides share one set of per-instantiation resource identity tokens.
|
|
125
|
+
* Available from construction, which is what makes import wrappers usable
|
|
126
|
+
* for the whole of instantiation.
|
|
127
|
+
*/
|
|
128
|
+
loaded;
|
|
129
|
+
#bridge;
|
|
130
|
+
/**
|
|
131
|
+
* Releases for reps minted while lowering the CURRENT call's arguments.
|
|
132
|
+
* Argument lowering is synchronous and uninterrupted (no `await` between
|
|
133
|
+
* `#lowerScope = […]` and the reset), so a single slot is race-free even
|
|
134
|
+
* with concurrent export calls in flight.
|
|
135
|
+
*/
|
|
136
|
+
#lowerScope = null;
|
|
137
|
+
/** ResourceIndex -> registry, for diagnostics (see INTERNAL_HOST_REGISTRIES). */
|
|
138
|
+
hostRegistries = new Map();
|
|
139
|
+
/** True once `buildExports` has run: guest resource classes then exist. */
|
|
140
|
+
#exportsBuilt = false;
|
|
141
|
+
constructor(artifacts, providers) {
|
|
142
|
+
this.artifacts = artifacts;
|
|
143
|
+
this.#resolver = new ImportResolver(providers);
|
|
144
|
+
this.loaded = loadPlan(artifacts.plan);
|
|
145
|
+
// `ResourceTypeInfo` identity -> `ResourceIndex`. Both halves are static
|
|
146
|
+
// (the tokens are ours; `resourceTables` is wire data), so this map is
|
|
147
|
+
// complete before instantiation starts — a host import that fires from a
|
|
148
|
+
// guest `start` function can resolve resource types normally.
|
|
149
|
+
//
|
|
150
|
+
// One resource TYPE can be reached through several resource TABLES
|
|
151
|
+
// (plan-format.md C2 amendment #1: a type export's index is a table
|
|
152
|
+
// index, and the executor sets impl/dtor on every table whose `resource`
|
|
153
|
+
// matches), hence index-keyed bindings with tokens as aliases.
|
|
154
|
+
artifacts.plan.resourceTables.forEach((table, i) => {
|
|
155
|
+
if (table.kind !== "concrete")
|
|
156
|
+
return;
|
|
157
|
+
const token = this.loaded.resourceTokens[i];
|
|
158
|
+
if (token !== undefined)
|
|
159
|
+
this.#tokenIndex.set(token, table.resource);
|
|
160
|
+
});
|
|
161
|
+
this.leaves = requiredImports(this.loaded);
|
|
162
|
+
// A component that imports a resource TYPE cannot be wired without
|
|
163
|
+
// `plan.importedResources`: that table is the only thing mapping the
|
|
164
|
+
// import back to a `ResourceIndex` (plan-format.md v0.1 amendment #2 /
|
|
165
|
+
// v0.2). Without it every own/borrow of that type would fail late, deep
|
|
166
|
+
// inside a call, as an unattributable `InvalidHandleError`.
|
|
167
|
+
const resourceLeaves = this.leaves.filter((l) => l.kind === "resource");
|
|
168
|
+
if (resourceLeaves.length > 0 &&
|
|
169
|
+
(artifacts.plan.importedResources ?? []).length === 0) {
|
|
170
|
+
throw new PlanError(`this component imports the resource type(s) ` +
|
|
171
|
+
`${resourceLeaves.map((l) => `'${l.leaf}'`).join(", ")}, but the ` +
|
|
172
|
+
`plan carries no \`importedResources\` table, so they cannot be ` +
|
|
173
|
+
`bound to a ResourceIndex (contracts/plan-format.md v0.2). ` +
|
|
174
|
+
`Re-translate with a shim that emits it.`);
|
|
175
|
+
}
|
|
176
|
+
this.#bridge = this.#makeBridge();
|
|
177
|
+
this.#buildRawImports();
|
|
178
|
+
this.#bindHostResources();
|
|
179
|
+
}
|
|
180
|
+
// -- resource-type identity ------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* Consistency check after instantiation.
|
|
183
|
+
*
|
|
184
|
+
* The facade no longer *learns* anything here — it handed its own
|
|
185
|
+
* `LoadedPlan` to the executor precisely so that nothing about types or
|
|
186
|
+
* resource identity depends on instantiation having finished. All this does
|
|
187
|
+
* is assert the executor did not silently re-load (which would give it a
|
|
188
|
+
* second, disjoint set of `ResourceTypeInfo` tokens and make every
|
|
189
|
+
* `own`/`borrow` unresolvable).
|
|
190
|
+
*/
|
|
191
|
+
bind(handle) {
|
|
192
|
+
if (handle.loadedPlan !== this.loaded) {
|
|
193
|
+
throw new PlanError("the executor instantiated from a different LoadedPlan than the " +
|
|
194
|
+
"facade built its import wrappers from; resource identity tokens " +
|
|
195
|
+
"would not match");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
#indexOf(rt) {
|
|
199
|
+
const i = this.#tokenIndex.get(rt);
|
|
200
|
+
if (i === undefined) {
|
|
201
|
+
throw new PlanError("resource type is not bound to any resource table in this plan");
|
|
202
|
+
}
|
|
203
|
+
return i;
|
|
204
|
+
}
|
|
205
|
+
#binding(rt) {
|
|
206
|
+
const index = this.#indexOf(rt);
|
|
207
|
+
let b = this.#bindings.get(index);
|
|
208
|
+
if (b === undefined) {
|
|
209
|
+
// A GUEST-implemented resource. Unlike host-implemented ones (bound at
|
|
210
|
+
// construction from static plan data), a guest resource's class is
|
|
211
|
+
// assembled from the component's own lifted `[constructor]`/`[method]`
|
|
212
|
+
// exports, which do not exist until instantiation has finished. If a
|
|
213
|
+
// guest `start` function hands one to a host import, say so precisely
|
|
214
|
+
// rather than surfacing a half-built wrapper.
|
|
215
|
+
if (!this.#exportsBuilt) {
|
|
216
|
+
throw new PlanError(`a guest-implemented resource (ResourceIndex ${index}) crossed the ` +
|
|
217
|
+
`boundary before instantiation finished — a guest \`start\` ` +
|
|
218
|
+
`function passed an own/borrow handle to a host import. Its class ` +
|
|
219
|
+
`is assembled from the component's own lifted exports, which do ` +
|
|
220
|
+
`not exist yet. Host-implemented resources are unaffected. If a ` +
|
|
221
|
+
`real component needs this, the class must be built lazily from ` +
|
|
222
|
+
`the plan's export table instead of the runtime's export surface.`);
|
|
223
|
+
}
|
|
224
|
+
// Post-instantiation: a guest resource with no exported type and no
|
|
225
|
+
// exported leaves. Still a valid handle, just anonymous.
|
|
226
|
+
b = { kind: "guest", name: `resource-${index}` };
|
|
227
|
+
this.#bindings.set(index, b);
|
|
228
|
+
}
|
|
229
|
+
return b;
|
|
230
|
+
}
|
|
231
|
+
// deno-lint-ignore no-explicit-any
|
|
232
|
+
#guestClass(b) {
|
|
233
|
+
b.cls ??= buildGuestResourceClass({ name: b.name, ctor: null, ctorParams: null, methods: [], statics: [] },
|
|
234
|
+
// The rt is supplied per wrapper, so an anonymous class needs none here.
|
|
235
|
+
{ impl: null, dtor: null }, () => Promise.reject(new TypeError("no methods")), () => []);
|
|
236
|
+
return b.cls;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Bind host-implemented resource types to their `ResourceIndex`.
|
|
240
|
+
*
|
|
241
|
+
* Everything this needs is static wire data (`plan.importedResources`, whose
|
|
242
|
+
* entries are back-references into `plan.imports`), so it runs at
|
|
243
|
+
* construction — before instantiation, and therefore before a guest `start`
|
|
244
|
+
* function can call an import that carries an `own`/`borrow` of one.
|
|
245
|
+
* Imported resources occupy `ResourceIndex` 0..n-1 in `importedResources`
|
|
246
|
+
* order (plan-format.md v0.1 amendment #2 / v0.2).
|
|
247
|
+
*/
|
|
248
|
+
#bindHostResources() {
|
|
249
|
+
const importedResources = this.artifacts.plan.importedResources ?? [];
|
|
250
|
+
for (const p of this.#pendingHostResources) {
|
|
251
|
+
const at = importedResources.findIndex((ir) => ir.import === p.importIndex);
|
|
252
|
+
if (at < 0)
|
|
253
|
+
continue;
|
|
254
|
+
this.#bindings.set(at, {
|
|
255
|
+
kind: "host",
|
|
256
|
+
name: this.leaves[p.importIndex].leaf,
|
|
257
|
+
registry: p.registry,
|
|
258
|
+
cls: p.cls,
|
|
259
|
+
});
|
|
260
|
+
this.hostRegistries.set(at, p.registry);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// -- the value bridge ------------------------------------------------------
|
|
264
|
+
#makeBridge() {
|
|
265
|
+
const self = this;
|
|
266
|
+
return {
|
|
267
|
+
liftOwn(rep, t) {
|
|
268
|
+
const b = self.#binding(t.rt);
|
|
269
|
+
// Host-implemented R: "the host's own instance back; the guest's
|
|
270
|
+
// handle is gone; no dispose call" (contract 2x4 table).
|
|
271
|
+
if (b.kind === "host")
|
|
272
|
+
return b.registry.release(rep);
|
|
273
|
+
return makeWrapper(self.#guestClass(b), rep, t.rt, true);
|
|
274
|
+
},
|
|
275
|
+
liftBorrow(rep, t, scope) {
|
|
276
|
+
const b = self.#binding(t.rt);
|
|
277
|
+
// Host-implemented R: "the host's own instance; borrow scoping is
|
|
278
|
+
// guest-side bookkeeping" — the mapping is kept.
|
|
279
|
+
if (b.kind === "host")
|
|
280
|
+
return b.registry.lookup(rep);
|
|
281
|
+
const w = makeWrapper(self.#guestClass(b), rep, t.rt, false);
|
|
282
|
+
scope.add(() => invalidateWrapper(w));
|
|
283
|
+
return w;
|
|
284
|
+
},
|
|
285
|
+
lowerOwn(v, t) {
|
|
286
|
+
const b = self.#binding(t.rt);
|
|
287
|
+
if (b.kind === "host")
|
|
288
|
+
return b.registry.repFor(v);
|
|
289
|
+
return takeRep(v, true, `own<${b.name}>`);
|
|
290
|
+
},
|
|
291
|
+
lowerBorrow(v, t) {
|
|
292
|
+
const b = self.#binding(t.rt);
|
|
293
|
+
if (b.kind === "host") {
|
|
294
|
+
// Contract 2x4 table, bottom-right: "a never-registered instance
|
|
295
|
+
// gets a rep allocated **for the call's duration**". A rep minted
|
|
296
|
+
// here is call-scoped, so it is released when the call returns —
|
|
297
|
+
// otherwise it would sit in the registry's STRONG rep->instance map
|
|
298
|
+
// forever, since a guest dropping a borrow handle runs no dtor.
|
|
299
|
+
const known = b.registry.hasInstance(v);
|
|
300
|
+
const rep = b.registry.repFor(v);
|
|
301
|
+
if (!known) {
|
|
302
|
+
self.#lowerScope?.push(() => b.registry.releaseIfPresent(rep));
|
|
303
|
+
}
|
|
304
|
+
return rep;
|
|
305
|
+
}
|
|
306
|
+
// Host `own` wrapper lowered as `borrow<R>` (#86): record the lend
|
|
307
|
+
// for the duration of this call, so a `drop()` or a GC finalization
|
|
308
|
+
// in the window cannot destroy a rep the guest still borrows.
|
|
309
|
+
// definitions.py `lift_borrow` -> `Subtask.add_lender` (line 890);
|
|
310
|
+
// `#lowerScope` is released where that subtask delivers its
|
|
311
|
+
// resolution, i.e. when the call ends.
|
|
312
|
+
const rep = takeRep(v, false, `borrow<${b.name}>`);
|
|
313
|
+
const release = lendWrapper(v);
|
|
314
|
+
if (self.#lowerScope === null) {
|
|
315
|
+
// No enclosing lowering scope (a raw/one-off lowering): the lend
|
|
316
|
+
// has no observable window, so it must not be left dangling.
|
|
317
|
+
release();
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
self.#lowerScope.push(release);
|
|
321
|
+
}
|
|
322
|
+
return rep;
|
|
323
|
+
},
|
|
324
|
+
dropOwn(rep, t) {
|
|
325
|
+
// A13: a lowered `own` the guest will never take (an un-taken
|
|
326
|
+
// stream element). Destroy it exactly as a guest-side drop would:
|
|
327
|
+
// host-implemented R runs the instance's [Symbol.dispose] through
|
|
328
|
+
// the registry; guest-implemented R runs the guest dtor via the
|
|
329
|
+
// gated path (a host-initiated drop, `caller = None`).
|
|
330
|
+
const b = self.#binding(t.rt);
|
|
331
|
+
if (b.kind === "host") {
|
|
332
|
+
b.registry.dtor(rep);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
hostDtorCall(t.rt, rep);
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
#opts(where) {
|
|
340
|
+
return { bridge: this.#bridge, where };
|
|
341
|
+
}
|
|
342
|
+
#funcType(index, what) {
|
|
343
|
+
const loaded = this.loaded;
|
|
344
|
+
if (index === undefined)
|
|
345
|
+
throw new PlanError(`${what}: no type index`);
|
|
346
|
+
const t = loaded.types[index];
|
|
347
|
+
if (t === undefined || t.kind !== "func") {
|
|
348
|
+
throw new PlanError(`${what}: type ${index} is not a function type`);
|
|
349
|
+
}
|
|
350
|
+
return t.funcType;
|
|
351
|
+
}
|
|
352
|
+
// -- imports ---------------------------------------------------------------
|
|
353
|
+
#buildRawImports() {
|
|
354
|
+
// Group by the record key so an instance import lands as one nested object.
|
|
355
|
+
this.leaves.forEach((leaf, importIndex) => {
|
|
356
|
+
const provider = this.#provider(leaf);
|
|
357
|
+
const target = leaf.path.length === 0
|
|
358
|
+
? null
|
|
359
|
+
: nest(this.rawImports, leaf.interfaceId, leaf.path.slice(0, -1));
|
|
360
|
+
const value = this.#wrapLeaf(leaf, importIndex, provider);
|
|
361
|
+
if (target === null)
|
|
362
|
+
this.rawImports[leaf.interfaceId] = value;
|
|
363
|
+
else
|
|
364
|
+
target[leaf.path[leaf.path.length - 1]] = value;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** Resolve the container object a leaf's implementation is read from. */
|
|
368
|
+
#provider(leaf) {
|
|
369
|
+
// A world-level MEMBER leaf (`[method]ticket.value` with no containing
|
|
370
|
+
// interface) dispatches on the resource's class, which is registered
|
|
371
|
+
// under the resource's own name — the mangled leaf name is never a
|
|
372
|
+
// record key. Interface-level members find their class inside the
|
|
373
|
+
// interface record via the normal path walk below.
|
|
374
|
+
if (leaf.path.length === 0 && leaf.member.form !== "plain") {
|
|
375
|
+
const r = leaf.member.resource;
|
|
376
|
+
const hit = this.#resolver.resolve(r) ??
|
|
377
|
+
this.#resolver.resolve(camelCase(r));
|
|
378
|
+
if (hit === undefined) {
|
|
379
|
+
throw new PlanError(`host import '${label(leaf)}' not provided: the component ` +
|
|
380
|
+
`imports the world-level resource '${r}'; provide its class ` +
|
|
381
|
+
`under the key '${camelCase(r)}' (registered: ` +
|
|
382
|
+
`${this.#resolver.keys().join(", ") || "<none>"})`);
|
|
383
|
+
}
|
|
384
|
+
return hit.value;
|
|
385
|
+
}
|
|
386
|
+
const hit = this.#resolver.resolve(leaf.interfaceId) ??
|
|
387
|
+
(leaf.path.length === 0
|
|
388
|
+
? this.#resolver.resolve(camelCase(leaf.interfaceId))
|
|
389
|
+
: undefined);
|
|
390
|
+
if (hit === undefined) {
|
|
391
|
+
throw new PlanError(`host import '${label(leaf)}' not provided (no key ` +
|
|
392
|
+
`'${leaf.interfaceId}' in imports; registered: ` +
|
|
393
|
+
`${this.#resolver.keys().join(", ") || "<none>"})`);
|
|
394
|
+
}
|
|
395
|
+
let value = hit.value;
|
|
396
|
+
// Walk everything but the final segment; the leaf itself is read by
|
|
397
|
+
// `#wrapLeaf`, which knows how to decode a mangled name.
|
|
398
|
+
for (const seg of leaf.path.slice(0, -1)) {
|
|
399
|
+
if (value === null || typeof value !== "object") {
|
|
400
|
+
throw new PlanError(`host import '${label(leaf)}': '${seg}' is not reachable ` +
|
|
401
|
+
`(${describe(value)})`);
|
|
402
|
+
}
|
|
403
|
+
value = value[seg];
|
|
404
|
+
}
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
#wrapLeaf(leaf, importIndex, provider) {
|
|
408
|
+
if (leaf.kind === "resource") {
|
|
409
|
+
return this.#wrapResourceType(leaf, importIndex, provider);
|
|
410
|
+
}
|
|
411
|
+
if (leaf.kind !== "func") {
|
|
412
|
+
// `instance` leaves never appear as plan imports in their own right
|
|
413
|
+
// (the plan flattens them into paths); anything else is out of scope.
|
|
414
|
+
throw new PlanError(`host import '${label(leaf)}': unsupported import kind '${leaf.kind}'`);
|
|
415
|
+
}
|
|
416
|
+
const dispatch = this.#dispatcher(leaf, provider);
|
|
417
|
+
// The function type is resolved LAZILY, on first call. It must come from
|
|
418
|
+
// the *executor's* loaded plan: the `own`/`borrow` types in it carry the
|
|
419
|
+
// per-instantiation `ResourceTypeInfo` identity tokens the bridge keys on,
|
|
420
|
+
// and those objects do not exist until `instantiateComponent` has run —
|
|
421
|
+
// which is after this wrapper has to be handed to it.
|
|
422
|
+
let impl = null;
|
|
423
|
+
const wrapper = (...raw) => {
|
|
424
|
+
if (impl === null) {
|
|
425
|
+
const ft = this.#funcType(this.artifacts.plan.imports[importIndex].type, `import '${label(leaf)}'`);
|
|
426
|
+
impl = this.#wrapImportFn(leaf, ft, dispatch);
|
|
427
|
+
}
|
|
428
|
+
return impl(...raw);
|
|
429
|
+
};
|
|
430
|
+
// A1 brand relay, layer 2 of 2 (see #dispatcher): the executor reads the
|
|
431
|
+
// brand off this wrapper, which is what lands in its hostImports record.
|
|
432
|
+
return isSuspending(dispatch) ? suspending(wrapper) : wrapper;
|
|
433
|
+
}
|
|
434
|
+
/** A host-implemented resource type: register the class, own the mapping. */
|
|
435
|
+
#wrapResourceType(leaf, importIndex, provider) {
|
|
436
|
+
// `#provider` already walked every path segment but the last, so a
|
|
437
|
+
// path-bearing resource import reads its class off `provider`; a
|
|
438
|
+
// world-level one IS `provider`.
|
|
439
|
+
const cls = leaf.path.length === 0
|
|
440
|
+
? provider
|
|
441
|
+
: pick(provider, [], [pascalCase(leaf.leaf), leaf.leaf]);
|
|
442
|
+
if (cls === undefined) {
|
|
443
|
+
throw new PlanError(`host import '${label(leaf)}': the component imports the resource ` +
|
|
444
|
+
`type '${leaf.leaf}'; provide the implementing class as ` +
|
|
445
|
+
`'${pascalCase(leaf.leaf)}'`);
|
|
446
|
+
}
|
|
447
|
+
const registry = new HostResourceRegistry(pascalCase(leaf.leaf));
|
|
448
|
+
this.#pendingHostResources.push({ importIndex, registry, cls });
|
|
449
|
+
return hostResourceType({
|
|
450
|
+
name: leaf.leaf,
|
|
451
|
+
// The guest dropped its last own handle: run the destructor, which for
|
|
452
|
+
// a host-implemented resource is `instance[Symbol.dispose]?.()`.
|
|
453
|
+
dtor: (rep) => registry.dtor(rep),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
#pendingHostResources = [];
|
|
457
|
+
/** The JS call a lifted import leaf dispatches to. */
|
|
458
|
+
#dispatcher(leaf, provider) {
|
|
459
|
+
const m = leaf.member;
|
|
460
|
+
if (m.form === "plain") {
|
|
461
|
+
const fn = leaf.path.length === 0
|
|
462
|
+
? provider
|
|
463
|
+
: pick(provider, [], [camelCase(m.name), m.name]);
|
|
464
|
+
if (typeof fn !== "function") {
|
|
465
|
+
throw new PlanError(`host import '${label(leaf)}' missing or not a function (got ` +
|
|
466
|
+
`${describe(fn)}); expected '${camelCase(m.name)}'`);
|
|
467
|
+
}
|
|
468
|
+
// A1: the `suspending()` brand rides the dispatch closure so #wrapLeaf
|
|
469
|
+
// can relay it onto the value the executor actually receives.
|
|
470
|
+
//
|
|
471
|
+
// A2 receiver rule: an interface member is invoked with its containing
|
|
472
|
+
// object as receiver (matching the static arm's `apply(cls)`), so a
|
|
473
|
+
// class INSTANCE is a fully supported spelling of an interface
|
|
474
|
+
// provider — methods reading instance state work. A world-level bare
|
|
475
|
+
// import has no containing object and stays unbound. (Previously the
|
|
476
|
+
// plain arm called extracted functions unbound: a class-instance
|
|
477
|
+
// provider type-checked, worked while stateless, and broke with
|
|
478
|
+
// `this === undefined` the moment a method touched state — the silent
|
|
479
|
+
// liberal-acceptance failure the contract forbids.)
|
|
480
|
+
const receiver = leaf.path.length === 0 ? undefined : provider;
|
|
481
|
+
const dispatch = (args) => fn.apply(receiver, args);
|
|
482
|
+
return isSuspending(fn) ? suspending(dispatch) : dispatch;
|
|
483
|
+
}
|
|
484
|
+
const clsName = pascalCase(m.resource);
|
|
485
|
+
// World-level member leaves resolved the class itself (`#provider`);
|
|
486
|
+
// interface members read it out of the interface record.
|
|
487
|
+
const cls = leaf.path.length === 0
|
|
488
|
+
? provider
|
|
489
|
+
: pick(provider, [], [clsName, m.resource]);
|
|
490
|
+
if (cls === undefined) {
|
|
491
|
+
throw new PlanError(`host import '${label(leaf)}': no class '${clsName}' provided`);
|
|
492
|
+
}
|
|
493
|
+
switch (m.form) {
|
|
494
|
+
case "constructor":
|
|
495
|
+
// Never markable: guest-driven construction of a host resource is
|
|
496
|
+
// synchronous by the C2 amendment, and stage-3 reserves no
|
|
497
|
+
// constructor-decorator position.
|
|
498
|
+
// deno-lint-ignore no-explicit-any
|
|
499
|
+
return (args) => new cls(...args);
|
|
500
|
+
case "method": {
|
|
501
|
+
// A2: the brand authority for an instance method is the CLASS
|
|
502
|
+
// PROTOTYPE, read at wrap time — the Suspending-wrap decision is
|
|
503
|
+
// per-declaration and taken at instantiation, before any instance
|
|
504
|
+
// exists. Instance-level method overrides do not change
|
|
505
|
+
// suspendability (marking follows the WIT declaration, not the
|
|
506
|
+
// object); the per-call lookup below still dispatches to the
|
|
507
|
+
// override's BODY as before.
|
|
508
|
+
//
|
|
509
|
+
// The probe must not INVOKE accessors: a platform getter (e.g.
|
|
510
|
+
// `URLSearchParams.prototype.size`) brand-checks its receiver, and a
|
|
511
|
+
// raw `prototype[member]` read runs it with `this` = the prototype —
|
|
512
|
+
// an engine TypeError at instantiation, even for guests that never
|
|
513
|
+
// call the member. Only a data-property function can carry the A2
|
|
514
|
+
// mark (stage-3 method decorators install data properties), so an
|
|
515
|
+
// accessor-backed member yields no wrap-time function here and stays
|
|
516
|
+
// a call-time concern for the per-call lookup below.
|
|
517
|
+
const protoFn = dataMember(cls?.prototype, camelCase(m.member));
|
|
518
|
+
const dispatch = (args) => {
|
|
519
|
+
const [self, ...rest] = args;
|
|
520
|
+
const fn = self?.[camelCase(m.member)];
|
|
521
|
+
if (typeof fn !== "function") {
|
|
522
|
+
throw new Trap(`host import '${label(leaf)}': the ${clsName} instance has no ` +
|
|
523
|
+
`method '${camelCase(m.member)}'`);
|
|
524
|
+
}
|
|
525
|
+
return fn.apply(self, rest);
|
|
526
|
+
};
|
|
527
|
+
return isSuspending(protoFn) ? suspending(dispatch) : dispatch;
|
|
528
|
+
}
|
|
529
|
+
case "static": {
|
|
530
|
+
const fn = cls[camelCase(m.member)];
|
|
531
|
+
if (typeof fn !== "function") {
|
|
532
|
+
throw new PlanError(`host import '${label(leaf)}': ${clsName} has no static ` +
|
|
533
|
+
`'${camelCase(m.member)}'`);
|
|
534
|
+
}
|
|
535
|
+
// A2: a static's brand sits on the function itself (a stage-3
|
|
536
|
+
// static-method decorator marks the function value), readable here
|
|
537
|
+
// at wrap time.
|
|
538
|
+
const dispatch = (args) => fn.apply(cls, args);
|
|
539
|
+
return isSuspending(fn) ? suspending(dispatch) : dispatch;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The raw (definitions.py-shaped) function the executor lowers, wrapping a
|
|
545
|
+
* conventions-shaped host implementation.
|
|
546
|
+
*
|
|
547
|
+
* Error model (contract §"Error model"), the inversion of jco's convention:
|
|
548
|
+
* * a returned value is the ok side;
|
|
549
|
+
* * `throw new ComponentException(payload)` is the err side of a `result<T, E>`;
|
|
550
|
+
* * a `Trap` passes through unchanged;
|
|
551
|
+
* * **any other throw is a host bug and becomes a trap naming the import**
|
|
552
|
+
* — never a guest-visible err. This is what makes the consumers'
|
|
553
|
+
* defensive `platformCall`-style wrappers unnecessary by construction.
|
|
554
|
+
*/
|
|
555
|
+
#wrapImportFn(leaf, ft, dispatch) {
|
|
556
|
+
const where = `import '${label(leaf)}'`;
|
|
557
|
+
const o = this.#opts(where);
|
|
558
|
+
const resultType = ft.results.length === 0 ? null : ft.results[0];
|
|
559
|
+
const isResult = resultType !== null && resultType.kind === "result";
|
|
560
|
+
const ok = (v) => {
|
|
561
|
+
if (resultType === null)
|
|
562
|
+
return undefined;
|
|
563
|
+
if (isResult) {
|
|
564
|
+
const rt = resultType;
|
|
565
|
+
return { ok: rt.ok === null ? null : fromHost(v, rt.ok, o) };
|
|
566
|
+
}
|
|
567
|
+
return fromHost(v, resultType, o);
|
|
568
|
+
};
|
|
569
|
+
const fail = (e, args) => {
|
|
570
|
+
// Brand, not class (amendment A9): a `ComponentException` thrown by a host module
|
|
571
|
+
// that resolved a DIFFERENT runtime copy — or hand-rolled with the
|
|
572
|
+
// registry symbol — is the same value here (issue #83).
|
|
573
|
+
if (isComponentException(e) && isResult) {
|
|
574
|
+
const rt = resultType;
|
|
575
|
+
return {
|
|
576
|
+
error: rt.error === null ? null : fromHost(e.payload, rt.error, o),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
// Every remaining branch traps the component. The import's lifted
|
|
580
|
+
// stream/future arguments were transferred to the host when the params
|
|
581
|
+
// were converted (the guest's ends are gone), and a trapping import is
|
|
582
|
+
// a declared host bug — nothing owns them anymore, so drop them here:
|
|
583
|
+
// a peer parked on one (a host writer feeding the stream this import
|
|
584
|
+
// just received, the #66 E2 shape) settles with the truthful "reader
|
|
585
|
+
// went away" instead of hanging forever. The err-VALUE branch above
|
|
586
|
+
// deliberately does NOT do this: a fallible import returning err is a
|
|
587
|
+
// normal outcome whose implementation may retain the handles.
|
|
588
|
+
releaseAsyncArgs(args);
|
|
589
|
+
if (isTrap(e))
|
|
590
|
+
throw e;
|
|
591
|
+
if (isComponentException(e)) {
|
|
592
|
+
throw new Trap(`${where} threw a ComponentException, but its WIT type has no err side; ` +
|
|
593
|
+
`only a fallible import may signal an error value`);
|
|
594
|
+
}
|
|
595
|
+
// The #83 signature: in a graph with several copies, an UNBRANDED throw
|
|
596
|
+
// is usually a pre-A9 copy's `ComponentException` (its brand rode class identity,
|
|
597
|
+
// which does not survive the copy boundary). Say so rather than leaving
|
|
598
|
+
// the latent puzzle that motivated amendment A9.
|
|
599
|
+
const census = copyCensus();
|
|
600
|
+
throw new Trap(`${where} threw ${describeThrow(e)}. An unbranded throw from a host ` +
|
|
601
|
+
`import is a host bug and becomes a trap: signal a WIT error with ` +
|
|
602
|
+
`\`throw new ComponentException(payload)\`.` +
|
|
603
|
+
(census === ""
|
|
604
|
+
? ""
|
|
605
|
+
: ` (${census} — an error carrying no polyengine brand in a ` +
|
|
606
|
+
`multi-copy graph usually means a pre-A9 runtime copy threw ` +
|
|
607
|
+
`it, issue #83.)`));
|
|
608
|
+
};
|
|
609
|
+
return (...raw) => {
|
|
610
|
+
const scope = new BorrowScope();
|
|
611
|
+
const args = ft.params.map((p, i) => toHost(raw[i], p, o, scope));
|
|
612
|
+
let out;
|
|
613
|
+
try {
|
|
614
|
+
out = dispatch(args);
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
scope.end();
|
|
618
|
+
return fail(e, args);
|
|
619
|
+
}
|
|
620
|
+
if (isThenable(out)) {
|
|
621
|
+
// Amendment A12: when the WIT result type is `future<T>`, a thenable
|
|
622
|
+
// return IS the future source ("for `future<T>`, a `Promise<T>` or
|
|
623
|
+
// `Future<T>`" — §"Streams and futures"), not the call's async
|
|
624
|
+
// completion. The import completes immediately with the lowered
|
|
625
|
+
// future; the producer settles it on its own schedule. Without this,
|
|
626
|
+
// the natural spelling of the wasi:sockets 0.3 TCP `send` shape —
|
|
627
|
+
// `func(data: stream<u8>) -> future<result>`, an async method whose
|
|
628
|
+
// promise resolves when transmission completes — would park the
|
|
629
|
+
// call, and a future whose settlement depends on post-return guest
|
|
630
|
+
// action (the guest writes `data` AFTER `send` returns) livelocks.
|
|
631
|
+
// This branch also covers a returned `Future` handle, which is a
|
|
632
|
+
// PromiseLike and would otherwise be adopted and mis-lowered.
|
|
633
|
+
if (resultType !== null && resultType.kind === "future") {
|
|
634
|
+
scope.end();
|
|
635
|
+
return ok(out);
|
|
636
|
+
}
|
|
637
|
+
return out.then((v) => {
|
|
638
|
+
scope.end();
|
|
639
|
+
return ok(v);
|
|
640
|
+
}, (e) => {
|
|
641
|
+
scope.end();
|
|
642
|
+
return fail(e, args);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
scope.end();
|
|
646
|
+
return ok(out);
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
// -- exports ---------------------------------------------------------------
|
|
650
|
+
// deno-lint-ignore no-explicit-any
|
|
651
|
+
buildExports(handle) {
|
|
652
|
+
this.#exportsBuilt = true;
|
|
653
|
+
// deno-lint-ignore no-explicit-any
|
|
654
|
+
const out = {};
|
|
655
|
+
const worldLeaves = [];
|
|
656
|
+
for (const exp of this.artifacts.plan.exports) {
|
|
657
|
+
if (exp.kind === "instance") {
|
|
658
|
+
out[exp.name] = this.#buildInterface(exp.name, exp.exports, handle.exports[exp.name]);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
worldLeaves.push(exp);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (worldLeaves.length > 0) {
|
|
665
|
+
Object.assign(out, this.#buildInterface("", worldLeaves, handle.exports));
|
|
666
|
+
}
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
// deno-lint-ignore no-explicit-any
|
|
670
|
+
#buildInterface(id, exps, raw) {
|
|
671
|
+
// deno-lint-ignore no-explicit-any
|
|
672
|
+
const obj = {};
|
|
673
|
+
/** jsName -> the WIT leaf that claimed it (camelCase collision guard). */
|
|
674
|
+
const claimed = new Map();
|
|
675
|
+
const claim = (js, leaf) => {
|
|
676
|
+
const held = claimed.get(js);
|
|
677
|
+
if (held !== undefined) {
|
|
678
|
+
throw new NameCollisionError(`export '${id || "<world>"}': the leaves '${held}' and '${leaf}' ` +
|
|
679
|
+
`both map to the JS name '${js}'. Rename one in the WIT; the ` +
|
|
680
|
+
`conventions layer will not guess which one wins.`);
|
|
681
|
+
}
|
|
682
|
+
claimed.set(js, leaf);
|
|
683
|
+
return js;
|
|
684
|
+
};
|
|
685
|
+
const specs = new Map();
|
|
686
|
+
const specRt = new Map();
|
|
687
|
+
const spec = (name) => {
|
|
688
|
+
let s = specs.get(name);
|
|
689
|
+
if (s === undefined) {
|
|
690
|
+
s = { name, ctor: null, ctorParams: null, methods: [], statics: [] };
|
|
691
|
+
specs.set(name, s);
|
|
692
|
+
}
|
|
693
|
+
return s;
|
|
694
|
+
};
|
|
695
|
+
for (const exp of exps) {
|
|
696
|
+
if (exp.kind === "type") {
|
|
697
|
+
// A `resource` type export names the class; the ResourceIndex comes
|
|
698
|
+
// from the resource TABLE it points at (the wire field is a table
|
|
699
|
+
// index, like `own`/`borrow`).
|
|
700
|
+
if (exp.type.kind === "resource") {
|
|
701
|
+
const token = this.loaded.resourceTokens[exp.type.resource];
|
|
702
|
+
if (token !== undefined && this.#tokenIndex.has(token)) {
|
|
703
|
+
const index = this.#tokenIndex.get(token);
|
|
704
|
+
const held = this.#bindings.get(index);
|
|
705
|
+
if (held === undefined) {
|
|
706
|
+
this.#bindings.set(index, { kind: "guest", name: exp.name });
|
|
707
|
+
}
|
|
708
|
+
else if (held.kind === "guest") {
|
|
709
|
+
held.name = exp.name;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (exp.kind === "module") {
|
|
716
|
+
// Not WIT-expressible, digest-excluded (plan-format.md v4 amendment
|
|
717
|
+
// 2): the WIT-shaped facade skips it, the type-export precedent. The
|
|
718
|
+
// raw executor export surface still carries the compiled module.
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (exp.kind === "instance") {
|
|
722
|
+
// The plan flattens the world's instance exports at the top level; a
|
|
723
|
+
// nested one would need a nested facade, which nothing produces today.
|
|
724
|
+
// Refuse rather than silently drop the whole sub-interface.
|
|
725
|
+
throw new PlanError(`export '${id || "<world>"}/${exp.name}': nested instance exports ` +
|
|
726
|
+
`are not surfaced by the conventions layer (only one level of ` +
|
|
727
|
+
`interface nesting exists in plan v2)`);
|
|
728
|
+
}
|
|
729
|
+
if (exp.kind !== "lifted-func") {
|
|
730
|
+
throw new PlanError(`export '${id || "<world>"}/${exp.name}': ` +
|
|
731
|
+
`unsupported export kind ` +
|
|
732
|
+
`'${exp.kind}'`);
|
|
733
|
+
}
|
|
734
|
+
const fn = raw[exp.name];
|
|
735
|
+
if (typeof fn !== "function") {
|
|
736
|
+
throw new PlanError(`export '${id || "<world>"}/${exp.name}': the runtime produced no ` +
|
|
737
|
+
`callable for this lifted function`);
|
|
738
|
+
}
|
|
739
|
+
const ft = this.#funcType(exp.type, `export '${id}/${exp.name}'`);
|
|
740
|
+
const member = parseLeafName(exp.name);
|
|
741
|
+
const where = id === "" ? exp.name : `${id}#${exp.name}`;
|
|
742
|
+
switch (member.form) {
|
|
743
|
+
case "plain":
|
|
744
|
+
obj[claim(camelCase(member.name), member.name)] = this
|
|
745
|
+
.#wrapExportFn(fn, ft, where);
|
|
746
|
+
break;
|
|
747
|
+
case "constructor": {
|
|
748
|
+
const s = spec(member.resource);
|
|
749
|
+
// Prefer the plain-entered variant in jspi mode: the JS `new`
|
|
750
|
+
// cannot await the Promise a promising-wrapped entry returns
|
|
751
|
+
// (exec/boundary.ts CONSTRUCTOR_SYNC_ENTRY).
|
|
752
|
+
s.ctor = (fn[CONSTRUCTOR_SYNC_ENTRY] ?? fn);
|
|
753
|
+
s.ctorParams = ft.params;
|
|
754
|
+
rtOf(ft.results[0], specRt, member.resource);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "method": {
|
|
758
|
+
spec(member.resource).methods.push({
|
|
759
|
+
member: member.member,
|
|
760
|
+
raw: fn,
|
|
761
|
+
params: ft.params,
|
|
762
|
+
results: ft.results,
|
|
763
|
+
});
|
|
764
|
+
rtOf(ft.params[0], specRt, member.resource);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "static": {
|
|
768
|
+
spec(member.resource).statics.push({
|
|
769
|
+
member: member.member,
|
|
770
|
+
raw: fn,
|
|
771
|
+
params: ft.params,
|
|
772
|
+
results: ft.results,
|
|
773
|
+
});
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (const [name, s] of specs) {
|
|
779
|
+
const rt = specRt.get(name);
|
|
780
|
+
if (rt === undefined) {
|
|
781
|
+
throw new PlanError(`export '${id}': resource '${name}' has leaves but no own/borrow ` +
|
|
782
|
+
`type to identify it by`);
|
|
783
|
+
}
|
|
784
|
+
const cls = buildGuestResourceClass(s, rt, (fn, params, results, where, args) => this.#wrapExportFn(fn, { params, results }, where)(...args), (args, params, where) => args.map((a, i) => fromHost(a, params[i], this.#opts(where))));
|
|
785
|
+
obj[claim(pascalCase(name), name)] = cls;
|
|
786
|
+
const index = this.#tokenIndex.get(rt);
|
|
787
|
+
if (index !== undefined) {
|
|
788
|
+
this.#bindings.set(index, { kind: "guest", name, cls });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return obj;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Lower a call's arguments, collecting the releases for anything that was
|
|
795
|
+
* allocated *for the duration of this call* (see `lowerBorrow`).
|
|
796
|
+
*
|
|
797
|
+
* The collection window is the synchronous argument-lowering phase only —
|
|
798
|
+
* `#lowerScope` is set and cleared with no `await` in between — so a single
|
|
799
|
+
* slot is correct even with concurrent export calls in flight.
|
|
800
|
+
*/
|
|
801
|
+
#lowerParams(params, args, o) {
|
|
802
|
+
const scope = [];
|
|
803
|
+
const outer = this.#lowerScope;
|
|
804
|
+
this.#lowerScope = scope;
|
|
805
|
+
let lowered;
|
|
806
|
+
try {
|
|
807
|
+
lowered = params.map((p, i) => fromHost(args[i], p, o));
|
|
808
|
+
}
|
|
809
|
+
catch (e) {
|
|
810
|
+
for (const r of scope)
|
|
811
|
+
r();
|
|
812
|
+
throw e;
|
|
813
|
+
}
|
|
814
|
+
finally {
|
|
815
|
+
this.#lowerScope = outer;
|
|
816
|
+
}
|
|
817
|
+
let released = false;
|
|
818
|
+
return {
|
|
819
|
+
lowered,
|
|
820
|
+
release: () => {
|
|
821
|
+
if (released)
|
|
822
|
+
return;
|
|
823
|
+
released = true;
|
|
824
|
+
for (const r of scope)
|
|
825
|
+
r();
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Wrap one lifted export.
|
|
831
|
+
*
|
|
832
|
+
* Uniformly Promise-shaped (contract §"Functions and async"): a sync
|
|
833
|
+
* completion resolves immediately, so there is one calling convention.
|
|
834
|
+
* A `result<T, E>` in *function-result* position resolves `T` or rejects
|
|
835
|
+
* `ComponentException<E>`; a result nested inside a value is plain `{kind, value}` data
|
|
836
|
+
* and never throws.
|
|
837
|
+
*/
|
|
838
|
+
#wrapExportFn(fn, ft, where) {
|
|
839
|
+
const o = this.#opts(where);
|
|
840
|
+
const resultType = ft.results.length === 0 ? null : ft.results[0];
|
|
841
|
+
if (resultType !== null && resultType.kind === "future") {
|
|
842
|
+
// See `Future.deferred`: a `future<T>` result cannot be delivered
|
|
843
|
+
// *through* a Promise, because promise resolution adopts thenables and
|
|
844
|
+
// `Future<T>` is one. The handle is returned eagerly instead; it is
|
|
845
|
+
// PromiseLike, so `await` still yields `T`.
|
|
846
|
+
const element = resultType.element;
|
|
847
|
+
return (...args) => {
|
|
848
|
+
// Advisory 9: the generic branch checks arity; so must this one.
|
|
849
|
+
if (args.length !== ft.params.length) {
|
|
850
|
+
throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ` +
|
|
851
|
+
`${args.length}`);
|
|
852
|
+
}
|
|
853
|
+
const { lowered, release } = this.#lowerParams(ft.params, args, o);
|
|
854
|
+
let pending;
|
|
855
|
+
try {
|
|
856
|
+
pending = Promise.resolve(fn(...lowered));
|
|
857
|
+
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
release();
|
|
860
|
+
throw e;
|
|
861
|
+
}
|
|
862
|
+
void pending.then(release, release);
|
|
863
|
+
return Future.deferred(pending, elementCodec(element, o));
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return async (...args) => {
|
|
867
|
+
if (args.length !== ft.params.length) {
|
|
868
|
+
throw new TypeError(`${where}: expected ${ft.params.length} argument(s), got ${args.length}`);
|
|
869
|
+
}
|
|
870
|
+
const { lowered, release } = this.#lowerParams(ft.params, args, o);
|
|
871
|
+
let raw;
|
|
872
|
+
try {
|
|
873
|
+
raw = await fn(...lowered);
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
// Call-scoped reps minted for `borrow<R>` arguments of a
|
|
877
|
+
// host-implemented resource live exactly as long as the call.
|
|
878
|
+
release();
|
|
879
|
+
}
|
|
880
|
+
if (resultType === null)
|
|
881
|
+
return undefined;
|
|
882
|
+
if (resultType.kind === "result") {
|
|
883
|
+
const v = raw;
|
|
884
|
+
if ("error" in v) {
|
|
885
|
+
throw new ComponentException(resultType.error === null
|
|
886
|
+
? undefined
|
|
887
|
+
: toHost(v["error"], resultType.error, o));
|
|
888
|
+
}
|
|
889
|
+
return resultType.ok === null
|
|
890
|
+
? undefined
|
|
891
|
+
: toHost(v["ok"], resultType.ok, o);
|
|
892
|
+
}
|
|
893
|
+
return toHost(raw, resultType, o);
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
function rtOf(t, into, name) {
|
|
899
|
+
if (t === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (t.kind === "own" || t.kind === "borrow")
|
|
902
|
+
into.set(name, t.rt);
|
|
903
|
+
}
|
|
904
|
+
function label(leaf) {
|
|
905
|
+
return leaf.path.length === 0
|
|
906
|
+
? leaf.interfaceId
|
|
907
|
+
: `${leaf.interfaceId}/${leaf.path.join("/")}`;
|
|
908
|
+
}
|
|
909
|
+
function nest(root, key, path) {
|
|
910
|
+
let cur = (root[key] ??= {});
|
|
911
|
+
for (const seg of path) {
|
|
912
|
+
cur = (cur[seg] ??= {});
|
|
913
|
+
}
|
|
914
|
+
return cur;
|
|
915
|
+
}
|
|
916
|
+
/** Read `names` in order from `container` after walking `path`. */
|
|
917
|
+
function pick(container, path, names) {
|
|
918
|
+
let v = container;
|
|
919
|
+
for (const seg of path) {
|
|
920
|
+
if (v === null || typeof v !== "object")
|
|
921
|
+
return undefined;
|
|
922
|
+
v = v[seg];
|
|
923
|
+
}
|
|
924
|
+
if (v === null || typeof v !== "object") {
|
|
925
|
+
return names.length === 0 ? v : undefined;
|
|
926
|
+
}
|
|
927
|
+
for (const n of names) {
|
|
928
|
+
const hit = v[n];
|
|
929
|
+
if (hit !== undefined)
|
|
930
|
+
return hit;
|
|
931
|
+
}
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Read a DATA property from `obj` (walking its prototype chain, nearest own
|
|
936
|
+
* descriptor wins) without ever invoking accessors. Accessor-backed and
|
|
937
|
+
* absent members both yield `undefined`. Used by the A2 wrap-time suspending
|
|
938
|
+
* probe, which must not run platform getters against a bare prototype.
|
|
939
|
+
*/
|
|
940
|
+
function dataMember(obj, key) {
|
|
941
|
+
for (let o = obj; o !== null && (typeof o === "object" || typeof o === "function"); o = Object.getPrototypeOf(o)) {
|
|
942
|
+
const d = Object.getOwnPropertyDescriptor(o, key);
|
|
943
|
+
if (d !== undefined)
|
|
944
|
+
return "value" in d ? d.value : undefined;
|
|
945
|
+
}
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
function isThenable(v) {
|
|
949
|
+
return v !== null && typeof v === "object" && "then" in v &&
|
|
950
|
+
typeof v.then === "function";
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Drop the lifted stream/future arguments a trapping import abandoned (#66).
|
|
954
|
+
* Top-level parameters only: those are the shapes whose peers park host
|
|
955
|
+
* operations; a stream nested inside a record is exotic enough to leave to
|
|
956
|
+
* the negligence rules. Uses the teardown drop, not the plain one — the
|
|
957
|
+
* calling instance is about to be poisoned by this very trap, and a DROPPED
|
|
958
|
+
* notification must not queue a phantom event into its waitables (review
|
|
959
|
+
* B2; see task/streams.ts `dropSharedForTeardown`).
|
|
960
|
+
*/
|
|
961
|
+
function releaseAsyncArgs(args) {
|
|
962
|
+
for (const a of args) {
|
|
963
|
+
if (a instanceof Stream || a instanceof Future) {
|
|
964
|
+
try {
|
|
965
|
+
a.dropForTeardown();
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// Best-effort teardown: the component is already trapping, and that
|
|
969
|
+
// trap — not a secondary drop failure — is the error to surface.
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function describeThrow(e) {
|
|
975
|
+
if (e instanceof Error)
|
|
976
|
+
return `${e.name}: ${e.message}`;
|
|
977
|
+
return describe(e);
|
|
978
|
+
}
|