jz 0.6.0 → 0.8.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.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -12,8 +12,8 @@ import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64
12
12
  import { emit, deps, call } from '../src/bridge.js'
13
13
  import { valTypeOf } from '../src/kind.js'
14
14
  import { VAL, lookupValType } from '../src/reps.js'
15
- import { hasOwnContinue, isBlockBody } from '../src/ast.js'
16
- import { ctx, inc, PTR, LAYOUT, getter, declGlobal } from '../src/ctx.js'
15
+ import { hasOwnContinue, isBlockBody, isLiteralStr } from '../src/ast.js'
16
+ import { ctx, inc, PTR, LAYOUT, registerGetter, declGlobal } from '../src/ctx.js'
17
17
  import { STR_INTERN_BIT } from '../layout.js'
18
18
 
19
19
  const SET_ENTRY = 16 // hash + key
@@ -43,6 +43,20 @@ function numConstLiteral(expr) {
43
43
  return null
44
44
  }
45
45
 
46
+ // Compile-time probe hash for a LITERAL collection/property key, else null. A numeric
47
+ // constant → numHashLiteral; an ASCII string literal → strHashLiteral. The string case is
48
+ // ASCII-only on purpose: strHashLiteral folds `charCodeAt(i) & 0xFF`, which equals __str_hash /
49
+ // __map_hash (FNV-1a over the UTF-8 bytes) ONLY for code points < 0x80 — a non-ASCII literal
50
+ // would fold a different hash than its stored key and silently miss, so it falls back to the
51
+ // runtime hash. Lets `m.get("if")` / `m.has("x")` skip the per-access __map_hash call.
52
+ const ASCII_KEY = /^[\x00-\x7f]*$/
53
+ const litKeyHash = (key) => {
54
+ const num = numConstLiteral(key)
55
+ if (num != null) return numHashLiteral(num)
56
+ if (isLiteralStr(key) && ASCII_KEY.test(key[1])) return strHashLiteral(key[1])
57
+ return null
58
+ }
59
+
46
60
  // Equality expressions for probe templates
47
61
  const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
48
62
  const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
@@ -55,9 +69,22 @@ const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8
55
69
  // walking shared-prefix bytes through __str_eq (31M unequal content-compares
56
70
  // per self-host bench run before this). If-expression for short-circuit:
57
71
  // i32.and would still evaluate the call.
72
+ //
73
+ // On a hash hit, an inline `storedKey == queryKey` (one i64.eq on the slot's key
74
+ // word) decides the overwhelmingly-common identity case — interned/SSO literals and
75
+ // the same heap pointer are bit-equal — WITHOUT the __str_eq / __same_value_zero call
76
+ // frame. Sound for both: bit-equality implies string-equality and SameValueZero (the
77
+ // only cross-bit-pattern equals — +0/-0, distinct NaN payloads — fall through to the
78
+ // full compare, never the reverse). Only a content-equal-but-bit-distinct key (a heap
79
+ // string vs a literal) still pays the call. This attacks the __str_eq call tax directly
80
+ // and helps the runtimes that DON'T inline tiny callees (wasmtime, wasm2c).
58
81
  const hashGuard = (eqExpr) =>
59
82
  `(if (result i32) (i32.eq (i32.load (local.get $slot)) (local.get $h))
60
- (then ${eqExpr}) (else (i32.const 0)))`
83
+ (then (if (result i32)
84
+ (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
85
+ (then (i32.const 1))
86
+ (else ${eqExpr})))
87
+ (else (i32.const 0)))`
61
88
  const strEqG = hashGuard(strEq)
62
89
  const sameValueZeroEqG = hashGuard(sameValueZeroEq)
63
90
 
@@ -367,34 +394,39 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
367
394
  (i64.const ${missing}))`
368
395
  }
369
396
 
