improve collapse_vars on AST_Var (#1828)

Perform the same cascaded scanning within `var` statement as we do on array of statements.
This commit is contained in:
Alex Lam S.L
2017-04-19 04:49:09 +08:00
committed by GitHub
parent b4b9305db0
commit 4dcff038cb
2 changed files with 123 additions and 95 deletions

View File

@@ -644,21 +644,21 @@ merge(Compressor.prototype, {
var scope = compressor.find_parent(AST_Scope); var scope = compressor.find_parent(AST_Scope);
var toplevel = compressor.option("toplevel"); var toplevel = compressor.option("toplevel");
for (var stat_index = statements.length; --stat_index >= 0;) { var stat_index;
var stat = statements[stat_index]; var prev_stat_index;
var def_stat_index;
var var_names_seen = Object.create(null); var stat;
var side_effects_encountered = false; var var_defs;
var lvalues_encountered = false; var var_defs_index;
var lvalues = Object.create(null); for (stat_index = statements.length; --stat_index >= 0;) {
var prev_stat_index, var_defs, var_defs_index; stat = statements[stat_index];
// Scan variable definitions from right to left. // Scan variable definitions from right to left.
if (stat instanceof AST_Definitions) { if (stat instanceof AST_Definitions) {
prev_stat_index = stat_index; prev_stat_index = stat_index;
var_defs = stat.definitions; var_defs = stat.definitions;
for (var_defs_index = var_defs.length - 1; --var_defs_index >= 0;) { for (def_stat_index = var_defs.length; --def_stat_index >= 1;) {
if (collapse(var_defs[var_defs_index + 1])) break; stat = var_defs[def_stat_index];
scan_var_defs(def_stat_index);
} }
} else if (stat_index > 0) { } else if (stat_index > 0) {
// The variable definition must precede a statement. // The variable definition must precede a statement.
@@ -666,103 +666,107 @@ merge(Compressor.prototype, {
var prev_stat = statements[prev_stat_index]; var prev_stat = statements[prev_stat_index];
if (!(prev_stat instanceof AST_Definitions)) continue; if (!(prev_stat instanceof AST_Definitions)) continue;
var_defs = prev_stat.definitions; var_defs = prev_stat.definitions;
for (var_defs_index = var_defs.length; --var_defs_index >= 0;) { scan_var_defs(var_defs.length);
if (collapse(stat)) break;
}
} }
} }
return statements; return statements;
function collapse(stat) { function scan_var_defs(end_pos) {
var var_decl = var_defs[var_defs_index]; var var_names_seen = Object.create(null);
// `drop_unused()` shuffles variables without values to the top, var side_effects_encountered = false;
// so we can terminate upon first sighting as an optimization. var lvalues_encountered = false;
if (var_decl.value == null) return true; var lvalues = Object.create(null);
var var_name = var_decl.name.name; for (var_defs_index = end_pos; --var_defs_index >= 0;) {
var var_decl = var_defs[var_defs_index];
// `drop_unused()` shuffles variables without values to the top,
// so we can terminate upon first sighting as an optimization.
if (var_decl.value == null) break;
var var_name = var_decl.name.name;
// Bail if we've seen a var definition of same name before. // Bail if we've seen a var definition of same name before.
if (var_name in var_names_seen) return true; if (var_name in var_names_seen) break;
var_names_seen[var_name] = true; var_names_seen[var_name] = true;
// Only interested in non-constant values. // Only interested in non-constant values.
if (var_decl.value.is_constant()) return; if (var_decl.value.is_constant()) continue;
// Only interested in cases with just one reference to the variable. // Only interested in cases with just one reference to the variable.
var def = var_decl.name.definition(); var def = var_decl.name.definition();
if (def.references.length !== 1 if (def.references.length !== 1
|| var_name == "arguments" || (!toplevel && def.global)) { || var_name == "arguments" || (!toplevel && def.global)) {
side_effects_encountered = true; side_effects_encountered = true;
return; 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) return true;
// Restrict var replacement to constants if side effects encountered.
if (side_effects_encountered |= lvalues_encountered) return;
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;
return;
}
// 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 ref = def.references[0];
var_decl.value.walk(tw);
// Replace the non-constant single use var in statement if side effect free. // Don't replace ref if eval() or with statement in scope.
var unwind = false; if (ref.scope.uses_eval || ref.scope.uses_with) break;
var tt = new TreeTransformer(
function preorder(node) { // Restrict var replacement to constants if side effects encountered.
if (unwind || node instanceof AST_Scope && node !== scope) return node; if (side_effects_encountered |= lvalues_encountered) continue;
var parent = tt.parent();
if (node instanceof AST_Try var value_has_side_effects = var_decl.value.has_side_effects(compressor);
|| node instanceof AST_With // Non-constant single use vars can only be replaced in same scope.
|| node instanceof AST_Case if (ref.scope !== scope) {
|| node instanceof AST_IterationStatement side_effects_encountered |= value_has_side_effects;
|| (parent instanceof AST_If && node !== parent.condition) continue;
|| (parent instanceof AST_Conditional && node !== parent.condition) }
|| (node instanceof AST_SymbolRef
&& value_has_side_effects // Detect lvalues in var value.
&& !are_references_in_scope(node.definition(), scope)) var tw = new TreeWalker(function(node){
|| (parent instanceof AST_Binary if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) {
&& (parent.operator == "&&" || parent.operator == "||") lvalues[node.name] = lvalues_encountered = true;
&& node === parent.right)
|| (parent instanceof AST_Switch && node !== parent.expression)) {
return side_effects_encountered = unwind = true, node;
} }
function are_references_in_scope(def, scope) { });
if (def.orig.length === 1 var_decl.value.walk(tw);
&& def.orig[0] instanceof AST_SymbolDefun) return true;
if (def.scope !== scope) return false; // Replace the non-constant single use var in statement if side effect free.
var refs = def.references; var unwind = false;
for (var i = 0, len = refs.length; i < len; i++) { var tt = new TreeTransformer(
if (refs[i].scope !== scope) return false; function preorder(node) {
if (unwind || node instanceof AST_Scope && node !== scope) return node;
var parent = tt.parent();
if (node instanceof AST_Try
|| node instanceof AST_With
|| node instanceof AST_Case
|| node instanceof AST_IterationStatement
|| (parent instanceof AST_If && node !== parent.condition)
|| (parent instanceof AST_Conditional && node !== parent.condition)
|| (node instanceof AST_SymbolRef
&& value_has_side_effects
&& !are_references_in_scope(node.definition(), scope))
|| (parent instanceof AST_Binary
&& (parent.operator == "&&" || parent.operator == "||")
&& node === parent.right)
|| (parent instanceof AST_Switch && node !== parent.expression)) {
return side_effects_encountered = unwind = true, node;
}
function are_references_in_scope(def, scope) {
if (def.orig.length === 1
&& def.orig[0] instanceof AST_SymbolDefun) return true;
if (def.scope !== scope) return false;
var refs = def.references;
for (var i = 0, len = refs.length; i < len; i++) {
if (refs[i].scope !== scope) return false;
}
return true;
}
},
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;
} }
return true;
} }
}, );
function postorder(node) { stat.transform(tt);
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);
} }
function is_lvalue(node, parent) { function is_lvalue(node, parent) {
@@ -777,6 +781,7 @@ merge(Compressor.prototype, {
var_decl.value = null; var_decl.value = null;
var_defs.splice(var_defs_index, 1); var_defs.splice(var_defs_index, 1);
def_stat_index--;
if (var_defs.length === 0) { if (var_defs.length === 0) {
statements.splice(prev_stat_index, 1); statements.splice(prev_stat_index, 1);
stat_index--; stat_index--;

View File

@@ -1654,3 +1654,26 @@ iife_2: {
}(bar()); }(bar());
} }
} }
var_defs: {
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;
console.log(a + b);
};
f1("1", 0);
}
expect: {
var f1 = function(x, y) {
var r = x + y, a = r * r - r, b = 7;
console.log(a + b);
};
f1("1", 0);
}
expect_stdout: "97"
}