watr 4.6.6 → 4.6.8

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.8",
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/encode.js CHANGED
@@ -129,7 +129,7 @@ const _u8 = new Uint8Array(_buf), _i32 = new Int32Array(_buf), _f32 = new Float3
129
129
  i64.parse = n => {
130
130
  n = cleanInt(n)
131
131
  const neg = n[0] === '-'
132
- const body = neg ? n.slice(1) : n
132
+ const body = neg || n[0] === '+' ? n.slice(1) : n
133
133
  // Range check on the literal string before BigInt conversion (lexicographic compare on clean digits).
134
134
  let max
135
135
  if (body[0] === '0' && (body[1] === 'x' || body[1] === 'X')) {
package/src/optimize.js CHANGED
@@ -7,40 +7,9 @@
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 { i32, i64 } from './encode.js'
11
+ import { walk, walkPost, clone } from './util.js'
12
+ import { resultType } from './const.js'
44
13
 
45
14
  /**
46
15
  * Recursively count AST nodes — fast size heuristic without compiling.
@@ -77,64 +46,6 @@ const equal = (a, b) => {
77
46
  return true
78
47
  }
79
48
 
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
49
  /**
139
50
  * Locate the parts of an `(if ...)` node:
140
51
  * condIdx → index of the condition expression
@@ -344,117 +255,117 @@ const i64c = (fn) => (a, b) => fn(a, b) ? 1 : 0
344
255
  const u64c = (fn) => (a, b) => fn(BigInt.asUintN(64, a), BigInt.asUintN(64, b)) ? 1 : 0
345
256
 
346
257
  /**
347
- * Constant folders, keyed by op. Each entry is [fn, resultType].
348
- * Comparisons return i32, conversions return their named output type.
258
+ * Constant folders, keyed by op. Each entry is the fold function; the result
259
+ * value-type is derived once via `resultType` (see `fold`).
349
260
  */
350
261
  const FOLDABLE = {
351
262
  // 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'],
263
+ 'i32.add': (a, b) => (a + b) | 0,
264
+ 'i32.sub': (a, b) => (a - b) | 0,
265
+ 'i32.mul': (a, b) => Math.imul(a, b),
266
+ 'i32.div_s': (a, b) => b !== 0 ? (a / b) | 0 : null,
267
+ 'i32.div_u': (a, b) => b !== 0 ? ((a >>> 0) / (b >>> 0)) | 0 : null,
268
+ 'i32.rem_s': (a, b) => b !== 0 ? (a % b) | 0 : null,
269
+ 'i32.rem_u': (a, b) => b !== 0 ? ((a >>> 0) % (b >>> 0)) | 0 : null,
270
+ 'i32.and': (a, b) => a & b,
271
+ 'i32.or': (a, b) => a | b,
272
+ 'i32.xor': (a, b) => a ^ b,
273
+ 'i32.shl': (a, b) => a << (b & 31),
274
+ 'i32.shr_s': (a, b) => a >> (b & 31),
275
+ 'i32.shr_u': (a, b) => a >>> (b & 31),
276
+ 'i32.rotl': (a, b) => { b &= 31; return ((a << b) | (a >>> (32 - b))) | 0 },
277
+ 'i32.rotr': (a, b) => { b &= 31; return ((a >>> b) | (a << (32 - b))) | 0 },
278
+ 'i32.eq': i32c((a, b) => a === b),
279
+ 'i32.ne': i32c((a, b) => a !== b),
280
+ 'i32.lt_s': i32c((a, b) => a < b),
281
+ 'i32.lt_u': u32c((a, b) => a < b),
282
+ 'i32.gt_s': i32c((a, b) => a > b),
283
+ 'i32.gt_u': u32c((a, b) => a > b),
284
+ 'i32.le_s': i32c((a, b) => a <= b),
285
+ 'i32.le_u': u32c((a, b) => a <= b),
286
+ 'i32.ge_s': i32c((a, b) => a >= b),
287
+ 'i32.ge_u': u32c((a, b) => a >= b),
288
+ 'i32.eqz': (a) => a === 0 ? 1 : 0,
289
+ 'i32.clz': (a) => Math.clz32(a),
290
+ 'i32.ctz': (a) => a === 0 ? 32 : 31 - Math.clz32(a & -a),
291
+ 'i32.popcnt': (a) => { let c = 0; while (a) { c += a & 1; a >>>= 1 } return c },
292
+ 'i32.wrap_i64': (a) => Number(BigInt.asIntN(32, a)),
293
+ 'i32.extend8_s': (a) => (a << 24) >> 24,
294
+ 'i32.extend16_s': (a) => (a << 16) >> 16,
384
295
 
385
296
  // 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'],
297
+ 'i64.add': (a, b) => BigInt.asIntN(64, a + b),
298
+ 'i64.sub': (a, b) => BigInt.asIntN(64, a - b),
299
+ 'i64.mul': (a, b) => BigInt.asIntN(64, a * b),
300
+ 'i64.div_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a / b) : null,
301
+ 'i64.div_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) / BigInt.asUintN(64, b)) : null,
302
+ 'i64.rem_s': (a, b) => b !== 0n ? BigInt.asIntN(64, a % b) : null,
303
+ 'i64.rem_u': (a, b) => b !== 0n ? BigInt.asUintN(64, BigInt.asUintN(64, a) % BigInt.asUintN(64, b)) : null,
304
+ 'i64.and': (a, b) => BigInt.asIntN(64, a & b),
305
+ 'i64.or': (a, b) => BigInt.asIntN(64, a | b),
306
+ 'i64.xor': (a, b) => BigInt.asIntN(64, a ^ b),
307
+ 'i64.shl': (a, b) => BigInt.asIntN(64, a << (b & 63n)),
308
+ 'i64.shr_s': (a, b) => BigInt.asIntN(64, a >> (b & 63n)),
309
+ 'i64.shr_u': (a, b) => BigInt.asUintN(64, BigInt.asUintN(64, a) >> (b & 63n)),
310
+ 'i64.eq': i64c((a, b) => a === b),
311
+ 'i64.ne': i64c((a, b) => a !== b),
312
+ 'i64.lt_s': i64c((a, b) => a < b),
313
+ 'i64.lt_u': u64c((a, b) => a < b),
314
+ 'i64.gt_s': i64c((a, b) => a > b),
315
+ 'i64.gt_u': u64c((a, b) => a > b),
316
+ 'i64.le_s': i64c((a, b) => a <= b),
317
+ 'i64.le_u': u64c((a, b) => a <= b),
318
+ 'i64.ge_s': i64c((a, b) => a >= b),
319
+ 'i64.ge_u': u64c((a, b) => a >= b),
320
+ 'i64.eqz': (a) => a === 0n ? 1 : 0,
321
+ 'i64.extend_i32_s': (a) => BigInt(a),
322
+ 'i64.extend_i32_u': (a) => BigInt(a >>> 0),
323
+ 'i64.extend8_s': (a) => BigInt.asIntN(64, BigInt.asIntN(8, a)),
324
+ 'i64.extend16_s': (a) => BigInt.asIntN(64, BigInt.asIntN(16, a)),
325
+ 'i64.extend32_s': (a) => BigInt.asIntN(64, BigInt.asIntN(32, a)),
415
326
 
416
327
  // 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'],
328
+ 'f32.add': (a, b) => Math.fround(a + b),
329
+ 'f32.sub': (a, b) => Math.fround(a - b),
330
+ 'f32.mul': (a, b) => Math.fround(a * b),
331
+ 'f32.div': (a, b) => Math.fround(a / b),
332
+ 'f32.neg': (a) => Math.fround(-a),
333
+ 'f32.abs': (a) => Math.fround(Math.abs(a)),
334
+ 'f32.sqrt': (a) => Math.fround(Math.sqrt(a)),
335
+ 'f32.ceil': (a) => Math.fround(Math.ceil(a)),
336
+ 'f32.floor': (a) => Math.fround(Math.floor(a)),
337
+ 'f32.trunc': (a) => Math.fround(Math.trunc(a)),
338
+ 'f32.nearest': (a) => Math.fround(roundEven(a)),
339
+
340
+ 'f64.add': (a, b) => a + b,
341
+ 'f64.sub': (a, b) => a - b,
342
+ 'f64.mul': (a, b) => a * b,
343
+ 'f64.div': (a, b) => a / b,
344
+ 'f64.neg': (a) => -a,
345
+ 'f64.abs': Math.abs,
346
+ 'f64.sqrt': Math.sqrt,
347
+ 'f64.ceil': Math.ceil,
348
+ 'f64.floor': Math.floor,
349
+ 'f64.trunc': Math.trunc,
350
+ 'f64.nearest': roundEven,
440
351
 
441
352
  // 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'],
353
+ 'i32.reinterpret_f32': i32FromF32,
354
+ 'f32.reinterpret_i32': f32FromI32,
355
+ 'i64.reinterpret_f64': i64FromF64,
356
+ 'f64.reinterpret_i64': f64FromI64,
446
357
 
447
358
  // 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'],
359
+ 'f32.convert_i32_s': (a) => Math.fround(a | 0),
360
+ 'f32.convert_i32_u': (a) => Math.fround(a >>> 0),
361
+ 'f32.convert_i64_s': (a) => Math.fround(Number(BigInt.asIntN(64, a))),
362
+ 'f32.convert_i64_u': (a) => Math.fround(Number(BigInt.asUintN(64, a))),
363
+ 'f64.convert_i32_s': (a) => (a | 0),
364
+ 'f64.convert_i32_u': (a) => (a >>> 0),
365
+ 'f64.convert_i64_s': (a) => Number(BigInt.asIntN(64, a)),
366
+ 'f64.convert_i64_u': (a) => Number(BigInt.asUintN(64, a)),
367
+ 'f32.demote_f64': (a) => Math.fround(a),
368
+ 'f64.promote_f32': (a) => Math.fround(a),
458
369
  }
459
370
 
460
371
  /**
@@ -487,8 +398,8 @@ const _parseNanF32 = (s, i = s?.indexOf?.('nan')) => {
487
398
  const getConst = (node) => {
488
399
  if (!Array.isArray(node) || node.length !== 2) return null
489
400
  const [op, val] = node
490
- if (op === 'i32.const') return { type: 'i32', value: Number(val) | 0 }
491
- if (op === 'i64.const') return { type: 'i64', value: BigInt(val) }
401
+ if (op === 'i32.const') return { type: 'i32', value: (typeof val === 'string' ? i32.parse(val) : val) | 0 }
402
+ if (op === 'i64.const') return { type: 'i64', value: typeof val === 'string' ? i64.parse(val) : BigInt(val) }
492
403
  if (op === 'f32.const') {
493
404
  const n = _parseNanF32(val)
494
405
  return { type: 'f32', value: n !== null ? n : Math.fround(Number(val)) }
@@ -522,9 +433,8 @@ const makeConst = (type, value) => {
522
433
  const fold = (ast) => {
523
434
  return walkPost(ast, (node) => {
524
435
  if (!Array.isArray(node)) return
525
- const entry = FOLDABLE[node[0]]
526
- if (!entry) return
527
- const [fn, t] = entry
436
+ const fn = FOLDABLE[node[0]]
437
+ if (!fn) return
528
438
 
529
439
  // Unary
530
440
  if (fn.length === 1 && node.length === 2) {
@@ -532,7 +442,7 @@ const fold = (ast) => {
532
442
  if (!a) return
533
443
  const r = fn(a.value)
534
444
  if (r === null) return
535
- return makeConst(t, r)
445
+ return makeConst(resultType(node[0]), r)
536
446
  }
537
447
  // Binary
538
448
  if (fn.length === 2 && node.length === 3) {
@@ -540,7 +450,7 @@ const fold = (ast) => {
540
450
  if (!a || !b) return
541
451
  const r = fn(a.value, b.value)
542
452
  if (r === null) return
543
- return makeConst(t, r)
453
+ return makeConst(resultType(node[0]), r)
544
454
  }
545
455
  })
546
456
  }
@@ -1344,8 +1254,7 @@ const inline = (ast) => {
1344
1254
  }
1345
1255
 
1346
1256
  // Substitute params with args
1347
- const substituted = clone(body)
1348
- walkPost(substituted, (n) => {
1257
+ const substituted = walkPost(clone(body), (n) => {
1349
1258
  if (!Array.isArray(n) || n[0] !== 'local.get') return
1350
1259
  const local = n[1]
1351
1260
  const paramIdx = params.findIndex(p => p.name === local)
@@ -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
@@ -1946,10 +1855,6 @@ const PEEPHOLE = {
1946
1855
  'i64.sub': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
1947
1856
  'i32.xor': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
1948
1857
  'i64.xor': (a, b) => equal(a, b) ? ['i64.const', 0n] : null,
1949
- 'i32.and': (a, b) => equal(a, b) ? a : null,
1950
- 'i64.and': (a, b) => equal(a, b) ? a : null,
1951
- 'i32.or': (a, b) => equal(a, b) ? a : null,
1952
- 'i64.or': (a, b) => equal(a, b) ? a : null,
1953
1858
  'i32.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1954
1859
  'i64.eq': (a, b) => equal(a, b) ? ['i32.const', 1] : null,
1955
1860
  'i32.ne': (a, b) => equal(a, b) ? ['i32.const', 0] : null,
@@ -1983,22 +1888,25 @@ const PEEPHOLE = {
1983
1888
  return null
1984
1889
  },
1985
1890
  'i32.and': (a, b) => {
1891
+ if (equal(a, b)) return a
1986
1892
  const ca = getConst(a), cb = getConst(b)
1987
1893
  if (ca?.value === 0 || cb?.value === 0) return ['i32.const', 0]
1988
- // x & x → x handled above in self-operands, but null here lets that win
1989
1894
  return null
1990
1895
  },
1991
1896
  'i64.and': (a, b) => {
1897
+ if (equal(a, b)) return a
1992
1898
  const ca = getConst(a), cb = getConst(b)
1993
1899
  if (ca?.value === 0n || cb?.value === 0n) return ['i64.const', 0n]
1994
1900
  return null
1995
1901
  },
1996
1902
  'i32.or': (a, b) => {
1903
+ if (equal(a, b)) return a
1997
1904
  const ca = getConst(a), cb = getConst(b)
1998
1905
  if (ca?.value === -1 || cb?.value === -1) return ['i32.const', -1]
1999
1906
  return null
2000
1907
  },
2001
1908
  'i64.or': (a, b) => {
1909
+ if (equal(a, b)) return a
2002
1910
  const ca = getConst(a), cb = getConst(b)
2003
1911
  if (ca?.value === -1n || cb?.value === -1n) return ['i64.const', -1n]
2004
1912
  return null
@@ -2920,6 +2828,66 @@ const reorder = (ast) => {
2920
2828
 
2921
2829
  // ==================== MAIN ====================
2922
2830
 
2831
+ /**
2832
+ * Optimization passes, in the order they run within each round. Each entry is
2833
+ * `[optionKey, fn, defaultOn, doc]` — the single source of truth that the
2834
+ * dispatch loop, the `OPTS` catalogue, and `normalize` all derive from.
2835
+ * Passes that are off by default can bloat output or are expensive — opt-in.
2836
+ */
2837
+ const PASSES = [
2838
+ ['stripmut', stripmut, true, 'strip mut from never-written globals'],
2839
+ ['globals', globals, true, 'propagate immutable global constants'],
2840
+ ['fold', fold, true, 'constant folding'],
2841
+ ['identity', identity, true, 'remove identity ops (x + 0 → x)'],
2842
+ ['peephole', peephole, true, 'x-x→0, x&0→0, etc.'],
2843
+ ['strength', strength, true, 'strength reduction (x * 2 → x << 1)'],
2844
+ ['branch', branch, true, 'simplify constant branches'],
2845
+ ['propagate', propagate, true, 'forward-propagate single-use locals & tiny consts (never inflates)'],
2846
+ ['inlineOnce', inlineOnce, true, 'inline single-call functions into their lone caller (never duplicates)'],
2847
+ ['inline', inline, false, 'inline tiny functions — can duplicate bodies'],
2848
+ ['offset', offset, true, 'fold add+const into load/store offset'],
2849
+ ['unbranch', unbranch, true, 'remove redundant br at end of own block'],
2850
+ ['loopify', loopify, true, 'collapse block+loop+brif while-idiom into loop+if'],
2851
+ ['brif', brif, true, 'if-then-br → br_if'],
2852
+ ['foldarms', foldarms, false, 'merge identical trailing if arms — can add block wrapper'],
2853
+ ['deadcode', deadcode, true, 'eliminate dead code after unreachable/br/return'],
2854
+ ['vacuum', vacuum, true, 'remove nops, drop-of-pure, empty branches'],
2855
+ ['mergeBlocks', mergeBlocks, true, 'unwrap `(block $L …)` whose label is never targeted'],
2856
+ ['coalesce', coalesceLocals, true, 'share local slots between same-type non-overlapping locals'],
2857
+ ['locals', localReuse, true, 'remove unused locals'],
2858
+ ['dedupe', dedupe, true, 'eliminate duplicate functions'],
2859
+ ['dedupTypes', dedupTypes, true, 'merge identical type definitions'],
2860
+ ['packData', packData, true, 'trim trailing zeros, merge adjacent data segments'],
2861
+ ['reorder', reorder, false, 'put hot functions first — no AST reduction'],
2862
+ ['treeshake', treeshake, true, 'remove unused funcs/globals/types/tables'],
2863
+ ['minifyImports', minifyImports, false, 'shorten import names — enable only when you control the host'],
2864
+ ]
2865
+
2866
+ /** Option name → default-on map — the public catalogue of passes. */
2867
+ const OPTS = Object.fromEntries(PASSES.map(p => [p[0], p[2]]))
2868
+
2869
+ /**
2870
+ * Normalize options to a { passName: bool } map. An explicit object is kept
2871
+ * as-is (preserving `log`/`verbose`), with any unmentioned pass filled to its
2872
+ * default; `true` selects the defaults; a string selects only the named
2873
+ * passes (or all of them via `'all'`).
2874
+ *
2875
+ * @param {boolean|string|Object} opts
2876
+ * @returns {Object}
2877
+ */
2878
+ const normalize = (opts) => {
2879
+ if (opts === false) return {}
2880
+ if (opts !== true && typeof opts !== 'string') {
2881
+ const m = { ...opts }
2882
+ for (const p of PASSES) if (m[p[0]] === undefined) m[p[0]] = p[2]
2883
+ return m
2884
+ }
2885
+ const set = typeof opts === 'string' ? new Set(opts.split(/\s+/).filter(Boolean)) : null
2886
+ const m = {}
2887
+ for (const p of PASSES) m[p[0]] = set ? (set.has('all') || set.has(p[0])) : p[2]
2888
+ return m
2889
+ }
2890
+
2923
2891
  /**
2924
2892
  * Optimize AST.
2925
2893
  *
@@ -2953,32 +2921,7 @@ export default function optimize(ast, opts = true) {
2953
2921
  beforeRound = clone(ast)
2954
2922
  const sizeBefore = binarySize(ast)
2955
2923
 
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)
2924
+ for (const [key, fn] of PASSES) if (opts[key]) ast = fn(ast)
2982
2925
  // Second propagate sweep: `inlineOnce`/`inline` (above) leave fresh
2983
2926
  // `(local.set $p arg) … (local.get $p)` wrappers around each inlined call;
2984
2927
  // re-running propagation collapses them within this same round, so the size