watr 4.3.4 → 4.4.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/src/optimize.js CHANGED
@@ -18,11 +18,40 @@ const OPTS = {
18
18
  branch: true, // simplify constant branches
19
19
  propagate: true, // constant propagation through locals
20
20
  inline: true, // inline tiny functions
21
+ vacuum: true, // remove nops, drop-of-pure, empty branches
22
+ peephole: true, // x-x→0, x&0→0, etc.
23
+ globals: true, // propagate immutable global constants
24
+ offset: true, // fold add+const into load/store offset
25
+ unbranch: true, // remove redundant br at end of own block
26
+ stripmut: true, // strip mut from never-written globals
27
+ brif: true, // if-then-br → br_if
28
+ foldarms: true, // merge identical trailing if arms
29
+ // minify: true, // NOTE: disabled — renaming $ids has no binary-size effect
30
+ // without a names section, and risks local-name collisions.
31
+ dedupe: true, // eliminate duplicate functions
32
+ reorder: true, // put hot functions first for smaller LEBs
33
+ dedupTypes: true, // merge identical type definitions
34
+ packData: true, // trim trailing zeros, merge adjacent data segments
35
+ minifyImports: false, // shorten import names — enable only when you control the host
21
36
  }
22
37
 
23
38
  /** All optimization names */
24
39
  const ALL = Object.keys(OPTS)
25
40
 
41
+ /**
42
+ * Fast structural equality of two AST nodes.
43
+ * Stops at first difference. Handles BigInt without stringification.
44
+ */
45
+ const equal = (a, b) => {
46
+ if (a === b) return true
47
+ if (typeof a !== typeof b) return false
48
+ if (typeof a === 'bigint') return a === b
49
+ if (!Array.isArray(a) || !Array.isArray(b)) return false
50
+ if (a.length !== b.length) return false
51
+ for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false
52
+ return true
53
+ }
54
+
26
55
  /**
27
56
  * Normalize options to { opt: bool } map.
28
57
  * @param {boolean|string|Object} opts
@@ -33,7 +62,8 @@ const normalize = (opts) => {
33
62
  if (opts === false) return {}
34
63
  if (typeof opts === 'string') {
35
64
  const set = new Set(opts.split(/\s+/).filter(Boolean))
36
- // If single optimization name, enable just that one
65
+ // Special case: a single explicit pass name (e.g. 'fold') enables only that pass,
66
+ // rather than treating it as a sparse map where everything else is disabled.
37
67
  if (set.size === 1 && ALL.includes([...set][0])) {
38
68
  return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
39
69
  }
@@ -83,6 +113,34 @@ const walkPost = (node, fn, parent, idx) => {
83
113
  return result !== undefined ? result : node
84
114
  }
85
115
 
116
+ /**
117
+ * Locate the parts of an `(if ...)` node:
118
+ * condIdx → index of the condition expression
119
+ * cond → the condition expression itself
120
+ * thenBranch / elseBranch → the (then ...) / (else ...) sub-arrays, or null
121
+ * The condition sits after any leading `param`/`result` annotations and before
122
+ * the `then`/`else` arms.
123
+ */
124
+ const parseIf = (node) => {
125
+ let condIdx = 1
126
+ while (condIdx < node.length) {
127
+ const c = node[condIdx]
128
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else' || c[0] === 'result' || c[0] === 'param')) {
129
+ condIdx++
130
+ continue
131
+ }
132
+ break
133
+ }
134
+ let thenBranch = null, elseBranch = null
135
+ for (let i = condIdx + 1; i < node.length; i++) {
136
+ const c = node[i]
137
+ if (!Array.isArray(c)) continue
138
+ if (c[0] === 'then') thenBranch = c
139
+ else if (c[0] === 'else') elseBranch = c
140
+ }
141
+ return { condIdx, cond: node[condIdx], thenBranch, elseBranch }
142
+ }
143
+
86
144
  // ==================== TREESHAKE ====================
87
145
 
88
146
  /**
@@ -94,192 +152,136 @@ const walkPost = (node, fn, parent, idx) => {
94
152
  const treeshake = (ast) => {
95
153
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
96
154
 
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 = []
155
+ // Index spaces. Each entry is shared between its $name key and its numeric idx
156
+ // key, so name/index lookups hit the same record. nodeMap covers reverse lookup
157
+ // (used during the filtering pass for unnamed definitions).
158
+ const funcs = new Map(), globals = new Map(), types = new Map()
159
+ const tables = new Map(), memories = new Map()
160
+ const nodeMap = new Map() // node → entry
161
+
162
+ const register = (map, node, idx, isImport = false) => {
163
+ const named = typeof node[1] === 'string' && node[1][0] === '$'
164
+ const name = named ? node[1] : idx
165
+ const inlineExported = !isImport && node.some(s => Array.isArray(s) && s[0] === 'export')
166
+ const entry = { node, idx, used: inlineExported, isImport }
167
+ map.set(name, entry)
168
+ if (named) map.set(idx, entry)
169
+ nodeMap.set(node, entry)
170
+ return entry
171
+ }
105
172
 
106
- let funcIdx = 0, globalIdx = 0, typeIdx = 0, tableIdx = 0, memIdx = 0, importFuncIdx = 0
173
+ let funcIdx = 0, globalIdx = 0, typeIdx = 0, tableIdx = 0, memIdx = 0
174
+ const elems = [], exports = [], starts = []
107
175
 
108
176
  for (const node of ast.slice(1)) {
109
177
  if (!Array.isArray(node)) continue
110
178
  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
- }
179
+ if (kind === 'type') register(types, node, typeIdx++)
180
+ else if (kind === 'func') register(funcs, node, funcIdx++)
181
+ else if (kind === 'global') register(globals, node, globalIdx++)
182
+ else if (kind === 'table') register(tables, node, tableIdx++)
183
+ else if (kind === 'memory') register(memories, node, memIdx++)
147
184
  else if (kind === 'import') {
148
- // Imports are always kept; mark as used
185
+ // Each import sub-item occupies its own slot in the relevant index space.
149
186
  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
- }
187
+ if (!Array.isArray(sub)) continue
188
+ if (sub[0] === 'func') register(funcs, sub, funcIdx++, true)
189
+ else if (sub[0] === 'global') register(globals, sub, globalIdx++, true)
190
+ else if (sub[0] === 'table') register(tables, sub, tableIdx++, true)
191
+ else if (sub[0] === 'memory') register(memories, sub, memIdx++, true)
157
192
  }
158
193
  }
159
- else if (kind === 'export') {
160
- exports.push(node)
161
- }
162
- else if (kind === 'start') {
163
- starts.push(node)
164
- }
194
+ else if (kind === 'export') exports.push(node)
195
+ else if (kind === 'start') starts.push(node)
196
+ else if (kind === 'elem') elems.push(node)
197
+ }
198
+
199
+ // Worklist: function entries whose body still needs to be scanned for refs.
200
+ const work = []
201
+ const enqueue = (entry) => { if (entry && !entry.scanned) work.push(entry) }
202
+ const markFunc = (ref) => {
203
+ const e = funcs.get(ref); if (!e) return
204
+ if (!e.used) e.used = true
205
+ enqueue(e)
165
206
  }
207
+ const markGlobal = (ref) => { const e = globals.get(ref); if (e) e.used = true }
208
+ const markTable = (ref) => { const e = tables.get(ref); if (e) e.used = true }
209
+ const markMemory = (ref) => { const e = memories.get(ref); if (e) e.used = true }
210
+ const markType = (ref) => { const e = types.get(ref); if (e) e.used = true }
166
211
 
167
- // Mark exports as used
212
+ // Roots: explicit exports, start funcs, elem-referenced funcs, inline-exported items.
168
213
  for (const exp of exports) {
169
214
  for (const sub of exp) {
170
215
  if (!Array.isArray(sub)) continue
171
216
  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
217
+ if (kind === 'func') markFunc(ref)
218
+ else if (kind === 'global') markGlobal(ref)
219
+ else if (kind === 'table') markTable(ref)
220
+ else if (kind === 'memory') markMemory(ref)
176
221
  }
177
222
  }
178
-
179
- // Mark start function as used
180
223
  for (const start of starts) {
181
224
  let ref = start[1]
182
- // Convert numeric string refs to numbers
183
225
  if (typeof ref === 'string' && ref[0] !== '$') ref = +ref
184
- if (funcs.has(ref)) funcs.get(ref).used = true
226
+ markFunc(ref)
185
227
  }
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 }
228
+ for (const elem of elems) {
229
+ walk(elem, n => {
230
+ if (Array.isArray(n) && n[0] === 'ref.func') markFunc(n[1])
231
+ else if (typeof n === 'string' && n[0] === '$') markFunc(n)
232
+ })
194
233
  }
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
234
+ for (const m of [funcs, globals, tables, memories]) for (const e of m.values()) if (e.used) enqueue(e)
235
+
236
+ // If nothing anchors the module (no exports, start, elem, or inline exports),
237
+ // assume the module is consumed elsewhere and keep everything.
238
+ const hasAnchor = exports.length > 0 || starts.length > 0 || elems.length > 0 || work.length > 0
239
+ if (!hasAnchor) {
240
+ for (const m of [funcs, globals, tables, memories]) for (const e of m.values()) e.used = true
241
+ return ast
202
242
  }
203
243
 
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
244
+ // Drain worklist: each function body gets walked exactly once.
245
+ while (work.length) {
246
+ const entry = work.pop()
247
+ if (entry.scanned) continue
248
+ entry.scanned = true
249
+ if (entry.isImport) continue
250
+ walk(entry.node, n => {
251
+ if (!Array.isArray(n)) {
252
+ if (typeof n === 'string' && n[0] === '$') markFunc(n)
253
+ return
254
+ }
255
+ const [op, ref] = n
256
+ if (op === 'call' || op === 'return_call' || op === 'ref.func') markFunc(ref)
257
+ else if (op === 'global.get' || op === 'global.set') markGlobal(ref)
258
+ else if (op === 'type') markType(ref)
259
+ else if (op === 'call_indirect' || op === 'return_call_indirect') {
260
+ for (const sub of n) if (typeof sub === 'string' && sub[0] === '$') markTable(sub)
211
261
  }
212
- // Also plain func refs in elem
213
- if (typeof n === 'string' && n[0] === '$' && funcs.has(n)) funcs.get(n).used = true
214
262
  })
215
263
  }
216
264
 
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
265
+ // Filter: keep used definitions. nodeMap handles unnamed entries directly.
259
266
  const result = ['module']
260
267
  for (const node of ast.slice(1)) {
261
268
  if (!Array.isArray(node)) { result.push(node); continue }
262
269
  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 {
270
+ if (kind === 'func' || kind === 'global' || kind === 'type') {
271
+ if (nodeMap.get(node)?.used) result.push(node)
272
+ } else if (kind === 'import') {
273
+ // Keep import only if any of its sub-items is used.
274
+ let used = false
275
+ for (const sub of node) {
276
+ if (!Array.isArray(sub)) continue
277
+ const e = nodeMap.get(sub)
278
+ if (e?.used) { used = true; break }
279
+ }
280
+ if (used) result.push(node)
281
+ } else {
279
282
  result.push(node)
280
283
  }
281
284
  }
282
-
283
285
  return result
284
286
  }
285
287
 
@@ -288,92 +290,109 @@ const treeshake = (ast) => {
288
290
  /** IEEE 754 roundTiesToEven (bankers' rounding) */
289
291
  const roundEven = (x) => x - Math.floor(x) !== 0.5 ? Math.round(x) : 2 * Math.round(x / 2)
290
292
 
