jz 0.1.1 → 0.2.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/src/host.js CHANGED
@@ -20,10 +20,23 @@ import { wasi } from '../wasi.js'
20
20
 
21
21
  // NaN-boxing encode/decode — shared 8-byte scratch buffer
22
22
  const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf)
23
- // Sentinel NaN for "undefined/missing arg" (payload=1, distinct from JS NaN payload=0)
24
- _u32[1] = 0x7FF80000; _u32[0] = 1; export const UNDEF_NAN = _f64[0]
25
- // Null NaN: type=0 (ATOM), aux=1, offset=0 distinct from 0, NaN, and UNDEF_NAN
23
+ // Cross-typed-array view for i64↔f64 reinterpretation. Used at every wasm↔JS
24
+ // boundary that carries a NaN-boxed pointer as i64 bits — V8 may canonicalize
25
+ // f64 NaN payloads at the boundary, so the carrier is BigInt and reinterpret
26
+ // runs once on each side. Separate buffer so it never aliases _u32/_f64.
27
+ const _bi64 = (() => {
28
+ const ab = new ArrayBuffer(8), bi = new BigInt64Array(ab), fv = new Float64Array(ab)
29
+ return {
30
+ i64ToF64: (big) => { bi[0] = big; return fv[0] },
31
+ f64ToI64: (f) => { fv[0] = f; return bi[0] },
32
+ }
33
+ })()
34
+ export const i64ToF64 = _bi64.i64ToF64
35
+ export const f64ToI64 = _bi64.f64ToI64
36
+ // Reserved atoms (type=0, offset=0): aux=1 → null, aux=2 → undefined.
37
+ // Distinct from 0, JS NaN (payload=0), and all pointers.
26
38
  _u32[1] = 0x7FF80001; _u32[0] = 0; export const NULL_NAN = _f64[0]
39
+ _u32[1] = 0x7FF80002; _u32[0] = 0; export const UNDEF_NAN = _f64[0]
27
40
 
28
41
  // Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
29
42
  export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
@@ -32,8 +45,9 @@ export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN :
32
45
  const decode = v => {
33
46
  if (v === v) return v // fast path: non-NaN
34
47
  _f64[0] = v
35
- if (_u32[1] === 0x7FF80001 && _u32[0] === 0) return null
36
- if (_u32[1] === 0x7FF80000 && _u32[0] === 1) return undefined
48
+ if (_u32[0] !== 0) return v
49
+ if (_u32[1] === 0x7FF80001) return null
50
+ if (_u32[1] === 0x7FF80002) return undefined
37
51
  return v
38
52
  }
39
53
 
