jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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,38 +121,58 @@ 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
144
134
 
145
- // Decode f64 return value: null/undefined sentinels → JS values, numbers pass through
135
+ // Memory-free decode of an f64 boundary value: numbers pass through, the reserved
136
+ // atoms become null/undefined/false/true, and an SSO string unpacks its ≤4 inline
137
+ // ASCII bytes straight from the NaN payload. These are exactly the value forms a
138
+ // *memoryless* module can carry across the boundary — a heap string/array/object
139
+ // would need linear memory to exist, so it can't reach here. Heap-carrying modules
140
+ // route through `mem.read` instead (which also handles these, via memory).
146
141
  const decode = v => {
147
- if (v === v) return v // fast path: non-NaN
142
+ if (v === v) return v // fast path: non-NaN number
148
143
  _f64[0] = v
144
+ // STRING (type 4) + SSO bit: content is the low 32 bits, length the low aux bits.
145
+ if (decodePtrType(_u32[1]) === 4) {
146
+ const a = decodePtrAux(_u32[1])
147
+ if (a & LAYOUT.SSO_BIT) {
148
+ const len = a & 0x7, off = _u32[0]; let s = ''
149
+ for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
150
+ return s
151
+ }
152
+ }
149
153
  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
154
+ if (_u32[1] === ATOM_HI[ATOM.NULL]) return null
155
+ if (_u32[1] === ATOM_HI[ATOM.UNDEF]) return undefined
156
+ if (_u32[1] === ATOM_HI[ATOM.FALSE]) return false
157
+ if (_u32[1] === ATOM_HI[ATOM.TRUE]) return true
154
158
  return v
155
159
  }
156
160
 
161
+ // Decode a boundary value that arrives as i64 bits (BigInt carrier) into a JS
162
+ // value. Heap-carrying modules go through `mem.read`; a memoryless module's value
163
+ // is a number/atom/SSO string, decodable from the bits alone.
164
+ const readArgBits = (state, big) => {
165
+ const f = i64ToF64(big)
166
+ return state.mem ? state.mem.read(f) : decode(f)
167
+ }
168
+
157
169
  export const ptr = (type, aux, offset) => {
158
- _u32[1] = (0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
170
+ _u32[1] = encodePtrHi(type, aux)
159
171
  _u32[0] = offset >>> 0; return _f64[0]
160
172
  }
161
173
  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 }
174
+ export const type = (p) => { _f64[0] = p; return decodePtrType(_u32[1]) }
175
+ export const aux = (p) => { _f64[0] = p; return decodePtrAux(_u32[1]) }
164
176
 
165
177
  // Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
