jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/interop.js CHANGED
@@ -24,7 +24,13 @@
24
24
  * @module jz/interop
25
25
  */
26
26
 
27
- import { wasi } from './wasi.js'
27
+ import { wasi, attachTimers } from './wasi.js'
28
+ import { HEAP, encodePtrHi, decodePtrType, decodePtrAux, ATOM, ATOM_HI, LAYOUT } from './layout.js'
29
+
30
+ // Stateless + reusable — one instance avoids a per-call allocation on the hot
31
+ // string read/write paths (mem.String / mem.read STRING).
32
+ const TEXT_ENC = new TextEncoder()
33
+ const TEXT_DEC = new TextDecoder()
28
34
 
29
35
  // ── WASI linking ────────────────────────────────────────────────────────────
30
36
 
@@ -43,15 +49,12 @@ const envFuncNames = (mod) =>
43
49
  // threads must share a pointer cell in linear memory). 8-byte aligned bump on
44
50
  // the JS side; wasm `_alloc` takes over if exported.
45
51
 
46
- const HEAP_PTR_ADDR = 1020
47
- const HEAP_START = 1024
48
-
49
52
  const makeJsAllocator = (mem, heapGlobal) => {
50
53
  const dv = () => new DataView(mem.buffer)
51
- const getPtr = heapGlobal ? () => heapGlobal.value : () => dv().getInt32(HEAP_PTR_ADDR, true)
52
- const setPtr = heapGlobal ? v => { heapGlobal.value = v } : v => dv().setInt32(HEAP_PTR_ADDR, v, true)
54
+ const getPtr = heapGlobal ? () => heapGlobal.value : () => dv().getInt32(HEAP.PTR_ADDR, true)
55
+ const setPtr = heapGlobal ? v => { heapGlobal.value = v } : v => dv().setInt32(HEAP.PTR_ADDR, v, true)
53
56
  // Rewind target: the global's post-static-init value, else the fixed start.
54
- const base = heapGlobal ? heapGlobal.value : HEAP_START
57
+ const base = heapGlobal ? heapGlobal.value : HEAP.START
55
58
  const alloc = (bytes) => {
56
59
  const aligned = (getPtr() + 7) & ~7
57
60
  const next = aligned + bytes
@@ -66,7 +69,7 @@ const makeJsAllocator = (mem, heapGlobal) => {
66
69
  const initHeapPtr = () => {
67
70
  if (heapGlobal) return
68
71
  const d = dv()
69
- if (d.getInt32(HEAP_PTR_ADDR, true) < HEAP_START) d.setInt32(HEAP_PTR_ADDR, HEAP_START, true)
72
+ if (d.getInt32(HEAP.PTR_ADDR, true) < HEAP.START) d.setInt32(HEAP.PTR_ADDR, HEAP.START, true)
70
73
  }
71
74
  return { alloc, reset, initHeapPtr }
72
75
  }
@@ -101,17 +104,6 @@ const sectionReader = (bytes) => {
101
104
  }
102
105
  }
103
106
 
104
- const attachTimers = (inst) => {
105
- if (!inst.exports.__timer_tick) return
106
- const tick = inst.exports.__timer_tick
107
- let hadTimers = false
108
- const id = setInterval(() => {
109
- const remaining = tick()
110
- if (remaining > 0) hadTimers = true
111
- if (hadTimers && remaining <= 0) clearInterval(id)
112
- }, 1)
113
- }
114
-
115
107
  // ── NaN-box codec ───────────────────────────────────────────────────────────
116
108
 
117
109
  // NaN-boxing encode/decode — shared 8-byte scratch buffer
@@ -129,15 +121,13 @@ const _bi64 = (() => {
129
121
  })()
130
122
  export const i64ToF64 = _bi64.i64ToF64
131
123
  export const f64ToI64 = _bi64.f64ToI64
124
+
132
125
  // Reserved atoms (type=0, offset=0): aux=1 → null, aux=2 → undefined.
133
126
  // Distinct from 0, JS NaN (payload=0), and all pointers.
134
- _u32[1] = 0x7FF80001; _u32[0] = 0; export const NULL_NAN = _f64[0]
135
- _u32[1] = 0x7FF80002; _u32[0] = 0; export const UNDEF_NAN = _f64[0]
136
- // Boxed-boolean atoms (type=0, offset=0): aux=4 false, aux=5 → true. The
137
- // low aux bit is the truth value; only exported scalars cross as these — the
138
- // inner 0/1 number carrier never escapes the wasm boundary.
139
- _u32[1] = 0x7FF80004; _u32[0] = 0; export const FALSE_NAN = _f64[0]
140
- _u32[1] = 0x7FF80005; _u32[0] = 0; export const TRUE_NAN = _f64[0]
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]
141
131
 
