jz 0.1.1 → 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,10 +8,10 @@
8
8
  * @module collection
9
9
  */
10
10
 
11
- import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr } from '../src/ir.js'
11
+ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
12
12
  import { emit, emitFlat } from '../src/emit.js'
13
13
  import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
14
- import { inc, PTR } from '../src/ctx.js'
14
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
15
15
 
16
16
  const SET_ENTRY = 16 // hash + key
17
17
  const MAP_ENTRY = 24 // hash + key + value
@@ -28,8 +28,10 @@ const HASH_F64 = new Float64Array(HASH_BUF)
28
28
  const HASH_U32 = new Uint32Array(HASH_BUF)
29
29
 
30
30
  export function numHashLiteral(n) {
31
+ if (Object.is(n, 0) || Object.is(n, -0)) return 2
31
32
  HASH_F64[0] = n
32
- 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
33
35
  }
34
36
 
35
37
  function numConstLiteral(expr) {
@@ -39,40 +41,39 @@ function numConstLiteral(expr) {
39
41
  }
40
42
 
41
43
  // Equality expressions for probe templates
42
- const f64Eq = '(f64.eq (f64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
43
- 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))'
44
46
 
45
47
  /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
46
48
  * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
47
49
  * Gated off → type mismatch just returns coll unchanged. */
48
50
  function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
49
- const valParam = hasVal ? '(param $val f64) ' : ''
50
- 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))` : ''
51
53
  const onMatch = hasVal
52
- ? `(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))`
53
55
  : `(then (br $done))`
54
56
 
55
57
  const extBranch = hasVal
56
58
  ? '(then (call $__ext_set (local.get $coll) (local.get $key) (local.get $val)) drop)'
57
59
  : '(then (nop))'
58
- 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})))`
59
61
  const typeGuard = hasExt
60
62
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL})) ${extBranch}) (return (local.get $coll))))`
61
63
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
62
- return `(func $${name} (param $coll f64) (param $key f64) ${valParam}(result f64)
63
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
64
- (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)
65
66
  ${typeGuard}
66
- (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}))))
67
68
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
68
69
  (local.set $h (call ${hashFn} (local.get $key)))
69
70
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
70
71
  (block $done (loop $probe
71
72
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
72
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
73
+ (if (i64.eqz (i64.load (local.get $slot)))
73
74
  (then
74
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
75
- (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}
76
77
  (i32.store (i32.sub (local.get $off) (i32.const 8))
77
78
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
78
79
  (br $done)))
@@ -87,34 +88,35 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
87
88
  * wantValue=false: return i32 0/1 existence flag.
88
89
  * hasExt: emit EXTERNAL fallthrough (delegate to __ext_prop/__ext_has). */
89
90
  function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, hasExt) {
90
- const rt = wantValue ? 'f64' : 'i32'
91
+ const rt = wantValue ? 'i64' : 'i32'
91
92
  const onEmpty = wantValue
92
- ? `(return (f64.const nan:${NULL_NAN}))`
93
+ ? `(return (i64.const ${NULL_NAN}))`
93
94
  : '(return (i32.const 0))'
94
95
  const onFound = wantValue
95
- ? '(return (f64.load (i32.add (local.get $slot) (i32.const 16))))'
96
+ ? '(return (i64.load (i32.add (local.get $slot) (i32.const 16))))'
96
97
  : '(return (i32.const 1))'
97
98
  const notFound = wantValue
98
- ? `(f64.const nan:${NULL_NAN})`
99
+ ? `(i64.const ${NULL_NAN})`
99
100
  : '(i32.const 0)'
100
- 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})))`
101
102
  const typeGuard = hasExt
102
103
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
103
- (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))'}))
104
107
  (else ${onEmpty}))))`
105
108
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
106
109
 