166
178
  const ELEMS = {
@@ -206,7 +218,11 @@ export const memory = (src) => {
206
218
  // Instance result: { module, instance, exports, extMap }
207
219
  const raw = src?.instance?.exports || src?.exports || src
208
220
  mem = src?.exports?.memory || raw.memory
209
- if (!mem) return null // pure scalar module — no memory
221
+ // Memoryless module (SSO strings / atoms / numbers only — no linear memory):
222
+ // hand back a minimal reader instead of null so callers can still decode its
223
+ // boundary values from bits. `read`/`wrapVal` cover the value forms that exist
224
+ // without memory; `scalar` flags the fast path that skips heap marshaling.
225
+ if (!mem) return { read: decode, wrapVal: coerce, scalar: true }
210
226
  wasmExports = { ...raw, memory: mem }
211
227
  extMap = src.extMap || null
212
228
  mod = src.module || null
@@ -259,8 +275,7 @@ export const memory = (src) => {
259
275
  if (_enhanced.has(mem)) {
260
276
  mem.schemas = schemas
261
277
  if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
262
- if (wasmExports?._clear) mem.reset = wasmExports._clear
263
- else if (!mem.reset) mem.reset = jsReset
278
+ mem.reset = jsReset // post-init rewind — see the note at the first-enhance path
264
279
  if (extMap) mem._extMap = extMap
265
280
  return mem
266
281
  }
@@ -285,9 +300,9 @@ export const memory = (src) => {
285
300
  if (str.length <= 4 && /^[\x00-\x7f]*$/.test(str)) {
286
301
  let packed = 0
287
302
  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
303
+ return ptr(4, LAYOUT.SSO_BIT | str.length, packed) // STRING + SSO_BIT
289
304
  }
290
- const enc = new TextEncoder().encode(str)
305
+ const enc = TEXT_ENC.encode(str)
291
306
  const n = enc.length, raw = alloc(4 + n), m = dv()
292
307
  m.setInt32(raw, n, true)
293
308
  const off = raw + 4
@@ -307,23 +322,22 @@ export const memory = (src) => {
307
322
  mem.wrapVal = function(v) {
308
323
  if (v === null || v === undefined) return coerce(v)
309
324
  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)
325
+ if (typeof v === 'string') return mem.String(v)
326
+ // BigInt as a data value crosses the boundary as a decimal-string; wasm-side
327
+ // numeric parsers accept string form.
328
+ if (typeof v === 'bigint') return mem.String(v.toString())
329
+ if (Array.isArray(v)) return mem.Array(v)
330
+ if (v instanceof ArrayBuffer) return mem.Buffer(v)
331
+ if (v instanceof DataView) return mem.Buffer(v.buffer)
318
332
  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)
333
+ if (typedName && ELEMS[typedName]) return mem[typedName](v)
334
+ if (typeof v === 'object' || typeof v === 'function') return mem.External(v)
321
335
  return UNDEF_NAN
322
336
  }
323
337
 
324
338
  mem.External = function(obj) {
325
339
  if (obj === null || obj === undefined) return coerce(obj)
326
- const map = this._extMap
340
+ const map = mem._extMap
327
341
  if (!map) return UNDEF_NAN
328
342
  let id = map.indexOf(obj)
329
343
  if (id === -1) { id = map.length; map.push(obj) }
@@ -333,14 +347,14 @@ export const memory = (src) => {
333
347
  mem.Object = function(obj) {
334
348
  const objKeys = Object.keys(obj)
335
349
  const key = objKeys.join(',')
336
- const schemas = this.schemas
350
+ const schemas = mem.schemas
337
351
  let sid = schemas.findIndex(s => s.join(',') === key)
338
352
  if (sid === -1) {
339
353
  const matches = schemas.reduce((a, s, i) =>
340
354
  (s.length === objKeys.length && objKeys.every(k => s.includes(k)) ? a.concat(i) : a), [])
341
355
  if (matches.length === 1) sid = matches[0]
342
356
  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)
357
+ else if (mem._extMap) return mem.External(obj)
344
358
  else throw Error(`No schema for {${key}}`)
345
359
  }
346
360
  const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
@@ -350,8 +364,8 @@ export const memory = (src) => {
350
364
  for (let i = 0; i < n; i++) {
351
365
  let v = obj[schema[i]]
352
366
  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)
367
+ else if (typeof v === 'string') v = mem.String(v)
368
+ else if (Array.isArray(v)) v = mem.Array(v)
355
369
  wrapped[i] = f64ToI64(v)
356
370
  }
357
371
  const dst = new BigInt64Array(mem.buffer, raw, n)
@@ -360,7 +374,7 @@ export const memory = (src) => {
360
374
  }
361
375
 
362
376
  mem.read = function(p) {
363
- if (Array.isArray(p)) return p.map(v => this.read(v)) // multi-value tuple
377
+ if (Array.isArray(p)) return p.map(v => mem.read(v)) // multi-value tuple
364
378
  if (p === p) return p // regular number passthrough (NaN fails ===)
365
379
  const t = type(p), a = aux(p), off = offset(p)
366
380
  if (t === 0 && off === 0) {
@@ -369,13 +383,13 @@ export const memory = (src) => {
369
383
  if (a === 4) return false
370
384
  if (a === 5) return true
371
385
  }
372
- if (t === 11 && this._extMap) return this._extMap[off]
386
+ if (t === 11 && mem._extMap) return mem._extMap[off]
373
387
  if (t === 1) { // ARRAY
374
388
  let m = dv(), aOff = off
375
389
  // Follow forwarding pointers (cap === -1 means array was reallocated)
376
390
  while (m.getInt32(aOff - 4, true) === -1) aOff = m.getInt32(aOff - 8, true)
377
391
  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))
392
+ for (let i = 0; i < len; i++) out[i] = mem.read(m.getFloat64(aOff + i * 8, true))
379
393
  return out
380
394
  }
381
395
  if (t === 3) { // TYPED
@@ -396,21 +410,21 @@ export const memory = (src) => {
396
410
  new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
397
411
  return out
398
412
  }
399
- if (t === 4) { // STRING (aux bit 0x4000 = SSO inline, else heap)
413
+ if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
400
414
  const a2 = aux(p)
401
- if (a2 & 0x4000) {
415
+ if (a2 & LAYOUT.SSO_BIT) {
402
416
  const len = a2 & 0x7; let s = ''
403
417
  for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
404
418
  return s
405
419
  }
406
420
  const len = dv().getInt32(off - 4, true)
407
- return new TextDecoder().decode(new Uint8Array(mem.buffer, off, len))
421
+ return TEXT_DEC.decode(new Uint8Array(mem.buffer, off, len))
408
422
  }
409
423
  if (t === 6) { // OBJECT
410
- const m = dv(), sid = aux(p), keys = this.schemas[sid]
424
+ const m = dv(), sid = aux(p), keys = mem.schemas[sid]
411
425
  if (!keys) return p
412
426
  const obj = {}
413
- for (let i = 0; i < keys.length; i++) obj[keys[i]] = this.read(m.getFloat64(off + i * 8, true))
427
+ for (let i = 0; i < keys.length; i++) obj[keys[i]] = mem.read(m.getFloat64(off + i * 8, true))
414
428
  return obj
415
429
  }
416
430
  if (t === 7) { // HASH
@@ -419,8 +433,8 @@ export const memory = (src) => {
419
433
  for (let i = 0, found = 0; i < cap && found < size; i++) {
420
434
  const hash = m.getFloat64(off + i * 24, true)
421
435
  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))
436
+ const key = mem.read(m.getFloat64(off + i * 24 + 8, true))
437
+ obj[key] = mem.read(m.getFloat64(off + i * 24 + 16, true))
424
438
  found++
425
439
  }
426
440
  }
@@ -431,7 +445,7 @@ export const memory = (src) => {
431
445
  const set = new Set()
432
446
  for (let i = 0; i < cap && set.size < size; i++) {
433
447
  const hash = m.getFloat64(off + i * 16, true)
434
- if (hash !== 0) set.add(this.read(m.getFloat64(off + i * 16 + 8, true)))
448
+ if (hash !== 0) set.add(mem.read(m.getFloat64(off + i * 16 + 8, true)))
435
449
  }
436
450
  return set
437
451
  }
@@ -440,7 +454,7 @@ export const memory = (src) => {
440
454
  const map = new Map()
441
455
  for (let i = 0; i < cap && map.size < size; i++) {
442
456
  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)))
457
+ if (hash !== 0) map.set(mem.read(m.getFloat64(off + i * 24 + 8, true)), mem.read(m.getFloat64(off + i * 24 + 16, true)))
444
458
  }
445
459
  return map
446
460
  }
@@ -470,7 +484,7 @@ export const memory = (src) => {
470
484
  for (let i = 0; i < data.length; i++) m[setter](off + i * stride, data[i], true)
471
485
  }
472
486
  } else if (t === 6) {
473
- const schema = this.schemas[aux(p)]
487
+ const schema = mem.schemas[aux(p)]
474
488
  if (!schema) throw Error(`write: unknown schema`)
475
489
  for (const k of Object.keys(data)) {
476
490
  const i = schema.indexOf(k)
@@ -482,13 +496,29 @@ export const memory = (src) => {
482
496
  }
483
497
 
484
498
  mem.alloc = alloc
485
- mem.reset = wasmExports?._clear || jsReset
499
+ // Rewind to the JS-captured post-init heap mark, NOT the wasm `_clear` (which
500
+ // rewinds to the static-data end and would clobber module-global heap values —
501
+ // a top-level `let o = {…}` — on the first alloc after reset). `jsReset`'s base
502
+ // is `$__heap` read after instantiation (start ran), i.e. exactly the high-water
503
+ // mark above all module-init allocations. Both share `$__heap`, so a wasm `_alloc`
504
+ // and this reset stay consistent. (Shared memory has no `$__heap` global → base
505
+ // is the fixed start, preserving prior behavior.)
506
+ mem.reset = jsReset
486
507
 
487
508
  // TypedArray constructors: memory.Float64Array(data), etc.
509
+ // Bulk-copy path: when input is a TypedArray whose element type matches
510
+ // the target (same stride), use .set() for a fast memcpy instead of
511
+ // per-element DataView writes. Falls back to DataView for mismatched types.
512
+ const TA = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]
488
513
  for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
489
514
  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)
515
+ const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
516
+ if (stride >= 2 && TA[elemId] && data instanceof TA[elemId]) {
517
+ new TA[elemId](mem.buffer, off, n).set(data)
518
+ } else {
519
+ const m = dv()
520
+ for (let i = 0; i < n; i++) m[setter](off + i * stride, data[i], true)
521
+ }
492
522
  return ptr(3, elemId, off)
493
523
  }
494
524
  }
