From 36bca6934de494a79a1c1af952653eaaf9c15ed8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 28 Feb 2018 15:19:32 +0800 Subject: [PATCH] enhance `collapse_vars` (#2952) - `a = b, b` => `a = b` - `a.b = c, c()` => `(a.b = c)()` --- lib/compress.js | 84 +++++-- test/compress/collapse_vars.js | 389 +++++++++++++++++++++++++++++++++ test/compress/pure_getters.js | 331 ++++++++++++++++++++++++++++ test/compress/sequences.js | 3 +- 4 files changed, 782 insertions(+), 25 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f9382f0d..ed094874 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -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; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 60505509..d4ab444e 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -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" +} diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index e2662868..f9edcc8d 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -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" +} diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 03075bf1..12acbcf7 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -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; } } -- 2.34.1