jz 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +281 -142
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +461 -185
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +591 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +600 -205
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/array.js CHANGED
@@ -8,11 +8,15 @@
8
8
  * @module array
9
9
  */
10
10
 
11
- import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr } from '../src/ir.js'
12
- import { emit, buildArrayWithSpreads } from '../src/emit.js'
13
- import { valTypeOf, lookupValType, lookupNotString, VAL, extractParams, updateRep, staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/analyze.js'
11
+ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr, ptrTypeEq } from '../src/ir.js'
12
+ import { inBoundsArrIdx } from '../src/type.js'
13
+ import { emit, spread, deps } from '../src/bridge.js'
14
+ import { valTypeOf } from '../src/kind.js'
15
+ import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
16
+ import { staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/static.js'
17
+ import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
14
18
  import { structInline } from '../src/abi/index.js'
15
- import { ctx, inc, err, PTR, LAYOUT } from '../src/ctx.js'
19
+ import { ctx, inc, err, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
16
20
  import { strHashLiteral } from './collection.js'
17
21
 
18
22
 
@@ -22,17 +26,23 @@ function allocArray(len, cap) {
22
26
  return { local: a.local, setup: [a.init], ptr: a.ptr }
23
27
  }
24
28
 
25
- /** Pack literal i64 slots as a static ARRAY: writes [len][cap][slots...] into the data segment
26
- * and returns a folded ARRAY pointer to the first slot. */
29
+ /** Pack literal i64 slots as a static ARRAY into the data segment, returning a
30
+ * folded ARRAY pointer to the first slot. The 16-byte header MUST match
31
+ * __alloc_hdr (core.js): a zeroed dyn-props word at off-16, then len/cap at
32
+ * off-8/-4. Heap arrays get that props word zeroed for free; a static array with
33
+ * only an 8-byte header left off-16 pointing at adjacent data-segment bytes, so
34
+ * for-in / named-prop lookup (which read off-16 as the props-sidecar pointer)
35
+ * walked garbage → OOB (test262 built-ins/Object/keys sparse-array). */
27
36
  function staticArrayPtr(slots) {
28
37
  if (!ctx.runtime.data) ctx.runtime.data = ''
29
38
  while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
30
39
  const headerOff = ctx.runtime.data.length
31
40
  const len = slots.length
32
- const hdr = new Uint8Array(8); new DataView(hdr.buffer).setInt32(0, len, true); new DataView(hdr.buffer).setInt32(4, len, true)
33
- for (let i = 0; i < 8; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
41
+ const hdr = new Uint8Array(16); const dv = new DataView(hdr.buffer)
42
+ dv.setInt32(8, len, true); dv.setInt32(12, len, true) // off-8: len, off-4: cap (props word at 0..7 stays 0)
43
+ for (let i = 0; i < 16; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
34
44
  appendStaticSlots(slots)
35
- return mkPtrIR(PTR.ARRAY, 0, headerOff + 8)
45
+ return mkPtrIR(PTR.ARRAY, 0, headerOff + 16)
36
46
  }
37
47
 
38
48
  function hoistArrayValue(arr) {
@@ -94,16 +104,8 @@ function substExpr(node, mapping) {
94
104
  return out
95
105
  }
96
106
 
97
- // Check whether a name is referenced inside a pure expression body.
98
- // Mirrors substExpr's traversal — skips property names on '.'/'?.' and object keys on ':'.
99
107
  function exprUses(node, name) {
100
- if (typeof node === 'string') return node === name
101
- if (!Array.isArray(node)) return false
102
- const op = node[0]
103
- if (op === '.' || op === '?.') return exprUses(node[1], name)
104
- if (op === ':') return exprUses(node[2], name)
105
- for (let i = 1; i < node.length; i++) if (exprUses(node[i], name)) return true
106
- return false
108
+ return refsName(node, name, REFS_IN_EXPR)
107
109
  }
108
110
 
109
111
  // Callback factory: returns { setup, call, usedParams } where call(argExprs) emits the invocation.
@@ -239,26 +241,32 @@ const headerPropsToGlobalIR = () => needsArrayDynMove() ? `
239
241
  (global.set $__dyn_props (local.get $root)))))) ` : ''
240
242
 
241
243
  export default (ctx) => {
242
- Object.assign(ctx.core.stdlibDeps, {
243
- __arr_idx: [],
244
+ deps({
245
+ __arr_idx: ['__ptr_offset_fwd'],
244
246
  __arr_grow: arrayGrowDeps(false),
245
247
  __arr_grow_known: arrayGrowDeps(true),
246
248
  __arr_shift: () => [
247
249
  '__ptr_offset',
248
250
  ...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
249
251
  ],
252
+ __arr_fill: ['__ptr_offset'],
250
253
  __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
251
254
  __arr_push1: ['__arr_grow_known', '__ptr_offset'],
255
+ __arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
252
256
  __arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
257
+ __arr_splice: ['__arr_grow', '__len', '__ptr_offset', '__alloc_hdr', '__mkptr'],
253
258
  __typed_idx: () => ctx.features.typedarray || ctx.features.external
254
- ? ['__len']
255
- : ['__len', '__ptr_offset'],
259
+ ? ['__len', '__ptr_offset_fwd']
260
+ : ['__len', '__ptr_offset', '__ptr_offset_fwd'],
261
+ __arr_idx_known: ['__ptr_offset_fwd'],
256
262
  })
257
263
 
258
- // Iteration methods (.map/.filter/.reduce/.forEach/...) invoke callbacks with
259
- // (item, idx) internally closure width must accommodate arity 2 even if no
260
- // source-level closure has that arity.
261
- ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 2)
264
+ // Iteration methods invoke callbacks with an implicit trailing index: .map/
265
+ // .filter/.forEach pass (item, idx); .reduce/.reduceRight pass (acc, item, idx).
266
+ // The uniform closure width must accommodate the widest of these (arity 3) even
267
+ // when no source-level closure declares that arity — otherwise a reduce callback
268
+ // routed through the closure path overflows the call_indirect signature.
269
+ ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 3)
262
270
 
263
271
  inc('__ptr_offset', '__ptr_type', '__len', '__set_len', '__typed_idx', '__is_truthy')
264
272
 
@@ -268,7 +276,7 @@ export default (ctx) => {
268
276
  const t = temp('t')
269
277
  return typed(['i32.and',
270
278
  ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
271
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.ARRAY]]], 'i32')
279
+ ptrTypeEq(['local.get', `$${t}`], PTR.ARRAY)], 'i32')
272
280
  }
273
281
 
274
282
  ctx.core.emit['new.Array'] = (len) => {
@@ -298,13 +306,7 @@ export default (ctx) => {
298
306
  (then (f64.const nan:${UNDEF_NAN}))
299
307
  (else
300
308
  (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
301
- (block $done
302
- (loop $follow
303
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
304
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
305
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
306
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
307
- (br $follow)))
309
+ ${followForwardingWat('$off', { lowGuard: true })}
308
310
  (if (result f64)
309
311
  (i32.and
310
312
  (i32.ge_u (local.get $off) (i32.const 8))
@@ -317,13 +319,7 @@ export default (ctx) => {
317
319
  ctx.core.stdlib['__arr_idx_known'] = `(func $__arr_idx_known (param $ptr i64) (param $i i32) (result f64)
318
320
  (local $off i32)
319
321
  (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
320
- (block $done
321
- (loop $follow
322
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
323
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
324
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
325
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
326
- (br $follow)))
322
+ ${followForwardingWat('$off', { lowGuard: true })}
327
323
  (if (result f64)
328
324
  (i32.and
329
325
  (i32.ge_u (local.get $off) (i32.const 8))
@@ -357,12 +353,7 @@ export default (ctx) => {
357
353
  ;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
358
354
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
359
355
  (then
360
- (block $done
361
- (loop $follow
362
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
363
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
364
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
365
- (br $follow)))
356
+ ${followForwardingWat('$off', { lowGuard: false })}
366
357
  (return (if (result f64)
367
358
  (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)))))
368
359
  (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
@@ -433,7 +424,7 @@ export default (ctx) => {
433
424
  const op = n[0]
434
425
  if (op == null) return true // [null,x] literal — null/number/bigint
435
426
  if (op === '=>') return false // arrow function — callable
436
- if (op === '{}' || op === 'str' || op === 'strcat' || op === '//') return true
427
+ if (op === '{}' || op === 'str' || op === 'strcat' || op === '//' || op === 'bool') return true // object/string/regexp/boolean literal
437
428
  if (op === '[]' && n.length < 3) return true // array literal
438
429
  if (op === '()' && n[1] === 'Symbol') return true // Symbol(...) result
439
430
  return false // calls / member access — unknown
@@ -515,11 +506,14 @@ export default (ctx) => {
515
506
 
516
507
  // Grow array if capacity insufficient. Returns (possibly new) NaN-boxed pointer.
517
508
  // Old storage is left behind as a forwarding header so existing aliases keep
518
- // seeing the current backing store after growth.
519
- ctx.core.stdlib['__arr_grow'] = () => `(func $__arr_grow (param $ptr i64) (param $minCap i32) (result f64)
520
- (local $t i32) (local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
509
+ // seeing the current backing store after growth. `defensive` adds a type/bounds
510
+ // guard (non-array ptr fresh 4-cap buffer) for untyped call sites; the `_known`
511
+ // variant skips it for hot paths that already proved the receiver is an array.
512
+ // Single source of truth: both stdlib entries share the grow/relocate tail.
513
+ const arrGrow = (name, defensive) => `(func $${name} (param $ptr i64) (param $minCap i32) (result f64)
514
+ ${defensive ? '(local $t i32) ' : ''}(local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
521
515
  ${needsArrayDynMove() ? '(local $oldProps f64)' : ''}
522
- (local.set $t (call $__ptr_type (local.get $ptr)))
516
+ ${defensive ? `(local.set $t (call $__ptr_type (local.get $ptr)))
523
517
  (local.set $off (call $__ptr_offset (local.get $ptr)))
524
518
  ;; Defensive path: invalid/non-array pointer -> create fresh array buffer.
525
519
  (if
@@ -529,7 +523,7 @@ export default (ctx) => {
529
523
  (then
530
524
  (local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
531
525
  (local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)))
532
- (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))
526
+ (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))` : `(local.set $off (call $__ptr_offset (local.get $ptr)))`}
533
527
  (local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
534
528
  (if (i32.ge_s (local.get $oldCap) (local.get $minCap))
535
529
  (then (return (f64.reinterpret_i64 (local.get $ptr)))))
@@ -546,25 +540,8 @@ export default (ctx) => {
546
540
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
547
541
  (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
548
542
 
549
- ctx.core.stdlib['__arr_grow_known'] = () => `(func $__arr_grow_known (param $ptr i64) (param $minCap i32) (result f64)
550
- (local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
551
- ${needsArrayDynMove() ? '(local $oldProps f64)' : ''}
552
- (local.set $off (call $__ptr_offset (local.get $ptr)))
553
- (local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
554
- (if (i32.ge_s (local.get $oldCap) (local.get $minCap))
555
- (then (return (f64.reinterpret_i64 (local.get $ptr)))))
556
- (local.set $newCap (select
557
- (local.get $minCap)
558
- (i32.shl (local.get $oldCap) (i32.const 1))
559
- (i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
560
- (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
561
- (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
562
- (memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
563
- ${headerPropsCopyIR()}
564
- ${maybeDynMoveIR()}
565
- (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
566
- (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
567
- (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
543
+ ctx.core.stdlib['__arr_grow'] = () => arrGrow('__arr_grow', true)
544
+ ctx.core.stdlib['__arr_grow_known'] = () => arrGrow('__arr_grow_known', false)
568
545
 
569
546
  // Hot for arr[i] = val (~18M calls in watr self-host). Compute base via __ptr_offset
570
547
  // once and read len from the inline header (i32.load base-8) — avoids __len's separate
@@ -609,6 +586,37 @@ export default (ctx) => {
609
586
  (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
610
587
  (local.get $p))`
611
588
 
589
+ // arr.length = N. Truncation (N ≤ len) just rewrites the len word in place — no
590
+ // relocation, so aliases keep their pointer. Growth past capacity relocates via
591
+ // __arr_grow_known (old header forward-marked) and undefined-fills the new slots,
592
+ // matching JS sparse-array semantics. Non-ARRAY receivers are left unchanged so a
593
+ // mistyped `.length =` cannot corrupt object/collection headers. Returns the
594
+ // (possibly relocated) pointer; the assignment's value is N (computed at the call site).
595
+ ctx.core.stdlib['__arr_set_length'] = `(func $__arr_set_length (param $ptr i64) (param $n i32) (result f64)
596
+ (local $base i32) (local $p f64) (local $oldLen i32) (local $cap i32) (local $k i32)
597
+ (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
598
+ (if (i32.ne (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.ARRAY}))
599
+ (then (return (local.get $p))))
600
+ (if (i32.lt_s (local.get $n) (i32.const 0)) (then (local.set $n (i32.const 0))))
601
+ (local.set $base (call $__ptr_offset (local.get $ptr)))
602
+ (if (i32.lt_u (local.get $base) (i32.const 8)) (then (return (local.get $p))))
603
+ (local.set $oldLen (i32.load (i32.sub (local.get $base) (i32.const 8))))
604
+ (local.set $cap (i32.load (i32.sub (local.get $base) (i32.const 4))))
605
+ (if (i32.gt_s (local.get $n) (local.get $cap))
606
+ (then
607
+ (local.set $p (call $__arr_grow_known (local.get $ptr) (local.get $n)))
608
+ (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
609
+ (if (i32.gt_u (local.get $n) (local.get $oldLen))
610
+ (then
611
+ (local.set $k (local.get $oldLen))
612
+ (block $fdone (loop $fill
613
+ (br_if $fdone (i32.ge_u (local.get $k) (local.get $n)))
614
+ (i64.store (i32.add (local.get $base) (i32.shl (local.get $k) (i32.const 3))) (i64.const ${UNDEF_NAN}))
615
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))
616
+ (br $fill)))))
617
+ (i32.store (i32.sub (local.get $base) (i32.const 8)) (local.get $n))
618
+ (local.get $p))`
619
+
612
620
  // === Array literal ===
613
621
 
614
622
  ctx.core.emit['['] = (...elems) => {
@@ -633,10 +641,10 @@ export default (ctx) => {
633
641
  return typed(['block', ['result', 'f64'], ...body], 'f64')
634
642
  }
635
643
 
636
- // Spread literal: buildArrayWithSpreads pre-sums the total length, allocates
644
+ // Spread literal: spread pre-sums the total length, allocates
637
645
  // exact, and bulk-copies ARRAY sources with a single memory.copy — vs the
638
646
  // per-element __arr_set_idx_ptr grow loop. Normalise the parser's `['...', x]`.
639
- return buildArrayWithSpreads(elems.map(e =>
647
+ return spread(elems.map(e =>
640
648
  Array.isArray(e) && e[0] === '...' ? ['__spread', e[1]] : e))
641
649
  }
642
650
 
@@ -674,8 +682,8 @@ export default (ctx) => {
674
682
  const fi = fo.names.indexOf(litKey)
675
683
  if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
676
684
  }
677
- if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
678
- const slot = ctx.schema.find(arr, litKey)
685
+ if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
686
+ const slot = ctx.schema.slotOf(arr, litKey)
679
687
  if (slot >= 0) {
680
688
  inc('__ptr_offset')
681
689
  return typed(ctx.abi.object.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(arr))]], slot), 'f64')
@@ -776,10 +784,19 @@ export default (ctx) => {
776
784
  // f64.load is correct regardless of elem kind (returns raw f64 for NUMBER,
777
785
  // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
778
786
  // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
779
- // (Array<NUMBER> from `.map(x => x*k)` etc.).
787
+ // (Array<NUMBER> from `.map(x => x*k)` etc.) — both are proven facts from
788
+ // the narrowing fixpoint (carried onto params via paramReps).
780
789
  const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
781
- const hasElemFact = rep?.arrayElemSchema != null
782
- if (hasElemFact && keyIsNum) {
790
+ const hasElemFact = rep?.arrayElemSchema != null || rep?.arrayElemValType != null
791
+ // Take the unchecked inline load ONLY when the index is proven in-bounds by an
792
+ // enclosing canonical loop `for (let i=C; i<arr.length; i++)`. Skipping the bounds
793
+ // check on an arbitrary numeric index is unsound: `a[1]` on a length-1 array would
794
+ // read the raw (uninitialized) cell instead of undefined. Constant / unproven
795
+ // indices fall through to __arr_idx(_known) below, which bounds-checks → UNDEF_NAN.
796
+ const idxProvenInBounds = hasElemFact && keyIsNum
797
+ && typeof arr === 'string' && typeof idx === 'string'
798
+ && inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
799
+ if (idxProvenInBounds) {
783
800
  inc('__ptr_offset')
784
801
  // __ptr_offset returns i32 — base local must be i32 (not the default
785
802
  // f64 NaN-box temp). Flat tee form so downstream peepholes can fold
@@ -790,6 +807,23 @@ export default (ctx) => {
790
807
  ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
791
808
  vi), 'f64')
792
809
  }
810
+ // Known-elem array, numeric key, NOT proven in-bounds → inline bounds-checked
811
+ // load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
812
+ // inline, so watr hoists the loop-invariant len load and CSEs the base — the
813
+ // residual cost is a single (predictable) compare per access, not a call. Skipping
814
+ // the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
815
+ if (hasElemFact && keyIsNum) {
816
+ inc('__ptr_offset')
817
+ const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
818
+ return typed(['if', ['result', 'f64'],
819
+ ['i32.lt_u',
820
+ ['local.tee', `$${idxI32}`, vi],
821
+ ['i32.load', ['i32.sub',
822
+ ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
823
+ ['i32.const', 8]]]],
824
+ ['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
825
+ ['else', undefExpr()]], 'f64')
826
+ }
793
827
  const baseTmp = temp()
794
828
  // Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
795
829
  // arrays don't honor string-key access for numeric keys (keys aren't coerced
@@ -842,7 +876,7 @@ export default (ctx) => {
842
876
  const keyI32 = asI32(typed(keyExpr, 'f64'))
843
877
  if (ctx.module.modules['string'] && !notString) {
844
878
  return ['if', ['result', 'f64'],
845
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
879
+ ptrTypeEq(ptrExpr, PTR.STRING),
846
880
  ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])],
847
881
  ['else', (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])]]
848
882
  }
@@ -852,7 +886,7 @@ export default (ctx) => {
852
886
  if (ctx.module.modules['string'] && !notString)
853
887
  return typed(
854
888
  ['if', ['result', 'f64'],
855
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
889
+ ptrTypeEq(ptrExpr, PTR.STRING),
856
890
  ['then', stringLoad()],
857
891
  ['else', arrayLoad]],
858
892
  'f64')
@@ -1021,6 +1055,42 @@ export default (ctx) => {
1021
1055
  (i32.store (i32.sub (local.get $rawOff) (i32.const 4)) (i32.const -1))))
1022
1056
  (local.get $val))))))`
1023
1057
 
1058
+ // .fill(value, start?, end?) — overwrite [start, end) with value; mutate + return the
1059
+ // array. start/end default to 0 / length and accept negatives (offset from end). The
1060
+ // INT_MAX end sentinel clamps to length in the helper, which also normalizes negatives.
1061
+ ctx.core.emit['.fill'] = (arr, val, start, end) => {
1062
+ inc('__arr_fill')
1063
+ return typed(['call', '$__arr_fill',
1064
+ asI64(emit(arr)),
1065
+ val == null ? undefExpr() : asF64(emit(val)),
1066
+ start == null ? ['i32.const', 0] : asI32(emit(start)),
1067
+ end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
1068
+ }
1069
+
1070
+ ctx.core.stdlib['__arr_fill'] = `(func $__arr_fill (param $arr i64) (param $val f64) (param $start i32) (param $end i32) (result f64)
1071
+ (local $off i32) (local $len i32) (local $i i32)
1072
+ (if (i32.eq
1073
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $arr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
1074
+ (i32.const ${PTR.ARRAY}))
1075
+ (then
1076
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1077
+ (if (i32.ge_u (local.get $off) (i32.const 8))
1078
+ (then
1079
+ (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
1080
+ (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
1081
+ (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.const 0))))
1082
+ (if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
1083
+ (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
1084
+ (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
1085
+ (if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
1086
+ (local.set $i (local.get $start))
1087
+ (block $done (loop $fill
1088
+ (br_if $done (i32.ge_s (local.get $i) (local.get $end)))
1089
+ (f64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (local.get $val))
1090
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1091
+ (br $fill)))))))
1092
+ (f64.reinterpret_i64 (local.get $arr)))`
1093
+
1024
1094
  // .splice(start) | .splice(start, deleteCount) → remove range, return removed as new array
1025
1095
  ctx.core.emit['.splice'] = (arr, start, deleteCount) => {
1026
1096
  const recv = hoistArrayValue(arr)
@@ -1098,59 +1168,111 @@ export default (ctx) => {
1098
1168
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
1099
1169
  (f64.convert_i32_s (i32.add (local.get $len) (i32.const 1))))`
1100
1170
 
1101
- // .some(fn) return 1 if any element passes, else 0 (early exit)
1102
- ctx.core.emit['.some'] = (arr, fn) => {
1171
+ // .splice(start, deleteCount, ...items) with inserts __arr_splice. Deletes
1172
+ // `del` elements at `s`, inserts the elements of `ins`, returns the removed
1173
+ // elements as a new array. Mutation is in place: a grow relocates the buffer
1174
+ // but leaves a forwarding pointer, so the caller's NaN-box still resolves —
1175
+ // no write-back needed (mirrors __arr_unshift). memory.copy is memmove, so the
1176
+ // tail shift is correct whether the array grew (shift right) or shrank (left).
1177
+ ctx.core.stdlib['__arr_splice'] = `(func $__arr_splice (param $arr i64) (param $start i32) (param $del i32) (param $ins i64) (result f64)
1178
+ (local $off i32) (local $len i32) (local $s i32) (local $cnt i32)
1179
+ (local $m i32) (local $newLen i32) (local $tail i32) (local $out f64) (local $a f64)
1180
+ (local.set $len (call $__len (local.get $arr)))
1181
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1182
+ (local.set $s (local.get $start))
1183
+ (if (i32.lt_s (local.get $s) (i32.const 0))
1184
+ (then
1185
+ (local.set $s (i32.add (local.get $s) (local.get $len)))
1186
+ (if (i32.lt_s (local.get $s) (i32.const 0)) (then (local.set $s (i32.const 0))))))
1187
+ (if (i32.gt_s (local.get $s) (local.get $len)) (then (local.set $s (local.get $len))))
1188
+ (local.set $cnt (local.get $del))
1189
+ (if (i32.lt_s (local.get $cnt) (i32.const 0)) (then (local.set $cnt (i32.const 0))))
1190
+ (if (i32.gt_s (i32.add (local.get $s) (local.get $cnt)) (local.get $len))
1191
+ (then (local.set $cnt (i32.sub (local.get $len) (local.get $s)))))
1192
+ (local.set $m (call $__len (local.get $ins)))
1193
+ (local.set $newLen (i32.add (i32.sub (local.get $len) (local.get $cnt)) (local.get $m)))
1194
+ (local.set $tail (i32.sub (i32.sub (local.get $len) (local.get $s)) (local.get $cnt)))
1195
+ (local.set $out (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (call $__alloc_hdr (local.get $cnt) (local.get $cnt))))
1196
+ (memory.copy
1197
+ (call $__ptr_offset (i64.reinterpret_f64 (local.get $out)))
1198
+ (i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
1199
+ (i32.shl (local.get $cnt) (i32.const 3)))
1200
+ (local.set $a (f64.reinterpret_i64 (local.get $arr)))
1201
+ (if (i32.gt_s (local.get $newLen) (local.get $len))
1202
+ (then (local.set $a (call $__arr_grow (local.get $arr) (local.get $newLen)))))
1203
+ (local.set $off (call $__ptr_offset (i64.reinterpret_f64 (local.get $a))))
1204
+ (memory.copy
1205
+ (i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $m)) (i32.const 3)))
1206
+ (i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $cnt)) (i32.const 3)))
1207
+ (i32.shl (local.get $tail) (i32.const 3)))
1208
+ (memory.copy
1209
+ (i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
1210
+ (call $__ptr_offset (local.get $ins))
1211
+ (i32.shl (local.get $m) (i32.const 3)))
1212
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newLen))
1213
+ (local.get $out))`
1214
+
1215
+ // Early-exit callback iterator: init value, exit test, value on match.
1216
+ const earlyExitMethod = ({ tag, init, test, onMatch, reverse }) => (arr, fn) => {
1103
1217
  const recv = hoistArrayValue(arr)
1104
- const r = temp('sr')
1218
+ const r = temp(tag)
1105
1219
  const exit = `$exit${ctx.func.uniq++}`
1106
1220
  const cb = makeCallback(fn, callbackArgReps(arr))
1107
1221
  const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1108
- ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
1109
- ['then', ['local.set', `$${r}`, ['f64.const', 1]], ['br', exit]]]
1110
- ])
1222
+ ['if', test(cb, i, item),
1223
+ ['then', ['local.set', `$${r}`, onMatch(cb, i, item)], ['br', exit]]]
1224
+ ], undefined, undefined, reverse)
1111
1225
  return typed(['block', ['result', 'f64'],
1112
1226
  recv.setup,
1113
1227
  cb.setup,
1114
- ['local.set', `$${r}`, ['f64.const', 0]],
1228
+ ['local.set', `$${r}`, init],
1115
1229
  ['block', exit, ...loop],
1116
1230
  ['local.get', `$${r}`]], 'f64')
1117
1231
  }
1118
1232
 
1119
- // .every(fn) return 1 if all elements pass, else 0 (early exit)
1120
- ctx.core.emit['.every'] = (arr, fn) => {
1121
- const recv = hoistArrayValue(arr)
1122
- const r = temp('ev')
1123
- const exit = `$exit${ctx.func.uniq++}`
1124
- const cb = makeCallback(fn, callbackArgReps(arr))
1125
- const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1126
- ['if', ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
1127
- ['then', ['local.set', `$${r}`, ['f64.const', 0]], ['br', exit]]]
1128
- ])
1129
- return typed(['block', ['result', 'f64'],
1130
- recv.setup,
1131
- cb.setup,
1132
- ['local.set', `$${r}`, ['f64.const', 1]],
1133
- ['block', exit, ...loop],
1134
- ['local.get', `$${r}`]], 'f64')
1135
- }
1233
+ ctx.core.emit['.some'] = earlyExitMethod({
1234
+ tag: 'sr',
1235
+ init: ['f64.const', 0],
1236
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1237
+ onMatch: () => ['f64.const', 1],
1238
+ })
1136
1239
 
1137
- // .findIndex(fn) return index of first matching element, or -1 (early exit)
1138
- ctx.core.emit['.findIndex'] = (arr, fn) => {
1139
- const recv = hoistArrayValue(arr)
1140
- const r = temp('fi')
1141
- const exit = `$exit${ctx.func.uniq++}`
1142
- const cb = makeCallback(fn, callbackArgReps(arr))
1143
- const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1144
- ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
1145
- ['then', ['local.set', `$${r}`, ['f64.convert_i32_s', ['local.get', `$${i}`]]], ['br', exit]]]
1146
- ])
1147
- return typed(['block', ['result', 'f64'],
1148
- recv.setup,
1149
- cb.setup,
1150
- ['local.set', `$${r}`, ['f64.const', -1]],
1151
- ['block', exit, ...loop],
1152
- ['local.get', `$${r}`]], 'f64')
1153
- }
1240
+ ctx.core.emit['.every'] = earlyExitMethod({
1241
+ tag: 'ev',
1242
+ init: ['f64.const', 1],
1243
+ test: (cb, i, item) => ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
1244
+ onMatch: () => ['f64.const', 0],
1245
+ })
1246
+
1247
+ ctx.core.emit['.findIndex'] = earlyExitMethod({
1248
+ tag: 'fi',
1249
+ init: ['f64.const', -1],
1250
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1251
+ onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
1252
+ })
1253
+
1254
+ ctx.core.emit['.find'] = earlyExitMethod({
1255
+ tag: 'ff',
1256
+ init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
1257
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1258
+ onMatch: (_cb, _i, item) => item,
1259
+ })
1260
+
1261
+ ctx.core.emit['.findLastIndex'] = earlyExitMethod({
1262
+ tag: 'fli',
1263
+ init: ['f64.const', -1],
1264
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1265
+ onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
1266
+ reverse: true,
1267
+ })
1268
+
1269
+ ctx.core.emit['.findLast'] = earlyExitMethod({
1270
+ tag: 'fl',
1271
+ init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
1272
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1273
+ onMatch: (_cb, _i, item) => item,
1274
+ reverse: true,
1275
+ })
1154
1276
 
1155
1277
  // === Array methods ===
1156
1278
 
@@ -1170,13 +1292,13 @@ export default (ctx) => {
1170
1292
  }
1171
1293
  function isPureCallback(fn) {
1172
1294
  if (!Array.isArray(fn) || fn[0] !== '=>') return false
1173
- const body = fn[2]
1174
1295
  const params = new Set()
1175
- const p = fn[1]
1176
- const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : p[0] === '()' ? (p[1] == null ? [] : Array.isArray(p[1]) && p[1][0] === ',' ? p[1].slice(1) : [p[1]]) : [p]) : [p]
1177
- for (const r of raw) params.add(Array.isArray(r) && r[0] === '...' ? r[1] : typeof r === 'string' ? r : Array.isArray(r) && r[0] === '=' ? r[1] : null)
1296
+ for (const r of extractParams(fn[1])) {
1297
+ const p = classifyParam(r)
1298
+ if (p.name) params.add(p.name)
1299
+ }
1178
1300
  const locals = new Set(params)
1179
- collectLocals(body, locals)
1301
+ collectLocals(fn[2], locals)
1180
1302
  let pure = true
1181
1303
  ;(function walk(node) {
1182
1304
  if (!pure || !Array.isArray(node)) return
@@ -1184,13 +1306,13 @@ export default (ctx) => {
1184
1306
  if (op === '=>') return
1185
1307
  if (op === '()' || op === '?.()' || op === 'new') { pure = false; return }
1186
1308
  if (op === '++' || op === '--') { pure = false; return }
1187
- if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%=' || op === '&=' || op === '|=' || op === '^=' || op === '>>=' || op === '<<=' || op === '>>>=' || op === '||=' || op === '&&=' || op === '??=') {
1309
+ if (ASSIGN_OPS.has(op)) {
1188
1310
  const t = args[0]
1189
1311
  if (typeof t === 'string') { if (!locals.has(t)) { pure = false; return } }
1190
1312
  else { pure = false; return }
1191
1313
  }
1192
1314
  for (const a of args) walk(a)
1193
- })(body)
1315
+ })(fn[2])
1194
1316
  return pure
1195
1317
  }
1196
1318
 
@@ -1315,30 +1437,56 @@ export default (ctx) => {
1315
1437
  const acc = temp('ra'), mapped = temp('mv')
1316
1438
  const upReps = callbackArgReps(up.source)
1317
1439
  const mapCb = makeCallback(up.fn, upReps), redCb = makeCallback(fn)
1440
+ const mget = typed(['local.get', `$${mapped}`], 'f64')
1441
+ // map preserves indices → the reduce callback's index is the loop counter.
1442
+ const fold = i => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), mget, idxArg(redCb, i, 2)]))]
1318
1443
  const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1319
1444
  ['local.set', `$${mapped}`, asF64(mapCb.call([item, idxArg(mapCb, i)]))],
1320
- ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), typed(['local.get', `$${mapped}`], 'f64')]))]
1445
+ // No-init: seed accumulator with the first mapped element (see base path).
1446
+ init !== undefined ? fold(i)
1447
+ : ['if', ['i32.eqz', ['local.get', `$${i}`]],
1448
+ ['then', ['local.set', `$${acc}`, mget]],
1449
+ ['else', fold(i)]]
1321
1450
  ])
1322
1451
  return typed(['block', ['result', 'f64'],
1323
1452
  recv.setup, mapCb.setup, redCb.setup,
1324
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1453
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1325
1454
  ...loop, ['local.get', `$${acc}`]], 'f64')
1326
1455
  }
1327
1456
  // .filter(f).reduce(g, init) → single loop: test f, accumulate with g if passes
1328
1457
  if (up && up.method === 'filter') {
1329
1458
  const recv = hoistArrayValue(up.source)
1330
1459
  const acc = temp('ra')
1460
+ // No-init: seed is the first *passing* element, whose index isn't known
1461
+ // statically (filter), so track a seeded flag rather than i==0.
1462
+ const seeded = init !== undefined ? null : tempI32('rs')
1331
1463
  const upReps = callbackArgReps(up.source)
1332
1464
  const filterCb = makeCallback(up.fn, upReps)
1333
1465
  // reduce cb signature: (acc, item, idx). Item rep mirrors upstream's item rep.
1334
1466
  const redCb = makeCallback(fn, [null, upReps[0], { val: VAL.NUMBER }])
1467
+ // filter renumbers: the reduce index counts *passing* elements, not the
1468
+ // source position, so track a dedicated filtered-position counter (only when
1469
+ // the callback actually reads its index — else idxArg drops the arg anyway).
1470
+ const usesIdx = redCb.usedParams ? !!redCb.usedParams[2] : true
1471
+ const fpos = usesIdx ? tempI32('rp') : null
1472
+ const fold = item => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item, fpos ? idxArg(redCb, fpos, 2) : null]))]
1473
+ const bump = fpos ? [['local.set', `$${fpos}`, ['i32.add', ['local.get', `$${fpos}`], ['i32.const', 1]]]] : []
1474
+ const accumulate = item => ['block',
1475
+ seeded
1476
+ ? ['if', ['local.get', `$${seeded}`],
1477
+ ['then', fold(item)],
1478
+ ['else', ['block', ['local.set', `$${acc}`, asF64(item)], ['local.set', `$${seeded}`, ['i32.const', 1]]]]]
1479
+ : fold(item),
1480
+ ...bump]
1335
1481
  const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1336
1482
  ['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
1337
- ['then', ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item]))]]]
1483
+ ['then', accumulate(item)]]
1338
1484
  ])
1339
1485
  return typed(['block', ['result', 'f64'],
1340
1486
  recv.setup, filterCb.setup, redCb.setup,
1341
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1487
+ ...(fpos ? [['local.set', `$${fpos}`, ['i32.const', 0]]] : []),
1488
+ ...(seeded ? [['local.set', `$${seeded}`, ['i32.const', 0]]] : []),
1489
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1342
1490
  ...loop, ['local.get', `$${acc}`]], 'f64')
1343
1491
  }
1344
1492
  const recv = hoistArrayValue(arr)
@@ -1346,13 +1494,47 @@ export default (ctx) => {
1346
1494
  // reduce cb signature: (acc, item, idx). Item rep mirrors recv's elem val type.
1347
1495
  const reps = callbackArgReps(arr)
1348
1496
  const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
1349
- const loop = arrayLoop(recv.value, (_ptr, _len, _i, item) => [
1350
- ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item]))]
1497
+ // No initial value: JS seeds the accumulator with element 0 and folds from
1498
+ // index 1 — NOT a 0 seed folded over every element. A 0 seed is invisible
1499
+ // for `+` (additive identity) but wrong for `*` (→0) and corrupts non-numeric
1500
+ // folds (string reduce emits a bare `0` in the joined result). Seed at i==0.
1501
+ const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
1502
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1503
+ init !== undefined ? fold(item, i)
1504
+ : ['if', ['i32.eqz', ['local.get', `$${i}`]],
1505
+ ['then', ['local.set', `$${acc}`, asF64(item)]],
1506
+ ['else', fold(item, i)]]
1351
1507
  ])
1352
1508
  return typed(['block', ['result', 'f64'],
1353
1509
  recv.setup,
1354
1510
  cb.setup,
1355
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1511
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1512
+ ...loop,
1513
+ ['local.get', `$${acc}`]], 'f64')
1514
+ }
1515
+
1516
+ // .reduceRight(fn, init) — same accumulator fold as .reduce but the last
1517
+ // arrayLoop arg (reverse) walks elements len-1→0. No map/filter fusion: the
1518
+ // reverse-fused shapes don't occur in practice and the base case is the only
1519
+ // form jz emits. (Previously absent despite being autoload-declared + test262-
1520
+ // tracked — a phantom builtin that silently returned undefined.)
1521
+ ctx.core.emit['.reduceRight'] = (arr, fn, init) => {
1522
+ const recv = hoistArrayValue(arr)
1523
+ const acc = temp('ra')
1524
+ const reps = callbackArgReps(arr)
1525
+ const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
1526
+ // No-init: reverse walk seeds with the last element (i == len-1), folds down.
1527
+ const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
1528
+ const loop = arrayLoop(recv.value, (_ptr, len, i, item) => [
1529
+ init !== undefined ? fold(item, i)
1530
+ : ['if', ['i32.eq', ['local.get', `$${i}`], ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]]],
1531
+ ['then', ['local.set', `$${acc}`, asF64(item)]],
1532
+ ['else', fold(item, i)]]
1533
+ ], null, null, true)
1534
+ return typed(['block', ['result', 'f64'],
1535
+ recv.setup,
1536
+ cb.setup,
1537
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1356
1538
  ...loop,
1357
1539
  ['local.get', `$${acc}`]], 'f64')
1358
1540
  }
@@ -1391,23 +1573,6 @@ export default (ctx) => {
1391
1573
  return typed(['block', ['result', 'f64'], recv.setup, cb.setup, ...loop, ['f64.const', 0]], 'f64')
1392
1574
  }
1393
1575
 
1394
- ctx.core.emit['.find'] = (arr, fn) => {
1395
- const recv = hoistArrayValue(arr)
1396
- const result = temp('ff')
1397
- const exit = `$exit${ctx.func.uniq++}`
1398
- const cb = makeCallback(fn, callbackArgReps(arr))
1399
- const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1400
- ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
1401
- ['then', ['local.set', `$${result}`, item], ['br', exit]]]
1402
- ])
1403
- return typed(['block', ['result', 'f64'],
1404
- recv.setup,
1405
- cb.setup,
1406
- ['local.set', `$${result}`, ['f64.reinterpret_i64', ['i64.const', NULL_NAN]]],
1407
- ['block', exit, ...loop],
1408
- ['local.get', `$${result}`]], 'f64')
1409
- }
1410
-
1411
1576
  // .reverse() → in-place swap arr[i] ↔ arr[len-1-i], returns the array.
1412
1577
  ctx.core.emit['.reverse'] = (arr) => {
1413
1578
  const recv = hoistArrayValue(arr)
@@ -1697,6 +1862,10 @@ export default (ctx) => {
1697
1862
  body.push(out.ptr)
1698
1863
  return typed(['block', ['result', 'f64'], ...body], 'f64')
1699
1864
  }
1865
+ // Unqualified alias so an untyped-receiver `.concat` gets emit's runtime
1866
+ // string-vs-array ptr-type branch (string → `.string:concat`, array → this),
1867
+ // mirroring `.at`. A known-ARRAY receiver still dispatches here directly.
1868
+ ctx.core.emit['.concat'] = ctx.core.emit['.array:concat']
1700
1869
 
1701
1870
  // .flat() → flatten one level of nested arrays
1702
1871
  ctx.core.stdlib['__arr_flat'] = `(func $__arr_flat (param $src i64) (result f64)