Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/unsafe_optimizations.mjs
4128 views
1
#!/usr/bin/env node
2
3
/** Implements a set of potentially unsafe JavaScript AST optimizations for aggressive code size optimizations.
4
Enabled when building with -sMINIMAL_RUNTIME=2 linker flag. */
5
6
import * as fs from 'node:fs';
7
import * as acorn from 'acorn';
8
import * as terser from '../third_party/terser/terser.js';
9
import {parseArgs} from 'node:util';
10
11
// Starting at the AST node 'root', calls the given callback function 'func' on all children and grandchildren of 'root'
12
// that are of any of the type contained in array 'types'.
13
function visitNodes(root, types, func) {
14
// Visit the given node if it is of desired type.
15
if (types.includes(root.type)) {
16
const continueTraversal = func(root);
17
if (continueTraversal === false) return false;
18
}
19
20
// Traverse all children of this node to find nodes of desired type.
21
for (const member in root) {
22
if (Array.isArray(root[member])) {
23
for (const elem of root[member]) {
24
if (elem?.type) {
25
const continueTraversal = visitNodes(elem, types, func);
26
if (continueTraversal === false) return false;
27
}
28
}
29
} else if (root[member]?.type) {
30
const continueTraversal = visitNodes(root[member], types, func);
31
if (continueTraversal === false) return false;
32
}
33
}
34
}
35
36
function optPassSimplifyModularizeFunction(ast) {
37
visitNodes(ast, ['FunctionExpression'], (node) => {
38
if (node.params.length == 1 && node.params[0].name == 'Module') {
39
const body = node.body.body;
40
// Nuke 'Module = Module || {};'
41
if (
42
body[0].type == 'ExpressionStatement' &&
43
body[0].expression.type == 'AssignmentExpression' &&
44
body[0].expression.left.name == 'Module'
45
) {
46
body.splice(0, 1);
47
}
48
// Replace 'function(Module) {var f = Module;' -> 'function(f) {'
49
if (
50
body[0].type == 'VariableDeclaration' &&
51
body[0].declarations[0]?.init?.name == 'Module'
52
) {
53
node.params[0].name = body[0].declarations[0].id.name;
54
body[0].declarations.splice(0, 1);
55
if (body[0].declarations.length == 0) {
56
body.splice(0, 1);
57
}
58
}
59
return false;
60
}
61
});
62
}
63
64
// Finds redundant operator new statements that are not assigned anywhere.
65
// (we aren't interested in side effects of the calls if no assignment)
66
function optPassRemoveRedundantOperatorNews(ast) {
67
// Remove standalone operator new statements that don't have any meaning.
68
visitNodes(ast, ['BlockStatement', 'Program'], (node) => {
69
const nodeArray = node.body;
70
for (let i = 0; i < nodeArray.length; ++i) {
71
const n = nodeArray[i];
72
if (n.type == 'ExpressionStatement' && n.expression.type == 'NewExpression') {
73
// Make an exception for new `new Promise` which is sometimes used
74
// in emscripten with real side effects. For example, see
75
// loadWasmModuleToWorker which returns a `new Promise` that is never
76
// referenced (a least in some builds).
77
//
78
// Another exception is made for `new WebAssembly.*` since we create and
79
// unused `WebAssembly.Memory` when probing for wasm64 features.
80
if (
81
n.expression.callee.name !== 'Promise' &&
82
n.expression.callee.object?.name !== 'WebAssembly'
83
) {
84
nodeArray.splice(i--, 1);
85
}
86
}
87
}
88
});
89
90
// Remove comma sequence chained operator news ('new foo(), new foo();')
91
visitNodes(ast, ['SequenceExpression'], (node) => {
92
const nodeArray = node.expressions;
93
// Delete operator news that don't have any meaning.
94
for (let i = 0; i < nodeArray.length; ++i) {
95
const n = nodeArray[i];
96
if (n.type == 'NewExpression') {
97
nodeArray.splice(i--, 1);
98
}
99
}
100
});
101
}
102
103
// Merges empty VariableDeclarators to previous VariableDeclarations.
104
// 'var a,b; ...; var c,d;'' -> 'var a,b,c,d; ...;'
105
function optPassMergeEmptyVarDeclarators(ast) {
106
let progress = false;
107
108
visitNodes(ast, ['BlockStatement', 'Program'], (node) => {
109
const nodeArray = node.body;
110
for (let i = 0; i < nodeArray.length; ++i) {
111
const n = nodeArray[i];
112
if (n.type != 'VariableDeclaration') continue;
113
// Look back to find a preceding VariableDeclaration that empty declarators from this declaration could be fused to.
114
for (let j = i - 1; j >= 0; --j) {
115
const p = nodeArray[j];
116
if (p.type == 'VariableDeclaration') {
117
for (let k = 0; k < n.declarations.length; ++k) {
118
if (!n.declarations[k].init) {
119
p.declarations.push(n.declarations[k]);
120
n.declarations.splice(k--, 1);
121
progress = true;
122
}
123
}
124
125
if (n.declarations.length == 0) nodeArray.splice(i--, 1);
126
break;
127
}
128
}
129
}
130
});
131
return progress;
132
}
133
134
// Finds multiple consecutive VariableDeclaration nodes, and fuses them together.
135
// 'var a = 1; var b = 2;' -> 'var a = 1, b = 2;'
136
function optPassMergeVarDeclarations(ast) {
137
let progress = false;
138
139
visitNodes(ast, ['BlockStatement', 'Program'], (node) => {
140
const nodeArray = node.body;
141
for (let i = 0; i < nodeArray.length; ++i) {
142
const n = nodeArray[i];
143
if (n.type != 'VariableDeclaration') continue;
144
// Look back to find if there is a preceding VariableDeclaration that this declaration could be fused to.
145
for (let j = i - 1; j >= 0; --j) {
146
const p = nodeArray[j];
147
if (p.type == 'VariableDeclaration') {
148
p.declarations = p.declarations.concat(n.declarations);
149
nodeArray.splice(i--, 1);
150
progress = true;
151
break;
152
} else if (!['FunctionDeclaration'].includes(p.type)) {
153
break;
154
}
155
}
156
}
157
});
158
return progress;
159
}
160
161
// Merges "var a,b;a = ...;" to "var b, a = ...;"
162
function optPassMergeVarInitializationAssignments(ast) {
163
// Tests if the assignment expression at nodeArray[i] is the first assignment to the given variable, and it was undefined before that.
164
function isUndefinedBeforeThisAssignment(nodeArray, i) {
165
const name = nodeArray[i].expression.left.name;
166
for (let j = i - 1; j >= 0; --j) {
167
const n = nodeArray[j];
168
if (
169
n.type == 'ExpressionStatement' &&
170
n.expression.type == 'AssignmentExpression' &&
171
n.expression.left.name == name
172
) {
173
return [null, null];
174
}
175
if (n.type == 'VariableDeclaration') {
176
for (let k = n.declarations.length - 1; k >= 0; --k) {
177
const d = n.declarations[k];
178
if (d.id.name == name) {
179
if (d.init) return [null, null];
180
else return [n, k];
181
}
182
}
183
}
184
}
185
return [null, null];
186
}
187
188
// Find all assignments that are preceded by a variable declaration.
189
let progress = false;
190
visitNodes(ast, ['BlockStatement', 'Program'], (node) => {
191
const nodeArray = node.body;
192
for (let i = 1; i < nodeArray.length; ++i) {
193
const n = nodeArray[i];
194
if (n.type != 'ExpressionStatement' || n.expression.type != 'AssignmentExpression') continue;
195
if (nodeArray[i - 1].type != 'VariableDeclaration') continue;
196
const [declaration, declarationIndex] = isUndefinedBeforeThisAssignment(nodeArray, i);
197
if (!declaration) continue;
198
const declarator = declaration.declarations[declarationIndex];
199
declarator.init = n.expression.right;
200
declaration.declarations.splice(declarationIndex, 1);
201
nodeArray[i - 1].declarations.push(declarator);
202
nodeArray.splice(i--, 1);
203
progress = true;
204
}
205
});
206
return progress;
207
}
208
209
function runOnJsText(js, pretty = false) {
210
const ast = acorn.parse(js, {ecmaVersion: 2021});
211
212
optPassRemoveRedundantOperatorNews(ast);
213
214
let progress = true;
215
while (progress) {
216
progress = optPassMergeVarDeclarations(ast);
217
progress = progress || optPassMergeVarInitializationAssignments(ast);
218
progress = progress || optPassMergeEmptyVarDeclarators(ast);
219
}
220
221
optPassSimplifyModularizeFunction(ast);
222
223
const terserAst = terser.AST_Node.from_mozilla_ast(ast);
224
const output = terserAst.print_to_string({
225
wrap_func_args: false,
226
beautify: pretty,
227
indent_level: pretty ? 2 : 0,
228
});
229
230
return output;
231
}
232
233
function runOnFile(input, pretty = false, output = null) {
234
let js = fs.readFileSync(input).toString();
235
js = runOnJsText(js, pretty);
236
if (output) fs.writeFileSync(output, js);
237
else console.log(js);
238
}
239
240
let numTestFailures = 0;
241
242
function test(input, expected) {
243
const observed = runOnJsText(input);
244
if (observed != expected) {
245
console.error(`ERROR: Input: ${input}\nobserved: ${observed}\nexpected: ${expected}\n`);
246
++numTestFailures;
247
} else {
248
console.log(`OK: ${input} -> ${expected}`);
249
}
250
}
251
252
function runTests() {
253
// optPassSimplifyModularizeFunction:
254
test(
255
'var Module = function(Module) {Module = Module || {};var f = Module;}',
256
'var Module=function(f){};',
257
);
258
259
// optPassRemoveRedundantOperatorNews:
260
test('new Uint16Array(a);', '');
261
test('new Uint16Array(a),new Uint16Array(a);', ';');
262
test("new function(a) {new TextDecoder(a);}('utf8');", '');
263
test(
264
'WebAssembly.instantiate(c.wasm,{}).then((a) => {new Int8Array(b);});',
265
'WebAssembly.instantiate(c.wasm,{}).then(a=>{});',
266
);
267
test('let x=new Uint16Array(a);', 'let x=new Uint16Array(a);');
268
// new Promise should be preserved
269
test('new Promise();', 'new Promise;');
270
271
// optPassMergeVarDeclarations:
272
test('var a; var b;', 'var a,b;');
273
test('var a=1; var b=2;', 'var a=1,b=2;');
274
test('var a=1; function foo(){} var b=2;', 'var a=1,b=2;function foo(){}');
275
276
// optPassMergeEmptyVarDeclarators:
277
test('var a;a=1;', 'var a=1;');
278
test('var a = 1, b; ++a; var c;', 'var a=1,b,c;++a;');
279
280
// Interaction between multiple passes:
281
test(
282
'var d, f; f = new Uint8Array(16); var h = f.buffer; d = new Uint8Array(h);',
283
'var f=new Uint8Array(16),h=f.buffer,d=new Uint8Array(h);',
284
);
285
286
// Older versions of terser would produce sub-optimal output for this.
287
// We keep this test around to prevent regression.
288
test('var i=new Image;i.onload=()=>{}', 'var i=new Image;i.onload=()=>{};');
289
290
// Test that arrays containing nulls don't cause issues
291
test('[,];', '[,];');
292
293
// Test optional chaining operator
294
test('console?.log("");', 'console?.log("");');
295
296
process.exit(numTestFailures);
297
}
298
299
const {
300
values: {test: testMode, pretty, output},
301
positionals: [input],
302
} = parseArgs({
303
options: {
304
test: {type: 'boolean'},
305
pretty: {type: 'boolean'},
306
output: {type: 'string', short: 'o'},
307
},
308
allowPositionals: true,
309
});
310
311
if (testMode) {
312
runTests();
313
} else {
314
runOnFile(input, pretty, output);
315
}
316
317