@@ -513,19 +543,8 @@ export const wrap = (memSrc, inst) => {
513
543
  restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
514
544
  } catch (e) { /* ignore */ }
515
545
  }
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
546
  // 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
547
+ // (jsstring carrier — js-host only). JS values at these positions pass through
529
548
  // unchanged — no `mem.wrapVal` (would NaN-box into f64, defeating the point).
530
549
  // `def` (optional) maps idx → default-string for jsstring params whose
531
550
  // default substitution happens JS-side (the wasm side never sees null).
@@ -542,7 +561,6 @@ export const wrap = (memSrc, inst) => {
542
561
  }
543
562
  } catch { /* ignore */ }
544
563
  }
545
-
546
564
  const mem = memory(memSrc)
547
565
  const lastErrBits = realInst.exports.__jz_last_err_bits
548
566
  const decodeThrown = error => {
@@ -550,7 +568,9 @@ export const wrap = (memSrc, inst) => {
550
568
  const bits = lastErrBits.value
551
569
  _u32[0] = Number(bits & 0xffffffffn)
552
570
  _u32[1] = Number((bits >> 32n) & 0xffffffffn)
553
- const value = mem ? mem.read(_f64[0]) : _f64[0]
571
+ // Memoryless module: the thrown value is a number/atom/SSO string decode it
572
+ // from bits. (A heap Error/string can only exist when the module has memory.)
573
+ const value = mem ? mem.read(_f64[0]) : decode(_f64[0])
554
574
  if (value instanceof Error) throw value
555
575
  const wrapped = new Error(typeof value === 'string' ? value : String(value))
556
576
  wrapped.cause = error
@@ -558,118 +578,52 @@ export const wrap = (memSrc, inst) => {
558
578
  throw wrapped
559
579
  }
560
580
  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
- }
581
+ // Wrap one positional arg. Externref slots (jsstring carrier) pass the JS
582
+ // value straight through `mem.wrapVal` would NaN-box it substituting a
583
+ // jsstring literal default for a missing arg. Every other slot marshals via
584
+ // `box`: `coerce` for pure-scalar modules, `mem.wrapVal` for heap modules.
585
+ // Quiet-NaN ABI: every export value is f64, so there is no per-position i64
586
+ // carrier args flow straight to the wasm call, results straight to decode/read.
587
+ const wrapArgAt = (ext, i, x, box) =>
588
+ ext?.has(i) ? (x === undefined && ext.def?.has(i) ? ext.def.get(i) : x) : box(x)
607
589
 
608
590
  // Pure scalar module (no memory): pass f64 values directly, no marshaling
609
- if (!mem) {
591
+ if (!mem || mem.scalar) {
610
592
  for (const [name, fn] of Object.entries(realInst.exports)) {
611
593
  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
594
  const ext = extExp.get(name)
615
595
  const len = fn.length
616
- try {
617
- exports[name] = makeFastWrapper(fn, len, p, r, decode, null, ext)
618
- continue
619
- } catch { /* CSP fallback */ }
620
596
  exports[name] = (...args) => {
621
597
  while (args.length < len) args.push(undefined)
622
598
  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))
599
+ return decode(fn(...args.map((x, i) => wrapArgAt(ext, i, x, coerce))))
628
600
  } catch (e) { decodeThrown(e) }
629
601
  }
630
602
  }
