[acorn-opt] Add inlineConstants pass to inline primitive JS constants Inlines immutable primitive constants (numbers, booleans, and bigints) defined via `const` into their expression uses in `tools/acorn-optimizer.mjs`. When combined with `JSDCE`, this allows constants defined in JS library code (such as pthread/worker message command IDs and socket states) to be inlined and their declarations eliminated in builds that do not run Closure Compiler. Also add unit tests in `test/test_other.py`.
diff --git a/test/js_optimizer/inlineConstants-dce-output.js b/test/js_optimizer/inlineConstants-dce-output.js new file mode 100644 index 0000000..7003858 --- /dev/null +++ b/test/js_optimizer/inlineConstants-dce-output.js
@@ -0,0 +1,11 @@ +export const EXPORTED = 3; + +function handle(cmd) { + if (cmd === 1) return 10; + if (cmd === 2) return 20; + return 0; +} + +export function run(cmd) { + return handle(cmd) + 3; +}
diff --git a/test/js_optimizer/inlineConstants-dce.js b/test/js_optimizer/inlineConstants-dce.js new file mode 100644 index 0000000..fea9bca --- /dev/null +++ b/test/js_optimizer/inlineConstants-dce.js
@@ -0,0 +1,14 @@ +const CMD_LOAD = 1; +const CMD_RUN = 2; +const UNUSED = 99; +export const EXPORTED = 3; + +function handle(cmd) { + if (cmd === CMD_LOAD) return 10; + if (cmd === CMD_RUN) return 20; + return 0; +} + +export function run(cmd) { + return handle(cmd) + EXPORTED; +}
diff --git a/test/js_optimizer/inlineConstants-output.js b/test/js_optimizer/inlineConstants-output.js new file mode 100644 index 0000000..abdaefd --- /dev/null +++ b/test/js_optimizer/inlineConstants-output.js
@@ -0,0 +1,76 @@ +const NUM = 42; + +const FLOAT = 3.14; + +const BOOL_T = true; + +const BOOL_F = false; + +const NEG = -1; + +const BIG = 100n; + +const NEG_BIG = -1n; + +const ALIAS = 42; + +const MULTI_A = 1, MULTI_B = 2; + +const UNUSED = 999; + +export const EXPORTED_CONST = 77; + +function testExpressions(cmd) { + if (cmd === 42) return true; + if (cmd < 3.14) return false; + if (cmd === -1) return -1n; + switch (cmd) { + case 1: + return 10; + + case 2: + return 20; + } + return 42 + 100n; +} + +const obj = { + NUM: 42, + prop: 3.14, + [-1]: "computed", + NUM: "uncomputed key should not be changed" +}; + +function testMember(x) { + var a = x[42]; + var b = x.NUM; + return a + b; +} + +function testParamShadow(NUM) { + return NUM; +} + +function testLocalConstShadow() { + const NUM = 100; + return 100; +} + +function testLocalVarShadow() { + if (true) { + var NUM = 200; + } + return NUM; +} + +function testCatchShadow() { + try { + throw 1; + } catch (NUM) { + return NUM; + } +} + +const arrow = x => x + 42; + +export { NUM };
diff --git a/test/js_optimizer/inlineConstants.js b/test/js_optimizer/inlineConstants.js new file mode 100644 index 0000000..88df070 --- /dev/null +++ b/test/js_optimizer/inlineConstants.js
@@ -0,0 +1,73 @@ +// Constants of various primitive types +const NUM = 42; +const FLOAT = 3.14; +const BOOL_T = true; +const BOOL_F = false; +const NEG = -1; +const BIG = 100n; +const NEG_BIG = -1n; +const ALIAS = NUM; +const MULTI_A = 1, MULTI_B = 2; +const UNUSED = 999; +export const EXPORTED_CONST = 77; + +// Uses in expressions +function testExpressions(cmd) { + if (cmd === NUM) return BOOL_T; + if (cmd < FLOAT) return BOOL_F; + if (cmd === NEG) return NEG_BIG; + switch (cmd) { + case MULTI_A: return 10; + case MULTI_B: return 20; + } + return ALIAS + BIG; +} + +// Uses in objects +const obj = { + NUM, + prop: FLOAT, + [NEG]: "computed", + NUM: "uncomputed key should not be changed" +}; + +// Uses in member expressions +function testMember(x) { + var a = x[NUM]; + var b = x.NUM; + return a + b; +} + +// Shadowing: parameter shadows outer const +function testParamShadow(NUM) { + return NUM; +} + +// Shadowing: local const shadows outer const +function testLocalConstShadow() { + const NUM = 100; + return NUM; +} + +// Shadowing: local var shadows outer const +function testLocalVarShadow() { + if (true) { + var NUM = 200; + } + return NUM; +} + +// Shadowing: catch parameter shadows outer const +function testCatchShadow() { + try { + throw 1; + } catch (NUM) { + return NUM; + } +} + +// Arrow function with concise body +const arrow = (x) => x + NUM; + +// Export specifier +export { NUM };
diff --git a/test/test_other.py b/test/test_other.py index 5ad2542..48bfc73 100644 --- a/test/test_other.py +++ b/test/test_other.py
@@ -3121,6 +3121,8 @@ @parameterized({ 'minifyGlobals': (['minifyGlobals'],), 'minifyLocals': (['minifyLocals'],), + 'inlineConstants': (['inlineConstants', '--export-es6'],), + 'inlineConstants-dce': (['inlineConstants', 'JSDCE', '--export-es6'], 'inlineConstants-dce.js'), 'JSDCE': (['JSDCE', '--export-es6'],), 'JSDCE-sourcePhaseImports': (['JSDCE', '--export-es6'],), 'JSDCE-hasOwnProperty': (['JSDCE'],),
diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index feefd73..7db8e5e 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs
@@ -428,6 +428,324 @@ } }); } +// Inlines immutable primitive constants (numbers, booleans, bigints) defined via +// `const` into their expression uses. This allows constants defined in library code +// (such as pthread/worker command IDs, socket states, etc.) to be inlined and then +// eliminated by JSDCE in builds that do not run Closure Compiler. +function inlineConstants(ast) { + function isInlinableConstant(node) { + if (!node) return false; + if (node.type === 'Literal') { + const val = node.value; + return ( + (typeof val === 'number' && Number.isFinite(val)) || + typeof val === 'boolean' || + typeof val === 'bigint' + ); + } + if ( + node.type === 'UnaryExpression' && + node.prefix === true && + (node.operator === '-' || node.operator === '+') && + node.argument.type === 'Literal' + ) { + const val = node.argument.value; + return (typeof val === 'number' && Number.isFinite(val)) || typeof val === 'bigint'; + } + return false; + } + + function cloneConstant(node) { + if (node.type === 'Literal') { + return { + type: 'Literal', + value: node.value, + raw: node.raw, + bigint: node.bigint, + }; + } + if (node.type === 'UnaryExpression') { + return { + type: 'UnaryExpression', + operator: node.operator, + prefix: true, + argument: cloneConstant(node.argument), + }; + } + throw new Error('unexpected constant node type: ' + node.type); + } + + function replaceIdentifier(node, val) { + const replacement = cloneConstant(val); + for (const k of Object.keys(node)) { + delete node[k]; + } + Object.assign(node, replacement); + } + + class Scope { + constructor(isFunctionOrProgram = false) { + this.isFunctionOrProgram = isFunctionOrProgram; + this.bindings = new Map(); + } + } + + const scopes = []; + + function addVarBinding(name) { + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].isFunctionOrProgram) { + scopes[i].bindings.set(name, null); + return; + } + } + scopes[0].bindings.set(name, null); + } + + function lookup(name) { + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].bindings.has(name)) { + return scopes[i].bindings.get(name); + } + } + return null; + } + + function collectDeclarations(bodyList, scope) { + if (!Array.isArray(bodyList)) return; + const pendingConsts = []; + + for (const stmt of bodyList) { + let declNode = null; + if (stmt.type === 'VariableDeclaration') { + declNode = stmt; + } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'VariableDeclaration') { + declNode = stmt.declaration; + } else if (stmt.type === 'FunctionDeclaration') { + if (stmt.id) scope.bindings.set(stmt.id.name, null); + } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'FunctionDeclaration') { + if (stmt.declaration.id) scope.bindings.set(stmt.declaration.id.name, null); + } else if (stmt.type === 'ClassDeclaration') { + if (stmt.id) scope.bindings.set(stmt.id.name, null); + } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'ClassDeclaration') { + if (stmt.declaration.id) scope.bindings.set(stmt.declaration.id.name, null); + } + + if (declNode) { + if (declNode.kind === 'const') { + for (const decl of declNode.declarations) { + if (decl.id.type === 'Identifier') { + if (isInlinableConstant(decl.init)) { + scope.bindings.set(decl.id.name, cloneConstant(decl.init)); + } else if (decl.init?.type === 'Identifier') { + pendingConsts.push({ name: decl.id.name, initName: decl.init.name }); + } else { + scope.bindings.set(decl.id.name, null); + } + } else { + walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null)); + } + } + } else if (declNode.kind === 'let') { + for (const decl of declNode.declarations) { + walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null)); + } + } else if (declNode.kind === 'var') { + for (const decl of declNode.declarations) { + walkPattern(decl.id, () => {}, (name) => addVarBinding(name)); + } + } + } + } + + // Resolve any const aliases: const A = B; + let progress = true; + while (progress && pendingConsts.length > 0) { + progress = false; + for (let i = pendingConsts.length - 1; i >= 0; i--) { + const { name, initName } = pendingConsts[i]; + const val = lookup(initName) || scope.bindings.get(initName); + if (val) { + scope.bindings.set(name, cloneConstant(val)); + pendingConsts.splice(i, 1); + progress = true; + } + } + } + for (const { name } of pendingConsts) { + scope.bindings.set(name, null); + } + } + + function handleFunction(node, c) { + const scope = new Scope(true /* isFunctionOrProgram */); + scopes.push(scope); + if (node.type === 'FunctionExpression' && node.id) { + scope.bindings.set(node.id.name, null); + } + for (const param of node.params) { + walkPattern(param, (expr) => c(expr), (name) => scope.bindings.set(name, null)); + } + c(node.body); + scopes.pop(); + } + + const rootScope = new Scope(true /* isFunctionOrProgram */); + scopes.push(rootScope); + collectDeclarations(ast.body, rootScope); + + recursiveWalk(ast, { + BlockStatement(node, c) { + const scope = new Scope(false); + scopes.push(scope); + collectDeclarations(node.body, scope); + for (const stmt of node.body) { + c(stmt); + } + scopes.pop(); + }, + FunctionDeclaration(node, c) { + handleFunction(node, c); + }, + FunctionExpression(node, c) { + handleFunction(node, c); + }, + ArrowFunctionExpression(node, c) { + handleFunction(node, c); + }, + ForStatement(node, c) { + const scope = new Scope(false); + scopes.push(scope); + if (node.init?.type === 'VariableDeclaration') { + if (node.init.kind === 'var') { + for (const decl of node.init.declarations) { + walkPattern(decl.id, () => {}, (name) => addVarBinding(name)); + } + } else { + for (const decl of node.init.declarations) { + walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null)); + } + } + } + visitChildren(node, c); + scopes.pop(); + }, + ForInStatement(node, c) { + const scope = new Scope(false); + scopes.push(scope); + if (node.left.type === 'VariableDeclaration') { + if (node.left.kind === 'var') { + for (const decl of node.left.declarations) { + walkPattern(decl.id, () => {}, (name) => addVarBinding(name)); + } + } else { + for (const decl of node.left.declarations) { + walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null)); + } + } + } + visitChildren(node, c); + scopes.pop(); + }, + ForOfStatement(node, c) { + const scope = new Scope(false); + scopes.push(scope); + if (node.left.type === 'VariableDeclaration') { + if (node.left.kind === 'var') { + for (const decl of node.left.declarations) { + walkPattern(decl.id, () => {}, (name) => addVarBinding(name)); + } + } else { + for (const decl of node.left.declarations) { + walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null)); + } + } + } + visitChildren(node, c); + scopes.pop(); + }, + CatchClause(node, c) { + const scope = new Scope(false); + scopes.push(scope); + if (node.param) { + walkPattern(node.param, () => {}, (name) => scope.bindings.set(name, null)); + } + c(node.body); + scopes.pop(); + }, + VariableDeclarator(node, c) { + if (node.init) c(node.init); + }, + ExportNamedDeclaration(node, c) { + if (node.declaration) c(node.declaration); + }, + ExportDefaultDeclaration(node, c) { + c(node.declaration); + }, + ExportSpecifier() {}, + ImportDeclaration() {}, + MemberExpression(node, c) { + c(node.object); + if (node.computed) c(node.property); + }, + ObjectExpression(node, c) { + for (const prop of node.properties) { + if (prop.type === 'Property') { + if (prop.computed) c(prop.key); + if (prop.value) { + if (prop.shorthand && prop.value.type === 'Identifier') { + const val = lookup(prop.value.name); + if (val) { + prop.shorthand = false; + replaceIdentifier(prop.value, val); + continue; + } + } + c(prop.value); + } + } else if (prop.type === 'SpreadElement') { + c(prop.argument); + } + } + }, + AssignmentExpression(node, c) { + if (node.left.type !== 'Identifier') c(node.left); + c(node.right); + }, + UpdateExpression(node, c) { + if (node.argument.type !== 'Identifier') c(node.argument); + }, + ClassDeclaration(node, c) { + if (node.superClass) c(node.superClass); + c(node.body); + }, + ClassExpression(node, c) { + if (node.superClass) c(node.superClass); + c(node.body); + }, + MethodDefinition(node, c) { + if (node.computed) c(node.key); + c(node.value); + }, + PropertyDefinition(node, c) { + if (node.computed) c(node.key); + if (node.value) c(node.value); + }, + BreakStatement() {}, + ContinueStatement() {}, + LabeledStatement(node, c) { + c(node.body); + }, + Identifier(node, _c) { + const val = lookup(node.name); + if (val) { + replaceIdentifier(node, val); + } + }, + }); + + scopes.pop(); +} function isWasmImportsAssign(node) { // var wasmImports = .. @@ -1877,6 +2195,7 @@ JSDCE, AJSDCE, stripDefaultUndefined, + inlineConstants, applyImportAndExportNameChanges, emitDCEGraph, applyDCEGraphRemovals,
diff --git a/tools/building.py b/tools/building.py index 8cb2b78..e1e0878 100644 --- a/tools/building.py +++ b/tools/building.py
@@ -790,7 +790,7 @@ # start with JSDCE, to clean up obvious JS garbage. When optimizing for size, # use AJSDCE (aggressive JS DCE, performs multiple iterations). Clean up # whitespace if necessary too. - passes = [] + passes = ['inlineConstants'] if not settings.LINKABLE: passes.append('JSDCE' if not expensive_optimizations else 'AJSDCE') if not settings.USE_CLOSURE_COMPILER:
diff --git a/tools/js_optimizer.py b/tools/js_optimizer.py index 9f0cb8a..8b01950 100755 --- a/tools/js_optimizer.py +++ b/tools/js_optimizer.py
@@ -286,7 +286,7 @@ elif cleanup: if DEBUG: print('running cleanup on shell code', file=sys.stderr) - acorn_passes = ['JSDCE'] + acorn_passes = ['inlineConstants', 'JSDCE'] if '--minify-whitespace' in passes: acorn_passes.append('--minify-whitespace') cld = building.acorn_optimizer(cld, acorn_passes)