jz 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +23 -23
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6271
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +299 -44
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +892 -32
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +301 -84
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +71 -5
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +979 -54
  45. package/src/compile/index.js +271 -29
  46. package/src/compile/infer.js +34 -3
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/narrow.js +543 -15
  49. package/src/compile/peel-stencil.js +5 -0
  50. package/src/compile/plan/index.js +45 -3
  51. package/src/compile/plan/inline.js +8 -1
  52. package/src/compile/plan/literals.js +39 -0
  53. package/src/compile/plan/scope.js +430 -23
  54. package/src/compile/program-facts.js +732 -38
  55. package/src/ctx.js +64 -9
  56. package/src/helper-counters.js +7 -1
  57. package/src/ir.js +113 -5
  58. package/src/kind-traits.js +66 -3
  59. package/src/kind.js +84 -12
  60. package/src/op-policy.js +7 -4
  61. package/src/optimize/index.js +1060 -750
  62. package/src/optimize/recurse.js +2 -2
  63. package/src/optimize/vectorize.js +972 -67
  64. package/src/prepare/index.js +792 -63
  65. package/src/prepare/math-kernel.js +331 -0
  66. package/src/prepare/pre-eval.js +714 -0
  67. package/src/snapshot.js +194 -0
  68. package/src/type.js +1170 -56
  69. package/src/wat/assemble.js +403 -65
  70. package/src/wat/codegen.js +74 -14
  71. package/transform.js +113 -4
  72. package/wasi.js +3 -0
@@ -7,9 +7,11 @@
7
7
  * @module typed
8
8
  */
9
9
 
10
- import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, ptrTypeEq, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
10
+ import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, NULL_NAN, TRUE_NAN, FALSE_NAN, allocPtr, mkPtrIR, ptrOffsetIR, ptrTypeEq, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
11
11
  import { emit, idx, deps, call } from '../src/bridge.js'
12
+ import { strHashLiteral } from './collection.js'
12
13
  import { valTypeOf } from '../src/kind.js'
14
+ import { typedIdxProven } from '../src/type.js'
13
15
  import { VAL, lookupValType } from '../src/reps.js'
14
16
  import { nanPrefixHex, TYPED_ELEM_NAMES, TYPED_ELEM_CODE, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux } from '../layout.js'
15
17
  import { inc, PTR, LAYOUT, registerGetter } from '../src/ctx.js'
@@ -236,6 +238,40 @@ export default (ctx) => {
236
238
  const blen = call('__byte_length', 'I', 'i32')
237
239
  const boff = call('__byte_offset', 'I', 'i32')
238
240
 
241
+ // Unknown receiver for a buffer-family accessor: only BUFFER/TYPED own
242
+ // `.buffer`/`.byteLength`/`.byteOffset` in JS — on everything else the name
243
+ // is an ordinary own property (or undefined). Same dispatch shape as
244
+ // collection.js's `.size` (the dot-name hijack class, .work/todo.md
245
+ // 2026-07-11): a PROVEN BUFFER/TYPED receiver keeps the direct helper call;
246
+ // otherwise tag-dispatch at runtime — the NaN-check guards real numbers
247
+ // whose bit pattern could false-match the tag compare, and the prehashed
248
+ // dyn dispatcher covers OBJECT schema slots, HASH keys, sidecars, and
249
+ // primitives (→ undefined).
250
+ const bufAccessorDyn = (prop, helper, direct) => (obj) => {
251
+ const vt = valTypeOf(obj)
252
+ if (vt === VAL.BUFFER || vt === VAL.TYPED) return direct(obj)
253
+ inc('__ptr_type', '__dyn_get_expr_t_h', helper)
254
+ const o = temp('bad'), t = tempI32('badt')
255
+ const og = ['local.get', `$${o}`]
256
+ const helperCall = ['call', `$${helper}`, ['i64.reinterpret_f64', og]]
257
+ return typed(['block', ['result', 'f64'],
258
+ ['local.set', `$${o}`, asF64(emit(obj))],
259
+ ['local.set', `$${t}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', og]]],
260
+ ['if', ['result', 'f64'],
261
+ ['i32.and',
262
+ ['f64.ne', og, og],
263
+ ['i32.or',
264
+ ['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.BUFFER]],
265
+ ['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.TYPED]]]],
266
+ ['then', helper === '__to_buffer' ? typed(helperCall, 'f64') : ['f64.convert_i32_s', helperCall]],
267
+ ['else', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t_h',
268
+ ['i64.reinterpret_f64', og], asI64(emit(['str', prop])), ['local.get', `$${t}`],
269
+ ['i32.const', strHashLiteral(prop)]]]]]], 'f64')
270
+ }
271
+ const bufDyn = bufAccessorDyn('buffer', '__to_buffer', buf)
272
+ const blenDyn = bufAccessorDyn('byteLength', '__byte_length', blen)
273
+ const boffDyn = bufAccessorDyn('byteOffset', '__byte_offset', boff)
274
+
239
275
  // === Runtime helpers: byte length, buffer coerce ===
240
276
  // __typed_shift lives in core (needed by __len/__cap).
241
277
 
@@ -364,20 +400,51 @@ export default (ctx) => {
364
400
  ['local.get', `$${parentOff}`]],
365
401
  mkPtrIR(PTR.TYPED, typedAux(name, true), ['local.get', `$${dst}`])], 'f64')
366
402
  }
403
+ // TypedArray(typedArray) COPIES into fresh storage with element conversion —
404
+ // spec: only (buffer[, off, len]) constructs a view. Element reads go through
405
+ // __typed_idx (the source's elemType lives in its aux at runtime), stores
406
+ // convert to THIS view's elemType — `new Float64Array(int32Arr)` converts.
407
+ const copyFromTyped = (srcTemp) => {
408
+ inc('__typed_idx', '__len')
409
+ const cl = tempI32('tcl'), ci = tempI32('tci')
410
+ const out = allocPtr({ type: PTR.TYPED, aux,
411
+ len: ['i32.mul', ['local.get', `$${cl}`], ['i32.const', stride]], stride: 1, tag: 'tc' })
412
+ const conv = FROM_F64[elemType]
413
+ const srcElem = ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${srcTemp}`]], ['local.get', `$${ci}`]]
414
+ const cid = ctx.func.uniq++
415
+ return ['block', ['result', 'f64'],
416
+ ['local.set', `$${cl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${srcTemp}`]]]],
417
+ out.init,
418
+ ['local.set', `$${ci}`, ['i32.const', 0]],
419
+ ['block', `$tcb${cid}`, ['loop', `$tclp${cid}`,
420
+ ['br_if', `$tcb${cid}`, ['i32.ge_s', ['local.get', `$${ci}`], ['local.get', `$${cl}`]]],
421
+ [STORE[elemType],
422
+ ['i32.add', ['local.get', `$${out.local}`], ['i32.mul', ['local.get', `$${ci}`], ['i32.const', stride]]],
423
+ conv ? [conv, srcElem] : srcElem],
424
+ ['local.set', `$${ci}`, ['i32.add', ['local.get', `$${ci}`], ['i32.const', 1]]],
425
+ ['br', `$tclp${cid}`]]],
426
+ out.ptr]
427
+ }
367
428
  // Single arg array-like source: copy elements instead of treating the pointer as a length.
