jz 0.7.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 (53) 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 +1156 -984
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +29 -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 +26 -3
  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 +162 -156
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +29 -29
  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 +253 -23
  28. package/src/compile/index.js +198 -61
  29. package/src/compile/loop-divmod.js +12 -58
  30. package/src/compile/loop-model.js +91 -0
  31. package/src/compile/loop-square.js +102 -0
  32. package/src/compile/narrow.js +169 -32
  33. package/src/compile/peel-stencil.js +18 -64
  34. package/src/compile/plan/common.js +29 -0
  35. package/src/compile/plan/index.js +4 -1
  36. package/src/compile/plan/inline.js +176 -21
  37. package/src/compile/plan/literals.js +93 -19
  38. package/src/ctx.js +51 -12
  39. package/src/helper-counters.js +137 -0
  40. package/src/ir.js +102 -13
  41. package/src/kind-traits.js +7 -3
  42. package/src/kind.js +14 -1
  43. package/src/op-policy.js +5 -2
  44. package/src/ops.js +119 -0
  45. package/src/optimize/index.js +960 -136
  46. package/src/optimize/recurse.js +182 -0
  47. package/src/optimize/vectorize.js +1292 -144
  48. package/src/prepare/index.js +11 -7
  49. package/src/reps.js +4 -1
  50. package/src/type.js +53 -45
  51. package/src/wat/assemble.js +92 -9
  52. package/src/widen.js +21 -0
  53. package/src/wat/optimize.js +0 -3938
@@ -26,6 +26,8 @@ import {
26
26
  hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
27
27
  } from '../ast.js'
28
28
  import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
29
+ import { includeForStringOnly } from '../autoload.js'
30
+ import { FITS_I32_MAX } from '../widen.js'
29
31
  import { nonNegIntLiteral, intLiteralValue, staticPropertyKey } from '../static.js'
30
32
  import { findFreeVars } from './analyze.js'
