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
@@ -8,11 +8,13 @@
8
8
  * @module collection
9
9
  */
10
10
 
11
- import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
12
- import { emit, emitFlat } from '../src/emit.js'
13
- import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
14
- import { hasOwnContinue } from '../src/analyze.js'
15
- import { inc, PTR, LAYOUT } from '../src/ctx.js'
11
+ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad } from '../src/ir.js'
12
+ import { emit, deps, call } from '../src/bridge.js'
13
+ import { valTypeOf } from '../src/kind.js'
14
+ import { VAL, lookupValType } from '../src/reps.js'
15
+ import { hasOwnContinue, isBlockBody } from '../src/ast.js'
16
+ import { ctx, inc, PTR, LAYOUT, getter, declGlobal } from '../src/ctx.js'
17
+ import { STR_INTERN_BIT } from '../layout.js'
16
18
 
17
19
  const SET_ENTRY = 16 // hash + key
18
20
  const MAP_ENTRY = 24 // hash + key + value
@@ -45,6 +47,20 @@ function numConstLiteral(expr) {
45
47
  const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
46
48
  const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
47
49
 
50
+ // Hash-first probe guard: each slot stores the key's hash in its low 32 bits
51
+ // (high 32 = insertion seq), so a collision step can reject on one i32 compare
52
+ // instead of a full key compare. Equal keys always have equal stored hashes
53
+ // (the insert wrote strHash/mapHash of that very key), so the guard never
54
+ // skips a true match — it only skips the ~all-of-them unequal probes that were
55
+ // walking shared-prefix bytes through __str_eq (31M unequal content-compares
56
+ // per self-host bench run before this). If-expression for short-circuit:
57
+ // i32.and would still evaluate the call.
58
+ const hashGuard = (eqExpr) =>
59
+ `(if (result i32) (i32.eq (i32.load (local.get $slot)) (local.get $h))
60
+ (then ${eqExpr}) (else (i32.const 0)))`
61
+ const strEqG = hashGuard(strEq)
62
+ const sameValueZeroEqG = hashGuard(sameValueZeroEq)
63
+
48
64
  // Open-addressing probe walked additively by entrySize: avoids an i32.mul + mask per
49
65
  // step (vs recomputing slot = off + idx*entrySize). Needs $off/$cap/$h set and $end/$slot
50
66
  // locals declared. `idxExpr` is the first-slot index (defaults to h mod cap; cap is pow2).
@@ -55,15 +71,36 @@ const probeNext = (entrySize) =>
55
71
  `(local.set $slot (i32.add (local.get $slot) (i32.const ${entrySize})))
56
72
  (if (i32.ge_u (local.get $slot) (local.get $end)) (then (local.set $slot (local.get $off))))`
57
73
 
58
- /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
59
- * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
60
- * Gated off type mismatch just returns coll unchanged. */
74
+ // Store a fresh entry's hash word, packing a monotonic insertion sequence
75
+ // (global $__seq) into its free high 32 bits. The hash itself only ever occupies
76
+ // the low 32 (always ≥2), so "empty slot ⇔ word==0" and the i32.wrap_i64
77
+ // home-bucket math are untouched; rehash/back-shift copy the whole word, so the
78
+ // sequence rides along for free. Iteration reads it back (via __coll_order) to
79
+ // restore JS insertion order. Emitted only on the insert-new branch — updates
80
+ // keep the original entry (and its sequence) in place.
81
+ const seqStore = `(i64.store (local.get $slot)
82
+ (i64.or (i64.extend_i32_u (local.get $h)) (i64.shl (i64.extend_i32_u (global.get $__seq)) (i64.const 32))))
83
+ (global.set $__seq (i32.add (global.get $__seq) (i32.const 1)))`
84
+
85
+ /** Generate upsert (add/set) probe for a growable collection (Set/Map). hasVal: store
86
+ * value at slot+16. hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching
87
+ * type). Gated off → type mismatch just returns coll unchanged.
88
+ *
89
+ * The table grows at 75% load by allocating a 2× table, rehashing, and forward-marking
90
+ * the old header (cap=-1 sentinel, new offset at -8) — the array growth idiom. The boxed
91
+ * pointer the caller holds is returned UNCHANGED; future ops resolve it through
92
+ * __ptr_offset, which follows the chain. This is why Set/Map (held in caller locals, and
93
+ * possibly aliased) forward rather than remint like HASH (whose pointer lives in a single
94
+ * owner's propsPtr slot that genUpsertGrow can rewrite). */
61
95
  function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
62
96
  const valParam = hasVal ? '(param $val i64) ' : ''
63
97
  const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))` : ''
64
98
  const onMatch = hasVal
65
99
  ? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
66
100
  : `(then (br $done))`
101
+ const rehashVal = hasVal
102
+ ? `\n (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))`
103
+ : ''
67
104
 
68
105
  const extBranch = hasVal
69
106
  ? '(then (call $__ext_set (local.get $coll) (local.get $key) (local.get $val)) drop)'
@@ -74,15 +111,47 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
74
111
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
75
112
  return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
76
113
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
114
+ (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
115
+ (local $oldslot i32) (local $newidx i32) (local $newslot i32)
77
116
  ${typeGuard}
78
- (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
117
+ (local.set $off (call $__ptr_offset (local.get $coll)))
79
118
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
119
+ (local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
120
+ ;; Grow at 75% load (size*4 >= cap*3): 2× table, rehash, forward-mark old header.
121
+ (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
122
+ (then
123
+ (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
124
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
125
+ (i64.store (i32.sub (local.get $newptr) (i32.const 16)) (i64.load (i32.sub (local.get $off) (i32.const 16))))
126
+ (local.set $i (i32.const 0))
127
+ (block $rd (loop $rl
128
+ (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
129
+ (local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
130
+ (if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
131
+ (then
132
+ (local.set $h (call ${hashFn} (i64.load (i32.add (local.get $oldslot) (i32.const 8)))))
133
+ (local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
134
+ (block $ins (loop $probe2
135
+ (local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
136
+ (br_if $ins (i64.eqz (i64.load (local.get $newslot))))
137
+ (local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
138
+ (br $probe2)))
139
+ (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
140
+ (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))${rehashVal}
141
+ (i32.store (i32.sub (local.get $newptr) (i32.const 8))
142
+ (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
143
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
144
+ (br $rl)))
145
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
146
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
147
+ (local.set $off (local.get $newptr))
148
+ (local.set $cap (local.get $newcap))))
80
149
  (local.set $h (call ${hashFn} (local.get $key)))
81
150
  ${probeStart(entrySize)}
82
151
  (block $done (loop $probe
83
152
  (if (i64.eqz (i64.load (local.get $slot)))
84
153
  (then
85
- (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
154
+ ${seqStore}
86
155
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
87
156
  (i32.store (i32.sub (local.get $off) (i32.const 8))
88
157
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
@@ -117,11 +186,14 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
117
186
  : '(call $__ext_has (local.get $coll) (local.get $key))'}))
118
187
  (else ${onEmpty}))))`
119
188
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
189
+ // SET/MAP/HASH all grow by forward-marking the old header (genUpsert / genUpsertGrow
190
+ // with forward=true), so a boxed pointer may be stale → resolve through the chain.
191
+ const offExpr = '(call $__ptr_offset (local.get $coll))'
120
192
 
121
193
  return `(func $${name} (param $coll i64) (param $key i64) (result ${rt})
122
194
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
123
195
  ${typeGuard}
124
- (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
196
+ (local.set $off ${offExpr})
125
197
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
126
198
  (local.set $h (call ${hashFn} (local.get $key)))
127
199
  ${probeStart(entrySize)}
@@ -135,27 +207,55 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
135
207
  ${notFound})`
136
208
  }
137
209
 