107
- return `(func $${name} (param $coll f64) (param $key f64) (result ${rt})
108
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
109
- (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)
110
112
  ${typeGuard}
111
- (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}))))
112
114
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
113
115
  (local.set $h (call ${hashFn} (local.get $key)))
114
116
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
115
117
  (block $done (loop $probe
116
118
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
117
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0)) (then ${onEmpty}))
119
+ (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
118
120
  (if ${eqExpr} (then ${onFound}))
119
121
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
120
122
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
@@ -125,7 +127,7 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
125
127
 
126
128
  /** Generate delete probe function. Zero out entry on match. */
127
129
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
128
- return `(func $${name} (param $coll f64) (param $key f64) (result i32)
130
+ return `(func $${name} (param $coll i64) (param $key i64) (result i32)
129
131
  (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
130
132
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
131
133
  (local.set $off (call $__ptr_offset (local.get $coll)))
@@ -134,11 +136,11 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
134
136
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
135
137
  (block $done (loop $probe
136
138
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
137
- (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))))
138
140
  (if ${eqExpr}
139
141
  (then
140
- (f64.store (local.get $slot) (f64.const 0))
141
- (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))
142
144
  (return (i32.const 1))))
143
145
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
144
146
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
@@ -165,7 +167,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
165
167
  (then
166
168
  ${nonHashFallback}
167
169
  (return (local.get $obj))))`
168
- 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)
169
171
  (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
170
172
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
171
173
  (local $oldslot i32) (local $newidx i32) (local $newslot i32)
@@ -182,41 +184,41 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
182
184
  (block $rd (loop $rl
183
185
  (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
184
186
  (local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
185
- (if (f64.ne (f64.load (local.get $oldslot)) (f64.const 0))
187
+ (if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
186
188
  (then
187
- (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)))))
188
190
  (local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
189
191
  (block $ins (loop $probe2
190
192
  (local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
191
- (br_if $ins (f64.eq (f64.load (local.get $newslot)) (f64.const 0)))
193
+ (br_if $ins (i64.eqz (i64.load (local.get $newslot))))
192
194
  (local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
193
195
  (br $probe2)))
194
- (f64.store (local.get $newslot) (f64.load (local.get $oldslot)))
195
- (f64.store (i32.add (local.get $newslot) (i32.const 8)) (f64.load (i32.add (local.get $oldslot) (i32.const 8))))
196
- (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))))
197
199
  (i32.store (i32.sub (local.get $newptr) (i32.const 8))
198
200
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
199
201
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
200
202
  (br $rl)))
201
203
  (local.set $off (local.get $newptr))
202
204
  (local.set $cap (local.get $newcap))
203
- (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))))))
204
206
  ;; Insert/update
205
207
  (local.set $h (call ${hashFn} (local.get $key)))
206
208
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
207
209
  (block $done (loop $probe
208
210
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
209
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
211
+ (if (i64.eqz (i64.load (local.get $slot)))
210
212
  (then
211
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
212
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
213
- (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))
214
216
  (i32.store (i32.sub (local.get $off) (i32.const 8))
215
217
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
216
218
  (br $done)))
217
219
  (if ${eqExpr}
218
220
  (then
219
- (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))
220
222
  (br $done)))
221
223
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
222
224
  (br $probe)))
@@ -224,84 +226,81 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
224
226
  }
225
227
 
226
228
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
227
- return `(func $${name} (param $coll f64) (param $key f64) (result f64)
228
- (local $bits i64) (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
229
- (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)
230
231
  (if (i32.ne
231
- (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})))
232
233
  (i32.const ${expectedType}))
233
- (then (return (f64.const nan:${missing}))))
234
- (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}))))
235
236
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
236
237
  (local.set $h (call ${hashFn} (local.get $key)))
237
238
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
238
239
  (block $done (loop $probe
239
240
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
240
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
241
- (then (return (f64.const nan:${missing}))))
241
+ (if (i64.eqz (i64.load (local.get $slot)))
242
+ (then (return (i64.const ${missing}))))
242
243
  (if ${eqExpr}
243
- (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))))))
244
245
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
245
246
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
246
247
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
247
248
  (br $probe)))
248
- (f64.const nan:${missing}))`
249
+ (i64.const ${missing}))`
249
250
  }
250
251
 
251
252
  function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing = UNDEF_NAN, hasExt = false) {
252
- 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})))`
253
254
  const typeGuard = hasExt