@@ -99,16 +113,22 @@ export const memory = (src) => {
99
113
 
100
114
  // JS-side bump allocator (heap ptr at byte 1020, same convention as WASM)
101
115
  const jsAlloc = (bytes) => {
102
- const d = dv(), p = d.getInt32(1020, true)
116
+ let d = dv(), p = d.getInt32(1020, true)
103
117
  const aligned = (p + 7) & ~7 // 8-byte align
104
118
  const next = aligned + bytes
105
- if (next > mem.buffer.byteLength) mem.grow(Math.ceil((next - mem.buffer.byteLength) / 65536))
119
+ if (next > mem.buffer.byteLength) {
120
+ mem.grow(Math.ceil((next - mem.buffer.byteLength) / 65536))
121
+ d = dv() // buffer was detached by grow
122
+ }
106
123
  d.setInt32(1020, next, true)
107
124
  return aligned
108
125
  }
109
126
 
110
127
  // Use WASM allocator if available, else JS-side bump
111
128
  let alloc = wasmExports?._alloc || jsAlloc
129
+ // JS-side reset: rewind the bump pointer at byte 1020. Only used when no WASM
130
+ // _clear is present (otherwise the WASM global / shared slot is authoritative).
131
+ const jsReset = () => dv().setInt32(1020, 1024, true)
112
132
 
113
133
  // Initialize heap pointer if not yet set
114
134
  const initDv = dv()
@@ -150,7 +170,8 @@ export const memory = (src) => {
150
170
  if (_enhanced.has(mem)) {
151
171
  mem.schemas = schemas
152
172
  if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
153
- if (wasmExports?._reset) mem.reset = wasmExports._reset
173
+ if (wasmExports?._clear) mem.reset = wasmExports._clear
174
+ else if (!mem.reset) mem.reset = jsReset
154
175
  if (extMap) mem._extMap = extMap
155
176
  return mem
156
177
  }
@@ -160,8 +181,14 @@ export const memory = (src) => {
160
181
  mem._extMap = extMap
161
182
 
162
183
  mem.Array = (data) => {
163
- const n = data.length, off = hdr(n, n, n * 8), m = dv()
164
- for (let i = 0; i < n; i++) m.setFloat64(off + i * 8, mem.wrapVal(data[i]), true)
184
+ const n = data.length, off = hdr(n, n, n * 8)
185
+ // Stage as i64 bits, not as JS Numbers: V8 may transition a JS Array holding
186
+ // NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
187
+ // payload to 0x7FF8000000000000 — destroying the type/offset bits.
188
+ const wrapped = new BigInt64Array(n)
189
+ for (let i = 0; i < n; i++) wrapped[i] = f64ToI64(mem.wrapVal(data[i]))
190
+ const dst = new BigInt64Array(mem.buffer, off, n)
191
+ for (let i = 0; i < n; i++) dst[i] = wrapped[i]
165
192
  return ptr(1, 0, off)
166
193
  }
167
194
 
@@ -169,7 +196,7 @@ export const memory = (src) => {
169
196
  if (str.length <= 4 && /^[\x00-\x7f]*$/.test(str)) {
170
197
  let packed = 0
171
198
  for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
172
- return ptr(5, str.length, packed) // SSO
199
+ return ptr(4, 0x4000 | str.length, packed) // STRING + SSO_BIT
173
200
  }
174
201
  const enc = new TextEncoder().encode(str)
175
202
  const n = enc.length, raw = alloc(4 + n), m = dv()
@@ -192,6 +219,10 @@ export const memory = (src) => {
192
219
  if (v === null || v === undefined) return coerce(v)
193
220
  if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
194
221
  if (typeof v === 'string') return this.String(v)
222
+ // BigInt as a data value crosses the boundary as a decimal-string. wasm-side
223
+ // numeric parsers accept string form. Direct i64 function args go through this
224
+ // path too; the call wrappers below detect BigInt before adaptArgs reinterprets.
225
+ if (typeof v === 'bigint') return this.String(v.toString())
195
226
  if (Array.isArray(v)) return this.Array(v)
196
227
  if (v instanceof ArrayBuffer) return this.Buffer(v)
197
228
  if (v instanceof DataView) return this.Buffer(v.buffer)
@@ -223,14 +254,19 @@ export const memory = (src) => {
223
254
  else if (this._extMap) return this.External(obj)
224
255
  else throw Error(`No schema for {${key}}`)
225
256
  }
226
- const schema = schemas[sid], n = schema.length, raw = alloc(n * 8), m = dv()
257
+ const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
258
+ // Stage as i64 bits so V8 can't canonicalize NaN-payload pointers across
259
+ // recursive allocations. See mem.Array for the same pattern.
260
+ const wrapped = new BigInt64Array(n)
227
261
  for (let i = 0; i < n; i++) {
228
262
  let v = obj[schema[i]]
229
263
  if (v === null || v === undefined) v = coerce(v)
230
264
  else if (typeof v === 'string') v = this.String(v)
231
265
  else if (Array.isArray(v)) v = this.Array(v)
232
- m.setFloat64(raw + i * 8, v, true)
266
+ wrapped[i] = f64ToI64(v)
233
267
  }
268
+ const dst = new BigInt64Array(mem.buffer, raw, n)
269
+ for (let i = 0; i < n; i++) dst[i] = wrapped[i]
234
270
  return ptr(6, sid, raw)
235
271
  }
236
272
 
@@ -238,8 +274,10 @@ export const memory = (src) => {
238
274
  if (Array.isArray(p)) return p.map(v => this.read(v)) // multi-value tuple
239
275
  if (p === p) return p // regular number passthrough (NaN fails ===)
240
276
  const t = type(p), a = aux(p), off = offset(p)
241
- if (t === 0 && a === 1 && off === 0) return null
242
- if (t === 0 && a === 0 && off === 1) return undefined
277
+ if (t === 0 && off === 0) {
278
+ if (a === 1) return null
279
+ if (a === 2) return undefined
280
+ }
243
281
  if (t === 11 && this._extMap) return this._extMap[off]
244
282
  if (t === 1) { // ARRAY
245
283
  let m = dv(), aOff = off
@@ -267,15 +305,16 @@ export const memory = (src) => {
267
305
  new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
268
306
  return out
269
307
  }
270
- if (t === 4) { // STRING (heap)
308
+ if (t === 4) { // STRING (aux bit 0x4000 = SSO inline, else heap)
309
+ const a2 = aux(p)
310
+ if (a2 & 0x4000) {
311
+ const len = a2 & 0x7; let s = ''
312
+ for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
313
+ return s
314
+ }
271
315
  const len = dv().getInt32(off - 4, true)
272
316
  return new TextDecoder().decode(new Uint8Array(mem.buffer, off, len))
273
317
  }
274
- if (t === 5) { // STRING_SSO
275
- const len = aux(p); let s = ''
276
- for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
277
- return s
278
- }
279
318
  if (t === 6) { // OBJECT
280
319
  const m = dv(), sid = aux(p), keys = this.schemas[sid]
281
320
  if (!keys) return p
@@ -352,7 +391,7 @@ export const memory = (src) => {
352
391
  }
353
392
 
354
393
  mem.alloc = alloc
355
- mem.reset = wasmExports?._reset || null
394
+ mem.reset = wasmExports?._clear || jsReset
356
395
 
357
396
  // TypedArray constructors: memory.Float64Array(data), etc.
358
397
  for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
@@ -382,6 +421,17 @@ export const wrap = (memSrc, inst) => {
382
421
  restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
383
422
  } catch (e) { /* ignore */ }
384
423
  }
424
+ // i64-ABI exports: boundary-wrapped funcs whose NaN-boxed pointer params/
425
+ // result ride i64 to dodge V8's NaN canonicalization. Map: name → { p, r }
426
+ // with p = bit mask of i64 params, r = 1 iff result is i64. JS side
427
+ // reinterprets f64↔BigInt only at those positions (see
428
+ // synthesizeBoundaryWrappers).
429
+ const i64Exp = new Map(), EMPTY_SET = new Set()
430
+ const i64Secs = WebAssembly.Module.customSections(mod, 'jz:i64exp')
431
+ if (i64Secs.length) {
432
+ try { for (const e of JSON.parse(new TextDecoder().decode(i64Secs[0]))) i64Exp.set(e.name, { p: new Set(e.p), r: e.r }) }
433
+ catch { /* ignore */ }
434
+ }
385
435
 
386
436
  const mem = memory(memSrc)
387
437
  const lastErrBits = realInst.exports.__jz_last_err_bits
@@ -398,32 +448,87 @@ export const wrap = (memSrc, inst) => {
398
448
  throw wrapped
399
449
  }
400
450
  const exports = {}
451
+ // Per-position carrier swap: f64 stays Number, i64 positions reinterpret to
452
+ // BigInt before the call and back to Number after. p is a Set of i64 param
453
+ // indices; r = result is i64. Numeric (f64) positions pass through unchanged.
454
+ const adaptArgs = (a, p) => p.size === 0 ? a : a.map((x, i) => p.has(i) ? f64ToI64(x) : x)
455
+ const adaptRet = (ret, r) => r ? i64ToF64(ret) : ret
456
+
457
+ // Arity-specialized wrapper: rest-spread + .map() + .apply() costs ~85ns/call
458
+ // on hot loops (mandelbrot benchmark: 51ms wrapped vs 35ms direct over 200K
459
+ // calls). Generating positional `function(a0, a1, ...)` via Function lets V8
460
+ // fully inline the WASM call. Falls back to the spread-form wrapper if the
461
+ // Function constructor is unavailable (CSP) or arity is unusually large.
462
+ const makeFastWrapper = (fn, len, p, r, decode_, wrap_) => {
463
+ const params = [], wrapped = []
464
+ for (let i = 0; i < len; i++) {
465
+ const a = `a${i}`
466
+ params.push(a)
467
+ const w = wrap_ ? `wrap_(${a})` : `coerce(${a})`
468
+ wrapped.push(p.has(i) ? `f64ToI64(${w})` : w)
469
+ }
470
+ const callExpr = `fn(${wrapped.join(',')})`
471
+ const retExpr = r ? `i64ToF64(${callExpr})` : callExpr
472
+ const body = `return function(${params.join(',')}) {\n` +
473
+ ` try { return decode_(${retExpr}) } catch (e) { decodeThrown(e) }\n` +
474
+ `}`
475
+ return new Function('fn', 'wrap_', 'coerce', 'decode_', 'f64ToI64', 'i64ToF64', 'decodeThrown', body)(
476
+ fn, wrap_, coerce, decode_, f64ToI64, i64ToF64, decodeThrown)
477
+ }
478
+
401
479
  // Pure scalar module (no memory): pass f64 values directly, no marshaling
402
480
  if (!mem) {
403
- for (const [name, fn] of Object.entries(realInst.exports))
404
- exports[name] = typeof fn === 'function'
405
- ? (...args) => { while (args.length < fn.length) args.push(undefined); try { return decode(fn(...args.map(coerce))) } catch (e) { decodeThrown(e) } }
406
- : fn
481
+ for (const [name, fn] of Object.entries(realInst.exports)) {
482
+ if (typeof fn !== 'function') { exports[name] = fn; continue }
483
+ const sig = i64Exp.get(name)
484
+ const p = sig?.p || EMPTY_SET, r = sig?.r || 0
485
+ const len = fn.length
486
+ try {
487
+ exports[name] = makeFastWrapper(fn, len, p, r, decode, null)
488
+ continue
489
+ } catch { /* CSP fallback */ }
490
+ exports[name] = (...args) => {
491
+ while (args.length < len) args.push(undefined)
492
+ try {
493
+ const wasmArgs = adaptArgs(args.map(coerce), p)
494
+ return decode(adaptRet(fn(...wasmArgs), r))
495
+ } catch (e) { decodeThrown(e) }
496
+ }
497
+ }
407
498
  return exports
408
499
  }
500
+ const memWrapVal = mem.wrapVal.bind(mem)
501
+ const memRead = mem.read.bind(mem)
409
502
  for (const [name, fn] of Object.entries(realInst.exports)) {
410
503
  if (restFuncs.has(name) && typeof fn === 'function') {
411
504
  const fixed = restFuncs.get(name)
505
+ const sig = i64Exp.get(name)
506
+ const p = sig?.p || EMPTY_SET, r = sig?.r || 0
412
507
  exports[name] = (...args) => {
413
508
  const a = args.slice(0, fixed).map(x => mem.wrapVal(x))
414
509
  while (a.length < fixed) a.push(UNDEF_NAN)
415
510
  a.push(mem.Array(args.slice(fixed)))
416
511
  try {
417
- return mem.read(fn.apply(null, a))
512
+ const ret = fn.apply(null, adaptArgs(a, p))
513
+ return mem.read(adaptRet(ret, r))
418
514
  } catch (error) {
419
515
  decodeThrown(error)
420
516
  }
421
517
  }
422
518
  } else if (typeof fn === 'function') {
519
+ const sig = i64Exp.get(name)
520
+ const p = sig?.p || EMPTY_SET, r = sig?.r || 0
521
+ const len = fn.length
522
+ try {
523
+ exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal)
524
+ continue
525
+ } catch { /* CSP fallback */ }
423
526
  exports[name] = (...args) => {
424
- while (args.length < fn.length) args.push(undefined)
527
+ while (args.length < len) args.push(undefined)
425
528
  try {
426
- return mem.read(fn.apply(null, args.map(x => mem.wrapVal(x))))
529
+ const boxed = args.map(x => mem.wrapVal(x))
530
+ const ret = fn.apply(null, adaptArgs(boxed, p))
531
+ return mem.read(adaptRet(ret, r))
427
532
  } catch (error) {
428
533
  decodeThrown(error)
429
534
  }
@@ -438,33 +543,106 @@ export const wrap = (memSrc, inst) => {
438
543
  const prepareInterop = (opts) => {
439
544
  const state = { extMap: [null], mem: null }
440
545
  opts._interp = opts._interp || {}
441
- opts._interp.__ext_prop = (objPtr, propPtr) => {
546
+ // __ext_* receive NaN-boxed pointers across the env boundary as i64 (BigInt
547
+ // in JS) — see module/collection.js header for rationale. f64 returns are
548
+ // wrapped back to BigInt so the wasm side reinterprets a non-canonicalized
549
+ // bit pattern.
550
+ opts._interp.__ext_prop = (objBig, propBig) => {
551
+ const objPtr = i64ToF64(objBig), propPtr = i64ToF64(propBig)
442
552
  const obj = state.extMap[offset(objPtr)]
443
553
  const prop = state.mem.read(propPtr)
444
- return state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop])
554
+ return f64ToI64(state.mem.wrapVal(typeof obj[prop] === 'function' ? obj[prop].bind(obj) : obj[prop]))
445
555
  }
446
- opts._interp.__ext_has = (objPtr, propPtr) => {
447
- return (state.mem.read(propPtr) in state.extMap[offset(objPtr)]) ? 1 : 0
556
+ opts._interp.__ext_has = (objBig, propBig) => {
557
+ return (state.mem.read(i64ToF64(propBig)) in state.extMap[offset(i64ToF64(objBig))]) ? 1 : 0
448
558
  }
449
- opts._interp.__ext_set = (objPtr, propPtr, valPtr) => {
450
- state.extMap[offset(objPtr)][state.mem.read(propPtr)] = state.mem.read(valPtr)
559
+ opts._interp.__ext_set = (objBig, propBig, valBig) => {
560
+ state.extMap[offset(i64ToF64(objBig))][state.mem.read(i64ToF64(propBig))] = state.mem.read(i64ToF64(valBig))
451
561
  return 1
452
562
  }
453
- opts._interp.__ext_call = (objPtr, propPtr, argsPtr) => {
454
- const obj = state.extMap[offset(objPtr)]
455
- const prop = state.mem.read(propPtr)
456
- const args = state.mem.read(argsPtr)
457
- return state.mem.wrapVal(obj[prop].apply(obj, args))
563
+ opts._interp.__ext_call = (objBig, propBig, argsBig) => {
564
+ const obj = state.extMap[offset(i64ToF64(objBig))]
565
+ const prop = state.mem.read(i64ToF64(propBig))
566
+ const args = state.mem.read(i64ToF64(argsBig))
567
+ return f64ToI64(state.mem.wrapVal(obj[prop].apply(obj, args)))
458
568
  }
459
569
  return state
460
570
  }
461
571
 
572
+ // Default JS-host wiring for env.print + env.now — auto-installed when the wasm
573
+ // imports them (host: 'js' mode lowering in module/console.js). Caller-provided
574
+ // opts.imports.env entries take precedence.
575
+ const installDefaultEnvImports = (mod, imports, state) => {
576
+ const envFns = new Set(WebAssembly.Module.imports(mod)
577
+ .filter(i => i.module === 'env' && i.kind === 'function').map(i => i.name))
578
+ if (!envFns.size) return
579
+ if (!imports.env) imports.env = {}
580
+ if (envFns.has('print') && !imports.env.print) {
581
+ const buf = ['', '', ''] // fd 0/1/2 line buffers
582
+ const pending = []
583
+ const flush = (fd) => {
584
+ const out = fd === 2 ? console.error : console.log
585
+ out(buf[fd])
586
+ buf[fd] = ''
587
+ }
588
+ // env.print's val param is i64 to dodge V8's f64 NaN canonicalization
589
+ // across the wasm→JS boundary (see module/console.js header). Reinterpret
590
+ // the BigInt's bits as f64 here so mem.read sees the original NaN-box.
591
+ const write = (valBig, fd, sep) => {
592
+ const v = state.mem.read(i64ToF64(valBig))
593
+ buf[fd] += String(v)
594
+ if (sep === 32) buf[fd] += ' '
595
+ else if (sep === 10) flush(fd)
596
+ }
597
+ imports.env.print = (val, fd, sep) => {
598
+ if (!state.mem) pending.push([val, fd, sep])
599
+ else write(val, fd, sep)
600
+ }
601
+ state.flushPrint = () => {
602
+ for (const args of pending) write(...args)
603
+ pending.length = 0
604
+ }
605
+ }
606
+ if (envFns.has('now') && !imports.env.now) {
607
+ imports.env.now = (clock) =>
608
+ clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
609
+ }
610
+ // host: 'js' timer wiring. Wasm calls env.setTimeout/clearTimeout; we drive
611
+ // callbacks back via the exported __invoke_closure trampoline (state.invoke).
612
+ // Each id maps to a cancel thunk so set/clear share state without tagging.
613
+ // env.setTimeout receives cbPtr as i64 bits (BigInt) — see module/timer.js;
614
+ // __invoke_closure also takes i64 now, so the BigInt feeds it directly.
615
+ if (envFns.has('setTimeout') || envFns.has('clearTimeout')) {
616
+ const cancel = new Map()
617
+ let nextId = 1
618
+ if (envFns.has('setTimeout') && !imports.env.setTimeout) imports.env.setTimeout = (cbBig, delayMs, repeat) => {
619
+ const id = nextId++
620
+ const fire = () => state.invoke?.(cbBig)
621
+ if (repeat) {
622
+ const h = setInterval(fire, delayMs)
623
+ cancel.set(id, () => clearInterval(h))
624
+ } else {
625
+ const h = setTimeout(() => { cancel.delete(id); fire() }, delayMs)
626
+ cancel.set(id, () => clearTimeout(h))
627
+ }
628
+ return id
629
+ }
630
+ if (envFns.has('clearTimeout') && !imports.env.clearTimeout) imports.env.clearTimeout = (id) => {
631
+ const c = cancel.get(id)
632
+ if (c) { c(); cancel.delete(id) }
633
+ return 0
634
+ }
635
+ }
636
+ }
637
+
462
638
  const buildImports = (mod, opts, state) => {
463
639
  const needsWasi = WebAssembly.Module.imports(mod).some(i => i.module === 'wasi_snapshot_preview1')
464
640
  const imports = needsWasi ? wasi(opts) : {}
465
641
  if (opts._interp) imports.env = { ...imports.env, ...opts._interp }
466
642
 
467
- // Host imports: decode NaN-boxed args for JS and wrap JS returns back into jz values.
643
+ // Host imports: decode NaN-boxed args for JS and wrap JS returns back into jz
644
+ // values. Args/return ride i64 across the boundary (Step 2c) so V8 cannot
645
+ // canonicalize the NaN payload — convert BigInt↔f64 via reinterpret bits.
468
646
  if (opts.imports) for (const [modName, fns] of Object.entries(opts.imports)) {
469
647
  if (!imports[modName]) imports[modName] = {}
470
648
  for (const name of Object.getOwnPropertyNames(fns)) {
@@ -472,26 +650,37 @@ const buildImports = (mod, opts, state) => {
472
650
  const fn = typeof spec === 'function' ? spec : (spec && typeof spec === 'object' ? spec.fn : null)
473
651
  if (typeof fn === 'function')
474
652
  imports[modName][name] = (...args) => {
475
- const ret = fn.call(fns, ...args.map(a => state.mem ? state.mem.read(a) : a))
476
- return state.mem ? state.mem.wrapVal(ret) : coerce(ret)
653
+ // i64 carrier: reinterpret BigInt bits f64 NaN-box. Pure-scalar modules
654
+ // have no memory so skip mem.read; the f64 IS the JS number for numerics.
655
+ const decoded = args.map(a => {
656
+ const f = typeof a === 'bigint' ? i64ToF64(a) : a
657
+ return state.mem ? state.mem.read(f) : decode(f)
658
+ })
659
+ const ret = fn.call(fns, ...decoded)
660
+ return f64ToI64(state.mem ? state.mem.wrapVal(ret) : coerce(ret))
477
661
  }
478
662
  }
479
663
  }
480
- // Shared memory: normalize (auto-wrap raw Memory), pass as import
481
- if (opts.memory) {
664
+
665
+ installDefaultEnvImports(mod, imports, state)
666
+ // Shared memory: normalize (auto-wrap raw Memory), pass as import.
667
+ // Numeric opts.memory is a compile-time page count shorthand, not an import.
668
+ if (opts.memory instanceof WebAssembly.Memory) {
482
669
  // Auto-wrap raw WebAssembly.Memory → enhanced jz.memory
483
- if (opts.memory instanceof WebAssembly.Memory && !_enhanced.has(opts.memory)) opts.memory = memory(opts.memory)
670
+ if (!_enhanced.has(opts.memory)) opts.memory = memory(opts.memory)
484
671
  if (!imports.env) imports.env = {}
485
- imports.env.memory = opts.memory instanceof WebAssembly.Memory ? opts.memory : opts.memory
672
+ imports.env.memory = opts.memory
486
673
  }
487
- // Auto-imported host globals: provide as WebAssembly.Global wrapping NaN-boxed external refs
674
+ // Auto-imported host globals: provide as WebAssembly.Global wrapping NaN-boxed
675
+ // external refs. Carrier is i64 so the NaN payload survives V8's boundary
676
+ // canonicalization — wasm side reinterprets to f64 (see asF64 in src/ir.js).
488
677
  for (const imp of WebAssembly.Module.imports(mod)) {
489
678
  if (imp.kind === 'global' && imp.module === 'env') {
490
679
  const host = globalThis[imp.name]
491
680
  if (host !== undefined) {
492
681
  if (!imports.env) imports.env = {}
493
682
  let id = state.extMap.indexOf(host); if (id === -1) { id = state.extMap.length; state.extMap.push(host) }
494
- imports.env[imp.name] = new WebAssembly.Global({ value: 'f64', mutable: true }, ptr(11, 0, id))
683
+ imports.env[imp.name] = new WebAssembly.Global({ value: 'i64', mutable: false }, f64ToI64(ptr(11, 0, id)))
495
684
  }
496
685
  }
497
686
  }
@@ -501,6 +690,9 @@ const buildImports = (mod, opts, state) => {
501
690
  const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
502
691
  if (needsWasi) imports._setMemory(inst.exports.memory)
503
692
 
693
+ // Trampoline used by env.setTimeout/clearTimeout to fire scheduled closures.
694
+ state.invoke = inst.exports.__invoke_closure || null
695
+
504
696
  // Drive WASM timer queue via JS scheduling (non-blocking)
505
697
  if (inst.exports.__timer_tick) {
506
698
  const tick = inst.exports.__timer_tick
@@ -512,11 +704,12 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
512
704
  }, 1)
513
705
  }
514
706
 
515
- // For shared memory, resolve memory from import; for own memory, from export
516
- const rawMemory = opts.memory || inst.exports.memory
707
+ // For shared memory, resolve memory from import; for own memory, from export.
708
+ const rawMemory = opts.memory instanceof WebAssembly.Memory ? opts.memory : inst.exports.memory
517
709
  const memSrc = { module: mod, instance: inst, exports: { ...inst.exports, memory: rawMemory }, extMap: state.extMap }
518
710
  const enhanced = memory(memSrc)
519
711
  state.mem = enhanced
712
+ state.flushPrint?.()
520
713
  return { exports: wrap(memSrc), memory: enhanced, instance: inst, module: mod }
521
714
  }
522
715