jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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, idx as emitIndex } 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, staticIndexKey } 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, warnDeopt, 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. */
27
- function staticArrayPtr(slots) {
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). */
36
+ export 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) => {
@@ -617,8 +625,14 @@ export default (ctx) => {
617
625
  if (!hasSpread) {
618
626
  const len = elems.length
619
627
  // R: Static data segment for arrays of pure-literal elements (own-memory only).
620
- // Saves N×(alloc+store) instructions in __start; raw f64 bits embedded directly.
621
- if (len >= 4 && !ctx.memory.shared) {
628
+ // Raw f64 bits embedded directly a constant array becomes a const pointer with no
629
+ // alloc and no per-element store. A static array aliases ONE shared data-segment
630
+ // region, so this is sound only when no caller expects a fresh instance per
631
+ // evaluation: at module scope the literal runs exactly once. A function-local
632
+ // literal (which would leak in-place mutations across calls — a latent bug the old
633
+ // len≥4 gate also had) allocs fresh instead. Module scope lifts the size floor too,
634
+ // so `const x = [1, 2, 3]` is a data segment, not an alloc.
635
+ if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
622
636
  // asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
623
637
  const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
624
638
  if (slots.every(b => b !== null)) return staticArrayPtr(slots)
@@ -633,10 +647,10 @@ export default (ctx) => {
633
647
  return typed(['block', ['result', 'f64'], ...body], 'f64')
634
648
  }
635
649
 
636
- // Spread literal: buildArrayWithSpreads pre-sums the total length, allocates
650
+ // Spread literal: spread pre-sums the total length, allocates
637
651
  // exact, and bulk-copies ARRAY sources with a single memory.copy — vs the
638
652
  // per-element __arr_set_idx_ptr grow loop. Normalise the parser's `['...', x]`.
639
- return buildArrayWithSpreads(elems.map(e =>
653
+ return spread(elems.map(e =>
640
654
  Array.isArray(e) && e[0] === '...' ? ['__spread', e[1]] : e))
641
655
  }
642
656
 
@@ -668,14 +682,17 @@ export default (ctx) => {
668
682
  const litKey = isLiteralStr(idx) ? idx[1]
669
683
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
670
684
  : null
671
- // SRoA flat object: `o['k']` → `local.get $o#i` (analyze.js scanFlatObjects).
672
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
685
+ // SRoA flat object/array: `o['k']` / `a[2]` → `local.get $o#i` (scanFlatObjects).
686
+ // A bare integer index resolves its slot key here (not via `litKey`, which stays
687
+ // null for arrays so the heap-array / schema paths below are untouched).
688
+ if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
673
689
  const fo = ctx.func.flatObjects.get(arr)
674
- const fi = fo.names.indexOf(litKey)
690
+ const flatKey = litKey != null ? litKey : staticIndexKey(idx)
691
+ const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
675
692
  if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
676
693
  }
677
- if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
678
- const slot = ctx.schema.find(arr, litKey)
694
+ if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
695
+ const slot = ctx.schema.slotOf(arr, litKey)
679
696
  if (slot >= 0) {
680
697
  inc('__ptr_offset')
681
698
  return typed(ctx.abi.object.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(arr))]], slot), 'f64')
@@ -692,10 +709,24 @@ export default (ctx) => {
692
709
  // Multi-value calls are materialized at call site (see '()' handler), so
693
710
  // func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
694
711
  const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
695
- const va = emit(arr), vi = asI32(emit(idx))
712
+ // Literal string key on a receiver that isn't a known array/typed/string:
713
+ // `x['a']` IS `x.a` — delegate to the dot emitter so an untyped/OBJECT/external
714
+ // receiver gets the same polymorphic dispatch (__dyn_get_any_t_h, host-external
715
+ // aware). The local `dynLoad` fallback below calls __dyn_get, which only probes
716
+ // the internal HASH layout — so `x['a']` on a host object silently returned
717
+ // undefined while `x.a` worked. (Known ARRAY/TYPED/STRING fall through unchanged:
718
+ // their string-key semantics differ from a HASH/OBJECT property read.)
719
+ if (litKey != null && vt !== VAL.ARRAY && vt !== VAL.TYPED && vt !== VAL.STRING)
720
+ return emit(['.', arr, litKey])
721
+ // emitIndex (not bare asI32(emit)) narrows integer index arithmetic — incl. a
722
+ // literal term like the `+1` of `a[i*W + x + 1]` — to i32 ops instead of the
723
+ // f64 convert/trunc round-trip. Non-i32 keys (string dispatch) fall back to
724
+ // asI32(emit) inside emitIndex, so this is a strict improvement for every branch.
725
+ const va = emit(arr), vi = emitIndex(idx)
696
726
  const ptrExpr = asF64(va)
697
727
  const dynLoad = (objExpr, keyExpr) => {
698
728
  if (ctx.transform.strict) err(`strict mode: dynamic property access \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>]\` falls back to __dyn_get. Use a literal key or known typed-array receiver, or pass { strict: false }.`)
729
+ warnDeopt('deopt-dyn-read', `dynamic property read \`${typeof arr === 'string' ? arr : '<expr>'}[…]\` couldn't resolve a static type — it falls back to a runtime hash lookup (~1.5–2× slower than a typed/slot read, far worse in a hot loop). Use a literal key, a typed-array receiver, or a Map for genuinely dynamic keys.`)
699
730
  inc('__dyn_get')
700
731
  return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
701
732
  }
@@ -745,6 +776,16 @@ export default (ctx) => {
745
776
  if (keyType === VAL.STRING)
746
777
  return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
747
778
  if (vt === 'array') {
779
+ // Base offset of the array's data region. A binding proven never relocated
780
+ // (scanNeverGrown — a fresh array literal whose every use is a pure read, so no
781
+ // grow op can ever run) skips the realloc-forwarding follow: its base is the raw
782
+ // post-header offset `wrap(reinterpret(ptr) & OFFSET_MASK)`, no __ptr_offset call.
783
+ // Memory-safe ONLY under that proof — a relocated array read through this stale
784
+ // base would corrupt memory (see scanNeverGrown's default-deny rationale).
785
+ const neverGrown = typeof arr === 'string' && ctx.func.localReps?.get(arr)?.neverGrown === true
786
+ const arrBase = () => neverGrown
787
+ ? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ptrExpr], ['i64.const', LAYOUT.OFFSET_MASK]]]
788
+ : (inc('__ptr_offset'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]])
748
789
  // structInline Array<S>: element i is K consecutive inline f64 schema
749
790
  // cells — no per-row heap object, no stored element pointer. `arr[i]` is
750
791
  // the byte address of the element's first cell, returned as a first-class
@@ -753,11 +794,10 @@ export default (ctx) => {
753
794
  // structInline handles (src/analyze.js analyzeStructInline).
754
795
  const inlSid = inlineArraySid(arr)
755
796
  if (inlSid != null) {
756
- inc('__ptr_offset')
757
797
  const baseI32 = tempI32('ab')
758
798
  const K = ctx.schema.list[inlSid].length
759
799
  const cell = typed(structInline(K).ops.elemAddr(
760
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
800
+ ['local.tee', `$${baseI32}`, arrBase()],
761
801
  vi), 'i32')
762
802
  cell.ptrKind = VAL.OBJECT
763
803
  cell.ptrAux = inlSid
@@ -767,29 +807,55 @@ export default (ctx) => {
767
807
  // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
768
808
  // plus type-dispatch overhead irrelevant for plain arrays).
769
809
  const keyIsNum = keyType === VAL.NUMBER
770
- // Inline fast path: when arr has a known element schema and key is a
771
- // known number, the type-tag check and bounds check inside __arr_idx are
772
- // dead weight in hot kernels. Emit (f64.load (i32.add base (shl i 3)))
773
- // directly. base goes through __ptr_offset (still does forwarding follow),
774
- // and hoistAddrBase will CSE the (base, i) pair across the iteration body.
775
- // Array<NUMBER>/<STRING>/<OBJECT> all use 8-byte f64 slot layout direct
776
- // f64.load is correct regardless of elem kind (returns raw f64 for NUMBER,
777
- // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
778
- // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
779
- // (Array<NUMBER> from `.map(x => x*k)` etc.).
780
- const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
781
- const hasElemFact = rep?.arrayElemSchema != null
782
- if (hasElemFact && keyIsNum) {
783
- inc('__ptr_offset')
784
- // __ptr_offset returns i32 — base local must be i32 (not the default
785
- // f64 NaN-box temp). Flat tee form so downstream peepholes can fold
810
+ // Inline fast path for any known plain ARRAY + numeric key: the type-tag
811
+ // dispatch and bounds check inside __arr_idx(_known) are dead weight in hot
812
+ // kernels most visibly AST walkers doing `node[i]` over heterogeneous
813
+ // arrays, where no element schema/valType is ever inferred. Emit the
814
+ // f64.load directly. base goes through __ptr_offset (still the forwarding
815
+ // follow), and hoistAddrBase CSEs the (base, i) pair across the iteration
816
+ // body. taggedLinear stores every element as one 8-byte f64 cell
817
+ // Array<NUMBER>/<STRING>/<OBJECT> alike so a direct f64.load is correct
818
+ // regardless of elem kind (raw f64 for NUMBER, NaN-boxed pointer for
819
+ // OBJECT/STRING; downstream typed() handles both). The load shape is fixed
820
+ // by the carrier, not by any rep fact, so we do NOT gate on a known element
821
+ // schema/valType: a bare `let a = [...]` walked by index gets the same
822
+ // inline load as an Array<{x,y,z}>. (structInline arrays returned above via
823
+ // inlineArraySid; typed arrays are VAL.TYPED, handled below.)
824
+ //
825
+ // Take the UNCHECKED inline load only when the index is proven in-bounds by
826
+ // an enclosing canonical loop `for (let i=C; i<arr.length; i++)` — a pure
827
+ // index<length structural proof (scanBoundedArrIdx, src/type.js), itself
828
+ // element-kind-independent. Skipping the bounds check on an arbitrary numeric
829
+ // index is unsound: `a[1]` on a length-1 array would read the raw cell instead
830
+ // of undefined; those fall through to the inline bounds-checked load below.
831
+ const idxProvenInBounds = keyIsNum
832
+ && typeof arr === 'string' && typeof idx === 'string'
833
+ && inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
834
+ if (idxProvenInBounds) {
835
+ // base local must be i32. Flat tee form so downstream peepholes can fold
786
836
  // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
787
837
  // when this load feeds a ptrUnboxed OBJECT field.
788
838
  const baseI32 = tempI32('ab')
789
839
  return typed(ctx.abi.array.ops.load(
790
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
840
+ ['local.tee', `$${baseI32}`, arrBase()],
791
841
  vi), 'f64')
792
842
  }
843
+ // Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
844
+ // load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
845
+ // inline, so watr hoists the loop-invariant len load and CSEs the base — the
846
+ // residual cost is a single (predictable) compare per access, not a call. Skipping
847
+ // the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
848
+ if (keyIsNum) {
849
+ const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
850
+ return typed(['if', ['result', 'f64'],
851
+ ['i32.lt_u',
852
+ ['local.tee', `$${idxI32}`, vi],
853
+ ['i32.load', ['i32.sub',
854
+ ['local.tee', `$${baseI32}`, arrBase()],
855
+ ['i32.const', 8]]]],
856
+ ['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
857
+ ['else', undefExpr()]], 'f64')
858
+ }
793
859
  const baseTmp = temp()
794
860
  // Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
795
861
  // arrays don't honor string-key access for numeric keys (keys aren't coerced
@@ -842,7 +908,7 @@ export default (ctx) => {
842
908
  const keyI32 = asI32(typed(keyExpr, 'f64'))
843
909
  if (ctx.module.modules['string'] && !notString) {
844
910
  return ['if', ['result', 'f64'],
845
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
911
+ ptrTypeEq(ptrExpr, PTR.STRING),
846
912
  ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])],
847
913
  ['else', (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])]]
848
914
  }
@@ -852,7 +918,7 @@ export default (ctx) => {
852
918
  if (ctx.module.modules['string'] && !notString)
853
919
  return typed(
854
920
  ['if', ['result', 'f64'],
855
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
921
+ ptrTypeEq(ptrExpr, PTR.STRING),
856
922
  ['then', stringLoad()],
857
923
  ['else', arrayLoad]],
858
924
  'f64')
@@ -1021,6 +1087,42 @@ export default (ctx) => {
1021
1087
  (i32.store (i32.sub (local.get $rawOff) (i32.const 4)) (i32.const -1))))
1022
1088
  (local.get $val))))))`
1023
1089
 
1090
+ // .fill(value, start?, end?) — overwrite [start, end) with value; mutate + return the
1091
+ // array. start/end default to 0 / length and accept negatives (offset from end). The
1092
+ // INT_MAX end sentinel clamps to length in the helper, which also normalizes negatives.
1093
+ ctx.core.emit['.fill'] = (arr, val, start, end) => {
1094
+ inc('__arr_fill')
1095
+ return typed(['call', '$__arr_fill',
1096
+ asI64(emit(arr)),
1097
+ val == null ? undefExpr() : asF64(emit(val)),
1098
+ start == null ? ['i32.const', 0] : asI32(emit(start)),
1099
+ end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
1100
+ }
1101
+
1102
+ ctx.core.stdlib['__arr_fill'] = `(func $__arr_fill (param $arr i64) (param $val f64) (param $start i32) (param $end i32) (result f64)
1103
+ (local $off i32) (local $len i32) (local $i i32)
1104
+ (if (i32.eq
1105
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $arr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
1106
+ (i32.const ${PTR.ARRAY}))
1107
+ (then
1108
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1109
+ (if (i32.ge_u (local.get $off) (i32.const 8))
1110
+ (then
1111
+ (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
1112
+ (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
1113
+ (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.const 0))))
1114
+ (if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
1115
+ (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
1116
+ (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
1117
+ (if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
1118
+ (local.set $i (local.get $start))
1119
+ (block $done (loop $fill
1120
+ (br_if $done (i32.ge_s (local.get $i) (local.get $end)))
1121
+ (f64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (local.get $val))
1122
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1123
+ (br $fill)))))))
1124
+ (f64.reinterpret_i64 (local.get $arr)))`
1125
+
1024
1126
  // .splice(start) | .splice(start, deleteCount) → remove range, return removed as new array
1025
1127
  ctx.core.emit['.splice'] = (arr, start, deleteCount) => {
1026
1128
  const recv = hoistArrayValue(arr)
@@ -1098,59 +1200,111 @@ export default (ctx) => {
1098
1200
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
1099
1201
  (f64.convert_i32_s (i32.add (local.get $len) (i32.const 1))))`
1100
1202
 
1101
- // .some(fn) return 1 if any element passes, else 0 (early exit)
1102
- ctx.core.emit['.some'] = (arr, fn) => {
1203
+ // .splice(start, deleteCount, ...items) with inserts __arr_splice. Deletes
1204
+ // `del` elements at `s`, inserts the elements of `ins`, returns the removed
1205
+ // elements as a new array. Mutation is in place: a grow relocates the buffer
1206
+ // but leaves a forwarding pointer, so the caller's NaN-box still resolves —
1207
+ // no write-back needed (mirrors __arr_unshift). memory.copy is memmove, so the
1208
+ // tail shift is correct whether the array grew (shift right) or shrank (left).
1209
+ ctx.core.stdlib['__arr_splice'] = `(func $__arr_splice (param $arr i64) (param $start i32) (param $del i32) (param $ins i64) (result f64)
1210
+ (local $off i32) (local $len i32) (local $s i32) (local $cnt i32)
1211
+ (local $m i32) (local $newLen i32) (local $tail i32) (local $out f64) (local $a f64)
1212
+ (local.set $len (call $__len (local.get $arr)))
1213
+ (local.set $off (call $__ptr_offset (local.get $arr)))
1214
+ (local.set $s (local.get $start))
1215
+ (if (i32.lt_s (local.get $s) (i32.const 0))
1216
+ (then
1217
+ (local.set $s (i32.add (local.get $s) (local.get $len)))
1218
+ (if (i32.lt_s (local.get $s) (i32.const 0)) (then (local.set $s (i32.const 0))))))
1219
+ (if (i32.gt_s (local.get $s) (local.get $len)) (then (local.set $s (local.get $len))))
1220
+ (local.set $cnt (local.get $del))
1221
+ (if (i32.lt_s (local.get $cnt) (i32.const 0)) (then (local.set $cnt (i32.const 0))))
1222
+ (if (i32.gt_s (i32.add (local.get $s) (local.get $cnt)) (local.get $len))
1223
+ (then (local.set $cnt (i32.sub (local.get $len) (local.get $s)))))
1224
+ (local.set $m (call $__len (local.get $ins)))
1225
+ (local.set $newLen (i32.add (i32.sub (local.get $len) (local.get $cnt)) (local.get $m)))
1226
+ (local.set $tail (i32.sub (i32.sub (local.get $len) (local.get $s)) (local.get $cnt)))
1227
+ (local.set $out (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (call $__alloc_hdr (local.get $cnt) (local.get $cnt))))
1228
+ (memory.copy
1229
+ (call $__ptr_offset (i64.reinterpret_f64 (local.get $out)))
1230
+ (i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
1231
+ (i32.shl (local.get $cnt) (i32.const 3)))
1232
+ (local.set $a (f64.reinterpret_i64 (local.get $arr)))
1233
+ (if (i32.gt_s (local.get $newLen) (local.get $len))
1234
+ (then (local.set $a (call $__arr_grow (local.get $arr) (local.get $newLen)))))
1235
+ (local.set $off (call $__ptr_offset (i64.reinterpret_f64 (local.get $a))))
1236
+ (memory.copy
1237
+ (i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $m)) (i32.const 3)))
1238
+ (i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $cnt)) (i32.const 3)))
1239
+ (i32.shl (local.get $tail) (i32.const 3)))
1240
+ (memory.copy
1241
+ (i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
1242
+ (call $__ptr_offset (local.get $ins))
1243
+ (i32.shl (local.get $m) (i32.const 3)))
1244
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newLen))
1245
+ (local.get $out))`
1246
+
1247
+ // Early-exit callback iterator: init value, exit test, value on match.
1248
+ const earlyExitMethod = ({ tag, init, test, onMatch, reverse }) => (arr, fn) => {
1103
1249
  const recv = hoistArrayValue(arr)
1104
- const r = temp('sr')
1250
+ const r = temp(tag)
1105
1251
  const exit = `$exit${ctx.func.uniq++}`
1106
1252
  const cb = makeCallback(fn, callbackArgReps(arr))
1107
1253
  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
- ])
1254
+ ['if', test(cb, i, item),
1255
+ ['then', ['local.set', `$${r}`, onMatch(cb, i, item)], ['br', exit]]]
1256
+ ], undefined, undefined, reverse)
1111
1257
  return typed(['block', ['result', 'f64'],
1112
1258
  recv.setup,
1113
1259
  cb.setup,
1114
- ['local.set', `$${r}`, ['f64.const', 0]],
1260
+ ['local.set', `$${r}`, init],
1115
1261
  ['block', exit, ...loop],
1116
1262
  ['local.get', `$${r}`]], 'f64')
1117
1263
  }
