jz 0.7.0 → 0.8.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.
Files changed (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. package/src/wat/optimize.js +0 -3938
package/module/core.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, FALSE_NAN, TRUE_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr, sidecarOverride, undefExpr } from '../src/ir.js'
13
- import { emit, spread, deps } from '../src/bridge.js'
13
+ import { emit, spread, deps, wat } from '../src/bridge.js'
14
14
  import { reconstructArgsWithSpreads } from '../src/ir.js'
15
15
  import { valTypeOf, shapeOf } from '../src/kind.js'
16
16
  import { T } from '../src/ast.js'
@@ -36,11 +36,12 @@ export default (ctx) => {
36
36
  __is_str_key: ['__ptr_type'],
37
37
  __str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
38
38
  __set_len: ['__ptr_offset_fwd'],
39
- __length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
39
+ __length: ['__ptr_type', '__str_len', '__len'],
40
40
  __alloc: ['__memgrow'],
41
41
  __alloc_hdr: ['__alloc'],
42
42
  __alloc_hdr_n: ['__alloc'],
43
43
  __coll_order: ['__alloc'],
44
+ __obj_clone: ['__ptr_type', '__ptr_aux', '__ptr_offset', '__len', '__cap', '__alloc_hdr', '__alloc_hdr_n', '__mkptr'],
44
45
  })
45
46
 
46
47
  ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
@@ -143,6 +144,79 @@ export default (ctx) => {
143
144
  (i64.shl (i64.and (i64.extend_i32_u (local.get $aux)) (i64.const ${LAYOUT.AUX_MASK})) (i64.const ${LAYOUT.AUX_SHIFT}))
144
145
  (i64.and (i64.extend_i32_u (local.get $offset)) (i64.const ${LAYOUT.OFFSET_MASK})))))))`
145
146
 
147
+ // Relative-index clamp to `[0, len]` — the JS `RelativeIndex`/`ToIntegerOrInfinity`
148
+ // bounds dance shared by slice/subarray/fill/copyWithin (string + typed + array).
149
+ // Single shared body so N method bodies don't each inline the same six branches.
150
+ wat('__clamp_idx', `(func $__clamp_idx (param $v i32) (param $len i32) (result i32)
151
+ (if (i32.lt_s (local.get $v) (i32.const 0)) (then (local.set $v (i32.add (local.get $v) (local.get $len)))))
152
+ (if (i32.lt_s (local.get $v) (i32.const 0)) (then (local.set $v (i32.const 0))))
153
+ (if (i32.gt_s (local.get $v) (local.get $len)) (then (local.set $v (local.get $len))))
154
+ (local.get $v))`)
155
+
156
+ // Polymorphic element read for any heap-indexable (ARRAY or TYPED). The one
157
+ // home for `arr[i]` lowering: ARRAY and typed reads both route here, plain-array
158
+ // programs get the ARRAY-only collapse, typed programs the full elem dispatch.
159
+ ctx.core.stdlib['__typed_idx'] = () => {
160
+ if (!ctx.features.typedarray && !ctx.features.external) {
161
+ return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
162
+ (local $len i32)
163
+ (local.set $len (call $__len (local.get $ptr)))
164
+ (if (result f64)
165
+ (i32.or
166
+ (i32.lt_s (local.get $i) (i32.const 0))
167
+ (i32.ge_u (local.get $i) (local.get $len)))
168
+ (then (f64.const nan:${UNDEF_NAN}))
169
+ (else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
170
+ }
171
+ // Hot (~37M calls in watr self-host). Type/aux/offset extracted once from $ptr.
172
+ return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
173
+ (local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
174
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
175
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
176
+ ;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
177
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
178
+ (then
179
+ ${followForwardingWat('$off', { lowGuard: false })}
180
+ (return (if (result f64)
181
+ (i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
182
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
183
+ (else (f64.const nan:${UNDEF_NAN}))))))
184
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
185
+ (if
186
+ (i32.and
187
+ (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
188
+ (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
189
+ (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
190
+ (local.set $len (call $__len (local.get $ptr)))
191
+ (if (result f64)
192
+ (i32.or
193
+ (i32.lt_s (local.get $i) (i32.const 0))
194
+ (i32.ge_u (local.get $i) (local.get $len)))
195
+ (then (f64.const nan:${UNDEF_NAN}))
196
+ (else
197
+ (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
198
+ (then
199
+ (local.set $et (i32.and (local.get $aux) (i32.const 7)))
200
+ (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
201
+ (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
202
+ (then (if (result f64) (i32.and (local.get $aux) (i32.const 16))
203
+ (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
204
+ (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
205
+ (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
206
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
207
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
208
+ (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
209
+ (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
210
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
211
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
212
+ (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
213
+ (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
214
+ (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
215
+ (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
216
+ (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
217
+ (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
218
+ }
219
+
146
220
  ctx.core.stdlib['__ptr_offset_fwd'] = ptrOffsetFwdWat()
147
221
 
148
222
  ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr i64) (result i32)
@@ -255,12 +329,29 @@ export default (ctx) => {
255
329
  (call $__memgrow (local.get $next))
256
330
  (i32.store (i32.const ${HEAP.PTR_ADDR}) (local.get $next))
257
331
  (local.get $ptr))`