254
255
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
255
256
  (then
256
257
  (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
257
258
  (then (return (call $__ext_prop (local.get $coll) (local.get $key))))
258
- (else (return (f64.const nan:${missing}))))))`
259
+ (else (return (i64.const ${missing}))))))`
259
260
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
260
- (then (return (f64.const nan:${missing}))))`
261
- return `(func $${name} (param $coll f64) (param $key f64) (param $h i32) (result f64)
262
- (local $bits i64) (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32) (local $tries i32)
263
- (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)
264
264
  ${typeGuard}
265
- (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}))))
266
266
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
267
267
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
268
268
  (block $done (loop $probe
269
269
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
270
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
271
- (then (return (f64.const nan:${missing}))))
270
+ (if (i64.eqz (i64.load (local.get $slot)))
271
+ (then (return (i64.const ${missing}))))
272
272
  (if ${eqExpr}
273
- (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))))))
274
274
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
275
275
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
276
276
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
277
277
  (br $probe)))
278
- (f64.const nan:${missing}))`
278
+ (i64.const ${missing}))`
279
279
  }
280
280
 
281
281
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
282
- return `(func $${name} (param $obj f64) (param $key f64) (param $h i32) (param $val f64) (result f64)
283
- (local $bits i64) (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32)
284
- (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)
285
284
  (if (i32.ne
286
- (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})))
287
286
  (i32.const ${expectedType}))
288
287
  (then (return (local.get $obj))))
289
- (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}))))
290
289
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
291
290
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
292
291
  (block $done (loop $probe
293
292
  (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
294
- (if (f64.eq (f64.load (local.get $slot)) (f64.const 0))
293
+ (if (i64.eqz (i64.load (local.get $slot)))
295
294
  (then
296
- (f64.store (local.get $slot) (f64.reinterpret_i64 (i64.extend_i32_u (local.get $h))))
297
- (f64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
298
- (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))
299
298
  (i32.store (i32.sub (local.get $off) (i32.const 8))
300
299
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
301
300
  (br $done)))
302
301
  (if ${eqExpr}
303
302
  (then
304
- (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))
305
304
  (br $done)))
306
305
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
307
306
  (br $probe)))
@@ -314,12 +313,16 @@ export default (ctx) => {
314
313
  // Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
315
314
  const ifExt = (name) => () => ctx.features.external ? [name] : []
316
315
  Object.assign(ctx.core.stdlibDeps, {
317
- __set_has: ifExt('__ext_has'),
318
- __set_delete: [],
319
- __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'],
320
322
  __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set'] : ['__map_set'],
321
- __map_get_h: () => ctx.features.external ? ['__ext_prop'] : [],
322
- __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'],
323
326
  __hash_set: () => ctx.features.external
324
327
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
325
328
  : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
@@ -330,12 +333,13 @@ export default (ctx) => {
330
333
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_has']
331
334
  : ['__str_hash', '__str_eq', '__ptr_type'],
332
335
  __hash_new: ['__alloc_hdr'],
336
+ __hash_new_small: ['__alloc_hdr', '__mkptr'],
333
337
  __hash_get_local: ['__str_hash', '__str_eq'],
334
338
  __hash_get_local_h: ['__str_eq'],
335
339
  __hash_set_local_h: ['__str_eq'],
336
340
  __hash_set_local: ['__str_hash', '__str_eq'],
337
- __ihash_get_local: ['__hash'],
338
- __ihash_set_local: ['__hash', '__alloc_hdr', '__mkptr'],
341
+ __ihash_get_local: ['__map_hash'],
342
+ __ihash_set_local: ['__map_hash', '__alloc_hdr', '__mkptr'],
339
343
  __dyn_get_t: ['__ihash_get_local', '__str_hash', '__str_eq', '__is_nullish'],
340
344
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
341
345
  __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
@@ -345,7 +349,7 @@ export default (ctx) => {
345
349
  ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
346
350
  : ['__dyn_get_t', '__hash_get_local'],
347
351
  __dyn_get_or: ['__dyn_get'],
348
- __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'],
349
353
  __dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
350
354
  })
351
355
 
@@ -353,6 +357,16 @@ export default (ctx) => {
353
357
 
354
358
  if (!ctx.scope.globals.has('__dyn_props'))
355
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))')
356
370
  // Schema name table for __dyn_get's OBJECT-schema fallback (polymorphic-receiver
357
371
  // `.prop` access). Same declaration as json.js — defined here too so collection
358
372
  // doesn't transitively require json. compile.js's schemaInit populates it when
@@ -360,17 +374,61 @@ export default (ctx) => {
360
374
  if (!ctx.scope.globals.has('__schema_tbl'))
361
375
  ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
362
376
 
363
- ctx.core.stdlib['__ext_prop'] = '(import "env" "__ext_prop" (func $__ext_prop (param f64 f64) (result f64)))'
364
- ctx.core.stdlib['__ext_has'] = '(import "env" "__ext_has" (func $__ext_has (param f64 f64) (result i32)))'
365
- ctx.core.stdlib['__ext_set'] = '(import "env" "__ext_set" (func $__ext_set (param f64 f64 f64) (result i32)))'
366
- 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)))'
367
385
  // Hash function: simple f64 → i32 hash
368
- ctx.core.stdlib['__hash'] = `(func $__hash (param $v f64) (result i32)
386
+ ctx.core.stdlib['__hash'] = `(func $__hash (param $v i64) (result i32)
369
387
  (i32.wrap_i64 (i64.xor
370
- (i64.reinterpret_f64 (local.get $v))
371
- (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)))))`
372
390
  inc('__hash')
373
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
+
374
432
  // __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
375
433
  ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
376
434
  (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
@@ -386,27 +444,27 @@ export default (ctx) => {
386
444
 
387
445
  ctx.core.emit['.add'] = (setExpr, val) => {
388
446
  inc('__set_add')
389
- 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')
390
448
  }
391
449
 
392
450
  ctx.core.emit['.has'] = (setExpr, val) => {
393
451
  inc('__set_has')
394
- 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')
395
453
  }
396
454
 
397
455
  ctx.core.emit['.delete'] = (setExpr, val) => {
398
456
  inc('__set_delete')
399
- 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')
400
458
  }
401
459
 
402
460
  ctx.core.emit['.size'] = (expr) => {
403
- 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')
404
462
  }
405
463
 
406
464
  // Generated Set probe functions
407
- ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__hash', f64Eq, PTR.SET, false, ctx.features.external)
408
- ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__hash', f64Eq, PTR.SET, false, ctx.features.external)
409
- 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)
410
468
 
411
469
  // === Map ===
412
470
 
@@ -418,7 +476,8 @@ export default (ctx) => {
418
476
 
419
477
  ctx.core.emit['.set'] = (mapExpr, key, val) => {
420
478
  inc('__map_set')
421
- 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')
422
481
  }
423
482
  ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
424
483
 
@@ -426,19 +485,31 @@ export default (ctx) => {
426
485
  const constKey = numConstLiteral(key)
427
486
  if (constKey != null) {
428
487
  inc('__map_get_h')
429
- 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')
430
489
  }
431
490
  inc('__map_get')
432
- 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')
433
492
  }
434
493
 
435
494
  ctx.core.emit['.get'] = emitMapGet
436
495
  ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
437
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
+
438
507
  // Generated Map probe functions
439
- ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__hash', f64Eq, PTR.MAP, true, ctx.features.external)
440
- ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__hash', f64Eq, PTR.MAP, true, ctx.features.external)
441
- 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)
442
513
 
443
514
  // === HASH — dynamic string-keyed object (type=7) ===
444
515
 
@@ -446,15 +517,15 @@ export default (ctx) => {
446
517
  // FNV-1a. ~95M calls in watr self-host. Inline char-fetch: hoist type/offset out of the
447
518
  // byte loop so SSO branch uses dword shifts and STRING branch uses raw load8_u — neither
448
519
  // calls anything per byte (vs original 1×__char_at → __ptr_type + __ptr_offset per byte).
449
- ctx.core.stdlib['__str_hash'] = `(func $__str_hash (param $s f64) (result i32)
450
- (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)
451
522
  (local.set $h (i32.const 0x811c9dc5))
452
- (local.set $bits (i64.reinterpret_f64 (local.get $s)))
453
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
454
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
455
- (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)))
456
527
  (then
457
- (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)))
458
529
  (block $ds (loop $ls
459
530
  (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
460
531
  (local.set $h (i32.mul
@@ -493,14 +564,21 @@ export default (ctx) => {
493
564
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
494
565
  (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
495
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
+
496
574
  ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
497
575
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
498
576
  ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
499
577
  ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true)
500
- // Outer __dyn_props hash: keyed by object offset (i32 as f64), value is per-object props hash.
501
- // Uses bit-hash + f64.eq — no string allocation for the unique integer key.
502
- ctx.core.stdlib['__ihash_get_local'] = genLookupStrict('__ihash_get_local', MAP_ENTRY, '$__hash', f64Eq, PTR.HASH)
503
- 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)
504
582
 
505
583
  // Inline __ptr_offset (forwarding-aware) and __hash_get_local body — dyn_get is the
506
584
  // single hottest stdlib symbol in watr self-host (~95M calls). props returned by
@@ -513,91 +591,184 @@ export default (ctx) => {
513
591
  // time but lives at runtime in the NaN-box aux bits. Gated on schema name table
514
592
  // presence (lifted in compile.js whenever __dyn_get is included). Static-shape
515
593
  // monomorphic OBJECTs hit the compile-time slot read path and never reach here.
516
- const hasSchemas = ctx.schema.list.length > 0
517
- 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')) ? `
518
615
  (if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
519
616
  (then
520
617
  (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
521
618
  (then
522
619
  (local.set $sid (i32.wrap_i64 (i64.and (i64.shr_u
523
- (i64.reinterpret_f64 (local.get $obj)) (i64.const 32)) (i64.const 0x7FFF))))
524
- (local.set $kbits (i64.reinterpret_f64
525
- (f64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))
526
- (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}))))
527
624
  (local.set $nkeys (i32.load (i32.sub (local.get $koff) (i32.const 8))))
528
625
  (local.set $idx (i32.const 0))
529
626
  (block $kdone (loop $kloop
530
627
  (br_if $kdone (i32.ge_s (local.get $idx) (local.get $nkeys)))
531
- (if (call $__str_eq
532
- (f64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
533
- (local.get $key))
534
- (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)))))))
535
630
  (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
536
631
  (br $kloop)))))))` : ''
537
- const objectSchemaLocals = hasSchemas
632
+ const buildObjectSchemaLocals = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj'))
538
633
  ? '(local $sid i32) (local $kbits i64) (local $koff i32) (local $nkeys i32)'
539
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)))))` : ''
540
665
 
