@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,264 @@
|
|
|
1
|
+
// Canonical ABI — provisional component value-type model.
|
|
2
|
+
//
|
|
3
|
+
// This is the v1 sketch of the "CABI descriptor IR" of docs/architecture.md §8: the type
|
|
4
|
+
// information the host-boundary lift/lower interpreter walks. It mirrors the
|
|
5
|
+
// type classes of the executable spec
|
|
6
|
+
// (third_party/component-model/design/mvp/canonical-abi/definitions.py) as a
|
|
7
|
+
// TypeScript discriminated union, holding only what lift/lower needs.
|
|
8
|
+
//
|
|
9
|
+
// PROVISIONAL — expected to change when the translator shim (docs/architecture.md §4.2)
|
|
10
|
+
// defines the real plan format. Open questions, tracked in runtime/README.md:
|
|
11
|
+
// - Serialized encoding (this in-memory shape vs the plan's wire format).
|
|
12
|
+
// - Whether labels stay strings or become interned indices in the IR.
|
|
13
|
+
// - Host-facing value representations for tuple/variant/option/result
|
|
14
|
+
// (currently the despecialized definitions.py shapes; bindgen §9 will
|
|
15
|
+
// want arrays / tagged unions / undefined-based options).
|
|
16
|
+
// - Resource types: here an opaque token; the plan will carry resource-type
|
|
17
|
+
// indices + dtor references instead.
|
|
18
|
+
export function coreFuncTypeEquals(a, b) {
|
|
19
|
+
return a.params.length === b.params.length &&
|
|
20
|
+
a.results.length === b.results.length &&
|
|
21
|
+
a.params.every((p, i) => p === b.params[i]) &&
|
|
22
|
+
a.results.every((r, i) => r === b.results[i]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* definitions.py `ResourceType`: identity + implementing instance + optional
|
|
26
|
+
* destructor. Compared by object identity everywhere.
|
|
27
|
+
*
|
|
28
|
+
* `dtorHost` is the **host-initiated**-drop entry (#85, reshaped by #160):
|
|
29
|
+
* the dtor built as a fully LIFTED sync function — definitions.py
|
|
30
|
+
* `canon_resource_drop` (line 2319) `inst.store.lift(dtor, ft, opts,
|
|
31
|
+
* rt.impl)` — so the destructor's activation gets a real Task/Thread, the
|
|
32
|
+
* reentrance bracket is released at its first park, and its suspension
|
|
33
|
+
* points are resumable by the scheduler. It is wired by exec/executor.ts
|
|
34
|
+
* (jspi-`promising` entry only when the dtor is suspension-capable, docs §7)
|
|
35
|
+
* and called through `hostDtorCall` (exec/boundary.ts), which also fills it
|
|
36
|
+
* in lazily for tokens built directly. It returns `undefined` or a Promise,
|
|
37
|
+
* so it is NOT callable from inside a guest activation.
|
|
38
|
+
*
|
|
39
|
+
* Guest-initiated drops (`callDtorGated`) always use `dtor` directly: they
|
|
40
|
+
* must complete synchronously (reference lifts the dtor with
|
|
41
|
+
* `async_ = False`), and any thenable there is a trap.
|
|
42
|
+
*/
|
|
43
|
+
export class ResourceTypeInfo {
|
|
44
|
+
impl;
|
|
45
|
+
dtor;
|
|
46
|
+
dtorHost;
|
|
47
|
+
constructor(impl, dtor = null, dtorHost = null) {
|
|
48
|
+
this.impl = impl;
|
|
49
|
+
this.dtor = dtor;
|
|
50
|
+
this.dtorHost = dtorHost;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function despecialize(t) {
|
|
54
|
+
switch (t.kind) {
|
|
55
|
+
case "tuple":
|
|
56
|
+
return {
|
|
57
|
+
kind: "record",
|
|
58
|
+
fields: t.elements.map((e, i) => ({ label: String(i), type: e })),
|
|
59
|
+
};
|
|
60
|
+
case "enum":
|
|
61
|
+
return {
|
|
62
|
+
kind: "variant",
|
|
63
|
+
cases: t.labels.map((l) => ({ label: l, type: null })),
|
|
64
|
+
};
|
|
65
|
+
case "option":
|
|
66
|
+
return {
|
|
67
|
+
kind: "variant",
|
|
68
|
+
cases: [{ label: "none", type: null }, { label: "some", type: t.type }],
|
|
69
|
+
};
|
|
70
|
+
case "result":
|
|
71
|
+
return {
|
|
72
|
+
kind: "variant",
|
|
73
|
+
cases: [{ label: "ok", type: t.ok }, { label: "error", type: t.error }],
|
|
74
|
+
};
|
|
75
|
+
case "map":
|
|
76
|
+
return {
|
|
77
|
+
kind: "list",
|
|
78
|
+
element: despecialize({
|
|
79
|
+
kind: "tuple",
|
|
80
|
+
elements: [t.key, t.value],
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
default:
|
|
84
|
+
return t;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Discriminants (definitions.py `discriminant_type`)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
export function discriminantType(cases) {
|
|
91
|
+
const n = cases.length;
|
|
92
|
+
if (!(0 < n && n < 2 ** 32))
|
|
93
|
+
throw new Error("assertion failed: case count");
|
|
94
|
+
// mirrors math.ceil(log2(n)/8): 0|1 -> u8, 2 -> u16, 3 -> u32
|
|
95
|
+
if (n <= 256)
|
|
96
|
+
return { kind: "u8" };
|
|
97
|
+
if (n <= 65536)
|
|
98
|
+
return { kind: "u16" };
|
|
99
|
+
return { kind: "u32" };
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Type predicates (definitions.py `contains_borrow` etc.)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
export function containsBorrow(t) {
|
|
105
|
+
return contains(t, (u) => u.kind === "borrow");
|
|
106
|
+
}
|
|
107
|
+
export function containsAsyncValue(t) {
|
|
108
|
+
return contains(t, (u) => u.kind === "stream" || u.kind === "future");
|
|
109
|
+
}
|
|
110
|
+
export function contains(t, p) {
|
|
111
|
+
if (t === null)
|
|
112
|
+
return false;
|
|
113
|
+
const d = despecialize(t);
|
|
114
|
+
switch (d.kind) {
|
|
115
|
+
case "list":
|
|
116
|
+
return p(d) || contains(d.element, p);
|
|
117
|
+
case "stream":
|
|
118
|
+
case "future":
|
|
119
|
+
return p(d) || contains(d.element, p);
|
|
120
|
+
case "record":
|
|
121
|
+
return p(d) || d.fields.some((f) => contains(f.type, p));
|
|
122
|
+
case "variant":
|
|
123
|
+
return p(d) || d.cases.some((c) => contains(c.type, p));
|
|
124
|
+
default:
|
|
125
|
+
return p(d);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Structural ValType equality and display
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* Structural `ValType` equality.
|
|
133
|
+
*
|
|
134
|
+
* CONTRACT (bugfix, TRACK C2-D; generalized during the #18 tls smoke): naive
|
|
135
|
+
* `JSON.stringify(a) === JSON.stringify(b)` recurses into `own`/`borrow`'s
|
|
136
|
+
* `ResourceTypeInfo` — a class whose `impl` field is documented "Compared by
|
|
137
|
+
* object identity everywhere" (see `ResourceTypeInfo` above) and which cycles
|
|
138
|
+
* back to the owning instance state (`impl.handles` holds live resource
|
|
139
|
+
* tables that reference their types), so `JSON.stringify` throws
|
|
140
|
+
* `TypeError: Converting circular structure to JSON` on ANY type containing
|
|
141
|
+
* `own<R>`/`borrow<R>` at any depth. First hit by `task.return` result types
|
|
142
|
+
* (C2-D, polymorph-test's `list<own<test-case>>`), then by stream/future
|
|
143
|
+
* element types (polymorph-tls streams carrying resource-bearing payloads).
|
|
144
|
+
* Object-identity types (`ResourceTypeInfo`) are compared by reference, per
|
|
145
|
+
* the documented invariant.
|
|
146
|
+
*/
|
|
147
|
+
export function valTypesEqual(a, b) {
|
|
148
|
+
return a.length === b.length && a.every((t, i) => valTypeEqual(t, b[i]));
|
|
149
|
+
}
|
|
150
|
+
export function valTypeEqual(a, b) {
|
|
151
|
+
if (a === b)
|
|
152
|
+
return true;
|
|
153
|
+
if (a.kind !== b.kind)
|
|
154
|
+
return false;
|
|
155
|
+
switch (a.kind) {
|
|
156
|
+
case "list": {
|
|
157
|
+
const bb = b;
|
|
158
|
+
return a.length === bb.length && valTypeEqual(a.element, bb.element);
|
|
159
|
+
}
|
|
160
|
+
case "record": {
|
|
161
|
+
const bb = b;
|
|
162
|
+
return a.fields.length === bb.fields.length &&
|
|
163
|
+
a.fields.every((f, i) => f.label === bb.fields[i].label && valTypeEqual(f.type, bb.fields[i].type));
|
|
164
|
+
}
|
|
165
|
+
case "tuple": {
|
|
166
|
+
const bb = b;
|
|
167
|
+
return a.elements.length === bb.elements.length &&
|
|
168
|
+
a.elements.every((e, i) => valTypeEqual(e, bb.elements[i]));
|
|
169
|
+
}
|
|
170
|
+
case "variant": {
|
|
171
|
+
const bb = b;
|
|
172
|
+
return a.cases.length === bb.cases.length &&
|
|
173
|
+
a.cases.every((c, i) => {
|
|
174
|
+
const other = bb.cases[i];
|
|
175
|
+
if (c.label !== other.label)
|
|
176
|
+
return false;
|
|
177
|
+
if (c.type === null || other.type === null)
|
|
178
|
+
return c.type === other.type;
|
|
179
|
+
return valTypeEqual(c.type, other.type);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
case "enum":
|
|
183
|
+
case "flags": {
|
|
184
|
+
const bb = b;
|
|
185
|
+
return a.labels.length === bb.labels.length &&
|
|
186
|
+
a.labels.every((l, i) => l === bb.labels[i]);
|
|
187
|
+
}
|
|
188
|
+
case "option": {
|
|
189
|
+
const bb = b;
|
|
190
|
+
return valTypeEqual(a.type, bb.type);
|
|
191
|
+
}
|
|
192
|
+
case "result": {
|
|
193
|
+
const bb = b;
|
|
194
|
+
if ((a.ok === null) !== (bb.ok === null))
|
|
195
|
+
return false;
|
|
196
|
+
if ((a.error === null) !== (bb.error === null))
|
|
197
|
+
return false;
|
|
198
|
+
return (a.ok === null || valTypeEqual(a.ok, bb.ok)) &&
|
|
199
|
+
(a.error === null || valTypeEqual(a.error, bb.error));
|
|
200
|
+
}
|
|
201
|
+
case "map": {
|
|
202
|
+
const bb = b;
|
|
203
|
+
return valTypeEqual(a.key, bb.key) && valTypeEqual(a.value, bb.value);
|
|
204
|
+
}
|
|
205
|
+
case "own":
|
|
206
|
+
case "borrow": {
|
|
207
|
+
const bb = b;
|
|
208
|
+
// Object-identity type (documented invariant): reference equality only.
|
|
209
|
+
return a.rt === bb.rt;
|
|
210
|
+
}
|
|
211
|
+
case "stream":
|
|
212
|
+
case "future": {
|
|
213
|
+
const bb = b;
|
|
214
|
+
if ((a.element === null) !== (bb.element === null))
|
|
215
|
+
return false;
|
|
216
|
+
return a.element === null || valTypeEqual(a.element, bb.element);
|
|
217
|
+
}
|
|
218
|
+
case "error-context":
|
|
219
|
+
return true;
|
|
220
|
+
default:
|
|
221
|
+
// Remaining kinds (primitives) carry no extra fields beyond `kind`.
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Cycle-safe display form for diagnostics. `JSON.stringify(t)` is UNSAFE on
|
|
227
|
+
* any resource-bearing type (see `valTypeEqual`'s contract note); this prints
|
|
228
|
+
* the structural shape and elides `ResourceTypeInfo` identities.
|
|
229
|
+
*/
|
|
230
|
+
export function fmtValType(t) {
|
|
231
|
+
if (t === null)
|
|
232
|
+
return "_";
|
|
233
|
+
switch (t.kind) {
|
|
234
|
+
case "list":
|
|
235
|
+
return t.length === undefined
|
|
236
|
+
? `list<${fmtValType(t.element)}>`
|
|
237
|
+
: `list<${fmtValType(t.element)}, ${t.length}>`;
|
|
238
|
+
case "record":
|
|
239
|
+
return `record{${t.fields.map((f) => `${f.label}: ${fmtValType(f.type)}`).join(", ")}}`;
|
|
240
|
+
case "tuple":
|
|
241
|
+
return `tuple<${t.elements.map(fmtValType).join(", ")}>`;
|
|
242
|
+
case "variant":
|
|
243
|
+
return `variant{${t.cases.map((c) => c.type === null ? c.label : `${c.label}(${fmtValType(c.type)})`)
|
|
244
|
+
.join(", ")}}`;
|
|
245
|
+
case "enum":
|
|
246
|
+
return `enum{${t.labels.join(", ")}}`;
|
|
247
|
+
case "flags":
|
|
248
|
+
return `flags{${t.labels.join(", ")}}`;
|
|
249
|
+
case "option":
|
|
250
|
+
return `option<${fmtValType(t.type)}>`;
|
|
251
|
+
case "result":
|
|
252
|
+
return `result<${fmtValType(t.ok)}, ${fmtValType(t.error)}>`;
|
|
253
|
+
case "map":
|
|
254
|
+
return `map<${fmtValType(t.key)}, ${fmtValType(t.value)}>`;
|
|
255
|
+
case "own":
|
|
256
|
+
case "borrow":
|
|
257
|
+
return `${t.kind}<resource>`;
|
|
258
|
+
case "stream":
|
|
259
|
+
case "future":
|
|
260
|
+
return `${t.kind}<${fmtValType(t.element)}>`;
|
|
261
|
+
default:
|
|
262
|
+
return t.kind;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Lifting and lowering of full value lists with spilling (definitions.py
|
|
2
|
+
// `## Lifting and Lowering Values`): parameter/result sequences that exceed
|
|
3
|
+
// the flat maximum are passed indirectly through a tuple in linear memory.
|
|
4
|
+
import { trapIf } from "./trap.js";
|
|
5
|
+
import { alignment, alignTo, elemSize } from "./layout.js";
|
|
6
|
+
import { load } from "./load.js";
|
|
7
|
+
import { store } from "./store.js";
|
|
8
|
+
import { liftFlat } from "./lift.js";
|
|
9
|
+
import { lowerFlat } from "./lower.js";
|
|
10
|
+
import { flattenTypes } from "./flatten.js";
|
|
11
|
+
import { requireMemory } from "./context.js";
|
|
12
|
+
import { asIndex } from "./memory.js";
|
|
13
|
+
export function liftFlatValues(cx, maxFlat, vi, ts) {
|
|
14
|
+
const flatTypes = flattenTypes(ts, cx.opts);
|
|
15
|
+
if (flatTypes.length > maxFlat) {
|
|
16
|
+
const mem = requireMemory(cx.opts);
|
|
17
|
+
const ptrRaw = vi.next(mem.ptrType());
|
|
18
|
+
const tupleType = { kind: "tuple", elements: ts };
|
|
19
|
+
const align = alignment(tupleType, mem.ptrType());
|
|
20
|
+
const size = elemSize(tupleType, mem.ptrType());
|
|
21
|
+
trapIf(BigInt(ptrRaw) % BigInt(align) !== 0n, "misaligned spill pointer");
|
|
22
|
+
trapIf(BigInt(ptrRaw) + BigInt(size) > BigInt(mem.length), "spill tuple out of bounds");
|
|
23
|
+
const ptr = asIndex(ptrRaw);
|
|
24
|
+
const tuple = load(cx, ptr, tupleType);
|
|
25
|
+
return Object.values(tuple);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return ts.map((t) => liftFlat(cx, vi, t));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function lowerFlatValues(cx, maxFlat, vs, ts, outParam = null) {
|
|
32
|
+
const flatTypes = flattenTypes(ts, cx.opts);
|
|
33
|
+
if (flatTypes.length > maxFlat) {
|
|
34
|
+
const mem = requireMemory(cx.opts);
|
|
35
|
+
const tupleType = { kind: "tuple", elements: ts };
|
|
36
|
+
const tupleValue = {};
|
|
37
|
+
vs.forEach((v, i) => {
|
|
38
|
+
tupleValue[String(i)] = v;
|
|
39
|
+
});
|
|
40
|
+
let ptr;
|
|
41
|
+
let flatVals;
|
|
42
|
+
const align = alignment(tupleType, mem.ptrType());
|
|
43
|
+
const size = elemSize(tupleType, mem.ptrType());
|
|
44
|
+
if (outParam === null) {
|
|
45
|
+
ptr = cx.allocate(align, size);
|
|
46
|
+
flatVals = mem.ptrType() === "i32" ? [ptr] : [BigInt(ptr)];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ptr = asIndex(outParam.next(mem.ptrType()));
|
|
50
|
+
flatVals = [];
|
|
51
|
+
}
|
|
52
|
+
trapIf(ptr !== alignTo(ptr, align), "misaligned spill pointer");
|
|
53
|
+
trapIf(ptr + size > mem.length, "spill tuple out of bounds");
|
|
54
|
+
store(cx, tupleValue, tupleType, ptr);
|
|
55
|
+
return flatVals;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const flatVals = [];
|
|
59
|
+
for (let i = 0; i < vs.length; i++) {
|
|
60
|
+
flatVals.push(...lowerFlat(cx, vs[i], ts[i]));
|
|
61
|
+
}
|
|
62
|
+
return flatVals;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Artifact cache — platform-neutral core (docs/architecture.md §10, layer 1: ours, bytes
|
|
2
|
+
// only; nothing here depends on the engine's own compiled-module cache,
|
|
3
|
+
// i.e. layer 2).
|
|
4
|
+
//
|
|
5
|
+
// GOAL: content-address a shim translation by
|
|
6
|
+
// `(component sha256, translator build hash, feature flags)` so a reload
|
|
7
|
+
// (same component, same shim build, same features) can skip the
|
|
8
|
+
// translate-with-the-shim step entirely.
|
|
9
|
+
//
|
|
10
|
+
// CACHE FAILURES ARE NEVER FATAL (issue #196): the cache is a pure
|
|
11
|
+
// optimization layered over `Translator.translateRaw` + `loadEnvelope`, both
|
|
12
|
+
// of which already succeed/fail on their own terms. A `get`/`put`/internal
|
|
13
|
+
// self-heal failure — including a read-only or otherwise unwritable cache
|
|
14
|
+
// root — must never turn into a failed translation; at worst it turns into
|
|
15
|
+
// a fresh (uncached) translation. `translateCached` swallows `get`/`put`
|
|
16
|
+
// failures (surfaced only via the opt-in `onCacheError` callback below);
|
|
17
|
+
// the backends (dir.ts, web.ts) swallow their internal self-heal evictions
|
|
18
|
+
// and turn any `get`-path I/O failure into a `null` (miss) rather than a
|
|
19
|
+
// throw. The one exception, by design, is `TranslateError` from
|
|
20
|
+
// `loadEnvelope`: that is a verdict about the *input component*, not a
|
|
21
|
+
// cache failure, and keeps propagating uncached. The public `evict()` also
|
|
22
|
+
// keeps throwing — an explicit caller asked for that specific effect and
|
|
23
|
+
// deserves to know if it didn't happen.
|
|
24
|
+
//
|
|
25
|
+
// PERSISTED-ARTIFACT-SET DECISION (governing: contracts/plan-format.md
|
|
26
|
+
// "Artifact set" + "No duplicate bytes" — plan-format.md:24-31,63-64):
|
|
27
|
+
// we persist `plan.json` (the wire `WirePlan`) and the FACT adapter modules
|
|
28
|
+
// only. We do NOT persist the original component bytes.
|
|
29
|
+
//
|
|
30
|
+
// Evidence this is correct, not merely convenient:
|
|
31
|
+
// - plan-format.md is explicit: "The original component binary is the
|
|
32
|
+
// third input at instantiation time; the plan never embeds it" and
|
|
33
|
+
// "Embedded core modules are referenced as `[offset, len)` byte ranges
|
|
34
|
+
// into the original component binary — the executor slices them
|
|
35
|
+
// itself" (decision 3). `instantiateComponent`/`Facade.instantiate`
|
|
36
|
+
// (runtime/src/embedder/instantiate.ts `ComponentArtifacts`) take
|
|
37
|
+
// `componentBytes` as a caller-supplied field *alongside* `plan` and
|
|
38
|
+
// `adapters` — never as something the plan or its loader manufacture.
|
|
39
|
+
// - Operationally: whoever calls `translateCached` already holds the
|
|
40
|
+
// component bytes (that's how they'd have a sha256 to form a cache key
|
|
41
|
+
// in the first place, and how `verifyComponent`'s length check runs
|
|
42
|
+
// without a bytes store at all). Reload use cases (browser page reload,
|
|
43
|
+
// Deno process restart) re-fetch/re-read the *component* from its own
|
|
44
|
+
// source of truth (network, disk) every time; only the *translation*
|
|
45
|
+
// (the expensive shim call) is worth skipping. Storing componentBytes a
|
|
46
|
+
// second time would be pure duplication with no consumer.
|
|
47
|
+
// - `get()` therefore verifies integrity using only the requested
|
|
48
|
+
// `CacheKey.componentSha256` against the value that was true at `put()`
|
|
49
|
+
// time (recorded in the stored metadata and cross-checked against the
|
|
50
|
+
// embedded `plan.component.sha256`) — never against fresh bytes, which
|
|
51
|
+
// this layer never sees.
|
|
52
|
+
import { loadEnvelope, PlanError, TranslateError } from "../plan/loader.js";
|
|
53
|
+
/** Cache layout version. Bumped on any incompatible on-disk/on-Cache-API
|
|
54
|
+
* schema change; an unrecognized version is read back as a miss (never a
|
|
55
|
+
* crash) so stale caches from an older build self-heal by re-translating —
|
|
56
|
+
* and this self-healing holds even when the cache root itself is
|
|
57
|
+
* unwritable (issue #196): the eviction attempt that a layout mismatch
|
|
58
|
+
* triggers is swallowed internally by the backend, so a read-only
|
|
59
|
+
* pre-warmed cache from an older layout degrades to "always miss, always
|
|
60
|
+
* re-translate" rather than throwing. */
|
|
61
|
+
export const CACHE_LAYOUT_VERSION = 1;
|
|
62
|
+
async function sha256Hex(bytes) {
|
|
63
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
|
|
64
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0"))
|
|
65
|
+
.join("");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The stable cache key string: sha256 over a canonical JSON encoding of the
|
|
69
|
+
* `CacheKey` (features sorted so order never causes a spurious miss).
|
|
70
|
+
*/
|
|
71
|
+
export async function keyHex(key) {
|
|
72
|
+
const canonical = JSON.stringify({
|
|
73
|
+
componentSha256: key.componentSha256,
|
|
74
|
+
translatorBuildHash: key.translatorBuildHash,
|
|
75
|
+
features: [...key.features].sort(),
|
|
76
|
+
});
|
|
77
|
+
return await sha256Hex(new TextEncoder().encode(canonical));
|
|
78
|
+
}
|
|
79
|
+
/** Compute the `CacheKey` for a `(translator, componentBytes)` pair. Throws
|
|
80
|
+
* if the translator has no `buildHash` (constructed from a pre-compiled
|
|
81
|
+
* `WebAssembly.Module` with no bytes available — see `Translator.create`). */
|
|
82
|
+
export async function keyFor(translator, componentBytes, features = []) {
|
|
83
|
+
if (translator.buildHash === null) {
|
|
84
|
+
throw new PlanError("artifact cache: translator has no buildHash (constructed from a " +
|
|
85
|
+
"WebAssembly.Module without source bytes); pass raw wasm bytes to " +
|
|
86
|
+
"Translator.create to enable caching");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
componentSha256: await sha256Hex(componentBytes),
|
|
90
|
+
translatorBuildHash: translator.buildHash,
|
|
91
|
+
features,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function reportCacheError(opts, op, err) {
|
|
95
|
+
try {
|
|
96
|
+
opts.onCacheError?.(op, err);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// A diagnostic callback throwing must not fail the translation either
|
|
100
|
+
// (issue #196): the caller asked for a diagnostic, not a veto.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Orchestration helper: cache hit -> stored artifacts, cache miss ->
|
|
105
|
+
* translate with the shim, store, return.
|
|
106
|
+
*/
|
|
107
|
+
export async function translateCached(translator, componentBytes, cache, opts = {}) {
|
|
108
|
+
const key = await keyFor(translator, componentBytes, opts.features ?? []);
|
|
109
|
+
// A `get` failure (issue #196) reads as a miss: the cache is a pure
|
|
110
|
+
// optimization, so any way it fails to answer degrades to "translate
|
|
111
|
+
// fresh" rather than failing the whole translation.
|
|
112
|
+
let hit;
|
|
113
|
+
try {
|
|
114
|
+
hit = await cache.get(key);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
reportCacheError(opts, "get", e);
|
|
118
|
+
hit = null;
|
|
119
|
+
}
|
|
120
|
+
if (hit !== null) {
|
|
121
|
+
return { plan: hit.plan, adapters: hit.adapters, fromCache: true };
|
|
122
|
+
}
|
|
123
|
+
const first = translator.translateRaw(componentBytes);
|
|
124
|
+
if (opts.verifyDeterminism) {
|
|
125
|
+
const second = translator.translateRaw(componentBytes);
|
|
126
|
+
if (first !== second) {
|
|
127
|
+
throw new Error("artifact cache: determinism guard failed — translating the same " +
|
|
128
|
+
"component bytes twice produced different envelopes");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// loadEnvelope both validates (throws TranslateError for a validation
|
|
132
|
+
// verdict — must propagate uncached, per TranslateError's docs: a
|
|
133
|
+
// validation verdict is a judgment about the *input component*, not
|
|
134
|
+
// something to cache-and-replay) and gives us the split plan/adapters.
|
|
135
|
+
let wire;
|
|
136
|
+
let adapters;
|
|
137
|
+
try {
|
|
138
|
+
({ wire, adapters } = loadEnvelope(first));
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (e instanceof TranslateError)
|
|
142
|
+
throw e;
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
// A `put` failure (issue #196) is swallowed: the translation already
|
|
146
|
+
// succeeded and was already validated above by `loadEnvelope` — failing
|
|
147
|
+
// to *store* it says nothing about the result. Return the fresh
|
|
148
|
+
// artifacts anyway.
|
|
149
|
+
try {
|
|
150
|
+
await cache.put(key, { plan: wire, adapters });
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
reportCacheError(opts, "put", e);
|
|
154
|
+
}
|
|
155
|
+
return { plan: wire, adapters, fromCache: false };
|
|
156
|
+
}
|
package/esm/cache/dir.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Deno filesystem backend for the artifact cache (docs/architecture.md §10).
|
|
2
|
+
//
|
|
3
|
+
// Layout, under `<path>/<keyhex>/`:
|
|
4
|
+
// meta.json CacheMeta (layoutVersion, componentSha256,
|
|
5
|
+
// translatorBuildHash, features)
|
|
6
|
+
// plan.json the wire WirePlan, JSON-serialized
|
|
7
|
+
// adapters/<name> one file per adapter, name = the tail of the wire
|
|
8
|
+
// plan's `modules[].file` (which is already
|
|
9
|
+
// `adapters/<idx>.wasm` shaped upstream — see
|
|
10
|
+
// contracts/plan-format.md "Artifact set" — so this
|
|
11
|
+
// backend nests one more `adapters/` level under the
|
|
12
|
+
// key directory: `<path>/<keyhex>/adapters/<idx>.wasm`)
|
|
13
|
+
//
|
|
14
|
+
// Deliberately does NOT store component bytes — see core.ts's
|
|
15
|
+
// "PERSISTED-ARTIFACT-SET DECISION" docs for why that's sound, not a gap.
|
|
16
|
+
import { CACHE_LAYOUT_VERSION, keyHex } from "./core.js";
|
|
17
|
+
import { loadPlan, PlanError } from "../plan/loader.js";
|
|
18
|
+
/** Reject any adapter file name that isn't a plain, non-traversing
|
|
19
|
+
* relative path — defense in depth even though the shim is trusted (a
|
|
20
|
+
* poisoned/tampered cache directory must not escape it). */
|
|
21
|
+
function safeRelName(name) {
|
|
22
|
+
if (name.length === 0 ||
|
|
23
|
+
name.startsWith("/") ||
|
|
24
|
+
name.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) {
|
|
25
|
+
throw new PlanError(`artifact cache: unsafe adapter file name '${name}'`);
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
async function exists(path) {
|
|
30
|
+
try {
|
|
31
|
+
await Deno.stat(path);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (e instanceof Deno.errors.NotFound)
|
|
36
|
+
return false;
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function rmIfExists(path) {
|
|
41
|
+
try {
|
|
42
|
+
await Deno.remove(path, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (!(e instanceof Deno.errors.NotFound))
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class DirCache {
|
|
50
|
+
root;
|
|
51
|
+
constructor(root) {
|
|
52
|
+
this.root = root;
|
|
53
|
+
}
|
|
54
|
+
async entryDir(key) {
|
|
55
|
+
return `${this.root}/${await keyHex(key)}`;
|
|
56
|
+
}
|
|
57
|
+
/** Internal self-heal eviction (issue #196): the caller is `get`'s own
|
|
58
|
+
* recovery path for a poisoned/stale entry, not an explicit caller of
|
|
59
|
+
* `evict()` — so a failure here (e.g. an unwritable cache root) must not
|
|
60
|
+
* escape and fail what would otherwise be a clean miss. The public
|
|
61
|
+
* `evict()` below keeps throwing; only this internal path swallows. */
|
|
62
|
+
async #tryEvict(key) {
|
|
63
|
+
try {
|
|
64
|
+
await this.evict(key);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Swallowed: see docs above.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async get(key) {
|
|
71
|
+
try {
|
|
72
|
+
const dir = await this.entryDir(key);
|
|
73
|
+
if (!(await exists(dir)))
|
|
74
|
+
return null;
|
|
75
|
+
const metaRaw = await Deno.readTextFile(`${dir}/meta.json`);
|
|
76
|
+
const meta = JSON.parse(metaRaw);
|
|
77
|
+
if (meta.layoutVersion !== CACHE_LAYOUT_VERSION) {
|
|
78
|
+
await this.#tryEvict(key);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Integrity: the entry must agree with the *requested* key on every
|
|
82
|
+
// field, not just live at the expected directory name (belt-and-
|
|
83
|
+
// suspenders against a hand-edited or corrupted cache).
|
|
84
|
+
if (meta.componentSha256 !== key.componentSha256 ||
|
|
85
|
+
meta.translatorBuildHash !== key.translatorBuildHash ||
|
|
86
|
+
JSON.stringify([...meta.features].sort()) !==
|
|
87
|
+
JSON.stringify([...key.features].sort())) {
|
|
88
|
+
await this.#tryEvict(key);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const planRaw = await Deno.readTextFile(`${dir}/plan.json`);
|
|
92
|
+
const plan = JSON.parse(planRaw);
|
|
93
|
+
if (plan.component.sha256 !== key.componentSha256) {
|
|
94
|
+
await this.#tryEvict(key);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
loadPlan(plan); // structural validation; throws on a corrupted plan
|
|
98
|
+
const adapters = new Map();
|
|
99
|
+
for (const m of plan.modules) {
|
|
100
|
+
if (m.kind !== "adapter")
|
|
101
|
+
continue;
|
|
102
|
+
const name = safeRelName(m.file);
|
|
103
|
+
adapters.set(m.file, await Deno.readFile(`${dir}/adapters/${name}`));
|
|
104
|
+
}
|
|
105
|
+
return { plan, adapters };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Any parse/read/structural/I-O failure = a poisoned entry (or an
|
|
109
|
+
// unreadable/unwritable cache root, issue #196): miss + best-effort
|
|
110
|
+
// evict, never trust and never throw out of `get` (dispatch
|
|
111
|
+
// requirement). This also covers `exists()`'s rethrow of non-
|
|
112
|
+
// `NotFound` stat errors (ENOTDIR, EACCES, ...).
|
|
113
|
+
await this.#tryEvict(key);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async put(key, artifacts) {
|
|
118
|
+
const dir = await this.entryDir(key);
|
|
119
|
+
// Write to a temp sibling then rename, so a crash mid-write never
|
|
120
|
+
// leaves a partially-written entry that `get` would (try to) read.
|
|
121
|
+
const tmp = `${dir}.tmp-${crypto.randomUUID()}`;
|
|
122
|
+
await rmIfExists(tmp);
|
|
123
|
+
try {
|
|
124
|
+
await Deno.mkdir(`${tmp}/adapters`, { recursive: true });
|
|
125
|
+
const meta = {
|
|
126
|
+
layoutVersion: CACHE_LAYOUT_VERSION,
|
|
127
|
+
componentSha256: key.componentSha256,
|
|
128
|
+
translatorBuildHash: key.translatorBuildHash,
|
|
129
|
+
features: key.features,
|
|
130
|
+
};
|
|
131
|
+
await Deno.writeTextFile(`${tmp}/meta.json`, JSON.stringify(meta));
|
|
132
|
+
await Deno.writeTextFile(`${tmp}/plan.json`, JSON.stringify(artifacts.plan));
|
|
133
|
+
for (const [file, bytes] of artifacts.adapters) {
|
|
134
|
+
const name = safeRelName(file);
|
|
135
|
+
await Deno.writeFile(`${tmp}/adapters/${name}`, bytes);
|
|
136
|
+
}
|
|
137
|
+
await rmIfExists(dir);
|
|
138
|
+
await Deno.rename(tmp, dir);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
// A `put` failure is non-fatal at the `translateCached` layer
|
|
142
|
+
// (issue #196), but repeated failures must not litter the cache
|
|
143
|
+
// root with orphaned `.tmp-<uuid>` scratch directories. Cleanup
|
|
144
|
+
// failures here are themselves swallowed — `put` still throws its
|
|
145
|
+
// original error either way.
|
|
146
|
+
try {
|
|
147
|
+
await rmIfExists(tmp);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Swallowed: best-effort cleanup only.
|
|
151
|
+
}
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async evict(key) {
|
|
156
|
+
await rmIfExists(await this.entryDir(key));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** A filesystem-backed `ArtifactCache` rooted at `path` (created on first
|
|
160
|
+
* `put` if missing). Deno only: every method calls `Deno.*` directly. On other
|
|
161
|
+
* platforms use `webCache()` (./web.ts), or supply your own `ArtifactCache` —
|
|
162
|
+
* the interface is three methods (./core.ts). */
|
|
163
|
+
export function dirCache(path) {
|
|
164
|
+
if (typeof Deno === "undefined") {
|
|
165
|
+
throw new Error("dirCache() is the Deno filesystem backend and this is not Deno — " +
|
|
166
|
+
"use webCache() (Cache API), or implement ArtifactCache over your " +
|
|
167
|
+
"platform's storage (see @polyengine/runtime/cache core.ts)");
|
|
168
|
+
}
|
|
169
|
+
return new DirCache(path);
|
|
170
|
+
}
|
package/esm/cache/mod.js
ADDED