jz 0.6.0 → 0.8.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 (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -12,7 +12,7 @@ import { emit, idx, deps, call } from '../src/bridge.js'
12
12
  import { valTypeOf } from '../src/kind.js'
13
13
  import { VAL, lookupValType } from '../src/reps.js'
14
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'
15
+ import { inc, PTR, LAYOUT, registerGetter } from '../src/ctx.js'
16
16
 
17
17
  const _NAN_BITS = nanPrefixHex()
18
18
 
@@ -28,6 +28,12 @@ const STORE = [
28
28
  'i32.store8', 'i32.store8', 'i32.store16', 'i32.store16',
29
29
  'i32.store', 'i32.store', 'f32.store', 'f64.store',
30
30
  ]
31
+ // f64 value → this element's stored representation (paired with STORE). Signed
32
+ // kinds trunc_s, unsigned trunc_u, f32 demotes, f64 stores as-is (null = identity).
33
+ const FROM_F64 = [
34
+ 'i32.trunc_f64_s', 'i32.trunc_f64_u', 'i32.trunc_f64_s', 'i32.trunc_f64_u',
35
+ 'i32.trunc_f64_s', 'i32.trunc_f64_u', 'f32.demote_f64', null,
36
+ ]
31
37
 
32
38
  // SIMD: vector width per element type (elements per v128)
33
39
  const VEC_WIDTH = [16, 16, 8, 8, 4, 4, 4, 2] // 128 bits / element bits
@@ -201,6 +207,18 @@ export default (ctx) => {
201
207
  __byte_offset: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
202
208
  __to_buffer: ['__ptr_type', '__ptr_offset', '__ptr_aux', '__mkptr'],
203
209
  __typed_set_idx: ['__ptr_aux', '__ptr_offset'],
210
+ __typed_get_idx: ['__ptr_aux', '__ptr_offset'],
211
+ // __clamp_idx is body-called by every range op (fill/copyWithin/subarray/slice). It has NO
212
+ // other manual-dep edge in the whole stdlib, so it's reachable ONLY via resolveIncludes'
213
+ // auto-scan — which diverges under self-host (jz.wasm), dropping it ("Unknown func
214
+ // $__clamp_idx" on typed .fill/.subarray in the kernel). Declare it manually here so the
215
+ // reliable dep path includes it. Pinned by test/selfhost-includes.js.
216
+ __typed_fill: ['__len', '__typed_set_idx', '__clamp_idx'],
217
+ __typed_reverse: ['__len', '__typed_get_idx', '__typed_set_idx'],
218
+ __typed_copyWithin: ['__len', '__typed_get_idx', '__typed_set_idx', '__clamp_idx'],
219
+ __typed_sort: ['__len', '__typed_get_idx', '__typed_set_idx'],
220
+ __subarray: ['__ptr_aux', '__ptr_offset', '__typed_shift', '__typed_data', '__len', '__mkptr', '__alloc', '__clamp_idx'],
221
+ __typed_slice_rt: ['__ptr_aux', '__typed_shift', '__typed_data', '__len', '__mkptr', '__alloc_hdr_n', '__clamp_idx'],
204
222
  // __str_join uses __typed_idx when typedarray is loaded (plain arrays promoted to
205
223
  // Int32Array by promoteIntArrayLiterals can produce PTR.TYPED results via .map()).
206
224
  __str_join: [...(ctx.core.stdlibDeps.__str_join ?? []), '__typed_idx'],
@@ -261,6 +279,60 @@ export default (ctx) => {
261
279
  (i32.load (i32.add (local.get $off) (i32.const 8)))))
262
280
  (else (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $off)))))))`
263
281
 
282
+ // __subarray(ptr, begin, end, useEnd) — runtime-dispatched `.subarray(...)` for
283
+ // when the receiver's elem type / view-ness isn't statically known (a binding
284
+ // reassigned owned→view: `let c = new T(d); c = c.subarray(n)`). Reads elem type
285
+ // and the view bit off the aux byte, so it is correct whether `ptr` is an owned
286
+ // typed array or an existing view. Mirrors the static `.typed:subarray` emitter:
287
+ // builds the 16-byte descriptor [byteLen][dataOff][rootOff] and tags TYPED|view.
288
+ // Cold path (slicing, not per-element) — correctness over speed.
289
+ ctx.core.stdlib['__subarray'] = `(func $__subarray (param $ptr i64) (param $begin i32) (param $end i32) (param $useEnd i32) (result f64)
290
+ (local $aux i32) (local $shift i32) (local $off i32) (local $data i32) (local $root i32)
291
+ (local $len i32) (local $lo i32) (local $hi i32) (local $n i32) (local $desc i32)
292
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
293
+ (local.set $shift (call $__typed_shift (i32.and (local.get $aux) (i32.const 7))))
294
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
295
+ (local.set $data (call $__typed_data (local.get $ptr)))
296
+ (local.set $root
297
+ (if (result i32) (i32.and (local.get $aux) (i32.const 8))
298
+ (then (i32.load (i32.add (local.get $off) (i32.const 8))))
299
+ (else (local.get $off))))
300
+ (local.set $len (call $__len (local.get $ptr)))
301
+ (local.set $lo (call $__clamp_idx (local.get $begin) (local.get $len)))
302
+ (if (local.get $useEnd)
303
+ (then
304
+ (local.set $hi (call $__clamp_idx (local.get $end) (local.get $len))))
305
+ (else (local.set $hi (local.get $len))))
306
+ (local.set $n (select (i32.sub (local.get $hi) (local.get $lo)) (i32.const 0) (i32.gt_s (local.get $hi) (local.get $lo))))
307
+ (local.set $desc (call $__alloc (i32.const 16)))
308
+ (i32.store (local.get $desc) (i32.shl (local.get $n) (local.get $shift)))
309
+ (i32.store (i32.add (local.get $desc) (i32.const 4)) (i32.add (local.get $data) (i32.shl (local.get $lo) (local.get $shift))))
310
+ (i32.store (i32.add (local.get $desc) (i32.const 8)) (local.get $root))
311
+ (call $__mkptr (i32.const ${PTR.TYPED}) (i32.or (local.get $aux) (i32.const 8)) (local.get $desc)))`
312
+
313
+ // __typed_slice_rt(ptr, begin, end, useEnd) — runtime-dispatched `.slice(...)` for
314
+ // a receiver whose elem type / view-ness isn't statically known. Unlike subarray
315
+ // this returns a fresh OWNED copy (bit-exact memory.copy, so bigint-safe too). Reads
316
+ // elem type + data address off the aux byte. Cold path — correctness over speed.
317
+ ctx.core.stdlib['__typed_slice_rt'] = `(func $__typed_slice_rt (param $ptr i64) (param $begin i32) (param $end i32) (param $useEnd i32) (result f64)
318
+ (local $aux i32) (local $et i32) (local $shift i32) (local $src i32)
319
+ (local $len i32) (local $lo i32) (local $hi i32) (local $n i32) (local $byteLen i32) (local $dst i32)
320
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
321
+ (local.set $et (i32.and (local.get $aux) (i32.const 7)))
322
+ (local.set $shift (call $__typed_shift (local.get $et)))
323
+ (local.set $src (call $__typed_data (local.get $ptr)))
324
+ (local.set $len (call $__len (local.get $ptr)))
325
+ (local.set $lo (call $__clamp_idx (local.get $begin) (local.get $len)))
326
+ (if (local.get $useEnd)
327
+ (then
328
+ (local.set $hi (call $__clamp_idx (local.get $end) (local.get $len))))
329
+ (else (local.set $hi (local.get $len))))
330
+ (local.set $n (select (i32.sub (local.get $hi) (local.get $lo)) (i32.const 0) (i32.gt_s (local.get $hi) (local.get $lo))))
331
+ (local.set $byteLen (i32.shl (local.get $n) (local.get $shift)))
332
+ (local.set $dst (call $__alloc_hdr_n (local.get $byteLen) (local.get $byteLen) (i32.const 1)))
333
+ (memory.copy (local.get $dst) (i32.add (local.get $src) (i32.shl (local.get $lo) (local.get $shift))) (local.get $byteLen))
334
+ (call $__mkptr (i32.const ${PTR.TYPED}) (i32.and (local.get $aux) (i32.const 23)) (local.get $dst)))`
335
+
264
336
  // Constructor: new Float64Array(len) | new F64Array(arr) | new F64Array(buf) | new F64Array(buf, off, len)
265
337
  for (const [name, elemType] of Object.entries(TYPED_ELEM_CODE)) {
266
338
  const aux = typedAux(name)
@@ -273,6 +345,7 @@ export default (ctx) => {
273
345
  // and tags the TYPED ptr with aux=elemType|8. Reads/writes alias the parent,
274
346
  // .buffer reconstructs the root BUFFER, .byteOffset = dataOff - parentOff.
275
347
  if (offsetExpr != null && lenExpr2 != null) {
348
+ ctx.features.typedView = true // subview aliases the parent buffer — SLP must not assume disjoint bases
276
349
  const src = temp('tvs')
277
350
  const parentOff = tempI32('tvp')
278
351
  const byteLen = tempI32('tvb')
@@ -298,9 +371,12 @@ export default (ctx) => {
298
371
  // TYPED retagged at the same offset — the byteLen header is shared with the parent.
299
372
  // __len(view) = byteLen >> shift computes elemCount for this view's elemType.
300
373
  if (srcType === VAL.BUFFER || srcType === VAL.TYPED) {
374
+ ctx.features.typedView = true // zero-copy reinterpret aliases the source — SLP must not pack across it
301
375
  return mkPtrIR(PTR.TYPED, aux, ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(lenExpr))]])
302
376
  }
303
377
  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
379
+
304
380
  // Runtime dispatch: number → allocate; array → copy elements; buffer/typed → zero-copy view.
305
381
  const src = temp('ts')
306
382
  const len = tempI32('tl')
@@ -405,7 +481,7 @@ export default (ctx) => {
405
481
  // .buffer — always aliased (zero-copy). BUFFER: passthrough.
406
482
  // Owned TYPED: retag as BUFFER at same offset — the byteLen header is shared.
407
483
  // TYPED view (incl. DataView): BUFFER at descriptor[8] (root parent data offset).
408
- ctx.core.emit['.buffer'] = getter((obj) => {
484
+ registerGetter('.buffer', (obj) => {
409
485
  if (typeof obj === 'string') {
410
486
  const ctor = ctx.types.typedElem?.get(obj)
411
487
  if (ctor === 'new.ArrayBuffer') return asF64(emit(obj))
@@ -425,7 +501,7 @@ export default (ctx) => {
425
501
 
426
502
  // .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
427
503
  // View TYPED (incl. DataView): descriptor[0], via the __byte_length fallback.
428
- ctx.core.emit['.byteLength'] = getter((obj) => {
504
+ registerGetter('.byteLength', (obj) => {
429
505
  if (typeof obj === 'string') {
430
506
  const ctor = ctx.types.typedElem?.get(obj)
431
507
  if (ctor === 'new.ArrayBuffer') {
@@ -449,7 +525,7 @@ export default (ctx) => {
449
525
  })
450
526
 
451
527
  // .byteOffset — owned: 0. View: descriptor[4] - descriptor[8].
452
- ctx.core.emit['.byteOffset'] = getter((obj) => {
528
+ registerGetter('.byteOffset', (obj) => {
453
529
  if (typeof obj === 'string') {
454
530
  const ctor = ctx.types.typedElem?.get(obj)
455
531
  if (ctor?.endsWith('.view')) {
@@ -538,6 +614,10 @@ export default (ctx) => {
538
614
  // We treat `undefined`/`null`/`0`/`''` as falsy (BE) per ToBoolean.
539
615
  const staticLE = (node) => {
540
616
  if (node === undefined) return false // arg omitted → BE
617
+ // Prepare lowers a literal `true`/`false` endianness flag to `['bool', 0|1]`;
618
+ // fold it so `dv.getInt16(p, true)` is one native load (no per-access LE branch
619
+ // + dead byte-swap arm — the dominant overhead in DataView codec loops).
620
+ if (Array.isArray(node) && node[0] === 'bool') return !!node[1]
541
621
  if (Array.isArray(node) && node[0] == null) {
542
622
  // [] → undefined, [null, x] → literal x
543
623
  if (node.length === 1) return false
@@ -598,7 +678,18 @@ export default (ctx) => {
598
678
  (f64.reinterpret_i64 (i64.const ${_NAN_BITS}))
599
679
  (local.get $v)
600
680
  (f64.ne (local.get $v) (local.get $v))))`
