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.
- 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 +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -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 +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- 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 +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- 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 +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- 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,83 @@ 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
|
+
|
|
1556
|
+
// Side-effect-free: no writes (assignment / ++ / --), no calls, no closures, no throw. UNLIKE
|
|
1557
|
+
// `isCheapPureVal` this ALLOWS loads, member reads, and `/` `%` — a side-effect-free expr may read
|
|
1558
|
+
// memory or trap. It is the right gate for an `if` CONDITION promoted to a `select` condition: the
|
|
1559
|
+
// condition is evaluated exactly once whether the lowering branches or selects (any trap fires the
|
|
1560
|
+
// same in both, the read order vs the pure value arm is immaterial), so it need only avoid MUTATING
|
|
1561
|
+
// state the value arm could read — i.e. be side-effect-free, not unconditionally-evaluable.
|
|
1562
|
+
const SIDE_EFFECT_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=',
|
|
1563
|
+
'>>>=', '||=', '&&=', '??=', '++', '--', '()', '=>', 'throw', 'new', 'await', 'yield'])
|
|
1564
|
+
const isSideEffectFree = (n) => {
|
|
1565
|
+
if (!Array.isArray(n)) return true
|
|
1566
|
+
if (typeof n[0] === 'string' && SIDE_EFFECT_OPS.has(n[0])) return false
|
|
1567
|
+
for (let i = 1; i < n.length; i++) if (!isSideEffectFree(n[i])) return false
|
|
1568
|
+
return true
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const isLit1 = (n) => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
1572
|
+
// A void statement whose whole effect is `x = <cheap pure value>` for a simple local `x` — the
|
|
1573
|
+
// shape if→select can lower to `x = cond ? value : x`. Recognizes the plain assignment plus the
|
|
1574
|
+
// increment forms `++x`/`--x` and their postfix lowerings `(++x) - 1` / `(--x) + 1` (prepare turns
|
|
1575
|
+
// `x++` in statement position into the latter; the discarded ∓1 is dead in void context, so the
|
|
1576
|
+
// net effect is the increment). Returns `{ lhs, val }` or null.
|
|
1577
|
+
function matchVoidLocalStore(s) {
|
|
1578
|
+
if (!Array.isArray(s)) return null
|
|
1579
|
+
if (s[0] === '=' && typeof s[1] === 'string' && isCheapPureVal(s[2])) return { lhs: s[1], val: s[2] }
|
|
1580
|
+
if ((s[0] === '++' || s[0] === '--') && typeof s[1] === 'string')
|
|
1581
|
+
return { lhs: s[1], val: [s[0] === '++' ? '+' : '-', s[1], [, 1]] }
|
|
1582
|
+
// postfix: `x++` → `(++x) - 1`, `x--` → `(--x) + 1`
|
|
1583
|
+
if ((s[0] === '-' || s[0] === '+') && isLit1(s[2]) && Array.isArray(s[1])
|
|
1584
|
+
&& (s[1][0] === '++' || s[1][0] === '--') && typeof s[1][1] === 'string') {
|
|
1585
|
+
const inc = s[1][0] === '++'
|
|
1586
|
+
if ((inc && s[0] === '-') || (!inc && s[0] === '+')) return { lhs: s[1][1], val: [inc ? '+' : '-', s[1][1], [, 1]] }
|
|
1587
|
+
}
|
|
1588
|
+
return null
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1390
1591
|
function emitLooseEq(a, b, negate) {
|
|
1391
1592
|
const eqOp = negate ? 'ne' : 'eq'
|
|
1392
1593
|
const sentinel = emitNum(negate ? 1 : 0)
|
|
@@ -1405,6 +1606,12 @@ function emitLooseEq(a, b, negate) {
|
|
|
1405
1606
|
const tc = emitTypeofCmp(a, b, eqOp); if (tc) return tc
|
|
1406
1607
|
const va = emit(a), vb = emit(b)
|
|
1407
1608
|
if (va.type === 'i32' && vb.type === 'i32') return typed([`i32.${eqOp}`, va, vb], 'i32')
|
|
1609
|
+
// Both operands integer-backed (e.g. an i32 local vs a `b[j]` u8 read materialized as f64):
|
|
1610
|
+
// compare the i32 sources directly, skipping the per-op widen to f64. Recovers `intElem ===
|
|
1611
|
+
// intElem` in hot loops (levenshtein's DP cell, where `a[i-1] === b[j-1]` was an f64.eq + 2
|
|
1612
|
+
// converts every iteration). Sound only when the widen can't change the answer (see i32EqSound).
|
|
1613
|
+
const pa = peelIntCmp(va), pb = peelIntCmp(vb)
|
|
1614
|
+
if (pa && pb && i32EqSound(pa, pb)) return typed([`i32.${eqOp}`, pa.src, pb.src], 'i32')
|
|
1408
1615
|
// Either side known-pure NUMBER (literal or typed) → f64.eq/ne is correct regardless
|
|
1409
1616
|
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1410
1617
|
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
@@ -1425,6 +1632,43 @@ function emitLooseEq(a, b, negate) {
|
|
|
1425
1632
|
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1426
1633
|
return typed([`i64.${eqOp}`, ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1427
1634
|
}
|
|
1635
|
+
// String-equality specialization — the hot `node[0] === 'literal'` AST-tag dispatch,
|
|
1636
|
+
// the compiler's single most-emitted comparison (5579 of its 6487 __eq sites). When one
|
|
1637
|
+
// side is statically a STRING, skip the generic __eq NaN-box dispatch (the #1 self-host
|
|
1638
|
+
// hot helper). jz's ==/=== never coerce (number-vs-string is false in __eq), so this is
|
|
1639
|
+
// sound for both. Two shapes by what the OTHER side is known to be:
|
|
1640
|
+
// both STRING → __str_eq directly (no number/NaN/tag test needed at all).
|
|
1641
|
+
// STRING vs unknown → i64.eq fast ? equal : (__is_str_key(u) ? __str_eq : not-equal).
|
|
1642
|
+
// Soundness of the fast path: the known string is a non-NaN STRING NaN-box, so a bit
|
|
1643
|
+
// match can ONLY be that same string (a normal f64 can't alias those bits). On bit
|
|
1644
|
+
// MISMATCH the unknown can still content-match — a heap string from `'i'+'f'` shares
|
|
1645
|
+
// content but not bits — so the fallback __str_eq stays (pure i64.eq is unsound here).
|
|
1646
|
+
// __is_str_key rejects the number-whose-bits-alias-the-STRING-tag case that a bare
|
|
1647
|
+
// __ptr_type would misroute into a wild __str_eq deref (see __eq's own guard).
|
|
1648
|
+
// INLINED (not a helper call): a single $__str_eq_lit helper measured 2.4% slower on
|
|
1649
|
+
// the corpus — V8 keeps the call at the hot miss path; inlining lets the optimizer fold
|
|
1650
|
+
// __is_str_key/__str_eq's prefix in, which is where the tag dispatch spends its time.
|
|
1651
|
+
// Behaviorally identical to __eq when one side is a string — proven by a 4584-case
|
|
1652
|
+
// spec-on/spec-off differential (zero divergence at optimize 0 and 2).
|
|
1653
|
+
const strEqResult = (r) => negate ? typed(['i32.eqz', r], 'i32') : r
|
|
1654
|
+
const aStr = rawA === VAL.STRING, bStr = rawB === VAL.STRING
|
|
1655
|
+
if (aStr && bStr) {
|
|
1656
|
+
inc('__str_eq')
|
|
1657
|
+
return strEqResult(typed(['call', '$__str_eq', asI64(va), asI64(vb)], 'i32'))
|
|
1658
|
+
}
|
|
1659
|
+
if ((bStr && rawA == null) || (aStr && rawB == null)) {
|
|
1660
|
+
const uVal = bStr ? va : vb, lVal = bStr ? vb : va // u: unknown side, l: known string
|
|
1661
|
+
inc('__is_str_key', '__str_eq')
|
|
1662
|
+
const u = tempI64('seq'), l = tempI64('seq'), uG = ['local.get', `$${u}`], lG = ['local.get', `$${l}`]
|
|
1663
|
+
return strEqResult(typed(['block', ['result', 'i32'],
|
|
1664
|
+
['local.set', `$${u}`, asI64(uVal)],
|
|
1665
|
+
['local.set', `$${l}`, asI64(lVal)],
|
|
1666
|
+
['if', ['result', 'i32'], ['i64.eq', uG, lG],
|
|
1667
|
+
['then', ['i32.const', 1]],
|
|
1668
|
+
['else', ['if', ['result', 'i32'], ['call', '$__is_str_key', uG],
|
|
1669
|
+
['then', ['call', '$__str_eq', uG, lG]],
|
|
1670
|
+
['else', ['i32.const', 0]]]]]], 'i32'))
|
|
1671
|
+
}
|
|
1428
1672
|
inc('__eq')
|
|
1429
1673
|
const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
1430
1674
|
return negate ? typed(['i32.eqz', call], 'i32') : call
|
|
@@ -1696,6 +1940,13 @@ function emitSpreadElementLoop(spreadExpr, bodyFn, { reverse = false } = {}) {
|
|
|
1696
1940
|
]
|
|
1697
1941
|
}
|
|
1698
1942
|
|
|
1943
|
+
function emitAsValue(fn) {
|
|
1944
|
+
const prev = ctx.func._expect
|
|
1945
|
+
ctx.func._expect = null
|
|
1946
|
+
try { return fn() }
|
|
1947
|
+
finally { ctx.func._expect = prev }
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1699
1950
|
function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
1700
1951
|
const inPlace = SPREAD_MUTATORS.has(method)
|
|
1701
1952
|
// unshift prepends each arg to the front — forward iteration reverses intent.
|
|
@@ -1704,11 +1955,11 @@ function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1704
1955
|
ctx.func.locals.set(acc, 'f64')
|
|
1705
1956
|
const ir = [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
1706
1957
|
if (parsed.normal.length > 0) {
|
|
1707
|
-
const r = asF64(methodEmitter(objArg, ...parsed.normal))
|
|
1958
|
+
const r = asF64(emitAsValue(() => methodEmitter(objArg, ...parsed.normal)))
|
|
1708
1959
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1709
1960
|
}
|
|
1710
1961
|
ir.push(...emitSpreadElementLoop(parsed.spreads[0].expr, (arr, idx) => {
|
|
1711
|
-
const body = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
|
|
1962
|
+
const body = asF64(emitAsValue(() => methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx])))
|
|
1712
1963
|
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1713
1964
|
}, { reverse }))
|
|
1714
1965
|
ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
|
|
@@ -1730,7 +1981,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1730
1981
|
let batch = []
|
|
1731
1982
|
const flushBatch = () => {
|
|
1732
1983
|
if (!batch.length) return
|
|
1733
|
-
const r = asF64(methodEmitter(recv, ...batch))
|
|
1984
|
+
const r = asF64(emitAsValue(() => methodEmitter(recv, ...batch)))
|
|
1734
1985
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1735
1986
|
batch = []
|
|
1736
1987
|
}
|
|
@@ -1738,7 +1989,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1738
1989
|
if (Array.isArray(item) && item[0] === '__spread') {
|
|
1739
1990
|
flushBatch()
|
|
1740
1991
|
ir.push(...emitSpreadElementLoop(item[1], (arr, idx) => {
|
|
1741
|
-
const body = asF64(methodEmitter(recv, ['[]', arr, idx]))
|
|
1992
|
+
const body = asF64(emitAsValue(() => methodEmitter(recv, ['[]', arr, idx])))
|
|
1742
1993
|
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1743
1994
|
}))
|
|
1744
1995
|
} else {
|
|
@@ -1815,7 +2066,7 @@ function tryCharCodeAtFast(callee, obj, method, parsed) {
|
|
|
1815
2066
|
return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
|
|
1816
2067
|
}
|
|
1817
2068
|
return typed(stringOps(obj).charCodeAt(
|
|
1818
|
-
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
|
|
2069
|
+
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false, true), 'i32')
|
|
1819
2070
|
}
|
|
1820
2071
|
}
|
|
1821
2072
|
|
|
@@ -2497,7 +2748,15 @@ export const emitter = {
|
|
|
2497
2748
|
inc('__to_num')
|
|
2498
2749
|
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
2499
2750
|
}
|
|
2500
|
-
|
|
2751
|
+
// Self-accumulation `x = x + …` (incl. desugared `x += …`): the new value REPLACES x, so x's
|
|
2752
|
+
// old buffer is dead — the one context where a string concat may bump-EXTEND it in place. The
|
|
2753
|
+
// `+` handler reads this flag for its immediate concat; nested operands clear it (not the target).
|
|
2754
|
+
const selfAccum = Array.isArray(val) && val[0] === '+' && val[1] === name
|
|
2755
|
+
const prevSA = ctx.func._selfAccumConcat
|
|
2756
|
+
ctx.func._selfAccumConcat = selfAccum ? name : null
|
|
2757
|
+
const ev = emit(val)
|
|
2758
|
+
ctx.func._selfAccumConcat = prevSA
|
|
2759
|
+
return writeVar(name, ev, void_)
|
|
2501
2760
|
},
|
|
2502
2761
|
|
|
2503
2762
|
// Compound assignments: read-modify-write with type coercion
|
|
@@ -2587,15 +2846,18 @@ export const emitter = {
|
|
|
2587
2846
|
// Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
|
|
2588
2847
|
'+': (a, b) => {
|
|
2589
2848
|
if (ctx.func._expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
|
|
2849
|
+
// A self-accumulation `a = a + …` lets the concat bump-EXTEND `a` in place (a is dead-after).
|
|
2850
|
+
// Read it for THIS concat, then clear so nested operands (not the accumulation target) stay fresh.
|
|
2851
|
+
const selfAccum = typeof a === 'string' && a === ctx.func._selfAccumConcat
|
|
2852
|
+
ctx.func._selfAccumConcat = null
|
|
2590
2853
|
// String concatenation: pure string operands skip generic ToString coercion.
|
|
2591
2854
|
const vtA = valTypeOf(a)
|
|
2592
2855
|
const vtB = valTypeOf(b)
|
|
2593
2856
|
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
2594
|
-
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
2595
|
-
//
|
|
2596
|
-
//
|
|
2597
|
-
|
|
2598
|
-
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2857
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction + generic concat dispatch
|
|
2858
|
+
// when rhs is a string-index. The byte flows straight from __char_at into memory and bump-
|
|
2859
|
+
// EXTENDS the heap-top lhs — so only when proven self-accumulating (else it mutates a live s).
|
|
2860
|
+
if (selfAccum && Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2599
2861
|
if (valTypeOf(b[1]) === VAL.STRING) {
|
|
2600
2862
|
inc('__str_append_byte', '__char_at')
|
|
2601
2863
|
return typed(['call', '$__str_append_byte',
|
|
@@ -2604,7 +2866,7 @@ export const emitter = {
|
|
|
2604
2866
|
], 'f64')
|
|
2605
2867
|
}
|
|
2606
2868
|
}
|
|
2607
|
-
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
2869
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx, selfAccum), 'f64')
|
|
2608
2870
|
}
|
|
2609
2871
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
2610
2872
|
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
@@ -2624,10 +2886,10 @@ export const emitter = {
|
|
|
2624
2886
|
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2625
2887
|
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2626
2888
|
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2627
|
-
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2628
|
-
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2629
|
-
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2630
|
-
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2889
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2890
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx, selfAccum), 'f64')
|
|
2891
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2892
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2631
2893
|
}
|
|
2632
2894
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
2633
2895
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -2637,6 +2899,12 @@ export const emitter = {
|
|
|
2637
2899
|
// operand needs the runtime check.
|
|
2638
2900
|
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
2639
2901
|
const tA = temp('add'), tB = temp('add')
|
|
2902
|
+
// Fully-untyped `+`: the string arm is a runtime-guarded cold path that the engine reaches
|
|
2903
|
+
// only if BOTH operands are strings at runtime, so it keeps the bump-extend `__str_concat`
|
|
2904
|
+
// (its body stays out-of-line — folding it to the smaller _fresh twin would inline this
|
|
2905
|
+
// never-numeric branch into every hot integer loop). The demonstrated `t = s + "lit"` mutation
|
|
2906
|
+
// is a TYPED concat (handled by concatRaw above); a both-untyped self-mutation stays the
|
|
2907
|
+
// documented rare-aliasing tradeoff. Self-accumulation is still safe to extend.
|
|
2640
2908
|
inc('__str_concat', '__is_str_key')
|
|
2641
2909
|
const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
|
|
2642
2910
|
const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
|
|
@@ -2661,6 +2929,7 @@ export const emitter = {
|
|
|
2661
2929
|
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
2662
2930
|
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
2663
2931
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
2932
|
+
const i32add = tryI32Arith('i32.add', '+', a, b, va, vb); if (i32add) return i32add
|
|
2664
2933
|
return typed(['f64.add', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2665
2934
|
},
|
|
2666
2935
|
'-': (a, b) => {
|
|
@@ -2676,6 +2945,7 @@ export const emitter = {
|
|
|
2676
2945
|
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
2677
2946
|
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
2678
2947
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
2948
|
+
const i32sub = tryI32Arith('i32.sub', '-', a, b, va, vb); if (i32sub) return i32sub
|
|
2679
2949
|
return typed(['f64.sub', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2680
2950
|
},
|
|
2681
2951
|
'u+': a => {
|
|
@@ -2705,7 +2975,8 @@ export const emitter = {
|
|
|
2705
2975
|
if (isLit(va) && litVal(va) === 0 && finiteFactor(vb)) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
2706
2976
|
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2707
2977
|
// `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')
|
|
2978
|
+
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && (mulFitsI32(va, vb) || mulBoundedFaithful(va, vb))) return typed(['i32.mul', va, vb], 'i32')
|
|
2979
|
+
const i32mul = tryI32Arith('i32.mul', '*', a, b, va, vb); if (i32mul) return i32mul
|
|
2709
2980
|
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2710
2981
|
},
|
|
2711
2982
|
'/': (a, b) => {
|
|
@@ -3075,6 +3346,21 @@ export const emitter = {
|
|
|
3075
3346
|
if (els != null) return emitVoid(els)
|
|
3076
3347
|
return null
|
|
3077
3348
|
}
|
|
3349
|
+
// If-conversion (speed tier): `if (cond) x = <cheap pure value>` (no else) → `x = cond ? value
|
|
3350
|
+
// : x`, which lowers to a branchless `select`. Removes the data-dependent branch (and its
|
|
3351
|
+
// misprediction) from min/max/clamp reductions — e.g. levenshtein's `if (ins < m) m = ins`,
|
|
3352
|
+
// ~27% faster — and from heapsort's child pick `if (a[c] < a[c+1]) c++`, the canonical
|
|
3353
|
+
// unpredictable compare that costs jz on x86 (Cranelift/V8-x64 keep the branch; Binaryen, which
|
|
3354
|
+
// AS uses, selects it). The condition is evaluated exactly once whether we branch or select, so
|
|
3355
|
+
// it need only be SIDE-EFFECT-FREE (loads allowed — sort's `a[c] < a[c+1]`); only the assigned
|
|
3356
|
+
// VALUE is evaluated unconditionally, hence must be a cheap, trap-free pure expr. `x++`/`x--`
|
|
3357
|
+
// are admitted as `x = x ± 1`. The already-emitted condition `ce` is reused (`__emitted`), so a
|
|
3358
|
+
// load-bearing condition is not emitted twice.
|
|
3359
|
+
if (els == null && ctx.transform.optimize?.boolConvertToSelect && isSideEffectFree(cond)) {
|
|
3360
|
+
const asg = Array.isArray(then) && then[0] === ';' && then.length === 2 ? then[1] : then
|
|
3361
|
+
const sel = matchVoidLocalStore(asg)
|
|
3362
|
+
if (sel) return emitVoid(['=', sel.lhs, ['?:', ['__emitted', ce], sel.val, sel.lhs]])
|
|
3363
|
+
}
|
|
3078
3364
|
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
3079
3365
|
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
3080
3366
|
const thenRefs = extractRefinements(cond, new Map(), true)
|
|
@@ -3336,6 +3622,10 @@ export function emit(node, expect) {
|
|
|
3336
3622
|
if (node.loc != null) ctx.error.loc = node.loc
|
|
3337
3623
|
}
|
|
3338
3624
|
if (node == null) return null
|
|
3625
|
+
// Pre-emitted IR passthrough: `['__emitted', ir]` returns `ir` untouched. Lets a caller that
|
|
3626
|
+
// already emitted a subtree (e.g. the `if` handler's condition) splice it into an AST-shaped
|
|
3627
|
+
// re-emit (a `?:` for if→select conversion) without emitting it a second time.
|
|
3628
|
+
if (Array.isArray(node) && node[0] === '__emitted') return node[1]
|
|
3339
3629
|
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
3340
3630
|
// AST), but their working representation is the plain number 0/1 — identical
|
|
3341
3631
|
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|