@polyengine/runtime 0.5.0 → 0.6.0
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/esm/cabi/async_values.js +6 -5
- package/esm/cabi/bulk_lists.js +0 -5
- package/esm/cabi/context.js +13 -3
- package/esm/cabi/flatten.js +41 -9
- package/esm/cabi/handles.js +57 -54
- package/esm/cabi/layout.js +113 -52
- package/esm/cabi/load.js +31 -23
- package/esm/cabi/store.js +33 -26
- package/esm/cabi/trap.js +2 -2
- package/esm/cabi/types.js +138 -25
- package/esm/cabi/values.js +25 -7
- package/esm/cache/core.js +2 -11
- package/esm/digest/digest.js +10 -8
- package/esm/digest/mod.js +1 -1
- package/esm/digest/verify.js +6 -86
- package/esm/embedder/casing.js +24 -9
- package/esm/embedder/copy.js +6 -6
- package/esm/embedder/errors.js +2 -2
- package/esm/embedder/imports.js +3 -3
- package/esm/embedder/instantiate.js +170 -41
- package/esm/embedder/mod.js +9 -8
- package/esm/embedder/resources.js +39 -16
- package/esm/embedder/streams.js +36 -37
- package/esm/embedder/sync.js +242 -0
- package/esm/embedder/values.js +84 -22
- package/esm/embedder/version.js +9 -9
- package/esm/exec/boundary.js +318 -165
- package/esm/exec/executor.js +51 -24
- package/esm/exec/host_streams.js +31 -31
- package/esm/intrinsics/async_builtins.js +19 -8
- package/esm/intrinsics/context.js +1 -1
- package/esm/intrinsics/errors.js +9 -9
- package/esm/intrinsics/fact_calls.js +37 -49
- package/esm/intrinsics/mod.js +54 -117
- package/esm/intrinsics/stream_builtins.js +2 -2
- package/esm/intrinsics/transcode.js +1 -1
- package/esm/jspi/bridge.js +4 -3
- package/esm/jspi/suspending.js +10 -5
- package/esm/plan/loader.js +5 -5
- package/esm/shim/translator.js +2 -2
- package/esm/task/mod.js +45 -182
- package/esm/task/scheduler.js +154 -185
- package/esm/task/streams.js +39 -54
- package/esm/task/subtask.js +2 -2
- package/esm/task/thread.js +20 -41
- package/esm/task/waitable.js +0 -1
- package/package.json +2 -2
- package/types/cabi/async_values.d.ts +3 -2
- package/types/cabi/bulk_lists.d.ts +0 -2
- package/types/cabi/context.d.ts +15 -5
- package/types/cabi/flatten.d.ts +2 -2
- package/types/cabi/handles.d.ts +15 -26
- package/types/cabi/layout.d.ts +22 -1
- package/types/cabi/load.d.ts +10 -2
- package/types/cabi/store.d.ts +4 -2
- package/types/cabi/types.d.ts +22 -3
- package/types/digest/mod.d.ts +1 -1
- package/types/digest/verify.d.ts +3 -19
- package/types/embedder/casing.d.ts +9 -1
- package/types/embedder/copy.d.ts +4 -4
- package/types/embedder/instantiate.d.ts +4 -4
- package/types/embedder/mod.d.ts +3 -2
- package/types/embedder/resources.d.ts +20 -7
- package/types/embedder/streams.d.ts +5 -6
- package/types/embedder/sync.d.ts +81 -0
- package/types/embedder/values.d.ts +2 -2
- package/types/exec/boundary.d.ts +66 -42
- package/types/exec/executor.d.ts +3 -2
- package/types/exec/host_streams.d.ts +8 -8
- package/types/intrinsics/errors.d.ts +3 -3
- package/types/intrinsics/mod.d.ts +1 -1
- package/types/intrinsics/stream_builtins.d.ts +2 -2
- package/types/jspi/bridge.d.ts +6 -5
- package/types/jspi/suspending.d.ts +1 -1
- package/types/plan/format.d.ts +11 -10
- package/types/plan/loader.d.ts +2 -2
- package/types/shim/translator.d.ts +2 -2
- package/types/task/mod.d.ts +26 -97
- package/types/task/scheduler.d.ts +81 -62
- package/types/task/streams.d.ts +23 -38
- package/types/task/subtask.d.ts +2 -2
- package/types/task/waitable.d.ts +0 -1
package/esm/cabi/store.js
CHANGED
|
@@ -3,17 +3,22 @@ import { assert_, trapIf } from "./trap.js";
|
|
|
3
3
|
import { bytesOf, storeInt, storePtr } from "./memory.js";
|
|
4
4
|
import { tryStoreNumericList } from "./bulk_lists.js";
|
|
5
5
|
import { encodeFloatAsI32, encodeFloatAsI64 } from "./float.js";
|
|
6
|
-
import {
|
|
6
|
+
import { alignTo, elemSizeFlags, layoutOf } from "./layout.js";
|
|
7
7
|
import { charToI32, REALLOC_I32_MAX, REALLOC_MISALIGNED, REALLOC_OOB, storeString, } from "./strings.js";
|
|
8
8
|
import { requireMemory } from "./context.js";
|
|
9
9
|
import { lowerBorrow, lowerOwn } from "./handles.js";
|
|
10
|
-
import {
|
|
10
|
+
import { caseIndexOf, } from "./types.js";
|
|
11
11
|
import { lowerErrorContext, lowerFuture, lowerStream, } from "./async_values.js";
|
|
12
12
|
export function store(cx, v, t, ptr) {
|
|
13
13
|
const mem = requireMemory(cx.opts);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// The load-side mirror: one cached layout node carrying the despecialized
|
|
15
|
+
// type as well (issue #261), and `ptr % align === 0` in place of
|
|
16
|
+
// `ptr === alignTo(ptr, align)` — identical for power-of-two alignments,
|
|
17
|
+
// minus the float divide.
|
|
18
|
+
const L = layoutOf(t, mem.ptrType());
|
|
19
|
+
assert_(ptr % L.align === 0, "store misaligned");
|
|
20
|
+
assert_(ptr + L.size <= mem.length, "store OOB");
|
|
21
|
+
const d = L.d;
|
|
17
22
|
switch (d.kind) {
|
|
18
23
|
case "bool":
|
|
19
24
|
storeInt(mem, Number(Boolean(v)), ptr, 1);
|
|
@@ -61,10 +66,12 @@ export function store(cx, v, t, ptr) {
|
|
|
61
66
|
storeList(cx, v, ptr, d.element, d.length ?? null);
|
|
62
67
|
return;
|
|
63
68
|
case "record":
|
|
64
|
-
storeRecord(cx, v, ptr, d.fields);
|
|
69
|
+
storeRecord(cx, v, ptr, d.fields, L.fieldOffsets);
|
|
65
70
|
return;
|
|
66
71
|
case "variant":
|
|
67
|
-
storeVariant(cx, v, ptr, d.cases
|
|
72
|
+
storeVariant(cx, v, ptr, d.cases,
|
|
73
|
+
// 0 only on the kinds without a discriminant; see loadVariant's note.
|
|
74
|
+
L.discSize, L.payloadOffset);
|
|
68
75
|
return;
|
|
69
76
|
case "flags":
|
|
70
77
|
storeFlags(cx, v, ptr, d.labels);
|
|
@@ -96,9 +103,9 @@ export function storeList(cx, v, ptr, elemType, maybeLength) {
|
|
|
96
103
|
}
|
|
97
104
|
export function storeListIntoRange(cx, v, elemType) {
|
|
98
105
|
const mem = requireMemory(cx.opts);
|
|
99
|
-
const
|
|
106
|
+
const { size, align } = layoutOf(elemType, mem.ptrType());
|
|
107
|
+
const byteLength = v.length * size;
|
|
100
108
|
assert_(byteLength <= REALLOC_I32_MAX);
|
|
101
|
-
const align = alignment(elemType, mem.ptrType());
|
|
102
109
|
const ptr = cx.allocate(align, byteLength);
|
|
103
110
|
trapIf(ptr !== alignTo(ptr, align), REALLOC_MISALIGNED);
|
|
104
111
|
trapIf(ptr + byteLength > mem.length, REALLOC_OOB);
|
|
@@ -107,7 +114,8 @@ export function storeListIntoRange(cx, v, elemType) {
|
|
|
107
114
|
}
|
|
108
115
|
export function storeListIntoValidRange(cx, v, ptr, elemType) {
|
|
109
116
|
const mem = requireMemory(cx.opts);
|
|
110
|
-
const
|
|
117
|
+
const L = layoutOf(elemType, mem.ptrType());
|
|
118
|
+
const kind = L.d.kind;
|
|
111
119
|
// docs/architecture.md §7: list<u8> is Uint8Array-shaped on the host, and
|
|
112
120
|
// both directions are bulk copies — this is the store-side mirror of
|
|
113
121
|
// load.ts `loadListFromValidRange`'s u8 fast path (issue #54: the
|
|
@@ -135,18 +143,17 @@ export function storeListIntoValidRange(cx, v, ptr, elemType) {
|
|
|
135
143
|
// platforms.
|
|
136
144
|
if (tryStoreNumericList(mem, v, ptr, kind))
|
|
137
145
|
return;
|
|
138
|
-
const size =
|
|
146
|
+
const size = L.size;
|
|
139
147
|
for (let i = 0; i < v.length; i++) {
|
|
140
148
|
store(cx, v[i], elemType, ptr + i * size);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
for (
|
|
147
|
-
|
|
148
|
-
store(cx, v[f.label], f.type,
|
|
149
|
-
p += elemSize(f.type, mem.ptrType());
|
|
151
|
+
/** The store-side mirror of `loadRecord`: indexed offset writes, no per-field
|
|
152
|
+
* layout recomputation (issue #261). */
|
|
153
|
+
export function storeRecord(cx, v, ptr, fields, offsets) {
|
|
154
|
+
for (let i = 0; i < fields.length; i++) {
|
|
155
|
+
const f = fields[i];
|
|
156
|
+
store(cx, v[f.label], f.type, ptr + offsets[i]);
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
/** definitions.py match_case: the value is a single-key object. */
|
|
@@ -154,20 +161,20 @@ export function matchCase(v, cases) {
|
|
|
154
161
|
const keys = Object.keys(v);
|
|
155
162
|
assert_(keys.length === 1, "variant value must have exactly one case");
|
|
156
163
|
const label = keys[0];
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
// `caseIndexOf` is the memoized form of the linear scan this used to run on
|
|
165
|
+
// every variant stored (issue #261); it maps a duplicated label to -1, so
|
|
166
|
+
// the "exactly one match" condition below is unchanged.
|
|
167
|
+
const i = caseIndexOf(cases).get(label);
|
|
168
|
+
assert_(i !== undefined && i >= 0, `variant case '${label}' not found`);
|
|
169
|
+
return [i, v[label]];
|
|
160
170
|
}
|
|
161
|
-
export function storeVariant(cx, v, ptr, cases) {
|
|
171
|
+
export function storeVariant(cx, v, ptr, cases, discSize, payloadOffset) {
|
|
162
172
|
const mem = requireMemory(cx.opts);
|
|
163
173
|
const [caseIndex, caseValue] = matchCase(v, cases);
|
|
164
|
-
const discSize = elemSize(discriminantType(cases), mem.ptrType());
|
|
165
174
|
storeInt(mem, caseIndex, ptr, discSize);
|
|
166
|
-
let p = ptr + discSize;
|
|
167
|
-
p = alignTo(p, maxCaseAlignment(cases, mem.ptrType()));
|
|
168
175
|
const c = cases[caseIndex];
|
|
169
176
|
if (c.type !== null) {
|
|
170
|
-
store(cx, caseValue, c.type,
|
|
177
|
+
store(cx, caseValue, c.type, ptr + payloadOffset);
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
export function storeFlags(cx, v, ptr, labels) {
|
package/esm/cabi/trap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Trap and assertion machinery (definitions.py `Trap`, `trap`, `trap_if`).
|
|
2
2
|
//
|
|
3
|
-
// `Trap`'s canonical definition lives in `@polyengine/protocol`
|
|
4
|
-
//
|
|
3
|
+
// `Trap`'s canonical definition lives in `@polyengine/protocol`
|
|
4
|
+
// (contracts/embedder-api.md §"Module identity": it is an embedder-contract value and must be recognizable across
|
|
5
5
|
// runtime copies, issue #83); it is re-exported here so every existing
|
|
6
6
|
// `from "../cabi/trap.ts"` import path is unchanged. The protocol package is
|
|
7
7
|
// dependency-free, so this import introduces no cycle.
|
package/esm/cabi/types.js
CHANGED
|
@@ -50,36 +50,90 @@ export class ResourceTypeInfo {
|
|
|
50
50
|
this.dtorHost = dtorHost;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Despecialization memo, keyed on input identity.
|
|
55
|
+
*
|
|
56
|
+
* `ValType` objects are built once per plan by plan/loader.ts `loadValType`
|
|
57
|
+
* and never mutated, so their identity is stable for the plan's lifetime —
|
|
58
|
+
* the same justification embedder/values.ts `checkedLabels` relies on. Issue
|
|
59
|
+
* #261: `despecialize` sits at the top of `load`, `store`, `alignment`,
|
|
60
|
+
* `elemSize`, `lowerFlat` and `contains`, and allocated a fresh record or
|
|
61
|
+
* variant on every one of those calls, per element and per field.
|
|
62
|
+
*
|
|
63
|
+
* The memo hands every caller the SAME object where each previously got a
|
|
64
|
+
* fresh one, so it is sound only as long as no caller mutates the result.
|
|
65
|
+
* That invariant is enforced in HALF the cases and merely relied upon in the
|
|
66
|
+
* other half, and the difference is worth knowing:
|
|
67
|
+
*
|
|
68
|
+
* - The five specialized kinds return an object built here, and everything
|
|
69
|
+
* built here is frozen. These modules are strict-mode, so a later
|
|
70
|
+
* mutation throws instead of silently corrupting every other holder.
|
|
71
|
+
* - The default branch returns the plan-owned `t` unchanged — which for a
|
|
72
|
+
* plan-declared record/variant/list is MOST types in practice — and
|
|
73
|
+
* freezing plan types is out of scope. Nothing enforces immutability
|
|
74
|
+
* there; it rests on plan/loader.ts `loadValType`, which builds every
|
|
75
|
+
* node bottom-up and never writes to one afterwards.
|
|
76
|
+
*
|
|
77
|
+
* Violating that second half is now worse than it used to be. Mutating a plan
|
|
78
|
+
* record's `fields` was previously self-correcting, because every `load`/
|
|
79
|
+
* `store` recomputed the layout from the current array; with layout.ts's
|
|
80
|
+
* cache the mutation instead desynchronizes a `Layout` whose `fieldOffsets`
|
|
81
|
+
* were computed from the pre-mutation shape — wrong bytes at wrong offsets,
|
|
82
|
+
* no error anywhere. A `ValType` reachable from a plan is immutable; treat
|
|
83
|
+
* that as a hard invariant of this layer, not a convention.
|
|
84
|
+
*/
|
|
85
|
+
const despecializedCache = new WeakMap();
|
|
53
86
|
export function despecialize(t) {
|
|
87
|
+
const hit = despecializedCache.get(t);
|
|
88
|
+
if (hit !== undefined)
|
|
89
|
+
return hit;
|
|
90
|
+
const d = despecializeUncached(t);
|
|
91
|
+
// Cached only after the computation returns: a throwing path must leave no
|
|
92
|
+
// entry behind, or the failure would be reported exactly once.
|
|
93
|
+
despecializedCache.set(t, d);
|
|
94
|
+
return d;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* definitions.py `despecialize` (line 1163), line for line. Only the objects
|
|
98
|
+
* built HERE are frozen; the default branch returns the plan-owned `t`
|
|
99
|
+
* unchanged, and freezing plan types is out of scope.
|
|
100
|
+
*/
|
|
101
|
+
function despecializeUncached(t) {
|
|
54
102
|
switch (t.kind) {
|
|
55
103
|
case "tuple":
|
|
56
|
-
return {
|
|
104
|
+
return Object.freeze({
|
|
57
105
|
kind: "record",
|
|
58
|
-
fields: t.elements.map((e, i) => ({ label: String(i), type: e })),
|
|
59
|
-
};
|
|
106
|
+
fields: Object.freeze(t.elements.map((e, i) => Object.freeze({ label: String(i), type: e }))),
|
|
107
|
+
});
|
|
60
108
|
case "enum":
|
|
61
|
-
return {
|
|
109
|
+
return Object.freeze({
|
|
62
110
|
kind: "variant",
|
|
63
|
-
cases: t.labels.map((l) => ({ label: l, type: null })),
|
|
64
|
-
};
|
|
111
|
+
cases: Object.freeze(t.labels.map((l) => Object.freeze({ label: l, type: null }))),
|
|
112
|
+
});
|
|
65
113
|
case "option":
|
|
66
|
-
return {
|
|
114
|
+
return Object.freeze({
|
|
67
115
|
kind: "variant",
|
|
68
|
-
cases: [
|
|
69
|
-
|
|
116
|
+
cases: Object.freeze([
|
|
117
|
+
Object.freeze({ label: "none", type: null }),
|
|
118
|
+
Object.freeze({ label: "some", type: t.type }),
|
|
119
|
+
]),
|
|
120
|
+
});
|
|
70
121
|
case "result":
|
|
71
|
-
return {
|
|
122
|
+
return Object.freeze({
|
|
72
123
|
kind: "variant",
|
|
73
|
-
cases: [
|
|
74
|
-
|
|
124
|
+
cases: Object.freeze([
|
|
125
|
+
Object.freeze({ label: "ok", type: t.ok }),
|
|
126
|
+
Object.freeze({ label: "error", type: t.error }),
|
|
127
|
+
]),
|
|
128
|
+
});
|
|
75
129
|
case "map":
|
|
76
|
-
return {
|
|
130
|
+
return Object.freeze({
|
|
77
131
|
kind: "list",
|
|
78
132
|
element: despecialize({
|
|
79
133
|
kind: "tuple",
|
|
80
134
|
elements: [t.key, t.value],
|
|
81
135
|
}),
|
|
82
|
-
};
|
|
136
|
+
});
|
|
83
137
|
default:
|
|
84
138
|
return t;
|
|
85
139
|
}
|
|
@@ -87,16 +141,78 @@ export function despecialize(t) {
|
|
|
87
141
|
// ---------------------------------------------------------------------------
|
|
88
142
|
// Discriminants (definitions.py `discriminant_type`)
|
|
89
143
|
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
/**
|
|
145
|
+
* The discriminant WIDTH — the single mirror of definitions.py
|
|
146
|
+
* `discriminant_type`'s arithmetic (line 1234), including its `assert(0 < n <
|
|
147
|
+
* (1 << 32))`. `math.ceil(log2(n)/8)`: 0|1 -> u8, 2 -> u16, 3 -> u32.
|
|
148
|
+
*
|
|
149
|
+
* The width is what the layout code actually wanted: on every variant lifted
|
|
150
|
+
* or lowered it used to build a `PrimType` purely to hand it to
|
|
151
|
+
* `alignment`/`elemSize` (issue #261). u8/u16/u32 each have alignment equal to
|
|
152
|
+
* their size, so this one number is both the discriminant's size and its
|
|
153
|
+
* alignment — which is why the variant layout kernels need nothing else.
|
|
154
|
+
*/
|
|
155
|
+
export function discriminantSize(caseCount) {
|
|
156
|
+
const n = caseCount;
|
|
92
157
|
if (!(0 < n && n < 2 ** 32))
|
|
93
158
|
throw new Error("assertion failed: case count");
|
|
94
|
-
// mirrors math.ceil(log2(n)/8): 0|1 -> u8, 2 -> u16, 3 -> u32
|
|
95
159
|
if (n <= 256)
|
|
96
|
-
return
|
|
160
|
+
return 1;
|
|
97
161
|
if (n <= 65536)
|
|
98
|
-
return
|
|
99
|
-
return
|
|
162
|
+
return 2;
|
|
163
|
+
return 4;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Module-level singletons rather than fresh literals per call: `flatten.ts`
|
|
167
|
+
* calls `discriminantType` once per variant flattened and passes the result
|
|
168
|
+
* to `flattenType`, which despecializes it — a fresh object there is one
|
|
169
|
+
* `despecializedCache` insert on immediate garbage every time (the same
|
|
170
|
+
* defeat-the-identity-cache pattern `values.ts` `spillTupleType` avoids).
|
|
171
|
+
* Frozen for the reason the despecialized nodes are: they are now shared, and
|
|
172
|
+
* the engine is a better guarantor of "nobody mutates this" than an audit.
|
|
173
|
+
*/
|
|
174
|
+
const U8 = Object.freeze({ kind: "u8" });
|
|
175
|
+
const U16 = Object.freeze({ kind: "u16" });
|
|
176
|
+
const U32 = Object.freeze({ kind: "u32" });
|
|
177
|
+
/**
|
|
178
|
+
* definitions.py `discriminant_type`. The table itself lives in
|
|
179
|
+
* `discriminantSize`; this is only the width -> type mapping, so the bound
|
|
180
|
+
* check, the thresholds and the error string exist once. Flattening
|
|
181
|
+
* (flatten.ts:157) and layout (layout.ts `alignmentVariant`,
|
|
182
|
+
* `elemSizeVariant`) therefore cannot drift apart on the discriminant width.
|
|
183
|
+
*/
|
|
184
|
+
export function discriminantType(cases) {
|
|
185
|
+
switch (discriminantSize(cases.length)) {
|
|
186
|
+
case 1:
|
|
187
|
+
return U8;
|
|
188
|
+
case 2:
|
|
189
|
+
return U16;
|
|
190
|
+
case 4:
|
|
191
|
+
return U32;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Label -> case index for a variant's `cases`, memoized on the array's
|
|
196
|
+
* identity (plan-owned and stable, same argument as the despecialization
|
|
197
|
+
* memo). Replaces the linear scan `matchCase` ran on every variant stored
|
|
198
|
+
* (issue #261).
|
|
199
|
+
*
|
|
200
|
+
* A label bound to more than one case maps to -1, because the scan this
|
|
201
|
+
* replaces asserted on finding exactly ONE match: collapsing duplicates to
|
|
202
|
+
* "last wins" would silently accept a variant the old code rejected.
|
|
203
|
+
*/
|
|
204
|
+
const caseIndexCache = new WeakMap();
|
|
205
|
+
export function caseIndexOf(cases) {
|
|
206
|
+
const hit = caseIndexCache.get(cases);
|
|
207
|
+
if (hit !== undefined)
|
|
208
|
+
return hit;
|
|
209
|
+
const m = new Map();
|
|
210
|
+
for (let i = 0; i < cases.length; i++) {
|
|
211
|
+
const label = cases[i].label;
|
|
212
|
+
m.set(label, m.has(label) ? -1 : i);
|
|
213
|
+
}
|
|
214
|
+
caseIndexCache.set(cases, m);
|
|
215
|
+
return m;
|
|
100
216
|
}
|
|
101
217
|
// ---------------------------------------------------------------------------
|
|
102
218
|
// Type predicates (definitions.py `contains_borrow` etc.)
|
|
@@ -104,9 +220,6 @@ export function discriminantType(cases) {
|
|
|
104
220
|
export function containsBorrow(t) {
|
|
105
221
|
return contains(t, (u) => u.kind === "borrow");
|
|
106
222
|
}
|
|
107
|
-
export function containsAsyncValue(t) {
|
|
108
|
-
return contains(t, (u) => u.kind === "stream" || u.kind === "future");
|
|
109
|
-
}
|
|
110
223
|
export function contains(t, p) {
|
|
111
224
|
if (t === null)
|
|
112
225
|
return false;
|
|
@@ -131,7 +244,7 @@ export function contains(t, p) {
|
|
|
131
244
|
/**
|
|
132
245
|
* Structural `ValType` equality.
|
|
133
246
|
*
|
|
134
|
-
* CONTRACT (bugfix,
|
|
247
|
+
* CONTRACT (bugfix, generalized during the #18 tls smoke): naive
|
|
135
248
|
* `JSON.stringify(a) === JSON.stringify(b)` recurses into `own`/`borrow`'s
|
|
136
249
|
* `ResourceTypeInfo` — a class whose `impl` field is documented "Compared by
|
|
137
250
|
* object identity everywhere" (see `ResourceTypeInfo` above) and which cycles
|
|
@@ -139,7 +252,7 @@ export function contains(t, p) {
|
|
|
139
252
|
* tables that reference their types), so `JSON.stringify` throws
|
|
140
253
|
* `TypeError: Converting circular structure to JSON` on ANY type containing
|
|
141
254
|
* `own<R>`/`borrow<R>` at any depth. First hit by `task.return` result types
|
|
142
|
-
* (
|
|
255
|
+
* (polymorph-test's `list<own<test-case>>`), then by stream/future
|
|
143
256
|
* element types (polymorph-tls streams carrying resource-bearing payloads).
|
|
144
257
|
* Object-identity types (`ResourceTypeInfo`) are compared by reference, per
|
|
145
258
|
* the documented invariant.
|
package/esm/cabi/values.js
CHANGED
|
@@ -7,15 +7,34 @@ import { load } from "./load.js";
|
|
|
7
7
|
import { store } from "./store.js";
|
|
8
8
|
import { liftFlat } from "./lift.js";
|
|
9
9
|
import { lowerFlat } from "./lower.js";
|
|
10
|
-
import {
|
|
10
|
+
import { flatCount } from "./flatten.js";
|
|
11
11
|
import { requireMemory } from "./context.js";
|
|
12
12
|
import { asIndex } from "./memory.js";
|
|
13
|
+
/**
|
|
14
|
+
* The spill tuple for a parameter/result list, memoized on the array's
|
|
15
|
+
* identity.
|
|
16
|
+
*
|
|
17
|
+
* Both spill paths used to synthesize `{ kind: "tuple", elements: ts }` on
|
|
18
|
+
* every call, which under the identity-keyed layout/despecialization caches
|
|
19
|
+
* (issue #261) is a guaranteed miss plus a wasted insert — it would make this
|
|
20
|
+
* path slower, not faster. `ts` is always a plan-owned `ft.params`/
|
|
21
|
+
* `ft.results` array (exec/boundary.ts, intrinsics/async_builtins.ts), so its
|
|
22
|
+
* identity is as stable as a `ValType`'s.
|
|
23
|
+
*/
|
|
24
|
+
const spillTuples = new WeakMap();
|
|
25
|
+
function spillTupleType(ts) {
|
|
26
|
+
const hit = spillTuples.get(ts);
|
|
27
|
+
if (hit !== undefined)
|
|
28
|
+
return hit;
|
|
29
|
+
const t = { kind: "tuple", elements: ts };
|
|
30
|
+
spillTuples.set(ts, t);
|
|
31
|
+
return t;
|
|
32
|
+
}
|
|
13
33
|
export function liftFlatValues(cx, maxFlat, vi, ts) {
|
|
14
|
-
|
|
15
|
-
if (flatTypes.length > maxFlat) {
|
|
34
|
+
if (flatCount(ts, cx.opts) > maxFlat) {
|
|
16
35
|
const mem = requireMemory(cx.opts);
|
|
17
36
|
const ptrRaw = vi.next(mem.ptrType());
|
|
18
|
-
const tupleType =
|
|
37
|
+
const tupleType = spillTupleType(ts);
|
|
19
38
|
const align = alignment(tupleType, mem.ptrType());
|
|
20
39
|
const size = elemSize(tupleType, mem.ptrType());
|
|
21
40
|
trapIf(BigInt(ptrRaw) % BigInt(align) !== 0n, "misaligned spill pointer");
|
|
@@ -29,10 +48,9 @@ export function liftFlatValues(cx, maxFlat, vi, ts) {
|
|
|
29
48
|
}
|
|
30
49
|
}
|
|
31
50
|
export function lowerFlatValues(cx, maxFlat, vs, ts, outParam = null) {
|
|
32
|
-
|
|
33
|
-
if (flatTypes.length > maxFlat) {
|
|
51
|
+
if (flatCount(ts, cx.opts) > maxFlat) {
|
|
34
52
|
const mem = requireMemory(cx.opts);
|
|
35
|
-
const tupleType =
|
|
53
|
+
const tupleType = spillTupleType(ts);
|
|
36
54
|
const tupleValue = {};
|
|
37
55
|
vs.forEach((v, i) => {
|
|
38
56
|
tupleValue[String(i)] = v;
|
package/esm/cache/core.js
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
// time (recorded in the stored metadata and cross-checked against the
|
|
50
50
|
// embedded `plan.component.sha256`) — never against fresh bytes, which
|
|
51
51
|
// this layer never sees.
|
|
52
|
-
import { loadEnvelope, PlanError
|
|
52
|
+
import { loadEnvelope, PlanError } from "../plan/loader.js";
|
|
53
53
|
/** Cache layout version. Bumped on any incompatible on-disk/on-Cache-API
|
|
54
54
|
* schema change; an unrecognized version is read back as a miss (never a
|
|
55
55
|
* crash) so stale caches from an older build self-heal by re-translating —
|
|
@@ -135,16 +135,7 @@ export async function translateCached(translator, componentBytes, cache, opts =
|
|
|
135
135
|
// verdict — must propagate uncached, per TranslateError's docs: a
|
|
136
136
|
// validation verdict is a judgment about the *input component*, not
|
|
137
137
|
// something to cache-and-replay) and gives us the split plan/adapters.
|
|
138
|
-
|
|
139
|
-
let adapters;
|
|
140
|
-
try {
|
|
141
|
-
({ wire, adapters } = loadEnvelope(first));
|
|
142
|
-
}
|
|
143
|
-
catch (e) {
|
|
144
|
-
if (e instanceof TranslateError)
|
|
145
|
-
throw e;
|
|
146
|
-
throw e;
|
|
147
|
-
}
|
|
138
|
+
const { wire, adapters } = loadEnvelope(first);
|
|
148
139
|
// A `put` failure (issue #196) is swallowed: the translation already
|
|
149
140
|
// succeeded and was already validated above by `loadEnvelope` — failing
|
|
150
141
|
// to *store* it says nothing about the result. Return the fresh
|
package/esm/digest/digest.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// must produce byte-identical canonical JSON (and therefore identical
|
|
6
6
|
// sha256) for a structurally-equivalent world — this equality IS the
|
|
7
7
|
// design validation (see runtime/tests/digest_test.ts's cross-language
|
|
8
|
-
// fixture test).
|
|
9
|
-
// (
|
|
8
|
+
// fixture test). The wire plan's own `worldDigest` field is legacy
|
|
9
|
+
// (contracts/plan-format.md), retained for wire compatibility only; this
|
|
10
|
+
// module computes the normative digest independently.
|
|
10
11
|
//
|
|
11
12
|
// THE NORMALIZATION SPEC IS DOCUMENTED ONCE, in crates/bindgen/src/digest.rs's
|
|
12
13
|
// module doc comment (kept in sync with this file) — read it first. Short
|
|
@@ -72,8 +73,8 @@ function hex(buf) {
|
|
|
72
73
|
// Resource naming (plan resourceTables index -> qualified name)
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
function buildResourceNameMap(plan) {
|
|
75
|
-
// CONTRACT: plan-format.md
|
|
76
|
-
//
|
|
76
|
+
// CONTRACT: the `importedResources` field (contracts/plan-format.md
|
|
77
|
+
// schema; format.ts:23-33). An imported resource occupies
|
|
77
78
|
// `ResourceIndex` slots *before* every defined (own/exported) resource
|
|
78
79
|
// (`ResourceIndex = importedResources.length + DefinedResourceIndex`), and
|
|
79
80
|
// this implementation has no alias map from those imported-resource
|
|
@@ -134,8 +135,9 @@ function walkExportsForResourceNames(exports, path, out) {
|
|
|
134
135
|
// ---------------------------------------------------------------------------
|
|
135
136
|
function canonImport(plan, imp, resourceNames) {
|
|
136
137
|
// CONTRACT: the plan's `imports` list is flat (`{name, path, kind, type}`)
|
|
137
|
-
// even for interface-qualified imports (
|
|
138
|
-
// "Untested: current corpus has no
|
|
138
|
+
// even for interface-qualified imports (`imports[].path`,
|
|
139
|
+
// contracts/plan-format.md schema — "Untested: current corpus has no
|
|
140
|
+
// imports"); no fixture in this repo's
|
|
139
141
|
// sync corpus (hello/values/resources) has any imports, so this path is
|
|
140
142
|
// exercised by no test. Best-effort flattened-name treatment, chosen to
|
|
141
143
|
// be structurally analogous to the export side's nested naming without
|
|
@@ -174,8 +176,8 @@ function canonExportItem(plan, exp, resourceNames) {
|
|
|
174
176
|
// digest.md's item rule: only functions and resources contribute as
|
|
175
177
|
// export items. A module export is not WIT-expressible (bindgen can
|
|
176
178
|
// never emit a digest containing one) and does not affect
|
|
177
|
-
// positional-calling ABI shape, so it is excluded —
|
|
178
|
-
//
|
|
179
|
+
// positional-calling ABI shape, so it is excluded — the `module` export
|
|
180
|
+
// kind (contracts/plan-format.md schema notes).
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
183
|
// exp.kind === "type"
|
package/esm/digest/mod.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* **This is not embedder API.** The entry point exists as a support surface
|
|
7
7
|
* for bindgen-generated bindings — whose typed `instantiate` wrapper
|
|
8
8
|
* verifies the digest before instantiating (contracts/embedder-api.md
|
|
9
|
-
*
|
|
9
|
+
* §"Module wiring and instantiation") — and for the runtime's own internals. No host program
|
|
10
10
|
* should hand-write an import of this module.
|
|
11
11
|
*
|
|
12
12
|
* **Its contents are completely unstable: there is no compatibility promise
|
package/esm/digest/verify.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// Runtime handshake (docs/architecture.md §9): verify a loaded plan's world
|
|
2
|
-
// digest embedded by generated bindgen code,
|
|
3
|
-
//
|
|
4
|
-
// than just "digests differ".
|
|
1
|
+
// Runtime handshake (docs/architecture.md §9): verify a loaded plan's world
|
|
2
|
+
// against the digest embedded by generated bindgen code, failing fast with
|
|
3
|
+
// the expected/actual digests on mismatch (contracts/digest.md).
|
|
5
4
|
import { computeWorldDigest } from "./digest.js";
|
|
6
5
|
/**
|
|
7
6
|
* Thrown by generated `instantiate` wrappers when the loaded plan's world
|
|
8
7
|
* digest does not match the constant bindgen embedded at generation time
|
|
9
|
-
* (contracts/digest.md: "fails fast
|
|
8
|
+
* (contracts/digest.md: "fails fast on mismatch").
|
|
10
9
|
* Raised BEFORE the component is instantiated, so no guest code has run
|
|
11
10
|
* when a caller catches this.
|
|
12
11
|
*
|
|
@@ -18,14 +17,11 @@ export class WorldDigestMismatchError extends Error {
|
|
|
18
17
|
name = "WorldDigestMismatchError";
|
|
19
18
|
/** The world these bindings were generated from. */
|
|
20
19
|
world;
|
|
21
|
-
/** The full mismatch report (expected/actual digest
|
|
20
|
+
/** The full mismatch report (expected/actual digest). */
|
|
22
21
|
mismatch;
|
|
23
22
|
constructor(world, mismatch) {
|
|
24
23
|
super(`world digest mismatch for \`${world}\`: bindings expect ` +
|
|
25
24
|
`${mismatch.expected}, loaded plan computes ${mismatch.actual}` +
|
|
26
|
-
(mismatch.firstDivergence
|
|
27
|
-
? ` (first divergence: ${mismatch.firstDivergence})`
|
|
28
|
-
: "") +
|
|
29
25
|
" — regenerate the bindings from the component's WIT");
|
|
30
26
|
this.world = world;
|
|
31
27
|
this.mismatch = mismatch;
|
|
@@ -42,8 +38,7 @@ export class WorldDigestMismatchError extends Error {
|
|
|
42
38
|
/**
|
|
43
39
|
* Verify `plan`'s computed world digest against `expectedDigest` (the
|
|
44
40
|
* constant bindgen embedded at generation time). Returns `null` on match,
|
|
45
|
-
* or a `DigestMismatch` report
|
|
46
|
-
* mismatch.
|
|
41
|
+
* or a `DigestMismatch` report on mismatch.
|
|
47
42
|
* @internal
|
|
48
43
|
*/
|
|
49
44
|
export async function verifyWorldDigest(plan, expectedDigest) {
|
|
@@ -53,80 +48,5 @@ export async function verifyWorldDigest(plan, expectedDigest) {
|
|
|
53
48
|
return {
|
|
54
49
|
expected: expectedDigest,
|
|
55
50
|
actual: actual.digest,
|
|
56
|
-
firstDivergence: null, // filled in by compareAgainstExpectedJson if available
|
|
57
51
|
};
|
|
58
52
|
}
|
|
59
|
-
/**
|
|
60
|
-
* `diffWorldDigest` — richer variant for tests/tooling: compare against
|
|
61
|
-
* another plan's (or a WIT-derived) canonical JSON directly, walking both
|
|
62
|
-
* trees in parallel to name the first divergent import/export/type path.
|
|
63
|
-
* `expectedCanonicalJson` is normally produced by `crates/bindgen`'s
|
|
64
|
-
* `digest --json` output, or by `computeWorldDigest` on a reference plan.
|
|
65
|
-
* @internal
|
|
66
|
-
*/
|
|
67
|
-
export async function diffWorldDigest(plan, expectedCanonicalJson) {
|
|
68
|
-
const actual = await computeWorldDigest(plan);
|
|
69
|
-
const expected = JSON.parse(expectedCanonicalJson);
|
|
70
|
-
const expectedDigestBytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(expectedCanonicalJson));
|
|
71
|
-
const expectedDigest = "sha256:" +
|
|
72
|
-
Array.from(new Uint8Array(expectedDigestBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
73
|
-
if (actual.digest === expectedDigest)
|
|
74
|
-
return null;
|
|
75
|
-
const actualParsed = JSON.parse(actual.canonicalJson);
|
|
76
|
-
return {
|
|
77
|
-
expected: expectedDigest,
|
|
78
|
-
actual: actual.digest,
|
|
79
|
-
firstDivergence: firstDivergentPath(expected, actualParsed, "$"),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Walk two canonical world trees in parallel (both already sorted by name
|
|
84
|
-
* at every `imports`/`exports`/`items` level — see digest.ts), returning a
|
|
85
|
-
* human-readable path to the first field that differs, or `null` if the
|
|
86
|
-
* trees are structurally identical (shouldn't happen if the digests
|
|
87
|
-
* differ, but the digest is a hash — collisions or caller error are
|
|
88
|
-
* possible, so this is a defensive `null`, not a promise of "same").
|
|
89
|
-
*/
|
|
90
|
-
function firstDivergentPath(expected, actual, path) {
|
|
91
|
-
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
92
|
-
if (expected.length !== actual.length) {
|
|
93
|
-
return `${path}: length ${expected.length} (expected) vs ${actual.length} (actual)`;
|
|
94
|
-
}
|
|
95
|
-
for (let i = 0; i < expected.length; i++) {
|
|
96
|
-
const label = itemLabel(expected[i]) ?? `[${i}]`;
|
|
97
|
-
const d = firstDivergentPath(expected[i], actual[i], `${path}.${label}`);
|
|
98
|
-
if (d)
|
|
99
|
-
return d;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
if (expected !== null && actual !== null &&
|
|
104
|
-
typeof expected === "object" && typeof actual === "object" &&
|
|
105
|
-
!Array.isArray(expected) && !Array.isArray(actual)) {
|
|
106
|
-
const e = expected;
|
|
107
|
-
const a = actual;
|
|
108
|
-
const keys = new Set([...Object.keys(e), ...Object.keys(a)]);
|
|
109
|
-
for (const k of [...keys].sort()) {
|
|
110
|
-
if (!(k in a))
|
|
111
|
-
return `${path}.${k}: present (expected) but missing (actual)`;
|
|
112
|
-
if (!(k in e))
|
|
113
|
-
return `${path}.${k}: missing (expected) but present (actual)`;
|
|
114
|
-
const d = firstDivergentPath(e[k], a[k], `${path}.${k}`);
|
|
115
|
-
if (d)
|
|
116
|
-
return d;
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
if (expected !== actual) {
|
|
121
|
-
return `${path}: ${JSON.stringify(expected)} (expected) vs ${JSON.stringify(actual)} (actual)`;
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function itemLabel(v) {
|
|
126
|
-
if (v !== null && typeof v === "object" && "name" in v) {
|
|
127
|
-
const name = v.name;
|
|
128
|
-
if (typeof name === "string")
|
|
129
|
-
return JSON.stringify(name);
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|