jz 0.7.0 → 0.8.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.
- package/README.md +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/index.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { parse } from './src/parse.js'
|
|
44
44
|
import watrCompile from "watr/compile";
|
|
45
45
|
import watrPrint from "watr/print";
|
|
46
|
-
import watOptimize from "
|
|
46
|
+
import watOptimize from "watr/optimize";
|
|
47
47
|
import { appendLateStdlib } from './src/wat/assemble.js'
|
|
48
48
|
import { ctx, reset, err, initWarnings, assertCtxInvariants } from './src/ctx.js'
|
|
49
49
|
import prepare, { GLOBALS } from './src/prepare/index.js'
|
|
@@ -51,7 +51,7 @@ import { liftIIFEs } from './src/prepare/lift-iife.js'
|
|
|
51
51
|
import compile from './src/compile/index.js'
|
|
52
52
|
import { resetProgramFactsCache } from './src/compile/program-facts.js'
|
|
53
53
|
import { emit, emitter, emitVoid as flat, emitBlockBody as body, emitBoolStr as bool, emitIndex as idx, buildArrayWithSpreads as spread } from './src/compile/emit.js'
|
|
54
|
-
import { optimizeFunc, foldStrDispatchF64, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize } from './src/optimize/index.js'
|
|
54
|
+
import { optimizeFunc, foldStrDispatchF64, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize, SIMD_PINNED } from './src/optimize/index.js'
|
|
55
55
|
import { findBodyStart } from './src/ir.js'
|
|
56
56
|
import { VAL } from './src/reps.js'
|
|
57
57
|
import jzify from './jzify/index.js'
|
|
@@ -462,6 +462,8 @@ const setupCtx = (code, opts) => {
|
|
|
462
462
|
}
|
|
463
463
|
if (opts.alloc === false) ctx.transform.alloc = false
|
|
464
464
|
if (opts.inspect) ctx.transform.inspect = true
|
|
465
|
+
if (opts.helperCounters) ctx.transform.helperCounters = true
|
|
466
|
+
if (opts.helperCallsites) ctx.transform.helperCallsites = opts.helperCallsites
|
|
465
467
|
if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
|
|
466
468
|
if (opts.randomSeed !== undefined) {
|
|
467
469
|
if (opts.randomSeed !== true && !Number.isFinite(opts.randomSeed))
|
|
@@ -536,11 +538,17 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
536
538
|
|
|
537
539
|
// opts.noSimd: force auto-vectorization off regardless of opt level — a
|
|
538
540
|
// portability escape hatch for engines without the SIMD proposal (parallels
|
|
539
|
-
// opts.noTailCall).
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
541
|
+
// opts.noTailCall). Must suppress EVERY jz-emitted v128, which is two passes:
|
|
542
|
+
// vectorizeLaneLocal (lane maps, reductions incl. reduceUnroll, byte scans) AND
|
|
543
|
+
// the SLP store-pair packer (within-iteration f64x2). Both off, so `noSimd` is a
|
|
544
|
+
// true scalar baseline — the oracle SIMD-vs-scalar correctness tests compare
|
|
545
|
+
// against (missing one let an SLP miscompile pass as "SIMD == scalar"). Explicit
|
|
546
|
+
// f32x4/i32x4 intrinsics in source are the user's own opt-in and stay. Applied
|
|
547
|
+
// after auto-config so it wins over any re-resolved preset.
|
|
548
|
+
if (opts.noSimd) {
|
|
549
|
+
ctx.transform.optimize.vectorizeLaneLocal = false
|
|
550
|
+
ctx.transform.optimize.experimentalSlp = false
|
|
551
|
+
}
|
|
544
552
|
|
|
545
553
|
// opts.whyNotSimd (CLI --why-not-simd): emit a `simd-why-not` warning per
|
|
546
554
|
// canonical loop that the auto-vectorizer declined, naming the blocking op —
|
|
@@ -582,6 +590,10 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
582
590
|
const cfg = ctx.transform.optimize
|
|
583
591
|
let watrOpts = typeof cfg.watr === 'object' ? { ...cfg.watr } : true
|
|
584
592
|
if (cfg.vectorizeLaneLocal) {
|
|
593
|
+
// Off at the speed tier: watr's loopify collapses the while-idiom to
|
|
594
|
+
// `loop { if C { …; br } }` (an UNfused back-jump — no win) AND would mangle the
|
|
595
|
+
// lane-vectorizer's loop shape. jz's own `rotateLoops` (optimize/index.js, post
|
|
596
|
+
// phase) does the rotation instead, emitting the FUSED `br_if $loop C` back-edge.
|
|
585
597
|
if (watrOpts === true) watrOpts = { loopify: false }
|
|
586
598
|
else if (typeof watrOpts === 'object' && watrOpts.loopify === undefined) watrOpts.loopify = false
|
|
587
599
|
}
|
|
@@ -594,10 +606,29 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
594
606
|
// cost of duplicating tiny bodies; level 2 (and the size budgets measured there)
|
|
595
607
|
// stay untouched.
|
|
596
608
|
if (cfg.inlineFns) {
|
|
597
|
-
if (watrOpts === true) watrOpts = { inline:
|
|
598
|
-
else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline =
|
|
609
|
+
if (watrOpts === true) watrOpts = { inline: 'simd' }
|
|
610
|
+
else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = 'simd'
|
|
611
|
+
}
|
|
612
|
+
// guardRefine folds jz's NaN-box tag reads — default-off in watr (general WAT
|
|
613
|
+
// never matches it), so jz always enables it explicitly.
|
|
614
|
+
if (watrOpts === true) watrOpts = { guardRefine: true }
|
|
615
|
+
else if (typeof watrOpts === 'object' && watrOpts.guardRefine === undefined) watrOpts.guardRefine = true
|
|
616
|
+
// Pin jz's scalar transcendentals (the PPC_CALL2 keys the auto-vectorizer rewrites to f64x2
|
|
617
|
+
// mirrors) so watr's inline passes don't dissolve the call nodes the lift needs. The protection
|
|
618
|
+
// policy lives here in jz — watr just honours the `pin` list (no jz names hardcoded in watr).
|
|
619
|
+
if (cfg.watr) {
|
|
620
|
+
if (watrOpts === true) watrOpts = {}
|
|
621
|
+
watrOpts.pin = watrOpts.pin ? [...watrOpts.pin, ...SIMD_PINNED] : SIMD_PINNED
|
|
599
622
|
}
|
|
623
|
+
// Capture the per-func alias facts (param-distinctness) the emit phase stamped as JS
|
|
624
|
+
// expandos — watOptimize round-trips the module through watr's printer/parser, which
|
|
625
|
+
// rebuilds plain arrays and DROPS every non-index property. Without re-attaching, the
|
|
626
|
+
// 'post' leaf passes (splitLoopPrivateScratch, hoistInvariantLoop) lose the proven
|
|
627
|
+
// alias model and fall back to weaker/heuristic invariance — the exact reason raytrace's
|
|
628
|
+
// read-only sphere loads couldn't be SOUNDLY proven loop-invariant after watr ran.
|
|
629
|
+
const aliasFacts = cfg.watr ? new Map(module.filter(n => Array.isArray(n) && n[0] === 'func' && n.distinctParams).map(n => [n[1], n.distinctParams])) : null
|
|
600
630
|
const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
|
|
631
|
+
if (aliasFacts?.size) for (const n of optimized) if (Array.isArray(n) && n[0] === 'func' && aliasFacts.has(n[1])) n.distinctParams = aliasFacts.get(n[1])
|
|
601
632
|
// Stable-pointee module globals: resolve the __ptr_offset once per function.
|
|
602
633
|
// Never-forwarding kinds — every PTR tag outside __ptr_offset's forwarding
|
|
603
634
|
// set {ARRAY, HASH, SET, MAP} — give the same offset for the same bits, so
|
package/interop.js
CHANGED
|
@@ -106,73 +106,81 @@ const sectionReader = (bytes) => {
|
|
|
106
106
|
|
|
107
107
|
// ── NaN-box codec ───────────────────────────────────────────────────────────
|
|
108
108
|
|
|
109
|
-
// NaN-
|
|
109
|
+
// NaN-box codec — integer / BigInt based. A box NEVER becomes a JS number: JSC (Safari)
|
|
110
|
+
// canonicalizes a NaN payload the instant it materializes as f64 (boundary return,
|
|
111
|
+
// Float64Array read, getFloat64), so a box is carried in JS-land as a BigInt (the i64
|
|
112
|
+
// bits) and decoded with integer ops. Only genuine (non-NaN) numbers ever touch f64.
|
|
113
|
+
const MASK32 = 0xffffffffn
|
|
114
|
+
// Reinterpret for GENUINE numbers (and freshly-built boxes leaving JS): `_f64` only ever
|
|
115
|
+
// holds a real number here, never a live NaN-box, so there is nothing for JSC to purify.
|
|
110
116
|
const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export const
|
|
123
|
-
export const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
_u32[1] = ATOM_HI[ATOM.NULL]; _u32[0] = 0; export const NULL_NAN = _f64[0]
|
|
128
|
-
_u32[1] = ATOM_HI[ATOM.UNDEF]; _u32[0] = 0; export const UNDEF_NAN = _f64[0]
|
|
129
|
-
_u32[1] = ATOM_HI[ATOM.FALSE]; _u32[0] = 0; export const FALSE_NAN = _f64[0]
|
|
130
|
-
_u32[1] = ATOM_HI[ATOM.TRUE]; _u32[0] = 0; export const TRUE_NAN = _f64[0]
|
|
131
|
-
|
|
132
|
-
// Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
|
|
117
|
+
export const f64ToI64 = (n) => { _f64[0] = n; return (BigInt(_u32[1]) << 32n) | BigInt(_u32[0] >>> 0) }
|
|
118
|
+
export const i64ToF64 = (b) => { _u32[0] = Number(b & MASK32); _u32[1] = Number((b >> 32n) & MASK32); return _f64[0] }
|
|
119
|
+
|
|
120
|
+
const hi32 = (b) => Number((b >> 32n) & MASK32)
|
|
121
|
+
// A NaN-box is a sign-0 quiet NaN — high u32 carries jz's 0x7FF8 prefix.
|
|
122
|
+
const isBox = (b) => (hi32(b) & 0x7FF80000) === 0x7FF80000
|
|
123
|
+
// i64 bits for a wrapVal result (BigInt box, or number → its f64 bits): memory staging + i64 params.
|
|
124
|
+
const bits = (v) => typeof v === 'bigint' ? v : f64ToI64(v)
|
|
125
|
+
|
|
126
|
+
// Reserved atoms (type=ATOM, offset=0): aux 1/2/4/5 → null/undefined/false/true. BigInt boxes.
|
|
127
|
+
export const NULL_NAN = BigInt(ATOM_HI[ATOM.NULL]) << 32n
|
|
128
|
+
export const UNDEF_NAN = BigInt(ATOM_HI[ATOM.UNDEF]) << 32n
|
|
129
|
+
export const FALSE_NAN = BigInt(ATOM_HI[ATOM.FALSE]) << 32n
|
|
130
|
+
export const TRUE_NAN = BigInt(ATOM_HI[ATOM.TRUE]) << 32n
|
|
131
|
+
|
|
132
|
+
// Coerce JS null/undefined → boxed atom (BigInt); everything else passes through.
|
|
133
133
|
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
|
|
134
134
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (decodePtrType(_u32[1]) === 4) {
|
|
146
|
-
const a = decodePtrAux(_u32[1])
|
|
147
|
-
if (a & LAYOUT.SSO_BIT) {
|
|
148
|
-
const len = a & 0x7, off = _u32[0]; let s = ''
|
|
149
|
-
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
150
|
-
return s
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (_u32[0] !== 0) return v
|
|
154
|
-
if (_u32[1] === ATOM_HI[ATOM.NULL]) return null
|
|
155
|
-
if (_u32[1] === ATOM_HI[ATOM.UNDEF]) return undefined
|
|
156
|
-
if (_u32[1] === ATOM_HI[ATOM.FALSE]) return false
|
|
157
|
-
if (_u32[1] === ATOM_HI[ATOM.TRUE]) return true
|
|
158
|
-
return v
|
|
135
|
+
// SSO-encode a string ≤6 ASCII chars to a NaN-box BigInt (no heap needed).
|
|
136
|
+
// Mirrors mem.String's SSO branch. Used when marshaling a string into an i64-carrier
|
|
137
|
+
// param of a memoryless module (no linear memory, so only self-contained bit encodings
|
|
138
|
+
// like SSO can survive the boundary). Non-SSO strings throw clearly rather than silently
|
|
139
|
+
// becoming NaN.
|
|
140
|
+
const encodeSSO = (s) => {
|
|
141
|
+
let p = 0n
|
|
142
|
+
for (let i = 0; i < s.length; i++) p |= BigInt(s.charCodeAt(i)) << BigInt(i * 7)
|
|
143
|
+
p |= BigInt(s.length) << 42n
|
|
144
|
+
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn))
|
|
159
145
|
}
|
|
160
146
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
// Accept either the i64 carrier (BigInt, canonical) or a legacy f64 NaN-box (intact on V8 —
|
|
148
|
+
// e.g. an adaptI64 result, or user code holding a pre-i64 pointer) — normalize before decode.
|
|
149
|
+
const asBits = (p) => typeof p === 'bigint' ? p : f64ToI64(p)
|
|
150
|
+
export const ptr = (type, aux, offset) => (BigInt(encodePtrHi(type, aux)) << 32n) | BigInt(offset >>> 0)
|
|
151
|
+
export const offset = (p) => Number(asBits(p) & MASK32)
|
|
152
|
+
export const type = (p) => decodePtrType(hi32(asBits(p)))
|
|
153
|
+
export const aux = (p) => decodePtrAux(hi32(asBits(p)))
|
|
154
|
+
|
|
155
|
+
// SSO string decode from i64 bits: 7-bit ASCII, char i at payload bit i*7, len at bits 42-44.
|
|
156
|
+
const decodeSSO = (b) => {
|
|
157
|
+
const a = decodePtrAux(hi32(b)), len = (a >>> 10) & 7
|
|
158
|
+
const payload = (BigInt(a) << 32n) | BigInt(Number(b & MASK32))
|
|
159
|
+
let s = ''
|
|
160
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode(Number((payload >> BigInt(i * 7)) & 0x7fn))
|
|
161
|
+
return s
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
// Memory-free decode of an i64-bits boundary value: numbers pass through, a box becomes
|
|
165
|
+
// its atom / SSO string. Exactly the forms a *memoryless* module can carry (no linear
|
|
166
|
+
// memory → no heap string/array/object). Heap-carrying modules route through `mem.read`.
|
|
167
|
+
const decode = v => {
|
|
168
|
+
if (Array.isArray(v)) return v.map(decode) // multi-value tuple — each lane is an i64-carrier (memoryless)
|
|
169
|
+
if (typeof v === 'number') { if (v === v) return v; v = f64ToI64(v) } // f64 NaN-box (intact on V8) → bits
|
|
170
|
+
else if (typeof v !== 'bigint') return v // already-decoded JS value
|
|
171
|
+
if (!isBox(v)) return i64ToF64(v) // non-NaN bits → number
|
|
172
|
+
if (type(v) === 4 && (aux(v) & LAYOUT.SSO_BIT)) return decodeSSO(v)
|
|
173
|
+
if (offset(v) === 0) {
|
|
174
|
+
if (v === NULL_NAN) return null
|
|
175
|
+
if (v === UNDEF_NAN) return undefined
|
|
176
|
+
if (v === FALSE_NAN) return false
|
|
177
|
+
if (v === TRUE_NAN) return true
|
|
178
|
+
}
|
|
179
|
+
return i64ToF64(v) // canonical NaN-number / unknown
|
|
172
180
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
|
|
182
|
+
// Decode a boundary value arriving as i64 bits (BigInt). Heap modules go through mem.read.
|
|
183
|
+
const readArgBits = (state, big) => state.mem ? state.mem.read(big) : decode(big)
|
|
176
184
|
|
|
177
185
|
// Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
|
|
178
186
|
const ELEMS = {
|
|
@@ -290,17 +298,19 @@ export const memory = (src) => {
|
|
|
290
298
|
// NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
|
|
291
299
|
// payload to 0x7FF8000000000000 — destroying the type/offset bits.
|
|
292
300
|
const wrapped = new BigInt64Array(n)
|
|
293
|
-
for (let i = 0; i < n; i++) wrapped[i] =
|
|
301
|
+
for (let i = 0; i < n; i++) wrapped[i] = bits(mem.wrapVal(data[i]))
|
|
294
302
|
const dst = new BigInt64Array(mem.buffer, off, n)
|
|
295
303
|
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
296
304
|
return ptr(1, 0, off)
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
mem.String = (str) => {
|
|
300
|
-
if (str.length <=
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
if (str.length <= 6 && /^[\x00-\x7f]*$/.test(str)) {
|
|
309
|
+
// 7-bit ASCII SSO: char i at payload bit i*7, len at bits 42-44 (see module/string.js codec).
|
|
310
|
+
let p = 0n
|
|
311
|
+
for (let i = 0; i < str.length; i++) p |= BigInt(str.charCodeAt(i)) << BigInt(i * 7)
|
|
312
|
+
p |= BigInt(str.length) << 42n
|
|
313
|
+
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn)) // STRING + SSO_BIT
|
|
304
314
|
}
|
|
305
315
|
const enc = TEXT_ENC.encode(str)
|
|
306
316
|
const n = enc.length, raw = alloc(4 + n), m = dv()
|
|
@@ -323,9 +333,10 @@ export const memory = (src) => {
|
|
|
323
333
|
if (v === null || v === undefined) return coerce(v)
|
|
324
334
|
if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
|
|
325
335
|
if (typeof v === 'string') return mem.String(v)
|
|
326
|
-
// BigInt
|
|
327
|
-
//
|
|
328
|
-
|
|
336
|
+
// A BigInt that is a NaN-box (jz's i64 carrier — e.g. a value pre-built via memory.String/
|
|
337
|
+
// ptr) passes straight through. A plain bigint *value* crosses as a decimal-string (wasm
|
|
338
|
+
// numeric parsers accept it).
|
|
339
|
+
if (typeof v === 'bigint') return isBox(v) ? v : mem.String(v.toString())
|
|
329
340
|
if (Array.isArray(v)) return mem.Array(v)
|
|
330
341
|
if (v instanceof ArrayBuffer) return mem.Buffer(v)
|
|
331
342
|
if (v instanceof DataView) return mem.Buffer(v.buffer)
|
|
@@ -366,7 +377,7 @@ export const memory = (src) => {
|
|
|
366
377
|
if (v === null || v === undefined) v = coerce(v)
|
|
367
378
|
else if (typeof v === 'string') v = mem.String(v)
|
|
368
379
|
else if (Array.isArray(v)) v = mem.Array(v)
|
|
369
|
-
wrapped[i] =
|
|
380
|
+
wrapped[i] = bits(v)
|
|
370
381
|
}
|
|
371
382
|
const dst = new BigInt64Array(mem.buffer, raw, n)
|
|
372
383
|
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
|
|
@@ -375,8 +386,15 @@ export const memory = (src) => {
|
|
|
375
386
|
|
|
376
387
|
mem.read = function(p) {
|
|
377
388
|
if (Array.isArray(p)) return p.map(v => mem.read(v)) // multi-value tuple
|
|
378
|
-
if (p ===
|
|
379
|
-
|
|
389
|
+
if (typeof p === 'number') {
|
|
390
|
+
if (p === p) return p // genuine number passthrough (NaN fails ===)
|
|
391
|
+
p = f64ToI64(p) // f64 NaN-box (intact on V8) → bits; decode below
|
|
392
|
+
} else if (typeof p !== 'bigint') {
|
|
393
|
+
return p // already a decoded JS value (string/object/…) — passthrough
|
|
394
|
+
}
|
|
395
|
+
// p is now i64 bits (BigInt). Decode with integer ops — never materialize as f64.
|
|
396
|
+
if (!isBox(p)) return i64ToF64(p) // non-NaN bits → genuine number
|
|
397
|
+
const m = dv(), t = type(p), a = aux(p), off = offset(p)
|
|
380
398
|
if (t === 0 && off === 0) {
|
|
381
399
|
if (a === 1) return null
|
|
382
400
|
if (a === 2) return undefined
|
|
@@ -385,19 +403,18 @@ export const memory = (src) => {
|
|
|
385
403
|
}
|
|
386
404
|
if (t === 11 && mem._extMap) return mem._extMap[off]
|
|
387
405
|
if (t === 1) { // ARRAY
|
|
388
|
-
let
|
|
406
|
+
let aOff = off
|
|
389
407
|
// Follow forwarding pointers (cap === -1 means array was reallocated)
|
|
390
408
|
while (m.getInt32(aOff - 4, true) === -1) aOff = m.getInt32(aOff - 8, true)
|
|
391
409
|
const len = m.getInt32(aOff - 8, true), out = new Array(len)
|
|
392
|
-
for (let i = 0; i < len; i++) out[i] = mem.read(m.
|
|
410
|
+
for (let i = 0; i < len; i++) out[i] = mem.read(m.getBigInt64(aOff + i * 8, true))
|
|
393
411
|
return out
|
|
394
412
|
}
|
|
395
413
|
if (t === 3) { // TYPED
|
|
396
|
-
const
|
|
414
|
+
const elem = a & 7
|
|
397
415
|
const [, stride] = ELEM_BY_ID[elem]
|
|
398
416
|
const Ctor = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array][elem]
|
|
399
|
-
|
|
400
|
-
if (a2 & 8) {
|
|
417
|
+
if (a & 8) {
|
|
401
418
|
const byteLen = m.getInt32(off, true), dataOff = m.getInt32(off + 4, true)
|
|
402
419
|
return new Ctor(mem.buffer, dataOff, byteLen / stride)
|
|
403
420
|
}
|
|
@@ -405,61 +422,47 @@ export const memory = (src) => {
|
|
|
405
422
|
return new Ctor(mem.buffer, off, byteLen / stride)
|
|
406
423
|
}
|
|
407
424
|
if (t === 2) { // BUFFER
|
|
408
|
-
const byteLen =
|
|
425
|
+
const byteLen = m.getInt32(off - 8, true)
|
|
409
426
|
const out = new ArrayBuffer(byteLen)
|
|
410
427
|
new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
|
|
411
428
|
return out
|
|
412
429
|
}
|
|
413
430
|
if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const len = a2 & 0x7; let s = ''
|
|
417
|
-
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
418
|
-
return s
|
|
419
|
-
}
|
|
420
|
-
const len = dv().getInt32(off - 4, true)
|
|
431
|
+
if (a & LAYOUT.SSO_BIT) return decodeSSO(p)
|
|
432
|
+
const len = m.getInt32(off - 4, true)
|
|
421
433
|
return TEXT_DEC.decode(new Uint8Array(mem.buffer, off, len))
|
|
422
434
|
}
|
|
423
435
|
if (t === 6) { // OBJECT
|
|
424
|
-
const
|
|
436
|
+
const keys = mem.schemas[a]
|
|
425
437
|
if (!keys) return p
|
|
426
438
|
const obj = {}
|
|
427
|
-
for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.
|
|
439
|
+
for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.getBigInt64(off + i * 8, true))
|
|
428
440
|
return obj
|
|
429
441
|
}
|
|
430
442
|
if (t === 7) { // HASH
|
|
431
|
-
const
|
|
432
|
-
const obj = {}
|
|
443
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), obj = {}
|
|
433
444
|
for (let i = 0, found = 0; i < cap && found < size; i++) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const key = mem.read(m.getFloat64(off + i * 24 + 8, true))
|
|
437
|
-
obj[key] = mem.read(m.getFloat64(off + i * 24 + 16, true))
|
|
445
|
+
if (m.getBigInt64(off + i * 24, true) !== 0n) {
|
|
446
|
+
obj[mem.read(m.getBigInt64(off + i * 24 + 8, true))] = mem.read(m.getBigInt64(off + i * 24 + 16, true))
|
|
438
447
|
found++
|
|
439
448
|
}
|
|
440
449
|
}
|
|
441
450
|
return obj
|
|
442
451
|
}
|
|
443
452
|
if (t === 8) { // SET
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const hash = m.getFloat64(off + i * 16, true)
|
|
448
|
-
if (hash !== 0) set.add(mem.read(m.getFloat64(off + i * 16 + 8, true)))
|
|
449
|
-
}
|
|
453
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), set = new Set()
|
|
454
|
+
for (let i = 0; i < cap && set.size < size; i++)
|
|
455
|
+
if (m.getBigInt64(off + i * 16, true) !== 0n) set.add(mem.read(m.getBigInt64(off + i * 16 + 8, true)))
|
|
450
456
|
return set
|
|
451
457
|
}
|
|
452
458
|
if (t === 9) { // MAP
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (hash !== 0) map.set(mem.read(m.getFloat64(off + i * 24 + 8, true)), mem.read(m.getFloat64(off + i * 24 + 16, true)))
|
|
458
|
-
}
|
|
459
|
+
const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), map = new Map()
|
|
460
|
+
for (let i = 0; i < cap && map.size < size; i++)
|
|
461
|
+
if (m.getBigInt64(off + i * 24, true) !== 0n)
|
|
462
|
+
map.set(mem.read(m.getBigInt64(off + i * 24 + 8, true)), mem.read(m.getBigInt64(off + i * 24 + 16, true)))
|
|
459
463
|
return map
|
|
460
464
|
}
|
|
461
|
-
|
|
462
|
-
return p
|
|
465
|
+
return i64ToF64(p) // canonical NaN-number / CLOSURE / unknown — reinterpret to f64
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
mem.write = function(p, data) {
|
|
@@ -468,7 +471,7 @@ export const memory = (src) => {
|
|
|
468
471
|
const cap = m.getInt32(off - 4, true)
|
|
469
472
|
if (data.length > cap) throw Error(`write: ${data.length} exceeds capacity ${cap}`)
|
|
470
473
|
m.setInt32(off - 8, data.length, true)
|
|
471
|
-
for (let i = 0; i < data.length; i++) m.
|
|
474
|
+
for (let i = 0; i < data.length; i++) m.setBigInt64(off + i * 8, bits(coerce(data[i])), true)
|
|
472
475
|
} else if (t === 3) {
|
|
473
476
|
const a2 = aux(p), elem = a2 & 7
|
|
474
477
|
const [, stride, , setter] = ELEM_BY_ID[elem]
|
|
@@ -488,7 +491,7 @@ export const memory = (src) => {
|
|
|
488
491
|
if (!schema) throw Error(`write: unknown schema`)
|
|
489
492
|
for (const k of Object.keys(data)) {
|
|
490
493
|
const i = schema.indexOf(k)
|
|
491
|
-
if (i >= 0) m.
|
|
494
|
+
if (i >= 0) m.setBigInt64(off + i * 8, bits(coerce(data[k])), true)
|
|
492
495
|
}
|
|
493
496
|
} else {
|
|
494
497
|
throw Error(`write: unsupported type ${t}`)
|
|
@@ -513,7 +516,9 @@ export const memory = (src) => {
|
|
|
513
516
|
for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
|
|
514
517
|
mem[name] = (data) => {
|
|
515
518
|
const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
|
|
516
|
-
|
|
519
|
+
// Same-type source → native memcpy via `.set` (incl. stride-1 Uint8Array:
|
|
520
|
+
// a multi-MB file copied byte-by-byte through DataView dominates decode).
|
|
521
|
+
if (TA[elemId] && data instanceof TA[elemId]) {
|
|
517
522
|
new TA[elemId](mem.buffer, off, n).set(data)
|
|
518
523
|
} else {
|
|
519
524
|
const m = dv()
|
|
@@ -523,6 +528,24 @@ export const memory = (src) => {
|
|
|
523
528
|
}
|
|
524
529
|
}
|
|
525
530
|
|
|
531
|
+
// Zero-copy input: reserve a typed-array region in wasm memory and return BOTH
|
|
532
|
+
// a live `view` over it and the NaN-box `box` pointer to pass as an argument.
|
|
533
|
+
// The caller fills `view` directly (one I/O-side copy, no second JS→wasm copy)
|
|
534
|
+
// and hands `box` to the export, which reads the bytes in place. Decoded typed
|
|
535
|
+
// arrays already come back as views (mem.read), so a decode can be copy-free
|
|
536
|
+
// end-to-end. LIFETIME: `view` is detached by any mem.grow() (alloc past the
|
|
537
|
+
// current buffer) and clobbered by mem.reset()/the next decode — re-derive a
|
|
538
|
+
// fresh view with mem.read(box) after growth, or copy out what must persist.
|
|
539
|
+
// Back the module with a shared memory (WebAssembly.Memory{shared:true}) to
|
|
540
|
+
// keep views valid across grow and to hand them to a worker/AudioWorklet.
|
|
541
|
+
mem.allocTyped = (Ctor, n) => {
|
|
542
|
+
const meta = ELEMS[Ctor?.name]
|
|
543
|
+
if (!meta) throw Error(`allocTyped: unsupported type ${Ctor?.name ?? Ctor}`)
|
|
544
|
+
const [elemId, stride] = meta
|
|
545
|
+
const bytes = n * stride, off = hdr(bytes, bytes, bytes)
|
|
546
|
+
return { view: new Ctor(mem.buffer, off, n), box: ptr(3, elemId, off) }
|
|
547
|
+
}
|
|
548
|
+
|
|
526
549
|
_enhanced.add(mem)
|
|
527
550
|
return mem
|
|
528
551
|
}
|
|
@@ -561,16 +584,24 @@ export const wrap = (memSrc, inst) => {
|
|
|
561
584
|
}
|
|
562
585
|
} catch { /* ignore */ }
|
|
563
586
|
}
|
|
587
|
+
// i64-carrier map: per export, which param positions ride i64 (BigInt) and whether the
|
|
588
|
+
// result does. The boxed (NaN-box) carrier crosses as i64 so JSC can't canonicalize the
|
|
589
|
+
// payload; we reinterpret BigInt↔f64 by bits at exactly those positions. A bigint result
|
|
590
|
+
// has no entry — its BigInt already IS the value. (Mirror of the test/data.js adapter.)
|
|
591
|
+
const i64Exp = new Map()
|
|
592
|
+
const i64Bytes = customSection(mod, 'jz:i64exp')
|
|
593
|
+
if (i64Bytes) {
|
|
594
|
+
try { for (const e of JSON.parse(td.decode(i64Bytes))) i64Exp.set(e.name, { p: new Set(e.p || []), r: !!e.r }) }
|
|
595
|
+
catch { /* ignore */ }
|
|
596
|
+
}
|
|
564
597
|
const mem = memory(memSrc)
|
|
565
598
|
const lastErrBits = realInst.exports.__jz_last_err_bits
|
|
566
599
|
const decodeThrown = error => {
|
|
567
600
|
if (!(error instanceof WebAssembly.Exception) || !lastErrBits) throw error
|
|
568
|
-
const
|
|
569
|
-
_u32[0] = Number(bits & 0xffffffffn)
|
|
570
|
-
_u32[1] = Number((bits >> 32n) & 0xffffffffn)
|
|
601
|
+
const errBits = lastErrBits.value // i64 bits (BigInt)
|
|
571
602
|
// Memoryless module: the thrown value is a number/atom/SSO string — decode it
|
|
572
603
|
// from bits. (A heap Error/string can only exist when the module has memory.)
|
|
573
|
-
const value = mem ? mem.read(
|
|
604
|
+
const value = mem ? mem.read(errBits) : decode(errBits)
|
|
574
605
|
if (value instanceof Error) throw value
|
|
575
606
|
const wrapped = new Error(typeof value === 'string' ? value : String(value))
|
|
576
607
|
wrapped.cause = error
|
|
@@ -582,21 +613,44 @@ export const wrap = (memSrc, inst) => {
|
|
|
582
613
|
// value straight through — `mem.wrapVal` would NaN-box it — substituting a
|
|
583
614
|
// jsstring literal default for a missing arg. Every other slot marshals via
|
|
584
615
|
// `box`: `coerce` for pure-scalar modules, `mem.wrapVal` for heap modules.
|
|
585
|
-
// Quiet-NaN ABI: every export value is f64, so there is no per-position i64
|
|
586
|
-
// carrier — args flow straight to the wasm call, results straight to decode/read.
|
|
587
616
|
const wrapArgAt = (ext, i, x, box) =>
|
|
588
617
|
ext?.has(i) ? (x === undefined && ext.def?.has(i) ? ext.def.get(i) : x) : box(x)
|
|
618
|
+
// Per-position arg marshaller: box the value, then for an i64-carrier param (per
|
|
619
|
+
// jz:i64exp) pass its i64 bits (a boxed value is already a BigInt; a numeric arg to a
|
|
620
|
+
// dynamic i64 param → its f64 bits). The box never materializes as f64, so JSC can't
|
|
621
|
+
// canonicalize it. Numeric/externref positions keep their f64/externref carrier.
|
|
622
|
+
const i64Arg = (ie, ext, box) => (x, i) => {
|
|
623
|
+
const w = wrapArgAt(ext, i, x, box)
|
|
624
|
+
if (ie && ie.p.has(i)) {
|
|
625
|
+
// i64-carrier slot: a string that coerce() left raw (scalar/memoryless module)
|
|
626
|
+
// must be NaN-box encoded. SSO handles ≤6 ASCII chars without heap memory; longer
|
|
627
|
+
// or non-ASCII strings need a heap that this module lacks — throw clearly.
|
|
628
|
+
if (typeof w === 'string') {
|
|
629
|
+
if (w.length > 6 || !/^[\x00-\x7f]*$/.test(w))
|
|
630
|
+
throw new Error('jz: string arg too long or non-ASCII for memoryless module — compile with a string operation to enable heap marshaling')
|
|
631
|
+
return encodeSSO(w)
|
|
632
|
+
}
|
|
633
|
+
return bits(w) // i64 param: pass the box bits
|
|
634
|
+
}
|
|
635
|
+
// f64 position: a box (BigInt) must reinterpret to f64. Happens for an un-wrapped export
|
|
636
|
+
// (e.g. a multi-value result skips wrapping) whose boxed param keeps the legacy f64 carrier
|
|
637
|
+
// — intact on V8; that path is inherently JSC-limited for boxed lanes anyway.
|
|
638
|
+
return typeof w === 'bigint' ? i64ToF64(w) : w
|
|
639
|
+
}
|
|
589
640
|
|
|
590
641
|
// Pure scalar module (no memory): pass f64 values directly, no marshaling
|
|
591
642
|
if (!mem || mem.scalar) {
|
|
592
643
|
for (const [name, fn] of Object.entries(realInst.exports)) {
|
|
593
644
|
if (typeof fn !== 'function') { exports[name] = fn; continue }
|
|
594
645
|
const ext = extExp.get(name)
|
|
646
|
+
const ie = i64Exp.get(name)
|
|
595
647
|
const len = fn.length
|
|
596
648
|
exports[name] = (...args) => {
|
|
597
649
|
while (args.length < len) args.push(undefined)
|
|
598
650
|
try {
|
|
599
|
-
|
|
651
|
+
const ret = fn(...args.map(i64Arg(ie, ext, coerce)))
|
|
652
|
+
// A bigint-value result returns raw; a boxed i64 result or an f64/number result decodes.
|
|
653
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : decode(ret)
|
|
600
654
|
} catch (e) { decodeThrown(e) }
|
|
601
655
|
}
|
|
602
656
|
}
|
|
@@ -607,23 +661,28 @@ export const wrap = (memSrc, inst) => {
|
|
|
607
661
|
if (restFuncs.has(name) && typeof fn === 'function') {
|
|
608
662
|
const fixed = restFuncs.get(name)
|
|
609
663
|
const ext = extExp.get(name)
|
|
664
|
+
const ie = i64Exp.get(name)
|
|
610
665
|
exports[name] = (...args) => {
|
|
611
|
-
const a = args.slice(0, fixed).map((
|
|
612
|
-
while (a.length < fixed) a.push(UNDEF_NAN)
|
|
613
|
-
|
|
666
|
+
const a = args.slice(0, fixed).map(i64Arg(ie, ext, memWrapVal))
|
|
667
|
+
while (a.length < fixed) { const i = a.length; a.push(ie && ie.p.has(i) ? UNDEF_NAN : i64ToF64(UNDEF_NAN)) }
|
|
668
|
+
const restArr = mem.Array(args.slice(fixed)) // BigInt box (i64 carrier)
|
|
669
|
+
a.push(ie && ie.p.has(fixed) ? restArr : i64ToF64(restArr))
|
|
614
670
|
try {
|
|
615
|
-
|
|
671
|
+
const ret = fn.apply(null, a)
|
|
672
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : mem.read(ret)
|
|
616
673
|
} catch (error) {
|
|
617
674
|
decodeThrown(error)
|
|
618
675
|
}
|
|
619
676
|
}
|
|
620
677
|
} else if (typeof fn === 'function') {
|
|
621
678
|
const ext = extExp.get(name)
|
|
679
|
+
const ie = i64Exp.get(name)
|
|
622
680
|
const len = fn.length
|
|
623
681
|
exports[name] = (...args) => {
|
|
624
682
|
while (args.length < len) args.push(undefined)
|
|
625
683
|
try {
|
|
626
|
-
|
|
684
|
+
const ret = fn.apply(null, args.map(i64Arg(ie, ext, memWrapVal)))
|
|
685
|
+
return typeof ret === 'bigint' && !(ie && ie.r) ? ret : mem.read(ret)
|
|
627
686
|
} catch (error) {
|
|
628
687
|
decodeThrown(error)
|
|
629
688
|
}
|
|
@@ -643,23 +702,22 @@ const prepareInterop = (opts) => {
|
|
|
643
702
|
// wrapped back to BigInt so the wasm side reinterprets a non-canonicalized
|
|
644
703
|
// bit pattern.
|
|
645
704
|
opts._interp.__ext_prop = (objBig, propBig) => {
|
|
646
|
-
const
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
return f64ToI64(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
|
|
705
|
+
const obj = state.extMap[offset(objBig)]
|
|
706
|
+
const prop = state.mem.read(propBig)
|
|
707
|
+
return bits(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
|
|
650
708
|
}
|
|
651
709
|
opts._interp.__ext_has = (objBig, propBig) => {
|
|
652
|
-
return (state.mem.read(
|
|
710
|
+
return (state.mem.read(propBig) in state.extMap[offset(objBig)]) ? 1 : 0
|
|
653
711
|
}
|
|
654
712
|
opts._interp.__ext_set = (objBig, propBig, valBig) => {
|
|
655
|
-
state.extMap[offset(
|
|
713
|
+
state.extMap[offset(objBig)][state.mem.read(propBig)] = state.mem.read(valBig)
|
|
656
714
|
return 1
|
|
657
715
|
}
|
|
658
716
|
opts._interp.__ext_call = (objBig, propBig, argsBig) => {
|
|
659
|
-
const obj = state.extMap[offset(
|
|
660
|
-
const prop = state.mem.read(
|
|
661
|
-
const args = state.mem.read(
|
|
662
|
-
return
|
|
717
|
+
const obj = state.extMap[offset(objBig)]
|
|
718
|
+
const prop = state.mem.read(propBig)
|
|
719
|
+
const args = state.mem.read(argsBig)
|
|
720
|
+
return bits(state.mem.wrapVal(obj[prop].apply(obj, args)))
|
|
663
721
|
}
|
|
664
722
|
return state
|
|
665
723
|
}
|
|
@@ -816,14 +874,11 @@ const buildImports = (mod, opts, state) => {
|
|
|
816
874
|
const fn = typeof spec === 'function' ? spec : (spec && typeof spec === 'object' ? spec.fn : null)
|
|
817
875
|
if (typeof fn === 'function')
|
|
818
876
|
imports[modName][name] = (...args) => {
|
|
819
|
-
// i64 carrier:
|
|
820
|
-
//
|
|
821
|
-
const decoded = args.map(a =>
|
|
822
|
-
const f = typeof a === 'bigint' ? i64ToF64(a) : a
|
|
823
|
-
return state.mem ? state.mem.read(f) : decode(f)
|
|
824
|
-
})
|
|
877
|
+
// i64 carrier: args arrive as BigInt bits (box) or number; decode with integer
|
|
878
|
+
// ops — never materialize a box as f64. Return the i64 bits of the wrapped result.
|
|
879
|
+
const decoded = args.map(a => state.mem ? state.mem.read(a) : decode(a))
|
|
825
880
|
const ret = fn.call(fns, ...decoded)
|
|
826
|
-
return
|
|
881
|
+
return bits(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
|
|
827
882
|
}
|
|
828
883
|
}
|
|
829
884
|
}
|
|
@@ -846,7 +901,7 @@ const buildImports = (mod, opts, state) => {
|
|
|
846
901
|
if (host !== undefined) {
|
|
847
902
|
if (!imports.env) imports.env = {}
|
|
848
903
|
let id = state.extMap.indexOf(host); if (id === -1) { id = state.extMap.length; state.extMap.push(host) }
|
|
849
|
-
imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false },
|
|
904
|
+
imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false }, ptr(11, 0, id))
|
|
850
905
|
}
|
|
851
906
|
}
|
|
852
907
|
}
|