Collapse single use var definitions
authorkzc <zaxxon2011@gmail.com>
Wed, 27 Jan 2016 07:17:06 +0000 (02:17 -0500)
committerMihai Bazon <mihai.bazon@gmail.com>
Wed, 27 Jan 2016 09:48:15 +0000 (11:48 +0200)
Fix #721

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

index 1f5988f..814e9a8 100644 (file)
@@ -66,6 +66,7 @@ function Compressor(options, false_by_default) {
         hoist_vars    : false,
         if_return     : !false_by_default,
         join_vars     : !false_by_default,
+        collapse_vars : false,
         cascade       : !false_by_default,
         side_effects  : !false_by_default,
         pure_getters  : false,
@@ -218,6 +219,9 @@ merge(Compressor.prototype, {
             if (compressor.option("join_vars")) {
                 statements = join_consecutive_vars(statements, compressor);
             }
+            if (compressor.option("collapse_vars")) {
+                statements = collapse_single_use_vars(statements, compressor);
+            }
         } while (CHANGED && max_iter-- > 0);
 
         if (compressor.option("negate_iife")) {
@@ -226,6 +230,148 @@ merge(Compressor.prototype, {
 
         return statements;
 
+        function collapse_single_use_vars(statements, compressor) {
+            // Iterate statements backwards looking for a statement with a var/const
+            // declaration immediately preceding it. Grab the rightmost var definition
+            // and if it has exactly one reference then attempt to replace its reference
+            // in the statement with the var value and then erase the var definition.
+
+            var self = compressor.self();
+            var var_defs_removed = false;
+            for (var stat_index = statements.length; --stat_index >= 0;) {
+                var stat = statements[stat_index];
+                if (stat instanceof AST_Definitions) continue;
+
+                // Process child blocks of statement if present.
+                [stat, stat.body, stat.alternative, stat.bcatch, stat.bfinally].forEach(function(node) {
+                    node && node.body && collapse_single_use_vars(node.body, compressor);
+                });
+
+                // The variable definition must precede a statement.
+                if (stat_index <= 0) break;
+                var prev_stat_index = stat_index - 1;
+                var prev_stat = statements[prev_stat_index];
+                if (!(prev_stat instanceof AST_Definitions)) continue;
+                var var_defs = prev_stat.definitions;
+                if (var_defs == null) continue;
+
+                // Scan variable definitions from right to left.
+                var side_effects_encountered = false;
+                var lvalues_encountered = false;
+                var lvalues = {};
+                for (var var_defs_index = var_defs.length; --var_defs_index >= 0;) {
+                    var var_decl = var_defs[var_defs_index];
+                    if (var_decl.value == null) continue;
+
+                    // Only interested in cases with just one reference to the variable.
+                    var var_name = var_decl.name.name;
+                    var def = self.find_variable && self.find_variable(var_name);
+                    if (!def || !def.references || def.references.length !== 1 || var_name == "arguments") {
+                        side_effects_encountered = true;
+                        continue;
+                    }
+                    var ref = def.references[0];
+
+                    // Don't replace ref if eval() or with statement in scope.
+                    if (ref.scope.uses_eval || ref.scope.uses_with) break;
+
+                    // Constant single use vars can be replaced in any scope.
+                    if (var_decl.value.is_constant(compressor)) {
+                        var ctt = new TreeTransformer(function(node) {
+                            if (node === ref)
+                                return replace_var(node, ctt.parent(), true);
+                        });
+                        stat.transform(ctt);
+                        continue;
+                    }
+
+                    // Restrict var replacement to constants if side effects encountered.
+                    if (side_effects_encountered |= lvalues_encountered) continue;
+
+                    // Non-constant single use vars can only be replaced in same scope.
+                    if (ref.scope !== self) {
+                        side_effects_encountered |= var_decl.value.has_side_effects(compressor);
+                        continue;
+                    }
+
+                    // Detect lvalues in var value.
+                    var tw = new TreeWalker(function(node){
+                        if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) {
+                            lvalues[node.name] = lvalues_encountered = true;
+                        }
+                    });
+                    var_decl.value.walk(tw);
+
+                    // Replace the non-constant single use var in statement if side effect free.
+                    var unwind = false;
+                    var tt = new TreeTransformer(
+                        function preorder(node) {
+                            if (unwind) return node;
+                            var parent = tt.parent();
+                            if (node instanceof AST_Lambda
+                                || node instanceof AST_Try
+                                || node instanceof AST_With
+                                || node instanceof AST_IterationStatement
+                                || (parent instanceof AST_Switch && node !== parent.expression)) {
+                                return unwind = true, node;
+                            }
+                        },
+                        function postorder(node) {
+                            if (unwind) return node;
+                            if (node === ref)
+                                return unwind = true, replace_var(node, tt.parent(), false);
+                            if (side_effects_encountered |= node.has_side_effects(compressor))
+                                return unwind = true, node;
+                            if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) {
+                                side_effects_encountered = true;
+                                return unwind = true, node;
+                            }
+                        }
+                    );
+                    stat.transform(tt);
+                }
+            }
+
+            // Remove extraneous empty statments in block after removing var definitions.
+            // Leave at least one statement in `statements`.
+            if (var_defs_removed) for (var i = statements.length; --i >= 0;) {
+                if (statements.length > 1 && statements[i] instanceof AST_EmptyStatement)
+                    statements.splice(i, 1);
+            }
+
+            return statements;
+
+            function is_lvalue(node, parent) {
+                return node instanceof AST_SymbolRef && (
+                    (parent instanceof AST_Assign && node === parent.left)
+                    || (parent instanceof AST_Unary && parent.expression === node
+                        && (parent.operator == "++" || parent.operator == "--")));
+            }
+            function replace_var(node, parent, is_constant) {
+                if (is_lvalue(node, parent)) return node;
+
+                // Remove var definition and return its value to the TreeTransformer to replace.
+                var value = var_decl.value;
+                var_decl.value = null;
+
+                var_defs.splice(var_defs_index, 1);
+                if (var_defs.length === 0) {
+                    statements[prev_stat_index] = make_node(AST_EmptyStatement, self);
+                    var_defs_removed = true;
+                }
+                // Further optimize statement after substitution.
+                stat.walk(new TreeWalker(function(node){
+                    delete node._squeezed;
+                    delete node._optimized;
+                }));
+
+                compressor.warn("Replacing " + (is_constant ? "constant" : "variable") +
+                    " " + var_name + " [{file}:{line},{col}]", node.start);
+                CHANGED = true;
+                return value;
+            }
+        }
+
         function process_for_angular(statements) {
             function has_inject(comment) {
                 return /@ngInject/.test(comment.value);
index 62e6e02..3018e8f 100644 (file)
@@ -64,7 +64,7 @@ TreeTransformer.prototype = new TreeWalker;
                     x = this;
                     descend(x, tw);
                 } else {
-                    tw.stack[tw.stack.length - 1] = x = this.clone();
+                    tw.stack[tw.stack.length - 1] = x = this;
                     descend(x, tw);
                     y = tw.after(x, in_list);
                     if (y !== undefined) x = y;
diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js
new file mode 100644 (file)
index 0000000..e023597
--- /dev/null
@@ -0,0 +1,1047 @@
+collapse_vars_side_effects_1: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1() {
+            var e = 7;
+            var s = "abcdef";
+            var i = 2;
+            var log = console.log.bind(console);
+            var x = s.charAt(i++);
+            var y = s.charAt(i++);
+            var z = s.charAt(i++);
+            log(x, y, z, e);
+        }
+        function f2() {
+            var e = 7;
+            var log = console.log.bind(console);
+            var s = "abcdef";
+            var i = 2;
+            var x = s.charAt(i++);
+            var y = s.charAt(i++);
+            var z = s.charAt(i++);
+            log(x, i, y, z, e);
+        }
+        function f3() {
+            var e = 7;
+            var s = "abcdef";
+            var i = 2;
+            var log = console.log.bind(console);
+            var x = s.charAt(i++);
+            var y = s.charAt(i++);
+            var z = s.charAt(i++);
+            log(x, z, y, e);
+        }
+        function f4() {
+            var log = console.log.bind(console),
+                i = 10,
+                x = i += 2,
+                y = i += 3,
+                z = i += 4;
+            log(x, z, y, i);
+        }
+    }
+    expect: {
+        function f1() {
+            var s = "abcdef", i = 2;
+            console.log.bind(console)(s.charAt(i++), s.charAt(i++), s.charAt(i++), 7);
+        }
+        function f2() {
+            var log = console.log.bind(console),
+                s = "abcdef",
+                i = 2,
+                x = s.charAt(i++),
+                y = s.charAt(i++),
+                z = s.charAt(i++);
+            log(x, i, y, z, 7);
+        }
+        function f3() {
+            var s = "abcdef",
+                i = 2,
+                log = console.log.bind(console),
+                x = s.charAt(i++),
+                y = s.charAt(i++);
+            log(x, s.charAt(i++), y, 7);
+        }
+        function f4() {
+            var log = console.log.bind(console),
+                i = 10,
+                x = i += 2,
+                y = i += 3;
+            log(x, i += 4, y, i);
+        }
+    }
+}
+
+collapse_vars_side_effects_2: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function fn(x) { return console.log(x), x; }
+
+        function p1() { var a = foo(), b = bar(), c = baz(); return a + b + c; }
+        function p2() { var a = foo(), c = bar(), b = baz(); return a + b + c; }
+        function p3() { var b = foo(), a = bar(), c = baz(); return a + b + c; }
+        function p4() { var b = foo(), c = bar(), a = baz(); return a + b + c; }
+        function p5() { var c = foo(), a = bar(), b = baz(); return a + b + c; }
+        function p6() { var c = foo(), b = bar(), a = baz(); return a + b + c; }
+
+        function q1() { var a = foo(), b = bar(), c = baz(); return fn(a + b + c); }
+        function q2() { var a = foo(), c = bar(), b = baz(); return fn(a + b + c); }
+        function q3() { var b = foo(), a = bar(), c = baz(); return fn(a + b + c); }
+        function q4() { var b = foo(), c = bar(), a = baz(); return fn(a + b + c); }
+        function q5() { var c = foo(), a = bar(), b = baz(); return fn(a + b + c); }
+        function q6() { var c = foo(), b = bar(), a = baz(); return fn(a + b + c); }
+
+        function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); }
+        function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); }
+        function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); }
+        function r4() { var b = foo(), c = bar(), a = baz(); return fn(a) + fn(b) + fn(c); }
+        function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); }
+        function r6() { var c = foo(), b = bar(), a = baz(); return fn(a) + fn(b) + fn(c); }
+
+        function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); }
+        function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); }
+
+        function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); }
+        function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); }
+    }
+    expect: {
+        function fn(x) { return console.log(x), x; }
+
+        function p1() { return foo() + bar() + baz(); }
+        function p2() { var a = foo(), c = bar(); return a + baz() + c; }
+        function p3() { var b = foo(); return bar() + b + baz(); }
+        function p4() { var b = foo(), c = bar(); return baz() + b + c; }
+        function p5() { var c = foo(); return bar() + baz() + c; }
+        function p6() { var c = foo(), b = bar(); return baz() + b + c; }
+
+        function q1() { return fn(foo() + bar() + baz()); }
+        function q2() { var a = foo(), c = bar(); return fn(a + baz() + c); }
+        function q3() { var b = foo(); return fn(bar() + b + baz()); }
+        function q4() { var b = foo(), c = bar(); return fn(baz() + b + c); }
+        function q5() { var c = foo(); return fn(bar() + baz() + c); }
+        function q6() { var c = foo(), b = bar(); return fn(baz() + b + c); }
+
+        function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); }
+        function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); }
+        function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); }
+        function r4() { var b = foo(), c = bar(); return fn(baz()) + fn(b) + fn(c); }
+        function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); }
+        function r6() { var c = foo(), b = bar(); return fn(baz()) + fn(b) + fn(c); }
+
+        function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); }
+        function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); }
+
+        function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); }
+        function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); }
+    }
+}
+
+collapse_vars_issue_721: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        define(["require", "exports", 'handlebars'], function (require, exports, hb) {
+            var win = window;
+            var _hb = win.Handlebars = hb;
+            return _hb;
+        });
+        def(function (hb) {
+            var win = window;
+            var prop = 'Handlebars';
+            var _hb = win[prop] = hb;
+            return _hb;
+        });
+        def(function (hb) {
+            var prop = 'Handlebars';
+            var win = window;
+            var _hb = win[prop] = hb;
+            return _hb;
+        });
+        def(function (hb) {
+            var prop = 'Handlebars';
+            var win = g();
+            var _hb = win[prop] = hb;
+            return _hb;
+        });
+        def(function (hb) {
+            var prop = g1();
+            var win = g2();
+            var _hb = win[prop] = hb;
+            return _hb;
+        });
+        def(function (hb) {
+            var win = g2();
+            var prop = g1();
+            var _hb = win[prop] = hb;
+            return _hb;
+        });
+    }
+    expect: {
+        define([ "require", "exports", "handlebars" ], function(require, exports, hb) {
+            return window.Handlebars = hb;
+        }),
+        def(function(hb) {
+            return window.Handlebars = hb;
+        }),
+        def(function(hb) {
+            return window.Handlebars = hb;
+        }),
+        def(function (hb) {
+            return g().Handlebars = hb;
+        }),
+        def(function (hb) {
+            var prop = g1();
+            return g2()[prop] = hb;
+        }),
+        def(function (hb) {
+            return g2()[g1()] = hb;
+        });
+    }
+}
+
+collapse_vars_properties: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1(obj) {
+            var prop = 'LiteralProperty';
+            return !!-+obj[prop];
+        }
+        function f2(obj) {
+            var prop1 = 'One';
+            var prop2 = 'Two';
+            return ~!!-+obj[prop1 + prop2];
+        }
+    }
+    expect: {
+        function f1(obj) {
+            return !!-+obj.LiteralProperty;
+        }
+        function f2(obj) {
+            return ~!!-+obj.OneTwo;
+        }
+    }
+}
+
+collapse_vars_if: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1() {
+            var not_used = sideeffect(), x = g1 + g2;
+            var y = x / 4, z = 'Bar' + y;
+            if ('x' != z) { return g9; }
+            else return g5;
+        }
+        function f2() {
+            var  x = g1 + g2, not_used = sideeffect();
+            var y = x / 4
+            var z = 'Bar' + y;
+            if ('x' != z) { return g9; }
+            else return g5;
+        }
+        function f3(x) {
+            if (x) {
+                var a = 1;
+                return a;
+            }
+            else {
+                var b = 2;
+                return b;
+            }
+        }
+    }
+    expect: {
+        function f1() {
+            sideeffect();
+            return "x" != "Bar" + (g1 + g2) / 4 ? g9 : g5;
+        }
+        function f2() {
+            var x = g1 + g2;
+            sideeffect();
+            return "x" != "Bar" + x / 4 ? g9 : g5;
+        }
+        function f3(x) {
+            if (x) {
+                return 1;
+            }
+            return 2;
+        }
+    }
+}
+
+collapse_vars_while: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:false, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1(y) {
+            // Neither the non-constant while condition `c` will be
+            // replaced, nor the non-constant `x` in the body.
+            var x = y, c = 3 - y;
+            while (c) { return x; }
+            var z = y * y;
+            return z;
+        }
+        function f2(y) {
+            // The constant `x` will be replaced in the while body.
+            var x = 7;
+            while (y) { return x; }
+            var z = y * y;
+            return z;
+        }
+        function f3(y) {
+            // The non-constant `n` will not be replaced in the while body.
+            var n = 5 - y;
+            while (y) { return n; }
+            var z = y * y;
+            return z;
+        }
+    }
+    expect: {
+        function f1(y) {
+            var x = y, c = 3 - y;
+            while (c) return x;
+            return y * y;
+        }
+        function f2(y) {
+            while (y) return 7;
+            return y * y
+        }
+        function f3(y) {
+            var n = 5 - y;
+            while (y) return n;
+            return y * y;
+        }
+    }
+}
+
+collapse_vars_do_while: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1(y) {
+            // The constant do-while condition `c` will be replaced.
+            var c = 9;
+            do { } while (c === 77);
+        }
+        function f2(y) {
+            // The non-constant do-while condition `c` will not be replaced.
+            var c = 5 - y;
+            do { } while (c);
+        }
+        function f3(y) {
+            // The constant `x` will be replaced in the do loop body.
+            function fn(n) { console.log(n); }
+            var a = 2, x = 7;
+            do {
+                fn(a = x);
+                break;
+            } while (y);
+        }
+        function f4(y) {
+            // The non-constant `a` will not be replaced in the do loop body.
+            var a = y / 4;
+            do {
+                return a;
+            } while (y);
+        }
+        function f5(y) {
+            function p(x) { console.log(x); }
+            do {
+                // The non-constant `a` will be replaced in p(a)
+                // because it is declared in same block.
+                var a = y - 3;
+                p(a);
+            } while (--y);
+        }
+    }
+    expect: {
+        function f1(y) {
+            do ; while (false);
+        }
+        function f2(y) {
+            var c = 5 - y;
+            do ; while (c);
+        }
+        function f3(y) {
+            function fn(n) { console.log(n); }
+            var a = 2;
+            do {
+                fn(a = 7);
+                break;
+            } while (y);
+        }
+        function f4(y) {
+            var a = y / 4;
+            do
+                return a;
+            while (y);
+        }
+        function f5(y) {
+            function p(x) { console.log(x); }
+            do {
+                p(y - 3);
+            } while (--y);
+        }
+    }
+}
+
+collapse_vars_seq: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        var f1 = function(x, y) {
+            var a, b, r = x + y, q = r * r, z = q - r;
+            a = z, b = 7;
+            return a + b;
+        };
+    }
+    expect: {
+        var f1 = function(x, y) {
+            var a, b, r = x + y;
+            return a = r * r - r, b = 7, a + b
+        };
+    }
+}
+
+collapse_vars_throw: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        var f1 = function(x, y) {
+            var a, b, r = x + y, q = r * r, z = q - r;
+            a = z, b = 7;
+            throw a + b;
+        };
+    }
+    expect: {
+        var f1 = function(x, y) {
+            var a, b, r = x + y;
+            throw a = r * r - r, b = 7, a + b
+        };
+    }
+}
+
+collapse_vars_switch: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1() {
+            var not_used = sideeffect(), x = g1 + g2;
+            var y = x / 4, z = 'Bar' + y;
+            switch (z) { case 0: return g9; }
+        }
+        function f2() {
+            var  x = g1 + g2, not_used = sideeffect();
+            var y = x / 4
+            var z = 'Bar' + y;
+            switch (z) { case 0: return g9; }
+        }
+        function f3(x) {
+            switch(x) { case 1: var a = 3 - x; return a; }
+        }
+    }
+    expect: {
+        function f1() {
+            sideeffect();
+            switch ("Bar" + (g1 + g2) / 4) { case 0: return g9 }
+        }
+        function f2() {
+            var x = g1 + g2;
+            sideeffect();
+            switch ("Bar" + x / 4) { case 0: return g9 }
+        }
+        function f3(x) {
+            // verify no extraneous semicolon in case block before return
+            // when the var definition was eliminated
+            switch(x) { case 1: return 3 - x; }
+        }
+    }
+}
+
+collapse_vars_assignment: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function log(x) { return console.log(x), x; }
+        function f0(c) {
+            var a = 3 / c;
+            return a = a;
+        }
+        function f1(c) {
+            const a = 3 / c;
+            const b = 1 - a;
+            return b;
+        }
+        function f2(c) {
+            var a = 3 / c;
+            var b = a - 7;
+            return log(c = b);
+        }
+        function f3(c) {
+            var a = 3 / c;
+            var b = a - 7;
+            return log(c |= b);
+        }
+        function f4(c) {
+            var a = 3 / c;
+            var b = 2;
+            return log(b += a);
+        }
+        function f5(c) {
+            var b = 2;
+            var a = 3 / c;
+            return log(b += a);
+        }
+        function f6(c) {
+            var b = g();
+            var a = 3 / c;
+            return log(b += a);
+        }
+    }
+    expect: {
+        function log(x) { return console.log(x), x; }
+        function f0(c) {
+            var a = 3 / c;
+            return a = a;
+        }
+        function f1(c) {
+            return 1 - 3 / c
+        }
+        function f2(c) {
+            return log(c = 3 / c - 7);
+        }
+        function f3(c) {
+            return log(c |= 3 / c - 7);
+        }
+        function f4(c) {
+            var b = 2;
+            return log(b += 3 / c);
+        }
+        function f5(c) {
+            var b = 2;
+            return log(b += 3 / c);
+        }
+        function f6(c) {
+            var b = g();
+            return log(b += 3 / c);
+        }
+    }
+}
+
+collapse_vars_lvalues: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f0(x) { var i = ++x; return x += i; }
+        function f1(x) { var a = (x -= 3); return x += a; }
+        function f2(x) { var z = x, a = ++z; return z += a; }
+        function f3(x) { var a = (x -= 3), b = x + a; return b; }
+        function f4(x) { var a = (x -= 3); return x + a; }
+        function f5(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return b - c; }
+        function f6(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return c - b; }
+        function f7(x) { var w = e1(), v = e2(), c = v - x, b = w = x; return b - c; }
+        function f8(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return b - c; }
+        function f9(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return c - b; }
+    }
+    expect: {
+        function f0(x) { var i = ++x; return x += i; }
+        function f1(x) { var a = (x -= 3); return x += a; }
+        function f2(x) { var z = x, a = ++z; return z += a; }
+        function f3(x) { var a = (x -= 3); return x + a; }
+        function f4(x) { var a = (x -= 3); return x + a; }
+        function f5(x) { var w = e1(), v = e2(), c = v = --x; return (w = x) - c; }
+        function f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); }
+        function f7(x) { var w = e1(), v = e2(), c = v - x; return (w = x) - c; }
+        function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); }
+        function f9(x) { var w = e1(); return e2() - x - (w = x); }
+
+    }
+}
+
+collapse_vars_misc1: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f0(o, a, h) {
+            var b = 3 - a;
+            var obj = o;
+            var seven = 7;
+            var prop = 'run';
+            var t = obj[prop](b)[seven] = h;
+            return t;
+        }
+        function f1(x) { var y = 5 - x; return y; }
+        function f2(x) { const z = foo(), y = z / (5 - x); return y; }
+        function f3(x) { var z = foo(), y = (5 - x) / z; return y; }
+        function f4(x) { var z = foo(), y = (5 - u) / z; return y; }
+        function f5(x) { const z = foo(), y = (5 - window.x) / z; return y; }
+        function f6() { var b = window.a * window.z; return b && zap(); }
+        function f7() { var b = window.a * window.z; return b + b; }
+        function f8() { var b = window.a * window.z; var c = b + 5; return b + c; }
+        function f9() { var b = window.a * window.z; return bar() || b; }
+        function f10(x) { var a = 5, b = 3; return a += b; }
+        function f11(x) { var a = 5, b = 3; return a += --b; }
+    }
+    expect: {
+        function f0(o, a, h) {
+            var b = 3 - a;
+            return o.run(b)[7] = h;
+        }
+        function f1(x) { return 5 - x }
+        function f2(x) { return foo() / (5 - x) }
+        function f3(x) { return (5 - x) / foo() }
+        function f4(x) { var z = foo(); return (5 - u) / z }
+        function f5(x) { const z = foo(); return (5 - window.x) / z }
+        function f6() { return window.a * window.z && zap() }
+        function f7() { var b = window.a * window.z; return b + b }
+        function f8() { var b = window.a * window.z; return b + (b + 5) }
+        function f9() { var b = window.a * window.z; return bar() || b }
+        function f10(x) { var a = 5; return a += 3; }
+        function f11(x) { var a = 5, b = 3; return a += --b; }
+    }
+}
+
+collapse_vars_self_reference: {
+    options = {
+        collapse_vars:true, unused:false,
+        sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        // avoid bug in self-referential declaration.
+        function f1() {
+            var self = {
+                inner: function() { return self; }
+            };
+        }
+        function f2() {
+            var self = { inner: self };
+        }
+    }
+    expect: {
+        // note: `unused` option is false
+        function f1() {
+            var self = {
+                inner: function() { return self }
+            };
+        }
+        function f2() {
+            var self = { inner: self };
+        }
+    }
+}
+
+collapse_vars_repeated: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1() {
+            var dummy = 3, a = 5, unused = 2, a = 1, a = 3;
+            return -a;
+        }
+        function f2() {
+            var a = 3, a = a + 2;
+            return a;
+        }
+    }
+    expect: {
+        function f1() {
+            return -3
+        }
+        function f2() {
+            var a = 3, a = a + 2;
+            return a
+        }
+    }
+}
+
+collapse_vars_closures: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function constant_vars_can_be_replaced_in_any_scope() {
+            var outer = 3;
+            return function() { return outer; }
+        }
+        function non_constant_vars_can_only_be_replace_in_same_scope(x) {
+            var outer = x;
+            return function() { return outer; }
+        }
+    }
+    expect: {
+        function constant_vars_can_be_replaced_in_any_scope() {
+            return function() { return 3 }
+        }
+        function non_constant_vars_can_only_be_replace_in_same_scope(x) {
+            var outer = x
+            return function() { return outer }
+        }
+    }
+}
+
+collapse_vars_unary: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f0(o, p) {
+            var x = o[p];
+            delete x;
+        }
+        function f1(n) {
+            var k = !!n;
+            return n > +k;
+        }
+        function f2(n) {
+            // test unary with constant
+            var k = 7;
+            return k--;
+        }
+        function f3(n) {
+            // test unary with constant
+            var k = 7;
+            return ++k;
+        }
+        function f4(n) {
+            // test unary with non-constant
+            var k = 8 - n;
+            return k--;
+        }
+        function f5(n) {
+            // test unary with non-constant
+            var k = 9 - n;
+            return ++k;
+        }
+    }
+    expect: {
+        function f0(o, p) {
+            delete o[p];
+        }
+        function f1(n) {
+            return n > +!!n
+        }
+        function f2(n) {
+            var k = 7;
+            return k--
+        }
+        function f3(n) {
+            var k = 7;
+            return ++k
+        }
+        function f4(n) {
+            var k = 8 - n;
+            return k--;
+        }
+        function f5(n) {
+            var k = 9 - n;
+            return ++k;
+        }
+    }
+}
+
+collapse_vars_try: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1() {
+            try {
+                var a = 1;
+                return a;
+            }
+            catch (ex) {
+                var b = 2;
+                return b;
+            }
+            finally {
+                var c = 3;
+                return c;
+            }
+        }
+        function f2() {
+            var t = could_throw(); // shouldn't be replaced in try block
+            try {
+                return t + might_throw();
+            }
+            catch (ex) {
+                return 3;
+            }
+        }
+    }
+    expect: {
+        function f1() {
+            try {
+                return 1;
+            }
+            catch (ex) {
+                return 2;
+            }
+            finally {
+                return 3;
+            }
+        }
+        function f2() {
+            var t = could_throw();
+            try {
+                return t + might_throw();
+            }
+            catch (ex) {
+                return 3;
+            }
+        }
+    }
+}
+
+collapse_vars_array: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1(x, y) {
+            var z = x + y;
+            return [z];
+        }
+        function f2(x, y) {
+            var z = x + y;
+            return [x, side_effect(), z];
+        }
+        function f3(x, y) {
+            var z = f(x + y);
+            return [ [3], [z, x, y], [g()] ];
+        }
+    }
+    expect: {
+        function f1(x, y) {
+            return [x + y]
+        }
+        function f2(x, y) {
+            var z = x + y
+            return [x, side_effect(), z]
+        }
+        function f3(x, y) {
+            return [ [3], [f(x + y), x, y], [g()] ]
+        }
+    }
+}
+
+collapse_vars_object: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f0(x, y) {
+            var z = x + y;
+            return {
+                get b() { return 7; },
+                r: z
+            };
+        }
+        function f1(x, y) {
+            var z = x + y;
+            return {
+                r: z,
+                get b() { return 7; }
+            };
+        }
+        function f2(x, y) {
+            var z = x + y;
+            var k = x - y;
+            return {
+                q: k,
+                r: g(x),
+                s: z
+            };
+        }
+        function f3(x, y) {
+            var z = f(x + y);
+            return [{
+                a: {q: x, r: y, s: z},
+                b: g()
+            }];
+        }
+    }
+    expect: {
+        function f0(x, y) {
+            var z = x + y;
+            return {
+                get b() { return 7; },
+                r: z
+            };
+        }
+        function f1(x, y) {
+            return {
+                r: x + y,
+                get b() { return 7; }
+            };
+        }
+        function f2(x, y) {
+            var z = x + y;
+            return {
+                q: x - y,
+                r: g(x),
+                s: z
+            };
+        }
+        function f3(x, y) {
+            return [{
+                a: {q: x, r: y, s: f(x + y)},
+                b: g()
+            }];
+        }
+    }
+}
+
+collapse_vars_eval_and_with: {
+    options = {
+        collapse_vars:true, sequences:false, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        // Don't attempt to collapse vars in presence of eval() or with statement.
+        (function f0() {
+            var a = 2;
+            console.log(a - 5);
+            eval("console.log(a);");
+        })();
+        (function f1() {
+            var o = {a: 1}, a = 2;
+            with (o) console.log(a);
+        })();
+        (function f2() {
+            var o = {a: 1}, a = 2;
+            return function() { with (o) console.log(a) };
+        })()();
+    }
+    expect: {
+        (function f0() {
+            var a = 2;
+            console.log(a - 5);
+            eval("console.log(a);");
+        })();
+        (function f1() {
+            var o = {a: 1}, a = 2;
+            with(o) console.log(a);
+        })();
+        (function f2() {
+            var o = {a: 1}, a = 2;
+            return function() { with (o) console.log(a) };
+        })()();
+    }
+}
+
+collapse_vars_constants: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        function f1(x) {
+            var a = 4, b = x.prop, c = 5, d = sideeffect1(), e = sideeffect2();
+            return b + (function() { return d - a * e - c; })();
+        }
+        function f2(x) {
+            var a = 4, b = x.prop, c = 5, not_used = sideeffect1(), e = sideeffect2();
+            return b + (function() { return -a * e - c; })();
+        }
+        function f3(x) {
+            var a = 4, b = x.prop, c = 5, not_used = sideeffect1();
+            return b + (function() { return -a - c; })();
+        }
+    }
+    expect: {
+        function f1(x) {
+            var b = x.prop, d = sideeffect1(), e = sideeffect2();
+            return b + (function() { return d - 4 * e - 5; })();
+        }
+        function f2(x) {
+            var b = x.prop, e = (sideeffect1(), sideeffect2());
+            return b + (function() { return -4 * e - 5; })();
+        }
+        function f3(x) {
+            var b = x.prop;
+            sideeffect1();
+            return b + (function() { return -9; })();
+        }
+    }
+}
+
+collapse_vars_arguments: {
+    options = {
+        collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true,
+        comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true,
+        keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true
+    }
+    input: {
+        var outer = function() {
+            // Do not replace `arguments` but do replace the constant `k` before it.
+            var k = 7, arguments = 5, inner = function() { console.log(arguments); }
+            inner(k, 1);
+        }
+        outer();
+    }
+    expect: {
+        (function() {
+            (function(){console.log(arguments);})(7, 1);
+        })();
+    }
+}
+