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/layout.js CHANGED
@@ -46,6 +46,24 @@ export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
46
46
  * tests membership in one shl+and, replacing a 4-way tag-equality OR. */
47
47
  export const FORWARDING_MASK = (1 << PTR.ARRAY) | (1 << PTR.HASH) | (1 << PTR.SET) | (1 << PTR.MAP)
48
48
 
49
+ // BigInt views of the NaN-box fields — the carrier is 64-bit, JS bit-ops are 32-bit.
50
+ const TAG_SHIFT = BigInt(LAYOUT.TAG_SHIFT), TAG_MASK = BigInt(LAYOUT.TAG_MASK)
51
+ const AUX_SHIFT = BigInt(LAYOUT.AUX_SHIFT), AUX_MASK = BigInt(LAYOUT.AUX_MASK)
52
+ const OFFSET_MASK = BigInt(LAYOUT.OFFSET_MASK)
53
+
54
+ /** Format an i64 BigInt as a zero-padded `0x…` hex literal for WAT/IR templates. */
55
+ export const i64Hex = bits => '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
56
+
57
+ /** Pack (type, aux, offset) into the i64 NaN-box carrier — the single source of
58
+ * truth for pointer bit layout. Compiler IR (`packPtrBits`/`mkPtrIR`), the
59
+ * `$__mkptr` inline specializer, and the interop high-word encoder all derive
60
+ * from this. `offset` defaults to 0 to yield the box prefix (offset OR'd later). */
61
+ export const ptrBits = (type, aux = 0, offset = 0) =>
62
+ LAYOUT.NAN_PREFIX_BITS
63
+ | ((BigInt(type) & TAG_MASK) << TAG_SHIFT)
64
+ | ((BigInt(aux) & AUX_MASK) << AUX_SHIFT)
65
+ | (BigInt(offset >>> 0) & OFFSET_MASK)
66
+
49
67
  // =============================================================================
50
68
  // PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
51
69
  // field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
@@ -94,20 +112,19 @@ export function ctorFromElemAux(aux) {
94
112
  return isView ? `new.${name}.view` : `new.${name}`
95
113
  }
96
114
 
97
- /** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop). */
115
+ /** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop) — the
116
+ * top 32 bits of `ptrBits(type, aux)` (offset lives in the low word). */
98
117
  export const encodePtrHi = (type, aux) =>
99
- (0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
118
+ ((LAYOUT.NAN_PREFIX << 16) | ((type & LAYOUT.TAG_MASK) << (LAYOUT.TAG_SHIFT - 32)) | (aux & LAYOUT.AUX_MASK)) >>> 0
100
119
 
101
- export const decodePtrType = hi => (hi >>> 15) & 0xF
102
- export const decodePtrAux = hi => hi & 0x7FFF
120
+ export const decodePtrType = hi => (hi >>> (LAYOUT.TAG_SHIFT - 32)) & LAYOUT.TAG_MASK
121
+ export const decodePtrAux = hi => hi & LAYOUT.AUX_MASK
103
122
 
104
123
  /** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
105
- export const nanPrefixHex = () =>
106
- '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
124
+ export const nanPrefixHex = () => i64Hex(LAYOUT.NAN_PREFIX_BITS)
107
125
 
108
126
  /** Atom sentinel as i64 hex (compiler WAT templates). */
109
- export const atomNanHex = atomId =>
110
- '0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
127
+ export const atomNanHex = atomId => i64Hex(LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << AUX_SHIFT))
111
128
 
112
129
  /** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
113
130
  * CANONICAL interned string — the static-pool copy (or an intern-table hit
@@ -120,22 +137,16 @@ export const atomNanHex = atomId =>
120
137
  export const STR_INTERN_BIT = 0x1
121
138
 
122
139
  /** Pre-shifted STRING SSO aux bit as i64 hex. */
123
- export const ssoBitI64Hex = () =>
124
- '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
140
+ export const ssoBitI64Hex = () => i64Hex(BigInt(LAYOUT.SSO_BIT) << AUX_SHIFT)
125
141
 
126
142
  /** Pre-shifted STRING slice/view aux bit as i64 hex. */