291
- /** Operators that can be constant-folded */
293
+ /** Build i32 comparison folder: returns 1/0 */
294
+ const i32c = (fn) => (a, b) => fn(a, b) ? 1 : 0
295
+ /** Build unsigned i32 comparison folder */
296
+ const u32c = (fn) => (a, b) => fn(a >>> 0, b >>> 0) ? 1 : 0
297
+ /** Build i64 comparison folder */
298
+ const i64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
299
+ /** Build unsigned i64 comparison folder */
300
+ const u64c = (fn) => (a, b) => fn(BigInt.asUintN(64, a), BigInt.asUintN(64, b)) ? 1 : 0
301
+
302
+ /**
303
+ * Constant folders, keyed by op. Each entry is [fn, resultType].
304
+ * Comparisons return i32, conversions return their named output type.
305
+ */
292
306
  const FOLDABLE = {
293
- // i32
294
- 'i32.add': (a, b) => (a + b) | 0,
295
- 'i32.sub': (a, b) => (a - b) | 0,
296
- 'i32.mul': (a, b) => Math.imul(a, b),
297
- 'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
298
- 'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
299
- 'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
300
- 'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
301
- 'i32.and': (a, b) => a & b,
302
- 'i32.or': (a, b) => a | b,
303
- 'i32.xor': (a, b) => a ^ b,
304
- 'i32.shl': (a, b) => a << (b & 31),
305
- 'i32.shr_s': (a, b) => a >> (b & 31),
306
- 'i32.shr_u': (a, b) => a >>> (b & 31),
307
- 'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
308
- 'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
309
- 'i32.eq': (a, b) => a === b ? 1 : 0,
310
- 'i32.ne': (a, b) => a !== b ? 1 : 0,
311
- 'i32.lt_s': (a, b) => a < b ? 1 : 0,
312
- 'i32.lt_u': (a, b) => (a >>> 0) < (b >>> 0) ? 1 : 0,
313
- 'i32.gt_s': (a, b) => a > b ? 1 : 0,
314
- 'i32.gt_u': (a, b) => (a >>> 0) > (b >>> 0) ? 1 : 0,
315
- 'i32.le_s': (a, b) => a <= b ? 1 : 0,
316
- 'i32.le_u': (a, b) => (a >>> 0) <= (b >>> 0) ? 1 : 0,
317
- 'i32.ge_s': (a, b) => a >= b ? 1 : 0,
318
- 'i32.ge_u': (a, b) => (a >>> 0) >= (b >>> 0) ? 1 : 0,
319
- 'i32.eqz': (a) => a === 0 ? 1 : 0,
320
- 'i32.clz': (a) => Math.clz32(a),
321
- 'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
322
- 'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
323
- 'i32.wrap_i64': (a) => Number(BigInt.asIntN(32, a)),
307
+ // i32 arithmetic
308
+ 'i32.add': [(a, b) => (a + b) | 0, 'i32'],
309
+ 'i32.sub': [(a, b) => (a - b) | 0, 'i32'],
310
+ 'i32.mul': [(a, b) => Math.imul(a, b), 'i32'],
311
+ 'i32.div_s': [(a, b) => b !== 0 ? (a / b) | 0 : null, 'i32'],
312
+ 'i32.div_u': [(a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null, 'i32'],
313
+ 'i32.rem_s': [(a, b) => b !== 0 ? (a % b) | 0 : null, 'i32'],
314
+ 'i32.rem_u': [(a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null, 'i32'],
315
+ 'i32.and': [(a, b) => a & b, 'i32'],
316
+ 'i32.or': [(a, b) => a | b, 'i32'],
317
+ 'i32.xor': [(a, b) => a ^ b, 'i32'],
318
+ 'i32.shl': [(a, b) => a << (b & 31), 'i32'],
319
+ 'i32.shr_s': [(a, b) => a >> (b & 31), 'i32'],
320
+ 'i32.shr_u': [(a, b) => a >>> (b & 31), 'i32'],
321
+ 'i32.rotl': [(a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 }, 'i32'],
322
+ 'i32.rotr': [(a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 }, 'i32'],
323
+ 'i32.eq': [i32c((a, b) => a === b), 'i32'],
324
+ 'i32.ne': [i32c((a, b) => a !== b), 'i32'],
325
+ 'i32.lt_s': [i32c((a, b) => a < b), 'i32'],
326
+ 'i32.lt_u': [u32c((a, b) => a < b), 'i32'],
327
+ 'i32.gt_s': [i32c((a, b) => a > b), 'i32'],
328
+ 'i32.gt_u': [u32c((a, b) => a > b), 'i32'],
329
+ 'i32.le_s': [i32c((a, b) => a <= b), 'i32'],
330
+ 'i32.le_u': [u32c((a, b) => a <= b), 'i32'],
331
+ 'i32.ge_s': [i32c((a, b) => a >= b), 'i32'],
332
+ 'i32.ge_u': [u32c((a, b) => a >= b), 'i32'],
333
+ 'i32.eqz': [(a) => a === 0 ? 1 : 0, 'i32'],
334
+ 'i32.clz': [(a) => Math.clz32(a), 'i32'],
335
+ 'i32.ctz': [(a) => a === 0 ? 32 : 31 - Math.clz32(a & -a), 'i32'],
336
+ 'i32.popcnt': [(a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c }, 'i32'],
337
+ 'i32.wrap_i64': [(a) => Number(BigInt.asIntN(32, a)), 'i32'],
338
+ 'i32.extend8_s': [(a) => (a << 24) >> 24, 'i32'],
339
+ 'i32.extend16_s': [(a) => (a << 16) >> 16, 'i32'],
324
340
 
325
341
  // i64 (using BigInt)
326
- 'i64.add': (a, b) => BigInt.asIntN(64, a + b),
327
- 'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
328
- 'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
329
- 'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
330
- 'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
331
- 'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
332
- 'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
333
- 'i64.and': (a, b) => BigInt.asIntN(64, a & b),
334
- 'i64.or': (a, b) => BigInt.asIntN(64, a | b),
335
- 'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
336
- 'i64.shl': (a, b) => BigInt.asIntN(64, a << (b & 63n)),
337
- 'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
338
- 'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
339
- 'i64.eq': (a, b) => a === b ? 1 : 0,
340
- 'i64.ne': (a, b) => a !== b ? 1 : 0,
341
- 'i64.lt_s': (a, b) => a < b ? 1 : 0,
342
- 'i64.lt_u': (a, b) => BigInt.asUintN(64, a) < BigInt.asUintN(64, b) ? 1 : 0,
343
- 'i64.gt_s': (a, b) => a > b ? 1 : 0,
344
- 'i64.gt_u': (a, b) => BigInt.asUintN(64, a) > BigInt.asUintN(64, b) ? 1 : 0,
345
- 'i64.le_s': (a, b) => a <= b ? 1 : 0,
346
- 'i64.le_u': (a, b) => BigInt.asUintN(64, a) <= BigInt.asUintN(64, b) ? 1 : 0,
347
- 'i64.ge_s': (a, b) => a >= b ? 1 : 0,
348
- 'i64.ge_u': (a, b) => BigInt.asUintN(64, a) >= BigInt.asUintN(64, b) ? 1 : 0,
349
- 'i64.eqz': (a) => a === 0n ? 1 : 0,
350
- 'i64.extend_i32_s': (a) => BigInt(a),
351
- 'i64.extend_i32_u': (a) => BigInt(a >>> 0),
352
-
353
- // f32/f64 - be careful with NaN/precision
354
- 'f32.add': (a, b) => Math.fround(a + b),
355
- 'f32.sub': (a, b) => Math.fround(a - b),
356
- 'f32.mul': (a, b) => Math.fround(a * b),
357
- 'f32.div': (a, b) => Math.fround(a / b),
358
- 'f32.neg': (a) => Math.fround(-a),
359
- 'f32.abs': (a) => Math.fround(Math.abs(a)),
360
- 'f32.sqrt': (a) => Math.fround(Math.sqrt(a)),
361
- 'f32.ceil': (a) => Math.fround(Math.ceil(a)),
362
- 'f32.floor': (a) => Math.fround(Math.floor(a)),
363
- 'f32.trunc': (a) => Math.fround(Math.trunc(a)),
364
- 'f32.nearest': (a) => Math.fround(roundEven(a)),
365
-
366
- 'f64.add': (a, b) => a + b,
367
- 'f64.sub': (a, b) => a - b,
368
- 'f64.mul': (a, b) => a * b,
369
- 'f64.div': (a, b) => a / b,
370
- 'f64.neg': (a) => -a,
371
- 'f64.abs': (a) => Math.abs(a),
372
- 'f64.sqrt': (a) => Math.sqrt(a),
373
- 'f64.ceil': (a) => Math.ceil(a),
374
- 'f64.floor': (a) => Math.floor(a),
375
- 'f64.trunc': (a) => Math.trunc(a),
376
- 'f64.nearest': roundEven,
342
+ 'i64.add': [(a, b) => BigInt.asIntN(64, a + b), 'i64'],
343
+ 'i64.sub': [(a, b) => BigInt.asIntN(64, a - b), 'i64'],
344
+ 'i64.mul': [(a, b) => BigInt.asIntN(64, a * b), 'i64'],
345
+ 'i64.div_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null, 'i64'],
346
+ 'i64.div_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null, 'i64'],
347
+ 'i64.rem_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null, 'i64'],
348
+ 'i64.rem_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null, 'i64'],
349
+ 'i64.and': [(a, b) => BigInt.asIntN(64, a & b), 'i64'],
350
+ 'i64.or': [(a, b) => BigInt.asIntN(64, a | b), 'i64'],
351
+ 'i64.xor': [(a, b) => BigInt.asIntN(64, a ^ b), 'i64'],
352
+ 'i64.shl': [(a, b) => BigInt.asIntN(64, a << (b & 63n)), 'i64'],
353
+ 'i64.shr_s': [(a, b) => BigInt.asIntN(64, a >> (b & 63n)), 'i64'],
354
+ 'i64.shr_u': [(a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)), 'i64'],
355
+ 'i64.eq': [i64c((a, b) => a === b), 'i32'],
356
+ 'i64.ne': [i64c((a, b) => a !== b), 'i32'],
357
+ 'i64.lt_s': [i64c((a, b) => a < b), 'i32'],
358
+ 'i64.lt_u': [u64c((a, b) => a < b), 'i32'],
359
+ 'i64.gt_s': [i64c((a, b) => a > b), 'i32'],
360
+ 'i64.gt_u': [u64c((a, b) => a > b), 'i32'],
361
+ 'i64.le_s': [i64c((a, b) => a <= b), 'i32'],
362
+ 'i64.le_u': [u64c((a, b) => a <= b), 'i32'],
363
+ 'i64.ge_s': [i64c((a, b) => a >= b), 'i32'],
364
+ 'i64.ge_u': [u64c((a, b) => a >= b), 'i32'],
365
+ 'i64.eqz': [(a) => a === 0n ? 1 : 0, 'i32'],
366
+ 'i64.extend_i32_s': [(a) => BigInt(a), 'i64'],
367
+ 'i64.extend_i32_u': [(a) => BigInt(a >>> 0), 'i64'],
368
+ 'i64.extend8_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(8, a)), 'i64'],
369
+ 'i64.extend16_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(16, a)), 'i64'],
370
+ 'i64.extend32_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(32, a)), 'i64'],
371
+
372
+ // f32/f64 (NaN/precision-aware via Math.fround)
373
+ 'f32.add': [(a, b) => Math.fround(a + b), 'f32'],
374
+ 'f32.sub': [(a, b) => Math.fround(a - b), 'f32'],
375
+ 'f32.mul': [(a, b) => Math.fround(a * b), 'f32'],
376
+ 'f32.div': [(a, b) => Math.fround(a / b), 'f32'],
377
+ 'f32.neg': [(a) => Math.fround(-a), 'f32'],
378
+ 'f32.abs': [(a) => Math.fround(Math.abs(a)), 'f32'],
379
+ 'f32.sqrt': [(a) => Math.fround(Math.sqrt(a)), 'f32'],
380
+ 'f32.ceil': [(a) => Math.fround(Math.ceil(a)), 'f32'],
381
+ 'f32.floor': [(a) => Math.fround(Math.floor(a)), 'f32'],
382
+ 'f32.trunc': [(a) => Math.fround(Math.trunc(a)), 'f32'],
383
+ 'f32.nearest': [(a) => Math.fround(roundEven(a)), 'f32'],
384
+
385
+ 'f64.add': [(a, b) => a + b, 'f64'],
386
+ 'f64.sub': [(a, b) => a - b, 'f64'],
387
+ 'f64.mul': [(a, b) => a * b, 'f64'],
388
+ 'f64.div': [(a, b) => a / b, 'f64'],
389
+ 'f64.neg': [(a) => -a, 'f64'],
390
+ 'f64.abs': [Math.abs, 'f64'],
391
+ 'f64.sqrt': [Math.sqrt, 'f64'],
392
+ 'f64.ceil': [Math.ceil, 'f64'],
393
+ 'f64.floor': [Math.floor, 'f64'],
394
+ 'f64.trunc': [Math.trunc, 'f64'],
395
+ 'f64.nearest': [roundEven, 'f64'],
377
396
  }
378
397
 
379
398
  /**
@@ -413,124 +432,80 @@ const makeConst = (type, value) => {
413
432
  const fold = (ast) => {
414
433
  return walkPost(clone(ast), (node) => {
415
434
  if (!Array.isArray(node)) return
416
- const op = node[0]
417
- const fn = FOLDABLE[op]
418
- if (!fn) return
435
+ const entry = FOLDABLE[node[0]]
436
+ if (!entry) return
437
+ const [fn, t] = entry
419
438
 
420
- // Unary ops
439
+ // Unary
421
440
  if (fn.length === 1 && node.length === 2) {
422
441
  const a = getConst(node[1])
423
442
  if (!a) return
424
- const result = fn(a.value)
425
- if (result === null) return
426
- const resultType = op.startsWith('i64.') && !op.includes('eqz') ? 'i64' :
427
- op.startsWith('f32.') ? 'f32' :
428
- op.startsWith('f64.') ? 'f64' : 'i32'
429
- return makeConst(resultType, result)
443
+ const r = fn(a.value)
444
+ if (r === null) return
445
+ return makeConst(t, r)
430
446
  }
431
-
432
- // Binary ops
447
+ // Binary
433
448
  if (fn.length === 2 && node.length === 3) {
434
- const a = getConst(node[1])
435
- const b = getConst(node[2])
449
+ const a = getConst(node[1]), b = getConst(node[2])
436
450
  if (!a || !b) return
437
- const result = fn(a.value, b.value)
438
- if (result === null) return
439
- // Comparisons return i32
440
- const isCompare = /\.(eq|ne|[lg][te])/.test(op)
441
- const resultType = isCompare ? 'i32' :
442
- op.startsWith('i64.') ? 'i64' :
443
- op.startsWith('f32.') ? 'f32' :
444
- op.startsWith('f64.') ? 'f64' : 'i32'
445
- return makeConst(resultType, result)
451
+ const r = fn(a.value, b.value)
452
+ if (r === null) return
453
+ return makeConst(t, r)
446
454
  }
447
455
  })
448
456
  }
449
457
 
450
458
  // ==================== IDENTITY REMOVAL ====================
451
459
 
460
+ /**
461
+ * Create identity checker for commutative binary ops:
462
+ * neutral op x → x and x op neutral → x
463
+ */
464
+ const commutativeIdentity = (neutral) => (a, b) => {
465
+ const ca = getConst(a), cb = getConst(b)
466
+ if (ca?.value === neutral) return b
467
+ if (cb?.value === neutral) return a
468
+ return null
469
+ }
470
+
471
+ /**
472
+ * Create identity checker for right-neutral binary ops:
473
+ * x op neutral → x
474
+ */
475
+ const rightIdentity = (neutral) => (a, b) => getConst(b)?.value === neutral ? a : null
476
+
452
477
  /** Identity operations that can be simplified */
453
478
  const IDENTITIES = {
454
479
  // x + 0 → x, 0 + x → x
455
- 'i32.add': (a, b) => {
456
- const ca = getConst(a), cb = getConst(b)
457
- if (ca?.value === 0) return b
458
- if (cb?.value === 0) return a
459
- return null
460
- },
461
- 'i64.add': (a, b) => {
462
- const ca = getConst(a), cb = getConst(b)
463
- if (ca?.value === 0n) return b
464
- if (cb?.value === 0n) return a
465
- return null
466
- },
480
+ 'i32.add': commutativeIdentity(0),
481
+ 'i64.add': commutativeIdentity(0n),
467
482
  // x - 0 → x
468
- 'i32.sub': (a, b) => getConst(b)?.value === 0 ? a : null,
469
- 'i64.sub': (a, b) => getConst(b)?.value === 0n ? a : null,
483
+ 'i32.sub': rightIdentity(0),
484
+ 'i64.sub': rightIdentity(0n),
470
485
  // x * 1 → x, 1 * x → x
471
- 'i32.mul': (a, b) => {
472
- const ca = getConst(a), cb = getConst(b)
473
- if (ca?.value === 1) return b
474
- if (cb?.value === 1) return a
475
- return null
476
- },
477
- 'i64.mul': (a, b) => {
478
- const ca = getConst(a), cb = getConst(b)
479
- if (ca?.value === 1n) return b
480
- if (cb?.value === 1n) return a
481
- return null
482
- },
486
+ 'i32.mul': commutativeIdentity(1),
487
+ 'i64.mul': commutativeIdentity(1n),
483
488
  // x / 1 → x
484
- 'i32.div_s': (a, b) => getConst(b)?.value === 1 ? a : null,
485
- 'i32.div_u': (a, b) => getConst(b)?.value === 1 ? a : null,
486
- 'i64.div_s': (a, b) => getConst(b)?.value === 1n ? a : null,
487
- 'i64.div_u': (a, b) => getConst(b)?.value === 1n ? a : null,
489
+ 'i32.div_s': rightIdentity(1),
490
+ 'i32.div_u': rightIdentity(1),
491
+ 'i64.div_s': rightIdentity(1n),
492
+ 'i64.div_u': rightIdentity(1n),
488
493
  // x & -1 → x, -1 & x → x (all bits set)
489
- 'i32.and': (a, b) => {
490
- const ca = getConst(a), cb = getConst(b)
491
- if (ca?.value === -1) return b
492
- if (cb?.value === -1) return a
493
- return null
494
- },
495
- 'i64.and': (a, b) => {
496
- const ca = getConst(a), cb = getConst(b)
497
- if (ca?.value === -1n) return b
498
- if (cb?.value === -1n) return a
499
- return null
500
- },
494
+ 'i32.and': commutativeIdentity(-1),
495
+ 'i64.and': commutativeIdentity(-1n),
501
496
  // x | 0 → x, 0 | x → x
502
- 'i32.or': (a, b) => {
503
- const ca = getConst(a), cb = getConst(b)
504
- if (ca?.value === 0) return b
505
- if (cb?.value === 0) return a
506
- return null
507
- },
508
- 'i64.or': (a, b) => {
509
- const ca = getConst(a), cb = getConst(b)
510
- if (ca?.value === 0n) return b
511
- if (cb?.value === 0n) return a
512
- return null
513
- },
497
+ 'i32.or': commutativeIdentity(0),
498
+ 'i64.or': commutativeIdentity(0n),
514
499
  // x ^ 0 → x, 0 ^ x → x
515
- 'i32.xor': (a, b) => {
516
- const ca = getConst(a), cb = getConst(b)
517
- if (ca?.value === 0) return b
518
- if (cb?.value === 0) return a
519
- return null
520
- },
521
- 'i64.xor': (a, b) => {
522
- const ca = getConst(a), cb = getConst(b)
523
- if (ca?.value === 0n) return b
524
- if (cb?.value === 0n) return a
525
- return null
526
- },
500
+ 'i32.xor': commutativeIdentity(0),
501
+ 'i64.xor': commutativeIdentity(0n),
527
502
  // x << 0 → x, x >> 0 → x
528
- 'i32.shl': (a, b) => getConst(b)?.value === 0 ? a : null,
529
- 'i32.shr_s': (a, b) => getConst(b)?.value === 0 ? a : null,
530
- 'i32.shr_u': (a, b) => getConst(b)?.value === 0 ? a : null,
531
- 'i64.shl': (a, b) => getConst(b)?.value === 0n ? a : null,
532
- 'i64.shr_s': (a, b) => getConst(b)?.value === 0n ? a : null,
533
- 'i64.shr_u': (a, b) => getConst(b)?.value === 0n ? a : null,
503
+ 'i32.shl': rightIdentity(0),
504
+ 'i32.shr_s': rightIdentity(0),
505
+ 'i32.shr_u': rightIdentity(0),
506
+ 'i64.shl': rightIdentity(0n),
507
+ 'i64.shr_s': rightIdentity(0n),
508
+ 'i64.shr_u': rightIdentity(0n),
534
509
  // f + 0 → x (careful with -0.0, skip for floats)
535
510
  // f * 1 → x (careful with NaN, skip for floats)
536
511
  }
@@ -636,50 +611,15 @@ const branch = (ast) => {
636
611
  // (if (i32.const 0) then else) → else
637
612
  // (if (i32.const N) then else) → then (N != 0)
638
613
  if (op === 'if') {
639
- // Find condition - first non-annotation child that's an expression
640
- let condIdx = 1
641
- while (condIdx < node.length) {
642
- const child = node[condIdx]
643
- if (Array.isArray(child) && (child[0] === 'then' || child[0] === 'else' || child[0] === 'result' || child[0] === 'param')) {
644
- condIdx++
645
- continue
646
- }
647
- break
648
- }
649
-
650
- const cond = node[condIdx]
614
+ const { cond, thenBranch, elseBranch } = parseIf(node)
651
615
  const c = getConst(cond)
652
616
  if (!c) return
653
-
654
- // Find then/else branches
655
- let thenBranch = null, elseBranch = null
656
- for (let i = condIdx + 1; i < node.length; i++) {
657
- const child = node[i]
658
- if (Array.isArray(child)) {
659
- if (child[0] === 'then') thenBranch = child
660
- else if (child[0] === 'else') elseBranch = child
661
- }
662
- }
663
-
664
- // Condition is truthy → replace with then contents
665
- if (c.value !== 0 && c.value !== 0n) {
666
- if (thenBranch && thenBranch.length > 1) {
667
- // Return block with then contents (or just contents if single)
668
- const contents = thenBranch.slice(1)
669
- if (contents.length === 1) return contents[0]
670
- return ['block', ...contents]
671
- }
672
- return ['nop']
673
- }
674
- // Condition is falsy → replace with else contents
675
- else {
676
- if (elseBranch && elseBranch.length > 1) {
677
- const contents = elseBranch.slice(1)
678
- if (contents.length === 1) return contents[0]
679
- return ['block', ...contents]
680
- }
681
- return ['nop']
617
+ const taken = c.value !== 0 && c.value !== 0n ? thenBranch : elseBranch
618
+ if (taken && taken.length > 1) {
619
+ const contents = taken.slice(1)
620
+ return contents.length === 1 ? contents[0] : ['block', ...contents]
682
621
  }
622
+ return ['nop']
683
623
  }
684
624
 
685
625
  // (br_if $label (i32.const 0)) → nop
@@ -847,14 +787,33 @@ const localReuse = (ast) => {
847
787
 
848
788
  // ==================== PROPAGATION & LOCAL ELIMINATION ====================
849
789
 
850
- /** Check if expression is pure (no side effects, no memory ops) */
790
+ /** Operators with side effects: calls, mutators, control flow, exceptions, drops. */
791
+ const IMPURE_OPS = new Set([
792
+ 'call', 'call_indirect', 'return_call', 'return_call_indirect',
793
+ 'table.set', 'table.grow', 'table.fill', 'table.copy', 'table.init',
794
+ 'struct.set', 'struct.new',
795
+ 'array.set', 'array.new', 'array.new_fixed', 'array.new_data', 'array.new_elem',
796
+ 'array.init_data', 'array.init_elem', 'ref.i31',
797
+ 'global.set', 'local.set', 'local.tee',
798
+ 'unreachable', 'return',
799
+ 'br', 'br_if', 'br_table', 'br_on_null', 'br_on_non_null', 'br_on_cast', 'br_on_cast_fail',
800
+ 'throw', 'rethrow', 'throw_ref', 'try_table',
801
+ 'data.drop', 'elem.drop',
802
+ ])
803
+
804
+ /** Substrings that flag an op as side-effecting (loads can trap, stores/atomics/memory ops mutate). */
805
+ const IMPURE_SUBSTRINGS = ['.store', 'memory.', '.atomic.']
806
+
807
+ /**
808
+ * Pure means: no side effects, no traps we care about, no control flow.
809
+ * Conservative — returns false for anything that might trap, mutate state, or branch.
810
+ */
851
811
  const isPure = (node) => {
852
812
  if (!Array.isArray(node)) return true
853
813
  const op = node[0]
854
814
  if (typeof op !== 'string') return false
855
- if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect') return false
856
- if (op.includes('.store') || op.includes('.load') || op.includes('memory.')) return false
857
- if (op === 'global.set') return false
815
+ if (IMPURE_OPS.has(op)) return false
816
+ for (const sub of IMPURE_SUBSTRINGS) if (op.includes(sub)) return false
858
817
  for (let i = 1; i < node.length; i++) if (Array.isArray(node[i]) && !isPure(node[i])) return false
859
818
  return true
860
819
  }
@@ -882,6 +841,151 @@ const substGets = (node, known) => walkPost(node, n => {
882
841
  if (k && canSubst(k)) return clone(k.val)
883
842
  })
884
843
 
844
+ /**
845
+ * Forward propagation pass: track local.set values and substitute local.gets.
846
+ * Returns true if any substitution was made.
847
+ * @param {Array} funcNode
848
+ * @param {Set<string>} params
849
+ * @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
850
+ */
851
+ const forwardPropagate = (funcNode, params, useCounts) => {
852
+ let changed = false
853
+ const getUseCount = name => useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
854
+ const known = new Map()
855
+
856
+ for (let i = 1; i < funcNode.length; i++) {
857
+ const instr = funcNode[i]
858
+ if (!Array.isArray(instr)) continue
859
+ const op = instr[0]
860
+
861
+ if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
862
+
863
+ // Track local.set values
864
+ if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
865
+ substGets(instr[2], known) // substitute known values in RHS
866
+ const uses = getUseCount(instr[1])
867
+ known.set(instr[1], {
868
+ val: instr[2], pure: isPure(instr[2]),
869
+ singleUse: uses.gets <= 1 && uses.sets <= 1 && uses.tees === 0
870
+ })
871
+ continue
872
+ }
873
+
874
+ // Invalidate at control-flow boundaries
875
+ if (op === 'block' || op === 'loop' || op === 'if') known.clear()
876
+ // Calls only invalidate non-constant tracked values
877
+ if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect')
878
+ for (const [key, tracked] of known) if (!getConst(tracked.val)) known.delete(key)
879
+
880
+ // Substitute: standalone local.get (walkPost can't replace root)
881
+ if (op === 'local.get' && instr.length === 2 && typeof instr[1] === 'string') {
882
+ const tracked = known.get(instr[1])
883
+ if (tracked && canSubst(tracked)) {
884
+ const replacement = clone(tracked.val)
885
+ instr.length = 0; instr.push(...(Array.isArray(replacement) ? replacement : [replacement]))
886
+ changed = true; continue
887
+ }
888
+ }
889
+
890
+ // Substitute nested local.gets (skip control-flow nodes — locals may be reassigned inside)
891
+ if (op !== 'block' && op !== 'loop' && op !== 'if') {
892
+ const prev = clone(instr)
893
+ substGets(instr, known)
894
+ if (!equal(prev, instr)) changed = true
895
+ }
896
+ }
897
+
898
+ return changed
899
+ }
900
+
901
+ /**
902
+ * Remove adjacent (local.set $x expr) (local.get $x) pairs when $x has no other uses.
903
+ * Returns true if any pair was removed.
904
+ * @param {Array} funcNode
905
+ * @param {Set<string>} params
906
+ * @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
907
+ */
908
+ const eliminateSetGetPairs = (funcNode, params, useCounts) => {
909
+ let changed = false
910
+
911
+ for (let i = 1; i < funcNode.length - 1; i++) {
912
+ const setNode = funcNode[i]
913
+ const getNode = funcNode[i + 1]
914
+ if (!Array.isArray(setNode) || setNode[0] !== 'local.set' || setNode.length !== 3) continue
915
+ if (!Array.isArray(getNode) || getNode[0] !== 'local.get' || getNode.length !== 2) continue
916
+ const name = setNode[1]
917
+ if (getNode[1] !== name || params.has(name)) continue
918
+ const uses = useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
919
+ // Must be exactly 1 set and 1 get (the pair), no tees
920
+ if (uses.sets !== 1 || uses.gets !== 1 || uses.tees !== 0) continue
921
+ // Replace the pair with just the expression
922
+ const expr = clone(setNode[2])
923
+ funcNode.splice(i, 2, ...(Array.isArray(expr) ? [expr] : [expr]))
924
+ changed = true
925
+ i-- // adjust index because we removed 2 and inserted 1
926
+ }
927
+
928
+ return changed
929
+ }
930
+
931
+ /**
932
+ * Convert (local.set $x expr) (local.get $x) to (local.tee $x expr)
933
+ * when $x has additional uses beyond this pair.
934
+ * @param {Array} funcNode
935
+ * @param {Set<string>} params
936
+ * @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
937
+ */
938
+ const createLocalTees = (funcNode, params, useCounts) => {
939
+ let changed = false
940
+
941
+ for (let i = 1; i < funcNode.length - 1; i++) {
942
+ const setNode = funcNode[i]
943
+ const getNode = funcNode[i + 1]
944
+ if (!Array.isArray(setNode) || setNode[0] !== 'local.set' || setNode.length !== 3) continue
945
+ if (!Array.isArray(getNode) || getNode[0] !== 'local.get' || getNode.length !== 2) continue
946
+ const name = setNode[1]
947
+ if (getNode[1] !== name || params.has(name)) continue
948
+ const uses = useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
949
+ // Only if there's more than just this set+get pair
950
+ if (uses.sets + uses.gets + uses.tees <= 2) continue
951
+ // Replace with local.tee (set+get combined)
952
+ funcNode.splice(i, 2, ['local.tee', name, clone(setNode[2])])
953
+ changed = true
954
+ }
955
+
956
+ return changed
957
+ }
958
+
959
+ /**
960
+ * Remove dead stores and unused local declarations in a reverse pass.
961
+ * Returns true if anything was removed.
962
+ * @param {Array} funcNode
963
+ * @param {Set<string>} params
964
+ * @param {Map<string,{gets:number,sets:number,tees:number}>} useCounts
965
+ */
966
+ const eliminateDeadStores = (funcNode, params, useCounts) => {
967
+ let changed = false
968
+ const getPostUseCount = name => useCounts.get(name) || { gets: 0, sets: 0, tees: 0 }
969
+
970
+ for (let i = funcNode.length - 1; i >= 1; i--) {
971
+ const sub = funcNode[i]
972
+ if (!Array.isArray(sub)) continue
973
+ const name = typeof sub[1] === 'string' ? sub[1] : null
974
+ if (!name || params.has(name)) continue
975
+ const uses = getPostUseCount(name)
976
+ // Dead store: set but never read, pure RHS
977
+ if (sub[0] === 'local.set' && uses.gets === 0 && uses.tees === 0 && isPure(sub[2])) {
978
+ funcNode.splice(i, 1); changed = true
979
+ }
980
+ // Unused local declaration
981
+ else if (sub[0] === 'local' && name[0] === '$' && uses.gets === 0 && uses.sets === 0 && uses.tees === 0) {
982
+ funcNode.splice(i, 1); changed = true
983
+ }
984
+ }
985
+
986
+ return changed
987
+ }
988
+
885
989
  /**
886
990
  * Propagate values through locals and eliminate single-use/dead locals.
887
991
  * Constants propagate to all uses; pure single-use exprs inline into get site.
@@ -897,73 +1001,15 @@ const propagate = (ast) => {
897
1001
  for (const sub of funcNode)
898
1002
  if (Array.isArray(sub) && sub[0] === 'param' && typeof sub[1] === 'string') params.add(sub[1])
899
1003
 
1004
+ // useCounts must be refreshed before every sub-pass: each mutation
1005
+ // (substitution, set/get pair removal, tee creation, dead-store removal)
1006
+ // changes the gets/sets/tees totals that downstream sub-passes rely on.
900
1007
  for (let pass = 0; pass < 4; pass++) {
901
1008
  let changed = false
902
- const uses = countLocalUses(funcNode)
903
- const getUses = name => uses.get(name) || { gets: 0, sets: 0, tees: 0 }
904
- const known = new Map()
905
-
906
- for (let i = 1; i < funcNode.length; i++) {
907
- const instr = funcNode[i]
908
- if (!Array.isArray(instr)) continue
909
- const op = instr[0]
910
-
911
- if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
912
-
913
- // Track local.set values
914
- if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
915
- substGets(instr[2], known) // substitute known values in RHS
916
- const u = getUses(instr[1])
917
- known.set(instr[1], {
918
- val: instr[2], pure: isPure(instr[2]),
919
- singleUse: u.gets <= 1 && u.sets <= 1 && u.tees === 0
920
- })
921
- continue
922
- }
923
-
924
- // Invalidate at control-flow boundaries
925
- if (op === 'block' || op === 'loop' || op === 'if') known.clear()
926
- // Calls only invalidate non-constant tracked values
927
- if (op === 'call' || op === 'call_indirect' || op === 'return_call' || op === 'return_call_indirect')
928
- for (const [k, v] of known) if (!getConst(v.val)) known.delete(k)
929
-
930
- // Substitute: standalone local.get (walkPost can't replace root)
931
- if (op === 'local.get' && instr.length === 2 && typeof instr[1] === 'string') {
932
- const k = known.get(instr[1])
933
- if (k && canSubst(k)) {
934
- const r = clone(k.val)
935
- instr.length = 0; instr.push(...(Array.isArray(r) ? r : [r]))
936
- changed = true; continue
937
- }
938
- }
939
-
940
- // Substitute nested local.gets (skip control-flow nodes — locals may be reassigned inside)
941
- if (op !== 'block' && op !== 'loop' && op !== 'if') {
942
- const prev = JSON.stringify(instr)
943
- substGets(instr, known)
944
- if (JSON.stringify(instr) !== prev) changed = true
945
- }
946
- }
947
-
948
- // Remove dead stores + unused local decls in one reverse pass
949
- const postUses = countLocalUses(funcNode)
950
- const pu = name => postUses.get(name) || { gets: 0, sets: 0, tees: 0 }
951
- for (let i = funcNode.length - 1; i >= 1; i--) {
952
- const sub = funcNode[i]
953
- if (!Array.isArray(sub)) continue
954
- const name = typeof sub[1] === 'string' ? sub[1] : null
955
- if (!name || params.has(name)) continue
956
- const u = pu(name)
957
- // Dead store: set but never read, pure RHS
958
- if (sub[0] === 'local.set' && u.gets === 0 && u.tees === 0 && isPure(sub[2])) {
959
- funcNode.splice(i, 1); changed = true
960
- }
961
- // Unused local declaration
962
- else if (sub[0] === 'local' && name[0] === '$' && u.gets === 0 && u.sets === 0 && u.tees === 0) {
963
- funcNode.splice(i, 1); changed = true
964
- }
965
- }
966
-
1009
+ if (forwardPropagate(funcNode, params, countLocalUses(funcNode))) changed = true
1010
+ if (eliminateSetGetPairs(funcNode, params, countLocalUses(funcNode))) changed = true
1011
+ if (createLocalTees(funcNode, params, countLocalUses(funcNode))) changed = true
1012
+ if (eliminateDeadStores(funcNode, params, countLocalUses(funcNode))) changed = true
967
1013
  if (!changed) break
968
1014
  }
969
1015
  })
@@ -1018,8 +1064,8 @@ const inline = (ast) => {
1018
1064
  }
1019
1065
  }
1020
1066
 
1021
- // Only inline: no locals, <= 2 params, single expression body, not exported
1022
- if (params && !hasLocals && !hasExport && params.length <= 2 && body.length === 1) {
1067
+ // Inline: no locals, <= 4 params, single expression body, not exported
1068
+ if (params && !hasLocals && !hasExport && params.length <= 4 && body.length === 1) {
1023
1069
  // Check if function mutates any of its params (local.set/tee on param)
1024
1070
  const paramNames = new Set(params.map(p => p.name))
1025
1071
  let mutatesParam = false
@@ -1068,135 +1114,915 @@ const inline = (ast) => {
1068
1114
  return result
1069
1115
  }
1070
1116
 
1071
- // ==================== COMMON SUBEXPRESSION ELIMINATION ====================
1117
+ // ==================== VACUUM ====================
1072
1118
 
1073
1119
  /**
1074
- * Hash an expression for comparison.
1075
- * @param {any} node
1076
- * @returns {string}
1120
+ * Remove no-op code: nops, drop of pure expressions, empty branches,
1121
+ * and select with identical arms.
1122
+ * @param {Array} ast
1123
+ * @returns {Array}
1077
1124
  */
1078
- const exprHash = (node) => JSON.stringify(node)
1125
+ const vacuum = (ast) => {
1126
+ return walkPost(clone(ast), (node) => {
1127
+ if (!Array.isArray(node)) return
1128
+ const op = node[0]
1129
+
1130
+ // Remove nop entirely (return array marker; parent or post-pass cleans it)
1131
+ if (op === 'nop') return ['nop']
1132
+
1133
+ // (drop PURE) → nop
1134
+ if (op === 'drop' && node.length === 2 && isPure(node[1])) {
1135
+ return ['nop']
1136
+ }
1137
+
1138
+ // (select x x cond) → x
1139
+ if (op === 'select' && node.length >= 4 && equal(node[1], node[2])) return node[1]
1140
+
1141
+ if (op === 'if') {
1142
+ const { cond, thenBranch, elseBranch } = parseIf(node)
1143
+ const thenEmpty = !thenBranch || thenBranch.length <= 1
1144
+ const elseEmpty = !elseBranch || elseBranch.length <= 1
1145
+
1146
+ // (if cond () ()) → nop or (drop cond)
1147
+ if (thenEmpty && elseEmpty) return isPure(cond) ? ['nop'] : ['drop', cond]
1148
+
1149
+ // (if cond (then X) (else)) → drop the empty else
1150
+ if (elseBranch && elseEmpty && !thenEmpty) {
1151
+ return node.filter(c => c !== elseBranch)
1152
+ }
1153
+ }
1154
+
1155
+ // Clean out nops, drop-of-pure sequences, and empty annotations from blocks
1156
+ if (op === 'func' || op === 'block' || op === 'loop' || op === 'then' || op === 'else') {
1157
+ const cleaned = [op]
1158
+ for (let i = 1; i < node.length; i++) {
1159
+ const child = node[i]
1160
+ if (child === 'nop' || (Array.isArray(child) && child[0] === 'nop')) continue
1161
+ // Pure expression followed by standalone drop → remove both
1162
+ const next = node[i + 1]
1163
+ const isDrop = next === 'drop' || (Array.isArray(next) && next[0] === 'drop' && next.length === 1)
1164
+ if (Array.isArray(child) && isPure(child) && isDrop) {
1165
+ i++ // skip the drop too
1166
+ continue
1167
+ }
1168
+ cleaned.push(child)
1169
+ }
1170
+ if (cleaned.length !== node.length) return cleaned
1171
+ }
1172
+ })
1173
+ }
1174
+
1175
+ // ==================== PEEPHOLE ====================
1176
+
1177
+ /** Peephole optimizations: simple algebraic identities */
1178
+ const PEEPHOLE = {
1179
+ // Self-cancelling / tautological binary ops
1180
+ 'i32.sub': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1181
+ 'i64.sub': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
1182
+ 'i32.xor': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1183
+ 'i64.xor': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
1184
+ 'i32.and': (a, b) => equal(a, b) ? a : null,
1185
+ 'i64.and': (a, b) => equal(a, b) ? a : null,
1186
+ 'i32.or': (a, b) => equal(a, b) ? a : null,
1187
+ 'i64.or': (a, b) => equal(a, b) ? a : null,
1188
+ 'i32.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1189
+ 'i64.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1190
+ 'i32.ne': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1191
+ 'i64.ne': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1192
+ 'i32.lt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1193
+ 'i32.lt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1194
+ 'i32.gt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1195
+ 'i32.gt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1196
+ 'i32.le_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1197
+ 'i32.le_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1198
+ 'i32.ge_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1199
+ 'i32.ge_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1200
+ 'i64.lt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1201
+ 'i64.lt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1202
+ 'i64.gt_s': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1203
+ 'i64.gt_u': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1204
+ 'i64.le_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1205
+ 'i64.le_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1206
+ 'i64.ge_s': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1207
+ 'i64.ge_u': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1208
+
1209
+ // Zero/all-bits absorption
1210
+ 'i32.mul': (a, b) => {
1211
+ const ca = getConst(a), cb = getConst(b)
1212
+ if (ca?.value === 0 || cb?.value === 0) return ['i32.const', 0]
1213
+ return null
1214
+ },
1215
+ 'i64.mul': (a, b) => {
1216
+ const ca = getConst(a), cb = getConst(b)
1217
+ if (ca?.value === 0n || cb?.value === 0n) return ['i64.const', 0n]
1218
+ return null
1219
+ },
1220
+ 'i32.and': (a, b) => {
1221
+ const ca = getConst(a), cb = getConst(b)
1222
+ if (ca?.value === 0 || cb?.value === 0) return ['i32.const', 0]
1223
+ // x & x → x handled above in self-operands, but null here lets that win
1224
+ return null
1225
+ },
1226
+ 'i64.and': (a, b) => {
1227
+ const ca = getConst(a), cb = getConst(b)
1228
+ if (ca?.value === 0n || cb?.value === 0n) return ['i64.const', 0n]
1229
+ return null
1230
+ },
1231
+ 'i32.or': (a, b) => {
1232
+ const ca = getConst(a), cb = getConst(b)
1233
+ if (ca?.value === -1 || cb?.value === -1) return ['i32.const', -1]
1234
+ return null
1235
+ },
1236
+ 'i64.or': (a, b) => {
1237
+ const ca = getConst(a), cb = getConst(b)
1238
+ if (ca?.value === -1n || cb?.value === -1n) return ['i64.const', -1n]
1239
+ return null
1240
+ },
1241
+
1242
+ // (local.set $x (local.get $x)) → nop
1243
+ 'local.set': (a, b) => Array.isArray(b) && b[0] === 'local.get' && b[1] === a ? ['nop'] : null,
1244
+ }
1245
+
1246
+ /**
1247
+ * Apply peephole optimizations.
1248
+ * @param {Array} ast
1249
+ * @returns {Array}
1250
+ */
1251
+ const peephole = (ast) => {
1252
+ return walkPost(clone(ast), (node) => {
1253
+ if (!Array.isArray(node) || node.length !== 3) return
1254
+ const fn = PEEPHOLE[node[0]]
1255
+ if (!fn) return
1256
+ const result = fn(node[1], node[2])
1257
+ if (result !== null) return result
1258
+ })
1259
+ }
1260
+
1261
+ // ==================== GLOBAL CONSTANT PROPAGATION ====================
1079
1262
 
1080
1263
  /**
1081
- * Eliminate common subexpressions by caching repeated computations.
1082
- * Limited to pure expressions within a function.
1264
+ * Replace global.get of immutable globals with their constant init values.
1083
1265
  * @param {Array} ast
1084
1266
  * @returns {Array}
1085
1267
  */
1086
- const cse = (ast) => {
1087
- // CSE is complex and can increase code size (extra locals)
1088
- // Simple version: detect and report, but actual elimination needs careful analysis
1089
- // For now, implement a basic version that works on adjacent identical expressions
1268
+ const globals = (ast) => {
1269
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1270
+ const result = clone(ast)
1271
+
1272
+ // Find immutable globals with const init
1273
+ const constGlobals = new Map() // name → const node
1274
+ const mutableGlobals = new Set()
1275
+
1276
+ for (const node of result.slice(1)) {
1277
+ if (!Array.isArray(node) || node[0] !== 'global') continue
1278
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1279
+ if (!name) continue
1280
+
1281
+ // Check mutability: (global $g (mut i32) init) vs (global $g i32 init)
1282
+ const hasName = typeof node[1] === 'string' && node[1][0] === '$'
1283
+ const initIdx = hasName ? 3 : 2
1284
+
1285
+ // Skip mutable globals
1286
+ const typeSlot = hasName ? node[2] : node[1]
1287
+ if (Array.isArray(typeSlot) && typeSlot[0] === 'mut') continue
1288
+
1289
+ const init = node[initIdx]
1290
+ if (getConst(init)) constGlobals.set(name, init)
1291
+ }
1292
+
1293
+ // Also mark any global that is ever written as mutable
1294
+ walk(result, (n) => {
1295
+ if (!Array.isArray(n) || n[0] !== 'global.set') return
1296
+ const ref = n[1]
1297
+ if (typeof ref === 'string' && ref[0] === '$') mutableGlobals.add(ref)
1298
+ })
1299
+
1300
+ // Remove mutable ones from propagation set
1301
+ for (const name of mutableGlobals) constGlobals.delete(name)
1302
+ if (constGlobals.size === 0) return result
1090
1303
 
1304
+ // Substitute global.get with const
1305
+ return walkPost(result, (node) => {
1306
+ if (!Array.isArray(node) || node[0] !== 'global.get' || node.length !== 2) return
1307
+ const ref = node[1]
1308
+ if (constGlobals.has(ref)) return clone(constGlobals.get(ref))
1309
+ })
1310
+ }
1311
+
1312
+ // ==================== LOAD/STORE OFFSET FOLDING ====================
1313
+
1314
+ /** Match (type.load/store (i32.add ptr (type.const N))) and fold offset */
1315
+ const offset = (ast) => {
1316
+ return walkPost(clone(ast), (node) => {
1317
+ if (!Array.isArray(node)) return
1318
+ const op = node[0]
1319
+ if (typeof op !== 'string' || (!op.endsWith('load') && !op.endsWith('store'))) return
1320
+
1321
+ // Memory ops have memarg as first immediate after optional memoryidx, then operands
1322
+ // In AST form from parse: (i32.load offset=4 align=8 ptr) or (i32.load ptr)
1323
+ // Store: (i32.store offset=4 ptr val) — ptr is second-to-last, val is last
1324
+ // Load: (i32.load offset=4 ptr) — ptr is last
1325
+ const isStore = op.endsWith('store')
1326
+
1327
+ // Find current offset from memparams
1328
+ let currentOffset = 0
1329
+ let memIdx = null
1330
+ let argStart = 1
1331
+
1332
+ // Check for memory index
1333
+ if (typeof node[1] === 'string' && (node[1][0] === '$' || !isNaN(node[1]))) {
1334
+ memIdx = node[1]
1335
+ argStart = 2
1336
+ }
1337
+
1338
+ // Check for memparams (offset=, align=)
1339
+ while (argStart < node.length && typeof node[argStart] === 'string' &&
1340
+ (node[argStart].startsWith('offset=') || node[argStart].startsWith('align='))) {
1341
+ if (node[argStart].startsWith('offset=')) {
1342
+ currentOffset = +node[argStart].slice(7)
1343
+ }
1344
+ argStart++
1345
+ }
1346
+
1347
+ // Determine pointer index
1348
+ const ptrIdx = isStore ? node.length - 2 : node.length - 1
1349
+ const valIdx = isStore ? node.length - 1 : -1
1350
+ if (ptrIdx < argStart) return
1351
+
1352
+ const ptr = node[ptrIdx]
1353
+ if (!Array.isArray(ptr) || ptr[0] !== 'i32.add' || ptr.length !== 3) return
1354
+
1355
+ const a = ptr[1], b = ptr[2]
1356
+ const ca = getConst(a), cb = getConst(b)
1357
+
1358
+ let base = null, addend = null
1359
+ if (ca && ca.type === 'i32') { addend = ca.value; base = b }
1360
+ else if (cb && cb.type === 'i32') { addend = cb.value; base = a }
1361
+ if (base === null || addend === null) return
1362
+
1363
+ const newOffset = currentOffset + addend
1364
+ const newNode = [op]
1365
+ if (memIdx !== null) newNode.push(memIdx)
1366
+ newNode.push(`offset=${newOffset}`)
1367
+ // Preserve align if present
1368
+ let alignParam = null
1369
+ for (let i = argStart; i < ptrIdx; i++) {
1370
+ if (typeof node[i] === 'string' && node[i].startsWith('align=')) {
1371
+ alignParam = node[i]
1372
+ }
1373
+ }
1374
+ if (alignParam) newNode.push(alignParam)
1375
+ newNode.push(base)
1376
+ if (isStore) newNode.push(node[valIdx])
1377
+ return newNode
1378
+ })
1379
+ }
1380
+
1381
+ // ==================== REDUNDANT BR REMOVAL ====================
1382
+
1383
+ /**
1384
+ * Remove br to a block's own label when it is the last instruction.
1385
+ * @param {Array} ast
1386
+ * @returns {Array}
1387
+ */
1388
+ const unbranch = (ast) => {
1091
1389
  const result = clone(ast)
1092
1390
 
1093
1391
  walk(result, (node) => {
1094
- if (!Array.isArray(node) || node[0] !== 'func') return
1392
+ if (!Array.isArray(node)) return
1393
+ const op = node[0]
1394
+ if (op !== 'block' && op !== 'loop') return
1395
+
1396
+ // Get the block's label
1397
+ let labelIdx = 1
1398
+ let label = null
1399
+ if (typeof node[1] === 'string' && node[1][0] === '$') {
1400
+ label = node[1]
1401
+ labelIdx = 2
1402
+ }
1403
+ if (!label) return
1404
+
1405
+ // Find the last executable instruction (skip result/type annotations)
1406
+ let lastIdx = -1
1407
+ for (let i = node.length - 1; i >= labelIdx; i--) {
1408
+ const child = node[i]
1409
+ if (!Array.isArray(child)) {
1410
+ if (child !== 'nop' && child !== 'end') lastIdx = i
1411
+ continue
1412
+ }
1413
+ const cop = child[0]
1414
+ if (cop === 'param' || cop === 'result' || cop === 'local' || cop === 'type' || cop === 'export') continue
1415
+ lastIdx = i
1416
+ break
1417
+ }
1418
+ if (lastIdx < 0) return
1419
+
1420
+ const last = node[lastIdx]
1421
+ if (Array.isArray(last) && last[0] === 'br' && last[1] === label) {
1422
+ node.splice(lastIdx, 1)
1423
+ }
1424
+ })
1425
+
1426
+ return result
1427
+ }
1428
+
1429
+ // ==================== STRIP MUT FROM GLOBALS ====================
1430
+
1431
+ /**
1432
+ * Strip mutability from globals that are never written.
1433
+ * Enables globals constant-propagation for more globals.
1434
+ * @param {Array} ast
1435
+ * @returns {Array}
1436
+ */
1437
+ const stripmut = (ast) => {
1438
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1439
+ const result = clone(ast)
1095
1440
 
1096
- // Find sequences of identical pure expressions
1097
- const seen = new Map() // hash → { node, count }
1441
+ const written = new Set()
1442
+ walk(result, (n) => {
1443
+ if (Array.isArray(n) && n[0] === 'global.set' && typeof n[1] === 'string') written.add(n[1])
1444
+ })
1445
+
1446
+ return walkPost(result, (node) => {
1447
+ if (!Array.isArray(node) || node[0] !== 'global') return
1448
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1449
+ if (!name || written.has(name)) return
1450
+
1451
+ const hasName = typeof node[1] === 'string' && node[1][0] === '$'
1452
+ const typeSlot = hasName ? node[2] : node[1]
1453
+ if (Array.isArray(typeSlot) && typeSlot[0] === 'mut') {
1454
+ const newNode = [...node]
1455
+ newNode[hasName ? 2 : 1] = typeSlot[1] // replace (mut T) with T
1456
+ return newNode
1457
+ }
1458
+ })
1459
+ }
1098
1460
 
1461
+ // ==================== IF-THEN-BR → BR_IF ====================
1462
+
1463
+ /**
1464
+ * Simplify (if cond (then (br $label))) → (br_if $label cond)
1465
+ * and (if cond (then) (else (br $label))) → (br_if $label (i32.eqz cond))
1466
+ * Only when the br is the sole instruction in the arm.
1467
+ * @param {Array} ast
1468
+ * @returns {Array}
1469
+ */
1470
+ const brif = (ast) => {
1471
+ return walkPost(clone(ast), (node) => {
1472
+ if (!Array.isArray(node) || node[0] !== 'if') return
1473
+ const { cond, thenBranch, elseBranch } = parseIf(node)
1474
+ const thenEmpty = !thenBranch || thenBranch.length <= 1
1475
+ const elseEmpty = !elseBranch || elseBranch.length <= 1
1476
+
1477
+ // (if cond (then (br $l))) → (br_if $l cond)
1478
+ if (!thenEmpty && elseEmpty && thenBranch.length === 2) {
1479
+ const t = thenBranch[1]
1480
+ if (Array.isArray(t) && t[0] === 'br' && t.length === 2) return ['br_if', t[1], cond]
1481
+ }
1482
+
1483
+ // (if cond (then) (else (br $l))) → (br_if $l (i32.eqz cond))
1484
+ if (thenEmpty && !elseEmpty && elseBranch.length === 2) {
1485
+ const e = elseBranch[1]
1486
+ if (Array.isArray(e) && e[0] === 'br' && e.length === 2) return ['br_if', e[1], ['i32.eqz', cond]]
1487
+ }
1488
+ })
1489
+ }
1490
+
1491
+ // ==================== MERGE IDENTICAL IF ARMS ====================
1492
+
1493
+ /**
1494
+ * Fold identical trailing code out of if/else arms.
1495
+ * (if cond (then A X) (else B X)) → (if cond (then A) (else B)) X
1496
+ * @param {Array} ast
1497
+ * @returns {Array}
1498
+ */
1499
+ const foldarms = (ast) => {
1500
+ return walkPost(clone(ast), (node) => {
1501
+ if (!Array.isArray(node) || node[0] !== 'if') return
1502
+ const { thenBranch, elseBranch } = parseIf(node)
1503
+ if (!thenBranch || !elseBranch) return
1504
+ if (thenBranch.length <= 1 || elseBranch.length <= 1) return
1505
+
1506
+ let common = 0
1507
+ const minLen = Math.min(thenBranch.length, elseBranch.length)
1508
+ for (let i = 1; i < minLen; i++) {
1509
+ if (!equal(thenBranch[thenBranch.length - i], elseBranch[elseBranch.length - i])) break
1510
+ common++
1511
+ }
1512
+ if (common === 0) return
1513
+
1514
+ const hoisted = thenBranch.slice(thenBranch.length - common)
1515
+ const newThen = thenBranch.slice(0, thenBranch.length - common)
1516
+ const newElse = elseBranch.slice(0, elseBranch.length - common)
1517
+
1518
+ const block = ['block']
1519
+ for (let i = 1; i < node.length; i++) {
1520
+ const c = node[i]
1521
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
1522
+ if (Array.isArray(c) && (c[0] === 'result' || c[0] === 'type')) block.push(c)
1523
+ }
1524
+
1525
+ const newIf = ['if']
1526
+ for (let i = 1; i < node.length; i++) {
1527
+ const c = node[i]
1528
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
1529
+ newIf.push(c)
1530
+ }
1531
+ newIf.push(newThen.length > 1 ? newThen : ['then'])
1532
+ newIf.push(newElse.length > 1 ? newElse : ['else'])
1533
+
1534
+ block.push(newIf, ...hoisted)
1535
+ return block
1536
+ })
1537
+ }
1538
+
1539
+ // ==================== DUPLICATE FUNCTION ELIMINATION ====================
1540
+
1541
+ /**
1542
+ * Fast structural hash for a function node, normalizing local names.
1543
+ * Uses a stack-based walk to avoid expensive JSON.stringify.
1544
+ */
1545
+ const hashFunc = (node, localNames) => {
1546
+ const parts = []
1547
+ const stack = [node]
1548
+ while (stack.length) {
1549
+ const v = stack.pop()
1550
+ if (Array.isArray(v)) {
1551
+ stack.push('|')
1552
+ for (let i = v.length - 1; i >= 0; i--) stack.push(v[i])
1553
+ stack.push('[')
1554
+ } else if (typeof v === 'string') {
1555
+ parts.push(localNames.has(v) ? '$__L' : v)
1556
+ } else if (typeof v === 'bigint') {
1557
+ parts.push(v.toString() + 'n')
1558
+ } else if (typeof v === 'number') {
1559
+ parts.push(v.toString())
1560
+ } else if (v === null) {
1561
+ parts.push('null')
1562
+ } else if (v === true) {
1563
+ parts.push('t')
1564
+ } else if (v === false) {
1565
+ parts.push('f')
1566
+ } else {
1567
+ parts.push(String(v))
1568
+ }
1569
+ }
1570
+ return parts.join(',')
1571
+ }
1572
+
1573
+ /**
1574
+ * Eliminate duplicate functions by hashing bodies.
1575
+ * Keeps the first occurrence and redirects all references to it.
1576
+ * @param {Array} ast
1577
+ * @returns {Array}
1578
+ */
1579
+ const dedupe = (ast) => {
1580
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1581
+ const result = clone(ast)
1582
+
1583
+ // Hash function bodies (normalize local/param names to avoid false negatives)
1584
+ const signatures = new Map() // hash → canonical $name
1585
+ const redirects = new Map() // duplicate $name → canonical $name
1586
+
1587
+ for (const node of result.slice(1)) {
1588
+ if (!Array.isArray(node) || node[0] !== 'func') continue
1589
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1590
+ if (!name) continue
1591
+
1592
+ // Collect names that are internal to this function: the func name itself,
1593
+ // its params, locals, and any block/loop labels nested in the body. All of
1594
+ // these get normalized to a single token in the hash so that two funcs
1595
+ // differing only in identifier choices still dedupe.
1596
+ const localNames = new Set()
1597
+ if (typeof node[1] === 'string' && node[1][0] === '$') localNames.add(node[1])
1099
1598
  walk(node, (n) => {
1100
- if (!Array.isArray(n)) return
1599
+ if (!Array.isArray(n) || typeof n[1] !== 'string' || n[1][0] !== '$') return
1101
1600
  const op = n[0]
1102
- // Only consider pure operations
1103
- if (!op || typeof op !== 'string') return
1104
- if (op.startsWith('i32.') || op.startsWith('i64.') || op.startsWith('f32.') || op.startsWith('f64.')) {
1105
- // Skip simple consts
1106
- if (op.endsWith('.const')) return
1107
- // Skip if has side effects (calls, memory ops)
1108
- let hasSideEffects = false
1109
- walk(n, (sub) => {
1110
- if (Array.isArray(sub) && (sub[0] === 'call' || sub[0]?.includes('load') || sub[0]?.includes('store'))) {
1111
- hasSideEffects = true
1112
- }
1113
- })
1114
- if (hasSideEffects) return
1115
-
1116
- const hash = exprHash(n)
1117
- if (seen.has(hash)) {
1118
- seen.get(hash).count++
1119
- } else {
1120
- seen.set(hash, { node: n, count: 1 })
1121
- }
1601
+ if (op === 'param' || op === 'local' || op === 'block' || op === 'loop' || op === 'if') {
1602
+ localNames.add(n[1])
1122
1603
  }
1123
1604
  })
1124
1605
 
1125
- // For now, just report - full CSE would require inserting locals
1126
- // which changes the function structure significantly
1606
+ const hash = hashFunc(node, localNames)
1607
+
1608
+ if (signatures.has(hash)) {
1609
+ redirects.set(name, signatures.get(hash))
1610
+ } else {
1611
+ signatures.set(hash, name)
1612
+ }
1613
+ }
1614
+
1615
+ if (redirects.size === 0) return result
1616
+
1617
+ // Rewrite all references: calls, ref.func, elem segments, call_indirect type
1618
+ walkPost(result, (node) => {
1619
+ if (!Array.isArray(node)) return
1620
+ const op = node[0]
1621
+ if ((op === 'call' || op === 'return_call') && redirects.has(node[1])) {
1622
+ return [op, redirects.get(node[1]), ...node.slice(2)]
1623
+ }
1624
+ if (op === 'ref.func' && redirects.has(node[1])) {
1625
+ return ['ref.func', redirects.get(node[1])]
1626
+ }
1627
+ if (op === 'elem') {
1628
+ const funcs = node[node.length - 1]
1629
+ if (Array.isArray(funcs)) {
1630
+ return [...node.slice(0, -1), funcs.map(f => redirects.get(f) || f)]
1631
+ }
1632
+ }
1633
+ if (op === 'call_indirect' && node.length >= 3) {
1634
+ const typeRef = node[1]
1635
+ if (typeof typeRef === 'string' && redirects.has(typeRef)) {
1636
+ return ['call_indirect', redirects.get(typeRef), ...node.slice(2)]
1637
+ }
1638
+ }
1127
1639
  })
1128
1640
 
1129
1641
  return result
1130
1642
  }
1131
1643
 
1132
- // ==================== LOOP INVARIANT HOISTING ====================
1644
+ // ==================== TYPE DEDUPLICATION ====================
1133
1645
 
1134
1646
  /**
1135
- * Hoist loop-invariant computations out of loops.
1647
+ * Merge structurally identical (type ...) definitions.
1648
+ * Keeps the first occurrence and redirects all references.
1136
1649
  * @param {Array} ast
1137
1650
  * @returns {Array}
1138
1651
  */
1139
- const hoist = (ast) => {
1652
+ const dedupTypes = (ast) => {
1653
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1140
1654
  const result = clone(ast)
1141
1655
 
1142
- walk(result, (node) => {
1143
- if (!Array.isArray(node) || node[0] !== 'func') return
1656
+ const signatures = new Map() // hash → canonical $name
1657
+ const redirects = new Map() // duplicate $name canonical $name
1144
1658
 
1145
- // Find loops
1146
- walk(node, (loopNode, parent, idx) => {
1147
- if (!Array.isArray(loopNode) || loopNode[0] !== 'loop') return
1659
+ for (const node of result.slice(1)) {
1660
+ if (!Array.isArray(node) || node[0] !== 'type') continue
1661
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1662
+ if (!name) continue
1148
1663
 
1149
- // Collect all locals modified in loop
1150
- const modifiedLocals = new Set()
1151
- walk(loopNode, (n) => {
1152
- if (!Array.isArray(n)) return
1153
- if (n[0] === 'local.set' || n[0] === 'local.tee') {
1154
- if (typeof n[1] === 'string') modifiedLocals.add(n[1])
1155
- }
1156
- })
1664
+ // Hash the type body, normalizing only the type's own name
1665
+ const hash = hashFunc(node, new Set([name]))
1157
1666
 
1158
- // Find invariant expressions (don't depend on modified locals or memory)
1159
- const invariants = []
1667
+ if (signatures.has(hash)) {
1668
+ redirects.set(name, signatures.get(hash))
1669
+ } else {
1670
+ signatures.set(hash, name)
1671
+ }
1672
+ }
1160
1673
 
1161
- for (let i = 1; i < loopNode.length; i++) {
1162
- const instr = loopNode[i]
1163
- if (!Array.isArray(instr)) continue
1674
+ if (redirects.size === 0) return result
1164
1675
 
1165
- const op = instr[0]
1166
- // Skip control flow
1167
- if (op === 'block' || op === 'loop' || op === 'if' || op === 'br' || op === 'br_if') continue
1676
+ // Remove duplicate type nodes
1677
+ for (let i = result.length - 1; i >= 0; i--) {
1678
+ const node = result[i]
1679
+ if (Array.isArray(node) && node[0] === 'type') {
1680
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1681
+ if (name && redirects.has(name)) result.splice(i, 1)
1682
+ }
1683
+ }
1168
1684
 
1169
- // Check if pure and invariant
1170
- let isInvariant = true
1171
- let isPure = true
1685
+ walkPost(result, (node) => {
1686
+ if (!Array.isArray(node)) return
1687
+ const op = node[0]
1172
1688
 
1173
- walk(instr, (n) => {
1174
- if (!Array.isArray(n)) return
1175
- const subOp = n[0]
1176
- // Side effects
1177
- if (subOp === 'call' || subOp === 'call_indirect' || subOp?.includes('store') || subOp?.includes('load')) {
1178
- isPure = false
1179
- }
1180
- // Depends on modified local
1181
- if (subOp === 'local.get' && typeof n[1] === 'string' && modifiedLocals.has(n[1])) {
1182
- isInvariant = false
1183
- }
1184
- })
1689
+ // (func $f (type $t) ...)
1690
+ if (op === 'func') {
1691
+ for (let i = 1; i < node.length; i++) {
1692
+ const sub = node[i]
1693
+ if (Array.isArray(sub) && sub[0] === 'type' && typeof sub[1] === 'string' && redirects.has(sub[1])) {
1694
+ node[i] = ['type', redirects.get(sub[1])]
1695
+ }
1696
+ }
1697
+ }
1185
1698
 
1186
- // Only hoist simple const expressions for safety
1187
- if (isPure && isInvariant && op?.endsWith('.const')) {
1188
- // Actually, consts are already cheap - skip
1699
+ // (import "m" "n" (func (type $t)))
1700
+ if (op === 'import') {
1701
+ for (let i = 1; i < node.length; i++) {
1702
+ const sub = node[i]
1703
+ if (Array.isArray(sub)) {
1704
+ for (let j = 1; j < sub.length; j++) {
1705
+ const inner = sub[j]
1706
+ if (Array.isArray(inner) && inner[0] === 'type' && typeof inner[1] === 'string' && redirects.has(inner[1])) {
1707
+ sub[j] = ['type', redirects.get(inner[1])]
1708
+ }
1709
+ }
1189
1710
  }
1190
1711
  }
1712
+ }
1191
1713
 
1192
- // Full hoisting would require inserting code before the loop
1193
- // This is complex and risky, so we keep it minimal
1194
- })
1714
+ // call_indirect $t or (call_indirect (type $t) ...)
1715
+ if (op === 'call_indirect' || op === 'return_call_indirect') {
1716
+ if (typeof node[1] === 'string' && redirects.has(node[1])) {
1717
+ return [op, redirects.get(node[1]), ...node.slice(2)]
1718
+ }
1719
+ if (Array.isArray(node[1]) && node[1][0] === 'type' && typeof node[1][1] === 'string' && redirects.has(node[1][1])) {
1720
+ return [op, ['type', redirects.get(node[1][1])], ...node.slice(2)]
1721
+ }
1722
+ }
1195
1723
  })
1196
1724
 
1197
1725
  return result
1198
1726
  }
1199
1727
 
1728
+ // ==================== DATA SEGMENT PACKING ====================
1729
+
1730
+ /** Parse a WAT data string literal into Uint8Array */
1731
+ const parseDataString = (str) => {
1732
+ if (typeof str !== 'string' || str.length < 2 || str[0] !== '"') return new Uint8Array()
1733
+ const inner = str.slice(1, -1)
1734
+ const bytes = []
1735
+ for (let i = 0; i < inner.length; i++) {
1736
+ if (inner[i] === '\\') {
1737
+ const next = inner[++i]
1738
+ if (next === 'x' || next === 'X') {
1739
+ bytes.push(parseInt(inner.slice(i + 1, i + 3), 16))
1740
+ i += 2
1741
+ } else if (/[0-9a-fA-F]/.test(next) && /[0-9a-fA-F]/.test(inner[i + 1])) {
1742
+ bytes.push(parseInt(inner.slice(i, i + 2), 16))
1743
+ i++
1744
+ } else if (next === 'n') bytes.push(10)
1745
+ else if (next === 't') bytes.push(9)
1746
+ else if (next === 'r') bytes.push(13)
1747
+ else if (next === '\\') bytes.push(92)
1748
+ else if (next === '"') bytes.push(34)
1749
+ else bytes.push(next.charCodeAt(0))
1750
+ } else {
1751
+ bytes.push(inner.charCodeAt(i))
1752
+ }
1753
+ }
1754
+ return new Uint8Array(bytes)
1755
+ }
1756
+
1757
+ /** Encode Uint8Array as WAT data string literal */
1758
+ const encodeDataString = (bytes) => {
1759
+ let str = '"'
1760
+ for (let i = 0; i < bytes.length; i++) {
1761
+ const b = bytes[i]
1762
+ if (b >= 32 && b < 127 && b !== 34 && b !== 92) {
1763
+ str += String.fromCharCode(b)
1764
+ } else {
1765
+ str += '\\' + b.toString(16).padStart(2, '0')
1766
+ }
1767
+ }
1768
+ return str + '"'
1769
+ }
1770
+
1771
+ /** Trim trailing zeros from data content items */
1772
+ const trimTrailingZeros = (items) => {
1773
+ const bytes = []
1774
+ for (const item of items) {
1775
+ if (typeof item === 'string') {
1776
+ bytes.push(...parseDataString(item))
1777
+ } else if (Array.isArray(item) && item[0] === 'i8') {
1778
+ for (let i = 1; i < item.length; i++) bytes.push(Number(item[i]) & 0xff)
1779
+ } else {
1780
+ return items // non-trimmable item
1781
+ }
1782
+ }
1783
+ let end = bytes.length
1784
+ while (end > 0 && bytes[end - 1] === 0) end--
1785
+ if (end === bytes.length) return items
1786
+ if (end === 0) return []
1787
+ return [encodeDataString(new Uint8Array(bytes.slice(0, end)))]
1788
+ }
1789
+
1790
+ /** Extract { memidx, offset } from an active data segment with constant offset */
1791
+ const getDataOffset = (node) => {
1792
+ let idx = 1
1793
+ if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
1794
+ if (Array.isArray(node[idx]) && node[idx][0] === 'memory') {
1795
+ const mem = node[idx][1]
1796
+ idx++
1797
+ const off = node[idx]
1798
+ if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
1799
+ return { memidx: mem, offset: Number(off[1]) }
1800
+ }
1801
+ return null
1802
+ }
1803
+ const off = node[idx]
1804
+ if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
1805
+ return { memidx: 0, offset: Number(off[1]) }
1806
+ }
1807
+ return null
1808
+ }
1809
+
1810
+ /** Get byte length of data segment content */
1811
+ const getDataLength = (node) => {
1812
+ let idx = 1
1813
+ if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
1814
+ if (Array.isArray(node[idx]) && node[idx][0] === 'memory') idx++
1815
+ if (Array.isArray(node[idx]) && typeof node[idx][0] === 'string' && !node[idx][0].startsWith('"')) idx++
1816
+ let len = 0
1817
+ for (let i = idx; i < node.length; i++) {
1818
+ const item = node[i]
1819
+ if (typeof item === 'string') len += parseDataString(item).length
1820
+ else if (Array.isArray(item) && item[0] === 'i8') len += item.length - 1
1821
+ else return null
1822
+ }
1823
+ return len
1824
+ }
1825
+
1826
+ /** Merge segment b into a (consecutive offsets, same memory) */
1827
+ const mergeDataSegments = (a, b) => {
1828
+ let aIdx = 1
1829
+ if (typeof a[aIdx] === 'string' && a[aIdx][0] === '$') aIdx++
1830
+ if (Array.isArray(a[aIdx]) && a[aIdx][0] === 'memory') aIdx++
1831
+ if (Array.isArray(a[aIdx]) && typeof a[aIdx][0] === 'string' && !a[aIdx][0].startsWith('"')) aIdx++
1832
+
1833
+ let bIdx = 1
1834
+ if (typeof b[bIdx] === 'string' && b[bIdx][0] === '$') bIdx++
1835
+ if (Array.isArray(b[bIdx]) && b[bIdx][0] === 'memory') bIdx++
1836
+ if (Array.isArray(b[bIdx]) && typeof b[bIdx][0] === 'string' && !b[bIdx][0].startsWith('"')) bIdx++
1837
+
1838
+ const aContent = a.slice(aIdx)
1839
+ const bContent = b.slice(bIdx)
1840
+
1841
+ if (aContent.length === 1 && bContent.length === 1 &&
1842
+ typeof aContent[0] === 'string' && typeof bContent[0] === 'string') {
1843
+ const aBytes = parseDataString(aContent[0])
1844
+ const bBytes = parseDataString(bContent[0])
1845
+ const merged = new Uint8Array(aBytes.length + bBytes.length)
1846
+ merged.set(aBytes)
1847
+ merged.set(bBytes, aBytes.length)
1848
+ a.length = aIdx
1849
+ a.push(encodeDataString(merged))
1850
+ return true
1851
+ }
1852
+
1853
+ a.length = aIdx
1854
+ a.push(...aContent, ...bContent)
1855
+ return true
1856
+ }
1857
+
1858
+ /**
1859
+ * Pack data segments: trim trailing zeros and merge adjacent constant-offset segments.
1860
+ * @param {Array} ast
1861
+ * @returns {Array}
1862
+ */
1863
+ const packData = (ast) => {
1864
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1865
+ let result = clone(ast)
1866
+
1867
+ // Trim trailing zeros
1868
+ for (const node of result) {
1869
+ if (!Array.isArray(node) || node[0] !== 'data') continue
1870
+ let contentStart = 1
1871
+ if (typeof node[1] === 'string' && node[1][0] === '$') contentStart = 2
1872
+ if (contentStart < node.length && Array.isArray(node[contentStart]) &&
1873
+ typeof node[contentStart][0] === 'string' && !node[contentStart][0].startsWith('"')) {
1874
+ contentStart++
1875
+ }
1876
+ const content = node.slice(contentStart)
1877
+ if (content.length === 0) continue
1878
+ const trimmed = trimTrailingZeros(content)
1879
+ if (trimmed.length !== content.length || (trimmed.length > 0 && trimmed[0] !== content[0])) {
1880
+ node.length = contentStart
1881
+ node.push(...trimmed)
1882
+ }
1883
+ }
1884
+
1885
+ // Merge adjacent active segments with same memory and consecutive offsets
1886
+ const dataNodes = []
1887
+ for (let i = 0; i < result.length; i++) {
1888
+ const node = result[i]
1889
+ if (Array.isArray(node) && node[0] === 'data') {
1890
+ const info = getDataOffset(node)
1891
+ if (info) {
1892
+ const len = getDataLength(node)
1893
+ if (len !== null) dataNodes.push({ ...info, node, index: i, len })
1894
+ }
1895
+ }
1896
+ }
1897
+
1898
+ dataNodes.sort((a, b) => {
1899
+ const ma = String(a.memidx), mb = String(b.memidx)
1900
+ if (ma !== mb) return ma.localeCompare(mb)
1901
+ return a.offset - b.offset
1902
+ })
1903
+
1904
+ const toRemove = new Set()
1905
+ for (let i = 0; i < dataNodes.length - 1; i++) {
1906
+ const a = dataNodes[i]
1907
+ const b = dataNodes[i + 1]
1908
+ if (toRemove.has(a.index) || String(a.memidx) !== String(b.memidx)) continue
1909
+ if (a.offset + a.len !== b.offset) continue
1910
+ if (mergeDataSegments(a.node, b.node)) {
1911
+ toRemove.add(b.index)
1912
+ a.len = getDataLength(a.node)
1913
+ }
1914
+ }
1915
+
1916
+ if (toRemove.size > 0) {
1917
+ result = result.filter((_, i) => !toRemove.has(i))
1918
+ }
1919
+
1920
+ return result
1921
+ }
1922
+
1923
+ // ==================== IMPORT FIELD MINIFICATION ====================
1924
+
1925
+ /** Create a shortener that maps names to a, b, ..., z, aa, ab, ... */
1926
+ const makeShortener = () => {
1927
+ const map = new Map()
1928
+ let n = 0
1929
+ return (name) => {
1930
+ if (!map.has(name)) {
1931
+ let id = '', x = n++
1932
+ do {
1933
+ id = String.fromCharCode(97 + (x % 26)) + id
1934
+ x = Math.floor(x / 26) - 1
1935
+ } while (x >= 0)
1936
+ map.set(name, id || 'a')
1937
+ }
1938
+ return map.get(name)
1939
+ }
1940
+ }
1941
+
1942
+ /**
1943
+ * Minify import module and field names for smaller binaries.
1944
+ * Only safe when you control the host environment.
1945
+ * @param {Array} ast
1946
+ * @returns {Array}
1947
+ */
1948
+ const minifyImports = (ast) => {
1949
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1950
+ const result = clone(ast)
1951
+ const shortMod = makeShortener()
1952
+ const shortField = makeShortener()
1953
+
1954
+ for (const node of result) {
1955
+ if (!Array.isArray(node) || node[0] !== 'import') continue
1956
+ if (typeof node[1] === 'string' && node[1][0] === '"') {
1957
+ node[1] = '"' + shortMod(node[1].slice(1, -1)) + '"'
1958
+ }
1959
+ if (typeof node[2] === 'string' && node[2][0] === '"') {
1960
+ node[2] = '"' + shortField(node[2].slice(1, -1)) + '"'
1961
+ }
1962
+ }
1963
+
1964
+ return result
1965
+ }
1966
+
1967
+ // ==================== REORDER FUNCTIONS ====================
1968
+
1969
+ /**
1970
+ * Count direct calls and sort functions so hot ones come first.
1971
+ * Smaller LEB128 indices for frequent calls reduce binary size.
1972
+ * Imports must stay before defined functions to preserve the index space.
1973
+ * @param {Array} ast
1974
+ * @returns {Array}
1975
+ */
1976
+ /** True iff every defined func has a $name and every func reference is by $name */
1977
+ const reorderSafe = (ast) => {
1978
+ let safe = true
1979
+ walk(ast, (n) => {
1980
+ if (!safe || !Array.isArray(n)) return
1981
+ const op = n[0]
1982
+ if (op === 'func' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1983
+ else if ((op === 'call' || op === 'return_call' || op === 'ref.func') &&
1984
+ (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1985
+ else if (op === 'start' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1986
+ else if (op === 'elem') {
1987
+ // Numeric func indices in elem segments would break too
1988
+ for (const sub of n) {
1989
+ if (typeof sub === 'string' && sub[0] !== '$' && /^\d/.test(sub)) { safe = false; break }
1990
+ if (Array.isArray(sub) && sub[0] === 'ref.func' &&
1991
+ (typeof sub[1] !== 'string' || sub[1][0] !== '$')) { safe = false; break }
1992
+ }
1993
+ }
1994
+ })
1995
+ return safe
1996
+ }
1997
+
1998
+ const reorder = (ast) => {
1999
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
2000
+ // Sorting changes the function index space. Skip if any reference is numeric,
2001
+ // since we'd silently retarget unnamed callers/start/elem entries.
2002
+ if (!reorderSafe(ast)) return ast
2003
+ const result = clone(ast)
2004
+
2005
+ const callCounts = new Map()
2006
+ walk(result, (n) => {
2007
+ if (!Array.isArray(n)) return
2008
+ if (n[0] === 'call' || n[0] === 'return_call') {
2009
+ callCounts.set(n[1], (callCounts.get(n[1]) || 0) + 1)
2010
+ }
2011
+ })
2012
+
2013
+ // Imports must precede defined funcs (compile.js assigns indices in AST order).
2014
+ const imports = [], funcs = [], others = []
2015
+ for (const node of result.slice(1)) {
2016
+ if (!Array.isArray(node)) { others.push(node); continue }
2017
+ if (node[0] === 'import') imports.push(node)
2018
+ else if (node[0] === 'func') funcs.push(node)
2019
+ else others.push(node)
2020
+ }
2021
+
2022
+ funcs.sort((a, b) => (callCounts.get(b[1]) || 0) - (callCounts.get(a[1]) || 0))
2023
+ return ['module', ...imports, ...funcs, ...others]
2024
+ }
2025
+
1200
2026
  // ==================== MAIN ====================
1201
2027
 
1202
2028
  /**
@@ -1216,17 +2042,38 @@ export default function optimize(ast, opts = true) {
1216
2042
  ast = clone(ast)
1217
2043
  opts = normalize(opts)
1218
2044
 
1219
- if (opts.fold) ast = fold(ast)
1220
- if (opts.identity) ast = identity(ast)
1221
- if (opts.strength) ast = strength(ast)
1222
- if (opts.branch) ast = branch(ast)
1223
- if (opts.propagate) ast = propagate(ast)
1224
- if (opts.inline) ast = inline(ast)
1225
- if (opts.deadcode) ast = deadcode(ast)
1226
- if (opts.locals) ast = localReuse(ast)
1227
- if (opts.treeshake) ast = treeshake(ast)
2045
+ // Each pass clones its input before mutating, so the original `before`
2046
+ // reference stays untouched and can be used for the convergence check
2047
+ // without an extra deep clone.
2048
+ for (let round = 0; round < 6; round++) {
2049
+ const before = ast
2050
+
2051
+ if (opts.stripmut) ast = stripmut(ast)
2052
+ if (opts.globals) ast = globals(ast)
2053
+ if (opts.fold) ast = fold(ast)
2054
+ if (opts.identity) ast = identity(ast)
2055
+ if (opts.peephole) ast = peephole(ast)
2056
+ if (opts.strength) ast = strength(ast)
2057
+ if (opts.branch) ast = branch(ast)
2058
+ if (opts.propagate) ast = propagate(ast)
2059
+ if (opts.inline) ast = inline(ast)
2060
+ if (opts.offset) ast = offset(ast)
2061
+ if (opts.unbranch) ast = unbranch(ast)
2062
+ if (opts.brif) ast = brif(ast)
2063
+ if (opts.foldarms) ast = foldarms(ast)
2064
+ if (opts.deadcode) ast = deadcode(ast)
2065
+ if (opts.vacuum) ast = vacuum(ast)
2066
+ if (opts.locals) ast = localReuse(ast)
2067
+ if (opts.dedupe) ast = dedupe(ast)
2068
+ if (opts.dedupTypes) ast = dedupTypes(ast)
2069
+ if (opts.packData) ast = packData(ast)
2070
+ if (opts.reorder) ast = reorder(ast)
2071
+ if (opts.treeshake) ast = treeshake(ast)
2072
+ if (opts.minifyImports) ast = minifyImports(ast)
2073
+ if (equal(before, ast)) break
2074
+ }
1228
2075
 
1229
2076
  return ast
1230
2077
  }
1231
2078
 
1232
- export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS }
2079
+ export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS, vacuum, peephole, globals, offset, unbranch, stripmut, brif, foldarms, dedupe, reorder, dedupTypes, packData, minifyImports }