370
- function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing = UNDEF_NAN, hasExt = false) {
397
+ // wantValue=true (default): return the slot value, missing `missing` (i64). wantValue=false:
398
+ // return an i32 0/1 existence flag (for `.has`). Mirrors genLookup's two-mode shape, prehashed.
399
+ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing = UNDEF_NAN, hasExt = false, wantValue = true) {
400
+ const rt = wantValue ? 'i64' : 'i32'
401
+ const onEmpty = wantValue ? `(return (i64.const ${missing}))` : '(return (i32.const 0))'
402
+ const onFound = wantValue ? '(return (i64.load (i32.add (local.get $slot) (i32.const 16))))' : '(return (i32.const 1))'
403
+ const notFound = wantValue ? `(i64.const ${missing})` : '(i32.const 0)'
404
+ const extHit = wantValue ? '(call $__ext_prop (local.get $coll) (local.get $key))' : '(call $__ext_has (local.get $coll) (local.get $key))'
371
405
  const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
372
406
  const typeGuard = hasExt
373
407
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
374
408
  (then
375
409
  (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL}))
376
- (then (return (call $__ext_prop (local.get $coll) (local.get $key))))
377
- (else (return (i64.const ${missing}))))))`
410
+ (then (return ${extHit}))
411
+ (else ${onEmpty}))))`
378
412
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
379
- (then (return (i64.const ${missing}))))`
413
+ (then ${onEmpty}))`
380
414
  // SET/MAP/HASH grow by forward-marking; a boxed pointer may be stale → follow the chain.
381
415
  const offExpr = '(call $__ptr_offset (local.get $coll))'
