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