541
- 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)
542
667
  (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
543
668
 
544
- ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj f64) (param $key f64) (param $type i32) (result f64)
545
- (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)
546
671
  (local $poff i32) (local $pcap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
547
- ${objectSchemaLocals}
548
- (local.set $bits (i64.reinterpret_f64 (local.get $obj)))
549
- (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}))))
550
674
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
551
675
  (then
552
676
  (block $done
553
677
  (loop $follow
554
- (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)))
555
679
  (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
556
680
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
557
681
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
558
682
  (br $follow)))))
559
683
  (block $dynDone
560
- (br_if $dynDone (f64.eq (global.get $__dyn_props) (f64.const 0)))
561
- (local.set $props (call $__ihash_get_local (global.get $__dyn_props)
562
- (f64.convert_i32_s (local.get $off))))
563
- (br_if $dynDone (call $__is_nullish (local.get $props)))
564
- (local.set $bits (i64.reinterpret_f64 (local.get $props)))
565
- (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}))))
566
737
  (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
567
738
  (local.set $h (call $__str_hash (local.get $key)))
568
739
  (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))))
569
740
  (block $hdone (loop $hprobe
570
741
  (local.set $slot (i32.add (local.get $poff) (i32.mul (local.get $idx) (i32.const ${MAP_ENTRY}))))
571
- (br_if $dynDone (f64.eq (f64.load (local.get $slot)) (f64.const 0)))
572
- (if (call $__str_eq (f64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
573
- (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))))))
574
745
  (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $pcap) (i32.const 1))))