368
429
  if (srcType === VAL.ARRAY && ctx.core.emit[`${name}.from`])
369
430
  return ctx.core.emit[`${name}.from`](lenExpr)
370
- // Reinterpret on a buffer or another typed array: zero-copy view.
371
- // TYPED retagged at the same offset — the byteLen header is shared with the parent.
372
- // __len(view) = byteLen >> shift computes elemCount for this view's elemType.
373
- if (srcType === VAL.BUFFER || srcType === VAL.TYPED) {
431
+ if (srcType === VAL.TYPED) {
432
+ const src = temp('ts')
433
+ return typed(['block', ['result', 'f64'],
434
+ ['local.set', `$${src}`, asF64(emit(lenExpr))],
435
+ copyFromTyped(src)], 'f64')
436
+ }
437
+ // Reinterpret on a buffer: zero-copy view. TYPED retagged at the same offset —
438
+ // the byteLen header is shared with the parent. __len(view) = byteLen >> shift
439
+ // computes elemCount for this view's elemType.
440
+ if (srcType === VAL.BUFFER) {
374
441
  ctx.features.typedView = true // zero-copy reinterpret aliases the source — SLP must not pack across it
375
442
  return mkPtrIR(PTR.TYPED, aux, ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(lenExpr))]])
376
443
  }
377
444
  if (srcType == null && ctx.core.emit[`${name}.from`]) {
378
- ctx.features.typedView = true // unknown arg: runtime may take the buffer/typed zero-copy-view branch
445
+ ctx.features.typedView = true // unknown arg: runtime may take the buffer zero-copy-view branch
379
446
 
380
- // Runtime dispatch: number → allocate; array → copy elements; buffer/typed → zero-copy view.
447
+ // Runtime dispatch: number → allocate; array/typed → copy elements; buffer → zero-copy view.
381
448
  const src = temp('ts')
382
449
  const len = tempI32('tl')
383
450
  const shift = SHIFT[elemType]
@@ -392,12 +459,15 @@ export default (ctx) => {
392
459
  ['local.set', `$${len}`, ['i32.trunc_sat_f64_s', ['local.get', `$${src}`]]],
393
460
  numAlloc.init,
394
461
  numAlloc.ptr]],