142
132
  // Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
143
133
  export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
@@ -147,20 +137,20 @@ const decode = v => {
147
137
  if (v === v) return v // fast path: non-NaN
148
138
  _f64[0] = v
149
139
  if (_u32[0] !== 0) return v
150
- if (_u32[1] === 0x7FF80001) return null
151
- if (_u32[1] === 0x7FF80002) return undefined
152
- if (_u32[1] === 0x7FF80004) return false
153
- if (_u32[1] === 0x7FF80005) return true
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
154
144
  return v
155
145
  }
156
146
 
157
147
  export const ptr = (type, aux, offset) => {
158
- _u32[1] = (0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
148
+ _u32[1] = encodePtrHi(type, aux)
159
149
  _u32[0] = offset >>> 0; return _f64[0]
160
150
  }
161
151
  export const offset = (p) => { _f64[0] = p; return _u32[0] }
162
- export const type = (p) => { _f64[0] = p; return (_u32[1] >>> 15) & 0xF }
163
- export const aux = (p) => { _f64[0] = p; return _u32[1] & 0x7FFF }
152
+ export const type = (p) => { _f64[0] = p; return decodePtrType(_u32[1]) }
153
+ export const aux = (p) => { _f64[0] = p; return decodePtrAux(_u32[1]) }
164
154
 
165
155
  // Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
166
156
  const ELEMS = {
@@ -285,9 +275,9 @@ export const memory = (src) => {
285
275
  if (str.length <= 4 && /^[\x00-\x7f]*$/.test(str)) {
286
276
  let packed = 0
287
277
  for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
288
- return ptr(4, 0x4000 | str.length, packed) // STRING + SSO_BIT
278
+ return ptr(4, LAYOUT.SSO_BIT | str.length, packed) // STRING + SSO_BIT
289
279
  }
290
- const enc = new TextEncoder().encode(str)
280
+ const enc = TEXT_ENC.encode(str)
291
281
  const n = enc.length, raw = alloc(4 + n), m = dv()
292
282
  m.setInt32(raw, n, true)
293
283
  const off = raw + 4
@@ -307,23 +297,22 @@ export const memory = (src) => {
307
297
  mem.wrapVal = function(v) {
308
298
  if (v === null || v === undefined) return coerce(v)
309
299
  if (typeof v === 'number' || typeof v === 'boolean') return Number(v)
310
- if (typeof v === 'string') return this.String(v)
311
- // BigInt as a data value crosses the boundary as a decimal-string. wasm-side
312
- // numeric parsers accept string form. Direct i64 function args go through this
313
- // path too; the call wrappers below detect BigInt before adaptArgs reinterprets.
314
- if (typeof v === 'bigint') return this.String(v.toString())
315
- if (Array.isArray(v)) return this.Array(v)
316
- if (v instanceof ArrayBuffer) return this.Buffer(v)
317
- if (v instanceof DataView) return this.Buffer(v.buffer)
300
+ 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())
304
+ if (Array.isArray(v)) return mem.Array(v)
305
+ if (v instanceof ArrayBuffer) return mem.Buffer(v)
306
+ if (v instanceof DataView) return mem.Buffer(v.buffer)
318
307
  const typedName = v?.constructor?.name
319
- if (typedName && ELEMS[typedName]) return this[typedName](v)
320
- if (typeof v === 'object' || typeof v === 'function') return this.External(v)
308
+ if (typedName && ELEMS[typedName]) return mem[typedName](v)
309
+ if (typeof v === 'object' || typeof v === 'function') return mem.External(v)
321
310
  return UNDEF_NAN
322
311
  }
323
312
 
324
313
  mem.External = function(obj) {
325
314
  if (obj === null || obj === undefined) return coerce(obj)
326
- const map = this._extMap
315
+ const map = mem._extMap
327
316
  if (!map) return UNDEF_NAN
328
317
  let id = map.indexOf(obj)
329
318
  if (id === -1) { id = map.length; map.push(obj) }
@@ -333,14 +322,14 @@ export const memory = (src) => {
333
322
  mem.Object = function(obj) {
334
323
  const objKeys = Object.keys(obj)
335
324
  const key = objKeys.join(',')
336
- const schemas = this.schemas
325
+ const schemas = mem.schemas
337
326
  let sid = schemas.findIndex(s => s.join(',') === key)
338
327
  if (sid === -1) {
339
328
  const matches = schemas.reduce((a, s, i) =>
340
329
  (s.length === objKeys.length && objKeys.every(k => s.includes(k)) ? a.concat(i) : a), [])
341
330
  if (matches.length === 1) sid = matches[0]
342
331
  else if (matches.length > 1) throw Error(`Ambiguous schema for {${key}} — pass keys in schema order`)
343
- else if (this._extMap) return this.External(obj)
332
+ else if (mem._extMap) return mem.External(obj)
344
333
  else throw Error(`No schema for {${key}}`)
345
334
  }
346
335
  const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
@@ -350,8 +339,8 @@ export const memory = (src) => {
350
339
  for (let i = 0; i < n; i++) {
351
340
  let v = obj[schema[i]]
352
341
  if (v === null || v === undefined) v = coerce(v)
353
- else if (typeof v === 'string') v = this.String(v)
354
- else if (Array.isArray(v)) v = this.Array(v)
342
+ else if (typeof v === 'string') v = mem.String(v)
343
+ else if (Array.isArray(v)) v = mem.Array(v)
355
344
  wrapped[i] = f64ToI64(v)
356
345
  }
357
346
  const dst = new BigInt64Array(mem.buffer, raw, n)
@@ -360,7 +349,7 @@ export const memory = (src) => {
360
349
  }
361
350
 
362
351
  mem.read = function(p) {
363
- if (Array.isArray(p)) return p.map(v => this.read(v)) // multi-value tuple
352
+ if (Array.isArray(p)) return p.map(v => mem.read(v)) // multi-value tuple
364
353
  if (p === p) return p // regular number passthrough (NaN fails ===)
365
354
  const t = type(p), a = aux(p), off = offset(p)
366
355
  if (t === 0 && off === 0) {
@@ -369,13 +358,13 @@ export const memory = (src) => {
369
358
  if (a === 4) return false
370
359
  if (a === 5) return true
371
360
  }
372
- if (t === 11 && this._extMap) return this._extMap[off]
361
+ if (t === 11 && mem._extMap) return mem._extMap[off]
373
362
  if (t === 1) { // ARRAY
374
363
  let m = dv(), aOff = off
375
364
  // Follow forwarding pointers (cap === -1 means array was reallocated)
376
365
  while (m.getInt32(aOff - 4, true) === -1) aOff = m.getInt32(aOff - 8, true)
377
366
  const len = m.getInt32(aOff - 8, true), out = new Array(len)
378
- for (let i = 0; i < len; i++) out[i] = this.read(m.getFloat64(aOff + i * 8, true))
367
+ for (let i = 0; i < len; i++) out[i] = mem.read(m.getFloat64(aOff + i * 8, true))
379
368
  return out
380
369
  }
381
370
  if (t === 3) { // TYPED
@@ -396,21 +385,21 @@ export const memory = (src) => {
396
385
  new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
397
386
  return out
398
387
  }
399
- if (t === 4) { // STRING (aux bit 0x4000 = SSO inline, else heap)
388
+ if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
400
389
  const a2 = aux(p)
401
- if (a2 & 0x4000) {
390
+ if (a2 & LAYOUT.SSO_BIT) {
402
391
  const len = a2 & 0x7; let s = ''
403
392
  for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
404
393
  return s
405
394
  }
406
395
  const len = dv().getInt32(off - 4, true)
407
- return new TextDecoder().decode(new Uint8Array(mem.buffer, off, len))
396
+ return TEXT_DEC.decode(new Uint8Array(mem.buffer, off, len))
408
397
  }
409
398
  if (t === 6) { // OBJECT
410
- const m = dv(), sid = aux(p), keys = this.schemas[sid]
399
+ const m = dv(), sid = aux(p), keys = mem.schemas[sid]
411
400
  if (!keys) return p
412
401
  const obj = {}
413
- for (let i = 0; i < keys.length; i++) obj[keys[i]] = this.read(m.getFloat64(off + i * 8, true))
402
+ for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.getFloat64(off + i * 8, true))
414
403
  return obj
415
404
  }
416
405
  if (t === 7) { // HASH
@@ -419,8 +408,8 @@ export const memory = (src) => {
419
408
  for (let i = 0, found = 0; i < cap && found < size; i++) {
420
409
  const hash = m.getFloat64(off + i * 24, true)
421
410
  if (hash !== 0) {
422
- const key = this.read(m.getFloat64(off + i * 24 + 8, true))
423
- obj[key] = this.read(m.getFloat64(off + i * 24 + 16, true))
411
+ const key = mem.read(m.getFloat64(off + i * 24 + 8, true))
412
+ obj[key] = mem.read(m.getFloat64(off + i * 24 + 16, true))
424
413
  found++
425
414
  }
426
415
  }
@@ -431,7 +420,7 @@ export const memory = (src) => {
431
420
  const set = new Set()
432
421
  for (let i = 0; i < cap && set.size < size; i++) {
433
422
  const hash = m.getFloat64(off + i * 16, true)
434
- if (hash !== 0) set.add(this.read(m.getFloat64(off + i * 16 + 8, true)))
423
+ if (hash !== 0) set.add(mem.read(m.getFloat64(off + i * 16 + 8, true)))
435
424
  }
436
425
  return set
437
426
  }
@@ -440,7 +429,7 @@ export const memory = (src) => {
440
429
  const map = new Map()
441
430
  for (let i = 0; i < cap && map.size < size; i++) {
442
431
  const hash = m.getFloat64(off + i * 24, true)
443
- if (hash !== 0) map.set(this.read(m.getFloat64(off + i * 24 + 8, true)), this.read(m.getFloat64(off + i * 24 + 16, 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)))
444
433
  }
445
434
  return map
446
435
  }
@@ -470,7 +459,7 @@ export const memory = (src) => {
470
459
  for (let i = 0; i < data.length; i++) m[setter](off + i * stride, data[i], true)
471
460
  }
472
461
  } else if (t === 6) {
473
- const schema = this.schemas[aux(p)]
462
+ const schema = mem.schemas[aux(p)]
474
463
  if (!schema) throw Error(`write: unknown schema`)
475
464
  for (const k of Object.keys(data)) {
476
465
  const i = schema.indexOf(k)
@@ -485,10 +474,19 @@ export const memory = (src) => {
485
474
  mem.reset = wasmExports?._clear || jsReset
486
475
 
487
476
  // TypedArray constructors: memory.Float64Array(data), etc.
477
+ // Bulk-copy path: when input is a TypedArray whose element type matches
478
+ // the target (same stride), use .set() for a fast memcpy instead of
479
+ // per-element DataView writes. Falls back to DataView for mismatched types.
480
+ const TA = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]
488
481
  for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
489
482
  mem[name] = (data) => {
490
- const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes), m = dv()
491
- for (let i = 0; i < n; i++) m[setter](off + i * stride, data[i], true)
483
+ const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
484
+ if (stride >= 2 && TA[elemId] && data instanceof TA[elemId]) {
485
+ new TA[elemId](mem.buffer, off, n).set(data)
486
+ } else {
487
+ const m = dv()
488
+ for (let i = 0; i < n; i++) m[setter](off + i * stride, data[i], true)
489
+ }
492
490
  return ptr(3, elemId, off)
493
491
  }
494
492
  }
@@ -513,19 +511,8 @@ export const wrap = (memSrc, inst) => {
513
511
  restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
514
512
  } catch (e) { /* ignore */ }
515
513
  }
516
- // i64-ABI exports: boundary-wrapped funcs whose NaN-boxed pointer params/
517
- // result ride i64 to dodge V8's NaN canonicalization. Map: name → { p, r }
518
- // with p = bit mask of i64 params, r = 1 iff result is i64. JS side
519
- // reinterprets f64↔BigInt only at those positions (see
520
- // synthesizeBoundaryWrappers).
521
- const i64Exp = new Map(), EMPTY_SET = new Set()
522
- const i64Bytes = customSection(mod, 'jz:i64exp')
523
- if (i64Bytes) {
524
- try { for (const e of JSON.parse(td.decode(i64Bytes))) i64Exp.set(e.name, { p: new Set(e.p), r: e.r }) }
525
- catch { /* ignore */ }
526
- }
527
514
  // externref-param exports: positions where the wasm side takes an externref
528
- // (e.g. jsstring boundary opt-in). JS values at these positions pass through
515
+ // (jsstring carrier — js-host only). JS values at these positions pass through
529
516
  // unchanged — no `mem.wrapVal` (would NaN-box into f64, defeating the point).
530
517
  // `def` (optional) maps idx → default-string for jsstring params whose
531
518
  // default substitution happens JS-side (the wasm side never sees null).
@@ -542,7 +529,6 @@ export const wrap = (memSrc, inst) => {
542
529
  }
543
530
  } catch { /* ignore */ }
544
531
  }
545
-
546
532
  const mem = memory(memSrc)
547
533
  const lastErrBits = realInst.exports.__jz_last_err_bits
548
534
  const decodeThrown = error => {
@@ -558,118 +544,52 @@ export const wrap = (memSrc, inst) => {
558
544
  throw wrapped
559
545
  }
560
546
  const exports = {}
561
- // Per-position carrier swap: f64 stays Number, i64 positions reinterpret to
562
- // BigInt before the call and back to Number after. p is a Set of i64 param
563
- // indices; r = result is i64. Numeric (f64) positions pass through unchanged.
564
- const adaptArgs = (a, p) => p.size === 0 ? a : a.map((x, i) => p.has(i) ? f64ToI64(x) : x)
565
- const adaptRet = (ret, r) => r ? i64ToF64(ret) : ret
566
- // Externref positions skip mem.wrapVal the raw JS value (string, object,
567
- // …) flows through as externref. mem.wrapVal would NaN-box it.
568
- const wrapArg = (ext, i, x) => ext?.has(i) ? x : mem.wrapVal(x)
569
-
570
- // Arity-specialized wrapper: rest-spread + .map() + .apply() costs ~85ns/call
571
- // on hot loops (mandelbrot benchmark: 51ms wrapped vs 35ms direct over 200K
572
- // calls). Generating positional `function(a0, a1, ...)` via Function lets V8
573
- // fully inline the WASM call. Falls back to the spread-form wrapper if the
574
- // Function constructor is unavailable (CSP) or arity is unusually large.
575
- // `ext` is a Set of externref param positions — those slots skip wrap/coerce
576
- // and pass the JS value directly (jsstring carrier: JS string → externref).
577
- // `ext.def` (optional Map idx→string) carries jsstring-literal defaults that
578
- // are substituted JS-side when the caller passes `undefined`.
579
- const makeFastWrapper = (fn, len, p, r, decode_, wrap_, ext) => {
580
- const params = [], wrapped = []
581
- const defs = ext?.def
582
- const defArgs = [], defNames = []
583
- for (let i = 0; i < len; i++) {
584
- const a = `a${i}`
585
- params.push(a)
586
- if (ext?.has(i)) {
587
- if (defs?.has(i)) {
588
- const dn = `def${i}`
589
- defNames.push(dn); defArgs.push(defs.get(i))
590
- wrapped.push(`(${a} === undefined ? ${dn} : ${a})`)
591
- } else {
592
- wrapped.push(a)
593
- }
594
- continue
595
- }
596
- const w = wrap_ ? `wrap_(${a})` : `coerce(${a})`
597
- wrapped.push(p.has(i) ? `f64ToI64(${w})` : w)
598
- }
599
- const callExpr = `fn(${wrapped.join(',')})`
600
- const retExpr = r ? `i64ToF64(${callExpr})` : callExpr
601
- const body = `return function(${params.join(',')}) {\n` +
602
- ` try { return decode_(${retExpr}) } catch (e) { decodeThrown(e) }\n` +
603
- `}`
604
- return new Function('fn', 'wrap_', 'coerce', 'decode_', 'f64ToI64', 'i64ToF64', 'decodeThrown', ...defNames, body)(
605
- fn, wrap_, coerce, decode_, f64ToI64, i64ToF64, decodeThrown, ...defArgs)
606
- }
547
+ // Wrap one positional arg. Externref slots (jsstring carrier) pass the JS
548
+ // value straight through `mem.wrapVal` would NaN-box it substituting a
549
+ // jsstring literal default for a missing arg. Every other slot marshals via
550
+ // `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
+ const wrapArgAt = (ext, i, x, box) =>
554
+ ext?.has(i) ? (x === undefined && ext.def?.has(i) ? ext.def.get(i) : x) : box(x)
607
555
 
608
556
  // Pure scalar module (no memory): pass f64 values directly, no marshaling
609
557
  if (!mem) {
610
558
  for (const [name, fn] of Object.entries(realInst.exports)) {
611
559
  if (typeof fn !== 'function') { exports[name] = fn; continue }
612
- const sig = i64Exp.get(name)
613
- const p = sig?.p || EMPTY_SET, r = sig?.r || 0
614
560
  const ext = extExp.get(name)
615
561
  const len = fn.length
616
- try {
617
- exports[name] = makeFastWrapper(fn, len, p, r, decode, null, ext)
618
- continue
619
- } catch { /* CSP fallback */ }
620
562
  exports[name] = (...args) => {
621
563
  while (args.length < len) args.push(undefined)
622
564
  try {
623
- const wasmArgs = adaptArgs(args.map((x, i) => {
624
- if (!ext?.has(i)) return coerce(x)
625
- return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
626
- }), p)
627
- return decode(adaptRet(fn(...wasmArgs), r))
565
+ return decode(fn(...args.map((x, i) => wrapArgAt(ext, i, x, coerce))))
628
566
  } catch (e) { decodeThrown(e) }
629
567
  }
630
568
  }
631
569
  return exports
632
570
  }
633
571
  const memWrapVal = mem.wrapVal.bind(mem)
634
- const memRead = mem.read.bind(mem)
635
572
  for (const [name, fn] of Object.entries(realInst.exports)) {
636
573
  if (restFuncs.has(name) && typeof fn === 'function') {
637
574
  const fixed = restFuncs.get(name)
638
- const sig = i64Exp.get(name)
639
- const p = sig?.p || EMPTY_SET, r = sig?.r || 0
640
575
  const ext = extExp.get(name)
641
576
  exports[name] = (...args) => {
642
- const a = args.slice(0, fixed).map((x, i) => {
643
- if (!ext?.has(i)) return mem.wrapVal(x)
644
- return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
645
- })
577
+ const a = args.slice(0, fixed).map((x, i) => wrapArgAt(ext, i, x, memWrapVal))
646
578
  while (a.length < fixed) a.push(UNDEF_NAN)
647
579
  a.push(mem.Array(args.slice(fixed)))
648
580
  try {
649
- const ret = fn.apply(null, adaptArgs(a, p))
650
- return mem.read(adaptRet(ret, r))
581
+ return mem.read(fn.apply(null, a))
651
582
  } catch (error) {
652
583
  decodeThrown(error)
653
584
  }
654
585
  }
655
586
  } else if (typeof fn === 'function') {
656
- const sig = i64Exp.get(name)
657
- const p = sig?.p || EMPTY_SET, r = sig?.r || 0
658
587
  const ext = extExp.get(name)
659
588
  const len = fn.length
660
- try {
661
- exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal, ext)
662
- continue
663
- } catch { /* CSP fallback */ }
664
589
  exports[name] = (...args) => {
665
590
  while (args.length < len) args.push(undefined)
666
591
  try {
667
- const boxed = args.map((x, i) => {
668
- if (!ext?.has(i)) return mem.wrapVal(x)
669
- return x === undefined && ext.def?.has(i) ? ext.def.get(i) : x
670
- })
671
- const ret = fn.apply(null, adaptArgs(boxed, p))
672
- return mem.read(adaptRet(ret, r))
592
+ return mem.read(fn.apply(null, args.map((x, i) => wrapArgAt(ext, i, x, memWrapVal))))
673
593
  } catch (error) {
674
594
  decodeThrown(error)
675
595
  }
@@ -747,6 +667,16 @@ const installDefaultEnvImports = (mod, imports, state) => {
747
667
  imports.env.now = (clock) =>
748
668
  clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
749
669
  }
670
+ // One i32 of entropy to seed Math.random — only present when compiled with
671
+ // { randomSeed: true }. Prefers crypto; falls back to Math.random.
672
+ if (envFns.has('rngSeed') && !imports.env.rngSeed) {
673
+ imports.env.rngSeed = () => {
674
+ const a = new Uint32Array(1)
675
+ if (globalThis.crypto?.getRandomValues) globalThis.crypto.getRandomValues(a)
676
+ else a[0] = (Math.random() * 0x100000000) >>> 0
677
+ return a[0] | 0
678
+ }
679
+ }
750
680
  if (envFns.has('parseFloat') && !imports.env.parseFloat) {
751
681
  imports.env.parseFloat = (valBig) => {
752
682
  const s = state.mem.read(i64ToF64(valBig))
@@ -811,8 +741,8 @@ const jssProbeNative = () => {
811
741
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // header
812
742
  0x01, 0x06, 0x01, 0x60, 0x01, 0x6f, 0x01, 0x7f, // type: (externref)→i32
813
743
  0x02, 0x18, 0x01, // import section
814
- 0x0f, ...new TextEncoder().encode('wasm:js-string'), // mod name
815
- 0x06, ...new TextEncoder().encode('length'), // name
744
+ 0x0f, ...TEXT_ENC.encode('wasm:js-string'), // mod name
745
+ 0x06, ...TEXT_ENC.encode('length'), // name
816
746
  0x00, 0x00, // kind=func, type=0
817
747
  ])
818
748
  const mod = new WebAssembly.Module(bytes, { builtins: ['js-string'] })
@@ -921,7 +851,6 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
921
851
  */
922
852
  export const instantiate = (wasm, opts = {}) => {
923
853
  const state = prepareInterop(opts)
924
- opts.extMap = state.extMap
925
854
  // Prefer native `wasm:js-string` builtins when the engine honors the option.
926
855
  // The option is silently accepted by V8 17+/Safari 18.4+; older engines that
927
856
  // don't recognize it either throw or ignore it — try-fallback handles both.
package/jz.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg width="630" height="630" viewBox="0 0 630 630" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M630 0H0V630H630V0Z" fill="black"/>
3
+ <path d="M165.65 526.474L213.863 497.296C223.164 513.788 231.625 527.74 251.92 527.74C271.374 527.74 283.639 520.13 283.639 490.53V289.23H342.843V491.367C342.843 552.688 306.899 580.599 254.458 580.599C207.096 580.599 179.605 556.07 165.65 526.469" fill="white"/>
4
+ <path d="M412.075 316.04H534.138L409.808 550.902H546.138" stroke="white" stroke-width="54" stroke-linecap="square"/>
5
+ </svg>
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `arguments` object + destructuring-param lowering for function→arrow conversion.
3
+ * @module jzify/arguments
4
+ */
5
+
6
+ import { paramList } from '../src/ast.js'
7
+ import { isDestructurePat, prependDecls } from './hoist-vars.js'
8
+
9
+ function usesArguments(node) {
10
+ if (node === 'arguments') return true
11
+ if (!Array.isArray(node)) return false
12
+ if (node[0] === 'function') return false
13
+ // Literal node `[, value]` (op === null) — node[1] is a string/number VALUE, not an
14
+ // identifier. A string literal `'arguments'` must not read as the arguments object.
15
+ if (node[0] == null) return false
16
+ if (node[0] === '.' || node[0] === '?.') return usesArguments(node[1])
17
+ if (node[0] === ':') return usesArguments(node[2])
18
+ for (let i = 1; i < node.length; i++) if (usesArguments(node[i])) return true
19
+ return false
20
+ }
21
+
22
+ function bindsArguments(body) {
23
+ const isArgDecl = s => Array.isArray(s) && (s[0] === 'var' || s[0] === 'let' || s[0] === 'const') &&
24
+ s.slice(1).some(d => d === 'arguments' || (Array.isArray(d) && d[0] === '=' && d[1] === 'arguments'))
25
+ let n = body
26
+ if (Array.isArray(n) && n[0] === '{}') n = n[1]
27
+ if (Array.isArray(n) && n[0] === ';') return n.slice(1).some(isArgDecl)
28
+ return isArgDecl(n)
29
+ }
30
+
31
+ function renameArguments(node, to) {
32
+ if (node === 'arguments') return to
33
+ if (!Array.isArray(node)) return node
34
+ if (node[0] === 'function') return node
35
+ // Literal node `[, value]` — node[1] is a value, not an identifier; leave untouched
36
+ // so a string literal `'arguments'` survives the rename.
37
+ if (node[0] == null) return node
38
+ if (node[0] === '.' || node[0] === '?.')
39
+ return [node[0], renameArguments(node[1], to), node[2]]
40
+ if (node[0] === ':')
41
+ return [node[0], node[1], renameArguments(node[2], to)]
42
+ return node.map(n => renameArguments(n, to))
43
+ }
44
+
45
+ function prependParamDecls(decl, body) {
46
+ if (Array.isArray(body) && body[0] === '{}') {
47
+ const inner = body[1]
48
+ if (Array.isArray(inner) && inner[0] === ';') return ['{}', [';', decl, ...inner.slice(1)]]
49
+ if (inner == null) return ['{}', decl]
50
+ return ['{}', [';', decl, inner]]
51
+ }
52
+ if (Array.isArray(body) && (body[0] === ';' || body[0] === 'return')) return [';', decl, body]
53
+ return ['{}', [';', decl, ['return', body]]]
54
+ }
55
+
56
+ /** @param {ReturnType<import('./names.js').createNames>} names */
57
+ export function createArgumentsLowering(names) {
58
+ function lowerArguments(params, body) {
59
+ if (bindsArguments(body)) body = renameArguments(body, names.arg())
60
+ const paramsNeedLowering = paramList(params).some(isDestructurePat)
61
+ const usesArgsObj = usesArguments(params) || usesArguments(body)
62
+ if (!paramsNeedLowering && !usesArgsObj) return [params, body]
63
+ const name = names.arg()
64
+ const decls = []
65
+ for (const [idx, param] of paramList(params).entries()) {
66
+ if (Array.isArray(param) && param[0] === '...') {
67
+ decls.push(['=', param[1], ['()', ['.', name, 'slice'], [null, idx]]])
68
+ continue
69
+ }
70
+ if (Array.isArray(param) && param[0] === '=') {
71
+ decls.push(['=', param[1], ['??', ['[]', name, [null, idx]], renameArguments(param[2], name)]])
72
+ continue
73
+ }
74
+ decls.push(['=', param, ['[]', name, [null, idx]]])
75
+ }
76
+ const renamed = usesArgsObj ? renameArguments(body, name) : body
77
+ return [['()', ['...', name]], decls.length ? prependParamDecls(['let', ...decls], renamed) : renamed]
78
+ }
79
+
80
+ let transformRef = null
81
+ const transformPattern = (node) => {
82
+ const transform = transformRef
83
+ if (node == null || !Array.isArray(node)) return node
84
+ const op = node[0]
85
+ if (op === '=') return ['=', transformPattern(node[1]), transform(node[2])]
86
+ if (op === ':') return [':', transform(node[1]), transformPattern(node[2])]
87
+ if (op === '...') return ['...', transformPattern(node[1])]
88
+ if (op === '[]' || op === '{}' || op === ',') return [op, ...node.slice(1).map(transformPattern)]
89
+ return transform(node)
90
+ }
91
+
92
+ return {
93
+ lowerArguments,
94
+ transformPattern,
95
+ bindTransform(fn) { transformRef = fn },
96
+ }
97
+ }