jz 0.4.0 → 0.5.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.
@@ -7,7 +7,7 @@
7
7
  * @module typed
8
8
  */
9
9
 
10
- import { typed, asF64, asI32, asI64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, temp, tempI32, undefExpr } from '../src/ir.js'
10
+ import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
11
11
  import { emit } from '../src/emit.js'
12
12
  import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
13
13
  import { inc, PTR } from '../src/ctx.js'
@@ -209,8 +209,11 @@ export default (ctx) => {
209
209
  __typed_set_idx: ['__ptr_aux', '__ptr_offset'],
210
210
  })
211
211
 
212
- // .map/.filter invoke callbacks with arity 1 internally.
213
- ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 1)
212
+ // .map invokes with arity 1; .forEach/.find/.some/.every/.filter/.findIndex
213
+ // invoke with (item, idx) → arity 2. Reduce with (acc, item) → arity 2.
214
+ // (jz omits the `arr` arg array-spec callbacks normally receive — matches
215
+ // array.js convention.)
216
+ ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 2)
214
217
 
215
218
  inc('__mkptr', '__alloc', '__len')
216
219
 
@@ -335,16 +338,61 @@ export default (ctx) => {
335
338
  // ArrayBuffer: first-class byte storage with [-8:byteLen][-4:byteCap][bytes].
336
339
  // DataView: passthrough ptr to the same BUFFER — DataView methods operate on raw bytes via offset.
337
340
 
338
- // new ArrayBuffer(n) allocate n bytes, return as BUFFER pointer
341
+ // ToIndex + allocation-ceiling for ArrayBuffer byte lengths: ToInteger (NaN→0,
342
+ // trunc toward zero), then a RangeError for negatives and for any size jz
343
+ // cannot represent as an i32 byte count. The ceiling sits just below 2^31 so
344
+ // the trunc never traps and leaves room for the 8-byte buffer header — this
345
+ // also rejects the spec's ≥2^53 case and genuinely un-allocatable sizes.
346
+ ctx.core.stdlib['__ab_len'] = `(func $__ab_len (param $n f64) (result i32)
347
+ (if (f64.ne (local.get $n) (local.get $n)) (then (local.set $n (f64.const 0))))
348
+ (local.set $n (f64.trunc (local.get $n)))
349
+ (if (i32.or
350
+ (f64.lt (local.get $n) (f64.const 0))
351
+ (f64.ge (local.get $n) (f64.const 2147483640)))
352
+ (then (throw $__jz_err (f64.const 0))))
353
+ (i32.trunc_f64_s (local.get $n)))`
354
+
355
+ // new ArrayBuffer(n) → allocate n bytes, return as BUFFER pointer.
356
+ // Length is ToNumber-coerced (ToIndex) so a Symbol length raises a TypeError;
357
+ // __ab_len then throws a RangeError on a negative or oversized request.
339
358
  const arrayBufferCtor = (sizeExpr) => {
340
- const n = asI32(emit(sizeExpr))
359
+ ctx.runtime.throws = true
360
+ inc('__ab_len')
361
+ const n = typed(['call', '$__ab_len', toNumF64(sizeExpr, emit(sizeExpr))], 'i32')
341
362
  const out = allocPtr({ type: PTR.BUFFER, len: n, stride: 1, tag: 'ab' })
342
363
  return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
343
364
  }
344
365
  ctx.core.emit['new.ArrayBuffer'] = arrayBufferCtor
345
366
 
346
- // new DataView(buffer) same ptr (BUFFER). Its type tag may differ from BUFFER but methods ignore type.
347
- ctx.core.emit['new.DataView'] = (bufExpr) => asF64(emit(bufExpr))
367
+ // new DataView(buffer, byteOffset?, byteLength?) a first-class view object,
368
+ // represented exactly like a typed-array subview: a 16-byte descriptor
369
+ // [byteLen:i32][dataOff:i32][parentOff:i32][pad] behind a TYPED|view pointer.
370
+ // byteOffset/byteLength are ToIndex-coerced; the no-length form snapshots the
371
+ // remaining buffer (buffer.byteLength - byteOffset). Because the view extent
372
+ // lives in the descriptor, .byteLength/.byteOffset/.buffer and the get/set
373
+ // bounds checks all read it at runtime — correct regardless of how the
374
+ // receiver variable was assigned, and ArrayBuffer.isView(dv) is now true.
375
+ ctx.core.emit['new.DataView'] = (bufExpr, offExpr, lenExpr) => {
376
+ ctx.features.typedarray = true
377
+ const src = temp('dvs')
378
+ const parentOff = tempI32('dvp')
379
+ const off = tempI32('dvo')
380
+ const dst = tempI32('dvd')
381
+ return typed(['block', ['result', 'f64'],
382
+ ['local.set', `$${src}`, asF64(emit(bufExpr))],
383
+ ['local.set', `$${parentOff}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
384
+ ['local.set', `$${off}`, offExpr == null ? ['i32.const', 0] : dvIndex(offExpr)],
385
+ ['local.set', `$${dst}`, ['call', '$__alloc', ['i32.const', 16]]],
386
+ ['i32.store', ['local.get', `$${dst}`],
387
+ lenExpr == null
388
+ ? ['i32.sub', ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]], ['local.get', `$${off}`]]
389
+ : dvIndex(lenExpr)],
390
+ ['i32.store', ['i32.add', ['local.get', `$${dst}`], ['i32.const', 4]],
391
+ ['i32.add', ['local.get', `$${parentOff}`], ['local.get', `$${off}`]]],
392
+ ['i32.store', ['i32.add', ['local.get', `$${dst}`], ['i32.const', 8]],
393
+ ['local.get', `$${parentOff}`]],
394
+ mkPtrIR(PTR.TYPED, 8, ['local.get', `$${dst}`])], 'f64')
395
+ }
348
396
 
349
397
  // BigInt64Array(buffer) (bare form, legacy): coerce to same data, Float64Array-compatible storage.
350
398
  ctx.core.emit['BigInt64Array'] = (bufExpr) => {
@@ -353,13 +401,13 @@ export default (ctx) => {
353
401
  return mkPtrIR(PTR.TYPED, typedAux('BigInt64Array'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', va]])
354
402
  }
355
403
 
356
- // .buffer — always aliased (zero-copy). BUFFER/DataView: passthrough.
404
+ // .buffer — always aliased (zero-copy). BUFFER: passthrough.
357
405
  // Owned TYPED: retag as BUFFER at same offset — the byteLen header is shared.
358
- // TYPED view: BUFFER at descriptor[8] (root parent data offset).
406
+ // TYPED view (incl. DataView): BUFFER at descriptor[8] (root parent data offset).
359
407
  ctx.core.emit['.buffer'] = (obj) => {
360
408
  if (typeof obj === 'string') {
361
409
  const ctor = ctx.types.typedElem?.get(obj)
362
- if (ctor === 'new.ArrayBuffer' || ctor === 'new.DataView') return asF64(emit(obj))
410
+ if (ctor === 'new.ArrayBuffer') return asF64(emit(obj))
363
411
  if (ctor?.startsWith('new.')) {
364
412
  const isView = ctor.endsWith('.view')
365
413
  const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
@@ -375,11 +423,12 @@ export default (ctx) => {
375
423
  return typed(['call', '$__to_buffer', asI64(emit(obj))], 'f64')
376
424
  }
377
425
 
378
- // .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride. View TYPED: descriptor[0].
426
+ // .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
427
+ // View TYPED (incl. DataView): descriptor[0], via the __byte_length fallback.
379
428
  ctx.core.emit['.byteLength'] = (obj) => {
380
429
  if (typeof obj === 'string') {
381
430
  const ctor = ctx.types.typedElem?.get(obj)
382
- if (ctor === 'new.ArrayBuffer' || ctor === 'new.DataView') {
431
+ if (ctor === 'new.ArrayBuffer') {
383
432
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(obj))]]], 'f64')
384
433
  }
385
434
  if (ctor && ctor.startsWith('new.')) {
@@ -433,8 +482,9 @@ export default (ctx) => {
433
482
  (i32.load (i32.add (local.get $off) (i32.const 8)))))
434
483
  (else (i32.const 0))))`
435
484
 
436
- // ArrayBuffer.isView(x) — true iff x is a TYPED pointer. (DataView passthrough cannot be
437
- // distinguished from ArrayBuffer since both are BUFFER pointers; both report false.)
485
+ // ArrayBuffer.isView(x) — true iff x is a TYPED pointer. Typed arrays and
486
+ // DataViews are both TYPED-tagged (a DataView is a TYPED|view descriptor), so
487
+ // both report true; a bare ArrayBuffer is BUFFER-tagged and reports false.
438
488
  ctx.core.emit['ArrayBuffer.isView'] = (v) => {
439
489
  if (v === undefined) return typed(['f64.const', 0], 'f64')
440
490
  const va = asF64(emit(v))
@@ -442,6 +492,19 @@ export default (ctx) => {
442
492
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', va]], ['i32.const', PTR.TYPED]]], 'f64')
443
493
  }
444
494
 
495
+ // x instanceof Float64Array | Int32Array | … — typed-pointer predicate emitted
496
+ // by jzify. NaN-check first, then __ptr_type === PTR.TYPED. Aux-byte ctor
497
+ // discrimination (Float64 vs Int32) lives downstream in typedElem analysis —
498
+ // this predicate only asserts "is some TypedArray". Result i32 (boolean).
499
+ ctx.core.emit['__is_typed'] = (x) => {
500
+ if (x === undefined) return typed(['i32.const', 0], 'i32')
501
+ const v = asF64(emit(x))
502
+ const t = temp('ityp')
503
+ return typed(['i32.and',
504
+ ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
505
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.TYPED]]], 'i32')
506
+ }
507
+
445
508
  // buf.slice(begin?, end?) on a BUFFER → fresh BUFFER with the byte range copied.
446
509
  // Only dispatches statically when obj is a tracked ArrayBuffer/DataView variable.
447
510
  ctx.core.emit['.buf:slice'] = (obj, beginExpr, endExpr) => {
@@ -470,44 +533,268 @@ export default (ctx) => {
470
533
  out.ptr], 'f64')
471
534
  }
472
535
 
473
- // DataView set methods: extract i32 offset from f64 ptr, store value
536
+ // DataView endianness wasm memory is natively little-endian. The DV `littleEndian`
537
+ // arg defaults to `false` per ECMA-262 (big-endian); when LE is false we have to
538
+ // byte-swap. `staticLE` peels a literal `[null, x]` AST node and returns:
539
+ // true/false — known endianness, no runtime branch needed
540
+ // null — dynamic, emit `if le (native) else (bswap)`
541
+ // We treat `undefined`/`null`/`0`/`''` as falsy (BE) per ToBoolean.
542
+ const staticLE = (node) => {
543
+ if (node === undefined) return false // arg omitted → BE
544
+ if (Array.isArray(node) && node[0] == null) {
545
+ // [] → undefined, [null, x] → literal x
546
+ if (node.length === 1) return false
547
+ const v = node[1]
548
+ if (v === undefined || v === null) return false
549
+ if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') return !!v
550
+ }
551
+ return null // dynamic
552
+ }
553
+
554
+ // bswap16: i32 bytes [b0 b1] → [b1 b0]. Result is unsigned 16-bit; signed-16
555
+ // sign-extension is applied by the load op (i32.load16_s) at the call site.
556
+ const bswap16I32 = (v) => {
557
+ const t = tempI32('bs16')
558
+ return ['block', ['result', 'i32'],
559
+ ['local.set', `$${t}`, v],
560
+ ['i32.or',
561
+ ['i32.shl', ['i32.and', ['local.get', `$${t}`], ['i32.const', 0xff]], ['i32.const', 8]],
562
+ ['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 8]], ['i32.const', 0xff]]]]
563
+ }
564
+
565
+ // bswap32: i32 bytes [b0 b1 b2 b3] → [b3 b2 b1 b0]. Symmetric — used for both
566
+ // load (after native LE load) and store (before native LE store).
567
+ const bswap32I32 = (v) => {
568
+ const t = tempI32('bs32')
569
+ return ['block', ['result', 'i32'],
570
+ ['local.set', `$${t}`, v],
571
+ ['i32.or',
572
+ ['i32.or',
573
+ ['i32.shl', ['local.get', `$${t}`], ['i32.const', 24]],
574
+ ['i32.shl', ['i32.and', ['local.get', `$${t}`], ['i32.const', 0xff00]], ['i32.const', 8]]],
575
+ ['i32.or',
576
+ ['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 8]], ['i32.const', 0xff00]],
577
+ ['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 24]], ['i32.const', 0xff]]]]]
578
+ }
579
+
580
+ // bswap64: rotate bytes via two 32-bit halves. We need a runtime helper because
581
+ // i64 doesn't have inline byte-swap and we'd otherwise emit a 64-line tree.
582
+ ctx.core.stdlib['__bswap64'] = `(func $__bswap64 (param $v i64) (result i64)
583
+ (local $r i64)
584
+ (local.set $r (i64.shl (i64.and (local.get $v) (i64.const 0xff)) (i64.const 56)))
585
+ (local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff00)) (i64.const 40))))
586
+ (local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff0000)) (i64.const 24))))
587
+ (local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff000000)) (i64.const 8))))
588
+ (local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff00000000)) (i64.const 8))))
589
+ (local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff0000000000)) (i64.const 24))))
590
+ (local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff000000000000)) (i64.const 40))))
591
+ (local.set $r (i64.or (local.get $r) (i64.shr_u (local.get $v) (i64.const 56))))
592
+ (local.get $r))`
593
+ const bswap64I64 = (v) => { inc('__bswap64'); return ['call', '$__bswap64', v] }
594
+
595
+ // DV float reads return raw f64 bits, which may be non-canonical NaN (sign-flipped
596
+ // or with payload). jz's `__eq` fast path only treats canonical NaN (0x7FF8…0000)
597
+ // as NaN, so non-canonical NaN would break `v !== v` semantics. Canonicalize here
598
+ // (cheap: 4 wasm ops) so `getFloat32`/`getFloat64` return a real-spec NaN value.
599
+ ctx.core.stdlib['__canon_nan'] = `(func $__canon_nan (param $v f64) (result f64)
600
+ (select
601
+ (f64.reinterpret_i64 (i64.const 0x7FF8000000000000))
602
+ (local.get $v)
603
+ (f64.ne (local.get $v) (local.get $v))))`
604
+ const canonNaN = (vIR) => { inc('__canon_nan'); return typed(['call', '$__canon_nan', vIR], 'f64') }
605
+
606
+ // ToIndex for DataView byte offsets (spec 25.1.2.x getViewValue/setViewValue):
607
+ // ToIntegerOrInfinity then reject negatives and >2^53-1 with a RangeError. The
608
+ // throw rides jz's $__jz_err tag so a user `try/catch` (or assert.throws) sees it.
609
+ // trunc_sat keeps a huge-but-passing index from trapping the conversion — the
610
+ // subsequent memory access fails the same way an oversized index always has.
611
+ ctx.core.stdlib['__dv_index'] = `(func $__dv_index (param $idx f64) (result i32)
612
+ (if (f64.ne (local.get $idx) (local.get $idx)) (then (local.set $idx (f64.const 0))))
613
+ (local.set $idx (f64.trunc (local.get $idx)))
614
+ (if (i32.or
615
+ (f64.lt (local.get $idx) (f64.const 0))
616
+ (f64.gt (local.get $idx) (f64.const 9007199254740991)))
617
+ (then (throw $__jz_err (f64.const 0))))
618
+ (i32.trunc_sat_f64_s (local.get $idx)))`
619
+
620
+ // ToIndex the DV byte offset, throwing RangeError on a negative/oversized value.
621
+ // The offset is ToNumber-coerced first (per ToIndex) so a Symbol byte offset
622
+ // raises a TypeError instead of silently truncating to 0.
623
+ const dvIndex = (offNode) => {
624
+ ctx.runtime.throws = true
625
+ inc('__dv_index')
626
+ return typed(['call', '$__dv_index', toNumF64(offNode, emit(offNode))], 'i32')
627
+ }
628
+
629
+ // A DataView receiver resolves to its 16-byte descriptor pointer
630
+ // [byteLen:i32][dataOff:i32][parentOff:i32]. get/set hoist this once, then
631
+ // read dataOff (desc[4]) for the access address and byteLen (desc[0]) as the
632
+ // view size for the bounds check.
633
+ const dvDescriptor = (dv) => ptrOffsetIR(emit(dv), VAL.TYPED)
634
+ const dvDataOff = (descLocal) => ['i32.load', ['i32.add', ['local.get', `$${descLocal}`], ['i32.const', 4]]]
635
+ const dvViewSize = (descLocal) => ['i32.load', ['local.get', `$${descLocal}`]]
636
+
637
+ // ToIndex the DV byte offset, then bounds-check `getIndex + elementSize > viewSize`
638
+ // (spec 25.3.1.1 GetViewValue / SetViewValue step 13) — a RangeError when the
639
+ // access would run past the view. elementSize moves to the static side of the
640
+ // compare so a saturated/huge index can't overflow the i32 addition. viewSize is
641
+ // read live from the descriptor, so the check holds for every DataView receiver.
642
+ const dvIndexChecked = (offNode, size, viewSize) => {
643
+ ctx.runtime.throws = true
644
+ const idxT = tempI32('dvb')
645
+ return typed(['block', ['result', 'i32'],
646
+ ['local.set', `$${idxT}`, dvIndex(offNode)],
647
+ ['if', ['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]],
648
+ ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
649
+ ['local.get', `$${idxT}`]], 'i32')
650
+ }
651
+
652
+ // DataView set methods: extract i32 offset from f64 ptr, optionally byte-swap, store.
653
+ // 8-bit ops ignore the LE arg entirely (single byte — no swap possible).
474
654
  const DV_SET = {
475
- setInt8: 'i32.store8', setUint8: 'i32.store8',
476
- setInt16: 'i32.store16', setUint16: 'i32.store16',
477
- setInt32: 'i32.store', setUint32: 'i32.store',
478
- setFloat32: 'f32.store', setFloat64: 'f64.store',
479
- setBigInt64: 'i64.store', setBigUint64: 'i64.store',
480
- }
481
- for (const [method, storeOp] of Object.entries(DV_SET)) {
482
- ctx.core.emit[`.${method}`] = (dv, off, val, _le) => {
483
- const dvOff = ptrOffsetIR(emit(dv), VAL.BUFFER)
484
- const addr = ['i32.add', dvOff, asI32(emit(off))]
485
- let v = emit(val)
486
- if (method.includes('BigInt') || method.includes('BigUint'))
487
- v = typed(['i64.reinterpret_f64', asF64(v)], 'i64')
488
- else if (method.includes('Float64')) v = asF64(v)
489
- else if (method.includes('Float32')) v = typed(['f32.demote_f64', asF64(v)], 'f32')
490
- else v = asI32(v)
491
- return [storeOp, addr, v]
655
+ setInt8: ['i32.store8', 'i32', 1], setUint8: ['i32.store8', 'i32', 1],
656
+ setInt16: ['i32.store16', 'i32', 2], setUint16: ['i32.store16', 'i32', 2],
657
+ setInt32: ['i32.store', 'i32', 4], setUint32: ['i32.store', 'i32', 4],
658
+ setFloat32: ['f32.store', 'f32', 4], setFloat64: ['f64.store', 'f64', 8],
659
+ setBigInt64: ['i64.store', 'i64', 8], setBigUint64: ['i64.store', 'i64', 8],
660
+ }
661
+ for (const [method, [storeOp, valType, size]] of Object.entries(DV_SET)) {
662
+ ctx.core.emit[`.${method}`] = (dv, off, val, leNode) => {
663
+ // Resolve the receiver's descriptor once; the store reads dataOff/byteLen from it.
664
+ const desc = tempI32('dvD')
665
+ const fin = (body) => ['block', ['local.set', `$${desc}`, dvDescriptor(dv)], body]
666
+ const addr = ['i32.add', dvDataOff(desc), dvIndexChecked(off, size, dvViewSize(desc))]
667
+ // Coerce value into the wasm value type the store op consumes. Non-BigInt
668
+ // stores ToNumber the value first (per SetViewValue) so a Symbol value
669
+ // raises a TypeError and a string value parses instead of truncating to 0.
670
+ let v
671
+ if (valType === 'i64') v = typed(['i64.reinterpret_f64', asF64(emit(val))], 'i64')
672
+ else if (valType === 'f64') v = asF64(toNumF64(val, emit(val)))
673
+ else if (valType === 'f32') v = typed(['f32.demote_f64', asF64(toNumF64(val, emit(val)))], 'f32')
674
+ else v = asI32(toNumF64(val, emit(val)))
675
+
676
+ if (size === 1) return fin([storeOp, addr, v])
677
+
678
+ // For BE we byte-swap the integer payload; floats route through bitcast i↔f.
679
+ const swap = (iVal) => size === 2 ? bswap16I32(iVal) : size === 4 ? bswap32I32(iVal) : bswap64I64(iVal)
680
+ const beStore = () => {
681
+ if (valType === 'f32') {
682
+ const swapped = typed(['f32.reinterpret_i32', swap(typed(['i32.reinterpret_f32', v], 'i32'))], 'f32')
683
+ return [storeOp, addr, swapped]
684
+ }
685
+ if (valType === 'f64') {
686
+ const swapped = typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.reinterpret_f64', v], 'i64'))], 'f64')
687
+ return [storeOp, addr, swapped]
688
+ }
689
+ return [storeOp, addr, swap(v)]
690
+ }
691
+
692
+ const le = staticLE(leNode)
693
+ if (le === true) return fin([storeOp, addr, v])
694
+ if (le === false) return fin(beStore())
695
+ // Dynamic: hoist value + addr into temps so each branch can re-use.
696
+ const aT = tempI32('dvsa')
697
+ const vLoc = valType === 'i64' ? tempI64('dvsv') : valType === 'i32' ? tempI32('dvsv') : temp('dvsv')
698
+ // Re-declare with correct type for f32 (temp() defaults to f64).
699
+ if (valType === 'f32') ctx.func.locals.set(vLoc, 'f32')
700
+ const leT = tempI32('dvsle')
701
+ return fin(['block',
702
+ ['local.set', `$${aT}`, addr],
703
+ ['local.set', `$${vLoc}`, v],
704
+ ['local.set', `$${leT}`, truthyIR(emit(leNode))],
705
+ ['if',
706
+ ['local.get', `$${leT}`],
707
+ ['then', [storeOp, ['local.get', `$${aT}`], typed(['local.get', `$${vLoc}`], valType)]],
708
+ ['else', (() => {
709
+ const refV = typed(['local.get', `$${vLoc}`], valType)
710
+ if (valType === 'f32') return [storeOp, ['local.get', `$${aT}`], typed(['f32.reinterpret_i32', swap(typed(['i32.reinterpret_f32', refV], 'i32'))], 'f32')]
711
+ if (valType === 'f64') return [storeOp, ['local.get', `$${aT}`], typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.reinterpret_f64', refV], 'i64'))], 'f64')]
712
+ return [storeOp, ['local.get', `$${aT}`], swap(refV)]
713
+ })()]]])
492
714
  }
493
715
  }
494
716
 
495
- // DataView get methods: extract i32 offset, load value, return as f64
717
+ // DataView get methods: extract i32 offset, load value, optionally byte-swap, return as f64.
718
+ // 8-bit ops ignore the LE arg entirely (single byte).
496
719
  const DV_GET = {
497
- getInt8: ['i32.load8_s', 'i32'], getUint8: ['i32.load8_u', 'i32'],
498
- getInt16: ['i32.load16_s', 'i32'], getUint16: ['i32.load16_u', 'i32'],
499
- getInt32: ['i32.load', 'i32'], getUint32: ['i32.load', 'i32'],
500
- getFloat32: ['f32.load', 'f32'], getFloat64: ['f64.load', 'f64'],
501
- getBigInt64: ['i64.load', 'i64'], getBigUint64: ['i64.load', 'i64'],
502
- }
503
- for (const [method, [loadOp, resultType]] of Object.entries(DV_GET)) {
504
- ctx.core.emit[`.${method}`] = (dv, off, _le) => {
505
- const addr = ['i32.add', ptrOffsetIR(emit(dv), VAL.BUFFER), asI32(emit(off))]
506
- const raw = typed([loadOp, addr], resultType)
507
- if (resultType === 'f64') return raw
508
- if (resultType === 'f32') return typed(['f64.promote_f32', raw], 'f64')
509
- if (resultType === 'i64') return typed(['f64.reinterpret_i64', raw], 'f64')
510
- return typed(['f64.convert_i32_s', raw], 'f64')
720
+ getInt8: ['i32.load8_s', 'i32', 1, true], getUint8: ['i32.load8_u', 'i32', 1, false],
721
+ getInt16: ['i32.load16_s', 'i32', 2, true], getUint16: ['i32.load16_u', 'i32', 2, false],
722
+ getInt32: ['i32.load', 'i32', 4, true], getUint32: ['i32.load', 'i32', 4, false],
723
+ getFloat32: ['f32.load', 'f32', 4, false], getFloat64: ['f64.load', 'f64', 8, false],
724
+ getBigInt64: ['i64.load', 'i64', 8, true], getBigUint64: ['i64.load', 'i64', 8, false],
725
+ }
726
+ for (const [method, [loadOp, resultType, size, signed]] of Object.entries(DV_GET)) {
727
+ ctx.core.emit[`.${method}`] = (dv, off, leNode) => {
728
+ // Resolve the receiver's descriptor once; the load reads dataOff/byteLen from it.
729
+ const desc = tempI32('dvD')
730
+ const fin = (body) => typed(['block', ['result', 'f64'],
731
+ ['local.set', `$${desc}`, dvDescriptor(dv)], asF64(body)], 'f64')
732
+ const addr = ['i32.add', dvDataOff(desc), dvIndexChecked(off, size, dvViewSize(desc))]
733
+
734
+ // Convert a wasm-typed raw value back into the f64 ABI return. Float reads
735
+ // canonicalize NaN (see __canon_nan) so downstream `v !== v` works.
736
+ const toF64 = (raw) => {
737
+ if (resultType === 'f64') return canonNaN(raw)
738
+ if (resultType === 'f32') return canonNaN(typed(['f64.promote_f32', raw], 'f64'))
739
+ if (resultType === 'i64') return typed(['f64.reinterpret_i64', raw], 'f64')
740
+ return typed(signed ? ['f64.convert_i32_s', raw] : ['f64.convert_i32_u', raw], 'f64')
741
+ }
742
+
743
+ if (size === 1) return fin(toF64(typed([loadOp, addr], resultType)))
744
+
745
+ // LE path: native wasm load is already little-endian.
746
+ // BE path: load as raw int (always little-endian on wasm), byte-swap, then
747
+ // reinterpret to the requested type so sign-extension matches.
748
+ // For unsigned 16-bit, bswap16 only spans the low half — no extra masking needed.
749
+ // For signed 16-bit BE, swap then sign-extend via `i32.extend16_s`.
750
+ const beLoad = () => {
751
+ if (size === 2) {
752
+ const rawU = bswap16I32(typed(['i32.load16_u', addr], 'i32'))
753
+ return toF64(typed(signed ? ['i32.extend16_s', rawU] : rawU, 'i32'))
754
+ }
755
+ if (size === 4) {
756
+ if (resultType === 'f32') {
757
+ return toF64(typed(['f32.reinterpret_i32', bswap32I32(typed(['i32.load', addr], 'i32'))], 'f32'))
758
+ }
759
+ return toF64(typed(bswap32I32(typed(['i32.load', addr], 'i32')), 'i32'))
760
+ }
761
+ // size === 8 (f64 or i64).
762
+ if (resultType === 'f64') {
763
+ return toF64(typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', addr], 'i64'))], 'f64'))
764
+ }
765
+ return typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', addr], 'i64'))], 'f64')
766
+ }
767
+
768
+ const le = staticLE(leNode)
769
+ if (le === true) return fin(toF64(typed([loadOp, addr], resultType)))
770
+ if (le === false) return fin(beLoad())
771
+ // Dynamic LE: hoist addr, branch on leNode at runtime.
772
+ const aT = tempI32('dvga')
773
+ const leT = tempI32('dvgle')
774
+ return fin(typed(['block', ['result', 'f64'],
775
+ ['local.set', `$${aT}`, addr],
776
+ ['local.set', `$${leT}`, truthyIR(emit(leNode))],
777
+ ['if', ['result', 'f64'],
778
+ ['local.get', `$${leT}`],
779
+ ['then', asF64(toF64(typed([loadOp, ['local.get', `$${aT}`]], resultType)))],
780
+ ['else', asF64((() => {
781
+ // Replicate beLoad but using hoisted addr.
782
+ const a = ['local.get', `$${aT}`]
783
+ if (size === 2) {
784
+ const rawU = bswap16I32(typed(['i32.load16_u', a], 'i32'))
785
+ return toF64(typed(signed ? ['i32.extend16_s', rawU] : rawU, 'i32'))
786
+ }
787
+ if (size === 4) {
788
+ if (resultType === 'f32') {
789
+ return toF64(typed(['f32.reinterpret_i32', bswap32I32(typed(['i32.load', a], 'i32'))], 'f32'))
790
+ }
791
+ return toF64(typed(bswap32I32(typed(['i32.load', a], 'i32')), 'i32'))
792
+ }
793
+ if (resultType === 'f64') {
794
+ return toF64(typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', a], 'i64'))], 'f64'))
795
+ }
796
+ return typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', a], 'i64'))], 'f64')
797
+ })())]]], 'f64'))
511
798
  }
512
799
  }
513
800
 
@@ -550,13 +837,27 @@ export default (ctx) => {
550
837
 
551
838
  // .length handled by ptr.js's __len (reads from memory header [-8:len])
552
839
 
553
- /** Resolve element type + view-ness for a known TypedArray variable.
554
- * Returns { et, isView, isBigInt } or null. */
840
+ /** Resolve element type + view-ness for a known TypedArray expression.
841
+ * Returns { et, isView, isBigInt } or null. Handles:
842
+ * - bare binding: `xs` (in typedElem)
843
+ * - element-preserving method chain: `xs.filter(...)` / `xs.map(...)` /
844
+ * `xs.slice(...)` — walks back to the root binding. View-ness clears
845
+ * at the chain output (the result is always an owned typed array). */
846
+ const TYPED_CHAIN_METHODS = new Set(['map', 'filter', 'slice'])
555
847
  const resolveElem = (arr) => {
556
- const ctor = typeof arr === 'string' && ctx.types.typedElem?.get(arr)
848
+ let receiver = arr, chainOutput = false
849
+ // Walk method-call chain inward. `arr.method(...)` parses as
850
+ // ['()', ['.', recv, 'method'], ...args] — peel until we hit a name.
851
+ while (Array.isArray(receiver) && receiver[0] === '()' &&
852
+ Array.isArray(receiver[1]) && receiver[1][0] === '.' &&
853
+ TYPED_CHAIN_METHODS.has(receiver[1][2])) {
854
+ receiver = receiver[1][1]
855
+ chainOutput = true
856
+ }
857
+ const ctor = typeof receiver === 'string' && ctx.types.typedElem?.get(receiver)
557
858
  if (!ctor) return null
558
- const isView = ctor.endsWith('.view')
559
- const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
859
+ const isView = !chainOutput && ctor.endsWith('.view')
860
+ const name = ctor.endsWith('.view') ? ctor.slice(4, -5) : ctor.slice(4)
560
861
  const et = ELEM[name]
561
862
  return et == null ? null : { et, isView, isBigInt: name === 'BigInt64Array' || name === 'BigUint64Array' }
562
863
  }
@@ -782,11 +1083,22 @@ export default (ctx) => {
782
1083
 
783
1084
  // .map() on TypedArrays — SIMD auto-vectorization when pattern detected
784
1085
  ctx.core.emit['.typed:map'] = (arr, fn) => {
785
- // Resolve element type + view-ness from variable tracking
786
- const ctor = typeof arr === 'string' && ctx.types.typedElem?.get(arr)
787
- const isView = ctor?.endsWith('.view')
788
- const elemName = isView ? ctor.slice(4, -5) : ctor?.slice(4)
789
- const elemType = elemName && ELEM[elemName]
1086
+ // Resolve element type + view-ness. `resolveElem` handles bare bindings AND
1087
+ // chained method receivers (`xs.filter(…).map(…)`) by walking back to the
1088
+ // root typedElem-tracked binding.
1089
+ const r = resolveElem(arr)
1090
+ const elemType = r?.et
1091
+ const isView = r?.isView
1092
+ const elemName = r && (r.isBigInt
1093
+ ? (elemType === 7 ? 'BigInt64Array' : 'BigUint64Array') // unreachable: SIMD path gated below
1094
+ : ['Int8Array','Uint8Array','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array'][elemType])
1095
+
1096
+ // BigInt typed arrays: SIMD path doesn't support them; defer to scalar map.
1097
+ if (r?.isBigInt) {
1098
+ // Fall through to generic .map below — keeps i64-via-NaN-box semantics intact.
1099
+ if (ctx.core.emit['.map']) return ctx.core.emit['.map'](arr, fn)
1100
+ return null
1101
+ }
790
1102
 
791
1103
  // Try SIMD: inline arrow with recognizable pattern
792
1104
  if (elemType != null && Array.isArray(fn) && fn[0] === '=>') {
@@ -846,4 +1158,309 @@ export default (ctx) => {
846
1158
  if (ctx.core.emit['.map']) return ctx.core.emit['.map'](arr, fn)
847
1159
  return null
848
1160
  }
1161
+
1162
+ // === Shared typed iteration core ===
1163
+ //
1164
+ // typedLoop(arr, bodyFn, opts?) emits the common shape every typed iteration
1165
+ // method needs: resolve elemType from the receiver's tracked ctor, set up
1166
+ // (ptr, len, i) locals, walk i in [0, len), per iteration load arr[i] as f64
1167
+ // and pass it to bodyFn. Returns the IR setup statements. Returns null if the
1168
+ // element type can't be resolved — callers fall back to the generic array
1169
+ // emitter.
1170
+ //
1171
+ // bodyFn(loadElem, i, len, ptr, exitLabel) returns IR statements. loadElem is
1172
+ // a function (called lazily so it isn't materialized for unused-item paths)
1173
+ // returning f64 IR for the current element. `exitLabel` lets the body break
1174
+ // out of the loop (e.g. `.find` after a hit, `.some`/`.every` on early
1175
+ // resolution). `i`/`len`/`ptr` are i32 local-name strings.
1176
+ const typedLoop = (arr, bodyFn) => {
1177
+ const r = resolveElem(arr)
1178
+ if (!r) return null
1179
+ const { et, isView, isBigInt } = r
1180
+ if (isBigInt) return null // BigInt: defer to generic .map (returns BigInts via f64-bits NaN-box)
1181
+ const va = emit(arr)
1182
+ const len = tempI32('tll'), ptr = tempI32('tlp'), i = tempI32('tli')
1183
+ const id = ctx.func.uniq++
1184
+ const exit = `$brk${id}`
1185
+ inc('__len')
1186
+ const loadElem = () => {
1187
+ const off = ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', SHIFT[et]]]]
1188
+ if (et === 7) return typed(['f64.load', off], 'f64')
1189
+ if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
1190
+ return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
1191
+ }
1192
+ const setup = [
1193
+ ['local.set', `$${ptr}`, typedDataAddr(asF64(va), isView)],
1194
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', asF64(va)]]],
1195
+ ['local.set', `$${i}`, ['i32.const', 0]],
1196
+ ['block', exit, ['loop', `$loop${id}`,
1197
+ ['br_if', exit, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
1198
+ ...bodyFn(loadElem, i, len, ptr, exit),
1199
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1200
+ ['br', `$loop${id}`]]]]
1201
+ return { setup, ptr, len, i, exit, et, isView, loadElem }
1202
+ }
1203
+
1204
+ // === Typed iteration emitters ===
1205
+ //
1206
+ // Each emitter dispatches via the `.typed:method` key on `ctx.core.emit`.
1207
+ // emit.js:2211 picks `.typed:<m>` over `.${m}` when the receiver's val type
1208
+ // is VAL.TYPED ('typed'), so a user-visible `arr.forEach(...)` on a tracked
1209
+ // typed-array binding routes here automatically.
1210
+ //
1211
+ // Calling convention: callbacks receive (item, idx) — matches array.js
1212
+ // (omits the `arr` arg that array-method spec passes; jz keeps the closure
1213
+ // ABI width at 2 to spare a slot across the whole program). Reduce passes
1214
+ // (acc, item). Closure invocation goes through `ctx.closure.call` directly.
1215
+ // The element-type-name list is needed by allocPtr for typedAux:
1216
+ const ET_NAME = ['Int8Array','Uint8Array','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array']
1217
+
1218
+ // .forEach: callback (item, idx). Result is 0 to match array.js's
1219
+ // convention (spec says undefined; both modules pick 0 since f() exposes the
1220
+ // result as f64 and NaN-boxed undef reads as NaN).
1221
+ // Pre-allocate locals BEFORE typedLoop — bodyFn captures `cbLoc` by closure;
1222
+ // TDZ would fire if we declared it after.
1223
+ ctx.core.emit['.typed:forEach'] = (arr, fn) => {
1224
+ const cbLoc = temp('tfc')
1225
+ const loop = typedLoop(arr, (load, i) => [
1226
+ ['drop', asF64(ctx.closure.call(
1227
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1228
+ [load(), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))]
1229
+ ])
1230
+ if (!loop) return null
1231
+ return typed(['block', ['result', 'f64'],
1232
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1233
+ ...loop.setup,
1234
+ ['f64.const', 0]], 'f64')
1235
+ }
1236
+
1237
+ // .reduce: callback (acc, item) → acc. Without init, slot 0 seeds acc and
1238
+ // the callback skips on iteration 0. Matches JS semantics; the (idx, arr)
1239
+ // callback args are dropped (consistent with array.js).
1240
+ ctx.core.emit['.typed:reduce'] = (arr, fn, init) => {
1241
+ const cbLoc = temp('trc'), acc = temp('trv'), seeded = init !== undefined
1242
+ const loop = typedLoop(arr, (load, i) => {
1243
+ const step = ['local.set', `$${acc}`, asF64(ctx.closure.call(
1244
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1245
+ [typed(['local.get', `$${acc}`], 'f64'), load()]))]
1246
+ if (seeded) return [step]
1247
+ // Unseeded: iteration 0 just stashes the value into acc.
1248
+ return [
1249
+ ['if', ['i32.eqz', ['local.get', `$${i}`]],
1250
+ ['then', ['local.set', `$${acc}`, load()]],
1251
+ ['else', step]]]
1252
+ })
1253
+ if (!loop) return null
1254
+ return typed(['block', ['result', 'f64'],
1255
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1256
+ ['local.set', `$${acc}`, seeded ? asF64(emit(init)) : undefExpr()],
1257
+ ...loop.setup,
1258
+ ['local.get', `$${acc}`]], 'f64')
1259
+ }
1260
+
1261
+ // .indexOf: scalar value-equality search. Returns -1 on miss. Compare on f64
1262
+ // — Array.prototype.indexOf uses strict equality (NaN ≠ NaN).
1263
+ ctx.core.emit['.typed:indexOf'] = (arr, val) => {
1264
+ const found = tempI32('tif'), needle = temp('tin')
1265
+ const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => [
1266
+ ['if', ['f64.eq', load(), ['local.get', `$${needle}`]],
1267
+ ['then',
1268
+ ['local.set', `$${found}`, ['local.get', `$${i}`]],
1269
+ ['br', exit]]]
1270
+ ])
1271
+ if (!loop) return null
1272
+ return typed(['block', ['result', 'f64'],
1273
+ ['local.set', `$${needle}`, asF64(emit(val))],
1274
+ ['local.set', `$${found}`, ['i32.const', -1]],
1275
+ ...loop.setup,
1276
+ ['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
1277
+ }
1278
+
1279
+ // .includes: like indexOf but NaN-equal-NaN (JS spec). Stash needle bits as
1280
+ // i64 and compare via i64.eq so two NaNs with matching bit patterns match
1281
+ // (f64.eq would say false).
1282
+ ctx.core.emit['.typed:includes'] = (arr, val) => {
1283
+ const found = tempI32('thf'), needle = temp('thn')
1284
+ const loop = typedLoop(arr, (load, _i, _len, _ptr, exit) => [
1285
+ ['if',
1286
+ ['i32.or',
1287
+ ['f64.eq', load(), ['local.get', `$${needle}`]],
1288
+ ['i64.eq',
1289
+ ['i64.reinterpret_f64', load()],
1290
+ ['i64.reinterpret_f64', ['local.get', `$${needle}`]]]],
1291
+ ['then', ['local.set', `$${found}`, ['i32.const', 1]], ['br', exit]]]
1292
+ ])
1293
+ if (!loop) return null
1294
+ return typed(['block', ['result', 'f64'],
1295
+ ['local.set', `$${needle}`, asF64(emit(val))],
1296
+ ['local.set', `$${found}`, ['i32.const', 0]],
1297
+ ...loop.setup,
1298
+ ['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
1299
+ }
1300
+
1301
+ // .find / .findIndex: linear scan, first truthy callback wins. Miss returns
1302
+ // undefined / -1 respectively.
1303
+ const findCommon = (arr, fn, returnIndex) => {
1304
+ const cbLoc = temp('tfc'), result = temp('tfr'), foundIdx = tempI32('tfi')
1305
+ const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => {
1306
+ const itemLoc = temp('tfit')
1307
+ return [
1308
+ ['local.set', `$${itemLoc}`, load()],
1309
+ ['if', truthyIR(ctx.closure.call(
1310
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1311
+ [typed(['local.get', `$${itemLoc}`], 'f64'),
1312
+ typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')])),
1313
+ ['then',
1314
+ returnIndex
1315
+ ? ['local.set', `$${foundIdx}`, ['local.get', `$${i}`]]
1316
+ : ['local.set', `$${result}`, typed(['local.get', `$${itemLoc}`], 'f64')],
1317
+ ['br', exit]]]
1318
+ ]
1319
+ })
1320
+ if (!loop) return null
1321
+ return typed(['block', ['result', 'f64'],
1322
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1323
+ returnIndex
1324
+ ? ['local.set', `$${foundIdx}`, ['i32.const', -1]]
1325
+ : ['local.set', `$${result}`, undefExpr()],
1326
+ ...loop.setup,
1327
+ returnIndex
1328
+ ? typed(['f64.convert_i32_s', ['local.get', `$${foundIdx}`]], 'f64')
1329
+ : typed(['local.get', `$${result}`], 'f64')], 'f64')
1330
+ }
1331
+ ctx.core.emit['.typed:find'] = (arr, fn) => findCommon(arr, fn, false)
1332
+ ctx.core.emit['.typed:findIndex'] = (arr, fn) => findCommon(arr, fn, true)
1333
+
1334
+ // .some / .every: short-circuit boolean reduction. some=∃, every=∀.
1335
+ const anyAllCommon = (arr, fn, isEvery) => {
1336
+ const cbLoc = temp('tac'), result = tempI32('tar')
1337
+ const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => {
1338
+ const test = truthyIR(ctx.closure.call(
1339
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1340
+ [load(), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))
1341
+ // every: exit on falsy with result=0. some: exit on truthy with result=1.
1342
+ return [
1343
+ ['if', isEvery ? ['i32.eqz', test] : test,
1344
+ ['then', ['local.set', `$${result}`, ['i32.const', isEvery ? 0 : 1]], ['br', exit]]]
1345
+ ]
1346
+ })
1347
+ if (!loop) return null
1348
+ return typed(['block', ['result', 'f64'],
1349
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1350
+ ['local.set', `$${result}`, ['i32.const', isEvery ? 1 : 0]],
1351
+ ...loop.setup,
1352
+ ['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
1353
+ }
1354
+ ctx.core.emit['.typed:some'] = (arr, fn) => anyAllCommon(arr, fn, false)
1355
+ ctx.core.emit['.typed:every'] = (arr, fn) => anyAllCommon(arr, fn, true)
1356
+
1357
+ // .filter: produces a TYPED array of the same element type. Allocates worst-
1358
+ // case (len slots) then patches the byte-count header at the end with the
1359
+ // actual passed count. Mirrors .filter in array.js but with typed-aware
1360
+ // load/store.
1361
+ ctx.core.emit['.typed:filter'] = (arr, fn) => {
1362
+ const r = resolveElem(arr)
1363
+ if (!r || r.isBigInt) return null
1364
+ const { et, isView } = r
1365
+ const cbLoc = temp('tfc'), arrLoc = temp('tfa')
1366
+ const count = tempI32('tfn'), maxLen = tempI32('tfm')
1367
+ const srcPtr = tempI32('tfsp'), srcLen = tempI32('tfsl'), srci = tempI32('tfi')
1368
+ inc('__len')
1369
+ const loadAt = (ptrLoc, iLoc) => {
1370
+ const off = ['i32.add', ['local.get', `$${ptrLoc}`], ['i32.shl', ['local.get', `$${iLoc}`], ['i32.const', SHIFT[et]]]]
1371
+ if (et === 7) return typed(['f64.load', off], 'f64')
1372
+ if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
1373
+ return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
1374
+ }
1375
+ const storeAt = (ptrLoc, iLoc, valF64) => {
1376
+ const off = ['i32.add', ['local.get', `$${ptrLoc}`], ['i32.shl', ['local.get', `$${iLoc}`], ['i32.const', SHIFT[et]]]]
1377
+ if (et === 7) return ['f64.store', off, valF64]
1378
+ if (et === 6) return ['f32.store', off, ['f32.demote_f64', valF64]]
1379
+ return [STORE[et], off, [(et & 1) ? 'i32.trunc_f64_u' : 'i32.trunc_f64_s', valF64]]
1380
+ }
1381
+ const dst = allocPtr({ type: PTR.TYPED, aux: typedAux(ET_NAME[et]),
1382
+ len: ['i32.shl', ['local.get', `$${maxLen}`], ['i32.const', SHIFT[et]]],
1383
+ stride: 1, tag: 'tfd' })
1384
+ const id = ctx.func.uniq++
1385
+ const passes = truthyIR(ctx.closure.call(
1386
+ typed(['local.get', `$${cbLoc}`], 'f64'),
1387
+ [loadAt(srcPtr, srci), typed(['f64.convert_i32_s', ['local.get', `$${srci}`]], 'f64')]))
1388
+ return typed(['block', ['result', 'f64'],
1389
+ ['local.set', `$${cbLoc}`, asF64(emit(fn))],
1390
+ ['local.set', `$${arrLoc}`, asF64(emit(arr))],
1391
+ ['local.set', `$${srcPtr}`, typedDataAddr(typed(['local.get', `$${arrLoc}`], 'f64'), isView)],
1392
+ ['local.set', `$${srcLen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrLoc}`]]]],
1393
+ ['local.set', `$${maxLen}`, ['local.get', `$${srcLen}`]],
1394
+ dst.init,
1395
+ ['local.set', `$${count}`, ['i32.const', 0]],
1396
+ ['local.set', `$${srci}`, ['i32.const', 0]],
1397
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
1398
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${srci}`], ['local.get', `$${srcLen}`]]],
1399
+ ['if', passes,
1400
+ ['then',
1401
+ storeAt(dst.local, count, loadAt(srcPtr, srci)),
1402
+ ['local.set', `$${count}`, ['i32.add', ['local.get', `$${count}`], ['i32.const', 1]]]]],
1403
+ ['local.set', `$${srci}`, ['i32.add', ['local.get', `$${srci}`], ['i32.const', 1]]],
1404
+ ['br', `$loop${id}`]]],
1405
+ // Patch byte-count header — __len reads from -8 then __typed_shift's by
1406
+ // the typed-elem stride, so we store `count * stride` bytes here.
1407
+ ['i32.store', ['i32.sub', ['local.get', `$${dst.local}`], ['i32.const', 8]],
1408
+ ['i32.shl', ['local.get', `$${count}`], ['i32.const', SHIFT[et]]]],
1409
+ dst.ptr], 'f64')
1410
+ }
1411
+
1412
+ // .slice(start, end): produces TYPED array of same element type. Mirrors JS
1413
+ // semantics — negative indices wrap from end, out-of-range clamps to len.
1414
+ // Bulk copy via `memory.copy`.
1415
+ ctx.core.emit['.typed:slice'] = (arr, start, end) => {
1416
+ const r = resolveElem(arr)
1417
+ if (!r || r.isBigInt) return null
1418
+ const { et, isView } = r
1419
+ const arrLoc = temp('tsa'), srcPtr = tempI32('tssp'), srcLen = tempI32('tssl')
1420
+ const lo = tempI32('tslo'), hi = tempI32('tshi'), n = tempI32('tsn')
1421
+ inc('__len')
1422
+ // ECMAScript ToInteger + clamp: idx := bound; idx := idx<0 ? max(0,idx+len) : min(len,idx).
1423
+ // Select-based to dodge nested if-arity rules.
1424
+ // defaultExpr: when boundExpr is omitted (e.g. arr.slice() / arr.slice(2)),
1425
+ // start defaults to 0 and end defaults to len.
1426
+ const clamp = (boundExpr, fallback, defaultExpr) => {
1427
+ if (boundExpr == null) return defaultExpr
1428
+ const idx = tempI32(fallback)
1429
+ return ['block', ['result', 'i32'],
1430
+ ['local.set', `$${idx}`, asI32(emit(boundExpr))],
1431
+ ['select',
1432
+ // negative branch: max(0, idx + len)
1433
+ ['select',
1434
+ ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]],
1435
+ ['i32.const', 0],
1436
+ ['i32.gt_s', ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]], ['i32.const', 0]]],
1437
+ // non-negative branch: min(len, idx)
1438
+ ['select',
1439
+ ['local.get', `$${srcLen}`],
1440
+ ['local.get', `$${idx}`],
1441
+ ['i32.gt_s', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]]],
1442
+ ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]]]
1443
+ }
1444
+ const dst = allocPtr({ type: PTR.TYPED, aux: typedAux(ET_NAME[et]),
1445
+ len: ['i32.shl', ['local.get', `$${n}`], ['i32.const', SHIFT[et]]],
1446
+ stride: 1, tag: 'tsd' })
1447
+ return typed(['block', ['result', 'f64'],
1448
+ ['local.set', `$${arrLoc}`, asF64(emit(arr))],
1449
+ ['local.set', `$${srcPtr}`, typedDataAddr(typed(['local.get', `$${arrLoc}`], 'f64'), isView)],
1450
+ ['local.set', `$${srcLen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrLoc}`]]]],
1451
+ ['local.set', `$${lo}`, clamp(start, 'tslo2', ['i32.const', 0])],
1452
+ ['local.set', `$${hi}`, clamp(end, 'tshi2', ['local.get', `$${srcLen}`])],
1453
+ ['local.set', `$${n}`,
1454
+ ['select',
1455
+ ['i32.sub', ['local.get', `$${hi}`], ['local.get', `$${lo}`]],
1456
+ ['i32.const', 0],
1457
+ ['i32.gt_s', ['local.get', `$${hi}`], ['local.get', `$${lo}`]]]],
1458
+ dst.init,
1459
+ // memory.copy dst, src+lo*stride, n*stride
1460
+ ['memory.copy',
1461
+ ['local.get', `$${dst.local}`],
1462
+ ['i32.add', ['local.get', `$${srcPtr}`], ['i32.shl', ['local.get', `$${lo}`], ['i32.const', SHIFT[et]]]],
1463
+ ['i32.shl', ['local.get', `$${n}`], ['i32.const', SHIFT[et]]]],
1464
+ dst.ptr], 'f64')
1465
+ }
849
1466
  }