watr 4.3.3 → 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,283 +152,247 @@ 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
 
286
288
  // ==================== CONSTANT FOLDING ====================
287
289
 
288
- /** Operators that can be constant-folded */
290
+ /** IEEE 754 roundTiesToEven (bankers' rounding) */
291
+ const roundEven = (x) => x - Math.floor(x) !== 0.5 ? Math.round(x) : 2 * Math.round(x / 2)
292
+
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
+ */
289
306
  const FOLDABLE = {
290
- // i32
291
- 'i32.add': (a, b) => (a + b) | 0,
292
- 'i32.sub': (a, b) => (a - b) | 0,
293
- 'i32.mul': (a, b) => Math.imul(a, b),
294
- 'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
295
- 'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
296
- 'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
297
- 'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
298
- 'i32.and': (a, b) => a & b,
299
- 'i32.or': (a, b) => a | b,
300
- 'i32.xor': (a, b) => a ^ b,
301
- 'i32.shl': (a, b) => a << (b & 31),
302
- 'i32.shr_s': (a, b) => a >> (b & 31),
303
- 'i32.shr_u': (a, b) => a >>> (b & 31),
304
- 'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
305
- 'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
306
- 'i32.eq': (a, b) => a === b ? 1 : 0,
307
- 'i32.ne': (a, b) => a !== b ? 1 : 0,
308
- 'i32.lt_s': (a, b) => a < b ? 1 : 0,
309
- 'i32.lt_u': (a, b) => (a >>> 0) < (b >>> 0) ? 1 : 0,
310
- 'i32.gt_s': (a, b) => a > b ? 1 : 0,
311
- 'i32.gt_u': (a, b) => (a >>> 0) > (b >>> 0) ? 1 : 0,
312
- 'i32.le_s': (a, b) => a <= b ? 1 : 0,
313
- 'i32.le_u': (a, b) => (a >>> 0) <= (b >>> 0) ? 1 : 0,
314
- 'i32.ge_s': (a, b) => a >= b ? 1 : 0,
315
- 'i32.ge_u': (a, b) => (a >>> 0) >= (b >>> 0) ? 1 : 0,
316
- 'i32.eqz': (a) => a === 0 ? 1 : 0,
317
- 'i32.clz': (a) => Math.clz32(a),
318
- 'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
319
- 'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
320
- 'i32.wrap_i64': (a) => Number(BigInt.asIntN(32, a)),
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'],
321
340
 
322
341
  // i64 (using BigInt)
323
- 'i64.add': (a, b) => BigInt.asIntN(64, a + b),
324
- 'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
325
- 'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
326
- 'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
327
- 'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
328
- 'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
329
- 'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
330
- 'i64.and': (a, b) => BigInt.asIntN(64, a & b),
331
- 'i64.or': (a, b) => BigInt.asIntN(64, a | b),
332
- 'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
333
- 'i64.shl': (a, b) => BigInt.asIntN(64, a << (b & 63n)),
334
- 'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
335
- 'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
336
- 'i64.eq': (a, b) => a === b ? 1 : 0,
337
- 'i64.ne': (a, b) => a !== b ? 1 : 0,
338
- 'i64.lt_s': (a, b) => a < b ? 1 : 0,
339
- 'i64.lt_u': (a, b) => BigInt.asUintN(64, a) < BigInt.asUintN(64, b) ? 1 : 0,
340
- 'i64.gt_s': (a, b) => a > b ? 1 : 0,
341
- 'i64.gt_u': (a, b) => BigInt.asUintN(64, a) > BigInt.asUintN(64, b) ? 1 : 0,
342
- 'i64.le_s': (a, b) => a <= b ? 1 : 0,
343
- 'i64.le_u': (a, b) => BigInt.asUintN(64, a) <= BigInt.asUintN(64, b) ? 1 : 0,
344
- 'i64.ge_s': (a, b) => a >= b ? 1 : 0,
345
- 'i64.ge_u': (a, b) => BigInt.asUintN(64, a) >= BigInt.asUintN(64, b) ? 1 : 0,
346
- 'i64.eqz': (a) => a === 0n ? 1 : 0,
347
- 'i64.extend_i32_s': (a) => BigInt(a),
348
- 'i64.extend_i32_u': (a) => BigInt(a >>> 0),
349
-
350
- // f32/f64 - be careful with NaN/precision
351
- 'f32.add': (a, b) => Math.fround(a + b),
352
- 'f32.sub': (a, b) => Math.fround(a - b),
353
- 'f32.mul': (a, b) => Math.fround(a * b),
354
- 'f32.div': (a, b) => Math.fround(a / b),
355
- 'f32.neg': (a) => Math.fround(-a),
356
- 'f32.abs': (a) => Math.fround(Math.abs(a)),
357
- 'f32.sqrt': (a) => Math.fround(Math.sqrt(a)),
358
- 'f32.ceil': (a) => Math.fround(Math.ceil(a)),
359
- 'f32.floor': (a) => Math.fround(Math.floor(a)),
360
- 'f32.trunc': (a) => Math.fround(Math.trunc(a)),
361
- 'f32.nearest': (a) => Math.fround(Math.round(a)),
362
-
363
- 'f64.add': (a, b) => a + b,
364
- 'f64.sub': (a, b) => a - b,
365
- 'f64.mul': (a, b) => a * b,
366
- 'f64.div': (a, b) => a / b,
367
- 'f64.neg': (a) => -a,
368
- 'f64.abs': (a) => Math.abs(a),
369
- 'f64.sqrt': (a) => Math.sqrt(a),
370
- 'f64.ceil': (a) => Math.ceil(a),
371
- 'f64.floor': (a) => Math.floor(a),
372
- 'f64.trunc': (a) => Math.trunc(a),
373
- 'f64.nearest': (a) => Math.round(a),
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'],
374
396
  }
