watr 4.3.4 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,921 @@ 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
+ // Only fold when the if has an explicit result type.
1507
+ // Without a result annotation the branches are void; hoisting a suffix
1508
+ // like `drop` can expose a value and leave the if branches ill-typed.
1509
+ const hasResult = node.some(c => Array.isArray(c) && c[0] === 'result')
1510
+ if (!hasResult) return
1511
+
1512
+ let common = 0
1513
+ const minLen = Math.min(thenBranch.length, elseBranch.length)
1514
+ for (let i = 1; i < minLen; i++) {
1515
+ if (!equal(thenBranch[thenBranch.length - i], elseBranch[elseBranch.length - i])) break
1516
+ common++
1517
+ }
1518
+ if (common === 0) return
1519
+
1520
+ const hoisted = thenBranch.slice(thenBranch.length - common)
1521
+ const newThen = thenBranch.slice(0, thenBranch.length - common)
1522
+ const newElse = elseBranch.slice(0, elseBranch.length - common)
1523
+
1524
+ const block = ['block']
1525
+ for (let i = 1; i < node.length; i++) {
1526
+ const c = node[i]
1527
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
1528
+ if (Array.isArray(c) && (c[0] === 'result' || c[0] === 'type')) block.push(c)
1529
+ }
1530
+
1531
+ const newIf = ['if']
1532
+ for (let i = 1; i < node.length; i++) {
1533
+ const c = node[i]
1534
+ if (Array.isArray(c) && (c[0] === 'then' || c[0] === 'else')) break
1535
+ newIf.push(c)
1536
+ }
1537
+ newIf.push(newThen.length > 1 ? newThen : ['then'])
1538
+ newIf.push(newElse.length > 1 ? newElse : ['else'])
1539
+
1540
+ block.push(newIf, ...hoisted)
1541
+ return block
1542
+ })
1543
+ }
1544
+
1545
+ // ==================== DUPLICATE FUNCTION ELIMINATION ====================
1546
+
1547
+ /**
1548
+ * Fast structural hash for a function node, normalizing local names.
1549
+ * Uses a stack-based walk to avoid expensive JSON.stringify.
1550
+ */
1551
+ const hashFunc = (node, localNames) => {
1552
+ const parts = []
1553
+ const stack = [node]
1554
+ while (stack.length) {
1555
+ const v = stack.pop()
1556
+ if (Array.isArray(v)) {
1557
+ stack.push('|')
1558
+ for (let i = v.length - 1; i >= 0; i--) stack.push(v[i])
1559
+ stack.push('[')
1560
+ } else if (typeof v === 'string') {
1561
+ parts.push(localNames.has(v) ? '$__L' : v)
1562
+ } else if (typeof v === 'bigint') {
1563
+ parts.push(v.toString() + 'n')
1564
+ } else if (typeof v === 'number') {
1565
+ parts.push(v.toString())
1566
+ } else if (v === null) {
1567
+ parts.push('null')
1568
+ } else if (v === true) {
1569
+ parts.push('t')
1570
+ } else if (v === false) {
1571
+ parts.push('f')
1572
+ } else {
1573
+ parts.push(String(v))
1574
+ }
1575
+ }
1576
+ return parts.join(',')
1577
+ }
1578
+
1579
+ /**
1580
+ * Eliminate duplicate functions by hashing bodies.
1581
+ * Keeps the first occurrence and redirects all references to it.
1582
+ * @param {Array} ast
1583
+ * @returns {Array}
1584
+ */
1585
+ const dedupe = (ast) => {
1586
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1587
+ const result = clone(ast)
1588
+
1589
+ // Hash function bodies (normalize local/param names to avoid false negatives)
1590
+ const signatures = new Map() // hash → canonical $name
1591
+ const redirects = new Map() // duplicate $name → canonical $name
1592
+
1593
+ for (const node of result.slice(1)) {
1594
+ if (!Array.isArray(node) || node[0] !== 'func') continue
1595
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1596
+ if (!name) continue
1597
+
1598
+ // Collect names that are internal to this function: the func name itself,
1599
+ // its params, locals, and any block/loop labels nested in the body. All of
1600
+ // these get normalized to a single token in the hash so that two funcs
1601
+ // differing only in identifier choices still dedupe.
1602
+ const localNames = new Set()
1603
+ if (typeof node[1] === 'string' && node[1][0] === '$') localNames.add(node[1])
1099
1604
  walk(node, (n) => {
1100
- if (!Array.isArray(n)) return
1605
+ if (!Array.isArray(n) || typeof n[1] !== 'string' || n[1][0] !== '$') return
1101
1606
  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
- }
1607
+ if (op === 'param' || op === 'local' || op === 'block' || op === 'loop' || op === 'if') {
1608
+ localNames.add(n[1])
1122
1609
  }
1123
1610
  })
1124
1611
 
1125
- // For now, just report - full CSE would require inserting locals
1126
- // which changes the function structure significantly
1612
+ const hash = hashFunc(node, localNames)
1613
+
1614
+ if (signatures.has(hash)) {
1615
+ redirects.set(name, signatures.get(hash))
1616
+ } else {
1617
+ signatures.set(hash, name)
1618
+ }
1619
+ }
1620
+
1621
+ if (redirects.size === 0) return result
1622
+
1623
+ // Rewrite all references: calls, ref.func, elem segments, call_indirect type
1624
+ walkPost(result, (node) => {
1625
+ if (!Array.isArray(node)) return
1626
+ const op = node[0]
1627
+ if ((op === 'call' || op === 'return_call') && redirects.has(node[1])) {
1628
+ return [op, redirects.get(node[1]), ...node.slice(2)]
1629
+ }
1630
+ if (op === 'ref.func' && redirects.has(node[1])) {
1631
+ return ['ref.func', redirects.get(node[1])]
1632
+ }
1633
+ if (op === 'elem') {
1634
+ const funcs = node[node.length - 1]
1635
+ if (Array.isArray(funcs)) {
1636
+ return [...node.slice(0, -1), funcs.map(f => redirects.get(f) || f)]
1637
+ }
1638
+ }
1639
+ if (op === 'call_indirect' && node.length >= 3) {
1640
+ const typeRef = node[1]
1641
+ if (typeof typeRef === 'string' && redirects.has(typeRef)) {
1642
+ return ['call_indirect', redirects.get(typeRef), ...node.slice(2)]
1643
+ }
1644
+ }
1127
1645
  })
