enhance `collapse_vars` (#2952)
authorAlex Lam S.L <alexlamsl@gmail.com>
Wed, 28 Feb 2018 07:19:32 +0000 (15:19 +0800)
committerGitHub <noreply@github.com>
Wed, 28 Feb 2018 07:19:32 +0000 (15:19 +0800)
- `a = b, b` => `a = b`
- `a.b = c, c()` => `(a.b = c)()`

lib/compress.js
test/compress/collapse_vars.js
test/compress/pure_getters.js
test/compress/sequences.js

index f9382f0..ed09487 100644 (file)
@@ -890,6 +890,11 @@ merge(Compressor.prototype, {
         return x;
     };
 
+    function root_expr(prop) {
+        while (prop instanceof AST_PropAccess) prop = prop.expression;
+        return prop;
+    }
+
     function is_iife_call(node) {
         if (node.TYPE != "Call") return false;
         return node.expression instanceof AST_Function || is_iife_call(node.expression);
@@ -994,17 +999,19 @@ merge(Compressor.prototype, {
                     return node;
                 }
                 // Stop only if candidate is found within conditional branches
-                if (!stop_if_hit && (!lhs_local || !replace_all)
+                if (!stop_if_hit
                     && (parent instanceof AST_Binary && lazy_op(parent.operator) && parent.left !== node
                         || parent instanceof AST_Conditional && parent.condition !== node
                         || parent instanceof AST_If && parent.condition !== node)) {
                     stop_if_hit = parent;
                 }
                 // Replace variable with assignment when found
+                var hit_lhs, hit_rhs;
                 if (can_replace
                     && !(node instanceof AST_SymbolDeclaration)
-                    && lhs.equivalent_to(node)) {
-                    if (stop_if_hit) {
+                    && (scan_lhs && (hit_lhs = lhs.equivalent_to(node))
+                        || scan_rhs && (hit_rhs = rhs.equivalent_to(node)))) {
+                    if (stop_if_hit && (hit_rhs || !lhs_local || !replace_all)) {
                         abort = true;
                         return node;
                     }
@@ -1056,7 +1063,7 @@ merge(Compressor.prototype, {
                     || node instanceof AST_PropAccess
                         && (side_effects || node.expression.may_throw_on_access(compressor))
                     || node instanceof AST_SymbolRef
-                        && (lvalues[node.name] || side_effects && may_modify(node))
+                        && (symbol_in_lvalues(node) || side_effects && may_modify(node))
                     || node instanceof AST_VarDef && node.value
                         && (node.name.name in lvalues || side_effects && may_modify(node.name))
                     || (sym = is_lhs(node.left, node))
@@ -1111,12 +1118,15 @@ merge(Compressor.prototype, {
                     var stop_after = null;
                     var stop_if_hit = null;
                     var lhs = get_lhs(candidate);
-                    if (!lhs || is_lhs_read_only(lhs) || lhs.has_side_effects(compressor)) continue;
+                    var rhs = get_rhs(candidate);
+                    var side_effects = lhs && lhs.has_side_effects(compressor);
+                    var scan_lhs = lhs && !side_effects && !is_lhs_read_only(lhs);
+                    var scan_rhs = rhs && foldable(rhs);
+                    if (!scan_lhs && !scan_rhs) continue;
                     // Locate symbols which may execute code outside of scanning range
                     var lvalues = get_lvalues(candidate);
                     var lhs_local = is_lhs_local(lhs);
-                    if (lhs instanceof AST_SymbolRef) lvalues[lhs.name] = false;
-                    var side_effects = value_has_side_effects(candidate);
+                    if (!side_effects) side_effects = value_has_side_effects(candidate);
                     var replace_all = replace_all_symbols();
                     var may_throw = candidate.may_throw(compressor);
                     var funarg = candidate.name instanceof AST_SymbolFunarg;
@@ -1221,9 +1231,7 @@ merge(Compressor.prototype, {
             function extract_candidates(expr) {
                 hit_stack.push(expr);
                 if (expr instanceof AST_Assign) {
-                    if (!expr.left.has_side_effects(compressor)) {
-                        candidates.push(hit_stack.slice());
-                    }
+                    candidates.push(hit_stack.slice());
                     extract_candidates(expr.right);
                 } else if (expr instanceof AST_Binary) {
                     extract_candidates(expr.left);
@@ -1361,21 +1369,47 @@ merge(Compressor.prototype, {
                 }
             }
 
+            function get_rhs(expr) {
+                if (!(candidate instanceof AST_Assign && candidate.operator == "=")) return;
+                return candidate.right;
+            }
+
             function get_rvalue(expr) {
                 return expr[expr instanceof AST_Assign ? "right" : "value"];
             }
 
+            function foldable(expr) {
+                if (expr.is_constant()) return true;
+                if (expr instanceof AST_Array) return false;
+                if (expr instanceof AST_Function) return false;
+                if (expr instanceof AST_Object) return false;
+                if (expr instanceof AST_RegExp) return false;
+                if (expr instanceof AST_Symbol) return true;
+                if (!(lhs instanceof AST_SymbolRef)) return false;
+                if (expr.has_side_effects(compressor)) return false;
+                var circular;
+                var def = lhs.definition();
+                expr.walk(new TreeWalker(function(node) {
+                    if (circular) return true;
+                    if (node instanceof AST_SymbolRef && node.definition() === def) {
+                        circular = true;
+                    }
+                }));
+                return !circular;
+            }
+
             function get_lvalues(expr) {
                 var lvalues = Object.create(null);
-                if (expr instanceof AST_Unary) return lvalues;
-                var tw = new TreeWalker(function(node, descend) {
-                    var sym = node;
-                    while (sym instanceof AST_PropAccess) sym = sym.expression;
+                if (candidate instanceof AST_VarDef) {
+                    lvalues[candidate.name.name] = lhs;
+                }
+                var tw = new TreeWalker(function(node) {
+                    var sym = root_expr(node);
                     if (sym instanceof AST_SymbolRef || sym instanceof AST_This) {
                         lvalues[sym.name] = lvalues[sym.name] || is_lhs(node, tw.parent());
                     }
                 });
-                get_rvalue(expr).walk(tw);
+                expr.walk(tw);
                 return lvalues;
             }
 
@@ -1408,11 +1442,11 @@ merge(Compressor.prototype, {
             }
 
             function is_lhs_local(lhs) {
-                while (lhs instanceof AST_PropAccess) lhs = lhs.expression;
-                return lhs instanceof AST_SymbolRef
-                    && lhs.definition().scope === scope
+                var sym = root_expr(lhs);
+                return sym instanceof AST_SymbolRef
+                    && sym.definition().scope === scope
                     && !(in_loop
-                        && (lhs.name in lvalues
+                        && (sym.name in lvalues && lvalues[sym.name] !== lhs
                             || candidate instanceof AST_Unary
                             || candidate instanceof AST_Assign && candidate.operator != "="));
             }
@@ -1434,6 +1468,13 @@ merge(Compressor.prototype, {
                 return false;
             }
 
+            function symbol_in_lvalues(sym) {
+                var lvalue = lvalues[sym.name];
+                if (!lvalue) return;
+                if (lvalue !== lhs) return true;
+                scan_rhs = false;
+            }
+
             function may_modify(sym) {
                 var def = sym.definition();
                 if (def.orig.length == 1 && def.orig[0] instanceof AST_SymbolDefun) return false;
@@ -3639,10 +3680,7 @@ merge(Compressor.prototype, {
                 return this;
             }
             this.write_only = true;
-            while (left instanceof AST_PropAccess) {
-                left = left.expression;
-            }
-            if (left.is_constant_expression(compressor.find_parent(AST_Scope))) {
+            if (root_expr(left).is_constant_expression(compressor.find_parent(AST_Scope))) {
                 return this.right.drop_side_effect_free(compressor);
             }
             return this;
index 6050550..d4ab444 100644 (file)
@@ -4770,3 +4770,392 @@ issue_2954_3: {
     }
     expect_stdout: Error("PASS")
 }
+
+collapse_rhs_conditional_1: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "PASS", b = "FAIL";
+        b = a;
+        "function" == typeof f && f(a);
+        console.log(a, b);
+    }
+    expect: {
+        var a = "PASS", b = "FAIL";
+        b = a;
+        "function" == typeof f && f(a);
+        console.log(a, b);
+    }
+    expect_stdout: "PASS PASS"
+}
+
+collapse_rhs_conditional_2: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "FAIL", b;
+        while ((a = "PASS", --b) && "PASS" == b);
+        console.log(a, b);
+    }
+    expect: {
+        var a = "FAIL", b;
+        while ((a = "PASS", --b) && "PASS" == b);
+        console.log(a, b);
+    }
+    expect_stdout: "PASS NaN"
+}
+
+collapse_rhs_lhs_1: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var c = 0;
+        new function() {
+            this[c++] = 1;
+            c += 1;
+        }();
+        console.log(c);
+    }
+    expect: {
+        var c = 0;
+        new function() {
+            this[c++] = 1;
+            c += 1;
+        }();
+        console.log(c);
+    }
+    expect_stdout: "2"
+}
+
+collapse_rhs_lhs_2: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var b = 1;
+        (function f(f) {
+            f = b;
+            f[b] = 0;
+        })();
+        console.log("PASS");
+    }
+    expect: {
+        var b = 1;
+        (function f(f) {
+            f = b;
+            f[b] = 0;
+        })();
+        console.log("PASS");
+    }
+    expect_stdout: "PASS"
+}
+
+collapse_rhs_side_effects: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = 1, c = 0;
+        new function f() {
+            this[a-- && f()] = 1;
+            c += 1;
+        }();
+        console.log(c);
+    }
+    expect: {
+        var a = 1, c = 0;
+        new function f() {
+            this[a-- && f()] = 1;
+            c += 1;
+        }();
+        console.log(c);
+    }
+    expect_stdout: "2"
+}
+
+collapse_rhs_vardef: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b = 1;
+        a = --b + function c() {
+            var b;
+            c[--b] = 1;
+        }();
+        b |= a;
+        console.log(a, b);
+    }
+    expect: {
+        var a, b = 1;
+        a = --b + function c() {
+            var b;
+            c[--b] = 1;
+        }();
+        b |= a;
+        console.log(a, b);
+    }
+    expect_stdout: "NaN 0"
+}
+
+collapse_rhs_array: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = [];
+            b = [];
+            return [];
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            a = [];
+            b = [];
+            return [];
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "false false false"
+}
+
+collapse_rhs_boolean: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = !0;
+            b = !0;
+            return !0;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            return b = a = !0;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
+
+collapse_rhs_function: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = function() {};
+            b = function() {};
+            return function() {};
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            a = function() {};
+            b = function() {};
+            return function() {};
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "false false false"
+}
+
+collapse_rhs_number: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = 42;
+            b = 42;
+            return 42;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            return b = a = 42;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
+
+collapse_rhs_object: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = {};
+            b = {};
+            return {};
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            a = {};
+            b = {};
+            return {};
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "false false false"
+}
+
+collapse_rhs_regexp: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = /bar/;
+            b = /bar/;
+            return /bar/;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            a = /bar/;
+            b = /bar/;
+            return /bar/;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "false false false"
+}
+
+collapse_rhs_string: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = "foo";
+            b = "foo";
+            return "foo";
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            return b = a = "foo";
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
+
+collapse_rhs_var: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = f;
+            b = f;
+            return f;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            return b = a = f;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
+
+collapse_rhs_this: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = this;
+            b = this;
+            return this;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            return b = a = this;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
+
+collapse_rhs_undefined: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a, b;
+        function f() {
+            a = void 0;
+            b = void 0;
+            return void 0;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect: {
+        var a, b;
+        function f() {
+            b = a = void 0;
+            return;
+        }
+        var c = f();
+        console.log(a === b, b === c, c === a);
+    }
+    expect_stdout: "true true true"
+}
index e266286..f9edcc8 100644 (file)
@@ -830,3 +830,334 @@ issue_2938_4: {
     }
     expect_stdout: "PASS"
 }
+
+collapse_vars_1_true: {
+    options = {
+        collapse_vars: true,
+        pure_getters: true,
+        unused: true,
+    }
+    input: {
+        function f(a, b) {
+            for (;;) {
+                var c = a.g();
+                var d = b.p;
+                if (c || d) break;
+            }
+        }
+    }
+    expect: {
+        function f(a, b) {
+            for (;;) {
+                if (a.g() || b.p) break;
+            }
+        }
+    }
+}
+
+collapse_vars_1_false: {
+    options = {
+        collapse_vars: true,
+        pure_getters: false,
+        unused: true,
+    }
+    input: {
+        function f(a, b) {
+            for (;;) {
+                var c = a.g();
+                var d = b.p;
+                if (c || d) break;
+            }
+        }
+    }
+    expect: {
+        function f(a, b) {
+            for (;;) {
+                var c = a.g();
+                var d = b.p;
+                if (c || d) break;
+            }
+        }
+    }
+}
+
+collapse_vars_1_strict: {
+    options = {
+        collapse_vars: true,
+        pure_getters: "strict",
+        unused: true,
+    }
+    input: {
+        function f(a, b) {
+            for (;;) {
+                var c = a.g();
+                var d = b.p;
+                if (c || d) break;
+            }
+        }
+    }
+    expect: {
+        function f(a, b) {
+            for (;;) {
+                var c = a.g();
+                var d = b.p;
+                if (c || d) break;
+            }
+        }
+    }
+}
+
+collapse_vars_2_true: {
+    options = {
+        collapse_vars: true,
+        pure_getters: true,
+        reduce_vars: true,
+    }
+    input: {
+        function f() {
+            function g() {}
+            g.a = function() {};
+            g.b = g.a;
+            return g;
+        }
+    }
+    expect: {
+        function f() {
+            function g() {}
+            g.b = g.a = function() {};
+            return g;
+        }
+    }
+}
+
+collapse_vars_2_false: {
+    options = {
+        collapse_vars: true,
+        pure_getters: false,
+        reduce_vars: true,
+    }
+    input: {
+        function f() {
+            function g() {}
+            g.a = function() {};
+            g.b = g.a;
+            return g;
+        }
+    }
+    expect: {
+        function f() {
+            function g() {}
+            g.a = function() {};
+            g.b = g.a;
+            return g;
+        }
+    }
+}
+
+collapse_vars_2_strict: {
+    options = {
+        collapse_vars: true,
+        pure_getters: "strict",
+        reduce_vars: true,
+    }
+    input: {
+        function f() {
+            function g() {}
+            g.a = function() {};
+            g.b = g.a;
+            return g;
+        }
+    }
+    expect: {
+        function f() {
+            function g() {}
+            g.b = g.a = function() {};
+            return g;
+        }
+    }
+}
+
+collapse_rhs_true: {
+    options = {
+        collapse_vars: true,
+        pure_getters: true,
+    }
+    input: {
+        console.log((42..length = "PASS", "PASS"));
+        console.log(("foo".length = "PASS", "PASS"));
+        console.log((false.length = "PASS", "PASS"));
+        console.log((function() {}.length = "PASS", "PASS"));
+        console.log(({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS", "PASS"));
+    }
+    expect: {
+        console.log(42..length = "PASS");
+        console.log("foo".length = "PASS");
+        console.log(false.length = "PASS");
+        console.log(function() {}.length = "PASS");
+        console.log({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS");
+    }
+    expect_stdout: [
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+    ]
+}
+
+collapse_rhs_false: {
+    options = {
+        collapse_vars: true,
+        pure_getters: false,
+    }
+    input: {
+        console.log((42..length = "PASS", "PASS"));
+        console.log(("foo".length = "PASS", "PASS"));
+        console.log((false.length = "PASS", "PASS"));
+        console.log((function() {}.length = "PASS", "PASS"));
+        console.log(({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS", "PASS"));
+    }
+    expect: {
+        console.log(42..length = "PASS");
+        console.log("foo".length = "PASS");
+        console.log(false.length = "PASS");
+        console.log(function() {}.length = "PASS");
+        console.log({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS");
+    }
+    expect_stdout: [
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+    ]
+}
+
+collapse_rhs_strict: {
+    options = {
+        collapse_vars: true,
+        pure_getters: "strict",
+    }
+    input: {
+        console.log((42..length = "PASS", "PASS"));
+        console.log(("foo".length = "PASS", "PASS"));
+        console.log((false.length = "PASS", "PASS"));
+        console.log((function() {}.length = "PASS", "PASS"));
+        console.log(({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS", "PASS"));
+    }
+    expect: {
+        console.log(42..length = "PASS");
+        console.log("foo".length = "PASS");
+        console.log(false.length = "PASS");
+        console.log(function() {}.length = "PASS");
+        console.log({
+            get length() {
+                return "FAIL";
+            }
+        }.length = "PASS");
+    }
+    expect_stdout: [
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+        "PASS",
+    ]
+}
+
+collapse_rhs_setter: {
+    options = {
+        collapse_vars: true,
+        pure_getters: "strict",
+    }
+    input: {
+        try {
+            console.log(({
+                set length(v) {
+                    throw "PASS";
+                }
+            }.length = "FAIL", "FAIL"));
+        } catch (e) {
+            console.log(e);
+        }
+    }
+    expect: {
+        try {
+            console.log({
+                set length(v) {
+                    throw "PASS";
+                }
+            }.length = "FAIL");
+        } catch (e) {
+            console.log(e);
+        }
+    }
+    expect_stdout: "PASS"
+}
+
+collapse_rhs_call: {
+    options = {
+        collapse_vars: true,
+        passes: 2,
+        pure_getters: "strict",
+        reduce_vars: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var o = {};
+        function f() {
+            console.log("PASS");
+        }
+        o.f = f;
+        f();
+    }
+    expect: {
+        ({}.f = function() {
+            console.log("PASS");
+        })();
+    }
+    expect_stdout: "PASS"
+}
+
+collapse_rhs_lhs: {
+    options = {
+        collapse_vars: true,
+        pure_getters: true,
+    }
+    input: {
+        function f(a, b) {
+            a.b = b, b += 2;
+            console.log(a.b, b);
+        }
+        f({}, 1);
+    }
+    expect: {
+        function f(a, b) {
+            a.b = b, b += 2;
+            console.log(a.b, b);
+        }
+        f({}, 1);
+    }
+    expect_stdout: "1 3"
+}
index 03075bf..12acbcf 100644 (file)
@@ -668,8 +668,7 @@ side_effects_cascade_2: {
     }
     expect: {
         function f(a, b) {
-            b = a,
-            !a + (b += a) || (b += a),
+            !(b = a) + (b += a) || (b += a),
             b = a;
         }
     }