601
- const canonNaN = (vIR) => { inc('__canon_nan'); return typed(['call', '$__canon_nan', vIR], 'f64') }
681
+ // Inline the NaN-canonicalization select rather than a call a DataView float
682
+ // read runs this per sample in codec loops, and the inlined `(select nan v (v≠v))`
683
+ // is the exact idiom the auto-vectorizer recognizes (vs an opaque call).
684
+ const canonNaN = (vIR) => {
685
+ const t = temp('cn')
686
+ return typed(['block', ['result', 'f64'],
687
+ ['local.set', `$${t}`, vIR],
688
+ ['select',
689
+ ['f64.reinterpret_i64', ['i64.const', _NAN_BITS]],
690
+ ['local.get', `$${t}`],
691
+ ['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
692
+ }
602
693
 
603
694
  // ToIndex for DataView byte offsets (spec 25.1.2.x getViewValue/setViewValue):
604
695
  // ToIntegerOrInfinity then reject negatives and >2^53-1 with a RangeError. The
@@ -639,8 +730,25 @@ export default (ctx) => {
639
730
  const dvIndexChecked = (offNode, size, viewSize) => {
640
731
  ctx.runtime.throws = true
641
732
  const idxT = tempI32('dvb')
733
+ const offIR = emit(offNode)
734
+ // Fast path: a proven-i32 byte offset (a loop-advanced `p`, `i*2`, a `|0` value)
735
+ // carries no fraction/NaN/negative-from-ToNumber surprise, so it skips the f64
736
+ // ToIndex round-trip and the `__dv_index` CALL — the dominant per-sample cost in
737
+ // DataView-based codec loops. An i32 range (`<0`) + bounds (`>viewSize-size`)
738
+ // check is all the spec needs here; this lowers `dv.getInt16(p,true)` to one
739
+ // `i32.load16_s`, matching the explicit-typed-view path.
740
+ if (offIR.type === 'i32') {
741
+ return typed(['block', ['result', 'i32'],
742
+ ['local.set', `$${idxT}`, offIR],
743
+ ['if', ['i32.or',
744
+ ['i32.lt_s', ['local.get', `$${idxT}`], ['i32.const', 0]],
745
+ ['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]]],
746
+ ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
747
+ ['local.get', `$${idxT}`]], 'i32')
748
+ }
749
+ inc('__dv_index')
642
750
  return typed(['block', ['result', 'i32'],
643
- ['local.set', `$${idxT}`, dvIndex(offNode)],
751
+ ['local.set', `$${idxT}`, typed(['call', '$__dv_index', toNumF64(offNode, offIR)], 'i32')],
644
752
  ['if', ['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]],
645
753
  ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
646
754
  ['local.get', `$${idxT}`]], 'i32')
@@ -807,16 +915,11 @@ export default (ctx) => {
807
915
  len: ['i32.mul', ['local.get', `$${len}`], ['i32.const', stride]], stride: 1, tag: 'tf' })
808
916
  const t = out.local
809
917
  const id = ctx.func.uniq++
810
- const storeExpr = elemType === 7 ? ['f64.store',
811
- ['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
812
- ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]
813
- : elemType === 6 ? ['f32.store',
814
- ['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
815
- ['f32.demote_f64', ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]]
816
- : [store,
817
- ['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
818
- [(elemType & 1) ? 'i32.trunc_f64_u' : 'i32.trunc_f64_s',
819
- ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]]
918
+ const conv = FROM_F64[elemType]
919
+ const srcF64 = ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]
920
+ const storeExpr = [store,
921
+ ['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
922
+ conv ? [conv, srcF64] : srcF64]
820
923
  return typed(['block', ['result', 'f64'],
821
924
  ['local.set', `$${srcL}`, asF64(emit(src))],
822
925
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${srcL}`]]]],
@@ -842,18 +945,28 @@ export default (ctx) => {
842
945
  * at the chain output (the result is always an owned typed array). */
843
946
  const TYPED_CHAIN_METHODS = new Set(['map', 'filter', 'slice'])
844
947
  const resolveElem = (arr) => {
845
- let receiver = arr, chainOutput = false
948
+ let receiver = arr, chainOutput = false, viewOutput = false
846
949
  // Walk method-call chain inward. `arr.method(...)` parses as
847
- // ['()', ['.', recv, 'method'], ...args] — peel until we hit a name.
950
+ // ['()', ['.', recv, 'method'], ...args] — peel until we hit a name. The OUTERMOST
951
+ // op decides view-ness: `.subarray(...)` yields a zero-copy VIEW (reads must indirect
952
+ // through the descriptor), whereas `.map`/`.slice`/… yield a fresh non-view copy.
848
953
  while (Array.isArray(receiver) && receiver[0] === '()' &&
849
954
  Array.isArray(receiver[1]) && receiver[1][0] === '.' &&
850
- TYPED_CHAIN_METHODS.has(receiver[1][2])) {
955
+ (TYPED_CHAIN_METHODS.has(receiver[1][2]) || receiver[1][2] === 'subarray')) {
956
+ if (!chainOutput) viewOutput = receiver[1][2] === 'subarray'
851
957
  receiver = receiver[1][1]
852
958
  chainOutput = true
853
959
  }
854
- const ctor = typeof receiver === 'string' && ctx.types.typedElem?.get(receiver)
960
+ // Nested receiver `arr[i]` where `arr`'s elements are typed arrays of a known
961
+ // ctor (`Array.from(n, () => new Float32Array())` — codec channelData). The i-th
962
+ // element IS that owned typed array; emit(receiver) already loads its pointer, so
963
+ // the standard typedDataAddr path inlines `arr[i][j]` to a direct load/store.
964
+ const ctor = (Array.isArray(receiver) && receiver[0] === '[]' && receiver.length === 3 && typeof receiver[1] === 'string'
965
+ ? ctx.func.localReps?.get(receiver[1])?.arrayElemTypedCtor
966
+ : null)
967
+ || (typeof receiver === 'string' && ctx.types.typedElem?.get(receiver))
855
968
  if (!ctor) return null
856
- const isView = !chainOutput && ctor.endsWith('.view')
969
+ const isView = viewOutput || (!chainOutput && ctor.endsWith('.view'))
857
970
  const name = ctor.endsWith('.view') ? ctor.slice(4, -5) : ctor.slice(4)
858
971
  const et = TYPED_ELEM_CODE[name]
859
972
  return et == null ? null : { et, isView, isBigInt: name === 'BigInt64Array' || name === 'BigUint64Array' }
@@ -863,71 +976,22 @@ export default (ctx) => {
863
976
  * Owned: low 32 bits of the NaN-box (or the unboxed local directly).
864
977
  * View: load descriptor[4]. Uses ptrOffsetIR so unboxed-TYPED locals pass through
865
978
  * without a rebox-then-unbox round trip, and globals fold to inline bit-extract. */
979
+ // A typed array (Float64Array/Int32Array/…) is a FIXED-SIZE allocation — it has no
980
+ // grow op, so it can never relocate, so its base needs no realloc-forwarding follow.
981
+ // An already-unboxed pointer is the offset itself; a boxed f64 pointer extracts its
982
+ // low-32 offset directly — no __ptr_offset call. (The general ptrOffsetIR keeps the
983
+ // forwarding follow because ARRAY/HASH/SET/MAP relocate and an *inferred* OBJECT can
984
+ // alias a relocated ARRAY — but VAL.TYPED is a narrow type that can only be a real
985
+ // typed array, so the follow is provably dead here.)
986
+ const typedBase = (objIR) => objIR.ptrKind != null && objIR.ptrKind !== VAL.ARRAY
987
+ ? objIR
988
+ : ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', asF64(objIR)], ['i64.const', LAYOUT.OFFSET_MASK]]]
866
989
  const typedDataAddr = (objIR, isView) => isView
867
- ? ['i32.load', ['i32.add', ptrOffsetIR(objIR, VAL.TYPED), ['i32.const', 4]]]
868
- : ptrOffsetIR(objIR, VAL.TYPED)
990
+ ? ['i32.load', ['i32.add', typedBase(objIR), ['i32.const', 4]]]
991
+ : typedBase(objIR)
869
992
 
870
993
  // Runtime-dispatch typed index: checks ptr_type + aux to load with correct stride.
871
994
  // For TYPED views (aux bit 3), $off indirects through descriptor[4] to real data.
872
- // Factory — collapses to ARRAY-only f64 indexing when no TYPED pointer can reach here.
873
- // Identical factory in array.js; whichever module loads last wins the registration.
874
- ctx.core.stdlib['__typed_idx'] = () => {
875
- if (!ctx.features.typedarray && !ctx.features.external) {
876
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
877
- (local $len i32)
878
- (local.set $len (call $__len (local.get $ptr)))
879
- (if (result f64)
880
- (i32.or
881
- (i32.lt_s (local.get $i) (i32.const 0))
882
- (i32.ge_u (local.get $i) (local.get $len)))
883
- (then (f64.const nan:${UNDEF_NAN}))
884
- (else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
885
- }
886
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
887
- (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
888
- (local.set $off (call $__ptr_offset (local.get $ptr)))
889
- ;; ARRAY fast path: __ptr_offset already followed any forwarding — read header len + f64.load, no $__len call.
890
- (if (i32.and (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
891
- (then (return (if (result f64)
892
- (i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
893
- (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
894
- (else (f64.const nan:${UNDEF_NAN}))))))
895
- (local.set $aux (call $__ptr_aux (local.get $ptr)))
896
- (if
897
- (i32.and
898
- (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.TYPED}))
899
- (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
900
- (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
901
- (local.set $len (call $__len (local.get $ptr)))
902
- (if (result f64)
903
- (i32.or
904
- (i32.lt_s (local.get $i) (i32.const 0))
905
- (i32.ge_u (local.get $i) (local.get $len)))
906
- (then (f64.const nan:${UNDEF_NAN}))
907
- (else
908
- (if (result f64) (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.TYPED}))
909
- (then
910
- (local.set $et (i32.and (local.get $aux) (i32.const 7)))
911
- (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
912
- (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
913
- (then (if (result f64) (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
914
- (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
915
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
916
- (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
917
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
918
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
919
- (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
920
- (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
921
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
922
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
923
- (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
924
- (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
925
- (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
926
- (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
927
- (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
928
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
929
- }
930
-
931
995
  ctx.core.stdlib['__typed_set_idx'] = `(func $__typed_set_idx (param $ptr i64) (param $i i32) (param $v f64) (result f64)
932
996
  (local $off i32) (local $aux i32) (local $et i32) (local $bits i32)
933
997
  (local.set $aux (call $__ptr_aux (local.get $ptr)))
@@ -956,6 +1020,209 @@ export default (ctx) => {
956
1020
  (else (i32.store8 (i32.add (local.get $off) (local.get $i)) (local.get $bits))))))))))))
957
1021
  (local.get $v))`
958
1022
 
1023
+ // .fill(value, start?, end?) for typed arrays. The plain-array __arr_fill gates
1024
+ // on PTR.ARRAY and silently no-ops a typed receiver (the storage layout and
1025
+ // element width differ); this loops the element-width-aware __typed_set_idx
1026
+ // over the clamped range so every element kind (u8…f64, BigInt) fills correctly.
1027
+ // start/end default 0/length, accept negatives, and clamp to [0, length].
1028
+ ctx.core.stdlib['__typed_fill'] = `(func $__typed_fill (param $ptr i64) (param $val f64) (param $start i32) (param $end i32) (result f64)
1029
+ (local $len i32) (local $i i32)
1030
+ (local.set $len (call $__len (local.get $ptr)))
1031
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
1032
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
1033
+ (local.set $i (local.get $start))
1034
+ (block $done (loop $fill
1035
+ (br_if $done (i32.ge_s (local.get $i) (local.get $end)))
1036
+ (drop (call $__typed_set_idx (local.get $ptr) (local.get $i) (local.get $val)))
1037
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1038
+ (br $fill)))
1039
+ (f64.reinterpret_i64 (local.get $ptr)))`
1040
+
1041
+ // Element-width/-kind-aware read: arr[i] → f64, the read mirror of __typed_set_idx.
1042
+ // Integers convert by signedness (et&1 ⇒ unsigned); BigInt returns the raw i64 bits
1043
+ // reinterpreted as f64 so a get→set roundtrip is bit-exact (set_idx stores the bits
1044
+ // back unchanged). Used by the in-place algorithms below for random-access reads.
1045
+ ctx.core.stdlib['__typed_get_idx'] = `(func $__typed_get_idx (param $ptr i64) (param $i i32) (result f64)
1046
+ (local $off i32) (local $aux i32) (local $et i32)
1047
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
1048
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
1049
+ (if (i32.and (local.get $aux) (i32.const 8))
1050
+ (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
1051
+ (local.set $et (i32.and (local.get $aux) (i32.const 7)))
1052
+ (if (result f64) (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
1053
+ (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
1054
+ (else (if (result f64) (i32.eq (local.get $et) (i32.const 7))
1055
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
1056
+ (else (if (result f64) (i32.eq (local.get $et) (i32.const 6))
1057
+ (then (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
1058
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
1059
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
1060
+ (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
1061
+ (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
1062
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
1063
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
1064
+ (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
1065
+ (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
1066
+ (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
1067
+ (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
1068
+ (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))))))`
1069
+
1070
+ // .reverse() — in-place, element-kind-agnostic via get/set (bit-exact for BigInt).
1071
+ ctx.core.stdlib['__typed_reverse'] = `(func $__typed_reverse (param $ptr i64) (result f64)
1072
+ (local $len i32) (local $i i32) (local $j i32) (local $t f64)
1073
+ (local.set $len (call $__len (local.get $ptr)))
1074
+ (local.set $i (i32.const 0))
1075
+ (local.set $j (i32.sub (local.get $len) (i32.const 1)))
1076
+ (block $done (loop $rev
1077
+ (br_if $done (i32.ge_s (local.get $i) (local.get $j)))
1078
+ (local.set $t (call $__typed_get_idx (local.get $ptr) (local.get $i)))
1079
+ (drop (call $__typed_set_idx (local.get $ptr) (local.get $i) (call $__typed_get_idx (local.get $ptr) (local.get $j))))
1080
+ (drop (call $__typed_set_idx (local.get $ptr) (local.get $j) (local.get $t)))
1081
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1082
+ (local.set $j (i32.sub (local.get $j) (i32.const 1)))
1083
+ (br $rev)))
1084
+ (f64.reinterpret_i64 (local.get $ptr)))`
1085
+
1086
+ // .copyWithin(target, start, end) — in-place overlap-safe move. Indices accept
1087
+ // negatives (from end) and clamp to [0, len]; count = min(end-start, len-target).
1088
+ // Direction picked so overlapping ranges don't clobber unread source elements.
1089
+ ctx.core.stdlib['__typed_copyWithin'] = `(func $__typed_copyWithin (param $ptr i64) (param $target i32) (param $start i32) (param $end i32) (result f64)
1090
+ (local $len i32) (local $count i32) (local $k i32)
1091
+ (local.set $len (call $__len (local.get $ptr)))
1092
+ (local.set $target (call $__clamp_idx (local.get $target) (local.get $len)))
1093
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
1094
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
1095
+ (local.set $count (i32.sub (local.get $end) (local.get $start)))
1096
+ (if (i32.gt_s (local.get $count) (i32.sub (local.get $len) (local.get $target)))
1097
+ (then (local.set $count (i32.sub (local.get $len) (local.get $target)))))
1098
+ (if (i32.lt_s (local.get $target) (local.get $start))
1099
+ (then
1100
+ (local.set $k (i32.const 0))
1101
+ (block $fd (loop $fl
1102
+ (br_if $fd (i32.ge_s (local.get $k) (local.get $count)))
1103
+ (drop (call $__typed_set_idx (local.get $ptr) (i32.add (local.get $target) (local.get $k))
1104
+ (call $__typed_get_idx (local.get $ptr) (i32.add (local.get $start) (local.get $k)))))
1105
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))
1106
+ (br $fl))))
1107
+ (else
1108
+ (local.set $k (local.get $count))
1109
+ (block $bd (loop $bl
1110
+ (br_if $bd (i32.le_s (local.get $k) (i32.const 0)))
1111
+ (local.set $k (i32.sub (local.get $k) (i32.const 1)))
1112
+ (drop (call $__typed_set_idx (local.get $ptr) (i32.add (local.get $target) (local.get $k))
1113
+ (call $__typed_get_idx (local.get $ptr) (i32.add (local.get $start) (local.get $k)))))
1114
+ (br $bl)))))
1115
+ (f64.reinterpret_i64 (local.get $ptr)))`
1116
+
1117
+ // .sort() — default numeric order (insertion sort, stable). NaN sorts to the end,
1118
+ // -0 before +0 (the equal-value tiebreak via signed-bit compare). BigInt arrays are
1119
+ // compared as signed i64 on their exact bits. A user comparator is handled inline by
1120
+ // the .typed:sort emitter (this helper is the no-argument numeric path).
1121
+ ctx.core.stdlib['__typed_sort'] = `(func $__typed_sort (param $ptr i64) (result f64)
1122
+ (local $isbig i32) (local $len i32) (local $i i32) (local $j i32) (local $saved f64) (local $nb f64)
1123
+ (local.set $isbig (i32.and (call $__ptr_aux (local.get $ptr)) (i32.const ${TYPED_ELEM_BIGINT_FLAG})))
1124
+ (local.set $len (call $__len (local.get $ptr)))
1125
+ (local.set $i (i32.const 1))
1126
+ (block $od (loop $ol
1127
+ (br_if $od (i32.ge_s (local.get $i) (local.get $len)))
1128
+ (local.set $saved (call $__typed_get_idx (local.get $ptr) (local.get $i)))
1129
+ (local.set $j (i32.sub (local.get $i) (i32.const 1)))
1130
+ (block $id (loop $il
1131
+ (br_if $id (i32.lt_s (local.get $j) (i32.const 0)))
1132
+ (local.set $nb (call $__typed_get_idx (local.get $ptr) (local.get $j)))
1133
+ (br_if $id (i32.eqz
1134
+ (if (result i32) (local.get $isbig)
1135
+ (then (i64.gt_s (i64.reinterpret_f64 (local.get $nb)) (i64.reinterpret_f64 (local.get $saved))))
1136
+ (else (if (result i32) (f64.ne (local.get $nb) (local.get $nb))
1137
+ (then (i32.eqz (f64.ne (local.get $saved) (local.get $saved))))
1138
+ (else (if (result i32) (f64.ne (local.get $saved) (local.get $saved))
1139
+ (then (i32.const 0))
1140
+ (else (if (result i32) (f64.gt (local.get $nb) (local.get $saved))
1141
+ (then (i32.const 1))
1142
+ (else (if (result i32) (f64.lt (local.get $nb) (local.get $saved))
1143
+ (then (i32.const 0))
1144
+ (else (i64.gt_s (i64.reinterpret_f64 (local.get $nb)) (i64.reinterpret_f64 (local.get $saved)))))))))))))))
1145
+ (drop (call $__typed_set_idx (local.get $ptr) (i32.add (local.get $j) (i32.const 1)) (local.get $nb)))
1146
+ (local.set $j (i32.sub (local.get $j) (i32.const 1)))
1147
+ (br $il)))
1148
+ (drop (call $__typed_set_idx (local.get $ptr) (i32.add (local.get $j) (i32.const 1)) (local.get $saved)))
1149
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1150
+ (br $ol)))
1151
+ (f64.reinterpret_i64 (local.get $ptr)))`
1152
+
1153
+ ctx.core.emit['.typed:fill'] = (arr, val, start, end) => {
1154
+ inc('__typed_fill')
1155
+ return typed(['call', '$__typed_fill',
1156
+ asI64(emit(arr)),
1157
+ val == null ? undefExpr() : asF64(emit(val)),
1158
+ start == null ? ['i32.const', 0] : asI32(emit(start)),
1159
+ end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
1160
+ }
1161
+
1162
+ // .reverse() / .copyWithin(...) for typed arrays. The plain-array helpers gate on
1163
+ // PTR.ARRAY and silently no-op a typed receiver; these go through the element-kind-
1164
+ // aware get/set helpers so every width and signedness reverses/moves correctly.
1165
+ ctx.core.emit['.typed:reverse'] = (arr) => {
1166
+ inc('__typed_reverse')
1167
+ return typed(['call', '$__typed_reverse', asI64(emit(arr))], 'f64')
1168
+ }
1169
+
1170
+ ctx.core.emit['.typed:copyWithin'] = (arr, target, start, end) => {
1171
+ inc('__typed_copyWithin')
1172
+ return typed(['call', '$__typed_copyWithin',
1173
+ asI64(emit(arr)),
1174
+ target == null ? ['i32.const', 0] : asI32(emit(target)),
1175
+ start == null ? ['i32.const', 0] : asI32(emit(start)),
1176
+ end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
1177
+ }
1178
+
1179
+ // .sort(compareFn?) for typed arrays. No argument → the numeric __typed_sort helper
1180
+ // (typed-array default is NUMERIC, unlike Array.sort's lexicographic default — so it
1181
+ // must NOT route through the plain-array string comparator). With a comparator, an
1182
+ // insertion sort is emitted inline, calling the closure per neighbor compare; a
1183
+ // positive return shifts (same convention as Array.prototype.sort).
1184
+ // Sort the typed array VALUE `arrValIR` in place and return it. Factored out so
1185
+ // .typed:sort sorts the receiver and .typed:toSorted sorts a fresh copy with one body.
1186
+ const emitTypedSort = (arrValIR, fn) => {
1187
+ if (fn == null) {
1188
+ inc('__typed_sort')
1189
+ return typed(['call', '$__typed_sort', asI64(arrValIR)], 'f64')
1190
+ }
1191
+ inc('__len', '__typed_get_idx', '__typed_set_idx')
1192
+ const arrL = temp('tsa'), cbL = temp('tsf')
1193
+ const len = tempI32('tsn'), i = tempI32('tsi'), j = tempI32('tsj')
1194
+ const cur = temp('tsc'), nb = temp('tsb')
1195
+ const id = ctx.func.uniq++
1196
+ const oE = `$tsoe${id}`, oL = `$tsol${id}`, iE = `$tsie${id}`, iL = `$tsil${id}`
1197
+ const ptr = () => ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]
1198
+ const jp1 = ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]
1199
+ return typed(['block', ['result', 'f64'],
1200
+ ['local.set', `$${arrL}`, asF64(arrValIR)],
1201
+ ['local.set', `$${cbL}`, asF64(emit(fn))],
1202
+ ['local.set', `$${len}`, ['call', '$__len', ptr()]],
1203
+ ['local.set', `$${i}`, ['i32.const', 1]],
1204
+ ['block', oE, ['loop', oL,
1205
+ ['br_if', oE, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
1206
+ ['local.set', `$${cur}`, ['call', '$__typed_get_idx', ptr(), ['local.get', `$${i}`]]],
1207
+ ['local.set', `$${j}`, ['i32.sub', ['local.get', `$${i}`], ['i32.const', 1]]],
1208
+ ['block', iE, ['loop', iL,
1209
+ ['br_if', iE, ['i32.lt_s', ['local.get', `$${j}`], ['i32.const', 0]]],
1210
+ ['local.set', `$${nb}`, ['call', '$__typed_get_idx', ptr(), ['local.get', `$${j}`]]],
1211
+ // Break unless cmp(neighbor, cur) > 0. f64.gt is false for NaN (spec NaN-as-0).
1212
+ ['br_if', iE, ['i32.eqz', ['f64.gt',
1213
+ asF64(ctx.closure.call(typed(['local.get', `$${cbL}`], 'f64'),
1214
+ [typed(['local.get', `$${nb}`], 'f64'), typed(['local.get', `$${cur}`], 'f64')])),
1215
+ ['f64.const', 0]]]],
1216
+ ['drop', ['call', '$__typed_set_idx', ptr(), jp1, ['local.get', `$${nb}`]]],
1217
+ ['local.set', `$${j}`, ['i32.sub', ['local.get', `$${j}`], ['i32.const', 1]]],
1218
+ ['br', iL]]],
1219
+ ['drop', ['call', '$__typed_set_idx', ptr(), jp1, ['local.get', `$${cur}`]]],
1220
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1221
+ ['br', oL]]],
1222
+ ['local.get', `$${arrL}`]], 'f64')
1223
+ }
1224
+ ctx.core.emit['.typed:sort'] = (arr, fn) => emitTypedSort(emit(arr), fn)
1225
+
959
1226
  // Type-aware TypedArray read: arr[i]
960
1227
  ctx.core.emit['.typed:[]'] = (arr, i) => {
961
1228
  const r = resolveElem(arr)
@@ -1007,20 +1274,28 @@ export default (ctx) => {
1007
1274
  ['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]],
1008
1275
  ['local.get', `$${vt}`]], void_ ? 'void' : 'f64') // Float32Array
1009
1276
  }
1010
- // Integer store: when the source is already i32-typed (bitwise ops, |0,
1011
- // typed-array load, known-i32 var), store it directly skip the f64
1012
- // detour that costs a sign branch + i64 trunc + i32 wrap on every write.
1013
- if (valIR.type === 'i32') {
1014
- const cheap = Array.isArray(valIR) &&
1015
- ((valIR[0] === 'local.get' && typeof valIR[1] === 'string') ||
1016
- (valIR[0] === 'i32.const' && (typeof valIR[1] === 'number' || typeof valIR[1] === 'string')))
1017
- if (void_ && cheap) return typed([STORE[et], off, valIR], 'void')
1277
+ // Integer store: when the source is already i32-typed (bitwise ops, |0, known-i32 var) —
1278
+ // OR an `f64.convert_i32_*` that peels back to i32 (an Int8/Uint8/Int16/… element READ
1279
+ // materialized as f64 by the universal value model) store the i32 low bits directly,
1280
+ // skipping the f64 detour that costs a sign branch + i64 trunc + i32 wrap on every write.
1281
+ // This eradicates the f64 round-trip on byte/typed-array TRANSFORMS — `out[i] = table[in[j]]`
1282
+ // and `dst[i] = src[j]` (base64, qoi, wav, blur) — where both sides are integer elements.
1283
+ // `store8/16` mask the low bits, so storing the convert's i32 source is bit-identical; the
1284
+ // non-void result reboxes that i32 to f64 (the assignment's RHS value, in element range here).
1285
+ const i32Backed = valIR.type === 'i32' ||
1286
+ (Array.isArray(valIR) && (valIR[0] === 'f64.convert_i32_s' || valIR[0] === 'f64.convert_i32_u'))
1287
+ 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')
1018
1293
  const v32 = tempI32('tw')
1019
1294
  return typed(void_ ? ['block',
1020
- ['local.set', `$${v32}`, valIR],
1295
+ ['local.set', `$${v32}`, vi],
1021
1296
  [STORE[et], off, ['local.get', `$${v32}`]]]
1022
1297
  : ['block', ['result', 'f64'],
1023
- ['local.set', `$${v32}`, valIR],
1298
+ ['local.set', `$${v32}`, vi],
1024
1299
  [STORE[et], off, ['local.get', `$${v32}`]],
1025
1300
  [(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', ['local.get', `$${v32}`]]], void_ ? 'void' : 'f64')
1026
1301
  }
@@ -1257,18 +1532,54 @@ export default (ctx) => {
1257
1532
 
1258
1533
  // .indexOf: scalar value-equality search. Returns -1 on miss. Compare on f64
1259
1534
  // — Array.prototype.indexOf uses strict equality (NaN ≠ NaN).
1260
- ctx.core.emit['.typed:indexOf'] = (arr, val) => {
1261
- const found = tempI32('tif'), needle = temp('tin')
1262
- const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => [
1263
- ['if', ['f64.eq', load(), ['local.get', `$${needle}`]],
1264
- ['then',
1265
- ['local.set', `$${found}`, ['local.get', `$${i}`]],
1266
- ['br', exit]]]
1267
- ])
1535
+ // Effective start index for a fromIndex arg: negative counts from the end. The match
1536
+ // guard is `i >= start` and i ≥ 0, so a start below 0 needs no clamp (always passes).
1537
+ const fromStart = (fiL, len) => ['if', ['result', 'i32'],
1538
+ ['i32.lt_s', ['local.get', `$${fiL}`], ['i32.const', 0]],
1539
+ ['then', ['i32.add', ['local.get', `$${fiL}`], ['local.get', `$${len}`]]],
1540
+ ['else', ['local.get', `$${fiL}`]]]
1541
+
1542
+ ctx.core.emit['.typed:indexOf'] = (arr, val, fromIndex) => {
1543
+ const found = tempI32('tif'), needle = temp('tin'), fiL = tempI32('tifx')
1544
+ const loop = typedLoop(arr, (load, i, len, _ptr, exit) => {
1545
+ const matched = ['f64.eq', load(), ['local.get', `$${needle}`]]
1546
+ const cond = fromIndex == null ? matched
1547
+ : ['i32.and', ['i32.ge_s', ['local.get', `$${i}`], fromStart(fiL, len)], matched]
1548
+ return [['if', cond, ['then',
1549
+ ['local.set', `$${found}`, ['local.get', `$${i}`]], ['br', exit]]]]
1550
+ })
1268
1551
  if (!loop) return null
1269
1552
  return typed(['block', ['result', 'f64'],
1270
1553
  ['local.set', `$${needle}`, asF64(emit(val))],
1271
1554
  ['local.set', `$${found}`, ['i32.const', -1]],
1555
+ ...(fromIndex == null ? [] : [['local.set', `$${fiL}`, asI32(emit(fromIndex))]]),
1556
+ ...loop.setup,
1557
+ ['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
1558
+ }
1559
+
1560
+ // .lastIndexOf(val): the last index whose element strictly-equals val, else -1.
1561
+ // Was unimplemented for typed arrays (threw). A forward scan that keeps the latest
1562
+ // match needs no reverse iteration — equivalent for the no-fromIndex form, which is
1563
+ // the common case. With a fromIndex the matcher additionally bounds i ≤ fromIndex
1564
+ // (negative counts from the end, resolved against the typedLoop len). NaN never
1565
+ // strict-equals (f64.eq), matching JS.
1566
+ ctx.core.emit['.typed:lastIndexOf'] = (arr, val, fromIndex) => {
1567
+ const found = tempI32('tlf'), needle = temp('tln'), fiL = tempI32('tlfi')
1568
+ const loop = typedLoop(arr, (load, i, len) => {
1569
+ const matched = ['f64.eq', load(), ['local.get', `$${needle}`]]
1570
+ if (fromIndex == null)
1571
+ return [['if', matched, ['then', ['local.set', `$${found}`, ['local.get', `$${i}`]]]]]
1572
+ const lim = ['if', ['result', 'i32'], ['i32.lt_s', ['local.get', `$${fiL}`], ['i32.const', 0]],
1573
+ ['then', ['i32.add', ['local.get', `$${fiL}`], ['local.get', `$${len}`]]],
1574
+ ['else', ['local.get', `$${fiL}`]]]
1575
+ return [['if', ['i32.and', ['i32.le_s', ['local.get', `$${i}`], lim], matched],
1576
+ ['then', ['local.set', `$${found}`, ['local.get', `$${i}`]]]]]
1577
+ })
1578
+ if (!loop) return null
1579
+ return typed(['block', ['result', 'f64'],
1580
+ ['local.set', `$${needle}`, asF64(emit(val))],
1581
+ ['local.set', `$${found}`, ['i32.const', -1]],
1582
+ ...(fromIndex == null ? [] : [['local.set', `$${fiL}`, asI32(emit(fromIndex))]]),
1272
1583
  ...loop.setup,
1273
1584
  ['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
1274
1585
  }
@@ -1276,21 +1587,23 @@ export default (ctx) => {
1276
1587
  // .includes: like indexOf but NaN-equal-NaN (JS spec). Stash needle bits as
1277
1588
  // i64 and compare via i64.eq so two NaNs with matching bit patterns match
1278
1589
  // (f64.eq would say false).
1279
- ctx.core.emit['.typed:includes'] = (arr, val) => {
1280
- const found = tempI32('thf'), needle = temp('thn')
1281
- const loop = typedLoop(arr, (load, _i, _len, _ptr, exit) => [
1282
- ['if',
1283
- ['i32.or',
1284
- ['f64.eq', load(), ['local.get', `$${needle}`]],
1285
- ['i64.eq',
1286
- ['i64.reinterpret_f64', load()],
1287
- ['i64.reinterpret_f64', ['local.get', `$${needle}`]]]],
1288
- ['then', ['local.set', `$${found}`, ['i32.const', 1]], ['br', exit]]]
1289
- ])
1590
+ ctx.core.emit['.typed:includes'] = (arr, val, fromIndex) => {
1591
+ const found = tempI32('thf'), needle = temp('thn'), fiL = tempI32('thx')
1592
+ const loop = typedLoop(arr, (load, i, len, _ptr, exit) => {
1593
+ const matched = ['i32.or',
1594
+ ['f64.eq', load(), ['local.get', `$${needle}`]],
1595
+ ['i64.eq',
1596
+ ['i64.reinterpret_f64', load()],
1597
+ ['i64.reinterpret_f64', ['local.get', `$${needle}`]]]]
1598
+ const cond = fromIndex == null ? matched
1599
+ : ['i32.and', ['i32.ge_s', ['local.get', `$${i}`], fromStart(fiL, len)], matched]
1600
+ return [['if', cond, ['then', ['local.set', `$${found}`, ['i32.const', 1]], ['br', exit]]]]
1601
+ })
1290
1602
  if (!loop) return null
1291
1603
  return typed(['block', ['result', 'f64'],
1292
1604
  ['local.set', `$${needle}`, asF64(emit(val))],
1293
1605
  ['local.set', `$${found}`, ['i32.const', 0]],
1606
+ ...(fromIndex == null ? [] : [['local.set', `$${fiL}`, asI32(emit(fromIndex))]]),
1294
1607
  ...loop.setup,
1295
1608
  ['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
1296
1609
  }
@@ -1328,6 +1641,40 @@ export default (ctx) => {
1328
1641
  ctx.core.emit['.typed:find'] = (arr, fn) => findCommon(arr, fn, false)
1329
1642
  ctx.core.emit['.typed:findIndex'] = (arr, fn) => findCommon(arr, fn, true)
1330
1643
 
1644
+ // .findLast / .findLastIndex — like find/findIndex but keep the LAST match instead of
1645
+ // the first, so no early break (a forward scan that overwrites on each hit). Without a
1646
+ // typed handler these routed through the plain-array versions, which read elements as
1647
+ // raw f64 and returned garbage for non-f64 typed arrays.
1648
+ const findLastCommon = (arr, fn, returnIndex) => {
1649
+ const cbLoc = temp('tLc'), result = temp('tLr'), foundIdx = tempI32('tLi')
1650
+ const loop = typedLoop(arr, (load, i) => {
1651
+ const itemLoc = temp('tLit')
1652
+ return [
1653
+ ['local.set', `$${itemLoc}`, load()],
1654
+ ['if', truthyIR(ctx.closure.call(
1655
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1656
+ [typed(['local.get', `$${itemLoc}`], 'f64'),
1657
+ typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')])),
1658
+ ['then',
1659
+ returnIndex
1660
+ ? ['local.set', `$${foundIdx}`, ['local.get', `$${i}`]]
1661
+ : ['local.set', `$${result}`, typed(['local.get', `$${itemLoc}`], 'f64')]]]
1662
+ ]
1663
+ })
1664
+ if (!loop) return null
1665
+ return typed(['block', ['result', 'f64'],
1666
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1667
+ returnIndex
1668
+ ? ['local.set', `$${foundIdx}`, ['i32.const', -1]]
1669
+ : ['local.set', `$${result}`, undefExpr()],
1670
+ ...loop.setup,
1671
+ returnIndex
1672
+ ? typed(['f64.convert_i32_s', ['local.get', `$${foundIdx}`]], 'f64')
1673
+ : typed(['local.get', `$${result}`], 'f64')], 'f64')
1674
+ }
1675
+ ctx.core.emit['.typed:findLast'] = (arr, fn) => findLastCommon(arr, fn, false)
1676
+ ctx.core.emit['.typed:findLastIndex'] = (arr, fn) => findLastCommon(arr, fn, true)
1677
+
1331
1678
  // .some / .every: short-circuit boolean reduction. some=∃, every=∀.
1332
1679
  const anyAllCommon = (arr, fn, isEvery) => {
1333
1680
  const cbLoc = temp('tac'), result = tempI32('tar')
@@ -1411,7 +1758,17 @@ export default (ctx) => {
1411
1758
  // Bulk copy via `memory.copy`.
1412
1759
  ctx.core.emit['.typed:slice'] = (arr, start, end) => {
1413
1760
  const r = resolveElem(arr)
1414
- if (!r || r.isBigInt) return null
1761
+ if (!r) {
1762
+ // Elem type / view-ness not statically known (owned→view reassigned binding).
1763
+ // Dispatch off the runtime aux byte instead of crashing on empty IR.
1764
+ inc('__typed_slice_rt')
1765
+ return typed(['call', '$__typed_slice_rt',
1766
+ ['i64.reinterpret_f64', asF64(emit(arr))],
1767
+ start == null ? ['i32.const', 0] : asI32(emit(start)),
1768
+ end == null ? ['i32.const', 0] : asI32(emit(end)),
1769
+ ['i32.const', end == null ? 0 : 1]], 'f64')
1770
+ }
1771
+ if (r.isBigInt) return null
1415
1772
  const { et, isView } = r
1416
1773
  const arrLoc = temp('tsa'), srcPtr = tempI32('tssp'), srcLen = tempI32('tssl')
1417
1774
  const lo = tempI32('tslo'), hi = tempI32('tshi'), n = tempI32('tsn')
@@ -1460,4 +1817,100 @@ export default (ctx) => {
1460
1817
  ['i32.shl', ['local.get', `$${n}`], ['i32.const', SHIFT[et]]]],
1461
1818
  dst.ptr], 'f64')
1462
1819
  }
1820
+
1821
+ // .toReversed() — a reversed COPY (receiver unchanged): full slice-copy, then reverse
1822
+ // the copy in place. (slice bails on BigInt, so BigInt receivers fall back to a throw.)
1823
+ ctx.core.emit['.typed:toReversed'] = (arr) => {
1824
+ const copy = ctx.core.emit['.typed:slice'](arr)
1825
+ if (!copy) return null
1826
+ inc('__typed_reverse')
1827
+ const c = temp('ttv')
1828
+ return typed(['block', ['result', 'f64'],
1829
+ ['local.set', `$${c}`, asF64(copy)],
1830
+ ['call', '$__typed_reverse', ['i64.reinterpret_f64', ['local.get', `$${c}`]]]], 'f64')
1831
+ }
1832
+
1833
+ // .toSorted(fn?) — a sorted COPY (receiver unchanged): slice-copy, then sort in place.
1834
+ ctx.core.emit['.typed:toSorted'] = (arr, fn) => {
1835
+ const copy = ctx.core.emit['.typed:slice'](arr)
1836
+ return copy ? emitTypedSort(copy, fn) : null
1837
+ }
1838
+
1839
+ // .with(index, value) — a COPY with one element replaced (receiver unchanged). Negative
1840
+ // index counts from the end; out of range throws RangeError ($__jz_err), per spec.
1841
+ ctx.core.emit['.typed:with'] = (arr, index, value) => {
1842
+ const copy = ctx.core.emit['.typed:slice'](arr)
1843
+ if (!copy) return null
1844
+ ctx.runtime.throws = true
1845
+ inc('__len', '__typed_set_idx')
1846
+ const c = temp('twc'), idx = tempI32('twi'), len = tempI32('twl')
1847
+ return typed(['block', ['result', 'f64'],
1848
+ ['local.set', `$${c}`, asF64(copy)],
1849
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${c}`]]]],
1850
+ ['local.set', `$${idx}`, asI32(emit(index))],
1851
+ ['if', ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
1852
+ ['then', ['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${len}`]]]]],
1853
+ ['if', ['i32.or',
1854
+ ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
1855
+ ['i32.ge_s', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
1856
+ ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
1857
+ ['drop', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${c}`]],
1858
+ ['local.get', `$${idx}`], asF64(emit(value))]],
1859
+ ['local.get', `$${c}`]], 'f64')
1860
+ }
1861
+
1862
+ // .subarray(begin, end) — a zero-copy VIEW sharing the receiver's buffer (writes alias,
1863
+ // NOT a copy). Builds the 16-byte descriptor [byteLen][dataOff][parentOff] and tags the
1864
+ // TYPED ptr with aux|view, exactly like new TypedArray(buffer, byteOffset, length).
1865
+ ctx.core.emit['.typed:subarray'] = (arr, begin, end) => {
1866
+ ctx.features.typedView = true // zero-copy view aliases the receiver — covers inline `a.subarray(1)[i]=…` the bound-decl path in analyze.js misses
1867
+ const r = resolveElem(arr)
1868
+ if (!r) {
1869
+ // Elem type / view-ness not statically known (owned→view reassigned binding).
1870
+ // Dispatch off the runtime aux byte instead of crashing on empty IR.
1871
+ inc('__subarray')
1872
+ return typed(['call', '$__subarray',
1873
+ ['i64.reinterpret_f64', asF64(emit(arr))],
1874
+ begin == null ? ['i32.const', 0] : asI32(emit(begin)),
1875
+ end == null ? ['i32.const', 0] : asI32(emit(end)),
1876
+ ['i32.const', end == null ? 0 : 1]], 'f64')
1877
+ }
1878
+ const { et, isView, isBigInt } = r
1879
+ const shift = SHIFT[et]
1880
+ const viewAux = et | 8 | (isBigInt ? 16 : 0)
1881
+ const arrL = temp('tua'), srcOff = tempI32('tuo'), data = tempI32('tud'), root = tempI32('tur')
1882
+ const len = tempI32('tul'), lo = tempI32('tulo'), hi = tempI32('tuhi'), n = tempI32('tun'), desc = tempI32('tude')
1883
+ inc('__len')
1884
+ const off4 = (o) => ['i32.load', ['i32.add', ['local.get', `$${srcOff}`], ['i32.const', o]]]
1885
+ const clamp = (boundExpr, dflt, name) => {
1886
+ if (boundExpr == null) return dflt
1887
+ const v = tempI32(name)
1888
+ return ['block', ['result', 'i32'],
1889
+ ['local.set', `$${v}`, asI32(emit(boundExpr))],
1890
+ ['if', ['i32.lt_s', ['local.get', `$${v}`], ['i32.const', 0]],
1891
+ ['then', ['local.set', `$${v}`, ['i32.add', ['local.get', `$${v}`], ['local.get', `$${len}`]]]]],
1892
+ ['if', ['i32.lt_s', ['local.get', `$${v}`], ['i32.const', 0]],
1893
+ ['then', ['local.set', `$${v}`, ['i32.const', 0]]]],
1894
+ ['if', ['i32.gt_s', ['local.get', `$${v}`], ['local.get', `$${len}`]],
1895
+ ['then', ['local.set', `$${v}`, ['local.get', `$${len}`]]]],
1896
+ ['local.get', `$${v}`]]
1897
+ }
1898
+ return typed(['block', ['result', 'f64'],
1899
+ ['local.set', `$${arrL}`, asF64(emit(arr))],
1900
+ ['local.set', `$${srcOff}`, typedBase(typed(['local.get', `$${arrL}`], 'f64'))],
1901
+ ['local.set', `$${data}`, isView ? off4(4) : ['local.get', `$${srcOff}`]],
1902
+ ['local.set', `$${root}`, isView ? off4(8) : ['local.get', `$${srcOff}`]],
1903
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
1904
+ ['local.set', `$${lo}`, clamp(begin, ['i32.const', 0], 'tub')],
1905
+ ['local.set', `$${hi}`, clamp(end, ['local.get', `$${len}`], 'tue')],
1906
+ ['local.set', `$${n}`, ['select',
1907
+ ['i32.sub', ['local.get', `$${hi}`], ['local.get', `$${lo}`]], ['i32.const', 0],
1908
+ ['i32.gt_s', ['local.get', `$${hi}`], ['local.get', `$${lo}`]]]],
1909
+ ['local.set', `$${desc}`, ['call', '$__alloc', ['i32.const', 16]]],
1910
+ ['i32.store', ['local.get', `$${desc}`], ['i32.shl', ['local.get', `$${n}`], ['i32.const', shift]]],
1911
+ ['i32.store', ['i32.add', ['local.get', `$${desc}`], ['i32.const', 4]],
1912
+ ['i32.add', ['local.get', `$${data}`], ['i32.shl', ['local.get', `$${lo}`], ['i32.const', shift]]]],
1913
+ ['i32.store', ['i32.add', ['local.get', `$${desc}`], ['i32.const', 8]], ['local.get', `$${root}`]],
1914
+ mkPtrIR(PTR.TYPED, viewAux, ['local.get', `$${desc}`])], 'f64')
1915
+ }
1463
1916
  }