631
603
  return exports
632
604
  }
633
605
  const memWrapVal = mem.wrapVal.bind(mem)
634
- const memRead = mem.read.bind(mem)
635
606
  for (const [name, fn] of Object.entries(realInst.exports)) {
636
607
  if (restFuncs.has(name) && typeof fn === 'function') {
637
608
  const fixed = restFuncs.get(name)
638
- const sig = i64Exp.get(name)
639
- const p = sig?.p || EMPTY_SET, r = sig?.r || 0
640
609
  const ext = extExp.get(name)
641
610
  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
- })
611
+ const a = args.slice(0, fixed).map((x, i) => wrapArgAt(ext, i, x, memWrapVal))
646
612
  while (a.length < fixed) a.push(UNDEF_NAN)
647
613
  a.push(mem.Array(args.slice(fixed)))
648
614
  try {
649
- const ret = fn.apply(null, adaptArgs(a, p))
650
- return mem.read(adaptRet(ret, r))
615
+ return mem.read(fn.apply(null, a))
651
616
  } catch (error) {
652
617
  decodeThrown(error)
653
618
  }
654
619
  }
655
620
  } else if (typeof fn === 'function') {
656
- const sig = i64Exp.get(name)
657
- const p = sig?.p || EMPTY_SET, r = sig?.r || 0
658
621
  const ext = extExp.get(name)
659
622
  const len = fn.length
660
- try {
661
- exports[name] = makeFastWrapper(fn, len, p, r, memRead, memWrapVal, ext)
662
- continue
663
- } catch { /* CSP fallback */ }
664
623
  exports[name] = (...args) => {
665
624
  while (args.length < len) args.push(undefined)
666
625
  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))