575
746
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
576
747
  (br_if $hdone (i32.ge_s (local.get $tries) (local.get $pcap)))
577
- (br $hprobe))))${objectSchemaArm}
578
- (f64.const nan:${UNDEF_NAN}))`
748
+ (br $hprobe))))${buildObjectSchemaArm()}
749
+ (i64.const ${UNDEF_NAN}))`
579
750
 
580
- ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj f64) (param $key f64) (param $fallback f64) (result f64)
581
- (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)
582
753
  (local.set $val (call $__dyn_get (local.get $obj) (local.get $key)))
583
- (if (result f64)
584
- (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}))
585
756
  (then (local.get $fallback))
586
757
  (else (local.get $val))))`
587
758
 
588
- 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)
589
760
  (call $__dyn_get_expr_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
590
761
 
591
- ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj f64) (param $key f64) (param $t i32) (result f64)
592
- (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)
593
764
  (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
594
- (if (result f64)
595
- (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}))
596
767
  (then (local.get $val))
597
768
  (else
598
- (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}))
599
770
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
600
- (else (f64.const nan:${NULL_NAN}))))))`
771
+ (else (i64.const ${NULL_NAN}))))))`
601
772
 
602
773
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
603
774
  // Used at call sites where receiver type is statically unknown.
@@ -607,24 +778,24 @@ export default (ctx) => {
607
778
  // dyn_props (those are for OBJECT/ARRAY attached props), so the original __dyn_get
608
779
  // call was always wasted work on hashes — and JSON.parse / Map-style code is the
609
780
  // dominant HASH consumer.
610
- 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)
611
782
  (call $__dyn_get_any_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
612
783
  }
613
784
 
614
785
  ctx.core.stdlib['__dyn_get_any_t'] = () => {
615
786
  const extArm = ctx.features.external
616
- ? `(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}))
617
788
  (then (call $__ext_prop (local.get $obj) (local.get $key)))
