jz 0.8.1 → 0.9.1

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 (72) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +23 -23
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6271
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +299 -44
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +892 -32
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +301 -84
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +71 -5
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +979 -54
  45. package/src/compile/index.js +271 -29
  46. package/src/compile/infer.js +34 -3
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/narrow.js +543 -15
  49. package/src/compile/peel-stencil.js +5 -0
  50. package/src/compile/plan/index.js +45 -3
  51. package/src/compile/plan/inline.js +8 -1
  52. package/src/compile/plan/literals.js +39 -0
  53. package/src/compile/plan/scope.js +430 -23
  54. package/src/compile/program-facts.js +732 -38
  55. package/src/ctx.js +64 -9
  56. package/src/helper-counters.js +7 -1
  57. package/src/ir.js +113 -5
  58. package/src/kind-traits.js +66 -3
  59. package/src/kind.js +84 -12
  60. package/src/op-policy.js +7 -4
  61. package/src/optimize/index.js +1060 -750
  62. package/src/optimize/recurse.js +2 -2
  63. package/src/optimize/vectorize.js +972 -67
  64. package/src/prepare/index.js +792 -63
  65. package/src/prepare/math-kernel.js +331 -0
  66. package/src/prepare/pre-eval.js +714 -0
  67. package/src/snapshot.js +194 -0
  68. package/src/type.js +1170 -56
  69. package/src/wat/assemble.js +403 -65
  70. package/src/wat/codegen.js +74 -14
  71. package/transform.js +113 -4
  72. package/wasi.js +3 -0
@@ -8,22 +8,192 @@
8
8
  * @module collection
9
9
  */
10
10
 
11
- import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad } from '../src/ir.js'
11
+ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, TOMB_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad, carrierF64 } from '../src/ir.js'
12
12
  import { emit, deps, call } from '../src/bridge.js'
13
13
  import { valTypeOf } from '../src/kind.js'
14
14
  import { VAL, lookupValType } from '../src/reps.js'
15
15
  import { hasOwnContinue, isBlockBody, isLiteralStr } from '../src/ast.js'
16
16
  import { ctx, inc, PTR, LAYOUT, registerGetter, declGlobal } from '../src/ctx.js'
17
- import { STR_INTERN_BIT } from '../layout.js'
17
+ import { STR_INTERN_BIT, STR_HCACHE_BIT, ssoBitI64Hex, encodePtrHi, i64Hex } from '../layout.js'
18
+ import { ssoEncode } from './string.js'
19
+
20
+ const SSO_BIT_I64 = ssoBitI64Hex()
21
+ // NaN-box bits of the SSO string 'length' — computed once; see the STRING
22
+ // arm in __dyn_get_t_h and __length's property-fallback arm (module/core.js).
23
+ // ssoEncode('length') never returns null (6 ASCII).
24
+ export const LENGTH_SSO_I64 = (() => { const e = ssoEncode('length'); return i64Hex((BigInt(encodePtrHi(4, e.aux) >>> 0) << 32n) | BigInt(e.offset)) })()
18
25
 
19
26
  const SET_ENTRY = 16 // hash + key
20
27
  const MAP_ENTRY = 24 // hash + key + value
21
28
  const INIT_CAP = 8 // initial capacity (must be power of 2)
22
29
 
