#!/usr/bin/env node
import * as acorn from 'acorn';
import * as terser from '../third_party/terser/terser.js';
import * as fs from 'node:fs';
import assert from 'node:assert';
import {parseArgs} from 'node:util';
function read(x) {
return fs.readFileSync(x, 'utf-8');
}
function assertAt(condition, node, message = '') {
if (!condition) {
if (!process.env.EMCC_DEBUG_SAVE) {
message += ' (use EMCC_DEBUG_SAVE=1 to preserve temporary inputs)';
}
let err = new Error(message);
err['loc'] = acorn.getLineInfo(input, node.start);
throw err;
}
}
function visitChildren(node, c) {
if (node.type === 'EmptyStatement') {
return;
}
function maybeChild(child) {
if (typeof child?.type === 'string') {
c(child);
return true;
}
return false;
}
for (const child of Object.values(node)) {
if (!maybeChild(child)) {
if (Array.isArray(child)) {
child.forEach(maybeChild);
}
}
}
}
function simpleWalk(node, cs) {
visitChildren(node, (child) => simpleWalk(child, cs));
if (node.type in cs) {
cs[node.type](node);
}
}
function fullWalk(node, c, pre) {
if (pre?.(node) !== false) {
visitChildren(node, (child) => fullWalk(child, c, pre));
c(node);
}
}
function recursiveWalk(node, cs) {
(function c(node) {
if (!(node.type in cs)) {
visitChildren(node, (child) => recursiveWalk(child, cs));
} else {
cs[node.type](node, c);
}
})(node);
}
function emptyOut(node) {
node.type = 'EmptyStatement';
}
function setLiteralValue(item, value) {
item.value = value;
item.raw = null;
}
function isLiteralString(node) {
return node.type === 'Literal' && typeof node.value === 'string';
}
function dump(node) {
console.log(JSON.stringify(node, null, ' '));
}
function walkPattern(node, onExpr, onBoundIdent) {
recursiveWalk(node, {
AssignmentPattern(node, c) {
c(node.left);
onExpr(node.right);
},
Property(node, c) {
if (node.computed) {
onExpr(node.key);
}
c(node.value);
},
Identifier({name}) {
onBoundIdent(name);
},
});
}
function hasSideEffects(node) {
let has = false;
fullWalk(
node,
(node) => {
switch (node.type) {
case 'ExpressionStatement':
if (node.directive) {
has = true;
}
break;
case 'Literal':
case 'Identifier':
case 'UnaryExpression':
case 'BinaryExpression':
case 'LogicalExpression':
case 'UpdateOperator':
case 'ConditionalExpression':
case 'VariableDeclaration':
case 'VariableDeclarator':
case 'ObjectExpression':
case 'Property':
case 'SpreadElement':
case 'BlockStatement':
case 'ArrayExpression':
case 'EmptyStatement': {
break;
}
case 'MemberExpression': {
if (node.object.type !== 'Identifier' || node.object.name !== 'Math') {
has = true;
}
break;
}
case 'NewExpression': {
if (node.callee.type === 'Identifier') {
const name = node.callee.name;
if (
name === 'TextDecoder' ||
name === 'ArrayBuffer' ||
name === 'Int8Array' ||
name === 'Uint8Array' ||
name === 'Int16Array' ||
name === 'Uint16Array' ||
name === 'Int32Array' ||
name === 'Uint32Array' ||
name === 'Float32Array' ||
name === 'Float64Array'
) {
break;
}
}
has = true;
break;
}
default: {
has = true;
}
}
},
(node) =>
!['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type),
);
return has;
}
function JSDCE(ast, aggressive) {
function iteration() {
let removed = 0;
const scopes = [{}];
function ensureData(scope, name) {
if (Object.prototype.hasOwnProperty.call(scope, name)) return scope[name];
scope[name] = {
def: 0,
use: 0,
param: 0,
};
return scope[name];
}
function cleanUp(ast, names) {
recursiveWalk(ast, {
ForStatement(node, c) {
visitChildren(node, c);
if (node.init?.type === 'EmptyStatement') {
node.init = null;
}
},
ForInStatement(node, c) {
c(node.right);
c(node.body);
},
ForOfStatement(node, c) {
c(node.right);
c(node.body);
},
VariableDeclaration(node, _c) {
let removedHere = 0;
node.declarations = node.declarations.filter((node) => {
assert(node.type === 'VariableDeclarator');
let keep = node.init && hasSideEffects(node.init);
walkPattern(
node.id,
(value) => {
keep ||= hasSideEffects(value);
},
(boundName) => {
keep ||= !names.has(boundName);
},
);
if (!keep) removedHere = 1;
return keep;
});
removed += removedHere;
if (node.declarations.length === 0) {
emptyOut(node);
}
},
ExpressionStatement(node, _c) {
if (aggressive && !hasSideEffects(node)) {
emptyOut(node);
removed++;
}
},
FunctionDeclaration(node, _c) {
if (names.has(node.id.name)) {
removed++;
emptyOut(node);
return;
}
},
FunctionExpression() {},
ArrowFunctionExpression() {},
});
}
function handleFunction(node, c, defun) {
if (defun) {
ensureData(scopes[scopes.length - 1], node.id.name).def = 1;
}
const scope = {};
scopes.push(scope);
for (const param of node.params) {
walkPattern(param, c, (name) => {
ensureData(scope, name).def = 1;
scope[name].param = 1;
});
}
c(node.body);
const ownName = defun ? node.id.name : '';
const names = new Set();
for (const name in scopes.pop()) {
if (name === ownName) continue;
const data = scope[name];
if (data.use && !data.def) {
ensureData(scopes[scopes.length - 1], name).use = 1;
continue;
}
if (data.def && !data.use && !data.param) {
names.add(name);
}
}
cleanUp(node.body, names);
}
recursiveWalk(ast, {
VariableDeclarator(node, c) {
walkPattern(node.id, c, (name) => {
ensureData(scopes[scopes.length - 1], name).def = 1;
});
if (node.init) c(node.init);
},
ObjectExpression(node, c) {
node.properties.forEach((node) => {
if (node.value) {
c(node.value);
} else if (node.argument) {
c(node.argument);
}
});
},
MemberExpression(node, c) {
c(node.object);
if (node.computed) {
c(node.property);
}
},
FunctionDeclaration(node, c) {
handleFunction(node, c, true );
},
FunctionExpression(node, c) {
handleFunction(node, c);
},
ArrowFunctionExpression(node, c) {
handleFunction(node, c);
},
Identifier(node, _c) {
const name = node.name;
ensureData(scopes[scopes.length - 1], name).use = 1;
},
ExportDefaultDeclaration(node, c) {
const name = node.declaration.id.name;
ensureData(scopes[scopes.length - 1], name).use = 1;
c(node.declaration);
},
ExportNamedDeclaration(node, c) {
if (node.declaration) {
if (node.declaration.type == 'FunctionDeclaration') {
const name = node.declaration.id.name;
ensureData(scopes[scopes.length - 1], name).use = 1;
} else {
assert(node.declaration.type == 'VariableDeclaration');
for (const decl of node.declaration.declarations) {
const name = decl.id.name;
ensureData(scopes[scopes.length - 1], name).use = 1;
}
}
c(node.declaration);
} else {
for (const specifier of node.specifiers) {
const name = specifier.local.name;
ensureData(scopes[scopes.length - 1], name).use = 1;
}
}
},
});
const scope = scopes.pop();
assert(scopes.length === 0);
const names = new Set();
for (const [name, data] of Object.entries(scope)) {
if (data.def && !data.use) {
assert(!data.param);
names.add(name);
}
}
cleanUp(ast, names);
return removed;
}
while (iteration() && aggressive) {}
}
function AJSDCE(ast) {
JSDCE(ast, true);
}
function isWasmImportsAssign(node) {
if (
node.type === 'AssignmentExpression' &&
node.left.name == 'wasmImports' &&
node.right.type == 'ObjectExpression'
) {
return true;
}
return (
node.type === 'VariableDeclaration' &&
node.declarations.length === 1 &&
node.declarations[0].id.name === 'wasmImports' &&
node.declarations[0].init &&
node.declarations[0].init.type === 'ObjectExpression'
);
}
function getWasmImportsValue(node) {
if (node.declarations) {
return node.declarations[0].init;
} else {
return node.right;
}
}
function isExportUse(node) {
return (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
isLiteralString(node.property) &&
node.object.name === 'wasmExports'
);
}
function getExportOrModuleUseName(node) {
return node.property.value;
}
function isModuleUse(node) {
return (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === 'Module' &&
isLiteralString(node.property)
);
}
function applyImportAndExportNameChanges(ast) {
const mapping = extraInfo.mapping;
fullWalk(ast, (node) => {
if (isWasmImportsAssign(node)) {
const assignedObject = getWasmImportsValue(node);
assignedObject.properties.forEach((item) => {
if (mapping[item.key.name]) {
item.key.name = mapping[item.key.name];
}
});
} else if (node.type === 'AssignmentExpression') {
const value = node.right;
if (isExportUse(value)) {
const name = value.property.value;
if (mapping[name]) {
setLiteralValue(value.property, mapping[name]);
}
}
} else if (node.type === 'CallExpression' && isExportUse(node.callee)) {
const callee = node.callee;
const name = callee.property.value;
if (mapping[name]) {
setLiteralValue(callee.property, mapping[name]);
}
} else if (isExportUse(node)) {
const prop = node.property;
const name = prop.value;
if (mapping[name]) {
setLiteralValue(prop, mapping[name]);
}
}
});
}
function isStaticDynCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'dynCall' &&
isLiteralString(node.arguments[0])
);
}
function getStaticDynCallName(node) {
return 'dynCall_' + node.arguments[0].value;
}
function isDynamicDynCall(node) {
return (
(node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'dynCall' &&
!isLiteralString(node.arguments[0])) ||
(isLiteralString(node) && node.value === 'dynCall_')
);
}
function emitDCEGraph(ast) {
const imports = [];
const defuns = [];
const dynCallNames = [];
const nameToGraphName = {};
const modulePropertyToGraphName = {};
const exportNameToGraphName = {};
let foundWasmImportsAssign = false;
let foundMinimalRuntimeExports = false;
function saveAsmExport(name, asmName) {
const graphName = getGraphName(name, 'export');
nameToGraphName[name] = graphName;
modulePropertyToGraphName[name] = graphName;
exportNameToGraphName[asmName] = graphName;
if (/^dynCall_/.test(name)) {
dynCallNames.push(graphName);
}
}
var specialScopes = 0;
fullWalk(
ast,
(node) => {
if (isWasmImportsAssign(node)) {
const assignedObject = getWasmImportsValue(node);
assignedObject.properties.forEach((item) => {
let value = item.value;
if (value.type === 'Literal' || value.type === 'FunctionExpression') {
return;
}
if (value.type === 'LogicalExpression') {
value = value.left;
}
assertAt(value.type === 'Identifier', value);
const nativeName = item.key.type == 'Literal' ? item.key.value : item.key.name;
assert(nativeName);
imports.push([value.name, nativeName]);
});
foundWasmImportsAssign = true;
emptyOut(node);
} else if (node.type === 'AssignmentExpression') {
const target = node.left;
if (isExportUse(target)) {
emptyOut(node);
}
} else if (node.type === 'VariableDeclaration') {
if (node.declarations.length === 1) {
const item = node.declarations[0];
const name = item.id.name;
const value = item.init;
if (value && isExportUse(value)) {
const asmName = getExportOrModuleUseName(value);
saveAsmExport(name, asmName);
emptyOut(node);
} else if (value && value.type === 'AssignmentExpression') {
const assigned = value.left;
if (isModuleUse(assigned) && getExportOrModuleUseName(assigned) === name) {
let found = 0;
let asmName;
fullWalk(value.right, (node) => {
if (isExportUse(node)) {
found++;
asmName = getExportOrModuleUseName(node);
}
});
if (found === 1) {
saveAsmExport(name, asmName);
emptyOut(node);
return;
}
if (value.right.type === 'Literal') {
assertAt(typeof value.right.value === 'number', value.right);
emptyOut(node);
}
}
}
}
if (!node.declarations.reduce((hasInit, decl) => hasInit || !!decl.init, false)) {
emptyOut(node);
}
} else if (node.type === 'FunctionDeclaration') {
const name = node.id.name;
if (
name == 'assignWasmExports' &&
node.params.length === 1 &&
node.params[0].type === 'Identifier' &&
node.params[0].name === 'wasmExports'
) {
const body = node.body.body;
assert(!foundMinimalRuntimeExports);
foundMinimalRuntimeExports = true;
for (let i = 0; i < body.length; i++) {
const item = body[i];
if (
item.type === 'ExpressionStatement' &&
item.expression.type === 'AssignmentExpression' &&
item.expression.operator === '=' &&
item.expression.left.type === 'Identifier' &&
item.expression.right.type === 'MemberExpression' &&
item.expression.right.object.type === 'Identifier' &&
item.expression.right.object.name === 'wasmExports' &&
item.expression.right.property.type === 'Literal'
) {
const name = item.expression.left.name;
const asmName = item.expression.right.property.value;
saveAsmExport(name, asmName);
emptyOut(item);
}
}
} else if (!specialScopes) {
defuns.push(node);
nameToGraphName[name] = getGraphName(name, 'defun');
emptyOut(node);
}
} else if (node.type === 'ArrowFunctionExpression') {
assert(specialScopes > 0);
specialScopes--;
} else if (node.type === 'Property' && node.method) {
assert(specialScopes > 0);
specialScopes--;
}
},
(node) => {
if (node.type === 'ArrowFunctionExpression' || (node.type === 'Property' && node.method)) {
specialScopes++;
}
},
);
assert(specialScopes === 0);
assert(
foundWasmImportsAssign,
'could not find the assignment to "wasmImports". perhaps --pre-js or --post-js code moved it out of the global scope? (things like that should be done after emcc runs, as they do not need to be run through the optimizer which is the special thing about --pre-js/--post-js code)',
);
if (extraInfo) {
for (const exp of extraInfo.exports) {
saveAsmExport(exp[0], exp[1]);
}
}
function getGraphName(name, what) {
return 'emcc$' + what + '$' + name;
}
const infos = {};
for (const [jsName, nativeName] of imports) {
const name = getGraphName(jsName, 'import');
const info = (infos[name] = {
name: name,
import: ['env', nativeName],
reaches: new Set(),
});
if (nameToGraphName.hasOwnProperty(jsName)) {
info.reaches.add(nameToGraphName[jsName]);
}
}
for (const [e, _] of Object.entries(exportNameToGraphName)) {
const name = exportNameToGraphName[e];
infos[name] = {
name: name,
export: e,
reaches: new Set(),
};
}
function visitNode(node, defunInfo) {
let reached;
if (node.type === 'Identifier') {
const name = node.name;
if (nameToGraphName.hasOwnProperty(name)) {
reached = nameToGraphName[name];
}
} else if (isModuleUse(node)) {
const name = getExportOrModuleUseName(node);
if (modulePropertyToGraphName.hasOwnProperty(name)) {
reached = modulePropertyToGraphName[name];
}
} else if (isStaticDynCall(node)) {
reached = getGraphName(getStaticDynCallName(node), 'export');
} else if (isDynamicDynCall(node)) {
reached = dynCallNames;
} else if (isExportUse(node)) {
const name = getExportOrModuleUseName(node);
if (exportNameToGraphName.hasOwnProperty(name)) {
infos[exportNameToGraphName[name]].root = true;
}
return;
}
if (reached) {
function addReach(reached) {
if (defunInfo) {
defunInfo.reaches.add(reached);
} else {
if (infos[reached]) {
infos[reached].root = true;
} else {
trace('metadce: missing declaration for ' + reached);
}
}
}
if (typeof reached === 'string') {
addReach(reached);
} else {
reached.forEach(addReach);
}
}
}
defuns.forEach((defun) => {
const name = getGraphName(defun.id.name, 'defun');
const info = (infos[name] = {
name: name,
reaches: new Set(),
});
fullWalk(defun.body, (node) => visitNode(node, info));
});
fullWalk(ast, (node) => visitNode(node, null));
const graph = Object.entries(infos)
.sort(([name1], [name2]) => (name1 > name2 ? 1 : -1))
.map(([_name, info]) => ({
...info,
reaches: Array.from(info.reaches).sort(),
}));
dump(graph);
}
function applyDCEGraphRemovals(ast) {
const unusedExports = new Set(extraInfo.unusedExports);
const unusedImports = new Set(extraInfo.unusedImports);
const foundUnusedImports = new Set();
const foundUnusedExports = new Set();
trace('unusedExports:', unusedExports);
trace('unusedImports:', unusedImports);
fullWalk(ast, (node) => {
if (isWasmImportsAssign(node)) {
const assignedObject = getWasmImportsValue(node);
assignedObject.properties = assignedObject.properties.filter((item) => {
const name = item.key.name;
const value = item.value;
if (unusedImports.has(name)) {
foundUnusedImports.add(name);
return hasSideEffects(value);
}
return true;
});
} else if (node.type === 'ExpressionStatement') {
let expr = node.expression;
if (expr.type == 'AssignmentExpression' && expr.right.type == 'AssignmentExpression') {
expr = expr.right;
}
if (expr.operator === '=' && expr.left.type === 'Identifier' && isExportUse(expr.right)) {
const export_name = getExportOrModuleUseName(expr.right);
if (unusedExports.has(export_name)) {
emptyOut(node);
foundUnusedExports.add(export_name);
}
}
}
});
for (const i of unusedImports) {
assert(foundUnusedImports.has(i), 'unused import not found: ' + i);
}
for (const e of unusedExports) {
assert(foundUnusedExports.has(e), 'unused export not found: ' + e);
}
}
function createLiteral(value) {
return {
type: 'Literal',
value: value,
raw: '' + value,
};
}
function makeCallExpression(node, name, args) {
Object.assign(node, {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: name,
},
arguments: args,
});
}
function isEmscriptenHEAP(name) {
switch (name) {
case 'HEAP8':
case 'HEAPU8':
case 'HEAP16':
case 'HEAPU16':
case 'HEAP32':
case 'HEAPU32':
case 'HEAP64':
case 'HEAPU64':
case 'HEAPF32':
case 'HEAPF64': {
return true;
}
default: {
return false;
}
}
}
function littleEndianHeap(ast) {
recursiveWalk(ast, {
FunctionDeclaration(node, c) {
if (
!(
node.id.type === 'Identifier' &&
(node.id.name.startsWith('LE_HEAP') || node.id.name.startsWith('LE_ATOMICS_'))
)
) {
c(node.body);
}
},
VariableDeclarator(node, c) {
if (!(node.id.type === 'Identifier' && node.id.name.startsWith('LE_ATOMICS_'))) {
c(node.id);
if (node.init) c(node.init);
}
},
AssignmentExpression(node, c) {
const target = node.left;
const value = node.right;
c(value);
if (!isHEAPAccess(target)) {
c(target);
} else {
const name = target.object.name;
const idx = target.property;
switch (name) {
case 'HEAP8':
case 'HEAPU8': {
break;
}
case 'HEAP16': {
makeCallExpression(node, 'LE_HEAP_STORE_I16', [multiply(idx, 2), value]);
break;
}
case 'HEAPU16': {
makeCallExpression(node, 'LE_HEAP_STORE_U16', [multiply(idx, 2), value]);
break;
}
case 'HEAP32': {
makeCallExpression(node, 'LE_HEAP_STORE_I32', [multiply(idx, 4), value]);
break;
}
case 'HEAPU32': {
makeCallExpression(node, 'LE_HEAP_STORE_U32', [multiply(idx, 4), value]);
break;
}
case 'HEAP64': {
makeCallExpression(node, 'LE_HEAP_STORE_I64', [multiply(idx, 8), value]);
break;
}
case 'HEAPU64': {
makeCallExpression(node, 'LE_HEAP_STORE_U64', [multiply(idx, 8), value]);
break;
}
case 'HEAPF32': {
makeCallExpression(node, 'LE_HEAP_STORE_F32', [multiply(idx, 4), value]);
break;
}
case 'HEAPF64': {
makeCallExpression(node, 'LE_HEAP_STORE_F64', [multiply(idx, 8), value]);
break;
}
}
}
},
CallExpression(node, c) {
if (node.arguments) {
for (var a of node.arguments) c(a);
}
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Atomics' &&
!node.callee.computed
) {
makeCallExpression(
node,
'LE_ATOMICS_' + node.callee.property.name.toUpperCase(),
node.arguments,
);
} else {
c(node.callee);
}
},
MemberExpression(node, c) {
c(node.property);
if (!isHEAPAccess(node)) {
c(node.object);
} else {
const idx = node.property;
switch (node.object.name) {
case 'HEAP8':
case 'HEAPU8': {
break;
}
case 'HEAP16': {
makeCallExpression(node, 'LE_HEAP_LOAD_I16', [multiply(idx, 2)]);
break;
}
case 'HEAPU16': {
makeCallExpression(node, 'LE_HEAP_LOAD_U16', [multiply(idx, 2)]);
break;
}
case 'HEAP32': {
makeCallExpression(node, 'LE_HEAP_LOAD_I32', [multiply(idx, 4)]);
break;
}
case 'HEAPU32': {
makeCallExpression(node, 'LE_HEAP_LOAD_U32', [multiply(idx, 4)]);
break;
}
case 'HEAP64': {
makeCallExpression(node, 'LE_HEAP_LOAD_I64', [multiply(idx, 8)]);
break;
}
case 'HEAPU64': {
makeCallExpression(node, 'LE_HEAP_LOAD_U64', [multiply(idx, 8)]);
break;
}
case 'HEAPF32': {
makeCallExpression(node, 'LE_HEAP_LOAD_F32', [multiply(idx, 4)]);
break;
}
case 'HEAPF64': {
makeCallExpression(node, 'LE_HEAP_LOAD_F64', [multiply(idx, 8)]);
break;
}
}
}
},
});
}
function growableHeap(ast) {
recursiveWalk(ast, {
ExportNamedDeclaration() {
},
FunctionDeclaration(node, c) {
if (
!(
node.id.type === 'Identifier' &&
(node.id.name === 'growMemViews' || node.id.name === 'LE_HEAP_UPDATE')
)
) {
c(node.body);
}
},
AssignmentExpression(node) {
if (node.left.type !== 'Identifier') {
growableHeap(node.left);
}
growableHeap(node.right);
},
VariableDeclarator(node) {
if (node.init) {
growableHeap(node.init);
}
},
Identifier(node) {
if (isEmscriptenHEAP(node.name)) {
Object.assign(node, {
type: 'SequenceExpression',
expressions: [
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'growMemViews',
},
arguments: [],
},
{...node},
],
});
}
},
});
}
function unsignPointers(ast) {
function isHeap(name) {
return isEmscriptenHEAP(name) || name === 'heap' || name === 'HEAP';
}
function unsign(node) {
if (node.type === 'BinaryExpression') {
if (node.operator === '>>') {
node.operator = '>>>';
return node;
}
}
return {
type: 'BinaryExpression',
left: node,
operator: '>>>',
right: {
type: 'Literal',
value: 0,
},
};
}
fullWalk(ast, (node) => {
if (node.type === 'MemberExpression') {
if (node.object.type === 'Identifier' && isHeap(node.object.name) && node.computed) {
node.property = unsign(node.property);
}
} else if (node.type === 'CallExpression') {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
isHeap(node.callee.object.name) &&
!node.callee.computed
) {
if (node.callee.property.name === 'set') {
if (node.arguments.length >= 2) {
node.arguments[1] = unsign(node.arguments[1]);
}
} else if (node.callee.property.name === 'subarray') {
if (node.arguments.length >= 1) {
node.arguments[0] = unsign(node.arguments[0]);
if (node.arguments.length >= 2) {
node.arguments[1] = unsign(node.arguments[1]);
}
}
} else if (node.callee.property.name === 'copyWithin') {
node.arguments[0] = unsign(node.arguments[0]);
node.arguments[1] = unsign(node.arguments[1]);
if (node.arguments.length >= 3) {
node.arguments[2] = unsign(node.arguments[2]);
}
}
}
}
});
}
function isHEAPAccess(node) {
return (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.computed &&
isEmscriptenHEAP(node.object.name)
);
}
function asanify(ast) {
recursiveWalk(ast, {
FunctionDeclaration(node, c) {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('_asan_js_') || node.id.name === 'establishStackSpace')
) {
} else {
c(node.body);
}
},
AssignmentExpression(node, c) {
const target = node.left;
const value = node.right;
c(value);
if (isHEAPAccess(target)) {
makeCallExpression(node, '_asan_js_store', [target.object, target.property, value]);
} else {
c(target);
}
},
MemberExpression(node, c) {
c(node.property);
if (!isHEAPAccess(node)) {
c(node.object);
} else {
makeCallExpression(node, '_asan_js_load', [node.object, node.property]);
}
},
});
}
function multiply(value, by) {
return {
type: 'BinaryExpression',
left: value,
operator: '*',
right: createLiteral(by),
};
}
function safeHeap(ast) {
recursiveWalk(ast, {
FunctionDeclaration(node, c) {
if (node.id.type === 'Identifier' && node.id.name.startsWith('SAFE_HEAP')) {
} else {
c(node.body);
}
},
AssignmentExpression(node, c) {
const target = node.left;
const value = node.right;
c(value);
if (isHEAPAccess(target)) {
makeCallExpression(node, 'SAFE_HEAP_STORE', [target.object, target.property, value]);
} else {
c(target);
}
},
MemberExpression(node, c) {
c(node.property);
if (!isHEAPAccess(node)) {
c(node.object);
} else {
makeCallExpression(node, 'SAFE_HEAP_LOAD', [node.object, node.property]);
}
},
});
}
const RESERVED = new Set([
'do',
'if',
'in',
'for',
'new',
'try',
'var',
'env',
'let',
'case',
'else',
'enum',
'void',
'this',
'void',
'with',
]);
const VALID_MIN_INITS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
const VALID_MIN_LATERS = VALID_MIN_INITS + '0123456789';
const minifiedNames = [];
const minifiedState = [0];
function ensureMinifiedNames(n) {
while (minifiedNames.length < n + 1) {
let name = VALID_MIN_INITS[minifiedState[0]];
for (let i = 1; i < minifiedState.length; i++) {
name += VALID_MIN_LATERS[minifiedState[i]];
}
if (!RESERVED.has(name)) minifiedNames.push(name);
let i = 0;
while (true) {
minifiedState[i]++;
if (minifiedState[i] < (i === 0 ? VALID_MIN_INITS : VALID_MIN_LATERS).length) break;
minifiedState[i] = 0;
i++;
if (i === minifiedState.length) minifiedState.push(-1);
}
}
}
function minifyLocals(ast) {
assert(extraInfo?.globals);
for (const fun of ast.body) {
if (fun.type !== 'FunctionDeclaration') {
continue;
}
const localNames = new Set();
for (const param of fun.params) {
localNames.add(param.name);
}
simpleWalk(fun, {
VariableDeclaration(node, _c) {
for (const dec of node.declarations) {
localNames.add(dec.id.name);
}
},
});
function isLocalName(name) {
return localNames.has(name);
}
const newNames = new Map();
const usedNames = new Set();
const funId = fun.id;
fun.id = null;
simpleWalk(fun, {
Identifier(node, _c) {
const name = node.name;
if (!isLocalName(name)) {
const minified = extraInfo.globals[name];
if (minified) {
newNames.set(name, minified);
usedNames.add(minified);
}
}
},
CallExpression(node, _c) {
if (node.callee.type === 'Identifier') {
assertAt(!isLocalName(node.callee.name), node.callee, 'cannot call a local');
}
},
});
let nextMinifiedName = 0;
function getNextMinifiedName() {
while (true) {
ensureMinifiedNames(nextMinifiedName);
const minified = minifiedNames[nextMinifiedName++];
if (!usedNames.has(minified) && !isLocalName(minified)) {
return minified;
}
}
}
for (const param of fun.params) {
const minified = getNextMinifiedName();
newNames.set(param.name, minified);
param.name = minified;
}
const labelNames = new Map();
let nextMinifiedLabel = 0;
function getNextMinifiedLabel() {
ensureMinifiedNames(nextMinifiedLabel);
return minifiedNames[nextMinifiedLabel++];
}
recursiveWalk(fun, {
Identifier(node) {
const name = node.name;
if (newNames.has(name)) {
node.name = newNames.get(name);
} else if (isLocalName(name)) {
const minified = getNextMinifiedName();
newNames.set(name, minified);
node.name = minified;
}
},
LabeledStatement(node, c) {
if (!labelNames.has(node.label.name)) {
labelNames.set(node.label.name, getNextMinifiedLabel());
}
node.label.name = labelNames.get(node.label.name);
c(node.body);
},
BreakStatement(node, _c) {
if (node.label) {
node.label.name = labelNames.get(node.label.name);
}
},
ContinueStatement(node, _c) {
if (node.label) {
node.label.name = labelNames.get(node.label.name);
}
},
});
fun.id = funId;
assert(extraInfo.globals.hasOwnProperty(fun.id.name));
fun.id.name = extraInfo.globals[fun.id.name];
}
}
function minifyGlobals(ast) {
assert(
ast.type === 'Program' &&
ast.body.length === 1 &&
ast.body[0].type === 'FunctionDeclaration' &&
ast.body[0].id.name === 'instantiate',
);
const fun = ast.body[0];
const funId = fun.id;
fun.id = null;
const declared = new Set();
const ignore = new Set();
simpleWalk(fun, {
FunctionDeclaration(node) {
if (node.id) {
declared.add(node.id.name);
}
for (const param of node.params) {
declared.add(param.name);
}
},
FunctionExpression(node) {
for (const param of node.params) {
declared.add(param.name);
}
},
VariableDeclaration(node) {
for (const decl of node.declarations) {
declared.add(decl.id.name);
}
},
MemberExpression(node) {
if (!node.computed) {
ignore.add(node.property);
}
},
});
let nextMinifiedName = 0;
function getNewMinifiedName() {
ensureMinifiedNames(nextMinifiedName);
return minifiedNames[nextMinifiedName++];
}
const minified = new Map();
function minify(name) {
if (!minified.has(name)) {
minified.set(name, getNewMinifiedName());
}
assert(minified.get(name));
return minified.get(name);
}
for (const name of declared) {
minify(name);
}
for (const name of extraInfo.globals) {
declared.add(name);
minify(name);
}
simpleWalk(fun, {
Identifier(node) {
if (declared.has(node.name) && !ignore.has(node)) {
node.name = minify(node.name);
}
},
});
fun.id = funId;
const json = {};
for (const x of minified.entries()) json[x[0]] = x[1];
suffix = '// EXTRA_INFO:' + JSON.stringify(json);
}
function reattachComments(ast, commentsMap) {
const symbols = [];
ast.walk(
new terser.TreeWalker((node) => {
if (node.start?.pos) {
symbols.push(node);
}
}),
);
symbols.sort((a, b) => a.start.pos - b.start.pos);
let j = 0;
for (const [pos, comments] of Object.entries(commentsMap)) {
while (j < symbols.length && symbols[j].start.pos < pos) {
++j;
}
if (j >= symbols.length) {
trace('dropping comments: no symbol comes after them');
break;
}
if (symbols[j].start.pos != pos) {
trace('dropping comments: not linked to any remaining AST node');
continue;
}
symbols[j].start.comments_before ??= [];
for (const comment of comments) {
trace('reattaching comment');
symbols[j].start.comments_before.push(
new terser.AST_Token(
comment.type == 'Line' ? 'comment1' : 'comment2',
comment.value,
undefined,
undefined,
false,
undefined,
undefined,
'0',
),
);
}
}
}
let suffix = '';
const {
values: {
'closure-friendly': closureFriendly,
'export-es6': exportES6,
verbose,
'no-print': noPrint,
'minify-whitespace': minifyWhitespace,
outfile,
},
positionals: [infile, ...passes],
} = parseArgs({
options: {
'closure-friendly': {type: 'boolean'},
'export-es6': {type: 'boolean'},
verbose: {type: 'boolean'},
'no-print': {type: 'boolean'},
'minify-whitespace': {type: 'boolean'},
outfile: {type: 'string', short: 'o'},
},
allowPositionals: true,
});
function trace(...args) {
if (verbose) {
console.warn(...args);
}
}
const input = read(infile);
const extraInfoStart = input.lastIndexOf('// EXTRA_INFO:');
let extraInfo = null;
if (extraInfoStart > 0) {
extraInfo = JSON.parse(input.slice(extraInfoStart + 14));
}
const sourceComments = {};
const params = {
ecmaVersion: 'latest',
sourceType: exportES6 ? 'module' : 'script',
allowAwaitOutsideFunction: true,
};
if (closureFriendly) {
const currentComments = [];
Object.assign(params, {
preserveParens: true,
onToken(token) {
sourceComments[token.start] = currentComments.slice();
currentComments.length = 0;
},
onComment: currentComments,
});
}
const registry = {
JSDCE,
AJSDCE,
applyImportAndExportNameChanges,
emitDCEGraph,
applyDCEGraphRemovals,
dump,
littleEndianHeap,
growableHeap,
unsignPointers,
minifyLocals,
asanify,
safeHeap,
minifyGlobals,
};
let ast;
try {
ast = acorn.parse(input, params);
for (let pass of passes) {
const resolvedPass = registry[pass];
assert(resolvedPass, `unknown optimizer pass: ${pass}`);
resolvedPass(ast);
}
} catch (err) {
if (err.loc) {
err.message +=
'\n' +
`${input.split(acorn.lineBreak)[err.loc.line - 1]}\n` +
`${' '.repeat(err.loc.column)}^ ${infile}:${err.loc.line}:${err.loc.column + 1}`;
}
throw err;
}
if (!noPrint) {
const terserAst = terser.AST_Node.from_mozilla_ast(ast);
if (closureFriendly) {
reattachComments(terserAst, sourceComments);
}
let output = terserAst.print_to_string({
beautify: !minifyWhitespace,
indent_level: minifyWhitespace ? 0 : 2,
keep_quoted_props: closureFriendly,
wrap_func_args: false,
comments: true,
shorthand: true,
});
output += '\n';
if (suffix) {
output += suffix + '\n';
}
if (outfile) {
fs.writeFileSync(outfile, output);
} else {
process.stdout.write(output);
}
}