1128
1646
 
1129
1647
  return result
1130
1648
  }
1131
1649
 
1132
- // ==================== LOOP INVARIANT HOISTING ====================
1650
+ // ==================== TYPE DEDUPLICATION ====================
1133
1651
 
1134
1652
  /**
1135
- * Hoist loop-invariant computations out of loops.
1653
+ * Merge structurally identical (type ...) definitions.
1654
+ * Keeps the first occurrence and redirects all references.
1136
1655
  * @param {Array} ast
1137
1656
  * @returns {Array}
1138
1657
  */
1139
- const hoist = (ast) => {
1658
+ const dedupTypes = (ast) => {
1659
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1140
1660
  const result = clone(ast)
1141
1661
 
1142
- walk(result, (node) => {
1143
- if (!Array.isArray(node) || node[0] !== 'func') return
1662
+ const signatures = new Map() // hash → canonical $name
1663
+ const redirects = new Map() // duplicate $name canonical $name
1144
1664
 
1145
- // Find loops
1146
- walk(node, (loopNode, parent, idx) => {
1147
- if (!Array.isArray(loopNode) || loopNode[0] !== 'loop') return
1665
+ for (const node of result.slice(1)) {
1666
+ if (!Array.isArray(node) || node[0] !== 'type') continue
1667
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1668
+ if (!name) continue
1148
1669
 
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
- })
1670
+ // Hash the type body, normalizing only the type's own name
1671
+ const hash = hashFunc(node, new Set([name]))
1157
1672
 
1158
- // Find invariant expressions (don't depend on modified locals or memory)
1159
- const invariants = []
1673
+ if (signatures.has(hash)) {
1674
+ redirects.set(name, signatures.get(hash))
1675
+ } else {
1676
+ signatures.set(hash, name)
1677
+ }
1678
+ }
1160
1679
 
1161
- for (let i = 1; i < loopNode.length; i++) {
1162
- const instr = loopNode[i]
1163
- if (!Array.isArray(instr)) continue
1680
+ if (redirects.size === 0) return result
1164
1681
 
1165
- const op = instr[0]
1166
- // Skip control flow
1167
- if (op === 'block' || op === 'loop' || op === 'if' || op === 'br' || op === 'br_if') continue
1682
+ // Remove duplicate type nodes
1683
+ for (let i = result.length - 1; i >= 0; i--) {
1684
+ const node = result[i]
1685
+ if (Array.isArray(node) && node[0] === 'type') {
1686
+ const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1687
+ if (name && redirects.has(name)) result.splice(i, 1)
1688
+ }
1689
+ }
1168
1690
 
1169
- // Check if pure and invariant
1170
- let isInvariant = true
1171
- let isPure = true
1691
+ walkPost(result, (node) => {
1692
+ if (!Array.isArray(node)) return
1693
+ const op = node[0]
1172
1694
 
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
- })
1695
+ // (func $f (type $t) ...)
1696
+ if (op === 'func') {
1697
+ for (let i = 1; i < node.length; i++) {
1698
+ const sub = node[i]
1699
+ if (Array.isArray(sub) && sub[0] === 'type' && typeof sub[1] === 'string' && redirects.has(sub[1])) {
1700
+ node[i] = ['type', redirects.get(sub[1])]
1701
+ }
1702
+ }
1703
+ }
1185
1704
 
1186
- // Only hoist simple const expressions for safety
1187
- if (isPure && isInvariant && op?.endsWith('.const')) {
1188
- // Actually, consts are already cheap - skip
1705
+ // (import "m" "n" (func (type $t)))
1706
+ if (op === 'import') {
1707
+ for (let i = 1; i < node.length; i++) {
1708
+ const sub = node[i]
1709
+ if (Array.isArray(sub)) {
1710
+ for (let j = 1; j < sub.length; j++) {
1711
+ const inner = sub[j]
1712
+ if (Array.isArray(inner) && inner[0] === 'type' && typeof inner[1] === 'string' && redirects.has(inner[1])) {
1713
+ sub[j] = ['type', redirects.get(inner[1])]
1714
+ }
1715
+ }
1189
1716
  }
1190
1717
  }
1718
+ }
1191
1719
 
1192
- // Full hoisting would require inserting code before the loop
1193
- // This is complex and risky, so we keep it minimal
1194
- })
1720
+ // call_indirect $t or (call_indirect (type $t) ...)
1721
+ if (op === 'call_indirect' || op === 'return_call_indirect') {
1722
+ if (typeof node[1] === 'string' && redirects.has(node[1])) {
1723
+ return [op, redirects.get(node[1]), ...node.slice(2)]
1724
+ }
1725
+ if (Array.isArray(node[1]) && node[1][0] === 'type' && typeof node[1][1] === 'string' && redirects.has(node[1][1])) {
1726
+ return [op, ['type', redirects.get(node[1][1])], ...node.slice(2)]
1727
+ }
1728
+ }
1195
1729
  })
1196
1730
 
1197
1731
  return result
1198
1732
  }
1199
1733
 
1734
+ // ==================== DATA SEGMENT PACKING ====================
1735
+
1736
+ /** Parse a WAT data string literal into Uint8Array */
1737
+ const parseDataString = (str) => {
1738
+ if (typeof str !== 'string' || str.length < 2 || str[0] !== '"') return new Uint8Array()
1739
+ const inner = str.slice(1, -1)
1740
+ const bytes = []
1741
+ for (let i = 0; i < inner.length; i++) {
1742
+ if (inner[i] === '\\') {
1743
+ const next = inner[++i]
1744
+ if (next === 'x' || next === 'X') {
1745
+ bytes.push(parseInt(inner.slice(i + 1, i + 3), 16))
1746
+ i += 2
1747
+ } else if (/[0-9a-fA-F]/.test(next) && /[0-9a-fA-F]/.test(inner[i + 1])) {
1748
+ bytes.push(parseInt(inner.slice(i, i + 2), 16))
1749
+ i++
1750
+ } else if (next === 'n') bytes.push(10)
1751
+ else if (next === 't') bytes.push(9)
1752
+ else if (next === 'r') bytes.push(13)
1753
+ else if (next === '\\') bytes.push(92)
1754
+ else if (next === '"') bytes.push(34)
1755
+ else bytes.push(next.charCodeAt(0))
1756
+ } else {
1757
+ bytes.push(inner.charCodeAt(i))
1758
+ }
1759
+ }
1760
+ return new Uint8Array(bytes)
1761
+ }
1762
+
1763
+ /** Encode Uint8Array as WAT data string literal */
1764
+ const encodeDataString = (bytes) => {
1765
+ let str = '"'
1766
+ for (let i = 0; i < bytes.length; i++) {
1767
+ const b = bytes[i]
1768
+ if (b >= 32 && b < 127 && b !== 34 && b !== 92) {
1769
+ str += String.fromCharCode(b)
1770
+ } else {
1771
+ str += '\\' + b.toString(16).padStart(2, '0')
1772
+ }
1773
+ }
1774
+ return str + '"'
1775
+ }
1776
+
1777
+ /** Trim trailing zeros from data content items */
1778
+ const trimTrailingZeros = (items) => {
1779
+ const bytes = []
1780
+ for (const item of items) {
1781
+ if (typeof item === 'string') {
1782
+ bytes.push(...parseDataString(item))
1783
+ } else if (Array.isArray(item) && item[0] === 'i8') {
1784
+ for (let i = 1; i < item.length; i++) bytes.push(Number(item[i]) & 0xff)
1785
+ } else {
1786
+ return items // non-trimmable item
1787
+ }
1788
+ }
1789
+ let end = bytes.length
1790
+ while (end > 0 && bytes[end - 1] === 0) end--
1791
+ if (end === bytes.length) return items
1792
+ if (end === 0) return []
1793
+ return [encodeDataString(new Uint8Array(bytes.slice(0, end)))]
1794
+ }
1795
+
1796
+ /** Extract { memidx, offset } from an active data segment with constant offset */
1797
+ const getDataOffset = (node) => {
1798
+ let idx = 1
1799
+ if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
1800
+ if (Array.isArray(node[idx]) && node[idx][0] === 'memory') {
1801
+ const mem = node[idx][1]
1802
+ idx++
1803
+ const off = node[idx]
1804
+ if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
1805
+ return { memidx: mem, offset: Number(off[1]) }
1806
+ }
1807
+ return null
1808
+ }
1809
+ const off = node[idx]
1810
+ if (Array.isArray(off) && (off[0] === 'i32.const' || off[0] === 'i64.const')) {
1811
+ return { memidx: 0, offset: Number(off[1]) }
1812
+ }
1813
+ return null
1814
+ }
1815
+
1816
+ /** Get byte length of data segment content */
1817
+ const getDataLength = (node) => {
1818
+ let idx = 1
1819
+ if (typeof node[idx] === 'string' && node[idx][0] === '$') idx++
1820
+ if (Array.isArray(node[idx]) && node[idx][0] === 'memory') idx++
1821
+ if (Array.isArray(node[idx]) && typeof node[idx][0] === 'string' && !node[idx][0].startsWith('"')) idx++
1822
+ let len = 0
1823
+ for (let i = idx; i < node.length; i++) {
1824
+ const item = node[i]
1825
+ if (typeof item === 'string') len += parseDataString(item).length
1826
+ else if (Array.isArray(item) && item[0] === 'i8') len += item.length - 1
1827
+ else return null
1828
+ }
1829
+ return len
1830
+ }
1831
+
1832
+ /** Merge segment b into a (consecutive offsets, same memory) */
1833
+ const mergeDataSegments = (a, b) => {
1834
+ let aIdx = 1
1835
+ if (typeof a[aIdx] === 'string' && a[aIdx][0] === '$') aIdx++
1836
+ if (Array.isArray(a[aIdx]) && a[aIdx][0] === 'memory') aIdx++
1837
+ if (Array.isArray(a[aIdx]) && typeof a[aIdx][0] === 'string' && !a[aIdx][0].startsWith('"')) aIdx++
1838
+
1839
+ let bIdx = 1
1840
+ if (typeof b[bIdx] === 'string' && b[bIdx][0] === '$') bIdx++
1841
+ if (Array.isArray(b[bIdx]) && b[bIdx][0] === 'memory') bIdx++
1842
+ if (Array.isArray(b[bIdx]) && typeof b[bIdx][0] === 'string' && !b[bIdx][0].startsWith('"')) bIdx++
1843
+
1844
+ const aContent = a.slice(aIdx)
1845
+ const bContent = b.slice(bIdx)
1846
+
1847
+ if (aContent.length === 1 && bContent.length === 1 &&
1848
+ typeof aContent[0] === 'string' && typeof bContent[0] === 'string') {
1849
+ const aBytes = parseDataString(aContent[0])
1850
+ const bBytes = parseDataString(bContent[0])
1851
+ const merged = new Uint8Array(aBytes.length + bBytes.length)
1852
+ merged.set(aBytes)
1853
+ merged.set(bBytes, aBytes.length)
1854
+ a.length = aIdx
1855
+ a.push(encodeDataString(merged))
1856
+ return true
1857
+ }
1858
+
1859
+ a.length = aIdx
1860
+ a.push(...aContent, ...bContent)
1861
+ return true
1862
+ }
1863
+
1864
+ /**
1865
+ * Pack data segments: trim trailing zeros and merge adjacent constant-offset segments.
1866
+ * @param {Array} ast
1867
+ * @returns {Array}
1868
+ */
1869
+ const packData = (ast) => {
1870
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1871
+ let result = clone(ast)
1872
+
1873
+ // Trim trailing zeros
1874
+ for (const node of result) {
1875
+ if (!Array.isArray(node) || node[0] !== 'data') continue
1876
+ let contentStart = 1
1877
+ if (typeof node[1] === 'string' && node[1][0] === '$') contentStart = 2
1878
+ if (contentStart < node.length && Array.isArray(node[contentStart]) &&
1879
+ typeof node[contentStart][0] === 'string' && !node[contentStart][0].startsWith('"')) {
1880
+ contentStart++
1881
+ }
1882
+ const content = node.slice(contentStart)
1883
+ if (content.length === 0) continue
1884
+ const trimmed = trimTrailingZeros(content)
1885
+ if (trimmed.length !== content.length || (trimmed.length > 0 && trimmed[0] !== content[0])) {
1886
+ node.length = contentStart
1887
+ node.push(...trimmed)
1888
+ }
1889
+ }
1890
+
1891
+ // Merge adjacent active segments with same memory and consecutive offsets
1892
+ const dataNodes = []
1893
+ for (let i = 0; i < result.length; i++) {
1894
+ const node = result[i]
1895
+ if (Array.isArray(node) && node[0] === 'data') {
1896
+ const info = getDataOffset(node)
1897
+ if (info) {
1898
+ const len = getDataLength(node)
1899
+ if (len !== null) dataNodes.push({ ...info, node, index: i, len })
1900
+ }
1901
+ }
1902
+ }
1903
+
1904
+ dataNodes.sort((a, b) => {
1905
+ const ma = String(a.memidx), mb = String(b.memidx)
1906
+ if (ma !== mb) return ma.localeCompare(mb)
1907
+ return a.offset - b.offset
1908
+ })
1909
+
1910
+ const toRemove = new Set()
1911
+ for (let i = 0; i < dataNodes.length - 1; i++) {
1912
+ const a = dataNodes[i]
1913
+ const b = dataNodes[i + 1]
1914
+ if (toRemove.has(a.index) || String(a.memidx) !== String(b.memidx)) continue
1915
+ if (a.offset + a.len !== b.offset) continue
1916
+ if (mergeDataSegments(a.node, b.node)) {
1917
+ toRemove.add(b.index)
1918
+ a.len = getDataLength(a.node)
1919
+ }
1920
+ }
1921
+
1922
+ if (toRemove.size > 0) {
1923
+ result = result.filter((_, i) => !toRemove.has(i))
1924
+ }
1925
+
1926
+ return result
1927
+ }
1928
+
1929
+ // ==================== IMPORT FIELD MINIFICATION ====================
1930
+
1931
+ /** Create a shortener that maps names to a, b, ..., z, aa, ab, ... */
1932
+ const makeShortener = () => {
1933
+ const map = new Map()
1934
+ let n = 0
1935
+ return (name) => {
1936
+ if (!map.has(name)) {
1937
+ let id = '', x = n++
1938
+ do {
1939
+ id = String.fromCharCode(97 + (x % 26)) + id
1940
+ x = Math.floor(x / 26) - 1
1941
+ } while (x >= 0)
1942
+ map.set(name, id || 'a')
1943
+ }
1944
+ return map.get(name)
1945
+ }
1946
+ }
1947
+
1948
+ /**
1949
+ * Minify import module and field names for smaller binaries.
1950
+ * Only safe when you control the host environment.
1951
+ * @param {Array} ast
1952
+ * @returns {Array}
1953
+ */
1954
+ const minifyImports = (ast) => {
1955
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1956
+ const result = clone(ast)
1957
+ const shortMod = makeShortener()
1958
+ const shortField = makeShortener()
1959
+
1960
+ for (const node of result) {
1961
+ if (!Array.isArray(node) || node[0] !== 'import') continue
1962
+ if (typeof node[1] === 'string' && node[1][0] === '"') {
1963
+ node[1] = '"' + shortMod(node[1].slice(1, -1)) + '"'
1964
+ }
1965
+ if (typeof node[2] === 'string' && node[2][0] === '"') {
1966
+ node[2] = '"' + shortField(node[2].slice(1, -1)) + '"'
1967
+ }
1968
+ }
1969
+
1970
+ return result
1971
+ }
1972
+
1973
+ // ==================== REORDER FUNCTIONS ====================
1974
+
1975
+ /**
1976
+ * Count direct calls and sort functions so hot ones come first.
1977
+ * Smaller LEB128 indices for frequent calls reduce binary size.
1978
+ * Imports must stay before defined functions to preserve the index space.
1979
+ * @param {Array} ast
1980
+ * @returns {Array}
1981
+ */
1982
+ /** True iff every defined func has a $name and every func reference is by $name */
1983
+ const reorderSafe = (ast) => {
1984
+ let safe = true
1985
+ walk(ast, (n) => {
1986
+ if (!safe || !Array.isArray(n)) return
1987
+ const op = n[0]
1988
+ if (op === 'func' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1989
+ else if ((op === 'call' || op === 'return_call' || op === 'ref.func') &&
1990
+ (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1991
+ else if (op === 'start' && (typeof n[1] !== 'string' || n[1][0] !== '$')) safe = false
1992
+ else if (op === 'elem') {
1993
+ // Numeric func indices in elem segments would break too
1994
+ for (const sub of n) {
1995
+ if (typeof sub === 'string' && sub[0] !== '$' && /^\d/.test(sub)) { safe = false; break }
1996
+ if (Array.isArray(sub) && sub[0] === 'ref.func' &&
1997
+ (typeof sub[1] !== 'string' || sub[1][0] !== '$')) { safe = false; break }
1998
+ }
1999
+ }
2000
+ })
2001
+ return safe
2002
+ }
2003
+
2004
+ const reorder = (ast) => {
2005
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
2006
+ // Sorting changes the function index space. Skip if any reference is numeric,
2007
+ // since we'd silently retarget unnamed callers/start/elem entries.
2008
+ if (!reorderSafe(ast)) return ast
2009
+ const result = clone(ast)
2010
+
2011
+ const callCounts = new Map()
2012
+ walk(result, (n) => {
2013
+ if (!Array.isArray(n)) return
2014
+ if (n[0] === 'call' || n[0] === 'return_call') {
2015
+ callCounts.set(n[1], (callCounts.get(n[1]) || 0) + 1)
2016
+ }
2017
+ })
2018
+
2019
+ // Imports must precede defined funcs (compile.js assigns indices in AST order).
2020
+ const imports = [], funcs = [], others = []
2021
+ for (const node of result.slice(1)) {
2022
+ if (!Array.isArray(node)) { others.push(node); continue }
2023
+ if (node[0] === 'import') imports.push(node)
2024
+ else if (node[0] === 'func') funcs.push(node)
2025
+ else others.push(node)
2026
+ }
2027
+
2028
+ funcs.sort((a, b) => (callCounts.get(b[1]) || 0) - (callCounts.get(a[1]) || 0))
2029
+ return ['module', ...imports, ...funcs, ...others]
2030
+ }
2031
+
1200
2032
  // ==================== MAIN ====================
1201
2033
 
1202
2034
  /**
@@ -1216,17 +2048,38 @@ export default function optimize(ast, opts = true) {
1216
2048
  ast = clone(ast)
1217
2049
  opts = normalize(opts)
1218
2050
 
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)
2051
+ // Each pass clones its input before mutating, so the original `before`
2052
+ // reference stays untouched and can be used for the convergence check
2053
+ // without an extra deep clone.
2054
+ for (let round = 0; round < 6; round++) {
2055
+ const before = ast
2056
+
2057
+ if (opts.stripmut) ast = stripmut(ast)
2058
+ if (opts.globals) ast = globals(ast)
2059
+ if (opts.fold) ast = fold(ast)
2060
+ if (opts.identity) ast = identity(ast)
2061
+ if (opts.peephole) ast = peephole(ast)
2062
+ if (opts.strength) ast = strength(ast)
2063
+ if (opts.branch) ast = branch(ast)
2064
+ if (opts.propagate) ast = propagate(ast)
2065
+ if (opts.inline) ast = inline(ast)
2066
+ if (opts.offset) ast = offset(ast)
2067
+ if (opts.unbranch) ast = unbranch(ast)
2068
+ if (opts.brif) ast = brif(ast)
2069
+ if (opts.foldarms) ast = foldarms(ast)
2070
+ if (opts.deadcode) ast = deadcode(ast)
2071
+ if (opts.vacuum) ast = vacuum(ast)
2072
+ if (opts.locals) ast = localReuse(ast)
2073
+ if (opts.dedupe) ast = dedupe(ast)
2074
+ if (opts.dedupTypes) ast = dedupTypes(ast)
2075
+ if (opts.packData) ast = packData(ast)
2076
+ if (opts.reorder) ast = reorder(ast)
2077
+ if (opts.treeshake) ast = treeshake(ast)
2078
+ if (opts.minifyImports) ast = minifyImports(ast)
2079
+ if (equal(before, ast)) break
2080
+ }
1228
2081
 
1229
2082
  return ast
1230
2083
  }
1231
2084
 
1232
- export { optimize, treeshake, fold, deadcode, localReuse, identity, strength, branch, propagate, inline, normalize, OPTS }
2085
+ 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 }