1118
1264
 
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
- }
1265
+ ctx.core.emit['.some'] = earlyExitMethod({
1266
+ tag: 'sr',
1267
+ init: ['f64.const', 0],
1268
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1269
+ onMatch: () => ['f64.const', 1],
1270
+ })
1136
1271
 
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
- }
1272
+ ctx.core.emit['.every'] = earlyExitMethod({
1273
+ tag: 'ev',
1274
+ init: ['f64.const', 1],
1275
+ test: (cb, i, item) => ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
1276
+ onMatch: () => ['f64.const', 0],
1277
+ })
1278
+
1279
+ ctx.core.emit['.findIndex'] = earlyExitMethod({
1280
+ tag: 'fi',
1281
+ init: ['f64.const', -1],
1282
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1283
+ onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
1284
+ })
1285
+
1286
+ ctx.core.emit['.find'] = earlyExitMethod({
1287
+ tag: 'ff',
1288
+ init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
1289
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1290
+ onMatch: (_cb, _i, item) => item,
1291
+ })
1292
+
1293
+ ctx.core.emit['.findLastIndex'] = earlyExitMethod({
1294
+ tag: 'fli',
1295
+ init: ['f64.const', -1],
1296
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1297
+ onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
1298
+ reverse: true,
1299
+ })
1300
+
1301
+ ctx.core.emit['.findLast'] = earlyExitMethod({
1302
+ tag: 'fl',
1303
+ init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
1304
+ test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
1305
+ onMatch: (_cb, _i, item) => item,
1306
+ reverse: true,
1307
+ })
1154
1308
 