332
+ // NOTE: shared memory rewinds to the raw HEAP.START, NOT a post-init high-water
333
+ // mark — so a shared module whose `__start` heap-allocates (strPool memory.init,
334
+ // module-init state) loses that state on `_clear`. Pre-existing; unlike the owned
335
+ // path below it has no `__heap_reset` analogue because the rewind target would need
336
+ // a reserved low-memory cell (the [0,HEAP.START) region is already spoken for —
337
+ // clock at 0, heap ptr at HEAP.PTR_ADDR). Owned memory (the self-host + default
338
+ // case) is the one fixed below; revisit shared if a thread-pooled reset hits it.
258
339
  ctx.core.stdlib['__clear'] = `(func $__clear
259
340
  (i32.store (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
260
341
  } else {
261
342
  // Own memory: heap offset in a global, exported so the JS-side adapter
262
343
  // (alloc:false, no `_alloc` export) shares the pointer.
263
344
  declGlobal('__heap', 'i32', HEAP.START, { export: '__heap' })
345
+ // `__clear` rewinds to the *post-module-init* high-water mark, not the static
346
+ // data end: a module whose top-level code heap-allocates (e.g. the self-host
347
+ // compiler building its GLOBALS/atom tables in `__start`) leaves live state
348
+ // above the data segment that a reset must preserve. `__heap_reset` is seeded
349
+ // to the data end (assemble.js heapBase patch) and overwritten by `__start`'s
350
+ // tail with the heap top after init runs (buildStartFn) — so for a module with
351
+ // no init allocations it equals the data end, and for self-host it spares the
352
+ // compiler's init state. (Distinct from `__heap_start`, the propsPtr watermark,
353
+ // which must stay at the data end or init-time heap objects misread as static.)
354
+ declGlobal('__heap_reset', 'i32', HEAP.START)
264
355
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
265
356
  (local $ptr i32) (local $next i32)
266
357
  (local.set $ptr (global.get $__heap))
@@ -269,7 +360,7 @@ export default (ctx) => {
269
360
  (global.set $__heap (local.get $next))
270
361
  (local.get $ptr))`
271
362
  ctx.core.stdlib['__clear'] = `(func $__clear
272
- (global.set $__heap (i32.const ${HEAP.START})))`
363
+ (global.set $__heap (global.get $__heap_reset)))`
273
364
  }
274
365
 
275
366
  // Build an insertion-ordered list of live slot offsets for a Set/Map/HASH
@@ -407,14 +498,15 @@ export default (ctx) => {
407
498
  (else (i32.load (i32.sub (local.get $off) (i32.const 4))))))
408
499
  (else (i32.const 0))))`
409
500
 
410
- // String length (UTF-8 byte count). Heap: [-4:len(i32)][chars...]; SSO: aux & 7.
501
+ // String length (UTF-8 byte count). Heap: [-4:len(i32)][chars...]; SSO (7-bit codec):
502
+ // len at aux bits 10-12 (= payload bits 42-44). See module/string.js codec.
411
503
  ctx.core.stdlib['__str_len'] = `(func $__str_len (param $ptr i64) (result i32)
412
504
  (local $off i32) (local $aux i32)
413
505
  (if (i32.ne (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.STRING}))
414
506
  (then (return (i32.const 0))))
415
507
  (local.set $aux (call $__ptr_aux (local.get $ptr)))
416
508
  (if (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
417
- (then (return (i32.and (local.get $aux) (i32.const 7)))))
509
+ (then (return (i32.and (i32.shr_u (local.get $aux) (i32.const 10)) (i32.const 7)))))
418
510
  (local.set $off (call $__ptr_offset (local.get $ptr)))
419
511
  (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
420
512
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -475,6 +567,56 @@ export default (ctx) => {
475
567
  (memory.fill (i32.add (local.get $ptr) (i32.const 16)) (i32.const 0) (i32.mul (local.get $cap) (local.get $stride)))
476
568
  (i32.add (local.get $ptr) (i32.const 16)))`
477
569
 