626
+ return mem.read(fn.apply(null, args.map((x, i) => wrapArgAt(ext, i, x, memWrapVal))))
673
627
  } catch (error) {
674
628
  decodeThrown(error)
675
629
  }
@@ -729,7 +683,7 @@ const installDefaultEnvImports = (mod, imports, state) => {
729
683
  // across the wasm→JS boundary (see module/console.js header). Reinterpret
730
684
  // the BigInt's bits as f64 here so mem.read sees the original NaN-box.
731
685
  const write = (valBig, fd, sep) => {
732
- const v = state.mem.read(i64ToF64(valBig))
686
+ const v = readArgBits(state, valBig)
733
687
  buf[fd] += String(v)
734
688
  if (sep === 32) buf[fd] += ' '
735
689
  else if (sep === 10) flush(fd)
@@ -747,15 +701,25 @@ const installDefaultEnvImports = (mod, imports, state) => {
747
701
  imports.env.now = (clock) =>
748
702
  clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
749
703
  }
704
+ // One i32 of entropy to seed Math.random — only present when compiled with
705
+ // { randomSeed: true }. Prefers crypto; falls back to Math.random.
706
+ if (envFns.has('rngSeed') && !imports.env.rngSeed) {
707
+ imports.env.rngSeed = () => {
708
+ const a = new Uint32Array(1)
709
+ if (globalThis.crypto?.getRandomValues) globalThis.crypto.getRandomValues(a)
710
+ else a[0] = (Math.random() * 0x100000000) >>> 0
711
+ return a[0] | 0
712
+ }
713
+ }
750
714
  if (envFns.has('parseFloat') && !imports.env.parseFloat) {
751
715
  imports.env.parseFloat = (valBig) => {
752
- const s = state.mem.read(i64ToF64(valBig))
716
+ const s = readArgBits(state, valBig)
753
717
  return parseFloat(s)
754
718
  }
755
719
  }
756
720
  if (envFns.has('parseInt') && !imports.env.parseInt) {
757
721
  imports.env.parseInt = (valBig, radix) => {
758
- const s = state.mem.read(i64ToF64(valBig))
722
+ const s = readArgBits(state, valBig)
759
723
  return parseInt(s, radix || undefined)
760
724
  }
761
725
  }
@@ -811,8 +775,8 @@ const jssProbeNative = () => {
811
775
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // header
812
776
  0x01, 0x06, 0x01, 0x60, 0x01, 0x6f, 0x01, 0x7f, // type: (externref)→i32
813
777
  0x02, 0x18, 0x01, // import section
814
- 0x0f, ...new TextEncoder().encode('wasm:js-string'), // mod name
815
- 0x06, ...new TextEncoder().encode('length'), // name
778
+ 0x0f, ...TEXT_ENC.encode('wasm:js-string'), // mod name
779
+ 0x06, ...TEXT_ENC.encode('length'), // name
816
780
  0x00, 0x00, // kind=func, type=0
817
781
  ])
818
782
  const mod = new WebAssembly.Module(bytes, { builtins: ['js-string'] })
@@ -904,7 +868,11 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
904
868
  const enhanced = memory(memSrc)
905
869
  state.mem = enhanced
906
870
  state.flushPrint?.()
907
- return { exports: wrap(memSrc), memory: enhanced, instance: inst, module: mod }
871
+ // A memoryless module keeps a minimal reader internally (state.mem, for decoding
872
+ // its SSO/atom boundary values), but the result's `.memory` stays null — the
873
+ // module genuinely exposes no linear memory. `jz.memory(result)` still hands back
874
+ // a usable reader on demand.
875
+ return { exports: wrap(memSrc), memory: enhanced?.scalar ? null : enhanced, instance: inst, module: mod }
908
876
  }