1155
1309
  // === Array methods ===
1156
1310
 
@@ -1170,13 +1324,13 @@ export default (ctx) => {
1170
1324
  }
1171
1325
  function isPureCallback(fn) {
1172
1326
  if (!Array.isArray(fn) || fn[0] !== '=>') return false
1173
- const body = fn[2]
1174
1327
  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)
1328
+ for (const r of extractParams(fn[1])) {
1329
+ const p = classifyParam(r)
1330
+ if (p.name) params.add(p.name)
1331
+ }
1178
1332
  const locals = new Set(params)
1179
- collectLocals(body, locals)
1333
+ collectLocals(fn[2], locals)
1180
1334
  let pure = true
1181
1335
  ;(function walk(node) {
1182
1336
  if (!pure || !Array.isArray(node)) return
@@ -1184,13 +1338,13 @@ export default (ctx) => {
1184
1338
  if (op === '=>') return
1185
1339
  if (op === '()' || op === '?.()' || op === 'new') { pure = false; return }
1186
1340
  if (op === '++' || op === '--') { pure = false; return }
1187
- if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%=' || op === '&=' || op === '|=' || op === '^=' || op === '>>=' || op === '<<=' || op === '>>>=' || op === '||=' || op === '&&=' || op === '??=') {
1341
+ if (ASSIGN_OPS.has(op)) {
1188
1342
  const t = args[0]
1189
1343
  if (typeof t === 'string') { if (!locals.has(t)) { pure = false; return } }
1190
1344
  else { pure = false; return }
1191
1345
  }
1192
1346
  for (const a of args) walk(a)
1193
- })(body)
1347
+ })(fn[2])
1194
1348
  return pure