31
33
  import {
@@ -76,6 +78,29 @@ const stringOps = (node) => {
76
78
  // instead route through ToNumber (`toNumF64`), which performs ToPrimitive.
77
79
  const isI32Num = (v) => v.type === 'i32' && v.ptrKind == null
78
80
 
81
+ // Peel an emitted operand back to its raw i32 value when it carries one: a value already
82
+ // typed i32 (integer literals included — they emit as i32.const), or an integer read wrapped
83
+ // in f64.convert_i32_s/u (typed-array / i32-global reads default to the f64 rep). Else null.
84
+ const peelI32 = (v) =>
85
+ isI32Num(v) ? v
86
+ : (Array.isArray(v) && (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u'))
87
+ ? (Array.isArray(v[1]) ? typed(v[1], 'i32') : v[1])
88
+ : null
89
+
90
+ // Native wrapping i32 arithmetic for `+`/`-`/`*` whose result is consumed as i32. Peels the
91
+ // f64.convert_i32_s/u that integer reads (`DX[i]`, a global Int32Array) wrap their load in, so
92
+ // `ax = ax + DX[i]` (ax and DX[i] both i32) lowers to one i32.add instead of the
93
+ // convert → f64.add → trunc_sat round-trip that doubled hot integer loops (ulam's spiral walk,
94
+ // ring-buffer indexing). Bit-identical for an i32 result: ToInt32(exact) ≡ two's-complement wrap.
95
+ // Gated on exprType(whole expr)==='i32' so an f64-consumed sum — or an unsigned-wide (uint32)
96
+ // operand, which exprType already reports as f64 — still widens. Returns null when inapplicable.
97
+ const tryI32Arith = (wasmOp, astOp, a, b, va, vb) => {
98
+ const pa = peelI32(va); if (pa == null) return null
99
+ const pb = peelI32(vb); if (pb == null) return null
100
+ if (exprType([astOp, a, b], ctx.func.locals) !== 'i32') return null
101
+ return typed([wasmOp, pa, pb], 'i32')
102
+ }
103
+
79
104
  // f64 arithmetic that can MINT a sign-nondeterministic NaN (0/0, ∞−∞, 0·∞, x%0): on x86
80
105
  // these are 0xFFF8…, on arm 0x7FF8…. sqrt/min/max/neg are NOT here — they canon at their
81
106
  // own emit (math.js / unary `-`), so they reach canonNum already canonical.
@@ -136,7 +161,27 @@ const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
136
161
  // so the inner per-op canon (local.set + select + f64.ne, ~3 ops) is dead on the
137
162
  // critical path. This is THE gap that put sqrt-heavy kernels ~23% behind V8
138
163
  // (julia/raymarcher/boids); stripping it makes them match native JS.
139
- const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
164
+ const stripCanon = (v) => {
165
+ if (!v) return v
166
+ if (v.canonOf != null) return typed(v.canonOf, 'f64')
167
+ // A NaN-canon nested in the VALUE arm of a `select` / `(if result f64)` is equally
168
+ // dead: the consumer that called stripCanon (f64.add/sub/mul/div, or a math call)
169
+ // propagates the NaN identically and the outermost escape re-canon-izes. Recurse into
170
+ // the arms so `(cond ? x : -x) + v` (the Perlin-gradient sign-select, and every other
171
+ // conditional negation) drops the per-neg select+f64.ne, same as a bare `x + -y`.
172
+ if (Array.isArray(v)) {
173
+ if (v[0] === 'select' && v.length === 4) {
174
+ const a = stripCanon(v[1]), b = stripCanon(v[2])
175
+ if (a !== v[1] || b !== v[2]) return typed(['select', a, b, v[3]], 'f64')
176
+ } else if (v[0] === 'if' && Array.isArray(v[1]) && v[1][0] === 'result' && v[1][1] === 'f64'
177
+ && Array.isArray(v[3]) && v[3][0] === 'then' && v[3].length === 2
178
+ && Array.isArray(v[4]) && v[4][0] === 'else' && v[4].length === 2) {
179
+ const t = stripCanon(v[3][1]), e = stripCanon(v[4][1])
180
+ if (t !== v[3][1] || e !== v[4][1]) return typed(['if', v[1], v[2], ['then', t], ['else', e]], 'f64')
181
+ }
182
+ }
183
+ return v
184
+ }
140
185
 
141
186
  const FIRST_CLASS_UNARY_MATH = {
142
187
  'math.abs': 'f64.abs',
@@ -177,10 +222,17 @@ const emitNeg = (a) => {
177
222
  // flipped NaN reads as a tagged value (truthy / not-NaN). Fold any NaN result back
178
223
  // to canonical — the same invariant math.sqrt/min/max keep via `canon` (module/math.js).
179
224
  const t = temp('ng')
180
- return typed(['block', ['result', 'f64'],
181
- ['local.set', `$${t}`, ['f64.neg', toNumF64(a, v)]],
225
+ const raw = ['f64.neg', toNumF64(a, v)]
226
+ const ir = typed(['block', ['result', 'f64'],
227
+ ['local.set', `$${t}`, raw],
182
228
  ['select', ['f64.const', 'nan'], ['local.get', `$${t}`],
183
229
  ['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
230
+ // Tag the un-canon'd `f64.neg` so a NaN-propagating consumer (f64.add/sub/mul/div, which
231
+ // canon-ize on their OWN escape) strips this redundant inner canon — same contract as the
232
+ // sqrt/min/max canons in math.js. A bare `x * -y` / `a - -b` then drops the per-neg
233
+ // select + f64.ne instead of carrying it into the multiply/add.
234
+ ir.canonOf = raw
235
+ return ir
184
236
  }
185
237
 
186
238
  /** Try constant-folding binary arith: returns emitNum(result) or null. */
@@ -194,20 +246,38 @@ const foldConst = (va, vb, fn, guard) =>
194
246
 
195
247
  // JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
196
248
  // Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
197
- // exact product stays f64-exact, i.e. |product| <= 2^53. Two i32 operands can
198
- // reach 2^62, so `i32.mul` is sound only when one side is bounded small enough
199
- // that, against the full i32 range (2^31) of the other, the product holds within
200
- // 2^53 i.e. its magnitude <= 2^22. A literal qualifies directly; so does a
201
- // masked operand (`x & 63`, `x >>> k`) whose value is provably bounded. Keeps
202
- // index arithmetic (`i*4`) and bitwise-masked scales (bytebeat's `t*(m&63)`) on
203
- // `i32.mul` while routing hash-mix-scale products to `f64.mul`.
204
- const FITS_I32_MAX = 0x400000 // 2^22 — see derivation above
249
+ // exact product stays f64-exact. A literal qualifies directly; so does a masked
250
+ // operand (`x & 63`, `x >>> k`) whose value is provably bounded. Keeps index
251
+ // arithmetic (`i*4`) and bitwise-masked scales (bytebeat's `t*(m&63)`) on
252
+ // `i32.mul` while routing hash-mix-scale products to `f64.mul`. The FITS_I32_MAX
253
+ // threshold (and the soundness contract with type.js exprType) lives in widen.js.
205
254
  const mulFitsI32 = (va, vb) =>
206
255
  (isLit(va) && Math.abs(litVal(va)) <= FITS_I32_MAX) ||
207
256
  (isLit(vb) && Math.abs(litVal(vb)) <= FITS_I32_MAX) ||
208
257
  (!isLit(va) && maskBound(va) <= FITS_I32_MAX) ||
209
258
  (!isLit(vb) && maskBound(vb) <= FITS_I32_MAX)
210
259
 
260
+ // Max |value| of an i32-typed operand from a narrowing typed-array load width — the
261
+ // element-read twin of maskBound's `x & 0xff` case (load8_u and `x & 0xff` carry the
262
+ // SAME [0,255] range). Infinity when the magnitude is unbounded. Signed loads reach
263
+ // −2^(w−1), so the magnitude bound is 2^(w−1).
264
+ const I32_LOAD_MAG = { 'i32.load8_s': 128, 'i32.load8_u': 255, 'i32.load16_s': 32768, 'i32.load16_u': 65535 }
265
+ const i32Mag = (v) =>
266
+ !Array.isArray(v) ? Infinity :
267
+ v[0] in I32_LOAD_MAG ? I32_LOAD_MAG[v[0]] :
268
+ (v[0] === 'i32.const' && typeof v[1] === 'number') ? Math.abs(v[1]) :
269
+ (v[0] === 'i32.and' || v[0] === 'i32.shr_u') ? maskBound(v) :
270
+ Infinity
271
+ // `int8[i]*int8[j]` and friends: a product of two range-bounded integer typed-array
272
+ // elements whose magnitudes multiply to ≤ 2^31−1 is FAITHFUL as i32.mul — the exact
273
+ // product fits signed i32, so i32.mul == the true value in EVERY consumer context
274
+ // (i32 sink AND f64 value), independent of the widen pass. Covers i8/u8/i16 pairs and
275
+ // i16×u16 (32768·65535 < 2^31); correctly EXCLUDES u16×u16 (65535² > 2^31). JS `*` of
276
+ // two such reads — the int-conv / correlation / quantised-MAC kernel shape — then rides
277
+ // the i32 ABI (one op, no f64 round-trip) on V8 / JSC / wasmtime alike, and the i32
278
+ // product is lane-vectorizable where the f64 form was not.
279
+ const mulBoundedFaithful = (va, vb) => i32Mag(va) * i32Mag(vb) <= 0x7fffffff
280
+
211
281
  /** Emit typeof comparison: typeof x == typeCode → type-aware check. */
212
282
  export function emitTypeofCmp(a, b, cmpOp) {
213
283
  let typeofExpr, code
@@ -372,6 +442,27 @@ function tryI32Index(e) {
372
442
  }
373
443
  export const emitIndex = (idx) => tryI32Index(idx) ?? asI32(emit(idx))
374
444
 
445
+ /**
446
+ * True when `e` is a pure integer `+`/`-`/`*` tree whose leaves are all i32-typed
447
+ * names/globals or integer literals — no calls, member reads, or indexed reads, so
448
+ * emitting it twice (or in a different rep) is side-effect-free. Used to recognise
449
+ * an i32-local initializer that `tryI32Index` can lower to native wrapping i32
450
+ * arithmetic instead of the f64 round-trip (`convert … f64.mul/add … trunc_sat`).
451
+ * The same residue-mod-2^32 argument as `tryI32Index`: ToInt32 of the exact integer
452
+ * value equals two's-complement wrapping i32, so for an i32 destination the two are
453
+ * bit-identical — even when an intermediate product overflows.
454
+ */
455
+ function isI32ArithTree(e) {
456
+ if (typeof e === 'number') return Number.isInteger(e)
457
+ if (typeof e === 'string') return exprType(e, ctx.func.locals) === 'i32'
458
+ if (!Array.isArray(e)) return false
459
+ const op = e[0]
460
+ if (op == null) return isI32ArithTree(e[1]) // literal wrapper [, v]
461
+ if ((op === '+' || op === '-' || op === '*') && e[2] != null)
462
+ return isI32ArithTree(e[1]) && isI32ArithTree(e[2])
463
+ return false
464
+ }
465
+
375
466
  function emitSingleCharIndexCmp(a, b, negate = false) {
376
467
  const leftLit = stringLiteral(a)
377
468
  const rightLit = stringLiteral(b)
@@ -1080,6 +1171,12 @@ export function emitDecl(...inits) {
1080
1171
  updateRep(name, { ptrAux: val.closureFuncIdx })
1081
1172
  coerced = val.ptrKind === ptrKind ? val
1082
1173
  : typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
1174
+ } else if (localType === 'i32' && val.type !== 'i32' && isI32ArithTree(init)) {
1175
+ // Integer index feeder (`let idx = py*W + qx`) bound to an i32 local: compute
1176
+ // it in native wrapping i32 instead of the f64 round-trip + trunc_sat. Bit-
1177
+ // identical for an i32 destination (ToInt32 ≡ two's-complement wrap), and the
1178
+ // i32.mul is hoistable when loop-invariant. Falls back to toI32 defensively.
1179
+ coerced = tryI32Index(init) ?? toI32(val)
1083
1180
  } else {
1084
1181
  coerced = localType === 'v128' ? val : localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
1085
1182
  }
@@ -1136,12 +1233,22 @@ function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
1136
1233
  const sidx = `${T}sidx${ctx.func.uniq++}`
1137
1234
  ctx.func.locals.set(sidx, 'i32')
1138
1235
  const loopId = ctx.func.uniq++
1139
- const elem = ctx.module.modules['string']
1140
- ? ['if', ['result', 'f64'],
1236
+ // When the source is statically known to be a typed array, __typed_idx suffices.
1237
+ // Otherwise (STRING, or unknown type whose runtime value may be a string) dispatch on
1238
+ // ptr_type: STRING→__str_idx, else→__typed_idx.
1239
+ // The old gate (ctx.module.modules['string']) was wrong: for `[...s]` with an untyped
1240
+ // param the string module is never loaded, so __typed_idx was used for strings —
1241
+ // __typed_idx calls __len which returns 0 for strings, making i>=len always true and
1242
+ // storing UNDEF into every element slot. Pull in the string module here so __str_idx
1243
+ // is registered before inc() adds it to the dependency set.
1244
+ const elem = staticVT === VAL.TYPED
1245
+ ? (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
1246
+ : (includeForStringOnly(),
1247
+ ['if', ['result', 'f64'],
1141
1248
  ['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
1142
1249
  ['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
1143
- ['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]]
1144
- : (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
1250
+ ['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]
1251
+ ])
1145
1252
  // Reset the counter on each entry — WASM zeroes locals once at function
1146
1253
  // entry, but this loop re-executes when the spread sits inside a JS loop;
1147
1254
  // a stale `sidx` (= prior srcLen) would skip the copy entirely.
@@ -1243,8 +1350,25 @@ export function buildArrayWithSpreads(items) {
1243
1350
  // emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
1244
1351
  sec.val = n ? undefined : valTypeOf(srcExpr)
1245
1352
  ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(srcExpr))])
1246
- // Cache __len once per spread; reused below for total-len sum and the copy.
1247
- ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
1353
+ // Cache the source length once per spread (reused for the total-len sum and the
1354
+ // copy). `__len` is ARRAY/typed length — WRONG for a STRING (returns 0, so `[...str]`
1355
+ // spreads an empty array). Pick the length to MATCH emitSpreadCopy's element decode:
1356
+ // a known string counts chars (__str_len, paired with the __str_idx per-char copy); a
1357
+ // statically-unknown source — `[...x]` / `[...fnParam]`, the compiler's own
1358
+ // `[...key]` — dispatches once at runtime (STRING→__str_len, else→__len), mirroring
1359
+ // emitSpreadCopy's ARRAY-vs-scalar branch. (Not __length: its `off>=8` guard returns
1360
+ // undefined for host/static typed arrays.) Known array/typed/multi keep plain __len.
1361
+ const srcI64 = () => ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]
1362
+ const lenIR = sec.val === VAL.STRING
1363
+ ? (inc('__str_len'), ['call', '$__str_len', srcI64()])
1364
+ : (sec.val === VAL.ARRAY || sec.val === VAL.TYPED || n)
1365
+ ? (inc('__len'), ['call', '$__len', srcI64()])
1366
+ : (inc('__str_len', '__len', '__ptr_type'),
1367
+ ['if', ['result', 'i32'],
1368
+ ['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
1369
+ ['then', ['call', '$__str_len', srcI64()]],
1370
+ ['else', ['call', '$__len', srcI64()]]])
1371
+ ir.push(['local.set', `$${sec.lenLocal}`, lenIR])
1248
1372
  }
1249
1373
  }
1250
1374
 
@@ -1387,6 +1511,48 @@ const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
1387
1511
  const nullableOperand = (n) =>
1388
1512
  typeof n === 'string' && !!(repOf(n)?.nullable || repOfGlobal(n)?.nullable)
1389
1513
 
1514
+ // An emitted value whose bit pattern is an i32, paired with how it widens to f64: a
1515
+ // `f64.convert_i32_s/u(x)` peels to its i32 source `x`; a bare i32 widens signed. Used to compare
1516
+ // two integer-backed operands directly in i32 instead of widening both to f64.
1517
+ const peelIntCmp = (v) => {
1518
+ if (Array.isArray(v) && (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u'))
1519
+ return { src: Array.isArray(v[1]) ? typed(v[1], 'i32') : v[1], sign: v[0] === 'f64.convert_i32_u' ? 'u' : 's' }
1520
+ if (v && v.type === 'i32') return { src: v, sign: 's' }
1521
+ return null
1522
+ }
1523
+ // The value's top bit is provably 0 (so its signed and unsigned readings agree): a u8/u16 load,
1524
+ // `>>>` (always clears the sign bit), `& m` with m a non-negative small const, or a small const.
1525
+ const i32TopBitClear = (n) => {
1526
+ if (typeof n === 'number') return n >= 0 && n < 0x80000000
1527
+ if (!Array.isArray(n)) return false
1528
+ if (n[0] == null) return typeof n[1] === 'number' && n[1] >= 0 && n[1] < 0x80000000
1529
+ if (n[0] === 'i32.load8_u' || n[0] === 'i32.load16_u') return true
1530
+ if (n[0] === 'i32.const') return typeof n[1] === 'number' ? (n[1] >= 0 && n[1] < 0x80000000) : false
1531
+ if (n[0] === 'i32.shr_u' || n[0] === '>>>') return true
1532
+ if (n[0] === 'i32.and' || n[0] === '&') return i32TopBitClear(n[1]) || i32TopBitClear(n[2])
1533
+ return false
1534
+ }
1535
+ // i32.eq/ne over the peeled sources equals the f64-widened compare when the signs match, or — for
1536
+ // a mixed signed/unsigned pair — when the unsigned-read source is top-bit-clear (then both readings
1537
+ // of equal bits agree, and unequal bits stay unequal under both).
1538
+ const i32EqSound = (pa, pb) => pa.sign === pb.sign ||
1539
+ i32TopBitClear((pa.sign === 'u' ? pa : pb).src)
1540
+
1541
+ // A memory-free, trap-free, side-effect-free expression — safe to evaluate UNCONDITIONALLY (as a
1542
+ // `select` arm does) and cheap enough that doing so never loses to a branch. Locals/consts and
1543
+ // arithmetic/bitwise/compare/logical over them. Excludes loads (`[]`, may read OOB when the guard
1544
+ // was protecting the access), calls, `.`/`?.` (dispatch), `/` `%` (int trap on 0), assignments.
1545
+ const CHEAP_PURE_OPS = new Set(['+', '-', '*', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>', '~',
1546
+ '<', '<=', '>', '>=', '==', '!=', '===', '!==', '&&', '||', '!', '?:'])
1547
+ const isCheapPureVal = (n) => {
1548
+ if (typeof n === 'string' || typeof n === 'number') return true
1549
+ if (!Array.isArray(n)) return false
1550
+ if (n[0] == null) return true // boxed literal [, v]
1551
+ if (n[0] === 'local.get') return true
1552
+ if (CHEAP_PURE_OPS.has(n[0])) { for (let i = 1; i < n.length; i++) if (!isCheapPureVal(n[i])) return false; return true }
1553
+ return false
1554
+ }
1555
+
1390
1556
  function emitLooseEq(a, b, negate) {
1391
1557
  const eqOp = negate ? 'ne' : 'eq'
1392
1558
  const sentinel = emitNum(negate ? 1 : 0)
@@ -1405,6 +1571,12 @@ function emitLooseEq(a, b, negate) {
1405
1571
  const tc = emitTypeofCmp(a, b, eqOp); if (tc) return tc
1406
1572
  const va = emit(a), vb = emit(b)
1407
1573
  if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${eqOp}`, va, vb], 'i32')
1574
+ // Both operands integer-backed (e.g. an i32 local vs a `b[j]` u8 read materialized as f64):
1575
+ // compare the i32 sources directly, skipping the per-op widen to f64. Recovers `intElem ===
1576
+ // intElem` in hot loops (levenshtein's DP cell, where `a[i-1] === b[j-1]` was an f64.eq + 2
1577
+ // converts every iteration). Sound only when the widen can't change the answer (see i32EqSound).
1578
+ const pa = peelIntCmp(va), pb = peelIntCmp(vb)
1579
+ if (pa && pb && i32EqSound(pa, pb)) return typed([`i32.${eqOp}`, pa.src, pb.src], 'i32')
1408
1580
  // Either side known-pure NUMBER (literal or typed) → f64.eq/ne is correct regardless
1409
1581
  // of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
1410
1582
  // reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
@@ -1425,6 +1597,43 @@ function emitLooseEq(a, b, negate) {
1425
1597
  if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1426
1598
  return typed([`i64.${eqOp}`, ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1427
1599
  }
1600
+ // String-equality specialization — the hot `node[0] === 'literal'` AST-tag dispatch,
1601
+ // the compiler's single most-emitted comparison (5579 of its 6487 __eq sites). When one
1602
+ // side is statically a STRING, skip the generic __eq NaN-box dispatch (the #1 self-host
1603
+ // hot helper). jz's ==/=== never coerce (number-vs-string is false in __eq), so this is
1604
+ // sound for both. Two shapes by what the OTHER side is known to be:
1605
+ // both STRING → __str_eq directly (no number/NaN/tag test needed at all).
1606
+ // STRING vs unknown → i64.eq fast ? equal : (__is_str_key(u) ? __str_eq : not-equal).
1607
+ // Soundness of the fast path: the known string is a non-NaN STRING NaN-box, so a bit
1608
+ // match can ONLY be that same string (a normal f64 can't alias those bits). On bit
1609
+ // MISMATCH the unknown can still content-match — a heap string from `'i'+'f'` shares
1610
+ // content but not bits — so the fallback __str_eq stays (pure i64.eq is unsound here).
1611
+ // __is_str_key rejects the number-whose-bits-alias-the-STRING-tag case that a bare
1612
+ // __ptr_type would misroute into a wild __str_eq deref (see __eq's own guard).
1613
+ // INLINED (not a helper call): a single $__str_eq_lit helper measured 2.4% slower on
1614
+ // the corpus — V8 keeps the call at the hot miss path; inlining lets the optimizer fold
1615
+ // __is_str_key/__str_eq's prefix in, which is where the tag dispatch spends its time.
1616
+ // Behaviorally identical to __eq when one side is a string — proven by a 4584-case
1617
+ // spec-on/spec-off differential (zero divergence at optimize 0 and 2).
1618
+ const strEqResult = (r) => negate ? typed(['i32.eqz', r], 'i32') : r
1619
+ const aStr = rawA === VAL.STRING, bStr = rawB === VAL.STRING
1620
+ if (aStr && bStr) {
1621
+ inc('__str_eq')
1622
+ return strEqResult(typed(['call', '$__str_eq', asI64(va), asI64(vb)], 'i32'))
1623
+ }
1624
+ if ((bStr && rawA == null) || (aStr && rawB == null)) {
1625
+ const uVal = bStr ? va : vb, lVal = bStr ? vb : va // u: unknown side, l: known string
1626
+ inc('__is_str_key', '__str_eq')
1627
+ const u = tempI64('seq'), l = tempI64('seq'), uG = ['local.get', `$${u}`], lG = ['local.get', `$${l}`]
1628
+ return strEqResult(typed(['block', ['result', 'i32'],
1629
+ ['local.set', `$${u}`, asI64(uVal)],
1630
+ ['local.set', `$${l}`, asI64(lVal)],
1631
+ ['if', ['result', 'i32'], ['i64.eq', uG, lG],
1632
+ ['then', ['i32.const', 1]],
1633
+ ['else', ['if', ['result', 'i32'], ['call', '$__is_str_key', uG],
1634
+ ['then', ['call', '$__str_eq', uG, lG]],
1635
+ ['else', ['i32.const', 0]]]]]], 'i32'))
1636
+ }
1428
1637
  inc('__eq')
1429
1638
  const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
1430
1639
  return negate ? typed(['i32.eqz', call], 'i32') : call
@@ -1696,6 +1905,13 @@ function emitSpreadElementLoop(spreadExpr, bodyFn, { reverse = false } = {}) {
1696
1905
  ]
1697
1906
  }
1698
1907
 
1908
+ function emitAsValue(fn) {
1909
+ const prev = ctx.func._expect
1910
+ ctx.func._expect = null
1911
+ try { return fn() }
1912
+ finally { ctx.func._expect = prev }
1913
+ }
1914
+
1699
1915
  function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
1700
1916
  const inPlace = SPREAD_MUTATORS.has(method)
1701
1917
  // unshift prepends each arg to the front — forward iteration reverses intent.
@@ -1704,11 +1920,11 @@ function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
1704
1920
  ctx.func.locals.set(acc, 'f64')
1705
1921
  const ir = [['local.set', `$${acc}`, asF64(emit(objArg))]]
1706
1922
  if (parsed.normal.length > 0) {
1707
- const r = asF64(methodEmitter(objArg, ...parsed.normal))
1923
+ const r = asF64(emitAsValue(() => methodEmitter(objArg, ...parsed.normal)))
1708
1924
  ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
1709
1925
  }
1710
1926
  ir.push(...emitSpreadElementLoop(parsed.spreads[0].expr, (arr, idx) => {
1711
- const body = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
1927
+ const body = asF64(emitAsValue(() => methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx])))
1712
1928
  return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
1713
1929
  }, { reverse }))
1714
1930
  ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
@@ -1730,7 +1946,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
1730
1946
  let batch = []
1731
1947
  const flushBatch = () => {
1732
1948
  if (!batch.length) return
1733
- const r = asF64(methodEmitter(recv, ...batch))
1949
+ const r = asF64(emitAsValue(() => methodEmitter(recv, ...batch)))
1734
1950
  ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
1735
1951
  batch = []
1736
1952
  }
@@ -1738,7 +1954,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
1738
1954
  if (Array.isArray(item) && item[0] === '__spread') {
1739
1955
  flushBatch()
1740
1956
  ir.push(...emitSpreadElementLoop(item[1], (arr, idx) => {
1741
- const body = asF64(methodEmitter(recv, ['[]', arr, idx]))
1957
+ const body = asF64(emitAsValue(() => methodEmitter(recv, ['[]', arr, idx])))
1742
1958
  return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
1743
1959
  }))
1744
1960
  } else {
@@ -1815,7 +2031,7 @@ function tryCharCodeAtFast(callee, obj, method, parsed) {
1815
2031
  return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
1816
2032
  }
1817
2033
  return typed(stringOps(obj).charCodeAt(
1818
- asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
2034
+ asF64(recv), asI32(emit(parsed.normal[0])), ctx, false, true), 'i32')
1819
2035
  }
1820
2036
  }
1821
2037
 
@@ -2661,6 +2877,7 @@ export const emitter = {
2661
2877
  // op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
2662
2878
  // Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
2663
2879
  if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
2880
+ const i32add = tryI32Arith('i32.add', '+', a, b, va, vb); if (i32add) return i32add
2664
2881
  return typed(['f64.add', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
2665
2882
  },
2666
2883
  '-': (a, b) => {
@@ -2676,6 +2893,7 @@ export const emitter = {
2676
2893
  // Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
2677
2894
  // so avoid the wrapping i32.sub fast-path. See `+` above.
2678
2895
  if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
2896
+ const i32sub = tryI32Arith('i32.sub', '-', a, b, va, vb); if (i32sub) return i32sub
2679
2897
  return typed(['f64.sub', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
2680
2898
  },
2681
2899
  'u+': a => {
@@ -2705,7 +2923,8 @@ export const emitter = {
2705
2923
  if (isLit(va) && litVal(va) === 0 && finiteFactor(vb)) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
2706
2924
  // `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
2707
2925
  // `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
2708
- if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
2926
+ if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && (mulFitsI32(va, vb) || mulBoundedFaithful(va, vb))) return typed(['i32.mul', va, vb], 'i32')
2927
+ const i32mul = tryI32Arith('i32.mul', '*', a, b, va, vb); if (i32mul) return i32mul
2709
2928
  return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
2710
2929
  },
2711
2930
  '/': (a, b) => {
@@ -3075,6 +3294,17 @@ export const emitter = {
3075
3294
  if (els != null) return emitVoid(els)
3076
3295
  return null
3077
3296
  }
3297
+ // If-conversion (speed tier): `if (cond) x = <cheap pure value>` (no else) → `x = cond ? value
3298
+ // : x`, which lowers to a branchless `select`. Removes the data-dependent branch (and its
3299
+ // misprediction) from min/max/clamp reductions — e.g. levenshtein's `if (ins < m) m = ins`,
3300
+ // ~27% faster. Gated to the same speed tier as boolConvertToSelect (the select latency/size
3301
+ // trade). Restricted to a plain assignment of a memory-/trap-free expr to a simple local, so
3302
+ // the unconditional false-case eval is free and identical in effect.
3303
+ if (els == null && ctx.transform.optimize?.boolConvertToSelect && isCheapPureVal(cond)) {
3304
+ const asg = Array.isArray(then) && then[0] === ';' && then.length === 2 ? then[1] : then
3305
+ if (Array.isArray(asg) && asg[0] === '=' && typeof asg[1] === 'string' && isCheapPureVal(asg[2]))
3306
+ return emitVoid(['=', asg[1], ['?:', cond, asg[2], asg[1]]]) // cond cheap-pure → re-emit is free
3307
+ }
3078
3308
  const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
3079
3309
  // Flow-sensitive type refinement: narrow types within each branch based on the guard.
3080
3310
  const thenRefs = extractRefinements(cond, new Map(), true)