909
877
 
910
878
  /**
@@ -919,24 +887,29 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
919
887
  * @param {object} [opts] host options: imports, memory, _interp, host-shape flags
920
888
  * @returns {{ exports, memory, instance, module }}
921
889
  */
890
+ /**
891
+ * Compile wasm bytes to a `WebAssembly.Module`, preferring native
892
+ * `wasm:js-string` builtins when the engine honors the option (V8 17+/Safari
893
+ * 18.4+; older engines throw or ignore it — try-fallback handles both). A
894
+ * `WebAssembly.Module` passed in is returned as-is. Factor it out so callers
895
+ * can compile once and instantiate many times without re-validating the bytes
896
+ * (`instantiate(toModule(wasm))` skips the per-call compile on hot loops).
897
+ *
898
+ * @param {Uint8Array|ArrayBuffer|WebAssembly.Module} wasm
899
+ * @returns {WebAssembly.Module}
900
+ */
901
+ export const toModule = (wasm) => {
902
+ if (wasm instanceof WebAssembly.Module) return wasm
903
+ if (jssProbeNative()) {
904
+ try { return new WebAssembly.Module(wasm, { builtins: ['js-string'] }) }
905
+ catch { return new WebAssembly.Module(wasm) }
906
+ }
907
+ return new WebAssembly.Module(wasm)
908
+ }
909
+
922
910
  export const instantiate = (wasm, opts = {}) => {
923
911
  const state = prepareInterop(opts)
924
- opts.extMap = state.extMap
925
- // Prefer native `wasm:js-string` builtins when the engine honors the option.
926
- // The option is silently accepted by V8 17+/Safari 18.4+; older engines that
927
- // don't recognize it either throw or ignore it — try-fallback handles both.
928
- let mod
929
- if (wasm instanceof WebAssembly.Module) {
930
- mod = wasm
931
- } else if (jssProbeNative()) {
932
- try {
933
- mod = new WebAssembly.Module(wasm, { builtins: ['js-string'] })
934
- } catch {
935
- mod = new WebAssembly.Module(wasm)
936
- }
937
- } else {
938
- mod = new WebAssembly.Module(wasm)
939
- }
912
+ const mod = toModule(wasm)
940
913
  const { imports, needsWasi } = buildImports(mod, opts, state)
941
914
  const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
942
915
  const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)
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>