375
397
 
376
398
  /**
@@ -410,124 +432,80 @@ const makeConst = (type, value) => {
410
432
  const fold = (ast) => {
411
433
  return walkPost(clone(ast), (node) => {
412
434
  if (!Array.isArray(node)) return
413
- const op = node[0]
414
- const fn = FOLDABLE[op]
415
- if (!fn) return
435
+ const entry = FOLDABLE[node[0]]
436
+ if (!entry) return
437
+ const [fn, t] = entry
416
438
 
417
- // Unary ops
439
+ // Unary
418
440
  if (fn.length === 1 && node.length === 2) {
419
441
  const a = getConst(node[1])
420
442
  if (!a) return
421
- const result = fn(a.value)
422
- if (result === null) return
423
- const resultType = op.startsWith('i64.') && !op.includes('eqz') ? 'i64' :
424
- op.startsWith('f32.') ? 'f32' :
425
- op.startsWith('f64.') ? 'f64' : 'i32'
426
- return makeConst(resultType, result)
443
+ const r = fn(a.value)
444
+ if (r === null) return
445
+ return makeConst(t, r)
427
446
  }
428
-
429
- // Binary ops
447
+ // Binary
430
448
  if (fn.length === 2 && node.length === 3) {
431
- const a = getConst(node[1])
432
- const b = getConst(node[2])
449
+ const a = getConst(node[1]), b = getConst(node[2])
433
450
  if (!a || !b) return
434
- const result = fn(a.value, b.value)
435
- if (result === null) return
436
- // Comparisons return i32
437
- const isCompare = /\.(eq|ne|[lg][te])/.test(op)
438
- const resultType = isCompare ? 'i32' :
439
- op.startsWith('i64.') ? 'i64' :
440
- op.startsWith('f32.') ? 'f32' :
441
- op.startsWith('f64.') ? 'f64' : 'i32'
442
- return makeConst(resultType, result)
451
+ const r = fn(a.value, b.value)
452
+ if (r === null) return
453
+ return makeConst(t, r)
443
454
  }
444
455
  })
445
456
  }
446
457
 
447
458
  // ==================== IDENTITY REMOVAL ====================
448
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
+
449
477
  /** Identity operations that can be simplified */
