jz 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/src/compile/emit.js
CHANGED
|
@@ -25,8 +25,10 @@ import {
|
|
|
25
25
|
commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
|
|
26
26
|
hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
|
|
27
27
|
} from '../ast.js'
|
|
28
|
-
import { ctx, err, inc, PTR } from '../ctx.js'
|
|
29
|
-
import {
|
|
28
|
+
import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
|
|
29
|
+
import { includeForStringOnly } from '../autoload.js'
|
|
30
|
+
import { FITS_I32_MAX } from '../widen.js'
|
|
31
|
+
import { nonNegIntLiteral, intLiteralValue, staticPropertyKey } from '../static.js'
|
|
30
32
|
import { findFreeVars } from './analyze.js'
|
|
31
33
|
import {
|
|
32
34
|
containsNestedClosure, containsNestedLoop, nestedSmallLoopBudget,
|
|
@@ -41,7 +43,7 @@ import {
|
|
|
41
43
|
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
42
44
|
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
43
45
|
mkPtrIR, ptrOffsetIR, ptrTypeIR, ptrTypeEq, dispatchByPtrType, sidecarOverride, valKindToPtr,
|
|
44
|
-
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64,
|
|
46
|
+
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64, maskBound,
|
|
45
47
|
truthyIR, toBoolFromEmitted, isPostfix,
|
|
46
48
|
isGlobal, isConst, usesDynProps, needsDynShadow,
|
|
47
49
|
temp, tempI32, tempI64, allocPtr,
|
|
@@ -76,7 +78,45 @@ 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
|
+
|
|
104
|
+
// f64 arithmetic that can MINT a sign-nondeterministic NaN (0/0, ∞−∞, 0·∞, x%0): on x86
|
|
105
|
+
// these are 0xFFF8…, on arm 0x7FF8…. sqrt/min/max/neg are NOT here — they canon at their
|
|
106
|
+
// own emit (math.js / unary `-`), so they reach canonNum already canonical.
|
|
107
|
+
const NAN_MINTING = new Set(['f64.div', 'f64.add', 'f64.sub', 'f64.mul'])
|
|
108
|
+
|
|
79
109
|
const canonNum = (node) => {
|
|
110
|
+
// Fold a possibly-non-canonical NaN to the canonical number-NaN before it reaches a
|
|
111
|
+
// bit-comparing consumer (__is_truthy / untyped === / typeof), which match the canonical
|
|
112
|
+
// NaN by bits and so misread x86's 0xFFF8 as truthy. ONLY an un-canon'd NaN-minting
|
|
113
|
+
// arithmetic op can carry such a value — literals, i32-conversions, opaque locals/calls
|
|
114
|
+
// (canonical by the canon-at-source invariant) and already-canon'd shapes don't — so
|
|
115
|
+
// skipping everything else keeps the size win. (The broken middle ground was
|
|
116
|
+
// `02873d0`'s `isNumericIR` skip, which dropped canon for f64.div too → x86 miscompile.)
|
|
117
|
+
const arith = Array.isArray(node) &&
|
|
118
|
+
(NAN_MINTING.has(node[0]) || (node[0] === 'call' && node[1] === '$__rem'))
|
|
119
|
+
if (!arith) return node
|
|
80
120
|
const t = temp('cn')
|
|
81
121
|
return typed(['block', ['result', 'f64'],
|
|
82
122
|
['local.set', `$${t}`, node],
|
|
@@ -86,6 +126,21 @@ const canonNum = (node) => {
|
|
|
86
126
|
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
87
127
|
}
|
|
88
128
|
|
|
129
|
+
// Is an emitted arm `v` (AST `node`) a plain NUMBER? The predicate the two-arm merges
|
|
130
|
+
// (?:, ??) share to decide canon: an i32 number, NUMBER-tagged IR, or a NUMBER
|
|
131
|
+
// value-type qualifies; a pointer/opaque arm does not. `vt` is the node's resolved
|
|
132
|
+
// value-type — pass it when already computed to avoid the re-resolve.
|
|
133
|
+
const isNumArm = (v, node, vt = resolveValType(node, valTypeOf, lookupValType)) =>
|
|
134
|
+
isI32Num(v) || v.valKind === VAL.NUMBER || vt === VAL.NUMBER
|
|
135
|
+
|
|
136
|
+
// One arm of a two-arm f64 merge (?:, ??, ||, &&) whose result may be bit-tested while
|
|
137
|
+
// untyped. Canon (canonNum, a no-op unless the arm is NaN-minting arithmetic) ONLY a
|
|
138
|
+
// LONE numeric arm: when both arms are numeric the merge is value-typed NUMBER and read
|
|
139
|
+
// NaN-by-value (no canon); when the other arm is opaque the result is untyped, so a
|
|
140
|
+
// non-canonical NaN here would be misread by __is_truthy — fold it. A pointer arm
|
|
141
|
+
// (isNum=false) is never touched (canon would destroy its NaN-box).
|
|
142
|
+
const canonArm = (f, isNum, otherNum) => isNum && !otherNum ? canonNum(f) : f
|
|
143
|
+
|
|
89
144
|
// Host globals auto-imported as `(import "env" "name" (global … i64))` when
|
|
90
145
|
// referenced as a value. Drained from ctx.core.hostGlobals at assembly.
|
|
91
146
|
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
@@ -98,6 +153,36 @@ const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'gl
|
|
|
98
153
|
// use — wrapping is exactly its intended semantics, so it stays on the i32 path.
|
|
99
154
|
const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
|
|
100
155
|
|
|
156
|
+
// Strip a redundant NaN-canon wrapper (math.js `canon`) from an operand that
|
|
157
|
+
// feeds a NaN-propagating f64 op. `f64.sqrt`/`min`/`max` mint a sign-nondeterministic
|
|
158
|
+
// NaN that math.js canon-izes so it can't be bit-confused with a NaN-boxed pointer in
|
|
159
|
+
// `===`/`typeof`. But when the result flows straight into `f64.add`/`sub`/`mul`/`div`,
|
|
160
|
+
// the consumer propagates that NaN identically and is itself canon-ized if IT escapes —
|
|
161
|
+
// so the inner per-op canon (local.set + select + f64.ne, ~3 ops) is dead on the
|
|
162
|
+
// critical path. This is THE gap that put sqrt-heavy kernels ~23% behind V8
|
|
163
|
+
// (julia/raymarcher/boids); stripping it makes them match native JS.
|
|
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
|
+
}
|
|
185
|
+
|
|
101
186
|
const FIRST_CLASS_UNARY_MATH = {
|
|
102
187
|
'math.abs': 'f64.abs',
|
|
103
188
|
'math.sqrt': 'f64.sqrt',
|
|
@@ -137,10 +222,17 @@ const emitNeg = (a) => {
|
|
|
137
222
|
// flipped NaN reads as a tagged value (truthy / not-NaN). Fold any NaN result back
|
|
138
223
|
// to canonical — the same invariant math.sqrt/min/max keep via `canon` (module/math.js).
|
|
139
224
|
const t = temp('ng')
|
|
140
|
-
|
|
141
|
-
|
|
225
|
+
const raw = ['f64.neg', toNumF64(a, v)]
|
|
226
|
+
const ir = typed(['block', ['result', 'f64'],
|
|
227
|
+
['local.set', `$${t}`, raw],
|
|
142
228
|
['select', ['f64.const', 'nan'], ['local.get', `$${t}`],
|
|
143
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
|
|
144
236
|
}
|
|
145
237
|
|
|
146
238
|
/** Try constant-folding binary arith: returns emitNum(result) or null. */
|
|
@@ -154,14 +246,37 @@ const foldConst = (va, vb, fn, guard) =>
|
|
|
154
246
|
|
|
155
247
|
// JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
|
|
156
248
|
// Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
|
|
157
|
-
// exact product stays f64-exact
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
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.
|
|
162
254
|
const mulFitsI32 = (va, vb) =>
|
|
163
|
-
(isLit(va) && Math.abs(litVal(va)) <=
|
|
164
|
-
(isLit(vb) && Math.abs(litVal(vb)) <=
|
|
255
|
+
(isLit(va) && Math.abs(litVal(va)) <= FITS_I32_MAX) ||
|
|
256
|
+
(isLit(vb) && Math.abs(litVal(vb)) <= FITS_I32_MAX) ||
|
|
257
|
+
(!isLit(va) && maskBound(va) <= FITS_I32_MAX) ||
|
|
258
|
+
(!isLit(vb) && maskBound(vb) <= FITS_I32_MAX)
|
|
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
|
|
165
280
|
|
|
166
281
|
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
167
282
|
export function emitTypeofCmp(a, b, cmpOp) {
|
|
@@ -307,6 +422,13 @@ function intIndexIR(key) {
|
|
|
307
422
|
*/
|
|
308
423
|
const I32_INDEX_OP = { '+': 'i32.add', '-': 'i32.sub', '*': 'i32.mul' }
|
|
309
424
|
function tryI32Index(e) {
|
|
425
|
+
// Integer literal first — a prepare-wrapped literal `[null, k]` (and a const-int
|
|
426
|
+
// name) is itself an Array, so the operator dispatch below would reject it and
|
|
427
|
+
// bail the WHOLE index to the f64 round-trip. The classic victim is the `+ 1` /
|
|
428
|
+
// `(j + 1)` of a bilinear/stencil gather (`a[(j+1)*W + i + 1]`): one literal leaf
|
|
429
|
+
// forced `convert_i32 … f64.mul/add … trunc_sat_f64_s` across every term.
|
|
430
|
+
const lit = nonNegIntLiteral(e)
|
|
431
|
+
if (lit != null) return typed(['i32.const', lit], 'i32')
|
|
310
432
|
if (Array.isArray(e)) {
|
|
311
433
|
const inner = I32_INDEX_OP[e[0]]
|
|
312
434
|
if (inner && e[2] != null) {
|
|
@@ -316,12 +438,31 @@ function tryI32Index(e) {
|
|
|
316
438
|
}
|
|
317
439
|
return null
|
|
318
440
|
}
|
|
319
|
-
const lit = nonNegIntLiteral(e)
|
|
320
|
-
if (lit != null) return ['i32.const', lit]
|
|
321
441
|
return exprType(e, ctx.func.locals) === 'i32' ? asI32(emit(e)) : null
|
|
322
442
|
}
|
|
323
443
|
export const emitIndex = (idx) => tryI32Index(idx) ?? asI32(emit(idx))
|
|
324
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
|
+
|
|
325
466
|
function emitSingleCharIndexCmp(a, b, negate = false) {
|
|
326
467
|
const leftLit = stringLiteral(a)
|
|
327
468
|
const rightLit = stringLiteral(b)
|
|
@@ -448,6 +589,55 @@ function emitSubstringEqCmp(a, b, negate = false) {
|
|
|
448
589
|
['i64.reinterpret_f64', ['local.get', `$${o}`]]])], 'i32')
|
|
449
590
|
}
|
|
450
591
|
|
|
592
|
+
// One half of a two-sided range test against a compile-time constant, normalized to
|
|
593
|
+
// an inclusive bound on a *local* `x`: `{ x, lo }` (x ≥ lo) or `{ x, hi }` (x ≤ hi).
|
|
594
|
+
// `>`/`<` fold to the inclusive neighbor; a const on either side is accepted. Returns
|
|
595
|
+
// null for anything else (so the caller leaves the expression untouched).
|
|
596
|
+
function rangeBound(n) {
|
|
597
|
+
if (!Array.isArray(n) || n.length !== 3) return null
|
|
598
|
+
const lc = intLiteralValue(n[1]), rc = intLiteralValue(n[2])
|
|
599
|
+
if (rc != null && typeof n[1] === 'string') { // x op CONST
|
|
600
|
+
if (n[0] === '>=') return { x: n[1], lo: rc }
|
|
601
|
+
if (n[0] === '>') return { x: n[1], lo: rc + 1 }
|
|
602
|
+
if (n[0] === '<=') return { x: n[1], hi: rc }
|
|
603
|
+
if (n[0] === '<') return { x: n[1], hi: rc - 1 }
|
|
604
|
+
}
|
|
605
|
+
if (lc != null && typeof n[2] === 'string') { // CONST op x
|
|
606
|
+
if (n[0] === '<=') return { x: n[2], lo: lc }
|
|
607
|
+
if (n[0] === '<') return { x: n[2], lo: lc + 1 }
|
|
608
|
+
if (n[0] === '>=') return { x: n[2], hi: lc }
|
|
609
|
+
if (n[0] === '>') return { x: n[2], hi: lc - 1 }
|
|
610
|
+
}
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI constants) → `(x - LO) <=u (HI - LO)`.
|
|
615
|
+
// One subtract + one unsigned compare replaces two signed compares, an AND, and the
|
|
616
|
+
// short-circuit branch — the classic range-check trick (valid for any integers via
|
|
617
|
+
// wrapping subtraction). Returns the fused IR, or null to leave `&&` lowering unchanged.
|
|
618
|
+
function fuseRangeCheck(a, b) {
|
|
619
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
620
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
621
|
+
const lo = ba.lo ?? bb.lo, hi = ba.hi ?? bb.hi
|
|
622
|
+
if (lo > hi) return null
|
|
623
|
+
const xv = emit(ba.x)
|
|
624
|
+
if (xv.type !== 'i32') return null // f64 (fractional) would mis-fuse
|
|
625
|
+
return typed(['i32.le_u', ['i32.sub', xv, ['i32.const', lo]], ['i32.const', hi - lo]], 'i32')
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// The complement: `x < LO || x > HI` (the two outside half-checks — one upper-bounded,
|
|
629
|
+
// one lower-bounded, with a gap between) → `(x - LO) >u (HI - LO)`, where [LO, HI] is the
|
|
630
|
+
// inside range. Same trick, negated; returns null to leave `||` lowering unchanged.
|
|
631
|
+
function fuseRangeCheckOr(a, b) {
|
|
632
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
633
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
634
|
+
const insideLo = (ba.hi ?? bb.hi) + 1, insideHi = (ba.lo ?? bb.lo) - 1
|
|
635
|
+
if (insideLo > insideHi) return null
|
|
636
|
+
const xv = emit(ba.x)
|
|
637
|
+
if (xv.type !== 'i32') return null
|
|
638
|
+
return typed(['i32.gt_u', ['i32.sub', xv, ['i32.const', insideLo]], ['i32.const', insideHi - insideLo]], 'i32')
|
|
639
|
+
}
|
|
640
|
+
|
|
451
641
|
// Flow-sensitive type refinement moved to ./flow-types.js (extractRefinements,
|
|
452
642
|
// predicateRefinement, mergeRefinement, withRefinements). emit.js imports them
|
|
453
643
|
// from there — see the import block at the top of this file.
|
|
@@ -469,6 +659,80 @@ function unrollSmallConstFor(init, cond, step, body) {
|
|
|
469
659
|
return out
|
|
470
660
|
}
|
|
471
661
|
|
|
662
|
+
// Max distinct keys a for-in unrolls over (bounds code size; larger key sets keep
|
|
663
|
+
// the pooled-keys loop, which is already allocation-free via __keys_ro).
|
|
664
|
+
const FORIN_UNROLL_MAX = 16
|
|
665
|
+
// Total-expansion ceiling: unroll emits one body copy per key, so the size cost is
|
|
666
|
+
// keys × body, not keys alone. A large body over many keys (e.g. watr's 15-key
|
|
667
|
+
// schema loop) blows up code size for no deopt win — the pooled fallback is already
|
|
668
|
+
// allocation-free. Cap keys × nodeSize(body); past it, keep the loop. (Tuned above
|
|
669
|
+
// every unroll the corpus actually wants — the 16-key cap test lands at 80.)
|
|
670
|
+
const FORIN_UNROLL_BUDGET = 128
|
|
671
|
+
const forInBodyCost = (node) => {
|
|
672
|
+
if (!Array.isArray(node)) return 1
|
|
673
|
+
let n = 1
|
|
674
|
+
for (let i = 1; i < node.length; i++) n += forInBodyCost(node[i])
|
|
675
|
+
return n
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Pull the for-in source out of prepare's keys expression: either a bare
|
|
679
|
+
// `__keys_ro(src)` call or the nullish-guarded `cond ? [] : __keys_ro(src)`.
|
|
680
|
+
function keysRoSrc(node) {
|
|
681
|
+
if (!Array.isArray(node)) return null
|
|
682
|
+
if (node[0] === '()' && node[1] === '__keys_ro') return node[2]
|
|
683
|
+
if (node[0] === '?:' || node[0] === '?') {
|
|
684
|
+
const last = node[node.length - 1]
|
|
685
|
+
if (Array.isArray(last) && last[0] === '()' && last[1] === '__keys_ro') return last[2]
|
|
686
|
+
}
|
|
687
|
+
return null
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Unroll `for (k in o)` over a static schema. Prepare lowers for-in to a plain
|
|
691
|
+
// for-loop whose key array comes from the for-in-exclusive `__keys_ro` intrinsic,
|
|
692
|
+
// so a loop carrying it IS a for-in. When `o` is a bare OBJECT var with a complete
|
|
693
|
+
// static schema (no computed-key writes — same gate as __keys_ro pooling), replace
|
|
694
|
+
// the loop with one substituted copy of the body per key: the loop variable becomes
|
|
695
|
+
// a string literal, so `o[k]` folds to a static schema slot — no keys array, no
|
|
696
|
+
// per-element dynamic get. Falls back (returns null) to the pooled loop otherwise.
|
|
697
|
+
function unrollForIn(init, cond, step, body) {
|
|
698
|
+
if (!Array.isArray(init) || init[0] !== 'let' || !Array.isArray(init[1]) || init[1][0] !== '=') return null
|
|
699
|
+
const ksVar = init[1][1]
|
|
700
|
+
const src = keysRoSrc(init[1][2])
|
|
701
|
+
if (typeof src !== 'string') return null
|
|
702
|
+
if (!Array.isArray(cond) || cond[0] !== '<') return null
|
|
703
|
+
const ixVar = cond[1]
|
|
704
|
+
if (!Array.isArray(step) || step[0] !== '++' || step[1] !== ixVar) return null
|
|
705
|
+
// body = [';', ['let', ['=', target, ['[]', ksVar, ixVar]]], ...realBody]
|
|
706
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
707
|
+
const bind = body[1]
|
|
708
|
+
if (!Array.isArray(bind) || bind[0] !== 'let' || !Array.isArray(bind[1]) || bind[1][0] !== '=') return null
|
|
709
|
+
const target = bind[1][1]
|
|
710
|
+
const acc = bind[1][2]
|
|
711
|
+
if (!Array.isArray(acc) || acc[0] !== '[]' || acc[1] !== ksVar || acc[2] !== ixVar) return null
|
|
712
|
+
|
|
713
|
+
// Unroll only with PROOF the schema is complete: a computed-key write adds
|
|
714
|
+
// enumerable keys, so bail if `src` takes one — or if the fact is unavailable
|
|
715
|
+
// (no proof ⇒ no unroll; unrolling drops the dynamic path, so erring safe matters).
|
|
716
|
+
if (!ctx.types.dynWriteVars || ctx.types.dynWriteVars.has(src)) return null
|
|
717
|
+
if (lookupValType(src) !== VAL.OBJECT) return null
|
|
718
|
+
const keys = ctx.schema.resolve(src)
|
|
719
|
+
if (!keys || !keys.length || keys.length > FORIN_UNROLL_MAX) return null
|
|
720
|
+
|
|
721
|
+
const rest = body.slice(2)
|
|
722
|
+
const realBody = rest.length === 1 ? rest[0] : [';', ...rest]
|
|
723
|
+
// Keep the pooled loop when unrolling would multiply a heavy body across many keys.
|
|
724
|
+
if (keys.length * forInBodyCost(realBody) > FORIN_UNROLL_BUDGET) return null
|
|
725
|
+
// Substitution safety, mirroring unrollSmallConstFor: no reassignment/redeclare
|
|
726
|
+
// of the loop var, no nested closure capturing it (cloneWithSubst skips `=>`),
|
|
727
|
+
// and no break/continue targeting this loop.
|
|
728
|
+
if (hasOwnBreakOrContinue(realBody) || containsNestedClosure(realBody) || containsDeclOf(realBody, target)) return null
|
|
729
|
+
if (isReassigned(realBody, target)) return null
|
|
730
|
+
|
|
731
|
+
const out = []
|
|
732
|
+
for (const key of keys) out.push(...emitVoid(cloneWithSubst(realBody, new Map([[target, ['str', key]]]))))
|
|
733
|
+
return out.length ? out : ['nop']
|
|
734
|
+
}
|
|
735
|
+
|
|
472
736
|
function canThrow(body, seen = new Set()) {
|
|
473
737
|
if (!Array.isArray(body)) return false
|
|
474
738
|
const op = body[0]
|
|
@@ -581,6 +845,40 @@ export function toBool(node) {
|
|
|
581
845
|
return toBoolFromEmitted(emit(node))
|
|
582
846
|
}
|
|
583
847
|
|
|
848
|
+
// `(a / b) | 0` (the JS integer-division idiom) → i32.div_s. jz otherwise lowers `/`
|
|
849
|
+
// to f64.div + ToInt32, paying two i32→f64 converts and the trunc; i32.div_s is
|
|
850
|
+
// direct and lets the wasm backend magic-multiply a constant divisor. Bit-exact for
|
|
851
|
+
// all i32 a,b: |a|<2³³≪2⁵³ so the f64 quotient never rounds across the truncation
|
|
852
|
+
// boundary — EXCEPT b=0 (`(a/0)|0` is ToInt32(±Inf)=0, but i32.div_s traps) and
|
|
853
|
+
// INT_MIN/-1 (ToInt32 wraps to INT_MIN, i32.div_s traps); both guarded. A constant
|
|
854
|
+
// divisor folds the guards away. `exprType==='i32'` excludes unsigned operands
|
|
855
|
+
// (those return 'f64'), where div_s would misread the sign. Returns IR or null.
|
|
856
|
+
const INT_MIN_I32 = -2147483648
|
|
857
|
+
function tryIntDivTrunc(aNode, bNode) {
|
|
858
|
+
const o = ctx.transform.optimize
|
|
859
|
+
if (!o || o.intDivLower === false) return null
|
|
860
|
+
const L = ctx.func.locals
|
|
861
|
+
if (exprType(aNode, L) !== 'i32' || exprType(bNode, L) !== 'i32') return null
|
|
862
|
+
const dv = intLiteralValue(bNode)
|
|
863
|
+
if (dv != null) { // constant divisor — no runtime guard
|
|
864
|
+
const va = asI32(emit(aNode))
|
|
865
|
+
if (dv === 0) return typed(['block', ['result', 'i32'], ['drop', va], ['i32.const', 0]], 'i32')
|
|
866
|
+
if (dv === -1) return typed(['i32.sub', ['i32.const', 0], va], 'i32') // -a, wraps at INT_MIN
|
|
867
|
+
return typed(['i32.div_s', va, ['i32.const', dv | 0]], 'i32')
|
|
868
|
+
}
|
|
869
|
+
// Runtime divisor needs a,b repeated across the guard; only intercept when both are
|
|
870
|
+
// simple re-emittable operands (var / literal) so re-emit is pure and side-effect-free.
|
|
871
|
+
const simple = (n) => typeof n === 'string' || intLiteralValue(n) != null
|
|
872
|
+
if (!simple(aNode) || !simple(bNode)) return null
|
|
873
|
+
const A = () => asI32(emit(aNode)), B = () => asI32(emit(bNode))
|
|
874
|
+
return typed(['if', ['result', 'i32'], ['i32.eqz', B()],
|
|
875
|
+
['then', ['i32.const', 0]],
|
|
876
|
+
['else', ['if', ['result', 'i32'],
|
|
877
|
+
['i32.and', ['i32.eq', A(), ['i32.const', INT_MIN_I32]], ['i32.eq', B(), ['i32.const', -1]]],
|
|
878
|
+
['then', A()],
|
|
879
|
+
['else', ['i32.div_s', A(), B()]]]]], 'i32')
|
|
880
|
+
}
|
|
881
|
+
|
|
584
882
|
/** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
|
|
585
883
|
* i32 offset), else falls back to numeric WASM type coercion. */
|
|
586
884
|
function coerceArg(ir, param) {
|
|
@@ -721,7 +1019,7 @@ export function emitDecl(...inits) {
|
|
|
721
1019
|
// Monotonic-extension fields (`o.newProp = …`) carry no literal value —
|
|
722
1020
|
// they init to undefined so a read before the write matches JS.
|
|
723
1021
|
const flatDecl = ctx.func.flatObjects?.get(name)
|
|
724
|
-
if (flatDecl && Array.isArray(init) && init[0] === '{}') {
|
|
1022
|
+
if (flatDecl && Array.isArray(init) && (init[0] === '{}' || init[0] === '[' || init[0] === '[]')) {
|
|
725
1023
|
for (let j = 0; j < flatDecl.names.length; j++)
|
|
726
1024
|
result.push(['local.set', `$${name}#${j}`,
|
|
727
1025
|
flatDecl.values[j] === undefined ? undefExpr() : asF64(emit(flatDecl.values[j]))])
|
|
@@ -790,6 +1088,17 @@ export function emitDecl(...inits) {
|
|
|
790
1088
|
if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
|
|
791
1089
|
ctx.func.directClosures.set(name, val.closureBodyName)
|
|
792
1090
|
}
|
|
1091
|
+
// Copy propagation of a direct closure: `let g = add`, where `add` is a non-escaping
|
|
1092
|
+
// directly-callable closure, makes `g` directly callable too — `g` holds the same
|
|
1093
|
+
// closure value, so `g(…)` calls add's body with g's value as env. This is what
|
|
1094
|
+
// devirtualizes `let arr = [add]; arr[0](…)`: array scalarization rewrites it to
|
|
1095
|
+
// `let g = add; g(…)` before emit (D3), and also covers the explicit `let g = arr[0]`.
|
|
1096
|
+
// Same soundness gate as the direct-closure case: stable binding (not reassigned),
|
|
1097
|
+
// not boxed, not global.
|
|
1098
|
+
if (typeof init === 'string' && ctx.func.directClosures?.has(init) && !ctx.func.boxed.has(name)
|
|
1099
|
+
&& !isGlobal(name) && ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
1100
|
+
ctx.func.directClosures.set(name, ctx.func.directClosures.get(init))
|
|
1101
|
+
}
|
|
793
1102
|
if (ctx.func.boxed.has(name)) {
|
|
794
1103
|
const cell = ctx.func.boxed.get(name)
|
|
795
1104
|
ctx.func.locals.set(cell, 'i32')
|
|
@@ -862,11 +1171,23 @@ export function emitDecl(...inits) {
|
|
|
862
1171
|
updateRep(name, { ptrAux: val.closureFuncIdx })
|
|
863
1172
|
coerced = val.ptrKind === ptrKind ? val
|
|
864
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)
|
|
865
1180
|
} else {
|
|
866
1181
|
coerced = localType === 'v128' ? val : localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
|
|
867
1182
|
}
|
|
868
|
-
|
|
1183
|
+
// `let x = 0` at function scope is normally elided — WASM zero-inits locals. But loop
|
|
1184
|
+
// unrolling flattens iteration bodies into one scope, so the 2nd+ `let x = 0` are
|
|
1185
|
+
// genuine RE-inits between iterations (e.g. a nested reduce's accumulator). Elide only
|
|
1186
|
+
// the FIRST per name; emit the rest as resets. (Names are preserved — no renaming.)
|
|
1187
|
+
const zeroInit = isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length
|
|
1188
|
+
if (!zeroInit || ctx.func.zeroInitSeen?.has(name))
|
|
869
1189
|
result.push(['local.set', `$${name}`, coerced])
|
|
1190
|
+
else (ctx.func.zeroInitSeen ??= new Set()).add(name)
|
|
870
1191
|
|
|
871
1192
|
const schemaId = ctx.schema.idOf?.(name)
|
|
872
1193
|
if (ctx.func.localProps?.has(name) && schemaId != null) {
|
|
@@ -912,12 +1233,22 @@ function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
|
|
|
912
1233
|
const sidx = `${T}sidx${ctx.func.uniq++}`
|
|
913
1234
|
ctx.func.locals.set(sidx, 'i32')
|
|
914
1235
|
const loopId = ctx.func.uniq++
|
|
915
|
-
|
|
916
|
-
|
|
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'],
|
|
917
1248
|
['i32.eq', ['call', '$__ptr_type', srcI64()], ['i32.const', PTR.STRING]],
|
|
918
1249
|
['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
|
|
919
|
-
['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]
|
|
920
|
-
|
|
1250
|
+
['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]
|
|
1251
|
+
])
|
|
921
1252
|
// Reset the counter on each entry — WASM zeroes locals once at function
|
|
922
1253
|
// entry, but this loop re-executes when the spread sits inside a JS loop;
|
|
923
1254
|
// a stale `sidx` (= prior srcLen) would skip the copy entirely.
|
|
@@ -1019,8 +1350,25 @@ export function buildArrayWithSpreads(items) {
|
|
|
1019
1350
|
// emitSpreadCopy resolve its kind at runtime via its one-time __ptr_type branch.
|
|
1020
1351
|
sec.val = n ? undefined : valTypeOf(srcExpr)
|
|
1021
1352
|
ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(srcExpr))])
|
|
1022
|
-
// Cache
|
|
1023
|
-
|
|
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])
|
|
1024
1372
|
}
|
|
1025
1373
|
}
|
|
1026
1374
|
|
|
@@ -1163,6 +1511,48 @@ const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
|
|
|
1163
1511
|
const nullableOperand = (n) =>
|
|
1164
1512
|
typeof n === 'string' && !!(repOf(n)?.nullable || repOfGlobal(n)?.nullable)
|
|
1165
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
|
+
|
|
1166
1556
|
function emitLooseEq(a, b, negate) {
|
|
1167
1557
|
const eqOp = negate ? 'ne' : 'eq'
|
|
1168
1558
|
const sentinel = emitNum(negate ? 1 : 0)
|
|
@@ -1181,6 +1571,12 @@ function emitLooseEq(a, b, negate) {
|
|
|
1181
1571
|
const tc = emitTypeofCmp(a, b, eqOp); if (tc) return tc
|
|
1182
1572
|
const va = emit(a), vb = emit(b)
|
|
1183
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')
|
|
1184
1580
|
// Either side known-pure NUMBER (literal or typed) → f64.eq/ne is correct regardless
|
|
1185
1581
|
// of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
|
|
1186
1582
|
// reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
|
|
@@ -1201,6 +1597,43 @@ function emitLooseEq(a, b, negate) {
|
|
|
1201
1597
|
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1202
1598
|
return typed([`i64.${eqOp}`, ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1203
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
|
+
}
|
|
1204
1637
|
inc('__eq')
|
|
1205
1638
|
const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
1206
1639
|
return negate ? typed(['i32.eqz', call], 'i32') : call
|
|
@@ -1472,6 +1905,13 @@ function emitSpreadElementLoop(spreadExpr, bodyFn, { reverse = false } = {}) {
|
|
|
1472
1905
|
]
|
|
1473
1906
|
}
|
|
1474
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
|
+
|
|
1475
1915
|
function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
1476
1916
|
const inPlace = SPREAD_MUTATORS.has(method)
|
|
1477
1917
|
// unshift prepends each arg to the front — forward iteration reverses intent.
|
|
@@ -1480,11 +1920,11 @@ function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1480
1920
|
ctx.func.locals.set(acc, 'f64')
|
|
1481
1921
|
const ir = [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
1482
1922
|
if (parsed.normal.length > 0) {
|
|
1483
|
-
const r = asF64(methodEmitter(objArg, ...parsed.normal))
|
|
1923
|
+
const r = asF64(emitAsValue(() => methodEmitter(objArg, ...parsed.normal)))
|
|
1484
1924
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1485
1925
|
}
|
|
1486
1926
|
ir.push(...emitSpreadElementLoop(parsed.spreads[0].expr, (arr, idx) => {
|
|
1487
|
-
const body = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
|
|
1927
|
+
const body = asF64(emitAsValue(() => methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx])))
|
|
1488
1928
|
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1489
1929
|
}, { reverse }))
|
|
1490
1930
|
ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
|
|
@@ -1506,7 +1946,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1506
1946
|
let batch = []
|
|
1507
1947
|
const flushBatch = () => {
|
|
1508
1948
|
if (!batch.length) return
|
|
1509
|
-
const r = asF64(methodEmitter(recv, ...batch))
|
|
1949
|
+
const r = asF64(emitAsValue(() => methodEmitter(recv, ...batch)))
|
|
1510
1950
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1511
1951
|
batch = []
|
|
1512
1952
|
}
|
|
@@ -1514,7 +1954,7 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1514
1954
|
if (Array.isArray(item) && item[0] === '__spread') {
|
|
1515
1955
|
flushBatch()
|
|
1516
1956
|
ir.push(...emitSpreadElementLoop(item[1], (arr, idx) => {
|
|
1517
|
-
const body = asF64(methodEmitter(recv, ['[]', arr, idx]))
|
|
1957
|
+
const body = asF64(emitAsValue(() => methodEmitter(recv, ['[]', arr, idx])))
|
|
1518
1958
|
return [inPlace ? ['drop', body] : ['local.set', `$${acc}`, body]]
|
|
1519
1959
|
}))
|
|
1520
1960
|
} else {
|
|
@@ -1591,7 +2031,7 @@ function tryCharCodeAtFast(callee, obj, method, parsed) {
|
|
|
1591
2031
|
return typed(['call', '$__jss_charCodeAt', recv, asI32(emit(parsed.normal[0]))], 'i32')
|
|
1592
2032
|
}
|
|
1593
2033
|
return typed(stringOps(obj).charCodeAt(
|
|
1594
|
-
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false), 'i32')
|
|
2034
|
+
asF64(recv), asI32(emit(parsed.normal[0])), ctx, false, true), 'i32')
|
|
1595
2035
|
}
|
|
1596
2036
|
}
|
|
1597
2037
|
|
|
@@ -1705,7 +2145,9 @@ function tryStaticDispatch({ obj, method, vt, callMethod }) {
|
|
|
1705
2145
|
// emitter which already knows how to handle them.
|
|
1706
2146
|
function tryRuntimeStringFork({ obj, method, vt, callMethod }) {
|
|
1707
2147
|
const strKey = `.string:${method}`, genKey = `.${method}`
|
|
1708
|
-
|
|
2148
|
+
// VAL.ARRAY is structurally incompatible with PTR.STRING — no fork needed.
|
|
2149
|
+
// Only fork when vt is truly unknown (!vt), not for proven types.
|
|
2150
|
+
if (!vt && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
|
|
1709
2151
|
const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
|
|
1710
2152
|
ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
|
|
1711
2153
|
const strEmitter = ctx.core.emit[strKey]
|
|
@@ -1840,6 +2282,7 @@ function externalMethodFallback({ obj, method, parsed }) {
|
|
|
1840
2282
|
// can target js and wasi from one source; users who want fail-fast
|
|
1841
2283
|
// pass `strict: true` (handled above).
|
|
1842
2284
|
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
2285
|
+
warnDeopt('deopt-method', `method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(…)\` on a value whose type couldn't be resolved dispatches through the JS host (\`__ext_call\`) — a wasm→JS round-trip per call, orders of magnitude slower than a direct call. Restructure so the receiver's type is provable, or keep it off the hot path.`)
|
|
1843
2286
|
inc('__ext_call')
|
|
1844
2287
|
ctx.features.external = true
|
|
1845
2288
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
@@ -2018,9 +2461,21 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2018
2461
|
// export-param path gives. An arg we can't prove numeric poisons the slot to false.
|
|
2019
2462
|
const pt = (ctx.closure.paramTypes ||= new Map())
|
|
2020
2463
|
let row = pt.get(bodyName); if (!row) pt.set(bodyName, row = [])
|
|
2464
|
+
// Parallel typed-array ctor lattice: a param passed the SAME typed-array ctor at
|
|
2465
|
+
// every direct call site is a TYPED param, so its body reads (`buf[i]`) take the
|
|
2466
|
+
// typed fast-path instead of the dynamic `__typed_idx`/`__len` route that drags in
|
|
2467
|
+
// the string runtime. `null` (sticky) once two sites disagree or an arg isn't a
|
|
2468
|
+
// known typed array — the same monotone meet as the numeric row. Mirrors the named-fn
|
|
2469
|
+
// applyTypedPointerParamAbi, restricted to non-escaping (directly-called) closures.
|
|
2470
|
+
const tc = (ctx.closure.paramTypedCtors ||= new Map())
|
|
2471
|
+
let tcRow = tc.get(bodyName); if (!tcRow) tc.set(bodyName, tcRow = [])
|
|
2021
2472
|
for (let i = 0; i < n; i++) {
|
|
2022
2473
|
const numeric = valTypeOf(parsed.normal[i]) === VAL.NUMBER
|
|
2023
2474
|
row[i] = row[i] === undefined ? numeric : (row[i] && numeric)
|
|
2475
|
+
const arg = parsed.normal[i]
|
|
2476
|
+
const ctor = typeof arg === 'string' && valTypeOf(arg) === VAL.TYPED ? (ctx.types.typedElem?.get(arg) ?? null) : null
|
|
2477
|
+
if (tcRow[i] === undefined) tcRow[i] = ctor
|
|
2478
|
+
else if (tcRow[i] !== ctor) tcRow[i] = null
|
|
2024
2479
|
}
|
|
2025
2480
|
// Track the fewest args any call passed: a slot at index ≥ minArgc is omitted by some call
|
|
2026
2481
|
// site (padded with UNDEF_NAN), so it may be undefined — emitClosureBody flags it nullable.
|
|
@@ -2373,9 +2828,22 @@ export const emitter = {
|
|
|
2373
2828
|
// A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
|
|
2374
2829
|
const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2375
2830
|
: vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2831
|
+
// Coercion-free sides are already strings: a known STRING is raw; OBJECT/BOOL
|
|
2832
|
+
// were stringified by `strOperand`. An unknown side still needs ToString, but
|
|
2833
|
+
// we can apply it *once* (explicit `__to_str` via `strI64`) and join with
|
|
2834
|
+
// concatRaw — equivalent to `__str_concat`'s internal `__to_str` on that side,
|
|
2835
|
+
// while NOT re-coercing the already-string side. This drops the redundant
|
|
2836
|
+
// per-append `__to_str` on the accumulator in `s += part` (s proven STRING):
|
|
2837
|
+
// - both coercion-free → concatRaw(ea, eb)
|
|
2838
|
+
// - one unknown → concatRaw(known, __to_str(unknown))
|
|
2839
|
+
// - both unknown → cat (unchanged; its runtime __to_str covers both)
|
|
2840
|
+
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2841
|
+
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2842
|
+
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2843
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2844
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2845
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2846
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2379
2847
|
}
|
|
2380
2848
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
2381
2849
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -2409,7 +2877,8 @@ export const emitter = {
|
|
|
2409
2877
|
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
2410
2878
|
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
2411
2879
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
2412
|
-
|
|
2880
|
+
const i32add = tryI32Arith('i32.add', '+', a, b, va, vb); if (i32add) return i32add
|
|
2881
|
+
return typed(['f64.add', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2413
2882
|
},
|
|
2414
2883
|
'-': (a, b) => {
|
|
2415
2884
|
if (ctx.func._expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
@@ -2424,7 +2893,8 @@ export const emitter = {
|
|
|
2424
2893
|
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
2425
2894
|
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
2426
2895
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
2427
|
-
|
|
2896
|
+
const i32sub = tryI32Arith('i32.sub', '-', a, b, va, vb); if (i32sub) return i32sub
|
|
2897
|
+
return typed(['f64.sub', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2428
2898
|
},
|
|
2429
2899
|
'u+': a => {
|
|
2430
2900
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
@@ -2453,8 +2923,9 @@ export const emitter = {
|
|
|
2453
2923
|
if (isLit(va) && litVal(va) === 0 && finiteFactor(vb)) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
2454
2924
|
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2455
2925
|
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
2456
|
-
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
|
|
2457
|
-
|
|
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
|
|
2928
|
+
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2458
2929
|
},
|
|
2459
2930
|
'/': (a, b) => {
|
|
2460
2931
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
@@ -2462,7 +2933,7 @@ export const emitter = {
|
|
|
2462
2933
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
2463
2934
|
if (_f) return _f
|
|
2464
2935
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2465
|
-
return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
2936
|
+
return typed(['f64.div', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2466
2937
|
},
|
|
2467
2938
|
'%': (a, b) => {
|
|
2468
2939
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
@@ -2539,6 +3010,15 @@ export const emitter = {
|
|
|
2539
3010
|
const elseRefs = extractRefinements(a, new Map(), false)
|
|
2540
3011
|
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
2541
3012
|
const vc = withRefinements(elseRefs, c, () => emit(c))
|
|
3013
|
+
// `cond ? 1 : 0` is the condition bit itself; `cond ? 0 : 1` its negation. `cond`
|
|
3014
|
+
// (truthyIR) is already canonical 0/1, so the select + two const arms collapse to
|
|
3015
|
+
// the bit. (Both arms are literals here, so dropping their emitted IR is side-effect
|
|
3016
|
+
// free.) Mirrors what `+(x > 0)` already produces.
|
|
3017
|
+
if (isLit(vb) && isLit(vc)) {
|
|
3018
|
+
const lb = litVal(vb), lc = litVal(vc)
|
|
3019
|
+
if (lb === 1 && lc === 0) return typed(cond, 'i32')
|
|
3020
|
+
if (lb === 0 && lc === 1) return typed(['i32.eqz', cond], 'i32')
|
|
3021
|
+
}
|
|
2542
3022
|
// L: Use WASM select for pure ternaries — branchless, smaller bytecode
|
|
2543
3023
|
if (vb.type === 'i32' && vc.type === 'i32') {
|
|
2544
3024
|
// A single i32 select is only sound when BOTH arms' i32 carriers mean the same
|
|
@@ -2577,10 +3057,24 @@ export const emitter = {
|
|
|
2577
3057
|
const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
|
|
2578
3058
|
|| vb.closureFuncIdx != null || vc.closureFuncIdx != null
|
|
2579
3059
|
|| isNaNBoxLit(fb) || isNaNBoxLit(fc)
|
|
2580
|
-
const numericB =
|
|
2581
|
-
const numericC =
|
|
2582
|
-
|
|
2583
|
-
|
|
3060
|
+
const numericB = isNumArm(vb, b, vtb)
|
|
3061
|
+
const numericC = isNumArm(vc, c, vtc)
|
|
3062
|
+
// Peephole: `cond ? 1 : 0` (or `cond ? 0 : 1`) is just `f64.convert_i32_s(cond)` —
|
|
3063
|
+
// the select collapses because cond is already 0/1. Saves 5 instructions.
|
|
3064
|
+
const isOneZero = (one, zero) => {
|
|
3065
|
+
const o = one, z = zero
|
|
3066
|
+
return o.type === 'i32' && Array.isArray(o) && o[0] === 'i32.const' && o[1] === 1 &&
|
|
3067
|
+
z.type === 'i32' && Array.isArray(z) && z[0] === 'i32.const' && z[1] === 0
|
|
3068
|
+
}
|
|
3069
|
+
if ((isOneZero(vb, vc) || isOneZero(vc, vb)) && !numericB && !numericC) {
|
|
3070
|
+
const condBool = truthyIR(emit(a))
|
|
3071
|
+
const n = isOneZero(vb, vc)
|
|
3072
|
+
? typed(['f64.convert_i32_s', condBool], 'f64')
|
|
3073
|
+
: typed(['f64.convert_i32_s', ['i32.eqz', condBool]], 'f64')
|
|
3074
|
+
n.valKind = VAL.NUMBER
|
|
3075
|
+
return n
|
|
3076
|
+
}
|
|
3077
|
+
const branchB = canonArm(fb, numericB, numericC), branchC = canonArm(fc, numericC, numericB)
|
|
2584
3078
|
const markNumeric = (n) => {
|
|
2585
3079
|
if (numericB && numericC) n.valKind = VAL.NUMBER
|
|
2586
3080
|
return n
|
|
@@ -2599,6 +3093,14 @@ export const emitter = {
|
|
|
2599
3093
|
},
|
|
2600
3094
|
|
|
2601
3095
|
'&&': (a, b) => {
|
|
3096
|
+
// Range-check fusion: `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI compile-time
|
|
3097
|
+
// constants) collapses to one unsigned compare `(x - LO) <=u (HI - LO)` — a subtract
|
|
3098
|
+
// plus a branch instead of two compares, an AND, and a short-circuit branch. This is
|
|
3099
|
+
// the per-char cost in scanners/parsers (digit/alpha classification) and in any
|
|
3100
|
+
// two-sided bounds check. Restricted to a local `x` so evaluating it once (the fused
|
|
3101
|
+
// form) matches the original's twice-read, side-effect-free semantics.
|
|
3102
|
+
const fused = fuseRangeCheck(a, b)
|
|
3103
|
+
if (fused) return fused
|
|
2602
3104
|
const va = emit(a)
|
|
2603
3105
|
// Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
|
|
2604
3106
|
if (isLit(va)) {
|
|
@@ -2630,14 +3132,24 @@ export const emitter = {
|
|
|
2630
3132
|
['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
|
|
2631
3133
|
}
|
|
2632
3134
|
const t = temp()
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
3135
|
+
const numA = isNumArm(va, a)
|
|
3136
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
3137
|
+
// `a` is the else-arm result (returned when falsy — incl NaN), so canon a lone-numeric
|
|
3138
|
+
// `a` before the tee: `$t` then feeds both the result and the cond canonically.
|
|
3139
|
+
const teed = typed(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)], 'f64')
|
|
3140
|
+
// A numeric left arm tests truthiness NaN-by-value (not __is_truthy, which mis-reads
|
|
3141
|
+
// x86's sign-set NaN as truthy) — tag it so truthyIR takes that path.
|
|
3142
|
+
if (numA) teed.valKind = VAL.NUMBER
|
|
3143
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
3144
|
+
['then', canonArm(asF64(vb), numB, numA)],
|
|
2637
3145
|
['else', ['local.get', `$${t}`]]], 'f64')
|
|
2638
3146
|
},
|
|
2639
3147
|
|
|
2640
3148
|
'||': (a, b) => {
|
|
3149
|
+
// Outside-range fusion (the complement of `&&`): `x < LO || x > HI` → one unsigned
|
|
3150
|
+
// compare `(x - LO) >u (HI - LO)`. Common in validation (`if (c < 'a' || c > 'z') …`).
|
|
3151
|
+
const fusedOr = fuseRangeCheckOr(a, b)
|
|
3152
|
+
if (fusedOr) return fusedOr
|
|
2641
3153
|
const va = emit(a)
|
|
2642
3154
|
// Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
|
|
2643
3155
|
if (isLit(va)) {
|
|
@@ -2665,22 +3177,30 @@ export const emitter = {
|
|
|
2665
3177
|
['else', asF64(vb)]], 'f64')
|
|
2666
3178
|
}
|
|
2667
3179
|
const t = temp()
|
|
3180
|
+
const numA = isNumArm(va, a)
|
|
3181
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
3182
|
+
// `a` (then-arm) is returned only when truthy — hence never NaN — so it needs no canon;
|
|
3183
|
+
// the cond's NaN-safety comes from the valKind tag. Only the else (b) arm can surface
|
|
3184
|
+
// as a numeric NaN.
|
|
2668
3185
|
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
2669
|
-
|
|
2670
|
-
|
|
3186
|
+
if (numA) teed.valKind = VAL.NUMBER // numeric left arm: NaN-safe truthiness (see `&&`)
|
|
3187
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
2671
3188
|
['then', ['local.get', `$${t}`]],
|
|
2672
|
-
['else', asF64(
|
|
3189
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2673
3190
|
},
|
|
2674
3191
|
|
|
2675
3192
|
// a ?? b: returns b only if a is nullish
|
|
2676
3193
|
'??': (a, b) => {
|
|
2677
|
-
const va = emit(a)
|
|
3194
|
+
const va = emit(a), vb = emit(b)
|
|
2678
3195
|
const t = temp()
|
|
3196
|
+
const numA = isNumArm(va, a), numB = isNumArm(vb, b)
|
|
3197
|
+
// Both arms can surface as the (untyped) result — `a` when non-nullish (a NaN is not
|
|
3198
|
+
// nullish, so it IS returned), `b` otherwise. Canon a lone-numeric arm; `a` before the
|
|
3199
|
+
// tee so `local.get $t` is canonical. The cond is isNullish, robust to non-canon NaN.
|
|
2679
3200
|
return typed(['if', ['result', 'f64'],
|
|
2680
|
-
|
|
2681
|
-
['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
|
|
3201
|
+
['i32.eqz', isNullish(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)])],
|
|
2682
3202
|
['then', ['local.get', `$${t}`]],
|
|
2683
|
-
['else', asF64(
|
|
3203
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2684
3204
|
},
|
|
2685
3205
|
|
|
2686
3206
|
'void': a => {
|
|
@@ -2725,6 +3245,10 @@ export const emitter = {
|
|
|
2725
3245
|
].map(([op, fn]) => [op, (a, b) => {
|
|
2726
3246
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2727
3247
|
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
3248
|
+
if (op === '|') { // `(x / y) | 0` integer-division idiom → i32.div_s
|
|
3249
|
+
const divN = intLiteralValue(b) === 0 ? a : intLiteralValue(a) === 0 ? b : null
|
|
3250
|
+
if (Array.isArray(divN) && divN[0] === '/') { const r = tryIntDivTrunc(divN[1], divN[2]); if (r) return r }
|
|
3251
|
+
}
|
|
2728
3252
|
const va = emit(a), vb = emit(b)
|
|
2729
3253
|
if (isLit(va) && isLit(vb)) {
|
|
2730
3254
|
const la = litVal(va), lb = litVal(vb)
|
|
@@ -2770,6 +3294,17 @@ export const emitter = {
|
|
|
2770
3294
|
if (els != null) return emitVoid(els)
|
|
2771
3295
|
return null
|
|
2772
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
|
+
}
|
|
2773
3308
|
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
2774
3309
|
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
2775
3310
|
const thenRefs = extractRefinements(cond, new Map(), true)
|
|
@@ -2794,6 +3329,12 @@ export const emitter = {
|
|
|
2794
3329
|
const unrolled = unrollSmallConstFor(init, cond, step, body)
|
|
2795
3330
|
if (unrolled) return unrolled
|
|
2796
3331
|
}
|
|
3332
|
+
// for-in over a static schema → unroll with key-literal substitution (folds
|
|
3333
|
+
// o[k] to schema slots). Recognized via the for-in-exclusive __keys_ro intrinsic.
|
|
3334
|
+
if (!labeledContinue && (!ctx.transform.optimize || ctx.transform.optimize.forInUnroll !== false)) {
|
|
3335
|
+
const fu = unrollForIn(init, cond, step, body)
|
|
3336
|
+
if (fu) return fu
|
|
3337
|
+
}
|
|
2797
3338
|
// Lift constant array/object literals out of the loop (allocate once, not per
|
|
2798
3339
|
// iteration) when they are read-only + non-escaping inside it. Strip them from the
|
|
2799
3340
|
// body up front so freshBoxed / continue analysis see the reduced body.
|