watr 4.6.6 → 4.6.7

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/dist/watr.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watr",
3
- "version": "4.6.6",
3
+ "version": "4.6.7",
4
4
  "description": "Light & fast WAT compiler – WebAssembly Text to binary, parse, print, transform",
5
5
  "main": "watr.js",
6
6
  "bin": {
package/src/const.js CHANGED
@@ -174,6 +174,29 @@ export const INSTR = [
174
174
  ]
175
175
  ]
176
176
 
177
+ /**
178
+ * Result value-type of an instruction, inferred from its name — the single
179
+ * source of truth for both constant folding and import type-inference.
180
+ * Comparisons and `eqz` on scalar int/float collapse to `i32`; otherwise the
181
+ * type is the name prefix (`i32.add`→`i32`, `f64.sqrt`→`f64`). Returns null
182
+ * when the type can't be inferred from the name alone.
183
+ *
184
+ * @param {string} op - Instruction name
185
+ * @returns {string|null}
186
+ */
187
+ export const resultType = (op) => {
188
+ if (typeof op !== 'string') return null
189
+ const dot = op.indexOf('.')
190
+ if (dot < 0) return null
191
+ const prefix = op.slice(0, dot)
192
+ const scalar = prefix === 'i32' || prefix === 'i64' || prefix === 'f32' || prefix === 'f64'
193
+ // comparisons & eqz on scalar types yield i32 regardless of operand type
194
+ if (scalar && /^(eqz?|ne|[lg][te])(_[su])?$/.test(op.slice(dot + 1))) return 'i32'
195
+ if (scalar || prefix === 'v128') return prefix
196
+ if (op === 'memory.size' || op === 'memory.grow') return 'i32'
197
+ return null
198
+ }
199
+
177
200
  // Binary section type codes
178
201
  export const SECTION = { custom: 0, type: 1, import: 2, func: 3, table: 4, memory: 5, tag: 13, strings: 14, global: 6, export: 7, start: 8, elem: 9, datacount: 12, code: 10, data: 11 }
179
202
 
package/src/optimize.js CHANGED
@@ -7,40 +7,8 @@
7
7
 
8
8
  import parse from './parse.js'
9
9
  import compile from './compile.js'
10
-
11
- /** Optimizations that can be applied.
12
- * Passes defaulting to false can bloat output or are expensive — opt-in only. */
13
- const OPTS = {
14
- treeshake: true, // remove unused funcs/globals/types/tables
15
- fold: true, // constant folding
16
- deadcode: true, // eliminate dead code after unreachable/br/return
17
- locals: true, // remove unused locals
18
- identity: true, // remove identity ops (x + 0 → x)
19
- strength: true, // strength reduction (x * 2 → x << 1)
20
- branch: true, // simplify constant branches
21
- propagate: true, // forward-propagate single-use locals & tiny consts (never inflates)
22
- inline: false, // inline tiny functions — can duplicate bodies
23
- inlineOnce: true, // inline single-call functions into their lone caller (never duplicates)
24
- vacuum: true, // remove nops, drop-of-pure, empty branches
25
- mergeBlocks: true, // unwrap `(block $L …)` whose label is never targeted
26
- coalesce: true, // share local slots between same-type non-overlapping locals
27
- peephole: true, // x-x→0, x&0→0, etc.
28
- globals: true, // propagate immutable global constants
29
- offset: true, // fold add+const into load/store offset
30
- unbranch: true, // remove redundant br at end of own block
31
- loopify: true, // collapse block+loop+brif while-idiom into loop+if
32
- stripmut: true, // strip mut from never-written globals
33
- brif: true, // if-then-br → br_if
34
- foldarms: false, // merge identical trailing if arms — can add block wrapper
35
- dedupe: true, // eliminate duplicate functions
36
- reorder: false, // put hot functions first — no AST reduction
37
- dedupTypes: true, // merge identical type definitions
38
- packData: true, // trim trailing zeros, merge adjacent data segments
39
- minifyImports: false, // shorten import names — enable only when you control the host
40
- }
41
-
42
- /** All optimization names */
43
- const ALL = Object.keys(OPTS)
10
+ import { walk, walkPost, clone } from './util.js'
11
+ import { resultType } from './const.js'
44
12
 
