From 924aa580602a4ad94f1079dcd157286314066553 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Fri, 14 Sep 2012 15:36:38 +0300 Subject: [PATCH] more optimizations that v1 does and some cleanups - a = a + x ==> a+=x - joining consecutive var statements (hoisting is not always desirable) - x == false ==> x == 0, x != true ==> x != 1 - x, x ==> x; x = exp(), x ==> x = exp() - discarding useless break-s --- bin/uglifyjs2 | 10 +- lib/ast.js | 53 +++++++++-- lib/compress.js | 245 ++++++++++++++++++++++++++++++++++++------------ lib/output.js | 13 +-- lib/parse.js | 4 +- tmp/todo | 10 +- 6 files changed, 256 insertions(+), 79 deletions(-) diff --git a/bin/uglifyjs2 b/bin/uglifyjs2 index fd57a662..e753631c 100755 --- a/bin/uglifyjs2 +++ b/bin/uglifyjs2 @@ -18,18 +18,20 @@ Use a single dash to read input from the standard input.\ .describe("v", "Verbose") .describe("b", "Beautify output") .describe("m", "Don't mangle names") - .describe("c", "Compressor options") + .describe("c", "Pass compressor options") .alias("p", "prefix") .alias("o", "output") .alias("v", "verbose") .alias("b", "beautify") .alias("c", "options") + .alias("m", "no-mangle") .boolean("b") .boolean("v") .boolean("stats") .boolean("m") + .string("c") .argv ; @@ -121,9 +123,9 @@ if (ARGS.stats) { /* -----[ functions ]----- */ function do_file_1(file) { - // if (ARGS.v) { - // sys.error("Compressing " + file); - // } + if (ARGS.v) { + sys.error("Compressing " + file); + } var code = read_whole_file(file); var ast; time_it("parse", function(){ diff --git a/lib/ast.js b/lib/ast.js index 5187b5ad..1a792861 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -70,7 +70,11 @@ function DEFNODE(type, props, methods, base) { ctor.prototype.TYPE = ctor.TYPE = type; } if (methods) for (i in methods) if (HOP(methods, i)) { - ctor.prototype[i] = methods[i]; + if (/^\$/.test(i)) { + ctor[i.substr(1)] = methods[i]; + } else { + ctor.prototype[i] = methods[i]; + } } ctor.DEFMETHOD = function(name, method) { this.prototype[name] = method; @@ -429,12 +433,45 @@ var AST_New = DEFNODE("New", null, { $documentation: "An object instantiation. Derives from a function call since it has exactly the same properties." }, AST_Call); -var AST_Seq = DEFNODE("Seq", "first second", { +var AST_Seq = DEFNODE("Seq", "car cdr", { $documentation: "A sequence expression (two comma-separated expressions)", + $cons: function(x, y) { + var seq = new AST_Seq(x); + seq.car = x; + seq.cdr = y; + return seq; + }, + $from_array: function(array) { + if (array.length == 0) return null; + if (array.length == 1) return array[0].clone(); + var list = null; + for (var i = array.length; --i >= 0;) { + list = AST_Seq.cons(array[i], list); + } + var p = list; + while (p) { + if (p.cdr && !p.cdr.cdr) { + p.cdr = p.cdr.car; + break; + } + p = p.cdr; + } + return list; + }, + add: function(node) { + var p = this; + while (p) { + if (!(p.cdr instanceof AST_Seq)) { + var cell = AST_Seq.cons(p.cdr, node); + return p.cdr = cell; + } + p = p.cdr; + } + }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.first._walk(visitor); - this.second._walk(visitor); + this.car._walk(visitor); + if (this.cdr) this.cdr._walk(visitor); }); } }); @@ -629,15 +666,19 @@ var AST_Undefined = DEFNODE("Undefined", null, { value: (function(){}()) }, AST_Atom); +var AST_Boolean = DEFNODE("Boolean", null, { + $documentation: "Base class for booleans", +}, AST_Atom); + var AST_False = DEFNODE("False", null, { $documentation: "The `false` atom", value: false -}, AST_Atom); +}, AST_Boolean); var AST_True = DEFNODE("True", null, { $documentation: "The `true` atom", value: true -}, AST_Atom); +}, AST_Boolean); /* -----[ TreeWalker ]----- */ diff --git a/lib/compress.js b/lib/compress.js index 15eab988..52dc0113 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -68,6 +68,8 @@ function Compressor(options, false_by_default) { hoist_funs : !false_by_default, hoist_vars : !false_by_default, if_return : !false_by_default, + join_vars : !false_by_default, + cascade : !false_by_default, warnings : true }); @@ -116,6 +118,11 @@ function Compressor(options, false_by_default) { return this; }); + AST_Node.DEFMETHOD("equivalent_to", function(node){ + // XXX: this is a rather expensive way to test two node's equivalence: + return this.print_to_string() == node.print_to_string(); + }); + function make_node(ctor, orig, props) { if (!props) props = {}; if (!props.start) props.start = orig.start; @@ -171,6 +178,9 @@ function Compressor(options, false_by_default) { if (compressor.option("if_return")) { statements = handle_if_return(statements, compressor); } + if (compressor.option("join_vars")) { + statements = join_consecutive_vars(statements, compressor); + } } while (CHANGED); return statements; @@ -210,11 +220,11 @@ function Compressor(options, false_by_default) { } } if (stat instanceof AST_If - && stat.body instanceof AST_Return + && stat.body instanceof AST_Exit && stat.body.value && !stat.alternative && i < last - && statements[i + 1] instanceof AST_Return + && statements[i + 1].TYPE == stat.body.TYPE && statements[i + 1].value) { CHANGED = true; return MAP.last(make_node(AST_If, stat, { @@ -223,6 +233,27 @@ function Compressor(options, false_by_default) { alternative: statements[i + 1] }).optimize(compressor)); } + if (stat instanceof AST_If + && stat.alternative instanceof AST_Exit) { + CHANGED = true; + return MAP.last(MAP.splice([ + // 1. negate IF and put the exit in the consequent + make_node(AST_If, stat, { + condition: stat.condition.negate(compressor), + body: stat.alternative + }), + // 2. the IF body can now go outside the if + stat.body + // and 3. add the rest of the statements + ].concat(statements.slice(i + 1)))); + return MAP.last(make_node(AST_If, stat, { + condition: stat.condition.negate(compressor), + body: stat.alternative, + alternative: make_node(AST_BlockStatement, stat.body, { + body: [ stat.body ].concat(statements.slice(i + 1)) + }) + }).optimize(compressor)); + } return stat; }); }; @@ -242,46 +273,63 @@ function Compressor(options, false_by_default) { }, []); }; - // XXX: this is destructive -- it modifies tree nodes. - function sequencesize(statements) { - var prev = null, last = statements.length - 1; - if (last) statements = statements.reduce(function(a, cur, i){ - if (prev instanceof AST_SimpleStatement - && cur instanceof AST_SimpleStatement) { - CHANGED = true; - var seq = make_node(AST_Seq, prev, { - first: prev.body, - second: cur.body - }); - prev.body = seq; - } - else if (i == last - && cur instanceof AST_Exit && cur.value - && a.length > 0 - && prev instanceof AST_SimpleStatement) { - CHANGED = true; - var seq = make_node(AST_Seq, prev, { - first: prev.body, - second: cur.value - }); - cur.value = seq; - a.pop(); - a.push(cur); - return a; + function sequencesize(statements, compressor) { + if (statements.length < 2) return statements; + var seq = [], ret = []; + function push_seq() { + seq = AST_Seq.from_array(seq); + if (seq) ret.push(make_node(AST_SimpleStatement, seq, { + body: seq + })); + seq = []; + }; + statements.forEach(function(stat){ + if (stat instanceof AST_SimpleStatement) seq.push(stat.body); + else push_seq(), ret.push(stat); + }); + push_seq(); + + // if the last node is return or throw, we can mix in the + // previous sequence which might help reducing the list to + // a single statement. + var exit = ret[ret.length - 1], prev = ret[ret.length - 2]; + if (prev instanceof AST_SimpleStatement + && exit instanceof AST_Exit && exit.value) { + ret.pop(); + ret.pop(); + if (prev.body instanceof AST_Seq) { + prev.body.add(exit.value); + prev.body = prev.body.optimize(compressor); + exit.value = prev.body; } else { - a.push(cur); - prev = cur; + exit.value = AST_Seq.cons(prev.body, exit.value).optimize(compressor); + } + ret.push(exit); + } + + CHANGED = ret.length != statements.length; + return ret; + }; + + function join_consecutive_vars(statements, compressor) { + var prev = null; + return statements.reduce(function(a, stat){ + if (stat instanceof AST_Definitions && prev && prev.TYPE == stat.TYPE) { + prev.definitions = prev.definitions.concat(stat.definitions); + CHANGED = true; + } else { + prev = stat; + a.push(stat); } return a; }, []); - return statements; }; }; function extract_declarations_from_unreachable_code(compressor, stat, target) { - warn_dead_code(stat); + compressor.warn("Dropping unreachable code [{line},{col}]", stat.start); stat.walk(new TreeWalker(function(node){ if (node instanceof AST_Definitions) { compressor.warn("Declarations in unreachable code! [{line},{col}]", node.start); @@ -322,7 +370,7 @@ function Compressor(options, false_by_default) { return this.operator == "=" && this.right.is_boolean(); }); def(AST_Seq, function(){ - return this.second.is_boolean(); + return this.cdr.is_boolean(); }); def(AST_True, function(){ return true }); def(AST_False, function(){ return true }); @@ -481,7 +529,7 @@ function Compressor(options, false_by_default) { }); def(AST_Seq, function(compressor){ var self = this.clone(); - self.second = self.second.negate(compressor); + self.cdr = self.cdr.negate(compressor); return self; }); def(AST_Conditional, function(compressor){ @@ -550,6 +598,21 @@ function Compressor(options, false_by_default) { || this.operator == "--"; }); def(AST_SymbolRef, function(){ return false }); + def(AST_Object, function(){ + for (var i = this.properties.length; --i >= 0;) + if (this.properties[i].has_side_effects()) + return true; + return false; + }); + def(AST_ObjectProperty, function(){ + return this.value.has_side_effects(); + }); + def(AST_Array, function(){ + for (var i = this.elements.length; --i >= 0;) + if (this.elements[i].has_side_effects()) + return true; + return false; + }); })(function(node, func){ node.DEFMETHOD("has_side_effects", func); }); @@ -634,7 +697,7 @@ function Compressor(options, false_by_default) { } }); self.walk(tw); - if (vars_found > 0 && vardecl.length > 1) { + if (vars_found > 0 && vardecl.length > 0) { vardecl.forEach(function(v){ v.hoisted = true }); var node = make_node(AST_Var, self, { definitions: Object.keys(vars).map(function(name){ @@ -657,7 +720,7 @@ function Compressor(options, false_by_default) { AST_SimpleStatement.DEFMETHOD("optimize", function(compressor){ if (!this.body.has_side_effects()) { - AST_Node.warn("Dropping side-effect-free statement [{line},{col}]", this.start); + compressor.warn("Dropping side-effect-free statement [{line},{col}]", this.start); return make_node(AST_EmptyStatement, this); } return this; @@ -674,10 +737,6 @@ function Compressor(options, false_by_default) { return self.optimize(compressor); }); - function warn_dead_code(node) { - AST_Node.warn("Dropping unreachable code [{line},{col}]", node.start); - }; - AST_DWLoop.DEFMETHOD("optimize", function(compressor){ var self = this; var cond = self.condition.evaluate(compressor); @@ -798,7 +857,7 @@ function Compressor(options, false_by_default) { self.condition = cond[0]; if (cond.length == 2) { if (cond[1]) { - AST_Node.warn("Condition always true [{line},{col}]", self.condition.start); + compressor.warn("Condition always true [{line},{col}]", self.condition.start); if (compressor.option("dead_code")) { var a = []; if (self.alternative) { @@ -808,7 +867,7 @@ function Compressor(options, false_by_default) { return make_node(AST_BlockStatement, self, { body: a }); } } else { - AST_Node.warn("Condition always false [{line},{col}]", self.condition.start); + compressor.warn("Condition always false [{line},{col}]", self.condition.start); if (compressor.option("dead_code")) { var a = []; extract_declarations_from_unreachable_code(compressor, self.body, a); @@ -909,7 +968,17 @@ function Compressor(options, false_by_default) { self = self.clone(); self.expression = self.expression.squeeze(compressor); self.body = self.body.squeeze(compressor); - return self; + return self.optimize(compressor); + }); + + AST_Switch.DEFMETHOD("optimize", function(compressor){ + var last_branch = this.body.body[this.body.body.length - 1]; + if (last_branch) { + var stat = last_branch.body[last_branch.body.length - 1]; // last statement + if (stat instanceof AST_Break && !stat.label) + last_branch.body.pop(); + } + return this; }); SQUEEZE(AST_Case, function(self, compressor){ @@ -952,8 +1021,8 @@ function Compressor(options, false_by_default) { var first = list[0]; if (list.length == 1) return first; return make_node(AST_Seq, first, { - first: first, - second: seq(list.slice(1)) + car: first, + cdr: seq(list.slice(1)) }); })(assignments); }); @@ -1007,7 +1076,7 @@ function Compressor(options, false_by_default) { if (compressor.option("unused_func")) { if (this.name.unreferenced() && !(this.parent_scope instanceof AST_Toplevel)) { - AST_Node.warn("Dropping unused function {name} [{line},{col}]", { + compressor.warn("Dropping unused function {name} [{line},{col}]", { name: this.name.name, line: this.start.line, col: this.start.col @@ -1077,12 +1146,32 @@ function Compressor(options, false_by_default) { } } } + return this; }); SQUEEZE(AST_Seq, function(self, compressor){ self = self.clone(); - self.first = self.first.squeeze(compressor); - self.second = self.second.squeeze(compressor); + self.car = self.car.squeeze(compressor); + self.cdr = self.cdr.squeeze(compressor); + return self.optimize(compressor); + }); + + AST_Seq.DEFMETHOD("optimize", function(compressor){ + var self = this; + if (self.cdr instanceof AST_Seq) + self.cdr = self.cdr.optimize(compressor); + if (compressor.option("cascade")) { + if (self.car instanceof AST_Assign + && !self.car.left.has_side_effects() + && self.car.left.equivalent_to(self.cdr)) { + return self.car; + } + if (!self.car.has_side_effects() + && !self.cdr.has_side_effects() + && self.car.equivalent_to(self.cdr)) { + return self.car; + } + } return self; }); @@ -1131,7 +1220,7 @@ function Compressor(options, false_by_default) { case "typeof": // typeof always returns a non-empty string, thus it's // always true in booleans - AST_Node.warn("Boolean expression always true [{line},{col}]", this.start); + compressor.warn("Boolean expression always true [{line},{col}]", this.start); return make_node(AST_True, this).optimize(compressor); } } @@ -1160,7 +1249,7 @@ function Compressor(options, false_by_default) { var ll = this.left.evaluate(compressor), left = ll[0]; var rr = this.right.evaluate(compressor), right = rr[0]; if ((ll.length == 2 && !ll[1]) || (rr.length == 2 && !rr[1])) { - AST_Node.warn("Boolean && always false [{line},{col}]", this.start); + compressor.warn("Boolean && always false [{line},{col}]", this.start); return make_node(AST_False, this).optimize(compressor); } if (ll.length == 2 && ll[1]) { @@ -1174,7 +1263,7 @@ function Compressor(options, false_by_default) { var ll = this.left.evaluate(compressor), left = ll[0]; var rr = this.right.evaluate(compressor), right = rr[0]; if ((ll.length == 2 && ll[1]) || (rr.length == 2 && rr[1])) { - AST_Node.warn("Boolean || always true [{line},{col}]", this.start); + compressor.warn("Boolean || always true [{line},{col}]", this.start); return make_node(AST_True, this).optimize(compressor); } if (ll.length == 2 && !ll[1]) { @@ -1189,7 +1278,7 @@ function Compressor(options, false_by_default) { var rr = this.right.evaluate(compressor), right = rr[0]; if ((ll.length == 2 && ll[0] instanceof AST_String && ll[1]) || (rr.length == 2 && rr[0] instanceof AST_String && rr[1])) { - AST_Node.warn("+ in boolean context always true [{line},{col}]", this.start); + compressor.warn("+ in boolean context always true [{line},{col}]", this.start); return make_node(AST_True, this).optimize(compressor); } break; @@ -1207,7 +1296,34 @@ function Compressor(options, false_by_default) { self.left = self.right; self.right = tmp; }; - if (self instanceof AST_Binary) switch (self.operator) { + switch (self.operator) { + case "==": + case "!=": + var ll = self.left.evaluate(compressor); + var rr = self.right.evaluate(compressor); + if (ll.length == 2 && typeof ll[1] == "boolean") { + compressor.warn("Non-strict equality against boolean: {operator} {value} [{line},{col}]", { + operator : self.operator, + value : ll[1], + line : self.start.line, + col : self.start.col + }); + self.left = make_node(AST_Number, self.left, { + value: +ll[1] + }); + } + if (rr.length == 2 && typeof rr[1] == "boolean") { + compressor.warn("Non-strict equality against boolean {operator} {value} [{line},{col}]", { + operator : self.operator, + value : rr[1], + line : self.start.line, + col : self.start.col + }); + self.right = make_node(AST_Number, self.right, { + value: +rr[1] + }); + } + break; case "<": reverse(">"); break; case "<=": reverse(">="); break; } @@ -1219,7 +1335,21 @@ function Compressor(options, false_by_default) { self = self.clone(); self.left = self.left.squeeze(compressor); self.right = self.right.squeeze(compressor); - return self; + return self.optimize(compressor); + }); + + var ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; + AST_Assign.DEFMETHOD("optimize", function(compressor){ + if (this.operator == "=" + && this.left instanceof AST_SymbolRef + && this.right instanceof AST_Binary + && this.right.left instanceof AST_SymbolRef + && this.right.left.name == this.left.name + && member(this.right.operator, ASSIGN_OPS)) { + this.operator = this.right.operator + "="; + this.right = this.right.right; + } + return this; }); SQUEEZE(AST_Conditional, function(self, compressor){ @@ -1236,10 +1366,10 @@ function Compressor(options, false_by_default) { var cond = self.condition.evaluate(compressor); if (cond.length == 2) { if (cond[1]) { - AST_Node.warn("Condition always true [{line},{col}]", self.start); + compressor.warn("Condition always true [{line},{col}]", self.start); return self.consequent; } else { - AST_Node.warn("Condition always false [{line},{col}]", self.start); + compressor.warn("Condition always false [{line},{col}]", self.start); return self.alternative; } } @@ -1256,8 +1386,7 @@ function Compressor(options, false_by_default) { if (consequent instanceof AST_Assign && alternative instanceof AST_Assign && consequent.operator == alternative.operator - // XXX: this is a rather expensive way to test two node's equivalence: - && consequent.left.print_to_string() == alternative.left.print_to_string() + && consequent.left.equivalent_to(alternative.left) ) { /* * Stuff like this: diff --git a/lib/output.js b/lib/output.js index f7cb47c6..a93f7387 100644 --- a/lib/output.js +++ b/lib/output.js @@ -779,9 +779,11 @@ function OutputStream(options) { AST_Call.prototype.print.call(self, output); }); DEFPRINT(AST_Seq, function(self, output){ - self.first.print(output); - output.comma(); - self.second.print(output); + self.car.print(output); + if (self.cdr) { + output.comma(); + self.cdr.print(output); + } }); DEFPRINT(AST_Dot, function(self, output){ var expr = self.expression; @@ -921,13 +923,12 @@ function OutputStream(options) { while (i > 0) { if (p instanceof AST_Statement && p.body === node) return true; - if ((p instanceof AST_Seq && p.first === node ) || + if ((p instanceof AST_Seq && p.car === node ) || (p instanceof AST_Call && p.expression === node ) || (p instanceof AST_Dot && p.expression === node ) || (p instanceof AST_Sub && p.expression === node ) || (p instanceof AST_Conditional && p.condition === node ) || - (p instanceof AST_Binary && p.first === node ) || - (p instanceof AST_Assign && p.first === node ) || + (p instanceof AST_Binary && p.left === node ) || (p instanceof AST_UnaryPostfix && p.expression === node )) { node = p; diff --git a/lib/parse.js b/lib/parse.js index 1c9e37fd..77c75343 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1455,8 +1455,8 @@ function parse($TEXT, exigent_mode) { next(); return new AST_Seq({ start : start, - first : expr, - second : expression(true, no_in), + car : expr, + cdr : expression(true, no_in), end : peek() }); } diff --git a/tmp/todo b/tmp/todo index 50bec924..b6f58dba 100644 --- a/tmp/todo +++ b/tmp/todo @@ -1,11 +1,11 @@ -a = a + x ==> a+=x +✓ a = a + x ==> a+=x ******* -join consecutive var statements +✓ join consecutive var statements ******* - +✓ x == false ==> x == 0 x == true ==> x == 1 @@ -14,6 +14,8 @@ JS is so sloppy that this could be an indication of a bug. ******* +✓ + x, x ==> x x = foo, x ==> x @@ -30,6 +32,8 @@ XXX? Not sure if this is worth the trouble. ******* +✓ + discard spurious break statements ******* -- 2.34.1