450
478
  const IDENTITIES = {
451
479
  // x + 0 → x, 0 + x → x
452
- 'i32.add': (a, b) => {
453
- const ca = getConst(a), cb = getConst(b)
454
- if (ca?.value === 0) return b
455
- if (cb?.value === 0) return a
456
- return null
457
- },
458
- 'i64.add': (a, b) => {
459
- const ca = getConst(a), cb = getConst(b)
460
- if (ca?.value === 0n) return b
461
- if (cb?.value === 0n) return a
462
- return null
463
- },
480
+ 'i32.add': commutativeIdentity(0),
481
+ 'i64.add': commutativeIdentity(0n),
464
482
  // x - 0 → x
465
- 'i32.sub': (a, b) => getConst(b)?.value === 0 ? a : null,
466
- 'i64.sub': (a, b) => getConst(b)?.value === 0n ? a : null,
483
+ 'i32.sub': rightIdentity(0),
484
+ 'i64.sub': rightIdentity(0n),
467
485
  // x * 1 → x, 1 * x → x
468
- 'i32.mul': (a, b) => {
469
- const ca = getConst(a), cb = getConst(b)
470
- if (ca?.value === 1) return b
471
- if (cb?.value === 1) return a
472
- return null
473
- },
474
- 'i64.mul': (a, b) => {
475
- const ca = getConst(a), cb = getConst(b)
476
- if (ca?.value === 1n) return b
477
- if (cb?.value === 1n) return a
478
- return null
479
- },
486
+ 'i32.mul': commutativeIdentity(1),
487
+ 'i64.mul': commutativeIdentity(1n),
480
488
  // x / 1 → x
481
- 'i32.div_s': (a, b) => getConst(b)?.value === 1 ? a : null,
482
- 'i32.div_u': (a, b) => getConst(b)?.value === 1 ? a : null,
483
- 'i64.div_s': (a, b) => getConst(b)?.value === 1n ? a : null,
484
- 'i64.div_u': (a, b) => getConst(b)?.value === 1n ? a : null,
489
+ 'i32.div_s': rightIdentity(1),
490
+ 'i32.div_u': rightIdentity(1),
491
+ 'i64.div_s': rightIdentity(1n),
492
+ 'i64.div_u': rightIdentity(1n),
485
493
  // x & -1 → x, -1 & x → x (all bits set)
486
- 'i32.and': (a, b) => {
487
- const ca = getConst(a), cb = getConst(b)
488
- if (ca?.value === -1) return b
489
- if (cb?.value === -1) return a
490
- return null
491
- },
492
- 'i64.and': (a, b) => {
493
- const ca = getConst(a), cb = getConst(b)
494
- if (ca?.value === -1n) return b
495
- if (cb?.value === -1n) return a
496
- return null
497
- },
494
+ 'i32.and': commutativeIdentity(-1),
495
+ 'i64.and': commutativeIdentity(-1n),
498
496
  // x | 0 → x, 0 | x → x
499
- 'i32.or': (a, b) => {
500
- const ca = getConst(a), cb = getConst(b)
501
- if (ca?.value === 0) return b
502
- if (cb?.value === 0) return a
503
- return null
504
- },
505
- 'i64.or': (a, b) => {
506
- const ca = getConst(a), cb = getConst(b)
507
- if (ca?.value === 0n) return b
508
- if (cb?.value === 0n) return a
509
- return null
510
- },
497
+ 'i32.or': commutativeIdentity(0),
498
+ 'i64.or': commutativeIdentity(0n),
511
499
  // x ^ 0 → x, 0 ^ x → x
512
- 'i32.xor': (a, b) => {
513
- const ca = getConst(a), cb = getConst(b)
514
- if (ca?.value === 0) return b
515
- if (cb?.value === 0) return a
516
- return null
517
- },
518
- 'i64.xor': (a, b) => {
519
- const ca = getConst(a), cb = getConst(b)
520
- if (ca?.value === 0n) return b
521
- if (cb?.value === 0n) return a
522
- return null
523
- },
500
+ 'i32.xor': commutativeIdentity(0),
501
+ 'i64.xor': commutativeIdentity(0n),
524
502
  // x << 0 → x, x >> 0 → x
525
- 'i32.shl': (a, b) => getConst(b)?.value === 0 ? a : null,
526
- 'i32.shr_s': (a, b) => getConst(b)?.value === 0 ? a : null,
527
- 'i32.shr_u': (a, b) => getConst(b)?.value === 0 ? a : null,
528
- 'i64.shl': (a, b) => getConst(b)?.value === 0n ? a : null,
529
- 'i64.shr_s': (a, b) => getConst(b)?.value === 0n ? a : null,
530
- 'i64.shr_u': (a, b) => getConst(b)?.value === 0n ? a : null,
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),
531
509
  // f + 0 → x (careful with -0.0, skip for floats)