23
- export function strHashLiteral(str) {
30
+ // __dyn_props global-table membership filter (see __dyn_props_filter's declGlobal
31
+ // comment). offExpr is an i32 WAT expr for the offset key. Mix folds the offset's
32
+ // mid bits (allocations are 8-byte aligned, so raw low bits are useless) down to
33
+ // a 6-bit bucket in a 64-bit bitset — never-false-negative, no false-negative risk
34
+ // from collisions (a collision just makes the filter's "maybe present" wider).
35
+ const dynPropsFilterBitIR = (offExpr) =>
36
+ `(i64.shl (i64.const 1) (i64.extend_i32_u (i32.and (i32.xor (i32.shr_u ${offExpr} (i32.const 3)) (i32.shr_u ${offExpr} (i32.const 9))) (i32.const 63))))`
37
+ // Set the filter bit for an offset key that was just inserted into the global table.
38
+ export const dynPropsFilterSetIR = (offExpr) =>
39
+ `(global.set $__dyn_props_filter (i64.or (global.get $__dyn_props_filter) ${dynPropsFilterBitIR(offExpr)}))`
40
+ // True (i32) when the filter bit is clear — i.e. offExpr is PROVEN never inserted,
41
+ // safe to skip the __ihash_get_local probe entirely. False (bit set) means "maybe
42
+ // present, maybe a collision" — falls through to the real probe.
43
+ export const dynPropsFilterMissIR = (offExpr) =>
44
+ `(i64.eqz (i64.and (global.get $__dyn_props_filter) ${dynPropsFilterBitIR(offExpr)}))`
45
+
46
+ // The post-init high-water mark (see module/core.js's __heap_reset) as a WAT operand —
47
+ // everything at/above it is EPHEMERAL (this compile's own arena, wiped by `_clear`);
48
+ // everything below it is DURABLE (module-init state, survives `_clear` forever). Falls
49
+ // back to a literal 0 (every offset reads as "ephemeral") when the module has no
50
+ // `__heap_reset` global at all (no allocator, or shared memory, whose reset is a plain
51
+ // HEAP.START rewind with no high-water-mark concept — see core.js). Read at
52
+ // template-EXPANSION time (thunked callers only — see durableFwdLogIR/heapResetWat
53
+ // consumers), so it observes the FINAL declaration state, not whatever was true when
54
+ // collection.js's own module body first ran.
55
+ //
56
+ // DURABLE-RECEIVER POLICY (dyn-props twin of the above): a receiver allocated
57
+ // at/below __heap_reset outlives `_clear()`, but a sidecar installed for it at
58
+ // RUNTIME lives in the round's arena — the surviving header slot then dangles
59
+ // across `_clear()` and the next round corrupts reused memory. So runtime
60
+ // dyn-prop writes on a durable receiver (off < __heap_reset) route to the
61
+ // GLOBAL __dyn_props table instead — __clear resets it, so prop lifetime
62
+ // matches storage lifetime. Init-time writes still land in durable sidecars
63
+ // (__heap_reset is seeded to data-end until __start's tail captures the
64
+ // post-init top, so off >= __heap_reset holds throughout init). Every read/
65
+ // write/delete/enumerate site that consults a header sidecar gates on this —
66
+ // see module/object.js's emitEnumerateObject and module/json.js's __json_obj
67
+ // for the array-IR / WAT-string twins that merge in the global table for
68
+ // durable receivers.
69
+ export const heapResetWat = () => ctx.scope.globals.has('__heap_reset') ? '(global.get $__heap_reset)' : '(i32.const 0)'
70
+
71
+ // A growable ARRAY/HASH/SET/MAP relocates by leaving a forwarding header behind
72
+ // (cap=-1 sentinel at off-4, new offset at off-8 — see layout.js's followForwardingWat).
73
+ // That is only safe WITHIN one compile round: `_clear()` rewinds the arena but never
74
+ // zeroes memory, so a forward written into a DURABLE header (offOld < __heap_reset,
75
+ // i.e. the block predates this round) permanently points at an EPHEMERAL target that
76
+ // the next round's allocations silently overwrite — any later chase through the
77
+ // durable alias then lands on garbage or goes OOB (.work/todo.md groundtruth archive,
78
+ // "array-growth forwarding is not _clear-safe"). Growing an EPHEMERAL block needs no
79
+ // protection: everything reachable from it is ephemeral too, so the whole chain (old
80
+ // header, new header, and every durable-side reference to it — there are none, by
81
+ // construction) is reclaimed together at `_clear()`.
82
+ //
83
+ // Fix: at the grow/shift site, BEFORE writing the forward (while off/len/cap — the
84
+ // header's pre-relocation state — are still live locals), log the durable→ephemeral
85
+ // transition to a small resettable side-table (module/core.js's __durable_fwd_log/
86
+ // __durable_fwd_heal) instead of (or rather: in addition to, so the in-round chase
87
+ // still works) trusting the header alone. `_clear()` then HEALS each logged header
88
+ // back to its exact pre-relocation (len, cap) — undoing the forward mark, so the
89
+ // durable block reverts to self-contained, non-forwarding, and correct (its own
90
+ // element/entry cells were never touched by the relocation; only the header words
91
+ // were). This keeps followForwardingWat/__ptr_offset_fwd (the hot chase, ~25% of
92
+ // self-host compile ticks) completely UNTOUCHED — the check only runs on the already-
93
+ // cold relocation path, and the heal sweep only runs inside `_clear()`, bounded by
94
+ // however many durable relocations happened that round (0 in the overwhelmingly
95
+ // common case).
96
+ //
97
+ // Checks BOTH ends, not just "is off durable": a fresh `__alloc_hdr`/`__alloc_hdr_n`
98
+ // target (grow, genUpsert/genUpsertGrow) is unconditionally ephemeral whenever the
99
+ // source-durable check can even fire (any allocation live past `__start`'s tail-
100
+ // capture is by construction >= the now-final `__heap_reset`), so newOff's own check
101
+ // is redundant there — but `.shift()`'s "new" header is just `off + 8`, a position
102
+ // INSIDE the same block, not a fresh allocation: ordinarily still durable (shifting a
103
+ // durable array is legitimate, persistent state and must NOT be undone at `_clear`),
104
+ // and only crosses into ephemeral in the one-in-8-bytes edge case where `off` sits
105
+ // exactly at `__heap_reset - 8`. Requiring both conditions everywhere makes the
106
+ // invariant self-evidently correct at every call site instead of relying on a
107
+ // per-caller argument about what its "new" offset can be.
108
+ // Emits nothing at all (not even a call site) when there's no `__heap_reset` to compare
109
+ // against — shared memory's `__clear` is a plain rewind-to-HEAP.START with no high-water
110
+ // mark (core.js), so EVERYTHING resets uniformly there and no state is ever "durable" to
111
+ // begin with (a separate, pre-existing, documented gap — see core.js's shared-memory
112
+ // `__clear` comment). Testing `ctx.scope.globals.has('__heap_reset')` directly (not just
113
+ // deferring to heapResetWat()'s own `(i32.const 0)` fallback, which would still emit an
114
+ // always-false-but-present call) matters for self-host inclusion: array.js's/
115
+ // collection.js's deps() edges declare '__durable_fwd_log' unconditionally at every grow/
116
+ // shift site, so core.js must ALSO unconditionally register the function whenever those
117
+ // sites exist — but core.js only defines __durable_fwd_log/__durable_fwd_heal in the
118
+ // owned-memory branch (they need __heap/__heap_reset, which shared memory doesn't have).
119
+ // A shared-memory build reaching this function with the fallback would therefore
120
+ // reference a never-registered stdlib name, tripping assemble.js's `internal: stdlib
121
+ // '__durable_fwd_log' was requested but never registered` sanity check.
122
+ export const durableFwdLogIR = (off, newOff, len, cap) => {
123
+ if (!ctx.scope.globals.has('__heap_reset')) return ''
124
+ return `
125
+ (if (i32.and (i32.lt_u (local.get $${off}) ${heapResetWat()}) (i32.ge_u (local.get $${newOff}) ${heapResetWat()}))
126
+ (then (call $__durable_fwd_log (local.get $${off}) (local.get $${len}) (local.get $${cap}))))`
127
+ }
128
+
129
+ // Value-write sibling of durableFwdLogIR: an EPHEMERAL boxed value stored into a
130
+ // DURABLE collection slot dangles across `_clear` (the corpus-wide warm trap — a
131
+ // durable memo dict handing round-1 node arrays into round-2's tree). Log the slot
132
+ // so `__durable_slot_heal` (wired into `__clear`) overwrites it with `undefined` —
133
+ // the pointed-at data dies with the arena, so entry-death is the only sound
134
+ // semantics. `slotLocal`+`byteOff` name the value slot; `valLocal` holds the boxed
135
+ // bits (i64). Same shared-memory gate as durableFwdLogIR (no watermark, no sweep).
136
+ export const durableSlotLogIR = (slotLocal, byteOff, valLocal) => {
137
+ if (!ctx.scope.globals.has('__heap_reset')) return ''
138
+ const addr = byteOff ? `(i32.add (local.get $${slotLocal}) (i32.const ${byteOff}))` : `(local.get $${slotLocal})`
139
+ return `
140
+ (if (i32.and (i32.lt_u ${addr} ${heapResetWat()}) (call $__is_eph_bits (local.get $${valLocal})))
141
+ (then (call $__durable_slot_log ${addr} (i32.const 0))))`
142
+ }
143
+
144
+ // ENTRY-insert variant: a NEW entry inserted into DURABLE table storage is
145
+ // round state regardless of what the key/value are — a fresh instance would not
146
+ // have the entry at all, and an ephemeral KEY can't even be value-healed (probes
147
+ // and enumeration would hash/compare the dangling box; measured: warm round 2
148
+ // hashed 15.5 MB of garbage-length "strings" where round 1 hashed 415 KB — the
149
+ // whole 2× warm-vs-fresh gap). Log the ENTRY base with bit0 set plus the table
150
+ // storage base; the heal turns the entry into a zombie — key ← TOMB_NAN
151
+ // (unforgeable, deref-free in every eq family), value ← undefined, table len
152
+ // decremented — that probes pass over and __coll_order/len-sized iterations
153
+ // skip. The slot stays occupied until the table grows (zombies never resurrect:
154
+ // nothing eq-matches TOMB_NAN). Entry addresses are 8-aligned → bit0 is free.
155
+ export const durableEntryLogIR = (slotLocal, offLocal) => {
156
+ if (!ctx.scope.globals.has('__heap_reset')) return ''
157
+ return `
158
+ (if (i32.lt_u (local.get $${slotLocal}) ${heapResetWat()})
159
+ (then (call $__durable_slot_log (i32.or (local.get $${slotLocal}) (i32.const 1)) (local.get $${offLocal}))))`
160
+ }
161
+
162
+ // Clamp to the >=2 convention (0=empty slot, 1=tombstone) — shared by every hash
163
+ // producer (SSO mix, byte-FNV, __jp_str, buildInternTable) so they all clamp identically.
164
+ const clampHash = (h) => (h <= 1 ? (h + 2) | 0 : h)
165
+
166
+ // SSO mix: 7 ops over the packed NaN-box lo/hi (see __str_hash's SSO branch,
167
+ // module/collection.js below, for the WAT twin — both MUST compute the same value).
168
+ // lo = offset (payload bits 0-31), hi = aux masked to bits 0-12 (length + char 4-5
169
+ // tail — SSO_BIT itself is excluded by the mask, so hi only carries discriminating
170
+ // content). Replaces the old 6-iteration per-char FNV loop with a fixed-cost mix.
171
+ const ssoMix = (lo, hi) => {
172
+ let h = Math.imul(hi ^ 0x9E3779B9, 0x85EBCA6B)
173
+ h = Math.imul(lo ^ h, 0xC2B2AE35)
174
+ h = (h ^ (h >>> 15)) | 0
175
+ return clampHash(h) >>> 0
176
+ }
177
+
178
+ // Byte-FNV-1a over UTF-8-ish bytes (charCodeAt & 0xFF — ASCII-only callers guarantee
179
+ // codepoint < 0x80, so this equals the byte value). Heap strings (>6 bytes or non-ASCII)
180
+ // keep this; __str_hash's heap branch and buildInternTable's static-intern prehash both
181
+ // compute the identical function — see module/string.js bind('str') and internProbeWat.
182
+ const byteFnv = (str) => {
24
183
  let h = 0x811c9dc5 | 0
25
184
  for (let i = 0; i < str.length; i++) h = Math.imul(h ^ (str.charCodeAt(i) & 0xFF), 0x01000193) | 0
26
- return h <= 1 ? (h + 2) | 0 : h
185
+ return clampHash(h)
186
+ }
187
+
188
+ // Compile-time hash for an ASCII string LITERAL — must equal __str_hash's runtime
189
+ // result for the same content: ≤6-ASCII strings are ALWAYS SSO (module/string.js header
190
+ // invariant), so they use the new ssoMix; longer/non-ASCII strings stay on heap and use
191
+ // byte-FNV. Callers (litKeyHash below, module/core.js, module/array.js, module/json.js)
192
+ // pass ASCII content — non-ASCII goes through the runtime __str_hash path instead.
193
+ export function strHashLiteral(str) {
194
+ const sso = ssoEncode(str)
195
+ if (sso) return ssoMix(sso.offset | 0, sso.aux & 0x1FFF)
196
+ return byteFnv(str)
27
197
  }
28
198
 
29
199
  const HASH_BUF = new ArrayBuffer(8)
@@ -45,10 +215,12 @@ function numConstLiteral(expr) {
45
215
 
46
216
  // Compile-time probe hash for a LITERAL collection/property key, else null. A numeric
47
217
  // constant → numHashLiteral; an ASCII string literal → strHashLiteral. The string case is
48
- // ASCII-only on purpose: strHashLiteral folds `charCodeAt(i) & 0xFF`, which equals __str_hash /
49
- // __map_hash (FNV-1a over the UTF-8 bytes) ONLY for code points < 0x80 — a non-ASCII literal
50
- // would fold a different hash than its stored key and silently miss, so it falls back to the
51
- // runtime hash. Lets `m.get("if")` / `m.has("x")` skip the per-access __map_hash call.
218
+ // ASCII-only on purpose: strHashLiteral's byte-FNV branch folds `charCodeAt(i) & 0xFF`,
219
+ // which equals __str_hash / __map_hash (FNV-1a over the UTF-8 bytes) ONLY for code points
220
+ // < 0x80 — a non-ASCII literal would fold a different hash than its stored key and silently
221
+ // miss, so it falls back to the runtime hash. (The ≤6-ASCII branch uses the SSO mix instead —
222
+ // still ASCII-only, since ssoEncode itself rejects non-ASCII and returns null.) Lets
223
+ // `m.get("if")` / `m.has("x")` skip the per-access __map_hash call.
52
224
  const ASCII_KEY = /^[\x00-\x7f]*$/
53
225
  const litKeyHash = (key) => {
54
226
  const num = numConstLiteral(key)
@@ -121,9 +293,10 @@ const seqStore = `(i64.store (local.get $slot)
121
293
  * owner's propsPtr slot that genUpsertGrow can rewrite). */
122
294
  function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
123
295
  const valParam = hasVal ? '(param $val i64) ' : ''
124
- const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))` : ''
296
+ const slotLog = hasVal ? durableSlotLogIR('slot', 16, 'val') : ''
297
+ const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${slotLog}` : ''
125
298
  const onMatch = hasVal
126
- ? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
299
+ ? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${slotLog}\n (br $done))`
127
300
  : `(then (br $done))`
128
301
  const rehashVal = hasVal
129
302
  ? `\n (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))`
@@ -139,10 +312,16 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
139
312
  return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
140
313
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
141
314
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
142
- (local $oldslot i32) (local $newidx i32) (local $newslot i32)
315
+ (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
143
316
  ${typeGuard}
144
- (local.set $off (call $__ptr_offset (local.get $coll)))
317
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
145
318
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
319
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
320
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
321
+ (if (i32.eq (local.get $cap) (i32.const -1))
322
+ (then
323
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
324
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
146
325
  (local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
147
326
  ;; Grow at 75% load (size*4 >= cap*3): 2× table, rehash, forward-mark old header.
148
327
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
@@ -169,22 +348,38 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
169
348
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
170
349
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
171
350
  (br $rl)))
351
+ ${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
172
352
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
173
353
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
174
354
  (local.set $off (local.get $newptr))
175
355
  (local.set $cap (local.get $newcap))))
176
356
  (local.set $h (call ${hashFn} (local.get $key)))
177
357
  ${probeStart(entrySize)}
358
+ ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys): remember the first
359
+ ;; zombie, keep probing to empty/match, insert into the zombie when the chain
360
+ ;; ends (or when cap tries exhaust — physically-full-of-zombies table).
178
361
  (block $done (loop $probe
179
362
  (if (i64.eqz (i64.load (local.get $slot)))
180
363
  (then
364
+ (if (local.get $zb) (then (local.set $slot (local.get $zb))))
181
365
  ${seqStore}
182
- (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
366
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
183
367
  (i32.store (i32.sub (local.get $off) (i32.const 8))
184
368
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
185
369
  (br $done)))
186
- (if ${eqExpr} ${onMatch})
370
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
371
+ (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
372
+ (else (if ${eqExpr} ${onMatch})))
187
373
  ${probeNext(entrySize)}
374
+ (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
375
+ (if (i32.ge_s (local.get $ztr) (local.get $cap))
376
+ (then
377
+ (local.set $slot (local.get $zb))
378
+ ${seqStore}
379
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
380
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
381
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
382
+ (br $done)))
188
383
  (br $probe)))
189
384
  (local.get $coll))`
190
385
  }
@@ -240,12 +435,30 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
240
435
  * the key was present (and len decremented), 0 otherwise. Home slots are recomputed
241
436
  * from the stored hash (low 32 bits), so no rehash of the key is needed during the shift. */
242
437
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
438
+ // for-in enum cache invalidation (core.js __hash_keys_ro / object.js
439
+ // emitEnumerateObject): delete is the one key-set change the cache's
440
+ // (off, len) key can miss — a later insert restores the cached len with a
441
+ // different key set. Unconditional clear (not off-compare): the OBJECT-arm
442
+ // cache is keyed by SIDECAR off, but a durable receiver's runtime props live
443
+ // in per-object hashes under __dyn_props whose offs the cache never sees —
444
+ // a delete there must still invalidate. HASH deletes are cold; SET/MAP
445
+ // tables never feed enumeration, so only the HASH instance pays.
446
+ const enumcInval = expectedType === PTR.HASH
447
+ ? `(global.set $__enumc_off (i32.const 0))
448
+ `
449
+ : ''
243
450
  return `(func $${name} (param $coll i64) (param $key i64) (result i32)
244
451
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
245
452
  (local $i i32) (local $j i32) (local $k i32) (local $n i32)
246
453
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
247
- (local.set $off (call $__ptr_offset (local.get $coll)))
454
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
248
455
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
456
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
457
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
458
+ (if (i32.eq (local.get $cap) (i32.const -1))
459
+ (then
460
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
461
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
249
462
  (local.set $h (call ${hashFn} (local.get $key)))
250
463
  ${probeStart(entrySize)}
251
464
  (block $found
@@ -280,7 +493,7 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
280
493
  (br $shift)))
281
494
  (i64.store (local.get $i) (i64.const 0))
282
495
  (i64.store (i32.add (local.get $i) (i32.const 8)) (i64.const 0))
283
- (i32.store (i32.sub (local.get $off) (i32.const 8))
496
+ ${enumcInval}(i32.store (i32.sub (local.get $off) (i32.const 8))
284
497
  (i32.sub (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
285
498
  (i32.const 1))`
286
499
  }
@@ -306,10 +519,16 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
306
519
  return `(func $${name} (param $obj i64) (param $key i64) (param $val i64) (result i64)
307
520
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
308
521
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
309
- (local $oldslot i32) (local $newidx i32) (local $newslot i32)
522
+ (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
310
523
  ${typeGuard}
311
- (local.set $off (call $__ptr_offset (local.get $obj)))
524
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
312
525
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
526
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
527
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
528
+ (if (i32.eq (local.get $cap) (i32.const -1))
529
+ (then
530
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
531
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
313
532
  (local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
314
533
  ;; Grow if load factor > 75%: size * 4 >= cap * 3
315
534
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
@@ -340,8 +559,12 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
340
559
  // Forward-mark the old header (cap=-1 sentinel at -4, new offset at -8) and
341
560
  // keep the boxed pointer the caller holds: any alias resolves through
342
561
  // __ptr_offset. This preserves JS reference identity for a grown dict held in
343
- // multiple places (e.g. ctx.core.emit), which remint cannot.
344
- ? `(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
562
+ // multiple places (e.g. ctx.core.emit), which remint cannot. Log the pre-grow
563
+ // (off, size, cap) first (durableFwdLogIR no-op unless $off predates this
564
+ // round) so `_clear` can heal a durable header instead of leaving it forwarded
565
+ // at an ephemeral target that the next round overwrites.
566
+ ? `${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
567
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
345
568
  (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
346
569
  (local.set $off (local.get $newptr))
347
570
  (local.set $cap (local.get $newcap))`
@@ -353,24 +576,151 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
353
576
  ;; Insert/update
354
577
  (local.set $h (call ${hashFn} (local.get $key)))
355
578
  ${probeStart(entrySize)}
579
+ ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
356
580
  (block $done (loop $probe
357
581
  (if (i64.eqz (i64.load (local.get $slot)))
358
582
  (then
583
+ (if (local.get $zb) (then (local.set $slot (local.get $zb))))
359
584
  ${seqStore}
360
- (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
361
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
585
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
586
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
362
587
  (i32.store (i32.sub (local.get $off) (i32.const 8))
363
588
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
364
589
  (br $done)))
365
- (if ${eqExpr}
590
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
591
+ (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
592
+ (else (if ${eqExpr}
593
+ (then
594
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
595
+ (br $done)))))
596
+ ${probeNext(entrySize)}
597
+ (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
598
+ (if (i32.ge_s (local.get $ztr) (local.get $cap))
366
599
  (then
367
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
600
+ (local.set $slot (local.get $zb))
601
+ ${seqStore}
602
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
603
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
604
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
605
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
368
606
  (br $done)))
369
- ${probeNext(entrySize)}
370
607
  (br $probe)))
371
608
  (local.get $obj))`
372
609
  }
373
610
 
611
+ /** RMW slot upsert — genUpsertGrow's exact machinery (grow + forward-mark +
612
+ * zombie-aware probe) returning the entry's VALUE SLOT ADDRESS instead of
613
+ * storing a value: `o[k] = f(o[k])` fusion (emit-assign.js) hashes and probes
614
+ * ONCE for the read-modify-write instead of a full get + set pair. On insert
615
+ * the value seeds `undefined` (what a plain read of a missing key yields) and
616
+ * the entry-log runs, so the caller's later __slot_write is an ordinary value
617
+ * update. Sound across growth because the caller's BOX never changes: the old
618
+ * header forward-marks and the returned address points into the new table.
619
+ * Returns 0 unless the receiver is a live HASH — caller falls back to the
620
+ * generic dyn read/write pair. */
621
+ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
622
+ return `(func $${name} (param $obj i64) (param $key i64) (result i32)
623
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
624
+ (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
625
+ (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
626
+ (local $kaux i32) (local $koff i32)
627
+ (if (i32.ne (call $__ptr_type (local.get $obj)) (i32.const ${PTR.HASH}))
628
+ (then (return (i32.const 0))))
629
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
630
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
631
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
632
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
633
+ (if (i32.eq (local.get $cap) (i32.const -1))
634
+ (then
635
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
636
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
637
+ (local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
638
+ (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
639
+ (then
640
+ (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
641
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
642
+ (local.set $i (i32.const 0))
643
+ (block $rd (loop $rl
644
+ (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
645
+ (local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
646
+ (if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
647
+ (then
648
+ (local.set $h (call ${hashFn} (i64.load (i32.add (local.get $oldslot) (i32.const 8)))))
649
+ (local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
650
+ (block $ins (loop $probe2
651
+ (local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
652
+ (br_if $ins (i64.eqz (i64.load (local.get $newslot))))
653
+ (local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
654
+ (br $probe2)))
655
+ (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
656
+ (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))
657
+ (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))
658
+ (i32.store (i32.sub (local.get $newptr) (i32.const 8))
659
+ (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
660
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
661
+ (br $rl)))
662
+ ${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
663
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
664
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
665
+ (local.set $off (local.get $newptr))
666
+ (local.set $cap (local.get $newcap))))
667
+ ${hashFn === '$__str_hash' ? `;; tiered $__str_hash: the two FAST arms inline — SSO arithmetic mix and
668
+ ;; the heap lazy-hash-cell load, one of which the dictionary-count hot path
669
+ ;; pays per probe. Cold shapes (interned statics, uncached walk — and the
670
+ ;; one-in-4G SSO mix that hashes to 0) call the helper, which recomputes
671
+ ;; identically. Gates mirror $__str_hash's own exactly.
672
+ (local.set $kaux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
673
+ (local.set $h (i32.const 0))
674
+ (if (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.STRING}))
675
+ (then
676
+ (local.set $koff (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK}))))
677
+ (if (i32.shr_u (local.get $kaux) (i32.const 14))
678
+ (then
679
+ (local.set $h (i32.mul
680
+ (i32.xor (local.get $koff) (i32.mul (i32.xor (i32.and (local.get $kaux) (i32.const 0x1FFF)) (i32.const 0x9E3779B9)) (i32.const 0x85EBCA6B)))
681
+ (i32.const 0xC2B2AE35)))
682
+ (local.set $h (i32.xor (local.get $h) (i32.shr_u (local.get $h) (i32.const 15))))
683
+ ;; $__str_hash's post-mix clamp, replicated EXACTLY (i32.le_s — it
684
+ ;; shifts every NEGATIVE-signed hash by 2, not just 0/1): the
685
+ ;; tiered value must be bit-equal to the helper's return and to
686
+ ;; the lazy hash cells (they cache post-clamp values).
687
+ (if (i32.le_s (local.get $h) (i32.const 1))
688
+ (then (local.set $h (i32.add (local.get $h) (i32.const 2))))))
689
+ (else
690
+ (if (i32.and (i32.ge_u (local.get $koff) (i32.const 8))
691
+ (i32.eq (i32.and (local.get $kaux) (i32.const ${LAYOUT.SLICE_BIT | STR_HCACHE_BIT})) (i32.const ${STR_HCACHE_BIT})))
692
+ (then (local.set $h (i32.load (i32.sub (local.get $koff) (i32.const 8))))))))))
693
+ (if (i32.eqz (local.get $h)) (then (local.set $h (call ${hashFn} (local.get $key)))))`
694
+ : `(local.set $h (call ${hashFn} (local.get $key)))`}
695
+ ${probeStart(entrySize)}
696
+ (block $done (loop $probe
697
+ (if (i64.eqz (i64.load (local.get $slot)))
698
+ (then
699
+ (if (local.get $zb) (then (local.set $slot (local.get $zb))))
700
+ ${seqStore}
701
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
702
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
703
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
704
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
705
+ (br $done)))
706
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
707
+ (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
708
+ (else (if ${eqExpr} (then (br $done)))))
709
+ ${probeNext(entrySize)}
710
+ (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
711
+ (if (i32.ge_s (local.get $ztr) (local.get $cap))
712
+ (then
713
+ (local.set $slot (local.get $zb))
714
+ ${seqStore}
715
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
716
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
717
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
718
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
719
+ (br $done)))
720
+ (br $probe)))
721
+ (i32.add (local.get $slot) (i32.const 16)))`
722
+ }
723
+
374
724
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
375
725
  return `(func $${name} (param $coll i64) (param $key i64) (result i64)
376
726
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
@@ -378,8 +728,14 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
378
728
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
379
729
  (i32.const ${expectedType}))
380
730
  (then (return (i64.const ${missing}))))
381
- (local.set $off (call $__ptr_offset (local.get $coll)))
731
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
382
732
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
733
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
734
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
735
+ (if (i32.eq (local.get $cap) (i32.const -1))
736
+ (then
737
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
738
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
383
739
  (local.set $h (call ${hashFn} (local.get $key)))
384
740
  ${probeStart(entrySize)}
385
741
  (block $done (loop $probe
@@ -411,13 +767,17 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
411
767
  (else ${onEmpty}))))`
412
768
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
413
769
  (then ${onEmpty}))`
414
- // SET/MAP/HASH grow by forward-marking; a boxed pointer may be stale → follow the chain.
415
- const offExpr = '(call $__ptr_offset (local.get $coll))'
416
770
  return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result ${rt})
417
771
  (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
418
772
  ${typeGuard}
419
- (local.set $off ${offExpr})
773
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
420
774
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
775
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
776
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
777
+ (if (i32.eq (local.get $cap) (i32.const -1))
778
+ (then
779
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
780
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
421
781
  ${probeStart(entrySize)}
422
782
  (block $done (loop $probe
423
783
  (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
@@ -431,28 +791,48 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
431
791
 
432
792
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
433
793
  return `(func $${name} (param $obj i64) (param $key i64) (param $h i32) (param $val i64) (result i64)
434
- (local $off i32) (local $cap i32) (local $end i32) (local $slot i32)
794
+ (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $zb i32) (local $ztr i32)
435
795
  (if (i32.ne
436
796
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
437
797
  (i32.const ${expectedType}))
438
798
  (then (return (local.get $obj))))
439
- (local.set $off (call $__ptr_offset (local.get $obj)))
799
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
440
800
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
801
+ ;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
802
+ ;; the live path pays zero extra — the per-probe __ptr_offset call drops
803
+ (if (i32.eq (local.get $cap) (i32.const -1))
804
+ (then
805
+ (local.set $off (call $__ptr_offset_fwd (local.get $off)))
806
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
441
807
  ${probeStart(entrySize)}
808
+ ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
442
809
  (block $done (loop $probe
443
810
  (if (i64.eqz (i64.load (local.get $slot)))
444
811
  (then
812
+ (if (local.get $zb) (then (local.set $slot (local.get $zb))))
445
813
  ${seqStore}
446
- (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
447
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
814
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
815
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
448
816
  (i32.store (i32.sub (local.get $off) (i32.const 8))
449
817
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
450
818
  (br $done)))
451
- (if ${eqExpr}
819
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
820
+ (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
821
+ (else (if ${eqExpr}
822
+ (then
823
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
824
+ (br $done)))))
825
+ ${probeNext(entrySize)}
826
+ (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
827
+ (if (i32.ge_s (local.get $ztr) (local.get $cap))
452
828
  (then
453
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
829
+ (local.set $slot (local.get $zb))
830
+ ${seqStore}
831
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
832
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
833
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
834
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
454
835
  (br $done)))
455
- ${probeNext(entrySize)}
456
836
  (br $probe)))
457
837
  (local.get $obj))`
458
838
  }
@@ -462,24 +842,55 @@ export default (ctx) => {
462
842
  // Feature-gated deps: EXTERNAL-dependent symbols are only pulled when features.external.
463
843
  // Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
464
844
  const ifExt = (name) => () => ctx.features.external ? [name] : []
845
+ // Whether durableFwdLogIR emits a real call (vs '' — see its own comment): only when
846
+ // __heap_reset exists (owned-memory builds; shared memory never declares it — core.js).
847
+ // Gates the deps() edges below the SAME way, so a shared-memory build (where core.js
848
+ // never registers __durable_fwd_log/__durable_fwd_heal) never requests a name nothing
849
+ // delivers.
850
+ const needsDurableFwdLog = () => ctx.scope.globals.has('__heap_reset')
851
+ // durableSlotLogIR's call pair, gated identically (see its comment): every helper
852
+ // that stores a VALUE into a collection slot may log a durable-slot write.
853
+ const slotLogDeps = () => needsDurableFwdLog() ? ['__durable_slot_log', '__is_eph_bits'] : []
465
854
  deps({
466
855
  __same_value_zero: ['__str_eq'],
467
856
  __map_hash: ['__hash', '__str_hash'],
468
- __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'],
469
- __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
857
+ // '__durable_fwd_log' on __set_add/__map_set/__hash_set/__hash_set_local: an
858
+ // EXPLICIT edge, not left to the auto-dep scan genUpsert/genUpsertGrow's
859
+ // `forward` branch always contains a durableFwdLogIR() call, but self-host's
860
+ // realize/regex-scan auto-deps path silently drops a helper reachable only that
861
+ // way (the "Unknown func $__clamp_idx" shape documented in
862
+ // test/selfhost-includes.js) — that test would fail (and the kernel would trap)
863
+ // without these edges.
864
+ // slotLogDeps: hasVal=false skips the VALUE slot-log, but the ENTRY-insert
865
+ // log (durableEntryLogIR) still calls $__durable_slot_log — without the
866
+ // explicit edge the kernel leg drops the helper (auto-scan divergence, the
867
+ // selfhost-includes class) and every `new Set(...)` fails to compile there.
868
+ __set_add: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
869
+ __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
470
870
  __set_delete: ['__map_hash', '__same_value_zero'],
471
- __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'],
472
- __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset'] : ['__map_set', '__ptr_offset'],
473
- __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset'] : ['__same_value_zero', '__ptr_offset'],
474
- __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
871
+ __set_add_all: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_add'],
872
+ __set_filter: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_add', '__set_has', '__map_has'],
873
+ __set_all: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_has', '__map_has'],
874
+ __sclone: ['__sclone_rec', '__mkptr', '__alloc_hdr_n'],
875
+ __sclone_rec: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__ptr_aux', '__is_nullish', '__len', '__alloc', '__alloc_hdr_n', '__mkptr', '__map_get', '__map_set', '__set_add', '__coll_order', '__arr_from', '__obj_clone', '__sclone_hash_vals'],
876
+ __sclone_hash_vals: ['__sclone_rec'],
877
+ __map_set: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
878
+ __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset', '__ptr_offset_fwd'] : ['__map_set', '__ptr_offset', '__ptr_offset_fwd'],
879
+ __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
880
+ __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
475
881
  // Prehashed has-probes: caller folds the hash, so no __map_hash dependency.
476
- __map_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
477
- __set_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
882
+ __map_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
883
+ __set_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
478
884
  __map_delete: ['__map_hash', '__same_value_zero'],
479
- __map_from: ['__ptr_type', '__ptr_offset', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
480
- __hash_set: () => ctx.features.external
481
- ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
482
- : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
885
+ __map_from: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
886
+ // own edge: __map_new's body calls $__alloc_hdr_n — auto-scan-only
887
+ // reachability vanishes under self-host (test/selfhost-includes.js)
888
+ __map_new: ['__alloc_hdr_n'],
889
+ __hash_set: () => [
890
+ ...(ctx.features.external ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set'] : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set']),
891
+ ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []),
892
+ ...slotLogDeps(),
893
+ ],
483
894
  __hash_get: () => ctx.features.external
484
895
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_prop']
485
896
  : ['__str_hash', '__str_eq', '__ptr_type'],
@@ -490,32 +901,35 @@ export default (ctx) => {
490
901
  __hash_new_small: ['__alloc_hdr_n', '__mkptr'],
491
902
  __hash_get_local: ['__str_hash', '__str_eq'],
492
903
  __hash_get_local_h: ['__str_eq'],
493
- __hash_set_local_h: ['__str_eq'],
494
- __hash_set_local: ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr'],
904
+ __hash_set_local_h: () => ['__str_eq', ...slotLogDeps()],
905
+ __hash_set_local: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
906
+ __hash_slot: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__ptr_type', '__ptr_offset', '__ptr_offset_fwd', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
907
+ __slot_write: () => slotLogDeps(),
495
908
  __ihash_get_local: ['__map_hash'],
496
- __ihash_set_local: ['__map_hash', '__alloc_hdr_n', '__mkptr'],
497
- __dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
498
- __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
909
+ __ihash_set_local: () => ['__map_hash', '__alloc_hdr_n', '__mkptr', ...slotLogDeps()],
910
+ __dyn_get_t: ['__dyn_get_t_h', '__str_hash', '__is_str_key', '__to_str'],
911
+ __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish', '__hash_get_local_h', '__str_arr_idx', '__str_byteLen'],
499
912
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
500
- __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
913
+ __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd'],
501
914
  __dyn_get_expr_t_h: ['__dyn_get_t_h', '__hash_get_local_h'],
502
915
  __dyn_get_expr: ['__dyn_get_expr_t', '__ptr_type'],
503
916
  __dyn_get_any: ['__dyn_get_any_t', '__ptr_type'],
504
917
  __dyn_get_any_t: () => ctx.features.external
505
- ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
506
- : ['__dyn_get_t', '__hash_get_local'],
918
+ ? ['__dyn_get_t', '__hash_get_local', '__ext_prop', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd']
919
+ : ['__dyn_get_t', '__hash_get_local', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd'],
507
920
  __dyn_get_any_t_h: () => ctx.features.external
508
921
  ? ['__dyn_get_t_h', '__hash_get_local_h', '__ext_prop']
509
922
  : ['__dyn_get_t_h', '__hash_get_local_h'],
510
923
  __dyn_get_or: ['__dyn_get'],
511
- __dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish', '__str_eq'],
924
+ __dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__ptr_offset_fwd', '__is_nullish', '__str_eq', '__is_str_key', '__to_str', '__arr_set_idx_ptr', '__str_arr_idx'],
512
925
  __dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
513
926
  __hash_del_local: ['__str_hash', '__str_eq', '__ptr_type'],
514
- __dyn_del: ['__hash_del_local', '__ihash_get_local', '__is_nullish'],
515
- __coll_clear: ['__ptr_type', '__ptr_offset'],
927
+ __dyn_del: ['__hash_del_local', '__ihash_get_local', '__is_nullish', '__is_str_key', '__to_str', '__str_arr_idx'],
928
+ __str_arr_idx: ['__str_byteLen', '__char_at'],
929
+ __coll_clear: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd'],
516
930
  })
517
931
 
518
- inc('__ptr_offset', '__cap')
932
+ inc('__ptr_offset', '__ptr_offset_fwd', '__cap')
519
933
 
520
934
  // Monotonic insertion counter packed into each entry's hash-word high 32 bits
521
935
  // (see seqStore). Restores JS insertion order at iteration without growing
@@ -524,8 +938,31 @@ export default (ctx) => {
524
938
  if (!ctx.scope.globals.has('__seq'))
525
939
  declGlobal('__seq', 'i32')
526
940
 
941
+ // for-in enum cache (core.js __hash_keys_ro): cached boxed key array keyed by
942
+ // (table off, live len). Declared here unconditionally — genDelete's HASH
943
+ // invalidation hook references $__enumc_off in its static WAT text, so the
944
+ // global must exist in any build that reaches __hash_del_local, for-in or not
945
+ // (same pattern as __seq/__dyn_props above; watr treeshakes them when unused).
946
+ if (!ctx.scope.globals.has('__enumc_off')) {
947
+ declGlobal('__enumc_off', 'i32')
948
+ declGlobal('__enumc_len', 'i32')
949
+ declGlobal('__enumc_arr', 'f64')
950
+ }
951
+
527
952
  if (!ctx.scope.globals.has('__dyn_props'))
528
953
  declGlobal('__dyn_props', 'f64')
954
+ // Never-false-negative membership filter over offsets ever inserted into the
955
+ // global __dyn_props table: a 64-bit bitset, bit = mix(off) & 63 for every
956
+ // offset key ever written there (see dynPropsFilterSetIR — all 3 insert
957
+ // sites: __dyn_set's global-table fallback, __dyn_move's own rekey, and
958
+ // array.js's headerPropsToGlobalIR). Probe sites (__dyn_move, __dyn_del's
959
+ // global fallback) test the bit first and skip the __ihash_get_local probe
960
+ // on a clear bit — sound because misses are never possible (bits are only
961
+ // ever set, never cleared; a false positive just falls through to the real
962
+ // probe). __dyn_props itself is never reset by __clear (see core.js), so
963
+ // this filter doesn't need resetting either.
964
+ if (!ctx.scope.globals.has('__dyn_props_filter'))
965
+ declGlobal('__dyn_props_filter', 'i64')
529
966
  // 1-slot inline cache for the global __dyn_props lookup. Hot path for
530
967
  // metacircular workloads (watr WAT parser): ~96% of execution sits in
531
968
  // __dyn_get_t / __ihash_get_local. Caches last-seen (off → propsPtr) at
@@ -716,8 +1153,39 @@ export default (ctx) => {
716
1153
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
717
1154
  (f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
718
1155
 
719
- registerGetter('.size', (expr) => {
1156
+ // `.size` on a PROVEN Set/Map: entry count at off-8, direct __len.
1157
+ const sizeLen = (expr) => {
1158
+ inc('__len')
720
1159
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
1160
+ }
1161
+ registerGetter(`.${VAL.SET}:size`, sizeLen)
1162
+ registerGetter(`.${VAL.MAP}:size`, sizeLen)
1163
+ // `.size` on an unproven receiver: in JS only Set/Map expose an entry count —
1164
+ // on everything else `size` is an ordinary own property (or undefined). The
1165
+ // old bare-__len form read the length HEADER of whatever arrived, silently
1166
+ // returning 0 for `{size: 4}` — which broke the self-host kernel reading
1167
+ // `ctx.runtime.internTable.size` (the last L2 byte-parity divergence).
1168
+ // NaN-check guards real numbers whose bit pattern could false-match the tag
1169
+ // compare; the dyn dispatcher covers OBJECT schema slots, HASH keys,
1170
+ // sidecars, and primitives (→ undefined).
1171
+ registerGetter('.size', (expr) => {
1172
+ inc('__len', '__ptr_type', '__dyn_get_expr_t_h')
1173
+ const o = temp('szv')
1174
+ const t = tempI32('szt')
1175
+ const og = ['local.get', `$${o}`]
1176
+ return typed(['block', ['result', 'f64'],
1177
+ ['local.set', `$${o}`, asF64(emit(expr))],
1178
+ ['local.set', `$${t}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', og]]],
1179
+ ['if', ['result', 'f64'],
1180
+ ['i32.and',
1181
+ ['f64.ne', og, og],
1182
+ ['i32.or',
1183
+ ['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.SET]],
1184
+ ['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.MAP]]]],
1185
+ ['then', ['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', og]]]],
1186
+ ['else', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t_h',
1187
+ ['i64.reinterpret_f64', og], asI64(emit(['str', 'size'])), ['local.get', `$${t}`],
1188
+ ['i32.const', strHashLiteral('size')]]]]]], 'f64')
721
1189
  })
722
1190
 
723
1191
  // x instanceof Map / Set — typed-pointer predicates emitted by jzify. NaN-check
@@ -744,6 +1212,60 @@ export default (ctx) => {
744
1212
  ctx.core.stdlib['__set_has_h'] = () => genLookupStrictPrehashed('__set_has_h', SET_ENTRY, sameValueZeroEqG, PTR.SET, UNDEF_NAN, ctx.features.external, false)
745
1213
  ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET)
746
1214
 
1215
+ // ES2025 Set algebra (union/intersection/difference/symmetricDifference +
1216
+ // isSubsetOf/isSupersetOf/isDisjointFrom). Three shared walkers over the
1217
+ // receiver's insertion order (__coll_order — result order is spec-exact); an
1218
+ // `other` that is not a real Set/Map is treated as empty (__set_has/__map_has
1219
+ // type-guard a wrong receiver to 0), the native-litmus line (a proven Set or
1220
+ // Map is in-model; an arbitrary set-like is not, no .has/.keys dispatch).
1221
+ // __set_add's SameValueZero dedup + insertion-seq stamping make add-order the
1222
+ // result order for free.
1223
+ // $stride is the src collection's entry stride (SET_ENTRY for a Set, MAP_ENTRY
1224
+ // for a Map) — a Map's keys sit at slot+8 too, so a Map `other` iterates as a
1225
+ // key set. The key is always at slot+8 in both layouts.
1226
+ const setWalkPreamble = `(local $off i32) (local $cap i32) (local $n i32) (local $ord i32) (local $i i32) (local $slot i32) (local $key i64) (local $has i32)
1227
+ (local.set $off (call $__ptr_offset (local.get $src)))
1228
+ (local.set $cap (call $__cap (local.get $src)))
1229
+ (local.set $n (call $__len (local.get $src)))
1230
+ (local.set $ord (call $__coll_order (local.get $off) (local.get $cap) (local.get $stride)))`
1231
+ const setWalkKey = `(local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
1232
+ (local.set $key (i64.load (i32.add (local.get $slot) (i32.const 8))))`
1233
+ const otherHas = `(if (result i32) (local.get $otherIsMap)
1234
+ (then (call $__map_has (local.get $other) (local.get $key)))
1235
+ (else (call $__set_has (local.get $other) (local.get $key))))`
1236
+ // dst ← dst ∪ src (add every src key in insertion order).
1237
+ ctx.core.stdlib['__set_add_all'] = `(func $__set_add_all (param $dst i64) (param $src i64) (param $stride i32) (result i64)
1238
+ ${setWalkPreamble}
1239
+ (block $d (loop $l
1240
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
1241
+ ${setWalkKey}
1242
+ (local.set $dst (call $__set_add (local.get $dst) (local.get $key)))
1243
+ (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
1244
+ (local.get $dst))`
1245
+ // For each src key, add to dst iff (other has key) == keep. keep=1 → intersection;
1246
+ // keep=0 → difference / symmetricDifference pass.
1247
+ ctx.core.stdlib['__set_filter'] = `(func $__set_filter (param $dst i64) (param $src i64) (param $stride i32) (param $other i64) (param $otherIsMap i32) (param $keep i32) (result i64)
1248
+ ${setWalkPreamble}
1249
+ (block $d (loop $l
1250
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
1251
+ ${setWalkKey}
1252
+ (local.set $has ${otherHas})
1253
+ (if (i32.eq (local.get $has) (local.get $keep))
1254
+ (then (local.set $dst (call $__set_add (local.get $dst) (local.get $key)))))
1255
+ (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
1256
+ (local.get $dst))`
1257
+ // Predicate: return 0 as soon as any src key's presence in other != want; else 1.
1258
+ // isSubsetOf(A,B) = all(A, B, want=1); isDisjointFrom(A,B) = all(A, B, want=0).
1259
+ ctx.core.stdlib['__set_all'] = `(func $__set_all (param $src i64) (param $stride i32) (param $other i64) (param $otherIsMap i32) (param $want i32) (result i32)
1260
+ ${setWalkPreamble}
1261
+ (block $d (loop $l
1262
+ (br_if $d (i32.ge_s (local.get $i) (local.get $n)))
1263
+ ${setWalkKey}
1264
+ (local.set $has ${otherHas})
1265
+ (if (i32.ne (local.get $has) (local.get $want)) (then (return (i32.const 0))))
1266
+ (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
1267
+ (i32.const 1))`
1268
+
747
1269
  // === Map ===
748
1270
 
749
1271
  ctx.core.emit['new.Map'] = (iterExpr) => {
@@ -762,8 +1284,10 @@ export default (ctx) => {
762
1284
 
763
1285
  ctx.core.emit['.set'] = (mapExpr, key, val) => {
764
1286
  inc('__map_set')
765
- const value = val === undefined ? asI64(undefExpr()) : asI64(emit(val))
766
- return typed(['f64.reinterpret_i64', ['call', '$__map_set', asI64(emit(mapExpr)), asI64(emit(key)), value]], 'f64')
1287
+ // Keys and values are boxed-value slots booleans cross as their atom so
1288
+ // set(true, )/get(true) agree on bits and stored values keep identity.
1289
+ const value = val === undefined ? asI64(undefExpr()) : asI64(carrierF64(val, emit(val)))
1290
+ return typed(['f64.reinterpret_i64', ['call', '$__map_set', asI64(emit(mapExpr)), asI64(carrierF64(key, emit(key))), value]], 'f64')
767
1291
  }
768
1292
  ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
769
1293
 
@@ -774,7 +1298,8 @@ export default (ctx) => {
774
1298
  return typed(['f64.reinterpret_i64', ['call', '$__map_get_h', asI64(emit(mapExpr)), asI64(emit(key)), ['i32.const', h]]], 'f64')
775
1299
  }
776
1300
  inc('__map_get')
777
- return typed(['f64.reinterpret_i64', ['call', '$__map_get', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
1301
+ // Key is a boxed-value slot — a bool key probes with the same atom bits .set stored.
1302
+ return typed(['f64.reinterpret_i64', ['call', '$__map_get', asI64(emit(mapExpr)), asI64(carrierF64(key, emit(key)))]], 'f64')
778
1303
  }
779
1304
 
780
1305
  ctx.core.emit['.get'] = emitMapGet
@@ -841,7 +1366,7 @@ export default (ctx) => {
841
1366
  // the uniform closure width (forEach autoloads array → closure floor 2). Uses
842
1367
  // the closure-call path like typedarray:forEach — forEach isn't a hot path.
843
1368
  const collForEach = (stride, valOff, keyOff) => (expr, fn) => {
844
- inc('__ptr_offset', '__cap', '__len', '__coll_order')
1369
+ inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order')
845
1370
  const t = temp('fe'), cb = temp('fecb')
846
1371
  const off = tempI32('feo'), cap = tempI32('fec'), n = tempI32('fen')
847
1372
  const i = tempI32('fei'), ord = tempI32('fer'), slot = tempI32('fes')
@@ -868,6 +1393,324 @@ export default (ctx) => {
868
1393
  ctx.core.emit[`.${VAL.MAP}:forEach`] = collForEach(MAP_ENTRY, 16, 8)
869
1394
  ctx.core.emit[`.${VAL.SET}:forEach`] = collForEach(SET_ENTRY, 8, 8)
870
1395
 
1396
+ // === ES2025 Set algebra emitters ===
1397
+ // A = receiver (proven SET). `other` (B) may be a Set or Map at runtime; its
1398
+ // stride/probe-kind is resolved once via __ptr_type (a Map's keys are its key
1399
+ // set). Set-returning ops thread the fresh dst through the walker (forwarding
1400
+ // is transparent, but re-capture is defensively correct); predicates fold the
1401
+ // i32 result to a boolean carrier.
1402
+ const isMapRT = (f64) => ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', f64]], ['i32.const', PTR.MAP]]
1403
+ const strideRT = (f64) => ['select', ['i32.const', MAP_ENTRY], ['i32.const', SET_ENTRY], isMapRT(f64)]
1404
+ const isMapI32 = (f64) => isMapRT(f64) // 1 if Map, 0 otherwise
1405
+ // Evaluate A,B into temps; hand their f64 accessors to `body`, which returns
1406
+ // the block's final value node. resultType picks Set (f64) vs predicate (i32).
1407
+ const setBin = (a, b, resultType, body) => {
1408
+ inc('__ptr_type')
1409
+ const aT = temp('sopa'), bT = temp('sopb')
1410
+ const aF = typed(['local.get', `$${aT}`], 'f64'), bF = typed(['local.get', `$${bT}`], 'f64')
1411
+ return typed(['block', ['result', resultType],
1412
+ ['local.set', `$${aT}`, asF64(emit(a))],
1413
+ ['local.set', `$${bT}`, asF64(emit(b))],
1414
+ body(aF, bF)], resultType)
1415
+ }
1416
+ // Build the fresh-dst + threaded-walker-call sequence, returning the dst temp.
1417
+ const buildSet = (steps, tag) => {
1418
+ const dst = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag })
1419
+ const dstT = temp('sopd')
1420
+ const dI = () => ['i64.reinterpret_f64', ['local.get', `$${dstT}`]]
1421
+ const seq = ['block', ['result', 'f64'], dst.init, ['local.set', `$${dstT}`, dst.ptr]]
1422
+ for (const call of steps(dstT, dI)) seq.push(['local.set', `$${dstT}`, ['f64.reinterpret_i64', call]])
1423
+ seq.push(['local.get', `$${dstT}`])
1424
+ return seq
1425
+ }
1426
+ const addAll = (dI, srcF, strideIR) => ['call', '$__set_add_all', dI(), ['i64.reinterpret_f64', srcF], strideIR]
1427
+ const filterInto = (dI, srcF, strideIR, otherF, otherIsMap, keep) =>
1428
+ ['call', '$__set_filter', dI(), ['i64.reinterpret_f64', srcF], strideIR, ['i64.reinterpret_f64', otherF], otherIsMap, ['i32.const', keep]]
1429
+ const allMatch = (srcF, strideIR, otherF, otherIsMap, want) =>
1430
+ ['call', '$__set_all', ['i64.reinterpret_f64', srcF], strideIR, ['i64.reinterpret_f64', otherF], otherIsMap, ['i32.const', want]]
1431
+
1432
+ ctx.core.emit['.set:union'] = (a, b) => { inc('__set_add_all')
1433
+ return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
1434
+ addAll(dI, aF, ['i32.const', SET_ENTRY]),
1435
+ addAll(dI, bF, strideRT(bF)),
1436
+ ], 'setu')) }
1437
+
1438
+ ctx.core.emit['.set:intersection'] = (a, b) => { inc('__set_filter', '__len')
1439
+ // Walk the SMALLER operand (ties → `this`, A) for spec-exact result order.
1440
+ return setBin(a, b, 'f64', (aF, bF) => {
1441
+ const aLen = ['call', '$__len', ['i64.reinterpret_f64', aF]]
1442
+ const bLen = ['call', '$__len', ['i64.reinterpret_f64', bF]]
1443
+ const walkA = buildSet((dstT, dI) => [filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 1)], 'seti')
1444
+ const walkB = buildSet((dstT, dI) => [filterInto(dI, bF, strideRT(bF), aF, ['i32.const', 0], 1)], 'seti')
1445
+ return ['if', ['result', 'f64'], ['i32.le_s', aLen, bLen], ['then', walkA], ['else', walkB]]
1446
+ }) }
1447
+
1448
+ ctx.core.emit['.set:difference'] = (a, b) => { inc('__set_filter')
1449
+ // Always A's order (spec: iterate `this`, keep those NOT in other).
1450
+ return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
1451
+ filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0),
1452
+ ], 'setd')) }
1453
+
1454
+ ctx.core.emit['.set:symmetricDifference'] = (a, b) => { inc('__set_filter')
1455
+ // (A not in B, A's order) then (B not in A, B's order).
1456
+ return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
1457
+ filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0),
1458
+ filterInto(dI, bF, strideRT(bF), aF, ['i32.const', 0], 0),
1459
+ ], 'setx')) }
1460
+
1461
+ ctx.core.emit['.set:isSubsetOf'] = (a, b) => { inc('__set_all')
1462
+ // every key of A is in B
1463
+ return setBin(a, b, 'i32', (aF, bF) => allMatch(aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 1)) }
1464
+
1465
+ ctx.core.emit['.set:isSupersetOf'] = (a, b) => { inc('__set_all')
1466
+ // every key of B is in A (walk B; A is always a Set → otherIsMap=0)
1467
+ return setBin(a, b, 'i32', (aF, bF) => allMatch(bF, strideRT(bF), aF, ['i32.const', 0], 1)) }
1468
+
1469
+ ctx.core.emit['.set:isDisjointFrom'] = (a, b) => { inc('__set_all')
1470
+ // no key of A is in B
1471
+ return setBin(a, b, 'i32', (aF, bF) => allMatch(aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0)) }
1472
+
1473
+ // === ES2024 Object.groupBy / Map.groupBy ===
1474
+ // Both bucket items by cb(item, i): Object.groupBy keys a dictionary (HASH)
1475
+ // by ToPropertyKey → __to_str; Map.groupBy keys a Map by SameValueZero (raw
1476
+ // boxed value). Buckets are plain arrays appended in iteration order. The
1477
+ // source normalizes through __iter_arr (Array/String/TypedArray pass through,
1478
+ // Set→keys, Map→entries) and reads elements via the polymorphic __typed_idx.
1479
+ const emitGroupBy = (isMap) => (items, fn) => {
1480
+ inc('__iter_arr', '__len', '__typed_idx', '__arr_push1')
1481
+ inc(...(isMap ? ['__map_set', '__map_get'] : ['__hash_new', '__hash_set', '__hash_get', '__to_str']))
1482
+ const recv = temp('gbs'), cb = temp('gbc'), result = temp('gbr')
1483
+ const len = tempI32('gbl'), i = tempI32('gbi')
1484
+ const item = temp('gbv'), key = tempI64('gbk'), bucket = temp('gbb')
1485
+ const id = ctx.func.uniq++
1486
+ const resI64 = ['i64.reinterpret_f64', ['local.get', `$${result}`]]
1487
+ const nb = allocPtr({ type: PTR.ARRAY, len: 0, cap: 0, tag: 'gbn' })
1488
+ const initResult = isMap
1489
+ ? (() => { const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'gbm' })
1490
+ return ['block', ['result', 'f64'], out.init, out.ptr] })()
1491
+ : ['call', '$__hash_new']
1492
+ const keyOf = (cbResult) => isMap ? asI64(cbResult) : ['call', '$__to_str', asI64(cbResult)]
1493
+ const get = isMap ? '$__map_get' : '$__hash_get'
1494
+ const set = isMap ? '$__map_set' : '$__hash_set'
1495
+ ctx.runtime.throws = true
1496
+ return typed(['block', ['result', 'f64'],
1497
+ ['local.set', `$${recv}`, asF64(emit(['()', '__iter_arr', items]))],
1498
+ ['local.set', `$${cb}`, asF64(emit(fn))],
1499
+ // spec GroupBy step 2: IsCallable(callbackfn) — throw before iterating,
1500
+ // not an indirect-call trap mid-loop
1501
+ ['if', ['i32.eqz', ptrTypeEq(typed(['local.get', `$${cb}`], 'f64'), PTR.CLOSURE)],
1502
+ ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
1503
+ ['local.set', `$${result}`, initResult],
1504
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${recv}`]]]],
1505
+ ['local.set', `$${i}`, ['i32.const', 0]],
1506
+ ['block', `$gbrk${id}`, ['loop', `$gloop${id}`,
1507
+ ['br_if', `$gbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
1508
+ ['local.set', `$${item}`, ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${recv}`]], ['local.get', `$${i}`]]],
1509
+ ['local.set', `$${key}`, keyOf(ctx.closure.call(typed(['local.get', `$${cb}`], 'f64'),
1510
+ [typed(['local.get', `$${item}`], 'f64'), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))],
1511
+ ['local.set', `$${bucket}`, ['f64.reinterpret_i64', ['call', get, resI64, ['local.get', `$${key}`]]]],
1512
+ // miss → fresh bucket, insert it (each distinct key allocates once)
1513
+ ['if', ['i64.eq', ['i64.reinterpret_f64', ['local.get', `$${bucket}`]], ['i64.const', UNDEF_NAN]],
1514
+ ['then',
1515
+ nb.init,
1516
+ ['local.set', `$${bucket}`, nb.ptr],
1517
+ ['local.set', `$${result}`, ['f64.reinterpret_i64',
1518
+ ['call', set, resI64, ['local.get', `$${key}`], ['i64.reinterpret_f64', ['local.get', `$${bucket}`]]]]]]],
1519
+ // push may relocate the bucket — re-store the (possibly moved) pointer
1520
+ ['local.set', `$${bucket}`, ['call', '$__arr_push1', ['i64.reinterpret_f64', ['local.get', `$${bucket}`]], typed(['local.get', `$${item}`], 'f64')]],
1521
+ ['local.set', `$${result}`, ['f64.reinterpret_i64',
1522
+ ['call', set, resI64, ['local.get', `$${key}`], ['i64.reinterpret_f64', ['local.get', `$${bucket}`]]]]],
1523
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1524
+ ['br', `$gloop${id}`]]],
1525
+ ['local.get', `$${result}`]], 'f64')
1526
+ }
1527
+ ctx.core.emit['Object.groupBy'] = emitGroupBy(false)
1528
+ ctx.core.emit['Map.groupBy'] = emitGroupBy(true)
1529
+
1530
+ // === structuredClone — deep arena clone ===
1531
+ // Walks the value graph copying every mutable container: arrays, schema
1532
+ // objects (incl. branded Dates), dictionary HASHes, Set/Map (insertion order
1533
+ // kept; Map keys AND values cloned, like the host), typed arrays, DataViews
1534
+ // and ArrayBuffers (a buffer shared by several views stays shared in the
1535
+ // clone). Numbers/atoms are immediate and strings immutable — passed through.
1536
+ // Closures and host handles raise (the host's DataCloneError). The `transfer`
1537
+ // option is out of model (arena memory has nothing to detach) and ignored.
1538
+ //
1539
+ // Identity memo: a real MAP keyed by boxed-pointer bits (SameValueZero on
1540
+ // non-string pointers ≡ bit identity) — cycles terminate, diamond sharing
1541
+ // dedupes. Every clone target is allocated at its final capacity, so no fill
1542
+ // can trigger a grow: memo'd pointers stay canonical and `===` identity
1543
+ // inside the cloned graph holds bit-exactly. The memo itself may grow, but
1544
+ // __map_set forward-marks, so a stale $memo in an outer frame still resolves.
1545
+
1546
+ ctx.core.stdlib['__sclone'] = `(func $__sclone (param $v f64) (result f64)
1547
+ (call $__sclone_rec (local.get $v)
1548
+ (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
1549
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))))`
1550
+
1551
+ // Deep-clone the values of a freshly copied HASH table, in place.
1552
+ ctx.core.stdlib['__sclone_hash_vals'] = `(func $__sclone_hash_vals (param $off i32) (param $memo i64)
1553
+ (local $cap i32) (local $i i32) (local $slot i32)
1554
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
1555
+ (block $d (loop $l
1556
+ (br_if $d (i32.ge_s (local.get $i) (local.get $cap)))
1557
+ (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${MAP_ENTRY}))))
1558
+ (if (i32.and
1559
+ (i64.ne (i64.load (local.get $slot)) (i64.const 0))
1560
+ (i64.ne (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN})))
1561
+ (then (i64.store (i32.add (local.get $slot) (i32.const 16))
1562
+ (i64.reinterpret_f64 (call $__sclone_rec
1563
+ (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 16))))
1564
+ (local.get $memo))))))
1565
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1566
+ (br $l))))`
1567
+
1568
+ ctx.core.stdlib['__sclone_rec'] = () => {
1569
+ // Guarded: template expansion (pullStdlib) runs AFTER assemble's schema-table
1570
+ // build — an unconditional declGlobal would reset the freshly-baked init offset
1571
+ // back to 0 and every schema consumer would see an empty table.
1572
+ if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
1573
+ return `(func $__sclone_rec (param $v f64) (param $memo i64) (result f64)
1574
+ (local $bits i64) (local $t i32) (local $hit i64) (local $out f64) (local $side i64)
1575
+ (local $src i32) (local $dst i32) (local $n i32) (local $cap i32) (local $i i32)
1576
+ (local $slot i32) (local $ord i32) (local $stride i32) (local $aux i32) (local $root i32) (local $newroot i32)
1577
+ ;; ordinary numbers (incl. ±Infinity) are immediate
1578
+ (if (f64.eq (local.get $v) (local.get $v)) (then (return (local.get $v))))
1579
+ (local.set $bits (i64.reinterpret_f64 (local.get $v)))
1580
+ ;; negative-NaN bit patterns are numeric NaN, never boxes
1581
+ (if (i64.eq (i64.and (local.get $bits) (i64.const 0xFFF0000000000000)) (i64.const 0xFFF0000000000000))
1582
+ (then (return (local.get $v))))
1583
+ (local.set $t (call $__ptr_type (local.get $bits)))
1584
+ ;; atoms (canonical NaN / undefined / null / booleans) + immutable strings: share
1585
+ (if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.ATOM})) (i32.eq (local.get $t) (i32.const ${PTR.STRING})))
1586
+ (then (return (local.get $v))))
1587
+ ;; functions / host handles: DataCloneError
1588
+ (if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.CLOSURE})) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL})))
1589
+ (then (throw $__jz_err (f64.const 0))))
1590
+ ;; already cloned? (cycle / diamond sharing) — __map_get yields raw i64 bits
1591
+ (local.set $hit (call $__map_get (local.get $memo) (local.get $bits)))
1592
+ (if (i32.eqz (call $__is_nullish (local.get $hit))) (then (return (f64.reinterpret_i64 (local.get $hit)))))
1593
+
1594
+ (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
1595
+ (then
1596
+ (local.set $out (call $__arr_from (local.get $bits)))
1597
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1598
+ (local.set $dst (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))))
1599
+ (local.set $n (call $__len (local.get $bits)))
1600
+ (block $ad (loop $al
1601
+ (br_if $ad (i32.ge_s (local.get $i) (local.get $n)))
1602
+ (local.set $slot (i32.add (local.get $dst) (i32.shl (local.get $i) (i32.const 3))))
1603
+ (f64.store (local.get $slot) (call $__sclone_rec (f64.load (local.get $slot)) (local.get $memo)))
1604
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1605
+ (br $al)))
1606
+ (return (local.get $out))))
1607
+
1608
+ (if (i32.eq (local.get $t) (i32.const ${PTR.OBJECT}))
1609
+ (then
1610
+ (local.set $out (call $__obj_clone (local.get $v)))
1611
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1612
+ ;; deep the schema slots — slot count via aux → schema table, like __obj_clone
1613
+ (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
1614
+ (then (local.set $n (call $__len
1615
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (call $__ptr_aux (local.get $bits)) (i32.const 3))))))))
1616
+ (local.set $dst (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))))
1617
+ (block $od (loop $ol
1618
+ (br_if $od (i32.ge_s (local.get $i) (local.get $n)))
1619
+ (local.set $slot (i32.add (local.get $dst) (i32.shl (local.get $i) (i32.const 3))))
1620
+ (f64.store (local.get $slot) (call $__sclone_rec (f64.load (local.get $slot)) (local.get $memo)))
1621
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1622
+ (br $ol)))
1623
+ ;; deep the dyn-props sidecar's values (__obj_clone already re-tabled it)
1624
+ (local.set $side (i64.load (i32.sub (local.get $dst) (i32.const 16))))
1625
+ (if (i32.eq (call $__ptr_type (local.get $side)) (i32.const ${PTR.HASH}))
1626
+ (then (call $__sclone_hash_vals (call $__ptr_offset (local.get $side)) (local.get $memo))))
1627
+ (return (local.get $out))))
1628
+
1629
+ (if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
1630
+ (then
1631
+ (local.set $out (call $__obj_clone (local.get $v)))
1632
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1633
+ (call $__sclone_hash_vals (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))) (local.get $memo))
1634
+ (return (local.get $out))))
1635
+
1636
+ (if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.SET})) (i32.eq (local.get $t) (i32.const ${PTR.MAP})))
1637
+ (then
1638
+ (local.set $stride (select (i32.const ${MAP_ENTRY}) (i32.const ${SET_ENTRY}) (i32.eq (local.get $t) (i32.const ${PTR.MAP}))))
1639
+ (local.set $src (call $__ptr_offset (local.get $bits)))
1640
+ (local.set $cap (i32.load (i32.sub (local.get $src) (i32.const 4))))
1641
+ (local.set $out (call $__mkptr (local.get $t) (i32.const 0)
1642
+ (call $__alloc_hdr_n (i32.const 0) (local.get $cap) (local.get $stride))))
1643
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1644
+ ;; walk the source in insertion order; ≤len inserts into cap slots never grow,
1645
+ ;; so $out's bits stay canonical (the memo entry above remains the pointer)
1646
+ (local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
1647
+ (local.set $ord (call $__coll_order (local.get $src) (local.get $cap) (local.get $stride)))
1648
+ (block $cd (loop $cl
1649
+ (br_if $cd (i32.ge_s (local.get $i) (local.get $n)))
1650
+ (local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
1651
+ (if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
1652
+ (then (drop (call $__map_set (i64.reinterpret_f64 (local.get $out))
1653
+ (i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 8)))) (local.get $memo)))
1654
+ (i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 16)))) (local.get $memo))))))
1655
+ (else (drop (call $__set_add (i64.reinterpret_f64 (local.get $out))
1656
+ (i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 8)))) (local.get $memo)))))))
1657
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1658
+ (br $cl)))
1659
+ (return (local.get $out))))
1660
+
1661
+ (if (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
1662
+ (then
1663
+ (local.set $aux (call $__ptr_aux (local.get $bits)))
1664
+ (local.set $src (call $__ptr_offset (local.get $bits)))
1665
+ (if (i32.and (local.get $aux) (i32.const 8))
1666
+ (then
1667
+ ;; view: clone the root buffer once — memo'd as its boxed BUFFER value,
1668
+ ;; the exact key .buffer reconstructs — and rebase the descriptor
1669
+ (local.set $root (i32.load (i32.add (local.get $src) (i32.const 8))))
1670
+ (local.set $newroot (call $__ptr_offset (i64.reinterpret_f64
1671
+ (call $__sclone_rec (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $root)) (local.get $memo)))))
1672
+ (local.set $dst (call $__alloc (i32.const 16)))
1673
+ (i32.store (local.get $dst) (i32.load (local.get $src)))
1674
+ (i32.store (i32.add (local.get $dst) (i32.const 4))
1675
+ (i32.add (local.get $newroot) (i32.sub (i32.load (i32.add (local.get $src) (i32.const 4))) (local.get $root))))
1676
+ (i32.store (i32.add (local.get $dst) (i32.const 8)) (local.get $newroot))
1677
+ (i32.store (i32.add (local.get $dst) (i32.const 12)) (i32.const 0)))
1678
+ (else
1679
+ ;; owned storage: raw byte copy (header len is in bytes)
1680
+ (local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
1681
+ (local.set $dst (call $__alloc_hdr_n (local.get $n) (local.get $n) (i32.const 1)))
1682
+ (memory.copy (local.get $dst) (local.get $src) (local.get $n))))
1683
+ (local.set $out (call $__mkptr (i32.const ${PTR.TYPED}) (local.get $aux) (local.get $dst)))
1684
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1685
+ (return (local.get $out))))
1686
+
1687
+ (if (i32.eq (local.get $t) (i32.const ${PTR.BUFFER}))
1688
+ (then
1689
+ (local.set $src (call $__ptr_offset (local.get $bits)))
1690
+ (local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
1691
+ (local.set $dst (call $__alloc_hdr_n (local.get $n) (local.get $n) (i32.const 1)))
1692
+ (memory.copy (local.get $dst) (local.get $src) (local.get $n))
1693
+ (local.set $out (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $dst)))
1694
+ (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1695
+ (return (local.get $out))))
1696
+
1697
+ ;; unrecognized tag — pass through
1698
+ (local.get $v))`
1699
+ }
1700
+
1701
+ // structuredClone(value[, options]) — options.transfer ignored (see above).
1702
+ ctx.core.emit['structuredClone'] = (val, _opts) => {
1703
+ inc('__sclone')
1704
+ // __obj_clone/__sclone_rec read $__schema_tbl but only join includes via the
1705
+ // pullStdlib dep-closure — after assemble has already decided whether to build
1706
+ // the table. Flag the consumption explicitly (same hook as the enumeration
1707
+ // scaffolds), or every OBJECT clones as zero slots.
1708
+ ctx.runtime.schemaTblConsumed = true
1709
+ // carrierF64: a bare boolean arg rides as a 0/1 carrier — box it to the
1710
+ // TRUE/FALSE atom so the clone round-trips `true`, not the number 1.
1711
+ return typed(['call', '$__sclone', carrierF64(val, emit(val))], 'f64')
1712
+ }
1713
+
871
1714
  // Generated Map probe functions
872
1715
  ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
873
1716
  ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
@@ -923,33 +1766,46 @@ export default (ctx) => {
923
1766
 
924
1767
  // === HASH — dynamic string-keyed object (type=7) ===
925
1768
 
926
- // FNV-1a hash of string content (works on both SSO and heap strings)
927
- // FNV-1a. ~95M calls in watr self-host. Inline char-fetch: hoist type/offset out of the
928
- // byte loop so SSO branch uses dword shifts and STRING branch uses raw load8_u neither
929
- // calls anything per byte (vs original 1×__char_at __ptr_type + __ptr_offset per byte).
1769
+ // SSO branch: fixed 7-op avalanche mix over the packed NaN-box lo/hi, replacing the
1770
+ // old 6-iteration per-char FNV loop (~36 ops worst case). lo = payload bits 0-31
1771
+ // (the $off field, which for an SSO ptr IS the packed chars 0-3 + low nibble of
1772
+ // char 4 — no memory, no per-char extraction), hi = aux masked to 13 bits (length
1773
+ // + char 4-5 tail; SSO_BIT itself sits at aux bit 14, outside the mask, so it never
1774
+ // pollutes the mix). Same lo/hi pair strHashLiteral (module/collection.js, JS side)
1775
+ // builds from ssoEncode's {offset, aux} — the two MUST compute the identical
1776
+ // constant-for-constant mix or literal-prehashed probes silently miss. Heap strings
1777
+ // (>6 bytes or non-ASCII) are unaffected: they keep byte-FNV-1a below.
1778
+ // ~95M calls in watr self-host; SSO is the overwhelming majority post-invariant
1779
+ // (ec6a229: any ≤6-byte ASCII string IS SSO).
930
1780
  ctx.core.stdlib['__str_hash'] = `(func $__str_hash (param $s i64) (result i32)
931
- (local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $aux i32) (local $w i32)
932
- (local.set $h (i32.const 0x811c9dc5))
1781
+ (local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $aux i32) (local $w i32) (local $hi i32) (local $cs i32)
933
1782
  (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
934
1783
  (local.set $off (i32.wrap_i64 (i64.and (local.get $s) (i64.const ${LAYOUT.OFFSET_MASK}))))
935
1784
  (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
936
1785
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.shr_u (local.get $aux) (i32.const 14)))
937
1786
  (then
938
- (local.set $len (i32.and (i32.shr_u (local.get $aux) (i32.const 10)) (i32.const 7)))
939
- (block $ds (loop $ls
940
- (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
941
- (local.set $h (i32.mul
942
- (i32.xor (local.get $h)
943
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7))) (i64.const 0x7f))))
944
- (i32.const 0x01000193)))
945
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
946
- (br $ls))))
1787
+ (local.set $hi (i32.and (local.get $aux) (i32.const 0x1FFF)))
1788
+ (local.set $h (i32.mul
1789
+ (i32.xor (local.get $off) (i32.mul (i32.xor (local.get $hi) (i32.const 0x9E3779B9)) (i32.const 0x85EBCA6B)))
1790
+ (i32.const 0xC2B2AE35)))
1791
+ (local.set $h (i32.xor (local.get $h) (i32.shr_u (local.get $h) (i32.const 15)))))
947
1792
  (else
1793
+ (local.set $h (i32.const 0x811c9dc5))
948
1794
  ;; canonical interned static: cached post-clamp FNV at -8 (see layout.js)
949
1795
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
950
1796
  (i32.and (i32.ge_u (local.get $off) (i32.const 8))
951
1797
  (i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
952
1798
  (then (return (i32.load (i32.sub (local.get $off) (i32.const 8))))))
1799
+ ;; runtime-built heap string with a lazy hash cell at -8 (STR_HCACHE_BIT,
1800
+ ;; layout.js): 0 = uncomputed — fall through to the FNV walk and fill it
1801
+ ;; below. Mask excludes SLICE (its aux[12:0] is a length, bit 1 incidental).
1802
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
1803
+ (i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT | STR_HCACHE_BIT})) (i32.const ${STR_HCACHE_BIT})))
1804
+ (then
1805
+ (local.set $cs (i32.sub (local.get $off) (i32.const 8)))
1806
+ (local.set $h (i32.load (local.get $cs)))
1807
+ (if (local.get $h) (then (return (local.get $h))))
1808
+ (local.set $h (i32.const 0x811c9dc5))))
953
1809
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
954
1810
  (then (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))))