618
- (else (f64.const nan:${NULL_NAN})))`
619
- : `(f64.const nan:${NULL_NAN})`
620
- return `(func $__dyn_get_any_t (param $obj f64) (param $key f64) (param $t i32) (result f64)
621
- (local $val f64)
622
- (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}))
623
794
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
624
795
  (else
625
796
  (local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
626
- (if (result f64)
627
- (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}))
628
799
  (then (local.get $val))
629
800
  (else ${extArm})))))`
630
801
  }
@@ -633,45 +804,101 @@ export default (ctx) => {
633
804
  // Defer the root insert to the end and gate it on props-ptr change: most calls hit
634
805
  // the no-grow case where the ptr is unchanged and the root slot already points to it.
635
806
  // __ptr_offset inlined (forwarding-aware) — only ARRAY ever has forwarding.
636
- ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj f64) (param $key f64) (param $val f64) (result f64)
637
- (local $root f64) (local $props f64) (local $oldProps f64) (local $objKey f64)
638
- (local $bits i64) (local $off i32)
639
- (local.set $root (global.get $__dyn_props))
640
- (if (f64.eq (local.get $root) (f64.const 0))
641
- (then (local.set $root (call $__hash_new))))
642
- (local.set $bits (i64.reinterpret_f64 (local.get $obj)))
643
- (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
644
- (if (i32.eq
645
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
646
- (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}))
647
813
  (then
648
814
  (block $done
649
815
  (loop $follow
650
- (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)))
651
817
  (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
652
818
  (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
653
819
  (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
654
820
  (br $follow)))))
655
- (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))))
656
881
  (local.set $oldProps (call $__ihash_get_local (local.get $root) (local.get $objKey)))
657
882
  (local.set $props
658
- (if (result f64) (call $__is_nullish (local.get $oldProps))
659
- (then (call $__hash_new))
883
+ (if (result i64) (call $__is_nullish (local.get $oldProps))
884
+ (then (i64.reinterpret_f64 (call $__hash_new_small)))
660
885
  (else (local.get $oldProps))))
661
886
  (local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
662
- (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))
663
888
  (then
664
889
  (local.set $root (call $__ihash_set_local (local.get $root) (local.get $objKey) (local.get $props)))
665
- (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)))))))
666
893
  (local.get $val))`
667
894
 
668
895
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
669
- (local $props f64) (local $root f64)
896
+ (local $props i64) (local $root i64)
670
897
  (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
671
- (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)))))
672
899
  (if (call $__is_nullish (local.get $props)) (then (return)))
