watr 4.5.0 → 4.5.3

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
@@ -6,8 +6,10 @@
6
6
  */
7
7
 
8
8
  import parse from './parse.js'
9
+ import compile from './compile.js'
9
10
 
10
- /** Optimizations that can be applied */
11
+ /** Optimizations that can be applied.
12
+ * Passes defaulting to false can bloat output or are expensive — opt-in only. */
11
13
  const OPTS = {
12
14
  treeshake: true, // remove unused funcs/globals/types/tables
13
15
  fold: true, // constant folding
@@ -16,8 +18,8 @@ const OPTS = {
16
18
  identity: true, // remove identity ops (x + 0 → x)
17
19
  strength: true, // strength reduction (x * 2 → x << 1)
18
20
  branch: true, // simplify constant branches
19
- propagate: true, // constant propagation through locals
20
- inline: true, // inline tiny functions
21
+ propagate: false, // constant propagation can duplicate expressions
22
+ inline: false, // inline tiny functions — can duplicate bodies
21
23
  vacuum: true, // remove nops, drop-of-pure, empty branches
22
24
  peephole: true, // x-x→0, x&0→0, etc.
23
25
  globals: true, // propagate immutable global constants
@@ -25,11 +27,9 @@ const OPTS = {
25
27
  unbranch: true, // remove redundant br at end of own block
26
28
  stripmut: true, // strip mut from never-written globals
27
29
  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.
30
+ foldarms: false, // merge identical trailing if arms — can add block wrapper
31
31
  dedupe: true, // eliminate duplicate functions
32
- reorder: true, // put hot functions first for smaller LEBs
32
+ reorder: false, // put hot functions first no AST reduction
33
33
  dedupTypes: true, // merge identical type definitions
34
34
  packData: true, // trim trailing zeros, merge adjacent data segments
35
35
  minifyImports: false, // shorten import names — enable only when you control the host
@@ -38,6 +38,27 @@ const OPTS = {
38
38
  /** All optimization names */
39
39
  const ALL = Object.keys(OPTS)
40
40
 
41
+ /**
42
+ * Recursively count AST nodes — fast size heuristic without compiling.
43
+ * @param {any} node
44
+ * @returns {number}
45
+ */
46
+ const count = (node) => {
47
+ if (!Array.isArray(node)) return 1
48
+ let n = 1
49
+ for (let i = 0; i < node.length; i++) n += count(node[i])
50
+ return n
51
+ }
52
+
53
+ /**
54
+ * Compile AST and measure binary size in bytes.
55
+ * @param {Array} ast
56
+ * @returns {number}
57
+ */
58
+ const binarySize = (ast) => {
59
+ try { return compile(ast).length } catch { return Infinity }
60
+ }
61
+
41
62
  /**
42
63
  * Fast structural equality of two AST nodes.
43
64
  * Stops at first difference. Handles BigInt without stringification.
@@ -62,12 +83,9 @@ const normalize = (opts) => {
62
83
  if (opts === false) return {}
63
84
  if (typeof opts === 'string') {
64
85
  const set = new Set(opts.split(/\s+/).filter(Boolean))
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.
67
- if (set.size === 1 && ALL.includes([...set][0])) {
68
- return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
69
- }
70
- return Object.fromEntries(ALL.map(f => [f, set.has(f) || set.has('all')]))
86
+ if (set.has('all')) return Object.fromEntries(ALL.map(f => [f, true]))
87
+ // Explicit pass names enable ONLY those passes (not the full default set).
88
+ return Object.fromEntries(ALL.map(f => [f, set.has(f)]))
71
89
  }
72
90
  return { ...OPTS, ...opts }
73
91
  }
@@ -440,7 +458,7 @@ const makeConst = (type, value) => {
440
458
  * @returns {Array}
441
459
  */
442
460
  const fold = (ast) => {
443
- return walkPost(clone(ast), (node) => {
461
+ return walkPost(ast, (node) => {
444
462
  if (!Array.isArray(node)) return
445
463
  const entry = FOLDABLE[node[0]]
446
464
  if (!entry) return
@@ -526,7 +544,7 @@ const IDENTITIES = {
526
544
  * @returns {Array}
527
545
  */
528
546
  const identity = (ast) => {
529
- return walkPost(clone(ast), (node) => {
547
+ return walkPost(ast, (node) => {
530
548
  if (!Array.isArray(node) || node.length !== 3) return
531
549
  const fn = IDENTITIES[node[0]]
532
550
  if (!fn) return
@@ -544,7 +562,7 @@ const identity = (ast) => {
544
562
  * @returns {Array}
545
563
  */
546
564
  const strength = (ast) => {
547
- return walkPost(clone(ast), (node) => {
565
+ return walkPost(ast, (node) => {
548
566
  if (!Array.isArray(node) || node.length !== 3) return
549
567
  const [op, a, b] = node
550
568
 
@@ -614,7 +632,7 @@ const strength = (ast) => {
614
632
  * @returns {Array}
615
633
  */
616
634
  const branch = (ast) => {
617
- return walkPost(clone(ast), (node) => {
635
+ return walkPost(ast, (node) => {
618
636
  if (!Array.isArray(node)) return
619
637
  const op = node[0]
620
638
 
@@ -665,10 +683,8 @@ const TERMINATORS = new Set(['unreachable', 'return', 'br', 'br_table'])
665
683
  * @returns {Array}
666
684
  */
667
685
  const deadcode = (ast) => {
668
- const result = clone(ast)
669
-
670
686
  // Process each function body
671
- walk(result, (node) => {
687
+ walk(ast, (node) => {
672
688
  if (!Array.isArray(node)) return
673
689
  const kind = node[0]
674
690
 
@@ -686,7 +702,7 @@ const deadcode = (ast) => {
686
702
  }
687
703
  })
688
704
 
689
- return result
705
+ return ast
690
706
  }
691
707
 
692
708
  /**
@@ -741,9 +757,7 @@ const eliminateDeadInBlock = (block) => {
741
757
  * @returns {Array}
742
758
  */
743
759
  const localReuse = (ast) => {
744
- const result = clone(ast)
745
-
746
- walk(result, (node) => {
760
+ walk(ast, (node) => {
747
761
  if (!Array.isArray(node) || node[0] !== 'func') return
748
762
 
749
763
  // Collect local declarations and their types
@@ -792,7 +806,7 @@ const localReuse = (ast) => {
792
806
  }
793
807
  })
794
808
 
795
- return result
809
+ return ast
796
810
  }
797
811
 
798
812
  // ==================== PROPAGATION & LOCAL ELIMINATION ====================
@@ -870,8 +884,9 @@ const forwardPropagate = (funcNode, params, useCounts) => {
870
884
 
871
885
  if (op === 'param' || op === 'result' || op === 'local' || op === 'type' || op === 'export') continue
872
886
 
873
- // Track local.set values
874
- if (op === 'local.set' && instr.length === 3 && typeof instr[1] === 'string') {
887
+ // Track local.set / local.tee values (tee writes too — its result also leaves
888
+ // the value on the stack but the local is updated identically to set).
889
+ if ((op === 'local.set' || op === 'local.tee') && instr.length === 3 && typeof instr[1] === 'string') {
875
890
  substGets(instr[2], known) // substitute known values in RHS
876
891
  const uses = getUseCount(instr[1])
877
892
  known.set(instr[1], {
@@ -902,6 +917,14 @@ const forwardPropagate = (funcNode, params, useCounts) => {
902
917
  const prev = clone(instr)
903
918
  substGets(instr, known)
904
919
  if (!equal(prev, instr)) changed = true
920
+ // Invalidate tracking for any names written by a nested set/tee — those
921
+ // writes happened mid-expression and the substGets above used the
922
+ // pre-write tracked value (correct), but later reads must see the new
923
+ // (untracked) value, not the stale constant.
924
+ walk(instr, n => {
925
+ if (Array.isArray(n) && (n[0] === 'local.set' || n[0] === 'local.tee') && typeof n[1] === 'string')
926
+ known.delete(n[1])
927
+ })
905
928
  }
906
929
  }
907
930
 
@@ -1002,9 +1025,7 @@ const eliminateDeadStores = (funcNode, params, useCounts) => {
1002
1025
  * Multi-pass with batch counting for convergence.
1003
1026
  */
1004
1027
  const propagate = (ast) => {
1005
- const result = clone(ast)
1006
-
1007
- walk(result, (funcNode) => {
1028
+ walk(ast, (funcNode) => {
1008
1029
  if (!Array.isArray(funcNode) || funcNode[0] !== 'func') return
1009
1030
 
1010
1031
  const params = new Set()
@@ -1024,7 +1045,7 @@ const propagate = (ast) => {
1024
1045
  }
1025
1046
  })
1026
1047
 
1027
- return result
1048
+ return ast
1028
1049
  }
1029
1050
 
1030
1051
  // ==================== FUNCTION INLINING ====================
@@ -1036,12 +1057,11 @@ const propagate = (ast) => {
1036
1057
  */
1037
1058
  const inline = (ast) => {
1038
1059
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1039
- const result = clone(ast)
1040
1060
 
1041
1061
  // Collect inlinable functions
1042
1062
  const inlinable = new Map() // $name → { body, params }
1043
1063
 
1044
- for (const node of result.slice(1)) {
1064
+ for (const node of ast.slice(1)) {
1045
1065
  if (!Array.isArray(node) || node[0] !== 'func') continue
1046
1066
 
1047
1067
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
@@ -1076,25 +1096,35 @@ const inline = (ast) => {
1076
1096
 
1077
1097
  // Inline: no locals, <= 4 params, single expression body, not exported
1078
1098
  if (params && !hasLocals && !hasExport && params.length <= 4 && body.length === 1) {
1079
- // Check if function mutates any of its params (local.set/tee on param)
1099
+ // Check if function mutates any of its params (local.set/tee on param),
1100
+ // or contains a control-transfer op (`return`, `return_call`,
1101
+ // `return_call_indirect`). Inlining such bodies into a different-typed
1102
+ // caller would propagate the transfer to the caller, returning from the
1103
+ // wrong function with the wrong type. Lifting the body into a
1104
+ // `(block $exit ...)` and rewriting returns to `(br $exit X)` would
1105
+ // unlock these — left for a future pass.
1080
1106
  const paramNames = new Set(params.map(p => p.name))
1081
1107
  let mutatesParam = false
1108
+ let hasReturn = false
1082
1109
  walk(body[0], (n) => {
1083
1110
  if (!Array.isArray(n)) return
1084
1111
  if ((n[0] === 'local.set' || n[0] === 'local.tee') && paramNames.has(n[1])) {
1085
1112
  mutatesParam = true
1086
1113
  }
1114
+ if (n[0] === 'return' || n[0] === 'return_call' || n[0] === 'return_call_indirect') {
1115
+ hasReturn = true
1116
+ }
1087
1117
  })
1088
- if (!mutatesParam) {
1118
+ if (!mutatesParam && !hasReturn) {
1089
1119
  inlinable.set(name, { body: body[0], params })
1090
1120
  }
1091
1121
  }
1092
1122
  }
1093
1123
 
1094
1124
  // Replace calls with inlined body
1095
- if (inlinable.size === 0) return result
1125
+ if (inlinable.size === 0) return ast
1096
1126
 
1097
- walkPost(result, (node) => {
1127
+ walkPost(ast, (node) => {
1098
1128
  if (!Array.isArray(node) || node[0] !== 'call') return
1099
1129
  const fname = node[1]
1100
1130
  if (!inlinable.has(fname)) return
@@ -1121,7 +1151,7 @@ const inline = (ast) => {
1121
1151
  return substituted
1122
1152
  })
1123
1153
 
1124
- return result
1154
+ return ast
1125
1155
  }
1126
1156
 
1127
1157
  // ==================== VACUUM ====================
@@ -1133,7 +1163,7 @@ const inline = (ast) => {
1133
1163
  * @returns {Array}
1134
1164
  */
1135
1165
  const vacuum = (ast) => {
1136
- return walkPost(clone(ast), (node) => {
1166
+ return walkPost(ast, (node) => {
1137
1167
  if (!Array.isArray(node)) return
1138
1168
  const op = node[0]
1139
1169
 
@@ -1259,7 +1289,7 @@ const PEEPHOLE = {
1259
1289
  * @returns {Array}
1260
1290
  */
1261
1291
  const peephole = (ast) => {
1262
- return walkPost(clone(ast), (node) => {
1292
+ return walkPost(ast, (node) => {
1263
1293
  if (!Array.isArray(node) || node.length !== 3) return
1264
1294
  const fn = PEEPHOLE[node[0]]
1265
1295
  if (!fn) return
@@ -1277,13 +1307,12 @@ const peephole = (ast) => {
1277
1307
  */
1278
1308
  const globals = (ast) => {
1279
1309
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1280
- const result = clone(ast)
1281
1310
 
1282
1311
  // Find immutable globals with const init
1283
1312
  const constGlobals = new Map() // name → const node
1284
1313
  const mutableGlobals = new Set()
1285
1314
 
1286
- for (const node of result.slice(1)) {
1315
+ for (const node of ast.slice(1)) {
1287
1316
  if (!Array.isArray(node) || node[0] !== 'global') continue
1288
1317
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1289
1318
  if (!name) continue
@@ -1301,7 +1330,7 @@ const globals = (ast) => {
1301
1330
  }
1302
1331
 
1303
1332
  // Also mark any global that is ever written as mutable
1304
- walk(result, (n) => {
1333
+ walk(ast, (n) => {
1305
1334
  if (!Array.isArray(n) || n[0] !== 'global.set') return
1306
1335
  const ref = n[1]
1307
1336
  if (typeof ref === 'string' && ref[0] === '$') mutableGlobals.add(ref)
@@ -1309,10 +1338,10 @@ const globals = (ast) => {
1309
1338
 
1310
1339
  // Remove mutable ones from propagation set
1311
1340
  for (const name of mutableGlobals) constGlobals.delete(name)
1312
- if (constGlobals.size === 0) return result
1341
+ if (constGlobals.size === 0) return ast
1313
1342
 
1314
1343
  // Substitute global.get with const
1315
- return walkPost(result, (node) => {
1344
+ return walkPost(ast, (node) => {
1316
1345
  if (!Array.isArray(node) || node[0] !== 'global.get' || node.length !== 2) return
1317
1346
  const ref = node[1]
1318
1347
  if (constGlobals.has(ref)) return clone(constGlobals.get(ref))
@@ -1323,7 +1352,7 @@ const globals = (ast) => {
1323
1352
 
1324
1353
  /** Match (type.load/store (i32.add ptr (type.const N))) and fold offset */
1325
1354
  const offset = (ast) => {
1326
- return walkPost(clone(ast), (node) => {
1355
+ return walkPost(ast, (node) => {
1327
1356
  if (!Array.isArray(node)) return
1328
1357
  const op = node[0]
1329
1358
  if (typeof op !== 'string' || (!op.endsWith('load') && !op.endsWith('store'))) return
@@ -1396,9 +1425,7 @@ const offset = (ast) => {
1396
1425
  * @returns {Array}
1397
1426
  */
1398
1427
  const unbranch = (ast) => {
1399
- const result = clone(ast)
1400
-
1401
- walk(result, (node) => {
1428
+ walk(ast, (node) => {
1402
1429
  if (!Array.isArray(node)) return
1403
1430
  const op = node[0]
1404
1431
  // Loops: `br $loop_label` jumps BACK to loop top (continue), not out.
@@ -1435,7 +1462,7 @@ const unbranch = (ast) => {
1435
1462
  }
1436
1463
  })
1437
1464
 
1438
- return result
1465
+ return ast
1439
1466
  }
1440
1467
 
1441
1468
  // ==================== STRIP MUT FROM GLOBALS ====================
@@ -1448,14 +1475,13 @@ const unbranch = (ast) => {
1448
1475
  */
1449
1476
  const stripmut = (ast) => {
1450
1477
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1451
- const result = clone(ast)
1452
1478
 
1453
1479
  const written = new Set()
1454
- walk(result, (n) => {
1480
+ walk(ast, (n) => {
1455
1481
  if (Array.isArray(n) && n[0] === 'global.set' && typeof n[1] === 'string') written.add(n[1])
1456
1482
  })
1457
1483
 
1458
- return walkPost(result, (node) => {
1484
+ return walkPost(ast, (node) => {
1459
1485
  if (!Array.isArray(node) || node[0] !== 'global') return
1460
1486
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1461
1487
  if (!name || written.has(name)) return
@@ -1480,7 +1506,7 @@ const stripmut = (ast) => {
1480
1506
  * @returns {Array}
1481
1507
  */
1482
1508
  const brif = (ast) => {
1483
- return walkPost(clone(ast), (node) => {
1509
+ return walkPost(ast, (node) => {
1484
1510
  if (!Array.isArray(node) || node[0] !== 'if') return
1485
1511
  const { cond, thenBranch, elseBranch } = parseIf(node)
1486
1512
  const thenEmpty = !thenBranch || thenBranch.length <= 1
@@ -1509,7 +1535,7 @@ const brif = (ast) => {
1509
1535
  * @returns {Array}
1510
1536
  */
1511
1537
  const foldarms = (ast) => {
1512
- return walkPost(clone(ast), (node) => {
1538
+ return walkPost(ast, (node) => {
1513
1539
  if (!Array.isArray(node) || node[0] !== 'if') return
1514
1540
  const { thenBranch, elseBranch } = parseIf(node)
1515
1541
  if (!thenBranch || !elseBranch) return
@@ -1601,13 +1627,12 @@ const hashFunc = (node, localNames) => {
1601
1627
  */
1602
1628
  const dedupe = (ast) => {
1603
1629
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1604
- const result = clone(ast)
1605
1630
 
1606
1631
  // Hash function bodies (normalize local/param names to avoid false negatives)
1607
1632
  const signatures = new Map() // hash → canonical $name
1608
1633
  const redirects = new Map() // duplicate $name → canonical $name
1609
1634
 
1610
- for (const node of result.slice(1)) {
1635
+ for (const node of ast.slice(1)) {
1611
1636
  if (!Array.isArray(node) || node[0] !== 'func') continue
1612
1637
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1613
1638
  if (!name) continue
@@ -1635,10 +1660,10 @@ const dedupe = (ast) => {
1635
1660
  }
1636
1661
  }
1637
1662
 
1638
- if (redirects.size === 0) return result
1663
+ if (redirects.size === 0) return ast
1639
1664
 
1640
1665
  // Rewrite all references: calls, ref.func, elem segments, call_indirect type
1641
- walkPost(result, (node) => {
1666
+ walkPost(ast, (node) => {
1642
1667
  if (!Array.isArray(node)) return
1643
1668
  const op = node[0]
1644
1669
  if ((op === 'call' || op === 'return_call') && redirects.has(node[1])) {
@@ -1661,7 +1686,7 @@ const dedupe = (ast) => {
1661
1686
  }
1662
1687
  })
1663
1688
 
1664
- return result
1689
+ return ast
1665
1690
  }
1666
1691
 
1667
1692
  // ==================== TYPE DEDUPLICATION ====================
@@ -1674,12 +1699,11 @@ const dedupe = (ast) => {
1674
1699
  */
1675
1700
  const dedupTypes = (ast) => {
1676
1701
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1677
- const result = clone(ast)
1678
1702
 
1679
1703
  const signatures = new Map() // hash → canonical $name
1680
1704
  const redirects = new Map() // duplicate $name → canonical $name
1681
1705
 
1682
- for (const node of result.slice(1)) {
1706
+ for (const node of ast.slice(1)) {
1683
1707
  if (!Array.isArray(node) || node[0] !== 'type') continue
1684
1708
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1685
1709
  if (!name) continue
@@ -1694,18 +1718,18 @@ const dedupTypes = (ast) => {
1694
1718
  }
1695
1719
  }
1696
1720
 
1697
- if (redirects.size === 0) return result
1721
+ if (redirects.size === 0) return ast
1698
1722
 
1699
1723
  // Remove duplicate type nodes
1700
- for (let i = result.length - 1; i >= 0; i--) {
1701
- const node = result[i]
1724
+ for (let i = ast.length - 1; i >= 0; i--) {
1725
+ const node = ast[i]
1702
1726
  if (Array.isArray(node) && node[0] === 'type') {
1703
1727
  const name = typeof node[1] === 'string' && node[1][0] === '$' ? node[1] : null
1704
- if (name && redirects.has(name)) result.splice(i, 1)
1728
+ if (name && redirects.has(name)) ast.splice(i, 1)
1705
1729
  }
1706
1730
  }
1707
1731
 
1708
- walkPost(result, (node) => {
1732
+ walkPost(ast, (node) => {
1709
1733
  if (!Array.isArray(node)) return
1710
1734
  const op = node[0]
1711
1735
 
@@ -1745,7 +1769,7 @@ const dedupTypes = (ast) => {
1745
1769
  }
1746
1770
  })
1747
1771
 
1748
- return result
1772
+ return ast
1749
1773
  }
1750
1774
 
1751
1775
  // ==================== DATA SEGMENT PACKING ====================
@@ -1885,10 +1909,9 @@ const mergeDataSegments = (a, b) => {
1885
1909
  */
1886
1910
  const packData = (ast) => {
1887
1911
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1888
- let result = clone(ast)
1889
1912
 
1890
1913
  // Trim trailing zeros
1891
- for (const node of result) {
1914
+ for (const node of ast) {
1892
1915
  if (!Array.isArray(node) || node[0] !== 'data') continue
1893
1916
  let contentStart = 1
1894
1917
  if (typeof node[1] === 'string' && node[1][0] === '$') contentStart = 2
@@ -1907,8 +1930,8 @@ const packData = (ast) => {
1907
1930
 
1908
1931
  // Merge adjacent active segments with same memory and consecutive offsets
1909
1932
  const dataNodes = []
1910
- for (let i = 0; i < result.length; i++) {
1911
- const node = result[i]
1933
+ for (let i = 0; i < ast.length; i++) {
1934
+ const node = ast[i]
1912
1935
  if (Array.isArray(node) && node[0] === 'data') {
1913
1936
  const info = getDataOffset(node)
1914
1937
  if (info) {
@@ -1937,10 +1960,10 @@ const packData = (ast) => {
1937
1960
  }
1938
1961
 
1939
1962
  if (toRemove.size > 0) {
1940
- result = result.filter((_, i) => !toRemove.has(i))
1963
+ ast = ast.filter((_, i) => !toRemove.has(i))
1941
1964
  }
1942
1965
 
1943
- return result
1966
+ return ast
1944
1967
  }
1945
1968
 
1946
1969
  // ==================== IMPORT FIELD MINIFICATION ====================
@@ -1970,11 +1993,10 @@ const makeShortener = () => {
1970
1993
  */
1971
1994
  const minifyImports = (ast) => {
1972
1995
  if (!Array.isArray(ast) || ast[0] !== 'module') return ast
1973
- const result = clone(ast)
1974
1996
  const shortMod = makeShortener()
1975
1997
  const shortField = makeShortener()
1976
1998
 
1977
- for (const node of result) {
1999
+ for (const node of ast) {
1978
2000
  if (!Array.isArray(node) || node[0] !== 'import') continue
1979
2001
  if (typeof node[1] === 'string' && node[1][0] === '"') {
1980
2002
  node[1] = '"' + shortMod(node[1].slice(1, -1)) + '"'
@@ -1984,7 +2006,7 @@ const minifyImports = (ast) => {
1984
2006
  }
1985
2007
  }
1986
2008
 
1987
- return result
2009
+ return ast
1988
2010
  }
1989
2011
 
1990
2012
  // ==================== REORDER FUNCTIONS ====================
@@ -2023,10 +2045,9 @@ const reorder = (ast) => {
2023
2045
  // Sorting changes the function index space. Skip if any reference is numeric,
2024
2046
  // since we'd silently retarget unnamed callers/start/elem entries.
2025
2047
  if (!reorderSafe(ast)) return ast
2026
- const result = clone(ast)
2027
2048
 
2028
2049
  const callCounts = new Map()
2029
- walk(result, (n) => {
2050
+ walk(ast, (n) => {
2030
2051
  if (!Array.isArray(n)) return
2031
2052
  if (n[0] === 'call' || n[0] === 'return_call') {
2032
2053
  callCounts.set(n[1], (callCounts.get(n[1]) || 0) + 1)
@@ -2035,7 +2056,7 @@ const reorder = (ast) => {
2035
2056
 
2036
2057
  // Imports must precede defined funcs (compile.js assigns indices in AST order).
2037
2058
  const imports = [], funcs = [], others = []
2038
- for (const node of result.slice(1)) {
2059
+ for (const node of ast.slice(1)) {
2039
2060
  if (!Array.isArray(node)) { others.push(node); continue }
2040
2061
  if (node[0] === 'import') imports.push(node)
2041
2062
  else if (node[0] === 'func') funcs.push(node)
@@ -2062,14 +2083,18 @@ const reorder = (ast) => {
2062
2083
  */
2063
2084
  export default function optimize(ast, opts = true) {
2064
2085
  if (typeof ast === 'string') ast = parse(ast)
2065
- ast = clone(ast)
2086
+ const strictGuard = opts === true // default: zero tolerance for bloat
2066
2087
  opts = normalize(opts)
2067
2088
 
2068
- // Each pass clones its input before mutating, so the original `before`
2069
- // reference stays untouched and can be used for the convergence check
2070
- // without an extra deep clone.
2071
- for (let round = 0; round < 6; round++) {
2072
- const before = ast
2089
+ const log = opts.log ? (msg, delta) => opts.log(msg, delta) : () => {}
2090
+ const verbose = opts.verbose || opts.log
2091
+
2092
+ ast = clone(ast)
2093
+ let beforeRound = null
2094
+
2095
+ for (let round = 0; round < 3; round++) {
2096
+ beforeRound = clone(ast)
2097
+ const sizeBefore = count(ast)
2073
2098
 
2074
2099
  if (opts.stripmut) ast = stripmut(ast)
2075
2100
  if (opts.globals) ast = globals(ast)
@@ -2093,10 +2118,29 @@ export default function optimize(ast, opts = true) {
2093
2118
  if (opts.reorder) ast = reorder(ast)
2094
2119
  if (opts.treeshake) ast = treeshake(ast)
2095
2120
  if (opts.minifyImports) ast = minifyImports(ast)
2096
- if (equal(before, ast)) break
2121
+
2122
+ const sizeAfter = count(ast)
2123
+ const delta = sizeAfter - sizeBefore
2124
+
2125
+ if (verbose || delta !== 0) {
2126
+ log(` round ${round + 1}: ${delta > 0 ? '+' : ''}${delta} nodes`, delta)
2127
+ }
2128
+
2129
+ // Size guard: default optimize must never inflate. Explicit passes
2130
+ // get leniency (+5 nodes) so inline/propagate/foldarms can chain.
2131
+ const tolerance = strictGuard ? 0 : 5
2132
+ if (delta > tolerance) {
2133
+ if (verbose) log(` ⚠ round ${round + 1} inflated by ${delta}, reverting`, delta)
2134
+ ast = beforeRound
2135
+ break
2136
+ }
2137
+
2138
+ if (equal(beforeRound, ast)) break
2097
2139
  }
2098
2140
 
2099
2141
  return ast
2100
2142
  }
2101
2143
 
2144
+ /** Count AST nodes (fast size heuristic). */
2145
+ export { count as size, count, binarySize }
2102
2146
  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 }
@@ -41,6 +41,8 @@ export namespace TYPE {
41
41
  export let i31: number;
42
42
  export let struct: number;
43
43
  export let array: number;
44
+ let data_1: number;
45
+ export { data_1 as data };
44
46
  export let cont: number;
45
47
  export let nocont: number;
46
48
  export let string: number;
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAoBpB;;IAID,4BAMC;;AAGD,gEAaC;;IA0DD,mCAA6D;;AAvD7D,gEAaC;;IAED,iDAsCC;;AA/LM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AA6HM,wCAKN"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/encode.js"],"names":[],"mappings":"AA6CA;;;;;;GAMG;AACH,6BAHW,MAAM,GACJ,MAAM,EAAE,CAapB;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;IAQD,4BAIC;;AAED;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAoBpB;;IAID,4BAmBC;;AAGD,gEAcC;;IAiED,mCAA6D;;AA9D7D,gEAcC;;IAED,iDA4CC;;AApNM,wBAJI,MAAM,GAAC,MAAM,GAAC,MAAM,GAAC,IAAI,WACzB,MAAM,EAAE,GACN,MAAM,EAAE,CA8BpB;AAsBD;;;;;;GAMG;AACH,sBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AApBD;;;;;;GAMG;AACH,uBAJW,MAAM,GAAC,MAAM,WACb,MAAM,EAAE,GACN,MAAM,EAAE,CAepB;;AAkJM,wCAKN"}
@@ -11,6 +11,18 @@
11
11
  * optimize(ast, { fold: true }) // explicit
12
12
  */
13
13
  export default function optimize(ast: any[] | string, opts?: boolean | string | any): any[];
14
+ /**
15
+ * Recursively count AST nodes — fast size heuristic without compiling.
16
+ * @param {any} node
17
+ * @returns {number}
18
+ */
19
+ export function count(node: any): number;
20
+ /**
21
+ * Compile AST and measure binary size in bytes.
22
+ * @param {Array} ast
23
+ * @returns {number}
24
+ */
25
+ export function binarySize(ast: any[]): number;
14
26
  /**
15
27
  * Remove unused functions, globals, types, tables.
16
28
  * Keeps exports and their transitive dependencies.
@@ -174,4 +186,5 @@ export function packData(ast: any[]): any[];
174
186
  * @returns {Array}
175
187
  */
176
188
  export function minifyImports(ast: any[]): any[];
189
+ export { count as size };
177
190
  //# sourceMappingURL=optimize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/optimize.js"],"names":[],"mappings":"AAkgEA;;;;;;;;;;;GAWG;AACH,sCATW,QAAM,MAAM,SACZ,OAAO,GAAC,MAAM,MAAO,SA6C/B;AAl6DD;;;;;GAKG;AACH,6CAgJC;AA6ID;;;;GAIG;AACH,wCAwBC;AAoMD;;;;GAIG;AACH,4CAuBC;AA+CD;;;;;GAKG;AACH,8CAqDC;AAjRD;;;;GAIG;AACH,4CASC;AAID;;;;GAIG;AACH,4CA6DC;AAID;;;;GAIG;AACH,0CAuCC;AAwVD;;;;GAIG;AACH,yCAwBC;AAID;;;;GAIG;AACH,0CAwFC;AA9iCD;;;;GAIG;AACH,gCAHW,OAAO,GAAC,MAAM,MAAO,OAgB/B;;;;;;;;;;;;;;;;;;;;;;;;;AAgiCD;;;;;GAKG;AACH,0CAgDC;AAyED;;;;GAIG;AACH,4CAQC;AAID;;;;GAIG;AACH,2CA0CC;AAID,2EAA2E;AAC3E,sCAgEC;AAID;;;;GAIG;AACH,4CAyCC;AAID;;;;;GAKG;AACH,4CAsBC;AAID;;;;;;GAMG;AACH,wCAmBC;AAID;;;;;GAKG;AACH,4CAiDC;AAoCD;;;;;GAKG;AACH,0CA+DC;AAoWD,uCA0BC;AA1XD;;;;;GAKG;AACH,8CA0EC;AAoID;;;;GAIG;AACH,4CA0DC;AAqBD;;;;;GAKG;AACH,iDAiBC"}
1
+ {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/optimize.js"],"names":[],"mappings":"AAuhEA;;;;;;;;;;;GAWG;AACH,sCATW,QAAM,MAAM,SACZ,OAAO,GAAC,MAAM,MAAO,SAkE/B;AArjED;;;;GAIG;AACH,4BAHW,GAAG,GACD,MAAM,CAOlB;AAED;;;;GAIG;AACH,wCAFa,MAAM,CAIlB;AAwGD;;;;;GAKG;AACH,6CAgJC;AA6ID;;;;GAIG;AACH,wCAwBC;AAoMD;;;;GAIG;AACH,4CAqBC;AA+CD;;;;;GAKG;AACH,8CAmDC;AA7QD;;;;GAIG;AACH,4CASC;AAID;;;;GAIG;AACH,4CA6DC;AAID;;;;GAIG;AACH,0CAuCC;AA6VD;;;;GAIG;AACH,yCAsBC;AAID;;;;GAIG;AACH,0CAiGC;AAvjCD;;;;GAIG;AACH,gCAHW,OAAO,GAAC,MAAM,MAAO,OAa/B;;;;;;;;;;;;;;;;;;;;;;;;;AA4iCD;;;;;GAKG;AACH,0CAgDC;AAyED;;;;GAIG;AACH,4CAQC;AAID;;;;GAIG;AACH,2CAyCC;AAID,2EAA2E;AAC3E,sCAgEC;AAID;;;;GAIG;AACH,4CAuCC;AAID;;;;;GAKG;AACH,4CAqBC;AAID;;;;;;GAMG;AACH,wCAmBC;AAID;;;;;GAKG;AACH,4CAiDC;AAoCD;;;;;GAKG;AACH,0CA8DC;AAiWD,uCAyBC;AAtXD;;;;;GAKG;AACH,8CAyEC;AAoID;;;;GAIG;AACH,4CAyDC;AAqBD;;;;;GAKG;AACH,iDAgBC"}