1195
1349
  }
1196
1350
 
@@ -1315,30 +1469,56 @@ export default (ctx) => {
1315
1469
  const acc = temp('ra'), mapped = temp('mv')
1316
1470
  const upReps = callbackArgReps(up.source)
1317
1471
  const mapCb = makeCallback(up.fn, upReps), redCb = makeCallback(fn)
1472
+ const mget = typed(['local.get', `$${mapped}`], 'f64')
1473
+ // map preserves indices → the reduce callback's index is the loop counter.
1474
+ const fold = i => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), mget, idxArg(redCb, i, 2)]))]
1318
1475
  const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1319
1476
  ['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')]))]
1477
+ // No-init: seed accumulator with the first mapped element (see base path).
1478
+ init !== undefined ? fold(i)
1479
+ : ['if', ['i32.eqz', ['local.get', `$${i}`]],
1480
+ ['then', ['local.set', `$${acc}`, mget]],
1481
+ ['else', fold(i)]]
1321
1482
  ])
1322
1483
  return typed(['block', ['result', 'f64'],
1323
1484
  recv.setup, mapCb.setup, redCb.setup,
1324
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1485
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1325
1486
  ...loop, ['local.get', `$${acc}`]], 'f64')
1326
1487
  }
1327
1488
  // .filter(f).reduce(g, init) → single loop: test f, accumulate with g if passes
