jz 0.2.1 → 0.3.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.
@@ -44,6 +44,16 @@ function numConstLiteral(expr) {
44
44
  const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
45
45
  const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
46
46
 
47
+ // Open-addressing probe walked additively by entrySize: avoids an i32.mul + mask per
48
+ // step (vs recomputing slot = off + idx*entrySize). Needs $off/$cap/$h set and $end/$slot
49
+ // locals declared. `idxExpr` is the first-slot index (defaults to h mod cap; cap is pow2).
50
+ const probeStart = (entrySize, idxExpr = '(i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1)))') =>
51
+ `(local.set $end (i32.add (local.get $off) (i32.mul (local.get $cap) (i32.const ${entrySize}))))
52
+ (local.set $slot (i32.add (local.get $off) (i32.mul ${idxExpr} (i32.const ${entrySize}))))`
53
+ const probeNext = (entrySize) =>
54
+ `(local.set $slot (i32.add (local.get $slot) (i32.const ${entrySize})))
55
+ (if (i32.ge_u (local.get $slot) (local.get $end)) (then (local.set $slot (local.get $off))))`
56
+
47
57
  /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
48
58
  * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
49
59
  * Gated off → type mismatch just returns coll unchanged. */
@@ -62,14 +72,13 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
62
72
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL})) ${extBranch}) (return (local.get $coll))))`
63
73
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
64
74
  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)
75
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
66
76
  ${typeGuard}
67
77
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
68
78
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
69
79
  (local.set $h (call ${hashFn} (local.get $key)))
70
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
80
+ ${probeStart(entrySize)}
71
81
  (block $done (loop $probe
72
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
73
82
  (if (i64.eqz (i64.load (local.get $slot)))
74
83
  (then
75
84
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -78,7 +87,7 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
78
87
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
79
88
  (br $done)))
80
89
  (if ${eqExpr} ${onMatch})
81
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
90
+ ${probeNext(entrySize)}
82
91
  (br $probe)))
83
92
  (local.get $coll))`
84
93
  }
@@ -108,17 +117,16 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
108
117
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
109
118
 
110
119
  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)
120
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
112
121
  ${typeGuard}
113
122
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
114
123
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
115
124
  (local.set $h (call ${hashFn} (local.get $key)))
116
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
125
+ ${probeStart(entrySize)}
117
126
  (block $done (loop $probe
118
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
119
127
  (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
120
128
  (if ${eqExpr} (then ${onFound}))
121
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
129
+ ${probeNext(entrySize)}
122
130
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
123
131
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
124
132
  (br $probe)))
@@ -128,21 +136,20 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
128
136
  /** Generate delete probe function. Zero out entry on match. */
129
137
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
130
138
  return `(func $${name} (param $coll i64) (param $key i64) (result i32)
131
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
139
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
132
140
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
133
141
  (local.set $off (call $__ptr_offset (local.get $coll)))
134
142
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
135
143
  (local.set $h (call ${hashFn} (local.get $key)))
136
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
144
+ ${probeStart(entrySize)}
137
145
  (block $done (loop $probe
138
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
139
146
  (if (i64.eqz (i64.load (local.get $slot))) (then (return (i32.const 0))))
140
147
  (if ${eqExpr}
141
148
  (then
142
149
  (i64.store (local.get $slot) (i64.const 0))
143
150
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (i64.const 0))
144
151
  (return (i32.const 1))))
145
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
152
+ ${probeNext(entrySize)}
146
153
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
147
154
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
148
155
  (br $probe)))
@@ -168,7 +175,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
168
175
  ${nonHashFallback}
169
176
  (return (local.get $obj))))`
170
177
  return `(func $${name} (param $obj i64) (param $key i64) (param $val i64) (result i64)
171
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
178
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
172
179
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
173
180
  (local $oldslot i32) (local $newidx i32) (local $newslot i32)
174
181
  ${typeGuard}
@@ -205,9 +212,8 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
205
212
  (local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))))
206
213
  ;; Insert/update
207
214
  (local.set $h (call ${hashFn} (local.get $key)))
208
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
215
+ ${probeStart(entrySize)}
209
216
  (block $done (loop $probe
210
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
211
217
  (if (i64.eqz (i64.load (local.get $slot)))
212
218
  (then
213
219
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -220,14 +226,14 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
220
226
  (then
221
227
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
222
228
  (br $done)))
223
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
229
+ ${probeNext(entrySize)}
224
230
  (br $probe)))
225
231
  (local.get $obj))`
226
232
  }
