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