1328
1489
  if (up && up.method === 'filter') {
1329
1490
  const recv = hoistArrayValue(up.source)
1330
1491
  const acc = temp('ra')
1492
+ // No-init: seed is the first *passing* element, whose index isn't known
1493
+ // statically (filter), so track a seeded flag rather than i==0.
1494
+ const seeded = init !== undefined ? null : tempI32('rs')
1331
1495
  const upReps = callbackArgReps(up.source)
1332
1496
  const filterCb = makeCallback(up.fn, upReps)
1333
1497
  // reduce cb signature: (acc, item, idx). Item rep mirrors upstream's item rep.
1334
1498
  const redCb = makeCallback(fn, [null, upReps[0], { val: VAL.NUMBER }])
1499
+ // filter renumbers: the reduce index counts *passing* elements, not the
1500
+ // source position, so track a dedicated filtered-position counter (only when
1501
+ // the callback actually reads its index — else idxArg drops the arg anyway).
1502
+ const usesIdx = redCb.usedParams ? !!redCb.usedParams[2] : true
1503
+ const fpos = usesIdx ? tempI32('rp') : null
1504
+ const fold = item => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item, fpos ? idxArg(redCb, fpos, 2) : null]))]
1505
+ const bump = fpos ? [['local.set', `$${fpos}`, ['i32.add', ['local.get', `$${fpos}`], ['i32.const', 1]]]] : []
1506
+ const accumulate = item => ['block',
1507
+ seeded
1508
+ ? ['if', ['local.get', `$${seeded}`],
1509
+ ['then', fold(item)],
1510
+ ['else', ['block', ['local.set', `$${acc}`, asF64(item)], ['local.set', `$${seeded}`, ['i32.const', 1]]]]]
1511
+ : fold(item),
1512
+ ...bump]
1335
1513
  const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1336
