jz 0.6.0 → 0.8.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.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
package/interop.js CHANGED
@@ -106,51 +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
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
+ export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
124
134
 
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]
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))
145
+ }
131
146
 
132
- // Coerce JS null/undefined NaN-boxed sentinels for WASM boundary
133
- export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
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
162
+ }
134
163
 
135
- // Decode f64 return value: null/undefined sentinels → JS values, numbers pass through
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`.
136
167
  const decode = v => {
137
- if (v === v) return v // fast path: non-NaN
138
- _f64[0] = v
139
- if (_u32[0] !== 0) return v
140
- if (_u32[1] === ATOM_HI[ATOM.NULL]) return null
141
- if (_u32[1] === ATOM_HI[ATOM.UNDEF]) return undefined
142
- if (_u32[1] === ATOM_HI[ATOM.FALSE]) return false
143
- if (_u32[1] === ATOM_HI[ATOM.TRUE]) return true
144
- return 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
145
180
  }
146
181
 
147
- export const ptr = (type, aux, offset) => {
148
- _u32[1] = encodePtrHi(type, aux)
149
- _u32[0] = offset >>> 0; return _f64[0]
150
- }
151
- export const offset = (p) => { _f64[0] = p; return _u32[0] }
152
- export const type = (p) => { _f64[0] = p; return decodePtrType(_u32[1]) }
153
- export const aux = (p) => { _f64[0] = p; return decodePtrAux(_u32[1]) }
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)
154
184
 
155
185
  // Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
156
186
  const ELEMS = {
@@ -196,7 +226,11 @@ export const memory = (src) => {
196
226
  // Instance result: { module, instance, exports, extMap }
197
227
  const raw = src?.instance?.exports || src?.exports || src
198
228
  mem = src?.exports?.memory || raw.memory
199
- if (!mem) return null // pure scalar module — no memory
229
+ // Memoryless module (SSO strings / atoms / numbers only — no linear memory):
230
+ // hand back a minimal reader instead of null so callers can still decode its
231
+ // boundary values from bits. `read`/`wrapVal` cover the value forms that exist
232
+ // without memory; `scalar` flags the fast path that skips heap marshaling.
233
+ if (!mem) return { read: decode, wrapVal: coerce, scalar: true }
200
234
  wasmExports = { ...raw, memory: mem }
201
235
  extMap = src.extMap || null
202
236
  mod = src.module || null
@@ -249,8 +283,7 @@ export const memory = (src) => {
249
283
  if (_enhanced.has(mem)) {
250
284
  mem.schemas = schemas
251
285
  if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
252
- if (wasmExports?._clear) mem.reset = wasmExports._clear
253
- else if (!mem.reset) mem.reset = jsReset
286
+ mem.reset = jsReset // post-init rewind — see the note at the first-enhance path
254
287
  if (extMap) mem._extMap = extMap
255
288
  return mem
256
289
  }
@@ -265,17 +298,19 @@ export const memory = (src) => {
265
298
  // NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
266
299
  // payload to 0x7FF8000000000000 — destroying the type/offset bits.
267
300
  const wrapped = new BigInt64Array(n)
268
- 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]))
269
302
  const dst = new BigInt64Array(mem.buffer, off, n)
270
303
  for (let i = 0; i < n; i++) dst[i] = wrapped[i]
271
304
  return ptr(1, 0, off)
272
305
  }
273
306
 
274
307
  mem.String = (str) => {
275
- if (str.length <= 4 && /^[\x00-\x7f]*$/.test(str)) {
276
- let packed = 0
277
- for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
278
- 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
279
314
  }
280
315
  const enc = TEXT_ENC.encode(str)
281
316
  const n = enc.length, raw = alloc(4 + n), m = dv()
@@ -298,9 +333,10 @@ export const memory = (src) => {
298
333
  if (v === null || v === undefined) return coerce(v)
299
334
  if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
300
335
  if (typeof v === 'string') return mem.String(v)
301
- // BigInt as a data value crosses the boundary as a decimal-string; wasm-side
302
- // numeric parsers accept string form.
303
- 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())
304
340
  if (Array.isArray(v)) return mem.Array(v)
305
341
  if (v instanceof ArrayBuffer) return mem.Buffer(v)
306
342
  if (v instanceof DataView) return mem.Buffer(v.buffer)
@@ -341,7 +377,7 @@ export const memory = (src) => {
341
377
  if (v === null || v === undefined) v = coerce(v)
342
378
  else if (typeof v === 'string') v = mem.String(v)
343
379
  else if (Array.isArray(v)) v = mem.Array(v)
344
- wrapped[i] = f64ToI64(v)
380
+ wrapped[i] = bits(v)
345
381
  }
346
382
  const dst = new BigInt64Array(mem.buffer, raw, n)
347
383
  for (let i = 0; i < n; i++) dst[i] = wrapped[i]
@@ -350,8 +386,15 @@ export const memory = (src) => {
350
386
 
351
387
  mem.read = function(p) {
352
388
  if (Array.isArray(p)) return p.map(v => mem.read(v)) // multi-value tuple
353
- if (p === p) return p // regular number passthrough (NaN fails ===)
354
- 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)
355
398
  if (t === 0 && off === 0) {
356
399
  if (a === 1) return null
357
400
  if (a === 2) return undefined
@@ -360,19 +403,18 @@ export const memory = (src) => {
360
403
  }
361
404
  if (t === 11 && mem._extMap) return mem._extMap[off]
362
405
  if (t === 1) { // ARRAY
363
- let m = dv(), aOff = off
406
+ let aOff = off
364
407
  // Follow forwarding pointers (cap === -1 means array was reallocated)
365
408
  while (m.getInt32(aOff - 4, true) === -1) aOff = m.getInt32(aOff - 8, true)
366
409
  const len = m.getInt32(aOff - 8, true), out = new Array(len)
367
- 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))
368
411
  return out
369
412
  }
370
413
  if (t === 3) { // TYPED
371
- const a2 = aux(p), elem = a2 & 7
414
+ const elem = a & 7
372
415
  const [, stride] = ELEM_BY_ID[elem]
373
416
  const Ctor = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array][elem]
374
- const m = dv()
375
- if (a2 & 8) {
417
+ if (a & 8) {
376
418
  const byteLen = m.getInt32(off, true), dataOff = m.getInt32(off + 4, true)
377
419
  return new Ctor(mem.buffer, dataOff, byteLen / stride)
378
420
  }
@@ -380,61 +422,47 @@ export const memory = (src) => {
380
422
  return new Ctor(mem.buffer, off, byteLen / stride)
381
423
  }
382
424
  if (t === 2) { // BUFFER
383
- const byteLen = dv().getInt32(off - 8, true)
425
+ const byteLen = m.getInt32(off - 8, true)
384
426
  const out = new ArrayBuffer(byteLen)
385
427
  new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
386
428
  return out
387
429
  }
388
430
  if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
389
- const a2 = aux(p)
390
- if (a2 & LAYOUT.SSO_BIT) {
391
- const len = a2 & 0x7; let s = ''
392
- for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
393
- return s
394
- }
395
- const len = dv().getInt32(off - 4, true)
431
+ if (a & LAYOUT.SSO_BIT) return decodeSSO(p)
432
+ const len = m.getInt32(off - 4, true)
396
433
  return TEXT_DEC.decode(new Uint8Array(mem.buffer, off, len))
397
434
  }
398
435
  if (t === 6) { // OBJECT
399
- const m = dv(), sid = aux(p), keys = mem.schemas[sid]
436
+ const keys = mem.schemas[a]
400
437
  if (!keys) return p
401
438
  const obj = {}
402
- 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))
403
440
  return obj
404
441
  }
405
442
  if (t === 7) { // HASH
406
- const m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
407
- const obj = {}
443
+ const size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true), obj = {}
408
444
  for (let i = 0, found = 0; i < cap && found < size; i++) {
409
- const hash = m.getFloat64(off + i * 24, true)
410
- if (hash !== 0) {
411
- const key = mem.read(m.getFloat64(off + i * 24 + 8, true))
412
- 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))
413
447
  found++
414
448
  }
415
449
  }
416
450
  return obj
417
451
  }
418
452
  if (t === 8) { // SET
419
- const m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
420
- const set = new Set()
421
- for (let i = 0; i < cap && set.size < size; i++) {
422
- const hash = m.getFloat64(off + i * 16, true)
423
- if (hash !== 0) set.add(mem.read(m.getFloat64(off + i * 16 + 8, true)))
424
- }
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)))
425
456
  return set
426
457
  }
427
458
  if (t === 9) { // MAP
428
- const m = dv(), size = m.getInt32(off - 8, true), cap = m.getInt32(off - 4, true)
429
- const map = new Map()
430
- for (let i = 0; i < cap && map.size < size; i++) {
431
- const hash = m.getFloat64(off + i * 24, true)
432
- if (hash !== 0) map.set(mem.read(m.getFloat64(off + i * 24 + 8, true)), mem.read(m.getFloat64(off + i * 24 + 16, true)))
433
- }
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)))
434
463
  return map
435
464
  }
436
- if (t === 10) return p // CLOSURE
437
- return p
465
+ return i64ToF64(p) // canonical NaN-number / CLOSURE / unknown — reinterpret to f64
438
466
  }
439
467
 
440
468
  mem.write = function(p, data) {
@@ -443,7 +471,7 @@ export const memory = (src) => {
443
471
  const cap = m.getInt32(off - 4, true)
444
472
  if (data.length > cap) throw Error(`write: ${data.length} exceeds capacity ${cap}`)
445
473
  m.setInt32(off - 8, data.length, true)
446
- 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)
447
475
  } else if (t === 3) {
448
476
  const a2 = aux(p), elem = a2 & 7
449
477
  const [, stride, , setter] = ELEM_BY_ID[elem]
@@ -463,7 +491,7 @@ export const memory = (src) => {
463
491
  if (!schema) throw Error(`write: unknown schema`)
464
492
  for (const k of Object.keys(data)) {
465
493
  const i = schema.indexOf(k)
466
- 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)
467
495
  }
468
496
  } else {
469
497
  throw Error(`write: unsupported type ${t}`)
@@ -471,7 +499,14 @@ export const memory = (src) => {
471
499
  }
472
500
 
473
501
  mem.alloc = alloc
474
- mem.reset = wasmExports?._clear || jsReset
502
+ // Rewind to the JS-captured post-init heap mark, NOT the wasm `_clear` (which
503
+ // rewinds to the static-data end and would clobber module-global heap values —
504
+ // a top-level `let o = {…}` — on the first alloc after reset). `jsReset`'s base
505
+ // is `$__heap` read after instantiation (start ran), i.e. exactly the high-water
506
+ // mark above all module-init allocations. Both share `$__heap`, so a wasm `_alloc`
507
+ // and this reset stay consistent. (Shared memory has no `$__heap` global → base
508
+ // is the fixed start, preserving prior behavior.)
509
+ mem.reset = jsReset
475
510
 
476
511
  // TypedArray constructors: memory.Float64Array(data), etc.
477
512
  // Bulk-copy path: when input is a TypedArray whose element type matches
@@ -481,7 +516,9 @@ export const memory = (src) => {
481
516
  for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
482
517
  mem[name] = (data) => {
483
518
  const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
484
- 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]) {
485
522
  new TA[elemId](mem.buffer, off, n).set(data)
486
523
  } else {
487
524
  const m = dv()
@@ -491,6 +528,24 @@ export const memory = (src) => {
491
528
  }
492
529
  }
493
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
+
494
549
  _enhanced.add(mem)
495
550
  return mem
496
551
  }
@@ -529,14 +584,24 @@ export const wrap = (memSrc, inst) => {
529
584
  }
530
585
  } catch { /* ignore */ }
531
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
+ }
532
597
  const mem = memory(memSrc)
533
598
  const lastErrBits = realInst.exports.__jz_last_err_bits
534
599
  const decodeThrown = error => {
535
600
  if (!(error instanceof WebAssembly.Exception) || !lastErrBits) throw error
536
- const bits = lastErrBits.value
537
- _u32[0] = Number(bits & 0xffffffffn)
538
- _u32[1] = Number((bits >> 32n) & 0xffffffffn)
539
- const value = mem ? mem.read(_f64[0]) : _f64[0]
601
+ const errBits = lastErrBits.value // i64 bits (BigInt)
602
+ // Memoryless module: the thrown value is a number/atom/SSO string — decode it
603
+ // from bits. (A heap Error/string can only exist when the module has memory.)
604
+ const value = mem ? mem.read(errBits) : decode(errBits)
540
605
  if (value instanceof Error) throw value
541
606
  const wrapped = new Error(typeof value === 'string' ? value : String(value))
542
607
  wrapped.cause = error
@@ -548,21 +613,44 @@ export const wrap = (memSrc, inst) => {
548
613
  // value straight through — `mem.wrapVal` would NaN-box it — substituting a
549
614
  // jsstring literal default for a missing arg. Every other slot marshals via
550
615
  // `box`: `coerce` for pure-scalar modules, `mem.wrapVal` for heap modules.
551
- // Quiet-NaN ABI: every export value is f64, so there is no per-position i64
552
- // carrier — args flow straight to the wasm call, results straight to decode/read.
553
616
  const wrapArgAt = (ext, i, x, box) =>
554
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
+ }
555
640
 
556
641
  // Pure scalar module (no memory): pass f64 values directly, no marshaling
557
- if (!mem) {
642
+ if (!mem || mem.scalar) {
558
643
  for (const [name, fn] of Object.entries(realInst.exports)) {
559
644
  if (typeof fn !== 'function') { exports[name] = fn; continue }
560
645
  const ext = extExp.get(name)
646
+ const ie = i64Exp.get(name)
561
647
  const len = fn.length
562
648
  exports[name] = (...args) => {
563
649
  while (args.length < len) args.push(undefined)
564
650
  try {
565
- 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)
566
654
  } catch (e) { decodeThrown(e) }
567
655
  }
568
656
  }
@@ -573,23 +661,28 @@ export const wrap = (memSrc, inst) => {
573
661
  if (restFuncs.has(name) && typeof fn === 'function') {
574
662
  const fixed = restFuncs.get(name)
575
663
  const ext = extExp.get(name)
664
+ const ie = i64Exp.get(name)
576
665
  exports[name] = (...args) => {
577
- const a = args.slice(0, fixed).map((x, i) => wrapArgAt(ext, i, x, memWrapVal))
578
- while (a.length < fixed) a.push(UNDEF_NAN)
579
- 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))
580
670
  try {
581
- 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)
582
673
  } catch (error) {
583
674
  decodeThrown(error)
584
675
  }
585
676
  }
586
677
  } else if (typeof fn === 'function') {
587
678
  const ext = extExp.get(name)
679
+ const ie = i64Exp.get(name)
588
680
  const len = fn.length
589
681
  exports[name] = (...args) => {
590
682
  while (args.length < len) args.push(undefined)
591
683
  try {
592
- 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)
593
686
  } catch (error) {
594
687
  decodeThrown(error)
595
688
  }
@@ -609,23 +702,22 @@ const prepareInterop = (opts) => {
609
702
  // wrapped back to BigInt so the wasm side reinterprets a non-canonicalized
610
703
  // bit pattern.
611
704
  opts._interp.__ext_prop = (objBig, propBig) => {
612
- const objPtr = i64ToF64(objBig), propPtr = i64ToF64(propBig)
613
- const obj = state.extMap[offset(objPtr)]
614
- const prop = state.mem.read(propPtr)
615
- 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]))
616
708
  }
617
709
  opts._interp.__ext_has = (objBig, propBig) => {
618
- 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
619
711
  }
620
712
  opts._interp.__ext_set = (objBig, propBig, valBig) => {
621
- 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)
622
714
  return 1
623
715
  }
624
716
  opts._interp.__ext_call = (objBig, propBig, argsBig) => {
625
- const obj = state.extMap[offset(i64ToF64(objBig))]
626
- const prop = state.mem.read(i64ToF64(propBig))
627
- const args = state.mem.read(i64ToF64(argsBig))
628
- 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)))
629
721
  }
630
722
  return state
631
723
  }
@@ -649,7 +741,7 @@ const installDefaultEnvImports = (mod, imports, state) => {
649
741
  // across the wasm→JS boundary (see module/console.js header). Reinterpret
650
742
  // the BigInt's bits as f64 here so mem.read sees the original NaN-box.
651
743
  const write = (valBig, fd, sep) => {
652
- const v = state.mem.read(i64ToF64(valBig))
744
+ const v = readArgBits(state, valBig)
653
745
  buf[fd] += String(v)
654
746
  if (sep === 32) buf[fd] += ' '
655
747
  else if (sep === 10) flush(fd)
@@ -679,13 +771,13 @@ const installDefaultEnvImports = (mod, imports, state) => {
679
771
  }
680
772
  if (envFns.has('parseFloat') && !imports.env.parseFloat) {
681
773
  imports.env.parseFloat = (valBig) => {
682
- const s = state.mem.read(i64ToF64(valBig))
774
+ const s = readArgBits(state, valBig)
683
775
  return parseFloat(s)
684
776
  }
685
777
  }
686
778
  if (envFns.has('parseInt') && !imports.env.parseInt) {
687
779
  imports.env.parseInt = (valBig, radix) => {
688
- const s = state.mem.read(i64ToF64(valBig))
780
+ const s = readArgBits(state, valBig)
689
781
  return parseInt(s, radix || undefined)
690
782
  }
691
783
  }
@@ -782,14 +874,11 @@ const buildImports = (mod, opts, state) => {
782
874
  const fn = typeof spec === 'function' ? spec : (spec && typeof spec === 'object' ? spec.fn : null)
783
875
  if (typeof fn === 'function')
784
876
  imports[modName][name] = (...args) => {
785
- // i64 carrier: reinterpret BigInt bits f64 NaN-box. Pure-scalar modules
786
- // have no memory so skip mem.read; the f64 IS the JS number for numerics.
787
- const decoded = args.map(a => {
788
- const f = typeof a === 'bigint' ? i64ToF64(a) : a
789
- return state.mem ? state.mem.read(f) : decode(f)
790
- })
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))
791
880
  const ret = fn.call(fns, ...decoded)
792
- return f64ToI64(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
881
+ return bits(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
793
882
  }
794
883
  }
795
884
  }
@@ -812,7 +901,7 @@ const buildImports = (mod, opts, state) => {
812
901
  if (host !== undefined) {
813
902
  if (!imports.env) imports.env = {}
814
903
  let id = state.extMap.indexOf(host); if (id === -1) { id = state.extMap.length; state.extMap.push(host) }
815
- 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))
816
905
  }
817
906
  }
818
907
  }
@@ -834,7 +923,11 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
834
923
  const enhanced = memory(memSrc)
835
924
  state.mem = enhanced
836
925
  state.flushPrint?.()
837
- return { exports: wrap(memSrc), memory: enhanced, instance: inst, module: mod }
926
+ // A memoryless module keeps a minimal reader internally (state.mem, for decoding
927
+ // its SSO/atom boundary values), but the result's `.memory` stays null — the
928
+ // module genuinely exposes no linear memory. `jz.memory(result)` still hands back
929
+ // a usable reader on demand.
930
+ return { exports: wrap(memSrc), memory: enhanced?.scalar ? null : enhanced, instance: inst, module: mod }
838
931
  }
839
932
 
840
933
  /**
@@ -849,23 +942,29 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
849
942
  * @param {object} [opts] host options: imports, memory, _interp, host-shape flags
850
943
  * @returns {{ exports, memory, instance, module }}
851
944
  */
945
+ /**
946
+ * Compile wasm bytes to a `WebAssembly.Module`, preferring native
947
+ * `wasm:js-string` builtins when the engine honors the option (V8 17+/Safari
948
+ * 18.4+; older engines throw or ignore it — try-fallback handles both). A
949
+ * `WebAssembly.Module` passed in is returned as-is. Factor it out so callers
950
+ * can compile once and instantiate many times without re-validating the bytes
951
+ * (`instantiate(toModule(wasm))` skips the per-call compile on hot loops).
952
+ *
953
+ * @param {Uint8Array|ArrayBuffer|WebAssembly.Module} wasm
954
+ * @returns {WebAssembly.Module}
955
+ */
956
+ export const toModule = (wasm) => {
957
+ if (wasm instanceof WebAssembly.Module) return wasm
958
+ if (jssProbeNative()) {
959
+ try { return new WebAssembly.Module(wasm, { builtins: ['js-string'] }) }
960
+ catch { return new WebAssembly.Module(wasm) }
961
+ }
962
+ return new WebAssembly.Module(wasm)
963
+ }
964
+
852
965
  export const instantiate = (wasm, opts = {}) => {
853
966
  const state = prepareInterop(opts)
854
- // Prefer native `wasm:js-string` builtins when the engine honors the option.
855
- // The option is silently accepted by V8 17+/Safari 18.4+; older engines that
856
- // don't recognize it either throw or ignore it — try-fallback handles both.
857
- let mod
858
- if (wasm instanceof WebAssembly.Module) {
859
- mod = wasm
860
- } else if (jssProbeNative()) {
861
- try {
862
- mod = new WebAssembly.Module(wasm, { builtins: ['js-string'] })
863
- } catch {
864
- mod = new WebAssembly.Module(wasm)
865
- }
866
- } else {
867
- mod = new WebAssembly.Module(wasm)
868
- }
967
+ const mod = toModule(wasm)
869
968
  const { imports, needsWasi } = buildImports(mod, opts, state)
870
969
  const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
871
970
  const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)