395
- // Pointer: array → copy elements; buffer/typed → zero-copy view on same offset
462
+ // Pointer: array → boxed-slot copy; typed → converted element copy; buffer → zero-copy view
396
463
  ['else', ['if', ['result', 'f64'],
397
464
  ptrTypeEq(['local.get', `$${src}`], PTR.ARRAY),
398
465
  ['then', ctx.core.emit[`${name}.from`](src)],
399
- ['else', mkPtrIR(PTR.TYPED, aux,
400
- ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]])]]]]], 'f64')
466
+ ['else', ['if', ['result', 'f64'],
467
+ ptrTypeEq(['local.get', `$${src}`], PTR.TYPED),
468
+ ['then', copyFromTyped(src)],
469
+ ['else', mkPtrIR(PTR.TYPED, aux,
470
+ ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]])]]]]]]], 'f64')
401
471
  }
402
472
  // Normal: allocate fresh typed array (lenExpr is numeric size). Header stores byteLen.
403
473
  const shift = SHIFT[elemType]
@@ -496,7 +566,7 @@ export default (ctx) => {
496
566
  }
497
567
  }
498
568
  }
499
- return buf(obj)
569
+ return bufDyn(obj)
500
570
  })
501
571
 
502
572
  // .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
@@ -521,7 +591,7 @@ export default (ctx) => {
521
591
  }
522
592
  }
523
593
  }
524
- return blen(obj)
594
+ return blenDyn(obj)
525
595
  })
526
596
 
527
597
  // .byteOffset — owned: 0. View: descriptor[4] - descriptor[8].
@@ -539,7 +609,7 @@ export default (ctx) => {
539
609
  }
540
610
  if (ctor?.startsWith('new.') && TYPED_ELEM_CODE[ctor.slice(4)] != null) return typed(['f64.const', 0], 'f64')
541
611
  }
542
- return boff(obj)
612
+ return boffDyn(obj)
543
613
  })
544
614
 
545
615
  // Runtime fallback for .byteOffset when variable view-ness is unknown.
