optimize constant switch blocks
authorMihai Bazon <mihai@bazon.net>
Wed, 14 Nov 2012 10:06:07 +0000 (12:06 +0200)
committerMihai Bazon <mihai@bazon.net>
Wed, 14 Nov 2012 10:06:07 +0000 (12:06 +0200)
ref. mishoo/UglifyJS#441

lib/compress.js
test/compress/switch.js [new file with mode: 0644]

index ed664a2..89be0ce 100644 (file)
@@ -826,10 +826,12 @@ merge(Compressor.prototype, {
     (function(def){
         def(AST_Statement, function(){ return null });
         def(AST_Jump, function(){ return this });
-        def(AST_BlockStatement, function(){
+        function block_aborts(){
             var n = this.body.length;
             return n > 0 && aborts(this.body[n - 1]);
-        });
+        };
+        def(AST_BlockStatement, block_aborts);
+        def(AST_SwitchBranch, block_aborts);
         def(AST_If, function(){
             return this.alternative && aborts(this.body) && aborts(this.alternative);
         });
@@ -1360,6 +1362,74 @@ merge(Compressor.prototype, {
             if (stat instanceof AST_Break && loop_body(compressor.loopcontrol_target(stat.label)) === self)
                 last_branch.body.pop();
         }
+        var exp = self.expression.evaluate(compressor);
+        out: if (exp.length == 2) try {
+            // constant expression
+            self.expression = exp[0];
+            if (!compressor.option("dead_code")) break out;
+            var value = exp[1];
+            var in_substat = false;
+            var started = false;
+            var stopped = false;
+            var ruined = false;
+            var tt = new TreeTransformer(function(node, descend, in_list){
+                if (node instanceof AST_Lambda || node instanceof AST_SimpleStatement) {
+                    // no need to descend these node types
+                    return node;
+                }
+                else if (node instanceof AST_Switch) {
+                    if (node === self) {
+                        node = node.clone();
+                        descend(node, this);
+                        return ruined ? node : make_node(AST_BlockStatement, node, {
+                            body: node.body.reduce(function(a, branch){
+                                return a.concat(branch.body);
+                            }, [])
+                        }).transform(compressor);
+                    }
+                }
+                else if (node instanceof AST_StatementWithBody || node instanceof AST_Switch || node instanceof AST_Try) {
+                    var save_substat = in_substat;
+                    in_substat = true;
+                    descend(node, this);
+                    in_substat = save_substat;
+                    return node;
+                }
+                else if (node instanceof AST_Break && this.loopcontrol_target(node.label) === self) {
+                    if (in_substat) {
+                        // won't handle situations like if (foo) break;
+                        ruined = true;
+                        return node;
+                    } else {
+                        stopped = true;
+                    }
+                    return in_list ? MAP.skip : make_node(AST_EmptyStatement, node);
+                }
+                else if (node instanceof AST_SwitchBranch && this.parent() === self) {
+                    if (stopped) return MAP.skip;
+                    if (node instanceof AST_Case) {
+                        var exp = node.expression.evaluate(compressor);
+                        if (exp.length < 2) {
+                            // got a case with non-constant expression, baling out
+                            throw self;
+                        }
+                        if (exp[1] === value || started) {
+                            started = true;
+                            if (aborts(node)) stopped = true;
+                            descend(node, this);
+                            return node;
+                        }
+                        return MAP.skip;
+                    }
+                    descend(node, this);
+                    return node;
+                }
+            });
+            tt.stack = compressor.stack; // so that's able to see parent nodes
+            self = self.transform(tt);
+        } catch(ex) {
+            if (ex !== self) throw ex;
+        }
         return self;
     });
 
diff --git a/test/compress/switch.js b/test/compress/switch.js
new file mode 100644 (file)
index 0000000..7c1e021
--- /dev/null
@@ -0,0 +1,186 @@
+constant_switch_1: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        switch (1+1) {
+          case 1: foo(); break;
+          case 1+1: bar(); break;
+          case 1+1+1: baz(); break;
+        }
+    }
+    expect: {
+        bar();
+    }
+}
+
+constant_switch_2: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        switch (1) {
+          case 1: foo();
+          case 1+1: bar(); break;
+          case 1+1+1: baz();
+        }
+    }
+    expect: {
+        foo();
+        bar();
+    }
+}
+
+constant_switch_3: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        switch (10) {
+          case 1: foo();
+          case 1+1: bar(); break;
+          case 1+1+1: baz();
+          default:
+            def();
+        }
+    }
+    expect: {
+        def();
+    }
+}
+
+constant_switch_4: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        switch (2) {
+          case 1:
+            x();
+            if (foo) break;
+            y();
+            break;
+          case 1+1:
+            bar();
+          default:
+            def();
+        }
+    }
+    expect: {
+        bar();
+        def();
+    }
+}
+
+constant_switch_5: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        switch (1) {
+          case 1:
+            x();
+            if (foo) break;
+            y();
+            break;
+          case 1+1:
+            bar();
+          default:
+            def();
+        }
+    }
+    expect: {
+        // the break inside the if ruins our job
+        // we can still get rid of irrelevant cases.
+        switch (1) {
+          case 1:
+            x();
+            if (foo) break;
+            y();
+        }
+        // XXX: we could optimize this better by inventing an outer
+        // labeled block, but that's kinda tricky.
+    }
+}
+
+constant_switch_6: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        OUT: {
+            foo();
+            switch (1) {
+              case 1:
+                x();
+                if (foo) break OUT;
+                y();
+              case 1+1:
+                bar();
+                break;
+              default:
+                def();
+            }
+        }
+    }
+    expect: {
+        OUT: {
+            foo();
+            x();
+            if (foo) break OUT;
+            y();
+            bar();
+        }
+    }
+}
+
+constant_switch_7: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        OUT: {
+            foo();
+            switch (1) {
+              case 1:
+                x();
+                if (foo) break OUT;
+                for (var x = 0; x < 10; x++) {
+                    if (x > 5) break; // this break refers to the for, not to the switch; thus it
+                                      // shouldn't ruin our optimization
+                    console.log(x);
+                }
+                y();
+              case 1+1:
+                bar();
+                break;
+              default:
+                def();
+            }
+        }
+    }
+    expect: {
+        OUT: {
+            foo();
+            x();
+            if (foo) break OUT;
+            for (var x = 0; x < 10; x++) {
+                if (x > 5) break;
+                console.log(x);
+            }
+            y();
+            bar();
+        }
+    }
+}
+
+constant_switch_8: {
+    options = { dead_code: true, evaluate: true };
+    input: {
+        OUT: switch (1) {
+          case 1:
+            x();
+            for (;;) break OUT;
+            y();
+            break;
+          case 1+1:
+            bar();
+          default:
+            def();
+        }
+    }
+    expect: {
+        OUT: switch (1) {
+          case 1:
+            x();
+            for (;;) break OUT;
+            y();
+        }
+    }
+}