382
- return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result i64)
416
+ return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result ${rt})
383
417
  (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
384
418
  ${typeGuard}
385
419
  (local.set $off ${offExpr})
386
420
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
387
421
  ${probeStart(entrySize)}
388
422
  (block $done (loop $probe
389
- (if (i64.eqz (i64.load (local.get $slot)))
390
- (then (return (i64.const ${missing}))))
391
- (if ${eqExpr}
392
- (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
423
+ (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
424
+ (if ${eqExpr} (then ${onFound}))
393
425
  ${probeNext(entrySize)}
394
426
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
395
427
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
396
428
  (br $probe)))
397
- (i64.const ${missing}))`
429
+ ${notFound})`
398
430
  }
399
431
 
400
432
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
@@ -440,6 +472,9 @@ export default (ctx) => {
440
472
  __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset'] : ['__map_set', '__ptr_offset'],
441
473
  __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset'] : ['__same_value_zero', '__ptr_offset'],
442
474
  __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
475
+ // Prehashed has-probes: caller folds the hash, so no __map_hash dependency.
476
+ __map_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
477
+ __set_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
443
478
  __map_delete: ['__map_hash', '__same_value_zero'],
444
479
  __map_from: ['__ptr_type', '__ptr_offset', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
445
480
  __hash_set: () => ctx.features.external
@@ -628,20 +663,35 @@ export default (ctx) => {
628
663
  // the Set probe carries a PTR.SET type guard that rejects a Map outright, so
629
664
  // routing a Map through `__set_has`/`__set_delete` makes every lookup/delete
630
665
  // silently report absent. Mirrors collViewDyn below; typed receivers skip it.
631
- const collProbeDyn = (mapFn, setFn) => (collExpr, key) => {
666
+ // `h` (optional precomputed key hash) appends to the call → routes to the `_h` prehashed
667
+ // probes, skipping __map_hash per access; omitted → the generic hashing probes.
668
+ const collProbeDyn = (mapFn, setFn) => (collExpr, key, h) => {
632
669
  inc(mapFn, setFn, '__ptr_type')
633
670
  const o = temp('cp'), k = tempI64('cpk')
671
+ const extra = h != null ? [['i32.const', h]] : []
634
672
  return typed(['block', ['result', 'f64'],
635
673
  ['local.set', `$${o}`, asF64(emit(collExpr))],
636
674
  ['local.set', `$${k}`, asI64(emit(key))],
637
675
  ['f64.convert_i32_s', ['if', ['result', 'i32'],
638
676
  ptrTypeEq(['local.get', `$${o}`], PTR.MAP),
639
- ['then', ['call', `$${mapFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]],
640
- ['else', ['call', `$${setFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]]]]], 'f64')
677
+ ['then', ['call', `$${mapFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`], ...extra]],
678
+ ['else', ['call', `$${setFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`], ...extra]]]]], 'f64')
679
+ }
680
+ // `.has` on an unproven receiver: a literal key folds its hash and uses the _h probes.
681
+ ctx.core.emit['.has'] = (collExpr, key) => {
682
+ const h = litKeyHash(key)
683
+ return h != null
684
+ ? collProbeDyn('__map_has_h', '__set_has_h')(collExpr, key, h)
685
+ : collProbeDyn('__map_has', '__set_has')(collExpr, key)
641
686
  }
642
- ctx.core.emit['.has'] = collProbeDyn('__map_has', '__set_has')
643
687
  ctx.core.emit['.delete'] = collProbeDyn('__map_delete', '__set_delete')
644
- ctx.core.emit[`.${VAL.SET}:has`] = call('__set_has', 'II', 'i32')
688
+ // Typed Set.has: literal key → prehashed __set_has_h, else the generic probe.
689
+ ctx.core.emit[`.${VAL.SET}:has`] = (collExpr, key) => {
690
+ const h = litKeyHash(key)
691
+ if (h == null) return call('__set_has', 'II', 'i32')(collExpr, key)
692
+ inc('__set_has_h')
693
+ return typed(['f64.convert_i32_s', ['call', '$__set_has_h', asI64(emit(collExpr)), asI64(emit(key)), ['i32.const', h]]], 'f64')
694
+ }
645
695
  ctx.core.emit[`.${VAL.SET}:delete`] = call('__set_delete', 'II', 'i32')
646
696
 
647
697
  // Map.prototype.clear / Set.prototype.clear — drop every entry. `.clear` only
@@ -666,7 +716,7 @@ export default (ctx) => {
666
716
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
667
717
  (f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
668
718
 
669
- ctx.core.emit['.size'] = getter((expr) => {
719
+ registerGetter('.size', (expr) => {
670
720
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
671
721
  })
672
722
 
@@ -691,6 +741,7 @@ export default (ctx) => {
691
741
  // Generated Set probe functions
692
742
  ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
693
743
  ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
744
+ ctx.core.stdlib['__set_has_h'] = () => genLookupStrictPrehashed('__set_has_h', SET_ENTRY, sameValueZeroEqG, PTR.SET, UNDEF_NAN, ctx.features.external, false)
694
745
  ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET)
695
746
 
696
747
  // === Map ===
@@ -717,10 +768,10 @@ export default (ctx) => {
717
768
  ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
718
769
 
719
770
  const emitMapGet = (mapExpr, key) => {
720
- const constKey = numConstLiteral(key)
721
- if (constKey != null) {
771
+ const h = litKeyHash(key)
772
+ if (h != null) {
722
773
  inc('__map_get_h')
723
- return typed(['f64.reinterpret_i64', ['call', '$__map_get_h', asI64(emit(mapExpr)), asI64(emit(key)), ['i32.const', numHashLiteral(constKey)]]], 'f64')
774
+ return typed(['f64.reinterpret_i64', ['call', '$__map_get_h', asI64(emit(mapExpr)), asI64(emit(key)), ['i32.const', h]]], 'f64')
724
775
  }
725
776
  inc('__map_get')
726
777
  return typed(['f64.reinterpret_i64', ['call', '$__map_get', asI64(emit(mapExpr)), asI64(emit(key))]], 'f64')
@@ -729,7 +780,13 @@ export default (ctx) => {
729
780
  ctx.core.emit['.get'] = emitMapGet
730
781
  ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
731
782
 
732
- ctx.core.emit[`.${VAL.MAP}:has`] = call('__map_has', 'II', 'i32')
783
+ // Typed Map.has: literal key → prehashed __map_has_h, else the generic probe.
784
+ ctx.core.emit[`.${VAL.MAP}:has`] = (collExpr, key) => {
785
+ const h = litKeyHash(key)
786
+ if (h == null) return call('__map_has', 'II', 'i32')(collExpr, key)
787
+ inc('__map_has_h')
788
+ return typed(['f64.convert_i32_s', ['call', '$__map_has_h', asI64(emit(collExpr)), asI64(emit(key)), ['i32.const', h]]], 'f64')
789
+ }
733
790
  ctx.core.emit[`.${VAL.MAP}:delete`] = call('__map_delete', 'II', 'i32')
734
791
 
735
792
  // Map/Set iteration views: keys() / values() / entries() materialize a dense
@@ -752,22 +809,30 @@ export default (ctx) => {
752
809
  // statically proven (e.g. a Map read off an object field): resolve MAP vs SET
753
810
  // vs ARRAY once at runtime. ARRAY.values() is the array itself; .keys() yields
754
811
  // indices; .entries() yields [i, el]. Any other receiver passes through.
755
- const collViewDyn = (mapWalk, setWalk, arrWalk) => (expr) => {
812
+ // A typed array (PTR.TYPED) is NOT PTR.ARRAY, so without an explicit branch it fell
813
+ // through to `local.get $t` (return the receiver). For .values that is correct (the
814
+ // receiver iterates its values), but .keys then yielded values instead of indices and
815
+ // .entries yielded scalars instead of [i, value] pairs. The typedWalk arg restores the
816
+ // right behavior for typed receivers (keys → index array, entries → typed-aware pairs).
817
+ const collViewDyn = (mapWalk, setWalk, arrWalk, typedWalk) => (expr) => {
756
818
  inc('__ptr_type')
757
819
  const t = temp('cd')
758
820
  const pt = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
759
821
  const branch = (tag, walk, rest) =>
760
822
  ['if', ['result', 'f64'], ['i32.eq', pt(), ['i32.const', tag]], ['then', walk(t)], ['else', rest]]
761
823
  const tree = branch(PTR.MAP, mapWalk, branch(PTR.SET, setWalk,
762
- branch(PTR.ARRAY, arrWalk, ['local.get', `$${t}`])))
824
+ branch(PTR.ARRAY, arrWalk, branch(PTR.TYPED, typedWalk, ['local.get', `$${t}`]))))
763
825
  return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], tree], 'f64')
764
826
  }
827
+ // keys: index array [0..len-1] for both plain and typed (length-based, no element reads).
765
828
  ctx.core.emit['.keys'] = collViewDyn(
766
- t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp)
829
+ t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp, arrIdxFromTemp)
830
+ // values: the receiver iterates its own elements (correct for plain and typed alike).
767
831
  ctx.core.emit['.values'] = collViewDyn(
768
- t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`])
832
+ t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`], t => ['local.get', `$${t}`])
833
+ // entries: [i, element] pairs; the typed variant reads elements width/kind-aware.
769
834
  ctx.core.emit['.entries'] = collViewDyn(
770
- t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp)
835
+ t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp, arrEntriesFromTempTyped)
771
836
 
772
837
  // Map/Set forEach(cb): invoke cb(value, key) per live entry in insertion order.
773
838
  // Map yields (value=val@16, key=key@8); Set yields (value=key@8, key@8) — the
@@ -807,6 +872,7 @@ export default (ctx) => {
807
872
  ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
808
873
  ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
809
874
  ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEqG, PTR.MAP, UNDEF_NAN, ctx.features.external)
875
+ ctx.core.stdlib['__map_has_h'] = () => genLookupStrictPrehashed('__map_has_h', MAP_ENTRY, sameValueZeroEqG, PTR.MAP, UNDEF_NAN, ctx.features.external, false)
810
876
  ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, false, ctx.features.external)
811
877
  ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP)
812
878
 
@@ -869,12 +935,12 @@ export default (ctx) => {
869
935
  (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
870
936
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.shr_u (local.get $aux) (i32.const 14)))
871
937
  (then
872
- (local.set $len (i32.and (local.get $aux) (i32.const 7)))
938
+ (local.set $len (i32.and (i32.shr_u (local.get $aux) (i32.const 10)) (i32.const 7)))
873
939
  (block $ds (loop $ls
874
940
  (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
875
941
  (local.set $h (i32.mul
876
942
  (i32.xor (local.get $h)
877
- (i32.and (i32.shr_u (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (i32.const 0xFF)))
943
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7))) (i64.const 0x7f))))
878
944
  (i32.const 0x01000193)))
879
945
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
880
946
  (br $ls))))
