diff options
Diffstat (limited to 'tools/js-optimizer.js')
-rw-r--r-- | tools/js-optimizer.js | 532 |
1 files changed, 525 insertions, 7 deletions
diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js index 91064de4..6987511c 100644 --- a/tools/js-optimizer.js +++ b/tools/js-optimizer.js @@ -136,6 +136,9 @@ var CONTROL_FLOW = set('do', 'while', 'for', 'if', 'switch'); var NAME_OR_NUM = set('name', 'num'); var ASSOCIATIVE_BINARIES = set('+', '*', '|', '&', '^'); +var BREAK_CAPTURERS = set('do', 'while', 'for', 'switch'); +var CONTINUE_CAPTURERS = LOOP; + var NULL_NODE = ['name', 'null']; var UNDEFINED_NODE = ['unary-prefix', 'void', ['num', 0]]; var TRUE_NODE = ['unary-prefix', '!', ['num', 0]]; @@ -405,7 +408,7 @@ function removeUnneededLabelSettings(ast) { // Various expression simplifications. Pre run before closure (where we still have metadata), Post run after. var USEFUL_BINARY_OPS = set('<<', '>>', '|', '&', '^'); -var COMPARE_OPS = set('<', '<=', '>', '>=', '==', '===', '!='); +var COMPARE_OPS = set('<', '<=', '>', '>=', '==', '===', '!=', '!=='); function simplifyExpressionsPre(ast) { // Simplify common expressions used to perform integer conversion operations @@ -504,6 +507,8 @@ function simplifyExpressionsPre(ast) { 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' } @@ -554,6 +559,12 @@ function simplifyExpressionsPre(ast) { } } } + } 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) { + 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') { @@ -1523,10 +1534,15 @@ function unVarify(vars, ret) { // transform var x=1, y=2 etc. into (x=1, y=2), i var ASM_INT = 0; var ASM_DOUBLE = 1; -function detectAsmCoercion(node) { +function detectAsmCoercion(node, asmInfo) { // for params, +x vs x|0, for vars, 0.0 vs 0 if (node[0] === 'num' && node[1].toString().indexOf('.') >= 0) return ASM_DOUBLE; - return node[0] === 'unary-prefix' ? ASM_DOUBLE : ASM_INT; + if (node[0] === 'unary-prefix') return ASM_DOUBLE; + if (asmInfo && node[0] == 'name') { + if (node[1] in asmInfo.vars) return asmInfo.vars[node[1]]; + if (node[1] in asmInfo.params) return asmInfo.params[node[1]]; + } + return ASM_INT; } function makeAsmParamCoercion(param, type) { @@ -1537,6 +1553,11 @@ function makeAsmVarDef(v, type) { return [v, type === ASM_INT ? ['num', 0] : ['unary-prefix', '+', ['num', 0]]]; } +function getAsmType(asmInfo, name) { + if (name in asmInfo.vars) return asmInfo.vars[name]; + return asmInfo.params[name]; +} + function normalizeAsm(func) { //printErr('pre-normalize \n\n' + astToSrc(func) + '\n\n'); var data = { @@ -2507,6 +2528,8 @@ function eliminate(ast, memSafe) { 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) { @@ -2786,6 +2809,494 @@ function relocate(ast) { }); } +// 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 outline(ast) { + function measureSize(ast) { + var size = 0; + traverse(ast, function() { + size++; + }); + return size; + } + + function aggressiveVariableElimination(func, asmData) { + // This removes as many variables as possible. This is often not the best thing because it increases + // code size, but it is far preferable to the risk of split functions needing to do more spilling. Specifically, + // it finds 'trivial' variables: ones with 1 definition, and that definition is not sensitive to any changes: it + // only depends on constants and local variables that are themselves trivial. We can unquestionably eliminate + // such variables in a trivial manner. + + var assignments = {}; + var appearances = {}; + var defs = {}; + var considered = {}; + + traverse(func, function(node, type) { + if (type == 'assign' && node[2][0] == 'name') { + var name = node[2][1]; + if (name in asmData.vars) { + assignments[name] = (assignments[name] || 0) + 1; + appearances[name] = (appearances[name] || 0) - 1; // this appearance is a definition, offset the counting later + defs[name] = node; + } else { + if (name in asmData.params) { + considered[name] = true; // this parameter is not ssa, it must be in a hand-optimized function, so it is not trivial + } + } + } else if (type == 'name') { + var name = node[1]; + if (name in asmData.vars) { + appearances[name] = (appearances[name] || 0) + 1; + } + } + }); + + var allTrivials = {}; // key of a trivial var => size of its (expanded) value, at least 1 + + // three levels of variables: + // 1. trivial: 1 def (or less), uses nothing sensitive, can be eliminated + // 2. safe: 1 def (or less), can be used in a trivial, but cannot itself be eliminated + // 3. sensitive: uses a global or memory or something else that prevents trivial elimination. + + function assessTriviality(name) { + // only care about vars with 0-1 assignments of (0 for parameters), and can ignore label (which is not explicitly initialized, but cannot be eliminated ever anyhow) + if (assignments[name] > 1 || (!(name in asmData.vars) && !(name in asmData.params)) || name == 'label') return false; + if (considered[name]) return allTrivials[name]; + considered[name] = true; + var sensitive = false; + var size = 0, originalSize = 0; + var def = defs[name]; + if (def) { + var value = def[3]; + originalSize = measureSize(value); + if (value) { + traverse(value, function recurseValue(node, type) { + var one = node[1]; + if (!(type in NODES_WITHOUT_ELIMINATION_SENSITIVITY)) { // || (type == 'binary' && !(one in FAST_ELIMINATION_BINARIES))) { + sensitive = true; + return true; + } + if (type == 'name' && !assessTriviality(one)) { + if (assignments[one] > 1 || (!(one in asmData.vars) && !(one in asmData.params))) { + sensitive = true; // directly using something sensitive + return true; + } // otherwise, not trivial, but at least safe. + } + // if this is a name, it must be a trivial variable (or a safe one) and we know its size + size += ((type == 'name') ? allTrivials[one] : 1) || 1; + }); + } + } + if (!sensitive) { + size = size || 1; + originalSize = originalSize || 1; + var factor = ((appearances[name] - 1) || 0) * (size - originalSize); // If no size change or just one appearance, always ok to trivially eliminate. otherwise, tradeoff + if (factor <= 12) { + allTrivials[name] = size; // trivial! + return true; + } + } + return false; + } + for (var name in asmData.vars) { + assessTriviality(name); + } + var trivials = {}; + + for (var name in allTrivials) { // from now on, ignore parameters + if (name in asmData.vars) trivials[name] = true; + } + + allTrivials = {}; + + var values = {}; + + function evaluate(name) { + var node = values[name]; + if (node) return node; + values[node] = null; // prevent infinite recursion + var def = defs[name]; + if (def) { + node = def[3]; + if (node[0] == 'name') { + var name2 = node[1]; + if (name2 in trivials) { + node = evaluate(name2); + } + } else { + traverse(node, function(node, type) { + if (type == 'name') { + var name2 = node[1]; + if (name2 in trivials) { + return evaluate(name2); + } + } + }); + } + values[name] = node; + } + return node; + } + + for (var name in trivials) { + evaluate(name); + } + + for (var name in trivials) { + var def = defs[name]; + if (def) { + def.length = 0; + def[0] = 'toplevel'; + def[1] = []; + } + delete asmData.vars[name]; + } + + // Perform replacements TODO: save list of uses objects before, replace directly, avoid extra traverse + traverse(func, function(node, type) { + if (type == 'name') { + 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 + } + } + }); + } + + // Prepares information for spilling of local variables + function analyzeFunction(func, asmData) { + var stack = []; // list of variables, each gets 8 bytes + for (var name in asmData.params) { + stack.push(name); + } + for (var name in asmData.vars) { + stack.push(name); + } + asmData.stackPos = {}; + for (var i = 0; i < stack.length; i++) { + asmData.stackPos[stack[i]] = i*8; + } + // Reserve an extra two spots: one for control flow var, the other for control flow data + asmData.stackSize = (stack.length + 2)*8; + asmData.controlStackPos = asmData.stackSize - 16; + asmData.controlDataStackPos = asmData.stackSize - 8; + asmData.splitCounter = 0; + } + + // Analyze uses - reads and writes - of variables in part of the AST of a function + function analyzeCode(func, asmData, ast) { + var labels = {}; + var labelCounter = 1; // 0 means no label + + traverse(ast, function(node, type) { + if ((type == 'label' || type in LOOP_FLOW) && node[1] && !(node[1] in labels)) { + labels[node[1]] = labelCounter++; + } + }); + + var writes = {}; + var appearances = {}; + var hasReturn = false, hasBreak = false, hasContinue = false; + var breaks = {}; // set of labels we break or continue + var continues = {}; // to. '0' is an unlabeled one + var breakCapturers = 0; + var continueCapturers = 0; + + traverse(ast, function(node, type) { + if (type == 'assign' && node[2][0] == 'name') { + var name = node[2][1]; + if (name in asmData.vars || name in asmData.params) { + writes[name] = 0; + appearances[name] = (appearances[name] || 0) - 1; // this appearance is a definition, offset the counting later + } + } else if (type == 'name') { + var name = node[1]; + if (name in asmData.vars || name in asmData.params) { + appearances[name] = (appearances[name] || 0) + 1; + } + } else if (type == 'return') { + hasReturn = true; + } else if (type == 'break') { + var label = node[1] || 0; + if (!label && breakCapturers > 0) return; // no label, and captured + if (label && (label in labels)) return; // label, and defined in this code, so captured + breaks[label || 0] = 0; + hasBreak = true; + } else if (type == 'continue') { + var label = node[1] || 0; + if (!label && continueCapturers > 0) return; // no label, and captured + if (label && (label in labels)) return; // label, and defined in this code, so captured + continues[label || 0] = 0; + hasContinue = true; + } else { + if (type in BREAK_CAPTURERS) { + breakCapturers++; + } + if (type in CONTINUE_CAPTURERS) { + continueCapturers++; + } + } + }, function(node, type) { + if (type in BREAK_CAPTURERS) { + breakCapturers--; + } + if (type in CONTINUE_CAPTURERS) { + continueCapturers--; + } + }); + + var reads = {}; + + for (var name in appearances) { + if (appearances[name] > 0) reads[name] = 0; + } + + return { writes: writes, reads: reads, hasReturn: hasReturn, breaks: breaks, continues: continues, labels: labels }; + } + + function makeAssign(dst, src) { + return ['assign', true, dst, src]; + } + function makeStackAccess(type, pos) { // TODO: float64, not 32 + return ['sub', ['name', type == ASM_INT ? 'HEAP32' : 'HEAPF32'], ['binary', '>>', ['binary', '+', ['name', 'sp'], ['num', pos]], ['num', '2']]]; + } + function makeIf(cond, then, else_) { + var ret = ['if', cond, ['block', then]]; + if (else_) ret.push(['block', else_]); + return ret; + } + function makeComparison(left, comp, right) { + return ['binary', comp, left, right]; + } + + var CONTROL_BREAK = 1, CONTROL_BREAK_LABEL = 2, CONTROL_CONTINUE = 3, CONTROL_CONTINUE_LABEL = 4, CONTROL_RETURN_VOID = 5, CONTROL_RETURN_INT = 6, CONTROL_RETURN_DOUBLE = 7; + + var sizeToOutline = extraInfo.sizeToOutline; + var level = 0; + + function doOutline(func, asmData, stats, start, end) { + printErr(' do outline ' + [func[1], level, 'range:', start, end, 'of', stats.length]); + var code = stats.slice(start, end+1); + var newIdent = func[1] + '$' + (asmData.splitCounter++); + // add spills and reads before and after the call to the outlined code, and in the outlined code itself + var codeInfo = analyzeCode(func, asmData, code); + var reps = []; + for (var v in codeInfo.reads) { + if (v != 'sp') { + reps.push(['stat', ['assign', true, ['sub', ['name', getAsmType(asmData, v) == ASM_INT ? 'HEAP32' : 'HEAPF32'], ['binary', '>>', ['binary', '+', ['name', 'sp'], ['num', asmData.stackPos[v]]], ['num', '2']]], ['name', v]]]); + code.unshift(['stat', ['assign', true, ['name', v], ['sub', ['name', getAsmType(asmData, v) == ASM_INT ? 'HEAP32' : 'HEAPF32'], ['binary', '>>', ['binary', '+', ['name', 'sp'], ['num', asmData.stackPos[v]]], ['num', '2']]]]]); + } + } + reps.push(['stat', ['call', ['name', newIdent], [['name', 'sp']]]]); + for (var v in codeInfo.writes) { + reps.push(['stat', ['assign', true, ['name', v], ['sub', ['name', getAsmType(asmData, v) == ASM_INT ? 'HEAP32' : 'HEAPF32'], ['binary', '>>', ['binary', '+', ['name', 'sp'], ['num', asmData.stackPos[v]]], ['num', '2']]]]]); + code.push(['stat', ['assign', true, ['sub', ['name', getAsmType(asmData, v) == ASM_INT ? 'HEAP32' : 'HEAPF32'], ['binary', '>>', ['binary', '+', ['name', 'sp'], ['num', asmData.stackPos[v]]], ['num', '2']]], ['name', v]]]); + } + // Generate new function + if (codeInfo.hasReturn || codeInfo.hasBreak || codeInfo.hasContinue) { + // we need to capture all control flow using a top-level labeled one-time loop in the outlined function + code = [['label', 'OL', ['do', ['num', 0], ['block', code]]]]; + var breakCapturers = 0; + var continueCapturers = 0; + traverse(code, function(node, type) { + // replace all break/continue/returns with code to break out of the main one-time loop, and set the control data + if (type == 'return') { + var ret = ['break', 'OL']; + if (!node[1]) { + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlStackPos), ['num', CONTROL_RETURN_VOID]), ret]; + } else { + var type = detectAsmCoercion(node[1], asmData); + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlStackPos), ['num', type == ASM_INT ? CONTROL_RETURN_INT : CONTROL_RETURN_DOUBLE]), ret]; + ret = ['seq', makeAssign(makeStackAccess(type, asmData.controlDataStackPos), node[1]), ret]; + } + return ret; + } else if (type == 'break') { + var label = node[1] || 0; + if (label == 'OL') return; // this was just added before us, it is new replacement code + if (!label && breakCapturers > 0) return; // no label, and captured + if (label && (label in codeInfo.labels)) return; // label, and defined in this code, so captured + var ret = ['break', 'OL']; + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlStackPos), ['num', label ? CONTROL_BREAK_LABEL : CONTROL_BREAK]), ret]; + if (label) { + assert(label in codeInfo.labels, label + ' in ' + keys(codeInfo.labels)); + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlDataStackPos), ['num', codeInfo.labels[label]]), ret]; + } + return ret; + } else if (type == 'continue') { + var label = node[1] || 0; + if (!label && continueCapturers > 0) return; // no label, and captured + if (label && (label in codeInfo.labels)) return; // label, and defined in this code, so captured + var ret = ['break', 'OL']; + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlStackPos), ['num', label ? CONTROL_CONTINUE_LABEL : CONTROL_CONTINUE]), ret]; + if (label) { + ret = ['seq', makeAssign(makeStackAccess(ASM_INT, asmData.controlDataStackPos), ['num', codeInfo.labels[label]]), ret]; + } + return ret; + } else { + if (type in BREAK_CAPTURERS) { + breakCapturers++; + } + if (type in CONTINUE_CAPTURERS) { + continueCapturers++; + } + } + }, function(node, type) { + if (type in BREAK_CAPTURERS) { + breakCapturers--; + } + if (type in CONTINUE_CAPTURERS) { + continueCapturers--; + } + }); + // read the control data at the callsite to the outlined function + if (codeInfo.hasReturn) { + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_RETURN_VOID]), + [['stat', ['return']]] + )); + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_RETURN_INT]), + [['stat', ['return', makeStackAccess(ASM_INT, asmData.controlDataStackPos)]]] + )); + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_RETURN_DOUBLE]), + [['stat', ['return', makeStackAccess(ASM_DOUBLE, asmData.controlDataStackPos)]]] + )); + } + if (codeInfo.hasBreak) { + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_BREAK]), + ['stat', ['break']] + )); + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_BREAK_LABEL]), + ['stat', ['break', makeStackAccess(ASM_INT, asmData.controlDataStackPos)]] // XXX here and below, need a switch overall possible labels + )); + } + if (codeInfo.hasContinue) { + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_CONTINUE]), + ['stat', ['break']] + )); + reps.push(makeIf( + makeComparison(makeStackAccess(ASM_INT, asmData.controlStackPos), '==', ['num', CONTROL_CONTINUE_LABEL]), + ['stat', ['continue', makeStackAccess(ASM_INT, asmData.controlDataStackPos)]] // XXX + )); + } + } + var newFunc = ['defun', newIdent, ['sp'], code]; + var newAsmData = { params: { sp: ASM_INT }, vars: {} }; + for (var v in codeInfo.reads) { + newAsmData.vars[v] = getAsmType(asmData, v); + } + for (var v in codeInfo.writes) { + newAsmData.vars[v] = getAsmType(asmData, v); + } + denormalizeAsm(newFunc, newAsmData); + // replace in stats + stats.splice.apply(stats, [start, end-start+1].concat(reps)); + return [newFunc]; + } + + function outlineStatements(func, asmData, stats, maxSize) { + level++; + if (measureSize(stats) < sizeToOutline) return; + var ret = []; + var sizeSeen = 0; + var end = stats.length-1; + var i = stats.length; + while (--i >= 0) { + var stat = stats[i]; + var size = measureSize(stat); + //printErr(level + ' size ' + [i, size]); + if (size >= sizeToOutline) { + // this by itself is big enough to inline, recurse into it and find statements to split on + var subStatements = null; + traverse(stat, function(node, type) { + if (type == 'block') { + if (measureSize(node) >= sizeToOutline) { + var subRet = outlineStatements(func, asmData, node[1], maxSize); + if (subRet && subRet.length > 0) ret.push.apply(ret, subRet); + } + return null; // do not recurse into children, outlineStatements will do so if necessary + } + }); + sizeSeen = 0; + continue; + } + sizeSeen += size; + // If this is big enough to outline, but no too big (if very close to the size of the full function, + // outlining is pointless; remove stats from the end to try to achieve the good case), then outline. + // Also, try to reduce the size if it is much larger than the hoped-for size + while ((sizeSeen > maxSize || sizeSeen > 2*sizeToOutline) && i < end) { + sizeSeen -= measureSize(stats[end]); + if (sizeSeen >= sizeToOutline) { + end--; + } else { + sizeSeen += measureSize(stats[end]); // abort, this took away too much + break; + } + } + if (sizeSeen >= sizeToOutline && sizeSeen <= maxSize) { + ret.push.apply(ret, doOutline(func, asmData, stats, i, end)); // outline [i, .. ,end] inclusive + sizeSeen = 0; + end = i-1; + } + } + level--; + return ret; + } + + // + + if (ast[0] !== 'toplevel') { + assert(ast[0] == 'defun'); + ast = ['toplevel', [ast]]; + } + + var funcs = ast[1]; + + var more = true; + while (more) { + more = false; + + var newFuncs = []; + + funcs.forEach(function(func) { + var asmData = normalizeAsm(func); + var size = measureSize(func); + if (size >= sizeToOutline) { + aggressiveVariableElimination(func, asmData); + analyzeFunction(func, asmData); + var ret = outlineStatements(func, asmData, getStatements(func), 0.5*size); + if (ret && ret.length > 0) newFuncs.push.apply(newFuncs, ret); + } + denormalizeAsm(func, asmData); + }); + + // TODO: control flow: route returns and breaks. outlined code should have all breaks/continues/returns break into the outermost scope, + // after setting a state variable, etc. + + if (newFuncs.length > 0) { + // We have outlined. Add stack support: header in which we allocate enough stack space TODO + // If sp was not present before, add it and before each return, pop the stack. also a final pop if not ending with a return TODO + // (none of this should be done in inner functions, of course, just the original) + + // add new functions to the toplevel, or create a toplevel if there isn't one + ast[1].push.apply(ast[1], newFuncs); + + funcs = newFuncs; + more = true; + } + } +} + // 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) @@ -2811,12 +3322,13 @@ function fixDotZero(js) { }); } -function asmLoopOptimizer(ast) { +function asmLastOpts(ast) { traverseGeneratedFunctions(ast, function(fun) { - // 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 traverse(fun, function(node, type) { if (type === 'while' && node[1][0] === 'num' && node[1][1] === 1 && node[2][0] === 'block') { + // 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]; @@ -2844,6 +3356,11 @@ function asmLoopOptimizer(ast) { node[1] = simplifyNotCompsDirect(['unary-prefix', '!', conditionToBreak]); return node; } + } else if (type == 'binary' && node[1] == '&' && 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; } }); }); @@ -2870,6 +3387,7 @@ var passes = { eliminateMemSafe: eliminateMemSafe, minifyGlobals: minifyGlobals, relocate: relocate, + outline: outline, minifyWhitespace: function() { minifyWhitespace = true }, noPrintMetadata: function() { printMetadata = false }, asm: function() { asm = true }, @@ -2901,7 +3419,7 @@ arguments_.slice(1).forEach(function(arg) { passes[arg](ast); }); if (asm && last) { - asmLoopOptimizer(ast); // TODO: move out of last, to make last faster when done later (as in side modules) + asmLastOpts(ast); // TODO: move out of last, to make last faster when done later (as in side modules) prepDotZero(ast); } var js = astToSrc(ast, minifyWhitespace), old; |