@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,657 @@
|
|
|
1
|
+
// Plan loader: formatVersion validation, structural checks, and conversion
|
|
2
|
+
// of the wire descriptor IR (contracts/descriptor-ir.md JSON) into the cabi
|
|
3
|
+
// in-memory type model (runtime/src/cabi/types.ts, the normative model).
|
|
4
|
+
//
|
|
5
|
+
// Wire -> in-memory deltas handled here (recorded in the M0 contract
|
|
6
|
+
// friction report):
|
|
7
|
+
// - `result.err` (wire, per descriptor-ir.md) -> `result.error` (types.ts)
|
|
8
|
+
// - func params `{label, type}[]` (wire) -> unlabeled `ValType[]`
|
|
9
|
+
// (types.ts drops ABI-irrelevant names; labels are preserved separately
|
|
10
|
+
// for bindgen/digest use)
|
|
11
|
+
// - own/borrow `resource: <table index>` (wire) -> `ResourceTypeInfo`
|
|
12
|
+
// identity tokens created per resource table at load time
|
|
13
|
+
import { ResourceTypeInfo, } from "../cabi/types.js";
|
|
14
|
+
/** Fault in the plan document itself (version/shape/reference errors). */
|
|
15
|
+
export class PlanError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "PlanError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A structured translation verdict from the shim (envelope `errorDetail`).
|
|
23
|
+
*
|
|
24
|
+
* Distinguished from `PlanError` on purpose: a `TranslateError` with
|
|
25
|
+
* `phase === "validation"` is *the translator's judgment about the input
|
|
26
|
+
* component* and is the only failure that satisfies `assert_invalid` /
|
|
27
|
+
* `assert_malformed`. `PlanError` and the other phases are failures of our
|
|
28
|
+
* own pipeline and must never be scored as conformance passes.
|
|
29
|
+
*/
|
|
30
|
+
export class TranslateError extends Error {
|
|
31
|
+
phase;
|
|
32
|
+
detail;
|
|
33
|
+
constructor(d) {
|
|
34
|
+
super(`translator error [${d.phase}]: ${d.message}`);
|
|
35
|
+
this.name = "TranslateError";
|
|
36
|
+
this.phase = d.phase;
|
|
37
|
+
this.detail = d.detail ?? d.message;
|
|
38
|
+
}
|
|
39
|
+
/** True iff the shim judged the *input component* invalid or malformed. */
|
|
40
|
+
get isValidationVerdict() {
|
|
41
|
+
return this.phase === "validation";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The single formatVersion this executor understands.
|
|
46
|
+
*
|
|
47
|
+
* v4 (2026-08-17, polyengine#13): `exports[]` gained the `"module"` kind — a
|
|
48
|
+
* component exporting one of its own embedded core modules, surfaced as the
|
|
49
|
+
* already-compiled `WebAssembly.Module`.
|
|
50
|
+
* v3 (2026-08-10, polyengine#89): `errorContextTables` — the index space the
|
|
51
|
+
* `error-context-transfer` trampoline actually uses (it was resolved through
|
|
52
|
+
* the *resource*-table mapping before, a different space) — and
|
|
53
|
+
* `task-return`'s `resultType` / raw `results` split, which lets a FACT
|
|
54
|
+
* callee task carry its declared result type.
|
|
55
|
+
* v2 (M2 phase 2c): `streamTables` / `futureTables` — the element types the
|
|
56
|
+
* stream and future built-ins need to size their copy buffers.
|
|
57
|
+
* v1 (contracts/plan-format.md v0.3): `CoreDef` gained `"unsafe-intrinsic"`.
|
|
58
|
+
* The change is purely additive, but the contract's compat rule is a strict
|
|
59
|
+
* equality check ("Validate `formatVersion` and fail fast on mismatch",
|
|
60
|
+
* producer and consumer bumped in the same commit), so v0 plans are refused
|
|
61
|
+
* rather than best-effort accepted — a stale cached artifact must be a loud
|
|
62
|
+
* failure, not a subtly different execution.
|
|
63
|
+
*/
|
|
64
|
+
export const SUPPORTED_FORMAT_VERSION = 4;
|
|
65
|
+
/**
|
|
66
|
+
* Validate a plan document and convert its type tables. Fails fast on
|
|
67
|
+
* formatVersion mismatch per contracts/plan-format.md "Executor obligations".
|
|
68
|
+
*/
|
|
69
|
+
export function loadPlan(wire) {
|
|
70
|
+
if (wire.formatVersion !== SUPPORTED_FORMAT_VERSION) {
|
|
71
|
+
throw new PlanError(`unsupported plan formatVersion ${wire.formatVersion} ` +
|
|
72
|
+
`(this runtime implements v${SUPPORTED_FORMAT_VERSION})`);
|
|
73
|
+
}
|
|
74
|
+
for (const required of [
|
|
75
|
+
"modules",
|
|
76
|
+
"initializers",
|
|
77
|
+
"trampolines",
|
|
78
|
+
"canonicalOptions",
|
|
79
|
+
"types",
|
|
80
|
+
"resourceTables",
|
|
81
|
+
// ISSUE #94(2): the shim (crates/translator-shim/src/plan.rs) has no
|
|
82
|
+
// `skip_serializing_if` on `stream_tables`/`future_tables` — a real
|
|
83
|
+
// emitted v2 plan always serializes these as arrays (`[]` when empty,
|
|
84
|
+
// never absent). Requiring presence here keeps the loader consistent
|
|
85
|
+
// with what the producer actually emits, rather than silently
|
|
86
|
+
// tolerating an absent field via `?? []` (which would also mask a
|
|
87
|
+
// genuinely malformed/truncated envelope).
|
|
88
|
+
"streamTables",
|
|
89
|
+
"futureTables",
|
|
90
|
+
// Same reasoning at v3 (the shim has no `skip_serializing_if` on
|
|
91
|
+
// `error_context_tables` either): presence is what the producer
|
|
92
|
+
// guarantees, so absence is a malformed envelope, not an empty table
|
|
93
|
+
// space.
|
|
94
|
+
"errorContextTables",
|
|
95
|
+
"imports",
|
|
96
|
+
"exports",
|
|
97
|
+
]) {
|
|
98
|
+
if (!Array.isArray(wire[required])) {
|
|
99
|
+
throw new PlanError(`plan.${required} missing or not an array`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ISSUE #94(3): deep-schema strictness. `initializers` / `trampolines` /
|
|
103
|
+
// `canonicalOptions` / `CoreDef`s reach `runInitializers` unchecked today;
|
|
104
|
+
// a malformed op object (e.g. `{"op":"instantiate-module"}` missing
|
|
105
|
+
// `args`) dies as a raw `TypeError` deep in the executor rather than a
|
|
106
|
+
// typed `PlanError` here at load time. Proportionate check: a
|
|
107
|
+
// discriminated-union switch per op/trampoline kind verifying required
|
|
108
|
+
// fields are present and primitively typed — not a full JSON-schema
|
|
109
|
+
// engine.
|
|
110
|
+
wire.initializers.forEach((init, i) => validateInitializer(init, `initializers[${i}]`));
|
|
111
|
+
wire.trampolines.forEach((t, i) => validateTrampoline(t, `trampolines[${i}]`));
|
|
112
|
+
wire.canonicalOptions.forEach((o, i) => validateCanonicalOptions(o, `canonicalOptions[${i}]`));
|
|
113
|
+
// plan v3: `errorContextTables` entries are `{ instance }` and nothing
|
|
114
|
+
// else; the executor routes real handle-table lookups through them, so a
|
|
115
|
+
// malformed entry must fail here rather than as an undefined index later.
|
|
116
|
+
wire.errorContextTables.forEach((t, i) => {
|
|
117
|
+
const where = `errorContextTables[${i}]`;
|
|
118
|
+
expect(isRecord(t), where, `must be an object, got ${describeValue(t)}`);
|
|
119
|
+
expectNumber(t, "instance", where);
|
|
120
|
+
});
|
|
121
|
+
// ISSUE #187: `modules[]` / `exports[]` / `imports[]` get the same
|
|
122
|
+
// deep-schema treatment as initializers/trampolines/canonicalOptions
|
|
123
|
+
// (#94(3)) — a malformed entry must die here as a typed `PlanError`,
|
|
124
|
+
// never as a raw TypeError in `Executor.buildExport` or (worse) as a
|
|
125
|
+
// silent negative-offset slice of the wrong component bytes in
|
|
126
|
+
// `compileModules` (executor.ts's only guard was an upper-bound check).
|
|
127
|
+
wire.modules.forEach((m, i) => validateModule(m, `modules[${i}]`));
|
|
128
|
+
wire.imports.forEach((imp, i) => validateImport(imp, `imports[${i}]`));
|
|
129
|
+
wire.exports.forEach((exp, i) => validateExport(exp, `exports[${i}]`));
|
|
130
|
+
const importedResources = wire.importedResources ?? [];
|
|
131
|
+
for (const [i, ir] of importedResources.entries()) {
|
|
132
|
+
if (typeof ir?.import !== "number" || ir.import < 0 ||
|
|
133
|
+
ir.import >= wire.imports.length) {
|
|
134
|
+
throw new PlanError(`importedResources[${i}].import = ${ir?.import} is not a valid ` +
|
|
135
|
+
`index into plan.imports (length ${wire.imports.length})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Identity tokens: one per RESOURCE, aliased through every table that
|
|
139
|
+
// names it — NOT one per table. plan-format.md C2 amendment #1: "one
|
|
140
|
+
// resource type can be reachable through several distinct table indices …
|
|
141
|
+
// Consumers keying per-resource state must key by `resourceTables[n]
|
|
142
|
+
// .resource`, treating table indices as aliases." Minting per-table broke
|
|
143
|
+
// exactly the way that warning predicts (found by the #18 polymorph-tls
|
|
144
|
+
// smoke): in a wac-composed component the source and destination future
|
|
145
|
+
// tables of a FACT transfer resolve `own<R>` through different table
|
|
146
|
+
// indices, and `valTypeEqual`'s documented reference-identity comparison
|
|
147
|
+
// (cabi/types.ts) saw two tokens for one resource — "future: destination
|
|
148
|
+
// element mismatch" on every resource-bearing element type. wasmtime
|
|
149
|
+
// interns identity at the `ResourceIndex` level and its transfer libcall
|
|
150
|
+
// never re-compares element types at runtime (47.0.3
|
|
151
|
+
// futures_and_streams.rs `guest_transfer`); unifying here restores parity
|
|
152
|
+
// for every structural-equality site at once. Abstract tables keep
|
|
153
|
+
// per-table tokens (no `resource` to key by; none in the current corpus).
|
|
154
|
+
const tokenByResource = new Map();
|
|
155
|
+
const resourceTokens = wire.resourceTables.map((table) => {
|
|
156
|
+
if (table.kind !== "concrete")
|
|
157
|
+
return new ResourceTypeInfo(null, null);
|
|
158
|
+
let token = tokenByResource.get(table.resource);
|
|
159
|
+
if (token === undefined) {
|
|
160
|
+
token = new ResourceTypeInfo(null, null);
|
|
161
|
+
tokenByResource.set(table.resource, token);
|
|
162
|
+
}
|
|
163
|
+
return token;
|
|
164
|
+
});
|
|
165
|
+
const types = wire.types.map((t, i) => loadTypeDecl(t, resourceTokens, `types[${i}]`));
|
|
166
|
+
// plan v3: the `task-return` decls double as the `TypeTupleIndex` ->
|
|
167
|
+
// `plan.types` dictionary (see `LoadedPlan.resultTupleTypes`). Two decls
|
|
168
|
+
// naming the same raw tuple must agree — they are interned from one
|
|
169
|
+
// wasmtime type, so disagreement means a hand-edited/corrupt plan.
|
|
170
|
+
const resultTupleTypes = new Map();
|
|
171
|
+
for (const [i, t] of wire.trampolines.entries()) {
|
|
172
|
+
if (t.kind !== "task-return")
|
|
173
|
+
continue;
|
|
174
|
+
const decl = t;
|
|
175
|
+
if (decl.resultType === null)
|
|
176
|
+
continue;
|
|
177
|
+
if (decl.resultType < 0 || decl.resultType >= wire.types.length) {
|
|
178
|
+
throw new PlanError(`trampolines[${i}].resultType = ${decl.resultType} is not a valid ` +
|
|
179
|
+
`index into plan.types (length ${wire.types.length})`);
|
|
180
|
+
}
|
|
181
|
+
const seen = resultTupleTypes.get(decl.results);
|
|
182
|
+
if (seen !== undefined && seen !== decl.resultType) {
|
|
183
|
+
throw new PlanError(`trampolines[${i}]: task-return tuple ${decl.results} maps to both ` +
|
|
184
|
+
`type ${seen} and type ${decl.resultType}`);
|
|
185
|
+
}
|
|
186
|
+
resultTupleTypes.set(decl.results, decl.resultType);
|
|
187
|
+
}
|
|
188
|
+
const elems = (ts, what) => (ts ?? []).map((t, i) => t.element === null
|
|
189
|
+
? null
|
|
190
|
+
: loadValType(t.element, resourceTokens, `${what}[${i}].element`));
|
|
191
|
+
return {
|
|
192
|
+
wire,
|
|
193
|
+
types,
|
|
194
|
+
resourceTokens,
|
|
195
|
+
numImportedResources: importedResources.length,
|
|
196
|
+
streamElems: elems(wire.streamTables, "streamTables"),
|
|
197
|
+
futureElems: elems(wire.futureTables, "futureTables"),
|
|
198
|
+
streamTableInstances: wire.streamTables.map((t) => t.instance),
|
|
199
|
+
futureTableInstances: wire.futureTables.map((t) => t.instance),
|
|
200
|
+
errorContextTableInstances: wire.errorContextTables.map((t) => t.instance),
|
|
201
|
+
resultTupleTypes,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Component-wide `ResourceIndex` for a `DefinedResourceIndex` (the `index`
|
|
206
|
+
* field of a `resource` initializer). Mirrors wasmtime
|
|
207
|
+
* `Component::resource_index` (wasmtime-environ 47.0.3
|
|
208
|
+
* `component/info.rs:222`).
|
|
209
|
+
*/
|
|
210
|
+
export function resourceIndexOfDefined(plan, definedIndex) {
|
|
211
|
+
return plan.numImportedResources + definedIndex;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Parse the shim's C-ABI JSON envelope into a validated wire plan + adapter
|
|
215
|
+
* bytes. The plan is validated (formatVersion, type tables) but returned in
|
|
216
|
+
* wire form: the executor re-runs `loadPlan` per instantiation so resource
|
|
217
|
+
* identity tokens are fresh per component instance.
|
|
218
|
+
*/
|
|
219
|
+
export function loadEnvelope(json) {
|
|
220
|
+
let envelope;
|
|
221
|
+
try {
|
|
222
|
+
envelope = JSON.parse(json);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
throw new PlanError(`envelope is not valid JSON: ${e}`);
|
|
226
|
+
}
|
|
227
|
+
if (envelope.error !== undefined) {
|
|
228
|
+
// v0.1 producers send only `error`; treat the missing structured verdict
|
|
229
|
+
// as "internal" — an unknown phase must never be read as a validation
|
|
230
|
+
// verdict (see TranslateError).
|
|
231
|
+
throw new TranslateError(envelope.errorDetail ??
|
|
232
|
+
{ phase: "internal", message: envelope.error });
|
|
233
|
+
}
|
|
234
|
+
if (!envelope.plan)
|
|
235
|
+
throw new PlanError("envelope missing `plan`");
|
|
236
|
+
loadPlan(envelope.plan); // validate early; discard (see docstring)
|
|
237
|
+
const adapters = new Map();
|
|
238
|
+
for (const a of envelope.adapters ?? []) {
|
|
239
|
+
adapters.set(a.file, base64Decode(a.wasm));
|
|
240
|
+
}
|
|
241
|
+
return { wire: envelope.plan, adapters };
|
|
242
|
+
}
|
|
243
|
+
function base64Decode(s) {
|
|
244
|
+
const bin = atob(s);
|
|
245
|
+
const out = new Uint8Array(bin.length);
|
|
246
|
+
for (let i = 0; i < bin.length; i++)
|
|
247
|
+
out[i] = bin.charCodeAt(i);
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
// --- ISSUE #94(3): deep-schema validation --------------------------------
|
|
251
|
+
//
|
|
252
|
+
// Proportionate shape-checking for the wire-format ops the executor runs
|
|
253
|
+
// strictly: required-field presence + primitive-type checks per
|
|
254
|
+
// discriminated-union arm, mirroring `format.ts`'s tagged unions. Not a
|
|
255
|
+
// full JSON-schema validator (no cross-field or index-bounds checks beyond
|
|
256
|
+
// what's already done for type/resource tables above) — just enough that
|
|
257
|
+
// a malformed op surfaces as a typed `PlanError` here instead of a raw
|
|
258
|
+
// `TypeError` mid-execution in the executor.
|
|
259
|
+
function isRecord(x) {
|
|
260
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
261
|
+
}
|
|
262
|
+
function expect(cond, where, what) {
|
|
263
|
+
if (!cond)
|
|
264
|
+
throw new PlanError(`${where}: ${what}`);
|
|
265
|
+
}
|
|
266
|
+
function expectNumber(o, field, where) {
|
|
267
|
+
expect(typeof o[field] === "number", where, `.${field} must be a number, got ${describeValue(o[field])}`);
|
|
268
|
+
}
|
|
269
|
+
function expectNumberOrNull(o, field, where) {
|
|
270
|
+
expect(o[field] === null || typeof o[field] === "number", where, `.${field} must be a number or null, got ${describeValue(o[field])}`);
|
|
271
|
+
}
|
|
272
|
+
function expectString(o, field, where) {
|
|
273
|
+
expect(typeof o[field] === "string", where, `.${field} must be a string, got ${describeValue(o[field])}`);
|
|
274
|
+
}
|
|
275
|
+
// ISSUE #187: `offset`/`len` reach `Uint8Array.slice` unchecked today; a
|
|
276
|
+
// negative or non-integer value is not merely "wrong type" (expectNumber
|
|
277
|
+
// would pass NaN and negatives through) but silently slices the *wrong*
|
|
278
|
+
// component bytes (`slice(-100, -8)` reads from the tail). Reject anything
|
|
279
|
+
// that is not a non-negative safe integer.
|
|
280
|
+
function expectNonNegativeInt(o, field, where) {
|
|
281
|
+
const v = o[field];
|
|
282
|
+
expect(typeof v === "number" && Number.isSafeInteger(v) && v >= 0, where, `.${field} must be a non-negative safe integer, got ${describeValue(v)}`);
|
|
283
|
+
}
|
|
284
|
+
function expectBoolean(o, field, where) {
|
|
285
|
+
expect(typeof o[field] === "boolean", where, `.${field} must be a boolean, got ${describeValue(o[field])}`);
|
|
286
|
+
}
|
|
287
|
+
function expectArray(o, field, where) {
|
|
288
|
+
expect(Array.isArray(o[field]), where, `.${field} must be an array, got ${describeValue(o[field])}`);
|
|
289
|
+
}
|
|
290
|
+
function describeValue(v) {
|
|
291
|
+
if (v === undefined)
|
|
292
|
+
return "undefined (missing)";
|
|
293
|
+
if (v === null)
|
|
294
|
+
return "null";
|
|
295
|
+
if (Array.isArray(v))
|
|
296
|
+
return `array (length ${v.length})`;
|
|
297
|
+
if (typeof v === "object")
|
|
298
|
+
return "object";
|
|
299
|
+
return JSON.stringify(v);
|
|
300
|
+
}
|
|
301
|
+
const CORE_TYPE_LANES = new Set(["i32", "i64", "f32", "f64"]);
|
|
302
|
+
function validateCoreDef(def, where) {
|
|
303
|
+
expect(isRecord(def), where, `must be an object, got ${describeValue(def)}`);
|
|
304
|
+
const d = def;
|
|
305
|
+
expectString(d, "kind", where);
|
|
306
|
+
switch (d.kind) {
|
|
307
|
+
case "export":
|
|
308
|
+
expectNumber(d, "instance", where);
|
|
309
|
+
expect(isRecord(d.item), where, `.item must be an object`);
|
|
310
|
+
validateExportItem(d.item, `${where}.item`);
|
|
311
|
+
return;
|
|
312
|
+
case "instance-flags":
|
|
313
|
+
expectNumber(d, "instance", where);
|
|
314
|
+
return;
|
|
315
|
+
case "trampoline":
|
|
316
|
+
expectNumber(d, "index", where);
|
|
317
|
+
return;
|
|
318
|
+
case "unsafe-intrinsic":
|
|
319
|
+
expectString(d, "intrinsic", where);
|
|
320
|
+
return;
|
|
321
|
+
case "task-may-block":
|
|
322
|
+
return;
|
|
323
|
+
default:
|
|
324
|
+
throw new PlanError(`${where}: unknown CoreDef kind ${describeValue(d.kind)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function validateExportItem(item, where) {
|
|
328
|
+
expect(isRecord(item), where, `must be an object`);
|
|
329
|
+
const it = item;
|
|
330
|
+
expectString(it, "name", where);
|
|
331
|
+
expect(typeof it.space === "string" &&
|
|
332
|
+
["func", "table", "memory", "global", "tag", "unknown"].includes(it.space), where, `.space must be one of func/table/memory/global/tag/unknown, got ` +
|
|
333
|
+
describeValue(it.space));
|
|
334
|
+
}
|
|
335
|
+
function validateCoreExport(exp, where) {
|
|
336
|
+
expect(isRecord(exp), where, `must be an object`);
|
|
337
|
+
const e = exp;
|
|
338
|
+
expectNumber(e, "instance", where);
|
|
339
|
+
expect(isRecord(e.item), where, `.item must be an object`);
|
|
340
|
+
validateExportItem(e.item, `${where}.item`);
|
|
341
|
+
}
|
|
342
|
+
function validateInitializer(init, where) {
|
|
343
|
+
expect(isRecord(init), where, `must be an object, got ${describeValue(init)}`);
|
|
344
|
+
const i = init;
|
|
345
|
+
expectString(i, "op", where);
|
|
346
|
+
switch (i.op) {
|
|
347
|
+
case "instantiate-module":
|
|
348
|
+
expectNumber(i, "module", where);
|
|
349
|
+
expectNumberOrNull(i, "instance", where);
|
|
350
|
+
expectArray(i, "args", where);
|
|
351
|
+
i.args.forEach((a, idx) => validateCoreDef(a, `${where}.args[${idx}]`));
|
|
352
|
+
return;
|
|
353
|
+
case "lower-import":
|
|
354
|
+
expectNumber(i, "index", where);
|
|
355
|
+
expectNumber(i, "import", where);
|
|
356
|
+
return;
|
|
357
|
+
case "extract-memory":
|
|
358
|
+
expectNumber(i, "index", where);
|
|
359
|
+
validateCoreExport(i.export, `${where}.export`);
|
|
360
|
+
return;
|
|
361
|
+
case "extract-realloc":
|
|
362
|
+
case "extract-callback":
|
|
363
|
+
case "extract-post-return":
|
|
364
|
+
expectNumber(i, "index", where);
|
|
365
|
+
validateCoreDef(i.def, `${where}.def`);
|
|
366
|
+
return;
|
|
367
|
+
case "extract-table":
|
|
368
|
+
expectNumber(i, "index", where);
|
|
369
|
+
validateCoreExport(i.export, `${where}.export`);
|
|
370
|
+
return;
|
|
371
|
+
case "resource":
|
|
372
|
+
expectNumber(i, "index", where);
|
|
373
|
+
expect(typeof i.rep === "string" && CORE_TYPE_LANES.has(i.rep), where, `.rep must be one of i32/i64/f32/f64, got ${describeValue(i.rep)}`);
|
|
374
|
+
expect(i.dtor === null || isRecord(i.dtor), where, `.dtor must be a CoreDef object or null`);
|
|
375
|
+
if (i.dtor !== null)
|
|
376
|
+
validateCoreDef(i.dtor, `${where}.dtor`);
|
|
377
|
+
expectNumber(i, "instance", where);
|
|
378
|
+
return;
|
|
379
|
+
default:
|
|
380
|
+
throw new PlanError(`${where}: unknown initializer op ${describeValue(i.op)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Trampoline kinds with precise wire shapes (format.ts's non-catch-all
|
|
384
|
+
// arms). Everything else falls to the `{ kind: string; index: number;
|
|
385
|
+
// [field: string]: unknown }` catch-all — milestone-aware unsupported
|
|
386
|
+
// kinds the executor rejects at instantiate time (contracts/intrinsics.md
|
|
387
|
+
// §B), so only `kind` (string) and `index` (number) are load-time
|
|
388
|
+
// invariants for those.
|
|
389
|
+
function validateTrampoline(t, where) {
|
|
390
|
+
expect(isRecord(t), where, `must be an object, got ${describeValue(t)}`);
|
|
391
|
+
const tr = t;
|
|
392
|
+
expectString(tr, "kind", where);
|
|
393
|
+
expectNumber(tr, "index", where);
|
|
394
|
+
switch (tr.kind) {
|
|
395
|
+
case "lower-import":
|
|
396
|
+
expectNumber(tr, "lowered", where);
|
|
397
|
+
expectNumber(tr, "options", where);
|
|
398
|
+
expectNumber(tr, "type", where);
|
|
399
|
+
return;
|
|
400
|
+
case "trap":
|
|
401
|
+
case "enter-sync-call":
|
|
402
|
+
case "exit-sync-call":
|
|
403
|
+
return;
|
|
404
|
+
case "task-return":
|
|
405
|
+
expectNumber(tr, "instance", where);
|
|
406
|
+
expectNumber(tr, "results", where);
|
|
407
|
+
// plan v3: required, `number | null`.
|
|
408
|
+
expectNumberOrNull(tr, "resultType", where);
|
|
409
|
+
expectNumber(tr, "options", where);
|
|
410
|
+
return;
|
|
411
|
+
case "resource-drop":
|
|
412
|
+
case "resource-new":
|
|
413
|
+
case "resource-rep":
|
|
414
|
+
expectNumber(tr, "instance", where);
|
|
415
|
+
expectNumber(tr, "resource", where);
|
|
416
|
+
return;
|
|
417
|
+
default:
|
|
418
|
+
// Catch-all: unknown/milestone-gated kind, only the common fields
|
|
419
|
+
// above are required.
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function validateCanonicalOptions(o, where) {
|
|
424
|
+
expect(isRecord(o), where, `must be an object, got ${describeValue(o)}`);
|
|
425
|
+
const co = o;
|
|
426
|
+
expectNumber(co, "instance", where);
|
|
427
|
+
expect(typeof co.stringEncoding === "string" &&
|
|
428
|
+
["utf8", "utf16", "latin1+utf16"].includes(co.stringEncoding), where, `.stringEncoding must be one of utf8/utf16/latin1+utf16, got ` +
|
|
429
|
+
describeValue(co.stringEncoding));
|
|
430
|
+
expectNumberOrNull(co, "memory", where);
|
|
431
|
+
expectNumberOrNull(co, "realloc", where);
|
|
432
|
+
expectNumberOrNull(co, "postReturn", where);
|
|
433
|
+
expectNumberOrNull(co, "callback", where);
|
|
434
|
+
expectBoolean(co, "async", where);
|
|
435
|
+
expectBoolean(co, "cancellable", where);
|
|
436
|
+
expect(isRecord(co.coreType), where, `.coreType must be an object`);
|
|
437
|
+
const ct = co.coreType;
|
|
438
|
+
expectArray(ct, "params", `${where}.coreType`);
|
|
439
|
+
expectArray(ct, "results", `${where}.coreType`);
|
|
440
|
+
for (const [field, lanes] of [["params", ct.params], ["results", ct.results]]) {
|
|
441
|
+
lanes.forEach((lane, idx) => {
|
|
442
|
+
expect(typeof lane === "string" && CORE_TYPE_LANES.has(lane), `${where}.coreType.${field}[${idx}]`, `must be one of i32/i64/f32/f64, got ${describeValue(lane)}`);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ISSUE #187: `modules[]` — mirrors format.ts's `WireModule` union exactly.
|
|
447
|
+
// `embedded`'s `offset`/`len` are the fields the negative-offset walk in
|
|
448
|
+
// the issue exploits (executor.ts's only guard was `end > length`, which a
|
|
449
|
+
// negative `offset` sails through); `adapter`'s `file`/`len`/`intrinsics`
|
|
450
|
+
// are what `compileModules`/intrinsic wiring dereference unchecked
|
|
451
|
+
// downstream.
|
|
452
|
+
function validateModule(m, where) {
|
|
453
|
+
expect(isRecord(m), where, `must be an object, got ${describeValue(m)}`);
|
|
454
|
+
const mm = m;
|
|
455
|
+
expectString(mm, "kind", where);
|
|
456
|
+
switch (mm.kind) {
|
|
457
|
+
case "embedded":
|
|
458
|
+
expectNonNegativeInt(mm, "offset", where);
|
|
459
|
+
expectNonNegativeInt(mm, "len", where);
|
|
460
|
+
return;
|
|
461
|
+
case "adapter":
|
|
462
|
+
expectString(mm, "file", where);
|
|
463
|
+
expectNonNegativeInt(mm, "len", where);
|
|
464
|
+
expectArray(mm, "intrinsics", where);
|
|
465
|
+
mm.intrinsics.forEach((entry, i) => validateIntrinsicEntry(entry, `${where}.intrinsics[${i}]`));
|
|
466
|
+
return;
|
|
467
|
+
default:
|
|
468
|
+
throw new PlanError(`${where}: unknown module kind ${describeValue(mm.kind)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function validateIntrinsicEntry(entry, where) {
|
|
472
|
+
expect(isRecord(entry), where, `must be an object, got ${describeValue(entry)}`);
|
|
473
|
+
const e = entry;
|
|
474
|
+
expectString(e, "module", where);
|
|
475
|
+
expectString(e, "name", where);
|
|
476
|
+
expectString(e, "category", where);
|
|
477
|
+
expect(isRecord(e.def), where, `.def must be an object`);
|
|
478
|
+
validateCoreDef(e.def, `${where}.def`);
|
|
479
|
+
}
|
|
480
|
+
// ISSUE #187: `imports[]` — mirrors format.ts's `WireImport`. `type` is
|
|
481
|
+
// optional on the wire (present only for imports that carry an interned
|
|
482
|
+
// type-table index), so it is checked only when present.
|
|
483
|
+
function validateImport(imp, where) {
|
|
484
|
+
expect(isRecord(imp), where, `must be an object, got ${describeValue(imp)}`);
|
|
485
|
+
const i = imp;
|
|
486
|
+
expectString(i, "name", where);
|
|
487
|
+
expectArray(i, "path", where);
|
|
488
|
+
i.path.forEach((p, idx) => {
|
|
489
|
+
expect(typeof p === "string", `${where}.path[${idx}]`, `must be a string, got ${describeValue(p)}`);
|
|
490
|
+
});
|
|
491
|
+
expectString(i, "kind", where);
|
|
492
|
+
if (i.type !== undefined)
|
|
493
|
+
expectNumber(i, "type", where);
|
|
494
|
+
}
|
|
495
|
+
// ISSUE #187: `exports[]` — mirrors format.ts's `WireExport` union,
|
|
496
|
+
// recursing into `instance`'s nested `exports[]` (the "each kind's fields
|
|
497
|
+
// shape-checked … recursive for nested instance export lists" requirement).
|
|
498
|
+
function validateExport(exp, where) {
|
|
499
|
+
expect(isRecord(exp), where, `must be an object, got ${describeValue(exp)}`);
|
|
500
|
+
const e = exp;
|
|
501
|
+
expectString(e, "kind", where);
|
|
502
|
+
switch (e.kind) {
|
|
503
|
+
case "lifted-func":
|
|
504
|
+
expectString(e, "name", where);
|
|
505
|
+
expect(isRecord(e.coreDef), where, `.coreDef must be an object`);
|
|
506
|
+
validateCoreDef(e.coreDef, `${where}.coreDef`);
|
|
507
|
+
expectNumber(e, "options", where);
|
|
508
|
+
expectNumber(e, "type", where);
|
|
509
|
+
return;
|
|
510
|
+
case "instance":
|
|
511
|
+
expectString(e, "name", where);
|
|
512
|
+
expectArray(e, "exports", where);
|
|
513
|
+
e.exports.forEach((nested, i) => validateExport(nested, `${where}.exports[${i}]`));
|
|
514
|
+
return;
|
|
515
|
+
case "type":
|
|
516
|
+
expectString(e, "name", where);
|
|
517
|
+
expect(isRecord(e.type), where, `.type must be an object`);
|
|
518
|
+
validateTypeExport(e.type, `${where}.type`);
|
|
519
|
+
return;
|
|
520
|
+
case "module":
|
|
521
|
+
// plan-format.md v4 amendment 2: exported embedded core module.
|
|
522
|
+
expectString(e, "name", where);
|
|
523
|
+
expectNumber(e, "module", where);
|
|
524
|
+
return;
|
|
525
|
+
default:
|
|
526
|
+
throw new PlanError(`${where}: unknown export kind ${describeValue(e.kind)}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function validateTypeExport(t, where) {
|
|
530
|
+
expect(isRecord(t), where, `must be an object, got ${describeValue(t)}`);
|
|
531
|
+
const tt = t;
|
|
532
|
+
expectString(tt, "kind", where);
|
|
533
|
+
switch (tt.kind) {
|
|
534
|
+
case "resource":
|
|
535
|
+
expectNumber(tt, "resource", where);
|
|
536
|
+
return;
|
|
537
|
+
case "value":
|
|
538
|
+
expectNumber(tt, "type", where);
|
|
539
|
+
return;
|
|
540
|
+
default:
|
|
541
|
+
throw new PlanError(`${where}: unknown type-export kind ${describeValue(tt.kind)}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function loadTypeDecl(t, resourceTokens, where) {
|
|
545
|
+
if (t.kind === "func") {
|
|
546
|
+
const decl = t;
|
|
547
|
+
return {
|
|
548
|
+
kind: "func",
|
|
549
|
+
funcType: {
|
|
550
|
+
params: decl.params.map((p) => loadValType(p.type, resourceTokens, `${where}.params.${p.label}`)),
|
|
551
|
+
results: decl.results.map((r, i) => loadValType(r, resourceTokens, `${where}.results[${i}]`)),
|
|
552
|
+
async: decl.async,
|
|
553
|
+
},
|
|
554
|
+
paramNames: decl.params.map((p) => p.label),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
kind: "value",
|
|
559
|
+
type: loadValType(t, resourceTokens, where),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
export function loadValType(t, resourceTokens, where) {
|
|
563
|
+
switch (t.kind) {
|
|
564
|
+
case "bool":
|
|
565
|
+
case "s8":
|
|
566
|
+
case "u8":
|
|
567
|
+
case "s16":
|
|
568
|
+
case "u16":
|
|
569
|
+
case "s32":
|
|
570
|
+
case "u32":
|
|
571
|
+
case "s64":
|
|
572
|
+
case "u64":
|
|
573
|
+
case "f32":
|
|
574
|
+
case "f64":
|
|
575
|
+
case "char":
|
|
576
|
+
case "string":
|
|
577
|
+
case "error-context":
|
|
578
|
+
return { kind: t.kind };
|
|
579
|
+
case "list":
|
|
580
|
+
return {
|
|
581
|
+
kind: "list",
|
|
582
|
+
element: loadValType(t.element, resourceTokens, `${where}.element`),
|
|
583
|
+
...(t.length !== undefined ? { length: t.length } : {}),
|
|
584
|
+
};
|
|
585
|
+
case "record":
|
|
586
|
+
return {
|
|
587
|
+
kind: "record",
|
|
588
|
+
fields: t.fields.map((f) => ({
|
|
589
|
+
label: f.label,
|
|
590
|
+
type: loadValType(f.type, resourceTokens, `${where}.${f.label}`),
|
|
591
|
+
})),
|
|
592
|
+
};
|
|
593
|
+
case "tuple":
|
|
594
|
+
return {
|
|
595
|
+
kind: "tuple",
|
|
596
|
+
elements: t.elements.map((e, i) => loadValType(e, resourceTokens, `${where}[${i}]`)),
|
|
597
|
+
};
|
|
598
|
+
case "variant":
|
|
599
|
+
return {
|
|
600
|
+
kind: "variant",
|
|
601
|
+
cases: t.cases.map((c) => ({
|
|
602
|
+
label: c.label,
|
|
603
|
+
type: c.type === null
|
|
604
|
+
? null
|
|
605
|
+
: loadValType(c.type, resourceTokens, `${where}.${c.label}`),
|
|
606
|
+
})),
|
|
607
|
+
};
|
|
608
|
+
case "enum":
|
|
609
|
+
return { kind: "enum", labels: [...t.labels] };
|
|
610
|
+
case "option":
|
|
611
|
+
return {
|
|
612
|
+
kind: "option",
|
|
613
|
+
type: loadValType(t.type, resourceTokens, `${where}.some`),
|
|
614
|
+
};
|
|
615
|
+
case "result":
|
|
616
|
+
return {
|
|
617
|
+
kind: "result",
|
|
618
|
+
ok: t.ok === null
|
|
619
|
+
? null
|
|
620
|
+
: loadValType(t.ok, resourceTokens, `${where}.ok`),
|
|
621
|
+
// Wire name is `err` (descriptor-ir.md); in-memory name is `error`
|
|
622
|
+
// (cabi/types.ts).
|
|
623
|
+
error: t.err === null
|
|
624
|
+
? null
|
|
625
|
+
: loadValType(t.err, resourceTokens, `${where}.err`),
|
|
626
|
+
};
|
|
627
|
+
case "map":
|
|
628
|
+
return {
|
|
629
|
+
kind: "map",
|
|
630
|
+
key: loadValType(t.key, resourceTokens, `${where}.key`),
|
|
631
|
+
value: loadValType(t.value, resourceTokens, `${where}.value`),
|
|
632
|
+
};
|
|
633
|
+
case "flags":
|
|
634
|
+
return { kind: "flags", labels: [...t.labels] };
|
|
635
|
+
case "own":
|
|
636
|
+
case "borrow": {
|
|
637
|
+
const rt = resourceTokens[t.resource];
|
|
638
|
+
if (rt === undefined) {
|
|
639
|
+
throw new PlanError(`${where}: ${t.kind} references resource table ${t.resource}, ` +
|
|
640
|
+
`but the plan has ${resourceTokens.length} resource tables`);
|
|
641
|
+
}
|
|
642
|
+
return { kind: t.kind, rt };
|
|
643
|
+
}
|
|
644
|
+
case "stream":
|
|
645
|
+
case "future":
|
|
646
|
+
return {
|
|
647
|
+
kind: t.kind,
|
|
648
|
+
element: t.element === null
|
|
649
|
+
? null
|
|
650
|
+
: loadValType(t.element, resourceTokens, `${where}.element`),
|
|
651
|
+
};
|
|
652
|
+
default: {
|
|
653
|
+
const exhaustive = t;
|
|
654
|
+
throw new PlanError(`${where}: unknown ValType kind ${exhaustive.kind}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
package/esm/plan/mod.js
ADDED
package/esm/shim/mod.js
ADDED