45
13
  /**
46
14
  * Recursively count AST nodes — fast size heuristic without compiling.
@@ -77,64 +45,6 @@ const equal = (a, b) => {
77
45
  return true
78
46
  }
79
47
 
80
- /**
81
- * Normalize options to { opt: bool } map.
82
- * @param {boolean|string|Object} opts
83
- * @returns {Object}
84
- */
85
- const normalize = (opts) => {
86
- if (opts === true) return { ...OPTS }
87
- if (opts === false) return {}
88
- if (typeof opts === 'string') {
89
- const set = new Set(opts.split(/\s+/).filter(Boolean))
90
- if (set.has('all')) return Object.fromEntries(ALL.map(f => [f, true]))
91
- // Explicit pass names enable ONLY those passes (not the full default set).
92
- return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
93
- }
94
- return { ...OPTS, ...opts }
95
- }
96
- /**
97
- * Deep clone AST.
98
- * @param {any} node
99
- * @returns {any}
100
- */
101
- const clone = (node) => {
102
- if (!Array.isArray(node)) return node
103
- return node.map(clone)
104
- }
105
-
106
- /**
107
- * Walk AST depth-first (pre-order).
108
- * @param {any} node
109
- * @param {Function} fn - (node, parent, idx) => void
110
- * @param {any} [parent]
111
- * @param {number} [idx]
112
- */
113
- const walk = (node, fn, parent, idx) => {
114
- fn(node, parent, idx)
115
- if (Array.isArray(node)) for (let i = 0; i < node.length; i++) walk(node[i], fn, node, i)
116
- }
117
-
118
- /**
119
- * Walk AST depth-first (post-order), transform children before parent.
120
- * Returns the (potentially replaced) node.
121
- * @param {any} node
122
- * @param {Function} fn - (node, parent, idx) => newNode|undefined
123
- * @param {any} [parent]
124
- * @param {number} [idx]
125
- * @returns {any}
126
- */
127
- const walkPost = (node, fn, parent, idx) => {
128
- if (Array.isArray(node)) {
129
- for (let i = 0; i < node.length; i++) {
130
- const result = walkPost(node[i], fn, node, i)
131
- if (result !== undefined) node[i] = result
132
- }
133
- }
134
- const result = fn(node, parent, idx)
135
- return result !== undefined ? result : node
136
- }
137
-
138
48
  /**
139
49
  * Locate the parts of an `(if ...)` node:
140
50
  * condIdx → index of the condition expression
@@ -344,117 +254,117 @@ const i64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
344
254
  const u64c = (fn) => (a, b) => fn(BigInt.asUintN(64, a), BigInt.asUintN(64, b)) ? 1 : 0
345
255
 
346
256
  /**
347
- * Constant folders, keyed by op. Each entry is [fn, resultType].
348
- * Comparisons return i32, conversions return their named output type.
257
+ * Constant folders, keyed by op. Each entry is the fold function; the result
258
+ * value-type is derived once via `resultType` (see `fold`).
349
259
  */