@@ -1754,3 +1820,31 @@ function arrEntriesFromTemp(t) {
1754
1820
  ['br', `$aeloop${id}`]]],
1755
1821
  out.ptr]
1756
1822
  }
1823
+
1824
+ // TypedArray.prototype.entries() → dense Array of [index, element] pairs, reading each
1825
+ // element width/kind-aware via __typed_get_idx (the plain arrEntriesFromTemp uses a raw
1826
+ // f64.load, wrong for non-f64 typed arrays). Guarded on __typed_get_idx being registered:
1827
+ // it only is when the typedarray module is loaded, which is exactly when a PTR.TYPED
1828
+ // value can exist — so when absent, this branch is dead and falls back to the receiver.
1829
+ function arrEntriesFromTempTyped(t) {
1830
+ if (!ctx.core.stdlib['__typed_get_idx']) return ['local.get', `$${t}`]
1831
+ inc('__len', '__typed_get_idx', '__alloc_hdr')
1832
+ const n = tempI32('aetn'), i = tempI32('aeti'), pair = tempI32('aetp')
1833
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aet' })
1834
+ const id = ctx.func.uniq++
1835
+ const P = () => ['i64.reinterpret_f64', ['local.get', `$${t}`]]
1836
+ return ['block', ['result', 'f64'],
1837
+ ['local.set', `$${n}`, ['call', '$__len', P()]],
1838
+ out.init,
1839
+ ['local.set', `$${i}`, ['i32.const', 0]],
1840
+ ['block', `$aetbrk${id}`, ['loop', `$aetloop${id}`,
1841
+ ['br_if', `$aetbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1842
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1843
+ ['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
1844
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1845
+ ['call', '$__typed_get_idx', P(), ['local.get', `$${i}`]]],
1846
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
1847
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1848
+ ['br', `$aetloop${id}`]]],
1849
+ out.ptr]
1850
+ }
package/module/console.js CHANGED
@@ -69,7 +69,7 @@ const setupWasi = (ctx) => {
69
69
  (local.set $aux (call $__ptr_aux (local.get $ptr)))
70
70
  (if (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
71
71
  (then
72
- (local.set $len (i32.and (local.get $aux) (i32.const 7)))
72
+ (local.set $len (i32.and (i32.shr_u (local.get $aux) (i32.const 10)) (i32.const 7)))
73
73
  (local.set $buf (call $__alloc (local.get $len)))
74
74
  (local.set $off (i32.const 0))
75
75
  (block $done (loop $loop
package/module/core.js CHANGED
@@ -10,13 +10,13 @@
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'
17
17
  import { inlineArraySid } from '../src/static.js'
18
18
  import { VAL, lookupValType, lookupNotString, repOf, updateRep } from '../src/reps.js'
19
- import { ctx, err, inc, PTR, LAYOUT, HEAP, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
19
+ import { ctx, err, inc, PTR, LAYOUT, HEAP, FORWARDING_MASK, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
20
20
  import { ptrOffsetFwdWat, STR_INTERN_BIT } from '../layout.js'
21
21
  import { nanPrefixHex } from '../layout.js'
22
22
  import { initSchema } from './schema.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)
@@ -153,9 +227,7 @@ export default (ctx) => {
153
227
  ;; (cap=-1 sentinel at -4, new offset at -8). Other types never forward, so they skip
154
228
  ;; the loop; a well-formed ptr without forwarding pays one bounds + cap check per hop.
155
229
  (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
156
- (if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
157
- (i32.or (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
158
- (i32.or (i32.eq (local.get $t) (i32.const ${PTR.SET})) (i32.eq (local.get $t) (i32.const ${PTR.MAP})))))
230
+ (if (i32.and (i32.shl (i32.const 1) (local.get $t)) (i32.const ${FORWARDING_MASK}))
159
231
  (then
160
232
  ${followForwardingWat('$off', { lowGuard: true })}))
161
233
  (local.get $off))`
@@ -257,12 +329,29 @@ export default (ctx) => {
257
329
  (call $__memgrow (local.get $next))
258
330
  (i32.store (i32.const ${HEAP.PTR_ADDR}) (local.get $next))
259
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.
260
339
  ctx.core.stdlib['__clear'] = `(func $__clear
261
340
  (i32.store (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
262
341
  } else {
263
342
  // Own memory: heap offset in a global, exported so the JS-side adapter
264
343
  // (alloc:false, no `_alloc` export) shares the pointer.
265
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)
266
355
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
267
356
  (local $ptr i32) (local $next i32)
268
357
  (local.set $ptr (global.get $__heap))
@@ -271,7 +360,7 @@ export default (ctx) => {
271
360
  (global.set $__heap (local.get $next))
272
361
  (local.get $ptr))`
273
362
  ctx.core.stdlib['__clear'] = `(func $__clear
274
- (global.set $__heap (i32.const ${HEAP.START})))`
363
+ (global.set $__heap (global.get $__heap_reset)))`
275
364
  }
276
365
 
277
366
  // Build an insertion-ordered list of live slot offsets for a Set/Map/HASH
@@ -409,14 +498,15 @@ export default (ctx) => {
409
498
  (else (i32.load (i32.sub (local.get $off) (i32.const 4))))))
410
499
  (else (i32.const 0))))`
411
500
 
412
- // 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.
413
503
  ctx.core.stdlib['__str_len'] = `(func $__str_len (param $ptr i64) (result i32)
414
504
  (local $off i32) (local $aux i32)
415
505
  (if (i32.ne (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.STRING}))
416
506
  (then (return (i32.const 0))))
417
507
  (local.set $aux (call $__ptr_aux (local.get $ptr)))
418
508
  (if (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
419
- (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)))))
420
510
  (local.set $off (call $__ptr_offset (local.get $ptr)))
421
511
  (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
422
512
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -477,6 +567,56 @@ export default (ctx) => {
477
567
  (memory.fill (i32.add (local.get $ptr) (i32.const 16)) (i32.const 0) (i32.mul (local.get $cap) (local.get $stride)))
478
568
  (i32.add (local.get $ptr) (i32.const 16)))`
479
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
+
480
620
  // Allocator + exports are deferred: only included when memory is actually needed.
481
621
  // Any module using allocPtr/inc('__alloc') pulls these in via ctx.core.stdlibDeps.
482
622
  // compile.js emits _alloc/_clear exports + memory section only when __alloc is in includes.
@@ -732,12 +872,14 @@ export default (ctx) => {
732
872
  const eqT = (n) => `(i32.eq (local.get $t) (i32.const ${n}))`
733
873
  let disj = eqT(types[0])
734
874
  for (let i = 1; i < types.length; i++) disj = `(i32.or ${disj} ${eqT(types[i])})`
735
- 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}
736
878
  (then
737
879
  (if (result f64) (i32.ge_u (local.get $off) (i32.const 8))
738
880
  (then (f64.convert_i32_s (call $__len (local.get $v))))
739
881
  (else (f64.const nan:${UNDEF_NAN}))))
740
- (else (f64.const nan:${UNDEF_NAN})))`
882
+ (else (f64.const nan:${UNDEF_NAN}))))`
741
883
  const stringArm = `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
742
884
  (then (f64.convert_i32_s (call $__str_len (local.get $v))))
743
885
  (else ${lenArm}))`
@@ -748,7 +890,6 @@ export default (ctx) => {
748
890
  (then (f64.const nan:${UNDEF_NAN}))
749
891
  (else
750
892
  (local.set $t (call $__ptr_type (local.get $v)))
751
- (local.set $off (call $__ptr_offset (local.get $v)))
752
893
  ${stringArm})))`
753
894
  }
754
895
 
@@ -825,8 +966,9 @@ export default (ctx) => {
825
966
  const ptRep = typeof obj === 'string' ? repOf(obj) : null
826
967
  const ptVt = ptRep ? ptRep.val : valTypeOf(obj)
827
968
  if (ptVt) {
828
- const tpEmitter = ctx.core.emit[`.${ptVt}:${prop}`]
829
- 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)
830
972
  }
831
973
 
832
974
  // valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
@@ -853,7 +995,7 @@ export default (ctx) => {
853
995
  // fall through to a real property read — `m.values` reads the "values" field.
854
996
  const propKey = `.${prop}`
855
997
  const propEmitter = ctx.core.emit[propKey]
856
- if (propEmitter?.getter) return propEmitter(obj)
998
+ if (propEmitter && ctx.core.getters.has(propKey)) return propEmitter(obj)
857
999
 
858
1000
  return emitPropAccess(emit(obj), obj, prop)
859
1001
  }
@@ -906,11 +1048,13 @@ export default (ctx) => {
906
1048
  // re-emitting `obj`. Without this `s?.size` fell straight to emitPropAccess (a
907
1049
  // plain field read) and returned undefined — a Set/Map size getter never ran.
908
1050
  if (vt) {
909
- const tg = ctx.core.emit[`.${vt}:${prop}`]
910
- 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)
911
1054
  }
912
- const g = ctx.core.emit[`.${prop}`]
913
- 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)
914
1058
  return emitPropAccess(typed(['local.get', `$${t}`], 'f64'), obj, prop, true)
915
1059
  })
916
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)