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.
@@ -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 }