673
- (local.set $root (call $__ihash_set_local (global.get $__dyn_props) (f64.convert_i32_s (local.get $newOff)) (local.get $props)))
674
- (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))))`
675
902
 
676
903
  // Generated HASH probe functions
677
904
  ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
@@ -702,10 +929,8 @@ export default (ctx) => {
702
929
  const objVal = ['local.get', `$${objTmp}`]
703
930
  const idxVal = ['local.get', `$${idxTmp}`]
704
931
  const typeVal = ['local.get', `$${typeTmp}`]
705
- const isStringKey = ['call', '$__is_str_key', keyVal]
706
- const isStringLike = ['i32.or',
707
- ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
708
- ['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]]
709
934
  const isArrayLike = ['i32.or',
710
935
  ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
711
936
  ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]]
@@ -716,9 +941,7 @@ export default (ctx) => {
716
941
  ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
717
942
  ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]],
718
943
  ['i32.or',
719
- ['i32.or',
720
- ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
721
- ['i32.eq', typeVal, ['i32.const', PTR.SSO]]],
944
+ ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
722
945
  ['i32.or',
723
946
  ['i32.or',
724
947
  ['i32.eq', typeVal, ['i32.const', PTR.SET]],
@@ -732,7 +955,7 @@ export default (ctx) => {
732
955
  ['local.set', `$${objTmp}`, asF64(emit(obj))],
733
956
  ['local.set', `$${keyTmp}`, asF64(emit(key))],
734
957
  ['local.set', `$${outTmp}`, ['i32.const', 0]],
735
- ['local.set', `$${typeTmp}`, ['call', '$__ptr_type', objVal]],
958
+ ['local.set', `$${typeTmp}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', objVal]]],
736
959
  ['local.set', `$${idxTmp}`, ['i32.trunc_sat_f64_s', keyVal]],
737
960
 
738
961
  ['if', ['i32.and',
@@ -742,52 +965,72 @@ export default (ctx) => {
742
965
  ['i32.ge_s', idxVal, ['i32.const', 0]]]],
743
966
  ['then',
744
967
  ['if', isStringLike,
745
- ['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]]]]]],
746
969
  ['if', isArrayLike,
747
- ['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]]]]]]]],
748
971
 
749
972
  ['if', isStringKey,
750
973
  ['then',
751
974
  ['if', hasDynProps,
752
975
  ['then', ['local.set', `$${outTmp}`,
753
- ['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]]]]]]]]],
754
977
 
755
978
  ['if', ['i32.eq', typeVal, ['i32.const', PTR.HASH]],
756
979
  ['then', ['local.set', `$${outTmp}`,
757
980
  ['if', ['result', 'i32'], isStringKey,
758
- ['then', ['call', '$__hash_has', objVal, keyVal]],
759
- ['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]]]]]]]],
760
983
 
761
984
  ...(ctx.features.external ? [['if', ['i32.eq', typeVal, ['i32.const', PTR.EXTERNAL]],
762
- ['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]]]]]] : []),
763
987
 
764
988
  ['local.get', `$${outTmp}`]], 'i32')
765
989
  }
766
990
 
767
991
  // === for...in on dynamic objects (HASH iteration) ===
768
992
 
769
- // 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.
770
996
  ctx.core.emit['for-in'] = (varName, src, body) => {
771
997
  const off = tempI32('ho'), cap = tempI32('hc')
772
998
  const i = tempI32('hi'), slot = tempI32('hs')
999
+ const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
773
1000
  if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
774
1001
  const id = ctx.func.uniq++
775
1002
  const va = asF64(emit(src))
776
1003
  const bodyFlat = emitFlat(body)
1004
+ inc('__ptr_type')
777
1005
  return [
778
- ['local.set', `$${off}`, ['call', '$__ptr_offset', va]],
779
- ['local.set', `$${cap}`, ['call', '$__cap', va]],
780
- ['local.set', `$${i}`, ['i32.const', 0]],
781
- ['block', `$brk${id}`, ['loop', `$loop${id}`,
782
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
783
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
784
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
785
- ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
786
- ['then',
787
- ['local.set', `$${varName}`, ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
788
- ...bodyFlat]],
789
- ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
790
- ['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}`]]]]]
791
1034
  ]
792
1035
  }
793
1036
  }