jz 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,793 @@
1
+ /**
2
+ * Collection module — Set, Map, HASH (dynamic string-keyed objects).
3
+ *
4
+ * Set: type=8, open addressing hash table. Entries: [hash:f64, key:f64] (16B each).
5
+ * Map: type=9, same but entries: [hash:f64, key:f64, val:f64] (24B each).
6
+ * HASH: type=7, same layout as Map but uses content-based string hash + equality.
7
+ *
8
+ * @module collection
9
+ */
10
+
11
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr } 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 } from '../src/ctx.js'
15
+
16
+ const SET_ENTRY = 16 // hash + key
17
+ const MAP_ENTRY = 24 // hash + key + value
18
+ const INIT_CAP = 8 // initial capacity (must be power of 2)
19
+
20
+ export function strHashLiteral(str) {
21
+ let h = 0x811c9dc5 | 0
22
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ (str.charCodeAt(i) & 0xFF), 0x01000193) | 0
23
+ return h <= 1 ? (h + 2) | 0 : h
24
+ }
25
+
26
+ const HASH_BUF = new ArrayBuffer(8)
27
+ const HASH_F64 = new Float64Array(HASH_BUF)
28
+ const HASH_U32 = new Uint32Array(HASH_BUF)
29
+
30
+ export function numHashLiteral(n) {
31
+ HASH_F64[0] = n
32
+ return (HASH_U32[0] ^ HASH_U32[1]) | 0
33
+ }
34
+
35
+ function numConstLiteral(expr) {
36
+ if (typeof expr === 'number' && Number.isFinite(expr)) return expr
37
+ if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isFinite(expr[1])) return expr[1]
38
+ return null
39
+ }
40
+
41
+ // 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
+
45
+ /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
46
+ * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
47
+ * Gated off → type mismatch just returns coll unchanged. */
48
+ 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 onMatch = hasVal
52
+ ? `(then\n (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
53
+ : `(then (br $done))`
54
+
55
+ const extBranch = hasVal
56
+ ? '(then (call $__ext_set (local.get $coll) (local.get $key) (local.get $val)) drop)'
57
+ : '(then (nop))'
58
+ const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))`
59
+ const typeGuard = hasExt
60
+ ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL})) ${extBranch}) (return (local.get $coll))))`
61
+ : `(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)))
65
+ ${typeGuard}
66
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
67
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
68
+ (local.set $h (call ${hashFn} (local.get $key)))
69
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
70
+ (block $done (loop $probe
71
+ (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
+ (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}
76
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
77
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
78
+ (br $done)))
79
+ (if ${eqExpr} ${onMatch})
80
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
81
+ (br $probe)))
82
+ (local.get $coll))`
83
+ }
84
+
85
+ /** Generate lookup probe function.
86
+ * wantValue=true: return slot value, missing => NULL_NAN sentinel.
87
+ * wantValue=false: return i32 0/1 existence flag.
88
+ * hasExt: emit EXTERNAL fallthrough (delegate to __ext_prop/__ext_has). */
89
+ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, hasExt) {
90
+ const rt = wantValue ? 'f64' : 'i32'
91
+ const onEmpty = wantValue
92
+ ? `(return (f64.const nan:${NULL_NAN}))`
93
+ : '(return (i32.const 0))'
94
+ const onFound = wantValue
95
+ ? '(return (f64.load (i32.add (local.get $slot) (i32.const 16))))'
96
+ : '(return (i32.const 1))'
97
+ const notFound = wantValue
98
+ ? `(f64.const nan:${NULL_NAN})`
99
+ : '(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 typeGuard = hasExt
102
+ ? `(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
+ (else ${onEmpty}))))`
105
+ : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
106
+
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
+ ${typeGuard}
111
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
112
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
113
+ (local.set $h (call ${hashFn} (local.get $key)))
114
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
115
+ (block $done (loop $probe
116
+ (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}))
118
+ (if ${eqExpr} (then ${onFound}))
119
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
120
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
121
+ (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
122
+ (br $probe)))
123
+ ${notFound})`
124
+ }
125
+
126
+ /** Generate delete probe function. Zero out entry on match. */
127
+ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
128
+ return `(func $${name} (param $coll f64) (param $key f64) (result i32)
129
+ (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
130
+ (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
131
+ (local.set $off (call $__ptr_offset (local.get $coll)))
132
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
133
+ (local.set $h (call ${hashFn} (local.get $key)))
134
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
135
+ (block $done (loop $probe
136
+ (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))))
138
+ (if ${eqExpr}
139
+ (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
+ (return (i32.const 1))))
143
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
144
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
145
+ (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
146
+ (br $probe)))
147
+ (i32.const 0))`
148
+ }
149
+
150
+ /** Generate growable upsert. Grows table at 75% load, rehashes, then inserts.
151
+ * strict=true: reject wrong type.
152
+ * strict=false: EXTERNAL → __ext_set, other non-HASH types → __dyn_set (global props).
153
+ * The non-strict fallback is critical for untyped variables (e.g. arrays from
154
+ * Object.create) that receive property writes — without it writes silently vanish. */
155
+ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = false, hasExt = false) {
156
+ const nonHashFallback = hasExt
157
+ ? `(if (i32.eq (call $__ptr_type (local.get $obj)) (i32.const ${PTR.EXTERNAL}))
158
+ (then (call $__ext_set (local.get $obj) (local.get $key) (local.get $val)) drop)
159
+ (else (call $__dyn_set (local.get $obj) (local.get $key) (local.get $val)) drop))`
160
+ : `(call $__dyn_set (local.get $obj) (local.get $key) (local.get $val)) drop`
161
+ const typeGuard = strict
162
+ ? `(if (i32.ne (call $__ptr_type (local.get $obj)) (i32.const ${typeConst}))
163
+ (then (return (local.get $obj))))`
164
+ : `(if (i32.ne (call $__ptr_type (local.get $obj)) (i32.const ${typeConst}))
165
+ (then
166
+ ${nonHashFallback}
167
+ (return (local.get $obj))))`
168
+ return `(func $${name} (param $obj f64) (param $key f64) (param $val f64) (result f64)
169
+ (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
170
+ (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
171
+ (local $oldslot i32) (local $newidx i32) (local $newslot i32)
172
+ ${typeGuard}
173
+ (local.set $off (call $__ptr_offset (local.get $obj)))
174
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
175
+ (local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
176
+ ;; Grow if load factor > 75%: size * 4 >= cap * 3
177
+ (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
178
+ (then
179
+ (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
180
+ (local.set $newptr (call $__alloc_hdr (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
181
+ (local.set $i (i32.const 0))
182
+ (block $rd (loop $rl
183
+ (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
184
+ (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))
186
+ (then
187
+ (local.set $h (call ${hashFn} (f64.load (i32.add (local.get $oldslot) (i32.const 8)))))
188
+ (local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
189
+ (block $ins (loop $probe2
190
+ (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)))
192
+ (local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
193
+ (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))))
197
+ (i32.store (i32.sub (local.get $newptr) (i32.const 8))
198
+ (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
199
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
200
+ (br $rl)))
201
+ (local.set $off (local.get $newptr))
202
+ (local.set $cap (local.get $newcap))
203
+ (local.set $obj (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr)))))
204
+ ;; Insert/update
205
+ (local.set $h (call ${hashFn} (local.get $key)))
206
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
207
+ (block $done (loop $probe
208
+ (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))
210
+ (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))
214
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
215
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
216
+ (br $done)))
217
+ (if ${eqExpr}
218
+ (then
219
+ (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
220
+ (br $done)))
221
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
222
+ (br $probe)))
223
+ (local.get $obj))`
224
+ }
225
+
226
+ 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)))
230
+ (if (i32.ne
231
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
232
+ (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))))
235
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
236
+ (local.set $h (call ${hashFn} (local.get $key)))
237
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
238
+ (block $done (loop $probe
239
+ (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}))))
242
+ (if ${eqExpr}
243
+ (then (return (f64.load (i32.add (local.get $slot) (i32.const 16))))))
244
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
245
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
246
+ (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
247
+ (br $probe)))
248
+ (f64.const nan:${missing}))`
249
+ }
250
+
251
+ 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 typeGuard = hasExt
254
+ ? `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
255
+ (then
256
+ (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
257
+ (then (return (call $__ext_prop (local.get $coll) (local.get $key))))
258
+ (else (return (f64.const nan:${missing}))))))`
259
+ : `(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)))
264
+ ${typeGuard}
265
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
266
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
267
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
268
+ (block $done (loop $probe
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}))))
272
+ (if ${eqExpr}
273
+ (then (return (f64.load (i32.add (local.get $slot) (i32.const 16))))))
274
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
275
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
276
+ (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
277
+ (br $probe)))
278
+ (f64.const nan:${missing}))`
279
+ }
280
+
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)))
285
+ (if (i32.ne
286
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
287
+ (i32.const ${expectedType}))
288
+ (then (return (local.get $obj))))
289
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
290
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
291
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
292
+ (block $done (loop $probe
293
+ (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))
295
+ (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))
299
+ (i32.store (i32.sub (local.get $off) (i32.const 8))
300
+ (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
301
+ (br $done)))
302
+ (if ${eqExpr}
303
+ (then
304
+ (f64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
305
+ (br $done)))
306
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
307
+ (br $probe)))
308
+ (local.get $obj))`
309
+ }
310
+
311
+
312
+ export default (ctx) => {
313
+ // Feature-gated deps: EXTERNAL-dependent symbols are only pulled when features.external.
314
+ // Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
315
+ const ifExt = (name) => () => ctx.features.external ? [name] : []
316
+ Object.assign(ctx.core.stdlibDeps, {
317
+ __set_has: ifExt('__ext_has'),
318
+ __set_delete: [],
319
+ __map_set: ifExt('__ext_set'),
320
+ __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set'] : ['__map_set'],
321
+ __map_get_h: () => ctx.features.external ? ['__ext_prop'] : [],
322
+ __map_delete: [],
323
+ __hash_set: () => ctx.features.external
324
+ ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
325
+ : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
326
+ __hash_get: () => ctx.features.external
327
+ ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_prop']
328
+ : ['__str_hash', '__str_eq', '__ptr_type'],
329
+ __hash_has: () => ctx.features.external
330
+ ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_has']
331
+ : ['__str_hash', '__str_eq', '__ptr_type'],
332
+ __hash_new: ['__alloc_hdr'],
333
+ __hash_get_local: ['__str_hash', '__str_eq'],
334
+ __hash_get_local_h: ['__str_eq'],
335
+ __hash_set_local_h: ['__str_eq'],
336
+ __hash_set_local: ['__str_hash', '__str_eq'],
337
+ __ihash_get_local: ['__hash'],
338
+ __ihash_set_local: ['__hash', '__alloc_hdr', '__mkptr'],
339
+ __dyn_get_t: ['__ihash_get_local', '__str_hash', '__str_eq', '__is_nullish'],
340
+ __dyn_get: ['__dyn_get_t', '__ptr_type'],
341
+ __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
342
+ __dyn_get_expr: ['__dyn_get_expr_t', '__ptr_type'],
343
+ __dyn_get_any: ['__dyn_get_any_t', '__ptr_type'],
344
+ __dyn_get_any_t: () => ctx.features.external
345
+ ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
346
+ : ['__dyn_get_t', '__hash_get_local'],
347
+ __dyn_get_or: ['__dyn_get'],
348
+ __dyn_set: ['__hash_new', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish'],
349
+ __dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
350
+ })
351
+
352
+ inc('__ptr_offset', '__cap')
353
+
354
+ if (!ctx.scope.globals.has('__dyn_props'))
355
+ ctx.scope.globals.set('__dyn_props', '(global $__dyn_props (mut f64) (f64.const 0))')
356
+ // Schema name table for __dyn_get's OBJECT-schema fallback (polymorphic-receiver
357
+ // `.prop` access). Same declaration as json.js — defined here too so collection
358
+ // doesn't transitively require json. compile.js's schemaInit populates it when
359
+ // schema list is non-empty AND (__stringify OR __dyn_get) is included.
360
+ if (!ctx.scope.globals.has('__schema_tbl'))
361
+ ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
362
+
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)))'
367
+ // Hash function: simple f64 → i32 hash
368
+ ctx.core.stdlib['__hash'] = `(func $__hash (param $v f64) (result i32)
369
+ (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)))))`
372
+ inc('__hash')
373
+
374
+ // __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
375
+ ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
376
+ (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
377
+ (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
378
+
379
+ // === Set ===
380
+
381
+ ctx.core.emit['new.Set'] = () => {
382
+ ctx.features.set = true
383
+ const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
384
+ return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
385
+ }
386
+
387
+ ctx.core.emit['.add'] = (setExpr, val) => {
388
+ inc('__set_add')
389
+ return typed(['call', '$__set_add', asF64(emit(setExpr)), asF64(emit(val))], 'f64')
390
+ }
391
+
392
+ ctx.core.emit['.has'] = (setExpr, val) => {
393
+ inc('__set_has')
394
+ return typed(['f64.convert_i32_s', ['call', '$__set_has', asF64(emit(setExpr)), asF64(emit(val))]], 'f64')
395
+ }
396
+
397
+ ctx.core.emit['.delete'] = (setExpr, val) => {
398
+ inc('__set_delete')
399
+ return typed(['f64.convert_i32_s', ['call', '$__set_delete', asF64(emit(setExpr)), asF64(emit(val))]], 'f64')
400
+ }
401
+
402
+ ctx.core.emit['.size'] = (expr) => {
403
+ return typed(['f64.convert_i32_s', ['call', '$__len', asF64(emit(expr))]], 'f64')
404
+ }
405
+
406
+ // 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)
410
+
411
+ // === Map ===
412
+
413
+ ctx.core.emit['new.Map'] = () => {
414
+ ctx.features.map = true
415
+ const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'map' })
416
+ return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
417
+ }
418
+
419
+ ctx.core.emit['.set'] = (mapExpr, key, val) => {
420
+ inc('__map_set')
421
+ return typed(['call', '$__map_set', asF64(emit(mapExpr)), asF64(emit(key)), asF64(emit(val))], 'f64')
422
+ }
423
+ ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
424
+
425
+ const emitMapGet = (mapExpr, key) => {
426
+ const constKey = numConstLiteral(key)
427
+ if (constKey != null) {
428
+ inc('__map_get_h')
429
+ return typed(['call', '$__map_get_h', asF64(emit(mapExpr)), asF64(emit(key)), ['i32.const', numHashLiteral(constKey)]], 'f64')
430
+ }
431
+ inc('__map_get')
432
+ return typed(['call', '$__map_get', asF64(emit(mapExpr)), asF64(emit(key))], 'f64')
433
+ }
434
+
435
+ ctx.core.emit['.get'] = emitMapGet
436
+ ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
437
+
438
+ // 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)
442
+
443
+ // === HASH — dynamic string-keyed object (type=7) ===
444
+
445
+ // FNV-1a hash of string content (works on both SSO and heap strings)
446
+ // FNV-1a. ~95M calls in watr self-host. Inline char-fetch: hoist type/offset out of the
447
+ // byte loop so SSO branch uses dword shifts and STRING branch uses raw load8_u — neither
448
+ // 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)
451
+ (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}))
456
+ (then
457
+ (local.set $len (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
458
+ (block $ds (loop $ls
459
+ (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
460
+ (local.set $h (i32.mul
461
+ (i32.xor (local.get $h)
462
+ (i32.and (i32.shr_u (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
463
+ (i32.const 0x01000193)))
464
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
465
+ (br $ls))))
466
+ (else
467
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
468
+ (then (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))))
469
+ ;; 4-byte unrolled FNV-1a: each iter loads i32, mixes 4 bytes (little-endian) sequentially.
470
+ (local.set $lenA (i32.and (local.get $len) (i32.const -4)))
471
+ (block $d4 (loop $l4
472
+ (br_if $d4 (i32.ge_s (local.get $i) (local.get $lenA)))
473
+ (local.set $w (i32.load (i32.add (local.get $off) (local.get $i))))
474
+ (local.set $h (i32.mul (i32.xor (local.get $h) (i32.and (local.get $w) (i32.const 0xFF))) (i32.const 0x01000193)))
475
+ (local.set $h (i32.mul (i32.xor (local.get $h) (i32.and (i32.shr_u (local.get $w) (i32.const 8)) (i32.const 0xFF))) (i32.const 0x01000193)))
476
+ (local.set $h (i32.mul (i32.xor (local.get $h) (i32.and (i32.shr_u (local.get $w) (i32.const 16)) (i32.const 0xFF))) (i32.const 0x01000193)))
477
+ (local.set $h (i32.mul (i32.xor (local.get $h) (i32.shr_u (local.get $w) (i32.const 24))) (i32.const 0x01000193)))
478
+ (local.set $i (i32.add (local.get $i) (i32.const 4)))
479
+ (br $l4)))
480
+ (block $dh (loop $lh
481
+ (br_if $dh (i32.ge_s (local.get $i) (local.get $len)))
482
+ (local.set $h (i32.mul
483
+ (i32.xor (local.get $h)
484
+ (i32.load8_u (i32.add (local.get $off) (local.get $i))))
485
+ (i32.const 0x01000193)))
486
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
487
+ (br $lh)))))
488
+ ;; Ensure >= 2 (0=empty, 1=tombstone)
489
+ (if (result i32) (i32.le_s (local.get $h) (i32.const 1))
490
+ (then (i32.add (local.get $h) (i32.const 2))) (else (local.get $h))))`
491
+
492
+ ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
493
+ (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
494
+ (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
495
+
496
+ ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
497
+ ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
498
+ ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
499
+ 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)
504
+
505
+ // Inline __ptr_offset (forwarding-aware) and __hash_get_local body — dyn_get is the
506
+ // single hottest stdlib symbol in watr self-host (~95M calls). props returned by
507
+ // __ihash_get_local is always HASH (or NULL_NAN, filtered by __is_nullish), so the
508
+ // inlined probe skips a redundant type check + bit unboxing per call.
509
+ //
510
+ // OBJECT receivers fall back to schema-aware slot lookup when __dyn_props has no
511
+ // entry — covers polymorphic-receiver patterns (e.g. `let o = w?n():s()` with
512
+ // structurally distinct schemas) where receiver schemaId is unknown at compile
513
+ // time but lives at runtime in the NaN-box aux bits. Gated on schema name table
514
+ // presence (lifted in compile.js whenever __dyn_get is included). Static-shape
515
+ // 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 ? `
518
+ (if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
519
+ (then
520
+ (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
521
+ (then
522
+ (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))))
527
+ (local.set $nkeys (i32.load (i32.sub (local.get $koff) (i32.const 8))))
528
+ (local.set $idx (i32.const 0))
529
+ (block $kdone (loop $kloop
530
+ (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)))))))
535
+ (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
536
+ (br $kloop)))))))` : ''
537
+ const objectSchemaLocals = hasSchemas
538
+ ? '(local $sid i32) (local $kbits i64) (local $koff i32) (local $nkeys i32)'
539
+ : ''
540
+
541
+ ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj f64) (param $key f64) (result f64)
542
+ (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
543
+
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)
546
+ (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))))
550
+ (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
551
+ (then
552
+ (block $done
553
+ (loop $follow
554
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
555
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
556
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
557
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
558
+ (br $follow)))))
559
+ (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))))
566
+ (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
567
+ (local.set $h (call $__str_hash (local.get $key)))
568
+ (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))))
569
+ (block $hdone (loop $hprobe
570
+ (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))))))
574
+ (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $pcap) (i32.const 1))))
575
+ (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
576
+ (br_if $hdone (i32.ge_s (local.get $tries) (local.get $pcap)))
577
+ (br $hprobe))))${objectSchemaArm}
578
+ (f64.const nan:${UNDEF_NAN}))`
579
+
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)
582
+ (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}))
585
+ (then (local.get $fallback))
586
+ (else (local.get $val))))`
587
+
588
+ ctx.core.stdlib['__dyn_get_expr'] = `(func $__dyn_get_expr (param $obj f64) (param $key f64) (result f64)
589
+ (call $__dyn_get_expr_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
590
+
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)
593
+ (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}))
596
+ (then (local.get $val))
597
+ (else
598
+ (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
599
+ (then (call $__hash_get_local (local.get $obj) (local.get $key)))
600
+ (else (f64.const nan:${NULL_NAN}))))))`
601
+
602
+ // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
603
+ // Used at call sites where receiver type is statically unknown.
604
+ // When features.external is off, collapses to __dyn_get_expr shape (no EXTERNAL probe).
605
+ ctx.core.stdlib['__dyn_get_any'] = () => {
606
+ // Fast path: HASH check first, route directly to __hash_get_local. Hashes never carry
607
+ // dyn_props (those are for OBJECT/ARRAY attached props), so the original __dyn_get
608
+ // call was always wasted work on hashes — and JSON.parse / Map-style code is the
609
+ // dominant HASH consumer.
610
+ return `(func $__dyn_get_any (param $obj f64) (param $key f64) (result f64)
611
+ (call $__dyn_get_any_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
612
+ }
613
+
614
+ ctx.core.stdlib['__dyn_get_any_t'] = () => {
615
+ const extArm = ctx.features.external
616
+ ? `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
617
+ (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}))
623
+ (then (call $__hash_get_local (local.get $obj) (local.get $key)))
624
+ (else
625
+ (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}))
628
+ (then (local.get $val))
629
+ (else ${extArm})))))`
630
+ }
631
+
632
+ // Hot for `node.loc = pos` patterns (e.g. watr's parser tags every nested level).
633
+ // Defer the root insert to the end and gate it on props-ptr change: most calls hit
634
+ // the no-grow case where the ptr is unchanged and the root slot already points to it.
635
+ // __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}))
647
+ (then
648
+ (block $done
649
+ (loop $follow
650
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
651
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
652
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
653
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
654
+ (br $follow)))))
655
+ (local.set $objKey (f64.convert_i32_s (local.get $off)))
656
+ (local.set $oldProps (call $__ihash_get_local (local.get $root) (local.get $objKey)))
657
+ (local.set $props
658
+ (if (result f64) (call $__is_nullish (local.get $oldProps))
659
+ (then (call $__hash_new))
660
+ (else (local.get $oldProps))))
661
+ (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)))
663
+ (then
664
+ (local.set $root (call $__ihash_set_local (local.get $root) (local.get $objKey) (local.get $props)))
665
+ (global.set $__dyn_props (local.get $root))))
666
+ (local.get $val))`
667
+
668
+ ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
669
+ (local $props f64) (local $root f64)
670
+ (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))))
672
+ (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)))`
675
+
676
+ // Generated HASH probe functions
677
+ ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
678
+ ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true, ctx.features.external)
679
+ ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
680
+
681
+ // === `in` operator: key in obj → HASH key existence check ===
682
+ ctx.core.emit['in'] = (key, obj) => {
683
+ const objType = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
684
+
685
+ if (Array.isArray(key) && key[0] === 'str') {
686
+ const prop = key[1]
687
+ if (prop === 'length' && (objType === VAL.ARRAY || objType === VAL.TYPED || objType === VAL.STRING || objType === VAL.SET || objType === VAL.MAP))
688
+ return typed(['i32.const', 1], 'i32')
689
+
690
+ const schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
691
+ if (schemaIdx >= 0) return typed(['i32.const', 1], 'i32')
692
+ if (objType === VAL.OBJECT) return typed(['i32.const', 0], 'i32')
693
+ }
694
+
695
+ const keyTmp = temp()
696
+ const objTmp = temp()
697
+ const idxTmp = tempI32('in_idx')
698
+ const typeTmp = tempI32('in_type')
699
+ const outTmp = tempI32('in_out')
700
+
701
+ const keyVal = ['local.get', `$${keyTmp}`]
702
+ const objVal = ['local.get', `$${objTmp}`]
703
+ const idxVal = ['local.get', `$${idxTmp}`]
704
+ 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]]]
709
+ const isArrayLike = ['i32.or',
710
+ ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
711
+ ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]]
712
+ const hasDynProps = ['i32.or',
713
+ ['i32.eq', typeVal, ['i32.const', PTR.OBJECT]],
714
+ ['i32.or',
715
+ ['i32.or',
716
+ ['i32.eq', typeVal, ['i32.const', PTR.ARRAY]],
717
+ ['i32.eq', typeVal, ['i32.const', PTR.TYPED]]],
718
+ ['i32.or',
719
+ ['i32.or',
720
+ ['i32.eq', typeVal, ['i32.const', PTR.STRING]],
721
+ ['i32.eq', typeVal, ['i32.const', PTR.SSO]]],
722
+ ['i32.or',
723
+ ['i32.or',
724
+ ['i32.eq', typeVal, ['i32.const', PTR.SET]],
725
+ ['i32.eq', typeVal, ['i32.const', PTR.MAP]]],
726
+ ['i32.eq', typeVal, ['i32.const', PTR.CLOSURE]]]]]]
727
+
728
+ inc('__ptr_type', '__len', '__str_byteLen', '__hash_has', '__is_str_key', '__to_str', '__dyn_get', '__is_nullish')
729
+ if (ctx.features.external) inc('__ext_has')
730
+
731
+ return typed(['block', ['result', 'i32'],
732
+ ['local.set', `$${objTmp}`, asF64(emit(obj))],
733
+ ['local.set', `$${keyTmp}`, asF64(emit(key))],
734
+ ['local.set', `$${outTmp}`, ['i32.const', 0]],
735
+ ['local.set', `$${typeTmp}`, ['call', '$__ptr_type', objVal]],
736
+ ['local.set', `$${idxTmp}`, ['i32.trunc_sat_f64_s', keyVal]],
737
+
738
+ ['if', ['i32.and',
739
+ ['f64.eq', keyVal, keyVal],
740
+ ['i32.and',
741
+ ['f64.eq', ['f64.convert_i32_s', idxVal], keyVal],
742
+ ['i32.ge_s', idxVal, ['i32.const', 0]]]],
743
+ ['then',
744
+ ['if', isStringLike,
745
+ ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__str_byteLen', objVal]]]]],
746
+ ['if', isArrayLike,
747
+ ['then', ['local.set', `$${outTmp}`, ['i32.lt_u', idxVal, ['call', '$__len', objVal]]]]]]],
748
+
749
+ ['if', isStringKey,
750
+ ['then',
751
+ ['if', hasDynProps,
752
+ ['then', ['local.set', `$${outTmp}`,
753
+ ['i32.eqz', ['call', '$__is_nullish', ['call', '$__dyn_get', objVal, keyVal]]]]]]]],
754
+
755
+ ['if', ['i32.eq', typeVal, ['i32.const', PTR.HASH]],
756
+ ['then', ['local.set', `$${outTmp}`,
757
+ ['if', ['result', 'i32'], isStringKey,
758
+ ['then', ['call', '$__hash_has', objVal, keyVal]],
759
+ ['else', ['call', '$__hash_has', objVal, ['call', '$__to_str', keyVal]]]]]]],
760
+
761
+ ...(ctx.features.external ? [['if', ['i32.eq', typeVal, ['i32.const', PTR.EXTERNAL]],
762
+ ['then', ['local.set', `$${outTmp}`, ['call', '$__ext_has', objVal, keyVal]]]]] : []),
763
+
764
+ ['local.get', `$${outTmp}`]], 'i32')
765
+ }
766
+
767
+ // === for...in on dynamic objects (HASH iteration) ===
768
+
769
+ // for-in: iterate HASH entries, binding key string to loop variable
770
+ ctx.core.emit['for-in'] = (varName, src, body) => {
771
+ const off = tempI32('ho'), cap = tempI32('hc')
772
+ const i = tempI32('hi'), slot = tempI32('hs')
773
+ if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
774
+ const id = ctx.func.uniq++
775
+ const va = asF64(emit(src))
776
+ const bodyFlat = emitFlat(body)
777
+ 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}`]]]
791
+ ]
792
+ }
793
+ }