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.
- package/README.md +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1156 -984
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +29 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +26 -3
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +162 -156
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +253 -23
- package/src/compile/index.js +198 -61
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +169 -32
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +960 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1292 -144
- package/src/prepare/index.js +11 -7
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/src/compile/emit.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
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
|
|
1247
|
-
|
|
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)
|