570
+ // Shallow clone of an OBJECT or HASH, preserving its runtime type — the copy
571
+ // semantics of a single unknown spread `{ ...src }` (module/object.js). Without
572
+ // this, `{ ...src }` aliases src, so any later write to the result mutates the
573
+ // source (a real bug: jz's own narrow.js had to route around it). Per JS spread,
574
+ // the clone is SHALLOW: scalar slots are copied by value; nested object/string
575
+ // pointers are shared (immutable strings; nested objects are aliased as in V8).
576
+ //
577
+ // - OBJECT: alloc a fresh header'd object with the same schemaId and copy its N
578
+ // schema slots (N = key count of __schema_tbl[sid], robust to static-segment
579
+ // sources that carry no len/cap header). Then deep-copy the per-instance
580
+ // dyn-props HASH (base-16) so `o[k]=v` keys added before the spread carry over
581
+ // independently — heap objects only; static-segment objects have no header.
582
+ // - HASH: copy header + every probe slot wholesale (entries hold immutable
583
+ // string keys + scalar/pointer values — a byte copy is an independent dict).
584
+ // - anything else (primitive): nothing to clone, return as-is.
585
+ ctx.core.stdlib['__obj_clone'] = `(func $__obj_clone (param $v f64) (result f64)
586
+ (local $bits i64) (local $t i32) (local $sid i32) (local $n i32) (local $cap i32)
587
+ (local $src i32) (local $dst i32) (local $props i64)
588
+ (local.set $bits (i64.reinterpret_f64 (local.get $v)))
589
+ (local.set $t (call $__ptr_type (local.get $bits)))
590
+ (if (i32.eq (local.get $t) (i32.const ${PTR.OBJECT}))
591
+ (then
592
+ (local.set $sid (call $__ptr_aux (local.get $bits)))
593
+ (local.set $src (call $__ptr_offset (local.get $bits)))
594
+ (local.set $n (i32.const 0))
595
+ (if (i32.ne (global.get $__schema_tbl) (i32.const 0))
596
+ (then (local.set $n (call $__len
597
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3))))))))
598
+ (local.set $cap (i32.add (local.get $n) (i32.eqz (local.get $n))))
599
+ (local.set $dst (call $__alloc_hdr (i32.const 0) (local.get $cap)))
600
+ (memory.copy (local.get $dst) (local.get $src) (i32.shl (local.get $n) (i32.const 3)))
601
+ (if (i32.ge_u (local.get $src) (global.get $__heap_start))
602
+ (then
603
+ (local.set $props (i64.load (i32.sub (local.get $src) (i32.const 16))))
604
+ (if (i32.eq (call $__ptr_type (local.get $props)) (i32.const ${PTR.HASH}))
605
+ (then (i64.store (i32.sub (local.get $dst) (i32.const 16))
606
+ (i64.reinterpret_f64 (call $__obj_clone (f64.reinterpret_i64 (local.get $props)))))))))
607
+ (return (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid) (local.get $dst)))))
608
+ (if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
609
+ (then
610
+ (local.set $cap (call $__cap (local.get $bits)))
611
+ (local.set $src (call $__ptr_offset (local.get $bits)))
612
+ (local.set $dst (call $__alloc_hdr_n (i32.const 0) (local.get $cap) (i32.const 24)))
613
+ (memory.copy
614
+ (i32.sub (local.get $dst) (i32.const 16))
615
+ (i32.sub (local.get $src) (i32.const 16))
616
+ (i32.add (i32.const 16) (i32.mul (local.get $cap) (i32.const 24))))
617
+ (return (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0) (local.get $dst)))))
618
+ (local.get $v))`
619
+
478
620
  // Allocator + exports are deferred: only included when memory is actually needed.
479
621
  // Any module using allocPtr/inc('__alloc') pulls these in via ctx.core.stdlibDeps.
480
622
  // compile.js emits _alloc/_clear exports + memory section only when __alloc is in includes.
@@ -730,12 +872,14 @@ export default (ctx) => {
730
872
  const eqT = (n) => `(i32.eq (local.get $t) (i32.const ${n}))`
731
873
  let disj = eqT(types[0])
732
874
  for (let i = 1; i < types.length; i++) disj = `(i32.or ${disj} ${eqT(types[i])})`
733
- const lenArm = `(if (result f64) ${disj}
875
+ const lenArm = `(block (result f64)
876
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $v) (i64.const ${LAYOUT.OFFSET_MASK}))))
877
+ (if (result f64) ${disj}
734
878
  (then
735
879
  (if (result f64) (i32.ge_u (local.get $off) (i32.const 8))
736
880
  (then (f64.convert_i32_s (call $__len (local.get $v))))
737
881
  (else (f64.const nan:${UNDEF_NAN}))))
738
- (else (f64.const nan:${UNDEF_NAN})))`
882
+ (else (f64.const nan:${UNDEF_NAN}))))`
739
883
  const stringArm = `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
740
884
  (then (f64.convert_i32_s (call $__str_len (local.get $v))))
741
885
  (else ${lenArm}))`
