jz 0.1.0 → 0.2.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 +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
package/src/vectorize.js
ADDED
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane-local SIMD-128 vectorizer.
|
|
3
|
+
*
|
|
4
|
+
* Recognizes inner loops of shape:
|
|
5
|
+
* for (let i = 0; i < N; i++) arr[i] = f(arr[i], …)
|
|
6
|
+
* where every body op is "lane-pure" — its k-th lane output depends only
|
|
7
|
+
* on k-th lane inputs. Lifts the body to SIMD-128, prefixed before the
|
|
8
|
+
* original (now tail) loop. Original loop runs the remainder.
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* • Lane-purity is a structural property, not a benchmark match. The op
|
|
12
|
+
* whitelist is the single source of truth (one entry per (lane-type, op)).
|
|
13
|
+
* • Lift is mechanical. The recognizer either matches the structure — in
|
|
14
|
+
* which case lifting is unambiguous — or skips. No bench-specific
|
|
15
|
+
* heuristics.
|
|
16
|
+
* • Tail loop is the original WAT, untouched. If anything regresses the
|
|
17
|
+
* SIMD recognizer just doesn't match, never miscompiles.
|
|
18
|
+
*
|
|
19
|
+
* Match conditions:
|
|
20
|
+
* 1. (block $brk (loop $L (br_if $brk !cond) BODY (i = i+1) (br $L)))
|
|
21
|
+
* 2. cond is `(i32.lt_s i BOUND)` or `i32.lt_u`; BOUND is loop-invariant.
|
|
22
|
+
* 3. All loads/stores in BODY use address `(add base (shl i K))` where
|
|
23
|
+
* base is loop-invariant and K matches the elem stride. Optional
|
|
24
|
+
* enclosing `local.tee` is allowed (and reused).
|
|
25
|
+
* 4. All loads share the same opcode → defines lane type.
|
|
26
|
+
* 5. All other ops in BODY are in the lane-pure whitelist for that type.
|
|
27
|
+
* 6. Each non-induction local in BODY is either purely loop-invariant
|
|
28
|
+
* (only read) or purely lane-local (first action is a write). Never
|
|
29
|
+
* both — that's a loop-carried scalar (reduction / stencil) → bail.
|
|
30
|
+
*
|
|
31
|
+
* Lift produces, before the original block:
|
|
32
|
+
* (local.set $__simd_bound{N} (i32.and BOUND (i32.const ~(LANES-1))))
|
|
33
|
+
* (block $__simd_brk{N}
|
|
34
|
+
* (loop $__simd_loop{N}
|
|
35
|
+
* (br_if $__simd_brk{N} (i32.eqz (i32.lt_s i $__simd_bound{N})))
|
|
36
|
+
* <body lifted op-by-op; lane-local locals routed to v128 shadows>
|
|
37
|
+
* (local.set $i (i32.add i (i32.const LANES)))
|
|
38
|
+
* (br $__simd_loop{N})))
|
|
39
|
+
*
|
|
40
|
+
* The original block runs immediately after with i pre-advanced; its own
|
|
41
|
+
* `i < BOUND` guard handles the tail.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// Local copy of findBodyStart (mirrors optimize.js): index of first non-decl
|
|
45
|
+
// child in a (func ...) form — past type/param/result/local declarations.
|
|
46
|
+
function findBodyStart(fn) {
|
|
47
|
+
for (let i = 2; i < fn.length; i++) {
|
|
48
|
+
const c = fn[i]
|
|
49
|
+
if (!Array.isArray(c)) continue
|
|
50
|
+
if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' ||
|
|
51
|
+
c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
|
|
52
|
+
return i
|
|
53
|
+
}
|
|
54
|
+
return fn.length
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isArr = Array.isArray
|
|
58
|
+
|
|
59
|
+
const exprEq = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
60
|
+
const localGetName = n => isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' ? n[1] : null
|
|
61
|
+
const f64Zero = n => isArr(n) && n[0] === 'f64.const' && Number(n[1]) === 0
|
|
62
|
+
|
|
63
|
+
const matchF64MulLocals = n => {
|
|
64
|
+
if (!isArr(n) || n[0] !== 'f64.mul') return null
|
|
65
|
+
const a = localGetName(n[1])
|
|
66
|
+
const b = localGetName(n[2])
|
|
67
|
+
return a && b ? [a, b] : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const matchAccumStep = (n, acc) => {
|
|
71
|
+
if (!isArr(n) || n[0] !== 'local.set' || n[1] !== acc) return null
|
|
72
|
+
const e = n[2]
|
|
73
|
+
if (!isArr(e) || e[0] !== 'f64.add') return null
|
|
74
|
+
if (localGetName(e[1]) === acc) return matchF64MulLocals(e[2])
|
|
75
|
+
if (localGetName(e[2]) === acc) return matchF64MulLocals(e[1])
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const matchDotStore = (n, acc) => {
|
|
80
|
+
if (!isArr(n) || n[0] !== 'local.set' || typeof n[1] !== 'string') return null
|
|
81
|
+
const e = n[2]
|
|
82
|
+
if (localGetName(e) === acc) return { out: n[1], addend: null }
|
|
83
|
+
if (!isArr(e) || e[0] !== 'f64.add') return null
|
|
84
|
+
if (localGetName(e[1]) === acc) return { out: n[1], addend: e[2] }
|
|
85
|
+
if (localGetName(e[2]) === acc) return { out: n[1], addend: e[1] }
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const matchF64DotSeq = (stmts, i) => {
|
|
90
|
+
const reset = stmts[i]
|
|
91
|
+
if (!isArr(reset) || reset[0] !== 'local.set' || typeof reset[1] !== 'string' || !f64Zero(reset[2])) return null
|
|
92
|
+
const acc = reset[1]
|
|
93
|
+
const left = [], right = []
|
|
94
|
+
for (let k = 0; k < 4; k++) {
|
|
95
|
+
const pair = matchAccumStep(stmts[i + 1 + k], acc)
|
|
96
|
+
if (!pair) return null
|
|
97
|
+
left.push(pair[0])
|
|
98
|
+
right.push(pair[1])
|
|
99
|
+
}
|
|
100
|
+
const store = matchDotStore(stmts[i + 5], acc)
|
|
101
|
+
return store ? { end: i + 6, acc, left, right, ...store } : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const f64x2Pair = (lo, hi) => ['f64x2.replace_lane', 1, ['f64x2.splat', ['local.get', lo]], ['local.get', hi]]
|
|
105
|
+
|
|
106
|
+
const dotPairExpr = (a, pairs) => {
|
|
107
|
+
let expr = ['f64x2.mul', ['f64x2.splat', ['local.get', a[0]]], pairs[0]]
|
|
108
|
+
for (let i = 1; i < 4; i++) {
|
|
109
|
+
expr = ['f64x2.add', expr, ['f64x2.mul', ['f64x2.splat', ['local.get', a[i]]], pairs[i]]]
|
|
110
|
+
}
|
|
111
|
+
return expr
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const vectorizeStraightLineF64DotPairsIn = (node, fnLocals, freshIdRef, newLocalDecls) => {
|
|
115
|
+
if (!isArr(node)) return
|
|
116
|
+
for (let i = 0; i < node.length; i++) {
|
|
117
|
+
const child = node[i]
|
|
118
|
+
if (isArr(child)) vectorizeStraightLineF64DotPairsIn(child, fnLocals, freshIdRef, newLocalDecls)
|
|
119
|
+
}
|
|
120
|
+
const addendTemps = new Map()
|
|
121
|
+
const pairTemps = new Map()
|
|
122
|
+
for (let i = 0; i < node.length;) {
|
|
123
|
+
const a = matchF64DotSeq(node, i)
|
|
124
|
+
if (!a) { i++; continue }
|
|
125
|
+
const b = matchF64DotSeq(node, a.end)
|
|
126
|
+
if (!b || a.acc !== b.acc || !exprEq(a.left, b.left) || !exprEq(a.addend, b.addend) ||
|
|
127
|
+
fnLocals.get(a.out) !== 'f64' || fnLocals.get(b.out) !== 'f64') {
|
|
128
|
+
i++
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const v = `$__dot2_${freshIdRef.next++}`
|
|
132
|
+
newLocalDecls.push(['local', v, 'v128'])
|
|
133
|
+
fnLocals.set(v, 'v128')
|
|
134
|
+
let prefix = []
|
|
135
|
+
let addend = a.addend
|
|
136
|
+
if (addend) {
|
|
137
|
+
const key = JSON.stringify(addend)
|
|
138
|
+
let tmp = addendTemps.get(key)
|
|
139
|
+
if (!tmp) {
|
|
140
|
+
tmp = `$__dotadd_${freshIdRef.next++}`
|
|
141
|
+
addendTemps.set(key, tmp)
|
|
142
|
+
newLocalDecls.push(['local', tmp, 'f64'])
|
|
143
|
+
fnLocals.set(tmp, 'f64')
|
|
144
|
+
prefix = [['local.set', tmp, addend]]
|
|
145
|
+
}
|
|
146
|
+
addend = ['local.get', tmp]
|
|
147
|
+
}
|
|
148
|
+
const pairs = []
|
|
149
|
+
for (let k = 0; k < 4; k++) {
|
|
150
|
+
const key = `${a.right[k]}\0${b.right[k]}`
|
|
151
|
+
let tmp = pairTemps.get(key)
|
|
152
|
+
if (!tmp) {
|
|
153
|
+
tmp = `$__dotpair_${freshIdRef.next++}`
|
|
154
|
+
pairTemps.set(key, tmp)
|
|
155
|
+
newLocalDecls.push(['local', tmp, 'v128'])
|
|
156
|
+
fnLocals.set(tmp, 'v128')
|
|
157
|
+
prefix.push(['local.set', tmp, f64x2Pair(a.right[k], b.right[k])])
|
|
158
|
+
}
|
|
159
|
+
pairs.push(['local.get', tmp])
|
|
160
|
+
}
|
|
161
|
+
const dot = dotPairExpr(a.left, pairs)
|
|
162
|
+
const expr = addend ? ['f64x2.add', dot, ['f64x2.splat', addend]] : dot
|
|
163
|
+
node.splice(i, b.end - i,
|
|
164
|
+
...prefix,
|
|
165
|
+
['local.set', v, expr],
|
|
166
|
+
['local.set', a.out, ['f64x2.extract_lane', 0, ['local.get', v]]],
|
|
167
|
+
['local.set', b.out, ['f64x2.extract_lane', 1, ['local.get', v]]],
|
|
168
|
+
)
|
|
169
|
+
i += prefix.length + 3
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- Lane type tables ------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
const LANE_INFO = {
|
|
176
|
+
i8: { lanes: 16, strideLog2: 0, stride: 1, splat: 'i8x16.splat', constOp: 'i32.const' },
|
|
177
|
+
i16: { lanes: 8, strideLog2: 1, stride: 2, splat: 'i16x8.splat', constOp: 'i32.const' },
|
|
178
|
+
i32: { lanes: 4, strideLog2: 2, stride: 4, splat: 'i32x4.splat', constOp: 'i32.const' },
|
|
179
|
+
i64: { lanes: 2, strideLog2: 3, stride: 8, splat: 'i64x2.splat', constOp: 'i64.const' },
|
|
180
|
+
f32: { lanes: 4, strideLog2: 2, stride: 4, splat: 'f32x4.splat', constOp: 'f32.const' },
|
|
181
|
+
f64: { lanes: 2, strideLog2: 3, stride: 8, splat: 'f64x2.splat', constOp: 'f64.const' },
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Narrow loads/stores (i32.load8_u etc.) define i8 / i16 lane types — values
|
|
185
|
+
// computed in i32 then truncated by store{8,16}, which matches i{8,16}xN wrap
|
|
186
|
+
// semantics exactly.
|
|
187
|
+
const LOAD_OPS = {
|
|
188
|
+
'i32.load8_u': 'i8', 'i32.load8_s': 'i8',
|
|
189
|
+
'i32.load16_u': 'i16','i32.load16_s': 'i16',
|
|
190
|
+
'i32.load': 'i32', 'i64.load': 'i64', 'f32.load': 'f32', 'f64.load': 'f64',
|
|
191
|
+
}
|
|
192
|
+
const STORE_OPS = {
|
|
193
|
+
'i32.store8': 'i8', 'i32.store16': 'i16',
|
|
194
|
+
'i32.store': 'i32', 'i64.store': 'i64', 'f32.store': 'f32', 'f64.store': 'f64',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// scalar op → SIMD op. shamtScalar:true means second operand stays scalar i32.
|
|
198
|
+
//
|
|
199
|
+
// For i8/i16 lanes the SCALAR ops are i32.* — wasm has no native i8/i16 ops,
|
|
200
|
+
// values flow as i32 and the trailing store{8,16} truncates. i{8,16}x{N}.add
|
|
201
|
+
// wraps within each lane the same way, so the observable result matches.
|
|
202
|
+
// Note: wasm SIMD has no i8x16.mul, so multiplication on byte arrays bails.
|
|
203
|
+
const LANE_PURE = {
|
|
204
|
+
// Right shifts intentionally omitted for narrow lanes: scalar emits
|
|
205
|
+
// i32.shr_{s,u} on a load8/load16 i32 (zero- or sign-extended), while
|
|
206
|
+
// i{8,16}x{N}.shr_{s,u} treats lanes as their narrow type. The two diverge
|
|
207
|
+
// when load and shift signedness mismatch (e.g. load8_u + shr_s on byte
|
|
208
|
+
// 0xFF: scalar=0x7F, SIMD=0xFF). Safe set excludes shr_*.
|
|
209
|
+
i8: new Map([
|
|
210
|
+
['i32.add', { simd: 'i8x16.add' }],
|
|
211
|
+
['i32.sub', { simd: 'i8x16.sub' }],
|
|
212
|
+
['i32.and', { simd: 'v128.and' }],
|
|
213
|
+
['i32.or', { simd: 'v128.or' }],
|
|
214
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
215
|
+
['i32.shl', { simd: 'i8x16.shl', shamtScalar: true }],
|
|
216
|
+
]),
|
|
217
|
+
i16: new Map([
|
|
218
|
+
['i32.add', { simd: 'i16x8.add' }],
|
|
219
|
+
['i32.sub', { simd: 'i16x8.sub' }],
|
|
220
|
+
['i32.mul', { simd: 'i16x8.mul' }],
|
|
221
|
+
['i32.and', { simd: 'v128.and' }],
|
|
222
|
+
['i32.or', { simd: 'v128.or' }],
|
|
223
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
224
|
+
['i32.shl', { simd: 'i16x8.shl', shamtScalar: true }],
|
|
225
|
+
]),
|
|
226
|
+
i32: new Map([
|
|
227
|
+
['i32.add', { simd: 'i32x4.add' }],
|
|
228
|
+
['i32.sub', { simd: 'i32x4.sub' }],
|
|
229
|
+
['i32.mul', { simd: 'i32x4.mul' }],
|
|
230
|
+
['i32.and', { simd: 'v128.and' }],
|
|
231
|
+
['i32.or', { simd: 'v128.or' }],
|
|
232
|
+
['i32.xor', { simd: 'v128.xor' }],
|
|
233
|
+
['i32.shl', { simd: 'i32x4.shl', shamtScalar: true }],
|
|
234
|
+
['i32.shr_s', { simd: 'i32x4.shr_s', shamtScalar: true }],
|
|
235
|
+
['i32.shr_u', { simd: 'i32x4.shr_u', shamtScalar: true }],
|
|
236
|
+
]),
|
|
237
|
+
i64: new Map([
|
|
238
|
+
['i64.add', { simd: 'i64x2.add' }],
|
|
239
|
+
['i64.sub', { simd: 'i64x2.sub' }],
|
|
240
|
+
['i64.mul', { simd: 'i64x2.mul' }],
|
|
241
|
+
['i64.and', { simd: 'v128.and' }],
|
|
242
|
+
['i64.or', { simd: 'v128.or' }],
|
|
243
|
+
['i64.xor', { simd: 'v128.xor' }],
|
|
244
|
+
['i64.shl', { simd: 'i64x2.shl', shamtScalar: true }],
|
|
245
|
+
['i64.shr_s', { simd: 'i64x2.shr_s', shamtScalar: true }],
|
|
246
|
+
['i64.shr_u', { simd: 'i64x2.shr_u', shamtScalar: true }],
|
|
247
|
+
]),
|
|
248
|
+
f32: new Map([
|
|
249
|
+
['f32.add', { simd: 'f32x4.add' }],
|
|
250
|
+
['f32.sub', { simd: 'f32x4.sub' }],
|
|
251
|
+
['f32.mul', { simd: 'f32x4.mul' }],
|
|
252
|
+
['f32.div', { simd: 'f32x4.div' }],
|
|
253
|
+
['f32.min', { simd: 'f32x4.min' }],
|
|
254
|
+
['f32.max', { simd: 'f32x4.max' }],
|
|
255
|
+
['f32.neg', { simd: 'f32x4.neg' }],
|
|
256
|
+
['f32.abs', { simd: 'f32x4.abs' }],
|
|
257
|
+
['f32.sqrt', { simd: 'f32x4.sqrt' }],
|
|
258
|
+
]),
|
|
259
|
+
f64: new Map([
|
|
260
|
+
['f64.add', { simd: 'f64x2.add' }],
|
|
261
|
+
['f64.sub', { simd: 'f64x2.sub' }],
|
|
262
|
+
['f64.mul', { simd: 'f64x2.mul' }],
|
|
263
|
+
['f64.div', { simd: 'f64x2.div' }],
|
|
264
|
+
['f64.min', { simd: 'f64x2.min' }],
|
|
265
|
+
['f64.max', { simd: 'f64x2.max' }],
|
|
266
|
+
['f64.neg', { simd: 'f64x2.neg' }],
|
|
267
|
+
['f64.abs', { simd: 'f64x2.abs' }],
|
|
268
|
+
['f64.sqrt', { simd: 'f64x2.sqrt' }],
|
|
269
|
+
]),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Horizontal reductions: associative+commutative ops applied to one
|
|
273
|
+
// loop-carried accumulator. Each entry maps the SCALAR op (which is also
|
|
274
|
+
// the op used to combine the SIMD result back into the accumulator at the
|
|
275
|
+
// end) to its SIMD lane op, lane extractor, and identity element.
|
|
276
|
+
//
|
|
277
|
+
// Floats (add) are not strictly associative — vectorized order produces
|
|
278
|
+
// ulp-level differences from scalar order. Acceptable for typical use
|
|
279
|
+
// (reductions over typed arrays of well-conditioned data); strict-equal
|
|
280
|
+
// callers must keep the pass off.
|
|
281
|
+
//
|
|
282
|
+
// Narrow lanes (i8/i16) intentionally absent: `s += a[i]` with a u8/u16
|
|
283
|
+
// load expands the value to i32 before the add, so the accumulator's lane
|
|
284
|
+
// type is always wider than the load's element type. That widening would
|
|
285
|
+
// require pairwise/extending-add ops (i16x8.extadd_pairwise_*) — separate
|
|
286
|
+
// recognizer.
|
|
287
|
+
const REDUCE_OPS = {
|
|
288
|
+
i32: {
|
|
289
|
+
'i32.add': { simd: 'i32x4.add', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
290
|
+
'i32.xor': { simd: 'v128.xor', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
291
|
+
'i32.and': { simd: 'v128.and', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', -1] },
|
|
292
|
+
'i32.or': { simd: 'v128.or', extract: 'i32x4.extract_lane', laneType: 'i32', constNode: ['i32.const', 0] },
|
|
293
|
+
},
|
|
294
|
+
i64: {
|
|
295
|
+
'i64.add': { simd: 'i64x2.add', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
296
|
+
'i64.xor': { simd: 'v128.xor', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
297
|
+
'i64.and': { simd: 'v128.and', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', -1] },
|
|
298
|
+
'i64.or': { simd: 'v128.or', extract: 'i64x2.extract_lane', laneType: 'i64', constNode: ['i64.const', 0] },
|
|
299
|
+
},
|
|
300
|
+
f32: {
|
|
301
|
+
'f32.add': { simd: 'f32x4.add', extract: 'f32x4.extract_lane', laneType: 'f32', constNode: ['f32.const', 0] },
|
|
302
|
+
},
|
|
303
|
+
f64: {
|
|
304
|
+
'f64.add': { simd: 'f64x2.add', extract: 'f64x2.extract_lane', laneType: 'f64', constNode: ['f64.const', 0] },
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// op-name → REDUCE entry across all lane types (the op-name itself encodes
|
|
309
|
+
// the lane type prefix, e.g. `i32.add` ⇒ i32 lanes).
|
|
310
|
+
const REDUCE_OP_LOOKUP = (() => {
|
|
311
|
+
const m = new Map()
|
|
312
|
+
for (const lt of Object.keys(REDUCE_OPS))
|
|
313
|
+
for (const op of Object.keys(REDUCE_OPS[lt]))
|
|
314
|
+
m.set(op, REDUCE_OPS[lt][op])
|
|
315
|
+
return m
|
|
316
|
+
})()
|
|
317
|
+
|
|
318
|
+
// ---- Recognizer ------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
function isLocalGet(node, name) {
|
|
321
|
+
return isArr(node) && node[0] === 'local.get' && (name == null || node[1] === name)
|
|
322
|
+
}
|
|
323
|
+
function isI32Const(node) {
|
|
324
|
+
return isArr(node) && node[0] === 'i32.const'
|
|
325
|
+
}
|
|
326
|
+
function constNum(node) {
|
|
327
|
+
if (!isI32Const(node)) return null
|
|
328
|
+
const v = node[1]
|
|
329
|
+
return typeof v === 'number' ? v : (typeof v === 'string' ? parseInt(v, 10) : null)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Match increment shape `(local.set $X (i32.add (local.get $X) (i32.const 1)))`.
|
|
334
|
+
* Returns $X or null.
|
|
335
|
+
*/
|
|
336
|
+
function matchInc1(stmt) {
|
|
337
|
+
if (!isArr(stmt) || stmt[0] !== 'local.set' || stmt.length !== 3) return null
|
|
338
|
+
const x = stmt[1]
|
|
339
|
+
const v = stmt[2]
|
|
340
|
+
if (!isArr(v) || v[0] !== 'i32.add' || v.length !== 3) return null
|
|
341
|
+
if (!isLocalGet(v[1], x)) return null
|
|
342
|
+
if (constNum(v[2]) !== 1) return null
|
|
343
|
+
return x
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Match `(br_if $LABEL (i32.eqz (i32.lt_{s,u} (local.get $I) BOUND)))`.
|
|
348
|
+
* Returns { ind, bound } or null.
|
|
349
|
+
*/
|
|
350
|
+
function matchExitBrIf(stmt, label) {
|
|
351
|
+
if (!isArr(stmt) || stmt[0] !== 'br_if' || stmt[1] !== label) return null
|
|
352
|
+
const cond = stmt[2]
|
|
353
|
+
if (!isArr(cond) || cond[0] !== 'i32.eqz') return null
|
|
354
|
+
const cmp = cond[1]
|
|
355
|
+
if (!isArr(cmp) || (cmp[0] !== 'i32.lt_s' && cmp[0] !== 'i32.lt_u')) return null
|
|
356
|
+
if (!isLocalGet(cmp[1])) return null
|
|
357
|
+
return { ind: cmp[1][1], bound: cmp[2] }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Walk node, collect set of local names that are written via local.set/local.tee
|
|
362
|
+
* anywhere within. Used to detect loop-invariant locals.
|
|
363
|
+
*/
|
|
364
|
+
function collectWrites(node, out) {
|
|
365
|
+
if (!isArr(node)) return
|
|
366
|
+
const op = node[0]
|
|
367
|
+
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string') {
|
|
368
|
+
out.add(node[1])
|
|
369
|
+
}
|
|
370
|
+
for (let i = 0; i < node.length; i++) collectWrites(node[i], out)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Return the FIRST kind of access for `name` in straight-line walk order.
|
|
375
|
+
* 'write' — local.set/local.tee seen first
|
|
376
|
+
* 'read' — local.get seen first
|
|
377
|
+
* null — not referenced
|
|
378
|
+
*/
|
|
379
|
+
function firstAccess(node, name) {
|
|
380
|
+
if (!isArr(node)) return null
|
|
381
|
+
const op = node[0]
|
|
382
|
+
// Walk children first — operands evaluate before the op. For local.set/tee
|
|
383
|
+
// the VALUE child (idx 2) runs before the write, so a `local.get name` in
|
|
384
|
+
// the value of `local.set name` is a read-before-write.
|
|
385
|
+
if ((op === 'local.set' || op === 'local.tee') && node[1] === name) {
|
|
386
|
+
if (node.length >= 3) {
|
|
387
|
+
const r = firstAccess(node[2], name)
|
|
388
|
+
if (r) return r
|
|
389
|
+
}
|
|
390
|
+
return 'write'
|
|
391
|
+
}
|
|
392
|
+
if (op === 'local.get' && node[1] === name) return 'read'
|
|
393
|
+
for (let i = 1; i < node.length; i++) {
|
|
394
|
+
const r = firstAccess(node[i], name)
|
|
395
|
+
if (r) return r
|
|
396
|
+
}
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Match an address expression `(i32.add base (i32.shl (local.get IND) (i32.const K)))`,
|
|
402
|
+
* with optional outer `(local.tee $A ...)`. Stride 1 fallback: `(i32.add base (local.get IND))`.
|
|
403
|
+
* Also accepts `(local.get $A)` when $A is a previously-recorded address tee.
|
|
404
|
+
*
|
|
405
|
+
* Returns { strideLog2, base, teeName?: string, viaLocal?: string } or null.
|
|
406
|
+
* `strideLog2` = K for i32.shl form, 0 for plain add form.
|
|
407
|
+
* `base` is the loop-invariant base subtree.
|
|
408
|
+
*/
|
|
409
|
+
function matchLaneAddr(addr, ind, addrLocals) {
|
|
410
|
+
let teeName = null
|
|
411
|
+
let n = addr
|
|
412
|
+
// (local.get $A) where $A holds a previously-tee'd lane-address.
|
|
413
|
+
if (isArr(n) && n[0] === 'local.get' && typeof n[1] === 'string' && addrLocals && addrLocals.has(n[1])) {
|
|
414
|
+
const e = addrLocals.get(n[1])
|
|
415
|
+
return { strideLog2: e.strideLog2, base: e.base, teeName: null, viaLocal: n[1] }
|
|
416
|
+
}
|
|
417
|
+
if (isArr(n) && n[0] === 'local.tee' && n.length === 3) {
|
|
418
|
+
teeName = n[1]
|
|
419
|
+
n = n[2]
|
|
420
|
+
}
|
|
421
|
+
if (!isArr(n) || n[0] !== 'i32.add' || n.length !== 3) return null
|
|
422
|
+
const a = n[1], b = n[2]
|
|
423
|
+
// case 1: (i32.add base (i32.shl (local.get ind) (i32.const K)))
|
|
424
|
+
if (isArr(b) && b[0] === 'i32.shl' && b.length === 3 && isLocalGet(b[1], ind)) {
|
|
425
|
+
const k = constNum(b[2])
|
|
426
|
+
if (k != null && k >= 0 && k <= 3) return { strideLog2: k, base: a, teeName }
|
|
427
|
+
}
|
|
428
|
+
// case 2: (i32.add base (local.get ind)) — stride 1
|
|
429
|
+
if (isLocalGet(b, ind)) return { strideLog2: 0, base: a, teeName }
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ---- Recognize a (block (loop)) pair --------------------------------------
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Try to vectorize the inner loop. Returns the replacement node array
|
|
437
|
+
* (synthetic outer block) or null on no match.
|
|
438
|
+
*/
|
|
439
|
+
function tryVectorize(blockNode, fnLocals, freshIdRef) {
|
|
440
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
441
|
+
// Find label and inner loop.
|
|
442
|
+
let blockLabel = null
|
|
443
|
+
let loopIdx = -1, loopNode = null
|
|
444
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
445
|
+
const c = blockNode[i]
|
|
446
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) {
|
|
447
|
+
blockLabel = c; continue
|
|
448
|
+
}
|
|
449
|
+
if (isArr(c) && c[0] === 'loop') {
|
|
450
|
+
if (loopNode) return null // multiple loops
|
|
451
|
+
loopIdx = i; loopNode = c
|
|
452
|
+
} else if (isArr(c)) {
|
|
453
|
+
return null // foreign content alongside the loop
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!loopNode || !blockLabel) return null
|
|
457
|
+
|
|
458
|
+
// Loop layout: ['loop', '$label', ...stmts]
|
|
459
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
460
|
+
if (!loopLabel) return null
|
|
461
|
+
|
|
462
|
+
// Find induction increment + back-branch at the END.
|
|
463
|
+
let endIdx = loopNode.length - 1
|
|
464
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
465
|
+
const incIdx = endIdx - 1
|
|
466
|
+
const incVar = matchInc1(loopNode[incIdx])
|
|
467
|
+
if (!incVar) return null
|
|
468
|
+
|
|
469
|
+
// First stmt must be the exit br_if.
|
|
470
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
471
|
+
if (!exitInfo) return null
|
|
472
|
+
if (exitInfo.ind !== incVar) return null
|
|
473
|
+
|
|
474
|
+
// Body = stmts between exit and increment.
|
|
475
|
+
const body = []
|
|
476
|
+
for (let i = 3; i < incIdx; i++) body.push(loopNode[i])
|
|
477
|
+
|
|
478
|
+
// Bound must be loop-invariant. For now, accept (local.get $L) where $L
|
|
479
|
+
// is declared but not written inside the body, OR (i32.const N).
|
|
480
|
+
let bound = exitInfo.bound
|
|
481
|
+
let boundLocal = null
|
|
482
|
+
if (isArr(bound) && bound[0] === 'local.get' && typeof bound[1] === 'string') {
|
|
483
|
+
boundLocal = bound[1]
|
|
484
|
+
} else if (isI32Const(bound)) {
|
|
485
|
+
// ok
|
|
486
|
+
} else {
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Detect lane type from the FIRST load in body.
|
|
491
|
+
let laneType = null
|
|
492
|
+
let stride = -1
|
|
493
|
+
const loadStoreSites = [] // {parent, idx, kind:'load'|'store'}
|
|
494
|
+
// Address tees: name → {strideLog2, base}. A `(local.tee NAME (lane-addr))`
|
|
495
|
+
// both validates the load's address AND records NAME so the matching store's
|
|
496
|
+
// `(local.get NAME)` is accepted as the same lane address.
|
|
497
|
+
const addrLocals = new Map()
|
|
498
|
+
|
|
499
|
+
function scanForLoadsStores(node, parent, pi) {
|
|
500
|
+
if (!isArr(node)) return true
|
|
501
|
+
const op = node[0]
|
|
502
|
+
if (LOAD_OPS[op]) {
|
|
503
|
+
if (laneType == null) {
|
|
504
|
+
laneType = LOAD_OPS[op]
|
|
505
|
+
stride = LANE_INFO[laneType].stride
|
|
506
|
+
} else if (LOAD_OPS[op] !== laneType) {
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
510
|
+
if (!m) return false
|
|
511
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
512
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
513
|
+
loadStoreSites.push({ parent, idx: pi, kind: 'load' })
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
if (STORE_OPS[op]) {
|
|
517
|
+
const sty = STORE_OPS[op]
|
|
518
|
+
if (laneType != null && sty !== laneType) return false
|
|
519
|
+
if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
|
|
520
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
521
|
+
if (!m) return false
|
|
522
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
523
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
524
|
+
loadStoreSites.push({ parent, idx: pi, kind: 'store' })
|
|
525
|
+
// Recurse into VALUE child (idx 2) — it's data, not address.
|
|
526
|
+
if (!scanForLoadsStores(node[2], node, 2)) return false
|
|
527
|
+
return true
|
|
528
|
+
}
|
|
529
|
+
// local.set/tee of an address local outside a load/store context (e.g.
|
|
530
|
+
// `(local.set $a (i32.add base (i32.shl i 2)))` as a standalone stmt) —
|
|
531
|
+
// record so a later `(local.get $a)` resolves.
|
|
532
|
+
if ((op === 'local.set' || op === 'local.tee') && typeof node[1] === 'string' && node.length === 3) {
|
|
533
|
+
const valM = matchLaneAddr(['local.tee', node[1], node[2]], incVar, addrLocals)
|
|
534
|
+
if (valM && valM.teeName) {
|
|
535
|
+
addrLocals.set(valM.teeName, { strideLog2: valM.strideLog2, base: valM.base })
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Recurse into all children
|
|
539
|
+
for (let i = 1; i < node.length; i++) {
|
|
540
|
+
if (!scanForLoadsStores(node[i], node, i)) return false
|
|
541
|
+
}
|
|
542
|
+
return true
|
|
543
|
+
}
|
|
544
|
+
for (const stmt of body) {
|
|
545
|
+
if (!scanForLoadsStores(stmt, null, -1)) return null
|
|
546
|
+
}
|
|
547
|
+
if (!laneType) return null // no memory ops — vectorizing buys nothing
|
|
548
|
+
if (loadStoreSites.length === 0) return null
|
|
549
|
+
|
|
550
|
+
// Classify all locals referenced in body.
|
|
551
|
+
// - induction var (incVar): exempt
|
|
552
|
+
// - bound local (if any): must be invariant
|
|
553
|
+
// - each other local: first access must not be a read-then-written pattern
|
|
554
|
+
const writes = new Set()
|
|
555
|
+
for (const s of body) collectWrites(s, writes)
|
|
556
|
+
if (boundLocal && writes.has(boundLocal)) return null // bound varies in body → bail
|
|
557
|
+
|
|
558
|
+
const localKind = new Map() // name → 'lane' | 'invariant' | 'addr'
|
|
559
|
+
// Walk to collect ALL referenced names
|
|
560
|
+
const referenced = new Set()
|
|
561
|
+
const collectRefs = (n) => {
|
|
562
|
+
if (!isArr(n)) return
|
|
563
|
+
const op = n[0]
|
|
564
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string')
|
|
565
|
+
referenced.add(n[1])
|
|
566
|
+
for (let i = 1; i < n.length; i++) collectRefs(n[i])
|
|
567
|
+
}
|
|
568
|
+
for (const s of body) collectRefs(s)
|
|
569
|
+
|
|
570
|
+
for (const name of referenced) {
|
|
571
|
+
if (name === incVar) continue
|
|
572
|
+
if (writes.has(name)) {
|
|
573
|
+
// Must be lane-local: first access is a write.
|
|
574
|
+
let firstKind = null
|
|
575
|
+
for (const s of body) {
|
|
576
|
+
const k = firstAccess(s, name)
|
|
577
|
+
if (k) { firstKind = k; break }
|
|
578
|
+
}
|
|
579
|
+
if (firstKind === 'read') return null // loop-carried (reduction or stencil)
|
|
580
|
+
// Discriminate lane-data vs address-tee. Address tees hold i32 addresses,
|
|
581
|
+
// not vector data. We classify by checking the local's declared type.
|
|
582
|
+
const decl = fnLocals.get(name)
|
|
583
|
+
if (decl === 'i32' && _isAddressLocal(body, name, incVar)) {
|
|
584
|
+
localKind.set(name, 'addr')
|
|
585
|
+
} else {
|
|
586
|
+
localKind.set(name, 'lane')
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
localKind.set(name, 'invariant')
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Build lifted body. If anything fails to lift, bail.
|
|
594
|
+
const newLanedLocals = new Map() // origName → { laneName, simdType }
|
|
595
|
+
const ctx = { laneType, incVar, localKind, newLanedLocals, fail: false, failReason: null }
|
|
596
|
+
const lifted = []
|
|
597
|
+
for (const s of body) {
|
|
598
|
+
const r = liftStmt(s, ctx)
|
|
599
|
+
if (ctx.fail) return null
|
|
600
|
+
if (r != null) {
|
|
601
|
+
if (Array.isArray(r) && r[0] === '__seq__') lifted.push(...r.slice(1))
|
|
602
|
+
else lifted.push(r)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (lifted.length === 0) return null
|
|
606
|
+
|
|
607
|
+
// Generate fresh names
|
|
608
|
+
const id = freshIdRef.next++
|
|
609
|
+
const simdBoundName = `$__simd_bound${id}`
|
|
610
|
+
const simdBrkLabel = `$__simd_brk${id}`
|
|
611
|
+
const simdLoopLabel = `$__simd_loop${id}`
|
|
612
|
+
|
|
613
|
+
const info = LANE_INFO[laneType]
|
|
614
|
+
const lanes = info.lanes
|
|
615
|
+
const mask = -lanes // bit pattern ~(lanes-1) in i32 two's complement
|
|
616
|
+
|
|
617
|
+
// Build SIMD prefix block.
|
|
618
|
+
const boundExpr = boundLocal
|
|
619
|
+
? ['local.get', boundLocal]
|
|
620
|
+
: bound // i32.const N
|
|
621
|
+
const simdBlock = ['block', simdBrkLabel,
|
|
622
|
+
['loop', simdLoopLabel,
|
|
623
|
+
['br_if', simdBrkLabel,
|
|
624
|
+
['i32.eqz', ['i32.lt_s', ['local.get', incVar], ['local.get', simdBoundName]]]],
|
|
625
|
+
...lifted,
|
|
626
|
+
['local.set', incVar,
|
|
627
|
+
['i32.add', ['local.get', incVar], ['i32.const', lanes]]],
|
|
628
|
+
['br', simdLoopLabel]
|
|
629
|
+
]
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
// Bound setup: simdBoundName = bound & ~(lanes-1)
|
|
633
|
+
const boundSetup = ['local.set', simdBoundName,
|
|
634
|
+
['i32.and', boundExpr, ['i32.const', mask]]]
|
|
635
|
+
|
|
636
|
+
// Synthetic outer wrapper — has no result, no label, just sequences.
|
|
637
|
+
// The original block is preserved unchanged as the tail.
|
|
638
|
+
const wrapper = ['block', boundSetup, simdBlock, blockNode]
|
|
639
|
+
|
|
640
|
+
// Locals to add to function header.
|
|
641
|
+
const newLocalDecls = [
|
|
642
|
+
['local', simdBoundName, 'i32'],
|
|
643
|
+
...[...newLanedLocals.values()].map(({ laneName }) => ['local', laneName, 'v128'])
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
return { wrapper, newLocalDecls }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ---- Reduction recognizer -------------------------------------------------
|
|
650
|
+
//
|
|
651
|
+
// Matches inner loops of shape:
|
|
652
|
+
// for (let i = 0; i < N; i++) S = OP(S, EXPR(arr[i], ...))
|
|
653
|
+
// where OP is associative+commutative (REDUCE_OPS table) and EXPR is lane-
|
|
654
|
+
// pure (operates on the loaded element with at most loop-invariant data).
|
|
655
|
+
// S is a SCALAR loop-carried accumulator — exempt from the lane-local
|
|
656
|
+
// "first access must be a write" check.
|
|
657
|
+
//
|
|
658
|
+
// Lift:
|
|
659
|
+
// acc = splat(IDENTITY)
|
|
660
|
+
// for (i = 0; i < bound & ~(L-1); i += L) acc = OP_v(acc, lifted EXPR)
|
|
661
|
+
// S = OP(S, horizontal_reduce(acc))
|
|
662
|
+
// <original scalar tail handles the remainder>
|
|
663
|
+
//
|
|
664
|
+
// Float adds are not strictly associative — vectorized reduction differs
|
|
665
|
+
// from scalar reduction by ulps. Acceptable when bit-exact equality is not
|
|
666
|
+
// required (which it isn't, by spec, in JS engines either).
|
|
667
|
+
function tryReduceVectorize(blockNode, fnLocals, freshIdRef) {
|
|
668
|
+
if (!isArr(blockNode) || blockNode[0] !== 'block') return null
|
|
669
|
+
|
|
670
|
+
// Match outer (block (loop)) structure. Same loop-shape as tryVectorize.
|
|
671
|
+
let blockLabel = null
|
|
672
|
+
let loopNode = null
|
|
673
|
+
for (let i = 1; i < blockNode.length; i++) {
|
|
674
|
+
const c = blockNode[i]
|
|
675
|
+
if (typeof c === 'string' && c.startsWith('$') && blockLabel == null && i === 1) { blockLabel = c; continue }
|
|
676
|
+
if (isArr(c) && c[0] === 'loop') {
|
|
677
|
+
if (loopNode) return null
|
|
678
|
+
loopNode = c
|
|
679
|
+
} else if (isArr(c)) return null
|
|
680
|
+
}
|
|
681
|
+
if (!loopNode || !blockLabel) return null
|
|
682
|
+
const loopLabel = typeof loopNode[1] === 'string' && loopNode[1].startsWith('$') ? loopNode[1] : null
|
|
683
|
+
if (!loopLabel) return null
|
|
684
|
+
const endIdx = loopNode.length - 1
|
|
685
|
+
if (!(isArr(loopNode[endIdx]) && loopNode[endIdx][0] === 'br' && loopNode[endIdx][1] === loopLabel)) return null
|
|
686
|
+
const incIdx = endIdx - 1
|
|
687
|
+
const incVar = matchInc1(loopNode[incIdx])
|
|
688
|
+
if (!incVar) return null
|
|
689
|
+
const exitInfo = matchExitBrIf(loopNode[2], blockLabel)
|
|
690
|
+
if (!exitInfo) return null
|
|
691
|
+
if (exitInfo.ind !== incVar) return null
|
|
692
|
+
|
|
693
|
+
// Body must be a single statement: the accumulator update.
|
|
694
|
+
if (incIdx - 3 !== 1) return null
|
|
695
|
+
const stmt = loopNode[3]
|
|
696
|
+
if (!isArr(stmt) || stmt[0] !== 'local.set' || stmt.length !== 3) return null
|
|
697
|
+
const accName = stmt[1]
|
|
698
|
+
if (typeof accName !== 'string') return null
|
|
699
|
+
const rhs = stmt[2]
|
|
700
|
+
if (!isArr(rhs) || rhs.length !== 3) return null
|
|
701
|
+
const opName = rhs[0]
|
|
702
|
+
const reduceEntry = REDUCE_OP_LOOKUP.get(opName)
|
|
703
|
+
if (!reduceEntry) return null
|
|
704
|
+
if (!isLocalGet(rhs[1], accName)) return null
|
|
705
|
+
const exprNode = rhs[2]
|
|
706
|
+
|
|
707
|
+
// Accumulator's declared local type must match the lane element type.
|
|
708
|
+
const accType = fnLocals.get(accName)
|
|
709
|
+
if (accType !== reduceEntry.laneType) return null
|
|
710
|
+
|
|
711
|
+
// Bound classification (same as tryVectorize).
|
|
712
|
+
let bound = exitInfo.bound
|
|
713
|
+
let boundLocal = null
|
|
714
|
+
if (isArr(bound) && bound[0] === 'local.get' && typeof bound[1] === 'string') boundLocal = bound[1]
|
|
715
|
+
else if (!isI32Const(bound)) return null
|
|
716
|
+
|
|
717
|
+
// Scan EXPR for lane-aligned loads. Stores forbidden. Re-references of
|
|
718
|
+
// accName forbidden (the accumulator only appears in the outer wrapper).
|
|
719
|
+
const laneType = reduceEntry.laneType
|
|
720
|
+
const stride = LANE_INFO[laneType].stride
|
|
721
|
+
const addrLocals = new Map()
|
|
722
|
+
let loadCount = 0
|
|
723
|
+
function scanExpr(node) {
|
|
724
|
+
if (!isArr(node)) return true
|
|
725
|
+
const op = node[0]
|
|
726
|
+
if (LOAD_OPS[op]) {
|
|
727
|
+
if (LOAD_OPS[op] !== laneType) return false
|
|
728
|
+
const m = matchLaneAddr(node[1], incVar, addrLocals)
|
|
729
|
+
if (!m) return false
|
|
730
|
+
if ((1 << m.strideLog2) !== stride) return false
|
|
731
|
+
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, base: m.base })
|
|
732
|
+
loadCount++
|
|
733
|
+
return true
|
|
734
|
+
}
|
|
735
|
+
if (STORE_OPS[op]) return false
|
|
736
|
+
if (op === 'local.set' || op === 'local.tee') return false // no intermediates
|
|
737
|
+
if (op === 'local.get' && node[1] === accName) return false
|
|
738
|
+
for (let i = 1; i < node.length; i++) if (!scanExpr(node[i])) return false
|
|
739
|
+
return true
|
|
740
|
+
}
|
|
741
|
+
if (!scanExpr(exprNode)) return null
|
|
742
|
+
if (loadCount === 0) return null
|
|
743
|
+
|
|
744
|
+
// Classify locals referenced in EXPR. Anything not the induction var or an
|
|
745
|
+
// address-tee is invariant (we forbade local.set/tee in scanExpr).
|
|
746
|
+
const referenced = new Set()
|
|
747
|
+
const collectRefs = (n) => {
|
|
748
|
+
if (!isArr(n)) return
|
|
749
|
+
if (n[0] === 'local.get' && typeof n[1] === 'string') referenced.add(n[1])
|
|
750
|
+
for (let i = 1; i < n.length; i++) collectRefs(n[i])
|
|
751
|
+
}
|
|
752
|
+
collectRefs(exprNode)
|
|
753
|
+
const localKind = new Map()
|
|
754
|
+
for (const name of referenced) {
|
|
755
|
+
if (name === incVar) continue
|
|
756
|
+
if (addrLocals.has(name)) { localKind.set(name, 'addr'); continue }
|
|
757
|
+
localKind.set(name, 'invariant')
|
|
758
|
+
}
|
|
759
|
+
for (const name of addrLocals.keys()) localKind.set(name, 'addr')
|
|
760
|
+
|
|
761
|
+
const ctx = { laneType, incVar, localKind, newLanedLocals: new Map(), fail: false, failReason: null }
|
|
762
|
+
const liftedExpr = liftExprV(exprNode, ctx)
|
|
763
|
+
if (ctx.fail) return null
|
|
764
|
+
if (ctx.newLanedLocals.size > 0) return null
|
|
765
|
+
|
|
766
|
+
// Synthesize SIMD prefix block + horizontal reduce + (preserved scalar tail).
|
|
767
|
+
const id = freshIdRef.next++
|
|
768
|
+
const simdBoundName = `$__simd_bound${id}`
|
|
769
|
+
const simdAccName = `$__simd_acc${id}`
|
|
770
|
+
const simdBrkLabel = `$__simd_brk${id}`
|
|
771
|
+
const simdLoopLabel = `$__simd_loop${id}`
|
|
772
|
+
const info = LANE_INFO[laneType]
|
|
773
|
+
const lanes = info.lanes
|
|
774
|
+
const mask = -lanes
|
|
775
|
+
const boundExpr = boundLocal ? ['local.get', boundLocal] : bound
|
|
776
|
+
|
|
777
|
+
const initAcc = ['local.set', simdAccName, [info.splat, reduceEntry.constNode]]
|
|
778
|
+
const simdBlock = ['block', simdBrkLabel,
|
|
779
|
+
['loop', simdLoopLabel,
|
|
780
|
+
['br_if', simdBrkLabel,
|
|
781
|
+
['i32.eqz', ['i32.lt_s', ['local.get', incVar], ['local.get', simdBoundName]]]],
|
|
782
|
+
['local.set', simdAccName,
|
|
783
|
+
[reduceEntry.simd, ['local.get', simdAccName], liftedExpr]],
|
|
784
|
+
['local.set', incVar, ['i32.add', ['local.get', incVar], ['i32.const', lanes]]],
|
|
785
|
+
['br', simdLoopLabel]
|
|
786
|
+
]
|
|
787
|
+
]
|
|
788
|
+
|
|
789
|
+
// Horizontal fold: scalar.op(extract 0, extract 1, …, extract L-1).
|
|
790
|
+
let horiz = [reduceEntry.extract, 0, ['local.get', simdAccName]]
|
|
791
|
+
for (let k = 1; k < lanes; k++) {
|
|
792
|
+
horiz = [opName, horiz, [reduceEntry.extract, k, ['local.get', simdAccName]]]
|
|
793
|
+
}
|
|
794
|
+
const mergeBack = ['local.set', accName, [opName, ['local.get', accName], horiz]]
|
|
795
|
+
const boundSetup = ['local.set', simdBoundName, ['i32.and', boundExpr, ['i32.const', mask]]]
|
|
796
|
+
|
|
797
|
+
const wrapper = ['block', boundSetup, initAcc, simdBlock, mergeBack, blockNode]
|
|
798
|
+
const newLocalDecls = [
|
|
799
|
+
['local', simdBoundName, 'i32'],
|
|
800
|
+
['local', simdAccName, 'v128'],
|
|
801
|
+
]
|
|
802
|
+
return { wrapper, newLocalDecls }
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Scalar locals that are ALWAYS computed as `(i32.add base (i32.shl ind K))`
|
|
806
|
+
// or aliased to such an address are "address tees", not lane data. They stay
|
|
807
|
+
// scalar i32 in the lifted body.
|
|
808
|
+
function _isAddressLocal(body, name, ind) {
|
|
809
|
+
let onlyAsAddrTee = true
|
|
810
|
+
let foundTee = false
|
|
811
|
+
function walk(n) {
|
|
812
|
+
if (!isArr(n)) return
|
|
813
|
+
if (n[0] === 'local.tee' && n[1] === name) {
|
|
814
|
+
foundTee = true
|
|
815
|
+
// Check the value is a lane-address shape
|
|
816
|
+
const m = matchLaneAddr(['local.tee', name, n[2]], ind)
|
|
817
|
+
if (!m) onlyAsAddrTee = false
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
if (n[0] === 'local.set' && n[1] === name) {
|
|
821
|
+
// A set-not-tee: check value shape
|
|
822
|
+
const m = matchLaneAddr(['local.tee', name, n[2]], ind)
|
|
823
|
+
if (!m) onlyAsAddrTee = false
|
|
824
|
+
foundTee = true
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
828
|
+
}
|
|
829
|
+
for (const s of body) walk(s)
|
|
830
|
+
return foundTee && onlyAsAddrTee
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---- Lifter ----------------------------------------------------------------
|
|
834
|
+
|
|
835
|
+
function getOrAllocLanedLocal(name, ctx) {
|
|
836
|
+
let r = ctx.newLanedLocals.get(name)
|
|
837
|
+
if (!r) {
|
|
838
|
+
r = { laneName: `${name}__v`, origName: name }
|
|
839
|
+
ctx.newLanedLocals.set(name, r)
|
|
840
|
+
}
|
|
841
|
+
return r
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Lift a statement. Returns lifted stmt, or null to skip, or ['__seq__', ...] for multiple. */
|
|
845
|
+
function liftStmt(stmt, ctx) {
|
|
846
|
+
if (!isArr(stmt)) {
|
|
847
|
+
// Bare strings like "drop" — produced by stack-form WAT. We unwrap value-blocks
|
|
848
|
+
// separately so an isolated "drop" should not appear here, but tolerate it.
|
|
849
|
+
if (stmt === 'drop') return null
|
|
850
|
+
ctx.fail = true; return null
|
|
851
|
+
}
|
|
852
|
+
const op = stmt[0]
|
|
853
|
+
|
|
854
|
+
if (op === 'local.set' && typeof stmt[1] === 'string' && stmt.length === 3) {
|
|
855
|
+
const name = stmt[1]
|
|
856
|
+
const kind = ctx.localKind.get(name)
|
|
857
|
+
if (kind === 'addr') {
|
|
858
|
+
// Address-only local: lift the value as-is (it's i32 arithmetic on ind).
|
|
859
|
+
return ['local.set', name, stmt[2]]
|
|
860
|
+
}
|
|
861
|
+
if (kind === 'lane') {
|
|
862
|
+
const { laneName } = getOrAllocLanedLocal(name, ctx)
|
|
863
|
+
const v = liftExprV(stmt[2], ctx)
|
|
864
|
+
if (ctx.fail) return null
|
|
865
|
+
return ['local.set', laneName, v]
|
|
866
|
+
}
|
|
867
|
+
ctx.fail = true; return null
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (STORE_OPS[op]) {
|
|
871
|
+
const simdStore = 'v128.store'
|
|
872
|
+
const addr = stmt[1] // we leave addresses as-is (scalar i32 expressions)
|
|
873
|
+
const val = liftExprV(stmt[2], ctx)
|
|
874
|
+
if (ctx.fail) return null
|
|
875
|
+
// Handle memarg if present (last positional after addr/val): unlikely in
|
|
876
|
+
// pre-watr IR for this shape; bail if more than 3 children.
|
|
877
|
+
if (stmt.length !== 3) { ctx.fail = true; return null }
|
|
878
|
+
return [simdStore, addr, val]
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// (block (result T) STMTS... TAIL_EXPR) followed by sibling "drop" — we get
|
|
882
|
+
// the block alone here; the "drop" is a separate sibling and is returned as
|
|
883
|
+
// null by the next call. Strip the wrapper, lift the inner stmts; the
|
|
884
|
+
// dropped-tail expr is discarded.
|
|
885
|
+
if (op === 'block') {
|
|
886
|
+
// Block may be: ['block', LABEL?, RESULT?, ...stmts]
|
|
887
|
+
let i = 1
|
|
888
|
+
if (typeof stmt[i] === 'string' && stmt[i].startsWith('$')) i++
|
|
889
|
+
const hasResult = isArr(stmt[i]) && stmt[i][0] === 'result'
|
|
890
|
+
if (hasResult) i++
|
|
891
|
+
const inner = stmt.slice(i)
|
|
892
|
+
const stmts = hasResult ? inner.slice(0, inner.length - 1) : inner
|
|
893
|
+
const out = ['__seq__']
|
|
894
|
+
for (const s of stmts) {
|
|
895
|
+
const lifted = liftStmt(s, ctx)
|
|
896
|
+
if (ctx.fail) return null
|
|
897
|
+
if (lifted == null) continue
|
|
898
|
+
if (Array.isArray(lifted) && lifted[0] === '__seq__') out.push(...lifted.slice(1))
|
|
899
|
+
else out.push(lifted)
|
|
900
|
+
}
|
|
901
|
+
return out
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Standalone expression-as-statement (e.g. a load that gets dropped) — bail.
|
|
905
|
+
ctx.fail = true; return null
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Lift a value expression into v128 context. */
|
|
909
|
+
function liftExprV(expr, ctx) {
|
|
910
|
+
if (!isArr(expr)) { ctx.fail = true; return null }
|
|
911
|
+
const op = expr[0]
|
|
912
|
+
const info = LANE_INFO[ctx.laneType]
|
|
913
|
+
|
|
914
|
+
// Loads → v128.load (preserving address, including any local.tee).
|
|
915
|
+
if (LOAD_OPS[op]) {
|
|
916
|
+
if (LOAD_OPS[op] !== ctx.laneType) { ctx.fail = true; return null }
|
|
917
|
+
return ['v128.load', expr[1]]
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Constants → splat.
|
|
921
|
+
if (op === info.constOp) {
|
|
922
|
+
return [info.splat, expr]
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// local.get
|
|
926
|
+
if (op === 'local.get' && typeof expr[1] === 'string') {
|
|
927
|
+
const name = expr[1]
|
|
928
|
+
const kind = ctx.localKind.get(name)
|
|
929
|
+
if (kind === 'lane') {
|
|
930
|
+
const { laneName } = getOrAllocLanedLocal(name, ctx)
|
|
931
|
+
return ['local.get', laneName]
|
|
932
|
+
}
|
|
933
|
+
if (kind === 'invariant') {
|
|
934
|
+
return [info.splat, ['local.get', name]]
|
|
935
|
+
}
|
|
936
|
+
if (kind === 'addr' || name === ctx.incVar) {
|
|
937
|
+
ctx.fail = true; return null // can't be in a value position
|
|
938
|
+
}
|
|
939
|
+
ctx.fail = true; return null
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Lane-pure op?
|
|
943
|
+
const table = LANE_PURE[ctx.laneType]
|
|
944
|
+
const entry = table?.get(op)
|
|
945
|
+
if (entry) {
|
|
946
|
+
const a = liftExprV(expr[1], ctx)
|
|
947
|
+
if (ctx.fail) return null
|
|
948
|
+
if (entry.shamtScalar) {
|
|
949
|
+
// Second operand stays scalar i32 — must be const or invariant local.
|
|
950
|
+
const b = expr[2]
|
|
951
|
+
if (!isI32Const(b) && !(isArr(b) && b[0] === 'local.get' && ctx.localKind.get(b[1]) === 'invariant')) {
|
|
952
|
+
ctx.fail = true; return null
|
|
953
|
+
}
|
|
954
|
+
return [entry.simd, a, b]
|
|
955
|
+
}
|
|
956
|
+
if (expr.length === 2) { // unary (neg, abs, sqrt)
|
|
957
|
+
return [entry.simd, a]
|
|
958
|
+
}
|
|
959
|
+
const b = liftExprV(expr[2], ctx)
|
|
960
|
+
if (ctx.fail) return null
|
|
961
|
+
return [entry.simd, a, b]
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
ctx.fail = true; return null
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ---- Pass entry ------------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Walk a function looking for vectorizable (block (loop)) pairs, in-place.
|
|
971
|
+
* Adds new locals to the function header.
|
|
972
|
+
*/
|
|
973
|
+
export function vectorizeLaneLocal(fn) {
|
|
974
|
+
if (!isArr(fn) || fn[0] !== 'func') return
|
|
975
|
+
const bodyStart = findBodyStart(fn)
|
|
976
|
+
if (bodyStart < 0) return
|
|
977
|
+
|
|
978
|
+
// Build local-name → wasm-type map.
|
|
979
|
+
const fnLocals = new Map()
|
|
980
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
981
|
+
const d = fn[i]
|
|
982
|
+
if (isArr(d) && d[0] === 'local' && typeof d[1] === 'string' && typeof d[2] === 'string') {
|
|
983
|
+
fnLocals.set(d[1], d[2])
|
|
984
|
+
} else if (isArr(d) && d[0] === 'param' && typeof d[1] === 'string' && typeof d[2] === 'string') {
|
|
985
|
+
fnLocals.set(d[1], d[2])
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const freshIdRef = { next: 0 }
|
|
990
|
+
const newLocalDeclsAll = []
|
|
991
|
+
|
|
992
|
+
vectorizeStraightLineF64DotPairsIn(fn, fnLocals, freshIdRef, newLocalDeclsAll)
|
|
993
|
+
|
|
994
|
+
// Walk body recursively. Process inner-most matches first (post-order)
|
|
995
|
+
// so we don't try to vectorize an outer loop whose inner is the lane-local one.
|
|
996
|
+
function walk(parent, idx) {
|
|
997
|
+
const node = parent[idx]
|
|
998
|
+
if (!isArr(node)) return
|
|
999
|
+
for (let i = 0; i < node.length; i++) {
|
|
1000
|
+
if (isArr(node[i])) walk(node, i)
|
|
1001
|
+
}
|
|
1002
|
+
if (node[0] === 'block') {
|
|
1003
|
+
const r = tryVectorize(node, fnLocals, freshIdRef)
|
|
1004
|
+
?? tryReduceVectorize(node, fnLocals, freshIdRef)
|
|
1005
|
+
if (r) {
|
|
1006
|
+
parent[idx] = r.wrapper
|
|
1007
|
+
newLocalDeclsAll.push(...r.newLocalDecls)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
for (let i = bodyStart; i < fn.length; i++) walk(fn, i)
|
|
1012
|
+
|
|
1013
|
+
if (newLocalDeclsAll.length) {
|
|
1014
|
+
fn.splice(bodyStart, 0, ...newLocalDeclsAll)
|
|
1015
|
+
}
|
|
1016
|
+
}
|