diff options
-rw-r--r-- | settings.py | 1 | ||||
-rw-r--r-- | tests/runner.py | 6 | ||||
-rw-r--r-- | tools/js-optimizer.js | 152 | ||||
-rw-r--r-- | tools/shared.py | 1 | ||||
-rw-r--r-- | tools/test-js-optimizer-output.js | 33 | ||||
-rw-r--r-- | tools/test-js-optimizer.js | 34 |
6 files changed, 227 insertions, 0 deletions
diff --git a/settings.py b/settings.py index 82c84285..f4483921 100644 --- a/settings.py +++ b/settings.py @@ -22,4 +22,5 @@ TIMEOUT = None # Tools CLOSURE_COMPILER = os.path.expanduser('~/Dev/closure-compiler/compiler.jar') +NODE_JS = 'node' diff --git a/tests/runner.py b/tests/runner.py index d02ce36b..68581409 100644 --- a/tests/runner.py +++ b/tests/runner.py @@ -4460,6 +4460,12 @@ TT = %s output = Popen([COFFEESCRIPT, VARIABLE_ELIMINATOR], stdin=PIPE, stdout=PIPE).communicate(input)[0] self.assertIdentical(expected, output) + def test_js_optimizer(self): + input = open(path_from_root('tools', 'test-js-optimizer.js')).read() + expected = open(path_from_root('tools', 'test-js-optimizer-output.js')).read() + output = Popen([NODE_JS, JS_OPTIMIZER], stdin=PIPE, stdout=PIPE).communicate(input)[0] + self.assertIdentical(expected, output.replace('\n\n', '\n')) + else: # Benchmarks. Run them with argument |benchmark|. To run a specific test, do # |benchmark.test_X|. diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js new file mode 100644 index 00000000..f8e65f55 --- /dev/null +++ b/tools/js-optimizer.js @@ -0,0 +1,152 @@ +//============================================================================== +// 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. Some of the modifications also work in +// conjunction with closure compiler. +//============================================================================== + +var uglify = require('../tools/eliminator/node_modules/uglify-js'); +var fs = require('fs'); + +// Make node environment compatible with JS shells + +function print(text) { + process.stdout.write(text + '\n'); +} +function read(filename) { + if (filename[0] != '/') filename = __dirname.split('/').slice(0, -1).join('/') + '/src/' + filename; + return fs.readFileSync(filename).toString(); +} + +// Load some modules + +eval(read('utility.js')); + +// Utilities + +var FUNCTION = set('defun', 'function'); + +// 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. +// @arg post: A callback to call after traversing all children. +// @arg stack: If true, a stack will be implemented: If pre does not push on +// the stack, we push a null. We pop when we leave the node. The +// stack is passed as a third parameter to the callbacks. +// @returns: If the root node was replaced, the new root node. If the traversal +// was stopped, true. Otherwise undefined. +function traverse(node, pre, post, stack) { + var type = node[0], result, len; + var relevant = typeof type == 'string'; + if (relevant) { + if (stack) len = stack.length; + var result = pre(node, type, stack); + if (result == true) return true; + if (stack && len == stack.length) stack.push(null); + } + for (var i = 0; i < node.length; i++) { + var subnode = node[i]; + if (typeof subnode == 'object' && subnode && subnode.length) { + var subresult = traverse(subnode, pre, post, stack); + if (subresult == true) return true; + if (typeof subresult == 'object') node[i] = subresult; + } + } + if (relevant) { + if (post) { + var postResult = post(node, type, stack); + result = result || postResult; + } + if (stack) stack.pop(); + } + return result; +} + +function makeEmptyNode() { + return ['toplevel', []] +} + +// Passes + +// Undos closure's creation of global variables with values true, false, +// undefined, null. These cut down on size, but do not affect gzip size +// and make JS engine's lives slightly harder (?) +function unGlobalize(ast) { + assert(ast[0] == 'toplevel'); + var values = {}; + // Find global renamings of the relevant values + ast[1].forEach(function(node, i) { + if (node[0] != 'var') return; + node[1] = node[1].filter(function(varItem) { + var key = varItem[0]; + var value = varItem[1]; + if (!value) return true; + if (value[0] == 'name' && value[1] == 'null') { + values[key] = ['name','null']; + return false; + } else if (value[0] == 'unary-prefix' && value[1] == 'void' && value[2] && value[2].length == 2 && value[2][0] == 'num' && value[2][1] == 0){ + values[key] = ['unary-prefix','void',['num',0]]; + return false; + } else if (value[0] == 'unary-prefix' && value[1] == '!' && value[2] && value[2].length == 2 && value[2][0] == 'num' && value[2][1] == 0){ + values[key] = ['unary-prefix','!',['num',0]]; + return false; + } else if (value[0] == 'unary-prefix' && value[1] == '!' && value[2] && value[2].length == 2 && value[2][0] == 'num' && value[2][1] == 1){ + values[key] = ['unary-prefix','!',['num',1]]; + return false; + } + return true; + }); + if (node[1].length == 0) { + ast[1][i] = makeEmptyNode(); + } + }); + // Walk the ast, with an understanding of JS variable scope (i.e., function-level scoping) + traverse(ast, function(node, type, stack) { + if (type in FUNCTION) { + stack.push({ type: 'function', vars: node[2] }); + } else if (type == 'var') { + // Find our function, add our vars + var func = stack[stack.length-1]; + if (func) { + func.vars = func.vars.concat(node[1].map(function(varItem) { return varItem[0] })); + } + } + }, function(node, type, stack) { + if (type in FUNCTION) { + // We know all of the variables that are seen here, proceed to do relevant replacements + var allVars = stack.map(function(item) { return item ? item.vars : [] }).reduce(concatenator, []); // FIXME dictionary for speed? + traverse(node, function(node, type, stack) { + // Be careful not to look into our inner functions. They have already been processed. + if (type == 'name' && sum(stack) == 1) { + var ident = node[1]; + if (ident in values && allVars.indexOf(ident) < 0) { + return copy(values[ident]); + } + } else if (type in FUNCTION) { + stack.push(1); + } else { + stack.push(0); + } + }, null, []); + } + }, []); +} + +// Main + +var src = fs.readFileSync('/dev/stdin').toString(); + +var ast = uglify.parser.parse(src); + +unGlobalize(ast); + +print(uglify.uglify.gen_code(ast, { + ascii_only: true, + beautify: true, + indent_level: 2 +})); + diff --git a/tools/shared.py b/tools/shared.py index 065c71b8..0a4219d8 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -32,6 +32,7 @@ DFE = path_from_root('tools', 'dead_function_eliminator.py') BINDINGS_GENERATOR = path_from_root('tools', 'bindings_generator.py') EXEC_LLVM = path_from_root('tools', 'exec_llvm.py') VARIABLE_ELIMINATOR = path_from_root('tools', 'eliminator', 'eliminator.coffee') +JS_OPTIMIZER = path_from_root('tools', 'js-optimizer.js') # Additional compiler options diff --git a/tools/test-js-optimizer-output.js b/tools/test-js-optimizer-output.js new file mode 100644 index 00000000..c50adde2 --- /dev/null +++ b/tools/test-js-optimizer-output.js @@ -0,0 +1,33 @@ +var c; +var b = 5; + +function abc() { + var cheez = void 0; + var fleefl; + cheez = 10; + fleefl = void 0; + var waka = void 0, flake = void 0, marfoosh = void 0; + var waka2 = 5, flake2 = void 0, marfoosh2 = void 0; + var waka3 = void 0, flake3 = 5, marfoosh3 = void 0; + var waka4 = void 0, flake4 = void 0, marfoosh4 = 5; + var test1 = !0, test2 = !1, test3 = null, test4 = z, test5 = b, test6 = void 0; +} +function xyz() { + var x = 52; + var cheez = x; + var t = 22; + function inner1(c) { + var i = x, j = null, k = 5, l = t, m = s2; + var s2 = 8; + } + var inner2 = (function(d) { + var i = x, j = null, k = 5, l = t, m = s2; + var s2 = 8; + }); +} +function xyz2(x) { + var cheez = x; +} +zzz = (function(nada) { + var cheez = void 0; +}); diff --git a/tools/test-js-optimizer.js b/tools/test-js-optimizer.js new file mode 100644 index 00000000..14306740 --- /dev/null +++ b/tools/test-js-optimizer.js @@ -0,0 +1,34 @@ +var c; +var b = 5, x = void 0, y = null, t = !0; +var s = !1, s2 = !1; +function abc() { + var cheez = x; + var fleefl; + cheez = 10; + fleefl = x; + var waka = x, flake = x, marfoosh = x; + var waka2 = 5, flake2 = x, marfoosh2 = x; + var waka3 = x, flake3 = 5, marfoosh3 = x; + var waka4 = x, flake4 = x, marfoosh4 = 5; + var test1 = t, test2 = s, test3 = y, test4 = z, test5 = b, test6 = x; +} +function xyz() { + var x = 52; + var cheez = x; + var t = 22; + function inner1(c) { + var i = x, j = y, k = 5, l = t, m = s2; + var s2 = 8; + } + var inner2 = function(d) { + var i = x, j = y, k = 5, l = t, m = s2; + var s2 = 8; + }; +} +function xyz2(x) { + var cheez = x; +} +zzz = function(nada) { + var cheez = x; +}; + |