blob: 3d0c622040c1a89db91548c1c2e2b7448c897c6f [file] [log] [blame] [edit]
#!/usr/bin/env node
// Copyright 2011 The Emscripten Authors. All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License. Both these licenses can be
// found in the LICENSE file.
//
// Optimizer tool. This is meant to be run after the emscripten compiler has
// finished generating code. These optimizations are done on the generated
// code to further improve it.
//
// Be aware that this is *not* a general JS optimizer. It assumes that the
// input is valid asm.js and makes strong assumptions based on this. It may do
// anything from crashing to optimizing incorrectly if the input is not valid!
//
// TODO: Optimize traverse to modify a node we want to replace, in-place,
// instead of returning it to the previous call frame where we check?
// TODO: Share EMPTY_NODE instead of emptyNode that constructs?
var uglify = require('../third_party/uglify-js');
var fs = require('fs');
var path = require('path');
var fs = require('fs');
var arguments_ = process['argv'].slice(2);
var debug = false;
function printErr(x) {
process.stderr.write(x + '\n');
}
function print(x) {
process.stdout.write(x + '\n');
}
function assert(x, msg) {
if (!x) throw 'assertion failed (' + msg + ') : ' + new Error().stack;
}
function read(filename) {
return fs.readFileSync(filename).toString();
}
function load(f) {
f = path.join(__dirname, f)
eval.call(null, read(f));
};
load('../src/utility.js');
// Utilities
var LOOP = set('do', 'while', 'for');
var LOOP_FLOW = set('break', 'continue');
var CONTROL_FLOW = set('do', 'while', 'for', 'if', 'switch');
var NAME_OR_NUM = set('name', 'num');
var ASSOCIATIVE_BINARIES = set('+', '*', '|', '&', '^');
var ALTER_FLOW = set('break', 'continue', 'return');
var BITWISE = set('|', '&', '^');
var BREAK_CAPTURERS = set('do', 'while', 'for', 'switch');
var CONTINUE_CAPTURERS = LOOP;
var COMMABLE = set('assign', 'binary', 'unary-prefix', 'unary-postfix', 'name', 'num', 'call', 'seq', 'conditional', 'sub');
var CONDITION_CHECKERS = set('if', 'do', 'while', 'switch');
var BOOLEAN_RECEIVERS = set('if', 'do', 'while', 'conditional');
var FUNCTIONS_THAT_ALWAYS_THROW = set('abort', '___resumeException', '___cxa_throw', '___cxa_rethrow');
var UNDEFINED_NODE = ['unary-prefix', 'void', ['num', 0]];
var GENERATED_FUNCTIONS_MARKER = '// EMSCRIPTEN_GENERATED_FUNCTIONS';
var generatedFunctions = false; // whether we have received only generated functions
var extraInfo = null;
function srcToAst(src) {
return uglify.parser.parse(src, false, debug);
}
function astToSrc(ast, minifyWhitespace) {
return uglify.uglify.gen_code(ast, {
debug: debug,
ascii_only: true,
beautify: !minifyWhitespace,
indent_level: 1
});
}
function srcToStat(src) {
return srcToAst(src)[1][0]; // look into toplevel
}
function srcToExp(src) {
return srcToStat(src)[1];
}
// Traverses the children of a node. If the traverse function returns an object,
// replaces the child. If it returns true, stop the traversal and return true.
function traverseChildren(node, traverse, pre, post) {
if (node[0] === 'var') {
// don't traverse the names, just the values
var children = node[1];
if (!Array.isArray(children)) return;
for (var i = 0; i < children.length; i++) {
var subnode = children[i];
if (subnode.length === 2) {
var value = subnode[1];
if (Array.isArray(value)) {
var subresult = traverse(value, pre, post);
if (subresult === true) return true;
if (subresult !== null && typeof subresult === 'object') subnode[1] = subresult;
}
}
}
} else if (node[0] === 'object') {
// don't traverse the names, just the values
var children = node[1];
if (!Array.isArray(children)) return;
for (var i = 0; i < children.length; i++) {
var subnode = children[i];
var value = subnode[1];
if (Array.isArray(value)) {
var subresult = traverse(value, pre, post);
if (subresult === true) return true;
if (subresult !== null && typeof subresult === 'object') subnode[1] = subresult;
}
}
} else {
// generic traversal
for (var i = 0; i < node.length; i++) {
var subnode = node[i];
if (Array.isArray(subnode)) {
var subresult = traverse(subnode, pre, post);
if (subresult === true) return true;
if (subresult !== null && typeof subresult === 'object') node[i] = subresult;
}
}
}
}
// Traverses a JavaScript syntax tree rooted at the given node calling the given
// callback for each node.
// @arg node: The root of the AST.
// @arg pre: The pre to call for each node. This will be called with
// the node as the first argument and its type as the second. If true is
// returned, the traversal is stopped. If an object is returned,
// it replaces the passed node in the tree. If null is returned, we stop
// traversing the subelements (but continue otherwise).
// @arg post: A callback to call after traversing all children.
// @returns: If the root node was replaced, the new root node. If the traversal
// was stopped, true. Otherwise undefined.
function traverse(node, pre, post) {
var type = node[0], result, len;
var relevant = typeof type === 'string';
if (relevant) {
var result = pre(node, type);
if (result === true) return true;
if (result && result !== null) node = result; // Continue processing on this node
}
if (result !== null) {
if (traverseChildren(node, traverse, pre, post) === true) return true;
}
if (relevant) {
if (post) {
var postResult = post(node, type);
result = result || postResult;
}
}
return result;
}
// Only walk through the generated functions
function traverseGeneratedFunctions(ast, callback) {
assert(generatedFunctions);
if (ast[0] === 'toplevel') {
var stats = ast[1];
for (var i = 0; i < stats.length; i++) {
var curr = stats[i];
if (curr[0] === 'defun') callback(curr);
}
} else if (ast[0] === 'defun') {
callback(ast);
}
}
function traverseGenerated(ast, pre, post) {
traverseGeneratedFunctions(ast, function(func) {
traverse(func, pre, post);
});
}
function deStat(node) {
if (node[0] === 'stat') return node[1];
return node;
}
function emptyNode() { // XXX do we need to create new nodes here? can't we reuse?
return ['toplevel', []]
}
function isEmptyNode(node) {
return node.length === 2 && node[0] === 'toplevel' && node[1].length === 0;
}
function clearEmptyNodes(list) {
for (var i = 0; i < list.length;) {
if (isEmptyNode(list[i]) || (list[i][0] === 'stat' && isEmptyNode(list[i][1]))) {
list.splice(i, 1);
} else {
i++;
}
}
}
function filterEmptyNodes(list) { // creates a copy and returns it
return list.filter(function(node) {
return !(isEmptyNode(node) || (node[0] === 'stat' && isEmptyNode(node[1])));
});
}
function removeEmptySubNodes(node) {
if (node[0] === 'defun') {
node[3] = filterEmptyNodes(node[3]);
} else if (node[0] === 'block' && node[1]) {
node[1] = filterEmptyNodes(node[1]);
} else if (node[0] === 'seq' && isEmptyNode(node[1])) {
return node[2];
}
/*
var stats = getStatements(node);
if (stats) clearEmptyNodes(stats);
*/
}
function removeAllEmptySubNodes(ast) {
traverse(ast, removeEmptySubNodes);
}
function astCompare(x, y) {
if (!Array.isArray(x)) return x === y;
if (!Array.isArray(y)) return false;
if (x.length !== y.length) return false;
for (var i = 0; i < x.length; i++) {
if (!astCompare(x[i], y[i])) return false;
}
return true;
}
// calls definitely on child nodes that will be called, in order, and calls maybe on nodes that might be called.
// a(); if (b) c(); d(); will call definitely(a, arg), maybe(c, arg), definitely(d, arg)
function traverseChildrenInExecutionOrder(node, definitely, maybe, arg) {
switch (node[0]) {
default: throw '!' + node[0];
case 'num': case 'var': case 'name': case 'toplevel': case 'string':
case 'break': case 'continue': break; // nodes with no interesting children; they themselves have already been definitely or maybe'd
case 'assign': case 'binary': {
definitely(node[2], arg);
definitely(node[3], arg);
break;
}
case 'sub': case 'seq': {
definitely(node[1], arg);
definitely(node[2], arg);
break;
}
case 'while': {
definitely(node[1], arg);
maybe(node[2], arg); // may never enter the loop
maybe(node[1], arg); // may enter the loop a second time
maybe(node[2], arg);
break;
}
case 'do': {
definitely(node[2], arg);
maybe(node[1], arg); // may never reach the condition if we break
maybe(node[2], arg);
maybe(node[1], arg);
break;
}
case 'binary': {
definitely(node[2], arg);
definitely(node[3], arg);
break;
}
case 'dot': {
definitely(node[1], arg);
break;
}
case 'unary-prefix': case 'label': {
definitely(node[2], arg);
break;
}
case 'call': {
definitely(node[1], arg);
var args = node[2];
for (var i = 0; i < args.length; i++) {
definitely(args[i], arg);
}
break;
}
case 'if': case 'conditional': {
definitely(node[1], arg);
maybe(node[2], arg);
if (node[3]) { maybe(node[3], arg); }
break;
}
case 'defun': case 'func': case 'block': {
var stats = getStatements(node);
if (!stats || stats.length === 0) break;
for (var i = 0; i < stats.length; i++) {
definitely(stats[i], arg);
// check if we might break, if we have more to do
if (i === stats.length - 1) break;
var labels = {};
var breakCaptured = 0;
var mightBreak = false;
traverse(stats[i], function(node, type) {
if (type === 'label') labels[node[1]] = true;
else if (type in BREAK_CAPTURERS) {
breakCaptured++;
} else if (type === 'break') {
if (node[1]) {
// labeled break
if (!(node[1] in labels)) mightBreak = true;
} else {
if (!breakCaptured) mightBreak = true;
}
}
}, function(node, type) {
if (type === 'label') delete labels[node[1]];
else if (type in BREAK_CAPTURERS) {
breakCaptured--;
}
});
if (mightBreak) {
// all the rest are in one big maybe
return maybe(['block', stats.slice(i+1)], arg);
}
}
break;
}
case 'stat': case 'return': {
definitely(node[1], arg);
break;
}
case 'switch': {
definitely(node[1], arg);
var cases = node[2];
for (var i = 0; i < cases.length; i++) {
var c = cases[i];
var stats = c[1];
var temp = ['block', stats];
maybe(temp, arg);
}
}
}
}
// Passes
// Dump the AST. Useful for debugging. For example,
// node tools/js-optimizer.js ABSOLUTE_PATH_TO_FILE dumpAst
function dumpAst(ast) {
printErr(JSON.stringify(ast, null, ' '));
}
function dumpSrc(ast) {
printErr(astToSrc(ast));
}
function overwrite(x, y) {
for (var i = 0; i < y.length; i++) x[i] = y[i];
x.length = y.length;
}
var STACK_ALIGN = 16;
function stackAlign(x) {
if (x % STACK_ALIGN) x += STACK_ALIGN - (x % STACK_ALIGN);
return x;
}
// Closure compiler, when inlining, will insert assignments to
// undefined for the shared variables. However, in compiled code
// - and in library/shell code too! - we should never rely on
// undefined being assigned. So we can simply remove those assignments.
//
// Note: An inlined function that kept a large value referenced, may
// keep that references when inlined, if we remove the setting to
// undefined. This is not dangerous in compiled code, but might be
// in supporting code (for example, holding on to the HEAP when copying).
//
// This pass assumes that unGlobalize has been run, so undefined
// is now explicit.
function removeAssignsToUndefined(ast) {
traverse(ast, function(node, type) {
if (type === 'assign' && jsonCompare(node[3], ['unary-prefix', 'void', ['num', 0]])) {
return emptyNode();
} else if (type === 'var') {
node[1] = node[1].map(function(varItem, j) {
var ident = varItem[0];
var value = varItem[1];
if (jsonCompare(value, UNDEFINED_NODE)) return [ident];
return [ident, value];
});
}
});
// cleanup (|x = y = void 0| leaves |x = ;| right now)
var modified = true;
while (modified) {
modified = false;
traverse(ast, function(node, type) {
if (type === 'assign' && jsonCompare(node[3], emptyNode())) {
modified = true;
return emptyNode();
} else if (type === 'var') {
node[1] = node[1].map(function(varItem, j) {
var ident = varItem[0];
var value = varItem[1];
if (value && jsonCompare(value, emptyNode())) return [ident];
return [ident, value];
});
}
});
}
}
// XXX This is an invalid optimization
// We sometimes leave some settings to label that are not needed, if later in
// the relooper we realize that we have a single entry, so no checks on label
// are actually necessary. It's easy to clean those up now.
function removeUnneededLabelSettings(ast) {
traverse(ast, function(node, type) {
if (type === 'defun') { // all of our compiled code is in defun nodes
// Find all checks
var checked = {};
traverse(node, function(node, type) {
if (type === 'binary' && node[1] === '==' && node[2][0] === 'name' && node[2][1] === 'label') {
assert(node[3][0] === 'num');
checked[node[3][1]] = 1;
}
});
// Remove unneeded sets
traverse(node, function(node, type) {
if (type === 'assign' && node[2][0] === 'name' && node[2][1] === 'label') {
assert(node[3][0] === 'num');
if (!(node[3][1] in checked)) return emptyNode();
}
});
}
});
}
// Various expression simplifications. Happens after elimination, which opens up many of these simplification opportunities.
function makeTempParseHeap() {
return { unsigned: false, float: false, bits: 0, type: 0 };
}
var parseHeapTemp = makeTempParseHeap();
function parseHeap(name, out) { // XXX this uses parseHeapTemp by default, which is global! If between your call and your use, something else can call - that is bad
out = out || parseHeapTemp;
if (name.substr(0, 4) != 'HEAP') return false;
out.unsigned = name[4] === 'U';
out.float = name[4] === 'F';
out.bits = parseInt(name.substr(out.unsigned || out.float ? 5 : 4));
out.type = !out.float ? ASM_INT : (out.bits === 64 ? ASM_DOUBLE : ASM_FLOAT);
return true;
}
var USEFUL_BINARY_OPS = set('<<', '>>', '|', '&', '^');
var COMPARE_OPS = set('<', '<=', '>', '>=', '==', '===', '!=', '!==');
function isFunctionTable(name) {
return /^FUNCTION_TABLE.*/.test(name);
}
function simplifyExpressions(ast) {
// Simplify common expressions used to perform integer conversion operations
// in cases where no conversion is needed.
function simplifyIntegerConversions(ast) {
traverse(ast, function(node, type) {
if (type === 'binary' && node[1] === '>>' && node[3][0] === 'num' &&
node[2][0] === 'binary' && node[2][1] === '<<' && node[2][3][0] === 'num' && node[3][1] === node[2][3][1]) {
// Transform (x&A)<<B>>B to X&A.
var innerNode = node[2][2];
var shifts = node[3][1];
if (innerNode[0] === 'binary' && innerNode[1] === '&' && innerNode[3][0] === 'num') {
var mask = innerNode[3][1];
if (mask << shifts >> shifts === mask) {
return innerNode;
}
}
} else if (type === 'binary' && (node[1] in BITWISE)) {
for (var i = 2; i <= 3; i++) {
var subNode = node[i];
if (subNode[0] === 'binary' && subNode[1] === '&' && subNode[3][0] === 'num' && subNode[3][1] == 1) {
// Rewrite (X < Y) & 1 to X < Y , when it is going into a bitwise operator. We could
// remove even more (just replace &1 with |0, then subsequent passes could remove the |0)
// but v8 issue #2513 means the code would then run very slowly in chrome.
var input = subNode[2];
if (input[0] === 'binary' && (input[1] in COMPARE_OPS)) {
node[i] = input;
}
}
}
}
});
}
// When there is a bunch of math like (((8+5)|0)+12)|0, only the external |0 is needed, one correction is enough.
// At each node, ((X|0)+Y)|0 can be transformed into (X+Y): The inner corrections are not needed
// TODO: Is the same is true for 0xff, 0xffff?
// Likewise, if we have |0 inside a block that will be >>'d, then the |0 is unnecessary because some
// 'useful' mathops already |0 anyhow.
function simplifyOps(ast) {
var SAFE_BINARY_OPS;
if (asm) {
SAFE_BINARY_OPS = set('+', '-'); // division is unsafe as it creates non-ints in JS; mod is unsafe as signs matter so we can't remove |0's; mul does not nest with +,- in asm
} else {
SAFE_BINARY_OPS = set('+', '-', '*');
}
var COERCION_REQUIRING_OPS = set('sub', 'unary-prefix'); // ops that in asm must be coerced right away
var COERCION_REQUIRING_BINARIES = set('*', '/', '%'); // binary ops that in asm must be coerced
var ZERO = ['num', 0];
function removeMultipleOrZero() {
var rerun = true;
while (rerun) {
rerun = false;
var stack = [];
traverse(ast, function process(node, type) {
if (type === 'binary' && node[1] === '|') {
if (node[2][0] === 'num' && node[3][0] === 'num') {
node[2][1] |= node[3][1];
stack.push(0);
return node[2];
}
var go = false;
if (jsonCompare(node[2], ZERO)) {
// canonicalize order
var temp = node[3];
node[3] = node[2];
node[2] = temp;
go = true;
} else if (jsonCompare(node[3], ZERO)) {
go = true;
}
if (!go) {
stack.push(1);
return;
}
// We might be able to remove this correction
for (var i = stack.length-1; i >= 0; i--) {
if (stack[i] >= 1) {
if (asm) {
if (stack[stack.length-1] < 2 && node[2][0] === 'call') break; // we can only remove multiple |0s on these
if (stack[stack.length-1] < 1 && (node[2][0] in COERCION_REQUIRING_OPS ||
(node[2][0] === 'binary' && node[2][1] in COERCION_REQUIRING_BINARIES))) break; // we can remove |0 or >>2
}
// we will replace ourselves with the non-zero side. Recursively process that node.
var result = jsonCompare(node[2], ZERO) ? node[3] : node[2], other;
// replace node in-place
node.length = result.length;
for (var j = 0; j < result.length; j++) {
node[j] = result[j];
}
rerun = true;
return process(result, result[0]);
} else if (stack[i] === -1) {
break; // Too bad, we can't
}
}
stack.push(2); // From here on up, no need for this kind of correction, it's done at the top
// (Add this at the end, so it is only added if we did not remove it)
} else if (type === 'binary' && node[1] in USEFUL_BINARY_OPS) {
stack.push(1);
} else if ((type === 'binary' && node[1] in SAFE_BINARY_OPS) || type === 'num' || type === 'name') {
stack.push(0); // This node is safe in that it does not interfere with this optimization
} else if (type === 'unary-prefix' && node[1] === '~') {
stack.push(1);
} else {
stack.push(-1); // This node is dangerous! Give up if you see this before you see '1'
}
}, function() {
stack.pop();
});
}
}
removeMultipleOrZero();
// & and heap-related optimizations
var hasTempDoublePtr = false, rerunOrZeroPass = false;
traverse(ast, function(node, type) {
// The "pre" visitor. Useful for detecting trees which should not
// be simplified.
if (type == 'sub' && node[1][0] == 'name' && isFunctionTable(node[1][1])) {
return null; // do not traverse subchildren here, we should not collapse 55 & 126.
}
}, function(node, type) {
// The "post" visitor. The simplifications are done in this visitor so
// that we simplify a node's operands before the node itself. This allows
// optimizations to cascade.
if (type === 'name') {
if (node[1] === 'tempDoublePtr') hasTempDoublePtr = true;
} else if (type === 'binary' && node[1] === '&' && node[3][0] === 'num') {
if (node[2][0] === 'num') return ['num', node[2][1] & node[3][1]];
var input = node[2];
var amount = node[3][1];
if (input[0] === 'binary' && input[1] === '&' && input[3][0] === 'num') {
// Collapse X & 255 & 1
node[3][1] = amount & input[3][1];
node[2] = input[2];
} else if (input[0] === 'sub' && input[1][0] === 'name') {
// HEAP8[..] & 255 => HEAPU8[..]
var name = input[1][1];
if (parseHeap(name)) {
if (amount === Math.pow(2, parseHeapTemp.bits)-1) {
if (!parseHeapTemp.unsigned) {
input[1][1] = 'HEAPU' + parseHeapTemp.bits; // make unsigned
}
if (asm) {
// we cannot return HEAPU8 without a coercion, but at least we do HEAP8 & 255 => HEAPU8 | 0
node[1] = '|';
node[3][1] = 0;
return node;
}
return input;
}
}
} else if (input[0] === 'binary' && input[1] === '>>' &&
input[2][0] === 'binary' && input[2][1] === '<<' &&
input[2][3][0] === 'num' && input[3][0] === 'num' &&
input[2][3][1] === input[3][1] &&
(~(-1 >>> input[3][1]) & amount) == 0) {
// x << 24 >> 24 & 255 => x & 255
return ['binary', '&', input[2][2], node[3]];
}
} else if (type === 'binary' && node[1] === '^') {
// LLVM represents bitwise not as xor with -1. Translate it back to an actual bitwise not.
if (node[3][0] === 'unary-prefix' && node[3][1] === '-' && node[3][2][0] === 'num' &&
node[3][2][1] === 1 &&
!(node[2][0] == 'unary-prefix' && node[2][1] == '~')) { // avoid creating ~~~ which is confusing for asm given the role of ~~
return ['unary-prefix', '~', node[2]];
}
} else if (type === 'binary' && node[1] === '>>' && node[3][0] === 'num' &&
node[2][0] === 'binary' && node[2][1] === '<<' && node[2][3][0] === 'num' &&
node[2][2][0] === 'sub' && node[2][2][1][0] === 'name') {
// collapse HEAPU?8[..] << 24 >> 24 etc. into HEAP8[..] | 0
var amount = node[3][1];
var name = node[2][2][1][1];
if (amount === node[2][3][1] && parseHeap(name)) {
if (parseHeapTemp.bits === 32 - amount) {
node[2][2][1][1] = 'HEAP' + parseHeapTemp.bits;
node[1] = '|';
node[2] = node[2][2];
node[3][1] = 0;
rerunOrZeroPass = true;
return node;
}
}
} else if (type === 'assign') {
// optimizations for assigning into HEAP32 specifically
if (node[1] === true && node[2][0] === 'sub' && node[2][1][0] === 'name') {
if (node[2][1][1] === 'HEAP32') {
// HEAP32[..] = x | 0 does not need the | 0 (unless it is a mandatory |0 of a call)
if (node[3][0] === 'binary' && node[3][1] === '|') {
if (node[3][2][0] === 'num' && node[3][2][1] === 0 && node[3][3][0] != 'call') {
node[3] = node[3][3];
} else if (node[3][3][0] === 'num' && node[3][3][1] === 0 && node[3][2][0] != 'call') {
node[3] = node[3][2];
}
}
} else if (node[2][1][1] === 'HEAP8') {
// HEAP8[..] = x & 0xff does not need the & 0xff
if (node[3][0] === 'binary' && node[3][1] === '&' && node[3][3][0] == 'num' && node[3][3][1] == 0xff) {
node[3] = node[3][2];
}
} else if (node[2][1][1] === 'HEAP16') {
// HEAP16[..] = x & 0xffff does not need the & 0xffff
if (node[3][0] === 'binary' && node[3][1] === '&' && node[3][3][0] == 'num' && node[3][3][1] == 0xffff) {
node[3] = node[3][2];
}
}
}
var value = node[3];
if (value[0] === 'binary' && value[1] === '|') {
// canonicalize order of |0 to end
if (value[2][0] === 'num' && value[2][1] === 0) {
var temp = value[2];
value[2] = value[3];
value[3] = temp;
}
// if a seq ends in an |0, remove an external |0
// note that it is only safe to do this in assigns, like we are doing here (return (x, y|0); is not valid)
if (value[2][0] === 'seq' && value[2][2][0] === 'binary' && value[2][2][1] in USEFUL_BINARY_OPS) {
node[3] = value[2];
}
}
} else if (type === 'binary' && node[1] === '>>' && node[2][0] === 'num' && node[3][0] === 'num') {
// optimize num >> num, in asm we need this since we do not optimize shifts in asm.js
node[0] = 'num';
node[1] = node[2][1] >> node[3][1];
node.length = 2;
return node;
} else if (type === 'binary' && node[1] === '+') {
// The most common mathop is addition, e.g. in getelementptr done repeatedly. We can join all of those,
// by doing (num+num) ==> newnum.
if (node[2][0] === 'num' && node[3][0] === 'num') {
node[2][1] += node[3][1];
return node[2];
}
}
});
if (rerunOrZeroPass) removeMultipleOrZero();
if (asm) {
if (hasTempDoublePtr) {
var asmData = normalizeAsm(ast);
traverse(ast, function(node, type) {
if (type === 'assign') {
if (node[1] === true && node[2][0] === 'sub' && node[2][1][0] === 'name' && node[2][1][1] === 'HEAP32') {
// remove bitcasts that are now obviously pointless, e.g.
// HEAP32[$45 >> 2] = HEAPF32[tempDoublePtr >> 2] = ($14 < $28 ? $14 : $28) - $42, HEAP32[tempDoublePtr >> 2] | 0;
var value = node[3];
if (value[0] === 'seq' && value[1][0] === 'assign' && value[1][2][0] === 'sub' && value[1][2][1][0] === 'name' && value[1][2][1][1] === 'HEAPF32' &&
value[1][2][2][0] === 'binary' && value[1][2][2][2][0] === 'name' && value[1][2][2][2][1] === 'tempDoublePtr') {
// transform to HEAPF32[$45 >> 2] = ($14 < $28 ? $14 : $28) - $42;
node[2][1][1] = 'HEAPF32';
node[3] = value[1][3];
}
}
} else if (type === 'seq') {
// (HEAP32[tempDoublePtr >> 2] = HEAP32[$37 >> 2], +HEAPF32[tempDoublePtr >> 2])
// ==>
// +HEAPF32[$37 >> 2]
if (node[0] === 'seq' && node[1][0] === 'assign' && node[1][2][0] === 'sub' && node[1][2][1][0] === 'name' &&
(node[1][2][1][1] === 'HEAP32' || node[1][2][1][1] === 'HEAPF32') &&
node[1][2][2][0] === 'binary' && node[1][2][2][2][0] === 'name' && node[1][2][2][2][1] === 'tempDoublePtr' &&
node[1][3][0] === 'sub' && node[1][3][1][0] === 'name' && (node[1][3][1][1] === 'HEAP32' || node[1][3][1][1] === 'HEAPF32') &&
node[2][0] !== 'seq') { // avoid (x, y, z) which can be used for tempDoublePtr on doubles for alignment fixes
if (node[1][2][1][1] === 'HEAP32') {
node[1][3][1][1] = 'HEAPF32';
return makeAsmCoercion(node[1][3], detectType(node[2]));
} else {
node[1][3][1][1] = 'HEAP32';
return ['binary', '|', node[1][3], ['num', 0]];
}
}
}
});
// finally, wipe out remaining ones by finding cases where all assignments to X are bitcasts, and all uses are writes to
// the other heap type, then eliminate the bitcast
var bitcastVars = {};
traverse(ast, function(node, type) {
if (type === 'assign' && node[1] === true && node[2][0] === 'name') {
var value = node[3];
if (value[0] === 'seq' && value[1][0] === 'assign' && value[1][2][0] === 'sub' && value[1][2][1][0] === 'name' &&
(value[1][2][1][1] === 'HEAP32' || value[1][2][1][1] === 'HEAPF32') &&
value[1][2][2][0] === 'binary' && value[1][2][2][2][0] === 'name' && value[1][2][2][2][1] === 'tempDoublePtr') {
var name = node[2][1];
if (!bitcastVars[name]) bitcastVars[name] = {
define_HEAP32: 0, define_HEAPF32: 0, use_HEAP32: 0, use_HEAPF32: 0, bad: false, namings: 0, defines: [], uses: []
};
bitcastVars[name]['define_' + value[1][2][1][1]]++;
bitcastVars[name].defines.push(node);
}
}
});
traverse(ast, function(node, type) {
if (type === 'name' && bitcastVars[node[1]]) {
bitcastVars[node[1]].namings++;
} else if (type === 'assign' && node[1] === true) {
var value = node[3];
if (value[0] === 'name') {
var name = value[1];
if (bitcastVars[name]) {
var target = node[2];
if (target[0] === 'sub' && target[1][0] === 'name' && (target[1][1] === 'HEAP32' || target[1][1] === 'HEAPF32')) {
bitcastVars[name]['use_' + target[1][1]]++;
bitcastVars[name].uses.push(node);
}
}
}
}
});
for (var v in bitcastVars) {
var info = bitcastVars[v];
// good variables define only one type, use only one type, have definitions and uses, and define as a different type than they use
if (info.define_HEAP32*info.define_HEAPF32 === 0 && info.use_HEAP32*info.use_HEAPF32 === 0 &&
info.define_HEAP32+info.define_HEAPF32 > 0 && info.use_HEAP32+info.use_HEAPF32 > 0 &&
info.define_HEAP32*info.use_HEAP32 === 0 && info.define_HEAPF32*info.use_HEAPF32 === 0 &&
v in asmData.vars && info.namings === info.define_HEAP32+info.define_HEAPF32+info.use_HEAP32+info.use_HEAPF32) {
var correct = info.use_HEAP32 ? 'HEAPF32' : 'HEAP32';
info.defines.forEach(function(define) {
define[3] = define[3][1][3];
if (correct === 'HEAP32') {
define[3] = ['binary', '|', define[3], ['num', 0]];
} else {
define[3] = makeAsmCoercion(define[3], ASM_FLOAT);
}
// do we want a simplifybitops on the new values here?
});
info.uses.forEach(function(use) {
use[2][1][1] = correct;
});
var correctType;
switch(asmData.vars[v]) {
case ASM_INT: correctType = ASM_FLOAT; break;
case ASM_FLOAT: case ASM_DOUBLE: correctType = ASM_INT; break;
}
asmData.vars[v] = correctType;
}
}
denormalizeAsm(ast, asmData);
}
}
}
function emitsBoolean(node) {
if (node[0] === 'num') {
return node[1] === 0 || node[1] === 1;
}
if (node[0] === 'binary') return node[1] in COMPARE_OPS;
if (node[0] === 'unary-prefix') return node[1] === '!';
if (node[0] === 'conditional') return emitsBoolean(node[2]) && emitsBoolean(node[3]);
return false;
}
// expensive | expensive can be turned into expensive ? 1 : expensive, and
// expensive | cheap can be turned into cheap ? 1 : expensive,
// so that we can avoid the expensive computation, if it has no side effects.
function conditionalize(ast) {
var MIN_COST = 7;
traverse(ast, function(node, type) {
if (node[0] === 'binary' && (node[1] === '|' || node[1] === '&') && node[3][0] !== 'num' && node[2][0] !== 'num') {
// logical operator on two non-numerical values
var left = node[2];
var right = node[3];
if (!emitsBoolean(left) || !emitsBoolean(right)) return;
var leftEffects = hasSideEffects(left);
var rightEffects = hasSideEffects(right);
if (leftEffects && rightEffects) return; // both must execute
// canonicalize with side effects, if any, happening on the left
if (rightEffects) {
if (measureCost(left) < MIN_COST) return; // avoidable code is too cheap
var temp = left;
left = right;
right = temp;
} else if (leftEffects) {
if (measureCost(right) < MIN_COST) return; // avoidable code is too cheap
} else {
// no side effects, reorder based on cost estimation
var leftCost = measureCost(left);
var rightCost = measureCost(right);
if (Math.max(leftCost, rightCost) < MIN_COST) return; // avoidable code is too cheap
// canonicalize with expensive code on the right
if (leftCost > rightCost) {
var temp = left;
left = right;
right = temp;
}
}
// worth it, perform conditionalization
var ret;
if (node[1] === '|') {
ret = ['conditional', left, ['num', 1], right];
} else { // &
ret = ['conditional', left, right, ['num', 0]];
}
if (left[0] === 'unary-prefix' && left[1] === '!') {
ret[1] = flipCondition(left);
var temp = ret[2];
ret[2] = ret[3];
ret[3] = temp;
}
return ret;
}
});
}
function simplifyNotZero(ast) {
traverse(ast, function(node, type) {
if (node[0] in BOOLEAN_RECEIVERS) {
var boolean = node[1];
if (boolean[0] === 'binary' && boolean[1] === '!=' && boolean[3][0] === 'num' && boolean[3][1] === 0) {
node[1] = boolean[2];
}
}
});
}
traverseGeneratedFunctions(ast, function(func) {
simplifyIntegerConversions(func);
simplifyOps(func);
simplifyNotComps(func);
conditionalize(func);
simplifyNotZero(func);
});
}
// Checks if a coercion is necessary for asm.js, and cannot be
// removed. Receives the node, and the expression stack, which
// includes the node at the end.
function isNecessaryCoercion(node, stack) {
assert(stack[stack.length - 1] === node);
var parent = stack[stack.length - 2];
if (!parent) return false;
if (parent[0] === 'sub') {
// We are x & mask in FUNCTION_TABLE[x & mask], and the mask
// is necessary.
return true;
}
return false;
}
function localCSE(ast) {
// very simple CSE/GVN type optimization, factor out common expressions in a single basic block
assert(asm);
var MIN_COST = 3;
traverseGeneratedFunctions(ast, function(func) {
var asmData = normalizeAsm(func);
var counter = 0;
var optimized = false;
traverse(func, function(node, type) {
var stats = getStatements(node);
if (!stats) return;
var exps = {}; // JSON'd expression => [i it first appears on, original node, replacement var, type, sign]
var deps = {}; // dependency (local name, or 'memory' or 'global')
function invalidate(what) {
var list = deps[what];
if (!list) return;
for (var i = 0; i < list.length; i++) {
delete exps[list[i]];
}
delete deps[what];
}
function doInvalidations(curr) {
return traverse(curr, function(node, type) {
if (type in CONTROL_FLOW) {
exps = {};
deps = {};
return true; // abort everything
}
if (type === 'assign') {
var target = node[2];
if (target[0] === 'name') {
var name = target[1];
if (name in asmData.params || name in asmData.vars) {
invalidate(name);
} else {
invalidate('<global>');
}
} else {
assert(target[0] === 'sub');
invalidate('<memory>');
}
}
if (type === 'call') {
invalidate('<global>');
invalidate('<memory>');
}
});
}
for (var i = 0; i < stats.length; i++) {
var curr = stats[i];
// first, look at the entire line and invalidate what we need to
if (doInvalidations(curr) === true) {
continue; // we saw control flow
}
// next, process the line and try to find useful expressions
var skips = [];
var stack = [];
traverse(curr, function seekExpressions(node, type) {
stack.push(node);
if (type === 'sub' && node[1][0] === 'name' && node[2][0] === 'binary' && node[2][1] === '>>') {
// skip over the shift, we can't cse that
skips.push(node[2]);
return;
}
if (type === 'binary' || type === 'unary-prefix') {
if (type === 'binary' && skips.indexOf(node) >= 0) return;
if (measureCost(node) < MIN_COST) return;
if (detectType(node, asmData) === ASM_NONE) return; // if we can't figure it out locally, forget it
// We cannot CSE out a necessary asm.js coercion
if (isNecessaryCoercion(node, stack)) return;
var str = JSON.stringify(node);
var lookup = exps[str];
if (!lookup) {
// add ourselves, and set up our deps
exps[str] = [i, node, null];
traverse(node, function(node, type) {
var names = [];
if (type === 'name') {
var name = node[1];
if (!(name in asmData.params || name in asmData.vars)) name = '<global>';
names.push(name);
} else if (type === 'sub') {
names.push('<memory>');
} else if (type === 'call') {
names.push('<memory>');
names.push('<global>');
}
names.forEach(function(name) {
if (!deps[name]) deps[name] = [];
deps[name].push(str);
});
});
} else {
//printErr('CSEing ' + str);
optimized = true;
var type, sign;
// with the original node plus us, this is worth optimizing out
if (lookup[2] === null) {
// this is the first node after the first. generate the saved var, and optimize out the original
lookup[2] = 'CSE$' + (counter++);
// ensure an asm coercion
type = lookup[3] = detectType(node, asmData);
sign = detectSign(node);
if (sign === ASM_FLEXIBLE) sign = ASM_SIGNED;
lookup[4] = sign;
asmData.vars[lookup[2]] = type;
overwrite(lookup[1], makeSignedAsmCoercion(['name', lookup[2]], type, sign));
stats.splice(lookup[0], 0, ['stat', ['assign', true, ['name', lookup[2]], makeSignedAsmCoercion(node, type, sign)]]);
// adjust indexes after that splice
i++; // i must be after lookup[0]
for (var e in exps) {
var curr = exps[e];
if (curr[2] === null) {
if (curr[0] >= lookup[0]) curr[0]++;
}
}
} else {
type = lookup[3];
sign = lookup[4];
}
// optimize out ourselves
return makeSignedAsmCoercion(['name', lookup[2]], type, sign);
}
}
}, function(node, type) { // post-traversal
stack.pop();
});
// finally, repeat invalidation processing, to not be sensitive to inter-line control flow
doInvalidations(curr);
}
});
denormalizeAsm(func, asmData);
if (optimized) {
simplifyExpressions(func); // remove double coercions, etc.
}
});
}
function safeLabelSettingInternal(func, asmData) {
if ('label' in asmData.vars) {
var stats = getStatements(func);
var seenVar = false;
for (var i = 0; i < stats.length; i++) {
var curr = stats[i];
if (curr[0] === 'stat') curr = curr[1];
if (curr[0] === 'var') {
seenVar = true;
} else if (seenVar && curr[0] !== 'var') {
// first location after the vars
stats.splice(i, 0, ['stat', ['assign', true, ['name', 'label'], ['num', 0]]]);
break;
}
}
}
}
function safeLabelSetting(ast) {
// Add an assign to label, if it exists, so that even after we minify/registerize variable names, we can tell if any vars use the asm init value of 0 - none will, so it's easy to tell
assert(asm);
traverseGeneratedFunctions(ast, function(func) {
var asmData = normalizeAsm(func);
safeLabelSettingInternal(func, asmData);
denormalizeAsm(func, asmData);
});
}
function simplifyIfs(ast) {
traverseGeneratedFunctions(ast, function(func) {
var simplifiedAnElse = false;
traverse(func, function(node, type) {
// simplify if (x) { if (y) { .. } } to if (x ? y : 0) { .. }
if (type === 'if') {
var body = node[2];
// recurse to handle chains
while (body[0] === 'block') {
var stats = body[1];
if (stats.length === 0) break;
var other = stats[stats.length-1];
if (other[0] !== 'if') {
// our if block does not end with an if. perhaps if have an else we can flip
if (node[3] && node[3][0] === 'block') {
var stats = node[3][1];
if (stats.length === 0) break;
var other = stats[stats.length-1];
if (other[0] === 'if') {
// flip node
node[1] = flipCondition(node[1]);
node[2] = node[3];
node[3] = body;
body = node[2];
} else break;
} else break;
}
// we can handle elses, but must be fully identical
if (node[3] || other[3]) {
if (!node[3]) break;
if (!astCompare(node[3], other[3])) {
// the elses are different, but perhaps if we flipped a condition we can do better
if (astCompare(node[3], other[2])) {
// flip other. note that other may not have had an else! add one if so; we will eliminate such things later
if (!other[3]) other[3] = ['block', []];
other[1] = flipCondition(other[1]);
var temp = other[2];
other[2] = other[3];
other[3] = temp;
} else break;
}
}
if (stats.length > 1) {
// try to commaify - turn everything between the ifs into a comma operator inside the second if
var ok = true;
for (var i = 0; i < stats.length-1; i++) {
var curr = stats[i];
if (curr[0] === 'stat') curr = curr[1];
if (!(curr[0] in COMMABLE)) ok = false;
}
if (!ok) break;
for (var i = stats.length-2; i >= 0; i--) {
var curr = stats[i];
if (curr[0] === 'stat') curr = curr[1];
other[1] = ['seq', curr, other[1]];
}
stats = body[1] = [other];
}
if (stats.length !== 1) break;
if (node[3]) simplifiedAnElse = true;
node[1] = ['conditional', node[1], other[1], ['num', 0]];
body = node[2] = other[2];
}
}
});
if (simplifiedAnElse) {
// there may be fusing opportunities
// we can only fuse if we remove all uses of the label. if there are
// other ones - if the label check can be reached from elsewhere -
// we must leave it
var abort = false;
var labelAssigns = {};
traverse(func, function(node, type) {
if (type === 'assign' && node[2][0] === 'name' && node[2][1] === 'label') {
if (node[3][0] === 'num') {
var value = node[3][1];
labelAssigns[value] = (labelAssigns[value] || 0) + 1;
} else {
// label is assigned a dynamic value (like from indirectbr), we cannot do anything
abort = true;
}
}
});
if (abort) return;
var labelChecks = {};
traverse(func, function(node, type) {
if (type === 'binary' && node[1] === '==' && node[2][0] === 'binary' && node[2][1] === '|' &&
node[2][2][0] === 'name' && node[2][2][1] === 'label') {
if (node[3][0] === 'num') {
var value = node[3][1];
labelChecks[value] = (labelChecks[value] || 0) + 1;
} else {
// label is checked vs a dynamic value (like from indirectbr), we cannot do anything
abort = true;
}
}
});
if (abort) return;
var inLoop = 0; // when in a loop, we do not emit label = 0; in the relooper as there is no need
traverse(func, function(node, type) {
if (type === 'while') inLoop++;
var stats = getStatements(node);
if (stats) {
for (var i = 0; i < stats.length-1; i++) {
var pre = stats[i];
var post = stats[i+1];
if (pre[0] === 'if' && pre[3] && post[0] === 'if' && !post[3]) {
var postCond = post[1];
if (postCond[0] === 'binary' && postCond[1] === '==' &&
postCond[2][0] === 'binary' && postCond[2][1] === '|' &&
postCond[2][2][0] === 'name' && postCond[2][2][1] === 'label' &&
postCond[2][3][0] === 'num' && postCond[2][3][1] === 0 &&
postCond[3][0] === 'num') {
var postValue = postCond[3][1];
var preElse = pre[3];
if (labelAssigns[postValue] === 1 && labelChecks[postValue] === 1 && preElse[0] === 'block' && preElse[1] && preElse[1].length === 1) {
var preStat = preElse[1][0];
if (preStat[0] === 'stat' && preStat[1][0] === 'assign' &&
preStat[1][1] === true && preStat[1][2][0] === 'name' && preStat[1][2][1] === 'label' &&
preStat[1][3][0] === 'num' && preStat[1][3][1] === postValue) {
// Conditions match, just need to make sure the post clears label
if (post[2][0] === 'block' && post[2][1] && post[2][1].length > 0) {
var postStat = post[2][1][0];
var haveClear =
postStat[0] === 'stat' && postStat[1][0] === 'assign' &&
postStat[1][1] === true && postStat[1][2][0] === 'name' && postStat[1][2][1] === 'label' &&
postStat[1][3][0] === 'num' && postStat[1][3][1] === 0;
if (!inLoop || haveClear) {
// Everything lines up, do it
pre[3] = post[2];
if (haveClear) pre[3][1].splice(0, 1); // remove the label clearing
stats.splice(i+1, 1); // remove the post entirely
}
}
}
}
}
}
}
}
}, function(node, type) {
if (type === 'while') inLoop--;
});
}
});
}
// We often have branchings that are simplified so one end vanishes, and
// we then get
// if (!(x < 5))
// or such. Simplifying these saves space and time.
function simplifyNotCompsDirect(node) {
if (node[0] === 'unary-prefix' && node[1] === '!') {
// de-morgan's laws do not work on floats, due to nans >:(
if (node[2][0] === 'binary' && (!asm || (detectType(node[2][2]) === ASM_INT && detectType(node[2][3]) === ASM_INT))) {
switch(node[2][1]) {
case '<': return ['binary', '>=', node[2][2], node[2][3]];
case '>': return ['binary', '<=', node[2][2], node[2][3]];
case '<=': return ['binary', '>', node[2][2], node[2][3]];
case '>=': return ['binary', '<', node[2][2], node[2][3]];
case '==': return ['binary', '!=', node[2][2], node[2][3]];
case '!=': return ['binary', '==', node[2][2], node[2][3]];
case '===': return ['binary', '!==', node[2][2], node[2][3]];
case '!==': return ['binary', '===', node[2][2], node[2][3]];
}
} else if (node[2][0] === 'unary-prefix' && node[2][1] === '!') {
return node[2][2];
}
}
if (!simplifyNotCompsPass) return node;
}
var SAFE_TO_DROP_COERCION = set('unary-prefix', 'name', 'num');
function canDropCoercion(node) {
if (node[0] in SAFE_TO_DROP_COERCION) return true;
if (node[0] === 'binary') {
switch (node[1]) {
case '>>': case '>>>': case '<<': case '|': case '^': case '&': return true;
}
}
return false;
}
function simplifyCondition(node) {
node = simplifyNotCompsDirect(node);
// on integers, if (x == 0) is the same as if (x), and if (x != 0) as if (!x)
if (node[0] === 'binary' && (node[1] === '==' || node[1] === '!=')) {
var target = null;
if (detectType(node[2]) === ASM_INT && node[3][0] === 'num' && node[3][1] === 0) {
target = node[2];
} else if (detectType(node[3]) === ASM_INT && node[2][0] === 'num' && node[2][1] === 0) {
target = node[3];
}
if (target) {
if (target[0] === 'binary' && (target[1] === '|' || target[1] === '>>>') && target[3][0] === 'num' && target[3][1] === 0 &&
canDropCoercion(target[2])) {
target = target[2]; // drop the coercion, in a condition it is ok to do if (x)
}
if (node[1] === '==') {
return ['unary-prefix', '!', target];
} else {
return target;
}
}
}
return node;
}
function flipCondition(cond) {
return simplifyNotCompsDirect(['unary-prefix', '!', cond]);
}
var simplifyNotCompsPass = false;
function simplifyNotComps(ast) {
simplifyNotCompsPass = true;
traverse(ast, simplifyNotCompsDirect);
simplifyNotCompsPass = false;
}
function isMathFunc(name) {
return /^Math_/.test(name);
}
function callHasSideEffects(node) { // checks if the call itself (not the args) has side effects (or is not statically known)
return !(node[1][0] === 'name' && isMathFunc(node[1][1]));
}
function hasSideEffects(node) { // this is 99% incomplete!
switch (node[0]) {
case 'num': case 'name': case 'string': return false;
case 'unary-prefix': return hasSideEffects(node[2]);
case 'binary': return hasSideEffects(node[2]) || hasSideEffects(node[3]);
case 'sub': return hasSideEffects(node[1]) || hasSideEffects(node[2]);
case 'call': {
if (callHasSideEffects(node)) return true;
// This is a statically known call, with no side effects. only args can side effect us
var args = node[2];
var num = args.length;
for (var i = 0; i < num; i++) {
if (hasSideEffects(args[i])) return true;
}
return false;
}
case 'conditional': return hasSideEffects(node[1]) || hasSideEffects(node[2]) || hasSideEffects(node[3]);
case 'function': case 'defun': return false;
case 'object': {
var children = node[1];
if (!Array.isArray(children)) return false;
for (var i = 0; i < children.length; i++) {
if (hasSideEffects(children[i][1])) return true;
}
return false;
}
case 'array': {
var children = node[1];
if (!Array.isArray(children)) return false;
for (var i = 0; i < children.length; i++) {
if (hasSideEffects(children[i])) return true;
}
return false;
}
case 'dot': {
// In theory any property access in JS can have side effects, but not in
// objects we assume are static.
if (node[1][0] === 'name') {
var name = node[1][1];
if (name === 'Math') return false;
}
return true;
}
default: return true;
}
}
// checks if a node has just basic operations, nothing with side effects nor that can notice side effects, which
// implies we can move it around in the code
function triviallySafeToMove(node, asmData) {
var ok = true;
traverse(node, function(node, type) {
switch (type) {
case 'stat': case 'binary': case 'unary-prefix': case 'assign': case 'num':
break;
case 'name':
if (!(node[1] in asmData.vars) && !(node[1] in asmData.params)) ok = false;
break;
case 'call':
if (callHasSideEffects(node)) ok = false;
break;
default:
ok = false;
}
});
return ok;
}
// Clear out empty ifs and blocks, and redundant blocks/stats and so forth
// Operates on generated functions only
function vacuum(ast) {
function isEmpty(node) {
if (!node) return true;
if (node[0] === 'toplevel' && (!node[1] || node[1].length === 0)) return true;
if (node[0] === 'block' && (!node[1] || (typeof node[1] != 'object') || node[1].length === 0 || (node[1].length === 1 && isEmpty(node[1])))) return true;
return false;
}
function simplifyList(node, si) {
var changed = false;
// Merge block items into this list, thus removing unneeded |{ .. }|'s
var statements = node[si];
var i = 0;
while (i < statements.length) {
var subNode = statements[i];
if (subNode[0] === 'block') {
statements.splice.apply(statements, [i, 1].concat(subNode[1] || []));
changed = true;
} else {
i++;
}
}
// Remove empty items
var pre = node[si].length;
node[si] = node[si].filter(function(node) { return !isEmpty(node) });
if (node[si].length < pre) changed = true;
if (changed) {
return node;
}
}
function vacuumInternal(node) {
traverseChildren(node, vacuumInternal);
var ret;
switch(node[0]) {
case 'block': {
if (node[1] && node[1].length === 1 && node[1][0][0] === 'block') {
return node[1][0];
} else if (typeof node[1] === 'object') {
ret = simplifyList(node, 1);
if (ret) return ret;
}
} break;
case 'stat': {
if (node[1][0] === 'block') {
return node[1];
} else if (!hasSideEffects(node[1])) {
return emptyNode();
}
} break;
case 'defun': {
if (node[3].length === 1 && node[3][0][0] === 'block') {
node[3] = node[3][0][1];
return node;
} else {
ret = simplifyList(node, 3);
if (ret) return ret;
}
} break;
case 'do': {
if (node[1][0] === 'num' && node[2][0] === 'toplevel' && (!node[2][1] || node[2][1].length === 0)) {
return emptyNode();
} else if (isEmpty(node[2]) && !hasSideEffects(node[1])) {
return emptyNode();
}
} break;
case 'label': {
if (node[2] && node[2][0] === 'toplevel' && (!node[2][1] || node[2][1].length === 0)) {
return emptyNode();
}
} break;
case 'if': {
var empty2 = isEmpty(node[2]), empty3 = isEmpty(node[3]), has3 = node.length === 4;
if (!empty2 && empty3 && has3) { // empty else clauses
return node.slice(0, 3);
} else if (empty2 && !empty3) { // empty if blocks
return ['if', ['unary-prefix', '!', node[1]], node[3]];
} else if (empty2 && empty3) {
if (hasSideEffects(node[1])) {
return ['stat', node[1]];
} else {
return emptyNode();
}
}
} break;
}
}
traverseGeneratedFunctions(ast, function(node) {
vacuumInternal(node);
simplifyNotComps(node);
removeEmptySubNodes(node);
});
}
function getStatements(node) {
if (node[0] === 'defun') {
return node[3];
} else if (node[0] === 'block') {
return node[1];
} else {
return null;
}
}
// Multiple blocks from the relooper are, in general, implemented by
// if (label === x) { } else if ..
// and branching into them by
// if (condition) { label === x } else ..
// We can hoist the multiple block into the condition, thus removing code and one 'if' check
function hoistMultiples(ast) {
traverseGeneratedFunctions(ast, function(node) {
traverse(node, function(node, type) {
var statements = getStatements(node);
if (!statements) return;
var modified = false;
for (var i = 0; i < statements.length-1; i++) {
var modifiedI = false;
var pre = statements[i];
if (pre[0] != 'if') continue;
var post = statements[i+1];
// Look into some block types. shell() will then recreate the shell that we looked into
var postInner = post;
var shellLabel = false, shellDo = false;
while (true) {
if (postInner[0] === 'label') {
shellLabel = postInner[1];
postInner = postInner[2];
} else if (postInner[0] === 'do') {
shellDo = postInner[1];
postInner = postInner[2][1][0];
} else {
break; // give up
}
}
if (postInner[0] != 'if') continue;
// Look into this if, and its elseifs
while (postInner && postInner[0] === 'if') {
var cond = postInner[1];
if (cond[0] === 'binary' && cond[1] === '==' && cond[2][0] === 'name' && cond[2][1] === 'label') {
assert(cond[3][0] === 'num');
// We have a valid Multiple check here. Try to hoist it, look for the source in |pre| and its else's
var labelNum = cond[3][1];
var labelBlock = postInner[2];
assert(labelBlock[0] === 'block');
var found = false;
traverse(pre, function(preNode, preType) {
if (!found && preType === 'assign' && preNode[2][0] === 'name' && preNode[2][1] === 'label') {
assert(preNode[3][0] === 'num');
if (preNode[3][1] === labelNum) {
// That's it! Hoist away. We can also throw away the label setting as its goal has already been achieved
found = true;
modifiedI = true;
postInner[2] = ['block', []];
return labelBlock;
}
}
});
}
postInner = postInner[3]; // Proceed to look in the else clause
}
if (modifiedI) {
if (shellDo) {
statements[i] = ['do', shellDo, ['block', [statements[i]]]];
}
if (shellLabel) {
statements[i] = ['label', shellLabel, statements[i]];
}
}
}
if (modified) return node;
});
// After hoisting in this function, it is safe to remove { label = x; } blocks, because
// if they were leading to the next code right after them, they would be hoisted, and if they
// are going to some other place entirely, they would break or continue. The only risky
// situation is if the code after us is a multiple, in which case we might be checking for
// this label inside it (or in a later multiple, even)
function tryEliminate(node) {
if (node[0] === 'if') {
var replaced;
if (replaced = tryEliminate(node[2])) node[2] = replaced;
if (node[3] && (replaced = tryEliminate(node[3]))) node[3] = replaced;
} else {
if (node[0] === 'block' && node[1] && node[1].length > 0) {
var subNode = node[1][node[1].length-1];
if (subNode[0] === 'stat' && subNode[1][0] === 'assign' && subNode[1][2][0] === 'name' &&
subNode[1][2][1] === 'label' && subNode[1][3][0] === 'num') {
if (node[1].length === 1) {
return emptyNode();
} else {
node[1].splice(node[1].length-1, 1);
return node;
}
}
}
}
return false;
}
function getActualStatement(node) { // find the actual active statement, ignoring a label and one-time do loop
if (node[0] === 'label') node = node[2];
if (node[0] === 'do') node = node[2];
if (node[0] === 'block' && node[1].length === 1) node = node[1][0];
return node;
}
vacuum(node);
traverse(node, function(node, type) {
var statements = getStatements(node);
if (!statements) return;
for (var i = 0; i < statements.length-1; i++) {
var curr = getActualStatement(statements[i]);
var next = statements[i+1];
if (curr[0] === 'if' && next[0] != 'if' && next[0] != 'label' && next[0] != 'do' && next[0] != 'while') {
tryEliminate(curr);
}
}
});
});
vacuum(ast);
// Afterpass: Reduce
// if (..) { .. break|continue } else { .. }
// to
// if (..) { .. break|continue } ..
traverseGenerated(ast, function(container, type) {
var statements = getStatements(container);
if (!statements) return;
for (var i = 0; i < statements.length; i++) {
var node = statements[i];
if (node[0] === 'if' && node[2][0] === 'block' && node[3] && node[3][0] === 'block') {
var stat1 = node[2][1], stat2 = node[3][1];
// If break|continue in the latter and not the former, reverse them
if (!(stat1[stat1.length-1][0] in LOOP_FLOW) && (stat2[stat2.length-1][0] in LOOP_FLOW)) {
var temp = node[3];
node[3] = node[2];
node[2] = temp;
node[1] = flipCondition(node[1]);
stat1 = node[2][1];
stat2 = node[3][1];
}
if (stat1[stat1.length-1][0] in LOOP_FLOW) {
statements.splice.apply(statements, [i+1, 0].concat(stat2));
node[3] = null;
}
}
}
});
}
// Simplifies loops
// WARNING: This assumes all loops and breaks/continues are labelled
function loopOptimizer(ast) {
// Remove unneeded labels and one-time (do while(0)) loops. It is convenient to do these both at once.
function passTwo(ast) {
var neededDos = [];
// Find unneeded labels
traverseGenerated(ast, function(node, type, stack) {
if (type === 'label' && node[2][0] in LOOP) {
// this is a labelled loop. we don't know if it's needed yet. Mark its label for removal for now now.
stack.push(node);
node[1] = '+' + node[1];
} else if (type in LOOP) {
stack.push(node);
} else if (type in LOOP_FLOW) {
// Find topmost loop, and its label if there is one
var lastLabel = null, lastLoop = null, i = stack.length-1;
while (i >= 0 && !lastLoop) {
if (stack[i][0] in LOOP) lastLoop = stack[i];
i--;
}
assert(lastLoop, 'Cannot break/continue without a Label');
while (i >= 0 && !lastLabel) {
if (stack[i][0] in LOOP) break; // another loop in the middle - no label for lastLoop
if (stack[i][0] === 'label') lastLabel = stack[i];
i--;
}
var ident = node[1]; // there may not be a label ident if this is a simple break; or continue;
var plus = '+' + ident;
if (lastLabel && ident && (ident === lastLabel[1] || plus === lastLabel[1])) {
// If this is a 'do' loop, this break means we actually need it.
neededDos.push(lastLoop);
// We don't need the control flow command to have a label - it's referring to the current loop
return [node[0]];
} else {
if (!ident) {
// No label on the break/continue, so keep the last loop alive (no need for its label though)
neededDos.push(lastLoop);
} else {
// Find the label node that needs to stay alive
stack.forEach(function(label) {
if (!label) return;
if (label[1] === plus) label[1] = label[1].substr(1); // Remove '+', marking it as needed
});
}
}
}
}, null, []);
// We return whether another pass is necessary
var more = false;
// Remove unneeded labels
traverseGenerated(ast, function(node, type) {
if (type === 'label' && node[1][0] === '+') {
more = true;
var ident = node[1].substr(1);
// Remove label from loop flow commands
traverse(node[2], function(node2, type) {
if (type in LOOP_FLOW && node2[1] === ident) {
return [node2[0]];
}
});
return node[2]; // Remove the label itself on the loop
}
});
// Remove unneeded one-time loops. We need such loops if (1) they have a label, or (2) they have a direct break so they are in neededDos.
// First, add all labeled loops of this nature to neededDos
traverseGenerated(ast, function(node, type) {
if (type === 'label' && node[2][0] === 'do') {
neededDos.push(node[2]);
}
});
// Remove unneeded dos, we know who they are now
traverseGenerated(ast, function(node, type) {
if (type === 'do' && neededDos.indexOf(node) < 0) {
assert(jsonCompare(node[1], ['num', 0]), 'Trying to remove a one-time do loop that is not one of our generated ones.;');
more = true;
return node[2];
}
});
return more;
}
// Go
// TODO: pass 1: Removal of unneeded continues, breaks if they get us to where we are already going. That will
// help the next pass.
// Multiple pass two runs may be needed, as we remove one-time loops and so forth
do {
var more = passTwo(ast);
vacuum(ast);
} while (more);
vacuum(ast);
}
function unVarify(vars, ret) { // transform var x=1, y=2 etc. into (x=1, y=2), i.e., the same assigns, but without a var definition
ret = ret || [];
ret[0] = 'stat';
if (vars.length === 1) {
ret[1] = ['assign', true, ['name', vars[0][0]], vars[0][1]];
} else {
ret[1] = [];
var curr = ret[1];
for (var i = 0; i < vars.length-1; i++) {
curr[0] = 'seq';
curr[1] = ['assign', true, ['name', vars[i][0]], vars[i][1]];
if (i != vars.length-2) curr = curr[2] = [];
}
curr[2] = ['assign', true, ['name', vars[vars.length-1][0]], vars[vars.length-1][1]];
}
return ret;
}
// asm.js support code - normalize (convert asm.js code to 'normal' JS, without
// annotations, plus explicit metadata) and denormalize (vice versa)
var ASM_INT = 0;
var ASM_DOUBLE = 1;
var ASM_FLOAT = 2;
var ASM_FLOAT32X4 = 3;
var ASM_FLOAT64X2 = 4;
var ASM_INT8X16 = 5;
var ASM_INT16X8 = 6;
var ASM_INT32X4 = 7;
var ASM_BOOL8X16 = 8;
var ASM_BOOL16X8 = 9;
var ASM_BOOL32X4 = 10;
var ASM_BOOL64X2 = 11;
var ASM_NONE = 12;
var ASM_SIG = {
0: 'i',
1: 'd',
2: 'f',
3: 'F',
4: 'D',
5: 'B',
6: 'S',
7: 'I',
8: 'Z', // For Bool SIMD.js types, arbitrarily use consecutive letters ZXCV.
9: 'X',
10: 'C',
11: 'V',
12: 'v'
};
var ASM_FLOAT_ZERO = null; // TODO: share the entire node?
function detectType(node, asmInfo, inVarDef) {
// for params, +x vs x|0, for vars, 0.0 vs 0
switch (node[0]) {
case 'num': {
if (node[1].toString().indexOf('.') >= 0) return ASM_DOUBLE;
return ASM_INT;
}
case 'unary-prefix': {
switch (node[1]) {
case '+': return ASM_DOUBLE;
case '-': return detectType(node[2], asmInfo, inVarDef);
case '!': case '~': return ASM_INT;
}
break;
}
case 'call': {
if (node[1][0] === 'name') {
switch (node[1][1]) {
case 'Math_fround': return ASM_FLOAT;
case 'SIMD_Float32x4':
case 'SIMD_Float32x4_check': return ASM_FLOAT32X4;
case 'SIMD_Float64x2':
case 'SIMD_Float64x2_check': return ASM_FLOAT64X2;
case 'SIMD_Int8x16':
case 'SIMD_Int8x16_check': return ASM_INT8X16;
case 'SIMD_Int16x8':
case 'SIMD_Int16x8_check': return ASM_INT16X8;
case 'SIMD_Int32x4':
case 'SIMD_Int32x4_check': return ASM_INT32X4;
case 'SIMD_Bool8x16':
case 'SIMD_Bool8x16_check': return ASM_BOOL8X16;
case 'SIMD_Bool16x8':
case 'SIMD_Bool16x8_check': return ASM_BOOL16X8;
case 'SIMD_Bool32x4':
case 'SIMD_Bool32x4_check': return ASM_BOOL32X4;
case 'SIMD_Bool64x2':
case 'SIMD_Bool64x2_check': return ASM_BOOL64X2;
default: break;
}
}
return ASM_NONE;
}
case 'name': {
if (asmInfo) {
var ret = getAsmType(node[1], asmInfo);
if (ret !== ASM_NONE) return ret;
}
if (!inVarDef) {
if (ASM_FLOAT_ZERO && ASM_FLOAT_ZERO === node[1]) return ASM_FLOAT;
switch (node[1]) {
case 'inf': case 'nan': return ASM_DOUBLE; // TODO: when minified
case 'tempRet0': return ASM_INT;
}
return ASM_NONE;
}
// We are in a variable definition, where Math_fround(0) optimized into a global constant becomes f0 = Math_fround(0)
if (!ASM_FLOAT_ZERO) ASM_FLOAT_ZERO = node[1];
else assert(ASM_FLOAT_ZERO === node[1]);
return ASM_FLOAT;
}
case 'binary': {
switch (node[1]) {
case '+': case '-':
case '*': case '/': case '%': {
var ret = detectType(node[2], asmInfo, inVarDef);
if (ret !== ASM_NONE) return ret;
return detectType(node[3], asmInfo, inVarDef)
}
case '|': case '&': case '^': case '<<': case '>>': case '>>>':
case '==': case '!=': case '<': case '<=': case '>': case '>=': {
return ASM_INT;
}
}
break;
}
case 'conditional': case 'seq': {
return detectType(node[2], asmInfo, inVarDef);
}
case 'sub': {
assert(node[1][0] === 'name');
if (!parseHeap(node[1][1])) return ASM_NONE;
return parseHeapTemp.float ? ASM_DOUBLE : ASM_INT; // XXX ASM_FLOAT?
}
case 'stat': return ASM_NONE;
}
assert(0 , 'horrible ' + JSON.stringify(node));
}
function isAsmCoercion(node) {
if (node[0] === 'binary' && ((node[1] === '|' && node[3][0] === 'num' && node[3][1] === 0) ||
(node[1] === '>>>' && node[3][0] === 'num' && node[3][1] === 0))) return true;
if (node[0] === 'unary-prefix' && (node[1] === '+' || (node[1] === '~' && node[2][0] === 'unary-prefix' && node[2][1] === '~'))) return true;
return false;
}
function makeAsmCoercion(node, type) {
switch (type) {
case ASM_INT: return ['binary', '|', node, ['num', 0]];
case ASM_DOUBLE: return ['unary-prefix', '+', node];
case ASM_FLOAT: return ['call', ['name', 'Math_fround'], [node]];
case ASM_FLOAT32X4: return ['call', ['name', 'SIMD_Float32x4_check'], [node]];
case ASM_FLOAT64X2: return ['call', ['name', 'SIMD_Float64x2_check'], [node]];
case ASM_INT8X16: return ['call', ['name', 'SIMD_Int8x16_check'], [node]];
case ASM_INT16X8: return ['call', ['name', 'SIMD_Int16x8_check'], [node]];
case ASM_INT32X4: return ['call', ['name', 'SIMD_Int32x4_check'], [node]];
case ASM_BOOL8X16: return ['call', ['name', 'SIMD_Bool8x16_check'], [node]];
case ASM_BOOL16X8: return ['call', ['name', 'SIMD_Bool16x8_check'], [node]];
case ASM_BOOL32X4: return ['call', ['name', 'SIMD_Bool32x4_check'], [node]];
case ASM_BOOL64X2: return ['call', ['name', 'SIMD_Bool64x2_check'], [node]];
case ASM_NONE:
default: return node; // non-validating code, emit nothing XXX this is dangerous, we should only allow this when we know we are not validating
}
}
function makeSignedAsmCoercion(node, type, sign) {
if (type !== ASM_INT || sign === ASM_SIGNED) return makeAsmCoercion(node, type);
if (sign === ASM_UNSIGNED) return ['binary', '>>>', node, ['num', 0]];
assert(0);
}
function makeAsmCoercedZero(type) {
switch (type) {
case ASM_INT: return ['num', 0];
case ASM_DOUBLE: return ['unary-prefix', '+', ['num', 0]];
case ASM_FLOAT: {
if (ASM_FLOAT_ZERO) {
return ['name', ASM_FLOAT_ZERO];
} else {
return ['call', ['name', 'Math_fround'], [['num', 0]]];
}
}
case ASM_FLOAT32X4: {
return ['call', ['name', 'SIMD_Float32x4'], [['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_FLOAT64X2: {
return ['call', ['name', 'SIMD_Float64x2'], [['num', 0], ['num', 0]]];
}
case ASM_INT8X16: {
return ['call', ['name', 'SIMD_Int8x16'], [['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_INT16X8: {
return ['call', ['name', 'SIMD_Int16x8'], [['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_INT32X4: {
return ['call', ['name', 'SIMD_Int32x4'], [['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_BOOL8X16: {
return ['call', ['name', 'SIMD_Bool8x16'], [['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_BOOL16X8: {
return ['call', ['name', 'SIMD_Bool16x8'], [['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_BOOL32X4: {
return ['call', ['name', 'SIMD_Bool32x4'], [['num', 0], ['num', 0], ['num', 0], ['num', 0]]];
}
case ASM_BOOL64X2: {
return ['call', ['name', 'SIMD_Bool64x2'], [['num', 0], ['num', 0]]];
}
default: throw 'wha? ' + JSON.stringify(type) + new Error().stack;
}
}
function makeAsmVarDef(v, type) {
return [v, makeAsmCoercedZero(type)];
}
function getAsmType(name, asmInfo) {
if (name in asmInfo.vars) return asmInfo.vars[name];
if (name in asmInfo.params) return asmInfo.params[name];
return ASM_NONE;
}
function getCombinedType(node1, node2, asmData, hint) {
var type1 = detectType(node1, asmData);
var type2 = detectType(node2, asmData);
if (type1 === ASM_NONE && type2 === ASM_NONE) {
assert(hint !== undefined);
return hint;
}
if (type1 === ASM_NONE) {
assert(type2 != ASM_NONE);
return type2;
} else if (type2 === ASM_NONE) {
assert(type1 != ASM_NONE);
return type1;
}
if (type1 !== type2) {
if (type1 === hint || type2 === hint) return hint;
assert(0, "can't figure it out " + JSON.stringify([node1, '....', node2, ' ', type1, type2, hint], null, ' '));
}
return type1;
}
var ASM_FLEXIBLE = 0; // small constants can be signed or unsigned, variables are also flexible
var ASM_SIGNED = 1;
var ASM_UNSIGNED = 2;
var ASM_NONSIGNED = 3;
function detectSign(node) {
switch (node[0]) {
case 'binary': {
switch(node[1]) {
case '|': case '&': case '^': case '<<': case '>>': return ASM_SIGNED;
case '>>>': return ASM_UNSIGNED;
case '+': case '-': return ASM_FLEXIBLE;
case '*': {
// a double, unless one is a small int and the other is an int, in
// which case one is a num. that can't be a double, since then it
// would be a +num.
if (node[2][0] === 'num' || node[3][0] === 'num') return ASM_FLEXIBLE;
return ASM_NONSIGNED;
}
case '/': case '%': return ASM_NONSIGNED; // without a coercion, this is double
case '==': case '!=': case '<': case '<=': case '>': case '>=': return ASM_SIGNED;
default: throw 'yikes ' + node[1];
}
break;
}
case 'unary-prefix': {
switch(node[1]) {
case '-': return ASM_FLEXIBLE;
case '+': return ASM_NONSIGNED; // XXX double
case '~': return ASM_SIGNED;
case '!': return ASM_FLEXIBLE;
default: throw 'yikes ' + node[1];
}
break;
}
case 'num': {
var value = node[1];
if (value < 0) return ASM_SIGNED;
if (value > (-1>>>0) || value % 1 !== 0) return ASM_NONSIGNED;
if (value === (value | 0)) return ASM_FLEXIBLE;
return ASM_UNSIGNED;
}
case 'name': return ASM_FLEXIBLE;
case 'conditional': case 'seq': {
return detectSign(node[2]);
}
case 'call': {
if (node[1][0] === 'name') {
switch (node[1][1]) {
case 'Math_fround': return ASM_NONSIGNED
default: break;
}
}
}
}
assert(0 , 'badd ' + JSON.stringify(node));
}
function getCombinedSign(node1, node2, hint) {
var sign1 = detectSign(node1);
var sign2 = detectSign(node2);
if (sign1 === ASM_FLEXIBLE && sign2 === ASM_FLEXIBLE) {
return ASM_FLEXIBLE;
}
if (sign1 === ASM_FLEXIBLE) {
assert(sign2 != ASM_FLEXIBLE);
return sign2;
} else if (sign2 === ASM_FLEXIBLE) {
assert(sign1 != ASM_FLEXIBLE);
return sign1;
}
if (sign1 === sign2) return sign1;
if (sign1 === hint || sign2 === hint) return hint;
if ((sign1 === ASM_SIGNED && sign2 === ASM_UNSIGNED) ||
(sign1 === ASM_UNSIGNED && sign2 === ASM_SIGNED)) {
return ASM_FLEXIBLE;
}
assert(0, JSON.stringify([node1, ' ', node2, sign1, sign2, hint]));
}
function getSignature(func, asmData) {
var ret = asmData.ret >= 0 ? ASM_SIG[asmData.ret] : 'v';
for (var i = 0; i < func[2].length; i++) {
ret += ASM_SIG[asmData.params[func[2][i]]];
}
return ret;
}
function normalizeAsm(func) {
//printErr('pre-normalize \n\n' + astToSrc(func) + '\n\n');
var data = {
params: {}, // ident => ASM_* type
vars: {}, // ident => ASM_* type
inlines: [], // list of inline assembly copies
ret: undefined,
};
// process initial params
var stats = func[3];
var i = 0;
while (i < stats.length) {
var node = stats[i];
if (node[0] != 'stat' || node[1][0] != 'assign' || node[1][2][0] != 'name') break;
node = node[1];
var name = node[2][1];
if (func[2] && func[2].indexOf(name) < 0) break; // not an assign into a parameter, but a global
if (name in data.params) break; // already done that param, must be starting function body
data.params[name] = detectType(node[3]);
stats[i] = emptyNode();
i++;
}
// process initial variable definitions
outer:
while (i < stats.length) {
var node = stats[i];
if (node[0] != 'var') break;
for (var j = 0; j < node[1].length; j++) {
var v = node[1][j];
var name = v[0];
var value = v[1];
if (!(name in data.vars)) {
data.vars[name] = detectType(value, null, true);
v.length = 1; // make an un-assigning var
} else {
assert(j === 0, 'cannot break in the middle');
break outer;
}
}
i++;
}
// look for other var definitions and collect them
while (i < stats.length) {
traverse(stats[i], function(node, type) {
if (type === 'var') {
assert(0, 'should be no vars to fix! ' + func[1] + ' : ' + JSON.stringify(node));
} else if (type === 'call' && node[1][0] === 'function') {
assert(!node[1][1]); // anonymous functions only
data.inlines.push(node[1]);
node[1] = ['name', 'inlinejs']; // empty out body, leave arguments, so they are eliminated/minified properly
}
});
i++;
}
// look for final 'return' statement to get return type.
var retStmt = stats[stats.length - 1];
if (retStmt && retStmt[0] === 'return' && retStmt[1]) {
data.ret = detectType(retStmt[1]);
}
//printErr('normalized \n\n' + astToSrc(func) + '\n\nwith: ' + JSON.stringify(data));
return data;
}
function denormalizeAsm(func, data) {
//printErr('pre-denormalize \n\n' + astToSrc(func) + '\n\nwith: ' + JSON.stringify(data));
var stats = func[3];
// Remove var definitions, if any
for (var i = 0; i < stats.length; i++) {
if (stats[i][0] === 'var') {
stats[i] = emptyNode();
} else {
if (!isEmptyNode(stats[i])) break;
}
}
// calculate variable definitions
var varDefs = [];
for (var v in data.vars) {
varDefs.push(makeAsmVarDef(v, data.vars[v]));
}
// each param needs a line; reuse emptyNodes as much as we can
var numParams = 0;
for (var i in data.params) numParams++;
var emptyNodes = 0;
while (emptyNodes < stats.length) {
if (!isEmptyNode(stats[emptyNodes])) break;
emptyNodes++;
}
var neededEmptyNodes = numParams + (varDefs.length ? 1 : 0); // params plus one big var if there are vars
if (neededEmptyNodes > emptyNodes) {
var args = [0, 0];
for (var i = 0; i < neededEmptyNodes - emptyNodes; i++) args[i+2] = 0;
stats.splice.apply(stats, args);
} else if (neededEmptyNodes < emptyNodes) {
stats.splice(0, emptyNodes - neededEmptyNodes);
}
// add param coercions
var next = 0;
func[2].forEach(function(param) {
stats[next++] = ['stat', ['assign', true, ['name', param], makeAsmCoercion(['name', param], data.params[param])]];
});
if (varDefs.length) {
stats[next] = ['var', varDefs];
}
if (data.inlines.length > 0) {
var i = 0;
traverse(func, function(node, type) {
if (type === 'call' && node[1][0] === 'name' && node[1][1] === 'inlinejs') {
node[1] = data.inlines[i++]; // swap back in the body
}
});
}
// ensure that there's a final 'return' statement if needed.
if (data.ret !== undefined) {
var retStmt = stats[stats.length - 1];
if (!retStmt || retStmt[0] !== 'return') {
stats.push(['return', makeAsmCoercedZero(data.ret)]);
}
}
//printErr('denormalized \n\n' + astToSrc(func) + '\n\n');
}
function getFirstIndexInNormalized(func, data) {
// In a normalized asm function, return the index of the first element that is not not defs or annotation
var stats = func[3];
var i = stats.length-1;
while (i >= 0) {
var stat = stats[i];
if (stat[0] == 'var') break;
i--;
}
return i+1;
}
function getStackBumpNode(ast) {
var found = null;
traverse(ast, function(node, type) {
if (type === 'assign' && node[2][0] === 'name' && node[2][1] === 'STACKTOP') {
var value = node[3];
if (value[0] === 'name') return true;
if (value[0] == 'binary' && value[1] == '&') return; // this is an alignment fix, ignore
assert(value[0] == 'binary' && value[1] == '|' && value[2][0] == 'binary' && value[2][1] == '+' && value[2][2][0] == 'name' && value[2][2][1] == 'STACKTOP');
if (value[2][3][0] !== 'num') return; // non-constant bump, ignore
found = node;
return true;
}
});
return found;
}
function getStackBumpSize(ast) {
var node = getStackBumpNode(ast);
return node ? node[3][2][3][1] : 0;
}
// Name minification
var RESERVED = set('do', 'if', 'in', 'for', 'new', 'try', 'var', 'env', 'let', 'case', 'else', 'enum', 'void', 'this', 'void', 'with');
var VALID_MIN_INITS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
var VALID_MIN_LATERS = VALID_MIN_INITS + '0123456789';
var minifiedNames = [];
var minifiedState = [0];
function ensureMinifiedNames(n) { // make sure the nth index in minifiedNames exists. done 100% deterministically
while (minifiedNames.length < n+1) {
// generate the current name
var name = VALID_MIN_INITS[minifiedState[0]];
for (var i = 1; i < minifiedState.length; i++) {
name += VALID_MIN_LATERS[minifiedState[i]];
}
if (!(name in RESERVED)) minifiedNames.push(name);
// increment the state
var i = 0;
while (1) {
minifiedState[i]++;
if (minifiedState[i] < (i === 0 ? VALID_MIN_INITS : VALID_MIN_LATERS).length) break;
// overflow
minifiedState[i] = 0;
i++;
if (i === minifiedState.length) minifiedState.push(-1); // will become 0 after increment in next loop head
}
}
}
// Very simple 'registerization', coalescing of variables into a smaller number.
//
// We do not optimize when there are switches, so this pass only makes sense with
// relooping.
// TODO: Consider how this fits in with the rest of the optimization toolchain. Do
// we still need the eliminator? Closure? And in what order? Perhaps just
// closure simple?
function registerize(ast) {
traverseGeneratedFunctions(ast, function(fun) {
if (asm) var asmData = normalizeAsm(fun);
if (!asm) {
var hasFunction = false;
traverse(fun, function(node, type) {
if (type === 'function') hasFunction = true;
});
if (hasFunction) {
return; // inline assembly, and not asm (where we protect it in normalize/denormalize), so abort registerize pass
}
}
// Add parameters as a first (fake) var (with assignment), so they get taken into consideration
var params = {}; // note: params are special, they can never share a register between them (see later)
if (fun[2] && fun[2].length) {
var assign = ['num', 0];
fun[3].unshift(['var', fun[2].map(function(param) {
params[param] = 1;
return [param, assign];
})]);
}
if (asm) {
// copy params into vars
for (var p in asmData.params) asmData.vars[p] = asmData.params[p];
//printErr('fake params: \n\n' + astToSrc(fun) + '\n\n');
}
// Replace all var definitions with assignments; we will add var definitions at the top after we registerize
// We also mark local variables - i.e., having a var definition
var localVars = {};
var allVars = {};
var hasSwitch = false; // we cannot optimize variables if there is a switch, unless in asm mode
traverse(fun, function(node, type) {
if (type === 'var') {
node[1].forEach(function(defined) { localVars[defined[0]] = 1 });
var vars = node[1].filter(function(varr) { return varr[1] });
if (vars.length >= 1) {
return unVarify(vars);
} else {
return emptyNode();
}
} else if (type === 'switch') {
hasSwitch = true;
} else if (type === 'name') {
allVars[node[1]] = 1;
}
});
vacuum(fun);
var regTypes = {};
function getNewRegName(num, name) {
var ret;
if (!asm) {
ret = 'r' + num;
} else {
var type = asmData.vars[name];
switch (type) {
case ASM_INT: ret = 'i'; break;
case ASM_DOUBLE: ret = 'd'; break;
case ASM_FLOAT: ret = 'f'; break;
case ASM_FLOAT32X4: ret = 'F4'; break;
case ASM_FLOAT64X2: ret = 'F2'; break;
case ASM_INT8X16: ret = 'I16'; break;
case ASM_INT16X8: ret = 'I8'; break;
case ASM_INT32X4: ret = 'I4'; break;
case ASM_BOOL8X16: ret = 'B16'; break;
case ASM_BOOL16X8: ret = 'B8'; break;
case ASM_BOOL32X4: ret = 'B4'; break;
case ASM_BOOL64X2: ret = 'B2'; break;
case ASM_NONE: ret = 'Z'; break;
default: assert(false, 'type ' + type + ' doesn\'t have a name yet');
}
ret += num;
regTypes[ret] = type;
}
if (ret in allVars) {
assert(ret in localVars, 'register shadows non-local name');
}
return ret;
}
// Find the # of uses of each variable.
// While doing so, check if all a variable's uses are dominated in a simple
// way by a simple assign, if so, then we can assign its register to it
// just for its definition to its last use, and not to the entire toplevel loop,
// we call such variables "optimizable"
var varUses = {};
var level = 1;
var levelDominations = {};
var varLevels = {};
var possibles = {};
var unoptimizables = {};
function purgeLevel() {
// Invalidate all dominating on this level, further users make it unoptimizable
for (var name in levelDominations[level]) {
varLevels[name] = 0;
}
levelDominations[level] = null;
level--;
}
traverse(fun, function possibilifier(node, type) {
if (type === 'name') {
var name = node[1];
if (localVars[name]) {
if (!varUses[name]) varUses[name] = 0;
varUses[name]++;
if (possibles[name] && !varLevels[name]) unoptimizables[name] = 1; // used outside of simple domination
}
} else if (type === 'assign' && typeof node[1] != 'string') {
if (node[2] && node[2][0] === 'name') {
var name = node[2][1];
// if local and not yet used, this might be optimizable if we dominate
// all other uses
if (localVars[name] && !varUses[name] && !varLevels[name]) {
possibles[name] = 1;
varLevels[name] = level;
if (!levelDominations[level]) levelDominations[level] = {};
levelDominations[level][name] = 1;
}
}
} else if (type in CONTROL_FLOW) {
// recurse children, in the context of a loop
switch(type) {
case 'while': case 'do': {
traverse(node[1], possibilifier);
level++;
traverse(node[2], possibilifier);
purgeLevel();
break;
}
case 'for': {
traverse(node[1], possibilifier);
for (var i = 2; i <= 4; i++) {
level++;
traverse(node[i], possibilifier);
purgeLevel();
}
break;
}
case 'if': {
traverse(node[1], possibilifier);
level++;
traverse(node[2], possibilifier);
purgeLevel();
if (node[3]) {
level++;
traverse(node[3], possibilifier);
purgeLevel();
}
break;
}
case 'switch': {
traverse(node[1], possibilifier);
var cases = node[2];
for (var i = 0; i < cases.length; i++) {
level++;
traverse(cases[i][1], possibilifier);
purgeLevel();
}
break;
}
default: throw dumpAst(node);
}
return null; // prevent recursion into children, which we already did
}
});
var optimizables = {};
if (!hasSwitch || asm) {
for (var possible in possibles) {
if (!unoptimizables[possible]) optimizables[possible] = 1;
}
}
//printErr('optimizables: ' + JSON.stringify(optimizables));
//printErr('unoptimizables: ' + JSON.stringify(unoptimizables));
// Go through the function's code, assigning 'registers'.
// The only tricky bit is to keep variables locked on a register through loops,
// since they can potentially be returned to. Optimizable variables lock onto
// loops that they enter, unoptimizable variables lock in a conservative way
// into the topmost loop.
// Note that we cannot lock onto a variable in a loop if it was used and free'd
// before! (then they could overwrite us in the early part of the loop). For now
// we just use a fresh register to make sure we avoid this, but it could be
// optimized to check for safe registers (free, and not used in this loop level).
var varRegs = {}; // maps variables to the register they will use all their life
var freeRegsClasses = asm ? [[], [], [], [], [], [], [], [], [], [], [], [], []] : []; // two classes for asm, one otherwise XXX - hardcoded length
var nextReg = 1;
var fullNames = {};
var loopRegs = {}; // for each loop nesting level, the list of bound variables
var loops = 0; // 0 is toplevel, 1 is first loop, etc
var saved = 0;
var activeOptimizables = {};
var optimizableLoops = {};
var paramRegs = {}; // true if the register is used by a parameter (and so needs no def at start of function; also cannot
// be shared with another param, each needs its own)
function decUse(name) {
if (!varUses[name]) return false; // no uses left, or not a relevant variable
if (optimizables[name]) activeOptimizables[name] = 1;
var reg = varRegs[name];
if (asm) assert(name in asmData.vars, name);
var freeRegs = asm ? freeRegsClasses[asmData.vars[name]] : freeRegsClasses;
if (!reg) {
// acquire register
if (optimizables[name] && freeRegs.length > 0 &&
!(params[name] && paramRegs[freeRegs[freeRegs.length-1]])) { // do not share registers between parameters
reg = freeRegs.pop();
saved++;
} else {
reg = nextReg++;
fullNames[reg] = getNewRegName(reg, name);
if (params[name]) paramRegs[reg] = 1;
}
varRegs[name] = reg;
}
varUses[name]--;
assert(varUses[name] >= 0);
if (varUses[name] === 0) {
if (optimizables[name]) delete activeOptimizables[name];
// If we are not in a loop, or we are optimizable and not bound to a loop
// (we might have been in one but left it), we can free the register now.
if (loops === 0 || (optimizables[name] && !optimizableLoops[name])) {
// free register
freeRegs.push(reg);
} else {
// when the relevant loop is exited, we will free the register
var releventLoop = optimizables[name] ? (optimizableLoops[name] || 1) : 1;
if (!loopRegs[releventLoop]) loopRegs[releventLoop] = [];
loopRegs[releventLoop].push(reg);
}
}
return true;
}
traverse(fun, function(node, type) { // XXX we rely on traversal order being the same as execution order here
if (type === 'name') {
var name = node[1];
if (decUse(name)) {
node[1] = fullNames[varRegs[name]];
}
} else if (type in LOOP) {
loops++;
// Active optimizables lock onto this loop, if not locked onto one that encloses this one
for (var name in activeOptimizables) {
if (!optimizableLoops[name]) {
optimizableLoops[name] = loops;
}
}
}
}, function(node, type) {
if (type in LOOP) {
// Free registers that were locked to this loop
if (loopRegs[loops]) {
if (asm) {
loopRegs[loops].forEach(function(loopReg) {
freeRegsClasses[regTypes[fullNames[loopReg]]].push(loopReg);
});
} else {
freeRegsClasses = freeRegsClasses.concat(loopRegs[loops]);
}
loopRegs[loops].length = 0;
}
loops--;
}
});
if (fun[2] && fun[2].length) {
fun[2].length = 0; // clear params, we will fill with registers
fun[3].shift(); // remove fake initial var
}
//printErr('var regs: ' + JSON.stringify(varRegs) + '\n\nparam regs: ' + JSON.stringify(paramRegs));
if (!asm) {
if (nextReg > 1) {
var vars = [];
for (var i = 1; i < nextReg; i++) {
var reg = fullNames[i];
if (!paramRegs[i]) {
vars.push([reg]);
} else {
fun[2].push(reg);
}
}
if (vars.length > 0) getStatements(fun).unshift(['var', vars]);
}
} else {
//printErr('unfake params: \n\n' + astToSrc(fun) + '\n\n');
var finalAsmData = {
params: {},
vars: {},
inlines: asmData.inlines,
ret: asmData.ret,
};
for (var i = 1; i < nextReg; i++) {
var reg = fullNames[i];
var type = regTypes[reg];
if (!paramRegs[i]) {
finalAsmData.vars[reg] = type;
} else {
finalAsmData.params[reg] = type;
fun[2].push(reg);
}
}
denormalizeAsm(fun, finalAsmData);
}
});
}
// Assign variables to 'registers', coalescing them onto a smaller number of shared
// variables.
//
// This does the same job as 'registerize' above, but burns a lot more cycles trying
// to reduce the total number of register variables. Key points about the operation:
//
// * we decompose the AST into a flow graph and perform a full liveness
// analysis, to determine which variables are live at each point.
//
// * variables that are live concurrently are assigned to different registers.
//
// * variables that are linked via 'x=y' style statements are assigned the same
// register if possible, so that the redundant assignment can be removed.
// (e.g. assignments used to pass state around through loops).
//
// * any code that cannot be reached through the flow-graph is removed.
// (e.g. redundant break statements like 'break L123; break;').
//
// * any assignments that we can prove are not subsequently used are removed.
// (e.g. unnecessary assignments to the 'label' variable).
//
function registerizeHarder(ast) {
assert(asm);
traverseGeneratedFunctions(ast, function(fun) {
// Do not try to process non-validating methods, like the heap replacer
var abort = false;
traverse(fun, function(node, type) {
if (type === 'new') abort = true;
});
if (abort) return;
// Do not process the dceable helper function for wasm, which declares
// types, we need to alive for asm2wasm
if (fun[1] == '__emscripten_dceable_type_decls') return;
var asmData = normalizeAsm(fun);
var localVars = asmData.vars;
for (var name in asmData.params) {
localVars[name] = asmData.params[name];
}
// Utilities for allocating register variables.
// We need distinct register pools for each type of variable.
var allRegsByType = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]; // XXX - hardcoded length
var regPrefixByType = ['i', 'd', 'f', 'F4', 'F2', 'I16', 'I8', 'I4', 'B16', 'B8', 'B4', 'B2', 'n'];
var nextReg = 1;
function createReg(forName) {
// Create a new register of type suitable for the given variable name.
var allRegs = allRegsByType[localVars[forName]];
var reg = nextReg++;
allRegs[reg] = regPrefixByType[localVars[forName]] + reg;
return reg;
}
// Traverse the tree in execution order and synthesize a basic flow-graph.
// It's convenient to build a kind of "dual" graph where the nodes identify
// the junctions between blocks at which control-flow may branch, and each
// basic block is an edge connecting two such junctions.
// For each junction we store:
// * set of blocks that originate at the junction
// * set of blocks that terminate at the junction
// For each block we store:
// * a single entry junction
// * a single exit junction
// * a 'use' and 'kill' set of names for the block
// * full sequence of 'name' and 'assign' nodes in the block
// * whether each such node appears as part of a larger expression
// (and therefore cannot be safely eliminated)
// * set of labels that can be used to jump to this block
var junctions = [];
var blocks = [];
var currEntryJunction = null;
var nextBasicBlock = null;
var isInExpr = 0;
var activeLabels = [{}];
var nextLoopLabel = null;
var ENTRY_JUNCTION = 0;
var EXIT_JUNCTION = 1;
var ENTRY_BLOCK = 0;
function addJunction() {
// Create a new junction, without inserting it into the graph.
// This is useful for e.g. pre-allocating an exit node.
var id = junctions.length;
junctions[id] = {id: id, inblocks: {}, outblocks: {}};
return id;
}
function markJunction(id) {
// Mark current traversal location as a junction.
// This makes a new basic block exiting at this position.
if (id === undefined || id === null) {
id = addJunction();
}
joinJunction(id, true);
return id;
}
function setJunction(id, force) {
// Set the next entry junction to the given id.
// This can be used to enter at a previously-declared point.
// You can't return to a junction with no incoming blocks
// unless the 'force' parameter is specified.
assert(nextBasicBlock.nodes.length === 0, 'refusing to abandon an in-progress basic block')
if (force || setSize(junctions[id].inblocks) > 0) {
currEntryJunction = id;
} else {
currEntryJunction = null;
}
}
function joinJunction(id, force) {
// Complete the pending basic block by exiting at this position.
// This can be used to exit at a previously-declared point.
if (currEntryJunction !== null) {
nextBasicBlock.id = blocks.length;
nextBasicBlock.entry = currEntryJunction;
nextBasicBlock.exit = id;
junctions[currEntryJunction].outblocks[nextBasicBlock.id] = 1;
junctions[id].inblocks[nextBasicBlock.id] = 1;
blocks.push(nextBasicBlock);
}
nextBasicBlock = { id: null, entry: null, exit: null, labels: {}, nodes: [], isexpr: [], use: {}, kill: {} };
setJunction(id, force);
return id;
}
function pushActiveLabels(onContinue, onBreak) {
// Push the target junctions for continuing/breaking a loop.
// This should be called before traversing into a loop.
var prevLabels = activeLabels[activeLabels.length-1];
var newLabels = copy(prevLabels);
newLabels[null] = [onContinue, onBreak];
if (nextLoopLabel) {
newLabels[nextLoopLabel] = [onContinue, onBreak];
nextLoopLabel = null;
}
// An unlabelled 'continue' should jump to innermost loop,
// ignoring any nested 'switch' statements.
if (onContinue === null && prevLabels[null]) {
newLabels[null][0] = prevLabels[null][0];
}
activeLabels.push(newLabels);
}
function popActiveLabels() {
// Pop the target junctions for continuing/breaking a loop.
// This should be called after traversing into a loop.
activeLabels.pop();
}
function markNonLocalJump(type, label) {
// Complete a block via 'return', 'break' or 'continue'.
// This joins the targetted junction and then sets the current junction to null.
// Any code traversed before we get back to an existing junction is dead code.
if (type === 'return') {
joinJunction(EXIT_JUNCTION);
} else {
label = label ? label : null;
var targets = activeLabels[activeLabels.length-1][label];
assert(targets, 'jump to unknown label');
if (type === 'continue') {
joinJunction(targets[0]);
} else if (type === 'break') {
joinJunction(targets[1]);
} else {
assert(false, 'unknown jump node type');
}
}
currEntryJunction = null;
}
function addUseNode(node) {
// Mark a use of the given name node in the current basic block.
assert(node[0] === 'name', 'not a use node');
var name = node[1];
if (name in localVars) {
nextBasicBlock.nodes.push(node);
nextBasicBlock.isexpr.push(isInExpr);
if (!nextBasicBlock.kill[name]) {
nextBasicBlock.use[name] = 1;
}
}
}
function addKillNode(node) {
// Mark an assignment to the given name node in the current basic block.
assert(node[0] === 'assign', 'not a kill node');
assert(node[1] === true, 'not a kill node');
assert(node[2][0] === 'name', 'not a kill node');
var name = node[2][1];
if (name in localVars) {
nextBasicBlock.nodes.push(node);
nextBasicBlock.isexpr.push(isInExpr);
nextBasicBlock.kill[name] = 1;
}
}
function lookThroughCasts(node) {
// Look through value-preserving casts, like "x | 0" => "x"
if (node[0] === 'binary' && node[1] === '|') {
if (node[3][0] === 'num' && node[3][1] === 0) {
return lookThroughCasts(node[2]);
}
}
return node;
}
function addBlockLabel(node) {
assert(nextBasicBlock.nodes.length === 0, 'cant add label to an in-progress basic block')
if (node[0] === 'num') {
nextBasicBlock.labels[node[1]] = 1;
}
}
function isTrueNode(node) {
// Check if the given node is statically truthy.
return (node[0] === 'num' && node[1] != 0);
}
function isFalseNode(node) {
// Check if the given node is statically falsy.
return (node[0] === 'num' && node[1] == 0);
}
function morphNode(node, newNode) {
// In-place morph a node into some other type of node.
var i = 0;
while (i < node.length && i < newNode.length) {
node[i] = newNode[i];
i++;
}
while (i < newNode.length) {
node.push(newNode[i]);
i++;
}
if (node.length > newNode.length) {
node.length = newNode.length;
}
}
function buildFlowGraph(node) {
// Recursive function to build up the flow-graph.
// It walks the tree in execution order, calling the above state-management
// functions at appropriate points in the traversal.
var type = node[0];
// Any code traversed without an active entry junction must be dead,
// as the resulting block could never be entered. Let's remove it.
if (currEntryJunction === null && junctions.length > 0) {
morphNode(node, ['block', []]);
return;
}
// Traverse each node type according to its particular control-flow semantics.
switch (type) {
case 'defun':
var jEntry = markJunction();
assert(jEntry === ENTRY_JUNCTION);
var jExit = addJunction();
assert(jExit === EXIT_JUNCTION);
for (var i = 0; i < node[3].length; i++) {
buildFlowGraph(node[3][i]);
}
joinJunction(jExit);
break;
case 'if':
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
var jEnter = markJunction();
var jExit = addJunction();
if (node[2]) {
// Detect and mark "if (label == N) { <labelled block> }".
if (node[1][0] === 'binary' && node[1][1] === '==') {
var lhs = lookThroughCasts(node[1][2]);
if (lhs[0] === 'name' && lhs[1] === 'label') {
addBlockLabel(lookThroughCasts(node[1][3]));
}
}
buildFlowGraph(node[2]);
}
joinJunction(jExit);
setJunction(jEnter);
if (node[3]) {
buildFlowGraph(node[3]);
}
joinJunction(jExit);
break;
case 'conditional':
isInExpr++;
// If the conditional has no side-effects, we can treat it as a single
// block, which might open up opportunities to remove it entirely.
if (!hasSideEffects(node)) {
buildFlowGraph(node[1]);
if (node[2]) {
buildFlowGraph(node[2]);
}
if (node[3]) {
buildFlowGraph(node[3]);
}
} else {
buildFlowGraph(node[1]);
var jEnter = markJunction();
var jExit = addJunction();
if (node[2]) {
buildFlowGraph(node[2]);
}
joinJunction(jExit);
setJunction(jEnter);
if (node[3]) {
buildFlowGraph(node[3]);
}
joinJunction(jExit);
}
isInExpr--;
break;
case 'while':
// Special-case "while (1) {}" to use fewer junctions,
// since emscripten generates a lot of these.
if (isTrueNode(node[1])) {
var jLoop = markJunction();
var jExit = addJunction();
pushActiveLabels(jLoop, jExit);
buildFlowGraph(node[2]);
popActiveLabels();
joinJunction(jLoop);
setJunction(jExit);
} else {
var jCond = markJunction();
var jLoop = addJunction();
var jExit = addJunction();
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
joinJunction(jLoop);
pushActiveLabels(jCond, jExit);
buildFlowGraph(node[2]);
popActiveLabels();
joinJunction(jCond);
// An empty basic-block linking condition exit to loop exit.
setJunction(jLoop);
joinJunction(jExit);
}
break;
case 'do':
// Special-case "do {} while (1)" and "do {} while (0)" to use
// fewer junctions, since emscripten generates a lot of these.
if (isFalseNode(node[1])) {
var jExit = addJunction();
pushActiveLabels(jExit, jExit);
buildFlowGraph(node[2]);
popActiveLabels();
joinJunction(jExit);
} else if (isTrueNode(node[1])) {
var jLoop = markJunction();
var jExit = addJunction();
pushActiveLabels(jLoop, jExit);
buildFlowGraph(node[2]);
popActiveLabels();
joinJunction(jLoop);
setJunction(jExit);
} else {
var jLoop = markJunction();
var jCond = addJunction();
var jCondExit = addJunction();
var jExit = addJunction();
pushActiveLabels(jCond, jExit);
buildFlowGraph(node[2]);
popActiveLabels();
joinJunction(jCond);
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
joinJunction(jCondExit);
joinJunction(jLoop);
setJunction(jCondExit);
joinJunction(jExit);
}
break;
case 'for':
var jTest = addJunction();
var jBody = addJunction();
var jStep = addJunction();
var jExit = addJunction();
buildFlowGraph(node[1]);
joinJunction(jTest);
isInExpr++;
buildFlowGraph(node[2]);
isInExpr--;
joinJunction(jBody);
pushActiveLabels(jStep, jExit);
buildFlowGraph(node[4]);
popActiveLabels();
joinJunction(jStep);
buildFlowGraph(node[3]);
joinJunction(jTest);
setJunction(jBody);
joinJunction(jExit);
break;
case 'label':
assert(node[2][0] in BREAK_CAPTURERS, 'label on non-loop, non-switch statement')
nextLoopLabel = node[1];
buildFlowGraph(node[2]);
break;
case 'switch':
// Emscripten generates switch statements of a very limited
// form: all case clauses are numeric literals, and all
// case bodies end with a (maybe implicit) break. So it's
// basically equivalent to a multi-way 'if' statement.
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
var condition = lookThroughCasts(node[1]);
var jCheckExit = markJunction();
var jExit = addJunction();
pushActiveLabels(null, jExit);
var hasDefault = false;
for (var i=0; i<node[2].length; i++) {
setJunction(jCheckExit);
// All case clauses are either 'default' or a numeric literal.
if (!node[2][i][0]) {
hasDefault = true;
} else {
// Detect switches dispatching to labelled blocks.
if (condition[0] === 'name' && condition[1] === 'label') {
addBlockLabel(lookThroughCasts(node[2][i][0]));
}
}
for (var j = 0; j < node[2][i][1].length; j++) {
buildFlowGraph(node[2][i][1][j]);
}
// Control flow will never actually reach the end of the case body.
// If there's live code here, assume it jumps to case exit.
if (currEntryJunction !== null && nextBasicBlock.nodes.length > 0) {
if (node[2][i][0]) {
markNonLocalJump('return');
} else {
joinJunction(jExit);
}
}
}
// If there was no default case, we also need an empty block
// linking straight from the test evaluation to the exit.
if (!hasDefault) {
setJunction(jCheckExit);
}
joinJunction(jExit);
popActiveLabels();
break;
case 'return':
if (node[1]) {
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
}
markNonLocalJump(type);
break;
case 'break':
case 'continue':
markNonLocalJump(type, node[1]);
break;
case 'assign':
isInExpr++;
buildFlowGraph(node[3]);
isInExpr--;
if (node[1] === true && node[2][0] === 'name') {
addKillNode(node);
} else {
buildFlowGraph(node[2]);
}
break;
case 'name':
addUseNode(node);
break;
case 'block':
case 'toplevel':
if (node[1]) {
for (var i = 0; i < node[1].length; i++) {
buildFlowGraph(node[1][i]);
}
}
break;
case 'stat':
buildFlowGraph(node[1]);
break;
case 'unary-prefix':
case 'unary-postfix':
isInExpr++;
buildFlowGraph(node[2]);
isInExpr--;
break;
case 'binary':
isInExpr++;
buildFlowGraph(node[2]);
buildFlowGraph(node[3]);
isInExpr--;
break;
case 'call':
isInExpr++;
buildFlowGraph(node[1]);
if (node[2]) {
for (var i = 0; i < node[2].length; i++) {
buildFlowGraph(node[2][i]);
}
}
isInExpr--;
// If the call is statically known to throw,
// treat it as a jump to function exit.
if (!isInExpr && node[1][0] === 'name') {
if (node[1][1] in FUNCTIONS_THAT_ALWAYS_THROW) {
markNonLocalJump('return');
}
}
break;
case 'seq':
case 'sub':
isInExpr++;
buildFlowGraph(node[1]);
buildFlowGraph(node[2]);
isInExpr--;
break;
case 'dot':
case 'throw':
isInExpr++;
buildFlowGraph(node[1]);
isInExpr--;
break;
case 'num':
case 'string':
case 'var':
break;
default:
printErr(JSON.stringify(node));
assert(false, 'unsupported node type: ' + type);
}
}
buildFlowGraph(fun);
assert(setSize(junctions[ENTRY_JUNCTION].inblocks) === 0, 'function entry must have no incoming blocks');
assert(setSize(junctions[EXIT_JUNCTION].outblocks) === 0, 'function exit must have no outgoing blocks');
assert(blocks[ENTRY_BLOCK].entry === ENTRY_JUNCTION, 'block zero must be the initial block');
// Fix up implicit jumps done by assigning to the 'label' variable.
// If a block ends with an assignment to 'label' and there's another block
// with that value of 'label' as precondition, we tweak the flow graph so
// that the former jumps straight to the later.
var labelledBlocks = {};
var labelledJumps = [];
FINDLABELLEDBLOCKS:
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
// Does it have any labels as preconditions to its entry?
for (var labelVal in block.labels) {
// If there are multiple blocks with the same label, all bets are off.
// This seems to happen sometimes for short blocks that end with a return.
// TODO: it should be safe to merge the duplicates if they're identical.
if (labelVal in labelledBlocks) {
labelledBlocks = {};
labelledJumps = [];
break FINDLABELLEDBLOCKS;
}
labelledBlocks[labelVal] = block;
}
// Does it assign a specific label value at exit?
if ('label' in block.kill) {
var finalNode = block.nodes[block.nodes.length - 1];
if (finalNode[0] === 'assign' && finalNode[2][1] === 'label') {
// If labels are computed dynamically then all bets are off.
// This can happen due to indirect branching in llvm output.
if (finalNode[3][0] !== 'num') {
labelledBlocks = {};
labelledJumps = [];
break FINDLABELLEDBLOCKS;
}
labelledJumps.push([finalNode[3][1], block]);
} else {
// If label is assigned a non-zero value elsewhere in the block
// then all bets are off. This can happen e.g. due to outlining
// saving/restoring label to the stack.
for (var j = 0; j < block.nodes.length - 1; j++) {
if (block.nodes[j][0] === 'assign' && block.nodes[j][2][1] === 'label') {
if (block.nodes[j][3][0] !== 'num' || block.nodes[j][3][1] !== 0) {
labelledBlocks = {};
labelledJumps = [];
break FINDLABELLEDBLOCKS;
}
}
}
}
}
}
for (var labelVal in labelledBlocks) {
var block = labelledBlocks[labelVal];
// Disconnect it from the graph, and create a
// new junction for jumps targetting this label.
delete junctions[block.entry].outblocks[block.id];
block.entry = addJunction();
junctions[block.entry].outblocks[block.id] = 1;
// Add a fake use of 'label' to keep it alive in predecessor.
block.use['label'] = 1;
block.nodes.unshift(['name', 'label']);
block.isexpr.unshift(1);
}
for (var i = 0; i < labelledJumps.length; i++) {
var labelVal = labelledJumps[i][0];
var block = labelledJumps[i][1];
var targetBlock = labelledBlocks[labelVal];
if (targetBlock) {
// Redirect its exit to entry of the target block.
delete junctions[block.exit].inblocks[block.id];
block.exit = targetBlock.entry;
junctions[block.exit].inblocks[block.id] = 1;
}
}
labelledBlocks = null;
labelledJumps = null;
// Do a backwards data-flow analysis to determine the set of live
// variables at each junction, and to use this information to eliminate
// any unused assignments.
// We run two nested phases. The inner phase builds the live set for each
// junction. The outer phase uses this to try to eliminate redundant
// stores in each basic block, which might in turn affect liveness info.
function analyzeJunction(junc) {
// Update the live set for this junction.
var live = {};
for (var b in junc.outblocks) {
var block = blocks[b];
var liveSucc = junctions[block.exit].live || {};
for (var name in liveSucc) {
if (!(name in block.kill)) {
live[name] = 1;
}
}
for (var name in block.use) {
live[name] = 1;
}
}
junc.live = live;
}
function analyzeBlock(block) {
// Update information about the behaviour of the block.
// This includes the standard 'use' and 'kill' information,
// plus a 'link' set naming values that flow through from entry
// to exit, possibly changing names via simple 'x=y' assignments.
// As we go, we eliminate assignments if the variable is not
// subsequently used.
var live = copy(junctions[block.exit].live);
var use = {};
var kill = {};
var link = {};
var lastUseLoc = {};
var firstDeadLoc = {};
var firstKillLoc = {};
var lastKillLoc = {};
for (var name in live) {
link[name] = name;
lastUseLoc[name] = block.nodes.length;
firstDeadLoc[name] = block.nodes.length;
}
for (var j = block.nodes.length - 1; j >=0 ; j--) {
var node = block.nodes[j];
if (node[0] === 'name') {
var name = node[1];
live[name] = 1;
use[name] = j;
if (lastUseLoc[name] === undefined) {
lastUseLoc[name] = j;
firstDeadLoc[name] = j;
}
} else {
var name = node[2][1];
// We only keep assignments if they will be subsequently used.
if (name in live) {
kill[name] = 1;
delete use[name];
delete live[name];
firstDeadLoc[name] = j;
firstKillLoc[name] = j;
if (lastUseLoc[name] === undefined) {
lastUseLoc[name] = j;
}
if (lastKillLoc[name] === undefined) {
lastKillLoc[name] = j;
}
// If it's an "x=y" and "y" is not live, then we can create a
// flow-through link from "y" to "x". If not then there's no
// flow-through link for "x".
var oldLink = link[name];
if (oldLink) {
delete link[name];
if (node[3][0] === 'name') {
if (node[3][1] in localVars) {
link[node[3][1]] = oldLink;
}
}
}
} else {
// The result of this assignment is never used, so delete it.
// We may need to keep the RHS for its value or its side-effects.
function removeUnusedNodes(j, n) {
for (var name in lastUseLoc) {
lastUseLoc[name] -= n;
}
for (var name in firstKillLoc) {
firstKillLoc[name] -= n;
}
for (var name in lastKillLoc) {
lastKillLoc[name] -= n;
}
for (var name in firstDeadLoc) {
firstDeadLoc[name] -= n;
}
block.nodes.splice(j, n);
block.isexpr.splice(j, n);
}
if (block.isexpr[j] || hasSideEffects(node[3])) {
morphNode(node, node[3]);
removeUnusedNodes(j, 1);
} else {
var numUsesInExpr = 0;
traverse(node[3], function(node, type) {
if (type === 'name' && node[1] in localVars) {
numUsesInExpr++;
}
});
morphNode(node, ['block', []]);
j = j - numUsesInExpr;
removeUnusedNodes(j, 1 + numUsesInExpr);
}
}
}
}
block.use = use;
block.kill = kill;
block.link = link;
block.lastUseLoc = lastUseLoc;
block.firstDeadLoc = firstDeadLoc;
block.firstKillLoc = firstKillLoc;
block.lastKillLoc = lastKillLoc;
}
var jWorklistMap = { EXIT_JUNCTION: 1 };
var jWorklist = [EXIT_JUNCTION];
var bWorklistMap = {};
var bWorklist = [];
// Be sure to visit every junction at least once.
// This avoids missing some vars because we disconnected them
// when processing the labelled jumps.
for (var i = junctions.length - 1; i >= EXIT_JUNCTION; i--) {
jWorklistMap[i] = 1;
jWorklist.push(i);
}
while (jWorklist.length > 0) {
// Iterate on just the junctions until we get stable live sets.
// The first run of this loop will grow the live sets to their maximal size.
// Subsequent runs will shrink them based on eliminated in-block uses.
while (jWorklist.length > 0) {
var junc = junctions[jWorklist.pop()];
delete jWorklistMap[junc.id];
var oldLive = junc.live || null;
analyzeJunction(junc);
if (!sortedJsonCompare(oldLive, junc.live)) {
// Live set changed, updated predecessor blocks and junctions.
for (var b in junc.inblocks) {
if (!(b in bWorklistMap)) {
bWorklistMap[b] = 1;
bWorklist.push(b);
}
var jPred = blocks[b].entry;
if (!(jPred in jWorklistMap)) {
jWorklistMap[jPred] = 1;
jWorklist.push(jPred);
}
}
}
}
// Now update the blocks based on the calculated live sets.
while (bWorklist.length > 0) {
var block = blocks[bWorklist.pop()];
delete bWorklistMap[block.id];
var oldUse = block.use;
analyzeBlock(block);
if (!sortedJsonCompare(oldUse, block.use)) {
// The use set changed, re-process the entry junction.
if (!(block.entry in jWorklistMap)) {
jWorklistMap[block.entry] = 1;
jWorklist.push(block.entry);
}
}
}
}
// Insist that all function parameters are alive at function entry.
// This ensures they will be assigned independent registers, even
// if they happen to be unused.
for (var name in asmData.params) {
junctions[ENTRY_JUNCTION].live[name] = 1;
}
// For variables that are live at one or more junctions, we assign them
// a consistent register for the entire scope of the function. Find pairs
// of variable that cannot use the same register (the "conflicts") as well
// as pairs of variables that we'd like to have share the same register
// (the "links").
var junctionVariables = {};
function initializeJunctionVariable(name) {
junctionVariables[name] = { conf: {}, link: {}, excl: {}, reg: null };
}
for (var i = 0; i < junctions.length; i++) {
var junc = junctions[i];
for (var name in junc.live) {
if (!junctionVariables[name]) initializeJunctionVariable(name);
// It conflicts with all other names live at this junction.
for (var otherName in junc.live) {
if (otherName == name) continue;
junctionVariables[name].conf[otherName] = 1;
}
for (var b in junc.outblocks) {
// It conflicts with any output vars of successor blocks,
// if they're assigned before it goes dead in that block.
var block = blocks[b];
var jSucc = junctions[block.exit];
for (var otherName in jSucc.live) {
if (junc.live[otherName]) continue;
if (block.lastKillLoc[otherName] < block.firstDeadLoc[name]) {
if (!junctionVariables[otherName]) initializeJunctionVariable(otherName);
junctionVariables[name].conf[otherName] = 1;
junctionVariables[otherName].conf[name] = 1;
}
}
// It links with any linkages in the outgoing blocks.
var linkName = block.link[name];
if (linkName && linkName !== name) {
if (!junctionVariables[linkName]) initializeJunctionVariable(linkName);
junctionVariables[name].link[linkName] = 1;
junctionVariables[linkName].link[name] = 1;
}
}
}
}
// Attempt to sort the junction variables to heuristically reduce conflicts.
// Simple starting point: handle the most-conflicted variables first.
// This seems to work pretty well.
var sortedJunctionVariables = keys(junctionVariables);
sortedJunctionVariables.sort(function(name1, name2) {
var jv1 = junctionVariables[name1];
var jv2 = junctionVariables[name2];
if (jv1.numConfs === undefined) {
jv1.numConfs = setSize(jv1.conf);
}
if (jv2.numConfs === undefined) {
jv2.numConfs = setSize(jv2.conf);
}
return jv2.numConfs - jv1.numConfs;
});
// We can now assign a register to each junction variable.
// Process them in order, trying available registers until we find
// one that works, and propagating the choice to linked/conflicted
// variables as we go.
function tryAssignRegister(name, reg) {
// Try to assign the given register to the given variable,
// and propagate that choice throughout the graph.
// Returns true if successful, false if there was a conflict.
var jv = junctionVariables[name];
if (jv.reg !== null) {
return jv.reg === reg;
}
if (jv.excl[reg]) {
return false;
}
jv.reg = reg;
// Exclude use of this register at all conflicting variables.
for (var confName in jv.conf) {
junctionVariables[confName].excl[reg] = 1;
}
// Try to propagate it into linked variables.
// It's not an error if we can't.
for (var linkName in jv.link) {
tryAssignRegister(linkName, reg);
}
return true;
}
NEXTVARIABLE:
for (var i = 0; i < sortedJunctionVariables.length; i++) {
var name = sortedJunctionVariables[i];
// It may already be assigned due to linked-variable propagation.
if (junctionVariables[name].reg !== null) {
continue NEXTVARIABLE;
}
// Try to use existing registers first.
var allRegs = allRegsByType[localVars[name]];
for (var reg in allRegs) {
if (tryAssignRegister(name, reg)) {
continue NEXTVARIABLE;
}
}
// They're all taken, create a new one.
tryAssignRegister(name, createReg(name));
}
// Each basic block can now be processed in turn.
// There may be internal-use-only variables that still need a register
// assigned, but they can be treated just for this block. We know
// that all inter-block variables are in a good state thanks to
// junction variable consistency.
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block.nodes.length === 0) continue;
var jEnter = junctions[block.entry];
var jExit = junctions[block.exit];
// Mark the point at which each input reg becomes dead.
// Variables alive before this point must not be assigned
// to that register.
var inputVars = {};
var inputDeadLoc = {};
var inputVarsByReg = {};
for (var name in jExit.live) {
if (!(name in block.kill)) {
inputVars[name] = 1;
var reg = junctionVariables[name].reg;
assert(reg !== null, 'input variable doesnt have a register');
inputDeadLoc[reg] = block.firstDeadLoc[name];
inputVarsByReg[reg] = name;
}
}
for (var name in block.use) {
if (!(name in inputVars)) {
inputVars[name] = 1;
var reg = junctionVariables[name].reg;
assert(reg !== null, 'input variable doesnt have a register');
inputDeadLoc[reg] = block.firstDeadLoc[name];
inputVarsByReg[reg] = name;
}
}
assert(setSize(setSub(inputVars, jEnter.live)) == 0);
// Scan through backwards, allocating registers on demand.
// Be careful to avoid conflicts with the input registers.
// We consume free registers in last-used order, which helps to
// eliminate "x=y" assignments that are the last use of "y".
var assignedRegs = {};
var freeRegsByType = copy(allRegsByType);
// Begin with all live vars assigned per the exit junction.
for (var name in jExit.live) {
var reg = junctionVariables[name].reg;
assert(reg !== null, 'output variable doesnt have a register');
assignedRegs[name] = reg;
delete freeRegsByType[localVars[name]][reg];
}
for (var j = 0; j < freeRegsByType.length; j++) {
freeRegsByType[j] = keys(freeRegsByType[j]);
}
// Scan through the nodes in sequence, modifying each node in-place
// and grabbing/freeing registers as needed.
var maybeRemoveNodes = [];
for (var j = block.nodes.length - 1; j >= 0; j--) {
var node = block.nodes[j];
var name = node[0] === 'assign' ? node[2][1] : node[1];
var allRegs = allRegsByType[localVars[name]];
var freeRegs = freeRegsByType[localVars[name]];
var reg = assignedRegs[name];
if (node[0] === 'name') {
// A use. Grab a register if it doesn't have one.
if (!reg) {
if (name in inputVars && j <= block.firstDeadLoc[name]) {
// Assignment to an input variable, must use pre-assigned reg.
reg = junctionVariables[name].reg;
assignedRegs[name] = reg;
for (var k = freeRegs.length - 1; k >= 0; k--) {
if (freeRegs[k] === reg) {
freeRegs.splice(k, 1);
break;
}
}
} else {
// Try to use one of the existing free registers.
// It must not conflict with an input register.
for (var k = freeRegs.length - 1; k >= 0; k--) {
reg = freeRegs[k];
// Check for conflict with input registers.
if (block.firstKillLoc[name] <= inputDeadLoc[reg]) {
if (name !== inputVarsByReg[reg]) {
continue;
}
}
// Found one!
assignedRegs[name] = reg;
freeRegs.splice(k, 1);
break;
}
// If we didn't find a suitable register, create a new one.
if (!assignedRegs[name]) {
reg = createReg(name);
assignedRegs[name] = reg;
}
}
}
node[1] = allRegs[reg];
} else {
// A kill. This frees the assigned register.
assert(reg, 'live variable doesnt have a reg?')
node[2][1] = allRegs[reg];
freeRegs.push(reg);
delete assignedRegs[name];
if (node[3][0] === 'name' && node[3][1] in localVars) {
maybeRemoveNodes.push([j, node]);
}
}
}
// If we managed to create any "x=x" assignments, remove them.
for (var j = 0; j < maybeRemoveNodes.length; j++) {
var node = maybeRemoveNodes[j][1];
if (node[2][1] === node[3][1]) {
if (block.isexpr[maybeRemoveNodes[j][0]]) {
morphNode(node, node[2]);
} else {
morphNode(node, ['block', []]);
}
}
}
}
// Assign registers to function params based on entry junction
var paramRegs = {};
if (fun[2]) {
for (var i = 0; i < fun[2].length; i++) {
var allRegs = allRegsByType[localVars[fun[2][i]]];
fun[2][i] = allRegs[junctionVariables[fun[2][i]].reg];
paramRegs[fun[2][i]] = 1;
}
}
// That's it!
// Re-construct the function with appropriate variable definitions.
var finalAsmData = {
params: {},
vars: {},
inlines: asmData.inlines,
ret: asmData.ret,
};
for (var i = 1; i < nextReg; i++) {
var reg;
for (var type=0; type<allRegsByType.length; type++) {
reg = allRegsByType[type][i];
if (reg) break;
}
if (!paramRegs[reg]) {
finalAsmData.vars[reg] = type;
} else {
finalAsmData.params[reg] = type;
}
}
denormalizeAsm(fun, finalAsmData);
vacuum(fun);
});
}
// Eliminator aka Expressionizer
//
// The goal of this pass is to eliminate unneeded variables (which represent one of the infinite registers in the LLVM
// model) and thus to generate complex expressions where possible, for example
//
// var x = a(10);
// var y = HEAP[20];
// print(x+y);
//
// can be transformed into
//
// print(a(10)+HEAP[20]);
//
// The basic principle is to scan along the code in the order of parsing/execution, and keep a list of tracked
// variables that are current contenders for elimination. We must untrack when we see something that we cannot
// cross, for example, a write to memory means we must invalidate variables that depend on reading from
// memory, since if we change the order then we do not preserve the computation.
//
// We rely on some assumptions about emscripten-generated code here, which means we can do a lot more than
// a general JS optimization can. For example, we assume that 'sub' nodes (indexing like HEAP[..]) are
// memory accesses or FUNCTION_TABLE accesses, and in both cases that the symbol cannot be replaced although
// the contents can. So we assume FUNCTION_TABLE might have its contents changed but not be pointed to
// a different object, which allows
//
// var x = f();
// FUNCTION_TABLE[x]();
//
// to be optimized (f could replace FUNCTION_TABLE, so in general JS eliminating x is not valid).
//
// In memSafe mode, we are more careful and assume functions can replace HEAP and FUNCTION_TABLE, which
// can happen in ALLOW_MEMORY_GROWTH mode
var ELIMINATION_SAFE_NODES = set('var', 'assign', 'call', 'if', 'toplevel', 'do', 'return', 'label', 'switch', 'binary', 'unary-prefix'); // do is checked carefully, however
var IGNORABLE_ELIMINATOR_SCAN_NODES = set('num', 'toplevel', 'string', 'break', 'continue', 'dot'); // dot can only be STRING_TABLE.*
var ABORTING_ELIMINATOR_SCAN_NODES = set('new', 'object', 'function', 'defun', 'for', 'while', 'array', 'throw'); // we could handle some of these, TODO, but nontrivial (e.g. for while, the condition is hit multiple times after the body)
var HEAP_NAMES = set('HEAP8', 'HEAP16', 'HEAP32', 'HEAPU8', 'HEAPU16', 'HEAPU32', 'HEAPF32', 'HEAPF64');
function isTempDoublePtrAccess(node) { // these are used in bitcasts; they are not really affecting memory, and should cause no invalidation
assert(node[0] === 'sub');
return (node[2][0] === 'name' && node[2][1] === 'tempDoublePtr') ||
(node[2][0] === 'binary' && ((node[2][2][0] === 'name' && node[2][2][1] === 'tempDoublePtr') ||
(node[2][3][0] === 'name' && node[2][3][1] === 'tempDoublePtr')));
}
function eliminate(ast, memSafe) {
// Find variables that have a single use, and if they can be eliminated, do so
traverseGeneratedFunctions(ast, function(func, type) {
if (asm) var asmData = normalizeAsm(func);
//printErr('eliminate in ' + func[1]);
// First, find the potentially eliminatable functions: that have one definition and one use
var definitions = {};
var uses = {};
var namings = {};
var values = {};
var locals = {};
var varsToRemove = {}; // variables being removed, that we can eliminate all 'var x;' of (this refers to 'var' nodes we should remove)
// 1 means we should remove it, 2 means we successfully removed it
var varsToTryToRemove = {}; // variables that have 0 uses, but have side effects - when we scan we can try to remove them
// add arguments as locals
if (func[2]) {
for (var i = 0; i < func[2].length; i++) {
locals[func[2][i]] = true;
}
}
// examine body and note locals
var hasSwitch = false;
traverse(func, function(node, type) {
if (type === 'var') {
var node1 = node[1];
for (var i = 0; i < node1.length; i++) {
var node1i = node1[i];
var name = node1i[0];
var value = node1i[1];
if (value) {
if (!(name in definitions)) definitions[name] = 0;
definitions[name]++;
if (!values[name]) values[name] = value;
}
if (!uses[name]) uses[name] = 0;
locals[name] = true;
}
} else if (type === 'name') {
var name = node[1];
if (!uses[name]) uses[name] = 0;
uses[name]++;
} else if (type === 'assign') {
var target = node[2];
if (target[0] === 'name') {
var name = target[1];
if (!(name in definitions)) definitions[name] = 0;
definitions[name]++;
if (!uses[name]) uses[name] = 0;
if (!values[name]) values[name] = node[3];
if (node[1] === true) { // not +=, -= etc., just =
uses[name]--; // because the name node will show up by itself in the previous case
if (!namings[name]) namings[name] = 0;
namings[name]++; // offset it here, this tracks the total times we are named
}
}
} else if (type === 'switch') {
hasSwitch = true;
}
});
for (var used in uses) {
namings[used] = (namings[used] || 0) + uses[used];
}
// we cannot eliminate variables if there is a switch
if (hasSwitch && !asm) return;
var potentials = {}; // local variables with 1 definition and 1 use
var sideEffectFree = {}; // whether a local variable has no side effects in its definition. Only relevant when there are no uses
function unprocessVariable(name) {
if (name in potentials) delete potentials[name];
if (name in varsToRemove) delete varsToRemove[name];
if (name in sideEffectFree) delete sideEffectFree[name];
if (name in varsToTryToRemove) delete varsToTryToRemove[name];
}
function processVariable(name) {
if (definitions[name] === 1 && uses[name] === 1) {
potentials[name] = 1;
} else if (uses[name] === 0 && (!definitions[name] || definitions[name] <= 1)) { // no uses, no def or 1 def (cannot operate on phis, and the llvm optimizer will remove unneeded phis anyhow) (no definition means it is a function parameter, or a local with just |var x;| but no defining assignment)
var sideEffects = false;
var value = values[name];
if (value) {
// TODO: merge with other side effect code
// First, pattern-match
// (HEAP32[((tempDoublePtr)>>2)]=((HEAP32[(($_sroa_0_0__idx1)>>2)])|0),HEAP32[(((tempDoublePtr)+(4))>>2)]=((HEAP32[((($_sroa_0_0__idx1)+(4))>>2)])|0),(+(HEAPF64[(tempDoublePtr)>>3])))
// which has no side effects and is the special form of converting double to i64.
if (!(value[0] === 'seq' && value[1][0] === 'assign' && value[1][2][0] === 'sub' && value[1][2][2][0] === 'binary' && value[1][2][2][1] === '>>' &&
value[1][2][2][2][0] === 'name' && value[1][2][2][2][1] === 'tempDoublePtr')) {
// If not that, then traverse and scan normally.
sideEffects = hasSideEffects(value);
}
}
if (!sideEffects) {
varsToRemove[name] = !definitions[name] ? 2 : 1; // remove it normally
sideEffectFree[name] = true;
// Each time we remove a variable with 0 uses, if its value has no
// side effects and vanishes too, then we can remove a use from variables
// appearing in it, and possibly eliminate again
if (value) {
traverse(value, function(node, type) {
if (type === 'name') {
var name = node[1];
node[1] = ''; // we can remove this - it will never be shown, and should not be left to confuse us as we traverse
if (name in locals) {
uses[name]--; // cannot be infinite recursion since we descend an energy function
assert(uses[name] >= 0);
unprocessVariable(name);
processVariable(name);
}
}
});
}
} else {
varsToTryToRemove[name] = 1; // try to remove it later during scanning
}
}
}
for (var name in locals) {
processVariable(name);
}
//printErr('defs: ' + JSON.stringify(definitions));
//printErr('uses: ' + JSON.stringify(uses));
//printErr('values: ' + JSON.stringify(values));
//printErr('locals: ' + JSON.stringify(locals));
//printErr('varsToRemove: ' + JSON.stringify(varsToRemove));
//printErr('varsToTryToRemove: ' + JSON.stringify(varsToTryToRemove));
values = null;
//printErr('potentials: ' + JSON.stringify(potentials));
// We can now proceed through the function. In each list of statements, we try to eliminate
var tracked = {};
var globalsInvalidated = false; // do not repeat invalidations, until we track something new
var memoryInvalidated = false;
var callsInvalidated = false;
function track(name, value, defNode) { // add a potential that has just been defined to the tracked list, we hope to eliminate it
var usesGlobals = false, usesMemory = false, deps = {}, doesCall = false, hasDeps = false;
var ignoreName = false; // one-time ignorings of names, as first op in sub and call
traverse(value, function(node, type) {
if (type === 'name') {
if (!ignoreName) {
var name = node[1];
if (!(name in locals)) {
usesGlobals = true;
}
if (!(name in potentials)) { // deps do not matter for potentials - they are defined once, so no complexity
deps[name] = 1;
hasDeps = true;
}
} else {
ignoreName = false;
}
} else if (type === 'sub') {
usesMemory = true;
ignoreName = true;
} else if (type === 'call') {
usesGlobals = true;
usesMemory = true;
doesCall = true;
ignoreName = true;
} else {
ignoreName = false;
}
});
tracked[name] = {
usesGlobals: usesGlobals,
usesMemory: usesMemory,
defNode: defNode,
deps: deps,
hasDeps: hasDeps,
doesCall: doesCall
};
globalsInvalidated = false;
memoryInvalidated = false;
callsInvalidated = false;
//printErr('track ' + [name, JSON.stringify(tracked[name])]);
}
var temp = [];
// TODO: invalidate using a sequence number for each type (if you were tracked before the last invalidation, you are cancelled). remove for.in loops
function invalidateGlobals() {
//printErr('invalidate globals');
temp.length = 0;
for (var name in tracked) {
var info = tracked[name];
if (info.usesGlobals) {
temp.push(name);
}
}
for (var i = 0; i < temp.length; i++) {
delete tracked[temp[i]];
}
}
function invalidateMemory() {
//printErr('invalidate memory');
temp.length = 0;
for (var name in tracked) {
var info = tracked[name];
if (info.usesMemory) {
temp.push(name);
}
}
for (var i = 0; i < temp.length; i++) {
delete tracked[temp[i]];
}
}
function invalidateByDep(dep) {
//printErr('invalidate by dep ' + dep);
temp.length = 0;
for (var name in tracked) {
var info = tracked[name];
if (info.deps[dep]) {
temp.push(name);
}
}
for (var i = 0; i < temp.length; i++) {
delete tracked[temp[i]];
}
}
function invalidateCalls() {
//printErr('invalidate calls');
temp.length = 0;
for (var name in tracked) {
var info = tracked[name];
if (info.doesCall) {
temp.push(name);
}
}
for (var i = 0; i < temp.length; i++) {
delete tracked[temp[i]];
}
}
// Generate the sequence of execution. This determines what is executed before what, so we know what can be reordered. Using
// that, performs invalidations and eliminations
function scan(node) {
//printErr('scan: ' + JSON.stringify(node).substr(0, 50) + ' : ' + keys(tracked));
var abort = false;
var allowTracking = true; // false inside an if; also prevents recursing in an if
//var nesting = 1; // printErr-related
function traverseInOrder(node, ignoreSub, ignoreName) {
if (abort) return;
//nesting++; // printErr-related
//printErr(JSON.stringify(node).substr(0, 50) + ' : ' + keys(tracked) + ' : ' + [allowTracking, ignoreSub, ignoreName]);
var type = node[0];
if (type === 'assign') {
var target = node[2];
var value = node[3];
var nameTarget = target[0] === 'name';
traverseInOrder(target, true, nameTarget); // evaluate left
traverseInOrder(value); // evaluate right
// do the actual assignment
if (nameTarget) {
var name = target[1];
if (!(name in potentials)) {
if (!(name in varsToTryToRemove)) {
// expensive check for invalidating specific tracked vars. This list is generally quite short though, because of
// how we just eliminate in short spans and abort when control flow happens TODO: history numbers instead
invalidateByDep(name); // can happen more than once per dep..
if (!(name in locals) && !globalsInvalidated) {
invalidateGlobals();
globalsInvalidated = true;
}
// if we can track this name (that we assign into), and it has 0 uses and we want to remove its 'var'
// definition - then remove it right now, there is no later chance
if (allowTracking && (name in varsToRemove) && uses[name] === 0) {
track(name, node[3], node);
doEliminate(name, node);
}
} else {
// replace it in-place
node.length = value.length;
for (var i = 0; i < value.length; i++) {
node[i] = value[i];
}
varsToRemove[name] = 2;
}
} else {
if (allowTracking) track(name, node[3], node);
}
} else if (target[0] === 'sub') {
if (isTempDoublePtrAccess(target)) {
if (!globalsInvalidated) {
invalidateGlobals();
globalsInvalidated = true;
}
} else if (!memoryInvalidated) {
invalidateMemory();
memoryInvalidated = true;
}
}
} else if (type === 'sub') {
traverseInOrder(node[1], false, !memSafe); // evaluate inner
traverseInOrder(node[2]); // evaluate outer
// ignoreSub means we are a write (happening later), not a read
if (!ignoreSub && !isTempDoublePtrAccess(node)) {
// do the memory access
if (!callsInvalidated) {
invalidateCalls();
callsInvalidated = true;
}
}
} else if (type === 'var') {
var vars = node[1];
for (var i = 0; i < vars.length; i++) {
var name = vars[i][0];
var value = vars[i][1];
if (value) {
traverseInOrder(value);
if (name in potentials && allowTracking) {
track(name, value, node);
} else {
invalidateByDep(name);
}
if (vars.length === 1 && name in varsToTryToRemove && value) {
// replace it in-place
value = ['stat', value];
node.length = value.length;
for (var i = 0; i < value.length; i++) {
node[i] = value[i];
}
varsToRemove[name] = 2;
}
}
}
} else if (type === 'binary') {
var flipped = false;
if (node[1] in ASSOCIATIVE_BINARIES && !(node[2][0] in NAME_OR_NUM) && node[3][0] in NAME_OR_NUM) { // TODO recurse here?
// associatives like + and * can be reordered in the simple case of one of the sides being a name, since we assume they are all just numbers
var temp = node[2];
node[2] = node[3];
node[3] = temp;
flipped = true;
}
traverseInOrder(node[2]);
traverseInOrder(node[3]);
if (flipped && node[2][0] in NAME_OR_NUM) { // dunno if we optimized, but safe to flip back - and keeps the code closer to the original and more readable
var temp = node[2];
node[2] = node[3];
node[3] = temp;
}
} else if (type === 'name') {
if (!ignoreName) { // ignoreName means we are the name of something like a call or a sub - irrelevant for us
var name = node[1];
if (name in tracked) {
doEliminate(name, node);
} else if (!(name in locals) && !callsInvalidated && (memSafe || !(name in HEAP_NAMES))) { // ignore HEAP8 etc when not memory safe, these are ok to
// access, e.g. SIMD_Int32x4_load(HEAP8, ...)
invalidateCalls();
callsInvalidated = true;
}
}
} else if (type === 'unary-prefix' || type === 'unary-postfix') {
traverseInOrder(node[2]);
} else if (type in IGNORABLE_ELIMINATOR_SCAN_NODES) {
} else if (type === 'call') {
traverseInOrder(node[1], false, true);
var args = node[2];
for (var i = 0; i < args.length; i++) {
traverseInOrder(args[i]);
}
if (callHasSideEffects(node)) {
// these two invalidations will also invalidate calls
if (!globalsInvalidated) {
invalidateGlobals();
globalsInvalidated = true;
}
if (!memoryInvalidated) {
invalidateMemory();
memoryInvalidated = true;
}
}
} else if (type === 'if') {
if (allowTracking) {
traverseInOrder(node[1]); // can eliminate into condition, but nowhere else
if (!callsInvalidated) { // invalidate calls, since we cannot eliminate them into an if that may not execute!
invalidateCalls();
callsInvalidated = true;
}
allowTracking = false;
traverseInOrder(node[2]); // 2 and 3 could be 'parallel', really..
if (node[3]) traverseInOrder(node[3]);
allowTracking = true;
} else {
tracked = {};
}
} else if (type === 'block') {
var stats = node[1];
if (stats) {
for (var i = 0; i < stats.length; i++) {
traverseInOrder(stats[i]);
}
}
} else if (type === 'stat') {
traverseInOrder(node[1]);
} else if (type === 'label') {
traverseInOrder(node[2]);
} else if (type === 'seq') {
traverseInOrder(node[1]);
traverseInOrder(node[2]);
} else if (type === 'do') {
if (node[1][0] === 'num' && node[1][1] === 0) { // one-time loop
traverseInOrder(node[2]);
} else {
tracked = {};
}
} else if (type === 'return') {
if (node[1]) traverseInOrder(node[1]);
} else if (type === 'conditional') {
if (!callsInvalidated) { // invalidate calls, since we cannot eliminate them into a branch of an LLVM select/JS conditional that does not execute
invalidateCalls();
callsInvalidated = true;
}
traverseInOrder(node[1]);
traverseInOrder(node[2]);
traverseInOrder(node[3]);
} else if (type === 'switch') {
traverseInOrder(node[1]);
var originalTracked = {};
for (var o in tracked) originalTracked[o] = 1;
var cases = node[2];
for (var i = 0; i < cases.length; i++) {
var c = cases[i];
assert(c[0] === null || c[0][0] === 'num' || (c[0][0] === 'unary-prefix' && c[0][2][0] === 'num'));
var stats = c[1];
for (var j = 0; j < stats.length; j++) {
traverseInOrder(stats[j]);
}
// We cannot track from one switch case into another if there are external dependencies, undo all new trackings
// Otherwise we can track, e.g. a var used in a case before assignment in another case is UB in asm.js, so no need for the assignment
// TODO: general framework here, use in if-else as well
for (var t in tracked) {
if (!(t in originalTracked)) {
var info = tracked[t];
if (info.usesGlobals || info.usesMemory || info.hasDeps) {
delete tracked[t];
}
}
}
}
tracked = {}; // do not track from inside the switch to outside
} else {
if (!(type in ABORTING_ELIMINATOR_SCAN_NODES)) {
printErr('unfamiliar eliminator scan node: ' + JSON.stringify(node));
}
tracked = {};
abort = true;
}
//nesting--; // printErr-related
}
traverseInOrder(node);
}
//var eliminationLimit = 0; // used to debugging purposes
function doEliminate(name, node) {
//if (eliminationLimit === 0) return;
//eliminationLimit--;
//printErr('elim!!!!! ' + name);
// yes, eliminate!
varsToRemove[name] = 2; // both assign and var definitions can have other vars we must clean up
var info = tracked[name];
delete tracked[name];
var defNode = info.defNode;
var value;
if (!sideEffectFree[name]) {
if (defNode[0] === 'var') {
defNode[1].forEach(function(pair) {
if (pair[0] === name) {
value = pair[1];
}
});
assert(value);
} else { // assign
value = defNode[3];
// wipe out the assign
defNode[0] = 'toplevel';
defNode[1] = [];
defNode.length = 2;
}
// replace this node in-place
node.length = 0;
for (var i = 0; i < value.length; i++) {
node[i] = value[i];
}
} else {
// This has no side effects and no uses, empty it out in-place
node.length = 0;
node[0] = 'toplevel';
node[1] = [];
}
}
traverse(func, function(block) {
// Look for statements, including while-switch pattern
var stats = getStatements(block) || (block[0] === 'while' && block[2][0] === 'switch' ? [block[2]] : stats);
if (!stats) return;
//printErr('Stats: ' + JSON.stringify(stats).substr(0,100));
tracked = {};
//printErr('new StatBlock');
for (var i = 0; i < stats.length; i++) {
var node = stats[i];
//printErr('StatBlock[' + i + '] => ' + JSON.stringify(node).substr(0,100));
var type = node[0];
if (type === 'stat') {
node = node[1];
type = node[0];
} else if (type == 'return' && i < stats.length-1) {
stats.length = i+1; // remove any code after a return
}
// Check for things that affect elimination
if (type in ELIMINATION_SAFE_NODES) {
scan(node);
} else {
tracked = {}; // not a var or assign, break all potential elimination so far
}
}
//printErr('delete StatBlock');
});
var seenUses = {}, helperReplacements = {}; // for looper-helper optimization
// clean up vars, and loop variable elimination
traverse(func, function(node, type) {
// pre
if (type === 'var') {
node[1] = node[1].filter(function(pair) { return !varsToRemove[pair[0]] });
if (node[1].length === 0) {
// wipe out an empty |var;|
node[0] = 'toplevel';
node[1] = [];
}
} else if (type === 'assign' && node[1] === true && node[2][0] === 'name' && node[3][0] === 'name' && node[2][1] === node[3][1]) {
// elimination led to X = X, which we can just remove
return emptyNode();
}
}, function(node, type) {
// post
if (type === 'name') {
var name = node[1];
if (name in helperReplacements) {
node[1] = helperReplacements[name];
return; // no need to track this anymore, we can't loop-optimize more than once
}
// track how many uses we saw. we need to know when a variable is no longer used (hence we run this in the post)
if (!(name in seenUses)) {
seenUses[name] = 1;
} else {
seenUses[name]++;
}
} else if (type === 'while') {
if (!asm) return;
// try to remove loop helper variables specifically
var stats = node[2][1];
var last = stats[stats.length-1];
if (last && last[0] === 'if' && last[2][0] === 'block' && last[3] && last[3][0] === 'block') {
var ifTrue = last[2];
var ifFalse = last[3];
clearEmptyNodes(ifTrue[1]);
clearEmptyNodes(ifFalse[1]);
var flip = false;
if (ifFalse[1][0] && ifFalse[1][ifFalse[1].length-1][0] === 'break') { // canonicalize break in the if-true
var temp = ifFalse;
ifFalse = ifTrue;
ifTrue = temp;
flip = true;
}
if (ifTrue[1][0] && ifTrue[1][ifTrue[1].length-1][0] === 'break') {
var assigns = ifFalse[1];
clearEmptyNodes(assigns);
var loopers = [], helpers = [];
for (var i = 0; i < assigns.length; i++) {
if (assigns[i][0] === 'stat' && assigns[i][1][0] === 'assign') {
var assign = assigns[i][1];
if (assign[1] === true && assign[2][0] === 'name' && assign[3][0] === 'name') {
var looper = assign[2][1];
var helper = assign[3][1];
if (definitions[helper] === 1 && seenUses[looper] === namings[looper] &&
!helperReplacements[helper] && !helperReplacements[looper]) {
loopers.push(looper);
helpers.push(helper);
}
}
}
}
// remove loop vars that are used in the rest of the else
for (var i = 0; i < assigns.length; i++) {
if (assigns[i][0] === 'stat' && assigns[i][1][0] === 'assign') {
var assign = assigns[i][1];
if (!(assign[1] === true && assign[2][0] === 'name' && assign[3][0] === 'name') || loopers.indexOf(assign[2][1]) < 0) {
// this is not one of the loop assigns
traverse(assign, function(node, type) {
if (type === 'name') {
var index = loopers.indexOf(node[1]);
if (index < 0) index = helpers.indexOf(node[1]);
if (index >= 0) {
loopers.splice(index, 1);
helpers.splice(index, 1);
}
}
});
}
}
}
// remove loop vars that are used in the if
traverse(ifTrue, function(node, type) {
if (type === 'name') {
var index = loopers.indexOf(node[1]);
if (index < 0) index = helpers.indexOf(node[1]);
if (index >= 0) {
loopers.splice(index, 1);
helpers.splice(index, 1);
}
}
});
if (loopers.length === 0) return;
for (var l = 0; l < loopers.length; l++) {
var looper = loopers[l];
var helper = helpers[l];
// the remaining issue is whether loopers are used after the assignment to helper and before the last line (where we assign to it)
var found = -1;
for (var i = stats.length-2; i >= 0; i--) {
var curr = stats[i];
if (curr[0] === 'stat' && curr[1][0] === 'assign') {
var currAssign = curr[1];
if (currAssign[1] === true && currAssign[2][0] === 'name') {
var to = currAssign[2][1];
if (to === helper) {
found = i;
break;
}
}
}
}
if (found < 0) return;
// if a loop variable is used after we assigned to the helper, we must save its value and use that.
// (note that this can happen due to elimination, if we eliminate an expression containing the
// loop var far down, past the assignment!)
// first, see if the looper and helpers overlap. Note that we check for this looper, compared to
// *ALL* the helpers. Helpers will be replaced by loopers as we eliminate them, potentially
// causing conflicts, so any helper is a concern.
var firstLooperUsage = -1;
var lastLooperUsage = -1;
var firstHelperUsage = -1;
for (var i = found+1; i < stats.length; i++) {
var curr = i < stats.length-1 ? stats[i] : last[1]; // on the last line, just look in the condition
traverse(curr, function(node, type) {
if (type === 'name') {
if (node[1] === looper) {
if (firstLooperUsage < 0) firstLooperUsage = i;
lastLooperUsage = i;
} else if (helpers.indexOf(node[1]) >= 0) {
if (firstHelperUsage < 0) firstHelperUsage = i;
}
}
});
}
if (firstLooperUsage >= 0) {
// the looper is used, we cannot simply merge the two variables
if ((firstHelperUsage < 0 || firstHelperUsage > lastLooperUsage) && lastLooperUsage+1 < stats.length && triviallySafeToMove(stats[found], asmData) &&
seenUses[helper] === namings[helper]) {
// the helper is not used, or it is used after the last use of the looper, so they do not overlap,
// and the last looper usage is not on the last line (where we could not append after it), and the
// helper is not used outside of the loop.
// just move the looper definition to after the looper's last use
stats.splice(lastLooperUsage+1, 0, stats[found]);
stats.splice(found, 1);
} else {
// they overlap, we can still proceed with the loop optimization, but we must introduce a
// loop temp helper variable
var temp = looper + '$looptemp';
assert(!(temp in asmData.vars));
for (var i = firstLooperUsage; i <= lastLooperUsage; i++) {
var curr = i < stats.length-1 ? stats[i] : last[1]; // on the last line, just look in the condition
traverse(curr, function looperToLooptemp(node, type) {
if (type === 'name') {
if (node[1] === looper) {
node[1] = temp;
}
} else if (type === 'assign' && node[2][0] === 'name') {
// do not traverse the assignment target, phi assignments to the loop variable must remain
traverse(node[3], looperToLooptemp);
return null;
}
});
}
asmData.vars[temp] = asmData.vars[looper];
stats.splice(found, 0, ['stat', ['assign', true, ['name', temp], ['name', looper]]]);
}
}
}
for (var l = 0; l < helpers.length; l++) {
for (var k = 0; k < helpers.length; k++) {
if (l != k && helpers[l] === helpers[k]) return; // it is complicated to handle a shared helper, abort
}
}
// hurrah! this is safe to do
//printErr("ELIM LOOP VAR " + JSON.stringify(loopers) + ' :: ' + JSON.stringify(helpers));
for (var l = 0; l < loopers.length; l++) {
var looper = loopers[l];
var helper = helpers[l];
varsToRemove[helper] = 2;
traverse(node, function(node, type) { // replace all appearances of helper with looper
if (type === 'name' && node[1] === helper) node[1] = looper;
});
helperReplacements[helper] = looper; // replace all future appearances of helper with looper
helperReplacements[looper] = looper; // avoid any further attempts to optimize looper in this manner (seenUses is wrong anyhow, too)
}
// simplify the if. we remove the if branch, leaving only the else
if (flip) {
last[1] = simplifyNotCompsDirect(['unary-prefix', '!', last[1]]);
var temp = last[2];
last[2] = last[3];
last[3] = temp;
}
if (loopers.length === assigns.length) {
last.pop();
} else {
var elseStats = getStatements(last[3]);
for (var i = 0; i < elseStats.length; i++) {
var stat = elseStats[i];
if (stat[0] === 'stat') stat = stat[1];
if (stat[0] === 'assign' && stat[2][0] === 'name') {
if (loopers.indexOf(stat[2][1]) >= 0) {
elseStats[i] = emptyNode();
}
}
}
}
}
}
}
});
if (asm) {
for (var v in varsToRemove) {
if (varsToRemove[v] === 2) delete asmData.vars[v];
}
denormalizeAsm(func, asmData);
}
});
if (!asm) { // TODO: deprecate in non-asm too
// A class for optimizing expressions. We know that it is legitimate to collapse
// 5+7 in the generated code, as it will always be numerical, for example. XXX do we need this? here?
function ExpressionOptimizer(node) {
this.node = node;
this.run = function() {
traverse(this.node, function(node, type) {
if (type === 'binary' && node[1] === '+') {
var names = [];
var num = 0;
var has_num = false;
var fail = false;
traverse(node, function(subNode, subType) {
if (subType === 'binary') {
if (subNode[1] !== '+') {
fail = true;
return false;
}
} else if (subType === 'name') {
names.push(subNode[1]);
return;
} else if (subType === 'num') {
num += subNode[1];
has_num = true;
return;
} else {
fail = true;
return false;
}
});
if (!fail && has_num) {
var ret = ['num', num];
for (var i = 0; i < names.length; i++) {
ret = ['binary', '+', ['name', names[i]], ret];
}
return ret;
}
}
});
};
}
new ExpressionOptimizer(ast).run();
}
removeAllEmptySubNodes(ast);
}
function eliminateMemSafe(ast) {
eliminate(ast, true);
}
function minifyGlobals(ast) {
// The input is in form
//
// function instantiate(asmLibraryArg, wasmMemory, wasmTable) {
// var helper..
// function asmFunc(global, env, buffer) {
// var memory = env.memory;
// var HEAP8 = new global.Int8Array(buffer);
//
// We want to minify the interior instantiate, basically everything but
// the name instantiate itself, which is used externally to call it.
//
// This is *not* a complete minification algorithm. It does not have a full
// understanding of nested scopes. Instead it assumes the code is fairly
// simple - as wasm2js output is - and looks at all the minifiable names as
// a whole. A possible bug here is something like
//
// function instantiate(asmLibraryArg, wasmMemory, wasmTable) {
// var x = foo;
// function asmFunc(global, env, buffer) {
// var foo = 10;
//
// Here foo is declared in an inner scope, and the outer use of foo looks
// to the global scope. The analysis here only thinks something is from the
// global scope if it is not in any var or function declaration. In practice,
// the globals used from wasm2js output are things like Int8Array that we
// don't declare as locals, but we should probably have a fully scope-aware
// analysis here. FIXME
assert(ast[0] === 'toplevel');
var instantiateFunc = ast[1][0];
assert(instantiateFunc[0] === 'defun');
assert(instantiateFunc[1] === 'instantiate');
var minified = new Map();
var next = 0;
function getMinified(name) {
assert(name);
if (minified.has(name)) return minified.get(name);
ensureMinifiedNames(next);
var m = minifiedNames[next++];
minified.set(name, m);
return m;
}
// name nodes, ['name', name]
var allNames = [];
// functions, whose element func[1] is the name
var allNamedFunctions = [];
// name arrays, as in func[2], [name1, name2] which are the arguments
var allNameArrays = [];
// var arrays, as in [[name, init], ..] in a var or const
var allVarArrays = [];
// Add instantiate's parameters, but *not* its own name, which is used
// externally.
allNameArrays.push(instantiateFunc[2]);
// First, find all the other things to minify.
instantiateFunc[3].forEach(function(ast) {
traverse(ast, function(node, type) {
if (type === 'defun') {
allNamedFunctions.push(node);
allNameArrays.push(node[2]);
} else if (type === 'function') {
allNameArrays.push(node[2]);
} else if (type === 'var' || type === 'const') {
allVarArrays.push(node[1]);
} else if (type === 'name') {
allNames.push(node);
}
});
});
// All the things that are valid to minify: names declared in vars, or
// params, etc.
var validNames = new Set();
// TODO: sort to find the optimal minification
allVarArrays.forEach(function(array) {
for (var i = 0; i < array.length; i++) {
var name = array[i][0];
validNames.add(name);
array[i][0] = getMinified(name);
}
});
// add all globals in function chunks, i.e. not here but passed to us
for (var i = 0; i < extraInfo.globals.length; i++) {
var name = extraInfo.globals[i];
validNames.add(name);
getMinified(name);
}
allNamedFunctions.forEach(function(node) {
var name = node[1];
validNames.add(name);
node[1] = getMinified(name);
});
allNameArrays.forEach(function(array) {
for (var i = 0; i < array.length; i++) {
var name = array[i];
validNames.add(name);
array[i] = getMinified(name);
}
});
allNames.forEach(function(node) {
var name = node[1];
// A name may refer to a true global like Int8Array, which is not a valid
// name to minify, as we didn't see it declared.
if (validNames.has(name)) {
node[1] = getMinified(name);
}
});
var json = {};
for (var x of minified.entries()) json[x[0]] = x[1];
suffix = '// EXTRA_INFO:' + JSON.stringify(json);
}
function minifyLocals(ast) {
assert(extraInfo && extraInfo.globals);
traverseGeneratedFunctions(ast, function(fun, type) {
// Find the list of local names, including params.
if (asm) {
// TODO: we can avoid modifying at all here - we just need a list of local vars+params
var asmData = normalizeAsm(fun);
denormalizeAsm(fun, asmData);
} else {
// non-asm.js code - scan the whole function, which is inefficient
var localNames = {};
for (var param of fun[2]) {
localNames[param] = 1;
}
traverse(fun, function(node, type) {
if (type === 'var') {
node[1].forEach(function(defn) {
var name = defn[0];
localNames[name] = 1;
});
}
});
}
var newNames = {};
var usedNames = {};
// Find all the globals that we need to minify using
// pre-assigned names. Don't actually minify them yet
// as that might interfere with local variable names.
function isLocalName(name) {
if (asm) {
return name in asmData.vars || name in asmData.params;
} else {
return Object.prototype.hasOwnProperty.call(localNames, name);
}
}
traverse(fun, function(node, type) {
if (type === 'name') {
var name = node[1];
if (!isLocalName(name)) {
var minified = extraInfo.globals[name];
if (minified){
newNames[name] = minified;
usedNames[minified] = 1;
}
}
} else if (type === 'call') {
// We should never call a local name, as in asm.js-style code our
// locals are just numbers, not functions; functions are all declared
// in the outer scope. If a local is called, that is a bug.
if (node[1][0] === 'name') {
var name = node[1][1];
assert(!isLocalName(name), 'cannot call a local');
}
}
});
// The first time we encounter a local name, we assign it a
// minified name that's not currently in use. Allocating on
// demand means they're processed in a predictable order,
// which is very handy for testing/debugging purposes.
var nextMinifiedName = 0;
function getNextMinifiedName() {
var minified;
while (1) {
ensureMinifiedNames(nextMinifiedName);
minified = minifiedNames[nextMinifiedName++];
// TODO: we can probably remove !isLocalName here
if (!usedNames[minified] && !isLocalName(minified)) {
return minified;
}
}
}
// We can also minify loop labels, using a separate namespace
// to the variable declarations.
var newLabels = {};
var nextMinifiedLabel = 0;
function getNextMinifiedLabel() {
ensureMinifiedNames(nextMinifiedLabel);
return minifiedNames[nextMinifiedLabel++];
}
// Traverse and minify all names.
assert(extraInfo.globals.hasOwnProperty(fun[1]));
fun[1] = extraInfo.globals[fun[1]];
assert(fun[1] && typeof fun[1] === 'string');
if (fun[2]) {
for (var i = 0; i < fun[2].length; i++) {
var minified = getNextMinifiedName();
newNames[fun[2][i]] = minified;
fun[2][i] = minified;
}
}
traverse(fun[3], function(node, type) {
if (type === 'name') {
var name = node[1];
var minified = newNames[name];
if (minified) {
node[1] = minified;
} else if (isLocalName(name)) {
minified = getNextMinifiedName();
newNames[name] = minified;
node[1] = minified;
}
} else if (type === 'var') {
node[1].forEach(function(defn) {
var name = defn[0];
if (!(name in newNames)) {
newNames[name] = getNextMinifiedName();
}
defn[0] = newNames[name];
});
} else if (type === 'label') {
if (!newLabels[node[1]]) {
newLabels[node[1]] = getNextMinifiedLabel();
}
node[1] = newLabels[node[1]];
} else if (type === 'break' || type === 'continue') {
if (node[1]) {
node[1] = newLabels[node[1]];
}
}
});
});
}
// Relocation pass for a shared module (for the functions part of the module)
//
// 1. Replace function names with alternate names as defined (to avoid colliding with
// names in the main module we are being linked to)
// 2. Hardcode function table offsets from F_BASE+x to const+x if x is a variable, or
// the constant sum of the base + offset
// 3. Hardcode heap offsets from H_BASE as well
function relocate(ast) {
assert(asm); // we also assume we are normalized
var replacements = extraInfo.replacements;
var fBases = extraInfo.fBases;
var hBase = extraInfo.hBase;
var m;
traverse(ast, function(node, type) {
switch(type) {
case 'name': case 'defun': {
var rep = replacements[node[1]];
if (rep) node[1] = rep;
break;
}
case 'binary': {
if (node[1] == '+' && node[2][0] == 'name') {
var base = null;
if (node[2][1] == 'H_BASE') {
base = hBase;
} else if (m = /^F_BASE_(\w+)$/.exec(node[2][1])) {
base = fBases[m[1]] || 0; // 0 if the parent has no function table for this, but the child does. so no relocation needed
}
if (base !== null) {
var other = node[3];
if (base === 0) return other;
if (other[0] == 'num') {
other[1] = (other[1] + base)|0;
return other;
} else {
node[2] = ['num', base];
}
}
}
break;
}
case 'var': {
var vars = node[1];
for (var i = 0; i < vars.length; i++) {
var name = vars[i][0];
assert(!(name in replacements)); // cannot shadow functions we are replacing TODO: fix that
}
break;
}
}
});
}
// Break up very large functions
var NODES_WITHOUT_ELIMINATION_SENSITIVITY = set('name', 'num', 'binary', 'unary-prefix');
var FAST_ELIMINATION_BINARIES = setUnion(setUnion(USEFUL_BINARY_OPS, COMPARE_OPS), set('+'));
function measureSize(ast) {
var size = 0;
traverse(ast, function(node, type) {
// FIXME backwards compatibility: measure var internal node too.
if (type === 'var') {
size += node[1].length;
}
size++;
});
return size;
}
function measureCost(ast) {
var size = 0;
traverse(ast, function(node, type) {
if (type === 'num' || type === 'unary-prefix') size--;
else if (type === 'binary') {
if (node[3][0] === 'num' && node[3][1] === 0) size--;
else if (node[1] === '/' || node[1] === '%') size += 2;
}
else if (type === 'call' && !callHasSideEffects(node)) size -= 2;
else if (type === 'sub') size++;
size++;
});
return size;
}
function fixPtr(ptr, heap) {
switch (heap) {
case 'HEAP8': case 'HEAPU8': break;
case 'HEAP16': case 'HEAPU16': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 1) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 2]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAP32': case 'HEAPU32': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 2) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 4]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAPF32': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 2) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 4]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAPF64': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 3) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 8]]; // was unshifted, convert to absolute address
}
break;
}
default: {
return ptr; // unchanged
}
}
ptr = ['binary', '|', ptr, ['num', 0]];
return ptr;
}
function safeHeap(ast) {
var SAFE_HEAP_FUNCS = set('SAFE_HEAP_LOAD', 'SAFE_HEAP_LOAD_D', 'SAFE_HEAP_STORE', 'SAFE_HEAP_STORE_D', 'SAFE_FT_MASK');
traverseGeneratedFunctions(ast, function(func) {
if (func[1] in SAFE_HEAP_FUNCS) return null;
traverseGenerated(func, function(node, type) {
var heap, ptr;
if (type === 'assign') {
if (node[1] === true && node[2][0] === 'sub') {
heap = node[2][1][1];
ptr = fixPtr(node[2][2], heap);
var value = node[3];
// SAFE_HEAP_STORE(ptr, value, bytes, isFloat)
switch (heap) {
case 'HEAP8': case 'HEAPU8': {
return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_INT), ['num', 1]]];
}
case 'HEAP16': case 'HEAPU16': {
return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_INT), ['num', 2]]];
}
case 'HEAP32': case 'HEAPU32': {
return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_INT), ['num', 4]]];
}
case 'HEAPF32': {
return ['call', ['name', 'SAFE_HEAP_STORE_D'], [ptr, makeAsmCoercion(value, ASM_DOUBLE), ['num', 4]]];
}
case 'HEAPF64': {
return ['call', ['name', 'SAFE_HEAP_STORE_D'], [ptr, makeAsmCoercion(value, ASM_DOUBLE), ['num', 8]]];
}
default: throw 'bad heap ' + heap;
}
}
} else if (type === 'sub') {
var target = node[1][1];
if (target[0] === 'H') {
// heap access
heap = target;
ptr = fixPtr(node[2], heap);
// SAFE_HEAP_LOAD(ptr, bytes, isFloat)
switch (heap) {
case 'HEAP8': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 1], ['num', 0]]], ASM_INT);
}
case 'HEAPU8': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 1], ['num', 1]]], ASM_INT);
}
case 'HEAP16': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 2], ['num', 0]]], ASM_INT);
}
case 'HEAPU16': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 2], ['num', 1]]], ASM_INT);
}
case 'HEAP32': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 4], ['num', 0]]], ASM_INT);
}
case 'HEAPU32': {
// Note that a 32-bit unsigned number should not be changed to a signed one.
return makeSignedAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 4], ['num', 1]]], ASM_INT, ASM_UNSIGNED);
}
case 'HEAPF32': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD_D'], [ptr, ['num', 4]]], ASM_DOUBLE);
}
case 'HEAPF64': {
return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD_D'], [ptr, ['num', 8]]], ASM_DOUBLE);
}
default: throw 'bad heap ' + heap;
}
} else {
assert(target[0] == 'F');
// function table indexing mask
assert(node[2][0] === 'binary' && node[2][1] === '&');
node[2][2] = makeAsmCoercion(['call', ['name', 'SAFE_FT_MASK'], [makeAsmCoercion(node[2][2], ASM_INT), makeAsmCoercion(node[2][3], ASM_INT)]], ASM_INT);
}
}
});
});
}
function fixPtrSlim(ptr, heap, shell) {
switch (heap) {
case 'HEAP8': case 'HEAPU8': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 0) {
ptr = ['binary', '|', ptr[2], ['num', 0]]; // smaller
}
break;
}
case 'HEAP16': case 'HEAPU16': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 1) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 2]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAP32': case 'HEAPU32': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 2) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 4]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAPF32': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 2) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 4]]; // was unshifted, convert to absolute address
}
break;
}
case 'HEAPF64': {
if (ptr[0] === 'binary' && ptr[1] === '>>' && ptr[3][0] === 'num' && ptr[3][1] === 3) {
ptr = ptr[2]; // skip the shift
} else {
ptr = ['binary', '*', ptr, ['num', 8]]; // was unshifted, convert to absolute address
}
break;
}
default: {
if (!shell) throw 'bad heap ' + heap;
return ptr; // unchanged
}
}
return ptr;
}
function splitMemory(ast, shell) {
traverse(ast, function(node, type) {
var heap, ptr;
if (type === 'assign') {
if (node[2][0] === 'sub' && node[2][1][0] === 'name') {
heap = node[2][1][1];
if (parseHeap(heap)) {
if (node[1] !== true) assert(0, 'bad assign, split memory cannot handle ' + JSON.stringify(node) + '= to a HEAP');
ptr = fixPtrSlim(node[2][2], heap, shell);
var value = node[3];
switch (heap) {
case 'HEAP8': return ['call', ['name', 'set8'], [ptr, value]];
case 'HEAP16': return ['call', ['name', 'set16'], [ptr, value]];
case 'HEAP32': return ['call', ['name', 'set32'], [ptr, value]];
case 'HEAPU8': return ['call', ['name', 'setU8'], [ptr, value]];
case 'HEAPU16': return ['call', ['name', 'setU16'], [ptr, value]];
case 'HEAPU32': return ['call', ['name', 'setU32'], [ptr, value]];
case 'HEAPF32': return ['call', ['name', 'setF32'], [ptr, value]];
case 'HEAPF64': return ['call', ['name', 'setF64'], [ptr, value]];
default: if (!shell) throw 'bad heap ' + heap;
}
}
}
} else if (type === 'sub') {
var target = node[1][1];
if (target[0] === 'H') {
// heap access
heap = target;
ptr = fixPtrSlim(node[2], heap, shell);
switch (heap) {
case 'HEAP8': return ['call', ['name', 'get8'], [ptr]];
case 'HEAP16': return ['call', ['name', 'get16'], [ptr]];
case 'HEAP32': return ['call', ['name', 'get32'], [ptr]];
case 'HEAPU8': return ['call', ['name', 'getU8'], [ptr]];
case 'HEAPU16': return ['call', ['name', 'getU16'], [ptr]];
case 'HEAPU32': return ['call', ['name', 'getU32'], [ptr]];
case 'HEAPF32': return ['call', ['name', 'getF32'], [ptr]];
case 'HEAPF64': return ['call', ['name', 'getF64'], [ptr]];
default: if (!shell) throw 'bad heap ' + heap;
}
}
}
});
var SPLIT_GETS = set('get8', 'get16', 'get32', 'getU8', 'getU16', 'getU32', 'getF32', 'getF64');
traverse(ast, function(node, type) {
if (type === 'binary' && node[1] === '|' && node[2][0] === 'call' && node[2][1][0] === 'name' && node[2][1][1] in SPLIT_GETS && node[3][0] === 'num' && node[3][1] === 0) {
return node[2];
} else if (type === 'unary-prefix' && node[1] === '+' && node[2][0] === 'call' && node[2][1][0] === 'name' && node[2][1][1] in SPLIT_GETS) {
return node[2];
}
});
}
function splitMemoryShell(ast) {
splitMemory(ast, true);
}
// Ensures that if label exists, it is assigned an initial value (to not assume the asm declaration has an effect, which we normally do not)
function ensureLabelSet(ast) {
assert(asm);
traverseGeneratedFunctions(ast, function(func) {
var asmData = normalizeAsm(func);
if ('label' in asmData.vars) {
var stats = getStatements(func);
for (var i = 0; i < stats.length; i++) {
var node = stats[i];
if (node[0] === 'stat') node = node[1];
if (node[0] === 'assign' && node[2][0] === 'name' && node[2][1] === 'label') {
break; // all good
}
if (node[0] === 'label' || (node[0] in CONTROL_FLOW)) {
// we haven't seen an assign, and we hit control flow. add an assign
stats.splice(i, 0, ['stat', ['assign', true, ['name', 'label'], ['num', 0]]]);
break;
}
}
}
denormalizeAsm(func, asmData);
});
}
// finds vars that may not be assigned to. misses out on vars that are assigned to in all branches of an if etc. TODO: optimize
function findUninitializedVars(func, asmData) {
var bad = {};
function definitely(node, uninitialized) {
if (!node) return;
if (node[0] === 'assign' && node[2][0] === 'name') {
var name = node[2][1];
if (name in uninitialized) {
delete uninitialized[name]; // this one is now ok
}
} else if (node[0] === 'name') {
var name = node[1];
if (name in uninitialized) {
bad[name] = 1;
delete uninitialized[name]; // this one is now bad, ignore it
}
}
traverseChildrenInExecutionOrder(node, definitely, maybe, uninitialized);
}
function maybe(node, uninitialized) {
uninitialized = copy(uninitialized); // copy it; changes here will not propagate
traverseChildrenInExecutionOrder(node, definitely, maybe, uninitialized);
}
traverseChildrenInExecutionOrder(func, definitely, maybe, copy(asmData.vars));
return bad;
}
// emits which functions are directly reachable, except for some that are
// ignored
function findReachable(ast) {
var IGNORED = set(extraInfo.ignored);
var reachable = {};
traverseGeneratedFunctions(ast, function(func) {
if (func[1] in IGNORED) return;
traverse(func, function(node, type) {
if (type === 'call' && node[1][0] === 'name') {
reachable[node[1][1]] = 1;
}
});
});
print('// REACHABLE ' + JSON.stringify(keys(reachable)));
}
// emits call graph information
function dumpCallGraph(ast) {
traverseGeneratedFunctions(ast, function(func) {
var reachable = {};
traverse(func, function(node, type) {
if (type === 'call') {
if (node[1][0] === 'name') {
reachable[node[1][1]] = 1;
} else {
// (FUNCTION_TABLE[..])(..)
assert(node[1][0] === 'sub')
assert(node[1][1][0] === 'name');
reachable[node[1][1][1]] = 1;
}
}
});
print('// REACHABLE ' + JSON.stringify([func[1], ' => ', keys(reachable)]));
});
}
// Last pass utilities
// Change +5 to DOT$ZERO(5). We then textually change 5 to 5.0 (uglify's ast
// cannot differentiate between 5 and 5.0 directly)
function prepDotZero(ast) {
traverse(ast, function(node, type) {
if (type === 'unary-prefix' && node[1] === '+') {
if (node[2][0] === 'num' ||
(node[2][0] === 'unary-prefix' && node[2][1] === '-' && node[2][2][0] === 'num')) {
return ['call', ['name', 'DOT$ZERO'], [node[2]]];
}
}
});
}
function fixDotZero(js) {
return js.replace(/-DOT\$ZERO\(-/g, '- DOT$ZERO(-') // avoid x - (-y.0) turning into x--y.0 when minified
.replace(/DOT\$ZERO\(([-+]?(0x)?[0-9a-f]*\.?[0-9]*([eE][-+]?[0-9]+)?)\)/g, function(m, num) {
if (num.substr(0, 2) === '0x' || num.substr(0, 3) === '-0x') {
var ret = eval(num).toString();
if (ret.indexOf('.') < 0) return ret + '.0';
return ret;
}
if (num.indexOf('.') >= 0) return num;
var e = num.indexOf('e');
if (e < 0) return num + '.0';
return num.substr(0, e) + '.0' + num.substr(e);
});
}
function asmLastOpts(ast) {
var statsStack = [];
traverseGeneratedFunctions(ast, function(fun) {
traverse(fun, function(node, type) {
var stats = getStatements(node);
if (stats) statsStack.push(stats);
if (type in CONDITION_CHECKERS) {
node[1] = simplifyCondition(node[1]);
}
if (type === 'while' && node[1][0] === 'num' && node[1][1] === 1 && node[2][0] === 'block' && node[2].length == 2) {
// This is at the end of the pipeline, we can assume all other optimizations are done, and we modify loops
// into shapes that might confuse other passes
// while (1) { .. if (..) { break } } ==> do { .. } while(..)
var stats = node[2][1];
var last = stats[stats.length-1];
if (last && last[0] === 'if' && !last[3] && last[2][0] === 'block' && last[2][1][0]) {
var lastStats = last[2][1];
var lastNum = lastStats.length;
var lastLast = lastStats[lastNum-1];
if (!(lastLast[0] === 'break' && !lastLast[1])) return;// if not a simple break, dangerous
for (var i = 0; i < lastNum; i++) {
if (lastStats[i][0] !== 'stat' && lastStats[i][0] !== 'break') return; // something dangerous
}
// ok, a bunch of statements ending in a break
var abort = false;
var stack = 0;
var breaks = 0;
traverse(stats, function(node, type) {
if (type === 'continue') {
if (stack === 0 || node[1]) { // abort if labeled (we do not analyze labels here yet), or a continue directly on us
abort = true;
return true;
}
} else if (type === 'break') {
if (stack === 0 || node[1]) { // relevant if labeled (we do not analyze labels here yet), or a break directly on us
breaks++;
}
} else if (type in LOOP) {
stack++;
}
}, function(node, type) {
if (type in LOOP) {
stack--;
}
});
if (abort) return;
assert(breaks > 0);
if (lastStats.length > 1 && breaks !== 1) return; // if we have code aside from the break, we can only move it out if there is just one break
// start to optimize
if (lastStats.length > 1) {
var parent = statsStack[statsStack.length-1];
var me = parent.indexOf(node);
if (me < 0) return; // not always directly on a stats, could be in a label for example
parent.splice.apply(parent, [me+1, 0].concat(lastStats.slice(0, lastStats.length-1)));
}
var conditionToBreak = last[1];
stats.pop();
node[0] = 'do';
node[1] = simplifyNotCompsDirect(['unary-prefix', '!', conditionToBreak]);
return node;
}
} else if (type === 'binary') {
if (node[1] === '&') {
if (node[3][0] === 'unary-prefix' && node[3][1] === '-' && node[3][2][0] === 'num' && node[3][2][1] === 1) {
// Change &-1 into |0, at this point the hint is no longer needed
node[1] = '|';
node[3] = node[3][2];
node[3][1] = 0;
}
} else if (node[1] === '-' && node[3][0] === 'unary-prefix') {
// avoid X - (-Y) because some minifiers buggily emit X--Y which is invalid as -- can be a unary. Transform to
// X + Y
if (node[3][1] === '-') { // integer
node[1] = '+';
node[3] = node[3][2];
} else if (node[3][1] === '+') { // float
if (node[3][2][0] === 'unary-prefix' && node[3][2][1] === '-') {
node[1] = '+';
node[3][2] = node[3][2][2];
}
}
}
}
}, function(node, type) {
var stats = getStatements(node);
if (stats) statsStack.pop();
});
if (!debug) { // dangerous in debug mode, as without braces things can end up on the same line, together with comments
// convert { singleton } into singleton
traverse(fun, function(node, type) {
if (type === 'block' && node[1] && node[1].length === 1) {
return node[1][0];
}
});
}
});
}
function removeFuncs(ast) {
assert(ast[0] === 'toplevel');
var keep = set(extraInfo.keep);
ast[1] = ast[1].filter(function(node) {
assert(node[0] === 'defun');
return node[1] in keep;
});
}
// Passes table
var minifyWhitespace = false, printMetadata = true, asm = false,
emitJSON = false, last = false,
emitAst = true;
var passes = {
// passes
dumpAst: dumpAst,
dumpSrc: dumpSrc,
removeAssignsToUndefined: removeAssignsToUndefined,
//removeUnneededLabelSettings: removeUnneededLabelSettings,
simplifyExpressions: simplifyExpressions,
localCSE: localCSE,
safeLabelSetting: safeLabelSetting,
simplifyIfs: simplifyIfs,
hoistMultiples: hoistMultiples,
loopOptimizer: loopOptimizer,
registerize: registerize,
registerizeHarder: registerizeHarder,
eliminate: eliminate,
eliminateMemSafe: eliminateMemSafe,
minifyGlobals: minifyGlobals,
minifyLocals: minifyLocals,
relocate: relocate,
safeHeap: safeHeap,
splitMemory: splitMemory,
splitMemoryShell: splitMemoryShell,
ensureLabelSet: ensureLabelSet,
findReachable: findReachable,
dumpCallGraph: dumpCallGraph,
asmLastOpts: asmLastOpts,
removeFuncs: removeFuncs,
noop: function() {},
// flags
minifyWhitespace: function() { minifyWhitespace = true },
noPrintMetadata: function() { printMetadata = false },
asm: function() { asm = true },
emitJSON: function() { emitJSON = true },
receiveJSON: function() { }, // handled in a special way, before passes are run
last: function() { last = true },
noEmitAst: function() { emitAst = false },
};
// Main
var suffix = '';
arguments_ = arguments_.filter(function (arg) {
if (!/^--/.test(arg)) return true;
if (arg === '--debug') debug = true;
else throw new Error('Unrecognized flag: ' + arg);
});
var src = read(arguments_[0]);
generatedFunctions = src.lastIndexOf(GENERATED_FUNCTIONS_MARKER) >= 0;
var extraInfoStart = src.lastIndexOf('// EXTRA_INFO:')
if (extraInfoStart > 0) extraInfo = JSON.parse(src.substr(extraInfoStart + 14));
//printErr(JSON.stringify(extraInfo));
var ast;
if (arguments_.indexOf('receiveJSON') < 0) {
ast = srcToAst(src);
} else {
var commentStart = src.indexOf('//');
if (commentStart >= 0) {
src = src.substr(0, commentStart); // JSON.parse will error on a trailing comment
}
ast = JSON.parse(src);
}
//printErr('ast: ' + JSON.stringify(ast));
arguments_.slice(1).forEach(function(arg) {
passes[arg](ast);
});
if (asm && last) {
prepDotZero(ast);
}
if (emitAst) {
if (!emitJSON) {
var js = astToSrc(ast, minifyWhitespace), old;
if (asm && last) {
js = fixDotZero(js);
}
// remove unneeded newlines+spaces, and print
do {
old = js;
js = js.replace(/\n *\n/g, '\n');
} while (js != old);
print(js);
print('\n');
print(suffix);
} else {
print(JSON.stringify(ast));
}
}