jz 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,10 @@
8
8
  * @module collection
9
9
  */
10
10
 
11
- import { emit, emitFlat, typed, asF64, asI32, valTypeOf, lookupValType, VAL, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr } from '../src/compile.js'
12
- import { inc, PTR } from '../src/ctx.js'
11
+ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
12
+ import { emit, emitFlat } from '../src/emit.js'
13
+ import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
14
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
13
15
 
14
16
  const SET_ENTRY = 16 // hash + key
15
17
  const MAP_ENTRY = 24 // hash + key + value
@@ -26,8 +28,10 @@ const HASH_F64 = new Float64Array(HASH_BUF)
26
28
  const HASH_U32 = new Uint32Array(HASH_BUF)
27
29
 
28
30
  export function numHashLiteral(n) {
31
+ if (Object.is(n, 0) || Object.is(n, -0)) return 2
29
32
  HASH_F64[0] = n
30
- return (HASH_U32[0] ^ HASH_U32[1]) | 0
33
+ const h = (HASH_U32[0] ^ HASH_U32[1]) | 0
34
+ return h <= 1 ? (h + 2) | 0 : h
31
35
  }
32
36
 
33
37
  function numConstLiteral(expr) {
@@ -37,40 +41,39 @@ function numConstLiteral(expr) {
37
41
  }
38
42
 
39
43
  // Equality expressions for probe templates
40
- const f64Eq = '(f64.eq (f64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
41
- const strEq = '(call $__str_eq (f64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
44
+ const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
45
+ const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
42
46
 
43
47
  /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
44
48
  * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
45
49
  * Gated off → type mismatch just returns coll unchanged. */
46
50
  function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
47
- const valParam = hasVal ? '(param $val f64) ' : ''
48
- const storeVal = hasVal ? `\n (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))` : ''
51
+ const valParam = hasVal ? '(param $val i64) ' : ''
52
+ const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))` : ''
49
53
  const onMatch = hasVal
50
- ? `(then\n (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
54
+ ? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
51
55
  : `(then (br $done))`
52
56
 
53
57
  const extBranch = hasVal
54
58
  ? '(then (call $__ext_set (local.get $coll) (local.get $key) (local.get $val)) drop)'
55
59
  : '(then (nop))'
56
- const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))`
60
+ const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
57
61
  const typeGuard = hasExt
58
62
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL})) ${extBranch}) (return (local.get $coll))))`
59
63
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
60
- return `(func $${name} (param $coll f64) (param $key f64) ${valParam}(result f64)
61
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
62
- (local.set $bits (i64.reinterpret_f64 (local.get $coll)))
64
+ return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
65
+ (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
63
66
  ${typeGuard}
64
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
67
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
65
68
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
66
69
  (local.set $h (call ${hashFn} (local.get $key)))
67
70
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
68
71
  (block $done (loop $probe
69
72
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
70
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
73
+ (if (i64.eqz (i64.load (local.get $slot)))
71
74
  (then
72
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
73
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
75
+ (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
76
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
74
77
  (i32.store (i32.sub (local.get $off) (i32.const 8))
75
78
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
76
79
  (br $done)))
@@ -85,34 +88,35 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
85
88
  * wantValue=false: return i32 0/1 existence flag.
86
89
  * hasExt: emit EXTERNAL fallthrough (delegate to __ext_prop/__ext_has). */
87
90
  function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, hasExt) {
88
- const rt = wantValue ? 'f64' : 'i32'
91
+ const rt = wantValue ? 'i64' : 'i32'
89
92
  const onEmpty = wantValue
90
- ? `(return (f64.const nan:${NULL_NAN}))`
93
+ ? `(return (i64.const ${NULL_NAN}))`
91
94
  : '(return (i32.const 0))'
92
95
  const onFound = wantValue
93
- ? '(return (f64.load (i32.add (local.get $slot) (i32.const 16))))'
96
+ ? '(return (i64.load (i32.add (local.get $slot) (i32.const 16))))'
94
97
  : '(return (i32.const 1))'
95
98
  const notFound = wantValue
96
- ? `(f64.const nan:${NULL_NAN})`
99
+ ? `(i64.const ${NULL_NAN})`
97
100
  : '(i32.const 0)'
98
- const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))`
101
+ const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
99
102
  const typeGuard = hasExt