350
260
  const FOLDABLE = {
351
261
  // i32 arithmetic
352
- 'i32.add': [(a, b) => (a + b) | 0, 'i32'],
353
- 'i32.sub': [(a, b) => (a - b) | 0, 'i32'],
354
- 'i32.mul': [(a, b) => Math.imul(a, b), 'i32'],
355
- 'i32.div_s': [(a, b) => b !== 0 ? (a / b) | 0 : null, 'i32'],
356
- 'i32.div_u': [(a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null, 'i32'],
357
- 'i32.rem_s': [(a, b) => b !== 0 ? (a % b) | 0 : null, 'i32'],
358
- 'i32.rem_u': [(a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null, 'i32'],
359
- 'i32.and': [(a, b) => a & b, 'i32'],
360
- 'i32.or': [(a, b) => a | b, 'i32'],
361
- 'i32.xor': [(a, b) => a ^ b, 'i32'],
362
- 'i32.shl': [(a, b) => a << (b & 31), 'i32'],
363
- 'i32.shr_s': [(a, b) => a >> (b & 31), 'i32'],
364
- 'i32.shr_u': [(a, b) => a >>> (b & 31), 'i32'],
365
- 'i32.rotl': [(a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 }, 'i32'],
366
- 'i32.rotr': [(a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 }, 'i32'],
367
- 'i32.eq': [i32c((a, b) => a === b), 'i32'],
368
- 'i32.ne': [i32c((a, b) => a !== b), 'i32'],
369
- 'i32.lt_s': [i32c((a, b) => a < b), 'i32'],
370
- 'i32.lt_u': [u32c((a, b) => a < b), 'i32'],
371
- 'i32.gt_s': [i32c((a, b) => a > b), 'i32'],
372
- 'i32.gt_u': [u32c((a, b) => a > b), 'i32'],
373
- 'i32.le_s': [i32c((a, b) => a <= b), 'i32'],
374
- 'i32.le_u': [u32c((a, b) => a <= b), 'i32'],
375
- 'i32.ge_s': [i32c((a, b) => a >= b), 'i32'],
376
- 'i32.ge_u': [u32c((a, b) => a >= b), 'i32'],
377
- 'i32.eqz': [(a) => a === 0 ? 1 : 0, 'i32'],
378
- 'i32.clz': [(a) => Math.clz32(a), 'i32'],
379
- 'i32.ctz': [(a) => a === 0 ? 32 : 31 - Math.clz32(a & -a), 'i32'],
380
- 'i32.popcnt': [(a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c }, 'i32'],
381
- 'i32.wrap_i64': [(a) => Number(BigInt.asIntN(32, a)), 'i32'],
382
- 'i32.extend8_s': [(a) => (a << 24) >> 24, 'i32'],
383
- 'i32.extend16_s': [(a) => (a << 16) >> 16, 'i32'],
262
+ 'i32.add': (a, b) => (a + b) | 0,
263
+ 'i32.sub': (a, b) => (a - b) | 0,
264
+ 'i32.mul': (a, b) => Math.imul(a, b),
265
+ 'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
266
+ 'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
267
+ 'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
268
+ 'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
269
+ 'i32.and': (a, b) => a & b,
270
+ 'i32.or': (a, b) => a | b,
271
+ 'i32.xor': (a, b) => a ^ b,
272
+ 'i32.shl': (a, b) => a << (b & 31),
273
+ 'i32.shr_s': (a, b) => a >> (b & 31),
274
+ 'i32.shr_u': (a, b) => a >>> (b & 31),
275
+ 'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
276
+ 'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
277
+ 'i32.eq': i32c((a, b) => a === b),
278
+ 'i32.ne': i32c((a, b) => a !== b),
279
+ 'i32.lt_s': i32c((a, b) => a < b),
280
+ 'i32.lt_u': u32c((a, b) => a < b),
281
+ 'i32.gt_s': i32c((a, b) => a > b),
282
+ 'i32.gt_u': u32c((a, b) => a > b),
283
+ 'i32.le_s': i32c((a, b) => a <= b),
284
+ 'i32.le_u': u32c((a, b) => a <= b),
285
+ 'i32.ge_s': i32c((a, b) => a >= b),
286
+ 'i32.ge_u': u32c((a, b) => a >= b),
287
+ 'i32.eqz': (a) => a === 0 ? 1 : 0,
288
+ 'i32.clz': (a) => Math.clz32(a),
289
+ 'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
290
+ 'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
291
+ 'i32.wrap_i64': (a) => Number(BigInt.asIntN(32, a)),
292
+ 'i32.extend8_s': (a) => (a << 24) >> 24,
293
+ 'i32.extend16_s': (a) => (a << 16) >> 16,
384
294
 
385
295
  // i64 (using BigInt)
386
- 'i64.add': [(a, b) => BigInt.asIntN(64, a + b), 'i64'],
387
- 'i64.sub': [(a, b) => BigInt.asIntN(64, a - b), 'i64'],
388
- 'i64.mul': [(a, b) => BigInt.asIntN(64, a * b), 'i64'],
389
- 'i64.div_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null, 'i64'],
390
- 'i64.div_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null, 'i64'],
391
- 'i64.rem_s': [(a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null, 'i64'],
392
- 'i64.rem_u': [(a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null, 'i64'],
393
- 'i64.and': [(a, b) => BigInt.asIntN(64, a & b), 'i64'],
394
- 'i64.or': [(a, b) => BigInt.asIntN(64, a | b), 'i64'],
395
- 'i64.xor': [(a, b) => BigInt.asIntN(64, a ^ b), 'i64'],
396
- 'i64.shl': [(a, b) => BigInt.asIntN(64, a << (b & 63n)), 'i64'],
397
- 'i64.shr_s': [(a, b) => BigInt.asIntN(64, a >> (b & 63n)), 'i64'],
398
- 'i64.shr_u': [(a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)), 'i64'],
399
- 'i64.eq': [i64c((a, b) => a === b), 'i32'],
400
- 'i64.ne': [i64c((a, b) => a !== b), 'i32'],
401
- 'i64.lt_s': [i64c((a, b) => a < b), 'i32'],
402
- 'i64.lt_u': [u64c((a, b) => a < b), 'i32'],
403
- 'i64.gt_s': [i64c((a, b) => a > b), 'i32'],
404
- 'i64.gt_u': [u64c((a, b) => a > b), 'i32'],
405
- 'i64.le_s': [i64c((a, b) => a <= b), 'i32'],
406
- 'i64.le_u': [u64c((a, b) => a <= b), 'i32'],
407
- 'i64.ge_s': [i64c((a, b) => a >= b), 'i32'],
408
- 'i64.ge_u': [u64c((a, b) => a >= b), 'i32'],
409
- 'i64.eqz': [(a) => a === 0n ? 1 : 0, 'i32'],
410
- 'i64.extend_i32_s': [(a) => BigInt(a), 'i64'],
411
- 'i64.extend_i32_u': [(a) => BigInt(a >>> 0), 'i64'],
412
- 'i64.extend8_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(8, a)), 'i64'],
413
- 'i64.extend16_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(16, a)), 'i64'],
414
- 'i64.extend32_s': [(a) => BigInt.asIntN(64, BigInt.asIntN(32, a)), 'i64'],
296
+ 'i64.add': (a, b) => BigInt.asIntN(64, a + b),
297
+ 'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
298
+ 'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
299
+ 'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
300
+ 'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
301
+ 'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
302
+ 'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
303
+ 'i64.and': (a, b) => BigInt.asIntN(64, a & b),
304
+ 'i64.or': (a, b) => BigInt.asIntN(64, a | b),
305
+ 'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
306
+ 'i64.shl': (a, b) => BigInt.asIntN(64, a << (b & 63n)),
307
+ 'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
308
+ 'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
309
+ 'i64.eq': i64c((a, b) => a === b),
310
+ 'i64.ne': i64c((a, b) => a !== b),
311
+ 'i64.lt_s': i64c((a, b) => a < b),
312
+ 'i64.lt_u': u64c((a, b) => a < b),
313
+ 'i64.gt_s': i64c((a, b) => a > b),
314
+ 'i64.gt_u': u64c((a, b) => a > b),
315
+ 'i64.le_s': i64c((a, b) => a <= b),
316
+ 'i64.le_u': u64c((a, b) => a <= b),
317
+ 'i64.ge_s': i64c((a, b) => a >= b),
318
+ 'i64.ge_u': u64c((a, b) => a >= b),
319
+ 'i64.eqz': (a) => a === 0n ? 1 : 0,
320
+ 'i64.extend_i32_s': (a) => BigInt(a),
321
+ 'i64.extend_i32_u': (a) => BigInt(a >>> 0),
322
+ 'i64.extend8_s': (a) => BigInt.asIntN(64, BigInt.asIntN(8, a)),
323
+ 'i64.extend16_s': (a) => BigInt.asIntN(64, BigInt.asIntN(16, a)),
324
+ 'i64.extend32_s': (a) => BigInt.asIntN(64, BigInt.asIntN(32, a)),
415
325
 
416
326
  // f32/f64 (NaN/precision-aware via Math.fround)
417
- 'f32.add': [(a, b) => Math.fround(a + b), 'f32'],
418
- 'f32.sub': [(a, b) => Math.fround(a - b), 'f32'],
419
- 'f32.mul': [(a, b) => Math.fround(a * b), 'f32'],
420
- 'f32.div': [(a, b) => Math.fround(a / b), 'f32'],
421
- 'f32.neg': [(a) => Math.fround(-a), 'f32'],
422
- 'f32.abs': [(a) => Math.fround(Math.abs(a)), 'f32'],
423
- 'f32.sqrt': [(a) => Math.fround(Math.sqrt(a)), 'f32'],
424
- 'f32.ceil': [(a) => Math.fround(Math.ceil(a)), 'f32'],
425
- 'f32.floor': [(a) => Math.fround(Math.floor(a)), 'f32'],
426
- 'f32.trunc': [(a) => Math.fround(Math.trunc(a)), 'f32'],
427
- 'f32.nearest': [(a) => Math.fround(roundEven(a)), 'f32'],
428
-
429
- 'f64.add': [(a, b) => a + b, 'f64'],
430
- 'f64.sub': [(a, b) => a - b, 'f64'],
431
- 'f64.mul': [(a, b) => a * b, 'f64'],
432
- 'f64.div': [(a, b) => a / b, 'f64'],
433
- 'f64.neg': [(a) => -a, 'f64'],
434
- 'f64.abs': [Math.abs, 'f64'],
435
- 'f64.sqrt': [Math.sqrt, 'f64'],
436
- 'f64.ceil': [Math.ceil, 'f64'],
437
- 'f64.floor': [Math.floor, 'f64'],
438
- 'f64.trunc': [Math.trunc, 'f64'],
439
- 'f64.nearest': [roundEven, 'f64'],
327
+ 'f32.add': (a, b) => Math.fround(a + b),
328
+ 'f32.sub': (a, b) => Math.fround(a - b),
329
+ 'f32.mul': (a, b) => Math.fround(a * b),
330
+ 'f32.div': (a, b) => Math.fround(a / b),
331
+ 'f32.neg': (a) => Math.fround(-a),
332
+ 'f32.abs': (a) => Math.fround(Math.abs(a)),
333
+ 'f32.sqrt': (a) => Math.fround(Math.sqrt(a)),
334
+ 'f32.ceil': (a) => Math.fround(Math.ceil(a)),
335
+ 'f32.floor': (a) => Math.fround(Math.floor(a)),
336
+ 'f32.trunc': (a) => Math.fround(Math.trunc(a)),
337
+ 'f32.nearest': (a) => Math.fround(roundEven(a)),
338
+
339
+ 'f64.add': (a, b) => a + b,
340
+ 'f64.sub': (a, b) => a - b,
341
+ 'f64.mul': (a, b) => a * b,
342
+ 'f64.div': (a, b) => a / b,
343
+ 'f64.neg': (a) => -a,
344
+ 'f64.abs': Math.abs,
345
+ 'f64.sqrt': Math.sqrt,
346
+ 'f64.ceil': Math.ceil,
347
+ 'f64.floor': Math.floor,
348
+ 'f64.trunc': Math.trunc,
349
+ 'f64.nearest': roundEven,
440
350
 
441
351
  // Bit-exact reinterprets (preserve NaN payloads)
442
- 'i32.reinterpret_f32': [i32FromF32, 'i32'],
443
- 'f32.reinterpret_i32': [f32FromI32, 'f32'],
444
- 'i64.reinterpret_f64': [i64FromF64, 'i64'],
445
- 'f64.reinterpret_i64': [f64FromI64, 'f64'],
352
+ 'i32.reinterpret_f32': i32FromF32,
353
+ 'f32.reinterpret_i32': f32FromI32,
354
+ 'i64.reinterpret_f64': i64FromF64,
355
+ 'f64.reinterpret_i64': f64FromI64,
446
356
 
447
357
  // Numeric conversions (value-preserving where representable)
448
- 'f32.convert_i32_s': [(a) => Math.fround(a | 0), 'f32'],
449
- 'f32.convert_i32_u': [(a) => Math.fround(a >>> 0), 'f32'],
450
- 'f32.convert_i64_s': [(a) => Math.fround(Number(BigInt.asIntN(64, a))), 'f32'],
451
- 'f32.convert_i64_u': [(a) => Math.fround(Number(BigInt.asUintN(64, a))), 'f32'],
452
- 'f64.convert_i32_s': [(a) => (a | 0), 'f64'],
453
- 'f64.convert_i32_u': [(a) => (a >>> 0), 'f64'],
454
- 'f64.convert_i64_s': [(a) => Number(BigInt.asIntN(64, a)), 'f64'],
455
- 'f64.convert_i64_u': [(a) => Number(BigInt.asUintN(64, a)), 'f64'],
456
- 'f32.demote_f64': [(a) => Math.fround(a), 'f32'],
457
- 'f64.promote_f32': [(a) => Math.fround(a), 'f64'],
358
+ 'f32.convert_i32_s': (a) => Math.fround(a | 0),
359
+ 'f32.convert_i32_u': (a) => Math.fround(a >>> 0),
360
+ 'f32.convert_i64_s': (a) => Math.fround(Number(BigInt.asIntN(64, a))),
361
+ 'f32.convert_i64_u': (a) => Math.fround(Number(BigInt.asUintN(64, a))),
362
+ 'f64.convert_i32_s': (a) => (a | 0),
363
+ 'f64.convert_i32_u': (a) => (a >>> 0),
364
+ 'f64.convert_i64_s': (a) => Number(BigInt.asIntN(64, a)),
365
+ 'f64.convert_i64_u': (a) => Number(BigInt.asUintN(64, a)),
366
+ 'f32.demote_f64': (a) => Math.fround(a),
367
+ 'f64.promote_f32': (a) => Math.fround(a),
458
368
  }
459
369
 
460
370
  /**
@@ -522,9 +432,8 @@ const makeConst = (type, value) => {
522
432
  const fold = (ast) => {
523
433
  return walkPost(ast, (node) => {
524
434
  if (!Array.isArray(node)) return
525
- const entry = FOLDABLE[node[0]]
526
- if (!entry) return
527
- const [fn, t] = entry
435
+ const fn = FOLDABLE[node[0]]
436
+ if (!fn) return
528
437
 
529
438
  // Unary
530
439
  if (fn.length === 1 && node.length === 2) {
@@ -532,7 +441,7 @@ const fold = (ast) => {
532
441
  if (!a) return
533
442
  const r = fn(a.value)
534
443
  if (r === null) return
535
- return makeConst(t, r)
444
+ return makeConst(resultType(node[0]), r)
536
445
  }
537
446
  // Binary
538
447
  if (fn.length === 2 && node.length === 3) {
@@ -540,7 +449,7 @@ const fold = (ast) => {
540
449
  if (!a || !b) return
541
450
  const r = fn(a.value, b.value)
542
451
  if (r === null) return
543
- return makeConst(t, r)
452
+ return makeConst(resultType(node[0]), r)
544
453
  }
545
454
  })
546
455
  }
@@ -1539,12 +1448,12 @@ const inlineOnce = (ast) => {
1539
1448
 
1540
1449
  const callee = funcByName.get(calleeName)
1541
1450
  const params = [], locals = []
1542
- let resultType = null
1451
+ let inlResult = null
1543
1452
  for (let i = 2; i < callee.length; i++) {
1544
1453
  const c = callee[i]
1545
1454
  if (typeof c === 'string' || !Array.isArray(c)) continue
1546
1455
  if (c[0] === 'param') params.push({ name: c[1], type: c[2] })
1547
- else if (c[0] === 'result') { if (c.length > 1) resultType = c[1] }
1456
+ else if (c[0] === 'result') { if (c.length > 1) inlResult = c[1] }
1548
1457
  else if (c[0] === 'local') locals.push({ name: c[1], type: c[2] })
1549
1458
  else if (c[0] === 'export' || c[0] === 'type') continue
1550
1459
  else break
@@ -1600,8 +1509,8 @@ const inlineOnce = (ast) => {
1600
1509
  .map(l => ['local.set', rename.get(l.name), zeroFor(l.type)])
1601
1510
  const inner = cBody.map(sub)
1602
1511
  done = true
1603
- return resultType
1604
- ? ['block', exit, ['result', resultType], ...setup, ...resets, ...inner]
1512
+ return inlResult
1513
+ ? ['block', exit, ['result', inlResult], ...setup, ...resets, ...inner]
1605
1514
  : ['block', exit, ...setup, ...resets, ...inner]
1606
1515
  })
1607
1516
  if (replaced !== fn[i]) fn[i] = replaced
@@ -2920,6 +2829,66 @@ const reorder = (ast) => {
2920
2829
 
2921
2830
  // ==================== MAIN ====================
2922
2831
 
2832
+ /**
2833
+ * Optimization passes, in the order they run within each round. Each entry is
2834
+ * `[optionKey, fn, defaultOn, doc]` — the single source of truth that the
2835
+ * dispatch loop, the `OPTS` catalogue, and `normalize` all derive from.
2836
+ * Passes that are off by default can bloat output or are expensive — opt-in.
2837
+ */
2838
+ const PASSES = [
2839
+ ['stripmut', stripmut, true, 'strip mut from never-written globals'],
2840
+ ['globals', globals, true, 'propagate immutable global constants'],
2841
+ ['fold', fold, true, 'constant folding'],
2842
+ ['identity', identity, true, 'remove identity ops (x + 0 → x)'],
2843
+ ['peephole', peephole, true, 'x-x→0, x&0→0, etc.'],
2844
+ ['strength', strength, true, 'strength reduction (x * 2 → x << 1)'],
2845
+ ['branch', branch, true, 'simplify constant branches'],
2846
+ ['propagate', propagate, true, 'forward-propagate single-use locals & tiny consts (never inflates)'],
2847
+ ['inlineOnce', inlineOnce, true, 'inline single-call functions into their lone caller (never duplicates)'],
2848
+ ['inline', inline, false, 'inline tiny functions — can duplicate bodies'],
2849
+ ['offset', offset, true, 'fold add+const into load/store offset'],
2850
+ ['unbranch', unbranch, true, 'remove redundant br at end of own block'],
2851
+ ['loopify', loopify, true, 'collapse block+loop+brif while-idiom into loop+if'],
2852
+ ['brif', brif, true, 'if-then-br → br_if'],
2853
+ ['foldarms', foldarms, false, 'merge identical trailing if arms — can add block wrapper'],
2854
+ ['deadcode', deadcode, true, 'eliminate dead code after unreachable/br/return'],
2855
+ ['vacuum', vacuum, true, 'remove nops, drop-of-pure, empty branches'],
2856
+ ['mergeBlocks', mergeBlocks, true, 'unwrap `(block $L …)` whose label is never targeted'],
2857
+ ['coalesce', coalesceLocals, true, 'share local slots between same-type non-overlapping locals'],
2858
+ ['locals', localReuse, true, 'remove unused locals'],
2859
+ ['dedupe', dedupe, true, 'eliminate duplicate functions'],
2860
+ ['dedupTypes', dedupTypes, true, 'merge identical type definitions'],
2861
+ ['packData', packData, true, 'trim trailing zeros, merge adjacent data segments'],
2862
+ ['reorder', reorder, false, 'put hot functions first — no AST reduction'],
2863
+ ['treeshake', treeshake, true, 'remove unused funcs/globals/types/tables'],
2864
+ ['minifyImports', minifyImports, false, 'shorten import names — enable only when you control the host'],
2865
+ ]
2866
+
2867
+ /** Option name → default-on map — the public catalogue of passes. */
2868
+ const OPTS = Object.fromEntries(PASSES.map(p => [p[0], p[2]]))
2869
+
2870
+ /**
2871
+ * Normalize options to a { passName: bool } map. An explicit object is kept
2872
+ * as-is (preserving `log`/`verbose`), with any unmentioned pass filled to its
2873
+ * default; `true` selects the defaults; a string selects only the named
2874
+ * passes (or all of them via `'all'`).
2875
+ *
2876
+ * @param {boolean|string|Object} opts
2877
+ * @returns {Object}
2878
+ */
2879
+ const normalize = (opts) => {
2880
+ if (opts === false) return {}
2881
+ if (opts !== true && typeof opts !== 'string') {
2882
+ const m = { ...opts }
2883
+ for (const p of PASSES) if (m[p[0]] === undefined) m[p[0]] = p[2]
2884
+ return m
2885
+ }
2886
+ const set = typeof opts === 'string' ? new Set(opts.split(/\s+/).filter(Boolean)) : null
2887
+ const m = {}
2888
+ for (const p of PASSES) m[p[0]] = set ? (set.has('all') || set.has(p[0])) : p[2]
2889
+ return m
2890
+ }
2891
+
2923
2892
  /**
2924
2893
  * Optimize AST.
2925
2894
  *
@@ -2953,32 +2922,7 @@ export default function optimize(ast, opts = true) {
2953
2922
  beforeRound = clone(ast)
2954
2923
  const sizeBefore = binarySize(ast)
2955
2924
 
2956
- if (opts.stripmut) ast = stripmut(ast)
2957
- if (opts.globals) ast = globals(ast)
2958
- if (opts.fold) ast = fold(ast)
2959
- if (opts.identity) ast = identity(ast)
2960
- if (opts.peephole) ast = peephole(ast)
2961
- if (opts.strength) ast = strength(ast)
2962
- if (opts.branch) ast = branch(ast)
2963
- if (opts.propagate) ast = propagate(ast)
2964
- if (opts.inlineOnce) ast = inlineOnce(ast)
2965
- if (opts.inline) ast = inline(ast)
2966
- if (opts.offset) ast = offset(ast)
2967
- if (opts.unbranch) ast = unbranch(ast)
2968
- if (opts.loopify) ast = loopify(ast)
2969
- if (opts.brif) ast = brif(ast)
2970
- if (opts.foldarms) ast = foldarms(ast)
2971
- if (opts.deadcode) ast = deadcode(ast)
2972
- if (opts.vacuum) ast = vacuum(ast)
2973
- if (opts.mergeBlocks) ast = mergeBlocks(ast)
2974
- if (opts.coalesce) ast = coalesceLocals(ast)
2975
- if (opts.locals) ast = localReuse(ast)
2976
- if (opts.dedupe) ast = dedupe(ast)
2977
- if (opts.dedupTypes) ast = dedupTypes(ast)
2978
- if (opts.packData) ast = packData(ast)
2979
- if (opts.reorder) ast = reorder(ast)
2980
- if (opts.treeshake) ast = treeshake(ast)
2981
- if (opts.minifyImports) ast = minifyImports(ast)
2925
+ for (const [key, fn] of PASSES) if (opts[key]) ast = fn(ast)
2982
2926
  // Second propagate sweep: `inlineOnce`/`inline` (above) leave fresh
2983
2927
  // `(local.set $p arg) … (local.get $p)` wrappers around each inlined call;
2984
2928
  // re-running propagation collapses them within this same round, so the size