@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/async_values.js
CHANGED
|
@@ -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
|
|
30
|
-
* without being one of THIS copy's `ErrorContext`s (
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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);
|
package/esm/cabi/bulk_lists.js
CHANGED
|
@@ -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;
|
package/esm/cabi/context.js
CHANGED
|
@@ -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
|
|
34
|
-
* through `canon_lift
|
|
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;
|
package/esm/cabi/flatten.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/esm/cabi/handles.js
CHANGED
|
@@ -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
|
-
// (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
142
|
-
*
|
|
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
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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
|
|
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
|
-
*
|
|
166
|
-
*
|
|
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 =
|
|
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
|
|
176
|
-
// `impl === null` by construction (executor.ts
|
|
177
|
-
//
|
|
178
|
-
// supply a bare `{handles, mayLeave}`
|
|
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
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
//
|
|
202
|
-
//
|
|
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");
|
package/esm/cabi/layout.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
87
|
+
return recordLayout(d, d.fields, ptrType);
|
|
36
88
|
case "variant":
|
|
37
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
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);
|