138
- /** Generate delete probe function. Zero out entry on match. */
210
+ /** Generate delete probe function. Backward-shift deletion: after removing an entry,
211
+ * pull back any following entry whose home slot lies outside the opened gap, so the
212
+ * "empty slot ⇒ end of probe chain" invariant holds without tombstones. Returns 1 if
213
+ * the key was present (and len decremented), 0 otherwise. Home slots are recomputed
214
+ * from the stored hash (low 32 bits), so no rehash of the key is needed during the shift. */
139
215
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
140
216
  return `(func $${name} (param $coll i64) (param $key i64) (result i32)
141
217
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
218
+ (local $i i32) (local $j i32) (local $k i32) (local $n i32)
142
219
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
143
220
  (local.set $off (call $__ptr_offset (local.get $coll)))
144
221
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
145
222
  (local.set $h (call ${hashFn} (local.get $key)))
146
223
  ${probeStart(entrySize)}
147
- (block $done (loop $probe
148
- (if (i64.eqz (i64.load (local.get $slot))) (then (return (i32.const 0))))
149
- (if ${eqExpr}
150
- (then
151
- (i64.store (local.get $slot) (i64.const 0))
152
- (i64.store (i32.add (local.get $slot) (i32.const 8)) (i64.const 0))
153
- (return (i32.const 1))))
154
- ${probeNext(entrySize)}
155
- (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
156
- (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
157
- (br $probe)))
158
- (i32.const 0))`
224
+ (block $found
225
+ (block $absent (loop $probe
226
+ (if (i64.eqz (i64.load (local.get $slot))) (then (br $absent)))
227
+ (if ${eqExpr} (then (br $found)))
228
+ ${probeNext(entrySize)}
229
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
230
+ (br_if $absent (i32.ge_s (local.get $tries) (local.get $cap)))
231
+ (br $probe)))
232
+ (return (i32.const 0)))
233
+ ;; $slot holds the entry to remove. Walk forward; move back any entry whose home
234
+ ;; is not cyclically within (i, j], else it would become unreachable from its home.
235
+ (local.set $i (local.get $slot))
236
+ (local.set $j (local.get $slot))
237
+ (block $stop (loop $shift
238
+ (local.set $j (i32.add (local.get $j) (i32.const ${entrySize})))
239
+ (if (i32.ge_u (local.get $j) (local.get $end)) (then (local.set $j (local.get $off))))
240
+ (br_if $stop (i64.eqz (i64.load (local.get $j))))
241
+ ;; Empty slot ends the cluster (load < 100%). A 100%-full table has none — lookups
242
+ ;; tolerate that via the $tries<cap bound, so delete must too: after $cap advances $j
243
+ ;; has cycled back to the gap origin; stop and clear the final gap.
244
+ (local.set $n (i32.add (local.get $n) (i32.const 1)))
245
+ (br_if $stop (i32.ge_u (local.get $n) (local.get $cap)))
246
+ (local.set $k (i32.add (local.get $off)
247
+ (i32.mul (i32.and (i32.wrap_i64 (i64.load (local.get $j))) (i32.sub (local.get $cap) (i32.const 1))) (i32.const ${entrySize}))))
248
+ (if (i32.le_u (local.get $i) (local.get $j))
249
+ (then (br_if $shift (i32.and (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j)))))
250
+ (else (br_if $shift (i32.or (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j))))))
251
+ (memory.copy (local.get $i) (local.get $j) (i32.const ${entrySize}))
252
+ (local.set $i (local.get $j))
253
+ (br $shift)))
254
+ (i64.store (local.get $i) (i64.const 0))
255
+ (i64.store (i32.add (local.get $i) (i32.const 8)) (i64.const 0))
256
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
257
+ (i32.sub (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
258
+ (i32.const 1))`
159
259
  }
160
260
 
161
261
  /** Generate growable upsert. Grows table at 75% load, rehashes, then inserts.
@@ -163,7 +263,7 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
163
263
  * strict=false: EXTERNAL → __ext_set, other non-HASH types → __dyn_set (global props).
164
264
  * The non-strict fallback is critical for untyped variables (e.g. arrays from
165
265
  * Object.create) that receive property writes — without it writes silently vanish. */
166
- function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = false, hasExt = false) {
266
+ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = false, hasExt = false, forward = false) {
167
267
  const nonHashFallback = hasExt
168
268
  ? `(if (i32.eq (call $__ptr_type (local.get $obj)) (i32.const ${PTR.EXTERNAL}))
169
269
  (then (call $__ext_set (local.get $obj) (local.get $key) (local.get $val)) drop)
@@ -209,16 +309,27 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
209
309
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
210
310
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
211
311
  (br $rl)))
312
+ ${forward
313
+ // Forward-mark the old header (cap=-1 sentinel at -4, new offset at -8) and
314
+ // keep the boxed pointer the caller holds: any alias resolves through
315
+ // __ptr_offset. This preserves JS reference identity for a grown dict held in
316
+ // multiple places (e.g. ctx.core.emit), which remint cannot.
317
+ ? `(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
318
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
212
319
  (local.set $off (local.get $newptr))
320
+ (local.set $cap (local.get $newcap))`
321
+ // Remint: hand back a fresh boxed pointer. Only safe when a single owner
322
+ // (a local threaded via the return, or the global __dyn_props) is updated.
323
+ : `(local.set $off (local.get $newptr))
213
324
  (local.set $cap (local.get $newcap))
214
- (local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))))
325
+ (local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))`}))
215
326
  ;; Insert/update
216
327
  (local.set $h (call ${hashFn} (local.get $key)))
217
328
  ${probeStart(entrySize)}
