more optimizations for ifs/conditionals
authorMihai Bazon <mihai@bazon.net>
Mon, 3 Sep 2012 16:38:45 +0000 (19:38 +0300)
committerMihai Bazon <mihai@bazon.net>
Mon, 3 Sep 2012 16:38:45 +0000 (19:38 +0300)
(XXX: should add tests before anything else)

lib/compress.js

index 139d54a..91a6c59 100644 (file)
@@ -60,7 +60,7 @@ function Compressor(options, false_by_default) {
         keep_comps    : !false_by_default,
         drop_debugger : !false_by_default,
         unsafe        : !false_by_default,
-        ifs           : !false_by_default,
+        conditionals  : !false_by_default,
         comparations  : !false_by_default,
         evaluate      : !false_by_default,
 
@@ -88,6 +88,10 @@ function Compressor(options, false_by_default) {
         return this;
     });
 
+    AST_Node.DEFMETHOD("optimize", function(){
+        return this;
+    });
+
     function make_node(ctor, orig, props) {
         if (!props) props = {};
         if (!props.start) props.start = orig.start;
@@ -143,7 +147,7 @@ function Compressor(options, false_by_default) {
                 if (stat instanceof AST_Defun) {
                     a.push(stat);
                 }
-                else if (compressor.option("warnings")) {
+                else {
                     stat.walk(new TreeWalker(function(node){
                         if (node instanceof AST_Definitions || node instanceof AST_Defun) {
                             compressor.warn("Declarations in unreachable code! [{line},{col}]", node.start);
@@ -254,13 +258,22 @@ function Compressor(options, false_by_default) {
         node.DEFMETHOD("is_string", func);
     });
 
-    // function best_of(ast1, ast2) {
-    //     return ast1.print_to_string().length > ast2.print_to_string().length ? ast2 : ast1;
-    // };
+    function best_of(ast1, ast2) {
+        return ast1.print_to_string({ beautify: false }).length >
+            ast2.print_to_string({ beautify: false }).length
+            ? ast2 : ast1;
+    };
 
     // methods to evaluate a constant expression
     (function (def){
-        AST_Node.DEFMETHOD("evaluate", function(compressor, constant, not_constant){
+        // The evaluate method returns an array with one or two
+        // elements.  If the node has been successfully reduced to a
+        // constant, then the second element tells us the value;
+        // otherwise the second element is missing.  The first element
+        // of the array is always an AST_Node descendant; when
+        // evaluation was successful it's a node that represents the
+        // constant; otherwise it's the original node.
+        AST_Node.DEFMETHOD("evaluate", function(compressor){
             if (!compressor.option("evaluate")) return this;
             try {
                 var val = this._eval(), ast;
@@ -290,15 +303,13 @@ function Compressor(options, false_by_default) {
                         type: typeof val
                     }));
                 }
-                if (constant) return constant(ast, val);
-                return ast;
+                return [ ast, val ];
             } catch(ex) {
                 if (ex !== def) throw ex;
-                if (not_constant) return not_constant(this);
-                return this;
+                return [ this ];
             }
         });
-        function evaluate(node) {
+        function ev(node) {
             return node._eval();
         };
         def(AST_Node, function(){
@@ -310,47 +321,47 @@ function Compressor(options, false_by_default) {
         def(AST_UnaryPrefix, function(){
             var e = this.expression;
             switch (this.operator) {
-              case "!": return !evaluate(e);
-              case "typeof": return typeof evaluate(e);
-              case "~": return ~evaluate(e);
-              case "-": return -evaluate(e);
-              case "+": return +evaluate(e);
+              case "!": return !ev(e);
+              case "typeof": return typeof ev(e);
+              case "~": return ~ev(e);
+              case "-": return -ev(e);
+              case "+": return +ev(e);
             }
             throw def;
         });
         def(AST_Binary, function(){
             var left = this.left, right = this.right;
             switch (this.operator) {
-              case "&&"         : return evaluate(left) &&         evaluate(right);
-              case "||"         : return evaluate(left) ||         evaluate(right);
-              case "|"          : return evaluate(left) |          evaluate(right);
-              case "&"          : return evaluate(left) &          evaluate(right);
-              case "^"          : return evaluate(left) ^          evaluate(right);
-              case "+"          : return evaluate(left) +          evaluate(right);
-              case "*"          : return evaluate(left) *          evaluate(right);
-              case "/"          : return evaluate(left) /          evaluate(right);
-              case "%"          : return evaluate(left) %          evaluate(right);
-              case "-"          : return evaluate(left) -          evaluate(right);
-              case "<<"         : return evaluate(left) <<         evaluate(right);
-              case ">>"         : return evaluate(left) >>         evaluate(right);
-              case ">>>"        : return evaluate(left) >>>        evaluate(right);
-              case "=="         : return evaluate(left) ==         evaluate(right);
-              case "==="        : return evaluate(left) ===        evaluate(right);
-              case "!="         : return evaluate(left) !=         evaluate(right);
-              case "!=="        : return evaluate(left) !==        evaluate(right);
-              case "<"          : return evaluate(left) <          evaluate(right);
-              case "<="         : return evaluate(left) <=         evaluate(right);
-              case ">"          : return evaluate(left) >          evaluate(right);
-              case ">="         : return evaluate(left) >=         evaluate(right);
-              case "in"         : return evaluate(left) in         evaluate(right);
-              case "instanceof" : return evaluate(left) instanceof evaluate(right);
+              case "&&"         : return ev(left) &&         ev(right);
+              case "||"         : return ev(left) ||         ev(right);
+              case "|"          : return ev(left) |          ev(right);
+              case "&"          : return ev(left) &          ev(right);
+              case "^"          : return ev(left) ^          ev(right);
+              case "+"          : return ev(left) +          ev(right);
+              case "*"          : return ev(left) *          ev(right);
+              case "/"          : return ev(left) /          ev(right);
+              case "%"          : return ev(left) %          ev(right);
+              case "-"          : return ev(left) -          ev(right);
+              case "<<"         : return ev(left) <<         ev(right);
+              case ">>"         : return ev(left) >>         ev(right);
+              case ">>>"        : return ev(left) >>>        ev(right);
+              case "=="         : return ev(left) ==         ev(right);
+              case "==="        : return ev(left) ===        ev(right);
+              case "!="         : return ev(left) !=         ev(right);
+              case "!=="        : return ev(left) !==        ev(right);
+              case "<"          : return ev(left) <          ev(right);
+              case "<="         : return ev(left) <=         ev(right);
+              case ">"          : return ev(left) >          ev(right);
+              case ">="         : return ev(left) >=         ev(right);
+              case "in"         : return ev(left) in         ev(right);
+              case "instanceof" : return ev(left) instanceof ev(right);
             }
             throw def;
         });
         def(AST_Conditional, function(){
-            return evaluate(this.condition)
-                ? evaluate(this.consequent)
-                : evaluate(this.alternative);
+            return ev(this.condition)
+                ? ev(this.consequent)
+                : ev(this.alternative);
         });
     })(function(node, func){
         node.DEFMETHOD("_eval", func);
@@ -371,8 +382,8 @@ function Compressor(options, false_by_default) {
             throw new Error("Cannot evaluate a statement");
         });
         def(AST_UnaryPrefix, function(){
-            if (this.operator == "!" && this.expression.is_boolean())
-                return this.expression();
+            if (this.operator == "!")
+                return this.expression;
             return basic_negation(this);
         });
         def(AST_Seq, function(compressor){
@@ -498,14 +509,58 @@ function Compressor(options, false_by_default) {
         self.body = self.body.squeeze(compressor);
         if (self.alternative)
             self.alternative = self.alternative.squeeze(compressor);
-        if (!compressor.option("ifs")) return self;
+        return compressor.option("conditionals") ? self.optimize(compressor) : self;
+    });
+
+    AST_If.DEFMETHOD("optimize", function(compressor){
+        // if condition can be statically determined, warn and drop
+        // one of the blocks.  note, statically determined implies
+        // “has no side effects”; also it doesn't work for cases like
+        // `x && true`, though it probably should.
+        var self = this;
+        var cond = self.condition.evaluate(compressor);
+        if (cond.length == 2) {
+            if (cond[1]) {
+                AST_Node.warn("Condition always true [{line},{col}]", self.condition.start);
+                return self.body;
+            } else {
+                AST_Node.warn("Condition always false [{line},{col}]", self.condition.start);
+                return self.alternative || new AST_EmptyStatement(self);
+            }
+        }
+        if (self.body instanceof AST_SimpleStatement
+            && self.alternative instanceof AST_SimpleStatement) {
+            return make_node(AST_SimpleStatement, self, {
+                body: make_node(AST_Conditional, self, {
+                    condition   : self.condition,
+                    consequent  : self.body.body,
+                    alternative : self.alternative.body
+                }).optimize(compressor)
+            });
+        }
+        if (!self.alternative && self.body instanceof AST_SimpleStatement) {
+            return make_node(AST_SimpleStatement, self, {
+                body: make_node(AST_Binary, self, {
+                    operator: "&&",
+                    left: self.condition,
+                    right: self.body.body
+                }).optimize(compressor)
+            });
+        }
+        if (self.body instanceof AST_EmptyStatement
+            && self.alternative
+            && !(self.alternative instanceof AST_EmptyStatement)) {
+            return make_node(AST_SimpleStatement, self, {
+                body: make_node(AST_Binary, self, {
+                    operator: "||",
+                    left: self.condition,
+                    right: self.alternative.body
+                }).optimize(compressor)
+            });
+        }
         return self;
     });
 
-    AST_If.DEFMETHOD("switch_branches", function(){
-
-    });
-
     SQUEEZE(AST_Switch, function(self, compressor){
         self = self.clone();
         self.expression = self.expression.squeeze(compressor);
@@ -599,14 +654,14 @@ function Compressor(options, false_by_default) {
     SQUEEZE(AST_UnaryPrefix, function(self, compressor){
         self = self.clone();
         self.expression = self.expression.squeeze(compressor);
-        return self.evaluate(compressor);
+        return self.evaluate(compressor)[0];
     });
 
     SQUEEZE(AST_Binary, function(self, compressor){
         self = self.clone();
         self.left = self.left.squeeze(compressor);
         self.right = self.right.squeeze(compressor);
-        return self.evaluate(compressor);
+        return self.evaluate(compressor)[0];
     });
 
     SQUEEZE(AST_Assign, function(self, compressor){
@@ -621,7 +676,17 @@ function Compressor(options, false_by_default) {
         self.condition = self.condition.squeeze(compressor);
         self.consequent = self.consequent.squeeze(compressor);
         self.alternative = self.alternative.squeeze(compressor);
-        return self;
+        return compressor.option("conditionals") ? self.optimize(compressor) : self;
+    });
+
+    AST_Conditional.DEFMETHOD("optimize", function(compressor){
+        var self = this;
+        var rev = self.clone();
+        rev.condition = rev.condition.negate(compressor);
+        var tmp = rev.consequent;
+        rev.consequent = rev.alternative;
+        rev.alternative = tmp;
+        return best_of(self, rev);
     });
 
     SQUEEZE(AST_Array, function(self, compressor){