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/dist/watr.js +139 -100
- package/dist/watr.min.js +6 -6
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/compile.js +9 -2
- package/src/const.js +2 -2
- package/src/encode.js +33 -12
- package/src/optimize.js +131 -87
- package/types/src/const.d.ts +2 -0
- package/types/src/encode.d.ts.map +1 -1
- package/types/src/optimize.d.ts +13 -0
- package/types/src/optimize.d.ts.map +1 -1
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:
|
|
20
|
-
inline:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1125
|
+
if (inlinable.size === 0) return ast
|
|
1096
1126
|
|
|
1097
|
-
walkPost(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
1341
|
+
if (constGlobals.size === 0) return ast
|
|
1313
1342
|
|
|
1314
1343
|
// Substitute global.get with const
|
|
1315
|
-
return walkPost(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1663
|
+
if (redirects.size === 0) return ast
|
|
1639
1664
|
|
|
1640
1665
|
// Rewrite all references: calls, ref.func, elem segments, call_indirect type
|
|
1641
|
-
walkPost(
|
|
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
|
|
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
|
|
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
|
|
1721
|
+
if (redirects.size === 0) return ast
|
|
1698
1722
|
|
|
1699
1723
|
// Remove duplicate type nodes
|
|
1700
|
-
for (let i =
|
|
1701
|
-
const node =
|
|
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))
|
|
1728
|
+
if (name && redirects.has(name)) ast.splice(i, 1)
|
|
1705
1729
|
}
|
|
1706
1730
|
}
|
|
1707
1731
|
|
|
1708
|
-
walkPost(
|
|
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
|
|
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
|
|
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 <
|
|
1911
|
-
const node =
|
|
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
|
-
|
|
1963
|
+
ast = ast.filter((_, i) => !toRemove.has(i))
|
|
1941
1964
|
}
|
|
1942
1965
|
|
|
1943
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
2086
|
+
const strictGuard = opts === true // default: zero tolerance for bloat
|
|
2066
2087
|
opts = normalize(opts)
|
|
2067
2088
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
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
|
-
|
|
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 }
|
package/types/src/const.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/types/src/optimize.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|