955
1811
  ;; 4-byte unrolled FNV-1a: each iter loads i32, mixes 4 bytes (little-endian) sequentially.
@@ -972,8 +1828,11 @@ export default (ctx) => {
972
1828
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
973
1829
  (br $lh)))))
974
1830
  ;; Ensure >= 2 (0=empty, 1=tombstone)
975
- (if (result i32) (i32.le_s (local.get $h) (i32.const 1))
976
- (then (i32.add (local.get $h) (i32.const 2))) (else (local.get $h))))`
1831
+ (if (i32.le_s (local.get $h) (i32.const 1))
1832
+ (then (local.set $h (i32.add (local.get $h) (i32.const 2)))))
1833
+ ;; fill the lazy hash cell (post-clamp, so 0 stays unambiguous "uncomputed")
1834
+ (if (local.get $cs) (then (i32.store (local.get $cs) (local.get $h))))
1835
+ (local.get $h))`
977
1836
 
978
1837
  ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
979
1838
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
@@ -991,15 +1850,29 @@ export default (ctx) => {
991
1850
 
992
1851
  ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
993
1852
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEqG, PTR.HASH)
994
- ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
995
- ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, false, true)
1853
+ ctx.core.stdlib['__hash_set_local_h'] = () => genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
1854
+ // Thunked (not called eagerly) so genUpsertGrow's durableFwdLogIR reads
1855
+ // heapResetWat()'s FINAL declaration state — see collection.js's heapResetWat
1856
+ // comment; module load order isn't otherwise settled at the time this string
1857
+ // would eagerly evaluate (same reasoning as module/core.js's __obj_clone).
1858
+ ctx.core.stdlib['__hash_set_local'] = () => genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, false, true)
1859
+ ctx.core.stdlib['__hash_slot'] = () => genSlotUpsert('__hash_slot', MAP_ENTRY, '$__str_hash', strEqG)
1860
+ // The RMW fusion's value update — the store plus the durable-heal protocol
1861
+ // (mirrors durableSlotLogIR exactly; kept in the kernel so heal logic has one home).
1862
+ ctx.core.stdlib['__slot_write'] = () => ctx.scope.globals.has('__heap_reset')
1863
+ ? `(func $__slot_write (param $a i32) (param $v i64)
1864
+ (i64.store (local.get $a) (local.get $v))
1865
+ (if (i32.and (i32.lt_u (local.get $a) ${heapResetWat()}) (call $__is_eph_bits (local.get $v)))
1866
+ (then (call $__durable_slot_log (local.get $a) (i32.const 0)))))`
1867
+ : `(func $__slot_write (param $a i32) (param $v i64)
1868
+ (i64.store (local.get $a) (local.get $v)))`
996
1869
  // Tombstones an entry in a HASH (string keys). Returns 1 if found+deleted, 0 otherwise.