532
510
  // f * 1 → x (careful with NaN, skip for floats)
533
511
  }
@@ -633,50 +611,15 @@ const branch = (ast) => {
633
611
  // (if (i32.const 0) then else) → else
634
612
  // (if (i32.const N) then else) → then (N != 0)
635
613
  if (op === 'if') {
636
- // Find condition - first non-annotation child that's an expression
637
- let condIdx = 1
638
- while (condIdx < node.length) {
639
- const child = node[condIdx]
640
- if (Array.isArray(child) && (child[0] === 'then' || child[0] === 'else' || child[0] === 'result' || child[0] === 'param')) {
641
- condIdx++
642
- continue
643
- }
644
- break
645
- }
646
-
647
- const cond = node[condIdx]
614
+ const { cond, thenBranch, elseBranch } = parseIf(node)
648
615
  const c = getConst(cond)
649
616
  if (!c) return
650
-
651
- // Find then/else branches
652
- let thenBranch = null, elseBranch = null
653
- for (let i = condIdx + 1; i < node.length; i++) {
654
- const child = node[i]
655
- if (Array.isArray(child)) {
656
- if (child[0] === 'then') thenBranch = child
657
- else if (child[0] === 'else') elseBranch = child
658
- }
659
- }
660
-
661
- // Condition is truthy → replace with then contents
662
- if (c.value !== 0 && c.value !== 0n) {
663
- if (thenBranch && thenBranch.length > 1) {
664
- // Return block with then contents (or just contents if single)
665
- const contents = thenBranch.slice(1)
666
- if (contents.length === 1) return contents[0]
667
- return ['block', ...contents]
668
- }
669
- return ['nop']
670
- }
671
- // Condition is falsy → replace with else contents
672
- else {
673
- if (elseBranch && elseBranch.length > 1) {
674
- const contents = elseBranch.slice(1)
675
- if (contents.length === 1) return contents[0]
676
- return ['block', ...contents]
677
- }
678
- return ['nop']
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]
679
621
  }
622
+ return ['nop']
680
623
  }
681
624
 
682
625
  // (br_if $label (i32.const 0)) → nop
@@ -842,82 +785,233 @@ const localReuse = (ast) => {
842
785
  return result
843
786
  }
844
787
 
845
- // ==================== CONSTANT PROPAGATION ====================
788
+ // ==================== PROPAGATION & LOCAL ELIMINATION ====================
789
+
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.']
846
806
 
847
807
  /**
848
- * Propagate constant values through local variables.
849
- * When a local is set to a constant and not modified before use, replace the get with the constant.
850
- * @param {Array} ast
851
- * @returns {Array}
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.
852
810
  */