100
103
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
101
- (then (return (call $__ext_${wantValue ? 'prop' : 'has'} (local.get $coll) (local.get $key))))
104
+ (then (return ${wantValue
105
+ ? '(call $__ext_prop (local.get $coll) (local.get $key))'
106
+ : '(call $__ext_has (local.get $coll) (local.get $key))'}))
102
107
  (else ${onEmpty}))))`
103
108
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
104
109
 
105
- return `(func $${name} (param $coll f64) (param $key f64) (result ${rt})
106
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
107
- (local.set $bits (i64.reinterpret_f64 (local.get $coll)))
110
+ return `(func $${name} (param $coll i64) (param $key i64) (result ${rt})
111
+ (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
108
112
  ${typeGuard}
109
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
113
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
110
114
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
111
115
  (local.set $h (call ${hashFn} (local.get $key)))
112
116
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
113
117
  (block $done (loop $probe
114
118
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
115
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0)) (then ${onEmpty}))
119
+ (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
116
120
  (if ${eqExpr} (then ${onFound}))
117
121
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
118
122
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
@@ -123,7 +127,7 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
123
127
 
124
128
  /** Generate delete probe function. Zero out entry on match. */
125
129
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
126
- return `(func $${name} (param $coll f64) (param $key f64) (result i32)
130
+ return `(func $${name} (param $coll i64) (param $key i64) (result i32)
127
131
  (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
128
132
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
129
133
  (local.set $off (call $__ptr_offset (local.get $coll)))
@@ -132,11 +136,11 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
132
136
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
133
137
  (block $done (loop $probe
134
138
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
135
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0)) (then (return (i32.const 0))))
139
+ (if (i64.eqz (i64.load (local.get $slot))) (then (return (i32.const 0))))
136
140
  (if ${eqExpr}
137
141
  (then
138
- (f64.store (local.get $slot) (f64.const 0))
139
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (f64.const 0))
142
+ (i64.store (local.get $slot) (i64.const 0))
143
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (i64.const 0))
140
144
  (return (i32.const 1))))
141
145
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
142
146
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
@@ -163,7 +167,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
163
167
  (then
164
168
  ${nonHashFallback}
165
169
  (return (local.get $obj))))`
166
- return `(func $${name} (param $obj f64) (param $key f64) (param $val f64) (result f64)
170
+ return `(func $${name} (param $obj i64) (param $key i64) (param $val i64) (result i64)
167
171
  (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
168
172
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
169
173
  (local $oldslot i32) (local $newidx i32) (local $newslot i32)
@@ -180,41 +184,41 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
180
184
  (block $rd (loop $rl
181
185
  (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
182
186
  (local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
183
- (if (f64.ne (f64.load (local.get $oldslot)) (f64.const 0))
187
+ (if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
184
188
  (then
185
- (local.set $h (call ${hashFn} (f64.load (i32.add (local.get $oldslot) (i32.const 8)))))
189
+ (local.set $h (call ${hashFn} (i64.load (i32.add (local.get $oldslot) (i32.const 8)))))
186
190
  (local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
187
191
  (block $ins (loop $probe2
188
192
  (local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
189
- (br_if $ins (f64.eq (f64.load (local.get $newslot)) (f64.const 0)))
193
+ (br_if $ins (i64.eqz (i64.load (local.get $newslot))))
190
194
  (local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
191
195
  (br $probe2)))
192
- (f64.store (local.get $newslot) (f64.load (local.get $oldslot)))
193
- (f64.store (i32.add (local.get $newslot) (i32.const 8)) (f64.load (i32.add (local.get $oldslot) (i32.const 8))))
194
- (f64.store (i32.add (local.get $newslot) (i32.const 16)) (f64.load (i32.add (local.get $oldslot) (i32.const 16))))
196
+ (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
197
+ (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))
198
+ (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))
195
199
  (i32.store (i32.sub (local.get $newptr) (i32.const 8))
196
200
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
197
201
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
198
202
  (br $rl)))
199
203
  (local.set $off (local.get $newptr))
200
204
  (local.set $cap (local.get $newcap))
201
- (local.set $obj (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr)))))
205
+ (local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))))
202
206
  ;; Insert/update
203
207
  (local.set $h (call ${hashFn} (local.get $key)))
204
208
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
205
209
  (block $done (loop $probe
206
210
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
207
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
211
+ (if (i64.eqz (i64.load (local.get $slot)))
208
212
  (then
209
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
210
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
211
- (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
213
+ (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
214
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
215
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
212
216
  (i32.store (i32.sub (local.get $off) (i32.const 8))
213
217
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
214
218
  (br $done)))
215
219
  (if ${eqExpr}
216
220
  (then
217
- (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
221
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
218
222
  (br $done)))
219
223
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
220
224
  (br $probe)))
@@ -222,84 +226,81 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
222
226
  }
223
227
 
224
228
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
225
- return `(func $${name} (param $coll f64) (param $key f64) (result f64)
226
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
227
- (local.set $bits (i64.reinterpret_f64 (local.get $coll)))
229
+ return `(func $${name} (param $coll i64) (param $key i64) (result i64)
230
+ (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
228
231
  (if (i32.ne
229
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
232
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
230
233
  (i32.const ${expectedType}))
231
- (then (return (f64.const nan:${missing}))))
232
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
234
+ (then (return (i64.const ${missing}))))
235
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
233
236
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
234
237
  (local.set $h (call ${hashFn} (local.get $key)))
235
238
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
236
239
  (block $done (loop $probe
237
240
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
238
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
239
- (then (return (f64.const nan:${missing}))))
241
+ (if (i64.eqz (i64.load (local.get $slot)))
242
+ (then (return (i64.const ${missing}))))
240
243
  (if ${eqExpr}
241
- (then (return (f64.load (i32.add (local.get $slot) (i32.const 16))))))
244
+ (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
242
245
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
243
246
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
244
247
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
245
248
  (br $probe)))
246
- (f64.const nan:${missing}))`
249
+ (i64.const ${missing}))`
247
250
  }
248
251
 
249
252
  function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing = UNDEF_NAN, hasExt = false) {
250
- const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))`
253
+ const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
251
254
  const typeGuard = hasExt
252
255
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
253
256
  (then
254
257
  (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
255
258
  (then (return (call $__ext_prop (local.get $coll) (local.get $key))))
256
- (else (return (f64.const nan:${missing}))))))`
259
+ (else (return (i64.const ${missing}))))))`
257
260
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
258
- (then (return (f64.const nan:${missing}))))`
259
- return `(func $${name} (param $coll f64) (param $key f64) (param $h i32) (result f64)
260
- (local $bits i64) (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32) (local $tries i32)
261
- (local.set $bits (i64.reinterpret_f64 (local.get $coll)))
261
+ (then (return (i64.const ${missing}))))`
262
+ return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result i64)
263
+ (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32) (local $tries i32)
262
264
  ${typeGuard}
263
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
265
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
264
266
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
265
267
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
266
268
  (block $done (loop $probe
267
269
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
268
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
269
- (then (return (f64.const nan:${missing}))))
270
+ (if (i64.eqz (i64.load (local.get $slot)))
271
+ (then (return (i64.const ${missing}))))
270
272
  (if ${eqExpr}
271
- (then (return (f64.load (i32.add (local.get $slot) (i32.const 16))))))
273
+ (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
272
274
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
273
275
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
274
276
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
275
277
  (br $probe)))
276
- (f64.const nan:${missing}))`
278
+ (i64.const ${missing}))`
277
279
  }
278
280
 
