jz 0.5.1 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +266 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +414 -186
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +586 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +589 -202
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,1874 @@
|
|
|
1
|
+
import { findBodyStart } from '../ir.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lane-local SIMD-128 vectorizer.
|
|
5
|
+
*
|
|
6
|
+
* Recognizes inner loops of shape:
|
|
7
|
+
* for (let i = 0; i < N; i++) arr[i] = f(arr[i], …)
|
|
8
|
+
* where every body op is "lane-pure" — its k-th lane output depends only
|
|
9
|
+
* on k-th lane inputs. Lifts the body to SIMD-128, prefixed before the
|
|
10
|
+
* original (now tail) loop. Original loop runs the remainder.
|
|
11
|
+
*
|
|
12
|
+
* Design:
|
|
13
|
+
* • Lane-purity is a structural property, not a benchmark match. The op
|
|
14
|
+
* whitelist is the single source of truth (one entry per (lane-type, op)).
|
|
15
|
+
* • Lift is mechanical. The recognizer either matches the structure — in
|
|
16
|
+
* which case lifting is unambiguous — or skips. No bench-specific
|
|
17
|
+
* heuristics.
|
|
18
|
+
* • Tail loop is the original WAT, untouched. If anything regresses the
|
|
19
|
+
* SIMD recognizer just doesn't match, never miscompiles.
|
|
20
|
+
*
|
|
21
|
+
* Match conditions:
|
|
22
|
+
* 1. (block $brk (loop $L (br_if $brk !cond) BODY (i = i+1) (br $L)))
|
|
23
|
+
* 2. cond is `(i32.lt_s i BOUND)` or `i32.lt_u`; BOUND is loop-invariant.
|
|
24
|
+
* 3. All loads/stores in BODY use address `(add base (shl i K))` where
|
|
25
|
+
* base is loop-invariant and K matches the elem stride. Optional
|
|
26
|
+
* enclosing `local.tee` is allowed (and reused).
|
|
27
|
+
* 4. All loads share the same opcode → defines lane type.
|
|
28
|
+
* 5. All other ops in BODY are in the lane-pure whitelist for that type.
|
|
29
|
+
* 6. Each non-induction local in BODY is either purely loop-invariant
|
|
30
|
+
* (only read) or purely lane-local (first action is a write). Never
|
|
31
|
+
* both — that's a loop-carried scalar (reduction / stencil) → bail.
|
|
32
|
+
*
|
|
33
|
+
* Lift produces, before the original block:
|
|
34
|
+
* (local.set $__simd_bound{N} (i32.and BOUND (i32.const ~(LANES-1))))
|
|
35
|
+
* (block $__simd_brk{N}
|
|
36
|
+
* (loop $__simd_loop{N}
|
|
37
|
+
* (br_if $__simd_brk{N} (i32.eqz (i32.lt_s i $__simd_bound{N})))
|
|
38
|
+
* <body lifted op-by-op; lane-local locals routed to v128 shadows>
|
|
39
|
+
* (local.set $i (i32.add i (i32.const LANES)))
|
|
40
|
+
* (br $__simd_loop{N})))
|
|
41
|
+
*
|
|
42
|
+
* The original block runs immediately after with i pre-advanced; its own
|
|
43
|
+
* `i < BOUND` guard handles the tail.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const isArr = n => Array.isArray(n)
|
|
49
|
+
|
|
50
|
+
const exprEq = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
51
|
+
const localGetName = n => isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' ? n[1] : null
|
|
52
|
+
const f64Zero = n => isArr(n) && n[0] === 'f64.const' && Number(n[1]) === 0
|
|
53
|
+
|
|
54
|
+
// jz wraps every NaN-producing float builtin (Math.sqrt/min/max/…) in a
|
|
55
|
+
// canonicalizing select so a non-canonical NaN never crosses to JS:
|
|
56
|
+
// (select C X (T.ne X X)) — "use C where X is NaN, else X".
|
|
57
|
+
// The condition `X != X` is true iff X is NaN, so this shape is unambiguously
|
|
58
|
+
// the canonicalization idiom. C is the canonical-NaN value, materialized either
|
|
59
|
+
// inline (T.const) or hoisted into a const-pool global (global.get $__fcN) when
|
|
60
|
+
// reused. We splat C verbatim — faithful regardless of what C holds — so the
|
|
61
|
+
// recognizer never needs to resolve the global's value.
|
|
62
|
+
const isSplatConst = (n, constOp) =>
|
|
63
|
+
isArr(n) && (n[0] === constOp || n[0] === 'global.get')
|
|
64
|
+
|
|
65
|
+
// Match `(select C X (T.ne X X))`. Returns { val: X, C } or null.
|
|
66
|
+
function matchCanonSelect(sel, laneType) {
|
|
67
|
+
if (!isArr(sel) || sel[0] !== 'select') return null
|
|
68
|
+
const C = sel[1], val = sel[2], cond = sel[3]
|
|
69
|
+
const neOp = laneType === 'f32' ? 'f32.ne' : 'f64.ne'
|
|
70
|
+
if (!isSplatConst(C, LANE_INFO[laneType].constOp)) return null
|
|
71
|
+
if (!(isArr(cond) && cond[0] === neOp && exprEq(cond[1], val) && exprEq(cond[2], val))) return null
|
|
72
|
+
return { val, C }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Replace every `(local.tee N v)` with `(local.get N)` so a value that tee's its
|
|
76
|
+
// address in one place (the comparison) and reloads it in another (the chosen branch)
|
|
77
|
+
// compares structurally equal. Used only for matching — emission keeps the tee.
|
|
78
|
+
function normTee(n) {
|
|
79
|
+
if (!isArr(n)) return n
|
|
80
|
+
if (n[0] === 'local.tee' && n.length === 3) return ['local.get', n[1]]
|
|
81
|
+
return n.map(normTee)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Recognize an integer min/max reduction body. WASM has no scalar i32.min/max, so
|
|
85
|
+
// `m = max(m, a[i])` — written `Math.max(m,a[i])|0` or `a[i]>m?a[i]:m` — lowers, after
|
|
86
|
+
// the ToInt32-through-`?:` fold, to a select-shaped body:
|
|
87
|
+
// (local.set m (if (result i32) COND (then BR_T) (else BR_E))) [or the (select …) form]
|
|
88
|
+
// where {BR_T,BR_E} = {laneLoad, m} and COND is a signed i32 comparison of the two.
|
|
89
|
+
// Returns { exprNode, isMax } — exprNode is the lane expr carrying the address tee (fed
|
|
90
|
+
// to liftExprV); null when not a clean min/max. All four comparison directions × two
|
|
91
|
+
// branch orderings collapse to `isMax` below. gt/ge (and lt/le) are equivalent for the
|
|
92
|
+
// RESULT — equal operands tie to the same value — so only the direction axis matters.
|
|
93
|
+
function matchIntMinMaxReduce(rhs, accName) {
|
|
94
|
+
if (!isArr(rhs)) return null
|
|
95
|
+
let cond, T, E
|
|
96
|
+
if (rhs[0] === 'if') {
|
|
97
|
+
let i = 1
|
|
98
|
+
if (!(isArr(rhs[i]) && rhs[i][0] === 'result' && rhs[i][1] === 'i32')) return null
|
|
99
|
+
i++
|
|
100
|
+
if (rhs.length !== i + 3) return null
|
|
101
|
+
cond = rhs[i]
|
|
102
|
+
const thenB = rhs[i + 1], elseB = rhs[i + 2]
|
|
103
|
+
if (!(isArr(thenB) && thenB[0] === 'then' && thenB.length === 2)) return null
|
|
104
|
+
if (!(isArr(elseB) && elseB[0] === 'else' && elseB.length === 2)) return null
|
|
105
|
+
T = thenB[1]; E = elseB[1]
|
|
106
|
+
} else if (rhs[0] === 'select' && rhs.length === 4) {
|
|
107
|
+
T = rhs[1]; E = rhs[2]; cond = rhs[3] // (select a b c) = a if c else b
|
|
108
|
+
} else return null
|
|
109
|
+
// Which branch is the accumulator, which is the lane EXPR.
|
|
110
|
+
let exprBr, takeExprWhenTrue
|
|
111
|
+
if (isLocalGet(E, accName) && !isLocalGet(T, accName)) { exprBr = T; takeExprWhenTrue = true }
|
|
112
|
+
else if (isLocalGet(T, accName) && !isLocalGet(E, accName)) { exprBr = E; takeExprWhenTrue = false }
|
|
113
|
+
else return null
|
|
114
|
+
// Strip a boolean-normalizing `(i32.ne X 0)` around the comparison (as liftExprV does).
|
|
115
|
+
let cmp = cond
|
|
116
|
+
if (isArr(cmp) && cmp[0] === 'i32.ne' && isI32Const(cmp[2]) && cmp[2][1] === 0) cmp = cmp[1]
|
|
117
|
+
if (!isArr(cmp) || cmp.length !== 3) return null
|
|
118
|
+
const dir = { 'i32.gt_s': 'gt', 'i32.ge_s': 'gt', 'i32.lt_s': 'lt', 'i32.le_s': 'lt' }[cmp[0]]
|
|
119
|
+
if (!dir) return null
|
|
120
|
+
// Comparison operands must be {acc, EXPR}; take the non-acc side as the canonical lane
|
|
121
|
+
// expr (it carries the address tee). exprIsLeftOfCmp records its position.
|
|
122
|
+
let condExpr, exprIsLeftOfCmp
|
|
123
|
+
if (isLocalGet(cmp[2], accName)) { condExpr = cmp[1]; exprIsLeftOfCmp = true }
|
|
124
|
+
else if (isLocalGet(cmp[1], accName)) { condExpr = cmp[2]; exprIsLeftOfCmp = false }
|
|
125
|
+
else return null
|
|
126
|
+
// The compared expr and the chosen branch must be the SAME lane (tee vs reload aside).
|
|
127
|
+
if (!exprEq(normTee(condExpr), normTee(exprBr))) return null
|
|
128
|
+
// cond true ⟺ EXPR > acc ⇒ picking EXPR-when-true is a max; picking-when-false a min.
|
|
129
|
+
const predExprGreater = dir === 'gt' ? exprIsLeftOfCmp : !exprIsLeftOfCmp
|
|
130
|
+
return { exprNode: condExpr, isMax: takeExprWhenTrue === predExprGreater }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Match the un-flattened canon, emitted when a Math.* result feeds another op
|
|
134
|
+
// in expression position:
|
|
135
|
+
// (block (result T) (local.set $t CORE) (select C (local.get $t) (T.ne …)))
|
|
136
|
+
// Returns { core: CORE, C } or null.
|
|
137
|
+
function matchCanonBlock(blk, laneType) {
|
|
138
|
+
if (!isArr(blk) || blk[0] !== 'block') return null
|
|
139
|
+
let i = 1
|
|
140
|
+
if (typeof blk[i] === 'string' && blk[i].startsWith('$')) i++
|
|
141
|
+
if (!(isArr(blk[i]) && blk[i][0] === 'result')) return null
|
|
142
|
+
i++
|
|
143
|
+
if (blk.length - i !== 2) return null
|
|
144
|
+
const setStmt = blk[i]
|
|
145
|
+
if (!isArr(setStmt) || setStmt[0] !== 'local.set' || typeof setStmt[1] !== 'string') return null
|
|
146
|
+
const m = matchCanonSelect(blk[i + 1], laneType)
|
|
147
|
+
if (!m || !isLocalGet(m.val, setStmt[1])) return null
|
|
148
|
+
return { core: setStmt[2], C: m.C }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const matchF64MulLocals = n => {
|
|
152
|
+
if (!isArr(n) || n[0] !== 'f64.mul') return null
|
|
153
|
+
const a = localGetName(n[1])
|
|
154
|
+
const b = localGetName(n[2])
|
|
155
|
+
return a && b ? [a, b] : null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const matchAccumStep = (n, acc) => {
|
|
159
|
+
if (!isArr(n) || n[0] !== 'local.set' || n[1] !== acc) return null
|
|
160
|
+
const e = n[2]
|
|
161
|
+
if (!isArr(e) || e[0] !== 'f64.add') return null
|
|
162
|
+
if (localGetName(e[1]) === acc) return matchF64MulLocals(e[2])
|
|
163
|
+
if (localGetName(e[2]) === acc) return matchF64MulLocals(e[1])
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const matchDotStore = (n, acc) => {
|
|
168
|
+
if (!isArr(n) || n[0] !== 'local.set' || typeof n[1] !== 'string') return null
|
|
169
|
+
const e = n[2]
|
|
170
|
+
if (localGetName(e) === acc) return { out: n[1], addend: null }
|
|
171
|
+
if (!isArr(e) || e[0] !== 'f64.add') return null
|
|
172
|
+
if (localGetName(e[1]) === acc) return { out: n[1], addend: e[2] }
|
|
173
|
+
if (localGetName(e[2]) === acc) return { out: n[1], addend: e[1] }
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const matchF64DotSeq = (stmts, i) => {
|
|
178
|
+
const reset = stmts[i]
|
|
179
|
+
if (!isArr(reset) || reset[0] !== 'local.set' || typeof reset[1] !== 'string' || !f64Zero(reset[2])) return null
|
|
180
|
+
const acc = reset[1]
|
|
181
|
+
const left = [], right = []
|
|
182
|
+
for (let k = 0; k < 4; k++) {
|
|
183
|
+
const pair = matchAccumStep(stmts[i + 1 + k], acc)
|
|
184
|
+
if (!pair) return null
|
|
185
|
+
left.push(pair[0])
|
|
186
|
+
right.push(pair[1])
|
|
187
|
+
}
|
|
188
|
+
const store = matchDotStore(stmts[i + 5], acc)
|
|
189
|
+
return store ? { end: i + 6, acc, left, right, ...store } : null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const f64x2Pair = (lo, hi) => ['f64x2.replace_lane', 1, ['f64x2.splat', ['local.get', lo]], ['local.get', hi]]
|
|
193
|
+
|
|
194
|
+
const dotPairExpr = (a, pairs) => {
|
|
195
|
+
let expr = ['f64x2.mul', ['f64x2.splat', ['local.get', a[0]]], pairs[0]]
|
|
196
|
+
for (let i = 1; i < 4; i++) {
|
|
197
|
+
expr = ['f64x2.add', expr, ['f64x2.mul', ['f64x2.splat', ['local.get', a[i]]], pairs[i]]]
|
|
198
|
+
}
|
|
199
|
+
return expr
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const vectorizeStraightLineF64DotPairsIn = (node, fnLocals, freshIdRef, newLocalDecls) => {
|
|
203
|
+
if (!isArr(node)) return
|
|
204
|
+
for (let i = 0; i < node.length; i++) {
|
|
205
|
+
const child = node[i]
|
|
206
|
+
if (isArr(child)) vectorizeStraightLineF64DotPairsIn(child, fnLocals, freshIdRef, newLocalDecls)
|
|
207
|
+
}
|
|
208
|
+
const addendTemps = new Map()
|
|
209
|
+
const pairTemps = new Map()
|
|
210
|
+
for (let i = 0; i < node.length;) {
|
|
211
|
+
const a = matchF64DotSeq(node, i)
|
|
212
|
+
if (!a) { i++; continue }
|
|
213
|
+
const b = matchF64DotSeq(node, a.end)
|
|
214
|
+
if (!b || a.acc !== b.acc || !exprEq(a.left, b.left) || !exprEq(a.addend, b.addend) ||
|
|
215
|
+
fnLocals.get(a.out) !== 'f64' || fnLocals.get(b.out) !== 'f64') {
|
|
216
|
+
i++
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
const v = `$__dot2_${freshIdRef.next++}`
|
|
220
|
+
newLocalDecls.push(['local', v, 'v128'])
|
|
221
|
+
fnLocals.set(v, 'v128')
|
|
222
|
+
let prefix = []
|
|
223
|
+
let addend = a.addend
|
|
224
|
+
if (addend) {
|
|
225
|
+
const key = JSON.stringify(addend)
|
|
226
|
+
let tmp = addendTemps.get(key)
|
|
227
|
+
if (!tmp) {
|
|
228
|
+
tmp = `$__dotadd_${freshIdRef.next++}`
|
|
229
|
+
addendTemps.set(key, tmp)
|
|
230
|
+
newLocalDecls.push(['local', tmp, 'f64'])
|
|
231
|
+
fnLocals.set(tmp, 'f64')
|
|
232
|
+
prefix = [['local.set', tmp, addend]]
|
|
233
|
+
}
|
|
234
|
+
addend = ['local.get', tmp]
|
|
235
|
+
}
|
|
236
|
+
const pairs = []
|
|
237
|
+
for (let k = 0; k < 4; k++) {
|
|
238
|
+
const key = `${a.right[k]}\0${b.right[k]}`
|
|
239
|
+
let tmp = pairTemps.get(key)
|
|
240
|
+
if (!tmp) {
|
|
241
|
+
tmp = `$__dotpair_${freshIdRef.next++}`
|
|
242
|
+
pairTemps.set(key, tmp)
|
|
243
|
+
newLocalDecls.push(['local', tmp, 'v128'])
|
|
244
|
+
fnLocals.set(tmp, 'v128')
|
|
245
|
+
prefix.push(['local.set', tmp, f64x2Pair(a.right[k], b.right[k])])
|
|
246
|
+
}
|
|
247
|
+
pairs.push(['local.get', tmp])
|
|
248
|
+
}
|
|
249
|
+
const dot = dotPairExpr(a.left, pairs)
|
|
250
|
+
const expr = addend ? ['f64x2.add', dot, ['f64x2.splat', addend]] : dot
|
|
251
|
+
node.splice(i, b.end - i,
|
|
252
|
+
...prefix,
|
|
253
|
+
['local.set', v, expr],
|
|
254
|
+
['local.set', a.out, ['f64x2.extract_lane', 0, ['local.get', v]]],
|
|
255
|
+
['local.set', b.out, ['f64x2.extract_lane', 1, ['local.get', v]]],
|
|
256
|
+
)
|
|
257
|
+
i += prefix.length + 3
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- Lane type tables ------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const LANE_INFO = {
|
|
264
|
+
i8: { lanes: 16, strideLog2: 0, stride: 1, splat: 'i8x16.splat', constOp: 'i32.const' },
|
|
265
|
+
i16: { lanes: 8, strideLog2: 1, stride: 2, splat: 'i16x8.splat', constOp: 'i32.const' },
|
|
266
|
+
i32: { lanes: 4, strideLog2: 2, stride: 4, splat: 'i32x4.splat', constOp: 'i32.const' },
|
|
267
|
+
i64: { lanes: 2, strideLog2: 3, stride: 8, splat: 'i64x2.splat', constOp: 'i64.const' },
|
|
268
|
+
f32: { lanes: 4, strideLog2: 2, stride: 4, splat: 'f32x4.splat', constOp: 'f32.const' },
|
|
269
|
+
f64: { lanes: 2, strideLog2: 3, stride: 8, splat: 'f64x2.splat', constOp: 'f64.const' },
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Narrow loads/stores (i32.load8_u etc.) define i8 / i16 lane types — values
|
|
273
|
+
// computed in i32 then truncated by store{8,16}, which matches i{8,16}xN wrap
|
|
274
|
+
// semantics exactly.
|
|
275
|
+
const LOAD_OPS = {
|
|
276
|
+
'i32.load8_u': 'i8', 'i32.load8_s': 'i8',
|
|
277
|
+
'i32.load16_u': 'i16','i32.load16_s': 'i16',
|
|
278
|
+
'i32.load': 'i32', 'i64.load': 'i64', 'f32.load': 'f32', 'f64.load': 'f64',
|
|
279
|
+
}
|
|
280
|
+
const STORE_OPS = {
|
|
281
|
+
'i32.store8': 'i8', 'i32.store16': 'i16',
|
|
282
|
+
'i32.store': 'i32', 'i64.store': 'i64', 'f32.store': 'f32', 'f64.store': 'f64',
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// scalar op → SIMD op. shamtScalar:true means second operand stays scalar i32.
|
|
286
|
+
//
|
|
287
|
+
// For i8/i16 lanes the SCALAR ops are i32.* — wasm has no native i8/i16 ops,
|
|
288
|
+
// values flow as i32 and the trailing store{8,16} truncates. i{8,16}x{N}.add
|
|
289
|
+
// wraps within each lane the same way, so the observable result matches.
|
|
290
|
+
// Note: wasm SIMD has no i8x16.mul, so multiplication on byte arrays bails.
|
|
291
|
+
const LANE_PURE = {
|
|
292
|
+
// Right shifts intentionally omitted for narrow lanes: scalar emits
|
|
293
|
+
// i32.shr_{s,u} on a load8/load16 i32 (zero- or sign-extended), while
|
|
294
|
+
// i{8,16}x{N}.shr_{s,u} treats lanes as their narrow type. The two diverge
|
|
295
|
+
// when load and shift signedness mismatch (e.g. load8_u + shr_s on byte
|
|
296
|
+
// 0xFF: scalar=0x7F, SIMD=0xFF). Safe set excludes shr_*.
|
|
297
|
+
i8: new Map([
|
|
298
|
+
['i32.add', { simd: 'i8x16.add' }],
|
|
299
|
+
['i32.sub', { simd: 'i8x16.sub' }],
|
|
300
|
+
['i32.and', { simd: 'v128.and' }],
|
|
301
|
+
['i32.or', { simd: 'v128.or' }],
|
|
302
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
303
|
+
['i32.shl', { simd: 'i8x16.shl', shamtScalar: true }],
|
|
304
|
+
]),
|
|
305
|
+
i16: new Map([
|
|
306
|
+
['i32.add', { simd: 'i16x8.add' }],
|
|
307
|
+
['i32.sub', { simd: 'i16x8.sub' }],
|
|
308
|
+
['i32.mul', { simd: 'i16x8.mul' }],
|
|
309
|
+
['i32.and', { simd: 'v128.and' }],
|
|
310
|
+
['i32.or', { simd: 'v128.or' }],
|
|
311
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
312
|
+
['i32.shl', { simd: 'i16x8.shl', shamtScalar: true }],
|
|
313
|
+
]),
|
|
314
|
+
i32: new Map([
|
|
315
|
+
['i32.add', { simd: 'i32x4.add' }],
|
|
316
|
+
['i32.sub', { simd: 'i32x4.sub' }],
|
|
317
|
+
['i32.mul', { simd: 'i32x4.mul' }],
|
|
318
|
+
['i32.and', { simd: 'v128.and' }],
|
|
319
|
+
['i32.or', { simd: 'v128.or' }],
|
|
320
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
321
|
+
['i32.shl', { simd: 'i32x4.shl', shamtScalar: true }],
|
|
322
|
+
['i32.shr_s', { simd: 'i32x4.shr_s', shamtScalar: true }],
|
|
323
|
+
['i32.shr_u', { simd: 'i32x4.shr_u', shamtScalar: true }],
|
|
324
|
+
]),
|
|
325
|
+
i64: new Map([
|
|
326
|
+
['i64.add', { simd: 'i64x2.add' }],
|
|
327
|
+
['i64.sub', { simd: 'i64x2.sub' }],
|
|
328
|
+
['i64.mul', { simd: 'i64x2.mul' }],
|
|
329
|
+
['i64.and', { simd: 'v128.and' }],
|
|
330
|
+
['i64.or', { simd: 'v128.or' }],
|
|
331
|
+
['i64.xor', { simd: 'v128.xor' }],
|
|
332
|
+
['i64.shl', { simd: 'i64x2.shl', shamtScalar: true }],
|
|
333
|
+
['i64.shr_s', { simd: 'i64x2.shr_s', shamtScalar: true }],
|
|
334
|
+
['i64.shr_u', { simd: 'i64x2.shr_u', shamtScalar: true }],
|
|
335
|
+
]),
|
|
336
|
+
f32: new Map([
|
|
337
|
+
['f32.add', { simd: 'f32x4.add' }],
|
|
338
|
+
['f32.sub', { simd: 'f32x4.sub' }],
|
|
339
|
+
['f32.mul', { simd: 'f32x4.mul' }],
|
|
340
|
+
['f32.div', { simd: 'f32x4.div' }],
|
|
341
|
+
['f32.min', { simd: 'f32x4.min' }],
|
|
342
|
+
['f32.max', { simd: 'f32x4.max' }],
|
|
343
|
+
['f32.neg', { simd: 'f32x4.neg' }],
|
|
344
|
+
['f32.abs', { simd: 'f32x4.abs' }],
|
|
345
|
+
['f32.sqrt', { simd: 'f32x4.sqrt' }],
|
|
346
|
+
]),
|
|
347
|
+
f64: new Map([
|
|
348
|
+
['f64.add', { simd: 'f64x2.add' }],
|
|
349
|
+
['f64.sub', { simd: 'f64x2.sub' }],
|
|
350
|
+
['f64.mul', { simd: 'f64x2.mul' }],
|
|
351
|
+
['f64.div', { simd: 'f64x2.div' }],
|
|
352
|
+
['f64.min', { simd: 'f64x2.min' }],
|
|
353
|
+
['f64.max', { simd: 'f64x2.max' }],
|
|
354
|
+
['f64.neg', { simd: 'f64x2.neg' }],
|
|
355
|
+
['f64.abs', { simd: 'f64x2.abs' }],
|
|
356
|
+
['f64.sqrt', { simd: 'f64x2.sqrt' }],
|
|
357
|
+
]),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Horizontal reductions: associative+commutative ops applied to one
|
|
361
|
+
// loop-carried accumulator. Each entry maps the SCALAR op (which is also
|
|
362
|
+
// the op used to combine the SIMD result back into the accumulator at the
|
|
363
|
+
// end) to its SIMD lane op, lane extractor, and identity element.
|
|
364
|
+
//
|
|
365
|
+
// Floats (add, mul) are not strictly associative — vectorized order produces
|
|
366
|
+
// ulp-level differences from scalar order. Acceptable for typical use
|
|
367
|
+
// (reductions over typed arrays of well-conditioned data); strict-equal
|
|
368
|
+
// callers must keep the pass off.
|
|
369
|
+
//
|
|
370
|
+
// Integer mul (`p *= a[i]`) IS associative+commutative mod 2³² / 2⁶⁴, so its
|
|
371
|
+
// vectorization is value-exact. Identity is 1 (the multiplicative neutral).
|
|
372
|
+
//
|
|
373
|
+
// Narrow lanes (i8/i16) intentionally absent: `s += a[i]` with a u8/u16
|
|
374
|
+
// load expands the value to i32 before the add, so the accumulator's lane
|
|
375
|
+
// type is always wider than the load's element type. That widening would
|
|
376
|
+
// require pairwise/extending-add ops (i16x8.extadd_pairwise_*) — separate
|
|
377
|
+
// recognizer. Integer min/max likewise: WASM has no scalar i32.min, so they
|
|
378
|
+
// arrive as a `select`, not a binary op — a separate recognizer branch.
|
|
379
|
+
const REDUCE_OPS = {
|
|
380
|
+
i32: {
|
|
381
|
+
'i32.add': { simd: 'i32x4.add', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
382
|
+
'i32.mul': { simd: 'i32x4.mul', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 1] },
|
|
383
|
+
'i32.xor': { simd: 'v128.xor', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
384
|
+
'i32.and': { simd: 'v128.and', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', -1] },
|
|
385
|
+
'i32.or': { simd: 'v128.or', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
386
|
+
},
|
|
387
|
+
i64: {
|
|
388
|
+
'i64.add': { simd: 'i64x2.add', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
389
|
+
'i64.mul': { simd: 'i64x2.mul', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 1] },
|
|
390
|
+
'i64.xor': { simd: 'v128.xor', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
391
|
+
'i64.and': { simd: 'v128.and', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', -1] },
|
|
392
|
+
'i64.or': { simd: 'v128.or', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
393
|
+
},
|
|
394
|
+
f32: {
|
|
395
|
+
'f32.add': { simd: 'f32x4.add', extract: 'f32x4.extract_lane', laneType: 'f32', constNode: ['f32.const', 0] },
|
|
396
|
+
'f32.mul': { simd: 'f32x4.mul', extract: 'f32x4.extract_lane', laneType: 'f32', constNode: ['f32.const', 1] },
|
|
397
|
+
},
|
|
398
|
+
f64: {
|
|
399
|
+
'f64.add': { simd: 'f64x2.add', extract: 'f64x2.extract_lane', laneType: 'f64', constNode: ['f64.const', 0] },
|
|
400
|
+
'f64.mul': { simd: 'f64x2.mul', extract: 'f64x2.extract_lane', laneType: 'f64', constNode: ['f64.const', 1] },
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Widening byte/short sums: an i32 accumulator fed by ONE bare narrow load
|
|
405
|
+
// (`s += u8[i]`). The lane data is i8/i16 but the accumulator is i32, so the
|
|
406
|
+
// plain lane-add path can't apply — instead each 16-byte vector collapses via
|
|
407
|
+
// extadd_pairwise into i32x4 partial sums. VALUE-EXACT mod 2³² (unlike float
|
|
408
|
+
// reductions): pairwise intermediates can't overflow (2×255 < 2¹⁶, 2×(−128)
|
|
409
|
+
// fits i16; the i16→i32 step extends before adding), and wrap-add is
|
|
410
|
+
// associative+commutative. Restricted to a BARE load: arithmetic on the
|
|
411
|
+
// narrow lanes before widening would wrap at lane width where the scalar
|
|
412
|
+
// code widens first.
|
|
413
|
+
const WIDEN_LOADS = {
|
|
414
|
+
'i32.load8_u': { laneType: 'i8', steps: ['i16x8.extadd_pairwise_i8x16_u', 'i32x4.extadd_pairwise_i16x8_u'] },
|
|
415
|
+
'i32.load8_s': { laneType: 'i8', steps: ['i16x8.extadd_pairwise_i8x16_s', 'i32x4.extadd_pairwise_i16x8_s'] },
|
|
416
|
+
'i32.load16_u': { laneType: 'i16', steps: ['i32x4.extadd_pairwise_i16x8_u'] },
|
|
417
|
+
'i32.load16_s': { laneType: 'i16', steps: ['i32x4.extadd_pairwise_i16x8_s'] },
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Widening min/max over a BARE narrow load (`m = Math.max(m, u8[i])` with an
|
|
421
|
+
// i32 accumulator). Unlike the widening SUM there is no overflow concern:
|
|
422
|
+
// min/max at the load's own lane width over its own sign is value-exact, so
|
|
423
|
+
// the fold stays at lane width (16/8 lanes per vector) and only the final
|
|
424
|
+
// horizontal merge widens, via the sign-matched extract. Identity seeds the
|
|
425
|
+
// vector accumulator with the op's neutral: type-min for max, type-max for min.
|
|
426
|
+
const MINMAX_WIDEN = {
|
|
427
|
+
'i32.load8_u': { pre: 'i8x16', sign: 'u', laneType: 'i8', lo: 0, hi: 255 },
|
|
428
|
+
'i32.load8_s': { pre: 'i8x16', sign: 's', laneType: 'i8', lo: -128, hi: 127 },
|
|
429
|
+
'i32.load16_u': { pre: 'i16x8', sign: 'u', laneType: 'i16', lo: 0, hi: 65535 },
|
|
430
|
+
'i32.load16_s': { pre: 'i16x8', sign: 's', laneType: 'i16', lo: -32768, hi: 32767 },
|
|
431
|
+
}
|
|
432
|
+
// jz's number model converts narrow loads to f64 before Math.min/max, so the
|
|
433
|
+
// canon reduce arrives as (f64.max acc (f64.convert_i32_x LOAD)). The convert
|
|
434
|
+
// sign must match the load sign for the lane fold to be value-exact.
|
|
435
|
+
const MINMAX_CVT = { 'f64.convert_i32_u': 'u', 'f64.convert_i32_s': 's' }
|
|
436
|
+
|
|
437
|
+
// op-name → REDUCE entry across all lane types (the op-name itself encodes
|
|
438
|
+
// the lane type prefix, e.g. `i32.add` ⇒ i32 lanes).
|
|
439
|
+
const REDUCE_OP_LOOKUP = (() => {
|
|
440
|
+
const m = new Map()
|
|
441
|
+
for (const lt of Object.keys(REDUCE_OPS))
|
|
442
|
+
for (const op of Object.keys(REDUCE_OPS[lt]))
|
|
443
|
+
m.set(op, REDUCE_OPS[lt][op])
|
|
444
|
+
return m
|
|
445
|
+
})()
|
|
446
|
+
|
|
447
|
+
// Min/max reductions (`m = Math.max(m, a[i])`). jz wraps every Math.min/max in
|
|
448
|
+
// a NaN-canonicalizing select, so these arrive as a TWO-statement body —
|
|
449
|
+
// (local.set $cn (OP (local.get $acc) EXPR))
|
|
450
|
+
// (local.set $acc (select C (local.get $cn) (OP-type.ne $cn $cn)))
|
|
451
|
+
// — handled separately from the bare single-statement reductions above.
|
|
452
|
+
//
|
|
453
|
+
// max/min ARE associative and commutative (exact reassociation, unlike add),
|
|
454
|
+
// so vectorization is value-exact, INCLUDING NaN: f64x2.max/min propagate a
|
|
455
|
+
// NaN lane just as scalar does, and we re-apply the canon to the merged result
|
|
456
|
+
// so the final NaN bit pattern is canonical even when N is a multiple of LANES
|
|
457
|
+
// (zero tail iterations). Identity is the op's annihilator-free neutral:
|
|
458
|
+
// -inf for max, +inf for min.
|
|
459
|
+
const REDUCE_CANON = {
|
|
460
|
+
'f64.max': { simd: 'f64x2.max', extract: 'f64x2.extract_lane', laneType: 'f64', identity: ['f64.const', '-inf'] },
|
|
461
|
+
'f64.min': { simd: 'f64x2.min', extract: 'f64x2.extract_lane', laneType: 'f64', identity: ['f64.const', 'inf'] },
|
|
462
|
+
'f32.max': { simd: 'f32x4.max', extract: 'f32x4.extract_lane', laneType: 'f32', identity: ['f32.const', '-inf'] },
|
|
463
|
+
'f32.min': { simd: 'f32x4.min', extract: 'f32x4.extract_lane', laneType: 'f32', identity: ['f32.const', 'inf'] },
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Scalar comparison op → SIMD lane comparison, per lane type. Used to vectorize a
|
|
467
|
+
// conditional map `buf[i] = cond ? X : Y`, which jz lowers to `(if (result T) COND
|
|
468
|
+
// (then X)(else Y))`: COND becomes an all-ones/all-zeros lane mask fed to
|
|
469
|
+
// `v128.bitselect`. NaN behaves identically lane-wise — every ordered compare is
|
|
470
|
+
// false on a NaN operand in both scalar and SIMD, and `ne` is true — so no
|
|
471
|
+
// canonicalization is needed. i64x2 has no unsigned compares in baseline SIMD, so
|
|
472
|
+
// those simply aren't listed (the loop stays scalar).
|
|
473
|
+
const LANE_COMPARE = {
|
|
474
|
+
f64: { 'f64.eq': 'f64x2.eq', 'f64.ne': 'f64x2.ne', 'f64.lt': 'f64x2.lt', 'f64.gt': 'f64x2.gt', 'f64.le': 'f64x2.le', 'f64.ge': 'f64x2.ge' },
|
|
475
|
+
f32: { 'f32.eq': 'f32x4.eq', 'f32.ne': 'f32x4.ne', 'f32.lt': 'f32x4.lt', 'f32.gt': 'f32x4.gt', 'f32.le': 'f32x4.le', 'f32.ge': 'f32x4.ge' },
|
|
476
|
+
i32: { 'i32.eq': 'i32x4.eq', 'i32.ne': 'i32x4.ne', 'i32.lt_s': 'i32x4.lt_s', 'i32.lt_u': 'i32x4.lt_u', 'i32.gt_s': 'i32x4.gt_s', 'i32.gt_u': 'i32x4.gt_u', 'i32.le_s': 'i32x4.le_s', 'i32.le_u': 'i32x4.le_u', 'i32.ge_s': 'i32x4.ge_s', 'i32.ge_u': 'i32x4.ge_u' },
|
|
477
|
+
i64: { 'i64.eq': 'i64x2.eq', 'i64.ne': 'i64x2.ne', 'i64.lt_s': 'i64x2.lt_s', 'i64.gt_s': 'i64x2.gt_s', 'i64.le_s': 'i64x2.le_s', 'i64.ge_s': 'i64x2.ge_s' },
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---- Recognizer ------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
function isLocalGet(node, name) {
|
|
483
|
+
return isArr(node) && node[0] === 'local.get' && (name == null || node[1] === name)
|
|
484
|
+
}
|
|
485
|
+
function isI32Const(node) {
|
|
486
|
+
return isArr(node) && node[0] === 'i32.const'
|
|
487
|
+
}
|
|
488
|
+
function constNum(node) {
|
|
489
|
+
if (!isI32Const(node)) return null
|
|
490
|
+
const v = node[1]
|
|
491
|
+
return typeof v === 'number' ? v : (typeof v === 'string' ? parseInt(v, 10) : null)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Match increment shape `(local.set $X (i32.add (local.get $X) (i32.const 1)))`.
|
|
496
|
+
* Returns $X or null.
|
|
497
|
+
*/
|
|
498
|
+
function matchInc1(stmt) {
|
|
499
|
+
if (!isArr(stmt) || stmt[0] !== 'local.set' || stmt.length !== 3) return null
|
|
500
|
+
const x = stmt[1]
|
|
501
|
+
const v = stmt[2]
|
|
502
|
+
if (!isArr(v) || v[0] !== 'i32.add' || v.length !== 3) return null
|
|
503
|
+
if (!isLocalGet(v[1], x)) return null
|
|
504
|
+
if (constNum(v[2]) !== 1) return null
|
|
505
|
+
return x
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Match `(br_if $LABEL (i32.eqz (i32.lt_{s,u} (local.get $I) BOUND)))`.
|
|
510
|
+
* Returns { ind, bound } or null.
|
|
511
|
+
*/
|
|
512
|
+
function matchExitBrIf(stmt, label) {
|
|
513
|
+
if (!isArr(stmt) || stmt[0] !== 'br_if' || stmt[1] !== label) return null
|
|
514
|
+
const cond = stmt[2]
|
|
515
|
+
if (!isArr(cond) || cond[0] !== 'i32.eqz') return null
|
|
516
|
+
const cmp = cond[1]
|
|
517
|
+
if (!isArr(cmp) || (cmp[0] !== 'i32.lt_s' && cmp[0] !== 'i32.lt_u')) return null
|
|
518
|
+
if (!isLocalGet(cmp[1])) return null
|
|
519
|
+
return { ind: cmp[1][1], bound: cmp[2] }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Walk node, collect set of local names that are written via local.set/local.tee
|
|
524
|
+
* anywhere within. Used to detect loop-invariant locals.
|
|
525
|
+
*/
|
|
526
|
+
function collectWrites(node, out) {
|
|
527
|
+
if (!isArr(node)) return
|
|
528
|
+
const op = node[0]
|
|
529
|
+
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') {
|
|
530
|
+
out.add(node[1])
|
|
531
|
+
}
|
|
532
|
+
for (let i = 0; i < node.length; i++) collectWrites(node[i], out)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Return the FIRST kind of access for `name` in straight-line walk order.
|
|
537
|
+
* 'write' — local.set/local.tee seen first
|
|
538
|
+
* 'read' — local.get seen first
|
|
539
|
+
* null — not referenced
|
|
540
|
+
*/
|
|
541
|
+
function firstAccess(node, name) {
|
|
542
|
+
if (!isArr(node)) return null
|
|
543
|
+
const op = node[0]
|
|
544
|
+
// Walk children first — operands evaluate before the op. For local.set/tee
|
|
545
|
+
// the VALUE child (idx 2) runs before the write, so a `local.get name` in
|
|
546
|
+
// the value of `local.set name` is a read-before-write.
|
|
547
|
+
if ((op === 'local.set' || op === 'local.tee') && node[1] === name) {
|
|
548
|
+
if (node.length >= 3) {
|
|
549
|
+
const r = firstAccess(node[2], name)
|
|
550
|
+
if (r) return r
|
|
551
|
+
}
|
|
552
|
+
return 'write'
|
|
553
|
+
}
|
|
554
|
+
if (op === 'local.get' && node[1] === name) return 'read'
|
|
555
|
+
for (let i = 1; i < node.length; i++) {
|
|
556
|
+
const r = firstAccess(node[i], name)
|
|
557
|
+
if (r) return r
|
|
558
|
+
}
|
|
559
|
+
return null
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Match the index-offset operand of a lane address — the part that scales the
|
|
564
|
+
* induction variable inside `(i32.add base OFFSET)`. OFFSET is one of:
|
|
565
|
+
* (i32.shl (local.get IND) (i32.const K)) → strideLog2 K
|
|
566
|
+
* (local.get IND) → strideLog2 0
|
|
567
|
+
* (local.tee $T <either of the above>) → records $T
|
|
568
|
+
* (local.get $T) where $T is a recorded offset-tee → that tee's strideLog2
|
|
569
|
+
*
|
|
570
|
+
* The tee'd form arises from CSE: a map loop `b[i] = f(a[i])` over two distinct
|
|
571
|
+
* base pointers shares one `i << K` offset (`(local.tee $T (i32.shl i K))` in
|
|
572
|
+
* the first address, `(local.get $T)` in the second). `offsetTees` (Map
|
|
573
|
+
* name→strideLog2) carries that across calls.
|
|
574
|
+
*
|
|
575
|
+
* Returns { strideLog2, teeName?: string } or null.
|
|
576
|
+
*/
|
|
577
|
+
function matchLaneOffset(off, ind, offsetTees) {
|
|
578
|
+
if (isArr(off) && off[0] === 'local.get' && typeof off[1] === 'string' &&
|
|
579
|
+
offsetTees && offsetTees.has(off[1])) {
|
|
580
|
+
return { strideLog2: offsetTees.get(off[1]), teeName: null }
|
|
581
|
+
}
|
|
582
|
+
let teeName = null
|
|
583
|
+
let n = off
|
|
584
|
+
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) { teeName = n[1]; n = n[2] }
|
|
585
|
+
// (i32.shl (local.get ind) (i32.const K))
|
|
586
|
+
if (isArr(n) && n[0] === 'i32.shl' && n.length === 3 && isLocalGet(n[1], ind)) {
|
|
587
|
+
const k = constNum(n[2])
|
|
588
|
+
if (k != null && k >= 0 && k <= 3) return { strideLog2: k, teeName }
|
|
589
|
+
}
|
|
590
|
+
// (local.get ind) — stride 1
|
|
591
|
+
if (isLocalGet(n, ind)) return { strideLog2: 0, teeName }
|
|
592
|
+
return null
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Match an address expression `(i32.add base OFFSET)`, with optional outer
|
|
597
|
+
* `(local.tee $A ...)`. OFFSET is matched by matchLaneOffset (which also
|
|
598
|
+
* accepts a CSE'd `(local.tee $T (i32.shl ind K))` / `(local.get $T)` pair).
|
|
599
|
+
* Also accepts `(local.get $A)` when $A is a previously-recorded address tee.
|
|
600
|
+
*
|
|
601
|
+
* Returns { strideLog2, base, teeName?, offsetTeeName?, viaLocal? } or null.
|
|
602
|
+
* `strideLog2` = K for i32.shl form, 0 for plain add form.
|
|
603
|
+
* `base` is the loop-invariant base subtree.
|
|
604
|
+
*/
|
|
605
|
+
function matchLaneAddr(addr, ind, addrLocals, offsetTees) {
|
|
606
|
+
let teeName = null
|
|
607
|
+
let n = addr
|
|
608
|
+
// (local.get $A) where $A holds a previously-tee'd FULL lane-address.
|
|
609
|
+
if (isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' && addrLocals && addrLocals.has(n[1])) {
|
|
610
|
+
const e = addrLocals.get(n[1])
|
|
611
|
+
return { strideLog2: e.strideLog2, base: e.base, teeName: null, viaLocal: n[1] }
|
|
612
|
+
}
|
|
613
|
+
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) {
|
|
614
|
+
teeName = n[1]
|
|
615
|
+
n = n[2]
|
|
616
|
+
}
|
|
617
|
+
if (!isArr(n) || n[0] !== 'i32.add' || n.length !== 3) return null
|
|
618
|
+
const a = n[1], b = n[2]
|
|
619
|
+
const off = matchLaneOffset(b, ind, offsetTees)
|
|
620
|
+
if (!off) return null
|
|
621
|
+
return { strideLog2: off.strideLog2, base: a, teeName, offsetTeeName: off.teeName }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* A scalar i32 local that is ONLY ever assigned a lane offset — `(i32.shl ind K)`
|
|
626
|
+
* (or bare `ind` for stride 0) — is a CSE'd offset shared across base pointers.
|
|
627
|
+
* Returns the consistent strideLog2, or null if any write to it diverges.
|
|
628
|
+
* This soundness check backs every `(local.get $T)` resolved via `offsetTees`.
|
|
629
|
+
*/
|
|
630
|
+
function _offsetLocalStride(body, name, ind) {
|
|
631
|
+
let stride = null, found = false, ok = true
|
|
632
|
+
function walk(n) {
|
|
633
|
+
if (!isArr(n)) return
|
|
634
|
+
if ((n[0] === 'local.tee' || n[0] === 'local.set') && n[1] === name && n.length === 3) {
|
|
635
|
+
found = true
|
|
636
|
+
const v = n[2]
|
|
637
|
+
let k = null
|
|
638
|
+
if (isArr(v) && v[0] === 'i32.shl' && v.length === 3 && isLocalGet(v[1], ind)) {
|
|
639
|
+
k = constNum(v[2])
|
|
640
|
+
if (k == null || k < 0 || k > 3) ok = false
|
|
641
|
+
} else if (isLocalGet(v, ind)) {
|
|
642
|
+
k = 0
|
|
643
|
+
} else ok = false
|
|
644
|
+
if (k != null) {
|
|
645
|
+
if (stride == null) stride = k
|
|
646
|
+
else if (stride !== k) ok = false
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
650
|
+
}
|
|
651
|
+
for (const s of body) walk(s)
|
|
652
|
+
return found && ok ? stride : null
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// True if any node in the tree writes a global. When false for a loop body,
|
|
656
|
+
// every `global.get` inside it is loop-invariant — safe to splat for SIMD.
|
|
657
|
+
const hasGlobalSet = (node) => {
|
|
658
|
+
if (!isArr(node)) return false
|
|
659
|
+
if (node[0] === 'global.set') return true
|
|
660
|
+
for (let i = 1; i < node.length; i++) if (hasGlobalSet(node[i])) return true
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ---- Recognize a (block (loop)) pair --------------------------------------
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Try to vectorize the inner loop. Returns the replacement node array
|
|
668
|
+
* (synthetic outer block) or null on no match.
|
|
669
|
+
*/
|
|
670
|
+
function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
671
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
672
|
+
// Find label and inner loop.
|
|
673
|
+
let blockLabel = null
|
|
674
|
+
let loopIdx = -1, loopNode = null
|
|
675
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
676
|
+
const c = blockNode[i]
|
|
677
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) {
|
|
678
|
+
blockLabel = c; continue
|
|
679
|
+
}
|
|
680
|
+
if (isArr(c) && c[0] === 'loop') {
|
|
681
|
+
if (loopNode) return null // multiple loops
|
|
682
|
+
loopIdx = i; loopNode = c
|
|
683
|
+
} else if (isArr(c)) {
|
|
684
|
+
return null // foreign content alongside the loop
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!loopNode || !blockLabel) return null
|
|
688
|
+
|
|
689
|
+
// Loop layout: ['loop', '$label', ...stmts]
|
|
690
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
691
|
+
if (!loopLabel) return null
|
|
692
|
+
|
|
693
|
+
// Find induction increment + back-branch at the END.
|
|
694
|
+
let endIdx = loopNode.length - 1
|
|
695
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
696
|
+
const incIdx = endIdx - 1
|
|
697
|
+
const incVar = matchInc1(loopNode[incIdx])
|
|
698
|
+
if (!incVar) return null
|
|
699
|
+
|
|
700
|
+
// First stmt must be the exit br_if.
|
|
701
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
702
|
+
if (!exitInfo) return null
|
|
703
|
+
if (exitInfo.ind !== incVar) return null
|
|
704
|
+
|
|
705
|
+
// Body = stmts between exit and increment.
|
|
706
|
+
const body = []
|
|
707
|
+
for (let i = 3; i < incIdx; i++) body.push(loopNode[i])
|
|
708
|
+
|
|
709
|
+
// Bound must be loop-invariant. For now, accept (local.get $L) where $L
|
|
710
|
+
// is declared but not written inside the body, OR (i32.const N).
|
|
711
|
+
let bound = exitInfo.bound
|
|
712
|
+
let boundLocal = null
|
|
713
|
+
if (isArr(bound) && bound[0] === 'local.get' && typeof bound[1] === 'string') {
|
|
714
|
+
boundLocal = bound[1]
|
|
715
|
+
} else if (isI32Const(bound)) {
|
|
716
|
+
// ok
|
|
717
|
+
} else {
|
|
718
|
+
return null
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Detect lane type from the FIRST load in body.
|
|
722
|
+
let laneType = null
|
|
723
|
+
let stride = -1
|
|
724
|
+
const loadStoreSites = [] // {parent, idx, kind:'load'|'store'}
|
|
725
|
+
// Address tees: name → {strideLog2, base}. A `(local.tee NAME (lane-addr))`
|
|
726
|
+
// both validates the load's address AND records NAME so the matching store's
|
|
727
|
+
// `(local.get NAME)` is accepted as the same lane address.
|
|
728
|
+
const addrLocals = new Map()
|
|
729
|
+
// Offset tees: name → strideLog2. A CSE'd `i << K` shared across base
|
|
730
|
+
// pointers (map loops over distinct arrays). Soundness re-checked post-scan.
|
|
731
|
+
const offsetTees = new Map()
|
|
732
|
+
|
|
733
|
+
function scanForLoadsStores(node, parent, pi) {
|
|
734
|
+
if (!isArr(node)) return true
|
|
735
|
+
const op = node[0]
|
|
736
|
+
if (LOAD_OPS[op]) {
|
|
737
|
+
if (laneType == null) {
|
|
738
|
+
laneType = LOAD_OPS[op]
|
|
739
|
+
stride = LANE_INFO[laneType].stride
|
|
740
|
+
} else if (LOAD_OPS[op] !== laneType) {
|
|
741
|
+
return false
|
|
742
|
+
}
|
|
743
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
744
|
+
if (!m) return false
|
|
745
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
746
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
747
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
748
|
+
loadStoreSites.push({ parent, idx: pi, kind: 'load' })
|
|
749
|
+
return true
|
|
750
|
+
}
|
|
751
|
+
if (STORE_OPS[op]) {
|
|
752
|
+
const sty = STORE_OPS[op]
|
|
753
|
+
if (laneType != null && sty !== laneType) return false
|
|
754
|
+
if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
|
|
755
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
756
|
+
if (!m) return false
|
|
757
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
758
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
759
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
760
|
+
loadStoreSites.push({ parent, idx: pi, kind: 'store' })
|
|
761
|
+
// Recurse into VALUE child (idx 2) — it's data, not address.
|
|
762
|
+
if (!scanForLoadsStores(node[2], node, 2)) return false
|
|
763
|
+
return true
|
|
764
|
+
}
|
|
765
|
+
// local.set/tee of an address local outside a load/store context (e.g.
|
|
766
|
+
// `(local.set $a (i32.add base (i32.shl i 2)))` as a standalone stmt) —
|
|
767
|
+
// record so a later `(local.get $a)` resolves.
|
|
768
|
+
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string' && node.length === 3) {
|
|
769
|
+
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals, offsetTees)
|
|
770
|
+
if (valM && valM.teeName) {
|
|
771
|
+
addrLocals.set(valM.teeName, { strideLog2: valM.strideLog2, base: valM.base })
|
|
772
|
+
}
|
|
773
|
+
// Standalone offset compute: `(local.set $t (i32.shl i K))`.
|
|
774
|
+
const offM = matchLaneOffset(node[2], incVar, offsetTees)
|
|
775
|
+
if (offM) offsetTees.set(node[1], offM.strideLog2)
|
|
776
|
+
}
|
|
777
|
+
// Recurse into all children
|
|
778
|
+
for (let i = 1; i < node.length; i++) {
|
|
779
|
+
if (!scanForLoadsStores(node[i], node, i)) return false
|
|
780
|
+
}
|
|
781
|
+
return true
|
|
782
|
+
}
|
|
783
|
+
for (const stmt of body) {
|
|
784
|
+
if (!scanForLoadsStores(stmt, null, -1)) return null
|
|
785
|
+
}
|
|
786
|
+
if (body.some(hasGlobalSet)) return null // a global write breaks the "global.get is invariant" splat
|
|
787
|
+
if (!laneType) return null // no memory ops — vectorizing buys nothing
|
|
788
|
+
if (loadStoreSites.length === 0) return null
|
|
789
|
+
|
|
790
|
+
// Soundness gate for offset-tee resolution: every `(local.get $T)` we
|
|
791
|
+
// accepted as `i << K` is only valid if EVERY write of $T is that offset.
|
|
792
|
+
for (const [name, k] of offsetTees) {
|
|
793
|
+
if (_offsetLocalStride(body, name, incVar) !== k) return null
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Classify all locals referenced in body.
|
|
797
|
+
// - induction var (incVar): exempt
|
|
798
|
+
// - bound local (if any): must be invariant
|
|
799
|
+
// - each other local: first access must not be a read-then-written pattern
|
|
800
|
+
const writes = new Set()
|
|
801
|
+
for (const s of body) collectWrites(s, writes)
|
|
802
|
+
if (boundLocal && writes.has(boundLocal)) return null // bound varies in body → bail
|
|
803
|
+
|
|
804
|
+
const localKind = new Map() // name → 'lane' | 'invariant' | 'addr'
|
|
805
|
+
// Walk to collect ALL referenced names
|
|
806
|
+
const referenced = new Set()
|
|
807
|
+
const collectRefs = (n) => {
|
|
808
|
+
if (!isArr(n)) return
|
|
809
|
+
const op = n[0]
|
|
810
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string')
|
|
811
|
+
referenced.add(n[1])
|
|
812
|
+
for (let i = 1; i < n.length; i++) collectRefs(n[i])
|
|
813
|
+
}
|
|
814
|
+
for (const s of body) collectRefs(s)
|
|
815
|
+
|
|
816
|
+
for (const name of referenced) {
|
|
817
|
+
if (name === incVar) continue
|
|
818
|
+
if (writes.has(name)) {
|
|
819
|
+
// Must be lane-local: first access is a write.
|
|
820
|
+
let firstKind = null
|
|
821
|
+
for (const s of body) {
|
|
822
|
+
const k = firstAccess(s, name)
|
|
823
|
+
if (k) { firstKind = k; break }
|
|
824
|
+
}
|
|
825
|
+
if (firstKind === 'read') return null // loop-carried (reduction or stencil)
|
|
826
|
+
// Discriminate lane-data vs address-tee. Address tees hold i32 addresses,
|
|
827
|
+
// not vector data. We classify by checking the local's declared type.
|
|
828
|
+
const decl = fnLocals.get(name)
|
|
829
|
+
if (decl === 'i32' && (offsetTees.has(name) || _isAddressLocal(body, name, incVar))) {
|
|
830
|
+
localKind.set(name, 'addr')
|
|
831
|
+
} else {
|
|
832
|
+
localKind.set(name, 'lane')
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
localKind.set(name, 'invariant')
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Build lifted body. If anything fails to lift, bail.
|
|
840
|
+
const newLanedLocals = new Map() // origName → { laneName, simdType }
|
|
841
|
+
const extraLocals = [] // canon temps allocated during lift
|
|
842
|
+
const ctx = { laneType, incVar, localKind, newLanedLocals, extraLocals, freshIdRef, fail: false, failReason: null }
|
|
843
|
+
const lifted = []
|
|
844
|
+
for (const s of body) {
|
|
845
|
+
const r = liftStmt(s, ctx)
|
|
846
|
+
if (ctx.fail) return null
|
|
847
|
+
if (r != null) {
|
|
848
|
+
if (Array.isArray(r) && r[0] === '__seq__') lifted.push(...r.slice(1))
|
|
849
|
+
else lifted.push(r)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (lifted.length === 0) return null
|
|
853
|
+
|
|
854
|
+
// Generate fresh names
|
|
855
|
+
const id = freshIdRef.next++
|
|
856
|
+
const simdBoundName = `$__simd_bound${id}`
|
|
857
|
+
const simdBrkLabel = `$__simd_brk${id}`
|
|
858
|
+
const simdLoopLabel = `$__simd_loop${id}`
|
|
859
|
+
|
|
860
|
+
const info = LANE_INFO[laneType]
|
|
861
|
+
const lanes = info.lanes
|
|
862
|
+
const mask = -lanes // bit pattern ~(lanes-1) in i32 two's complement
|
|
863
|
+
|
|
864
|
+
// Build SIMD prefix block.
|
|
865
|
+
const boundExpr = boundLocal
|
|
866
|
+
? ['local.get', boundLocal]
|
|
867
|
+
: bound // i32.const N
|
|
868
|
+
const simdBlock = ['block', simdBrkLabel,
|
|
869
|
+
['loop', simdLoopLabel,
|
|
870
|
+
['br_if', simdBrkLabel,
|
|
871
|
+
['i32.eqz', ['i32.lt_s', ['local.get', incVar], ['local.get', simdBoundName]]]],
|
|
872
|
+
...lifted,
|
|
873
|
+
['local.set', incVar,
|
|
874
|
+
['i32.add', ['local.get', incVar], ['i32.const', lanes]]],
|
|
875
|
+
['br', simdLoopLabel]
|
|
876
|
+
]
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
// Bound setup: simdBoundName = bound & ~(lanes-1)
|
|
880
|
+
const boundSetup = ['local.set', simdBoundName,
|
|
881
|
+
['i32.and', boundExpr, ['i32.const', mask]]]
|
|
882
|
+
|
|
883
|
+
// Synthetic outer wrapper — has no result, no label, just sequences.
|
|
884
|
+
// The original block is preserved unchanged as the tail.
|
|
885
|
+
const wrapper = ['block', boundSetup, simdBlock, blockNode]
|
|
886
|
+
|
|
887
|
+
// Locals to add to function header.
|
|
888
|
+
const newLocalDecls = [
|
|
889
|
+
['local', simdBoundName, 'i32'],
|
|
890
|
+
...[...newLanedLocals.values()].map(({ laneName }) => ['local', laneName, 'v128']),
|
|
891
|
+
...extraLocals,
|
|
892
|
+
]
|
|
893
|
+
|
|
894
|
+
return { wrapper, newLocalDecls }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ---- Reduction recognizer -------------------------------------------------
|
|
898
|
+
//
|
|
899
|
+
// Matches inner loops of shape:
|
|
900
|
+
// for (let i = 0; i < N; i++) S = OP(S, EXPR(arr[i], ...))
|
|
901
|
+
// where OP is associative+commutative (REDUCE_OPS table) and EXPR is lane-
|
|
902
|
+
// pure (operates on the loaded element with at most loop-invariant data).
|
|
903
|
+
// S is a SCALAR loop-carried accumulator — exempt from the lane-local
|
|
904
|
+
// "first access must be a write" check.
|
|
905
|
+
//
|
|
906
|
+
// Lift:
|
|
907
|
+
// acc = splat(IDENTITY)
|
|
908
|
+
// for (i = 0; i < bound & ~(L-1); i += L) acc = OP_v(acc, lifted EXPR)
|
|
909
|
+
// S = OP(S, horizontal_reduce(acc))
|
|
910
|
+
// <original scalar tail handles the remainder>
|
|
911
|
+
//
|
|
912
|
+
// Float adds are not strictly associative — vectorized reduction differs
|
|
913
|
+
// from scalar reduction by ulps. Acceptable when bit-exact equality is not
|
|
914
|
+
// required (which it isn't, by spec, in JS engines either).
|
|
915
|
+
function tryReduceVectorize(blockNode, fnLocals, freshIdRef) {
|
|
916
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
917
|
+
|
|
918
|
+
// Match outer (block (loop)) structure. Same loop-shape as tryVectorize.
|
|
919
|
+
let blockLabel = null
|
|
920
|
+
let loopNode = null
|
|
921
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
922
|
+
const c = blockNode[i]
|
|
923
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) { blockLabel = c; continue }
|
|
924
|
+
if (isArr(c) && c[0] === 'loop') {
|
|
925
|
+
if (loopNode) return null
|
|
926
|
+
loopNode = c
|
|
927
|
+
} else if (isArr(c)) return null
|
|
928
|
+
}
|
|
929
|
+
if (!loopNode || !blockLabel) return null
|
|
930
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
931
|
+
if (!loopLabel) return null
|
|
932
|
+
const endIdx = loopNode.length - 1
|
|
933
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
934
|
+
const incIdx = endIdx - 1
|
|
935
|
+
const incVar = matchInc1(loopNode[incIdx])
|
|
936
|
+
if (!incVar) return null
|
|
937
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
938
|
+
if (!exitInfo) return null
|
|
939
|
+
if (exitInfo.ind !== incVar) return null
|
|
940
|
+
|
|
941
|
+
// Body is either a bare single-statement reduction —
|
|
942
|
+
// (local.set $acc (OP (local.get $acc) EXPR)) add/xor/and/or
|
|
943
|
+
// — or a NaN-canonicalized two-statement min/max reduction —
|
|
944
|
+
// (local.set $cn (OP (local.get $acc) EXPR))
|
|
945
|
+
// (local.set $acc (select C (local.get $cn) (T.ne $cn $cn)))
|
|
946
|
+
const bodyLen = incIdx - 3
|
|
947
|
+
let accName, opName, reduceEntry, exprNode, canonC = null
|
|
948
|
+
if (bodyLen === 1) {
|
|
949
|
+
const stmt = loopNode[3]
|
|
950
|
+
if (!isArr(stmt) || stmt[0] !== 'local.set' || stmt.length !== 3) return null
|
|
951
|
+
accName = stmt[1]
|
|
952
|
+
if (typeof accName !== 'string') return null
|
|
953
|
+
const rhs = stmt[2]
|
|
954
|
+
if (!isArr(rhs)) return null
|
|
955
|
+
const minmax = matchIntMinMaxReduce(rhs, accName)
|
|
956
|
+
if (minmax) {
|
|
957
|
+
// Synthetic entry: WASM has the SIMD i32x4.max_s/min_s but no scalar i32.max, so the
|
|
958
|
+
// horizontal fold + merge below use select (flagged by minmaxSelect). Identity is the
|
|
959
|
+
// op's neutral — INT_MIN for max, INT_MAX for min. A bare narrow load instead folds
|
|
960
|
+
// at its own lane width/sign (MINMAX_WIDEN), 16 or 8 lanes per vector.
|
|
961
|
+
const w = isArr(minmax.exprNode) && minmax.exprNode.length === 2
|
|
962
|
+
? MINMAX_WIDEN[minmax.exprNode[0]] : null
|
|
963
|
+
reduceEntry = w ? {
|
|
964
|
+
simd: `${w.pre}.${minmax.isMax ? 'max' : 'min'}_${w.sign}`,
|
|
965
|
+
extract: `${w.pre}.extract_lane_${w.sign}`, laneType: w.laneType,
|
|
966
|
+
identity: ['i32.const', minmax.isMax ? w.lo : w.hi],
|
|
967
|
+
minmaxSelect: true, isMax: minmax.isMax, accI32: true,
|
|
968
|
+
} : {
|
|
969
|
+
simd: minmax.isMax ? 'i32x4.max_s' : 'i32x4.min_s',
|
|
970
|
+
extract: 'i32x4.extract_lane', laneType: 'i32',
|
|
971
|
+
identity: ['i32.const', minmax.isMax ? -2147483648 : 2147483647],
|
|
972
|
+
minmaxSelect: true, isMax: minmax.isMax,
|
|
973
|
+
}
|
|
974
|
+
exprNode = minmax.exprNode
|
|
975
|
+
} else if (rhs[0] === 'block') {
|
|
976
|
+
// Un-flattened NaN-canon float min/max — the same reduction as the two-statement
|
|
977
|
+
// canon (bodyLen===2 below), but with the cn-temp set + select still wrapped in a
|
|
978
|
+
// value-block: (local.set acc (block (result T) (local.set cn (OP acc expr))
|
|
979
|
+
// (select C (local.get cn) (T.ne cn cn)))). mergeBlocks normally hoists this to the
|
|
980
|
+
// flat form; recognize the block form directly so vectorization doesn't hinge on
|
|
981
|
+
// that hoist having run.
|
|
982
|
+
let bi = 1
|
|
983
|
+
if (typeof rhs[bi] === 'string' && rhs[bi].startsWith('$')) bi++
|
|
984
|
+
if (isArr(rhs[bi]) && rhs[bi][0] === 'result') bi++
|
|
985
|
+
const inner = rhs[bi]
|
|
986
|
+
const op = isArr(inner) && inner[0] === 'local.set' && isArr(inner[2]) ? inner[2][0] : null
|
|
987
|
+
reduceEntry = op ? REDUCE_CANON[op] : null
|
|
988
|
+
if (!reduceEntry) return null
|
|
989
|
+
const cb = matchCanonBlock(rhs, reduceEntry.laneType)
|
|
990
|
+
if (!cb || !isArr(cb.core) || cb.core.length !== 3 || !isLocalGet(cb.core[1], accName)) return null
|
|
991
|
+
opName = op
|
|
992
|
+
exprNode = cb.core[2]
|
|
993
|
+
canonC = cb.C
|
|
994
|
+
} else {
|
|
995
|
+
if (rhs.length !== 3) return null
|
|
996
|
+
opName = rhs[0]
|
|
997
|
+
reduceEntry = REDUCE_OP_LOOKUP.get(opName)
|
|
998
|
+
if (!reduceEntry || !isLocalGet(rhs[1], accName)) return null
|
|
999
|
+
exprNode = rhs[2]
|
|
1000
|
+
}
|
|
1001
|
+
} else if (bodyLen === 2) {
|
|
1002
|
+
const s1 = loopNode[3], s2 = loopNode[4]
|
|
1003
|
+
if (!isArr(s1) || s1[0] !== 'local.set' || s1.length !== 3) return null
|
|
1004
|
+
if (!isArr(s2) || s2[0] !== 'local.set' || s2.length !== 3) return null
|
|
1005
|
+
const cnName = s1[1], rhs = s1[2]
|
|
1006
|
+
if (typeof cnName !== 'string' || !isArr(rhs) || rhs.length !== 3) return null
|
|
1007
|
+
opName = rhs[0]
|
|
1008
|
+
reduceEntry = REDUCE_CANON[opName]
|
|
1009
|
+
if (!reduceEntry) return null
|
|
1010
|
+
accName = s2[1]
|
|
1011
|
+
if (typeof accName !== 'string' || accName === cnName) return null
|
|
1012
|
+
const canon = matchCanonSelect(s2[2], reduceEntry.laneType)
|
|
1013
|
+
if (!canon || !isLocalGet(canon.val, cnName)) return null
|
|
1014
|
+
if (!isLocalGet(rhs[1], accName)) return null
|
|
1015
|
+
canonC = canon.C
|
|
1016
|
+
exprNode = rhs[2]
|
|
1017
|
+
} else return null
|
|
1018
|
+
|
|
1019
|
+
// Accumulator's declared local type must match the lane element type.
|
|
1020
|
+
// Exception: the widening byte/short sum — i32 accumulator fed by ONE bare
|
|
1021
|
+
// narrow load (`s += u8[i]`), whose LANE type is i8/i16 but reduces into i32.
|
|
1022
|
+
const accType = fnLocals.get(accName)
|
|
1023
|
+
const widen = (opName === 'i32.add' && accType === 'i32' && canonC == null
|
|
1024
|
+
&& isArr(exprNode) && exprNode.length === 2 && WIDEN_LOADS[exprNode[0]]) || null
|
|
1025
|
+
// Widening float min/max: the canon over a sign-matched converted narrow load
|
|
1026
|
+
// (`m = Math.max(m, u8[i])`, acc f64) folds at the load's own width — exact,
|
|
1027
|
+
// since min/max never rounds and u8…i16 values are exact in f64. Only the one
|
|
1028
|
+
// horizontal result converts to f64 for the merge (+ re-canon for a NaN acc).
|
|
1029
|
+
if (canonC != null && (opName === 'f64.max' || opName === 'f64.min') && accType === 'f64'
|
|
1030
|
+
&& isArr(exprNode) && exprNode.length === 2 && MINMAX_CVT[exprNode[0]] && isArr(exprNode[1])) {
|
|
1031
|
+
const w = MINMAX_WIDEN[exprNode[1][0]]
|
|
1032
|
+
if (w && w.sign === MINMAX_CVT[exprNode[0]]) {
|
|
1033
|
+
const isMax = opName === 'f64.max'
|
|
1034
|
+
reduceEntry = {
|
|
1035
|
+
simd: `${w.pre}.${isMax ? 'max' : 'min'}_${w.sign}`,
|
|
1036
|
+
extract: `${w.pre}.extract_lane_${w.sign}`, laneType: w.laneType,
|
|
1037
|
+
identity: ['i32.const', isMax ? w.lo : w.hi],
|
|
1038
|
+
minmaxSelect: true, isMax, accF64: exprNode[0], canonC,
|
|
1039
|
+
}
|
|
1040
|
+
exprNode = exprNode[1]
|
|
1041
|
+
canonC = null
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (!widen && accType !== (reduceEntry.accI32 ? 'i32' : reduceEntry.accF64 ? 'f64' : reduceEntry.laneType)) return null
|
|
1045
|
+
|
|
1046
|
+
// Bound classification (same as tryVectorize).
|
|
1047
|
+
let bound = exitInfo.bound
|
|
1048
|
+
let boundLocal = null
|
|
1049
|
+
if (isArr(bound) && bound[0] === 'local.get' && typeof bound[1] === 'string') boundLocal = bound[1]
|
|
1050
|
+
else if (!isI32Const(bound)) return null
|
|
1051
|
+
|
|
1052
|
+
// Scan EXPR for lane-aligned loads. Stores forbidden. Re-references of
|
|
1053
|
+
// accName forbidden (the accumulator only appears in the outer wrapper).
|
|
1054
|
+
const laneType = widen ? widen.laneType : reduceEntry.laneType
|
|
1055
|
+
const stride = LANE_INFO[laneType].stride
|
|
1056
|
+
const addrLocals = new Map()
|
|
1057
|
+
const offsetTees = new Map()
|
|
1058
|
+
let loadCount = 0
|
|
1059
|
+
function scanExpr(node) {
|
|
1060
|
+
if (!isArr(node)) return true
|
|
1061
|
+
const op = node[0]
|
|
1062
|
+
if (LOAD_OPS[op]) {
|
|
1063
|
+
if (LOAD_OPS[op] !== laneType) return false
|
|
1064
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals, offsetTees)
|
|
1065
|
+
if (!m) return false
|
|
1066
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
1067
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
1068
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
1069
|
+
loadCount++
|
|
1070
|
+
return true
|
|
1071
|
+
}
|
|
1072
|
+
if (STORE_OPS[op]) return false
|
|
1073
|
+
if (op === 'local.set' || op === 'local.tee') return false // no intermediates
|
|
1074
|
+
if (op === 'local.get' && node[1] === accName) return false
|
|
1075
|
+
for (let i = 1; i < node.length; i++) if (!scanExpr(node[i])) return false
|
|
1076
|
+
return true
|
|
1077
|
+
}
|
|
1078
|
+
if (!scanExpr(exprNode)) return null
|
|
1079
|
+
if (loadCount === 0) return null
|
|
1080
|
+
// Soundness gate for offset-tee resolution (see tryVectorize).
|
|
1081
|
+
for (const [name, k] of offsetTees) {
|
|
1082
|
+
if (_offsetLocalStride([exprNode], name, incVar) !== k) return null
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Classify locals referenced in EXPR. Anything not the induction var or an
|
|
1086
|
+
// address-tee is invariant (we forbade local.set/tee in scanExpr).
|
|
1087
|
+
const referenced = new Set()
|
|
1088
|
+
const collectRefs = (n) => {
|
|
1089
|
+
if (!isArr(n)) return
|
|
1090
|
+
if (n[0] === 'local.get' && typeof n[1] === 'string') referenced.add(n[1])
|
|
1091
|
+
for (let i = 1; i < n.length; i++) collectRefs(n[i])
|
|
1092
|
+
}
|
|
1093
|
+
collectRefs(exprNode)
|
|
1094
|
+
const localKind = new Map()
|
|
1095
|
+
for (const name of referenced) {
|
|
1096
|
+
if (name === incVar) continue
|
|
1097
|
+
if (addrLocals.has(name) || offsetTees.has(name)) { localKind.set(name, 'addr'); continue }
|
|
1098
|
+
localKind.set(name, 'invariant')
|
|
1099
|
+
}
|
|
1100
|
+
for (const name of addrLocals.keys()) localKind.set(name, 'addr')
|
|
1101
|
+
for (const name of offsetTees.keys()) localKind.set(name, 'addr')
|
|
1102
|
+
|
|
1103
|
+
const ctx = { laneType, incVar, localKind, newLanedLocals: new Map(), extraLocals: [], freshIdRef, fail: false, failReason: null }
|
|
1104
|
+
const liftedExpr = liftExprV(exprNode, ctx)
|
|
1105
|
+
if (ctx.fail) return null
|
|
1106
|
+
if (ctx.newLanedLocals.size > 0 || ctx.extraLocals.length > 0) return null
|
|
1107
|
+
|
|
1108
|
+
// Synthesize SIMD prefix block + horizontal reduce + (preserved scalar tail).
|
|
1109
|
+
const id = freshIdRef.next++
|
|
1110
|
+
const simdBoundName = `$__simd_bound${id}`
|
|
1111
|
+
const simdAccName = `$__simd_acc${id}`
|
|
1112
|
+
const simdBrkLabel = `$__simd_brk${id}`
|
|
1113
|
+
const simdLoopLabel = `$__simd_loop${id}`
|
|
1114
|
+
const info = LANE_INFO[laneType]
|
|
1115
|
+
const lanes = info.lanes
|
|
1116
|
+
const boundExpr = boundLocal ? ['local.get', boundLocal] : bound
|
|
1117
|
+
|
|
1118
|
+
// Widening sum: the ACCUMULATOR vector is i32x4 regardless of the (narrow)
|
|
1119
|
+
// lane type; each iteration's 16-byte load collapses via extadd_pairwise.
|
|
1120
|
+
const accSplat = widen ? 'i32x4.splat' : info.splat
|
|
1121
|
+
const accumOperand = widen ? widen.steps.reduce((e, s) => [s, e], liftedExpr) : liftedExpr
|
|
1122
|
+
const initAcc = ['local.set', simdAccName, [accSplat, reduceEntry.constNode ?? reduceEntry.identity]]
|
|
1123
|
+
const simdBlock = ['block', simdBrkLabel,
|
|
1124
|
+
['loop', simdLoopLabel,
|
|
1125
|
+
['br_if', simdBrkLabel,
|
|
1126
|
+
['i32.eqz', ['i32.lt_s', ['local.get', incVar], ['local.get', simdBoundName]]]],
|
|
1127
|
+
['local.set', simdAccName,
|
|
1128
|
+
[reduceEntry.simd, ['local.get', simdAccName], accumOperand]],
|
|
1129
|
+
['local.set', incVar, ['i32.add', ['local.get', incVar], ['i32.const', lanes]]],
|
|
1130
|
+
['br', simdLoopLabel]
|
|
1131
|
+
]
|
|
1132
|
+
]
|
|
1133
|
+
|
|
1134
|
+
// Horizontal fold + merge into the live accumulator.
|
|
1135
|
+
const extraDecls = []
|
|
1136
|
+
let mergeStmts
|
|
1137
|
+
if (reduceEntry.minmaxSelect) {
|
|
1138
|
+
// No scalar i32.max/min — fold via select through an i32 temp (no exponential
|
|
1139
|
+
// operand duplication): ht = lane0; ht = minmax(ht, lane_k); acc = minmax(acc, ht).
|
|
1140
|
+
// `select(a,b,(gt|lt)_s a b)` = a when it's the larger/smaller, i.e. minmax(a,b).
|
|
1141
|
+
const cmpOp = reduceEntry.isMax ? 'i32.gt_s' : 'i32.lt_s'
|
|
1142
|
+
const ht = `$__simd_h${id}`
|
|
1143
|
+
extraDecls.push(['local', ht, 'i32'])
|
|
1144
|
+
const lane = (k) => [reduceEntry.extract, k, ['local.get', simdAccName]]
|
|
1145
|
+
const minmaxSel = (a, b) => ['select', a, b, [cmpOp, a, b]]
|
|
1146
|
+
mergeStmts = [['local.set', ht, lane(0)]]
|
|
1147
|
+
for (let k = 1; k < lanes; k++) mergeStmts.push(['local.set', ht, minmaxSel(lane(k), ['local.get', ht])])
|
|
1148
|
+
if (reduceEntry.accF64) {
|
|
1149
|
+
// Widening canon merge: one convert of the horizontal result, the scalar
|
|
1150
|
+
// f64 op against the live acc, then re-canon (a NaN-seeded acc must still
|
|
1151
|
+
// cross as the canonical NaN when the scalar tail is empty).
|
|
1152
|
+
mergeStmts.push(['local.set', accName,
|
|
1153
|
+
[opName, ['local.get', accName], [reduceEntry.accF64, ['local.get', ht]]]])
|
|
1154
|
+
mergeStmts.push(['local.set', accName,
|
|
1155
|
+
['select', reduceEntry.canonC, ['local.get', accName],
|
|
1156
|
+
['f64.ne', ['local.get', accName], ['local.get', accName]]]])
|
|
1157
|
+
} else {
|
|
1158
|
+
mergeStmts.push(['local.set', accName, minmaxSel(['local.get', accName], ['local.get', ht])])
|
|
1159
|
+
}
|
|
1160
|
+
} else {
|
|
1161
|
+
// Horizontal fold: scalar.op(extract 0, extract 1, …, extract L-1).
|
|
1162
|
+
// Widening sum folds the 4 i32x4 PARTIALS, not the (narrow) data lanes.
|
|
1163
|
+
const foldLanes = widen ? 4 : lanes
|
|
1164
|
+
let horiz = [reduceEntry.extract, 0, ['local.get', simdAccName]]
|
|
1165
|
+
for (let k = 1; k < foldLanes; k++) {
|
|
1166
|
+
horiz = [opName, horiz, [reduceEntry.extract, k, ['local.get', simdAccName]]]
|
|
1167
|
+
}
|
|
1168
|
+
// Merge the SIMD result into the live accumulator. For canon (min/max) the
|
|
1169
|
+
// merged value is re-canonicalized so a NaN that surfaced only in the SIMD
|
|
1170
|
+
// range still crosses as the canonical NaN when the scalar tail is empty.
|
|
1171
|
+
const merged = [opName, ['local.get', accName], horiz]
|
|
1172
|
+
mergeStmts = canonC == null
|
|
1173
|
+
? [['local.set', accName, merged]]
|
|
1174
|
+
: [['local.set', accName, merged],
|
|
1175
|
+
['local.set', accName,
|
|
1176
|
+
['select', canonC, ['local.get', accName],
|
|
1177
|
+
[`${laneType}.ne`, ['local.get', accName], ['local.get', accName]]]]]
|
|
1178
|
+
}
|
|
1179
|
+
// Overshoot-safe SIMD bound: stop while a full `lanes`-wide load stays in
|
|
1180
|
+
// range, for ANY induction start (the min/max idiom seeds m=a[0] and starts
|
|
1181
|
+
// at i=1, which `& ~(lanes-1)` masking would run one lane past the end). For
|
|
1182
|
+
// a lane-aligned start this yields the same iteration set as masking; the
|
|
1183
|
+
// scalar tail (original `i<bound` guard) cleans up regardless.
|
|
1184
|
+
const boundSetup = ['local.set', simdBoundName, ['i32.sub', boundExpr, ['i32.const', lanes - 1]]]
|
|
1185
|
+
|
|
1186
|
+
// Narrow-widened entries seed the vector acc with a LANE-domain neutral (e.g.
|
|
1187
|
+
// 0 for u8-max) — only neutral once real lanes fold in. Guard the whole SIMD
|
|
1188
|
+
// prefix incl. the merge so a zero-iteration range can't clamp the live acc
|
|
1189
|
+
// toward the identity. Full-width entries use absolute neutrals; unguarded.
|
|
1190
|
+
const core = reduceEntry.accI32 || reduceEntry.accF64
|
|
1191
|
+
? [['if', ['i32.lt_s', ['local.get', incVar], ['local.get', simdBoundName]],
|
|
1192
|
+
['then', initAcc, simdBlock, ...mergeStmts]]]
|
|
1193
|
+
: [initAcc, simdBlock, ...mergeStmts]
|
|
1194
|
+
const wrapper = ['block', boundSetup, ...core, blockNode]
|
|
1195
|
+
const newLocalDecls = [
|
|
1196
|
+
['local', simdBoundName, 'i32'],
|
|
1197
|
+
['local', simdAccName, 'v128'],
|
|
1198
|
+
...extraDecls,
|
|
1199
|
+
]
|
|
1200
|
+
return { wrapper, newLocalDecls }
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Scalar locals that are ALWAYS computed as `(i32.add base (i32.shl ind K))`
|
|
1204
|
+
// or aliased to such an address are "address tees", not lane data. They stay
|
|
1205
|
+
// scalar i32 in the lifted body.
|
|
1206
|
+
function _isAddressLocal(body, name, ind) {
|
|
1207
|
+
let onlyAsAddrTee = true
|
|
1208
|
+
let foundTee = false
|
|
1209
|
+
function walk(n) {
|
|
1210
|
+
if (!isArr(n)) return
|
|
1211
|
+
if (n[0] === 'local.tee' && n[1] === name) {
|
|
1212
|
+
foundTee = true
|
|
1213
|
+
// Check the value is a lane-address shape
|
|
1214
|
+
const m = matchLaneAddr(['local.tee', name, n[2]], ind)
|
|
1215
|
+
if (!m) onlyAsAddrTee = false
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
if (n[0] === 'local.set' && n[1] === name) {
|
|
1219
|
+
// A set-not-tee: check value shape
|
|
1220
|
+
const m = matchLaneAddr(['local.tee', name, n[2]], ind)
|
|
1221
|
+
if (!m) onlyAsAddrTee = false
|
|
1222
|
+
foundTee = true
|
|
1223
|
+
return
|
|
1224
|
+
}
|
|
1225
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
1226
|
+
}
|
|
1227
|
+
for (const s of body) walk(s)
|
|
1228
|
+
return foundTee && onlyAsAddrTee
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// ---- Lifter ----------------------------------------------------------------
|
|
1232
|
+
|
|
1233
|
+
function getOrAllocLanedLocal(name, ctx) {
|
|
1234
|
+
let r = ctx.newLanedLocals.get(name)
|
|
1235
|
+
if (!r) {
|
|
1236
|
+
r = { laneName: `${name}__v`, origName: name }
|
|
1237
|
+
ctx.newLanedLocals.set(name, r)
|
|
1238
|
+
}
|
|
1239
|
+
return r
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Wrap an already-lifted v128 value `coreV` in per-lane NaN canonicalization:
|
|
1243
|
+
// v128.bitselect(splat(C), coreV, laneNe(coreV, coreV))
|
|
1244
|
+
// coreV is referenced three times. When it's a bare local.get (the common
|
|
1245
|
+
// flattened form, where the core was already hoisted to a temp) we share it
|
|
1246
|
+
// directly — matching the scalar select, which likewise reads the temp thrice.
|
|
1247
|
+
// Otherwise we materialize a fresh v128 temp so the core evaluates once.
|
|
1248
|
+
function liftCanon(coreV, C, ctx, info) {
|
|
1249
|
+
const laneNe = ctx.laneType === 'f32' ? 'f32x4.ne' : 'f64x2.ne'
|
|
1250
|
+
const splatC = [info.splat, C]
|
|
1251
|
+
if (isArr(coreV) && coreV[0] === 'local.get') {
|
|
1252
|
+
return ['v128.bitselect', splatC, coreV, [laneNe, coreV, coreV]]
|
|
1253
|
+
}
|
|
1254
|
+
const tmp = `$__canon${ctx.freshIdRef.next++}`
|
|
1255
|
+
ctx.extraLocals.push(['local', tmp, 'v128'])
|
|
1256
|
+
const g = ['local.get', tmp]
|
|
1257
|
+
return ['block', ['result', 'v128'],
|
|
1258
|
+
['local.set', tmp, coreV],
|
|
1259
|
+
['v128.bitselect', splatC, g, [laneNe, g, g]]]
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** Lift a statement. Returns lifted stmt, or null to skip, or ['__seq__', ...] for multiple. */
|
|
1263
|
+
function liftStmt(stmt, ctx) {
|
|
1264
|
+
if (!isArr(stmt)) {
|
|
1265
|
+
// Bare strings like "drop" — produced by stack-form WAT. We unwrap value-blocks
|
|
1266
|
+
// separately so an isolated "drop" should not appear here, but tolerate it.
|
|
1267
|
+
if (stmt === 'drop') return null
|
|
1268
|
+
ctx.fail = true; return null
|
|
1269
|
+
}
|
|
1270
|
+
const op = stmt[0]
|
|
1271
|
+
|
|
1272
|
+
if (op === 'local.set' && typeof stmt[1] === 'string' && stmt.length === 3) {
|
|
1273
|
+
const name = stmt[1]
|
|
1274
|
+
const kind = ctx.localKind.get(name)
|
|
1275
|
+
if (kind === 'addr') {
|
|
1276
|
+
// Address-only local: lift the value as-is (it's i32 arithmetic on ind).
|
|
1277
|
+
return ['local.set', name, stmt[2]]
|
|
1278
|
+
}
|
|
1279
|
+
if (kind === 'lane') {
|
|
1280
|
+
const { laneName } = getOrAllocLanedLocal(name, ctx)
|
|
1281
|
+
const v = liftExprV(stmt[2], ctx)
|
|
1282
|
+
if (ctx.fail) return null
|
|
1283
|
+
return ['local.set', laneName, v]
|
|
1284
|
+
}
|
|
1285
|
+
ctx.fail = true; return null
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (STORE_OPS[op]) {
|
|
1289
|
+
const simdStore = 'v128.store'
|
|
1290
|
+
const addr = stmt[1] // we leave addresses as-is (scalar i32 expressions)
|
|
1291
|
+
const val = liftExprV(stmt[2], ctx)
|
|
1292
|
+
if (ctx.fail) return null
|
|
1293
|
+
// Handle memarg if present (last positional after addr/val): unlikely in
|
|
1294
|
+
// pre-watr IR for this shape; bail if more than 3 children.
|
|
1295
|
+
if (stmt.length !== 3) { ctx.fail = true; return null }
|
|
1296
|
+
return [simdStore, addr, val]
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// (block (result T) STMTS... TAIL_EXPR) followed by sibling "drop" — we get
|
|
1300
|
+
// the block alone here; the "drop" is a separate sibling and is returned as
|
|
1301
|
+
// null by the next call. Strip the wrapper, lift the inner stmts; the
|
|
1302
|
+
// dropped-tail expr is discarded.
|
|
1303
|
+
if (op === 'block') {
|
|
1304
|
+
// Block may be: ['block', LABEL?, RESULT?, ...stmts]
|
|
1305
|
+
let i = 1
|
|
1306
|
+
if (typeof stmt[i] === 'string' && stmt[i].startsWith('$')) i++
|
|
1307
|
+
const hasResult = isArr(stmt[i]) && stmt[i][0] === 'result'
|
|
1308
|
+
if (hasResult) i++
|
|
1309
|
+
const inner = stmt.slice(i)
|
|
1310
|
+
const stmts = hasResult ? inner.slice(0, inner.length - 1) : inner
|
|
1311
|
+
const out = ['__seq__']
|
|
1312
|
+
for (const s of stmts) {
|
|
1313
|
+
const lifted = liftStmt(s, ctx)
|
|
1314
|
+
if (ctx.fail) return null
|
|
1315
|
+
if (lifted == null) continue
|
|
1316
|
+
if (Array.isArray(lifted) && lifted[0] === '__seq__') out.push(...lifted.slice(1))
|
|
1317
|
+
else out.push(lifted)
|
|
1318
|
+
}
|
|
1319
|
+
return out
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Standalone expression-as-statement (e.g. a load that gets dropped) — bail.
|
|
1323
|
+
ctx.fail = true; return null
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** Lift a value expression into v128 context. */
|
|
1327
|
+
function liftExprV(expr, ctx) {
|
|
1328
|
+
if (!isArr(expr)) { ctx.fail = true; return null }
|
|
1329
|
+
const op = expr[0]
|
|
1330
|
+
const info = LANE_INFO[ctx.laneType]
|
|
1331
|
+
|
|
1332
|
+
// Loads → v128.load (preserving address, including any local.tee).
|
|
1333
|
+
if (LOAD_OPS[op]) {
|
|
1334
|
+
if (LOAD_OPS[op] !== ctx.laneType) { ctx.fail = true; return null }
|
|
1335
|
+
return ['v128.load', expr[1]]
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Constants → splat.
|
|
1339
|
+
if (op === info.constOp) {
|
|
1340
|
+
return [info.splat, expr]
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// local.get
|
|
1344
|
+
if (op === 'local.get' && typeof expr[1] === 'string') {
|
|
1345
|
+
const name = expr[1]
|
|
1346
|
+
const kind = ctx.localKind.get(name)
|
|
1347
|
+
if (kind === 'lane') {
|
|
1348
|
+
const { laneName } = getOrAllocLanedLocal(name, ctx)
|
|
1349
|
+
return ['local.get', laneName]
|
|
1350
|
+
}
|
|
1351
|
+
if (kind === 'invariant') {
|
|
1352
|
+
return [info.splat, ['local.get', name]]
|
|
1353
|
+
}
|
|
1354
|
+
if (kind === 'addr' || name === ctx.incVar) {
|
|
1355
|
+
ctx.fail = true; return null // can't be in a value position
|
|
1356
|
+
}
|
|
1357
|
+
ctx.fail = true; return null
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Loop-invariant global (e.g. a hoistConstantPool'd const, or any global the
|
|
1361
|
+
// loop never writes) → splat. The recognizer bails when the body contains a
|
|
1362
|
+
// global.set, so every global.get reaching here is invariant across lanes.
|
|
1363
|
+
if (op === 'global.get' && typeof expr[1] === 'string') {
|
|
1364
|
+
return [info.splat, expr]
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// NaN-canonicalization wrapper (float lanes only; integer lanes never carry
|
|
1368
|
+
// it). Both the flattened `select` form and the un-flattened `block` form
|
|
1369
|
+
// lift to a per-lane v128.bitselect — canonical value in NaN lanes, X
|
|
1370
|
+
// elsewhere — exactly reproducing the scalar canonicalization lane-by-lane.
|
|
1371
|
+
if (ctx.laneType === 'f64' || ctx.laneType === 'f32') {
|
|
1372
|
+
if (op === 'select') {
|
|
1373
|
+
const m = matchCanonSelect(expr, ctx.laneType)
|
|
1374
|
+
if (!m) { ctx.fail = true; return null }
|
|
1375
|
+
const coreV = liftExprV(m.val, ctx)
|
|
1376
|
+
return ctx.fail ? null : liftCanon(coreV, m.C, ctx, info)
|
|
1377
|
+
}
|
|
1378
|
+
if (op === 'block') {
|
|
1379
|
+
const m = matchCanonBlock(expr, ctx.laneType)
|
|
1380
|
+
if (!m) { ctx.fail = true; return null }
|
|
1381
|
+
const coreV = liftExprV(m.core, ctx)
|
|
1382
|
+
return ctx.fail ? null : liftCanon(coreV, m.C, ctx, info)
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Conditional select — jz lowers `cond ? X : Y` to (if (result LT) COND (then X)
|
|
1387
|
+
// (else Y)). Lift to v128.bitselect(X, Y, mask), where mask is COND as an
|
|
1388
|
+
// all-ones/all-zeros lane comparison. Both branches are lane-pure (recursion
|
|
1389
|
+
// forbids stores/sets) and trap-free (no liftable op traps — int div/rem aren't
|
|
1390
|
+
// lane-pure), so speculatively evaluating both is safe; bitselect keeps the
|
|
1391
|
+
// chosen lane. The mask is hoisted to a temp and computed FIRST: bitselect
|
|
1392
|
+
// evaluates X,Y before its 3rd operand, but any address `local.tee` lives in
|
|
1393
|
+
// COND and must run before the branches read it (matching scalar order).
|
|
1394
|
+
if (op === 'if') {
|
|
1395
|
+
if (!isArr(expr[1]) || expr[1][0] !== 'result' || expr[1][1] !== ctx.laneType) { ctx.fail = true; return null }
|
|
1396
|
+
const thenN = expr[3], elseN = expr[4]
|
|
1397
|
+
if (!isArr(thenN) || thenN[0] !== 'then' || thenN.length !== 2) { ctx.fail = true; return null }
|
|
1398
|
+
if (!isArr(elseN) || elseN[0] !== 'else' || elseN.length !== 2) { ctx.fail = true; return null }
|
|
1399
|
+
let cond = expr[2]
|
|
1400
|
+
if (isArr(cond) && cond[0] === 'i32.ne' && isI32Const(cond[2]) && cond[2][1] === 0) cond = cond[1] // strip `!= 0`
|
|
1401
|
+
const cmpSimd = isArr(cond) && cond.length === 3 ? LANE_COMPARE[ctx.laneType]?.[cond[0]] : null
|
|
1402
|
+
if (!cmpSimd) { ctx.fail = true; return null }
|
|
1403
|
+
const ca = liftExprV(cond[1], ctx); if (ctx.fail) return null
|
|
1404
|
+
const cb = liftExprV(cond[2], ctx); if (ctx.fail) return null
|
|
1405
|
+
const x = liftExprV(thenN[1], ctx); if (ctx.fail) return null
|
|
1406
|
+
const y = liftExprV(elseN[1], ctx); if (ctx.fail) return null
|
|
1407
|
+
const mtmp = `$__mask${ctx.freshIdRef.next++}`
|
|
1408
|
+
ctx.extraLocals.push(['local', mtmp, 'v128'])
|
|
1409
|
+
return ['block', ['result', 'v128'],
|
|
1410
|
+
['local.set', mtmp, [cmpSimd, ca, cb]],
|
|
1411
|
+
['v128.bitselect', x, y, ['local.get', mtmp]]]
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Lane-pure op?
|
|
1415
|
+
const table = LANE_PURE[ctx.laneType]
|
|
1416
|
+
const entry = table?.get(op)
|
|
1417
|
+
if (entry) {
|
|
1418
|
+
const a = liftExprV(expr[1], ctx)
|
|
1419
|
+
if (ctx.fail) return null
|
|
1420
|
+
if (entry.shamtScalar) {
|
|
1421
|
+
// Second operand stays scalar i32 — must be const or invariant local.
|
|
1422
|
+
const b = expr[2]
|
|
1423
|
+
if (!isI32Const(b) && !(isArr(b) && b[0] === 'local.get' && ctx.localKind.get(b[1]) === 'invariant')) {
|
|
1424
|
+
ctx.fail = true; return null
|
|
1425
|
+
}
|
|
1426
|
+
return [entry.simd, a, b]
|
|
1427
|
+
}
|
|
1428
|
+
if (expr.length === 2) { // unary (neg, abs, sqrt)
|
|
1429
|
+
return [entry.simd, a]
|
|
1430
|
+
}
|
|
1431
|
+
const b = liftExprV(expr[2], ctx)
|
|
1432
|
+
if (ctx.fail) return null
|
|
1433
|
+
return [entry.simd, a, b]
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
ctx.fail = true; return null
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ---- Induction-variable strength reduction --------------------------------
|
|
1440
|
+
|
|
1441
|
+
// Match `(i32.add (local.get $base) (i32.shl (local.get $ind) (i32.const K)))` in either
|
|
1442
|
+
// operand order, or `(i32.add (local.get $base) (local.get $ind))` (K=0). Returns
|
|
1443
|
+
// {base, k} — the address of element $ind in array $base, byte stride 1<<k — or null.
|
|
1444
|
+
function matchAffineAddr(node, ind) {
|
|
1445
|
+
if (!isArr(node) || node[0] !== 'i32.add' || node.length !== 3) return null
|
|
1446
|
+
const pair = (baseN, offN) => {
|
|
1447
|
+
if (!isLocalGet(baseN) || baseN[1] === ind) return null
|
|
1448
|
+
if (isLocalGet(offN, ind)) return { base: baseN[1], k: 0 }
|
|
1449
|
+
if (isArr(offN) && offN[0] === 'i32.shl' && offN.length === 3 && isLocalGet(offN[1], ind)) {
|
|
1450
|
+
const k = constNum(offN[2])
|
|
1451
|
+
if (k != null && k >= 0 && k <= 3) return { base: baseN[1], k }
|
|
1452
|
+
}
|
|
1453
|
+
return null
|
|
1454
|
+
}
|
|
1455
|
+
return pair(node[1], node[2]) || pair(node[2], node[1])
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Strength-reduce induction-variable addressing in an affine loop the vectorizer
|
|
1460
|
+
* couldn't lift (an early `break`, a call, a non-lane body). For each loop-invariant
|
|
1461
|
+
* array `base` and shift `K`, every `base + (i<<K)` in the body is replaced by a strided
|
|
1462
|
+
* pointer `$p`, initialized to `base + (i<<K)` before the loop and bumped by `1<<K` in
|
|
1463
|
+
* lockstep with `i`. Drops the per-iteration shift+add — V8 does NOT strength-reduce this
|
|
1464
|
+
* itself (measured ~6% faster, the additive keep-`i` form used here). Canonical shape only
|
|
1465
|
+
* (single +1 IV, bottom increment, br_if exit). Bails if the body writes `i` or any `base`
|
|
1466
|
+
* (not invariant), or branches to the loop label (which would skip the pointer bump — a
|
|
1467
|
+
* `br_if` to the *block* label, i.e. an early break, is fine: the loop is exiting). Runs
|
|
1468
|
+
* only where the vectorizer runs (speed levels), so it never grows the size-tuned build.
|
|
1469
|
+
*/
|
|
1470
|
+
function tryStrengthReduceIV(blockNode, fnLocals, freshIdRef) {
|
|
1471
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
1472
|
+
let blockLabel = null, loopNode = null
|
|
1473
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
1474
|
+
const c = blockNode[i]
|
|
1475
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) { blockLabel = c; continue }
|
|
1476
|
+
if (isArr(c) && c[0] === 'loop') { if (loopNode) return null; loopNode = c }
|
|
1477
|
+
else if (isArr(c)) return null
|
|
1478
|
+
}
|
|
1479
|
+
if (!loopNode || !blockLabel) return null
|
|
1480
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
1481
|
+
if (!loopLabel) return null
|
|
1482
|
+
const endIdx = loopNode.length - 1
|
|
1483
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
1484
|
+
const incIdx = endIdx - 1
|
|
1485
|
+
const incVar = matchInc1(loopNode[incIdx])
|
|
1486
|
+
if (!incVar) return null
|
|
1487
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
1488
|
+
if (!exitInfo || exitInfo.ind !== incVar) return null
|
|
1489
|
+
|
|
1490
|
+
// Scan the body (stmts between the exit br_if and the bottom increment): collect
|
|
1491
|
+
// affine-address sites, track every written local, and bail on a loop-label branch.
|
|
1492
|
+
const sites = [] // { parent, idx, base, k }
|
|
1493
|
+
const written = new Set()
|
|
1494
|
+
let bail = false
|
|
1495
|
+
const scan = (node, parent, pi) => {
|
|
1496
|
+
if (bail || !isArr(node)) return
|
|
1497
|
+
const op = node[0]
|
|
1498
|
+
if ((op === 'br' || op === 'br_if') && node[1] === loopLabel) { bail = true; return }
|
|
1499
|
+
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') written.add(node[1])
|
|
1500
|
+
const m = matchAffineAddr(node, incVar)
|
|
1501
|
+
if (m) sites.push({ parent, idx: pi, base: m.base, k: m.k })
|
|
1502
|
+
for (let i = 1; i < node.length; i++) scan(node[i], node, i)
|
|
1503
|
+
}
|
|
1504
|
+
for (let i = 3; i < incIdx; i++) scan(loopNode[i], loopNode, i)
|
|
1505
|
+
if (bail || !sites.length || written.has(incVar)) return null
|
|
1506
|
+
|
|
1507
|
+
// Group by (base, k); keep only loop-invariant i32 bases.
|
|
1508
|
+
const groups = new Map() // `base|k` → { base, k, sites }
|
|
1509
|
+
for (const s of sites) {
|
|
1510
|
+
if (written.has(s.base) || fnLocals.get(s.base) !== 'i32') continue
|
|
1511
|
+
const key = s.base + '|' + s.k
|
|
1512
|
+
let g = groups.get(key)
|
|
1513
|
+
if (!g) groups.set(key, g = { base: s.base, k: s.k, sites: [] })
|
|
1514
|
+
g.sites.push(s)
|
|
1515
|
+
}
|
|
1516
|
+
if (!groups.size) return null
|
|
1517
|
+
|
|
1518
|
+
// One strided pointer per group: init before the block, bump after the i increment,
|
|
1519
|
+
// every matched address → (local.get $p).
|
|
1520
|
+
const id = freshIdRef.next++
|
|
1521
|
+
const preInits = [], bumps = [], newLocalDecls = []
|
|
1522
|
+
let gi = 0
|
|
1523
|
+
for (const g of groups.values()) {
|
|
1524
|
+
const p = `$__iv${id}_${gi++}`
|
|
1525
|
+
newLocalDecls.push(['local', p, 'i32'])
|
|
1526
|
+
const off = g.k === 0 ? ['local.get', incVar] : ['i32.shl', ['local.get', incVar], ['i32.const', g.k]]
|
|
1527
|
+
preInits.push(['local.set', p, ['i32.add', ['local.get', g.base], off]])
|
|
1528
|
+
bumps.push(['local.set', p, ['i32.add', ['local.get', p], ['i32.const', 1 << g.k]]])
|
|
1529
|
+
for (const s of g.sites) s.parent[s.idx] = ['local.get', p]
|
|
1530
|
+
}
|
|
1531
|
+
loopNode.splice(incIdx + 1, 0, ...bumps) // after the induction increment, before the br
|
|
1532
|
+
return { wrapper: ['block', ...preInits, blockNode], newLocalDecls }
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// ---- memory.copy / memory.fill loop idioms ---------------------------------
|
|
1536
|
+
|
|
1537
|
+
// Same-width store←load pairs (byte-window moves) and their element stride.
|
|
1538
|
+
// Sign-variant narrow loads are interchangeable for a MOVE: load8_u/load8_s
|
|
1539
|
+
// then store8 write the same byte back.
|
|
1540
|
+
const MEMOP_STORES = {
|
|
1541
|
+
'f64.store': { k: 3, loads: new Set(['f64.load']) },
|
|
1542
|
+
'i64.store': { k: 3, loads: new Set(['i64.load']) },
|
|
1543
|
+
'f32.store': { k: 2, loads: new Set(['f32.load']) },
|
|
1544
|
+
'i32.store': { k: 2, loads: new Set(['i32.load']) },
|
|
1545
|
+
'i32.store16': { k: 1, loads: new Set(['i32.load16_u', 'i32.load16_s']) },
|
|
1546
|
+
'i32.store8': { k: 0, loads: new Set(['i32.load8_u', 'i32.load8_s']) },
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Replace whole copy/fill loops with the engine's bulk-memory ops:
|
|
1551
|
+
*
|
|
1552
|
+
* for (i < N) a[i] = b[i] → memory.copy (overlap-guarded)
|
|
1553
|
+
* for (i < N) a[i] = 0 → memory.fill 0 (any element width)
|
|
1554
|
+
* for (i < N) u8[i] = C → memory.fill C (byte stores only)
|
|
1555
|
+
*
|
|
1556
|
+
* V8 lowers memory.copy/fill to memmove/memset — typically several times the
|
|
1557
|
+
* throughput of even a SIMD lane loop, and a handful of bytes instead of one.
|
|
1558
|
+
* Runs BEFORE the lane vectorizer so these loops never pay the lift.
|
|
1559
|
+
*
|
|
1560
|
+
* Exactness:
|
|
1561
|
+
* - COPY moves the same byte window the scalar loop wrote (same-width
|
|
1562
|
+
* store←load pairs only — see MEMOP_STORES; f64 load/store round-trips are
|
|
1563
|
+
* bit-exact per spec, so NaN payloads survive). memory.copy is memmove
|
|
1564
|
+
* (as-if-buffered); the FORWARD loop differs from that exactly when the
|
|
1565
|
+
* destination starts strictly inside the source window (dst reads bytes an
|
|
1566
|
+
* earlier iteration already overwrote), so that case keeps the original
|
|
1567
|
+
* loop behind a two-compare runtime guard — every other layout (disjoint,
|
|
1568
|
+
* dst ≤ src, same array) is bit-identical.
|
|
1569
|
+
* - FILL with 0 is width-agnostic (all-zero bytes; −0.0 is excluded — its
|
|
1570
|
+
* sign bit is not zero). Non-zero fills only for byte stores with a
|
|
1571
|
+
* loop-invariant constant ∈ [0,255].
|
|
1572
|
+
* - The induction variable ends at `bound` exactly as the loop would leave
|
|
1573
|
+
* it; a zero-trip range (i ≥ bound) leaves everything untouched.
|
|
1574
|
+
* - In-bounds for the same reason the lane vectorizer's wide loads are: the
|
|
1575
|
+
* moved window is exactly the byte range the scalar iterations touch.
|
|
1576
|
+
*/
|
|
1577
|
+
function tryMemCopyFill(blockNode, fnLocals, freshIdRef) {
|
|
1578
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
1579
|
+
let blockLabel = null, loopNode = null
|
|
1580
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
1581
|
+
const c = blockNode[i]
|
|
1582
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) { blockLabel = c; continue }
|
|
1583
|
+
if (isArr(c) && c[0] === 'loop') { if (loopNode) return null; loopNode = c }
|
|
1584
|
+
else if (isArr(c)) return null
|
|
1585
|
+
}
|
|
1586
|
+
if (!loopNode || !blockLabel) return null
|
|
1587
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
1588
|
+
if (!loopLabel) return null
|
|
1589
|
+
// Shape: [loop, $l, boundExit, (set,)? store, inc, br] — 1- or 2-statement body.
|
|
1590
|
+
if (loopNode.length !== 6 && loopNode.length !== 7) return null
|
|
1591
|
+
const endIdx = loopNode.length - 1
|
|
1592
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
1593
|
+
const incVar = matchInc1(loopNode[endIdx - 1])
|
|
1594
|
+
if (!incVar) return null
|
|
1595
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
1596
|
+
if (!exitInfo || exitInfo.ind !== incVar) return null
|
|
1597
|
+
const bound = exitInfo.bound
|
|
1598
|
+
if (!(isI32Const(bound) || (isArr(bound) && bound[0] === 'local.get' && typeof bound[1] === 'string' && bound[1] !== incVar))) return null
|
|
1599
|
+
|
|
1600
|
+
// Body shapes (emit produces the two-statement form with a temp + shared
|
|
1601
|
+
// offset tee; fold/propagate sometimes collapse it to the bare store):
|
|
1602
|
+
// 1-stmt: (T.store DADDR CONST | (T.load SADDR))
|
|
1603
|
+
// 2-stmt: (local.set $t (T.load SADDR)) (T.store DADDR (local.get $t))
|
|
1604
|
+
const addrLocals = new Map(), offsetTees = new Map()
|
|
1605
|
+
const laneAddr = (a) => {
|
|
1606
|
+
const m = matchLaneAddr(a, incVar, addrLocals, offsetTees)
|
|
1607
|
+
if (!m || m.viaLocal) return null
|
|
1608
|
+
if (m.offsetTeeName) offsetTees.set(m.offsetTeeName, m.strideLog2)
|
|
1609
|
+
if (!(isArr(m.base) && m.base[0] === 'local.get' && typeof m.base[1] === 'string' && m.base[1] !== incVar)) return null
|
|
1610
|
+
if (fnLocals.get(m.base[1]) !== 'i32') return null
|
|
1611
|
+
return m
|
|
1612
|
+
}
|
|
1613
|
+
let storeStmt, valNode, tempName = null
|
|
1614
|
+
if (loopNode.length === 6) {
|
|
1615
|
+
storeStmt = loopNode[3]
|
|
1616
|
+
if (!isArr(storeStmt) || storeStmt.length !== 3) return null
|
|
1617
|
+
valNode = storeStmt[2]
|
|
1618
|
+
} else {
|
|
1619
|
+
const s1 = loopNode[3]
|
|
1620
|
+
storeStmt = loopNode[4]
|
|
1621
|
+
if (!isArr(s1) || s1[0] !== 'local.set' || s1.length !== 3 || typeof s1[1] !== 'string') return null
|
|
1622
|
+
if (!isArr(storeStmt) || storeStmt.length !== 3) return null
|
|
1623
|
+
if (!(isArr(storeStmt[2]) && storeStmt[2][0] === 'local.get' && storeStmt[2][1] === s1[1])) return null
|
|
1624
|
+
tempName = s1[1]
|
|
1625
|
+
if (tempName === incVar) return null
|
|
1626
|
+
valNode = s1[2]
|
|
1627
|
+
}
|
|
1628
|
+
const entry = MEMOP_STORES[storeStmt[0]]
|
|
1629
|
+
if (!entry) return null
|
|
1630
|
+
|
|
1631
|
+
// Classify VALUE first (the load side carries the offset tee in the 2-stmt form).
|
|
1632
|
+
let fillByte = null, srcM = null
|
|
1633
|
+
if (isArr(valNode) && valNode.length === 2 && (valNode[0] === 'i32.const' || valNode[0] === 'i64.const' || valNode[0] === 'f64.const' || valNode[0] === 'f32.const') && typeof valNode[1] === 'number') {
|
|
1634
|
+
if (valNode[1] === 0 && !Object.is(valNode[1], -0)) fillByte = 0
|
|
1635
|
+
else if (storeStmt[0] === 'i32.store8' && valNode[0] === 'i32.const' && Number.isInteger(valNode[1]) && valNode[1] >= 0 && valNode[1] <= 255) fillByte = valNode[1]
|
|
1636
|
+
else return null
|
|
1637
|
+
} else if (isArr(valNode) && valNode.length === 2 && entry.loads.has(valNode[0])) {
|
|
1638
|
+
srcM = laneAddr(valNode[1])
|
|
1639
|
+
if (!srcM || srcM.strideLog2 !== entry.k) return null
|
|
1640
|
+
} else return null
|
|
1641
|
+
|
|
1642
|
+
const dstM = laneAddr(storeStmt[1])
|
|
1643
|
+
if (!dstM || dstM.strideLog2 !== entry.k) return null
|
|
1644
|
+
// Soundness for any shared offset tee resolved via `(local.get $T)` (see tryVectorize).
|
|
1645
|
+
for (const [name, k] of offsetTees) {
|
|
1646
|
+
if (_offsetLocalStride([loopNode[3], storeStmt], name, incVar) !== k) return null
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const id = freshIdRef.next++
|
|
1650
|
+
const lenB = `$__mc${id}_len`, dstA = `$__mc${id}_dst`
|
|
1651
|
+
const newLocalDecls = [['local', lenB, 'i32'], ['local', dstA, 'i32']]
|
|
1652
|
+
const shl = (x) => entry.k === 0 ? x : ['i32.shl', x, ['i32.const', entry.k]]
|
|
1653
|
+
const boundC = () => cloneNode(bound)
|
|
1654
|
+
const setup = [
|
|
1655
|
+
['local.set', lenB, shl(['i32.sub', boundC(), ['local.get', incVar]])],
|
|
1656
|
+
['local.set', dstA, ['i32.add', ['local.get', dstM.base[1]], shl(['local.get', incVar])]],
|
|
1657
|
+
]
|
|
1658
|
+
// Exact loop-exit state: i ends at bound; a matched offset tee holds the
|
|
1659
|
+
// LAST iteration's offset; the value temp holds the last element moved.
|
|
1660
|
+
const finish = [['local.set', incVar, boundC()]]
|
|
1661
|
+
const lastOff = () => shl(['i32.sub', boundC(), ['i32.const', 1]])
|
|
1662
|
+
for (const name of offsetTees.keys()) finish.push(['local.set', name, lastOff()])
|
|
1663
|
+
if (tempName != null && srcM) finish.push(['local.set', tempName,
|
|
1664
|
+
[valNode[0], ['i32.add', ['local.get', srcM.base[1]], lastOff()]]])
|
|
1665
|
+
if (tempName != null && srcM == null) finish.push(['local.set', tempName, cloneNode(valNode)])
|
|
1666
|
+
|
|
1667
|
+
let action
|
|
1668
|
+
if (srcM == null) {
|
|
1669
|
+
action = [['memory.fill', ['local.get', dstA], ['i32.const', fillByte], ['local.get', lenB]], ...finish]
|
|
1670
|
+
} else {
|
|
1671
|
+
const srcA = `$__mc${id}_src`
|
|
1672
|
+
newLocalDecls.push(['local', srcA, 'i32'])
|
|
1673
|
+
setup.push(['local.set', srcA, ['i32.add', ['local.get', srcM.base[1]], shl(['local.get', incVar])]])
|
|
1674
|
+
// Forward-loop ≡ memmove unless dst starts strictly inside (src, src+len).
|
|
1675
|
+
action = [['if',
|
|
1676
|
+
['i32.or',
|
|
1677
|
+
['i32.le_u', ['local.get', dstA], ['local.get', srcA]],
|
|
1678
|
+
['i32.ge_u', ['local.get', dstA], ['i32.add', ['local.get', srcA], ['local.get', lenB]]]],
|
|
1679
|
+
['then',
|
|
1680
|
+
['memory.copy', ['local.get', dstA], ['local.get', srcA], ['local.get', lenB]],
|
|
1681
|
+
...finish],
|
|
1682
|
+
['else', blockNode]]]
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const wrapper = ['block',
|
|
1686
|
+
['if', ['i32.lt_s', ['local.get', incVar], boundC()],
|
|
1687
|
+
['then', ...setup, ...action]]]
|
|
1688
|
+
return { wrapper, newLocalDecls }
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// ---- Byte-scan (memchr) vectorization -------------------------------------
|
|
1692
|
+
|
|
1693
|
+
// Match a single-byte compare against a constant or a loop-invariant target:
|
|
1694
|
+
// (f64.eq|ne (f64.convert_i32_u|s (i32.load8_u|s (base + i))) TARGET) [value-model form]
|
|
1695
|
+
// (i32.eq|ne (i32.load8_u|s (base + i)) TARGET) [folded i32 form]
|
|
1696
|
+
// TARGET is a const byte or a `local.get` (the `memchr(buf, delim)` runtime case).
|
|
1697
|
+
// Returns { base, eq, isF64, c, targetLocal } — exactly one of c (∈[0,255]) / targetLocal set.
|
|
1698
|
+
function matchByteCompare(node, ind) {
|
|
1699
|
+
if (!isArr(node) || node.length !== 3) return null
|
|
1700
|
+
let eq
|
|
1701
|
+
if (node[0] === 'f64.eq' || node[0] === 'i32.eq') eq = true
|
|
1702
|
+
else if (node[0] === 'f64.ne' || node[0] === 'i32.ne') eq = false
|
|
1703
|
+
else return null
|
|
1704
|
+
const isF64 = node[0][0] === 'f'
|
|
1705
|
+
const constOf = (x) => isF64
|
|
1706
|
+
? (isArr(x) && x[0] === 'f64.const' && typeof x[1] === 'number' && Number.isInteger(x[1]) ? x[1] : null)
|
|
1707
|
+
: constNum(x)
|
|
1708
|
+
// Identify which operand is the byte load and which is the target.
|
|
1709
|
+
const isLoadSide = (x) => {
|
|
1710
|
+
let l = x
|
|
1711
|
+
if (isF64) { if (!(isArr(l) && (l[0] === 'f64.convert_i32_u' || l[0] === 'f64.convert_i32_s') && l.length === 2)) return null; l = l[1] }
|
|
1712
|
+
if (!(isArr(l) && (l[0] === 'i32.load8_u' || l[0] === 'i32.load8_s') && l.length === 2)) return null
|
|
1713
|
+
const m = matchAffineAddr(l[1], ind)
|
|
1714
|
+
return m && m.k === 0 ? m.base : null // byte stride only
|
|
1715
|
+
}
|
|
1716
|
+
let base = isLoadSide(node[1]), target = node[2]
|
|
1717
|
+
if (base == null) { base = isLoadSide(node[2]); target = node[1] }
|
|
1718
|
+
if (base == null) return null
|
|
1719
|
+
const c = constOf(target)
|
|
1720
|
+
if (c != null) return c >= 0 && c <= 255 ? { base, eq, isF64, c } : null
|
|
1721
|
+
// Runtime target: a loop-invariant local (the minimal scan body writes only `i`).
|
|
1722
|
+
if (isArr(target) && target[0] === 'local.get' && typeof target[1] === 'string' && target[1] !== ind)
|
|
1723
|
+
return { base, eq, isF64, targetLocal: target[1] }
|
|
1724
|
+
return null
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* SIMD byte scan — vectorize a memchr-shaped loop the engine runs one byte at a time.
|
|
1729
|
+
* Recognizes the pure scan
|
|
1730
|
+
* (block $b (loop $l (br_if $b (eqz (i<bound))) (br_if $b (buf[i] ==/!= C)) (i := i+1) (br $l)))
|
|
1731
|
+
* — "find the first index where buf[i] (Uint8/Int8Array) ==/!= a constant byte" — and
|
|
1732
|
+
* rewrites it to scan 16 bytes per step with `i8x16.eq` + `i8x16.bitmask`, locating the
|
|
1733
|
+
* exact first match via `i32.ctz`, with the original loop kept as the <16-byte tail.
|
|
1734
|
+
* Measured ~8× over the scalar scan on V8 (which doesn't auto-vectorize it). Fails closed:
|
|
1735
|
+
* any deviation from the exact shape leaves the scalar loop. The 16-wide `v128.load` is
|
|
1736
|
+
* in-bounds because it only fires while `i+16 <= bound` and `bound` bounds the scalar
|
|
1737
|
+
* reads too. (charCodeAt over a jz string is out of scope — it lowers to per-char
|
|
1738
|
+
* bounds/SSO/heap/decode branches, not a flat byte load.)
|
|
1739
|
+
*/
|
|
1740
|
+
function tryByteScan(blockNode, fnLocals, freshIdRef) {
|
|
1741
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
1742
|
+
let blockLabel = null, loopNode = null
|
|
1743
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
1744
|
+
const c = blockNode[i]
|
|
1745
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) { blockLabel = c; continue }
|
|
1746
|
+
if (isArr(c) && c[0] === 'loop') { if (loopNode) return null; loopNode = c }
|
|
1747
|
+
else if (isArr(c)) return null
|
|
1748
|
+
}
|
|
1749
|
+
if (!loopNode || !blockLabel) return null
|
|
1750
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
1751
|
+
if (!loopLabel) return null
|
|
1752
|
+
// Exact shape: [loop, $l, boundExit, matchExit, inc, br] — nothing else.
|
|
1753
|
+
if (loopNode.length !== 6) return null
|
|
1754
|
+
if (!(isArr(loopNode[5]) && loopNode[5][0] === 'br' && loopNode[5][1] === loopLabel)) return null
|
|
1755
|
+
const incVar = matchInc1(loopNode[4])
|
|
1756
|
+
if (!incVar) return null
|
|
1757
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel) // (br_if $b (eqz (i < bound)))
|
|
1758
|
+
if (!exitInfo || exitInfo.ind !== incVar) return null
|
|
1759
|
+
const matchExit = loopNode[3]
|
|
1760
|
+
if (!(isArr(matchExit) && matchExit[0] === 'br_if' && matchExit[1] === blockLabel && matchExit.length === 3)) return null
|
|
1761
|
+
const bc = matchByteCompare(matchExit[2], incVar)
|
|
1762
|
+
if (!bc) return null
|
|
1763
|
+
if (fnLocals.get(bc.base) !== 'i32' || bc.base === incVar) return null
|
|
1764
|
+
|
|
1765
|
+
const id = freshIdRef.next++
|
|
1766
|
+
const sd = `$__bscan_brk${id}`, sl = `$__bscan_loop${id}`, mask = `$__bscan_m${id}`
|
|
1767
|
+
const baseGet = ['local.get', bc.base]
|
|
1768
|
+
const iGet = ['local.get', incVar]
|
|
1769
|
+
const bound = exitInfo.bound
|
|
1770
|
+
const newLocalDecls = [['local', mask, 'i32']]
|
|
1771
|
+
|
|
1772
|
+
// The byte to splat across 16 lanes, plus a runtime guard. A constant needs none.
|
|
1773
|
+
// A runtime `delim` (f64-boxed) is only a valid SIMD target when it's an integer in
|
|
1774
|
+
// [0,255]; outside that, NO byte (0–255) equals it, so the scalar tail — which we keep —
|
|
1775
|
+
// reproduces the exact result. The guard makes that branch explicit; cb caches the byte.
|
|
1776
|
+
let splat, guard = null, cbInit = null
|
|
1777
|
+
if (bc.c != null) {
|
|
1778
|
+
splat = ['i32.const', bc.c]
|
|
1779
|
+
} else {
|
|
1780
|
+
const cb = `$__bscan_c${id}`
|
|
1781
|
+
newLocalDecls.push(['local', cb, 'i32'])
|
|
1782
|
+
const tGet = ['local.get', bc.targetLocal]
|
|
1783
|
+
const inRange = ['i32.and', ['i32.ge_s', ['local.get', cb], ['i32.const', 0]], ['i32.le_s', ['local.get', cb], ['i32.const', 255]]]
|
|
1784
|
+
if (bc.isF64) {
|
|
1785
|
+
// cb = (i32)delim; valid iff it round-trips (delim is an integer) and is in [0,255].
|
|
1786
|
+
cbInit = ['local.set', cb, ['i32.trunc_sat_f64_s', cloneNode(tGet)]]
|
|
1787
|
+
guard = ['i32.and', ['f64.eq', ['f64.convert_i32_s', ['local.get', cb]], tGet], inRange]
|
|
1788
|
+
} else {
|
|
1789
|
+
// i32 delim: valid iff already in [0,255] (splat takes the low byte regardless).
|
|
1790
|
+
cbInit = ['local.set', cb, tGet]
|
|
1791
|
+
guard = inRange
|
|
1792
|
+
}
|
|
1793
|
+
splat = ['local.get', cb]
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// bitmask of the 16-lane eq; for `!=` flip the low 16 bits so ctz finds the first non-match.
|
|
1797
|
+
const eqMask = ['i8x16.bitmask', ['i8x16.eq', ['v128.load', ['i32.add', baseGet, iGet]], ['i8x16.splat', splat]]]
|
|
1798
|
+
const scanMask = bc.eq ? eqMask : ['i32.xor', eqMask, ['i32.const', 0xffff]]
|
|
1799
|
+
const simdBlock = ['block', sd,
|
|
1800
|
+
['loop', sl,
|
|
1801
|
+
// Stop before a 16-wide load would pass `bound`; the scalar tail mops up the rest.
|
|
1802
|
+
['br_if', sd, ['i32.gt_s', ['i32.add', iGet, ['i32.const', 16]], cloneNode(bound)]],
|
|
1803
|
+
['local.set', mask, scanMask],
|
|
1804
|
+
['if', ['local.get', mask],
|
|
1805
|
+
['then',
|
|
1806
|
+
['local.set', incVar, ['i32.add', iGet, ['i32.ctz', ['local.get', mask]]]],
|
|
1807
|
+
['br', blockLabel]]],
|
|
1808
|
+
['local.set', incVar, ['i32.add', iGet, ['i32.const', 16]]],
|
|
1809
|
+
['br', sl]
|
|
1810
|
+
]
|
|
1811
|
+
]
|
|
1812
|
+
// Const target: SIMD then scalar tail. Runtime target: cache cb, guard the SIMD, tail.
|
|
1813
|
+
const pre = guard ? [cbInit, ['if', guard, ['then', simdBlock]]] : [simdBlock]
|
|
1814
|
+
return {
|
|
1815
|
+
wrapper: ['block', blockLabel, ...pre, loopNode],
|
|
1816
|
+
newLocalDecls,
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
const cloneNode = (n) => Array.isArray(n) ? n.map(cloneNode) : n
|
|
1821
|
+
|
|
1822
|
+
// ---- Pass entry ------------------------------------------------------------
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Walk a function looking for vectorizable (block (loop)) pairs, in-place.
|
|
1826
|
+
* Adds new locals to the function header.
|
|
1827
|
+
*/
|
|
1828
|
+
export function vectorizeLaneLocal(fn) {
|
|
1829
|
+
if (!isArr(fn) || fn[0] !== 'func') return
|
|
1830
|
+
const bodyStart = findBodyStart(fn)
|
|
1831
|
+
if (bodyStart < 0) return
|
|
1832
|
+
|
|
1833
|
+
// Build local-name → wasm-type map.
|
|
1834
|
+
const fnLocals = new Map()
|
|
1835
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
1836
|
+
const d = fn[i]
|
|
1837
|
+
if (isArr(d) && d[0] === 'local' && typeof d[1] === 'string' && typeof d[2] === 'string') {
|
|
1838
|
+
fnLocals.set(d[1], d[2])
|
|
1839
|
+
} else if (isArr(d) && d[0] === 'param' && typeof d[1] === 'string' && typeof d[2] === 'string') {
|
|
1840
|
+
fnLocals.set(d[1], d[2])
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const freshIdRef = { next: 0 }
|
|
1845
|
+
const newLocalDeclsAll = []
|
|
1846
|
+
|
|
1847
|
+
vectorizeStraightLineF64DotPairsIn(fn, fnLocals, freshIdRef, newLocalDeclsAll)
|
|
1848
|
+
|
|
1849
|
+
// Walk body recursively. Process inner-most matches first (post-order)
|
|
1850
|
+
// so we don't try to vectorize an outer loop whose inner is the lane-local one.
|
|
1851
|
+
function walk(parent, idx) {
|
|
1852
|
+
const node = parent[idx]
|
|
1853
|
+
if (!isArr(node)) return
|
|
1854
|
+
for (let i = 0; i < node.length; i++) {
|
|
1855
|
+
if (isArr(node[i])) walk(node, i)
|
|
1856
|
+
}
|
|
1857
|
+
if (node[0] === 'block') {
|
|
1858
|
+
const r = tryMemCopyFill(node, fnLocals, freshIdRef)
|
|
1859
|
+
?? tryVectorize(node, fnLocals, freshIdRef)
|
|
1860
|
+
?? tryReduceVectorize(node, fnLocals, freshIdRef)
|
|
1861
|
+
?? tryByteScan(node, fnLocals, freshIdRef)
|
|
1862
|
+
?? tryStrengthReduceIV(node, fnLocals, freshIdRef)
|
|
1863
|
+
if (r) {
|
|
1864
|
+
parent[idx] = r.wrapper
|
|
1865
|
+
newLocalDeclsAll.push(...r.newLocalDecls)
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
for (let i = bodyStart; i < fn.length; i++) walk(fn, i)
|
|
1870
|
+
|
|
1871
|
+
if (newLocalDeclsAll.length) {
|
|
1872
|
+
fn.splice(bodyStart, 0, ...newLocalDeclsAll)
|
|
1873
|
+
}
|
|
1874
|
+
}
|