more optimizations that v1 does and some cleanups
authorMihai Bazon <mihai@bazon.net>
Fri, 14 Sep 2012 12:36:38 +0000 (15:36 +0300)
committerMihai Bazon <mihai@bazon.net>
Fri, 14 Sep 2012 12:36:38 +0000 (15:36 +0300)
- 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
lib/ast.js
lib/compress.js
lib/output.js
lib/parse.js
tmp/todo

index fd57a66..e753631 100755 (executable)
@@ -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(){
index 5187b5a..1a79286 100644 (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 ]----- */
 
index 15eab98..52dc011 100644 (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:
index f7cb47c..a93f738 100644 (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;
index 1c9e37f..77c7534 100644 (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()
             });
         }
index 50bec92..b6f58db 100644 (file)
--- 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
 
 *******