853
- const propagate = (ast) => {
854
- const result = clone(ast)
811
+ const isPure = (node) => {
812
+ if (!Array.isArray(node)) return true
813
+ const op = node[0]
814
+ if (typeof op !== 'string') return false
815
+ if (IMPURE_OPS.has(op)) return false
816
+ for (const sub of IMPURE_SUBSTRINGS) if (op.includes(sub)) return false
817
+ for (let i = 1; i < node.length; i++) if (Array.isArray(node[i]) && !isPure(node[i])) return false
818
+ return true
819
+ }
855
820
 
856
- walk(result, (node) => {
857
- if (!Array.isArray(node) || node[0] !== 'func') return
821
+ /** Count all local.get/set/tee occurrences in one walk */
822
+ const countLocalUses = (node) => {
823
+ const counts = new Map()
824
+ const ensure = name => { if (!counts.has(name)) counts.set(name, { gets: 0, sets: 0, tees: 0 }); return counts.get(name) }
825
+ walk(node, n => {
826
+ if (!Array.isArray(n) || n.length < 2 || typeof n[1] !== 'string') return
827
+ if (n[0] === 'local.get') ensure(n[1]).gets++
828
+ else if (n[0] === 'local.set') ensure(n[1]).sets++
829
+ else if (n[0] === 'local.tee') ensure(n[1]).tees++
830
+ })
831
+ return counts
832
+ }
858
833
 
859
- // Track which locals have known constant values
860
- // This is a simple single-pass analysis within straight-line code
861
- const constLocals = new Map() // $name → const node
862
-
863
- // Process function body in order
864
- const processBlock = (block, startIdx = 1) => {
865
- for (let i = startIdx; i < block.length; i++) {
866
- const instr = block[i]
867
- if (!Array.isArray(instr)) continue
868
-
869
- const op = instr[0]
870
-
871
- // local.set $x (const) → remember constant
872
- if (op === 'local.set' && instr.length === 3) {
873
- const local = instr[1]
874
- const val = instr[2]
875
- const c = getConst(val)
876
- if (c && typeof local === 'string') {
877
- constLocals.set(local, val)
878
- } else if (typeof local === 'string') {
879
- constLocals.delete(local) // invalidate if set to non-const
880
- }
881
- }
882
- // local.tee also sets
883
- else if (op === 'local.tee' && instr.length === 3) {
884
- const local = instr[1]
885
- const val = instr[2]
886
- const c = getConst(val)
887
- if (c && typeof local === 'string') {
888
- constLocals.set(local, val)
889
- } else if (typeof local === 'string') {
890
- constLocals.delete(local)
891
- }
892
- }
893
- // local.get $x → replace with const if known
894
- else if (op === 'local.get' && instr.length === 2) {
895
- const local = instr[1]
896
- if (typeof local === 'string' && constLocals.has(local)) {
897
- const constVal = constLocals.get(local)
898
- // Replace in place
899
- instr.length = 0
900
- instr.push(...clone(constVal))
901
- }
902
- }
903
- // Control flow invalidates all knowledge (conservative)
904
- else if (op === 'block' || op === 'loop' || op === 'if' || op === 'call' || op === 'call_indirect') {
905
- constLocals.clear()
906
- }
834
+ /** Can this tracked value be substituted for a local.get? */
835
+ const canSubst = (k) => getConst(k.val) || (k.pure && k.singleUse)
907
836
 
908
- // Recursively process nested expressions that might have local.get
909
- walkPost(instr, (n) => {
910
- if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
911
- const local = n[1]
912
- if (typeof local === 'string' && constLocals.has(local)) {
913
- const constVal = constLocals.get(local)
914
- return clone(constVal)
915
- }
916
- })
837
+ /** Try substitute local.get nodes with known values */
838
+ const substGets = (node, known) => walkPost(node, n => {
839
+ if (!Array.isArray(n) || n[0] !== 'local.get' || n.length !== 2) return
840
+ const k = typeof n[1] === 'string' && known.get(n[1])
841
+ if (k && canSubst(k)) return clone(k.val)
842
+ })
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
917
887
  }
918
888
  }
919
889
 
920
- processBlock(node)
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
+
989
+ /**
990
+ * Propagate values through locals and eliminate single-use/dead locals.
991
+ * Constants propagate to all uses; pure single-use exprs inline into get site.
992
+ * Multi-pass with batch counting for convergence.
993
+ */
994
+ const propagate = (ast) => {
995
+ const result = clone(ast)
996
+
997
+ walk(result, (funcNode) => {
998
+ if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
999
+
1000
+ const params = new Set()
1001
+ for (const sub of funcNode)
1002
+ if (Array.isArray(sub) && sub[0] === 'param' && typeof sub[1] === 'string') params.add(sub[1])
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.
1007
+ for (let pass = 0; pass < 4; pass++) {
1008
+ let changed = false
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
1013
+ if (!changed) break
1014
+ }
921
1015
  })
922
1016
 
923
1017
  return result
@@ -970,8 +1064,8 @@ const inline = (ast) => {
970
1064
  }
971
1065
  }
972
1066
 
973
- // Only inline: no locals, <= 2 params, single expression body, not exported
974
- 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) {
975
1069
  // Check if function mutates any of its params (local.set/tee on param)
976
1070
  const paramNames = new Set(params.map(p => p.name))
977
1071
  let mutatesParam = false
@@ -1020,135 +1114,915 @@ const inline = (ast) => {
1020
1114
  return result
1021
1115
  }
1022
1116
 
1023
- // ==================== COMMON SUBEXPRESSION ELIMINATION ====================
1117
+ // ==================== VACUUM ====================
1024
1118
 
1025
1119
  /**
1026
- * Hash an expression for comparison.
1027
- * @param {any} node
1028
- * @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}
1029
1124
  */
1030
- 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
+ }
1031
1245
 
1032
1246
  /**
1033
- * Eliminate common subexpressions by caching repeated computations.
1034
- * Limited to pure expressions within a function.
1247
+ * Apply peephole optimizations.
1035
1248
  * @param {Array} ast
1036
1249
  * @returns {Array}
1037
1250
  */
1038
- const cse = (ast) => {
1039
- // CSE is complex and can increase code size (extra locals)
1040
- // Simple version: detect and report, but actual elimination needs careful analysis
1041
- // For now, implement a basic version that works on adjacent identical expressions
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 ====================
1042
1262
 
1263
+ /**
1264
+ * Replace global.get of immutable globals with their constant init values.
1265
+ * @param {Array} ast
1266
+ * @returns {Array}
1267
+ */
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
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) => {
1043
1389
  const result = clone(ast)
1044
1390
 
1045
1391
  walk(result, (node) => {
1046
- 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)
1440
+
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
+ }
1047
1460
 
1048
- // Find sequences of identical pure expressions
1049
- const seen = new Map() // hash → { node, count }
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
+ }
1050
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])
1051
1598
  walk(node, (n) => {
1052
- if (!Array.isArray(n)) return
1599
+ if (!Array.isArray(n) || typeof n[1] !== 'string' || n[1][0] !== '$') return
1053
1600
  const op = n[0]
1054
- // Only consider pure operations
1055
- if (!op || typeof op !== 'string') return
1056
- if (op.startsWith('i32.') || op.startsWith('i64.') || op.startsWith('f32.') || op.startsWith('f64.')) {
1057
- // Skip simple consts
1058
- if (op.endsWith('.const')) return
1059
- // Skip if has side effects (calls, memory ops)
1060
- let hasSideEffects = false
1061
- walk(n, (sub) => {
1062
- if (Array.isArray(sub) && (sub[0] === 'call' || sub[0]?.includes('load') || sub[0]?.includes('store'))) {
1063
- hasSideEffects = true
1064
- }
1065
- })
1066
- if (hasSideEffects) return
1067
-
1068
- const hash = exprHash(n)
1069
- if (seen.has(hash)) {
1070
- seen.get(hash).count++
1071
- } else {
1072
- seen.set(hash, { node: n, count: 1 })
1073
- }
1601
+ if (op === 'param' || op === 'local' || op === 'block' || op === 'loop' || op === 'if') {
1602
+ localNames.add(n[1])
1074
1603
  }
1075
1604
  })
1076
1605
 
1077
- // For now, just report - full CSE would require inserting locals
1078
- // 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
+ }
1079
1639
  })