127
- export const sliceBitI64Hex = () =>
128
- '0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
143
+ export const sliceBitI64Hex = () => i64Hex(BigInt(LAYOUT.SLICE_BIT) << AUX_SHIFT)
129
144
 
130
145
  /** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
131
- export const ptrNanHex = (ptrType, aux = 0) =>
132
- '0x' + ptrBoxPrefixBigInt(ptrType, aux).toString(16).toUpperCase().padStart(16, '0')
146
+ export const ptrNanHex = (ptrType, aux = 0) => i64Hex(ptrBits(ptrType, aux))
133
147
 
134
148
  /** Compile-time i64 prefix for mkPtrIR (before offset OR). */
135
- export const ptrBoxPrefixBigInt = (ptrType, aux = 0) =>
136
- (0x7FF8n << 48n)
137
- | ((BigInt(ptrType) & 0xFn) << 47n)
138
- | ((BigInt(aux) & 0x7FFFn) << 32n)
149
+ export const ptrBoxPrefixBigInt = (ptrType, aux = 0) => ptrBits(ptrType, aux)
139
150
 
140
151
  /** Host-side atom sentinel high-u32 values (interop f64 decode). */
141
152
  export const ATOM_HI = {
package/module/array.js CHANGED
@@ -13,7 +13,7 @@ import { inBoundsArrIdx } from '../src/type.js'
13
13
  import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
14
14
  import { valTypeOf } from '../src/kind.js'
15
15
  import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
16
- import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey } from '../src/static.js'
16
+ import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey, intLiteralValue } from '../src/static.js'
17
17
  import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
18
18
  import { structInline } from '../src/abi/index.js'
19
19
  import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
@@ -249,9 +249,9 @@ export default (ctx) => {
249
249
  '__ptr_offset',
250
250
  ...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
251
251
  ],
252
- __arr_fill: ['__ptr_offset'],
252
+ __arr_fill: ['__ptr_offset', '__clamp_idx'], // body-calls __clamp_idx; declare it (self-host auto-scan can't be relied on — see test/selfhost-includes.js)
253
253
  __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
254
- __arr_push1: ['__arr_grow_known', '__ptr_offset'],
254
+ __arr_push1: ['__arr_grow_known', '__ptr_offset_fwd'],
255
255
  __arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
256
256
  __arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
257
257
  __arr_splice: ['__arr_grow', '__len', '__ptr_offset', '__alloc_hdr', '__mkptr'],