@@ -746,7 +890,6 @@ export default (ctx) => {
746
890
  (then (f64.const nan:${UNDEF_NAN}))
747
891
  (else
748
892
  (local.set $t (call $__ptr_type (local.get $v)))
749
- (local.set $off (call $__ptr_offset (local.get $v)))
750
893
  ${stringArm})))`
751
894
  }
752
895
 
@@ -823,8 +966,9 @@ export default (ctx) => {
823
966
  const ptRep = typeof obj === 'string' ? repOf(obj) : null
824
967
  const ptVt = ptRep ? ptRep.val : valTypeOf(obj)
825
968
  if (ptVt) {
826
- const tpEmitter = ctx.core.emit[`.${ptVt}:${prop}`]
827
- if (tpEmitter?.getter) return tpEmitter(obj)
969
+ const tpKey = `.${ptVt}:${prop}`
970
+ const tpEmitter = ctx.core.emit[tpKey]
971
+ if (tpEmitter && ctx.core.getters.has(tpKey)) return tpEmitter(obj)
828
972
  }
829
973
 
830
974
  // valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
@@ -851,7 +995,7 @@ export default (ctx) => {
851
995
  // fall through to a real property read — `m.values` reads the "values" field.
852
996
  const propKey = `.${prop}`
853
997
  const propEmitter = ctx.core.emit[propKey]
854
- if (propEmitter?.getter) return propEmitter(obj)
998
+ if (propEmitter && ctx.core.getters.has(propKey)) return propEmitter(obj)
855
999
 
856
1000
  return emitPropAccess(emit(obj), obj, prop)
857
1001
  }
@@ -904,11 +1048,13 @@ export default (ctx) => {
904
1048
  // re-emitting `obj`. Without this `s?.size` fell straight to emitPropAccess (a
905
1049
  // plain field read) and returned undefined — a Set/Map size getter never ran.
906
1050
  if (vt) {
907
- const tg = ctx.core.emit[`.${vt}:${prop}`]
908
- if (tg?.getter) return tg(t)
1051
+ const tgKey = `.${vt}:${prop}`
1052
+ const tg = ctx.core.emit[tgKey]
1053
+ if (tg && ctx.core.getters.has(tgKey)) return tg(t)
909
1054
  }
910
- const g = ctx.core.emit[`.${prop}`]
911
- if (g?.getter) return g(t)
1055
+ const gKey = `.${prop}`
1056
+ const g = ctx.core.emit[gKey]
1057
+ if (g && ctx.core.getters.has(gKey)) return g(t)
912
1058
  return emitPropAccess(typed(['local.get', `$${t}`], 'f64'), obj, prop, true)
913
1059
  })
914
1060
 
package/module/json.js CHANGED
@@ -711,12 +711,12 @@ export default (ctx) => {
711
711
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
712
712
  ${ADV(2)})
713
713
  (else
714
- ;; Pack first 4 bytes into SSO slot (used only when len ≤ 4).
714
+ ;; Pack first 4 bytes into SSO slot (used only when len ≤ 4): 7-bit ASCII, char at bit len*7.
715
715
  (if (i32.lt_u (local.get $len) (i32.const 4))
716
716
  (then (local.set $sso
717
717
  (i32.or (local.get $sso)
718
718
  (i32.shl (i32.and (local.get $ch) (i32.const 0xFF))
719
- (i32.shl (local.get $len) (i32.const 3)))))))
719
+ (i32.mul (local.get $len) (i32.const 7)))))))
720
720
  (local.set $h (i32.mul (i32.xor (local.get $h) (i32.and (local.get $ch) (i32.const 0xFF))) (i32.const 0x01000193)))
721
721
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
722
722
  ${ADV(1)}))
@@ -735,7 +735,7 @@ export default (ctx) => {
735
735
  ;; SSO fast path: ≤4 ASCII chars, no escapes — bytes already packed inline.
736
736
  (if (i32.and (local.get $simple) (i32.le_u (local.get $len) (i32.const 4)))
737
737
  (then
738
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $sso)))))
738
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $len) (i32.const 10))) (local.get $sso)))))
739
739
  ;; Simple STRING fast path: no escapes, len > 4 — bulk memcpy from parse buffer,
740
740
  ;; skip rewind + per-byte escape-decode loop. Hits 5+ char keys without escapes.
741
741
  (if (local.get $simple)