1080
1640
 
1081
1641
  return result
1082
1642
  }
1083
1643
 
1084
- // ==================== LOOP INVARIANT HOISTING ====================
1644
+ // ==================== TYPE DEDUPLICATION ====================
1085
1645
 
1086
1646
  /**
1087
- * Hoist loop-invariant computations out of loops.
1647
+ * Merge structurally identical (type ...) definitions.
1648
+ * Keeps the first occurrence and redirects all references.
1088
1649
  * @param {Array} ast
1089
1650
  * @returns {Array}
1090
1651
  */
1091
- const hoist = (ast) => {
1652
+ const dedupTypes = (ast) => {
1653
+ if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1092
1654
  const result = clone(ast)
1093
1655
 
1094
- walk(result, (node) => {
1095
- 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
1096
1658
 
1097
- // Find loops
1098
- walk(node, (loopNode, parent, idx) => {
1099
- 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
1100
1663
 
1101
- // Collect all locals modified in loop
1102
- const modifiedLocals = new Set()
1103
- walk(loopNode, (n) => {
1104
- if (!Array.isArray(n)) return
1105
- if (n[0] === 'local.set' || n[0] === 'local.tee') {
1106
- if (typeof n[1] === 'string') modifiedLocals.add(n[1])
1107
- }
1108
- })
1664
+ // Hash the type body, normalizing only the type's own name
1665
+ const hash = hashFunc(node, new Set([name]))
1109
1666
 
1110
- // Find invariant expressions (don't depend on modified locals or memory)
1111
- const invariants = []
1667
+ if (signatures.has(hash)) {
1668
+ redirects.set(name, signatures.get(hash))
1669
+ } else {
1670
+ signatures.set(hash, name)
1671
+ }
1672
+ }
1112
1673
 
1113
- for (let i = 1; i < loopNode.length; i++) {
1114
- const instr = loopNode[i]
1115
- if (!Array.isArray(instr)) continue
1674
+ if (redirects.size === 0) return result
1116
1675
 
1117
- const op = instr[0]
1118
- // Skip control flow
1119
- 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
+ }
1120
1684
 
1121
- // Check if pure and invariant
1122
- let isInvariant = true
1123
- let isPure = true
1685
+ walkPost(result, (node) => {
1686
+ if (!Array.isArray(node)) return
1687
+ const op = node[0]
1124
1688
 
1125
- walk(instr, (n) => {
1126
- if (!Array.isArray(n)) return
1127
- const subOp = n[0]
1128
- // Side effects
1129
- if (subOp === 'call' || subOp === 'call_indirect' || subOp?.includes('store') || subOp?.includes('load')) {
1130
- isPure = false
1131
- }
1132
- // Depends on modified local
1133
- if (subOp === 'local.get' && typeof n[1] === 'string' && modifiedLocals.has(n[1])) {
1134
- isInvariant = false
1135
- }
1136
- })
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
+ }
1137
1698
 
1138
- // Only hoist simple const expressions for safety
1139
- if (isPure && isInvariant && op?.endsWith('.const')) {
1140
- // 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
+ }
1141
1710
  }
1142
1711
  }
1712
+ }
1143
1713
 
1144
- // Full hoisting would require inserting code before the loop
1145
- // This is complex and risky, so we keep it minimal
1146
- })
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
+ }
1147
1723
  })
1148
1724
 
1149
1725
  return result
1150
1726
  }
1151
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
+
1152
2026
  // ==================== MAIN ====================
1153
2027
 
1154
2028
  /**
@@ -1168,17 +2042,38 @@ export default function optimize(ast, opts = true) {
1168
2042
  ast = clone(ast)
1169
2043
  opts = normalize(opts)
1170
2044
 
1171
- if (opts.fold) ast = fold(ast)
1172
- if (opts.identity) ast = identity(ast)
1173
- if (opts.strength) ast = strength(ast)
1174
- if (opts.branch) ast = branch(ast)
1175
- if (opts.propagate) ast = propagate(ast)
1176
- if (opts.inline) ast = inline(ast)
1177
- if (opts.deadcode) ast = deadcode(ast)
1178
- if (opts.locals) ast = localReuse(ast)
1179
- if (opts.treeshake) ast = treeshake(ast)
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
+ }
1180
2075
 
1181
2076
  return ast
1182
2077
  }
1183
2078
 
1184
- 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 }