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
This commit is contained in:
Mihai Bazon
2012-09-14 15:36:38 +03:00
parent 93b973c99d
commit 924aa58060
6 changed files with 256 additions and 79 deletions

View File

@@ -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(){

View File

@@ -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 ]----- */

View File

@@ -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:

View File

@@ -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;

View File

@@ -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()
});
}

View File

@@ -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
*******