1514
  ['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
1337
- ['then', ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item]))]]]
1515
+ ['then', accumulate(item)]]
1338
1516
  ])
1339
1517
  return typed(['block', ['result', 'f64'],
1340
1518
  recv.setup, filterCb.setup, redCb.setup,
1341
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1519
+ ...(fpos ? [['local.set', `$${fpos}`, ['i32.const', 0]]] : []),
1520
+ ...(seeded ? [['local.set', `$${seeded}`, ['i32.const', 0]]] : []),
1521
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1342
1522
  ...loop, ['local.get', `$${acc}`]], 'f64')
1343
1523
  }
1344
1524
  const recv = hoistArrayValue(arr)
@@ -1346,13 +1526,47 @@ export default (ctx) => {
1346
1526
  // reduce cb signature: (acc, item, idx). Item rep mirrors recv's elem val type.
1347
1527
  const reps = callbackArgReps(arr)
1348
1528
  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]))]
1529
+ // No initial value: JS seeds the accumulator with element 0 and folds from
1530
+ // index 1 — NOT a 0 seed folded over every element. A 0 seed is invisible
1531
+ // for `+` (additive identity) but wrong for `*` (→0) and corrupts non-numeric
1532
+ // folds (string reduce emits a bare `0` in the joined result). Seed at i==0.
1533
+ const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
1534
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1535
+ init !== undefined ? fold(item, i)
1536
+ : ['if', ['i32.eqz', ['local.get', `$${i}`]],
1537
+ ['then', ['local.set', `$${acc}`, asF64(item)]],
1538
+ ['else', fold(item, i)]]
1351
1539
  ])
