Skip to content

Commit 63c648f

Browse files
authored
Ensure all unused elements are removed during applyDCEGraphRemovals. NFC (#21840)
There were bugs in `applyDCEGraphRemovals` that were preventing it from finding certain unused exports in certain cases. This change cleanup up the pass and adds assertions that all unused imports and exports are actually located and removed, thus preventing these types of bugs from sneaking in again. The test we had for `applyDCEGraphRemovals` were worked, but they were writing in a slightly different style to the current emscripten output. In particular the export name and JS name were matching, even though the actual compiler output produces JS names that contain a leading underscore. I also updates the tests to match this style for consistency. I believe the effects of this bug were not captured by our code size tests because they all run closure compiler afterwards which was removing the unused exports itself.
1 parent 286c206 commit 63c648f

8 files changed

+164
-126
lines changed

test/optimizer/applyDCEGraphRemovals-output.js

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,30 @@ var wasmImports = {
55
save2: 2
66
};
77

8-
var expD1 = Module["expD1"] = wasmExports["expD1"];
8+
var _expD1 = Module["_expD1"] = wasmExports["expD1"];
99

10-
var expD2 = Module["expD2"] = wasmExports["expD2"];
10+
var _expD2 = Module["_expD2"] = wasmExports["expD2"];
1111

12-
var expD3 = Module["expD3"] = wasmExports["expD3"];
12+
var _expD3 = Module["_expD3"] = wasmExports["expD3"];
1313

14-
var expD4;
14+
var _expD5 = wasmExports["expD5"];
1515

16-
var expD5 = wasmExports["expD5"];
16+
var _expI1 = Module["_expI1"] = () => (expI1 = Module["_expI1"] = wasmExports["expI1"])();
1717

18-
var expD6;
18+
var _expI2 = Module["_expI2"] = () => (expI2 = Module["_expI2"] = wasmExports["expI2"])();
1919

20-
var expI1 = Module["expI1"] = () => (expI1 = Module["expI1"] = wasmExports["expI1"])();
20+
var _expI3 = Module["_expI3"] = () => (expI3 = Module["_expI3"] = wasmExports["expI3"])();
2121

22-
var expI2 = Module["expI2"] = () => (expI2 = Module["expI2"] = wasmExports["expI2"])();
22+
var _expI5 = () => (_expI5 = wasmExports["expI5"])();
2323

24-
var expI3 = Module["expI3"] = () => (expI3 = Module["expI3"] = wasmExports["expI3"])();
24+
_expD1;
2525

26-
var expI4;
26+
Module["_expD2"];
2727

28-
var expI5 = () => (expI5 = wasmExports["expI5"])();
28+
wasmExports["_expD3"];
2929

30-
var expI6;
30+
_expI1;
3131

32-
expD1;
32+
Module["_expI2"];
3333

34-
Module["expD2"];
35-
36-
wasmExports["expD3"];
37-
38-
expI1;
39-
40-
Module["expI2"];
41-
42-
wasmExports["expI3"];
34+
wasmExports["_expI3"];
Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
11
var name;
2-
var wasmImports = { save1: 1, number: 33, name: name, func: function() {}, save2: 2 };
2+
var wasmImports = {
3+
save1: 1,
4+
number: 33,
5+
name: name,
6+
func: function() {},
7+
save2: 2
8+
};
39

410
// exports gotten directly
5-
var expD1 = Module['expD1'] = wasmExports['expD1'];
6-
var expD2 = Module['expD2'] = wasmExports['expD2'];
7-
var expD3 = Module['expD3'] = wasmExports['expD3'];
8-
var expD4 = Module['expD4'] = wasmExports['expD4'];
11+
var _expD1 = Module['_expD1'] = wasmExports['expD1'];
12+
var _expD2 = Module['_expD2'] = wasmExports['expD2'];
13+
var _expD3 = Module['_expD3'] = wasmExports['expD3'];
14+
var _expD4 = Module['_expD4'] = wasmExports['expD4'];
915
// Like above, but not exported on the Module
10-
var expD5 = wasmExports['expD5'];
11-
var expD6 = wasmExports['expD6'];
16+
var _expD5 = wasmExports['expD5'];
17+
var _expD6 = wasmExports['expD6'];
1218

1319
// exports gotten indirectly (async compilation
14-
var expI1 = Module['expI1'] = () => (expI1 = Module['expI1'] = wasmExports['expI1'])();
15-
var expI2 = Module['expI2'] = () => (expI2 = Module['expI2'] = wasmExports['expI2'])();
16-
var expI3 = Module['expI3'] = () => (expI3 = Module['expI3'] = wasmExports['expI3'])();
17-
var expI4 = Module['expI4'] = () => (expI4 = Module['expI4'] = wasmExports['expI4'])();
20+
var _expI1 = Module['_expI1'] = () => (expI1 = Module['_expI1'] = wasmExports['expI1'])();
21+
var _expI2 = Module['_expI2'] = () => (expI2 = Module['_expI2'] = wasmExports['expI2'])();
22+
var _expI3 = Module['_expI3'] = () => (expI3 = Module['_expI3'] = wasmExports['expI3'])();
23+
var _expI4 = Module['_expI4'] = () => (expI4 = Module['_expI4'] = wasmExports['expI4'])();
1824

1925
// Like above, but not exported on the Module
20-
var expI5 = () => (expI5 = wasmExports['expI5'])();
21-
var expI6 = () => (expI6 = wasmExports['expI6'])();
26+
var _expI5 = () => (_expI5 = wasmExports['expI5'])();
27+
var _expI6 = () => (_expI6 = wasmExports['expI6'])();
2228

2329
// add uses for some of them, leave *4 as non-roots
24-
expD1;
25-
Module['expD2'];
26-
wasmExports['expD3'];
30+
_expD1;
31+
Module['_expD2'];
32+
wasmExports['_expD3'];
2733

28-
expI1;
29-
Module['expI2'];
30-
wasmExports['expI3'];
34+
_expI1;
35+
Module['_expI2'];
36+
wasmExports['_expI3'];
3137

32-
// EXTRA_INFO: { "unused": ["emcc$import$number", "emcc$import$name", "emcc$import$func", "emcc$export$expD4", "emcc$export$expD6", "emcc$export$expI4", "emcc$export$expI6"] }
38+
// EXTRA_INFO: { "unusedImports": ["number", "name", "func"], "unusedExports": ["expD4", "expD6", "expI4", "expI6"] }

test/optimizer/minimal-runtime-applyDCEGraphRemovals-output.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ var wasmImports = {
77

88
WebAssembly.instantiate(Module["wasm"], imports).then(output => {
99
wasmExports = output.instance.exports;
10-
expD1 = wasmExports["expD1"];
11-
expD2 = wasmExports["expD2"];
12-
expD3 = wasmExports["expD3"];
10+
_expD1 = wasmExports["expD1"];
11+
_expD2 = wasmExports["expD2"];
12+
_expD3 = wasmExports["expD3"];
1313
initRuntime(wasmExports);
1414
ready();
1515
});
1616

17-
expD1;
17+
_expD1;
1818

19-
Module["expD2"];
19+
Module["_expD2"];
2020

21-
wasmExports["expD3"];
21+
wasmExports["_expD3"];
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
var name;
2-
var wasmImports = { save1: 1, number: 33, name: name, func: function() {}, save2: 2 };
2+
var wasmImports = {
3+
save1: 1,
4+
number: 33,
5+
name: name,
6+
func: function() {},
7+
save2: 2
8+
};
39

410
// exports gotten directly in the minimal runtime style
511
WebAssembly.instantiate(Module["wasm"], imports).then((output) => {
612
wasmExports = output.instance.exports;
7-
expD1 = wasmExports['expD1'];
8-
expD2 = wasmExports['expD2'];
9-
expD3 = wasmExports['expD3'];
10-
expD4 = wasmExports['expD4'];
13+
_expD1 = wasmExports['expD1'];
14+
_expD2 = wasmExports['expD2'];
15+
_expD3 = wasmExports['expD3'];
16+
_expD4 = wasmExports['expD4'];
1117
initRuntime(wasmExports);
1218
ready();
1319
});
1420

1521
// add uses for some of them, leave *4 as non-roots
16-
expD1;
17-
Module['expD2'];
18-
wasmExports['expD3'];
22+
_expD1;
23+
Module['_expD2'];
24+
wasmExports['_expD3'];
1925

20-
// EXTRA_INFO: { "unused": ["emcc$import$number", "emcc$import$name", "emcc$import$func", "emcc$export$expD4", "emcc$export$expI4"] }
26+
// EXTRA_INFO: { "unusedImports": ["number", "name", "func"], "unusedExports": ["expD4"] }

tools/acorn-optimizer.mjs

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,6 @@ function emptyOut(node) {
8989
node.type = 'EmptyStatement';
9090
}
9191

92-
// This converts the node into something that terser will ignore in a var
93-
// declaration, that is, it is a way to get rid of initial values.
94-
function convertToNothingInVarInit(node) {
95-
node.type = 'Literal';
96-
node.value = undefined;
97-
node.raw = 'undefined';
98-
}
99-
10092
function convertToNullStatement(node) {
10193
node.type = 'ExpressionStatement';
10294
node.expression = {
@@ -591,18 +583,25 @@ function isDynamicDynCall(node) {
591583

592584
//
593585
// Matches the wasm export wrappers generated by emcc (see make_export_wrappers
594-
// in emscripten.py). For example:
586+
// in emscripten.py). For example, the right hand side of these assignments:
595587
//
596588
// var _foo = (a0, a1) => (_foo = wasmExports['foo'])(a0, a1):
597589
//
590+
// or
591+
//
592+
// var _foo = (a0, a1) => (_foo = Module['_foo'] = wasmExports['foo'])(a0, a1):
593+
//
598594
function isExportWrapperFunction(f) {
599595
if (f.body.type != 'CallExpression') return null;
600596
let callee = f.body.callee;
601597
if (callee.type == 'ParenthesizedExpression') {
602598
callee = callee.expression;
603599
}
604-
if (callee.type != 'AssignmentExpression' || callee.right.type != 'MemberExpression') return null;
600+
if (callee.type != 'AssignmentExpression') return null;
605601
var rhs = callee.right;
602+
if (rhs.type == 'AssignmentExpression') {
603+
rhs = rhs.right;
604+
}
606605
if (rhs.type != 'MemberExpression' || !isExportUse(rhs)) return null;
607606
return getExportOrModuleUseName(rhs);
608607
}
@@ -740,7 +739,9 @@ function emitDCEGraph(ast) {
740739
emptyOut(node);
741740
} else if (value && value.type === 'ArrowFunctionExpression') {
742741
// this is
743-
// var x = () => (x = wasmExports['x'])(..)
742+
// () => (x = wasmExports['x'])(..)
743+
// or
744+
// () => (x = Module['_x'] = wasmExports['x'])(..)
744745
let asmName = isExportWrapperFunction(value);
745746
if (asmName) {
746747
saveAsmExport(name, asmName);
@@ -978,67 +979,84 @@ function emitDCEGraph(ast) {
978979
// way (and we leave further DCE on the JS and wasm sides to their respective
979980
// optimizers, closure compiler and binaryen).
980981
function applyDCEGraphRemovals(ast) {
981-
const unused = new Set(extraInfo.unused);
982+
const unusedExports = new Set(extraInfo.unusedExports);
983+
const unusedImports = new Set(extraInfo.unusedImports);
984+
const foundUnusedImports = new Set();
985+
const foundUnusedExports = new Set();
986+
trace('unusedExports:', unusedExports);
987+
trace('unusedImports:', unusedImports);
982988

983989
fullWalk(ast, (node) => {
984990
if (isWasmImportsAssign(node)) {
985991
const assignedObject = getWasmImportsValue(node);
986992
assignedObject.properties = assignedObject.properties.filter((item) => {
987993
const name = item.key.name;
988994
const value = item.value;
989-
const full = 'emcc$import$' + name;
990-
return !(unused.has(full) && !hasSideEffects(value));
991-
});
992-
} else if (node.type === 'AssignmentExpression') {
993-
// when we assign to a thing we don't need, we can just remove the assign
994-
// var x = Module['x'] = wasmExports['x'];
995-
const target = node.left;
996-
if (isExportUse(target) || isModuleUse(target)) {
997-
const name = getExportOrModuleUseName(target);
998-
const full = 'emcc$export$' + name;
999-
const value = node.right;
1000-
if (unused.has(full) && (isExportUse(value) || !hasSideEffects(value))) {
1001-
// This will be in a var init, and we just remove that value.
1002-
convertToNothingInVarInit(node);
995+
if (unusedImports.has(name)) {
996+
foundUnusedImports.add(name);
997+
return hasSideEffects(value);
1003998
}
1004-
}
999+
return true;
1000+
});
10051001
} else if (node.type === 'VariableDeclaration') {
1006-
// Handle the case we declare a variable but don't assign to the module:
1007-
// var x = wasmExports['x'];
1008-
// and
1009-
// var x = () => (x = wasmExports['x']).apply(...);
1002+
// Handle the various ways in which we extract wasmExports:
1003+
// 1. var _x = wasmExports['x'];
1004+
// or
1005+
// 2. var _x = Module['_x'] = wasmExports['x'];
1006+
//
1007+
// Or for delayed instantiation:
1008+
// 3. var _x = () => (_x = wasmExports['x'])(...);
1009+
// or
1010+
// 4. var _x = Module['_x] = () => (_x = Module['_x'] = wasmExports['x'])(...);
10101011
const init = node.declarations[0].init;
1011-
if (init) {
1012-
if (isExportUse(init)) {
1013-
const name = getExportOrModuleUseName(init);
1014-
const full = 'emcc$export$' + name;
1015-
if (unused.has(full)) {
1016-
convertToNothingInVarInit(init);
1017-
}
1018-
} else if (init.type == 'ArrowFunctionExpression') {
1019-
const name = isExportWrapperFunction(init);
1020-
const full = 'emcc$export$' + name;
1021-
if (unused.has(full)) {
1022-
convertToNothingInVarInit(init);
1023-
}
1012+
if (!init) {
1013+
return;
1014+
}
1015+
1016+
// Look through the optional `Module['_x']`
1017+
let realInit = init;
1018+
if (init.type == 'AssignmentExpression' && isModuleUse(init.left)) {
1019+
realInit = init.right;
1020+
}
1021+
1022+
if (isExportUse(realInit)) {
1023+
const export_name = getExportOrModuleUseName(realInit);
1024+
if (unusedExports.has(export_name)) {
1025+
// Case (1) and (2)
1026+
trace('found unused export:', export_name);
1027+
emptyOut(node);
1028+
foundUnusedExports.add(export_name);
1029+
}
1030+
} else if (realInit.type == 'ArrowFunctionExpression') {
1031+
const export_name = isExportWrapperFunction(realInit);
1032+
if (unusedExports.has(export_name)) {
1033+
// Case (3) and (4)
1034+
trace('found unused export:', export_name);
1035+
emptyOut(node);
1036+
foundUnusedExports.add(export_name);
10241037
}
10251038
}
10261039
} else if (node.type === 'ExpressionStatement') {
10271040
const expr = node.expression;
10281041
// In the MINIMAL_RUNTIME code pattern we have just
1029-
// x = wasmExports['x']
1042+
// _x = wasmExports['x']
10301043
// and never in a var.
10311044
if (expr.operator === '=' && expr.left.type === 'Identifier' && isExportUse(expr.right)) {
1032-
const name = expr.left.name;
1033-
if (name === getExportOrModuleUseName(expr.right)) {
1034-
const full = 'emcc$export$' + name;
1035-
if (unused.has(full)) {
1036-
emptyOut(node);
1037-
}
1045+
const export_name = getExportOrModuleUseName(expr.right);
1046+
if (unusedExports.has(export_name)) {
1047+
emptyOut(node);
1048+
foundUnusedExports.add(export_name);
10381049
}
10391050
}
10401051
}
10411052
});
1053+
1054+
for (const i of unusedImports) {
1055+
assert(foundUnusedImports.has(i), 'unused import not found: ' + i);
1056+
}
1057+
for (const e of unusedExports) {
1058+
assert(foundUnusedExports.has(e), 'unused export not found: ' + e);
1059+
}
10421060
}
10431061

10441062
// Need a parser to pass to acorn.Node constructor.
@@ -2050,7 +2068,10 @@ const registry = {
20502068
minifyGlobals: minifyGlobals,
20512069
};
20522070

2053-
passes.forEach((pass) => registry[pass](ast));
2071+
passes.forEach((pass) => {
2072+
trace(`running AST pass: ${pass}`);
2073+
registry[pass](ast);
2074+
});
20542075

20552076
if (!noPrint) {
20562077
const terserAst = terser.AST_Node.from_mozilla_ast(ast);

0 commit comments

Comments
 (0)