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/module/symbol.js CHANGED
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import { typed, asF64, mkPtrIR } from '../src/ir.js'
22
- import { emit } from '../src/emit.js'
22
+ import { emit } from '../src/bridge.js'
23
23
  import { err, inc, PTR } from '../src/ctx.js'
24
24
 
25
25
  const RESERVED = 16 // first user atom ID
package/module/timer.js CHANGED
@@ -27,17 +27,12 @@
27
27
  */
28
28
 
29
29
  import { typed, asF64, asI64, UNDEF_NAN, MAX_CLOSURE_ARITY, temp, tempI64 } from '../src/ir.js'
30
- import { emit } from '../src/emit.js'
31
- import { inc, PTR, LAYOUT } from '../src/ctx.js'
30
+ import { emit, deps, hostImport } from '../src/bridge.js'
31
+ import { inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
32
32
 
33
33
  const MAX_TIMERS = 64
34
34
  const ENTRY_SIZE = 40
35
35
 
36
- const addImportOnce = (ctx, mod, name, fn) => {
37
- if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
38
- ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
39
- }
40
-
41
36
  // Shared "fire a NaN-boxed closure with 0 args" trampoline. Funcref index lives
42
37
  // in upper 16 bits of the pointer payload; remaining $ftN slots get UNDEF_NAN.
43
38
  // Closure is also passed as $__env so captures resolve via env-load.
@@ -55,7 +50,7 @@ const setupWasi = (ctx) => {
55
50
  // Always include init + tick + loop when timer module loads (structural, not per-emitter)
56
51
  inc('__timer_init', '__timer_tick', '__timer_loop')
57
52
 
58
- Object.assign(ctx.core.stdlibDeps, {
53
+ deps({
59
54
  __timer_init: ['__alloc'],
60
55
  __timer_add: ['__time_ns'],
61
56
  __timer_cancel: [],
@@ -68,7 +63,7 @@ const setupWasi = (ctx) => {
68
63
  // call_indirect always matches the $ftN type (env, argc, a0..a7)
69
64
  ctx.closure.floor = MAX_CLOSURE_ARITY
70
65
 
71
- addImportOnce(ctx, 'wasi_snapshot_preview1', 'clock_time_get',
66
+ hostImport('wasi_snapshot_preview1', 'clock_time_get',
72
67
  ['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']])
73
68
 
74
69
  // __time_ns() → i64 — current monotonic nanoseconds
@@ -235,9 +230,9 @@ const setupWasi = (ctx) => {
235
230
  // $__timer_queue: i32 — base address of timer array
236
231
  // $__timer_next_id: i32 — next timer ID
237
232
  // $__timer_count: i32 — number of active timers
238
- ctx.scope.globals.set('__timer_queue', '(global $__timer_queue (mut i32) (i32.const 0))')
239
- ctx.scope.globals.set('__timer_next_id', '(global $__timer_next_id (mut i32) (i32.const 0))')
240
- ctx.scope.globals.set('__timer_count', '(global $__timer_count (mut i32) (i32.const 0))')
233
+ declGlobal('__timer_queue', 'i32')
234
+ declGlobal('__timer_next_id', 'i32')
235
+ declGlobal('__timer_count', 'i32')
241
236
 
242
237
  // Emitter: setTimeout(closure, delay) → timer_id
243
238
  ctx.core.emit['setTimeout'] = (closureExpr, delayExpr) => {
@@ -279,9 +274,9 @@ const setupJsHost = (ctx) => {
279
274
  // canonicalization at the wasm→JS boundary (same reason as env.print —
280
275
  // see module/console.js header). delay is a real numeric f64 (no NaN-box
281
276
  // hazard), repeat is i32, return is the timer id (numeric int).
282
- const needSetTimeout = () => addImportOnce(ctx, 'env', 'setTimeout',
277
+ const needSetTimeout = () => hostImport('env', 'setTimeout',
283
278
  ['func', '$__set_timeout', ['param', 'i64'], ['param', 'f64'], ['param', 'i32'], ['result', 'f64']])
284
- const needClearTimeout = () => addImportOnce(ctx, 'env', 'clearTimeout',
279
+ const needClearTimeout = () => hostImport('env', 'clearTimeout',
285
280
  ['func', '$__clear_timeout', ['param', 'f64'], ['result', 'f64']])
286
281
 
287
282
  ctx.core.stdlib['__invoke_closure'] = invokeClosureFn(true)
@@ -7,23 +7,17 @@
7
7
  * @module typed
8
8
  */
9
9
 
10
- import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
11
- import { emit } from '../src/emit.js'
12
- import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
13
- import { inc, PTR } from '../src/ctx.js'
14
-
15
-
16
- // Element types and their byte sizes
17
- const ELEM = {
18
- Int8Array: 0, Uint8Array: 1,
19
- Int16Array: 2, Uint16Array: 3,
20
- Int32Array: 4, Uint32Array: 5,
21
- Float32Array: 6, Float64Array: 7,
22
- BigInt64Array: 7, BigUint64Array: 7,
23
- }
24
- const BIGINT_ELEM_FLAG = 16
25
- const typedAux = (name, isView = false) => ELEM[name] | (isView ? 8 : 0) |
26
- (name === 'BigInt64Array' || name === 'BigUint64Array' ? BIGINT_ELEM_FLAG : 0)
10
+ import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, ptrTypeEq, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
11
+ import { emit, idx, deps, call } from '../src/bridge.js'
12
+ import { valTypeOf } from '../src/kind.js'
13
+ import { VAL, lookupValType } from '../src/reps.js'
14
+ import { nanPrefixHex, TYPED_ELEM_NAMES, TYPED_ELEM_CODE, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux } from '../layout.js'
15
+ import { inc, PTR, getter } from '../src/ctx.js'
16
+
17
+ const _NAN_BITS = nanPrefixHex()
18
+
19
+
20
+ const typedAux = (name, isView = false) => encodeTypedElemAux(name, isView)
27
21
  const STRIDE = [1, 1, 2, 2, 4, 4, 4, 8]
28
22
  const SHIFT = [0, 0, 1, 1, 2, 2, 2, 3]
29
23
  const LOAD = [
@@ -202,11 +196,14 @@ function genSimdMap(name, elemType, pattern) {
202
196
 
203
197
 
204
198
  export default (ctx) => {
205
- Object.assign(ctx.core.stdlibDeps, {
199
+ deps({
206
200
  __byte_length: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
207
201
  __byte_offset: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
208
202
  __to_buffer: ['__ptr_type', '__ptr_offset', '__ptr_aux', '__mkptr'],
209
203
  __typed_set_idx: ['__ptr_aux', '__ptr_offset'],
204
+ // __str_join uses __typed_idx when typedarray is loaded (plain arrays promoted to
205
+ // Int32Array by promoteIntArrayLiterals can produce PTR.TYPED results via .map()).
206
+ __str_join: [...(ctx.core.stdlibDeps.__str_join ?? []), '__typed_idx'],
210
207
  })
211
208
 
212
209
  // .map invokes with arity 1; .forEach/.find/.some/.every/.filter/.findIndex
@@ -217,6 +214,10 @@ export default (ctx) => {
217
214
 
218
215
  inc('__mkptr', '__alloc', '__len')
219
216
 
217
+ const buf = call('__to_buffer', 'I')
218
+ const blen = call('__byte_length', 'I', 'i32')
219
+ const boff = call('__byte_offset', 'I', 'i32')
220
+
220
221
  // === Runtime helpers: byte length, buffer coerce ===
221
222
  // __typed_shift lives in core (needed by __len/__cap).
222
223
 
@@ -261,7 +262,7 @@ export default (ctx) => {
261
262
  (else (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $off)))))))`
262
263
 
263
264
  // Constructor: new Float64Array(len) | new F64Array(arr) | new F64Array(buf) | new F64Array(buf, off, len)
264
- for (const [name, elemType] of Object.entries(ELEM)) {
265
+ for (const [name, elemType] of Object.entries(TYPED_ELEM_CODE)) {
265
266
  const aux = typedAux(name)
266
267
  const stride = STRIDE[elemType]
267
268
  ctx.core.emit[`new.${name}`] = (lenExpr, offsetExpr, lenExpr2) => {
@@ -317,7 +318,7 @@ export default (ctx) => {
317
318
  numAlloc.ptr]],
318
319
  // Pointer: array → copy elements; buffer/typed → zero-copy view on same offset
319
320
  ['else', ['if', ['result', 'f64'],
320
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${src}`]]], ['i32.const', PTR.ARRAY]],
321
+ ptrTypeEq(['local.get', `$${src}`], PTR.ARRAY),
321
322
  ['then', ctx.core.emit[`${name}.from`](src)],
322
323
  ['else', mkPtrIR(PTR.TYPED, aux,
323
324
  ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]])]]]]], 'f64')
@@ -404,14 +405,14 @@ export default (ctx) => {
404
405
  // .buffer — always aliased (zero-copy). BUFFER: passthrough.
405
406
  // Owned TYPED: retag as BUFFER at same offset — the byteLen header is shared.
406
407
  // TYPED view (incl. DataView): BUFFER at descriptor[8] (root parent data offset).
407
- ctx.core.emit['.buffer'] = (obj) => {
408
+ ctx.core.emit['.buffer'] = getter((obj) => {
408
409
  if (typeof obj === 'string') {
409
410
  const ctor = ctx.types.typedElem?.get(obj)
410
411
  if (ctor === 'new.ArrayBuffer') return asF64(emit(obj))
411
412
  if (ctor?.startsWith('new.')) {
412
413
  const isView = ctor.endsWith('.view')
413
414
  const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
414
- if (ELEM[name] != null) {
415
+ if (TYPED_ELEM_CODE[name] != null) {
415
416
  const parentOff = isView
416
417
  ? ['i32.load', ['i32.add', ptrOffsetIR(emit(obj), VAL.TYPED), ['i32.const', 8]]]
417
418
  : ptrOffsetIR(emit(obj), VAL.TYPED)
@@ -419,13 +420,12 @@ export default (ctx) => {
419
420
  }
420
421
  }
421
422
  }
422
- inc('__to_buffer')
423
- return typed(['call', '$__to_buffer', asI64(emit(obj))], 'f64')
424
- }
423
+ return buf(obj)
424
+ })
425
425
 
426
426
  // .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
427
427
  // View TYPED (incl. DataView): descriptor[0], via the __byte_length fallback.
428
- ctx.core.emit['.byteLength'] = (obj) => {
428
+ ctx.core.emit['.byteLength'] = getter((obj) => {
429
429
  if (typeof obj === 'string') {
430
430
  const ctor = ctx.types.typedElem?.get(obj)
431
431
  if (ctor === 'new.ArrayBuffer') {
@@ -434,7 +434,7 @@ export default (ctx) => {
434
434
  if (ctor && ctor.startsWith('new.')) {
435
435
  const isView = ctor.endsWith('.view')
436
436
  const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
437
- const et = ELEM[name]
437
+ const et = TYPED_ELEM_CODE[name]
438
438
  if (et != null) {
439
439
  if (isView) {
440
440
  return typed(['f64.convert_i32_s',
@@ -445,12 +445,11 @@ export default (ctx) => {
445
445
  }
446
446
  }
447
447
  }
448
- inc('__byte_length')
449
- return typed(['f64.convert_i32_s', ['call', '$__byte_length', asI64(emit(obj))]], 'f64')
450
- }
448
+ return blen(obj)
449
+ })
451
450
 
452
451
  // .byteOffset — owned: 0. View: descriptor[4] - descriptor[8].
453
- ctx.core.emit['.byteOffset'] = (obj) => {
452
+ ctx.core.emit['.byteOffset'] = getter((obj) => {
454
453
  if (typeof obj === 'string') {
455
454
  const ctor = ctx.types.typedElem?.get(obj)
456
455
  if (ctor?.endsWith('.view')) {
@@ -462,11 +461,10 @@ export default (ctx) => {
462
461
  ['i32.load', ['i32.add', ['local.get', `$${t}`], ['i32.const', 4]]],
463
462
  ['i32.load', ['i32.add', ['local.get', `$${t}`], ['i32.const', 8]]]]]], 'f64')
464
463
  }
465
- if (ctor?.startsWith('new.') && ELEM[ctor.slice(4)] != null) return typed(['f64.const', 0], 'f64')
464
+ if (ctor?.startsWith('new.') && TYPED_ELEM_CODE[ctor.slice(4)] != null) return typed(['f64.const', 0], 'f64')
466
465
  }
467
- inc('__byte_offset')
468
- return typed(['f64.convert_i32_s', ['call', '$__byte_offset', asI64(emit(obj))]], 'f64')
469
- }
466
+ return boff(obj)
467
+ })
470
468
 
471
469
  // Runtime fallback for .byteOffset when variable view-ness is unknown.
472
470
  ctx.core.stdlib['__byte_offset'] = `(func $__byte_offset (param $ptr i64) (result i32)
@@ -488,8 +486,7 @@ export default (ctx) => {
488
486
  ctx.core.emit['ArrayBuffer.isView'] = (v) => {
489
487
  if (v === undefined) return typed(['f64.const', 0], 'f64')
490
488
  const va = asF64(emit(v))
491
- return typed(['f64.convert_i32_s',
492
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', va]], ['i32.const', PTR.TYPED]]], 'f64')
489
+ return typed(['f64.convert_i32_s', ptrTypeEq(va, PTR.TYPED)], 'f64')
493
490
  }
494
491
 
495
492
  // x instanceof Float64Array | Int32Array | … — typed-pointer predicate emitted
@@ -502,7 +499,7 @@ export default (ctx) => {
502
499
  const t = temp('ityp')
503
500
  return typed(['i32.and',
504
501
  ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
505
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.TYPED]]], 'i32')
502
+ ptrTypeEq(['local.get', `$${t}`], PTR.TYPED)], 'i32')
506
503
  }
507
504
 
508
505
  // buf.slice(begin?, end?) on a BUFFER → fresh BUFFER with the byte range copied.
@@ -598,7 +595,7 @@ export default (ctx) => {
598
595
  // (cheap: 4 wasm ops) so `getFloat32`/`getFloat64` return a real-spec NaN value.
599
596
  ctx.core.stdlib['__canon_nan'] = `(func $__canon_nan (param $v f64) (result f64)
600
597
  (select
601
- (f64.reinterpret_i64 (i64.const 0x7FF8000000000000))
598
+ (f64.reinterpret_i64 (i64.const ${_NAN_BITS}))
602
599
  (local.get $v)
603
600
  (f64.ne (local.get $v) (local.get $v))))`
604
601
  const canonNaN = (vIR) => { inc('__canon_nan'); return typed(['call', '$__canon_nan', vIR], 'f64') }
@@ -799,7 +796,7 @@ export default (ctx) => {
799
796
  }
800
797
 
801
798
  // TypedArray.from(arr) — convert regular array to typed array
802
- for (const [name, elemType] of Object.entries(ELEM)) {
799
+ for (const [name, elemType] of Object.entries(TYPED_ELEM_CODE)) {
803
800
  const aux = typedAux(name)
804
801
  const stride = STRIDE[elemType], store = STORE[elemType]
805
802
  ctx.core.emit[`${name}.from`] = (src) => {
@@ -858,7 +855,7 @@ export default (ctx) => {
858
855
  if (!ctor) return null
859
856
  const isView = !chainOutput && ctor.endsWith('.view')
860
857
  const name = ctor.endsWith('.view') ? ctor.slice(4, -5) : ctor.slice(4)
861
- const et = ELEM[name]
858
+ const et = TYPED_ELEM_CODE[name]
862
859
  return et == null ? null : { et, isView, isBigInt: name === 'BigInt64Array' || name === 'BigUint64Array' }
863
860
  }
864
861
 
@@ -913,7 +910,7 @@ export default (ctx) => {
913
910
  (local.set $et (i32.and (local.get $aux) (i32.const 7)))
914
911
  (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
915
912
  (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
916
- (then (if (result f64) (i32.and (local.get $aux) (i32.const ${BIGINT_ELEM_FLAG}))
913
+ (then (if (result f64) (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
917
914
  (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
918
915
  (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
919
916
  (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
@@ -938,7 +935,7 @@ export default (ctx) => {
938
935
  (if (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0))
939
936
  (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
940
937
  (local.set $et (i32.and (local.get $aux) (i32.const 7)))
941
- (if (i32.and (local.get $aux) (i32.const ${BIGINT_ELEM_FLAG}))
938
+ (if (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
942
939
  (then (i64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (i64.reinterpret_f64 (local.get $v))))
943
940
  (else
944
941
  (if (i32.eq (local.get $et) (i32.const 7))
@@ -960,11 +957,11 @@ export default (ctx) => {
960
957
  (local.get $v))`
961
958
 
962
959
  // Type-aware TypedArray read: arr[i]
963
- ctx.core.emit['.typed:[]'] = (arr, idx) => {
960
+ ctx.core.emit['.typed:[]'] = (arr, i) => {
964
961
  const r = resolveElem(arr)
965
962
  if (r == null) return null // unknown type, fallback to generic
966
963
  const { et, isView, isBigInt } = r
967
- const objIR = emit(arr), vi = asI32(emit(idx))
964
+ const objIR = emit(arr), vi = idx(i)
968
965
  const off = ['i32.add', typedDataAddr(objIR, isView), ['i32.shl', vi, ['i32.const', SHIFT[et]]]]
969
966
  if (isBigInt) return typed(['f64.reinterpret_i64', ['i64.load', off]], 'f64')
970
967
  if (et === 7) return typed(['f64.load', off], 'f64') // Float64Array
@@ -974,11 +971,11 @@ export default (ctx) => {
974
971
  }
975
972
 
976
973
  // Type-aware TypedArray write: arr[i] = val
977
- ctx.core.emit['.typed:[]='] = (arr, idx, val, void_ = false) => {
974
+ ctx.core.emit['.typed:[]='] = (arr, i, val, void_ = false) => {
978
975
  const r = resolveElem(arr)
979
976
  if (r == null) return null
980
977
  const { et, isView, isBigInt } = r
981
- const objIR = emit(arr), vi = asI32(emit(idx)), valIR = emit(val)
978
+ const objIR = emit(arr), vi = idx(i), valIR = emit(val)
982
979
  const off = ['i32.add', typedDataAddr(objIR, isView), ['i32.shl', vi, ['i32.const', SHIFT[et]]]]
983
980
  if (isBigInt) {
984
981
  const vt = temp('tw')
@@ -1213,7 +1210,7 @@ export default (ctx) => {
1213
1210
  // ABI width at 2 to spare a slot across the whole program). Reduce passes
1214
1211
  // (acc, item). Closure invocation goes through `ctx.closure.call` directly.
1215
1212
  // The element-type-name list is needed by allocPtr for typedAux:
1216
- const ET_NAME = ['Int8Array','Uint8Array','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array']
1213
+ const ET_NAME = TYPED_ELEM_NAMES
1217
1214
 
1218
1215
  // .forEach: callback (item, idx). Result is 0 to match array.js's
1219
1216
  // convention (spec says undefined; both modules pick 0 since f() exposes the
package/package.json CHANGED
@@ -1,37 +1,56 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Functional JS subset compiling to WASM",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
9
  "./wasi": "./wasi.js",
10
- "./interop": "./interop.js"
10
+ "./interop": "./interop.js",
11
+ "./transform": "./transform.js"
11
12
  },
12
13
  "files": [
13
14
  "index.js",
14
15
  "cli.js",
15
16
  "wasi.js",
16
17
  "interop.js",
18
+ "layout.js",
19
+ "transform.js",
20
+ "jzify",
17
21
  "src",
18
22
  "module",
19
- "README.md"
23
+ "README.md",
24
+ "jz.svg",
25
+ "alternatives.svg",
26
+ "bench/bench.svg"
20
27
  ],
21
28
  "bin": {
22
29
  "jz": "cli.js"
23
30
  },
24
31
  "scripts": {
25
32
  "test": "node test/index.js",
26
- "test262": "node test/test262.js",
27
- "test262:builtins": "node test/test262-builtins.js",
33
+ "test:opt0": "JZ_TEST_OPTIMIZE=0 node test/index.js",
34
+ "test:opt1": "JZ_TEST_OPTIMIZE=1 node test/index.js",
35
+ "test:opt3": "JZ_TEST_OPTIMIZE=3 node test/index.js",
36
+ "test:wasi": "JZ_TEST_HOST=wasi node test/index.js",
37
+ "test:wasm": "JZ_TEST_TARGET=jz.wasm node test/index.js",
38
+ "test:matrix": "npm test && npm run test:opt0 && npm run test:opt3 && npm run test:wasi",
39
+ "test:bench": "node test/bench.js",
40
+ "test:fuzz": "node test/fuzz.js --count=5000",
41
+ "test:ratchet": "node test/perf-ratchet.js",
42
+ "test:self": "node test/selfhost.js",
43
+ "test:262": "node test/test262.js",
44
+ "test:262:builtins": "node test/test262-builtins.js",
45
+ "test:all": "npm run test:matrix && npm run test:262 && npm run test:262:builtins && npm run test:bench && npm run test:self",
28
46
  "bench": "node bench/bench.mjs",
47
+ "bench:fuzz": "node scripts/fuzz-bench.mjs",
29
48
  "bench:size": "node scripts/bench-size.mjs",
30
49
  "bench:compile": "node scripts/bench-compile.mjs",
31
- "build:examples": "for dir in examples/*; do node \"$dir/build.mjs\"; done",
32
- "test:bench": "node test/bench.js",
33
- "test:all": "npm test && npm run test:bench",
34
- "prepublishOnly": "npm run test:all"
50
+ "bench:startup": "node scripts/bench-startup.mjs",
51
+ "build": "node scripts/build-dist.mjs",
52
+ "build:examples": "for dir in examples/*/; do [ -f \"$dir/build.mjs\" ] && node \"$dir/build.mjs\"; done",
53
+ "prepublishOnly": "npm test && npm run test:self"
35
54
  },
36
55
  "repository": {
37
56
  "type": "git",
@@ -44,8 +63,8 @@
44
63
  "author": "Dmitry Iv",
45
64
  "license": "MIT",
46
65
  "dependencies": {
47
- "subscript": "^10.4.15",
48
- "watr": "^4.6.9"
66
+ "subscript": "^10.4.17",
67
+ "watr": "^4.6.10"
49
68
  },
50
69
  "keywords": [
51
70
  "javascript",
@@ -53,9 +72,19 @@
53
72
  "compiler",
54
73
  "functional",
55
74
  "minimal",
56
- "wasm"
75
+ "wasm",
76
+ "aot",
77
+ "simd",
78
+ "wasi",
79
+ "wat",
80
+ "dsp",
81
+ "audio",
82
+ "creative-coding",
83
+ "assemblyscript",
84
+ "porffor"
57
85
  ],
58
86
  "devDependencies": {
87
+ "esbuild": "^0.28.0",
59
88
  "tst": "^9.4.0"
60
89
  }
61
90
  }
package/src/abi/index.js CHANGED
@@ -1,21 +1,5 @@
1
1
  /**
2
- * src/abi — internal codegen carriers.
3
- *
4
- * The `abi/` directory hosts compiler-internal codegen modules — one file per
5
- * value type, each exporting every carrier (slot strategy) the compiler may
6
- * pick for that type. **No user surface.** `opts.host` is the only knob users
7
- * see; internal representation is analysis-driven and per-site.
8
- *
9
- * Today the narrower has not yet been wired to pick carriers per site, so
10
- * each type module's `default` export is used as the carrier for every site
11
- * of that type. `ctx.abi.<type>` resolves to that default; codegen reads
12
- * `ctx.abi.string.ops.byteLen(...)` etc. Per-site dispatch arrives by
13
- * exposing all carriers (`ctx.abi.string.sso`, `.jsstring`) and letting
14
- * `narrow.js` tag each binding with a carrier choice.
15
- *
16
- * No presets, no preset-name discriminant, no public ABI knob — carrier
17
- * choice is analysis-driven, not user-pickable. See `.work/todo.md`
18
- * "Boundary protocol and internal representation" for the policy.
2
+ * src/abi — internal codegen carriers + per-site dispatch.
19
3
  *
20
4
  * @module src/abi
21
5
  */
@@ -25,11 +9,43 @@ import sso, { jsstring } from './string.js'
25
9
  import tagged from './object.js'
26
10
  import taggedLinear, { structInline } from './array.js'
27
11
 
28
- /** The default carrier bundlewhat `ctx.abi` resolves to. Identity-stable
29
- * reference so codegen can compare without string keys. */
30
- export const DEFAULTS = Object.freeze({ number: nanboxF64, string: sso, object: tagged, array: taggedLinear })
12
+ /** All carriers per value type keyed by stable id for rep.carrier lookup. */
13
+ export const CARRIERS = Object.freeze({
14
+ number: Object.freeze({ nanboxF64 }),
15
+ string: Object.freeze({ sso, jsstring }),
16
+ object: Object.freeze({ tagged }),
17
+ array: Object.freeze({ taggedLinear, structInline }),
18
+ })
19
+
20
+ const DEFAULT_ID = Object.freeze({
21
+ number: 'nanboxF64',
22
+ string: 'sso',
23
+ object: 'tagged',
24
+ array: 'taggedLinear',
25
+ })
26
+
27
+ /** Pick carrier bundle for `type` given optional binding rep hints. */
28
+ export function resolveCarrier(type, rep) {
29
+ const table = CARRIERS[type]
30
+ if (!table) return null
31
+ const id = rep?.carrier ?? DEFAULT_ID[type]
32
+ return table[id] ?? table[DEFAULT_ID[type]]
33
+ }
34
+
35
+ /** Default carrier ops bundle — backward-compatible flat shape on ctx.abi. */
36
+ export const DEFAULTS = Object.freeze({
37
+ number: nanboxF64,
38
+ string: sso,
39
+ object: tagged,
40
+ array: taggedLinear,
41
+ })
42
+
43
+ /** ctx.abi bundle: default carriers + resolve() + registry. All access is by
44
+ * fixed key (`ctx.abi.object.ops`, `.string`, `.resolve`, …), so a plain
45
+ * literal — the shape jz compiles directly — is equivalent to the former
46
+ * null-proto `Object.assign` merge. */
47
+ export function makeAbi() {
48
+ return { ...DEFAULTS, carriers: CARRIERS, resolve: resolveCarrier }
49
+ }
31
50
 
32
- // Carrier re-exports — for tests and tools that want to reach a specific
33
- // carrier directly. Per-site narrowing will use these via `ctx.abi.<type>`
34
- // once the narrower exposes the full carrier dictionary.
35
51
  export { nanboxF64, sso, jsstring, tagged, taggedLinear, structInline }
package/src/abi/string.js CHANGED
@@ -56,42 +56,12 @@
56
56
  // take. Kept as a one-liner so each op reads as a single `call`.
57
57
  const ssoI64 = (sF64) => ['i64.reinterpret_f64', sF64]
58
58
 
59
- // LAYOUT lives in `src/ctx.js`, which loads this module mid-bootstrap (line 10
60
- // of ctx.js, before LAYOUT itself is bound). ESM live bindings mean the import
61
- // resolves but the value is `undefined` until ctx.js finishes. All charCodeAt /
62
- // inline paths read these constants at *call* time (compile-time, after ctx is
63
- // fully initialized) — never at module top level — so this is safe.
64
- import { LAYOUT } from '../ctx.js'
59
+ import { isReassigned } from '../ast.js'
60
+ import { LAYOUT, oobNanIR, ssoBitI64Hex } from '../../layout.js'
65
61
 
66
- // Local copy of analyze.js#isReassigned pulling the real one creates a load-
67
- // order cycle (abi/string ← ctx ← analyze ← ir ← ctx). Kept minimal: detects
68
- // any `=`/compound-assign/`++`/`--` of `name` anywhere in `body`. `let`/`const`
69
- // declarations are NOT reassignments (the param can't be redeclared anyway,
70
- // but a shadowing inner `let s = ...` shouldn't poison the outer param's
71
- // fast path). False positives are conservative — we just disable the fast
72
- // path and fall through to shape 2.
73
- const _CC_ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
74
- const _paramReassigned = (body, name) => {
75
- if (!Array.isArray(body)) return false
76
- const op = body[0]
77
- if (_CC_ASSIGN_OPS.has(op) && body[1] === name) return true
78
- if ((op === '++' || op === '--') && body[1] === name) return true
79
- if (op === 'let' || op === 'const') {
80
- for (let i = 1; i < body.length; i++) {
81
- const d = body[i]
82
- if (Array.isArray(d) && d[0] === '=' && d[2] != null && _paramReassigned(d[2], name)) return true
83
- }
84
- return false
85
- }
86
- for (let i = 1; i < body.length; i++) if (_paramReassigned(body[i], name)) return true
87
- return false
88
- }
89
-
90
- /** Pre-shifted SSO discriminator (bit 46 of the i64 ptr). Computed lazily on
91
- * first read since LAYOUT is undefined when this module's top level runs.
92
- * Memoized in the closure so all later call sites pay zero overhead. */
62
+ /** Pre-shifted SSO discriminator — layout.js is cycle-free; memoized at first use. */
93
63
  let _ssoBitI64 = null
94
- const ssoBitI64 = () => _ssoBitI64 ??= '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
64
+ const ssoBitI64 = () => _ssoBitI64 ??= ssoBitI64Hex()
95
65
 
96
66
  /** Allocate a fresh i64 local in the current function. Replicated here (not
97
67
  * imported from `src/ir.js`) to keep this module loadable during ctx.js
@@ -134,7 +104,7 @@ function emitDecompCharRead(dec, iI32, ctx, oobNan) {
134
104
  ['else', heapByteExpr]]
135
105
  const use = ['if', ['result', rt],
136
106
  ['i32.ge_u', idx, ['local.get', `$${dec.len}`]],
137
- ['then', oobNan ? ['f64.const', 'nan:0x7FF8000000000000'] : ['i32.const', 0]],
107
+ ['then', oobNan ? oobNanIR() : ['i32.const', 0]],
138
108
  ['else', oobNan ? ['f64.convert_i32_u', ccByte] : ccByte]]
139
109
  return spill
140
110
  ? ['block', ['result', rt], ['local.set', `$${spill}`, iI32], use]
@@ -252,7 +222,7 @@ export const sso = {
252
222
  // being false to terminate; an i32 `0` would loop forever.
253
223
  // A fresh OOB node per use — IR nodes must not be structurally shared
254
224
  // (later passes mutate in place).
255
- const mkOob = () => oobNan ? ['f64.const', 'nan:0x7FF8000000000000'] : ['i32.const', 0]
225
+ const mkOob = () => oobNan ? oobNanIR() : ['i32.const', 0]
256
226
  const rt = oobNan ? 'f64' : 'i32'
257
227
  const widen = b => oobNan ? ['f64.convert_i32_u', b] : b
258
228
  // Shape 1: receiver is a `local.get` of a non-boxed function parameter.
@@ -273,7 +243,7 @@ export const sso = {
273
243
  // to actually be `f64` (i64.reinterpret_f64 below would fail
274
244
  // validation otherwise — narrowed-to-int params have type 'i32').
275
245
  if (param && param.type === 'f64' && param.ptrKind == null
276
- && !isBoxed && ctx.func.body && !_paramReassigned(ctx.func.body, name)) {
246
+ && !isBoxed && ctx.func.body && !isReassigned(ctx.func.body, name)) {
277
247
  if (!ctx.func.charDecomp) ctx.func.charDecomp = new Map()
278
248
  let dec = ctx.func.charDecomp.get(name)
279
249
  if (!dec) {
@@ -362,10 +332,31 @@ export const sso = {
362
332
  return preface
363
333
  },
364
334
 
365
- /** Content equality. Both args: f64 slot carriers. Returns i32 boolean. */
335
+ /** Content equality. Both args: f64 slot carriers. Returns i32 boolean.
336
+ * The bit-eq fast path is inlined at the site: static-literal dedup, SSO
337
+ * packing and slice interning make identical bits the DOMINANT equal case
338
+ * (a compiler comparing tree tags against literals hits it ~always), so
339
+ * most comparisons skip the __str_eq call entirely. Content compare only
340
+ * on bit-mismatch. */
366
341
  eq: (aF64, bF64, ctx) => {
367
342
  ctx.core.includes.add('__str_eq')
368
- return ['call', '$__str_eq', ssoI64(aF64), ssoI64(bF64)]
343
+ // i64 temps allocated through the passed ctx — importing ir.js's tempI64
344
+ // here would close the abi→ir→ctx→abi module cycle the kernel bundler
345
+ // rejects (mirrors freshLocal's registration).
346
+ const fresh = () => {
347
+ let n
348
+ do { n = `seq${ctx.func.uniq++}` } while (ctx.func.locals.has(n))
349
+ ctx.func.locals.set(n, 'i64')
350
+ return n
351
+ }
352
+ const ta = fresh(), tb = fresh()
353
+ return ['block', ['result', 'i32'],
354
+ ['local.set', `$${ta}`, ssoI64(aF64)],
355
+ ['local.set', `$${tb}`, ssoI64(bF64)],
356
+ ['if', ['result', 'i32'],
357
+ ['i64.eq', ['local.get', `$${ta}`], ['local.get', `$${tb}`]],
358
+ ['then', ['i32.const', 1]],
359
+ ['else', ['call', '$__str_eq', ['local.get', `$${ta}`], ['local.get', `$${tb}`]]]]]
369
360
  },
370
361
 
371
362
  /** Three-way byte compare. Both args: f64 slot carriers. Returns i32 ∈ {-1, 0, 1}. */
@@ -375,8 +366,14 @@ export const sso = {
375
366
  },
376
367
 
377
368
  /** Concat with ToString coercion on both sides. Both args: f64 slot
378
- * carriers. Returns f64 (the new STRING ptr's slot carrier). */
379
- concat: (aF64, bF64, ctx) => {
369
+ * carriers. Returns f64 (the new STRING ptr's slot carrier).
370
+ * Named `cat`, not `concat`: under self-host this op is invoked as a
371
+ * method on the statically-untyped `ctx.abi.string.ops` receiver, and the
372
+ * name `concat` collides with `Array.prototype.concat` — the method-call
373
+ * dispatcher's string/array runtime guess (emit.js) would hijack it into a
374
+ * bogus array concat. A non-builtin name routes through dynamic property
375
+ * dispatch (load the closure slot, call it) correctly. */
376
+ cat: (aF64, bF64, ctx) => {
380
377
  ctx.core.includes.add('__str_concat')
381
378
  return ['call', '$__str_concat', ssoI64(aF64), ssoI64(bF64)]
382
379
  },