@polyengine/runtime 0.5.1 → 0.6.1

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.
Files changed (81) hide show
  1. package/esm/cabi/async_values.js +6 -5
  2. package/esm/cabi/bulk_lists.js +0 -5
  3. package/esm/cabi/context.js +13 -3
  4. package/esm/cabi/flatten.js +41 -9
  5. package/esm/cabi/handles.js +57 -54
  6. package/esm/cabi/layout.js +113 -52
  7. package/esm/cabi/load.js +31 -23
  8. package/esm/cabi/store.js +33 -26
  9. package/esm/cabi/trap.js +2 -2
  10. package/esm/cabi/types.js +138 -25
  11. package/esm/cabi/values.js +25 -7
  12. package/esm/cache/core.js +2 -11
  13. package/esm/digest/digest.js +10 -8
  14. package/esm/digest/mod.js +1 -1
  15. package/esm/digest/verify.js +6 -86
  16. package/esm/embedder/casing.js +24 -9
  17. package/esm/embedder/copy.js +6 -6
  18. package/esm/embedder/errors.js +2 -2
  19. package/esm/embedder/imports.js +3 -3
  20. package/esm/embedder/instantiate.js +132 -37
  21. package/esm/embedder/mod.js +9 -8
  22. package/esm/embedder/resources.js +39 -16
  23. package/esm/embedder/streams.js +36 -37
  24. package/esm/embedder/sync.js +242 -0
  25. package/esm/embedder/values.js +84 -22
  26. package/esm/embedder/version.js +9 -9
  27. package/esm/exec/boundary.js +123 -161
  28. package/esm/exec/executor.js +37 -25
  29. package/esm/exec/host_streams.js +31 -31
  30. package/esm/intrinsics/async_builtins.js +15 -7
  31. package/esm/intrinsics/context.js +1 -1
  32. package/esm/intrinsics/errors.js +9 -9
  33. package/esm/intrinsics/fact_calls.js +37 -49
  34. package/esm/intrinsics/mod.js +54 -117
  35. package/esm/intrinsics/stream_builtins.js +2 -2
  36. package/esm/intrinsics/transcode.js +1 -1
  37. package/esm/jspi/bridge.js +4 -3
  38. package/esm/jspi/suspending.js +5 -5
  39. package/esm/plan/loader.js +5 -5
  40. package/esm/shim/translator.js +2 -2
  41. package/esm/task/mod.js +45 -182
  42. package/esm/task/scheduler.js +154 -185
  43. package/esm/task/streams.js +39 -54
  44. package/esm/task/subtask.js +2 -2
  45. package/esm/task/thread.js +20 -41
  46. package/esm/task/waitable.js +0 -1
  47. package/package.json +2 -2
  48. package/types/cabi/async_values.d.ts +3 -2
  49. package/types/cabi/bulk_lists.d.ts +0 -2
  50. package/types/cabi/context.d.ts +15 -5
  51. package/types/cabi/flatten.d.ts +2 -2
  52. package/types/cabi/handles.d.ts +15 -26
  53. package/types/cabi/layout.d.ts +22 -1
  54. package/types/cabi/load.d.ts +10 -2
  55. package/types/cabi/store.d.ts +4 -2
  56. package/types/cabi/types.d.ts +22 -3
  57. package/types/digest/mod.d.ts +1 -1
  58. package/types/digest/verify.d.ts +3 -19
  59. package/types/embedder/casing.d.ts +9 -1
  60. package/types/embedder/copy.d.ts +4 -4
  61. package/types/embedder/instantiate.d.ts +4 -4
  62. package/types/embedder/mod.d.ts +3 -2
  63. package/types/embedder/resources.d.ts +20 -7
  64. package/types/embedder/streams.d.ts +5 -6
  65. package/types/embedder/sync.d.ts +81 -0
  66. package/types/embedder/values.d.ts +2 -2
  67. package/types/exec/boundary.d.ts +55 -44
  68. package/types/exec/executor.d.ts +3 -2
  69. package/types/exec/host_streams.d.ts +8 -8
  70. package/types/intrinsics/errors.d.ts +3 -3
  71. package/types/intrinsics/mod.d.ts +1 -1
  72. package/types/intrinsics/stream_builtins.d.ts +2 -2
  73. package/types/jspi/bridge.d.ts +6 -5
  74. package/types/plan/format.d.ts +11 -10
  75. package/types/plan/loader.d.ts +2 -2
  76. package/types/shim/translator.d.ts +2 -2
  77. package/types/task/mod.d.ts +26 -97
  78. package/types/task/scheduler.d.ts +81 -62
  79. package/types/task/streams.d.ts +23 -38
  80. package/types/task/subtask.d.ts +2 -2
  81. 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 { alignment, alignTo, elemSize, elemSizeFlags, maxCaseAlignment, } from "./layout.js";
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 { despecialize, discriminantType, } from "./types.js";
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
- assert_(ptr === alignTo(ptr, alignment(t, mem.ptrType())), "store misaligned");
15
- assert_(ptr + elemSize(t, mem.ptrType()) <= mem.length, "store OOB");
16
- const d = despecialize(t);
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 byteLength = v.length * elemSize(elemType, mem.ptrType());
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 kind = despecialize(elemType).kind;
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 = elemSize(elemType, mem.ptrType());
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
- export function storeRecord(cx, v, ptr, fields) {
144
- const mem = requireMemory(cx.opts);
145
- let p = ptr;
146
- for (const f of fields) {
147
- p = alignTo(p, alignment(f.type, mem.ptrType()));
148
- store(cx, v[f.label], f.type, p);
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
- const matches = cases.flatMap((c, i) => (c.label === label ? [i] : []));
158
- assert_(matches.length === 1, `variant case '${label}' not found`);
159
- return [matches[0], v[label]];
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, p);
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` since amendment
4
- // A9 (it is an embedder-contract value and must be recognizable across
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: [{ label: "none", type: null }, { label: "some", type: t.type }],
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: [{ label: "ok", type: t.ok }, { label: "error", type: t.error }],
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
- export function discriminantType(cases) {
91
- const n = cases.length;
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 { kind: "u8" };
160
+ return 1;
97
161
  if (n <= 65536)
98
- return { kind: "u16" };
99
- return { kind: "u32" };
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, TRACK C2-D; generalized during the #18 tls smoke): naive
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
- * (C2-D, polymorph-test's `list<own<test-case>>`), then by stream/future
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.
@@ -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 { flattenTypes } from "./flatten.js";
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
- const flatTypes = flattenTypes(ts, cx.opts);
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 = { kind: "tuple", elements: ts };
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
- const flatTypes = flattenTypes(ts, cx.opts);
33
- if (flatTypes.length > maxFlat) {
51
+ if (flatCount(ts, cx.opts) > maxFlat) {
34
52
  const mem = requireMemory(cx.opts);
35
- const tupleType = { kind: "tuple", elements: ts };
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, TranslateError } from "../plan/loader.js";
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
- let wire;
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
@@ -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). This resolves contracts/plan-format.md v0.1 amendment #7
9
- // ("worldDigest needs redesign before M1 bindgen").
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 v0.1 amendment #2 / v0.2 proposal
76
- // `importedResources` (format.ts:23-33). An imported resource occupies
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 (plan-format.md v0.1 amendment #4,
138
- // "Untested: current corpus has no imports"); no fixture in this repo's
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 — plan-format.md v4
178
- // amendment 2.
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
- * amendment A17) — and for the runtime's own internals. No host program
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
@@ -1,12 +1,11 @@
1
- // Runtime handshake (docs/architecture.md §9): verify a loaded plan's world against the
2
- // digest embedded by generated bindgen code, producing a rich mismatch
3
- // report that names the first divergent import/export/type path rather
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 with a structural diff on mismatch").
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, first divergence). */
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 naming the first divergent path on
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
- }