enhance collapse_vars

- extend expression types
  - `a++`
  - `a=x;`
- extend scan range
  - `for(init;;);`
  - `switch(expr){case expr:}`
  - `a = x; a = a || y;`
- terminate upon `debugger;`

fixes #27
fixes #1858
This commit is contained in:
alexlamsl 2017-05-01 21:43:49 +08:00
parent 5a25d24b56
commit 6c40202625
3 changed files with 600 additions and 170 deletions

View File

@ -512,6 +512,10 @@ merge(Compressor.prototype, {
return fixed(); return fixed();
}); });
function is_lhs_read_only(lhs) {
return lhs instanceof AST_SymbolRef && lhs.definition().orig[0] instanceof AST_SymbolLambda;
}
function find_variable(compressor, name) { function find_variable(compressor, name) {
var scope, i = 0; var scope, i = 0;
while (scope = compressor.parent(i++)) { while (scope = compressor.parent(i++)) {
@ -643,174 +647,223 @@ merge(Compressor.prototype, {
statements = join_consecutive_vars(statements, compressor); statements = join_consecutive_vars(statements, compressor);
} }
if (compressor.option("collapse_vars")) { if (compressor.option("collapse_vars")) {
statements = collapse_single_use_vars(statements, compressor); statements = collapse(statements, compressor);
} }
} while (CHANGED && max_iter-- > 0); } while (CHANGED && max_iter-- > 0);
return statements; return statements;
function collapse_single_use_vars(statements, compressor) { // Search from right to left for assignment-like expressions:
// Iterate statements backwards looking for a statement with a var/const // - `var a = x;`
// declaration immediately preceding it. Grab the rightmost var definition // - `a = x;`
// and if it has exactly one reference then attempt to replace its reference // - `++a`
// in the statement with the var value and then erase the var definition. // For each candidate, scan from left to right for first usage, then try
// to fold assignment into the site for compression.
// Will not attempt to collapse assignments into or past code blocks
// which are not sequentially executed, e.g. loops and conditionals.
function collapse(statements, compressor) {
var scope = compressor.find_parent(AST_Scope); var scope = compressor.find_parent(AST_Scope);
var stat_index; if (scope.uses_eval || scope.uses_with) return statements;
var prev_stat_index; var candidates = [];
var def_stat_index; var stat_index = statements.length;
var stat; while (--stat_index >= 0) {
var var_defs; extract_candidates(statements[stat_index]);
var var_defs_index; while (candidates.length > 0) {
for (stat_index = statements.length; --stat_index >= 0;) { var candidate = candidates.pop();
stat = statements[stat_index]; var lhs = get_lhs(candidate);
// Scan variable definitions from right to left. if (!lhs || is_lhs_read_only(lhs)) continue;
if (stat instanceof AST_Definitions) { var lvalues = get_lvalues(candidate);
prev_stat_index = stat_index; var side_effects = value_has_side_effects(candidate);
var_defs = stat.definitions; var hit = false, abort = false, replaced = false;
for (def_stat_index = var_defs.length; --def_stat_index >= 1;) { var tt = new TreeTransformer(function(node, descend) {
stat = var_defs[def_stat_index]; if (abort) return node;
scan_var_defs(def_stat_index); // Skip nodes before `candidate` as quickly as possible
if (!hit) {
if (node === candidate) {
hit = true;
return node;
}
return;
}
// Stop immediately if these node types are encountered
var parent = tt.parent();
if (node instanceof AST_Debugger
|| node instanceof AST_IterationStatement && !(node instanceof AST_For)
|| node instanceof AST_SymbolRef && node.undeclared()
|| node instanceof AST_Try
|| node instanceof AST_With
|| parent instanceof AST_For && node !== parent.init) {
abort = true;
return node;
}
// Replace variable with assignment when found
if (lhs.equivalent_to(node)) {
abort = true;
if (is_lhs(node, parent)) return node;
CHANGED = replaced = true;
compressor.info("Collapsing {name} [{file}:{line},{col}]", {
name: node.print_to_string(),
file: node.start.file,
line: node.start.line,
col: node.start.col
});
if (candidate instanceof AST_UnaryPostfix) {
return make_node(AST_UnaryPrefix, candidate, candidate);
}
if (candidate instanceof AST_VarDef) {
if (candidate.name.definition().references.length == 1) {
return maintain_this_binding(parent, node, candidate.value);
}
return make_node(AST_Assign, candidate, {
operator: "=",
left: make_node(AST_SymbolRef, candidate.name, candidate.name),
right: candidate.value
});
}
return candidate;
}
if (lhs instanceof AST_SymbolRef) {
// `a = x; a = a + y;` => `a = x + y;`
if (node instanceof AST_Assign
&& node.operator == "="
&& lhs.equivalent_to(node.left)) {
node.right = node.right.transform(tt);
abort = true;
return node;
}
// `var a = x, a = a + y;` => `var a = x + y;`
if (node instanceof AST_VarDef
&& node.value
&& node.name.definition() === lhs.definition()) {
node.value = node.value.transform(tt);
abort = true;
return node;
}
}
// These node types have child nodes that execute sequentially,
// but are otherwise not safe to scan into or beyond them.
var sym;
if (node instanceof AST_Call
|| node instanceof AST_Exit
|| node instanceof AST_PropAccess
|| node instanceof AST_SymbolRef
&& (lvalues && lvalues[node.name]
|| side_effects && !references_in_scope(node.definition()))
|| lvalues && (sym = is_lhs(node.left, node)) && get_symbol(sym).name in lvalues
|| parent instanceof AST_Binary
&& (parent.operator == "&&" || parent.operator == "||")
|| parent instanceof AST_Case
|| parent instanceof AST_Conditional
|| parent instanceof AST_For
|| parent instanceof AST_If) {
if (!(node instanceof AST_Scope)) descend(node, tt);
abort = true;
return node;
}
// Skip (non-executed) functions and (leading) default case in switch statements
if (node instanceof AST_Default || node instanceof AST_Scope) return node;
});
for (var i = stat_index; !abort && i < statements.length; i++) {
statements[i].transform(tt);
} }
} else if (stat_index > 0) { if (replaced && !remove_candidate(candidate)) statements.splice(stat_index, 1);
// The variable definition must precede a statement.
prev_stat_index = stat_index - 1;
var prev_stat = statements[prev_stat_index];
if (!(prev_stat instanceof AST_Definitions)) continue;
var_defs = prev_stat.definitions;
scan_var_defs(var_defs.length);
} }
} }
return statements; return statements;
function scan_var_defs(end_pos) { function extract_candidates(expr) {
var var_names_seen = Object.create(null); if (expr instanceof AST_Assign && !expr.left.has_side_effects(compressor)
var side_effects_encountered = false; || expr instanceof AST_Unary && (expr.operator == "++" || expr.operator == "--")) {
var lvalues_encountered = false; candidates.push(expr);
var lvalues = Object.create(null); } else if (expr instanceof AST_Sequence) {
for (var_defs_index = end_pos; --var_defs_index >= 0;) { expr.expressions.forEach(extract_candidates);
var var_decl = var_defs[var_defs_index]; } else if (expr instanceof AST_Definitions) {
// `drop_unused()` shuffles variables without values to the top, expr.definitions.forEach(function(var_def) {
// so we can terminate upon first sighting as an optimization. if (var_def.value) {
if (var_decl.value == null) break; candidates.push(var_def);
var var_name = var_decl.name.name;
// Bail if we've seen a var definition of same name before.
if (var_name in var_names_seen) break;
var_names_seen[var_name] = true;
// Only interested in non-constant values.
if (var_decl.value.is_constant()) continue;
// Only interested in cases with just one reference to the variable.
var def = var_decl.name.definition();
if (def.references.length !== 1
|| var_name == "arguments"
|| def.global && !compressor.toplevel(def)) {
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;
// Restrict var replacement to constants if side effects encountered.
if (side_effects_encountered |= lvalues_encountered) continue;
var value_has_side_effects = var_decl.value.has_side_effects(compressor);
// Non-constant single use vars can only be replaced in same scope.
if (ref.scope !== scope) {
side_effects_encountered |= value_has_side_effects;
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); } else if (expr instanceof AST_SimpleStatement) {
extract_candidates(expr.body);
} else if (expr instanceof AST_For && expr.init) {
extract_candidates(expr.init);
}
}
// Replace the non-constant single use var in statement if side effect free. function get_lhs(expr) {
var unwind = false; if (expr instanceof AST_VarDef) {
var tt = new TreeTransformer( var def = expr.name.definition();
function preorder(node) { if (def.orig.length > 1
if (unwind || node instanceof AST_Scope && node !== scope) return node; || def.references.length == 1 && (!def.global || compressor.toplevel(def))) {
var parent = tt.parent(); return make_node(AST_SymbolRef, expr.name, expr.name);
if (node instanceof AST_Try }
|| node instanceof AST_With } else {
|| node instanceof AST_Case return expr[expr instanceof AST_Assign ? "left" : "expression"];
|| node instanceof AST_IterationStatement }
|| (parent instanceof AST_If && node !== parent.condition) }
|| (parent instanceof AST_Conditional && node !== parent.condition)
|| (node instanceof AST_SymbolRef function get_symbol(node) {
&& value_has_side_effects while (node instanceof AST_PropAccess) node = node.expression;
&& !are_references_in_scope(node.definition(), scope)) return node;
|| (parent instanceof AST_Binary }
&& (parent.operator == "&&" || parent.operator == "||")
&& node === parent.right) function get_lvalues(expr) {
|| (parent instanceof AST_Switch && node !== parent.expression)) { if (expr instanceof AST_Unary) return;
return side_effects_encountered = unwind = true, node; var lvalues, scope;
} var tw = new TreeWalker(function(node, descend) {
function are_references_in_scope(def, scope) { if (node instanceof AST_Scope) {
if (def.orig.length === 1 var save_scope = scope;
&& def.orig[0] instanceof AST_SymbolDefun) return true; descend();
if (def.scope !== scope) return false; scope = save_scope;
var refs = def.references; return true;
for (var i = 0, len = refs.length; i < len; i++) { }
if (refs[i].scope !== scope) return false; if (node instanceof AST_SymbolRef || node instanceof AST_PropAccess) {
} var sym = get_symbol(node);
return true; if (sym instanceof AST_SymbolRef) {
} if (!lvalues) lvalues = Object.create(null);
}, lvalues[sym.name] = lvalues[sym.name] || is_lhs(node, tw.parent());
function postorder(node) {
if (unwind) return node;
if (node === ref)
return unwind = true, replace_var(var_decl, 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); });
} expr[expr instanceof AST_Assign ? "right" : "value"].walk(tw);
} return lvalues;
}
function is_lvalue(node, parent) {
return node instanceof AST_SymbolRef && is_lhs(node, parent); function remove_candidate(expr) {
} var found = false;
return statements[stat_index].transform(new TreeTransformer(function(node, descend, in_list) {
function replace_var(var_decl, node, parent, is_constant) { if (found) return node;
if (is_lvalue(node, parent)) return node; if (node === expr) {
found = true;
// Remove var definition and return its value to the TreeTransformer to replace. if (node instanceof AST_VarDef) {
var value = maintain_this_binding(parent, node, var_decl.value); var orig = node.name.definition().orig;
var_decl.value = null; orig.splice(orig.indexOf(node.name), 1);
}
var_defs.splice(var_defs_index, 1); return in_list ? MAP.skip : null;
def_stat_index--; }
if (var_defs.length === 0) { }, function(node) {
statements.splice(prev_stat_index, 1); if (node instanceof AST_Sequence) switch (node.expressions.length) {
stat_index--; case 0: return null;
} case 1: return node.expressions[0];
// Further optimize statement after substitution. }
stat.reset_opt_flags(compressor); if (node instanceof AST_Definitions && node.definitions.length == 0
|| node instanceof AST_SimpleStatement && !node.body) {
compressor.info("Collapsing {type} {name} [{file}:{line},{col}]", { return null;
type: is_constant ? "constant" : "variable", }
name: var_decl.name.name, }));
file: node.start.file, }
line: node.start.line,
col: node.start.col function value_has_side_effects(expr) {
if (expr instanceof AST_Unary) return false;
return expr[expr instanceof AST_Assign ? "right" : "value"].has_side_effects(compressor);
}
function references_in_scope(def) {
if (def.orig.length == 1 && def.orig[0] instanceof AST_SymbolDefun) return true;
if (def.scope !== scope) return false;
return def.references.every(function(ref) {
return ref.scope === scope;
}); });
CHANGED = true;
return value;
} }
} }
@ -3130,9 +3183,7 @@ merge(Compressor.prototype, {
&& (left.operator == "++" || left.operator == "--")) { && (left.operator == "++" || left.operator == "--")) {
left = left.expression; left = left.expression;
} else left = null; } else left = null;
if (!left || if (!left || is_lhs_read_only(left)) {
left instanceof AST_SymbolRef
&& left.definition().orig[0] instanceof AST_SymbolLambda) {
expressions[++i] = cdr; expressions[++i] = cdr;
continue; continue;
} }

View File

@ -68,11 +68,10 @@ collapse_vars_side_effects_1: {
log(x, s.charAt(i++), y, 7); log(x, s.charAt(i++), y, 7);
} }
function f4() { function f4() {
var log = console.log.bind(console), var i = 10,
i = 10,
x = i += 2, x = i += 2,
y = i += 3; y = i += 3;
log(x, i += 4, y, i); console.log.bind(console)(x, i += 4, y, i);
} }
f1(), f2(), f3(), f4(); f1(), f2(), f3(), f4();
} }
@ -671,8 +670,8 @@ collapse_vars_lvalues: {
function f4(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 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 f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); }
function f7(x) { var w = e1(), c = e2() - x; return (w = x) - c; } function f7(x) { var w = e1(); return (w = x) - (e2() - x); }
function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } function f8(x) { var w = e1(); return (w = x) - (e2() - x); }
function f9(x) { var w = e1(); return e2() - x - (w = x); } function f9(x) { var w = e1(); return e2() - x - (w = x); }
} }
} }
@ -703,8 +702,8 @@ collapse_vars_lvalues_drop_assign: {
function f4(x) { var a = (x -= 3); return x + a; } function f4(x) { var a = (x -= 3); return x + a; }
function f5(x) { e1(); var v = e2(), c = v = --x; return x - c; } function f5(x) { e1(); var v = e2(), c = v = --x; return x - c; }
function f6(x) { e1(), e2(); return --x - x; } function f6(x) { e1(), e2(); return --x - x; }
function f7(x) { e1(); var c = e2() - x; return x - c; } function f7(x) { e1(); return x - (e2() - x); }
function f8(x) { e1(); var v = e2(); return x - (v - x); } function f8(x) { e1(); return x - (e2() - x); }
function f9(x) { e1(); return e2() - x - x; } function f9(x) { e1(); return e2() - x - x; }
} }
} }
@ -1047,10 +1046,9 @@ collapse_vars_object: {
} }
expect: { expect: {
function f0(x, y) { function f0(x, y) {
var z = x + y;
return { return {
get b() { return 7; }, get b() { return 7; },
r: z r: x + y
}; };
} }
function f1(x, y) { function f1(x, y) {
@ -1677,3 +1675,387 @@ var_defs: {
} }
expect_stdout: "97" expect_stdout: "97"
} }
assignment: {
options = {
collapse_vars: true,
unused: true,
}
input: {
function f() {
var a;
a = x;
return a;
}
}
expect: {
function f() {
return x;
}
}
}
for_init: {
options = {
collapse_vars: true,
unused: true,
}
input: {
function f(x, y) {
var a = x;
var b = y;
for (a; b;);
}
}
expect: {
function f(x, y) {
var b = y;
for (x; b;);
}
}
}
switch_case: {
options = {
collapse_vars: true,
unused: true,
}
input: {
function f(x, y, z) {
var a = x();
var b = y();
var c = z;
switch (a) {
default: d();
case b: e();
case c: f();
}
}
}
expect: {
function f(x, y, z) {
var c = z;
switch (x()) {
default: d();
case y(): e();
case c: f();
}
}
}
}
issue_27: {
options = {
collapse_vars: true,
unused: true,
}
input: {
(function(jQuery) {
var $;
$ = jQuery;
$("body").addClass("foo");
})(jQuery);
}
expect: {
(function(jQuery) {
jQuery("body").addClass("foo");
})(jQuery);
}
}
modified: {
options = {
collapse_vars: true,
unused: true,
}
input: {
function f1(b) {
var a = b;
return b + a;
}
function f2(b) {
var a = b;
return b++ + a;
}
function f3(b) {
var a = b++;
return b + a;
}
function f4(b) {
var a = b++;
return b++ + a;
}
function f5(b) {
var a = function() {
return b;
}();
return b++ + a;
}
console.log(f1(1), f2(1), f3(1), f4(1), f5(1));
}
expect: {
function f1(b) {
return b + b;
}
function f2(b) {
var a = b;
return b++ + a;
}
function f3(b) {
var a = b++;
return b + a;
}
function f4(b) {
var a = b++;
return b++ + a;
}
function f5(b) {
var a = function() {
return b;
}();
return b++ + a;
}
console.log(f1(1), f2(1), f3(1), f4(1), f5(1));
}
expect_stdout: "2 2 3 3 2"
}
issue_1858: {
options = {
collapse_vars: true,
pure_getters: true,
unused: true,
}
input: {
console.log(function(x) {
var a = {}, b = a.b = x;
return a.b + b;
}(1));
}
expect: {
console.log(function(x) {
var a = {}, b = a.b = x;
return a.b + b;
}(1));
}
expect_stdout: "2"
}
anonymous_function: {
options = {
collapse_vars: true,
}
input: {
console.log(function f(a) {
f ^= 0;
return f * a;
}(1));
}
expect: {
console.log(function f(a) {
f ^= 0;
return f * a;
}(1));
}
expect_stdout: true
}
side_effects_property: {
options = {
collapse_vars: true,
}
input: {
var a = [];
var b = 0;
a[b++] = function() { return 42;};
var c = a[b++]();
console.log(c);
}
expect: {
var a = [];
var b = 0;
a[b++] = function() { return 42;};
var c = a[b++]();
console.log(c);
}
expect_stdout: true
}
undeclared: {
options = {
collapse_vars: true,
unused: true,
}
input: {
function f(x, y) {
var a;
a = x;
b = y;
return b + a;
}
}
expect: {
function f(x, y) {
var a;
a = x;
b = y;
return b + a;
}
}
}
ref_scope: {
options = {
collapse_vars: true,
unused: true,
}
input: {
console.log(function() {
var a = 1, b = 2, c = 3;
var a = c++, b = b /= a;
return function() {
return a;
}() + b;
}());
}
expect: {
console.log(function() {
var a = 1, b = 2, c = 3;
b = b /= a = c++;
return function() {
return a;
}() + b;
}());
}
expect_stdout: true
}
chained_1: {
options = {
collapse_vars: true,
}
input: {
var a = 2;
var a = 3 / a;
console.log(a);
}
expect: {
var a = 3 / (a = 2);
console.log(a);
}
expect_stdout: true
}
chained_2: {
options = {
collapse_vars: true,
}
input: {
var a;
var a = 2;
a = 3 / a;
console.log(a);
}
expect: {
var a;
a = 3 / (a = 2);
console.log(a);
}
expect_stdout: true
}
chained_3: {
options = {
collapse_vars: true,
unused: true,
}
input: {
console.log(function(a, b) {
var c = a, c = b;
b++;
return c;
}(1, 2));
}
expect: {
console.log(function(a, b) {
var c = a, c = b;
b++;
return c;
}(1, 2));
}
expect_stdout: "2"
}
boolean_binary_1: {
options = {
collapse_vars: true,
}
input: {
var a = 1;
a++;
(function() {} || a || 3).toString();
console.log(a);
}
expect: {
var a = 1;
a++;
(function() {} || a || 3).toString();
console.log(a);
}
expect_stdout: true
}
boolean_binary_2: {
options = {
collapse_vars: true,
}
input: {
var c = 0;
c += 1;
(function() {
c = 1 + c;
} || 9).toString();
console.log(c);
}
expect: {
var c = 0;
c += 1;
(function() {
c = 1 + c;
} || 9).toString();
console.log(c);
}
expect_stdout: true
}
inner_lvalues: {
options = {
collapse_vars: true,
unused: true,
}
input: {
var a, b = 10;
var a = (--b || a || 3).toString(), c = --b + -a;
console.log(null, a, b);
}
expect: {
var a, b = 10;
var a = (--b || a || 3).toString(), c = --b + -a;
console.log(null, a, b);
}
expect_stdout: true
}
double_def: {
options = {
collapse_vars: true,
}
input: {
var a = x, a = a && y;
a();
}
expect: {
var a = x;
(a = a && y)();
}
}

View File

@ -18,9 +18,7 @@ chained_evaluation_1: {
expect: { expect: {
(function() { (function() {
(function() { (function() {
var c; f(1).bar = 1;
c = f(1);
c.bar = 1;
})(); })();
})(); })();
} }
@ -46,9 +44,8 @@ chained_evaluation_2: {
expect: { expect: {
(function() { (function() {
(function() { (function() {
var c, b = "long piece of string"; var b = "long piece of string";
c = f(b); f(b).bar = b;
c.bar = b;
})(); })();
})(); })();
} }