997
1870
  // Used as the bucket-level primitive for __dyn_del.
998
1871
  ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
999
1872
  // Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
1000
1873
  // Uses bit-hash + i64.eq — no string allocation for the unique integer key.
1001
1874
  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)
1002
- ctx.core.stdlib['__ihash_set_local'] = genUpsertGrow('__ihash_set_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH, true)
1875
+ ctx.core.stdlib['__ihash_set_local'] = () => genUpsertGrow('__ihash_set_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH, true)
1003
1876
 
1004
1877
  // Inline __ptr_offset (forwarding-aware) and __hash_get_local body — dyn_get is the
1005
1878
  // single hottest stdlib symbol in watr self-host (~95M calls). props returned by
@@ -1031,10 +1904,38 @@ export default (ctx) => {
1031
1904
  // when the bit-eq already decided — that bare call per schema-key step was
1032
1905
  // the hottest __str_eq producer in the self-host (the kernel includes __jp
1033
1906
  // for optJSON, so the fallback arm is always compiled in).
1907
+ // The __str_eq fallback (JSON-parsed heap keys) is prefixed by an inline
1908
+ // one-SSO⇒ne test when SSO is on: any SSO operand with unequal bits cannot
1909
+ // content-match (≤6-ASCII⇒SSO invariant, module/string.js), so the call —
1910
+ // the hottest __str_eq producer in the self-host — is skipped for every
1911
+ // SSO-keyed miss step; only heap-vs-heap candidates still pay it.
1912
+ // Types allocated via __alloc_hdr/__alloc_hdr_n (see core.js) reserve a 16-byte
1913
+ // header with a propsPtr slot at off-16: ARRAY, OBJECT, TYPED, SET, MAP. HASH is
1914
+ // its own storage (no sidecar — handled by its own dedicated arm). Every OTHER
1915
+ // type (STRING, CLOSURE, ATOM, BUFFER, REGEX, DATE, EXTERNAL, …) has NO such
1916
+ // slot — reading off-16 for one of them walks into whatever memory precedes it
1917
+ // (for a static/interned STRING literal, unrelated PRECEDING STATIC DATA) and
1918
+ // misreads it as a props pointer, occasionally matching the HASH tag by chance
1919
+ // and handing a garbage pointer to __hash_get_local_h → OOB trap. The ORIGINAL
1920
+ // (pre-durable-policy) code never had this gap: each header-type arm below
1921
+ // individually gated on its OWN type tag before ever touching off-16. The
1922
+ // durable-receiver policy's sidecar-check (__dyn_get_t_h, __dyn_del) must gate
1923
+ // on this SAME explicit set, not the broader "not HASH" the global-table probe
1924
+ // uses (that probe is a safe opaque off-keyed hash lookup for any type).
1925
+ const hasPropsSidecarWat = (typeExpr) =>
1926
+ `(i32.or (i32.eq ${typeExpr} (i32.const ${PTR.ARRAY}))
1927
+ (i32.or (i32.eq ${typeExpr} (i32.const ${PTR.OBJECT}))
1928
+ (i32.or (i32.eq ${typeExpr} (i32.const ${PTR.TYPED}))
1929
+ (i32.or (i32.eq ${typeExpr} (i32.const ${PTR.SET}))
1930
+ (i32.eq ${typeExpr} (i32.const ${PTR.MAP}))))))`
1034
1931
  const schemaKeyEq = (storedKey, userKey) => ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
1035
1932
  ? `(if (result i32) (i64.eq ${storedKey} ${userKey})
1036
1933
  (then (i32.const 1))
1037
- (else (call $__str_eq ${storedKey} ${userKey})))`
1934
+ (else ${ctx.features.sso
1935
+ ? `(if (result i32) (i64.ne (i64.and (i64.or ${storedKey} ${userKey}) (i64.const ${SSO_BIT_I64})) (i64.const 0))
1936
+ (then (i32.const 0))
1937
+ (else (call $__str_eq ${storedKey} ${userKey})))`
1938
+ : `(call $__str_eq ${storedKey} ${userKey})`}))`
1038
1939
  : `(i64.eq ${storedKey} ${userKey})`
1039
1940
  const buildObjectSchemaArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
1040
1941
  (if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
@@ -1086,22 +1987,79 @@ export default (ctx) => {
1086
1987
  (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
1087
1988
  (br $schemaSetLoop)))))` : ''
1088
1989
 
1990
+ // Canonical array-index parse of a string key: '0' | [1-9][0-9]{0,9} within
1991
+ // i32 range → the index, else -1. JS property semantics: a canonical numeric
1992
+ // string on an ARRAY receiver addresses the ELEMENT ('1' ≡ 1), so every
1993
+ // string-keyed dyn entry must classify before probing the props sidecar.
1994
+ // __char_at returns the true byte (0 only past the REAL length, which
1995
+ // $__str_byteLen bounds first), so embedded-NUL keys can't false-match.
1996
+ ctx.core.stdlib['__str_arr_idx'] = `(func $__str_arr_idx (param $key i64) (result i32)
1997
+ (local $len i32) (local $i i32) (local $c i32) (local $n i64)
1998
+ (local.set $len (call $__str_byteLen (local.get $key)))
1999
+ (if (i32.or (i32.eqz (local.get $len)) (i32.gt_u (local.get $len) (i32.const 10)))
2000
+ (then (return (i32.const -1))))
2001
+ (if (i32.and (i32.eq (call $__char_at (local.get $key) (i32.const 0)) (i32.const 48))
2002
+ (i32.gt_u (local.get $len) (i32.const 1)))
2003
+ (then (return (i32.const -1))))
2004
+ (block $bad
2005
+ (loop $l
2006
+ (if (i32.ge_u (local.get $i) (local.get $len))
2007
+ (then
2008
+ (if (i64.gt_u (local.get $n) (i64.const 2147483646)) (then (return (i32.const -1))))
2009
+ (return (i32.wrap_i64 (local.get $n)))))
2010
+ (local.set $c (i32.sub (call $__char_at (local.get $key) (local.get $i)) (i32.const 48)))
2011
+ (br_if $bad (i32.gt_u (local.get $c) (i32.const 9)))
2012
+ (local.set $n (i64.add (i64.mul (local.get $n) (i64.const 10)) (i64.extend_i32_u (local.get $c))))
2013
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
2014
+ (br $l)))
2015
+ (i32.const -1))`
2016
+
1089
2017
  ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
1090
2018
  (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
1091
2019
 
1092
- // Thin wrapper: hash the key once, delegate to the prehashed body. Constant-key
1093
- // call sites bypass this and call $__dyn_get_t_h directly with strHashLiteral().
2020
+ // Thin wrapper: ToPropertyKey the runtime key, hash it once, delegate to the
2021
+ // prehashed body. Constant-key call sites bypass this and call $__dyn_get_t_h
2022
+ // directly with strHashLiteral() (compile-time keys are already strings).
2023
+ // ToPropertyKey: a non-string key (number/bool/null/undefined) addresses the
2024
+ // same slot as its string form — o[97] ≡ o['97'] (JS spec). Writes stringify
2025
+ // (emit-assign's staticPropertyKey fold, __dyn_set below), so reads must too.
1094
2026
  ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
2027
+ (if (i32.eqz (call $__is_str_key (local.get $key)))
2028
+ (then (local.set $key (call $__to_str (local.get $key)))))
1095
2029
  (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $type) (call $__str_hash (local.get $key))))`
1096
2030
 
1097
2031
  ctx.core.stdlib['__dyn_get_t_h'] = () => `(func $__dyn_get_t_h (param $obj i64) (param $key i64) (param $type i32) (param $h i32) (result i64)
1098
- (local $props i64) (local $off i32)
2032
+ (local $props i64) (local $off i32) (local $val i64)
1099
2033
  (local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
1100
2034
  ${buildObjectSchemaLocals()}
1101
2035
  ;; Real-number receiver (f===f — pointers are NaN-boxed) has no props: bail before
1102
2036
  ;; treating its bits as a heap offset (\`(5).foo\`/\`(5)[k]\` → undefined, not OOB).
1103
2037
  (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1104
2038
  (then (return (i64.const ${UNDEF_NAN}))))
2039
+ ;; STRING receiver + 'length' key → aux/byte length directly. Strings are
2040
+ ;; primitives — they can never carry dyn props, yet an SSO string's packed
2041
+ ;; chars LOOK like a tiny durable heap offset, so \`op.length\` in a parser
2042
+ ;; loop (jessie: 1.96M reads/run, ~30% of runtime, causally measured by
2043
+ ;; probe-doubling) took the durable global-probe arm for nothing. One i64
2044
+ ;; compare: a runtime 'length' key is ALWAYS SSO (≤6 ASCII invariant), so
2045
+ ;; constant-vs-key bit equality is exact. Other keys fall through unchanged.
2046
+ ;; Form-proof: the const key at _h call sites may be DATA-INTERNED rather
2047
+ ;; than SSO, so bit-equality alone goes dead — gate on the prehashed key
2048
+ ;; hash (compile-time constant, zero cost) and confirm content via
2049
+ ;; __str_eq (SSO/heap-form agnostic).
2050
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
2051
+ (i32.eq (local.get $h) (i32.const ${strHashLiteral('length')})))
2052
+ (then
2053
+ (if (call $__str_eq (local.get $key) (i64.const ${LENGTH_SSO_I64}))
2054
+ (then (return (i64.reinterpret_f64 (f64.convert_i32_s (call $__str_byteLen (local.get $obj)))))))))
2055
+ ;; STRING receivers END here: strings are primitives — no dyn props, no
2056
+ ;; sidecar, no global-table entries (writes below drop, JS semantics), so
2057
+ ;; the probe chain can never produce a value. Method-name lookups on
2058
+ ;; unproven string receivers (cur.charCodeAt in a parser loop — jessie:
2059
+ ;; 1.38M reads/run, every one a guaranteed miss) made this the largest
2060
+ ;; single dyn sink after the array-index fix.
2061
+ (if (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
2062
+ (then (return (i64.const ${UNDEF_NAN}))))
1105
2063
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
1106
2064
  ;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
1107
2065
  ;; global __dyn_props hash on the function table index (negative — can't collide
@@ -1114,20 +2072,125 @@ export default (ctx) => {
1114
2072
  (block $done
1115
2073
  (loop $follow
1116
2074
  (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
1117
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
2075
+ ;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
2076
+ ;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
2077
+ ;; see layout.js's followForwardingWat comment for the full account.
2078
+ (br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
1118
2079
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
1119
2080
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
1120
- (br $follow)))))
2081
+ (br $follow)))
2082
+ ;; Canonical-index string key ('1' ≡ 1, JS array-index semantics) →
2083
+ ;; ELEMENT, not sidecar. This is the single string-keyed net covering
2084
+ ;; every read entry (dot, expr slow path, any, prehashed const keys):
2085
+ ;; __dyn_set routes such keys to elements, so the sidecar can never
2086
+ ;; hold them and an in-range miss is definitively undefined.
2087
+ ;; Inline first-char digit reject: identifier keys ('loc', 'length' —
2088
+ ;; the kernel-hot shape on array AST nodes) skip the parse call.
2089
+ (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2090
+ (i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
2091
+ (then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
2092
+ (else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10)))
2093
+ (then
2094
+ (local.set $idx (call $__str_arr_idx (local.get $key)))
2095
+ (if (i32.ge_s (local.get $idx) (i32.const 0))
2096
+ (then
2097
+ (if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $off) (i32.const 8))))
2098
+ (then (return (i64.load (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3)))))))
2099
+ (return (i64.const ${UNDEF_NAN}))))))))
2100
+ ;; DURABLE-RECEIVER POLICY: a receiver allocated at/below the post-init
2101
+ ;; high-water mark (__heap_reset) outlives _clear, but a sidecar CREATED
2102
+ ;; FOR IT AT RUNTIME lives in the round's arena — the receiver's header
2103
+ ;; slot survives _clear while the sidecar behind it doesn't, so __dyn_set
2104
+ ;; routes runtime (post-init) writes on a durable receiver to the GLOBAL
2105
+ ;; __dyn_props table instead (which __clear resets — prop lifetime
2106
+ ;; matches storage). BUT a durable receiver's off-16 sidecar can ALSO
2107
+ ;; hold real data: __heap_reset is only seeded to its final value at the
2108
+ ;; TAIL of __start, so a prop written *during* init (e.g. a module-level
2109
+ ;; IIFE mutating an init-built table — jz's own compile.js population of
2110
+ ;; watr's opcode dict) sees off >= __heap_reset (still low) at write time
2111
+ ;; and lands in the sidecar — which is itself durable (allocated before
2112
+ ;; the same tail-capture), so nothing dangles. The same receiver can thus
2113
+ ;; carry keys in BOTH places: untouched init-time keys in the sidecar,
2114
+ ;; keys added or reassigned at runtime in the global table. Check global
2115
+ ;; first (a key present in both was necessarily reassigned at runtime,
2116
+ ;; so the newer global entry wins), then the sidecar. HASH is exempted —
2117
+ ;; it is its own storage, no sidecar/global split applies to it.
2118
+ (if (i32.and (i32.ne (local.get $type) (i32.const ${PTR.HASH}))
2119
+ (i32.lt_u (local.get $off) ${heapResetWat()}))
2120
+ (then
2121
+ ;; Header-carrying durable receiver: read its off-16 word ONCE. bit0 =
2122
+ ;; RUNTIME-SHADOWED (set by __dyn_set's global route): only then can a
2123
+ ;; global-table entry exist, so unmarked receivers skip the probe
2124
+ ;; entirely — 60% of jessie's 1.07M durable probes were such
2125
+ ;; always-miss reads (causally measured via probe-doubling, ~18% of
2126
+ ;; runtime). Root≠0 guards the post-_clear stale-marker case (the
2127
+ ;; wiped table must not be probed through a null root). Receivers
2128
+ ;; without a header slot keep the unconditional bloom+probe path.
2129
+ (if (i32.and ${hasPropsSidecarWat('(local.get $type)')} (i32.ge_u (local.get $off) (i32.const 16)))
2130
+ (then
2131
+ (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
2132
+ ;; a shifted ARRAY's word holds forwarding bytes (not 0, not
2133
+ ;; HASH-tagged): __arr_shift migrated its props to the global
2134
+ ;; table and CANNOT mark — such receivers keep the unconditional
2135
+ ;; probe (jump to the no-header path below via $probe).
2136
+ (if (i32.eqz (i32.or (i64.eqz (local.get $props))
2137
+ (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.HASH}))))
2138
+ (then (local.set $props (i64.const 0)) (local.set $tries (i32.const -1))))
2139
+ (if (i32.and (i32.eq (local.get $tries) (i32.const 0))
2140
+ (i32.and (i32.and (i32.wrap_i64 (local.get $props)) (i32.const 1))
2141
+ (f64.ne (global.get $__dyn_props) (f64.const 0))))
2142
+ (then
2143
+ (if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
2144
+ (then
2145
+ (local.set $val (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props))
2146
+ (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
2147
+ (if (i32.eqz (call $__is_nullish (local.get $val)))
2148
+ (then
2149
+ (local.set $val (call $__hash_get_local_h (local.get $val) (local.get $key) (local.get $h)))
2150
+ (if (i64.ne (local.get $val) (i64.const ${UNDEF_NAN})) (then (return (local.get $val))))))))))
2151
+ (local.set $props (i64.and (local.get $props) (i64.const -2)))
2152
+ (if (i32.eq
2153
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
2154
+ (i32.const ${PTR.HASH}))
2155
+ (then (return (call $__hash_get_local_h (local.get $props) (local.get $key) (local.get $h))))))
2156
+ (else (local.set $tries (i32.const -1))))
2157
+ (if (i32.eq (local.get $tries) (i32.const -1))
2158
+ (then
2159
+ (local.set $tries (i32.const 0))
2160
+ (if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
2161
+ (then
2162
+ (local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props))
2163
+ (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
2164
+ (if (i32.eqz (call $__is_nullish (local.get $props)))
2165
+ (then
2166
+ (local.set $val (call $__hash_get_local_h (local.get $props) (local.get $key) (local.get $h)))
2167
+ (if (i64.ne (local.get $val) (i64.const ${UNDEF_NAN})) (then (return (local.get $val))))))))))
2168
+ ;; Miss on both global and sidecar: an OBJECT still needs the
2169
+ ;; schema-slot arm before giving up — a schema-poisoned variable
2170
+ ;; (one variable bound to two different object shapes) resolves its
2171
+ ;; field via the runtime schemaId lookup, not via any dyn-props
2172
+ ;; path. A durable such object has no dyn props at all, so both
2173
+ ;; checks above always miss for it and this must still run before
2174
+ ;; concluding UNDEF. Self-contained here (not a fallthrough into the
2175
+ ;; block below) so this arm's control flow never depends on the
2176
+ ;; ephemeral-only header arms or their shared global-fallback code.
2177
+ ${buildObjectSchemaArm()}
2178
+ (return (i64.const ${UNDEF_NAN}))))
1121
2179
  (block $dynDone
1122
2180
  (block $haveProps
2181
+ ;; Ephemeral-only from here down (durable receivers already
2182
+ ;; returned above) — the off >= __heap_reset conjuncts below are
2183
+ ;; therefore always true, kept for defensive clarity and a single
2184
+ ;; source of truth with __dyn_set/__dyn_del's mirrored gates.
1123
2185
  ;; ARRAY: header propsPtr at $off-16 is valid only when shift hasn't
1124
2186
  ;; rewritten the slot with forwarding bytes. Validate via HASH tag —
1125
2187
  ;; rejects 0 (no props) and forwarding garbage. Misses fall through to
1126
2188
  ;; the global hash, where __arr_shift migrates props on first .shift().
1127
2189
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
1128
- (i32.ge_u (local.get $off) (i32.const 16)))
2190
+ (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2191
+ (i32.ge_u (local.get $off) ${heapResetWat()})))
1129
2192
  (then
1130
- (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
2193
+ (local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1131
2194
  (br_if $haveProps (i32.eq
1132
2195
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
1133
2196
  (i32.const ${PTR.HASH})))
@@ -1138,13 +2201,15 @@ export default (ctx) => {
1138
2201
  (br_if $dynDone (i64.eqz (local.get $props)))
1139
2202
  (local.set $props (i64.const 0))))
1140
2203
  ;; OBJECT: heap-allocated (off >= __heap_start) carries propsPtr at
1141
- ;; off-16 from __alloc_hdr. The slot is either 0 (no dyn props yet) or
2204
+ ;; off-16 from __alloc_hdr but only when also ephemeral (durable-
2205
+ ;; receiver policy above). The slot is either 0 (no dyn props yet) or
1142
2206
  ;; a HASH — no forwarding-garbage case like ARRAY, so a bit-zero test
1143
- ;; is enough. Static-segment objects fall through to the global hash.
2207
+ ;; is enough. Static-segment and durable-heap objects fall through to
2208
+ ;; the global hash.
1144
2209
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
1145
- (i32.ge_u (local.get $off) (global.get $__heap_start)))
2210
+ (i32.ge_u (local.get $off) ${heapResetWat()}))
1146
2211
  (then
1147
- (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
2212
+ (local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1148
2213
  (br_if $dynDone (i64.eqz (local.get $props)))
1149
2214
  (br $haveProps)))
1150
2215
  ;; HASH: a plain dict whose string keys ARE its own bucket entries — the
@@ -1153,19 +2218,22 @@ export default (ctx) => {
1153
2218
  ;; __hash_get; this path serves receivers whose HASH type is only known at
1154
2219
  ;; runtime — e.g. a value read back through a function return, as in
1155
2220
  ;; derive(emitter)[op]. Without it, dyn-get reads the (absent) sidecar and
1156
- ;; reports every key missing.
2221
+ ;; reports every key missing. HASH is its own storage (no sidecar, no
2222
+ ;; global split) — durability is irrelevant here.
1157
2223
  (if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1158
2224
  (then
1159
2225
  (local.set $props (local.get $obj))
1160
2226
  (br $haveProps)))
1161
2227
  ;; Other header types (TYPED/SET/MAP) carry propsPtr at off-16
1162
- ;; directly, bypassing the global __dyn_props hash.
1163
- (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2228
+ ;; directly, bypassing the global __dyn_props hash — again only when
2229
+ ;; ephemeral.
2230
+ (if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2231
+ (i32.ge_u (local.get $off) ${heapResetWat()}))
1164
2232
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
1165
2233
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1166
2234
  (i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
1167
2235
  (then
1168
- (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
2236
+ (local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1169
2237
  (br_if $dynDone (i64.eqz (local.get $props)))
1170
2238
  (br $haveProps)))
1171
2239
  ;; Fall back to the global __dyn_props hash (CLOSURE, shifted ARRAY,
@@ -1216,35 +2284,52 @@ export default (ctx) => {
1216
2284
  (call $__dyn_get_expr_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
1217
2285
 
1218
2286
  ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
1219
- (local $val i64)
2287
+ (local $f f64) (local $idx i32) (local $base i32)
1220
2288
  ;; Real-number receiver → no props; its garbage tag could match HASH and hit the
1221
- ;; __hash_get_local fallback below (heap read at a bogus offset → OOB).
2289
+ ;; __hash_get_local arm below (heap read at a bogus offset → OOB).
1222
2290
  (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1223
2291
  (then (return (i64.const ${UNDEF_NAN}))))
1224
- (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
1225
- (if (result i64)
1226
- (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
1227
- (then (local.get $val))
1228
- (else
1229
- (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
1230
- (then (call $__hash_get_local (local.get $obj) (local.get $key)))
1231
- (else (i64.const ${UNDEF_NAN}))))))`
2292
+ ;; ARRAY + raw integer key ELEMENT read (JS array-index semantics), BEFORE
2293
+ ;; ToPropertyKey. This is the generic \`a[i]\` fallback: the former
2294
+ ;; stringify+str_hash+durable-double-probe chain taxed every numeric read
2295
+ ;; on an unproven array (jessie's charcode tables: 3.7M reads/run). An
2296
+ ;; in-range integer key can only live in the elements (__dyn_set routes it
2297
+ ;; there), so OOB is definitively undefined. Fractional/negative/huge keys
2298
+ ;; fall through to the string path (they are sidecar keys, '1.5'/'-1').
2299
+ (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
2300
+ (then
2301
+ (local.set $f (f64.reinterpret_i64 (local.get $key)))
2302
+ (if (f64.eq (local.get $f) (local.get $f))
2303
+ (then
2304
+ (local.set $idx (i32.trunc_sat_f64_s (local.get $f)))
2305
+ (if (i32.and (f64.eq (f64.convert_i32_s (local.get $idx)) (local.get $f))
2306
+ (i32.ge_s (local.get $idx) (i32.const 0)))
2307
+ (then
2308
+ (local.set $base (call $__ptr_offset (local.get $obj)))
2309
+ (if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $base) (i32.const 8))))
2310
+ (then (return (i64.load (i32.add (local.get $base) (i32.shl (local.get $idx) (i32.const 3)))))))
2311
+ (return (i64.const ${UNDEF_NAN}))))))))
2312
+ ;; ToPropertyKey — see __dyn_get_t; normalized here so the HASH arm reads string-keyed.
2313
+ (if (i32.eqz (call $__is_str_key (local.get $key)))
2314
+ (then (local.set $key (call $__to_str (local.get $key)))))
2315
+ ;; HASH receivers FIRST: hashes never carry dyn_props (those attach to
2316
+ ;; OBJECT/ARRAY only — the invariant __dyn_get_any already exploits), so
2317
+ ;; the former dyn_get_t-then-fallback order paid the full str_hash +
2318
+ ;; durable double-probe chain per read just to miss.
2319
+ (if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
2320
+ (then (return (call $__hash_get_local (local.get $obj) (local.get $key)))))
2321
+ (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))`
1232
2322
 
1233
2323
  // Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
1234
2324
  // is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
1235
2325
  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)
1236
- (local $val i64)
1237
- ;; Real-number receiver → no props; guard the HASH fallback OOB (see __dyn_get_expr_t).
2326
+ ;; Real-number receiver → no props; guard the HASH arm OOB (see __dyn_get_expr_t).
1238
2327
  (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
1239
2328
  (then (return (i64.const ${UNDEF_NAN}))))
1240
- (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
1241
- (if (result i64)
1242
- (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
1243
- (then (local.get $val))
1244
- (else
1245
- (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
1246
- (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
1247
- (else (i64.const ${UNDEF_NAN}))))))`
2329
+ ;; HASH receivers first same wasted-chain argument as __dyn_get_expr_t.
2330
+ (if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
2331
+ (then (return (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))))
2332
+ (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))`
1248
2333
 
1249
2334
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
1250
2335
  // Used at call sites where receiver type is statically unknown.
@@ -1265,7 +2350,29 @@ export default (ctx) => {
1265
2350
  (else (i64.const ${UNDEF_NAN})))`
1266
2351
  : `(i64.const ${UNDEF_NAN})`
1267
2352
  return `(func $__dyn_get_any_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
1268
- (local $val i64)
2353
+ (local $val i64) (local $f f64) (local $idx i32) (local $base i32)
2354
+ ;; Real-number receiver → no props, and its garbage tag could match ARRAY
2355
+ ;; below (bogus base → OOB). Same guard the expression tail repeats.
2356
+ (if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
2357
+ (then (return (i64.const ${UNDEF_NAN}))))
2358
+ ;; ARRAY + raw integer key → element read, before ToPropertyKey — the same
2359
+ ;; generic array-index arm as __dyn_get_expr_t (see there for the analysis).
2360
+ (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
2361
+ (then
2362
+ (local.set $f (f64.reinterpret_i64 (local.get $key)))
2363
+ (if (f64.eq (local.get $f) (local.get $f))
2364
+ (then
2365
+ (local.set $idx (i32.trunc_sat_f64_s (local.get $f)))
2366
+ (if (i32.and (f64.eq (f64.convert_i32_s (local.get $idx)) (local.get $f))
2367
+ (i32.ge_s (local.get $idx) (i32.const 0)))
2368
+ (then
2369
+ (local.set $base (call $__ptr_offset (local.get $obj)))
2370
+ (if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $base) (i32.const 8))))
2371
+ (then (return (i64.load (i32.add (local.get $base) (i32.shl (local.get $idx) (i32.const 3)))))))
2372
+ (return (i64.const ${UNDEF_NAN}))))))))
2373
+ ;; ToPropertyKey — see __dyn_get_t; normalized here so the HASH arm reads string-keyed.
2374
+ (if (i32.eqz (call $__is_str_key (local.get $key)))
2375
+ (then (local.set $key (call $__to_str (local.get $key)))))
1269
2376
  ;; A real number receiver (f===f — NaN-boxed pointers are NaN) has no dynamic
1270
2377
  ;; props: \`(5).foo\` is undefined. Without this guard the bits are reinterpreted as
1271
2378
  ;; a pointer and \`__dyn_get_t\` reads heap at a bogus offset → OOB for large values.
@@ -1314,9 +2421,54 @@ export default (ctx) => {
1314
2421
  // __ptr_offset inlined (forwarding-aware) — only ARRAY ever has forwarding.
1315
2422
  ctx.core.stdlib['__dyn_set'] = () => `(func $__dyn_set (param $obj i64) (param $key i64) (param $val i64) (result i64)
1316
2423
  (local $root i64) (local $props i64) (local $oldProps i64) (local $objKey i64)
1317
- (local $off i32) (local $type i32) ${buildObjectSchemaSetLocals()}
2424
+ (local $off i32) (local $type i32) (local $kf f64) (local $kidx i32) ${buildObjectSchemaSetLocals()}
1318
2425
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
1319
2426
  (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
2427
+ ;; STRING receiver: primitives drop property writes (JS non-strict
2428
+ ;; semantics) — the read path above guarantees UNDEF for them, so
2429
+ ;; storing would only create unreadable entries. NaN guard: a real
2430
+ ;; number's garbage tag may alias STRING; those keep today's path.
2431
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
2432
+ (f64.ne (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj))))
2433
+ (then (return (local.get $val))))
2434
+ ;; ARRAY + integer key → ELEMENT store (grow + hole-fill via the same
2435
+ ;; helper the statically-proven \`a[i]=v\` path uses), matching JS index
2436
+ ;; semantics and the element arms in the dyn read entries. Guard real-
2437
+ ;; number receivers first — their garbage tag could match ARRAY and the
2438
+ ;; store would land OOB. Raw numeric arm before ToPropertyKey (hot);
2439
+ ;; canonical numeric STRING arm after it ('1' ≡ 1). __arr_set_idx_ptr
2440
+ ;; leaves a forwarding header on grow, which every dyn/static reader
2441
+ ;; already follows — binding-unaware callers stay correct.
2442
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
2443
+ (f64.ne (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj))))
2444
+ (then
2445
+ (local.set $kf (f64.reinterpret_i64 (local.get $key)))
2446
+ (if (f64.eq (local.get $kf) (local.get $kf))
2447
+ (then
2448
+ (local.set $kidx (i32.trunc_sat_f64_s (local.get $kf)))
2449
+ (if (i32.and (f64.eq (f64.convert_i32_s (local.get $kidx)) (local.get $kf))
2450
+ (i32.ge_s (local.get $kidx) (i32.const 0)))
2451
+ (then
2452
+ (drop (call $__arr_set_idx_ptr (local.get $obj) (local.get $kidx) (f64.reinterpret_i64 (local.get $val))))
2453
+ (return (local.get $val)))))
2454
+ (else
2455
+ ;; key is non-number here (NaN-boxed) — a bare tag test IS the
2456
+ ;; string test, no __is_str_key call (its NaN guard is redundant).
2457
+ (if (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.STRING}))
2458
+ (then
2459
+ ;; first-char digit reject — see __dyn_get_t_h's net
2460
+ (if (i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
2461
+ (then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
2462
+ (else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10))
2463
+ (then
2464
+ (local.set $kidx (call $__str_arr_idx (local.get $key)))
2465
+ (if (i32.ge_s (local.get $kidx) (i32.const 0))
2466
+ (then
2467
+ (drop (call $__arr_set_idx_ptr (local.get $obj) (local.get $kidx) (f64.reinterpret_i64 (local.get $val))))
2468
+ (return (local.get $val))))))))))))
2469
+ ;; ToPropertyKey — see __dyn_get_t. Stored keys are always strings.
2470
+ (if (i32.eqz (call $__is_str_key (local.get $key)))
2471
+ (then (local.set $key (call $__to_str (local.get $key)))))
1320
2472
  ;; CLOSURE with no env (offset 0): key __dyn_props on the function table index — see __dyn_get_t.
1321
2473
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
1322
2474
  (then (local.set $off (i32.sub (i32.const -1)
@@ -1326,7 +2478,10 @@ export default (ctx) => {
1326
2478
  (block $done
1327
2479
  (loop $follow
1328
2480
  (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
1329
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
2481
+ ;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
2482
+ ;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
2483
+ ;; see layout.js's followForwardingWat comment for the full account.
2484
+ (br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
1330
2485
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
1331
2486
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
1332
2487
  (br $follow)))))
@@ -1335,12 +2490,16 @@ export default (ctx) => {
1335
2490
  ;; skip the global __dyn_props hash entirely. ARRAY also uses this slot, but
1336
2491
  ;; only when shift hasn't overwritten it with forwarding bytes (HASH-tagged
1337
2492
  ;; check rejects 0 + forwarding garbage). Shifted ARRAYs fall back to the
1338
- ;; global __dyn_props where __arr_shift has migrated their props.
2493
+ ;; global __dyn_props where __arr_shift has migrated their props. DURABLE-
2494
+ ;; RECEIVER POLICY (see heapResetWat): a durable receiver (off < the
2495
+ ;; post-init high-water mark) always falls through to the global table too,
2496
+ ;; regardless of shift state — this mirrors __dyn_get_t_h's ARRAY arm.
1339
2497
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
1340
2498
  (then
1341
- (if (i32.ge_u (local.get $off) (i32.const 16))
2499
+ (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2500
+ (i32.ge_u (local.get $off) ${heapResetWat()}))
1342
2501
  (then
1343
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2502
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1344
2503
  (if (i32.or
1345
2504
  (i64.eqz (local.get $oldProps))
1346
2505
  (i32.eq
@@ -1355,13 +2514,14 @@ export default (ctx) => {
1355
2514
  (if (i64.ne (local.get $props) (local.get $oldProps))
1356
2515
  (then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
1357
2516
  (return (local.get $val))))))))
1358
- ;; OBJECT: heap-allocated (off >= __heap_start) writes propsPtr directly at
1359
- ;; off-16. The slot is 0 (init) or HASH — no forwarding-garbage like ARRAY.
1360
- ;; Static-segment OBJECTs fall through to the global __dyn_props.
2517
+ ;; OBJECT: heap-allocated AND ephemeral (durable-receiver policy) writes
2518
+ ;; propsPtr directly at off-16. The slot is 0 (init) or HASH — no
2519
+ ;; forwarding-garbage like ARRAY. Static-segment and durable-heap OBJECTs
2520
+ ;; fall through to the global __dyn_props.
1361
2521
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
1362
- (i32.ge_u (local.get $off) (global.get $__heap_start)))
2522
+ (i32.ge_u (local.get $off) ${heapResetWat()}))
1363
2523
  (then
1364
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2524
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1365
2525
  (local.set $props
1366
2526
  (if (result i64) (i64.eqz (local.get $oldProps))
1367
2527
  (then (i64.reinterpret_f64 (call $__hash_new_small)))
@@ -1378,12 +2538,13 @@ export default (ctx) => {
1378
2538
  (then
1379
2539
  (drop (call $__hash_set_local (local.get $obj) (local.get $key) (local.get $val)))
1380
2540
  (return (local.get $val))))
1381
- (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2541
+ ;; TYPED/SET/MAP header sidecar ephemeral only (durable-receiver policy).
2542
+ (if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16)) (i32.ge_u (local.get $off) ${heapResetWat()}))
1382
2543
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
1383
2544
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1384
2545
  (i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
1385
2546
  (then
1386
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2547
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1387
2548
  (local.set $props
1388
2549
  (if (result i64) (i64.eqz (local.get $oldProps))
1389
2550
  (then (i64.reinterpret_f64 (call $__hash_new_small)))
@@ -1397,18 +2558,50 @@ export default (ctx) => {
1397
2558
  (if (i64.eqz (local.get $root))
1398
2559
  (then (local.set $root (i64.reinterpret_f64 (call $__hash_new)))))
1399
2560
  (local.set $objKey (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off))))
1400
- (local.set $oldProps (call $__ihash_get_local (local.get $root) (local.get $objKey)))
2561
+ ;; Filter: a clear bit proves this offset was never inserted — the probe
2562
+ ;; would miss, so skip straight to "no existing props" without calling
2563
+ ;; __ihash_get_local. A set bit (maybe-present, or a collision) falls
2564
+ ;; through to the real probe. Never a false negative — see filter's decl.
2565
+ (local.set $oldProps
2566
+ (if (result i64) ${dynPropsFilterMissIR('(local.get $off)')}
2567
+ (then (i64.const ${UNDEF_NAN}))
2568
+ (else (call $__ihash_get_local (local.get $root) (local.get $objKey)))))
1401
2569
  (local.set $props
1402
2570
  (if (result i64) (call $__is_nullish (local.get $oldProps))
1403
2571
  (then (i64.reinterpret_f64 (call $__hash_new_small)))
1404
2572
  (else (local.get $oldProps))))
1405
2573
  (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
2574
+ ;; for-in enum cache: a global-side prop insert changes a durable receiver's
2575
+ ;; enumeration without touching the (sidecar-keyed) cache key — clear it.
2576
+ ;; Unconditional: an insert into an EXISTING per-object hash skips the
2577
+ ;; props≠oldProps rekey below, so this can't ride that guard. Cold path.
2578
+ (global.set $__enumc_off (i32.const 0))
1406
2579
  (if (i64.ne (local.get $props) (local.get $oldProps))
1407
2580
  (then
1408
2581
  (local.set $root (call $__ihash_set_local (local.get $root) (local.get $objKey) (local.get $props)))
1409
2582
  (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
2583
+ ${dynPropsFilterSetIR('(local.get $off)')}
1410
2584
  (if (i32.eq (local.get $off) (global.get $__dyn_get_cache_off))
1411
2585
  (then (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props)))))))
2586
+ ;; RUNTIME-SHADOWED MARKER: this durable receiver now has (at least one)
2587
+ ;; runtime-written prop living in the global table — set bit0 of its own
2588
+ ;; off-16 props word so the read path probes the global table ONLY for
2589
+ ;; shadowed receivers (unshadowed durable reads skip straight to the
2590
+ ;; init-time sidecar; measured: 60% of jessie's 1.07M probes are such
2591
+ ;; always-miss reads). Only header-carrying receivers whose word is 0 or
2592
+ ;; a real HASH ptr are marked — a shifted ARRAY's forwarding bytes (tag
2593
+ ;; 0xF) must never be touched, and CLOSURE pseudo-offsets have no header.
2594
+ ;; HASH ptr offsets are 8-aligned, so bit0 is free; every consumer of a
2595
+ ;; DURABLE off-16 word masks it back out (i64.and -2).
2596
+ (if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2597
+ (i32.lt_u (local.get $off) ${heapResetWat()}))
2598
+ ${hasPropsSidecarWat('(local.get $type)')})
2599
+ (then
2600
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
2601
+ (if (i32.or (i64.eqz (local.get $oldProps))
2602
+ (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.HASH})))
2603
+ (then (i64.store (i32.sub (local.get $off) (i32.const 16))
2604
+ (i64.or (local.get $oldProps) (i64.const 1)))))))
1412
2605
  (local.get $val))`
1413
2606
 
1414
2607
  // Tag-dispatched delete (mirrors __dyn_set's dispatch). Returns 1 if a slot was
@@ -1445,9 +2638,18 @@ export default (ctx) => {
1445
2638
 
1446
2639
  ctx.core.stdlib['__dyn_del'] = () => `(func $__dyn_del (param $obj i64) (param $key i64) (result i32)
1447
2640
  (local $root i64) (local $props i64) (local $oldProps i64)
1448
- (local $off i32) (local $type i32) (local $hit i32) ${buildObjectSchemaSetLocals()}
2641
+ (local $off i32) (local $type i32) (local $hit i32) (local $delidx i32) ${buildObjectSchemaSetLocals()}
2642
+ ;; ToPropertyKey — see __dyn_get_t. Stored keys are always strings.
2643
+ (if (i32.eqz (call $__is_str_key (local.get $key)))
2644
+ (then (local.set $key (call $__to_str (local.get $key)))))
1449
2645
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
1450
2646
  (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
2647
+ ;; HASH receiver is ITS OWN storage (dictionary-mode {} — __dyn_set/__dyn_get
2648
+ ;; write/read its entry table directly): delete the entry there. Every arm
2649
+ ;; below only probes the props SIDECAR, which a dictionary doesn't use for
2650
+ ;; its own keys — without this arm, delete d[k] silently no-ops.
2651
+ (if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
2652
+ (then (return (call $__hash_del_local (local.get $obj) (local.get $key)))))
1451
2653
  ${buildObjectSchemaDelArm()}
1452
2654
  ;; CLOSURE with no env: rekey to function table index (parallels __dyn_set / __dyn_get_t_h).
1453
2655
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
@@ -1459,50 +2661,114 @@ export default (ctx) => {
1459
2661
  (block $done
1460
2662
  (loop $follow
1461
2663
  (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
1462
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
2664
+ ;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
2665
+ ;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
2666
+ ;; see layout.js's followForwardingWat comment for the full account.
2667
+ (br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
1463
2668
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
1464
2669
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
1465
- (br $follow)))))
1466
- ;; ARRAY landed propsPtr (HASH-tagged means real sidecar; else fall through to global).
2670
+ (br $follow)))
2671
+ ;; Canonical-index key element home (mirrors __dyn_set/__dyn_get):
2672
+ ;; delete arr[i] leaves a hole (undefined), length unchanged — JS
2673
+ ;; semantics. OOB delete is a no-op that still reports success.
2674
+ (if (i32.ge_u (local.get $off) (i32.const 16))
2675
+ (then
2676
+ (local.set $delidx (i32.const -1))
2677
+ (if (i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
2678
+ (then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
2679
+ (else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10))
2680
+ (then (local.set $delidx (call $__str_arr_idx (local.get $key)))))
2681
+ (if (i32.ge_s (local.get $delidx) (i32.const 0))
2682
+ (then
2683
+ (if (i32.lt_u (local.get $delidx) (i32.load (i32.sub (local.get $off) (i32.const 8))))
2684
+ (then (i64.store (i32.add (local.get $off) (i32.shl (local.get $delidx) (i32.const 3))) (i64.const ${UNDEF_NAN}))))
2685
+ (return (i32.const 1))))))))
2686
+ ;; DURABLE-RECEIVER POLICY (see __dyn_get_t_h's declaration comment for the
2687
+ ;; full rationale): a durable receiver's key can live in EITHER the global
2688
+ ;; table (runtime-written) or its off-16 sidecar (init-time-written) — try
2689
+ ;; BOTH and OR the hit bits, not just the first that resolves. This is the
2690
+ ;; delete-specific twist: a key set at init then reassigned at runtime
2691
+ ;; exists in BOTH places (global shadows it for reads), so deleting only
2692
+ ;; the global copy would leave the stale sidecar entry to resurface on the
2693
+ ;; next get once the (now correctly empty) global lookup falls through to
2694
+ ;; it. HASH is exempted — it is its own storage.
2695
+ (if (i32.and (i32.ne (local.get $type) (i32.const ${PTR.HASH}))
2696
+ (i32.lt_u (local.get $off) ${heapResetWat()}))
2697
+ (then
2698
+ (if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
2699
+ (then
2700
+ (local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
2701
+ (if (i64.ne (local.get $root) (i64.const 0))
2702
+ (then
2703
+ (local.set $props (call $__ihash_get_local (local.get $root) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
2704
+ (if (i32.eqz (call $__is_nullish (local.get $props)))
2705
+ (then (local.set $hit (i32.or (local.get $hit) (call $__hash_del_local (local.get $props) (local.get $key))))))))))
2706
+ (if (i32.and ${hasPropsSidecarWat('(local.get $type)')} (i32.ge_u (local.get $off) (i32.const 16)))
2707
+ (then
2708
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
2709
+ (if (i32.eq
2710
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
2711
+ (i32.const ${PTR.HASH}))
2712
+ (then (local.set $hit (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))))
2713
+ (return (local.get $hit))))
2714
+ ;; ARRAY landed propsPtr (HASH-tagged means real sidecar; else fall through
2715
+ ;; to global). Ephemeral only — durable receivers already returned above.
1467
2716
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
1468
- (i32.ge_u (local.get $off) (i32.const 16)))
2717
+ (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2718
+ (i32.ge_u (local.get $off) ${heapResetWat()})))
1469
2719
  (then
1470
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2720
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1471
2721
  (if (i32.eq
1472
2722
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
1473
2723
  (i32.const ${PTR.HASH}))
1474
2724
  (then (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))))
1475
- ;; OBJECT heap: propsPtr directly at off-16.
2725
+ ;; OBJECT heap: propsPtr directly at off-16 — ephemeral only.
1476
2726
  (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
1477
- (i32.ge_u (local.get $off) (global.get $__heap_start)))
2727
+ (i32.ge_u (local.get $off) ${heapResetWat()}))
1478
2728
  (then
1479
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2729
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1480
2730
  (if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
1481
2731
  (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
1482
- ;; Other header types (TYPED/HASH/SET/MAP).
1483
- (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
2732
+ ;; Other header types (TYPED/HASH/SET/MAP) — ephemeral only.
2733
+ (if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16)) (i32.ge_u (local.get $off) ${heapResetWat()}))
1484
2734
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
1485
2735
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1486
2736
  (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1487
2737
  (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
1488
2738
  (then
1489
- (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
2739
+ (local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
1490
2740
  (if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
1491
2741
  (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
1492
2742
  ;; Fallback: global __dyn_props keyed by offset.
1493
2743
  (local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
1494
2744
  (if (i64.eqz (local.get $root)) (then (return (local.get $hit))))
2745
+ ;; Filter-proven absent: this offset was never inserted into __dyn_props,
2746
+ ;; so there's nothing to delete — skip the __ihash_get_local probe.
2747
+ (if ${dynPropsFilterMissIR('(local.get $off)')} (then (return (local.get $hit))))
1495
2748
  (local.set $props (call $__ihash_get_local (local.get $root) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
1496
2749
  (if (call $__is_nullish (local.get $props)) (then (return (local.get $hit))))
1497
2750
  (i32.or (local.get $hit) (call $__hash_del_local (local.get $props) (local.get $key))))`
1498
2751
 
1499
- ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
2752
+ // Called on EVERY array shift/grow once __dyn_set is included (module has any
2753
+ // dynamic-prop write) — almost always a miss (arrays with dyn props are rare).
2754
+ // The filter bit lets a miss return in O(1) globals-only work instead of an
2755
+ // __ihash_get_local probe (hash + bucket walk). Sound: a clear bit proves
2756
+ // $oldOff was never inserted, so the probe below would find nothing anyway.
2757
+ // Returns i32 1 if an entry was found+rekeyed, 0 on a no-op — callers use this
2758
+ // to know whether to mark the relocated array's header (see DYN_PROPS_GLOBAL_SENTINEL).
2759
+ ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32) (result i32)
1500
2760
  (local $props i64) (local $root i64)
1501
- (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
2761
+ (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return (i32.const 0))))
2762
+ (if ${dynPropsFilterMissIR('(local.get $oldOff)')} (then (return (i32.const 0))))
1502
2763
  (local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $oldOff)))))
