diff options
Diffstat (limited to 'tools/js-optimizer.js')
-rw-r--r-- | tools/js-optimizer.js | 1387 |
1 files changed, 1301 insertions, 86 deletions
diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js index 5324e15c..b35da99d 100644 --- a/tools/js-optimizer.js +++ b/tools/js-optimizer.js @@ -1603,6 +1603,7 @@ function normalizeAsm(func) { 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] = detectAsmCoercion(node[3]); stats[i] = emptyNode(); i++; @@ -1773,9 +1774,7 @@ function ensureMinifiedNames(n) { // make sure the nth index in minifiedNames ex } } -// Very simple 'registerization', coalescing of variables into a smaller number, -// as part of minification. Globals-level minification began in a previous pass, -// we receive extraInfo which tells us how to rename globals. (Only in asm.js.) +// 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. @@ -1811,6 +1810,7 @@ function registerize(ast) { // 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') { @@ -1823,74 +1823,25 @@ function registerize(ast) { } } else if (type === 'switch') { hasSwitch = true; + } else if (type === 'name') { + allVars[node[1]] = 1; } }); vacuum(fun); - if (extraInfo && extraInfo.globals) { - assert(asm); - var usedGlobals = {}; - var nextLocal = 0; - // Minify globals using the mapping we were given - traverse(fun, function(node, type) { - if (type === 'name') { - var name = node[1]; - var minified = extraInfo.globals[name]; - if (minified) { - assert(!localVars[name], name); // locals must not shadow globals, or else we don't know which is which - if (localVars[minified]) { - // trying to minify a global into a name used locally. rename all the locals - var newName = '$_newLocal_' + (nextLocal++); - assert(!localVars[newName]); - if (params[minified]) { - params[newName] = 1; - delete params[minified]; - } - localVars[newName] = 1; - delete localVars[minified]; - asmData.vars[newName] = asmData.vars[minified]; - delete asmData.vars[minified]; - asmData.params[newName] = asmData.params[minified]; - delete asmData.params[minified]; - traverse(fun, function(node, type) { - if (type === 'name' && node[1] === minified) { - node[1] = newName; - } - }); - if (fun[2]) { - for (var i = 0; i < fun[2].length; i++) { - if (fun[2][i] === minified) fun[2][i] = newName; - } - } - } - node[1] = minified; - usedGlobals[minified] = 1; - } - } - }); - if (fun[1] in extraInfo.globals) { // if fun was created by a previous optimization pass, it will not be here - fun[1] = extraInfo.globals[fun[1]]; - assert(fun[1]); - } - var nextRegName = 0; - } var regTypes = {}; function getNewRegName(num, name) { - if (!asm) return 'r' + num; - var type = asmData.vars[name]; - if (!extraInfo || !extraInfo.globals) { - var ret = (type ? 'd' : 'i') + num; + var ret; + if (!asm) { + ret = 'r' + num; + } else { + var type = asmData.vars[name]; + ret = (type ? 'd' : 'i') + num; regTypes[ret] = type; - return ret; } - // find the next free minified name that is not used by a global that shows up in this function - while (1) { - ensureMinifiedNames(nextRegName); - var ret = minifiedNames[nextRegName++]; - if (!usedGlobals[ret]) { - regTypes[ret] = type; - return ret; - } + 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 @@ -2111,37 +2062,1070 @@ function registerize(ast) { } } denormalizeAsm(fun, finalAsmData); - if (extraInfo && extraInfo.globals) { - // minify in asm var definitions, that denormalizeAsm just generated - function minify(value) { - if (value && value[0] === 'call' && value[1][0] === 'name') { - var name = value[1][1]; - var minified = extraInfo.globals[name]; - if (minified) { - value[1][1] = minified; + } + }); +} + + +// 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) { + + 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 = [{}, {}, {}]; + var regPrefixByType = ['i', 'd', 'f']; + var nextReg = 1; + + function createReg(forName) { + // Create a new register of type suitable for the given variable name. + var allRegs = allRegsByType[localVars[forName]]; + 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 + // * any preconditions satisfied at entry to the block + // * 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) + + 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); + return id; + } + + function setJunction(id) { + // Set the next entry junction to the given id. + // This can be used to enter at a previously-declared point. + assert(nextBasicBlock.nodes.length === 0, 'refusing to abandon an in-progress basic block') + currEntryJunction = id; + } + + function joinJunction(id) { + // 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, pre: {}, nodes: [], isexpr: [], use: {}, kill: {} }; + currEntryJunction = id; + 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 newLabels = copy(activeLabels[activeLabels.length-1]); + newLabels[null] = [onContinue, onBreak]; + if (nextLoopLabel) { + newLabels[nextLoopLabel] = [onContinue, onBreak]; + nextLoopLabel = null; + } + 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 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'); + } + } + setJunction(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 addPreCondTrue(node) { + // Add pre-conditions implied by truth of the + // given node to the current basic block. + assert(nextBasicBlock.nodes.length === 0, 'cant add preconditions to an in-progress basic block') + if (node[0] === 'binary' && node[1] === '==') { + var lhs = lookThroughCasts(node[2]); + var rhs = lookThroughCasts(node[3]); + if (lhs[0] === 'name' && rhs[0] === 'num') { + nextBasicBlock.pre[lhs[1]] = ['==', rhs[1]]; + } + } + } + + function addPreCondFalse(node) { + // Add pre-conditions implied by falsehood of the + // given node to the current basic block. + assert(nextBasicBlock.nodes.length === 0, 'cant add preconditions to an in-progress basic block') + if (node[0] === 'binary' && node[1] === '==') { + var lhs = lookThroughCasts(node[2]); + var rhs = lookThroughCasts(node[3]); + if (lhs[0] === 'name' && rhs[0] === 'num') { + nextBasicBlock.pre[lhs[1]] = ['!=', rhs[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(); + addPreCondTrue(node[1]); + if (node[2]) { + buildFlowGraph(node[2]); + } + var jExit = markJunction(); + setJunction(jEnter); + addPreCondFalse(node[1]); + if (node[3]) { + buildFlowGraph(node[3]); + } + joinJunction(jExit); + break; + case 'conditional': + isInExpr++; + buildFlowGraph(node[1]); + var jEnter = markJunction(); + addPreCondTrue(node[1]); + if (node[2]) { + buildFlowGraph(node[2]); + } + var jExit = markJunction(); + setJunction(jEnter); + addPreCondFalse(node[1]); + 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); + addPreCondTrue(node[1]); + 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': + // This is horrific. It need to capture the default flow-through + // logic of sequential case bodies, as well as the theoretical + // sequential evaluation of each case clause. + // TODO: simplify based on asmjs switch-statement restrictions. + isInExpr++; + buildFlowGraph(node[1]); + isInExpr--; + var jCheckExit = markJunction(); + var jExit = addJunction(); + pushActiveLabels(null, jExit); + // Process all cases as a sequential chain of checks. + // Process all case bodies as one big flow-through statement. + // They might break themselves out of it but this implements the + // default fall-through case logic. + var hasDefault = false; + var jPrevCaseExit = jCheckExit; + var jPrevBodyExit = jCheckExit; + for (var i=0; i<node[2].length; i++) { + // In the general case we'll need a basic block for the case clause. + // Try to avoid it for common, simple, non-var-using cases. + if (!node[2][i][0]) { + hasDefault = true; + } else { + if (node[2][i][0][0] !== 'num') { + setJunction(jPrevCaseExit); + isInExpr++; + buildFlowGraph(node[2][i][0]); + isInExpr--; + jPrevCaseExit = markJunction(); + } + } + // The next case body flows from the exit of the prev one, + // or may be entered directly from the case statement exit. + setJunction(jPrevCaseExit); + if (jPrevBodyExit !== jCheckExit) { + joinJunction(jPrevBodyExit); + } + for (var j = 0; j < node[2][i][1].length; j++) { + buildFlowGraph(node[2][i][1][j]); } + jPrevBodyExit = markJunction(); } + markJunction(jExit); + // If there was no default case, we also need an empty block + // linking straight from entry to exit. + if (!hasDefault && jCheckExit !== jPrevBodyExit) { + 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--; + 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 a specific label value as precondition? + var labelCond = block.pre['label']; + if (labelCond && labelCond[0] === '==') { + // 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. + if (labelCond[1] in labelledBlocks) { + labelledBlocks = {}; + labelledJumps = []; + break FINDLABELLEDBLOCKS; } - var stats = fun[3]; - for (var i = 0; i < stats.length; i++) { - var line = stats[i]; - if (i >= fun[2].length && line[0] !== 'var') break; // when we pass the arg and var coercions, break - if (line[0] === 'stat') { - assert(line[1][0] === 'assign'); - minify(line[1][3]); + labelledBlocks[labelCond[1]] = 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 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; + 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 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.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 conflits with any output vars of successor blocks, + // if they're assigned before it goes dead in that block. + 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]; + // Calculate the full internal liveness states for this block. + var liveness = [{}]; + for (var name in jExit.live) { + liveness[0][name] = 1; + } + for (var j=block.nodes.length-1; j>=0; j--) { + var node = block.nodes[j]; + liveness.unshift(copy(liveness[0])); + if (node[0] === 'assign') { + var name = node[2][1]; + delete liveness[0][name]; + } else if (node[0] === 'name') { + var name = node[1]; + liveness[0][name] = 1; + } else { + assert(false, 'unexpected node type: ' + node[0]); + } + } + assert(liveness.length == block.nodes.length + 1); + assert(setSize(setSub(liveness[0], jEnter.live)) == 0); + // Mark the point at which each output reg gets assigned. + // Variables that live past this point must not be assigned + // to that register. + var outputAssignLoc = {}; + var outputVars = {}; + for (var name in jExit.live) { + var reg = junctionVariables[name].reg; + assert(reg !== null, 'output variable doesnt have a register'); + outputAssignLoc[reg] = block.lastKillLoc[name]; + outputVars[reg] = name; + } + // Scan through in execution order, allocating registers on demand. + // Be careful to avoid conflicts with the output 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 entry-point. + for (var name in liveness[0]) { + var reg = junctionVariables[name].reg; + assert(reg !== null, 'input 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 freeing registers according to the calculated liveness info. + for (var j = 0; j < block.nodes.length; 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. It should already be in a register. + assert(liveness[j][name], 'use node, but name was not alive?') + assert(reg, 'live variable did not have a reg?') + node[1] = allRegs[reg]; + } else { + // A kill. This should assign it a new register. + assert(!liveness[j][name], 'kill node, but name was alive?') + assert(!reg, 'non-live variable still had a reg?') + if (name in jExit.live && j === block.lastKillLoc[name]) { + // Assignment to an output 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 { - assert(line[0] === 'var'); - var pairs = line[1]; - for (var j = 0; j < pairs.length; j++) { - minify(pairs[j][1]); + // Try to use one of the existing free registers. + // It must not conflict with an output register. + for (var k = freeRegs.length - 1; k >= 0; k--) { + reg = freeRegs[k]; + // Check for conflict with output registers. + if (block.lastUseLoc[name] > outputAssignLoc[reg]) { + if (name !== outputVars[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; } } + // Modify assignment to use the new name. + // If we happen to create a "x=x" type do-nothing assignment, + // we can safely morph it into a no-op. + node[2][1] = allRegs[reg]; + if (node[3][0] === 'name' && node[3][1] === node[2][1]) { + morphNode(node, ['block', []]); + } } + // Free the reg if it's not live in the next step. + if (!liveness[j+1][name]) { + delete assignedRegs[name]; + freeRegs.push(reg); + } + } + } + + // 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, + }; + 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 @@ -2322,7 +3306,7 @@ function eliminate(ast, memSafe) { 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; + 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') { @@ -2333,6 +3317,7 @@ function eliminate(ast, memSafe) { } 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; @@ -2354,6 +3339,7 @@ function eliminate(ast, memSafe) { usesMemory: usesMemory, defNode: defNode, deps: deps, + hasDeps: hasDeps, doesCall: doesCall }; globalsInvalidated = false; @@ -2426,7 +3412,7 @@ function eliminate(ast, memSafe) { function traverseInOrder(node, ignoreSub, ignoreName) { if (abort) return; //nesting++; // printErr-related - //printErr(spaces(2*(nesting+1)) + 'trav: ' + JSON.stringify(node).substr(0, 50) + ' : ' + keys(tracked) + ' : ' + [allowTracking, ignoreSub, ignoreName]); + //printErr(JSON.stringify(node).substr(0, 50) + ' : ' + keys(tracked) + ' : ' + [allowTracking, ignoreSub, ignoreName]); var type = node[0]; if (type === 'assign') { var target = node[2]; @@ -2602,6 +3588,8 @@ function eliminate(ast, memSafe) { 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]; @@ -2610,6 +3598,15 @@ function eliminate(ast, memSafe) { for (var j = 0; j < stats.length; j++) { traverseInOrder(stats[j]); } + // We cannot track from one switch case into another, undo all new trackings 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]; + } + } + } } } else { if (!(type in ABORTING_ELIMINATOR_SCAN_NODES)) { @@ -2900,6 +3897,111 @@ function minifyGlobals(ast) { suffix = '// EXTRA_INFO:' + JSON.stringify(minified); } + +function minifyLocals(ast) { + assert(asm) + assert(extraInfo && extraInfo.globals) + + traverseGeneratedFunctions(ast, function(fun, type) { + + // Analyse the asmjs to figure out local variable names, + // but operate on the original source tree so that we don't + // miss any global names in e.g. variable initializers. + var asmData = normalizeAsm(fun); denormalizeAsm(fun, asmData); // TODO: we can avoid modifying at all here - we just need a list of local vars+params + 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) { + return name in asmData.vars || name in asmData.params; + } + 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; + } + } + } + }); + + // 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 predicatable 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. + if (fun[1] in extraInfo.globals) { + fun[1] = extraInfo.globals[fun[1]]; + assert(fun[1]); + } + 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 @@ -3117,8 +4219,8 @@ function aggressiveVariableEliminationInternal(func, asmData) { var name = node[1]; if (name in trivials) { var value = values[name]; - if (!value) throw 'missing value: ' + [func[1], name, values[name]] + ' - faulty reliance on asm zero-init?'; - return copy(value); // must copy, or else the same object can be used multiple times + if (value) return copy(value); // must copy, or else the same object can be used multiple times + else return emptyNode(); } } }); @@ -3866,6 +4968,113 @@ function outline(ast) { }); } +function safeHeap(ast) { + function fixPtr(ptr, heap) { + switch (heap) { + case 'HEAP8': case 'HEAPU8': break; + case 'HEAP16': case 'HEAPU16': { + if (ptr[0] === 'binary') { + assert(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') { + assert(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') { + assert(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') { + assert(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: throw 'bad heap ' + heap; + } + ptr = ['binary', '|', ptr, ['num', 0]]; + return ptr; + } + traverseGenerated(ast, function(node, type) { + if (type === 'assign') { + if (node[1] === true && node[2][0] === 'sub') { + var heap = node[2][1][1]; + var 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], ['num', '0']]]; + } + case 'HEAP16': case 'HEAPU16': { + return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_INT), ['num', 2], ['num', '0']]]; + } + case 'HEAP32': case 'HEAPU32': { + return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_INT), ['num', 4], ['num', '0']]]; + } + case 'HEAPF32': { + return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_DOUBLE), ['num', 4], ['num', '1']]]; + } + case 'HEAPF64': { + return ['call', ['name', 'SAFE_HEAP_STORE'], [ptr, makeAsmCoercion(value, ASM_DOUBLE), ['num', 8], ['num', '1']]]; + } + default: throw 'bad heap ' + heap; + } + } + } else if (type === 'sub') { + var heap = node[1][1]; + if (heap[0] !== 'H') return; + var 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'], ['num', '0']]], ASM_INT); + } + case 'HEAPU8': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 1], ['num', '0'], ['num', '1']]], ASM_INT); + } + case 'HEAP16': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 2], ['num', '0'], ['num', '0']]], ASM_INT); + } + case 'HEAPU16': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 2], ['num', '0'], ['num', '1']]], ASM_INT); + } + case 'HEAP32': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 4], ['num', '0'], ['num', '0']]], ASM_INT); + } + case 'HEAPU32': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 4], ['num', '0'], ['num', '1']]], ASM_INT); + } + case 'HEAPF32': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 4], ['num', '1'], ['num', '0']]], ASM_DOUBLE); + } + case 'HEAPF64': { + return makeAsmCoercion(['call', ['name', 'SAFE_HEAP_LOAD'], [ptr, ['num', 8], ['num', '1'], ['num', '0']]], ASM_DOUBLE); + } + default: throw 'bad heap ' + heap; + } + } + }); +} + // 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) @@ -3956,6 +5165,7 @@ function asmLastOpts(ast) { var minifyWhitespace = false, printMetadata = true, asm = false, last = false; var passes = { + // passes dumpAst: dumpAst, dumpSrc: dumpSrc, unGlobalize: unGlobalize, @@ -3967,12 +5177,17 @@ var passes = { hoistMultiples: hoistMultiples, loopOptimizer: loopOptimizer, registerize: registerize, + registerizeHarder: registerizeHarder, eliminate: eliminate, eliminateMemSafe: eliminateMemSafe, aggressiveVariableElimination: aggressiveVariableElimination, minifyGlobals: minifyGlobals, + minifyLocals: minifyLocals, relocate: relocate, outline: outline, + safeHeap: safeHeap, + + // flags minifyWhitespace: function() { minifyWhitespace = true }, noPrintMetadata: function() { printMetadata = false }, asm: function() { asm = true }, |