@@ -333,67 +333,6 @@ export default (ctx) => {
333
333
  // Full body handles TYPED element types and view indirection since external host can
334
334
  // pass typed arrays even when typedarray module isn't loaded. When features.typedarray
335
335
  // and features.external are both off, collapses to ARRAY-only f64 indexing.
336
- ctx.core.stdlib['__typed_idx'] = () => {
337
- if (!ctx.features.typedarray && !ctx.features.external) {
338
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
339
- (local $len i32)
340
- (local.set $len (call $__len (local.get $ptr)))
341
- (if (result f64)
342
- (i32.or
343
- (i32.lt_s (local.get $i) (i32.const 0))
344
- (i32.ge_u (local.get $i) (local.get $len)))
345
- (then (f64.const nan:${UNDEF_NAN}))
346
- (else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
347
- }
348
- // Hot (~37M calls in watr self-host). Type/aux/offset extracted once from $ptr.
349
- return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
350
- (local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
351
- (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
352
- (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
353
- ;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
354
- (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
355
- (then
356
- ${followForwardingWat('$off', { lowGuard: false })}
357
- (return (if (result f64)
358
- (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)))))
359
- (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
360
- (else (f64.const nan:${UNDEF_NAN}))))))
361
- (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
362
- (if
363
- (i32.and
364
- (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
365
- (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
366
- (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
367
- (local.set $len (call $__len (local.get $ptr)))
368
- (if (result f64)
369
- (i32.or
370
- (i32.lt_s (local.get $i) (i32.const 0))
371
- (i32.ge_u (local.get $i) (local.get $len)))
372
- (then (f64.const nan:${UNDEF_NAN}))
373
- (else
374
- (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
375
- (then
376
- (local.set $et (i32.and (local.get $aux) (i32.const 7)))
377
- (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
378
- (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
379
- (then (if (result f64) (i32.and (local.get $aux) (i32.const 16))
380
- (then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
381
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
382
- (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
383
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
384
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
385
- (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
386
- (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
387
- (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
388
- (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
389
- (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
390
- (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
391
- (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
392
- (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
393
- (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
394
- (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
395
- }
396
-
397
336
  // Array.from(src) — shallow copy of array (memory.copy of f64 elements)
398
337
  ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src i64) (result f64)
399
338
  (local $len i32) (local $dst i32)
@@ -576,12 +515,13 @@ export default (ctx) => {
576
515
  ctx.core.stdlib['__arr_push1'] = `(func $__arr_push1 (param $ptr i64) (param $val f64) (result f64)
577
516
  (local $p f64) (local $base i32) (local $len i32)
578
517
  (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
579
- (local.set $base (call $__ptr_offset (local.get $ptr)))
518
+ (local.set $base (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
519
+ ${followForwardingWat('$base', { lowGuard: true })}
580
520
  (local.set $len (i32.load (i32.sub (local.get $base) (i32.const 8))))
581
521
  (if (i32.lt_s (i32.load (i32.sub (local.get $base) (i32.const 4))) (i32.add (local.get $len) (i32.const 1)))
582
522
  (then
583
523
  (local.set $p (call $__arr_grow_known (local.get $ptr) (i32.add (local.get $len) (i32.const 1))))
584
- (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
524
+ (local.set $base (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $p)) (i64.const ${LAYOUT.OFFSET_MASK}))))))
585
525
  (f64.store (i32.add (local.get $base) (i32.shl (local.get $len) (i32.const 3))) (local.get $val))
586
526
  (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
587
527
  (local.get $p))`
@@ -657,6 +597,14 @@ export default (ctx) => {
657
597
  // === Index read ===
658
598
 
659
599
  ctx.core.emit['[]'] = (arr, idx) => {
600
+ // A literal NEGATIVE index is always out of range → undefined (JS semantics), never a
601
+ // raw `payload + (-1)*8` load that reads heap before the allocation. A side-effecting
602
+ // receiver still evaluates. Mirrors VT['[]'] returning null for the same case.
603
+ { const li = intLiteralValue(idx)
604
+ if (li != null && li < 0)
605
+ return typeof arr === 'string'
606
+ ? undefExpr()
607
+ : typed(['block', ['result', 'f64'], ['drop', asF64(emit(arr))], undefExpr()], 'f64') }
660
608
  // Hoist non-identifier arr so side-effecting sources (e.g. `foo.shift()[i]`) execute once.
661
609
  // The rest of the handler inlines `emit(arr)` into multiple IR positions, which would
662
610
  // otherwise re-execute the source expression per use at runtime.
@@ -731,6 +679,12 @@ export default (ctx) => {
731
679
  return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
732
680
  }
733
681
  const stringLoad = () => (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], vi])
682
+ // A numeric index on an unknown receiver is array/typed access by design — kept
683
+ // lean (no OBJECT/HASH dyn-get fork): an object with numeric keys is a degenerate
684
+ // pattern not worth a per-access string-coercion + hash probe in every hot loop.
685
+ // The WRITE path still routes a numeric `o[i]=v` on an OBJECT to __dyn_set for
686
+ // SAFETY (no schema-slot corruption / OOB), so such a read returns undefined
687
+ // rather than corrupting — matching JS for an out-of-range typed/array index.
734
688
  const arrayLoad = (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], vi])
735
689
  const emitDynamicKeyDispatch = (objExpr, numericLoad) => {
736
690
  const keyTmp = temp()
@@ -929,6 +883,10 @@ export default (ctx) => {
929
883
 
930
884
  // .push(val) → append, increment len, return array (possibly reallocated pointer)
931
885
  ctx.core.emit['.push'] = (arr, ...vals) => {
886
+ // `_expect` is overwritten by recursive emit() calls below. Capture the
887
+ // statement-position hint now so a dropped `xs.push(v)` can skip computing
888
+ // the JS return length while still performing the mutation/writeback.
889
+ const void_ = ctx.func._expect === 'void'
932
890
  // structInline Array<S>: `.push({S})` writes the K schema fields as K
933
891
  // consecutive f64 cells. Flatten the struct literal into K schema-ordered
934
892
  // field-value nodes and fall through to the general multi-value store path
@@ -956,8 +914,10 @@ export default (ctx) => {
956
914
  const readVar = box ? ['f64.load', ['local.get', `$${box}`]] : isGlobal ? ['global.get', `$${arr}`] : ['local.get', `$${arr}`]
957
915
  const writeVar = v => box ? ['f64.store', ['local.get', `$${box}`], v] : isGlobal ? ['global.set', `$${arr}`, v] : ['local.set', `$${arr}`, v]
958
916
  const vv = asF64(emit(vals[0]))
917
+ const pushed = ['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]
918
+ if (void_) return typed(['block', writeVar(pushed)], 'void')
959
919
  return typed(['block', ['result', 'f64'],
960
- writeVar(['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]),
920
+ writeVar(pushed),
961
921
  ['f64.convert_i32_s', ['i32.load', ['i32.sub',
962
922
  ['call', '$__ptr_offset', ['i64.reinterpret_f64', readVar]], ['i32.const', 8]]]]], 'f64')
963
923
  }
@@ -1109,12 +1069,8 @@ export default (ctx) => {
1109
1069
  (if (i32.ge_u (local.get $off) (i32.const 8))
1110
1070
  (then
1111
1071
  (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
1112
- (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
1113
- (if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.const 0))))
1114
- (if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
1115
- (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
1116
- (if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
1117
- (if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
1072
+ (local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
1073
+ (local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
1118
1074
  (local.set $i (local.get $start))
1119
1075
  (block $done (loop $fill
1120
1076
  (br_if $done (i32.ge_s (local.get $i) (local.get $end)))
@@ -1764,6 +1720,26 @@ export default (ctx) => {
1764
1720
  ['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
1765
1721
  }
1766
1722
 
1723
+ // Mirror of .indexOf scanning to the highest matching index — no early break, the last hit wins.
1724
+ // Registering it (alongside .string:lastIndexOf) is what lets lastIndexOf leave STRING_ONLY_METHODS:
1725
+ // an untyped receiver now forks string-vs-array at runtime instead of force-narrowing to string
1726
+ // (which returned -1 for every array). fromIndex is unsupported, matching .indexOf's array path.
1727
+ ctx.core.emit['.lastIndexOf'] = (arr, val) => {
1728
+ const recv = hoistArrayValue(arr)
1729
+ const vv = asF64(emit(val))
1730
+ const eq = arrEqIR(val)
1731
+ const result = tempI32('lx')
1732
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1733
+ ['if', eq(item, vv),
1734
+ ['then', ['local.set', `$${result}`, ['local.get', `$${i}`]]]]
1735
+ ])
1736
+ return typed(['block', ['result', 'f64'],
1737
+ recv.setup,
1738
+ ['local.set', `$${result}`, ['i32.const', -1]],
1739
+ ...loop,
1740
+ ['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
1741
+ }
1742
+
1767
1743
  // .at(i) → array element with negative index support
1768
1744
  ctx.core.emit['.array:at'] = (arr, idx) => {
1769
1745
  const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
@@ -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
@@ -815,6 +872,7 @@ export default (ctx) => {
815
872
  ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
816
873
  ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
817
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)
818
876
  ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, false, ctx.features.external)
819
877
  ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP)
820
878
 
@@ -877,12 +935,12 @@ export default (ctx) => {
877
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}))))
878
936
  (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.shr_u (local.get $aux) (i32.const 14)))
879
937
  (then
880
- (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)))
881
939
  (block $ds (loop $ls
882
940
  (br_if $ds (i32.ge_s (local.get $i) (local.get $len)))
883
941
  (local.set $h (i32.mul
884
942
  (i32.xor (local.get $h)
885
- (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))))
886
944
  (i32.const 0x01000193)))
887
945
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
888
946
  (br $ls))))
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