218
329
  (block $done (loop $probe
219
330
  (if (i64.eqz (i64.load (local.get $slot)))
220
331
  (then
221
- (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
332
+ ${seqStore}
222
333
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
223
334
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
224
335
  (i32.store (i32.sub (local.get $off) (i32.const 8))
@@ -240,7 +351,7 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
240
351
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
241
352
  (i32.const ${expectedType}))
242
353
  (then (return (i64.const ${missing}))))
243
- (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
354
+ (local.set $off (call $__ptr_offset (local.get $coll)))
244
355
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
245
356
  (local.set $h (call ${hashFn} (local.get $key)))
246
357
  ${probeStart(entrySize)}
@@ -266,10 +377,12 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
266
377
  (else (return (i64.const ${missing}))))))`
267
378
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
268
379
  (then (return (i64.const ${missing}))))`
380
+ // SET/MAP/HASH grow by forward-marking; a boxed pointer may be stale → follow the chain.
381
+ const offExpr = '(call $__ptr_offset (local.get $coll))'
269
382
  return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result i64)
270
383
  (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
271
384
  ${typeGuard}
272
- (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
385
+ (local.set $off ${offExpr})
273
386
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
274
387
  ${probeStart(entrySize)}
275
388
  (block $done (loop $probe
@@ -291,13 +404,13 @@ function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
291
404
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
292
405
  (i32.const ${expectedType}))
293
406
  (then (return (local.get $obj))))
294
- (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
407
+ (local.set $off (call $__ptr_offset (local.get $obj)))
295
408
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
296
409
  ${probeStart(entrySize)}
297
410
  (block $done (loop $probe
298
411
  (if (i64.eqz (i64.load (local.get $slot)))
299
412
  (then
300
- (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
413
+ ${seqStore}
301
414
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
302
415
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
303
416
  (i32.store (i32.sub (local.get $off) (i32.const 8))
@@ -317,17 +430,18 @@ export default (ctx) => {
317
430
  // Feature-gated deps: EXTERNAL-dependent symbols are only pulled when features.external.
318
431
  // Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
319
432
  const ifExt = (name) => () => ctx.features.external ? [name] : []
320
- Object.assign(ctx.core.stdlibDeps, {
433
+ deps({
321
434
  __same_value_zero: ['__str_eq'],
322
435
  __map_hash: ['__hash', '__str_hash'],
323
- __set_add: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
324
- __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
436
+ __set_add: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n'],
437
+ __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
325
438
  __set_delete: ['__map_hash', '__same_value_zero'],
326
- __map_set: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
327
- __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set'] : ['__map_set'],
328
- __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero'] : ['__same_value_zero'],
329
- __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
439
+ __map_set: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n'],
440
+ __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset'] : ['__map_set', '__ptr_offset'],
441
+ __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset'] : ['__same_value_zero', '__ptr_offset'],
442
+ __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
330
443
  __map_delete: ['__map_hash', '__same_value_zero'],
444
+ __map_from: ['__ptr_type', '__ptr_offset', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
331
445
  __hash_set: () => ctx.features.external
332
446
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
333
447
  : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
@@ -368,8 +482,15 @@ export default (ctx) => {
368
482
 
369
483
  inc('__ptr_offset', '__cap')
370
484
 
485
+ // Monotonic insertion counter packed into each entry's hash-word high 32 bits
486
+ // (see seqStore). Restores JS insertion order at iteration without growing
487
+ // entries or touching the lookup/delete hot paths. i32: wraps after 2^32 total
488
+ // inserts — unreachable in practice; fresh per wasm instance.
489
+ if (!ctx.scope.globals.has('__seq'))
490
+ declGlobal('__seq', 'i32')
491
+
371
492
  if (!ctx.scope.globals.has('__dyn_props'))
372
- ctx.scope.globals.set('__dyn_props', '(global $__dyn_props (mut f64) (f64.const 0))')
493
+ declGlobal('__dyn_props', 'f64')
373
494
  // 1-slot inline cache for the global __dyn_props lookup. Hot path for
374
495
  // metacircular workloads (watr WAT parser): ~96% of execution sits in
375
496
  // __dyn_get_t / __ihash_get_local. Caches last-seen (off → propsPtr) at
@@ -377,15 +498,15 @@ export default (ctx) => {
377
498
  // propsPtr is replaced (rehash on grow). Sentinel cache_off = -1 cannot
378
499
  // collide with a real memory offset (always non-negative i32).
379
500
  if (!ctx.scope.globals.has('__dyn_get_cache_off'))
380
- ctx.scope.globals.set('__dyn_get_cache_off', '(global $__dyn_get_cache_off (mut i32) (i32.const -1))')
501
+ declGlobal('__dyn_get_cache_off', 'i32', -1)
381
502
  if (!ctx.scope.globals.has('__dyn_get_cache_props'))
382
- ctx.scope.globals.set('__dyn_get_cache_props', '(global $__dyn_get_cache_props (mut f64) (f64.const 0))')
503
+ declGlobal('__dyn_get_cache_props', 'f64')
383
504
  // Schema name table for __dyn_get's OBJECT-schema fallback (polymorphic-receiver
384
505
  // `.prop` access). Same declaration as json.js — defined here too so collection
385
506
  // doesn't transitively require json. compile.js's schemaInit populates it when
386
507
  // schema list is non-empty AND (__stringify OR __dyn_get) is included.
387
508
  if (!ctx.scope.globals.has('__schema_tbl'))
388
- ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
509
+ declGlobal('__schema_tbl', 'i32')
389
510
 
390
511
  // __ext_* imports carry NaN-boxed pointers across the env boundary as i64
391
512
  // (not f64) to dodge V8's f64 NaN canonicalization at the wasm↔JS edge —
@@ -455,16 +576,17 @@ export default (ctx) => {
455
576
  const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
456
577
  return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
457
578
  }
458
- // new Set(iterable): seed from an array argument's elements. __set_add does
459
- // SameValueZero dedup + −0 normalization. A non-array argument leaves the set
460
- // empty the ptr_type guard zeroes the length so the loop is skipped.
579
+ // new Set(iterable): __iter_arr normalizes any iterable to an index-iterable
580
+ // dense array (Set→keys, Map→[k,v] entries, Array/String/TypedArray pass
581
+ // through), so a Set/Map/Array source all seed uniformly. __set_add does
582
+ // SameValueZero dedup + −0 normalization. A non-iterable normalizes to a
583
+ // non-array value — the ptr_type guard zeroes the length so the loop is skipped.
461
584
  //
462
- // __set_add is a FIXED-capacity probe (genUpsert never grows the table); a
463
- // full table makes its probe loop spin forever. So the table is pre-sized to
464
- // the smallest power of two greater than 2*len: distinct entries ≤ len, so the
465
- // table stays50% full and every insert finds a free slot. cap = 1 <<
466
- // (32 clz(m−1)) with m = 2*len + INIT_CAP rounds 2*len up to a power of two
467
- // and floors at INIT_CAP for the empty/short case.
585
+ // __set_add grows on demand, but pre-sizing the table to fit the source array
586
+ // skips the rehash churn of building it up from INIT_CAP. cap = 1 << (32
587
+ // clz(m−1)) with m = 2*len + INIT_CAP is the smallest power of two > 2*len:
588
+ // distinct entrieslen, so the table lands ≤50% full and never needs to grow
589
+ // while seeding. Floors at INIT_CAP for the empty/short case.
468
590
  inc('__set_add', '__ptr_type', '__len', '__typed_idx')
469
591
  const setL = temp('nss'), arrL = temp('nsa')
470
592
  const iL = tempI32('nsi'), lenL = tempI32('nsl')
@@ -475,7 +597,7 @@ export default (ctx) => {
475
597
  ['i32.const', 1]]]]]
476
598
  const out = allocPtr({ type: PTR.SET, len: 0, cap: capExpr, stride: SET_ENTRY, tag: 'set' })
477
599
  return typed(['block', ['result', 'f64'],
478
- ['local.set', `$${arrL}`, asF64(emit(iterExpr))],
600
+ ['local.set', `$${arrL}`, asF64(emit(['()', '__iter_arr', iterExpr]))],
479
601
  ['local.set', `$${lenL}`, ['i32.const', 0]],
480
602
  ['if', ['i32.eq',
481
603
  ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]],
@@ -498,28 +620,34 @@ export default (ctx) => {
498
620
  ['local.get', `$${setL}`]], 'f64')
499
621
  }
500
622
 
501
- ctx.core.emit['.add'] = (setExpr, val) => {
502
- inc('__set_add')
503
- return typed(['f64.reinterpret_i64', ['call', '$__set_add', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
504
- }
505
-
506
- ctx.core.emit['.has'] = (setExpr, val) => {
507
- inc('__set_has')
508
- return typed(['f64.convert_i32_s', ['call', '$__set_has', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
509
- }
510
-
511
- ctx.core.emit['.delete'] = (setExpr, val) => {
512
- inc('__set_delete')
513
- return typed(['f64.convert_i32_s', ['call', '$__set_delete', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
623
+ ctx.core.emit['.add'] = call('__set_add', 'II', 'i64')
624
+
625
+ // `.has` / `.delete` exist on BOTH Set and Map, which differ only in entry
626
+ // stride (16 vs 24). A receiver of unproven kind (e.g. a Map read off a nested
627
+ // object field like `ctx.scope.globals`) must resolve MAP vs SET at runtime:
628
+ // the Set probe carries a PTR.SET type guard that rejects a Map outright, so
629
+ // routing a Map through `__set_has`/`__set_delete` makes every lookup/delete
630
+ // silently report absent. Mirrors collViewDyn below; typed receivers skip it.
631
+ const collProbeDyn = (mapFn, setFn) => (collExpr, key) => {
632
+ inc(mapFn, setFn, '__ptr_type')
633
+ const o = temp('cp'), k = tempI64('cpk')
634
+ return typed(['block', ['result', 'f64'],
635
+ ['local.set', `$${o}`, asF64(emit(collExpr))],
636
+ ['local.set', `$${k}`, asI64(emit(key))],
637
+ ['f64.convert_i32_s', ['if', ['result', 'i32'],
638
+ ptrTypeEq(['local.get', `$${o}`], PTR.MAP),
639
+ ['then', ['call', `$${mapFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]],
640
+ ['else', ['call', `$${setFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]]]]], 'f64')
514
641
  }
642
+ ctx.core.emit['.has'] = collProbeDyn('__map_has', '__set_has')
643
+ ctx.core.emit['.delete'] = collProbeDyn('__map_delete', '__set_delete')
644
+ ctx.core.emit[`.${VAL.SET}:has`] = call('__set_has', 'II', 'i32')
645
+ ctx.core.emit[`.${VAL.SET}:delete`] = call('__set_delete', 'II', 'i32')
515
646
 
516
647
  // Map.prototype.clear / Set.prototype.clear — drop every entry. `.clear` only
517
648
  // exists on Map/Set in JS, so a single generic emitter is unambiguous; the
518
649
  // stdlib reads the ptr tag to pick the entry stride.
519
- ctx.core.emit['.clear'] = (collExpr) => {
520
- inc('__coll_clear')
521
- return typed(['call', '$__coll_clear', asI64(emit(collExpr))], 'f64')
522
- }
650
+ ctx.core.emit['.clear'] = call('__coll_clear', 'I')
523
651
 
524
652
  // Zero every slot (so probes see empty) and reset the length header. Entry
525
653
  // stride is 16 for a SET, 24 for a MAP — `(t == MAP) << 3` adds the 8-byte
@@ -538,41 +666,47 @@ export default (ctx) => {
538
666
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
539
667
  (f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
540
668
 
541
- ctx.core.emit['.size'] = (expr) => {
669
+ ctx.core.emit['.size'] = getter((expr) => {
542
670
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
543
- }
671
+ })
544
672
 
545
673
  // x instanceof Map / Set — typed-pointer predicates emitted by jzify. NaN-check
546
674
  // first (non-pointer numbers must report false), then compare __ptr_type tag.
547
675
  // Mirrors module/array.js's Array.isArray inline form. Result is i32 (boolean).
548
676
  ctx.core.emit['__is_map'] = (x) => {
549
- inc('__ptr_type')
550
677
  const v = asF64(emit(x))
551
678
  const t = temp('imap')
552
679
  return typed(['i32.and',
553
680
  ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
554
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.MAP]]], 'i32')
681
+ ptrTypeEq(['local.get', `$${t}`], PTR.MAP)], 'i32')
555
682
  }
556
683
  ctx.core.emit['__is_set'] = (x) => {
557
- inc('__ptr_type')
558
684
  const v = asF64(emit(x))
559
685
  const t = temp('iset')
560
686
  return typed(['i32.and',
561
687
  ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
562
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.SET]]], 'i32')
688
+ ptrTypeEq(['local.get', `$${t}`], PTR.SET)], 'i32')
563
689
  }
564
690
 
565
691
  // Generated Set probe functions
566
- ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
567
- ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
568
- ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET)
692
+ ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
693
+ ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
694
+ ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET)
569
695
 
570
696
  // === Map ===
571
697
 
572
- ctx.core.emit['new.Map'] = () => {
698
+ ctx.core.emit['new.Map'] = (iterExpr) => {
573
699
  ctx.features.map = true
574
- const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'map' })
575
- return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
700
+ if (iterExpr == null) {
701
+ const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'map' })
702
+ return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
703
+ }
704
+ // new Map(iterable): seed from another Map or an array of [key, value] pairs.
705
+ // Delegated to a stdlib helper (vs inlined like Set) — `new Map(x)` is heavily
706
+ // used (the compiler copies fact Maps per function), so one shared helper keeps
707
+ // output small. Non-Map/Array args yield an empty map (guarded in the helper).
708
+ inc('__map_from')
709
+ return typed(['call', '$__map_from', asI64(emit(iterExpr))], 'f64')
576
710
  }
577
711
 
578
712
  ctx.core.emit['.set'] = (mapExpr, key, val) => {
@@ -595,22 +729,139 @@ export default (ctx) => {
595
729
  ctx.core.emit['.get'] = emitMapGet
596
730
  ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
597
731
 
598
- ctx.core.emit[`.${VAL.MAP}:has`] = (mapExpr, key) => {
599
- inc('__map_has')
600
- return typed(['f64.convert_i32_s', ['call', '$__map_has', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
732
+ ctx.core.emit[`.${VAL.MAP}:has`] = call('__map_has', 'II', 'i32')
733
+ ctx.core.emit[`.${VAL.MAP}:delete`] = call('__map_delete', 'II', 'i32')
734
+
735
+ // Map/Set iteration views: keys() / values() / entries() materialize a dense
736
+ // Array snapshot (jz models iterators as arrays — for-of/spread consume them
737
+ // directly). Set keys===values===elements; Set entries yield [v, v] pairs.
738
+ // Registered per concrete type; an unproven receiver resolves SET vs MAP at
739
+ // runtime via `.keys`/`.values`/`.entries` below.
740
+ const collView = (walk) => (expr) => {
741
+ const t = temp('cv')
742
+ return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], walk(t)], 'f64')
601
743
  }
602
-
603
- ctx.core.emit[`.${VAL.MAP}:delete`] = (mapExpr, key) => {
604
- inc('__map_delete')
605
- return typed(['f64.convert_i32_s', ['call', '$__map_delete', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
744
+ ctx.core.emit[`.${VAL.MAP}:keys`] = collView(t => collKeysFromTemp(t, MAP_ENTRY, 8))
745
+ ctx.core.emit[`.${VAL.MAP}:values`] = collView(t => collKeysFromTemp(t, MAP_ENTRY, 16))
746
+ ctx.core.emit[`.${VAL.MAP}:entries`] = collView(t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16))
747
+ ctx.core.emit[`.${VAL.SET}:keys`] = collView(t => collKeysFromTemp(t, SET_ENTRY, 8))
748
+ ctx.core.emit[`.${VAL.SET}:values`] = ctx.core.emit[`.${VAL.SET}:keys`]
749
+ ctx.core.emit[`.${VAL.SET}:entries`] = collView(t => collEntriesFromTemp(t, SET_ENTRY, 8, 8))
750
+
751
+ // Generic keys()/values()/entries() for a receiver whose collection kind isn't
752
+ // statically proven (e.g. a Map read off an object field): resolve MAP vs SET
753
+ // vs ARRAY once at runtime. ARRAY.values() is the array itself; .keys() yields
754
+ // indices; .entries() yields [i, el]. Any other receiver passes through.
755
+ // A typed array (PTR.TYPED) is NOT PTR.ARRAY, so without an explicit branch it fell
756
+ // through to `local.get $t` (return the receiver). For .values that is correct (the
757
+ // receiver iterates its values), but .keys then yielded values instead of indices and
758
+ // .entries yielded scalars instead of [i, value] pairs. The typedWalk arg restores the
759
+ // right behavior for typed receivers (keys → index array, entries → typed-aware pairs).
760
+ const collViewDyn = (mapWalk, setWalk, arrWalk, typedWalk) => (expr) => {
761
+ inc('__ptr_type')
762
+ const t = temp('cd')
763
+ const pt = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
764
+ const branch = (tag, walk, rest) =>
765
+ ['if', ['result', 'f64'], ['i32.eq', pt(), ['i32.const', tag]], ['then', walk(t)], ['else', rest]]
766
+ const tree = branch(PTR.MAP, mapWalk, branch(PTR.SET, setWalk,
767
+ branch(PTR.ARRAY, arrWalk, branch(PTR.TYPED, typedWalk, ['local.get', `$${t}`]))))
768
+ return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], tree], 'f64')
769
+ }
770
+ // keys: index array [0..len-1] for both plain and typed (length-based, no element reads).
771
+ ctx.core.emit['.keys'] = collViewDyn(
772
+ t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp, arrIdxFromTemp)
773
+ // values: the receiver iterates its own elements (correct for plain and typed alike).
774
+ ctx.core.emit['.values'] = collViewDyn(
775
+ t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`], t => ['local.get', `$${t}`])
776
+ // entries: [i, element] pairs; the typed variant reads elements width/kind-aware.
777
+ ctx.core.emit['.entries'] = collViewDyn(
778
+ t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp, arrEntriesFromTempTyped)
779
+
780
+ // Map/Set forEach(cb): invoke cb(value, key) per live entry in insertion order.
781
+ // Map yields (value=val@16, key=key@8); Set yields (value=key@8, key@8) — the
782
+ // spec passes the element as both value and key. The trailing collection arg is
783
+ // dropped (as array/typedarray forEach drop the array arg) so we never exceed
784
+ // the uniform closure width (forEach autoloads array → closure floor 2). Uses
785
+ // the closure-call path like typedarray:forEach — forEach isn't a hot path.
786
+ const collForEach = (stride, valOff, keyOff) => (expr, fn) => {
787
+ inc('__ptr_offset', '__cap', '__len', '__coll_order')
788
+ const t = temp('fe'), cb = temp('fecb')
789
+ const off = tempI32('feo'), cap = tempI32('fec'), n = tempI32('fen')
790
+ const i = tempI32('fei'), ord = tempI32('fer'), slot = tempI32('fes')
791
+ const id = ctx.func.uniq++
792
+ const at = (o) => typed(['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', o]]], 'f64')
793
+ return typed(['block', ['result', 'f64'],
794
+ ['local.set', `$${t}`, asF64(emit(expr))],
795
+ ['local.set', `$${cb}`, asF64(emit(fn))],
796
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
797
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
798
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
799
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
800
+ ['local.set', `$${i}`, ['i32.const', 0]],
801
+ ['block', `$febrk${id}`, ['loop', `$feloop${id}`,
802
+ ['br_if', `$febrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
803
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
804
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
805
+ ['drop', asF64(ctx.closure.call(typed(['local.get', `$${cb}`], 'f64'),
806
+ [at(valOff), at(keyOff)]))],
807
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
808
+ ['br', `$feloop${id}`]]],
809
+ ['f64.const', 0]], 'f64')
606
810
  }
811
+ ctx.core.emit[`.${VAL.MAP}:forEach`] = collForEach(MAP_ENTRY, 16, 8)
812
+ ctx.core.emit[`.${VAL.SET}:forEach`] = collForEach(SET_ENTRY, 8, 8)
607
813
 
608
814
  // Generated Map probe functions
609
- ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
610
- ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
611
- ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEq, PTR.MAP, UNDEF_NAN, ctx.features.external)
612
- ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, false, ctx.features.external)
613
- ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP)
815
+ ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
816
+ ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
817
+ ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEqG, PTR.MAP, UNDEF_NAN, ctx.features.external)
818
+ ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, false, ctx.features.external)
819
+ ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP)
820
+
821
+ // new Map(iterable) seeder. Source is another Map (copy live [key,val] slots) or
822
+ // an array of [key,value] pairs (`new Map([["a",1],…])`); any other arg yields an
823
+ // empty map. Pre-sizes cap to fit (smallest pow2 > 2·n, floor INIT_CAP) so seeding
824
+ // never triggers a rehash. Occupied MAP slot ⇔ hash word ≠ 0 (genDelete shift-back
825
+ // writes 0, leaving no tombstones — matches the rehash loop's own occupancy test).
826
+ ctx.core.stdlib['__map_from'] = `(func $__map_from (param $src i64) (result f64)
827
+ (local $map i64) (local $t i32) (local $off i32) (local $cap i32)
828
+ (local $i i32) (local $n i32) (local $slot i32) (local $entry i64) (local $newcap i32) (local $ord i32)
829
+ (local.set $t (call $__ptr_type (local.get $src)))
830
+ (if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
831
+ (then
832
+ (local.set $off (call $__ptr_offset (local.get $src)))
833
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
834
+ (local.set $n (i32.load (i32.sub (local.get $off) (i32.const 8)))))
835
+ (else (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
836
+ (then (local.set $n (call $__len (local.get $src)))))))
837
+ (local.set $newcap (i32.shl (i32.const 1)
838
+ (i32.sub (i32.const 32) (i32.clz
839
+ (i32.sub (i32.add (i32.shl (local.get $n) (i32.const 1)) (i32.const ${INIT_CAP})) (i32.const 1))))))
840
+ (local.set $map (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
841
+ (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${MAP_ENTRY})))))
842
+ (if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
843
+ (then
844
+ ;; Copy in source insertion order so the new map enumerates identically.
845
+ (local.set $ord (call $__coll_order (local.get $off) (local.get $cap) (i32.const ${MAP_ENTRY})))
846
+ (block $dm (loop $lm
847
+ (br_if $dm (i32.ge_s (local.get $i) (local.get $n)))
848
+ (local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
849
+ (local.set $map (call $__map_set (local.get $map)
850
+ (i64.load (i32.add (local.get $slot) (i32.const 8)))
851
+ (i64.load (i32.add (local.get $slot) (i32.const 16)))))
852
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
853
+ (br $lm))))
854
+ (else (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
855
+ (then
856
+ (block $da (loop $la
857
+ (br_if $da (i32.ge_s (local.get $i) (local.get $n)))
858
+ (local.set $entry (i64.reinterpret_f64 (call $__typed_idx (local.get $src) (local.get $i))))
859
+ (local.set $map (call $__map_set (local.get $map)
860
+ (i64.reinterpret_f64 (call $__typed_idx (local.get $entry) (i32.const 0)))
861
+ (i64.reinterpret_f64 (call $__typed_idx (local.get $entry) (i32.const 1)))))
862
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
863
+ (br $la)))))))
864
+ (f64.reinterpret_i64 (local.get $map)))`
614
865
 
615
866
  // === HASH — dynamic string-keyed object (type=7) ===
616
867
 
@@ -636,6 +887,11 @@ export default (ctx) => {
636
887
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
637
888
  (br $ls))))
638
889
  (else
890
+ ;; canonical interned static: cached post-clamp FNV at -8 (see layout.js)
891
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
892
+ (i32.and (i32.ge_u (local.get $off) (i32.const 8))
893
+ (i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
894
+ (then (return (i32.load (i32.sub (local.get $off) (i32.const 8))))))
639
895
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
640
896
  (then (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))))
641
897
  ;; 4-byte unrolled FNV-1a: each iter loads i32, mixes 4 bytes (little-endian) sequentially.
@@ -675,13 +931,13 @@ export default (ctx) => {
675
931
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
676
932
  (call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY}))))`
677
933
 
678
- ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
679
- ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
680
- ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
681
- ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true)
934
+ ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
935
+ ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEqG, PTR.HASH)
936
+ ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
937
+ ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, false, true)
682
938
  // Tombstones an entry in a HASH (string keys). Returns 1 if found+deleted, 0 otherwise.
683
939
  // Used as the bucket-level primitive for __dyn_del.
684
- ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
940
+ ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
685
941
  // Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
686
942
  // Uses bit-hash + i64.eq — no string allocation for the unique integer key.
687
943
  ctx.core.stdlib['__ihash_get_local'] = genLookupStrict('__ihash_get_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH)
@@ -713,10 +969,14 @@ export default (ctx) => {
713
969
  // literals — single bit-eq decides). Falls back to __str_eq when bits differ
714
970
  // so runtime-registered schemas (e.g. JSON.parse OBJECTs whose keys are
715
971
  // freshly heap-allocated by __jp_str) still resolve correctly.
972
+ // If-expression, NOT i32.or: `or` evaluates both arms, calling __str_eq even
973
+ // when the bit-eq already decided — that bare call per schema-key step was
974
+ // the hottest __str_eq producer in the self-host (the kernel includes __jp
975
+ // for optJSON, so the fallback arm is always compiled in).
716
976
  const schemaKeyEq = (storedKey, userKey) => ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
717
- ? `(i32.or
718
- (i64.eq ${storedKey} ${userKey})
719
- (call $__str_eq ${storedKey} ${userKey}))`
977
+ ? `(if (result i32) (i64.eq ${storedKey} ${userKey})
978
+ (then (i32.const 1))
979
+ (else (call $__str_eq ${storedKey} ${userKey})))`
720
980
  : `(i64.eq ${storedKey} ${userKey})`
721
981
  const buildObjectSchemaArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
722
982
  (if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
@@ -761,9 +1021,7 @@ export default (ctx) => {
761
1021
  (local.set $idx (i32.const 0))
762
1022
  (block $schemaSetDone (loop $schemaSetLoop
763
1023
  (br_if $schemaSetDone (i32.ge_s (local.get $idx) (local.get $nkeys)))
764
- (if (call $__str_eq
765
- (i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
766
- (local.get $key))
1024
+ (if ${schemaKeyEq(`(i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))`, `(local.get $key)`)}
767
1025
  (then
768
1026
  (i64.store (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3))) (local.get $val))
769
1027
  (br $schemaSetDone)))
@@ -782,6 +1040,10 @@ export default (ctx) => {
782
1040
  (local $props i64) (local $off i32)
783
1041
  (local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
784
1042
  ${buildObjectSchemaLocals()}
1043
+ ;; Real-number receiver (f===f — pointers are NaN-boxed) has no props: bail before
1044
+ ;; treating its bits as a heap offset (\`(5).foo\`/\`(5)[k]\` → undefined, not OOB).
1045
+ (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1046
+ (then (return (i64.const ${UNDEF_NAN}))))
785
1047
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
786
1048
  ;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
787
1049
  ;; global __dyn_props hash on the function table index (negative — can't collide
@@ -827,13 +1089,23 @@ export default (ctx) => {
827
1089
  (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
828
1090
  (br_if $dynDone (i64.eqz (local.get $props)))
829
1091
  (br $haveProps)))
830
- ;; Other header types (TYPED/HASH/SET/MAP) carry propsPtr at off-16
1092
+ ;; HASH: a plain dict whose string keys ARE its own bucket entries — the
1093
+ ;; receiver IS its props table, so probe it directly (a HASH never carries
1094
+ ;; an off-16 dyn sidecar). A statically HASH-typed h[k] already inlines
1095
+ ;; __hash_get; this path serves receivers whose HASH type is only known at
1096
+ ;; runtime — e.g. a value read back through a function return, as in
1097
+ ;; derive(emitter)[op]. Without it, dyn-get reads the (absent) sidecar and
1098
+ ;; reports every key missing.
1099
+ (if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1100
+ (then
1101
+ (local.set $props (local.get $obj))
1102
+ (br $haveProps)))
1103
+ ;; Other header types (TYPED/SET/MAP) carry propsPtr at off-16
831
1104
  ;; directly, bypassing the global __dyn_props hash.
832
1105
  (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
833
1106
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
834
- (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
835
- (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
836
- (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
1107
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1108
+ (i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
837
1109
  (then
838
1110
  (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
839
1111
  (br_if $dynDone (i64.eqz (local.get $props)))
@@ -856,14 +1128,17 @@ export default (ctx) => {
856
1128
  (br $dynDone))
857
1129
  (else
858
1130
  (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props))))))))
859
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $props) (i64.const ${LAYOUT.OFFSET_MASK}))))
1131
+ (local.set $poff (call $__ptr_offset (local.get $props)))
860
1132
  (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
861
1133
  (local.set $pend (i32.add (local.get $poff) (i32.mul (local.get $pcap) (i32.const ${MAP_ENTRY}))))
862
1134
  (local.set $slot (i32.add (local.get $poff) (i32.mul (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))) (i32.const ${MAP_ENTRY}))))
863
1135
  (block $hdone (loop $hprobe
864
1136
  (br_if $dynDone (i64.eqz (i64.load (local.get $slot))))
865
- (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
866
- (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
1137
+ ;; hash-first: slot stores the key's hash in its low 32 bits — one i32
1138
+ ;; compare rejects collision steps without walking key bytes.
1139
+ (if (i32.eq (i32.load (local.get $slot)) (local.get $h))
1140
+ (then (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
1141
+ (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))))
867
1142
  (local.set $slot (i32.add (local.get $slot) (i32.const ${MAP_ENTRY})))
868
1143
  (if (i32.ge_u (local.get $slot) (local.get $pend)) (then (local.set $slot (local.get $poff))))
869
1144
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
@@ -884,6 +1159,10 @@ export default (ctx) => {
884
1159
 
885
1160
  ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
886
1161
  (local $val i64)
1162
+ ;; Real-number receiver → no props; its garbage tag could match HASH and hit the
1163
+ ;; __hash_get_local fallback below (heap read at a bogus offset → OOB).
1164
+ (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1165
+ (then (return (i64.const ${UNDEF_NAN}))))
887
1166
  (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
888
1167
  (if (result i64)
889
1168
  (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
@@ -891,12 +1170,15 @@ export default (ctx) => {
891
1170
  (else
892
1171
  (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
893
1172
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
894
- (else (i64.const ${NULL_NAN}))))))`
1173
+ (else (i64.const ${UNDEF_NAN}))))))`
895
1174
 
896
1175
  // Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
897
1176
  // is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
898
1177
  ctx.core.stdlib['__dyn_get_expr_t_h'] = `(func $__dyn_get_expr_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
899
1178
  (local $val i64)
1179
+ ;; Real-number receiver → no props; guard the HASH fallback OOB (see __dyn_get_expr_t).
1180
+ (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1181
+ (then (return (i64.const ${UNDEF_NAN}))))
900
1182
  (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
901
1183
  (if (result i64)
902
1184
  (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
@@ -904,7 +1186,7 @@ export default (ctx) => {
904
1186
  (else
905
1187
  (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
906
1188
  (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
907
- (else (i64.const ${NULL_NAN}))))))`
1189
+ (else (i64.const ${UNDEF_NAN}))))))`
908
1190
 
909
1191
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
910
1192
  // Used at call sites where receiver type is statically unknown.
@@ -922,18 +1204,24 @@ export default (ctx) => {
922
1204
  const extArm = ctx.features.external
923
1205
  ? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
924
1206
  (then (call $__ext_prop (local.get $obj) (local.get $key)))
925
- (else (i64.const ${NULL_NAN})))`
926
- : `(i64.const ${NULL_NAN})`
1207
+ (else (i64.const ${UNDEF_NAN})))`
1208
+ : `(i64.const ${UNDEF_NAN})`
927
1209
  return `(func $__dyn_get_any_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
928
1210
  (local $val i64)
929
- (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
930
- (then (call $__hash_get_local (local.get $obj) (local.get $key)))
1211
+ ;; A real number receiver (f===f — NaN-boxed pointers are NaN) has no dynamic
1212
+ ;; props: \`(5).foo\` is undefined. Without this guard the bits are reinterpreted as
1213
+ ;; a pointer and \`__dyn_get_t\` reads heap at a bogus offset → OOB for large values.
1214
+ (if (result i64) (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1215
+ (then (i64.const ${UNDEF_NAN}))
931
1216
  (else
932
- (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
933
- (if (result i64)
934
- (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
935
- (then (local.get $val))
936
- (else ${extArm})))))`
1217
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
1218
+ (then (call $__hash_get_local (local.get $obj) (local.get $key)))
1219
+ (else
1220
+ (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
1221
+ (if (result i64)
1222
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
1223
+ (then (local.get $val))
1224
+ (else ${extArm})))))))`
937
1225
  }
938
1226
 
939
1227
  // Prehashed variant of __dyn_get_any_t for constant string keys: the FNV hash
@@ -944,18 +1232,22 @@ export default (ctx) => {
944
1232
  const extArm = ctx.features.external
945
1233
  ? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
946
1234
  (then (call $__ext_prop (local.get $obj) (local.get $key)))
947
- (else (i64.const ${NULL_NAN})))`
948
- : `(i64.const ${NULL_NAN})`
1235
+ (else (i64.const ${UNDEF_NAN})))`
1236
+ : `(i64.const ${UNDEF_NAN})`
949
1237
  return `(func $__dyn_get_any_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
950
1238
  (local $val i64)
951
- (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
952
- (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
1239
+ ;; Real-number receiver no dynamic props (see __dyn_get_any_t); guard the OOB.
1240
+ (if (result i64) (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1241
+ (then (i64.const ${UNDEF_NAN}))
953
1242
  (else
954
- (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
955
- (if (result i64)
956
- (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
957
- (then (local.get $val))
958
- (else ${extArm})))))`
1243
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
1244
+ (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
1245
+ (else
1246
+ (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
1247
+ (if (result i64)
1248
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
1249
+ (then (local.get $val))
1250
+ (else ${extArm})))))))`
959
1251
  }
960
1252
 
961
1253
  // Hot for `node.loc = pos` patterns (e.g. watr's parser tags every nested level).
@@ -1020,11 +1312,18 @@ export default (ctx) => {
1020
1312
  (if (i64.ne (local.get $props) (local.get $oldProps))
1021
1313
  (then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
1022
1314
  (return (local.get $val))))
1315
+ ;; HASH: a plain dict — its string keys ARE its own bucket entries (there is no
1316
+ ;; off-16 sidecar; that belongs to TYPED/SET/MAP, which have a native shape PLUS
1317
+ ;; ad-hoc props). Write directly into the receiver, mirroring __dyn_get's HASH arm.
1318
+ ;; __hash_set_local forwards on grow, so the caller's boxed pointer stays valid.
1319
+ (if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1320
+ (then
1321
+ (drop (call $__hash_set_local (local.get $obj) (local.get $key) (local.get $val)))
1322
+ (return (local.get $val))))
1023
1323
  (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
1024
1324
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
1025
- (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1026
- (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1027
- (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
1325
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1326
+ (i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
1028
1327
  (then
1029
1328
  (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
1030
1329
  (local.set $props
@@ -1148,9 +1447,9 @@ export default (ctx) => {
1148
1447
  (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root))))`
1149
1448
 
1150
1449
  // Generated HASH probe functions
1151
- ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
1152
- ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true, ctx.features.external)
1153
- ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
1450
+ ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external, true)
1451
+ ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, ctx.features.external)
1452
+ ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external)
1154
1453
 
1155
1454
  // === `delete obj[k]`: lift from prepare for computed-key removal ===
1156
1455
  // Static-key `delete obj.x` / `delete obj["x"]` is rejected in prepare (fixed schema);
@@ -1170,9 +1469,14 @@ export default (ctx) => {
1170
1469
  if (prop === 'length' && (objType === VAL.ARRAY || objType === VAL.TYPED || objType === VAL.STRING || objType === VAL.SET || objType === VAL.MAP))
1171
1470
  return typed(['i32.const', 1], 'i32')
1172
1471
 
1173
- const schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
1472
+ const schemaIdx = typeof obj === 'string' ? ctx.schema.slotOf(obj, prop) : ctx.schema.slotOf(null, prop)
1174
1473
  if (schemaIdx >= 0) return typed(['i32.const', 1], 'i32')
1175
- if (objType === VAL.OBJECT) return typed(['i32.const', 0], 'i32')
1474
+ // A schema MISS does not prove absence: an OBJECT can carry off-schema
1475
+ // dynamic props (`o.z = …` → __dyn_set's propsPtr), and under the self-host
1476
+ // kernel schema.slotOf can under-resolve even an in-schema key. Don't fold to
1477
+ // a static 0 — fall through to the runtime probe below, which reads the
1478
+ // actual property via __dyn_get (OBJECT is in `hasDynProps`) and reports
1479
+ // presence by non-nullish, exactly as the `.`/`[]` READ path resolves it.
1176
1480
  }
1177
1481
 
1178
1482
  const keyTmp = temp()
@@ -1244,13 +1548,61 @@ export default (ctx) => {
1244
1548
  ['local.get', `$${outTmp}`]], 'i32')
1245
1549
  }
1246
1550
 
1551
+ // === iterable normalization: Set/Map → dense Array ===
1552
+
1553
+ // `for (x of coll)` and `[...coll]` iterate in *value* order, but a Set/Map
1554
+ // stores entries in a sparse open-addressing table (live slots scattered among
1555
+ // empties). Index access `coll[i]` would read raw slot words. So normalize a
1556
+ // Set→keys-array / Map→[k,v]-entries-array once at loop/spread setup; an Array,
1557
+ // String, or TypedArray is already index-iterable and passes through untouched
1558
+ // (no copy). `valTypeOf(['()','__iter_arr',x])` (src/kind.js) mirrors this:
1559
+ // Set/Map → ARRAY, everything else → x's own type, so the downstream `arr[i]`
1560
+ // / `.length` dispatch stays statically typed.
1561
+ ctx.core.emit['__iter_arr'] = (src) => {
1562
+ const vt = valTypeOf(src)
1563
+ if (vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.TYPED || vt === VAL.BUFFER)
1564
+ return asF64(emit(src))
1565
+ const t = temp('iter')
1566
+ const bind = ['local.set', `$${t}`, asF64(emit(src))]
1567
+ if (vt === VAL.SET) return typed(['block', ['result', 'f64'], bind, collKeysFromTemp(t, SET_ENTRY)], 'f64')
1568
+ if (vt === VAL.MAP) return typed(['block', ['result', 'f64'], bind, collEntriesFromTemp(t, MAP_ENTRY)], 'f64')
1569
+ // Unknown receiver: resolve the kind once at runtime (loop-invariant).
1570
+ inc('__ptr_type')
1571
+ const ptrType = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
1572
+ return typed(['block', ['result', 'f64'], bind,
1573
+ ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.SET]],
1574
+ ['then', collKeysFromTemp(t, SET_ENTRY)],
1575
+ ['else', ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.MAP]],
1576
+ ['then', collEntriesFromTemp(t, MAP_ENTRY)],
1577
+ ['else', ['local.get', `$${t}`]]]]]], 'f64')
1578
+ }
1579
+
1247
1580
  // === for...in on dynamic objects (HASH iteration) ===
1248
1581
 
1582
+ // Flatten a statement/block to void IR — a self-host-robust inline of
1583
+ // emitVoid+emitBlockBody (see the call site in for-in for why the bridge
1584
+ // emitVoid can't be used here). Recurses into `{}` blocks; emits each leaf
1585
+ // statement in void position and drops any leftover value.
1586
+ const emitFlatVoid = (node) => {
1587
+ if (isBlockBody(node)) {
1588
+ if (node.length === 1) return []
1589
+ const inner = node[1]
1590
+ const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
1591
+ const out = []
1592
+ for (const s of stmts) if (s != null && typeof s !== 'number') out.push(...emitFlatVoid(s))
1593
+ return out
1594
+ }
1595
+ const ir = emit(node, 'void')
1596
+ if (ir == null) return []
1597
+ const items = Array.isArray(ir) && (typeof ir[0] === 'string' || ir[0] == null) ? [ir] : ir
1598
+ return ir.type && ir.type !== 'void' ? [...items, 'drop'] : items
1599
+ }
1600
+
1249
1601
  // for-in: iterate HASH entries, binding key string to loop variable.
1250
1602
  // Also handles OBJECT/ARRAY/etc whose dynamic props are stored at off-16
1251
1603
  // as a HASH (see __dyn_set). Non-HASH receivers redirect to that props HASH.
1252
1604
  ctx.core.emit['for-in'] = (varName, src, body) => {
1253
- const off = tempI32('ho'), cap = tempI32('hc')
1605
+ const off = tempI32('ho'), cap = tempI32('hc'), n = tempI32('hn'), ord = tempI32('hr')
1254
1606
  const i = tempI32('hi'), slot = tempI32('hs')
1255
1607
  const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
1256
1608
  if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
@@ -1260,10 +1612,17 @@ export default (ctx) => {
1260
1612
  const needsCont = hasOwnContinue(body)
1261
1613
  ctx.func.stack.push({ brk, loop: needsCont ? cont : loop })
1262
1614
  let bodyFlat
1263
- try { bodyFlat = emitFlat(body) }
1615
+ // NOTE: `flat(body)` (the bridge-dispatched emitVoid) miscompiles in this
1616
+ // self-host call context — it returns [] for a void-postfix body
1617
+ // (`for (k in o) n++`, lowered to `(++n)-1`), silently dropping the loop
1618
+ // body so the kernel-compiled for-in iterates but does nothing. The exact
1619
+ // same emit+flatten logic inlined here compiles correctly. emitFlatVoid
1620
+ // mirrors emitVoid/emitBlockBody (minus early-return refinement narrowing,
1621
+ // which a loop body does not need).
1622
+ try { bodyFlat = emitFlatVoid(body) }
1264
1623
  finally { ctx.func.stack.pop() }
1265
1624
  const bodyBlock = needsCont ? [['block', cont, ...bodyFlat]] : bodyFlat
1266
- inc('__ptr_type')
1625
+ inc('__ptr_type', '__len', '__coll_order')
1267
1626
  return [
1268
1627
  // Save source ptr as i64
1269
1628
  ['local.set', `$${ptrI64}`, ['i64.reinterpret_f64', va]],
@@ -1282,11 +1641,16 @@ export default (ctx) => {
1282
1641
  ['then',
1283
1642
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
1284
1643
  ['local.set', `$${cap}`, ['call', '$__cap', ['local.get', `$${ptrI64}`]]],
1644
+ ['local.set', `$${n}`, ['call', '$__len', ['local.get', `$${ptrI64}`]]],
1645
+ // Snapshot live slots in insertion order (JS for-in spec order). Walk
1646
+ // the snapshot; re-check occupancy so a key the body deletes before it
1647
+ // is reached is skipped rather than re-bound from an emptied slot.
1648
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', MAP_ENTRY]]],
1285
1649
  ['local.set', `$${i}`, ['i32.const', 0]],
1286
1650
  ['block', brk, ['loop', loop,
1287
- ['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
1288
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
1289
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
1651
+ ['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1652
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
1653
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
1290
1654
  ['if', ['i64.ne', ['i64.load', ['local.get', `$${slot}`]], ['i64.const', 0]],
1291
1655
  ['then',
1292
1656
  ['local.set', `$${varName}`, ['f64.reinterpret_i64', ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]],
@@ -1296,3 +1660,133 @@ export default (ctx) => {
1296
1660
  ]
1297
1661
  }
1298
1662
  }
1663
+
1664
+ // Walk a Set/Map backing table (bound f64 local `t`), copying one column of each
1665
+ // live entry into a fresh dense Array sized to the live count. `stride` is the
1666
+ // entry size (Set 16, Map 24); the first f64 word of each slot is the stored
1667
+ // hash — 0 marks an empty slot (no tombstones: delete back-shifts). `fieldOff`
1668
+ // picks the column: 8 = key (Set element / Map key), 16 = Map value. Mirrors
1669
+ // object.js's hash*FromTemp. Used by `__iter_arr` and `.keys()`/`.values()`.
1670
+ function collKeysFromTemp(t, stride, fieldOff = 8) {
1671
+ inc('__ptr_offset', '__cap', '__len', '__coll_order')
1672
+ const off = tempI32('cko'), cap = tempI32('ckc'), n = tempI32('ckn')
1673
+ const i = tempI32('cki'), ord = tempI32('ckr'), slot = tempI32('cks')
1674
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cka' })
1675
+ const id = ctx.func.uniq++
1676
+ return ['block', ['result', 'f64'],
1677
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1678
+ out.init,
1679
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1680
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1681
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
1682
+ ['local.set', `$${i}`, ['i32.const', 0]],
1683
+ ['block', `$ckbrk${id}`, ['loop', `$ckloop${id}`,
1684
+ ['br_if', `$ckbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1685
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
1686
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
1687
+ elemStore(out.local, i,
1688
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', fieldOff]]]),
1689
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1690
+ ['br', `$ckloop${id}`]]],
1691
+ out.ptr]
1692
+ }
1693
+
1694
+ // Like collKeysFromTemp but builds 2-element pair arrays — Map/Set `entries()`
1695
+ // and `[...map]` yield pairs. Each live slot contributes a fresh 2-element Array
1696
+ // [slot+aOff, slot+bOff] boxed into the output: Map entries use (8,16) → [k,v];
1697
+ // Set entries use (8,8) → [v,v].
1698
+ function collEntriesFromTemp(t, stride, aOff = 8, bOff = 16) {
1699
+ inc('__ptr_offset', '__cap', '__len', '__alloc_hdr', '__coll_order')
1700
+ const off = tempI32('ceo'), cap = tempI32('cec'), n = tempI32('cen')
1701
+ const i = tempI32('cei'), ord = tempI32('cer'), slot = tempI32('ces'), pair = tempI32('cep')
1702
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cea' })
1703
+ const id = ctx.func.uniq++
1704
+ return ['block', ['result', 'f64'],
1705
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1706
+ out.init,
1707
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1708
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1709
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
1710
+ ['local.set', `$${i}`, ['i32.const', 0]],
1711
+ ['block', `$cebrk${id}`, ['loop', `$celoop${id}`,
1712
+ ['br_if', `$cebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1713
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
1714
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
1715
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1716
+ ['f64.store', ['local.get', `$${pair}`],
1717
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', aOff]]]],
1718
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1719
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', bOff]]]],
1720
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
1721
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1722
+ ['br', `$celoop${id}`]]],
1723
+ out.ptr]
1724
+ }
1725
+
1726
+ // Array.prototype.keys() → dense Array of indices [0, 1, …, len-1] as numbers.
1727
+ function arrIdxFromTemp(t) {
1728
+ inc('__len')
1729
+ const n = tempI32('ain'), i = tempI32('aii')
1730
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aia' })
1731
+ const id = ctx.func.uniq++
1732
+ return ['block', ['result', 'f64'],
1733
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1734
+ out.init,
1735
+ ['local.set', `$${i}`, ['i32.const', 0]],
1736
+ ['block', `$aibrk${id}`, ['loop', `$ailoop${id}`,
1737
+ ['br_if', `$aibrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1738
+ elemStore(out.local, i, ['f64.convert_i32_s', ['local.get', `$${i}`]]),
1739
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1740
+ ['br', `$ailoop${id}`]]],
1741
+ out.ptr]
1742
+ }
1743
+
1744
+ // Array.prototype.entries() → dense Array of [index, element] pair arrays.
1745
+ function arrEntriesFromTemp(t) {
1746
+ inc('__len', '__ptr_offset', '__alloc_hdr')
1747
+ const n = tempI32('aen'), i = tempI32('aei'), src = tempI32('aes'), pair = tempI32('aep')
1748
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aea' })
1749
+ const id = ctx.func.uniq++
1750
+ return ['block', ['result', 'f64'],
1751
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1752
+ out.init,
1753
+ ['local.set', `$${src}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1754
+ ['local.set', `$${i}`, ['i32.const', 0]],
1755
+ ['block', `$aebrk${id}`, ['loop', `$aeloop${id}`,
1756
+ ['br_if', `$aebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1757
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1758
+ ['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
1759
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]], elemLoad(src, i)],
1760
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
1761
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1762
+ ['br', `$aeloop${id}`]]],
1763
+ out.ptr]
1764
+ }
1765
+
1766
+ // TypedArray.prototype.entries() → dense Array of [index, element] pairs, reading each
1767
+ // element width/kind-aware via __typed_get_idx (the plain arrEntriesFromTemp uses a raw
1768
+ // f64.load, wrong for non-f64 typed arrays). Guarded on __typed_get_idx being registered:
1769
+ // it only is when the typedarray module is loaded, which is exactly when a PTR.TYPED
1770
+ // value can exist — so when absent, this branch is dead and falls back to the receiver.
1771
+ function arrEntriesFromTempTyped(t) {
1772
+ if (!ctx.core.stdlib['__typed_get_idx']) return ['local.get', `$${t}`]
1773
+ inc('__len', '__typed_get_idx', '__alloc_hdr')
1774
+ const n = tempI32('aetn'), i = tempI32('aeti'), pair = tempI32('aetp')
1775
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aet' })
1776
+ const id = ctx.func.uniq++
1777
+ const P = () => ['i64.reinterpret_f64', ['local.get', `$${t}`]]
1778
+ return ['block', ['result', 'f64'],
1779
+ ['local.set', `$${n}`, ['call', '$__len', P()]],
1780
+ out.init,
1781
+ ['local.set', `$${i}`, ['i32.const', 0]],
1782
+ ['block', `$aetbrk${id}`, ['loop', `$aetloop${id}`,
1783
+ ['br_if', `$aetbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1784
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1785
+ ['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
1786
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1787
+ ['call', '$__typed_get_idx', P(), ['local.get', `$${i}`]]],
1788
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
1789
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1790
+ ['br', `$aetloop${id}`]]],
1791
+ out.ptr]
1792
+ }