watr 4.1.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/watr.js +29 -6
- package/dist/watr.js +829 -17
- package/dist/watr.min.js +5 -5
- package/package.json +1 -1
- package/readme.md +13 -38
- package/src/compile.js +20 -10
- package/src/optimize.js +1184 -0
- package/src/parse.js +2 -2
- package/src/util.js +4 -4
- package/types/src/compile.d.ts.map +1 -1
- package/types/src/optimize.d.ts +88 -0
- package/types/src/optimize.d.ts.map +1 -0
- package/types/src/util.d.ts +3 -3
- package/types/src/util.d.ts.map +1 -1
- package/types/watr.d.ts +6 -3
- package/types/watr.d.ts.map +1 -1
- package/watr.js +12 -6
package/src/optimize.js
ADDED
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST optimizations for WebAssembly modules.
|
|
3
|
+
* Reduces code size and improves runtime performance.
|
|
4
|
+
*
|
|
5
|
+
* @module watr/optimize
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import parse from './parse.js'
|
|
9
|
+
|
|
10
|
+
/** Optimizations that can be applied */
|
|
11
|
+
const OPTS = {
|
|
12
|
+
treeshake: true, // remove unused funcs/globals/types/tables
|
|
13
|
+
fold: true, // constant folding
|
|
14
|
+
deadcode: true, // eliminate dead code after unreachable/br/return
|
|
15
|
+
locals: true, // remove unused locals
|
|
16
|
+
identity: true, // remove identity ops (x + 0 → x)
|
|
17
|
+
strength: true, // strength reduction (x * 2 → x << 1)
|
|
18
|
+
branch: true, // simplify constant branches
|
|
19
|
+
propagate: true, // constant propagation through locals
|
|
20
|
+
inline: true, // inline tiny functions
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** All optimization names */
|
|
24
|
+
const ALL = Object.keys(OPTS)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize options to { opt: bool } map.
|
|
28
|
+
* @param {boolean|string|Object} opts
|
|
29
|
+
* @returns {Object}
|
|
30
|
+
*/
|
|
31
|
+
const normalize = (opts) => {
|
|
32
|
+
if (opts === true) return { ...OPTS }
|
|
33
|
+
if (opts === false) return {}
|
|
34
|
+
if (typeof opts === 'string') {
|
|
35
|
+
const set = new Set(opts.split(/\s+/).filter(Boolean))
|
|
36
|
+
// If single optimization name, enable just that one
|
|
37
|
+
if (set.size === 1 && ALL.includes([...set][0])) {
|
|
38
|
+
return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
|
|
39
|
+
}
|
|
40
|
+
return Object.fromEntries(ALL.map(f => [f, set.has(f) || set.has('all')]))
|
|
41
|
+
}
|
|
42
|
+
return { ...OPTS, ...opts }
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Deep clone AST.
|
|
46
|
+
* @param {any} node
|
|
47
|
+
* @returns {any}
|
|
48
|
+
*/
|
|
49
|
+
const clone = (node) => {
|
|
50
|
+
if (!Array.isArray(node)) return node
|
|
51
|
+
return node.map(clone)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Walk AST depth-first (pre-order).
|
|
56
|
+
* @param {any} node
|
|
57
|
+
* @param {Function} fn - (node, parent, idx) => void
|
|
58
|
+
* @param {any} [parent]
|
|
59
|
+
* @param {number} [idx]
|
|
60
|
+
*/
|
|
61
|
+
const walk = (node, fn, parent, idx) => {
|
|
62
|
+
fn(node, parent, idx)
|
|
63
|
+
if (Array.isArray(node)) for (let i = 0; i < node.length; i++) walk(node[i], fn, node, i)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Walk AST depth-first (post-order), transform children before parent.
|
|
68
|
+
* Returns the (potentially replaced) node.
|
|
69
|
+
* @param {any} node
|
|
70
|
+
* @param {Function} fn - (node, parent, idx) => newNode|undefined
|
|
71
|
+
* @param {any} [parent]
|
|
72
|
+
* @param {number} [idx]
|
|
73
|
+
* @returns {any}
|
|
74
|
+
*/
|
|
75
|
+
const walkPost = (node, fn, parent, idx) => {
|
|
76
|
+
if (Array.isArray(node)) {
|
|
77
|
+
for (let i = 0; i < node.length; i++) {
|
|
78
|
+
const result = walkPost(node[i], fn, node, i)
|
|
79
|
+
if (result !== undefined) node[i] = result
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const result = fn(node, parent, idx)
|
|
83
|
+
return result !== undefined ? result : node
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ==================== TREESHAKE ====================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove unused functions, globals, types, tables.
|
|
90
|
+
* Keeps exports and their transitive dependencies.
|
|
91
|
+
* @param {Array} ast
|
|
92
|
+
* @returns {Array}
|
|
93
|
+
*/
|
|
94
|
+
const treeshake = (ast) => {
|
|
95
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
96
|
+
|
|
97
|
+
// Collect all definitions
|
|
98
|
+
const funcs = new Map() // $name|idx → node
|
|
99
|
+
const globals = new Map()
|
|
100
|
+
const types = new Map()
|
|
101
|
+
const tables = new Map()
|
|
102
|
+
const memories = new Map()
|
|
103
|
+
const exports = []
|
|
104
|
+
const starts = []
|
|
105
|
+
|
|
106
|
+
let funcIdx = 0, globalIdx = 0, typeIdx = 0, tableIdx = 0, memIdx = 0, importFuncIdx = 0
|
|
107
|
+
|
|
108
|
+
for (const node of ast.slice(1)) {
|
|
109
|
+
if (!Array.isArray(node)) continue
|
|
110
|
+
const kind = node[0]
|
|
111
|
+
|
|
112
|
+
if (kind === 'type') {
|
|
113
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : typeIdx
|
|
114
|
+
types.set(name, { node, idx: typeIdx, used: false })
|
|
115
|
+
if (typeof name === 'string') types.set(typeIdx, types.get(name))
|
|
116
|
+
typeIdx++
|
|
117
|
+
}
|
|
118
|
+
else if (kind === 'func') {
|
|
119
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : funcIdx
|
|
120
|
+
// Check for inline export: (func $name (export "...") ...)
|
|
121
|
+
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
122
|
+
funcs.set(name, { node, idx: funcIdx, used: hasInlineExport })
|
|
123
|
+
if (typeof name === 'string') funcs.set(funcIdx, funcs.get(name))
|
|
124
|
+
funcIdx++
|
|
125
|
+
}
|
|
126
|
+
else if (kind === 'global') {
|
|
127
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : globalIdx
|
|
128
|
+
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
129
|
+
globals.set(name, { node, idx: globalIdx, used: hasInlineExport })
|
|
130
|
+
if (typeof name === 'string') globals.set(globalIdx, globals.get(name))
|
|
131
|
+
globalIdx++
|
|
132
|
+
}
|
|
133
|
+
else if (kind === 'table') {
|
|
134
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : tableIdx
|
|
135
|
+
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
136
|
+
tables.set(name, { node, idx: tableIdx, used: hasInlineExport })
|
|
137
|
+
if (typeof name === 'string') tables.set(tableIdx, tables.get(name))
|
|
138
|
+
tableIdx++
|
|
139
|
+
}
|
|
140
|
+
else if (kind === 'memory') {
|
|
141
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : memIdx
|
|
142
|
+
const hasInlineExport = node.some(sub => Array.isArray(sub) && sub[0] === 'export')
|
|
143
|
+
memories.set(name, { node, idx: memIdx, used: hasInlineExport })
|
|
144
|
+
if (typeof name === 'string') memories.set(memIdx, memories.get(name))
|
|
145
|
+
memIdx++
|
|
146
|
+
}
|
|
147
|
+
else if (kind === 'import') {
|
|
148
|
+
// Imports are always kept; mark as used
|
|
149
|
+
for (const sub of node) {
|
|
150
|
+
if (Array.isArray(sub) && sub[0] === 'func') {
|
|
151
|
+
const name = typeof sub[1] === 'string' && sub[1][0] === '$' ? sub[1] : importFuncIdx
|
|
152
|
+
funcs.set(name, { node, idx: importFuncIdx, used: true, isImport: true })
|
|
153
|
+
if (typeof name === 'string') funcs.set(importFuncIdx, funcs.get(name))
|
|
154
|
+
importFuncIdx++
|
|
155
|
+
funcIdx++
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (kind === 'export') {
|
|
160
|
+
exports.push(node)
|
|
161
|
+
}
|
|
162
|
+
else if (kind === 'start') {
|
|
163
|
+
starts.push(node)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Mark exports as used
|
|
168
|
+
for (const exp of exports) {
|
|
169
|
+
for (const sub of exp) {
|
|
170
|
+
if (!Array.isArray(sub)) continue
|
|
171
|
+
const [kind, ref] = sub
|
|
172
|
+
if (kind === 'func' && funcs.has(ref)) funcs.get(ref).used = true
|
|
173
|
+
else if (kind === 'global' && globals.has(ref)) globals.get(ref).used = true
|
|
174
|
+
else if (kind === 'table' && tables.has(ref)) tables.get(ref).used = true
|
|
175
|
+
else if (kind === 'memory' && memories.has(ref)) memories.get(ref).used = true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Mark start function as used
|
|
180
|
+
for (const start of starts) {
|
|
181
|
+
let ref = start[1]
|
|
182
|
+
// Convert numeric string refs to numbers
|
|
183
|
+
if (typeof ref === 'string' && ref[0] !== '$') ref = +ref
|
|
184
|
+
if (funcs.has(ref)) funcs.get(ref).used = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Count items with inline exports
|
|
188
|
+
let hasExports = exports.length > 0 || starts.length > 0
|
|
189
|
+
if (!hasExports) {
|
|
190
|
+
for (const [, entry] of funcs) if (entry.used) { hasExports = true; break }
|
|
191
|
+
if (!hasExports) for (const [, entry] of globals) if (entry.used) { hasExports = true; break }
|
|
192
|
+
if (!hasExports) for (const [, entry] of tables) if (entry.used) { hasExports = true; break }
|
|
193
|
+
if (!hasExports) for (const [, entry] of memories) if (entry.used) { hasExports = true; break }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If no exports/start at all, keep everything (module may be used differently)
|
|
197
|
+
if (!hasExports) {
|
|
198
|
+
for (const [, entry] of funcs) entry.used = true
|
|
199
|
+
for (const [, entry] of globals) entry.used = true
|
|
200
|
+
for (const [, entry] of tables) entry.used = true
|
|
201
|
+
for (const [, entry] of memories) entry.used = true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Mark elem-referenced functions as used
|
|
205
|
+
for (const node of ast.slice(1)) {
|
|
206
|
+
if (!Array.isArray(node) || node[0] !== 'elem') continue
|
|
207
|
+
walk(node, n => {
|
|
208
|
+
if (Array.isArray(n) && n[0] === 'ref.func') {
|
|
209
|
+
const ref = n[1]
|
|
210
|
+
if (funcs.has(ref)) funcs.get(ref).used = true
|
|
211
|
+
}
|
|
212
|
+
// Also plain func refs in elem
|
|
213
|
+
if (typeof n === 'string' && n[0] === '$' && funcs.has(n)) funcs.get(n).used = true
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Propagate: find dependencies of used functions
|
|
218
|
+
let changed = true
|
|
219
|
+
while (changed) {
|
|
220
|
+
changed = false
|
|
221
|
+
for (const [, entry] of funcs) {
|
|
222
|
+
if (!entry.used || entry.isImport) continue
|
|
223
|
+
walk(entry.node, n => {
|
|
224
|
+
if (!Array.isArray(n)) {
|
|
225
|
+
// Direct func reference
|
|
226
|
+
if (typeof n === 'string' && n[0] === '$' && funcs.has(n) && !funcs.get(n).used) {
|
|
227
|
+
funcs.get(n).used = true
|
|
228
|
+
changed = true
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const [op, ref] = n
|
|
233
|
+
if ((op === 'call' || op === 'return_call' || op === 'ref.func') && funcs.has(ref) && !funcs.get(ref).used) {
|
|
234
|
+
funcs.get(ref).used = true
|
|
235
|
+
changed = true
|
|
236
|
+
}
|
|
237
|
+
if ((op === 'global.get' || op === 'global.set') && globals.has(ref) && !globals.get(ref).used) {
|
|
238
|
+
globals.get(ref).used = true
|
|
239
|
+
changed = true
|
|
240
|
+
}
|
|
241
|
+
if (op === 'call_indirect' || op === 'return_call_indirect') {
|
|
242
|
+
// Tables used by call_indirect
|
|
243
|
+
for (const sub of n) {
|
|
244
|
+
if (typeof sub === 'string' && sub[0] === '$' && tables.has(sub) && !tables.get(sub).used) {
|
|
245
|
+
tables.get(sub).used = true
|
|
246
|
+
changed = true
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (op === 'type' && types.has(ref) && !types.get(ref).used) {
|
|
251
|
+
types.get(ref).used = true
|
|
252
|
+
changed = true
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Filter AST keeping only used items
|
|
259
|
+
const result = ['module']
|
|
260
|
+
for (const node of ast.slice(1)) {
|
|
261
|
+
if (!Array.isArray(node)) { result.push(node); continue }
|
|
262
|
+
const kind = node[0]
|
|
263
|
+
|
|
264
|
+
if (kind === 'func') {
|
|
265
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
266
|
+
const entry = name ? funcs.get(name) : [...funcs.values()].find(e => e.node === node)
|
|
267
|
+
if (entry?.used) result.push(node)
|
|
268
|
+
}
|
|
269
|
+
else if (kind === 'global') {
|
|
270
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
271
|
+
const entry = name ? globals.get(name) : [...globals.values()].find(e => e.node === node)
|
|
272
|
+
if (entry?.used) result.push(node)
|
|
273
|
+
}
|
|
274
|
+
else if (kind === 'type') {
|
|
275
|
+
// Keep all types for now (complex to treeshake due to inline types)
|
|
276
|
+
result.push(node)
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
result.push(node)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ==================== CONSTANT FOLDING ====================
|
|
287
|
+
|
|
288
|
+
/** Operators that can be constant-folded */
|
|
289
|
+
const FOLDABLE = {
|
|
290
|
+
// i32
|
|
291
|
+
'i32.add': (a, b) => (a + b) | 0,
|
|
292
|
+
'i32.sub': (a, b) => (a - b) | 0,
|
|
293
|
+
'i32.mul': (a, b) => Math.imul(a, b),
|
|
294
|
+
'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
|
|
295
|
+
'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
|
|
296
|
+
'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
|
|
297
|
+
'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
|
|
298
|
+
'i32.and': (a, b) => a & b,
|
|
299
|
+
'i32.or': (a, b) => a | b,
|
|
300
|
+
'i32.xor': (a, b) => a ^ b,
|
|
301
|
+
'i32.shl': (a, b) => a << (b & 31),
|
|
302
|
+
'i32.shr_s': (a, b) => a >> (b & 31),
|
|
303
|
+
'i32.shr_u': (a, b) => a >>> (b & 31),
|
|
304
|
+
'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
|
|
305
|
+
'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
|
|
306
|
+
'i32.eq': (a, b) => a === b ? 1 : 0,
|
|
307
|
+
'i32.ne': (a, b) => a !== b ? 1 : 0,
|
|
308
|
+
'i32.lt_s': (a, b) => a < b ? 1 : 0,
|
|
309
|
+
'i32.lt_u': (a, b) => (a >>> 0) < (b >>> 0) ? 1 : 0,
|
|
310
|
+
'i32.gt_s': (a, b) => a > b ? 1 : 0,
|
|
311
|
+
'i32.gt_u': (a, b) => (a >>> 0) > (b >>> 0) ? 1 : 0,
|
|
312
|
+
'i32.le_s': (a, b) => a <= b ? 1 : 0,
|
|
313
|
+
'i32.le_u': (a, b) => (a >>> 0) <= (b >>> 0) ? 1 : 0,
|
|
314
|
+
'i32.ge_s': (a, b) => a >= b ? 1 : 0,
|
|
315
|
+
'i32.ge_u': (a, b) => (a >>> 0) >= (b >>> 0) ? 1 : 0,
|
|
316
|
+
'i32.eqz': (a) => a === 0 ? 1 : 0,
|
|
317
|
+
'i32.clz': (a) => Math.clz32(a),
|
|
318
|
+
'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
|
|
319
|
+
'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
|
|
320
|
+
'i32.wrap_i64': (a) => Number(BigInt.asIntN(32, a)),
|
|
321
|
+
|
|
322
|
+
// i64 (using BigInt)
|
|
323
|
+
'i64.add': (a, b) => BigInt.asIntN(64, a + b),
|
|
324
|
+
'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
|
|
325
|
+
'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
|
|
326
|
+
'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
|
|
327
|
+
'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
|
|
328
|
+
'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
|
|
329
|
+
'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
|
|
330
|
+
'i64.and': (a, b) => BigInt.asIntN(64, a & b),
|
|
331
|
+
'i64.or': (a, b) => BigInt.asIntN(64, a | b),
|
|
332
|
+
'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
|
|
333
|
+
'i64.shl': (a, b) => BigInt.asIntN(64, a << (b & 63n)),
|
|
334
|
+
'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
|
|
335
|
+
'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
|
|
336
|
+
'i64.eq': (a, b) => a === b ? 1 : 0,
|
|
337
|
+
'i64.ne': (a, b) => a !== b ? 1 : 0,
|
|
338
|
+
'i64.lt_s': (a, b) => a < b ? 1 : 0,
|
|
339
|
+
'i64.lt_u': (a, b) => BigInt.asUintN(64, a) < BigInt.asUintN(64, b) ? 1 : 0,
|
|
340
|
+
'i64.gt_s': (a, b) => a > b ? 1 : 0,
|
|
341
|
+
'i64.gt_u': (a, b) => BigInt.asUintN(64, a) > BigInt.asUintN(64, b) ? 1 : 0,
|
|
342
|
+
'i64.le_s': (a, b) => a <= b ? 1 : 0,
|
|
343
|
+
'i64.le_u': (a, b) => BigInt.asUintN(64, a) <= BigInt.asUintN(64, b) ? 1 : 0,
|
|
344
|
+
'i64.ge_s': (a, b) => a >= b ? 1 : 0,
|
|
345
|
+
'i64.ge_u': (a, b) => BigInt.asUintN(64, a) >= BigInt.asUintN(64, b) ? 1 : 0,
|
|
346
|
+
'i64.eqz': (a) => a === 0n ? 1 : 0,
|
|
347
|
+
'i64.extend_i32_s': (a) => BigInt(a),
|
|
348
|
+
'i64.extend_i32_u': (a) => BigInt(a >>> 0),
|
|
349
|
+
|
|
350
|
+
// f32/f64 - be careful with NaN/precision
|
|
351
|
+
'f32.add': (a, b) => Math.fround(a + b),
|
|
352
|
+
'f32.sub': (a, b) => Math.fround(a - b),
|
|
353
|
+
'f32.mul': (a, b) => Math.fround(a * b),
|
|
354
|
+
'f32.div': (a, b) => Math.fround(a / b),
|
|
355
|
+
'f32.neg': (a) => Math.fround(-a),
|
|
356
|
+
'f32.abs': (a) => Math.fround(Math.abs(a)),
|
|
357
|
+
'f32.sqrt': (a) => Math.fround(Math.sqrt(a)),
|
|
358
|
+
'f32.ceil': (a) => Math.fround(Math.ceil(a)),
|
|
359
|
+
'f32.floor': (a) => Math.fround(Math.floor(a)),
|
|
360
|
+
'f32.trunc': (a) => Math.fround(Math.trunc(a)),
|
|
361
|
+
'f32.nearest': (a) => Math.fround(Math.round(a)),
|
|
362
|
+
|
|
363
|
+
'f64.add': (a, b) => a + b,
|
|
364
|
+
'f64.sub': (a, b) => a - b,
|
|
365
|
+
'f64.mul': (a, b) => a * b,
|
|
366
|
+
'f64.div': (a, b) => a / b,
|
|
367
|
+
'f64.neg': (a) => -a,
|
|
368
|
+
'f64.abs': (a) => Math.abs(a),
|
|
369
|
+
'f64.sqrt': (a) => Math.sqrt(a),
|
|
370
|
+
'f64.ceil': (a) => Math.ceil(a),
|
|
371
|
+
'f64.floor': (a) => Math.floor(a),
|
|
372
|
+
'f64.trunc': (a) => Math.trunc(a),
|
|
373
|
+
'f64.nearest': (a) => Math.round(a),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Extract constant value from node.
|
|
378
|
+
* @param {any} node
|
|
379
|
+
* @returns {{type: string, value: number|bigint}|null}
|
|
380
|
+
*/
|
|
381
|
+
const getConst = (node) => {
|
|
382
|
+
if (!Array.isArray(node) || node.length !== 2) return null
|
|
383
|
+
const [op, val] = node
|
|
384
|
+
if (op === 'i32.const') return { type: 'i32', value: Number(val) | 0 }
|
|
385
|
+
if (op === 'i64.const') return { type: 'i64', value: BigInt(val) }
|
|
386
|
+
if (op === 'f32.const') return { type: 'f32', value: Math.fround(Number(val)) }
|
|
387
|
+
if (op === 'f64.const') return { type: 'f64', value: Number(val) }
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create const node from value.
|
|
393
|
+
* @param {string} type
|
|
394
|
+
* @param {number|bigint} value
|
|
395
|
+
* @returns {Array}
|
|
396
|
+
*/
|
|
397
|
+
const makeConst = (type, value) => {
|
|
398
|
+
if (type === 'i32') return ['i32.const', value | 0]
|
|
399
|
+
if (type === 'i64') return ['i64.const', value]
|
|
400
|
+
if (type === 'f32') return ['f32.const', Math.fround(value)]
|
|
401
|
+
if (type === 'f64') return ['f64.const', value]
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Fold constant expressions.
|
|
407
|
+
* @param {Array} ast
|
|
408
|
+
* @returns {Array}
|
|
409
|
+
*/
|
|
410
|
+
const fold = (ast) => {
|
|
411
|
+
return walkPost(clone(ast), (node) => {
|
|
412
|
+
if (!Array.isArray(node)) return
|
|
413
|
+
const op = node[0]
|
|
414
|
+
const fn = FOLDABLE[op]
|
|
415
|
+
if (!fn) return
|
|
416
|
+
|
|
417
|
+
// Unary ops
|
|
418
|
+
if (fn.length === 1 && node.length === 2) {
|
|
419
|
+
const a = getConst(node[1])
|
|
420
|
+
if (!a) return
|
|
421
|
+
const result = fn(a.value)
|
|
422
|
+
if (result === null) return
|
|
423
|
+
const resultType = op.startsWith('i64.') && !op.includes('eqz') ? 'i64' :
|
|
424
|
+
op.startsWith('f32.') ? 'f32' :
|
|
425
|
+
op.startsWith('f64.') ? 'f64' : 'i32'
|
|
426
|
+
return makeConst(resultType, result)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Binary ops
|
|
430
|
+
if (fn.length === 2 && node.length === 3) {
|
|
431
|
+
const a = getConst(node[1])
|
|
432
|
+
const b = getConst(node[2])
|
|
433
|
+
if (!a || !b) return
|
|
434
|
+
const result = fn(a.value, b.value)
|
|
435
|
+
if (result === null) return
|
|
436
|
+
// Comparisons return i32
|
|
437
|
+
const isCompare = /\.(eq|ne|[lg][te])/.test(op)
|
|
438
|
+
const resultType = isCompare ? 'i32' :
|
|
439
|
+
op.startsWith('i64.') ? 'i64' :
|
|
440
|
+
op.startsWith('f32.') ? 'f32' :
|
|
441
|
+
op.startsWith('f64.') ? 'f64' : 'i32'
|
|
442
|
+
return makeConst(resultType, result)
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ==================== IDENTITY REMOVAL ====================
|
|
448
|
+
|
|
449
|
+
/** Identity operations that can be simplified */
|
|
450
|
+
const IDENTITIES = {
|
|
451
|
+
// x + 0 → x, 0 + x → x
|
|
452
|
+
'i32.add': (a, b) => {
|
|
453
|
+
const ca = getConst(a), cb = getConst(b)
|
|
454
|
+
if (ca?.value === 0) return b
|
|
455
|
+
if (cb?.value === 0) return a
|
|
456
|
+
return null
|
|
457
|
+
},
|
|
458
|
+
'i64.add': (a, b) => {
|
|
459
|
+
const ca = getConst(a), cb = getConst(b)
|
|
460
|
+
if (ca?.value === 0n) return b
|
|
461
|
+
if (cb?.value === 0n) return a
|
|
462
|
+
return null
|
|
463
|
+
},
|
|
464
|
+
// x - 0 → x
|
|
465
|
+
'i32.sub': (a, b) => getConst(b)?.value === 0 ? a : null,
|
|
466
|
+
'i64.sub': (a, b) => getConst(b)?.value === 0n ? a : null,
|
|
467
|
+
// x * 1 → x, 1 * x → x
|
|
468
|
+
'i32.mul': (a, b) => {
|
|
469
|
+
const ca = getConst(a), cb = getConst(b)
|
|
470
|
+
if (ca?.value === 1) return b
|
|
471
|
+
if (cb?.value === 1) return a
|
|
472
|
+
return null
|
|
473
|
+
},
|
|
474
|
+
'i64.mul': (a, b) => {
|
|
475
|
+
const ca = getConst(a), cb = getConst(b)
|
|
476
|
+
if (ca?.value === 1n) return b
|
|
477
|
+
if (cb?.value === 1n) return a
|
|
478
|
+
return null
|
|
479
|
+
},
|
|
480
|
+
// x / 1 → x
|
|
481
|
+
'i32.div_s': (a, b) => getConst(b)?.value === 1 ? a : null,
|
|
482
|
+
'i32.div_u': (a, b) => getConst(b)?.value === 1 ? a : null,
|
|
483
|
+
'i64.div_s': (a, b) => getConst(b)?.value === 1n ? a : null,
|
|
484
|
+
'i64.div_u': (a, b) => getConst(b)?.value === 1n ? a : null,
|
|
485
|
+
// x & -1 → x, -1 & x → x (all bits set)
|
|
486
|
+
'i32.and': (a, b) => {
|
|
487
|
+
const ca = getConst(a), cb = getConst(b)
|
|
488
|
+
if (ca?.value === -1) return b
|
|
489
|
+
if (cb?.value === -1) return a
|
|
490
|
+
return null
|
|
491
|
+
},
|
|
492
|
+
'i64.and': (a, b) => {
|
|
493
|
+
const ca = getConst(a), cb = getConst(b)
|
|
494
|
+
if (ca?.value === -1n) return b
|
|
495
|
+
if (cb?.value === -1n) return a
|
|
496
|
+
return null
|
|
497
|
+
},
|
|
498
|
+
// x | 0 → x, 0 | x → x
|
|
499
|
+
'i32.or': (a, b) => {
|
|
500
|
+
const ca = getConst(a), cb = getConst(b)
|
|
501
|
+
if (ca?.value === 0) return b
|
|
502
|
+
if (cb?.value === 0) return a
|
|
503
|
+
return null
|
|
504
|
+
},
|
|
505
|
+
'i64.or': (a, b) => {
|
|
506
|
+
const ca = getConst(a), cb = getConst(b)
|
|
507
|
+
if (ca?.value === 0n) return b
|
|
508
|
+
if (cb?.value === 0n) return a
|
|
509
|
+
return null
|
|
510
|
+
},
|
|
511
|
+
// x ^ 0 → x, 0 ^ x → x
|
|
512
|
+
'i32.xor': (a, b) => {
|
|
513
|
+
const ca = getConst(a), cb = getConst(b)
|
|
514
|
+
if (ca?.value === 0) return b
|
|
515
|
+
if (cb?.value === 0) return a
|
|
516
|
+
return null
|
|
517
|
+
},
|
|
518
|
+
'i64.xor': (a, b) => {
|
|
519
|
+
const ca = getConst(a), cb = getConst(b)
|
|
520
|
+
if (ca?.value === 0n) return b
|
|
521
|
+
if (cb?.value === 0n) return a
|
|
522
|
+
return null
|
|
523
|
+
},
|
|
524
|
+
// x << 0 → x, x >> 0 → x
|
|
525
|
+
'i32.shl': (a, b) => getConst(b)?.value === 0 ? a : null,
|
|
526
|
+
'i32.shr_s': (a, b) => getConst(b)?.value === 0 ? a : null,
|
|
527
|
+
'i32.shr_u': (a, b) => getConst(b)?.value === 0 ? a : null,
|
|
528
|
+
'i64.shl': (a, b) => getConst(b)?.value === 0n ? a : null,
|
|
529
|
+
'i64.shr_s': (a, b) => getConst(b)?.value === 0n ? a : null,
|
|
530
|
+
'i64.shr_u': (a, b) => getConst(b)?.value === 0n ? a : null,
|
|
531
|
+
// f + 0 → x (careful with -0.0, skip for floats)
|
|
532
|
+
// f * 1 → x (careful with NaN, skip for floats)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Remove identity operations.
|
|
537
|
+
* @param {Array} ast
|
|
538
|
+
* @returns {Array}
|
|
539
|
+
*/
|
|
540
|
+
const identity = (ast) => {
|
|
541
|
+
return walkPost(clone(ast), (node) => {
|
|
542
|
+
if (!Array.isArray(node) || node.length !== 3) return
|
|
543
|
+
const fn = IDENTITIES[node[0]]
|
|
544
|
+
if (!fn) return
|
|
545
|
+
const result = fn(node[1], node[2])
|
|
546
|
+
if (result === null) return // no optimization, keep original
|
|
547
|
+
return result
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ==================== STRENGTH REDUCTION ====================
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Strength reduction: replace expensive ops with cheaper equivalents.
|
|
555
|
+
* @param {Array} ast
|
|
556
|
+
* @returns {Array}
|
|
557
|
+
*/
|
|
558
|
+
const strength = (ast) => {
|
|
559
|
+
return walkPost(clone(ast), (node) => {
|
|
560
|
+
if (!Array.isArray(node) || node.length !== 3) return
|
|
561
|
+
const [op, a, b] = node
|
|
562
|
+
|
|
563
|
+
// x * 2^n → x << n
|
|
564
|
+
if (op === 'i32.mul') {
|
|
565
|
+
const cb = getConst(b)
|
|
566
|
+
if (cb && cb.value > 0 && (cb.value & (cb.value - 1)) === 0) {
|
|
567
|
+
const shift = Math.log2(cb.value)
|
|
568
|
+
if (Number.isInteger(shift)) return ['i32.shl', a, ['i32.const', shift]]
|
|
569
|
+
}
|
|
570
|
+
const ca = getConst(a)
|
|
571
|
+
if (ca && ca.value > 0 && (ca.value & (ca.value - 1)) === 0) {
|
|
572
|
+
const shift = Math.log2(ca.value)
|
|
573
|
+
if (Number.isInteger(shift)) return ['i32.shl', b, ['i32.const', shift]]
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (op === 'i64.mul') {
|
|
577
|
+
const cb = getConst(b)
|
|
578
|
+
if (cb && cb.value > 0n && (cb.value & (cb.value - 1n)) === 0n) {
|
|
579
|
+
const shift = BigInt(cb.value.toString(2).length - 1)
|
|
580
|
+
return ['i64.shl', a, ['i64.const', shift]]
|
|
581
|
+
}
|
|
582
|
+
const ca = getConst(a)
|
|
583
|
+
if (ca && ca.value > 0n && (ca.value & (ca.value - 1n)) === 0n) {
|
|
584
|
+
const shift = BigInt(ca.value.toString(2).length - 1)
|
|
585
|
+
return ['i64.shl', b, ['i64.const', shift]]
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// x / 2^n → x >> n (unsigned only, signed division is more complex)
|
|
590
|
+
if (op === 'i32.div_u') {
|
|
591
|
+
const cb = getConst(b)
|
|
592
|
+
if (cb && cb.value > 0 && (cb.value & (cb.value - 1)) === 0) {
|
|
593
|
+
const shift = Math.log2(cb.value)
|
|
594
|
+
if (Number.isInteger(shift)) return ['i32.shr_u', a, ['i32.const', shift]]
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (op === 'i64.div_u') {
|
|
598
|
+
const cb = getConst(b)
|
|
599
|
+
if (cb && cb.value > 0n && (cb.value & (cb.value - 1n)) === 0n) {
|
|
600
|
+
const shift = BigInt(cb.value.toString(2).length - 1)
|
|
601
|
+
return ['i64.shr_u', a, ['i64.const', shift]]
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// x % 2^n → x & (2^n - 1) (unsigned only)
|
|
606
|
+
if (op === 'i32.rem_u') {
|
|
607
|
+
const cb = getConst(b)
|
|
608
|
+
if (cb && cb.value > 0 && (cb.value & (cb.value - 1)) === 0) {
|
|
609
|
+
return ['i32.and', a, ['i32.const', cb.value - 1]]
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (op === 'i64.rem_u') {
|
|
613
|
+
const cb = getConst(b)
|
|
614
|
+
if (cb && cb.value > 0n && (cb.value & (cb.value - 1n)) === 0n) {
|
|
615
|
+
return ['i64.and', a, ['i64.const', cb.value - 1n]]
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ==================== BRANCH SIMPLIFICATION ====================
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Simplify branches with constant conditions.
|
|
625
|
+
* @param {Array} ast
|
|
626
|
+
* @returns {Array}
|
|
627
|
+
*/
|
|
628
|
+
const branch = (ast) => {
|
|
629
|
+
return walkPost(clone(ast), (node) => {
|
|
630
|
+
if (!Array.isArray(node)) return
|
|
631
|
+
const op = node[0]
|
|
632
|
+
|
|
633
|
+
// (if (i32.const 0) then else) → else
|
|
634
|
+
// (if (i32.const N) then else) → then (N != 0)
|
|
635
|
+
if (op === 'if') {
|
|
636
|
+
// Find condition - first non-annotation child that's an expression
|
|
637
|
+
let condIdx = 1
|
|
638
|
+
while (condIdx < node.length) {
|
|
639
|
+
const child = node[condIdx]
|
|
640
|
+
if (Array.isArray(child) && (child[0] === 'then' || child[0] === 'else' || child[0] === 'result' || child[0] === 'param')) {
|
|
641
|
+
condIdx++
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
644
|
+
break
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const cond = node[condIdx]
|
|
648
|
+
const c = getConst(cond)
|
|
649
|
+
if (!c) return
|
|
650
|
+
|
|
651
|
+
// Find then/else branches
|
|
652
|
+
let thenBranch = null, elseBranch = null
|
|
653
|
+
for (let i = condIdx + 1; i < node.length; i++) {
|
|
654
|
+
const child = node[i]
|
|
655
|
+
if (Array.isArray(child)) {
|
|
656
|
+
if (child[0] === 'then') thenBranch = child
|
|
657
|
+
else if (child[0] === 'else') elseBranch = child
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Condition is truthy → replace with then contents
|
|
662
|
+
if (c.value !== 0 && c.value !== 0n) {
|
|
663
|
+
if (thenBranch && thenBranch.length > 1) {
|
|
664
|
+
// Return block with then contents (or just contents if single)
|
|
665
|
+
const contents = thenBranch.slice(1)
|
|
666
|
+
if (contents.length === 1) return contents[0]
|
|
667
|
+
return ['block', ...contents]
|
|
668
|
+
}
|
|
669
|
+
return ['nop']
|
|
670
|
+
}
|
|
671
|
+
// Condition is falsy → replace with else contents
|
|
672
|
+
else {
|
|
673
|
+
if (elseBranch && elseBranch.length > 1) {
|
|
674
|
+
const contents = elseBranch.slice(1)
|
|
675
|
+
if (contents.length === 1) return contents[0]
|
|
676
|
+
return ['block', ...contents]
|
|
677
|
+
}
|
|
678
|
+
return ['nop']
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// (br_if $label (i32.const 0)) → nop
|
|
683
|
+
// (br_if $label (i32.const N)) → br $label (N != 0)
|
|
684
|
+
if (op === 'br_if' && node.length >= 3) {
|
|
685
|
+
const cond = node[node.length - 1]
|
|
686
|
+
const c = getConst(cond)
|
|
687
|
+
if (!c) return
|
|
688
|
+
if (c.value === 0 || c.value === 0n) return ['nop']
|
|
689
|
+
return ['br', node[1]]
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// (select a b (i32.const 0)) → b
|
|
693
|
+
// (select a b (i32.const N)) → a (N != 0)
|
|
694
|
+
if (op === 'select' && node.length >= 4) {
|
|
695
|
+
const cond = node[node.length - 1]
|
|
696
|
+
const c = getConst(cond)
|
|
697
|
+
if (!c) return
|
|
698
|
+
if (c.value === 0 || c.value === 0n) return node[2] // b
|
|
699
|
+
return node[1] // a
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ==================== DEAD CODE ELIMINATION ====================
|
|
705
|
+
|
|
706
|
+
/** Control flow terminators */
|
|
707
|
+
const TERMINATORS = new Set(['unreachable', 'return', 'br', 'br_table'])
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Remove dead code after control flow terminators.
|
|
711
|
+
* @param {Array} ast
|
|
712
|
+
* @returns {Array}
|
|
713
|
+
*/
|
|
714
|
+
const deadcode = (ast) => {
|
|
715
|
+
const result = clone(ast)
|
|
716
|
+
|
|
717
|
+
// Process each function body
|
|
718
|
+
walk(result, (node) => {
|
|
719
|
+
if (!Array.isArray(node)) return
|
|
720
|
+
const kind = node[0]
|
|
721
|
+
|
|
722
|
+
// Process blocks: func, block, loop, if branches
|
|
723
|
+
if (kind === 'func' || kind === 'block' || kind === 'loop') {
|
|
724
|
+
eliminateDeadInBlock(node)
|
|
725
|
+
}
|
|
726
|
+
if (kind === 'if') {
|
|
727
|
+
// Process then/else branches
|
|
728
|
+
for (let i = 1; i < node.length; i++) {
|
|
729
|
+
if (Array.isArray(node[i]) && (node[i][0] === 'then' || node[i][0] === 'else')) {
|
|
730
|
+
eliminateDeadInBlock(node[i])
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
return result
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Remove instructions after terminators within a block.
|
|
741
|
+
* @param {Array} block
|
|
742
|
+
*/
|
|
743
|
+
const eliminateDeadInBlock = (block) => {
|
|
744
|
+
let terminated = false
|
|
745
|
+
let firstTerminator = -1
|
|
746
|
+
|
|
747
|
+
for (let i = 1; i < block.length; i++) {
|
|
748
|
+
const node = block[i]
|
|
749
|
+
|
|
750
|
+
// Skip type annotations
|
|
751
|
+
if (Array.isArray(node)) {
|
|
752
|
+
const op = node[0]
|
|
753
|
+
if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
|
|
754
|
+
|
|
755
|
+
if (terminated) {
|
|
756
|
+
if (firstTerminator === -1) firstTerminator = i
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (TERMINATORS.has(op)) {
|
|
760
|
+
terminated = true
|
|
761
|
+
firstTerminator = i + 1
|
|
762
|
+
}
|
|
763
|
+
} else if (typeof node === 'string') {
|
|
764
|
+
// String instructions like 'unreachable', 'return', 'drop', 'nop'
|
|
765
|
+
if (terminated) {
|
|
766
|
+
if (firstTerminator === -1) firstTerminator = i
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (TERMINATORS.has(node)) {
|
|
770
|
+
terminated = true
|
|
771
|
+
firstTerminator = i + 1
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Remove dead code
|
|
777
|
+
if (firstTerminator > 0 && firstTerminator < block.length) {
|
|
778
|
+
block.splice(firstTerminator)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ==================== LOCAL REUSE ====================
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Reuse locals of the same type to reduce total local count.
|
|
786
|
+
* Basic version: deduplicate unused locals.
|
|
787
|
+
* @param {Array} ast
|
|
788
|
+
* @returns {Array}
|
|
789
|
+
*/
|
|
790
|
+
const localReuse = (ast) => {
|
|
791
|
+
const result = clone(ast)
|
|
792
|
+
|
|
793
|
+
walk(result, (node) => {
|
|
794
|
+
if (!Array.isArray(node) || node[0] !== 'func') return
|
|
795
|
+
|
|
796
|
+
// Collect local declarations and their types
|
|
797
|
+
const localDecls = []
|
|
798
|
+
const localTypes = new Map() // $name → type
|
|
799
|
+
const usedLocals = new Set()
|
|
800
|
+
|
|
801
|
+
// Find all local declarations and usages
|
|
802
|
+
for (let i = 1; i < node.length; i++) {
|
|
803
|
+
const sub = node[i]
|
|
804
|
+
if (!Array.isArray(sub)) continue
|
|
805
|
+
|
|
806
|
+
if (sub[0] === 'local') {
|
|
807
|
+
localDecls.push({ idx: i, node: sub })
|
|
808
|
+
// (local $name type) or (local type)
|
|
809
|
+
if (typeof sub[1] === 'string' && sub[1][0] === '$') {
|
|
810
|
+
localTypes.set(sub[1], sub[2])
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (sub[0] === 'param') {
|
|
814
|
+
// Params are also locals
|
|
815
|
+
if (typeof sub[1] === 'string' && sub[1][0] === '$') {
|
|
816
|
+
localTypes.set(sub[1], sub[2])
|
|
817
|
+
usedLocals.add(sub[1]) // params always used
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Find which locals are actually used
|
|
823
|
+
walk(node, (n) => {
|
|
824
|
+
if (!Array.isArray(n)) return
|
|
825
|
+
const op = n[0]
|
|
826
|
+
if (op === 'local.get' || op === 'local.set' || op === 'local.tee') {
|
|
827
|
+
const ref = n[1]
|
|
828
|
+
if (typeof ref === 'string') usedLocals.add(ref)
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
// Remove unused local declarations
|
|
833
|
+
for (let i = localDecls.length - 1; i >= 0; i--) {
|
|
834
|
+
const { idx, node: decl } = localDecls[i]
|
|
835
|
+
const name = typeof decl[1] === 'string' && decl[1][0] === '$' ? decl[1] : null
|
|
836
|
+
if (name && !usedLocals.has(name)) {
|
|
837
|
+
node.splice(idx, 1)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
return result
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ==================== CONSTANT PROPAGATION ====================
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Propagate constant values through local variables.
|
|
849
|
+
* When a local is set to a constant and not modified before use, replace the get with the constant.
|
|
850
|
+
* @param {Array} ast
|
|
851
|
+
* @returns {Array}
|
|
852
|
+
*/
|
|
853
|
+
const propagate = (ast) => {
|
|
854
|
+
const result = clone(ast)
|
|
855
|
+
|
|
856
|
+
walk(result, (node) => {
|
|
857
|
+
if (!Array.isArray(node) || node[0] !== 'func') return
|
|
858
|
+
|
|
859
|
+
// Track which locals have known constant values
|
|
860
|
+
// This is a simple single-pass analysis within straight-line code
|
|
861
|
+
const constLocals = new Map() // $name → const node
|
|
862
|
+
|
|
863
|
+
// Process function body in order
|
|
864
|
+
const processBlock = (block, startIdx = 1) => {
|
|
865
|
+
for (let i = startIdx; i < block.length; i++) {
|
|
866
|
+
const instr = block[i]
|
|
867
|
+
if (!Array.isArray(instr)) continue
|
|
868
|
+
|
|
869
|
+
const op = instr[0]
|
|
870
|
+
|
|
871
|
+
// local.set $x (const) → remember constant
|
|
872
|
+
if (op === 'local.set' && instr.length === 3) {
|
|
873
|
+
const local = instr[1]
|
|
874
|
+
const val = instr[2]
|
|
875
|
+
const c = getConst(val)
|
|
876
|
+
if (c && typeof local === 'string') {
|
|
877
|
+
constLocals.set(local, val)
|
|
878
|
+
} else if (typeof local === 'string') {
|
|
879
|
+
constLocals.delete(local) // invalidate if set to non-const
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// local.tee also sets
|
|
883
|
+
else if (op === 'local.tee' && instr.length === 3) {
|
|
884
|
+
const local = instr[1]
|
|
885
|
+
const val = instr[2]
|
|
886
|
+
const c = getConst(val)
|
|
887
|
+
if (c && typeof local === 'string') {
|
|
888
|
+
constLocals.set(local, val)
|
|
889
|
+
} else if (typeof local === 'string') {
|
|
890
|
+
constLocals.delete(local)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// local.get $x → replace with const if known
|
|
894
|
+
else if (op === 'local.get' && instr.length === 2) {
|
|
895
|
+
const local = instr[1]
|
|
896
|
+
if (typeof local === 'string' && constLocals.has(local)) {
|
|
897
|
+
const constVal = constLocals.get(local)
|
|
898
|
+
// Replace in place
|
|
899
|
+
instr.length = 0
|
|
900
|
+
instr.push(...clone(constVal))
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Control flow invalidates all knowledge (conservative)
|
|
904
|
+
else if (op === 'block' || op === 'loop' || op === 'if' || op === 'call' || op === 'call_indirect') {
|
|
905
|
+
constLocals.clear()
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Recursively process nested expressions that might have local.get
|
|
909
|
+
walkPost(instr, (n) => {
|
|
910
|
+
if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
|
|
911
|
+
const local = n[1]
|
|
912
|
+
if (typeof local === 'string' && constLocals.has(local)) {
|
|
913
|
+
const constVal = constLocals.get(local)
|
|
914
|
+
return clone(constVal)
|
|
915
|
+
}
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
processBlock(node)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
return result
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ==================== FUNCTION INLINING ====================
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Inline tiny functions (single expression, no locals, no params or simple params).
|
|
930
|
+
* @param {Array} ast
|
|
931
|
+
* @returns {Array}
|
|
932
|
+
*/
|
|
933
|
+
const inline = (ast) => {
|
|
934
|
+
if (!Array.isArray(ast) || ast[0] !== 'module') return ast
|
|
935
|
+
const result = clone(ast)
|
|
936
|
+
|
|
937
|
+
// Collect inlinable functions
|
|
938
|
+
const inlinable = new Map() // $name → { body, params }
|
|
939
|
+
|
|
940
|
+
for (const node of result.slice(1)) {
|
|
941
|
+
if (!Array.isArray(node) || node[0] !== 'func') continue
|
|
942
|
+
|
|
943
|
+
const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
|
|
944
|
+
if (!name) continue
|
|
945
|
+
|
|
946
|
+
// Check if function is small enough to inline
|
|
947
|
+
let params = []
|
|
948
|
+
let body = []
|
|
949
|
+
let hasLocals = false
|
|
950
|
+
let hasExport = false
|
|
951
|
+
|
|
952
|
+
for (let i = 1; i < node.length; i++) {
|
|
953
|
+
const sub = node[i]
|
|
954
|
+
if (!Array.isArray(sub)) continue
|
|
955
|
+
if (sub[0] === 'param') {
|
|
956
|
+
// Collect param names and types
|
|
957
|
+
if (typeof sub[1] === 'string' && sub[1][0] === '$') {
|
|
958
|
+
params.push({ name: sub[1], type: sub[2] })
|
|
959
|
+
} else {
|
|
960
|
+
// Unnamed params - harder to inline
|
|
961
|
+
params = null
|
|
962
|
+
break
|
|
963
|
+
}
|
|
964
|
+
} else if (sub[0] === 'local') {
|
|
965
|
+
hasLocals = true
|
|
966
|
+
} else if (sub[0] === 'export') {
|
|
967
|
+
hasExport = true
|
|
968
|
+
} else if (sub[0] !== 'result' && sub[0] !== 'type') {
|
|
969
|
+
body.push(sub)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Only inline: no locals, <= 2 params, single expression body, not exported
|
|
974
|
+
if (params && !hasLocals && !hasExport && params.length <= 2 && body.length === 1) {
|
|
975
|
+
// Check if function mutates any of its params (local.set/tee on param)
|
|
976
|
+
const paramNames = new Set(params.map(p => p.name))
|
|
977
|
+
let mutatesParam = false
|
|
978
|
+
walk(body[0], (n) => {
|
|
979
|
+
if (!Array.isArray(n)) return
|
|
980
|
+
if ((n[0] === 'local.set' || n[0] === 'local.tee') && paramNames.has(n[1])) {
|
|
981
|
+
mutatesParam = true
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
if (!mutatesParam) {
|
|
985
|
+
inlinable.set(name, { body: body[0], params })
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Replace calls with inlined body
|
|
991
|
+
if (inlinable.size === 0) return result
|
|
992
|
+
|
|
993
|
+
walkPost(result, (node) => {
|
|
994
|
+
if (!Array.isArray(node) || node[0] !== 'call') return
|
|
995
|
+
const fname = node[1]
|
|
996
|
+
if (!inlinable.has(fname)) return
|
|
997
|
+
|
|
998
|
+
const { body, params } = inlinable.get(fname)
|
|
999
|
+
const args = node.slice(2)
|
|
1000
|
+
|
|
1001
|
+
// Simple case: no params
|
|
1002
|
+
if (params.length === 0) {
|
|
1003
|
+
return clone(body)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Substitute params with args
|
|
1007
|
+
const substituted = clone(body)
|
|
1008
|
+
walkPost(substituted, (n) => {
|
|
1009
|
+
if (!Array.isArray(n) || n[0] !== 'local.get') return
|
|
1010
|
+
const local = n[1]
|
|
1011
|
+
const paramIdx = params.findIndex(p => p.name === local)
|
|
1012
|
+
if (paramIdx !== -1 && args[paramIdx]) {
|
|
1013
|
+
return clone(args[paramIdx])
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
return substituted
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
return result
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ==================== COMMON SUBEXPRESSION ELIMINATION ====================
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Hash an expression for comparison.
|
|
1027
|
+
* @param {any} node
|
|
1028
|
+
* @returns {string}
|
|
1029
|
+
*/
|
|
1030
|
+
const exprHash = (node) => JSON.stringify(node)
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Eliminate common subexpressions by caching repeated computations.
|
|
1034
|
+
* Limited to pure expressions within a function.
|
|
1035
|
+
* @param {Array} ast
|
|
1036
|
+
* @returns {Array}
|
|
1037
|
+
*/
|
|
1038
|
+
const cse = (ast) => {
|
|
1039
|
+
// CSE is complex and can increase code size (extra locals)
|
|
1040
|
+
// Simple version: detect and report, but actual elimination needs careful analysis
|
|
1041
|
+
// For now, implement a basic version that works on adjacent identical expressions
|
|
1042
|
+
|
|
1043
|
+
const result = clone(ast)
|
|
1044
|
+
|
|
1045
|
+
walk(result, (node) => {
|
|
1046
|
+
if (!Array.isArray(node) || node[0] !== 'func') return
|
|
1047
|
+
|
|
1048
|
+
// Find sequences of identical pure expressions
|
|
1049
|
+
const seen = new Map() // hash → { node, count }
|
|
1050
|
+
|
|
1051
|
+
walk(node, (n) => {
|
|
1052
|
+
if (!Array.isArray(n)) return
|
|
1053
|
+
const op = n[0]
|
|
1054
|
+
// Only consider pure operations
|
|
1055
|
+
if (!op || typeof op !== 'string') return
|
|
1056
|
+
if (op.startsWith('i32.') || op.startsWith('i64.') || op.startsWith('f32.') || op.startsWith('f64.')) {
|
|
1057
|
+
// Skip simple consts
|
|
1058
|
+
if (op.endsWith('.const')) return
|
|
1059
|
+
// Skip if has side effects (calls, memory ops)
|
|
1060
|
+
let hasSideEffects = false
|
|
1061
|
+
walk(n, (sub) => {
|
|
1062
|
+
if (Array.isArray(sub) && (sub[0] === 'call' || sub[0]?.includes('load') || sub[0]?.includes('store'))) {
|
|
1063
|
+
hasSideEffects = true
|
|
1064
|
+
}
|
|
1065
|
+
})
|
|
1066
|
+
if (hasSideEffects) return
|
|
1067
|
+
|
|
1068
|
+
const hash = exprHash(n)
|
|
1069
|
+
if (seen.has(hash)) {
|
|
1070
|
+
seen.get(hash).count++
|
|
1071
|
+
} else {
|
|
1072
|
+
seen.set(hash, { node: n, count: 1 })
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
// For now, just report - full CSE would require inserting locals
|
|
1078
|
+
// which changes the function structure significantly
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
return result
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ==================== LOOP INVARIANT HOISTING ====================
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Hoist loop-invariant computations out of loops.
|
|
1088
|
+
* @param {Array} ast
|
|
1089
|
+
* @returns {Array}
|
|
1090
|
+
*/
|
|
1091
|
+
const hoist = (ast) => {
|
|
1092
|
+
const result = clone(ast)
|
|
1093
|
+
|
|
1094
|
+
walk(result, (node) => {
|
|
1095
|
+
if (!Array.isArray(node) || node[0] !== 'func') return
|
|
1096
|
+
|
|
1097
|
+
// Find loops
|
|
1098
|
+
walk(node, (loopNode, parent, idx) => {
|
|
1099
|
+
if (!Array.isArray(loopNode) || loopNode[0] !== 'loop') return
|
|
1100
|
+
|
|
1101
|
+
// Collect all locals modified in loop
|
|
1102
|
+
const modifiedLocals = new Set()
|
|
1103
|
+
walk(loopNode, (n) => {
|
|
1104
|
+
if (!Array.isArray(n)) return
|
|
1105
|
+
if (n[0] === 'local.set' || n[0] === 'local.tee') {
|
|
1106
|
+
if (typeof n[1] === 'string') modifiedLocals.add(n[1])
|
|
1107
|
+
}
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
// Find invariant expressions (don't depend on modified locals or memory)
|
|
1111
|
+
const invariants = []
|
|
1112
|
+
|
|
1113
|
+
for (let i = 1; i < loopNode.length; i++) {
|
|
1114
|
+
const instr = loopNode[i]
|
|
1115
|
+
if (!Array.isArray(instr)) continue
|
|
1116
|
+
|
|
1117
|
+
const op = instr[0]
|
|
1118
|
+
// Skip control flow
|
|
1119
|
+
if (op === 'block' || op === 'loop' || op === 'if' || op === 'br' || op === 'br_if') continue
|
|
1120
|
+
|
|
1121
|
+
// Check if pure and invariant
|
|
1122
|
+
let isInvariant = true
|
|
1123
|
+
let isPure = true
|
|
1124
|
+
|
|
1125
|
+
walk(instr, (n) => {
|
|
1126
|
+
if (!Array.isArray(n)) return
|
|
1127
|
+
const subOp = n[0]
|
|
1128
|
+
// Side effects
|
|
1129
|
+
if (subOp === 'call' || subOp === 'call_indirect' || subOp?.includes('store') || subOp?.includes('load')) {
|
|
1130
|
+
isPure = false
|
|
1131
|
+
}
|
|
1132
|
+
// Depends on modified local
|
|
1133
|
+
if (subOp === 'local.get' && typeof n[1] === 'string' && modifiedLocals.has(n[1])) {
|
|
1134
|
+
isInvariant = false
|
|
1135
|
+
}
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
// Only hoist simple const expressions for safety
|
|
1139
|
+
if (isPure && isInvariant && op?.endsWith('.const')) {
|
|
1140
|
+
// Actually, consts are already cheap - skip
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Full hoisting would require inserting code before the loop
|
|
1145
|
+
// This is complex and risky, so we keep it minimal
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
return result
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ==================== MAIN ====================
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Optimize AST.
|
|
1156
|
+
*
|
|
1157
|
+
* @param {Array|string} ast - AST or WAT source
|
|
1158
|
+
* @param {boolean|string|Object} [opts=true] - Optimization options
|
|
1159
|
+
* @returns {Array} Optimized AST
|
|
1160
|
+
*
|
|
1161
|
+
* @example
|
|
1162
|
+
* optimize(ast) // all optimizations
|
|
1163
|
+
* optimize(ast, 'treeshake') // only treeshake
|
|
1164
|
+
* optimize(ast, { fold: true }) // explicit
|
|
1165
|
+
*/
|
|
1166
|
+
export default function optimize(ast, opts = true) {
|
|
1167
|
+
if (typeof ast === 'string') ast = parse(ast)
|
|
1168
|
+
ast = clone(ast)
|
|
1169
|
+
opts = normalize(opts)
|
|
1170
|
+
|
|
1171
|
+
if (opts.fold) ast = fold(ast)
|
|
1172
|
+
if (opts.identity) ast = identity(ast)
|
|
1173
|
+
if (opts.strength) ast = strength(ast)
|
|
1174
|
+
if (opts.branch) ast = branch(ast)
|
|
1175
|
+
if (opts.propagate) ast = propagate(ast)
|
|
1176
|
+
if (opts.inline) ast = inline(ast)
|
|
1177
|
+
if (opts.deadcode) ast = deadcode(ast)
|
|
1178
|
+
if (opts.locals) ast = localReuse(ast)
|
|
1179
|
+
if (opts.treeshake) ast = treeshake(ast)
|
|
1180
|
+
|
|
1181
|
+
return ast
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS }
|