@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
@@ -26,8 +26,9 @@ import { assert_, trapIf } from "./trap.js";
26
26
  import { contains, fmtValType } from "./types.js";
27
27
  import { CopyState, ErrorContext, ReadableFutureEnd, ReadableStreamEnd, SharedFutureImpl, sameElemType, SharedStreamImpl, } from "../task/streams.js";
28
28
  /**
29
- * Diagnostic for a handle-table entry that carries the A9 error-context brand
30
- * without being one of THIS copy's `ErrorContext`s (amendment A9, issue #83).
29
+ * Diagnostic for a handle-table entry that carries the error-context brand
30
+ * without being one of THIS copy's `ErrorContext`s (contracts/embedder-api.md
31
+ * §"Module identity").
31
32
  *
32
33
  * A backstop, deliberately: the embedder's lowering site (embedder/values.ts)
33
34
  * refuses a foreign error-context before it can ever reach a handle table, so
@@ -46,7 +47,7 @@ export function errorContextTrapMessage(where, e) {
46
47
  return `${where}: this error-context was minted by a DIFFERENT polyengine ` +
47
48
  `runtime copy and cannot be used through this one` +
48
49
  `${census === "" ? "" : ` (${census})`} ` +
49
- `(contracts/embedder-api.md amendment A9, issue #83)`;
50
+ `(contracts/embedder-api.md §"Module identity")`;
50
51
  }
51
52
  /** definitions.py `contains_borrow` — async values may never carry borrows. */
52
53
  function containsBorrow(t) {
@@ -81,7 +82,7 @@ EndT, elem, what) {
81
82
  const holder = end.shared;
82
83
  const store = inst.store;
83
84
  if (holder.boundStore != null && store != null) {
84
- // A9: when several runtime copies are loaded, "a second store" is very
85
+ // module identity: when several runtime copies are loaded, "a second store" is very
85
86
  // often "a second COPY" — the shared object was minted by one runtime and
86
87
  // is being driven by another. The two stores are indistinguishable from
87
88
  // here (stores carry no copy identity), so the census is appended as the
@@ -94,7 +95,7 @@ EndT, elem, what) {
94
95
  `another)`));
95
96
  }
96
97
  holder.boundStore ??= store;
97
- // Host-wrapper re-arm hook (#162, embedder-api amendment A15): the readable
98
+ // Host-wrapper re-arm hook (#162, contracts/embedder-api.md §"Streams and futures"): the readable
98
99
  // end just left a guest table, so whoever receives it can act on it again.
99
100
  // See `bindOnLower` in exec/host_streams.ts for the retention rule.
100
101
  end.shared.onLifted?.(inst);
@@ -67,11 +67,6 @@ const FLOAT_CTORS = {
67
67
  f32: Float32Array,
68
68
  f64: Float64Array,
69
69
  };