@@ -580,20 +650,22 @@ export default (ctx) => {
580
650
 
581
651
  // buf.slice(begin?, end?) on a BUFFER → fresh BUFFER with the byte range copied.
582
652
  // Only dispatches statically when obj is a tracked ArrayBuffer/DataView variable.
653
+ // Indices normalize through __clamp_idx (negative wraps from the end, then
654
+ // clamp to [0, byteLength]) — the same bounds dance as every other range op.
583
655
  ctx.core.emit['.buf:slice'] = (obj, beginExpr, endExpr) => {
656
+ inc('__clamp_idx')
584
657
  const src = temp('bss')
585
658
  const beg = tempI32('bsb')
586
659
  const end = tempI32('bse')
587
660
  const bytes = tempI32('bsn')
588
661
  const out = allocPtr({ type: PTR.BUFFER, len: ['local.get', `$${bytes}`], stride: 1, tag: 'bsd' })
662
+ const lenWat = ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]
589
663
  const beginWat = beginExpr == null ? ['i32.const', 0] : asI32(emit(beginExpr))
590
- const endWat = endExpr == null
591
- ? ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]
592
- : asI32(emit(endExpr))
664
+ const endWat = endExpr == null ? lenWat : asI32(emit(endExpr))
593
665
  return typed(['block', ['result', 'f64'],
594
666
  ['local.set', `$${src}`, asF64(emit(obj))],
595
- ['local.set', `$${beg}`, beginWat],
596
- ['local.set', `$${end}`, endWat],
667
+ ['local.set', `$${beg}`, ['call', '$__clamp_idx', beginWat, lenWat]],
668
+ ['local.set', `$${end}`, ['call', '$__clamp_idx', endWat, lenWat]],
597
669
  ['local.set', `$${bytes}`, ['i32.sub', ['local.get', `$${end}`], ['local.get', `$${beg}`]]],
598
670
  ['if',
599
671
  ['i32.lt_s', ['local.get', `$${bytes}`], ['i32.const', 0]],
@@ -909,6 +981,29 @@ export default (ctx) => {
909
981
  const stride = STRIDE[elemType], store = STORE[elemType]
910
982
  ctx.core.emit[`${name}.from`] = (src) => {
911
983
  ctx.features.typedarray = true
984
+ // Bare array-literal source (`Int32Array.from([…])`, `new Int32Array([…])`): build the
985
+ // typed array directly — alloc + one native-typed store per element — instead of
986
+ // materializing an intermediate f64 ARRAY (every element a 9-byte f64.const) plus a
987
+ // per-element f64→elem copy loop. For an integer view each element then stores as an
988
+ // i32.const (2 B for small values), so a constant program / lookup table stops
989
+ // round-tripping through the boxed-f64 array. Fresh alloc per evaluation, so identity
990
+ // and mutation semantics are unchanged. BigInt views (elemType>7) keep the loop.
991
+ if (Array.isArray(src) && src[0] === '[' && elemType <= 7
992
+ && !src.slice(1).some(e => Array.isArray(e) && e[0] === '...')) {
993
+ const elems = src.slice(1)
994
+ const out = allocPtr({ type: PTR.TYPED, aux, len: ['i32.const', elems.length * stride], stride: 1, tag: 'tf' })
995
+ const body = [out.init]
996
+ for (let k = 0; k < elems.length; k++) {
997
+ const addr = k === 0 ? ['local.get', `$${out.local}`]
998
+ : ['i32.add', ['local.get', `$${out.local}`], ['i32.const', k * stride]]
999
+ const v = elemType <= 5 ? asI32(emit(elems[k]))
1000
+ : elemType === 6 ? ['f32.demote_f64', asF64(emit(elems[k]))]
1001
+ : asF64(emit(elems[k]))
1002
+ body.push([store, addr, v])
1003
+ }
1004
+ body.push(out.ptr)
1005
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
1006
+ }
912
1007
  const srcL = temp('tfs')
913
1008
  const len = tempI32('tfl'), i = tempI32('tfi'), off = tempI32('tfo')
914
1009
  const out = allocPtr({ type: PTR.TYPED, aux,
@@ -993,7 +1088,7 @@ export default (ctx) => {
993
1088
  // Runtime-dispatch typed index: checks ptr_type + aux to load with correct stride.
994
1089
  // For TYPED views (aux bit 3), $off indirects through descriptor[4] to real data.
995
1090
  ctx.core.stdlib['__typed_set_idx'] = `(func $__typed_set_idx (param $ptr i64) (param $i i32) (param $v f64) (result f64)
996
- (local $off i32) (local $aux i32) (local $et i32) (local $bits i32)
1091
+ (local $off i32) (local $aux i32) (local $et i32) (local $bits i32) (local $vb i64)
997
1092
  (local.set $aux (call $__ptr_aux (local.get $ptr)))
998
1093
  (local.set $off (call $__ptr_offset (local.get $ptr)))
999
1094
  (if (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0))
@@ -1002,6 +1097,19 @@ export default (ctx) => {
1002
1097
  (if (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
1003
1098
  (then (i64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (i64.reinterpret_f64 (local.get $v))))
1004
1099
  (else
1100
+ ;; ToNumber for NaN-boxed values (spec: typed element writes coerce).
1101
+ ;; true/false/null atoms store 1/0/0; other boxes (undefined, string,
1102
+ ;; object) store canonical NaN. Skipped on the BigInt arm above — raw
1103
+ ;; bigint i64 bits legitimately look like NaN through the f64 param.
1104
+ (if (f64.ne (local.get $v) (local.get $v))
1105
+ (then
1106
+ (local.set $vb (i64.reinterpret_f64 (local.get $v)))
1107
+ (local.set $v (f64.const nan))
1108
+ (if (i64.eq (local.get $vb) (i64.const ${TRUE_NAN}))
1109
+ (then (local.set $v (f64.const 1))))
1110
+ (if (i32.or (i64.eq (local.get $vb) (i64.const ${FALSE_NAN}))
1111
+ (i64.eq (local.get $vb) (i64.const ${NULL_NAN})))
1112
+ (then (local.set $v (f64.const 0))))))
1005
1113
  (if (i32.eq (local.get $et) (i32.const 7))
1006
1114
  (then (f64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (local.get $v)))
1007
1115
  (else
@@ -1223,55 +1331,107 @@ export default (ctx) => {
1223
1331
  }
1224
1332
  ctx.core.emit['.typed:sort'] = (arr, fn) => emitTypedSort(emit(arr), fn)
1225
1333
 
1226
- // Type-aware TypedArray read: arr[i]
1334
+ // Type-aware TypedArray read: arr[i]. The DIRECT unchecked load is gated on the
1335
+ // structural in-bounds proof (inBoundsArrIdx — the same canonical `for (i=C≥0;
1336
+ // i<arr.length; i++)` scan the generic ARRAY read uses), which keeps every proven
1337
+ // hot loop byte-identical (the vectorizer's shapes). An UNPROVEN index takes the
1338
+ // checked form: JS reads `undefined` past the end — the unchecked load read
1339
+ // ADJACENT HEAP or trapped past memory (the Root F silent-corruption class).
1340
+ // `i32.lt_u` folds the negative case in (a negative i32 is a huge u32). The
1341
+ // checked result is number|undefined, so it carries NO valKind tag.
1227
1342
  ctx.core.emit['.typed:[]'] = (arr, i) => {
1228
1343
  const r = resolveElem(arr)
1229
1344
  if (r == null) return null // unknown type, fallback to generic
1230
1345
  const { et, isView, isBigInt } = r
1346
+ const proven = typedIdxProven(arr, i)
1347
+ const loadOf = (off) => isBigInt ? ['f64.reinterpret_i64', ['i64.load', off]]
1348
+ : et === 7 ? ['f64.load', off]
1349
+ : et === 6 ? ['f64.promote_f32', ['f32.load', off]]
1350
+ : [(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]]
1351
+ if (!proven) {
1352
+ // BRANCHLESS checked read: `select(load(in ? idx : 0), undefined, in)`. The
1353
+ // address clamp makes the load unconditionally safe (index 0 of the data
1354
+ // region is mapped arena even for a 0-length array — the loaded value is
1355
+ // select-discarded), and the branch-free shape keeps checked reads inside
1356
+ // straight-line kernel bodies the SIMD recognizers can still lift.
1357
+ // len inlines to one header load for a RESOLVED elem type (no call — the
1358
+ // SIMD recognizers require call-free kernel bodies): owned byteLen at
1359
+ // base-8, view byteLen at descriptor[0]; elemCount = byteLen >> shift.
1360
+ const ti = tempI32('tbi'), tin = tempI32('tbn')
1361
+ const lenIR = ['i32.shr_u',
1362
+ ['i32.load', isView ? typedBase(emit(arr)) : ['i32.sub', typedBase(emit(arr)), ['i32.const', 8]]],
1363
+ ['i32.const', SHIFT[et]]]
1364
+ const off = ['i32.add', typedDataAddr(emit(arr), isView),
1365
+ ['i32.shl', ['select', ['local.get', `$${ti}`], ['i32.const', 0], ['local.get', `$${tin}`]],
1366
+ ['i32.const', SHIFT[et]]]]
1367
+ return typed(['block', ['result', 'f64'],
1368
+ ['local.set', `$${ti}`, idx(i)],
1369
+ ['local.set', `$${tin}`, ['i32.lt_u', ['local.get', `$${ti}`], lenIR]],
1370
+ ['select', loadOf(off), undefExpr(), ['local.get', `$${tin}`]]], 'f64')
1371
+ }
1231
1372
  const objIR = emit(arr), vi = idx(i)
1232
1373
  const off = ['i32.add', typedDataAddr(objIR, isView), ['i32.shl', vi, ['i32.const', SHIFT[et]]]]
1233
- if (isBigInt) return typed(['f64.reinterpret_i64', ['i64.load', off]], 'f64')
1234
- if (et === 7) return typed(['f64.load', off], 'f64') // Float64Array
1235
- if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64') // Float32Array
1236
- // Integer types: load and convert to f64 (unsigned types use unsigned conversion)
1237
- return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
1374
+ if (isBigInt) return typed(loadOf(off), 'f64')
1375
+ // Non-bigint typed elements are plain NUMBERS tag the load so numeric-arm
1376
+ // predicates (isNumArm: `+` dispatch numSide, ?:/?? canon) skip box guards.
1377
+ const t = typed(loadOf(off), 'f64'); t.valKind = VAL.NUMBER; return t
1238
1378
  }
1239
1379
 
1240
- // Type-aware TypedArray write: arr[i] = val
1380
+ // Type-aware TypedArray write: arr[i] = val. Same proof gate as the read: proven
1381
+ // indexes keep the direct unchecked store byte-identical; unproven ones evaluate
1382
+ // the RHS (its effects and the assignment's value are unconditional per spec),
1383
+ // then store only when `i u< len` — JS silently IGNORES out-of-bounds typed
1384
+ // writes, where the unchecked store corrupted adjacent heap (Root F).
1241
1385
  ctx.core.emit['.typed:[]='] = (arr, i, val, void_ = false) => {
1242
1386
  const r = resolveElem(arr)
1243
1387
  if (r == null) return null
1244
1388
  const { et, isView, isBigInt } = r
1245
- const objIR = emit(arr), vi = idx(i), valIR = emit(val)
1389
+ const proven = typedIdxProven(arr, i)
1390
+ const pre = []
1391
+ let vi
1392
+ if (proven) vi = idx(i)
1393
+ else {
1394
+ inc('__len')
1395
+ const ti = tempI32('tbi')
1396
+ pre.push(['local.set', `$${ti}`, idx(i)])
1397
+ vi = ['local.get', `$${ti}`]
1398
+ }
1399
+ // Wrap a store statement in the bounds guard on the unproven path. The value
1400
+ // temp is set OUTSIDE the guard (spec: RHS evaluates regardless).
1401
+ const guard = (store) => proven ? store
1402
+ : ['if', ['i32.lt_u', vi, ['i32.shr_u',
1403
+ ['i32.load', isView ? typedBase(emit(arr)) : ['i32.sub', typedBase(emit(arr)), ['i32.const', 8]]],
1404
+ ['i32.const', SHIFT[et]]]], ['then', store]]
1405
+ const objIR = emit(arr), valIR = emit(val)
1246
1406
  const off = ['i32.add', typedDataAddr(objIR, isView), ['i32.shl', vi, ['i32.const', SHIFT[et]]]]
1247
1407
  if (isBigInt) {
1248
1408
  const vt = temp('tw')
1249
- return typed(void_ ? ['block',
1409
+ return typed(void_ ? ['block', ...pre,
1250
1410
  ['local.set', `$${vt}`, asF64(valIR)],
1251
- ['i64.store', off, ['i64.reinterpret_f64', ['local.get', `$${vt}`]]]]
1252
- : ['block', ['result', 'f64'],
1411
+ guard(['i64.store', off, ['i64.reinterpret_f64', ['local.get', `$${vt}`]]])]
1412
+ : ['block', ['result', 'f64'], ...pre,
1253
1413
  ['local.set', `$${vt}`, asF64(valIR)],
1254
- ['i64.store', off, ['i64.reinterpret_f64', ['local.get', `$${vt}`]]],
1414
+ guard(['i64.store', off, ['i64.reinterpret_f64', ['local.get', `$${vt}`]]]),
1255
1415
  ['local.get', `$${vt}`]], void_ ? 'void' : 'f64')
1256
1416
  }
1257
1417
  if (et === 7) {
1258
1418
  const vt = temp('tw')
1259
- return typed(void_ ? ['block',
1419
+ return typed(void_ ? ['block', ...pre,
1260
1420
  ['local.set', `$${vt}`, asF64(valIR)],
1261
- ['f64.store', off, ['local.get', `$${vt}`]]]
1262
- : ['block', ['result', 'f64'],
1421
+ guard(['f64.store', off, ['local.get', `$${vt}`]])]
1422
+ : ['block', ['result', 'f64'], ...pre,
1263
1423
  ['local.set', `$${vt}`, asF64(valIR)],
1264
- ['f64.store', off, ['local.get', `$${vt}`]],
1424
+ guard(['f64.store', off, ['local.get', `$${vt}`]]),
1265
1425
  ['local.get', `$${vt}`]], void_ ? 'void' : 'f64') // Float64Array
1266
1426
  }
1267
1427
  if (et === 6) {
1268
1428
  const vt = temp('tw')
1269
- return typed(void_ ? ['block',
1429
+ return typed(void_ ? ['block', ...pre,
1270
1430
  ['local.set', `$${vt}`, asF64(valIR)],
1271
- ['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]]]
1272
- : ['block', ['result', 'f64'],
1431
+ guard(['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]])]
1432
+ : ['block', ['result', 'f64'], ...pre,
1273
1433
  ['local.set', `$${vt}`, asF64(valIR)],
1274
- ['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]],
1434
+ guard(['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]]),
1275
1435
  ['local.get', `$${vt}`]], void_ ? 'void' : 'f64') // Float32Array
1276
1436
  }
1277
1437
  // Integer store: when the source is already i32-typed (bitwise ops, |0, known-i32 var) —
@@ -1285,18 +1445,19 @@ export default (ctx) => {
1285
1445
  const i32Backed = valIR.type === 'i32' ||
1286
1446
  (Array.isArray(valIR) && (valIR[0] === 'f64.convert_i32_s' || valIR[0] === 'f64.convert_i32_u'))
1287
1447
  if (i32Backed) {
1288
- const vi = asI32(valIR)
1289
- const cheap = Array.isArray(vi) &&
1290
- ((vi[0] === 'local.get' && typeof vi[1] === 'string') ||
1291
- (vi[0] === 'i32.const' && (typeof vi[1] === 'number' || typeof vi[1] === 'string')))
1292
- if (void_ && cheap) return typed([STORE[et], off, vi], 'void')
1448
+ const vi32 = asI32(valIR)
1449
+ const cheap = Array.isArray(vi32) &&
1450
+ ((vi32[0] === 'local.get' && typeof vi32[1] === 'string') ||
1451
+ (vi32[0] === 'i32.const' && (typeof vi32[1] === 'number' || typeof vi32[1] === 'string')))
1452
+ if (void_ && cheap && proven) return typed([STORE[et], off, vi32], 'void')
1453
+ if (void_ && cheap) return typed(['block', ...pre, guard([STORE[et], off, vi32])], 'void')
1293
1454
  const v32 = tempI32('tw')
1294
- return typed(void_ ? ['block',
1295
- ['local.set', `$${v32}`, vi],
1296
- [STORE[et], off, ['local.get', `$${v32}`]]]
1297
- : ['block', ['result', 'f64'],
1298
- ['local.set', `$${v32}`, vi],
1299
- [STORE[et], off, ['local.get', `$${v32}`]],
1455
+ return typed(void_ ? ['block', ...pre,
1456
+ ['local.set', `$${v32}`, vi32],
1457
+ guard([STORE[et], off, ['local.get', `$${v32}`]])]
1458
+ : ['block', ['result', 'f64'], ...pre,
1459
+ ['local.set', `$${v32}`, vi32],
1460
+ guard([STORE[et], off, ['local.get', `$${v32}`]]),
1300
1461
  [(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', ['local.get', `$${v32}`]]], void_ ? 'void' : 'f64')
1301
1462
  }
1302
1463
  const vt = temp('tw')
@@ -1304,12 +1465,12 @@ export default (ctx) => {
1304
1465
  ['if', ['result', 'i64'], ['f64.lt', ['local.get', `$${vt}`], ['f64.const', 0]],
1305
1466
  ['then', ['i64.trunc_sat_f64_s', ['local.get', `$${vt}`]]],
1306
1467
  ['else', ['i64.trunc_sat_f64_u', ['local.get', `$${vt}`]]]]]
1307
- return typed(void_ ? ['block',
1468
+ return typed(void_ ? ['block', ...pre,
1308
1469
  ['local.set', `$${vt}`, asF64(valIR)],
1309
- [STORE[et], off, i32val]]
1310
- : ['block', ['result', 'f64'],
1470
+ guard([STORE[et], off, i32val])]
1471
+ : ['block', ['result', 'f64'], ...pre,
1311
1472
  ['local.set', `$${vt}`, asF64(valIR)],
1312
- [STORE[et], off, i32val],
1473
+ guard([STORE[et], off, i32val]),
1313
1474
  ['local.get', `$${vt}`]], void_ ? 'void' : 'f64')
1314
1475
  }
1315
1476
 
@@ -1436,9 +1597,7 @@ export default (ctx) => {
1436
1597
  // typedLoop(arr, bodyFn, opts?) emits the common shape every typed iteration
1437
1598
  // method needs: resolve elemType from the receiver's tracked ctor, set up
1438
1599
  // (ptr, len, i) locals, walk i in [0, len), per iteration load arr[i] as f64
1439
- // and pass it to bodyFn. Returns the IR setup statements. Returns null if the
1440
- // element type can't be resolved — callers fall back to the generic array
1441
- // emitter.
1600
+ // and pass it to bodyFn. Returns the IR setup statements.
1442
1601
  //
1443
1602
  // bodyFn(loadElem, i, len, ptr, exitLabel) returns IR statements. loadElem is
1444
1603
  // a function (called lazily so it isn't materialized for unused-item paths)
@@ -1447,30 +1606,63 @@ export default (ctx) => {
1447
1606
  // resolution). `i`/`len`/`ptr` are i32 local-name strings.
1448
1607
  const typedLoop = (arr, bodyFn) => {
1449
1608
  const r = resolveElem(arr)
1450
- if (!r) return null
1451
- const { et, isView, isBigInt } = r
1452
- if (isBigInt) return null // BigInt: defer to generic .map (returns BigInts via f64-bits NaN-box)
1453
- const va = emit(arr)
1454
- const len = tempI32('tll'), ptr = tempI32('tlp'), i = tempI32('tli')
1455
1609
  const id = ctx.func.uniq++
1456
1610
  const exit = `$brk${id}`
1457
- inc('__len')
1458
- const loadElem = () => {
1459
- const off = ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', SHIFT[et]]]]
1460
- if (et === 7) return typed(['f64.load', off], 'f64')
1461
- if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
1462
- return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
1611
+ const len = tempI32('tll'), i = tempI32('tli')
1612
+ // Static fast path: concrete element kind (and non-BigInt-ness) proven at
1613
+ // compile time direct-typed load, no per-element dispatch.
1614
+ if (r && !r.isBigInt) {
1615
+ const { et, isView } = r
1616
+ const va = emit(arr)
1617
+ const ptr = tempI32('tlp')
1618
+ inc('__len')
1619
+ const loadElem = () => {
1620
+ const off = ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', SHIFT[et]]]]
1621
+ if (et === 7) return typed(['f64.load', off], 'f64')
1622
+ if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
1623
+ return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
1624
+ }
1625
+ const setup = [
1626
+ ['local.set', `$${ptr}`, typedDataAddr(asF64(va), isView)],
1627
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', asF64(va)]]],
1628
+ ['local.set', `$${i}`, ['i32.const', 0]],
1629
+ ['block', exit, ['loop', `$loop${id}`,
1630
+ ['br_if', exit, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
1631
+ ...bodyFn(loadElem, i, len, ptr, exit),
1632
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1633
+ ['br', `$loop${id}`]]]]
1634
+ return { setup, ptr, len, i, exit, et, isView, loadElem }
1463
1635
  }
1636
+ // Dynamic fallback: concrete element kind (or BigInt-ness) isn't provable
1637
+ // statically — `resolveElem` only tracks a receiver traced back to a bare
1638
+ // `new XArray(...)` binding, so a TYPED value that instead flowed through
1639
+ // an object field, a return value, or any other opaque path (still proven
1640
+ // *some* typed array — that's how it dispatched here at all — just not
1641
+ // WHICH one) has no ctor to key off. Read every element through
1642
+ // __typed_get_idx: the SAME runtime aux-tag dispatch .reverse/.sort/.fill/
1643
+ // .copyWithin already use UNCONDITIONALLY (module/core.js), correct for
1644
+ // any concrete kind including BigInt, one indirect call slower per
1645
+ // element than the static path above. This used to `return null`, which
1646
+ // every caller propagated as "unsupported" up to emitMethodCall — for a
1647
+ // receiver already proven VAL.TYPED that either crashed downstream (null
1648
+ // IR reaching a consumer) or, if a caller "fell back to the generic array
1649
+ // emitter" instead, would misread the typed array's packed native bytes
1650
+ // as 8-byte f64 slots (module/array.js's arrayLoop is ARRAY-only) —
1651
+ // silent corruption, not a fallback. This is the sound one.
1652
+ inc('__typed_get_idx', '__len')
1653
+ const av = temp('tla')
1654
+ const loadElem = () => typed(['call', '$__typed_get_idx',
1655
+ ['i64.reinterpret_f64', ['local.get', `$${av}`]], ['local.get', `$${i}`]], 'f64')
1464
1656
  const setup = [
1465
- ['local.set', `$${ptr}`, typedDataAddr(asF64(va), isView)],
1466
- ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', asF64(va)]]],
1657
+ ['local.set', `$${av}`, asF64(emit(arr))],
1658
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${av}`]]]],
1467
1659
  ['local.set', `$${i}`, ['i32.const', 0]],
1468
1660
  ['block', exit, ['loop', `$loop${id}`,
1469
1661
  ['br_if', exit, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
1470
- ...bodyFn(loadElem, i, len, ptr, exit),
1662
+ ...bodyFn(loadElem, i, len, null, exit),
1471
1663
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1472
1664
  ['br', `$loop${id}`]]]]
1473
- return { setup, ptr, len, i, exit, et, isView, loadElem }
1665
+ return { setup, ptr: null, len, i, exit, et: null, isView: false, loadElem }
1474
1666
  }
1475
1667
 
1476
1668
  // === Typed iteration emitters ===
package/module/web.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * web — callable host web globals (`fetch(...)`), bound automatically.
3
+ *
4
+ * Under host:'js' a bare `fetch(url, opts?)` lowers to an `env.fetch` import
5
+ * that interop wires from `globalThis.fetch` with full marshalling — the
6
+ * returned thenable adopts into a jz promise, so `await fetch(url)` works and
7
+ * the Response crosses as an external handle (`.text()`/`.json()` dispatch
8
+ * host-side and are awaitable in turn). No import statement needed.
9
+ *
10
+ * Under host:'wasi' there is no JS host to bind — a warning is emitted and
11
+ * the env import is still declared, so an embedder MAY wire `env.fetch`
12
+ * itself; an unwired module fails at instantiation, not silently.
13
+ *
14
+ * @module web
15
+ */
16
+
17
+ import { typed, asI64, UNDEF_NAN } from '../src/ir.js'
18
+ import { emit, hostImport } from '../src/bridge.js'
19
+ import { warn } from '../src/ctx.js'
20
+
21
+ const ARITY = { fetch: 2 }
22
+
23
+ export default (ctx) => {
24
+ for (const [name, arity] of Object.entries(ARITY)) {
25
+ ctx.core.emit[name] = (...args) => {
26
+ if (ctx.transform.host === 'wasi')
27
+ warn('host-global', `\`${name}\` binds from the JS host — under host:'wasi' wire env.${name} yourself or instantiation will fail`, {})
28
+ hostImport('env', name, ['func', `$__env_${name}`,
29
+ ...Array.from({ length: arity }, () => ['param', 'i64']), ['result', 'i64']])
30
+ const ir = [`call`, `$__env_${name}`]
31
+ for (let i = 0; i < arity; i++)
32
+ ir.push(args[i] != null ? asI64(emit(args[i])) : ['i64.const', UNDEF_NAN])
33
+ return typed(['f64.reinterpret_i64', ir], 'f64')
34
+ }
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Ahead-of-time compiler for the numeric core of JavaScript (DSP, audio, math, parsers) to lean GC-free WASM — valid jz is valid JS, no type annotations, no runtime.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "test:bench": "node test/bench.js",
47
47
  "test:fuzz": "node test/fuzz.js --count=5000",
48
48
  "test:ratchet": "node test/perf-ratchet.js",
49
- "test:self": "node test/selfhost.js",
49
+ "test:self": "node test/selfhost.js && node test/selfhost-perf.js",
50
50
  "test:262": "node test/test262.js",
51
51
  "test:262:builtins": "node test/test262-builtins.js",
52
52
  "test:all": "npm run test:matrix && npm run test:262 && npm run test:262:builtins && npm run test:bench && npm run test:self",
@@ -60,7 +60,7 @@
60
60
  "audit:as": "node scripts/audit-assemblyscript.mjs",
61
61
  "build": "node scripts/build-dist.mjs",
62
62
  "build:examples": "node examples/build.mjs all",
63
- "prepublishOnly": "npm test && npm run build && npm run test:self",
63
+ "prepublishOnly": "npm test && npm run test:self",
64
64
  "prepare": "npm run build",
65
65
  "audit:fixpoint": "node scripts/audit-fixpoint.mjs"
66
66
  },
@@ -75,8 +75,8 @@
75
75
  "author": "Dmitry Iv",
76
76
  "license": "MIT",
77
77
  "dependencies": {
78
- "subscript": "^10.4.17",
79
- "watr": "^5.0.0"
78
+ "subscript": "^10.7.0",
79
+ "watr": "^5.4.2"
80
80
  },
81
81
  "keywords": [
82
82
  "javascript",