1352
1540
  return typed(['block', ['result', 'f64'],
1353
1541
  recv.setup,
1354
1542
  cb.setup,
1355
- ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1543
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1544
+ ...loop,
1545
+ ['local.get', `$${acc}`]], 'f64')
1546
+ }
1547
+
1548
+ // .reduceRight(fn, init) — same accumulator fold as .reduce but the last
1549
+ // arrayLoop arg (reverse) walks elements len-1→0. No map/filter fusion: the
1550
+ // reverse-fused shapes don't occur in practice and the base case is the only
1551
+ // form jz emits. (Previously absent despite being autoload-declared + test262-
1552
+ // tracked — a phantom builtin that silently returned undefined.)
1553
+ ctx.core.emit['.reduceRight'] = (arr, fn, init) => {
1554
+ const recv = hoistArrayValue(arr)
1555
+ const acc = temp('ra')
1556
+ const reps = callbackArgReps(arr)
1557
+ const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
1558
+ // No-init: reverse walk seeds with the last element (i == len-1), folds down.
1559
+ const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
1560
+ const loop = arrayLoop(recv.value, (_ptr, len, i, item) => [
1561
+ init !== undefined ? fold(item, i)
1562
+ : ['if', ['i32.eq', ['local.get', `$${i}`], ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]]],
1563
+ ['then', ['local.set', `$${acc}`, asF64(item)]],
1564
+ ['else', fold(item, i)]]
1565
+ ], null, null, true)
1566
+ return typed(['block', ['result', 'f64'],
1567
+ recv.setup,
1568
+ cb.setup,
1569
+ ['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
1356
1570
  ...loop,
1357
1571
  ['local.get', `$${acc}`]], 'f64')
1358
1572
  }
@@ -1391,23 +1605,6 @@ export default (ctx) => {
1391
1605
  return typed(['block', ['result', 'f64'], recv.setup, cb.setup, ...loop, ['f64.const', 0]], 'f64')
1392
1606
  }
1393
1607
 
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
1608
  // .reverse() → in-place swap arr[i] ↔ arr[len-1-i], returns the array.
1412
1609
  ctx.core.emit['.reverse'] = (arr) => {
1413
1610
  const recv = hoistArrayValue(arr)
@@ -1697,6 +1894,10 @@ export default (ctx) => {
1697
1894
  body.push(out.ptr)
1698
1895
  return typed(['block', ['result', 'f64'], ...body], 'f64')
1699
1896
  }
1897
+ // Unqualified alias so an untyped-receiver `.concat` gets emit's runtime
1898
+ // string-vs-array ptr-type branch (string → `.string:concat`, array → this),
1899
+ // mirroring `.at`. A known-ARRAY receiver still dispatches here directly.
1900
+ ctx.core.emit['.concat'] = ctx.core.emit['.array:concat']
1700
1901
 
1701
1902
  // .flat() → flatten one level of nested arrays
1702
1903
  ctx.core.stdlib['__arr_flat'] = `(func $__arr_flat (param $src i64) (result f64)