227
233
 
228
234
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
229
235
  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)
236
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
231
237
  (if (i32.ne
232
238
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
233
239
  (i32.const ${expectedType}))
@@ -235,14 +241,13 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
235
241
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
236
242
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
237
243
  (local.set $h (call ${hashFn} (local.get $key)))
238
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
244
+ ${probeStart(entrySize)}
239
245
  (block $done (loop $probe
240
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
241
246
  (if (i64.eqz (i64.load (local.get $slot)))
242
247
  (then (return (i64.const ${missing}))))
243
248
  (if ${eqExpr}
244
249
  (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
245
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
250
+ ${probeNext(entrySize)}
246
251
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
247
252
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
248
253
  (br $probe)))
@@ -260,18 +265,17 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
260
265
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
261
266
  (then (return (i64.const ${missing}))))`
262
267
  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)
268
+ (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
264
269
  ${typeGuard}
265
270
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
266
271
  (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))))
272
+ ${probeStart(entrySize)}
268
273
  (block $done (loop $probe
269
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
270
274
  (if (i64.eqz (i64.load (local.get $slot)))
271
275
  (then (return (i64.const ${missing}))))
272
276
  (if ${eqExpr}
273
277
  (then (return (i64.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))))
278
+ ${probeNext(entrySize)}
275
279
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
276
280
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
277
281
  (br $probe)))
@@ -280,16 +284,15 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
280
284
 
281
285
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
282
286
  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)
287
+ (local $off i32) (local $cap i32) (local $end i32) (local $slot i32)
284
288
  (if (i32.ne
285
289
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
286
290
  (i32.const ${expectedType}))
287
291
  (then (return (local.get $obj))))
288
292
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
289
293
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
290
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
294
+ ${probeStart(entrySize)}
291
295
  (block $done (loop $probe
292
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
293
296
  (if (i64.eqz (i64.load (local.get $slot)))
294
297
  (then
295
298
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -302,7 +305,7 @@ function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
302
305
  (then
303
306
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
304
307
  (br $done)))
305
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
308
+ ${probeNext(entrySize)}
306
309
  (br $probe)))
307
310
  (local.get $obj))`
308
311
  }
@@ -340,9 +343,11 @@ export default (ctx) => {
340
343
  __hash_set_local: ['__str_hash', '__str_eq'],
341
344
  __ihash_get_local: ['__map_hash'],
342
345
  __ihash_set_local: ['__map_hash', '__alloc_hdr', '__mkptr'],
343
- __dyn_get_t: ['__ihash_get_local', '__str_hash', '__str_eq', '__is_nullish'],
346
+ __dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
347
+ __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
344
348
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
345
349
  __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
350
+ __dyn_get_expr_t_h: ['__dyn_get_t_h', '__hash_get_local_h'],
346
351
  __dyn_get_expr: ['__dyn_get_expr_t', '__ptr_type'],
347
352
  __dyn_get_any: ['__dyn_get_any_t', '__ptr_type'],
348
353
  __dyn_get_any_t: () => ctx.features.external
@@ -666,11 +671,22 @@ export default (ctx) => {
666
671
  ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
667
672
  (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
668
673
 
669
- ctx.core.stdlib['__dyn_get_t'] = () => `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
674
+ // Thin wrapper: hash the key once, delegate to the prehashed body. Constant-key
675
+ // call sites bypass this and call $__dyn_get_t_h directly with strHashLiteral().
676
+ ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
677
+ (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $type) (call $__str_hash (local.get $key))))`
678
+
679
+ ctx.core.stdlib['__dyn_get_t_h'] = () => `(func $__dyn_get_t_h (param $obj i64) (param $key i64) (param $type i32) (param $h i32) (result i64)
670
680
  (local $props i64) (local $off i32)
671
- (local $poff i32) (local $pcap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
681
+ (local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
672
682
  ${buildObjectSchemaLocals()}
673
683
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
684
+ ;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
685
+ ;; global __dyn_props hash on the function table index (negative — can't collide
686
+ ;; with real heap/data offsets). Closures *with* env keep their unique env ptr.
687
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
688
+ (then (local.set $off (i32.sub (i32.const -1)
689
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK})))))))
674
690
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
675
691
  (then
676
692
  (block $done
@@ -735,14 +751,14 @@ export default (ctx) => {
735
751
  (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props))))))))
736
752
  (local.set $poff (i32.wrap_i64 (i64.and (local.get $props) (i64.const ${LAYOUT.OFFSET_MASK}))))
737
753
  (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
738
- (local.set $h (call $__str_hash (local.get $key)))
739
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))))
754
+ (local.set $pend (i32.add (local.get $poff) (i32.mul (local.get $pcap) (i32.const ${MAP_ENTRY}))))
755
+ (local.set $slot (i32.add (local.get $poff) (i32.mul (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))) (i32.const ${MAP_ENTRY}))))
740
756
  (block $hdone (loop $hprobe
741
- (local.set $slot (i32.add (local.get $poff) (i32.mul (local.get $idx) (i32.const ${MAP_ENTRY}))))
742
757
  (br_if $dynDone (i64.eqz (i64.load (local.get $slot))))
743
758
  (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
744
759
  (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
745
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $pcap) (i32.const 1))))
760
+ (local.set $slot (i32.add (local.get $slot) (i32.const ${MAP_ENTRY})))
761
+ (if (i32.ge_u (local.get $slot) (local.get $pend)) (then (local.set $slot (local.get $poff))))
746
762
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
747
763
  (br_if $hdone (i32.ge_s (local.get $tries) (local.get $pcap)))
748
764
  (br $hprobe))))${buildObjectSchemaArm()}
@@ -770,6 +786,19 @@ export default (ctx) => {
770
786
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
771
787
  (else (i64.const ${NULL_NAN}))))))`
772
788
 
789
+ // Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
790
+ // is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
791
+ ctx.core.stdlib['__dyn_get_expr_t_h'] = `(func $__dyn_get_expr_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
792
+ (local $val i64)
793
+ (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
794
+ (if (result i64)
795
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
796
+ (then (local.get $val))
797
+ (else
798
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
799
+ (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
800
+ (else (i64.const ${NULL_NAN}))))))`
801
+
773
802
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
774
803
  // Used at call sites where receiver type is statically unknown.
775
804
  // When features.external is off, collapses to __dyn_get_expr shape (no EXTERNAL probe).
@@ -809,6 +838,10 @@ export default (ctx) => {
809
838
  (local $off i32) (local $type i32) ${buildObjectSchemaSetLocals()}
810
839
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
811
840
  (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
841
+ ;; CLOSURE with no env (offset 0): key __dyn_props on the function table index — see __dyn_get_t.
842
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
843
+ (then (local.set $off (i32.sub (i32.const -1)
844
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK})))))))
812
845
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
813
846
  (then
814
847
  (block $done
package/module/core.js CHANGED
@@ -173,16 +173,25 @@ export default (ctx) => {
173
173
  } else {
174
174
  // Own memory: heap offset in a global, auto-grow when needed
175
175
  ctx.scope.globals.set('__heap', '(global $__heap (mut i32) (i32.const 1024))')
176
+ // Bump allocator with geometric growth. Growing one page at a time turns a
177
+ // long-running embedding (e.g. watr called thousands of times) into O(n²) —
178
+ // each memory.grow may relocate and copy the whole heap. So when we must
179
+ // grow, request at least the current size (≥2× total) in one shot; only on
180
+ // hitting the declared maximum do we fall back to the bare minimum.
176
181
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
177
- (local $ptr i32) (local $next i32)
182
+ (local $ptr i32) (local $next i32) (local $cur i32) (local $need i32)
178
183
  (local.set $ptr (global.get $__heap))
179
184
  ;; Align next allocation to 8 bytes
180
185
  (local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
181
- ;; Grow memory if needed (each page = 65536 bytes)
182
- (if (i32.gt_u (local.get $next) (i32.mul (memory.size) (i32.const 65536)))
183
- (then (if (i32.eq (memory.grow
184
- (i32.shr_u (i32.add (i32.sub (local.get $next) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
185
- (i32.const -1)) (then (unreachable)))))
186
+ (local.set $cur (i32.shl (memory.size) (i32.const 16)))
187
+ (if (i32.gt_u (local.get $next) (local.get $cur))
188
+ (then
189
+ (local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
190
+ (if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
191
+ (if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
192
+ (then (if (i32.eq (memory.grow
193
+ (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
194
+ (i32.const -1)) (then (unreachable)))))))
186
195
  (global.set $__heap (local.get $next))
187
196
  (local.get $ptr))`
188
197
  ctx.core.stdlib['__clear'] = `(func $__clear
@@ -404,9 +413,15 @@ export default (ctx) => {
404
413
  return ['call', '$__ptr_type', receiver]
405
414
  }
406
415
 
407
- function emitDynGetExprTyped(base, key, vt) {
408
- inc('__dyn_get_expr_t')
416
+ function emitDynGetExprTyped(base, key, vt, prop) {
409
417
  const receiver = asI64(base?.type ? base : typed(base, 'f64'))
418
+ // Constant string key: fold the FNV hash at compile time and call the
419
+ // prehashed body — no __str_hash on every access.
420
+ if (typeof prop === 'string') {
421
+ inc('__dyn_get_expr_t_h')
422
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t_h', receiver, key, emitTypeTag(receiver, vt), ['i32.const', strHashLiteral(prop)]]], 'f64')
423
+ }
424
+ inc('__dyn_get_expr_t')
410
425
  return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
411
426
  }
412
427
 
@@ -486,7 +501,7 @@ export default (ctx) => {
486
501
  if (typeof obj === 'string') {
487
502
  const vt = lookupValType(obj)
488
503
  if (usesDynProps(vt)) {
489
- return emitDynGetExprTyped(va, key, vt)
504
+ return emitDynGetExprTyped(va, key, vt, prop)
490
505
  }
491
506
  if (vt === VAL.HASH) {
492
507
  return emitHashGetLocalConst(va, key, prop)
@@ -495,12 +510,12 @@ export default (ctx) => {
495
510
  // at off-16 (set by __dyn_set). __hash_get assumes HASH bucket layout
496
511
  // and would mis-read OBJECT memory.
497
512
  if (vt === VAL.OBJECT) {
498
- return emitDynGetExprTyped(va, key, vt)
513
+ return emitDynGetExprTyped(va, key, vt, prop)
499
514
  }
500
515
  if (vt == null) {
501
516
  // In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
502
517
  // Skip the external branch and dispatch through the typed HASH/OBJECT path.
503
- if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt)
518
+ if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt, prop)
504
519
  ctx.features.external = true
505
520
  return emitDynGetAnyTyped(va, key, vt)
506
521
  }
@@ -598,7 +613,8 @@ export default (ctx) => {
598
613
 
599
614
  // Module-registered property emitter (.size, etc.)
600
615
  const propKey = `.${prop}`
601
- if (ctx.core.emit[propKey]) return ctx.core.emit[propKey](obj)
616
+ const propEmitter = ctx.core.emit[propKey]
617
+ if (propEmitter && propEmitter.length <= 1) return propEmitter(obj)
602
618
 
603
619
  return emitPropAccess(emit(obj), obj, prop)
604
620
  }
@@ -618,7 +634,7 @@ export default (ctx) => {
618
634
  if (typeof obj === 'string') {
619
635
  const objType = lookupValType(obj)
620
636
  if (usesDynProps(objType)) {
621
- access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
637
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
622
638
  } else if (objType === VAL.HASH) {
623
639
  access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
624
640
  } else if (objType == null) {
@@ -626,7 +642,7 @@ export default (ctx) => {
626
642
  // In JS host mode use __dyn_get_any_t but don't force features.external here
627
643
  // since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
628
644
  access = ctx.transform.host === 'wasi'
629
- ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
645
+ ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
630
646
  : emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
631
647
  } else {
632
648
  inc('__hash_get', '__str_hash', '__str_eq')
@@ -636,12 +652,20 @@ export default (ctx) => {
636
652
  if (valTypeOf(obj) === VAL.HASH) {
637
653
  access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
638
654
  } else {
639
- access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj))
655
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj), prop)
640
656
  }
641
657
  }
642
658
  }
643
659
  }
644
- return emitNullishGuarded(['local.tee', `$${t}`, va], access)
660
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
661
+ // inlines the null/undefined check as (i32.or (i64.eq X NULL) (i64.eq X UNDEF)),
662
+ // which duplicates X — a local.tee(block(call $sideEffect)) would run twice.
663
+ return typed(['block', ['result', 'f64'],
664
+ ['local.set', `$${t}`, va],
665
+ ['if', ['result', 'f64'],
666
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
667
+ ['then', access],
668
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
645
669
  }
646
670
 
647
671
  // Optional index: arr?.[i] → null if arr is null, else arr[i]
@@ -656,7 +680,15 @@ export default (ctx) => {
656
680
  if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
657
681
  ctx.types.typedElem.set(t, ctx.types.typedElem.get(arr))
658
682
  }
659
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(ctx.core.emit['[]'](t, idx)))
683
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
684
+ // inlines the null/undefined check as (i32.or (i64.eq X NULL) (i64.eq X UNDEF)),
685
+ // which duplicates X — a local.tee(block(call $sideEffect)) would run twice.
686
+ return typed(['block', ['result', 'f64'],
687
+ ['local.set', `$${t}`, va],
688
+ ['if', ['result', 'f64'],
689
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
690
+ ['then', asF64(ctx.core.emit['[]'](t, idx))],
691
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
660
692
  }
661
693
 
662
694
  // Optional call: fn?.(...args) → null if fn is null, else call fn
@@ -674,7 +706,14 @@ export default (ctx) => {
674
706
  const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
675
707
  if (vt) updateRep(t, { val: vt })
676
708
  const callResult = methodEmit(t, ...args)
677
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
709
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
710
+ // inlines the null/undefined check which duplicates the expression.
711
+ return typed(['block', ['result', 'f64'],
712
+ ['local.set', `$${t}`, va],
713
+ ['if', ['result', 'f64'],
714
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
715
+ ['then', asF64(callResult)],
716
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
678
717
  }
679
718
  }
680
719
  const t = temp()
@@ -682,7 +721,14 @@ export default (ctx) => {
682
721
  // If nullish → return NULL_NAN, else call via fn.call
683
722
  if (!ctx.closure.call) err('Optional call requires fn module')
684
723
  const callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
685
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
724
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
725
+ // inlines the null/undefined check which duplicates the expression.
726
+ return typed(['block', ['result', 'f64'],
727
+ ['local.set', `$${t}`, va],
728
+ ['if', ['result', 'f64'],
729
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
730
+ ['then', asF64(callResult)],
731
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
686
732
  }
687
733
 
688
734
  // typeof: returns JS-style string. Reachable results are number/undefined/string/function/symbol/object
package/module/date.js CHANGED
@@ -76,6 +76,11 @@ export default (ctx) => {
76
76
  ], 'f64')
77
77
  }
78
78
 
79
+ ctx.core.emit['Date.parse'] = (value) => {
80
+ inc('__date_from_value')
81
+ return typed(['call', '$__date_from_value', ['i64.reinterpret_f64', asF64(emit(value))]], 'f64')
82
+ }
83
+
79
84
  // ── Core algorithms ───────────────────────────────────────────────────────
80
85
 
81
86
  ctx.core.stdlib['__date_days_from_year'] = `(func $__date_days_from_year (param $y f64) (result f64)
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN } from '../src/ir.js'
14
- import { emit, isReassigned } from '../src/emit.js'
14
+ import { emit } from '../src/emit.js'
15
+ import { isReassigned } from '../src/ast.js'
15
16
  import { T, lookupValType, repOf, findFreeVars } from '../src/analyze.js'
16
17
  import { PTR, LAYOUT, inc, err } from '../src/ctx.js'
17
18
 
package/module/json.js CHANGED
@@ -50,7 +50,8 @@ export default (ctx) => {
50
50
  __jput_str: ['__char_at', '__str_byteLen'],
51
51
  __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
52
52
  __jp_val: ['__jp_str', '__jp_num', '__jp_arr', '__jp_obj'],
53
- __jp_str: ['__sso_char', '__char_at', '__str_byteLen'],
53
+ __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__utf8_enc'],
54
+ __hex4: ['__hex1'],
54
55
  __jp_num: ['__pow10'],
55
56
  __jp_arr: ['__jp_val'],
56
57
  __jp_obj: ['__jp_val', '__jp_str', '__jp_schema_get', '__alloc_hdr', '__mkptr'],
@@ -319,8 +320,45 @@ export default (ctx) => {
319
320
  // SSO byte packing for ≤4-char ASCII keys, and FNV-1a hash. The hash is
320
321
  // stashed in $__jp_keyh so __jp_obj can use the prehashed insert and skip
321
322
  // a redundant __str_hash call.
323
+ // Hex nibble: '0'-'9' / 'a'-'f' / 'A'-'F' → 0..15; anything else → 0 (lenient).
324
+ ctx.core.stdlib['__hex1'] = `(func $__hex1 (param $c i32) (result i32)
325
+ (if (i32.le_u (i32.sub (local.get $c) (i32.const 48)) (i32.const 9))
326
+ (then (return (i32.sub (local.get $c) (i32.const 48)))))
327
+ (if (i32.le_u (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 97)) (i32.const 5))
328
+ (then (return (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 87)))))
329
+ (i32.const 0))`
330
+
331
+ // Read 4 hex bytes at absolute address $p → 16-bit value.
332
+ ctx.core.stdlib['__hex4'] = `(func $__hex4 (param $p i32) (result i32)
333
+ (i32.or (i32.or (i32.or
334
+ (i32.shl (call $__hex1 (i32.load8_u (local.get $p))) (i32.const 12))
335
+ (i32.shl (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 1)))) (i32.const 8)))
336
+ (i32.shl (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 2)))) (i32.const 4)))
337
+ (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 3))))))`
338
+
339
+ // Encode code point $cp as UTF-8 at $off; returns bytes written (1-4).
340
+ ctx.core.stdlib['__utf8_enc'] = `(func $__utf8_enc (param $off i32) (param $cp i32) (result i32)
341
+ (if (i32.lt_u (local.get $cp) (i32.const 0x80))
342
+ (then (i32.store8 (local.get $off) (local.get $cp)) (return (i32.const 1))))
343
+ (if (i32.lt_u (local.get $cp) (i32.const 0x800))
344
+ (then
345
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6))))
346
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
347
+ (return (i32.const 2))))
348
+ (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
349
+ (then
350
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12))))
351
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))))
352
+ (i32.store8 (i32.add (local.get $off) (i32.const 2)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
353
+ (return (i32.const 3))))
354
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18))))
355
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))))
356
+ (i32.store8 (i32.add (local.get $off) (i32.const 2)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))))
357
+ (i32.store8 (i32.add (local.get $off) (i32.const 3)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
358
+ (i32.const 4))`
359
+
322
360
  ctx.core.stdlib['__jp_str'] = `(func $__jp_str (result f64)
323
- (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32) (local $h i32)
361
+ (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32) (local $h i32) (local $cp i32)
324
362
  (local.set $start (global.get $__jppos))
325
363
  (local.set $simple (i32.const 1))
326
364
  (local.set $h (i32.const 0x811c9dc5))
@@ -382,12 +420,32 @@ export default (ctx) => {
382
420
  ${ADV(1)}
383
421
  (local.set $ch ${PEEK})
384
422
  ${ADV(1)}
385
- ;; Decode escape: n→10 t→9 r→13 b→8 f→12, else literal
386
- (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
387
- (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
388
- (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
389
- (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
390
- (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))
423
+ (if (i32.eq (local.get $ch) (i32.const 117)) ;; \\uXXXX
424
+ (then
425
+ (local.set $cp (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
426
+ ${ADV(4)}
427
+ ;; High surrogate immediately followed by \\uXXXX low surrogate → combine.
428
+ (if (i32.and
429
+ (i32.eq (i32.and (local.get $cp) (i32.const 0xFC00)) (i32.const 0xD800))
430
+ (i32.and (i32.eq ${PEEK} (i32.const 92))
431
+ (i32.eq (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 1)))) (i32.const 117))))
432
+ (then
433
+ ${ADV(2)}
434
+ (local.set $i (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
435
+ ${ADV(4)}
436
+ (local.set $cp (i32.add (i32.const 0x10000)
437
+ (i32.or (i32.shl (i32.and (local.get $cp) (i32.const 0x3FF)) (i32.const 10))
438
+ (i32.and (local.get $i) (i32.const 0x3FF)))))))
439
+ (local.set $len (i32.add (local.get $len)
440
+ (call $__utf8_enc (i32.add (local.get $off) (local.get $len)) (local.get $cp))))
441
+ (br $l2))
442
+ (else
443
+ ;; Decode simple escape: n→10 t→9 r→13 b→8 f→12, else literal char.
444
+ (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
445
+ (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
446
+ (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
447
+ (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
448
+ (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))))
391
449
  (else ${ADV(1)}))
392
450
  (i32.store8 (i32.add (local.get $off) (local.get $len)) (local.get $ch))
393
451
  (local.set $len (i32.add (local.get $len) (i32.const 1)))