279
281
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
280
- return `(func $${name} (param $obj f64) (param $key f64) (param $h i32) (param $val f64) (result f64)
281
- (local $bits i64) (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32)
282
- (local.set $bits (i64.reinterpret_f64 (local.get $obj)))
282
+ return `(func $${name} (param $obj i64) (param $key i64) (param $h i32) (param $val i64) (result i64)
283
+ (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32)
283
284
  (if (i32.ne
284
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
285
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
285
286
  (i32.const ${expectedType}))
286
287
  (then (return (local.get $obj))))
287
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
288
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
288
289
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
289
290
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
290
291
  (block $done (loop $probe
291
292
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
292
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
293
+ (if (i64.eqz (i64.load (local.get $slot)))
293
294
  (then
294
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
295
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
296
- (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
295
+ (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
296
+ (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
297
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
297
298
  (i32.store (i32.sub (local.get $off) (i32.const 8))
298
299
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
299
300
  (br $done)))
300
301
  (if ${eqExpr}
301
302
  (then
302
- (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
303
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
303
304
  (br $done)))
304
305
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
305
306
  (br $probe)))
@@ -312,12 +313,16 @@ export default (ctx) => {
312
313
  // Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
313
314
  const ifExt = (name) => () => ctx.features.external ? [name] : []
314
315
  Object.assign(ctx.core.stdlibDeps, {
315
- __set_has: ifExt('__ext_has'),
316
- __set_delete: [],
317
- __map_set: ifExt('__ext_set'),
316
+ __same_value_zero: ['__str_eq'],
317
+ __map_hash: ['__hash', '__str_hash'],
318
+ __set_add: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
319
+ __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
320
+ __set_delete: ['__map_hash', '__same_value_zero'],
321
+ __map_set: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
318
322
  __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set'] : ['__map_set'],
319
- __map_get_h: () => ctx.features.external ? ['__ext_prop'] : [],
320
- __map_delete: [],
323
+ __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero'] : ['__same_value_zero'],
324
+ __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
325
+ __map_delete: ['__map_hash', '__same_value_zero'],
321
326
  __hash_set: () => ctx.features.external
322
327
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
323
328
  : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
@@ -328,12 +333,13 @@ export default (ctx) => {
328
333
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_has']
329
334
  : ['__str_hash', '__str_eq', '__ptr_type'],
330
335
  __hash_new: ['__alloc_hdr'],
336
+ __hash_new_small: ['__alloc_hdr', '__mkptr'],
331
337
  __hash_get_local: ['__str_hash', '__str_eq'],
332
338
  __hash_get_local_h: ['__str_eq'],
333
339
  __hash_set_local_h: ['__str_eq'],
334
340
  __hash_set_local: ['__str_hash', '__str_eq'],
335
- __ihash_get_local: ['__hash'],
336
- __ihash_set_local: ['__hash', '__alloc_hdr', '__mkptr'],
341
+ __ihash_get_local: ['__map_hash'],
342
+ __ihash_set_local: ['__map_hash', '__alloc_hdr', '__mkptr'],
337
343
  __dyn_get_t: ['__ihash_get_local', '__str_hash', '__str_eq', '__is_nullish'],
338
344
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
339
345
  __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
@@ -343,7 +349,7 @@ export default (ctx) => {
343
349
  ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
344
350
  : ['__dyn_get_t', '__hash_get_local'],
345
351
  __dyn_get_or: ['__dyn_get'],
346
- __dyn_set: ['__hash_new', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish'],
352
+ __dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish', '__str_eq'],
347
353
  __dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
348
354
  })
349
355
 
@@ -351,6 +357,16 @@ export default (ctx) => {
351
357
 
352
358
  if (!ctx.scope.globals.has('__dyn_props'))
353
359
  ctx.scope.globals.set('__dyn_props', '(global $__dyn_props (mut f64) (f64.const 0))')
360
+ // 1-slot inline cache for the global __dyn_props lookup. Hot path for
361
+ // metacircular workloads (watr WAT parser): ~96% of execution sits in
362
+ // __dyn_get_t / __ihash_get_local. Caches last-seen (off → propsPtr) at
363
+ // the top of __dyn_get_t; invalidated by __dyn_set when the same off's
364
+ // propsPtr is replaced (rehash on grow). Sentinel cache_off = -1 cannot
365
+ // collide with a real memory offset (always non-negative i32).
366
+ if (!ctx.scope.globals.has('__dyn_get_cache_off'))
367
+ ctx.scope.globals.set('__dyn_get_cache_off', '(global $__dyn_get_cache_off (mut i32) (i32.const -1))')
368
+ if (!ctx.scope.globals.has('__dyn_get_cache_props'))
369
+ ctx.scope.globals.set('__dyn_get_cache_props', '(global $__dyn_get_cache_props (mut f64) (f64.const 0))')
354
370
  // Schema name table for __dyn_get's OBJECT-schema fallback (polymorphic-receiver
355
371
  // `.prop` access). Same declaration as json.js — defined here too so collection
356
372
  // doesn't transitively require json. compile.js's schemaInit populates it when
@@ -358,17 +374,61 @@ export default (ctx) => {
358
374
  if (!ctx.scope.globals.has('__schema_tbl'))
359
375
  ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
360
376
 
361
- ctx.core.stdlib['__ext_prop'] = '(import "env" "__ext_prop" (func $__ext_prop (param f64 f64) (result f64)))'
362
- ctx.core.stdlib['__ext_has'] = '(import "env" "__ext_has" (func $__ext_has (param f64 f64) (result i32)))'
363
- ctx.core.stdlib['__ext_set'] = '(import "env" "__ext_set" (func $__ext_set (param f64 f64 f64) (result i32)))'
364
- ctx.core.stdlib['__ext_call'] = '(import "env" "__ext_call" (func $__ext_call (param f64 f64 f64) (result f64)))'
377
+ // __ext_* imports carry NaN-boxed pointers across the env boundary as i64
378
+ // (not f64) to dodge V8's f64 NaN canonicalization at the wasm↔JS edge
379
+ // same hazard as env.print / env.setTimeout (see module/console.js header).
380
+ // i32 returns (has/set) and arg shapes stay; only boxed-pointer carriers move.
381
+ ctx.core.stdlib['__ext_prop'] = '(import "env" "__ext_prop" (func $__ext_prop (param i64 i64) (result i64)))'
382
+ ctx.core.stdlib['__ext_has'] = '(import "env" "__ext_has" (func $__ext_has (param i64 i64) (result i32)))'
383
+ ctx.core.stdlib['__ext_set'] = '(import "env" "__ext_set" (func $__ext_set (param i64 i64 i64) (result i32)))'
384
+ ctx.core.stdlib['__ext_call'] = '(import "env" "__ext_call" (func $__ext_call (param i64 i64 i64) (result i64)))'
365
385
  // Hash function: simple f64 → i32 hash
366
- ctx.core.stdlib['__hash'] = `(func $__hash (param $v f64) (result i32)
386
+ ctx.core.stdlib['__hash'] = `(func $__hash (param $v i64) (result i32)
367
387
  (i32.wrap_i64 (i64.xor
368
- (i64.reinterpret_f64 (local.get $v))
369
- (i64.shr_u (i64.reinterpret_f64 (local.get $v)) (i64.const 32)))))`
388
+ (local.get $v)
389
+ (i64.shr_u (local.get $v) (i64.const 32)))))`
370
390
  inc('__hash')
371
391
 
392
+ ctx.core.stdlib['__same_value_zero'] = `(func $__same_value_zero (param $a i64) (param $b i64) (result i32)
393
+ (local $fa f64) (local $fb f64) (local $ta i32) (local $tb i32)
394
+ (if (result i32) (i64.eq (local.get $a) (local.get $b))
395
+ (then (i32.const 1))
396
+ (else
397
+ (local.set $fa (f64.reinterpret_i64 (local.get $a)))
398
+ (local.set $fb (f64.reinterpret_i64 (local.get $b)))
399
+ (if (result i32)
400
+ (i32.and
401
+ (f64.eq (local.get $fa) (local.get $fa))
402
+ (f64.eq (local.get $fb) (local.get $fb)))
403
+ (then (f64.eq (local.get $fa) (local.get $fb)))
404
+ (else
405
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
406
+ (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
407
+ (if (result i32)
408
+ (i32.and
409
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
410
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
411
+ (then (call $__str_eq (local.get $a) (local.get $b)))
412
+ (else (i32.const 0))))))))`
413
+
414
+ ctx.core.stdlib['__map_hash'] = `(func $__map_hash (param $v i64) (result i32)
415
+ (local $f f64) (local $t i32) (local $h i32)
416
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
417
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $v) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
418
+ ;; NaN-boxed strings carry the tag inside a NaN payload. Regular numbers
419
+ ;; (e.g. f64.convert_i32_s offsets used as __ihash keys) can alias mantissa
420
+ ;; bits onto the type slot — gate the str-hash dispatch on actual NaN.
421
+ (if (i32.and (f64.ne (local.get $f) (local.get $f))
422
+ (i32.eq (local.get $t) (i32.const ${PTR.STRING})))
423
+ (then (return (call $__str_hash (local.get $v)))))
424
+ (if (f64.eq (local.get $f) (f64.const 0)) (then (return (i32.const 2))))
425
+ (if (i32.and (i32.eq (local.get $t) (i32.const 0)) (f64.ne (local.get $f) (local.get $f)))
426
+ (then (return (i32.const 3))))
427
+ (local.set $h (call $__hash (local.get $v)))
428
+ (if (result i32) (i32.le_s (local.get $h) (i32.const 1))
429
+ (then (i32.add (local.get $h) (i32.const 2)))
430
+ (else (local.get $h))))`
431
+
372
432
  // __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
373
433
  ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
374
434
  (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
@@ -384,27 +444,27 @@ export default (ctx) => {
384
444
 
385
445
  ctx.core.emit['.add'] = (setExpr, val) => {
386
446
  inc('__set_add')
387
- return typed(['call', '$__set_add', asF64(emit(setExpr)), asF64(emit(val))], 'f64')
447
+ return typed(['f64.reinterpret_i64', ['call', '$__set_add', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
388
448
  }
389
449
 
390
450
  ctx.core.emit['.has'] = (setExpr, val) => {
391
451
  inc('__set_has')
392
- return typed(['f64.convert_i32_s', ['call', '$__set_has', asF64(emit(setExpr)), asF64(emit(val))]], 'f64')
452
+ return typed(['f64.convert_i32_s', ['call', '$__set_has', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
393
453
  }
394
454
 
395
455
  ctx.core.emit['.delete'] = (setExpr, val) => {
396
456
  inc('__set_delete')
397
- return typed(['f64.convert_i32_s', ['call', '$__set_delete', asF64(emit(setExpr)), asF64(emit(val))]], 'f64')
457
+ return typed(['f64.convert_i32_s', ['call', '$__set_delete', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
398
458
  }
399
459
 
400
460
  ctx.core.emit['.size'] = (expr) => {
401
- return typed(['f64.convert_i32_s', ['call', '$__len', asF64(emit(expr))]], 'f64')
461
+ return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
402
462
  }
403
463
 
404
464
  // Generated Set probe functions
405
- ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__hash', f64Eq, PTR.SET, false, ctx.features.external)
406
- ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__hash', f64Eq, PTR.SET, false, ctx.features.external)
407
- ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__hash', f64Eq, PTR.SET)
465
+ ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
466
+ ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
467
+ ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET)
408
468
 
409
469
  // === Map ===
410
470
 
@@ -416,7 +476,8 @@ export default (ctx) => {
416
476
 
417
477
  ctx.core.emit['.set'] = (mapExpr, key, val) => {
418
478
  inc('__map_set')
419
- return typed(['call', '$__map_set', asF64(emit(mapExpr)), asF64(emit(key)), asF64(emit(val))], 'f64')
479
+ const value = val === undefined ? asI64(undefExpr()) : asI64(emit(val))
480
+ return typed(['f64.reinterpret_i64', ['call', '$__map_set', asI64(emit(mapExpr)), asI64(emit(key)), value]], 'f64')
420
481
  }
421
482
  ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
422
483
 
@@ -424,19 +485,31 @@ export default (ctx) => {
424
485
  const constKey = numConstLiteral(key)
425
486
  if (constKey != null) {
426
487
  inc('__map_get_h')
427
- return typed(['call', '$__map_get_h', asF64(emit(mapExpr)), asF64(emit(key)), ['i32.const', numHashLiteral(constKey)]], 'f64')
488
+ return typed(['f64.reinterpret_i64', ['call', '$__map_get_h', asI64(emit(mapExpr)), asI64(emit(key)), ['i32.const', numHashLiteral(constKey)]]], 'f64')
428
489
  }
429
490
  inc('__map_get')
430
- return typed(['call', '$__map_get', asF64(emit(mapExpr)), asF64(emit(key))], 'f64')
491
+ return typed(['f64.reinterpret_i64', ['call', '$__map_get', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
431
492
  }
432
493
 
433
494
  ctx.core.emit['.get'] = emitMapGet
434
495
  ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
435
496
 
497
+ ctx.core.emit[`.${VAL.MAP}:has`] = (mapExpr, key) => {
498
+ inc('__map_has')
499
+ return typed(['f64.convert_i32_s', ['call', '$__map_has', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
500
+ }
501
+
502
+ ctx.core.emit[`.${VAL.MAP}:delete`] = (mapExpr, key) => {
503
+ inc('__map_delete')
504
+ return typed(['f64.convert_i32_s', ['call', '$__map_delete', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
505
+ }
506
+
436
507
  // Generated Map probe functions
437
- ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__hash', f64Eq, PTR.MAP, true, ctx.features.external)
438
- ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__hash', f64Eq, PTR.MAP, true, ctx.features.external)
439
- ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, f64Eq, PTR.MAP, NULL_NAN, ctx.features.external)
508
+ ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
509
+ ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
510
+ ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEq, PTR.MAP, NULL_NAN, ctx.features.external)
511
+ ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, false, ctx.features.external)
512
+ ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP)
440
513
 
441
514
  // === HASH — dynamic string-keyed object (type=7) ===
442
515
 
@@ -444,15 +517,15 @@ export default (ctx) => {
444
517
  // FNV-1a. ~95M calls in watr self-host. Inline char-fetch: hoist type/offset out of the
445
518
  // byte loop so SSO branch uses dword shifts and STRING branch uses raw load8_u — neither
446
519
  // calls anything per byte (vs original 1×__char_at → __ptr_type + __ptr_offset per byte).
447
- ctx.core.stdlib['__str_hash'] = `(func $__str_hash (param $s f64) (result i32)
448
- (local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $bits i64) (local $w i32)
520
+ ctx.core.stdlib['__str_hash'] = `(func $__str_hash (param $s i64) (result i32)
521
+ (local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $aux i32) (local $w i32)
449
522
  (local.set $h (i32.const 0x811c9dc5))
450
- (local.set $bits (i64.reinterpret_f64 (local.get $s)))
451
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
452
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
453
- (if (i32.eq (local.get $t) (i32.const ${PTR.SSO}))
523
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
524
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $s) (i64.const ${LAYOUT.OFFSET_MASK}))))
525
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
526
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.shr_u (local.get $aux) (i32.const 14)))
454
527
  (then
455
- (local.set $len (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
528
+ (local.set $len (i32.and (local.get $aux) (i32.const 7)))
456
529
  (block $ds (loop $ls
457
530
  (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
458
531
  (local.set $h (i32.mul
@@ -491,14 +564,21 @@ export default (ctx) => {
491
564
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
492
565
  (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
493
566
 
567
+ // Small initial capacity for propsPtr-style hashes (per-object dyn props).
568
+ // Most receivers in real code carry 0-2 dyn props; paying 8-slot up-front
569
+ // is wasted memory + probe-loop cache pressure. Grows to 4/8/... on demand.
570
+ ctx.core.stdlib['__hash_new_small'] = `(func $__hash_new_small (result f64)
571
+ (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
572
+ (call $__alloc_hdr (i32.const 0) (i32.const 2) (i32.const ${MAP_ENTRY}))))`
573
+
494
574
  ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
495
575
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
496
576
  ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
497
577
  ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true)
498
- // Outer __dyn_props hash: keyed by object offset (i32 as f64), value is per-object props hash.
499
- // Uses bit-hash + f64.eq — no string allocation for the unique integer key.
500
- ctx.core.stdlib['__ihash_get_local'] = genLookupStrict('__ihash_get_local', MAP_ENTRY, '$__hash', f64Eq, PTR.HASH)
501
- ctx.core.stdlib['__ihash_set_local'] = genUpsertGrow('__ihash_set_local', MAP_ENTRY, '$__hash', f64Eq, PTR.HASH, true)
578
+ // Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
579
+ // Uses bit-hash + i64.eq — no string allocation for the unique integer key.
580
+ 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)
581
+ 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)
502
582
 
503
583
  // Inline __ptr_offset (forwarding-aware) and __hash_get_local body — dyn_get is the
504
584
  // single hottest stdlib symbol in watr self-host (~95M calls). props returned by
@@ -511,91 +591,184 @@ export default (ctx) => {
511
591
  // time but lives at runtime in the NaN-box aux bits. Gated on schema name table
512
592
  // presence (lifted in compile.js whenever __dyn_get is included). Static-shape
513
593
  // monomorphic OBJECTs hit the compile-time slot read path and never reach here.
514
- const hasSchemas = ctx.schema.list.length > 0
515
- const objectSchemaArm = hasSchemas ? `
594
+ // Wrapped in a factory: `ctx.schema.list.length` is observed at template
595
+ // expansion time, after all schemas have been registered. Setting the
596
+ // template at module-init froze hasSchemas to false and dropped the arm
597
+ // for any schema registered later in the compile (the common case for
598
+ // anonymous-literal arguments crossing call boundaries).
599
+ // Schema-arm key compare uses i64.eq instead of __str_eq: schema keys and
600
+ // the call-site key both come from the interned string pool (same NaN-box
601
+ // bits for identical literals), so bit-equality is correct and skips a
602
+ // per-iter function call. Real-world strings sharing prefix bytes are not
603
+ // a concern here — keys are static literals from the source program.
604
+ // Schema-arm key compare: i64.eq first for the static-shape case (compile-time
605
+ // schemas hold pool-interned keys with identical NaN-box bits as call-site
606
+ // literals — single bit-eq decides). Falls back to __str_eq when bits differ
607
+ // so runtime-registered schemas (e.g. JSON.parse OBJECTs whose keys are
608
+ // freshly heap-allocated by __jp_str) still resolve correctly.
609
+ const schemaKeyEq = (storedKey, userKey) => ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
610
+ ? `(i32.or
611
+ (i64.eq ${storedKey} ${userKey})
612
+ (call $__str_eq ${storedKey} ${userKey}))`
613
+ : `(i64.eq ${storedKey} ${userKey})`
614
+ const buildObjectSchemaArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
516
615
  (if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
517
616
  (then
518
617
  (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
519
618
  (then
520
619
  (local.set $sid (i32.wrap_i64 (i64.and (i64.shr_u
521
- (i64.reinterpret_f64 (local.get $obj)) (i64.const 32)) (i64.const 0x7FFF))))
522
- (local.set $kbits (i64.reinterpret_f64
523
- (f64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
524
- (local.set $koff (i32.wrap_i64 (i64.and (local.get $kbits) (i64.const 0xFFFFFFFF))))
620
+ (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
621
+ (local.set $kbits
622
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3)))))
623
+ (local.set $koff (i32.wrap_i64 (i64.and (local.get $kbits) (i64.const ${LAYOUT.OFFSET_MASK}))))
525
624
  (local.set $nkeys (i32.load (i32.sub (local.get $koff) (i32.const 8))))
526
625
  (local.set $idx (i32.const 0))
527
626
  (block $kdone (loop $kloop
528
627
  (br_if $kdone (i32.ge_s (local.get $idx) (local.get $nkeys)))
529
- (if (call $__str_eq
530
- (f64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
531
- (local.get $key))
532
- (then (return (f64.load (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3)))))))
628
+ (if ${schemaKeyEq(`(i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))`, `(local.get $key)`)}
629
+ (then (return (i64.load (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3)))))))
533
630
  (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
534
631
  (br $kloop)))))))` : ''
535
- const objectSchemaLocals = hasSchemas
632
+ const buildObjectSchemaLocals = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj'))
536
633
  ? '(local $sid i32) (local $kbits i64) (local $koff i32) (local $nkeys i32)'
537
634
  : ''
635
+ // Same lazy-gating story as buildObjectSchemaArm above — observed at
636
+ // template-expansion time so schemas registered later in the compile
637
+ // still pull the arm in.
638
+ const buildObjectSchemaSetLocals = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj'))
639
+ ? '(local $sid i32) (local $kbits i64) (local $koff i32) (local $nkeys i32) (local $idx i32)'
640
+ : ''
641
+ const buildObjectSchemaSetArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
642
+ ;; If a dynamic write targets an existing fixed-shape field, update the
643
+ ;; payload slot as well as the dynamic sidecar below. Otherwise bracket
644
+ ;; writes and later dot reads can diverge.
645
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
646
+ (i32.ne (global.get $__schema_tbl) (i32.const 0)))
647
+ (then
648
+ (local.set $sid (i32.wrap_i64 (i64.and (i64.shr_u
649
+ (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
650
+ (local.set $kbits
651
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3)))))
652
+ (local.set $koff (i32.wrap_i64 (i64.and (local.get $kbits) (i64.const ${LAYOUT.OFFSET_MASK}))))
653
+ (local.set $nkeys (i32.load (i32.sub (local.get $koff) (i32.const 8))))
654
+ (local.set $idx (i32.const 0))
655
+ (block $schemaSetDone (loop $schemaSetLoop
656
+ (br_if $schemaSetDone (i32.ge_s (local.get $idx) (local.get $nkeys)))
657
+ (if (call $__str_eq
658
+ (i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
659
+ (local.get $key))
660
+ (then
661
+ (i64.store (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3))) (local.get $val))
662
+ (br $schemaSetDone)))
663
+ (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
664
+ (br $schemaSetLoop)))))` : ''
538
665
 
539
- ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj f64) (param $key f64) (result f64)
666
+ ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
540
667
  (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
541
668
 
542
- ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj f64) (param $key f64) (param $type i32) (result f64)
543
- (local $props f64) (local $bits i64) (local $off i32)
669
+ ctx.core.stdlib['__dyn_get_t'] = () => `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
670
+ (local $props i64) (local $off i32)
544
671
  (local $poff i32) (local $pcap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
545
- ${objectSchemaLocals}
546
- (local.set $bits (i64.reinterpret_f64 (local.get $obj)))
547
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
672
+ ${buildObjectSchemaLocals()}
673
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
548
674
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
549
675
  (then
550
676
  (block $done
551
677
  (loop $follow
552
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
678
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
553
679
  (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
554
680
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
555
681
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
556
682
  (br $follow)))))
557
683
  (block $dynDone
558
- (br_if $dynDone (f64.eq (global.get $__dyn_props) (f64.const 0)))
559
- (local.set $props (call $__ihash_get_local (global.get $__dyn_props)
560
- (f64.convert_i32_s (local.get $off))))
561
- (br_if $dynDone (call $__is_nullish (local.get $props)))
562
- (local.set $bits (i64.reinterpret_f64 (local.get $props)))
563
- (local.set $poff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
684
+ (block $haveProps
685
+ ;; ARRAY: header propsPtr at $off-16 is valid only when shift hasn't
686
+ ;; rewritten the slot with forwarding bytes. Validate via HASH tag —
687
+ ;; rejects 0 (no props) and forwarding garbage. Misses fall through to
688
+ ;; the global hash, where __arr_shift migrates props on first .shift().
689
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
690
+ (i32.ge_u (local.get $off) (i32.const 16)))
691
+ (then
692
+ (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
693
+ (br_if $haveProps (i32.eq
694
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
695
+ (i32.const ${PTR.HASH})))
696
+ (local.set $props (i64.const 0))))
697
+ ;; OBJECT: heap-allocated (off >= __heap_start) carries propsPtr at
698
+ ;; off-16 from __alloc_hdr. The slot is either 0 (no dyn props yet) or
699
+ ;; a HASH — no forwarding-garbage case like ARRAY, so a bit-zero test
700
+ ;; is enough. Static-segment objects fall through to the global hash.
701
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
702
+ (i32.ge_u (local.get $off) (global.get $__heap_start)))
703
+ (then
704
+ (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
705
+ (br_if $dynDone (i64.eqz (local.get $props)))
706
+ (br $haveProps)))
707
+ ;; Other header types (TYPED/HASH/SET/MAP) carry propsPtr at off-16
708
+ ;; directly, bypassing the global __dyn_props hash.
709
+ (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
710
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
711
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
712
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
713
+ (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
714
+ (then
715
+ (local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
716
+ (br_if $dynDone (i64.eqz (local.get $props)))
717
+ (br $haveProps)))
718
+ ;; Fall back to the global __dyn_props hash (CLOSURE, shifted ARRAY,
719
+ ;; static-segment OBJECT). 1-slot cache covers both hits and misses
720
+ ;; (props=0 sentinel) so header-less types skip __ihash_get_local probes.
721
+ (br_if $dynDone (f64.eq (global.get $__dyn_props) (f64.const 0)))
722
+ (if (i32.eq (local.get $off) (global.get $__dyn_get_cache_off))
723
+ (then
724
+ (local.set $props (i64.reinterpret_f64 (global.get $__dyn_get_cache_props)))
725
+ (br_if $dynDone (i64.eqz (local.get $props))))
726
+ (else
727
+ (local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props))
728
+ (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
729
+ (global.set $__dyn_get_cache_off (local.get $off))
730
+ (if (call $__is_nullish (local.get $props))
731
+ (then
732
+ (global.set $__dyn_get_cache_props (f64.const 0))
733
+ (br $dynDone))
734
+ (else
735
+ (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props))))))))
736
+ (local.set $poff (i32.wrap_i64 (i64.and (local.get $props) (i64.const ${LAYOUT.OFFSET_MASK}))))
564
737
  (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
565
738
  (local.set $h (call $__str_hash (local.get $key)))
566
739
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))))
567
740
  (block $hdone (loop $hprobe
568
741
  (local.set $slot (i32.add (local.get $poff) (i32.mul (local.get $idx) (i32.const ${MAP_ENTRY}))))
569
- (br_if $dynDone (f64.eq (f64.load (local.get $slot)) (f64.const 0)))
570
- (if (call $__str_eq (f64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
571
- (then (return (f64.load (i32.add (local.get $slot) (i32.const 16))))))
742
+ (br_if $dynDone (i64.eqz (i64.load (local.get $slot))))
743
+ (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
744
+ (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
572
745
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $pcap) (i32.const 1))))
573
746
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
574
747
  (br_if $hdone (i32.ge_s (local.get $tries) (local.get $pcap)))
575
- (br $hprobe))))${objectSchemaArm}
576
- (f64.const nan:${UNDEF_NAN}))`
748
+ (br $hprobe))))${buildObjectSchemaArm()}
749
+ (i64.const ${UNDEF_NAN}))`
577
750
 
578
- ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj f64) (param $key f64) (param $fallback f64) (result f64)
579
- (local $val f64)
751
+ ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj i64) (param $key i64) (param $fallback i64) (result i64)
752
+ (local $val i64)
580
753
  (local.set $val (call $__dyn_get (local.get $obj) (local.get $key)))
581
- (if (result f64)
582
- (i64.eq (i64.reinterpret_f64 (local.get $val)) (i64.const ${UNDEF_NAN}))
754
+ (if (result i64)
755
+ (i64.eq (local.get $val) (i64.const ${UNDEF_NAN}))
583
756
  (then (local.get $fallback))
584
757
  (else (local.get $val))))`
585
758
 
586
- ctx.core.stdlib['__dyn_get_expr'] = `(func $__dyn_get_expr (param $obj f64) (param $key f64) (result f64)
759
+ ctx.core.stdlib['__dyn_get_expr'] = `(func $__dyn_get_expr (param $obj i64) (param $key i64) (result i64)
587
760
  (call $__dyn_get_expr_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
588
761
 
589
- ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj f64) (param $key f64) (param $t i32) (result f64)
590
- (local $val f64)
762
+ ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
763
+ (local $val i64)
591
764
  (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
592
- (if (result f64)
593
- (i64.ne (i64.reinterpret_f64 (local.get $val)) (i64.const ${UNDEF_NAN}))
765
+ (if (result i64)
766
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
594
767
  (then (local.get $val))
595
768
  (else
596
- (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
769
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
597
770
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
598
- (else (f64.const nan:${NULL_NAN}))))))`
771
+ (else (i64.const ${NULL_NAN}))))))`
599
772
 
600
773
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
601
774
  // Used at call sites where receiver type is statically unknown.
@@ -605,24 +778,24 @@ export default (ctx) => {
605
778
  // dyn_props (those are for OBJECT/ARRAY attached props), so the original __dyn_get
606
779
  // call was always wasted work on hashes — and JSON.parse / Map-style code is the
607
780
  // dominant HASH consumer.
608
- return `(func $__dyn_get_any (param $obj f64) (param $key f64) (result f64)
781
+ return `(func $__dyn_get_any (param $obj i64) (param $key i64) (result i64)
609
782
  (call $__dyn_get_any_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
610
783
  }
611
784
 
612
785
  ctx.core.stdlib['__dyn_get_any_t'] = () => {
613
786
  const extArm = ctx.features.external
614
- ? `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
787
+ ? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
615
788
  (then (call $__ext_prop (local.get $obj) (local.get $key)))
616
- (else (f64.const nan:${NULL_NAN})))`
617
- : `(f64.const nan:${NULL_NAN})`
618
- return `(func $__dyn_get_any_t (param $obj f64) (param $key f64) (param $t i32) (result f64)
619
- (local $val f64)
620
- (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
789
+ (else (i64.const ${NULL_NAN})))`
790
+ : `(i64.const ${NULL_NAN})`
791
+ return `(func $__dyn_get_any_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
792
+ (local $val i64)
793
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
621
794
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
622
795
  (else
623
796
  (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
624
- (if (result f64)
625
- (i64.ne (i64.reinterpret_f64 (local.get $val)) (i64.const ${UNDEF_NAN}))
797
+ (if (result i64)
798
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
626
799
  (then (local.get $val))
627
800
  (else ${extArm})))))`
628
801
  }
@@ -631,45 +804,101 @@ export default (ctx) => {
631
804
  // Defer the root insert to the end and gate it on props-ptr change: most calls hit
632
805
  // the no-grow case where the ptr is unchanged and the root slot already points to it.
633
806
  // __ptr_offset inlined (forwarding-aware) — only ARRAY ever has forwarding.
634
- ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj f64) (param $key f64) (param $val f64) (result f64)
635
- (local $root f64) (local $props f64) (local $oldProps f64) (local $objKey f64)
636
- (local $bits i64) (local $off i32)
637
- (local.set $root (global.get $__dyn_props))
638
- (if (f64.eq (local.get $root) (f64.const 0))
639
- (then (local.set $root (call $__hash_new))))
640
- (local.set $bits (i64.reinterpret_f64 (local.get $obj)))
641
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
642
- (if (i32.eq
643
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
644
- (i32.const ${PTR.ARRAY}))
807
+ ctx.core.stdlib['__dyn_set'] = () => `(func $__dyn_set (param $obj i64) (param $key i64) (param $val i64) (result i64)
808
+ (local $root i64) (local $props i64) (local $oldProps i64) (local $objKey i64)
809
+ (local $off i32) (local $type i32) ${buildObjectSchemaSetLocals()}
810
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
811
+ (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
812
+ (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
645
813
  (then
646
814
  (block $done
647
815
  (loop $follow
648
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
816
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
649
817
  (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
650
818
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
651
819
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
652
820
  (br $follow)))))
653
- (local.set $objKey (f64.convert_i32_s (local.get $off)))
821
+ ${buildObjectSchemaSetArm()}
822
+ ;; Header types carry propsPtr at off-16. Read/grow/write directly there;
823
+ ;; skip the global __dyn_props hash entirely. ARRAY also uses this slot, but
824
+ ;; only when shift hasn't overwritten it with forwarding bytes (HASH-tagged
825
+ ;; check rejects 0 + forwarding garbage). Shifted ARRAYs fall back to the
826
+ ;; global __dyn_props where __arr_shift has migrated their props.
827
+ (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
828
+ (then
829
+ (if (i32.ge_u (local.get $off) (i32.const 16))
830
+ (then
831
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
832
+ (if (i32.or
833
+ (i64.eqz (local.get $oldProps))
834
+ (i32.eq
835
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
836
+ (i32.const ${PTR.HASH})))
837
+ (then
838
+ (local.set $props
839
+ (if (result i64) (i64.eqz (local.get $oldProps))
840
+ (then (i64.reinterpret_f64 (call $__hash_new_small)))
841
+ (else (local.get $oldProps))))
842
+ (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
843
+ (if (i64.ne (local.get $props) (local.get $oldProps))
844
+ (then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
845
+ (return (local.get $val))))))))
846
+ ;; OBJECT: heap-allocated (off >= __heap_start) writes propsPtr directly at
847
+ ;; off-16. The slot is 0 (init) or HASH — no forwarding-garbage like ARRAY.
848
+ ;; Static-segment OBJECTs fall through to the global __dyn_props.
849
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
850
+ (i32.ge_u (local.get $off) (global.get $__heap_start)))
851
+ (then
852
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
853
+ (local.set $props
854
+ (if (result i64) (i64.eqz (local.get $oldProps))
855
+ (then (i64.reinterpret_f64 (call $__hash_new_small)))
856
+ (else (local.get $oldProps))))
857
+ (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
858
+ (if (i64.ne (local.get $props) (local.get $oldProps))
859
+ (then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
860
+ (return (local.get $val))))
861
+ (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
862
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
863
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
864
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
865
+ (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
866
+ (then
867
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
868
+ (local.set $props
869
+ (if (result i64) (i64.eqz (local.get $oldProps))
870
+ (then (i64.reinterpret_f64 (call $__hash_new_small)))
871
+ (else (local.get $oldProps))))
872
+ (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
873
+ (if (i64.ne (local.get $props) (local.get $oldProps))
874
+ (then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
875
+ (return (local.get $val))))
876
+ ;; Fallback: non-header types use the global __dyn_props.
877
+ (local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
878
+ (if (i64.eqz (local.get $root))
879
+ (then (local.set $root (i64.reinterpret_f64 (call $__hash_new)))))
880
+ (local.set $objKey (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off))))
654
881
  (local.set $oldProps (call $__ihash_get_local (local.get $root) (local.get $objKey)))
655
882
  (local.set $props
656
- (if (result f64) (call $__is_nullish (local.get $oldProps))
657
- (then (call $__hash_new))
883
+ (if (result i64) (call $__is_nullish (local.get $oldProps))
884
+ (then (i64.reinterpret_f64 (call $__hash_new_small)))
658
885
  (else (local.get $oldProps))))
659
886
  (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
660
- (if (i64.ne (i64.reinterpret_f64 (local.get $props)) (i64.reinterpret_f64 (local.get $oldProps)))
887
+ (if (i64.ne (local.get $props) (local.get $oldProps))
661
888
  (then
662
889
  (local.set $root (call $__ihash_set_local (local.get $root) (local.get $objKey) (local.get $props)))
663
- (global.set $__dyn_props (local.get $root))))
890
+ (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
891
+ (if (i32.eq (local.get $off) (global.get $__dyn_get_cache_off))
892
+ (then (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props)))))))
664
893
  (local.get $val))`
665
894
 
666
895
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
667
- (local $props f64) (local $root f64)
896
+ (local $props i64) (local $root i64)
668
897
  (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
669
- (local.set $props (call $__ihash_get_local (global.get $__dyn_props) (f64.convert_i32_s (local.get $oldOff))))
898
+ (local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $oldOff)))))
670
899
  (if (call $__is_nullish (local.get $props)) (then (return)))
671
- (local.set $root (call $__ihash_set_local (global.get $__dyn_props) (f64.convert_i32_s (local.get $newOff)) (local.get $props)))
672
- (global.set $__dyn_props (local.get $root)))`
900
+ (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)))
901
+ (global.set $__dyn_props (f64.reinterpret_i64 (local.get $root))))`
673
902
 
674
903
  // Generated HASH probe functions
675
904
  ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
@@ -700,10 +929,8 @@ export default (ctx) => {
700
929
  const objVal = ['local.get', `$${objTmp}`]
701
930
  const idxVal = ['local.get', `$${idxTmp}`]
702
931
  const typeVal = ['local.get', `$${typeTmp}`]
703
- const isStringKey = ['call', '$__is_str_key', keyVal]
704
- const isStringLike = ['i32.or',
705
- ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
706
- ['i32.eq', typeVal, ['i32.const', PTR.SSO]]]
932
+ const isStringKey = ['call', '$__is_str_key', ['i64.reinterpret_f64', keyVal]]
933
+ const isStringLike = ['i32.eq', typeVal, ['i32.const', PTR.STRING]]
707
934
  const isArrayLike = ['i32.or',
708
935
  ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
709
936
  ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]]
@@ -714,9 +941,7 @@ export default (ctx) => {
714
941
  ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
715
942
  ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]],
716
943
  ['i32.or',
717
- ['i32.or',
718
- ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
719
- ['i32.eq', typeVal, ['i32.const', PTR.SSO]]],
944
+ ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
720
945
  ['i32.or',
721
946
  ['i32.or',
722
947
  ['i32.eq', typeVal, ['i32.const', PTR.SET]],
@@ -730,7 +955,7 @@ export default (ctx) => {
730
955
  ['local.set', `$${objTmp}`, asF64(emit(obj))],
731
956
  ['local.set', `$${keyTmp}`, asF64(emit(key))],
732
957
  ['local.set', `$${outTmp}`, ['i32.const', 0]],
733
- ['local.set', `$${typeTmp}`, ['call', '$__ptr_type', objVal]],
958
+ ['local.set', `$${typeTmp}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', objVal]]],
734
959
  ['local.set', `$${idxTmp}`, ['i32.trunc_sat_f64_s', keyVal]],
735
960
 
736
961
  ['if', ['i32.and',
@@ -740,52 +965,72 @@ export default (ctx) => {
740
965
  ['i32.ge_s', idxVal, ['i32.const', 0]]]],
741
966
  ['then',
742
967
  ['if', isStringLike,
743
- ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__str_byteLen', objVal]]]]],
968
+ ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__str_byteLen', ['i64.reinterpret_f64', objVal]]]]]],
744
969
  ['if', isArrayLike,
745
- ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__len', objVal]]]]]]],
970
+ ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__len', ['i64.reinterpret_f64', objVal]]]]]]]],
746
971
 
747
972
  ['if', isStringKey,
748
973
  ['then',
749
974
  ['if', hasDynProps,
750
975
  ['then', ['local.set', `$${outTmp}`,
751
- ['i32.eqz', ['call', '$__is_nullish', ['call', '$__dyn_get', objVal, keyVal]]]]]]]],
976
+ ['i32.eqz', ['call', '$__is_nullish', ['call', '$__dyn_get', ['i64.reinterpret_f64', objVal], ['i64.reinterpret_f64', keyVal]]]]]]]]],
752
977
 
753
978
  ['if', ['i32.eq', typeVal, ['i32.const', PTR.HASH]],
754
979
  ['then', ['local.set', `$${outTmp}`,
755
980
  ['if', ['result', 'i32'], isStringKey,
756
- ['then', ['call', '$__hash_has', objVal, keyVal]],
757
- ['else', ['call', '$__hash_has', objVal, ['call', '$__to_str', keyVal]]]]]]],
981
+ ['then', ['call', '$__hash_has', ['i64.reinterpret_f64', objVal], ['i64.reinterpret_f64', keyVal]]],
982
+ ['else', ['call', '$__hash_has', ['i64.reinterpret_f64', objVal], ['call', '$__to_str', ['i64.reinterpret_f64', keyVal]]]]]]]],
758
983
 
759
984
  ...(ctx.features.external ? [['if', ['i32.eq', typeVal, ['i32.const', PTR.EXTERNAL]],
760
- ['then', ['local.set', `$${outTmp}`, ['call', '$__ext_has', objVal, keyVal]]]]] : []),
985
+ ['then', ['local.set', `$${outTmp}`, ['call', '$__ext_has',
986
+ ['i64.reinterpret_f64', objVal], ['i64.reinterpret_f64', keyVal]]]]]] : []),
761
987
 
762
988
  ['local.get', `$${outTmp}`]], 'i32')
763
989
  }
764
990
 
765
991
  // === for...in on dynamic objects (HASH iteration) ===
766
992
 
767
- // for-in: iterate HASH entries, binding key string to loop variable
993
+ // for-in: iterate HASH entries, binding key string to loop variable.
994
+ // Also handles OBJECT/ARRAY/etc whose dynamic props are stored at off-16
995
+ // as a HASH (see __dyn_set). Non-HASH receivers redirect to that props HASH.
768
996
  ctx.core.emit['for-in'] = (varName, src, body) => {
769
997
  const off = tempI32('ho'), cap = tempI32('hc')
770
998
  const i = tempI32('hi'), slot = tempI32('hs')
999
+ const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
771
1000
  if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
772
1001
  const id = ctx.func.uniq++
773
1002
  const va = asF64(emit(src))
774
1003
  const bodyFlat = emitFlat(body)
1004
+ inc('__ptr_type')
775
1005
  return [
776
- ['local.set', `$${off}`, ['call', '$__ptr_offset', va]],
777
- ['local.set', `$${cap}`, ['call', '$__cap', va]],
778
- ['local.set', `$${i}`, ['i32.const', 0]],
779
- ['block', `$brk${id}`, ['loop', `$loop${id}`,
780
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
781
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
782
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
783
- ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
784
- ['then',
785
- ['local.set', `$${varName}`, ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
786
- ...bodyFlat]],
787
- ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
788
- ['br', `$loop${id}`]]]
1006
+ // Save source ptr as i64
1007
+ ['local.set', `$${ptrI64}`, ['i64.reinterpret_f64', va]],
1008
+ ['local.set', `$${srcType}`, ['call', '$__ptr_type', ['local.get', `$${ptrI64}`]]],
1009
+ // If not HASH, follow off-16 to props hash (or zero if no props yet).
1010
+ ['if', ['i32.ne', ['local.get', `$${srcType}`], ['i32.const', PTR.HASH]],
1011
+ ['then',
1012
+ ['local.set', `$${srcOff}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
1013
+ ['if', ['i32.ge_u', ['local.get', `$${srcOff}`], ['i32.const', 16]],
1014
+ ['then',
1015
+ ['local.set', `$${ptrI64}`, ['i64.load', ['i32.sub', ['local.get', `$${srcOff}`], ['i32.const', 16]]]]],
1016
+ ['else',
1017
+ ['local.set', `$${ptrI64}`, ['i64.const', 0]]]]]],
1018
+ // Empty / null props: skip iteration entirely.
1019
+ ['if', ['i64.ne', ['local.get', `$${ptrI64}`], ['i64.const', 0]],
1020
+ ['then',
1021
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
1022
+ ['local.set', `$${cap}`, ['call', '$__cap', ['local.get', `$${ptrI64}`]]],
1023
+ ['local.set', `$${i}`, ['i32.const', 0]],
1024
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
1025
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
1026
+ ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
1027
+ ['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
1028
+ ['if', ['i64.ne', ['i64.load', ['local.get', `$${slot}`]], ['i64.const', 0]],
1029
+ ['then',
1030
+ ['local.set', `$${varName}`, ['f64.reinterpret_i64', ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]],
1031
+ ...bodyFlat]],
1032
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1033
+ ['br', `$loop${id}`]]]]]
789
1034
  ]
790
1035
  }
791
1036
  }