1503
- (if (call $__is_nullish (local.get $props)) (then (return)))
2764
+ (if (call $__is_nullish (local.get $props)) (then (return (i32.const 0))))
1504
2765
  (local.set $root (call $__ihash_set_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $newOff))) (local.get $props)))
1505
- (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root))))`
2766
+ (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
2767
+ ${dynPropsFilterSetIR('(local.get $newOff)')}
2768
+ ;; for-in enum cache: props re-keyed to a relocated receiver — global-side
2769
+ ;; enumeration state changed without touching the sidecar-keyed cache. Clear.
2770
+ (global.set $__enumc_off (i32.const 0))
2771
+ (i32.const 1))`
1506
2772
 
1507
2773
  // Generated HASH probe functions
1508
2774
  ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external, true)
@@ -1625,9 +2891,17 @@ export default (ctx) => {
1625
2891
  if (vt === VAL.SET) return typed(['block', ['result', 'f64'], bind, collKeysFromTemp(t, SET_ENTRY)], 'f64')
1626
2892
  if (vt === VAL.MAP) return typed(['block', ['result', 'f64'], bind, collEntriesFromTemp(t, MAP_ENTRY)], 'f64')
1627
2893
  // Unknown receiver: resolve the kind once at runtime (loop-invariant).
1628
- inc('__ptr_type')
2894
+ // ES: for-of / spread over null/undefined is a TypeError ("x is not
2895
+ // iterable") — throw, per spec. The silent zero-iteration this replaces
2896
+ // masked two real self-host miscompiles (a folded undefined-guard and a
2897
+ // never-armed matchAll swallowed their wrong undefineds into empty loops)
2898
+ // before they were caught. Known-vt receivers skip the check entirely.
2899
+ ctx.runtime.throws = true
2900
+ inc('__ptr_type', '__is_nullish')
1629
2901
  const ptrType = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
1630
2902
  return typed(['block', ['result', 'f64'], bind,
2903
+ ['if', ['call', '$__is_nullish', ['i64.reinterpret_f64', ['local.get', `$${t}`]]],
2904
+ ['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
1631
2905
  ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.SET]],
1632
2906
  ['then', collKeysFromTemp(t, SET_ENTRY)],
1633
2907
  ['else', ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.MAP]],
@@ -1726,7 +3000,7 @@ export default (ctx) => {
1726
3000
  // picks the column: 8 = key (Set element / Map key), 16 = Map value. Mirrors
1727
3001
  // object.js's hash*FromTemp. Used by `__iter_arr` and `.keys()`/`.values()`.
1728
3002
  function collKeysFromTemp(t, stride, fieldOff = 8) {
1729
- inc('__ptr_offset', '__cap', '__len', '__coll_order')
3003
+ inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order')
1730
3004
  const off = tempI32('cko'), cap = tempI32('ckc'), n = tempI32('ckn')
1731
3005
  const i = tempI32('cki'), ord = tempI32('ckr'), slot = tempI32('cks')
1732
3006
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cka' })
@@ -1754,7 +3028,7 @@ function collKeysFromTemp(t, stride, fieldOff = 8) {
1754
3028
  // [slot+aOff, slot+bOff] boxed into the output: Map entries use (8,16) → [k,v];
1755
3029
  // Set entries use (8,8) → [v,v].
1756
3030
  function collEntriesFromTemp(t, stride, aOff = 8, bOff = 16) {
1757
- inc('__ptr_offset', '__cap', '__len', '__alloc_hdr', '__coll_order')
3031
+ inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__alloc_hdr', '__coll_order')
1758
3032
  const off = tempI32('ceo'), cap = tempI32('cec'), n = tempI32('cen')
1759
3033
  const i = tempI32('cei'), ord = tempI32('cer'), slot = tempI32('ces'), pair = tempI32('cep')
1760
3034
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cea' })
@@ -1801,7 +3075,7 @@ function arrIdxFromTemp(t) {
1801
3075
 
1802
3076
  // Array.prototype.entries() → dense Array of [index, element] pair arrays.
1803
3077
  function arrEntriesFromTemp(t) {
1804
- inc('__len', '__ptr_offset', '__alloc_hdr')
3078
+ inc('__len', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr')
1805
3079
  const n = tempI32('aen'), i = tempI32('aei'), src = tempI32('aes'), pair = tempI32('aep')
1806
3080
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aea' })
1807
3081
  const id = ctx.func.uniq++