jz 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
package/layout.js CHANGED
@@ -41,6 +41,29 @@ export const PTR = {
41
41
  /** Reserved atom aux ids (PTR.ATOM). */
42
42
  export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
43
43
 
44
+ /** Tags whose heap block can relocate on growth (ARRAY/HASH/SET/MAP) — leaving a
45
+ * forwarding header that `__ptr_offset` must follow. `(1 << tag) & FORWARDING_MASK`
46
+ * tests membership in one shl+and, replacing a 4-way tag-equality OR. */
47
+ export const FORWARDING_MASK = (1 << PTR.ARRAY) | (1 << PTR.HASH) | (1 << PTR.SET) | (1 << PTR.MAP)
48
+
49
+ // BigInt views of the NaN-box fields — the carrier is 64-bit, JS bit-ops are 32-bit.
50
+ const TAG_SHIFT = BigInt(LAYOUT.TAG_SHIFT), TAG_MASK = BigInt(LAYOUT.TAG_MASK)
51
+ const AUX_SHIFT = BigInt(LAYOUT.AUX_SHIFT), AUX_MASK = BigInt(LAYOUT.AUX_MASK)
52
+ const OFFSET_MASK = BigInt(LAYOUT.OFFSET_MASK)
53
+
54
+ /** Format an i64 BigInt as a zero-padded `0x…` hex literal for WAT/IR templates. */
55
+ export const i64Hex = bits => '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
56
+
57
+ /** Pack (type, aux, offset) into the i64 NaN-box carrier — the single source of
58
+ * truth for pointer bit layout. Compiler IR (`packPtrBits`/`mkPtrIR`), the
59
+ * `$__mkptr` inline specializer, and the interop high-word encoder all derive
60
+ * from this. `offset` defaults to 0 to yield the box prefix (offset OR'd later). */
61
+ export const ptrBits = (type, aux = 0, offset = 0) =>
62
+ LAYOUT.NAN_PREFIX_BITS
63
+ | ((BigInt(type) & TAG_MASK) << TAG_SHIFT)
64
+ | ((BigInt(aux) & AUX_MASK) << AUX_SHIFT)
65
+ | (BigInt(offset >>> 0) & OFFSET_MASK)
66
+
44
67
  // =============================================================================
45
68
  // PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
46
69
  // field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
@@ -89,20 +112,19 @@ export function ctorFromElemAux(aux) {
89
112
  return isView ? `new.${name}.view` : `new.${name}`
90
113
  }
91
114
 
92
- /** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop). */
115
+ /** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop) — the
116
+ * top 32 bits of `ptrBits(type, aux)` (offset lives in the low word). */
93
117
  export const encodePtrHi = (type, aux) =>
94
- (0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
118
+ ((LAYOUT.NAN_PREFIX << 16) | ((type & LAYOUT.TAG_MASK) << (LAYOUT.TAG_SHIFT - 32)) | (aux & LAYOUT.AUX_MASK)) >>> 0
95
119
 
96
- export const decodePtrType = hi => (hi >>> 15) & 0xF
97
- export const decodePtrAux = hi => hi & 0x7FFF
120
+ export const decodePtrType = hi => (hi >>> (LAYOUT.TAG_SHIFT - 32)) & LAYOUT.TAG_MASK
121
+ export const decodePtrAux = hi => hi & LAYOUT.AUX_MASK
98
122
 
99
123
  /** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
100
- export const nanPrefixHex = () =>
101
- '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
124
+ export const nanPrefixHex = () => i64Hex(LAYOUT.NAN_PREFIX_BITS)
102
125
 
103
126
  /** Atom sentinel as i64 hex (compiler WAT templates). */
104
- export const atomNanHex = atomId =>
105
- '0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
127
+ export const atomNanHex = atomId => i64Hex(LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << AUX_SHIFT))
106
128
 
107
129
  /** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
108
130
  * CANONICAL interned string — the static-pool copy (or an intern-table hit
@@ -115,22 +137,16 @@ export const atomNanHex = atomId =>
115
137
  export const STR_INTERN_BIT = 0x1
116
138
 
117
139
  /** Pre-shifted STRING SSO aux bit as i64 hex. */
118
- export const ssoBitI64Hex = () =>
119
- '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
140
+ export const ssoBitI64Hex = () => i64Hex(BigInt(LAYOUT.SSO_BIT) << AUX_SHIFT)
120
141
 
121
142
  /** Pre-shifted STRING slice/view aux bit as i64 hex. */
122
- export const sliceBitI64Hex = () =>
123
- '0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
143
+ export const sliceBitI64Hex = () => i64Hex(BigInt(LAYOUT.SLICE_BIT) << AUX_SHIFT)
124
144
 
125
145
  /** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
126
- export const ptrNanHex = (ptrType, aux = 0) =>
127
- '0x' + ptrBoxPrefixBigInt(ptrType, aux).toString(16).toUpperCase().padStart(16, '0')
146
+ export const ptrNanHex = (ptrType, aux = 0) => i64Hex(ptrBits(ptrType, aux))
128
147
 
129
148
  /** Compile-time i64 prefix for mkPtrIR (before offset OR). */
130
- export const ptrBoxPrefixBigInt = (ptrType, aux = 0) =>
131
- (0x7FF8n << 48n)
132
- | ((BigInt(ptrType) & 0xFn) << 47n)
133
- | ((BigInt(aux) & 0x7FFFn) << 32n)
149
+ export const ptrBoxPrefixBigInt = (ptrType, aux = 0) => ptrBits(ptrType, aux)
134
150
 
135
151
  /** Host-side atom sentinel high-u32 values (interop f64 decode). */
136
152
  export const ATOM_HI = {
package/module/array.js CHANGED
@@ -10,13 +10,13 @@
10
10
 
11
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
12
  import { inBoundsArrIdx } from '../src/type.js'
13
- import { emit, spread, deps } from '../src/bridge.js'
13
+ import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
14
14
  import { valTypeOf } from '../src/kind.js'
15
15
  import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
16
- import { staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/static.js'
16
+ import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey, intLiteralValue } from '../src/static.js'
17
17
  import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
18
18
  import { structInline } from '../src/abi/index.js'
19
- import { ctx, inc, err, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
19
+ import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
20
20
  import { strHashLiteral } from './collection.js'
21
21
 
22
22
 
@@ -33,7 +33,7 @@ function allocArray(len, cap) {
33
33
  * only an 8-byte header left off-16 pointing at adjacent data-segment bytes, so
34
34
  * for-in / named-prop lookup (which read off-16 as the props-sidecar pointer)
35
35
  * walked garbage → OOB (test262 built-ins/Object/keys sparse-array). */
36
- function staticArrayPtr(slots) {
36
+ export function staticArrayPtr(slots) {
37
37
  if (!ctx.runtime.data) ctx.runtime.data = ''
38
38
  while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
39
39
  const headerOff = ctx.runtime.data.length
@@ -249,9 +249,9 @@ export default (ctx) => {
249
249
  '__ptr_offset',
250
250
  ...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
251
251
  ],
252
- __arr_fill: ['__ptr_offset'],
252
+ __arr_fill: ['__ptr_offset', '__clamp_idx'], // body-calls __clamp_idx; declare it (self-host auto-scan can't be relied on — see test/selfhost-includes.js)
253
253
  __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
254
- __arr_push1: ['__arr_grow_known', '__ptr_offset'],
254
+ __arr_push1: ['__arr_grow_known', '__ptr_offset_fwd'],
255
255
  __arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
256
256
  __arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
257
257
  __arr_splice: ['__arr_grow', '__len', '__ptr_offset', '__alloc_hdr', '__mkptr'],
@@ -333,67 +333,6 @@ export default (ctx) => {
333
333
  // Full body handles TYPED element types and view indirection since external host can
334
334
  // pass typed arrays even when typedarray module isn't loaded. When features.typedarray
335
335
  // and features.external are both off, collapses to ARRAY-only f64 indexing.
336
- ctx.core.stdlib['__typed_idx'] = () => {
337
- if (!ctx.features.typedarray && !ctx.features.external) {
338
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
339
- (local $len i32)
340
- (local.set $len (call $__len (local.get $ptr)))
341
- (if (result f64)
342
- (i32.or
343
- (i32.lt_s (local.get $i) (i32.const 0))
344
- (i32.ge_u (local.get $i) (local.get $len)))
345
- (then (f64.const nan:${UNDEF_NAN}))
346
- (else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
347
- }
348
- // Hot (~37M calls in watr self-host). Type/aux/offset extracted once from $ptr.
349
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
350
- (local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
351
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
352
- (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
353
- ;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
354
- (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
355
- (then
356
- ${followForwardingWat('$off', { lowGuard: false })}
357
- (return (if (result f64)
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)))))
359
- (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
360
- (else (f64.const nan:${UNDEF_NAN}))))))
361
- (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
362
- (if
363
- (i32.and
364
- (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
365
- (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
366
- (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
367
- (local.set $len (call $__len (local.get $ptr)))
368
- (if (result f64)
369
- (i32.or
370
- (i32.lt_s (local.get $i) (i32.const 0))
371
- (i32.ge_u (local.get $i) (local.get $len)))
372
- (then (f64.const nan:${UNDEF_NAN}))
373
- (else
374
- (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
375
- (then
376
- (local.set $et (i32.and (local.get $aux) (i32.const 7)))
377
- (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
378
- (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
379
- (then (if (result f64) (i32.and (local.get $aux) (i32.const 16))
380
- (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
381
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
382
- (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
383
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
384
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
385
- (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
386
- (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
387
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
388
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
389
- (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
390
- (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
391
- (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
392
- (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
393
- (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
394
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
395
- }
396
-
397
336
  // Array.from(src) — shallow copy of array (memory.copy of f64 elements)
398
337
  ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src i64) (result f64)
399
338
  (local $len i32) (local $dst i32)
@@ -576,12 +515,13 @@ export default (ctx) => {
576
515
  ctx.core.stdlib['__arr_push1'] = `(func $__arr_push1 (param $ptr i64) (param $val f64) (result f64)
577
516
  (local $p f64) (local $base i32) (local $len i32)
578
517
  (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
579
- (local.set $base (call $__ptr_offset (local.get $ptr)))
518
+ (local.set $base (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
519
+ ${followForwardingWat('$base', { lowGuard: true })}
580
520
  (local.set $len (i32.load (i32.sub (local.get $base) (i32.const 8))))
581
521
  (if (i32.lt_s (i32.load (i32.sub (local.get $base) (i32.const 4))) (i32.add (local.get $len) (i32.const 1)))
582
522
  (then
583
523
  (local.set $p (call $__arr_grow_known (local.get $ptr) (i32.add (local.get $len) (i32.const 1))))
584
- (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
524
+ (local.set $base (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $p)) (i64.const ${LAYOUT.OFFSET_MASK}))))))
585
525
  (f64.store (i32.add (local.get $base) (i32.shl (local.get $len) (i32.const 3))) (local.get $val))
586
526
  (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
587
527
  (local.get $p))`
@@ -625,8 +565,14 @@ export default (ctx) => {
625
565
  if (!hasSpread) {
626
566
  const len = elems.length
627
567
  // R: Static data segment for arrays of pure-literal elements (own-memory only).
628
- // Saves N×(alloc+store) instructions in __start; raw f64 bits embedded directly.
629
- if (len >= 4 && !ctx.memory.shared) {
568
+ // Raw f64 bits embedded directly a constant array becomes a const pointer with no
569
+ // alloc and no per-element store. A static array aliases ONE shared data-segment
570
+ // region, so this is sound only when no caller expects a fresh instance per
571
+ // evaluation: at module scope the literal runs exactly once. A function-local
572
+ // literal (which would leak in-place mutations across calls — a latent bug the old
573
+ // len≥4 gate also had) allocs fresh instead. Module scope lifts the size floor too,
574
+ // so `const x = [1, 2, 3]` is a data segment, not an alloc.
575
+ if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
630
576
  // asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
631
577
  const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
632
578
  if (slots.every(b => b !== null)) return staticArrayPtr(slots)
@@ -651,6 +597,14 @@ export default (ctx) => {
651
597
  // === Index read ===
652
598
 
653
599
  ctx.core.emit['[]'] = (arr, idx) => {
600
+ // A literal NEGATIVE index is always out of range → undefined (JS semantics), never a
601
+ // raw `payload + (-1)*8` load that reads heap before the allocation. A side-effecting
602
+ // receiver still evaluates. Mirrors VT['[]'] returning null for the same case.
603
+ { const li = intLiteralValue(idx)
604
+ if (li != null && li < 0)
605
+ return typeof arr === 'string'
606
+ ? undefExpr()
607
+ : typed(['block', ['result', 'f64'], ['drop', asF64(emit(arr))], undefExpr()], 'f64') }
654
608
  // Hoist non-identifier arr so side-effecting sources (e.g. `foo.shift()[i]`) execute once.
655
609
  // The rest of the handler inlines `emit(arr)` into multiple IR positions, which would
656
610
  // otherwise re-execute the source expression per use at runtime.
@@ -676,10 +630,13 @@ export default (ctx) => {
676
630
  const litKey = isLiteralStr(idx) ? idx[1]
677
631
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
678
632
  : null
679
- // SRoA flat object: `o['k']` → `local.get $o#i` (analyze.js scanFlatObjects).
680
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
633
+ // SRoA flat object/array: `o['k']` / `a[2]` → `local.get $o#i` (scanFlatObjects).
634
+ // A bare integer index resolves its slot key here (not via `litKey`, which stays
635
+ // null for arrays so the heap-array / schema paths below are untouched).
636
+ if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
681
637
  const fo = ctx.func.flatObjects.get(arr)
682
- const fi = fo.names.indexOf(litKey)
638
+ const flatKey = litKey != null ? litKey : staticIndexKey(idx)
639
+ const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
683
640
  if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
684
641
  }
685
642
  if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
@@ -700,14 +657,34 @@ export default (ctx) => {
700
657
  // Multi-value calls are materialized at call site (see '()' handler), so
701
658
  // func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
702
659
  const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
703
- const va = emit(arr), vi = asI32(emit(idx))
660
+ // Literal string key on a receiver that isn't a known array/typed/string:
661
+ // `x['a']` IS `x.a` — delegate to the dot emitter so an untyped/OBJECT/external
662
+ // receiver gets the same polymorphic dispatch (__dyn_get_any_t_h, host-external
663
+ // aware). The local `dynLoad` fallback below calls __dyn_get, which only probes
664
+ // the internal HASH layout — so `x['a']` on a host object silently returned
665
+ // undefined while `x.a` worked. (Known ARRAY/TYPED/STRING fall through unchanged:
666
+ // their string-key semantics differ from a HASH/OBJECT property read.)
667
+ if (litKey != null && vt !== VAL.ARRAY && vt !== VAL.TYPED && vt !== VAL.STRING)
668
+ return emit(['.', arr, litKey])
669
+ // emitIndex (not bare asI32(emit)) narrows integer index arithmetic — incl. a
670
+ // literal term like the `+1` of `a[i*W + x + 1]` — to i32 ops instead of the
671
+ // f64 convert/trunc round-trip. Non-i32 keys (string dispatch) fall back to
672
+ // asI32(emit) inside emitIndex, so this is a strict improvement for every branch.
673
+ const va = emit(arr), vi = emitIndex(idx)
704
674
  const ptrExpr = asF64(va)
705
675
  const dynLoad = (objExpr, keyExpr) => {
706
676
  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 }.`)
677
+ 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.`)
707
678
  inc('__dyn_get')
708
679
  return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
709
680
  }
710
681
  const stringLoad = () => (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], vi])
682
+ // A numeric index on an unknown receiver is array/typed access by design — kept
683
+ // lean (no OBJECT/HASH dyn-get fork): an object with numeric keys is a degenerate
684
+ // pattern not worth a per-access string-coercion + hash probe in every hot loop.
685
+ // The WRITE path still routes a numeric `o[i]=v` on an OBJECT to __dyn_set for
686
+ // SAFETY (no schema-slot corruption / OOB), so such a read returns undefined
687
+ // rather than corrupting — matching JS for an out-of-range typed/array index.
711
688
  const arrayLoad = (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], vi])
712
689
  const emitDynamicKeyDispatch = (objExpr, numericLoad) => {
713
690
  const keyTmp = temp()
@@ -753,6 +730,16 @@ export default (ctx) => {
753
730
  if (keyType === VAL.STRING)
754
731
  return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
755
732
  if (vt === 'array') {
733
+ // Base offset of the array's data region. A binding proven never relocated
734
+ // (scanNeverGrown — a fresh array literal whose every use is a pure read, so no
735
+ // grow op can ever run) skips the realloc-forwarding follow: its base is the raw
736
+ // post-header offset `wrap(reinterpret(ptr) & OFFSET_MASK)`, no __ptr_offset call.
737
+ // Memory-safe ONLY under that proof — a relocated array read through this stale
738
+ // base would corrupt memory (see scanNeverGrown's default-deny rationale).
739
+ const neverGrown = typeof arr === 'string' && ctx.func.localReps?.get(arr)?.neverGrown === true
740
+ const arrBase = () => neverGrown
741
+ ? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ptrExpr], ['i64.const', LAYOUT.OFFSET_MASK]]]
742
+ : (inc('__ptr_offset'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]])
756
743
  // structInline Array<S>: element i is K consecutive inline f64 schema
757
744
  // cells — no per-row heap object, no stored element pointer. `arr[i]` is
758
745
  // the byte address of the element's first cell, returned as a first-class
@@ -761,11 +748,10 @@ export default (ctx) => {
761
748
  // structInline handles (src/analyze.js analyzeStructInline).
762
749
  const inlSid = inlineArraySid(arr)
763
750
  if (inlSid != null) {
764
- inc('__ptr_offset')
765
751
  const baseI32 = tempI32('ab')
766
752
  const K = ctx.schema.list[inlSid].length
767
753
  const cell = typed(structInline(K).ops.elemAddr(
768
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
754
+ ['local.tee', `$${baseI32}`, arrBase()],
769
755
  vi), 'i32')
770
756
  cell.ptrKind = VAL.OBJECT
771
757
  cell.ptrAux = inlSid
@@ -775,51 +761,51 @@ export default (ctx) => {
775
761
  // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
776
762
  // plus type-dispatch overhead irrelevant for plain arrays).
777
763
  const keyIsNum = keyType === VAL.NUMBER
778
- // Inline fast path: when arr has a known element schema and key is a
779
- // known number, the type-tag check and bounds check inside __arr_idx are
780
- // dead weight in hot kernels. Emit (f64.load (i32.add base (shl i 3)))
781
- // directly. base goes through __ptr_offset (still does forwarding follow),
782
- // and hoistAddrBase will CSE the (base, i) pair across the iteration body.
783
- // Array<NUMBER>/<STRING>/<OBJECT> all use 8-byte f64 slot layout direct
784
- // f64.load is correct regardless of elem kind (returns raw f64 for NUMBER,
785
- // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
786
- // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
787
- // (Array<NUMBER> from `.map(x => x*k)` etc.) both are proven facts from
788
- // the narrowing fixpoint (carried onto params via paramReps).
789
- const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
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
764
+ // Inline fast path for any known plain ARRAY + numeric key: the type-tag
765
+ // dispatch and bounds check inside __arr_idx(_known) are dead weight in hot
766
+ // kernels most visibly AST walkers doing `node[i]` over heterogeneous
767
+ // arrays, where no element schema/valType is ever inferred. Emit the
768
+ // f64.load directly. base goes through __ptr_offset (still the forwarding
769
+ // follow), and hoistAddrBase CSEs the (base, i) pair across the iteration
770
+ // body. taggedLinear stores every element as one 8-byte f64 cell
771
+ // Array<NUMBER>/<STRING>/<OBJECT> alike so a direct f64.load is correct
772
+ // regardless of elem kind (raw f64 for NUMBER, NaN-boxed pointer for
773
+ // OBJECT/STRING; downstream typed() handles both). The load shape is fixed
774
+ // by the carrier, not by any rep fact, so we do NOT gate on a known element
775
+ // schema/valType: a bare `let a = [...]` walked by index gets the same
776
+ // inline load as an Array<{x,y,z}>. (structInline arrays returned above via
777
+ // inlineArraySid; typed arrays are VAL.TYPED, handled below.)
778
+ //
779
+ // Take the UNCHECKED inline load only when the index is proven in-bounds by
780
+ // an enclosing canonical loop `for (let i=C; i<arr.length; i++)` a pure
781
+ // index<length structural proof (scanBoundedArrIdx, src/type.js), itself
782
+ // element-kind-independent. Skipping the bounds check on an arbitrary numeric
783
+ // index is unsound: `a[1]` on a length-1 array would read the raw cell instead
784
+ // of undefined; those fall through to the inline bounds-checked load below.
785
+ const idxProvenInBounds = keyIsNum
797
786
  && typeof arr === 'string' && typeof idx === 'string'
798
787
  && inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
799
788
  if (idxProvenInBounds) {
800
- inc('__ptr_offset')
801
- // __ptr_offset returns i32 — base local must be i32 (not the default
802
- // f64 NaN-box temp). Flat tee form so downstream peepholes can fold
789
+ // base local must be i32. Flat tee form so downstream peepholes can fold
803
790
  // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
804
791
  // when this load feeds a ptrUnboxed OBJECT field.
805
792
  const baseI32 = tempI32('ab')
806
793
  return typed(ctx.abi.array.ops.load(
807
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
794
+ ['local.tee', `$${baseI32}`, arrBase()],
808
795
  vi), 'f64')
809
796
  }
810
- // Known-elem array, numeric key, NOT proven in-bounds → inline bounds-checked
797
+ // Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
811
798
  // load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
812
799
  // inline, so watr hoists the loop-invariant len load and CSEs the base — the
813
800
  // residual cost is a single (predictable) compare per access, not a call. Skipping
814
801
  // 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')
802
+ if (keyIsNum) {
817
803
  const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
818
804
  return typed(['if', ['result', 'f64'],
819
805
  ['i32.lt_u',
820
806
  ['local.tee', `$${idxI32}`, vi],
821
807
  ['i32.load', ['i32.sub',
822
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
808
+ ['local.tee', `$${baseI32}`, arrBase()],
823
809
  ['i32.const', 8]]]],
824
810
  ['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
825
811
  ['else', undefExpr()]], 'f64')
@@ -897,6 +883,10 @@ export default (ctx) => {
897
883
 
898
884
  // .push(val) → append, increment len, return array (possibly reallocated pointer)
899
885
  ctx.core.emit['.push'] = (arr, ...vals) => {
886
+ // `_expect` is overwritten by recursive emit() calls below. Capture the
887
+ // statement-position hint now so a dropped `xs.push(v)` can skip computing
888
+ // the JS return length while still performing the mutation/writeback.
889
+ const void_ = ctx.func._expect === 'void'
900
890
  // structInline Array<S>: `.push({S})` writes the K schema fields as K
901
891
  // consecutive f64 cells. Flatten the struct literal into K schema-ordered
902
892
  // field-value nodes and fall through to the general multi-value store path
@@ -924,8 +914,10 @@ export default (ctx) => {
924
914
  const readVar = box ? ['f64.load', ['local.get', `$${box}`]] : isGlobal ? ['global.get', `$${arr}`] : ['local.get', `$${arr}`]
925
915
  const writeVar = v => box ? ['f64.store', ['local.get', `$${box}`], v] : isGlobal ? ['global.set', `$${arr}`, v] : ['local.set', `$${arr}`, v]
926
916
  const vv = asF64(emit(vals[0]))
917
+ const pushed = ['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]
918
+ if (void_) return typed(['block', writeVar(pushed)], 'void')
927
919
  return typed(['block', ['result', 'f64'],
928
- writeVar(['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]),
920
+ writeVar(pushed),
929
921
  ['f64.convert_i32_s', ['i32.load', ['i32.sub',
930
922
  ['call', '$__ptr_offset', ['i64.reinterpret_f64', readVar]], ['i32.const', 8]]]]], 'f64')
931
923
  }
@@ -1077,12 +1069,8 @@ export default (ctx) => {
1077
1069
  (if (i32.ge_u (local.get $off) (i32.const 8))
1078
1070
  (then
1079
1071
  (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))))
1072
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
1073
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
1086
1074
  (local.set $i (local.get $start))
1087
1075
  (block $done (loop $fill
1088
1076
  (br_if $done (i32.ge_s (local.get $i) (local.get $end)))