70
- /** Kinds these helpers handle (besides them, u8 has its own path). */
71
- export function isBulkListKind(kind) {
72
- return kind === "bool" || kind in INT_CTORS || kind in BIG_CTORS ||
73
- kind in FLOAT_CTORS;
74
- }
75
70
  function viewOf(ctor, mem, ptr, length) {
76
71
  if (!PLATFORM_LITTLE_ENDIAN)
77
72
  return null;
@@ -16,6 +16,17 @@ export function requireMemory(opts) {
16
16
  assert_(opts.memory !== null, "canonical option `memory` required");
17
17
  return opts.memory;
18
18
  }
19
+ /**
20
+ * Brand marking a value as a REAL component instance (task/mod.ts
21
+ * `ComponentInstanceState`), as opposed to the many structural
22
+ * `ComponentInstanceLike` stand-ins — imported/host resources carry no
23
+ * instance at all, and test harnesses supply bare `{handles, mayLeave}`
24
+ * doubles. cabi must not depend on task/, so the symbol lives here and
25
+ * `ComponentInstanceState` declares it; cabi/handles.ts `isComponentInstance`
26
+ * is the only reader. `ComponentInstanceLike` stays deliberately structural:
27
+ * the brand is NOT part of it.
28
+ */
29
+ export const COMPONENT_INSTANCE = Symbol("polyengine.ComponentInstance");
19
30
  export class LiftLowerContext {
20
31
  opts;
21
32
  inst;
@@ -30,9 +41,8 @@ export class LiftLowerContext {
30
41
  * with `may_leave` cleared, so a realloc that lowers an import traps
31
42
  * (`canon_lower`'s `trap_if(not ...may_leave)`, implemented here by
32
43
  * exec/boundary.ts `createLoweredImport`). That bracket is implemented
33
- * below. What remains deferred is only the reference's routing of the call
34
- * through `canon_lift`; upstream component-model PR #705 removes that
35
- * routing, leaving this bracket as the whole story. polyengine issue #147.
44
+ * below, and it is the whole story: the pinned reference does not route
45
+ * the call through `canon_lift` (CM#705). polyengine issue #147.
36
46
  */
37
47
  reallocate(old, oldByteLength, alignment, newByteLength) {
38
48
  const realloc = this.opts.realloc;
@@ -4,15 +4,7 @@ import { despecialize, discriminantType, } from "./types.js";
4
4
  import { requireMemory } from "./context.js";
5
5
  export const MAX_FLAT_PARAMS = 16;
6
6
  export const MAX_FLAT_ASYNC_PARAMS = 4;
7
- // Mutable to mirror run_tests.py toggling definitions.MAX_FLAT_RESULTS.
8
- export let MAX_FLAT_RESULTS = 1;
9
- // Test-only mirror of run_tests.py's constant toggling; not for production
10
- // use (naming convention: see `schedulerSeedForTesting`).
11
- export function setMaxFlatResultsForTesting(n) {
12
- const prev = MAX_FLAT_RESULTS;
13
- MAX_FLAT_RESULTS = n;
14
- return prev;
15
- }
7
+ export const MAX_FLAT_RESULTS = 1;
16
8
  export function flattenFunctype(opts, ft, context) {
17
9
  let flatParams = flattenTypes(ft.params, opts);
18
10
  let flatResults = flattenTypes(ft.results, opts);
@@ -62,6 +54,46 @@ export function flattenFunctype(opts, ft, context) {
62
54
  export function flattenTypes(ts, opts) {
63
55
  return ts.flatMap((t) => flattenType(t, opts));
64
56
  }
57
+ /**
58
+ * `flattenTypes(ts, opts).length`, memoized on (ts array identity, ptrType).
59
+ *
60
+ * Every arm of `flattenType` reaches `opts` only through
61
+ * `requireMemory(opts).ptrType()` (string/list-without-length read the
62
+ * pointer width; every other arm is opts-free or recurses structurally), so
63
+ * the flattened element count is a pure function of `(ts, ptrType)` — one
64
+ * map per pointer width is the whole of the cache key. `ts` is always a
65
+ * plan-owned `ft.params`/`ft.results` array, the same stability argument
66
+ * `spillTupleType` (values.ts) relies on, so its identity is a valid key.
67
+ *
68
+ * This caches the COUNT, not the flattened array: `values.ts`'s only use of
69
+ * `flattenTypes` on the per-call path is `.length`, and a cached number has
70
+ * no aliasing/mutation hazard to guard (a cached array would need freezing
71
+ * plus a lossy `readonly`-to-mutable cast at every read site — the shape
72
+ * this replaced). Callers that need the actual flat types (instantiate-time
73
+ * `flattenFunctype`) still call `flattenTypes` directly, uncached; that path
74
+ * runs once per function, not once per call, so it doesn't need this.
75
+ *
76
+ * The null-memory path is deliberately NOT cached: `requireMemory` throws
77
+ * when `opts.memory` is null (for any `ts` containing a string/unbounded
78
+ * list), and that throw must still surface on every call, not just the
79
+ * first.
80
+ */
81
+ const flatCountCacheByPtrType = {
82
+ i32: new WeakMap(),
83
+ i64: new WeakMap(),
84
+ };
85
+ export function flatCount(ts, opts) {
86
+ const mem = opts.memory;
87
+ if (mem === null)
88
+ return flattenTypes(ts, opts).length;
89
+ const cache = flatCountCacheByPtrType[mem.ptrType()];
90
+ const hit = cache.get(ts);
91
+ if (hit !== undefined)
92
+ return hit;
93
+ const count = flattenTypes(ts, opts).length;
94
+ cache.set(ts, count);
95
+ return count;
96
+ }
65
97
  export function flattenType(t, opts) {
66
98
  const d = despecialize(t);
67
99
  switch (d.kind) {
@@ -8,11 +8,12 @@
8
8
  // current_instance() from the running thread;
9
9
  // - canon_resource_drop routes the dtor through `callDtorGated` below,
10
10
  // which reconstructs the reference's store.lift/store.lower bracket
11
- // (may_enter gating + trap poisoning) around the destructor call (#85).
11
+ // (entry refusal + trap poisoning) around the destructor call (#85).
12
12
  // Host-initiated drops do NOT come here: they run the dtor through the
13
13
  // real lift harness (`hostDtorCall`, exec/boundary.ts) — see #160.
14
14
  import { assert_, Trap, trap, trapIf } from "./trap.js";
15
- import { NeedsJspi, notifyInstancePoisoned, PendingCapability, withPoisonCause, } from "../task/scheduler.js";
15
+ import { NeedsJspi, notifyInstancePoisoned, PendingCapability, entryRefusal, } from "../task/scheduler.js";
16
+ import { COMPONENT_INSTANCE } from "./context.js";
16
17
  export class Table {
17
18
  static MAX_LENGTH = 2 ** 28 - 1;
18
19
  array = [null];
@@ -111,12 +112,24 @@ export function canonResourceNew(inst, rt, rep) {
111
112
  const h = new ResourceHandle(rt, rep, true);
112
113
  return inst.handles.add(h);
113
114
  }
114
- function asGate(x) {
115
+ /**
116
+ * Is `x` a REAL component instance (`task/mod.ts` `ComponentInstanceState`),
117
+ * as opposed to something that has no instance behind it at all?
118
+ *
119
+ * Two populations must answer false, and both are load-bearing for
120
+ * `callDtorGated`: an imported/host-implemented resource, whose
121
+ * `ResourceTypeInfo.impl` is `null` by construction (exec/executor.ts
122
+ * `bindImportedResources`), and the bare `{handles, mayLeave}` doubles test
123
+ * harnesses supply — neither has an instance to refuse entry into or to
124
+ * poison. So this is deliberately NOT a structural match on
125
+ * `ComponentInstanceLike`, which those doubles satisfy: it reads the
126
+ * `COMPONENT_INSTANCE` brand, declared on `ComponentInstanceState` and
127
+ * defined in ./context.ts so that cabi does not have to import task/.
128
+ */
129
+ function isComponentInstance(x) {
115
130
  if (x === null || typeof x !== "object")
116
131
  return null;
117
- const g = x;
118
- return typeof g.mayEnterFrom === "function" &&
119
- typeof g.enterFrom === "function" && typeof g.leaveTo === "function"
132
+ return x[COMPONENT_INSTANCE] === true
120
133
  ? x
121
134
  : null;
122
135
  }
@@ -125,11 +138,8 @@ function isThenable(v) {
125
138
  typeof v.then === "function";
126
139
  }
127
140
  /**
128
- * Invoke a resource destructor through the reference's entry bracket.
129
- *
130
- * definitions.py `canon_resource_drop` (line 2319) does not call `rt.dtor`
131
- * directly. It builds the dtor into a function instance and calls it through
132
- * `Store.lift` / `Store.lower` (lines 2330-2333):
141
+ * Invoke a resource destructor, as definitions.py `canon_resource_drop`
142
+ * (@ 2f13265) does — through `Store.lift`/`Store.lower`:
133
143
  *
134
144
  * ```python
135
145
  * dtor = rt.dtor or (lambda rep: [])
@@ -138,69 +148,63 @@ function isThenable(v) {
138
148
  * caller([h.rep])
139
149
  * ```
140
150
  *
141
- * so the dtor inherits `Store.lift`'s gate verbatim (lines 579-584):
142
- * `trap_if(not inst.may_enter_from(caller))`, `enter_from(caller)`, the call,
143
- * then `leave_to(caller)` — which a trap skips, leaving the *implementing*
144
- * instance permanently unenterable (poisoned).
145
- *
146
- * Two consequences that are easy to get wrong, both taken from the reference
147
- * rather than from intuition:
151
+ * That lift carries NO gate (CM#705): dropping a handle whose implementing
152
+ * instance is mid-execution is VALID, including the dtor-less case.
148
153
  *
149
- * - the bracket runs even when `rt.dtor is None` (the `or (lambda rep: [])`
150
- * above), so a dtor-less resource whose impl instance is mid-execution is
151
- * still a trap. `may_enter_from`/`enter_from` walk `entering_set(caller)`
152
- * (line 230), which is empty when the caller *is* the implementing
153
- * instance that, not a special case, is the same-instance exemption:
154
- * a component dropping a handle to its own resource never traps.
155
- * - poisoning applies to `rt.impl`, not to the dropping instance. The
156
- * dropper's own bracket (its `Store.lift` frame) is broken by the same
157
- * propagating trap at its own level; here only the callee is retired.
154
+ * What this adds is polyengine's per-instance poisoning divergence, and it
155
+ * applies to `rt.impl`, not to the dropping instance: a trap out of the dtor
156
+ * buries the implementing instance (refusal names the original trap,
157
+ * polyengine#145; its live stream/future ends are retired, #66). The
158
+ * dropper is poisoned, if at all, by the same trap propagating at its own
159
+ * level. `entryRefusal`'s `caller !== callee` guard keeps a component
160
+ * dropping a handle to its OWN resource admissible even against a marked
161
+ * instance.
158
162
  *
159
163
  * Capability signals (`NeedsJspi`, `PendingCapability`) are not traps — see
160
- * `isCapabilitySignal` in exec/boundary.ts — so they release the gate.
164
+ * `isCapabilitySignal` in exec/boundary.ts — so they do not poison.
161
165
  *
162
166
  * SCOPE (#160): this is the **guest-initiated** path only. A guest-initiated
163
167
  * drop must complete synchronously (the reference lifts the dtor with
164
168
  * `async_ = False`), so a thenable here is a trap. The host-initiated path
165
- * used to share this function with an `allowAsync` flag that held the entry
166
- * bracket across the dtor's promise; it now goes through the full lift
167
- * harness instead (`hostDtorCall` in exec/boundary.ts), which is what
168
- * definitions.py actually does and what unwedges #160.
169
+ * goes through the full lift harness instead (`hostDtorCall` in
170
+ * exec/boundary.ts), which is what definitions.py actually does.
169
171
  */
170
172
  export function callDtorGated(rt, rep, caller) {
171
- const impl = asGate(rt.impl);
173
+ const impl = isComponentInstance(rt.impl);
172
174
  // Always the raw synchronous dtor: `dtorHost` is the host path's lifted
173
175
  // entry, which is not callable from inside a guest activation.
174
176
  const dtorFn = rt.dtor;
175
- // No gate available: an imported (host-implemented) resource has
176
- // `impl === null` by construction (executor.ts `bindImportedResources`),
177
- // and there is no component instance to gate entry into. Test doubles that
178
- // supply a bare `{handles, mayLeave}` instance land here too.
177
+ // No component instance behind the resource: an imported (host-implemented)
178
+ // resource has `impl === null` by construction (executor.ts
179
+ // `bindImportedResources`), so there is no instance to refuse entry into and
180
+ // none to poison. Test doubles that supply a bare `{handles, mayLeave}`
181
+ // instance land here too — see `isComponentInstance`.
179
182
  if (impl === null) {
180
183
  const r = dtorFn?.(rep);
181
184
  trapIf(isThenable(r), "resource destructor did not complete synchronously");
182
185
  return;
183
186
  }
184
- // definitions.py `entering_set` (line 230): `self_and_ancestors() -
185
- // caller.self_and_ancestors()`. The caller is only meaningful when it is a
186
- // real component instance; a host-initiated drop passes null, which is the
187
- // reference's `caller = None` (Store.invoke).
188
- const callerInst = asGate(caller) === null ? null : caller;
187
+ // The caller is only meaningful when it is a real component instance; a
188
+ // host-initiated drop passes null, which is the reference's `caller = None`
189
+ // (Store.invoke). It feeds `entryRefusal`'s `caller !== callee` guard below.
190
+ const callerInst = isComponentInstance(caller) === null ? null : caller;
189
191
  // A poisoned target's refusal names the original trap (polyengine#145).
190
- if (!impl.mayEnterFrom(callerInst)) {
191
- trap(withPoisonCause(impl, "cannot enter component instance"));
192
+ // `callerInst` can legitimately BE `impl` here (a guest dropping its own
193
+ // resource): `entryRefusal`'s self-call guard keeps that entry allowed
194
+ // even against a marked instance.
195
+ {
196
+ const refusal = entryRefusal(impl, callerInst, "cannot enter component instance");
197
+ if (refusal !== null)
198
+ trap(refusal);
192
199
  }
193
- impl.enterFrom(callerInst);
194
200
  const poison = (e) => {
195
- if (e instanceof NeedsJspi || e instanceof PendingCapability) {
196
- // Not a trap: the reference reaches `leave_to` on every execution these
197
- // stand in for, so the instance stays enterable.
198
- impl.leaveTo(callerInst);
201
+ // Capability signals are not traps: the operation they stand in for
202
+ // completes normally in the reference, so the instance stays healthy.
203
+ if (e instanceof NeedsJspi || e instanceof PendingCapability)
199
204
  return;
200
- }
201
- // `leave_to` is NOT reached (the gate stays taken, permanently), and the
202
- // poisoned instance's live stream/future ends are retired (#66) through
203
- // the same seam fact_calls.ts uses for its bracket-break sites.
205
+ // A real trap buries the implementing instance, and its live
206
+ // stream/future ends are retired (#66) through the same seam
207
+ // fact_calls.ts uses for its poisoning sites.
204
208
  notifyInstancePoisoned(impl, e);
205
209
  };
206
210
  let out;
@@ -219,7 +223,6 @@ export function callDtorGated(rt, rep, caller) {
219
223
  poison(e);
220
224
  throw e;
221
225
  }
222
- impl.leaveTo(callerInst);
223
226
  }
224
227
  export function canonResourceDrop(inst, rt, i) {
225
228
  trapIf(!inst.mayLeave, "may_leave violation");
@@ -2,48 +2,140 @@
2
2
  // Size`): byte layout of component values in linear memory.
3
3
  import { assert_ } from "./trap.js";
4
4
  import { ptrSize } from "./memory.js";
5
- import { despecialize, discriminantType, } from "./types.js";
5
+ import { despecialize, discriminantSize, } from "./types.js";
6
6
  export function alignTo(ptr, alignment) {
7
7
  return Math.ceil(ptr / alignment) * alignment;
8
8
  }
9
- export function alignment(t, ptrType) {
9
+ /**
10
+ * Layout memo, keyed on type identity — see the despecialization memo in
11
+ * types.ts for why identity is a sound key.
12
+ *
13
+ * One map PER POINTER WIDTH, because layout is a function of both: `string`,
14
+ * variable-length `list` and the handle types all size off `ptrSize(ptrType)`.
15
+ * A single map keyed on the type alone would return i32 layouts to a memory64
16
+ * instance — silently, and with no corpus coverage to catch it, which is why
17
+ * layout_cache_test.ts pins it explicitly.
18
+ */
19
+ const layoutsI32 = new WeakMap();
20
+ const layoutsI64 = new WeakMap();
21
+ export function layoutOf(t, ptrType) {
22
+ const cache = ptrType === "i32" ? layoutsI32 : layoutsI64;
23
+ const hit = cache.get(t);
24
+ if (hit !== undefined)
25
+ return hit;
26
+ const l = computeLayout(t, ptrType);
27
+ // Cached only after the computation returns, so a type that trips
28
+ // `elemSizeRecord`'s "empty record" assert keeps tripping it on every call
29
+ // instead of leaving a half-built node behind.
30
+ cache.set(t, l);
31
+ return l;
32
+ }
33
+ /** Frozen for the same reason the despecialized nodes are: the node is now
34
+ * shared by every caller, and the engine is a better guarantor of that than
35
+ * an audit of today's call sites. */
36
+ function mkLayout(d, align, size, fieldOffsets = null, discSize = 0, payloadOffset = 0) {
37
+ // The premise the whole cache rests on: every alignment is a power of two.
38
+ // It is what lets `load`/`store` substitute `ptr % align === 0` for
39
+ // `ptr === alignTo(ptr, align)`, and what lets the retained field/payload
40
+ // offsets be base-relative (the aligned base factors out of `alignTo`).
41
+ // True by induction over `computeLayout` — asserted here so it stays true,
42
+ // once per type rather than per value.
43
+ assert_(align > 0 && (align & (align - 1)) === 0, "layout alignment must be a power of two");
44
+ return Object.freeze({
45
+ d,
46
+ align,
47
+ size,
48
+ fieldOffsets: fieldOffsets === null ? null : Object.freeze(fieldOffsets),
49
+ discSize,
50
+ payloadOffset,
51
+ });
52
+ }
53
+ /**
54
+ * definitions.py `alignment` (line 1201) and `elem_size` (line 1259) fused:
55
+ * the two functions switch over the same despecialized kinds, and every hot
56
+ * caller wants both. The compound kinds delegate to the per-kind kernels
57
+ * below, which stay the line-by-line spec mirror.
58
+ */
59
+ function computeLayout(t, ptrType) {
10
60
  const d = despecialize(t);
11
61
  switch (d.kind) {
12
62
  case "bool":
13
63
  case "s8":
14
64
  case "u8":
15
- return 1;
65
+ return mkLayout(d, 1, 1);
16
66
  case "s16":
17
67
  case "u16":
18
- return 2;
68
+ return mkLayout(d, 2, 2);
19
69
  case "s32":
20
70
  case "u32":
21
71
  case "f32":
22
72
  case "char":
23
- return 4;
73
+ return mkLayout(d, 4, 4);
24
74
  case "s64":
25
75
  case "u64":
26
76
  case "f64":
27
- return 8;
77
+ return mkLayout(d, 8, 8);
28
78
  case "string":
29
- return ptrSize(ptrType);
79
+ return mkLayout(d, ptrSize(ptrType), 2 * ptrSize(ptrType));
30
80
  case "error-context":
31
- return 4;
32
- case "list":
33
- return alignmentList(d.element, d.length ?? null, ptrType);
81
+ return mkLayout(d, 4, 4);
82
+ case "list": {
83
+ const len = d.length ?? null;
84
+ return mkLayout(d, alignmentList(d.element, len, ptrType), elemSizeList(d.element, len, ptrType));
85
+ }
34
86
  case "record":
35
- return alignmentRecord(d.fields, ptrType);
87
+ return recordLayout(d, d.fields, ptrType);
36
88
  case "variant":
37
- return alignmentVariant(d.cases, ptrType);
89
+ return variantLayout(d, d.cases, ptrType);
38
90
  case "flags":
39
- return alignmentFlags(d.labels);
91
+ return mkLayout(d, alignmentFlags(d.labels), elemSizeFlags(d.labels));
92
+ // `own`/`borrow` are a constant 4 and their `ResourceTypeInfo` is NOT
93
+ // walked: that graph cycles back to live instance state (see the
94
+ // `valTypeEqual` contract note in types.ts).
40
95
  case "own":
41
96
  case "borrow":
42
97
  case "stream":
43
98
  case "future":
44
- return 4;
99
+ return mkLayout(d, 4, 4);
45
100
  }
46
101
  }
102
+ /**
103
+ * The field offsets are exactly the intermediate `p` that
104
+ * `elemSizeRecord` computes and discards — the same accumulation, retained
105
+ * rather than re-derived, so the offsets cannot drift from the spec's own
106
+ * arithmetic. Size and alignment still come from the kernels themselves,
107
+ * which remain the single source of truth (and the "empty record" assert).
108
+ *
109
+ * They are BASE-RELATIVE, where the spec realigns a running absolute `p`.
110
+ * The two agree because `load`/`store` have already asserted that the base is
111
+ * aligned to this record's own alignment, which is the max over its fields',
112
+ * and alignments are powers of two: `alignTo(base + p, a) == base +
113
+ * alignTo(p, a)` whenever `a` divides `base`.
114
+ */
115
+ function recordLayout(d, fields, ptrType) {
116
+ const offsets = [];
117
+ let p = 0;
118
+ for (const f of fields) {
119
+ p = alignTo(p, alignment(f.type, ptrType));
120
+ offsets.push(p);
121
+ p += elemSize(f.type, ptrType);
122
+ }
123
+ return mkLayout(d, alignmentRecord(fields, ptrType), elemSizeRecord(fields, ptrType), offsets);
124
+ }
125
+ /**
126
+ * `payloadOffset` is `elemSizeVariant`'s intermediate `s` after it has been
127
+ * aligned up to `maxCaseAlignment` — the offset every case payload sits at.
128
+ * Base-relative for the same reason as the record offsets above: the variant's
129
+ * alignment is at least `maxCaseAlignment`, so the asserted-aligned base
130
+ * factors out of the `alignTo`.
131
+ */
132
+ function variantLayout(d, cases, ptrType) {
133
+ const discSize = discriminantSize(cases.length);
134
+ return mkLayout(d, alignmentVariant(cases, ptrType), elemSizeVariant(cases, ptrType), null, discSize, alignTo(discSize, maxCaseAlignment(cases, ptrType)));
135
+ }
136
+ export function alignment(t, ptrType) {
137
+ return layoutOf(t, ptrType).align;
138
+ }
47
139
  export function alignmentList(elemType, maybeLength, ptrType) {
48
140
  if (maybeLength !== null)
49
141
  return alignment(elemType, ptrType);
@@ -56,7 +148,10 @@ export function alignmentRecord(fields, ptrType) {
56
148
  return a;
57
149
  }
58
150
  export function alignmentVariant(cases, ptrType) {
59
- return Math.max(alignment(discriminantType(cases), ptrType), maxCaseAlignment(cases, ptrType));
151
+ // definitions.py takes `alignment(discriminant_type(cases))`; the
152
+ // discriminant is always u8/u16/u32, whose alignment equals its size, so
153
+ // `discriminantSize` is the same number without allocating the PrimType.
154
+ return Math.max(discriminantSize(cases.length), maxCaseAlignment(cases, ptrType));
60
155
  }
61
156
  export function maxCaseAlignment(cases, ptrType) {
62
157
  let a = 1;
@@ -76,42 +171,7 @@ export function alignmentFlags(labels) {
76
171
  return 4;
77
172
  }
78
173
  export function elemSize(t, ptrType) {
79
- const d = despecialize(t);
80
- switch (d.kind) {
81
- case "bool":
82
- case "s8":
83
- case "u8":
84
- return 1;
85
- case "s16":
86
- case "u16":
87
- return 2;
88
- case "s32":
89
- case "u32":
90
- case "f32":
91
- case "char":
92
- return 4;
93
- case "s64":
94
- case "u64":
95
- case "f64":
96
- return 8;
97
- case "string":
98
- return 2 * ptrSize(ptrType);
99
- case "error-context":
100
- return 4;
101
- case "list":
102
- return elemSizeList(d.element, d.length ?? null, ptrType);
103
- case "record":
104
- return elemSizeRecord(d.fields, ptrType);
105
- case "variant":
106
- return elemSizeVariant(d.cases, ptrType);
107
- case "flags":
108
- return elemSizeFlags(d.labels);
109
- case "own":
110
- case "borrow":
111
- case "stream":
112
- case "future":
113
- return 4;
114
- }
174
+ return layoutOf(t, ptrType).size;
115
175
  }
116
176
  export function elemSizeList(elemType, maybeLength, ptrType) {
117
177
  if (maybeLength !== null)
@@ -128,7 +188,8 @@ export function elemSizeRecord(fields, ptrType) {
128
188
  return alignTo(s, alignmentRecord(fields, ptrType));
129
189
  }
130
190
  export function elemSizeVariant(cases, ptrType) {
131
- let s = elemSize(discriminantType(cases), ptrType);
191
+ // See `alignmentVariant`: size of u8/u16/u32 == `discriminantSize`.
192
+ let s = discriminantSize(cases.length);
132
193
  s = alignTo(s, maxCaseAlignment(cases, ptrType));
133
194
  let cs = 0;
134
195
  for (const c of cases) {
package/esm/cabi/load.js CHANGED
@@ -2,19 +2,23 @@
2
2
  import { assert_, trapIf } from "./trap.js";
3
3
  import { bytesOf, loadIntS, loadIntU, loadPtr } from "./memory.js";
4
4
  import { decodeI32AsFloat, decodeI64AsFloat } from "./float.js";
5
- import { alignment, alignTo, elemSize, elemSizeFlags, maxCaseAlignment, } from "./layout.js";
5
+ import { elemSizeFlags, layoutOf } from "./layout.js";
6
6
  import { convertI32ToChar, loadString } from "./strings.js";
7
7
  import { requireMemory } from "./context.js";
8
8
  import { tryLoadNumericList } from "./bulk_lists.js";
9
9
  import { liftBorrow, liftOwn } from "./handles.js";
10
- import { despecialize, discriminantType, } from "./types.js";
11
10
  import { liftErrorContext, liftFuture, liftStream, } from "./async_values.js";
12
11
  export const MAX_LIST_BYTE_LENGTH = (1 << 28) - 1;
13
12
  export function load(cx, ptr, t) {
14
13
  const mem = requireMemory(cx.opts);
15
- assert_(ptr === alignTo(ptr, alignment(t, mem.ptrType())), "load misaligned");
16
- assert_(ptr + elemSize(t, mem.ptrType()) <= mem.length, "load OOB");
17
- const d = despecialize(t);
14
+ // One cached layout node per (type, pointer width) issue #261; it carries
15
+ // the despecialized type too, so this is the only map lookup on the path.
16
+ const L = layoutOf(t, mem.ptrType());
17
+ // Alignments are always powers of two, so `ptr % align === 0` decides
18
+ // exactly what `ptr === alignTo(ptr, align)` did, without the float divide.
19
+ assert_(ptr % L.align === 0, "load misaligned");
20
+ assert_(ptr + L.size <= mem.length, "load OOB");
21
+ const d = L.d;
18
22
  switch (d.kind) {
19
23
  case "bool":
20
24
  return convertIntToBool(loadIntU(mem, ptr, 1));
@@ -47,9 +51,11 @@ export function load(cx, ptr, t) {
47
51
  case "list":
48
52
  return loadList(cx, ptr, d.element, d.length ?? null);
49
53
  case "record":
50
- return loadRecord(cx, ptr, d.fields);
54
+ return loadRecord(cx, ptr, d.fields, L.fieldOffsets);
51
55
  case "variant":
52
- return loadVariant(cx, ptr, d.cases);
56
+ // discSize is 0 only on the kinds that have no discriminant; inside
57
+ // this arm `variantLayout` established it as 1|2|4.
58
+ return loadVariant(cx, ptr, d.cases, L.discSize, L.payloadOffset);
53
59
  case "flags":
54
60
  return loadFlags(cx, ptr, d.labels);
55
61
  case "own":
@@ -77,8 +83,7 @@ export function loadList(cx, ptr, elemType, maybeLength) {
77
83
  }
78
84
  export function loadListFromRange(cx, ptr, length, elemType) {
79
85
  const mem = requireMemory(cx.opts);
80
- const size = elemSize(elemType, mem.ptrType());
81
- const align = alignment(elemType, mem.ptrType());
86
+ const { size, align } = layoutOf(elemType, mem.ptrType());
82
87
  const byteLengthBig = BigInt(length) * BigInt(size);
83
88
  trapIf(byteLengthBig > BigInt(MAX_LIST_BYTE_LENGTH), "list too long");
84
89
  const ptrBig = BigInt(ptr);
@@ -88,7 +93,8 @@ export function loadListFromRange(cx, ptr, length, elemType) {
88
93
  }
89
94
  export function loadListFromValidRange(cx, ptr, length, elemType) {
90
95
  const mem = requireMemory(cx.opts);
91
- const kind = despecialize(elemType).kind;
96
+ const L = layoutOf(elemType, mem.ptrType());
97
+ const kind = L.d.kind;
92
98
  // docs/architecture.md §7: list<u8> lifts to a Uint8Array copy.
93
99
  if (kind === "u8") {
94
100
  return bytesOf(mem, ptr, length).slice();
@@ -100,35 +106,37 @@ export function loadListFromValidRange(cx, ptr, length, elemType) {
100
106
  const bulk = tryLoadNumericList(mem, ptr, length, kind);
101
107
  if (bulk !== null)
102
108
  return bulk;
103
- const size = elemSize(elemType, mem.ptrType());
109
+ const size = L.size;
104
110
  const a = [];
105
111
  for (let i = 0; i < length; i++) {
106
112
  a.push(load(cx, ptr + i * size, elemType));
107
113
  }
108
114
  return a;
109
115
  }
110
- export function loadRecord(cx, ptr, fields) {
111
- const mem = requireMemory(cx.opts);
116
+ /**
117
+ * `offsets[i]` is field i's byte offset from `ptr`, precomputed on the layout
118
+ * node, so the loop is an indexed read rather than the per-field
119
+ * `alignTo`/`alignment`/`elemSize` recomputation it used to be (issue #261).
120
+ * Taking the offsets rather than the whole `Layout` keeps the function
121
+ * self-consistent: its two arguments are the ones the result depends on, and
122
+ * there is no unchecked "these came from the same type" invariant to violate.
123
+ */
124
+ export function loadRecord(cx, ptr, fields, offsets) {
112
125
  const record = {};
113
- let p = ptr;
114
- for (const field of fields) {
115
- p = alignTo(p, alignment(field.type, mem.ptrType()));
116
- record[field.label] = load(cx, p, field.type);
117
- p += elemSize(field.type, mem.ptrType());
126
+ for (let i = 0; i < fields.length; i++) {
127
+ const field = fields[i];
128
+ record[field.label] = load(cx, ptr + offsets[i], field.type);
118
129
  }
119
130
  return record;
120
131
  }
121
- export function loadVariant(cx, ptr, cases) {
132
+ export function loadVariant(cx, ptr, cases, discSize, payloadOffset) {
122
133
  const mem = requireMemory(cx.opts);
123
- const discSize = elemSize(discriminantType(cases), mem.ptrType());
124
134
  const caseIndex = loadIntU(mem, ptr, discSize);
125
- let p = ptr + discSize;
126
135
  trapIf(caseIndex >= cases.length, "invalid variant discriminant");
127
136
  const c = cases[caseIndex];
128
- p = alignTo(p, maxCaseAlignment(cases, mem.ptrType()));
129
137
  if (c.type === null)
130
138
  return { [c.label]: null };
131
- return { [c.label]: load(cx, p, c.type) };
139
+ return { [c.label]: load(cx, ptr + payloadOffset, c.type) };
132
140
  }
133
141
  export function loadFlags(cx, ptr, labels) {
134
142
  const mem = requireMemory(cx.opts);