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.
Files changed (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. 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 "./src/wat/optimize.js";
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). Disabling the vectorizeLaneLocal pass suppresses every
540
- // jz-emitted v128 (lane maps, reductions incl. reduceUnroll, byte scans);
541
- // explicit f32x4/i32x4 intrinsics in source are the user's own opt-in and stay.
542
- // Applied after auto-config so it wins over any re-resolved preset.
543
- if (opts.noSimd) ctx.transform.optimize.vectorizeLaneLocal = false
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: true }
598
- else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = true
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-boxing encode/decodeshared 8-byte scratch buffer
109
+ // NaN-box codecinteger / 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
- // Cross-typed-array view for i64↔f64 reinterpretation. Used at every wasm↔JS
112
- // boundary that carries a NaN-boxed pointer as i64 bits V8 may canonicalize
113
- // f64 NaN payloads at the boundary, so the carrier is BigInt and reinterpret
114
- // runs once on each side. Separate buffer so it never aliases _u32/_f64.
115
- const _bi64 = (() => {
116
- const ab = new ArrayBuffer(8), bi = new BigInt64Array(ab), fv = new Float64Array(ab)
117
- return {
118
- i64ToF64: (big) => { bi[0] = big; return fv[0] },
119
- f64ToI64: (f) => { fv[0] = f; return bi[0] },
120
- }
121
- })()
122
- export const i64ToF64 = _bi64.i64ToF64
123
- export const f64ToI64 = _bi64.f64ToI64
124
-
125
- // Reserved atoms (type=0, offset=0): aux=1 → null, aux=2 → undefined.
126
- // Distinct from 0, JS NaN (payload=0), and all pointers.
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
- // Memory-free decode of an f64 boundary value: numbers pass through, the reserved
136
- // atoms become null/undefined/false/true, and an SSO string unpacks its ≤4 inline
137
- // ASCII bytes straight from the NaN payload. These are exactly the value forms a
138
- // *memoryless* module can carry across the boundary a heap string/array/object
139
- // would need linear memory to exist, so it can't reach here. Heap-carrying modules
140
- // route through `mem.read` instead (which also handles these, via memory).
141
- const decode = v => {
142
- if (v === v) return v // fast path: non-NaN number
143
- _f64[0] = v
144
- // STRING (type 4) + SSO bit: content is the low 32 bits, length the low aux bits.
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
- // Decode a boundary value that arrives as i64 bits (BigInt carrier) into a JS
162
- // value. Heap-carrying modules go through `mem.read`; a memoryless module's value
163
- // is a number/atom/SSO string, decodable from the bits alone.
164
- const readArgBits = (state, big) => {
165
- const f = i64ToF64(big)
166
- return state.mem ? state.mem.read(f) : decode(f)
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
- export const ptr = (type, aux, offset) => {
170
- _u32[1] = encodePtrHi(type, aux)
171
- _u32[0] = offset >>> 0; return _f64[0]
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
- export const offset = (p) => { _f64[0] = p; return _u32[0] }
174
- export const type = (p) => { _f64[0] = p; return decodePtrType(_u32[1]) }
175
- export const aux = (p) => { _f64[0] = p; return decodePtrAux(_u32[1]) }
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] = f64ToI64(mem.wrapVal(data[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 <= 4 && /^[\x00-\x7f]*$/.test(str)) {
301
- let packed = 0
302
- for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
303
- return ptr(4, LAYOUT.SSO_BIT | str.length, packed) // STRING + SSO_BIT
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 as a data value crosses the boundary as a decimal-string; wasm-side
327
- // numeric parsers accept string form.
328
- if (typeof v === 'bigint') return mem.String(v.toString())
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] = f64ToI64(v)
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 === p) return p // regular number passthrough (NaN fails ===)
379
- const t = type(p), a = aux(p), off = offset(p)
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 m = dv(), aOff = off
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.getFloat64(aOff + i * 8, true))
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 a2 = aux(p), elem = a2 & 7
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
- const m = dv()
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 = dv().getInt32(off - 8, true)
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
- const a2 = aux(p)
415
- if (a2 & LAYOUT.SSO_BIT) {
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 m = dv(), sid = aux(p), keys = mem.schemas[sid]
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.getFloat64(off + i * 8, true))
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 m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
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
- const hash = m.getFloat64(off + i * 24, true)
435
- if (hash !== 0) {
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 m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
445
- const set = new Set()
446
- for (let i = 0; i < cap && set.size < size; i++) {
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 m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
454
- const map = new Map()
455
- for (let i = 0; i < cap && map.size < size; i++) {
456
- const hash = m.getFloat64(off + i * 24, true)
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
- if (t === 10) return p // CLOSURE
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.setFloat64(off + i * 8, coerce(data[i]), true)
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.setFloat64(off + i * 8, coerce(data[k]), true)
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
- if (stride >= 2 && TA[elemId] && data instanceof TA[elemId]) {
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 bits = lastErrBits.value
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(_f64[0]) : decode(_f64[0])
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
- return decode(fn(...args.map((x, i) => wrapArgAt(ext, i, x, coerce))))
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((x, i) => wrapArgAt(ext, i, x, memWrapVal))
612
- while (a.length < fixed) a.push(UNDEF_NAN)
613
- a.push(mem.Array(args.slice(fixed)))
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
- return mem.read(fn.apply(null, a))
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
- return mem.read(fn.apply(null, args.map((x, i) => wrapArgAt(ext, i, x, memWrapVal))))
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 objPtr = i64ToF64(objBig), propPtr = i64ToF64(propBig)
647
- const obj = state.extMap[offset(objPtr)]
648
- const prop = state.mem.read(propPtr)
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(i64ToF64(propBig)) in state.extMap[offset(i64ToF64(objBig))]) ? 1 : 0
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(i64ToF64(objBig))][state.mem.read(i64ToF64(propBig))] = state.mem.read(i64ToF64(valBig))
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(i64ToF64(objBig))]
660
- const prop = state.mem.read(i64ToF64(propBig))
661
- const args = state.mem.read(i64ToF64(argsBig))
662
- return f64ToI64(state.mem.wrapVal(obj[prop].apply(obj, args)))
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: reinterpret BigInt bits f64 NaN-box. Pure-scalar modules
820
- // have no memory so skip mem.read; the f64 IS the JS number for numerics.
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 f64ToI64(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
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 }, f64ToI64(ptr(11, 0, id)))
904
+ imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false }, ptr(11, 0, id))
850
905
  }
851
906
  }
852
907
  }