enhance collapse_vars (#1862)
- extend expression types
- `a++`
- `a=x;`
- extend scan range
- `for(init;;);`
- `switch(expr){case expr:}`
- `a = x; a = a || y;`
- terminate upon `debugger;`
closes #1821
fixes #27
fixes #315
fixes #1858
This commit is contained in:
357
lib/compress.js
357
lib/compress.js
@@ -512,6 +512,10 @@ merge(Compressor.prototype, {
|
||||
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) {
|
||||
var scope, i = 0;
|
||||
while (scope = compressor.parent(i++)) {
|
||||
@@ -643,174 +647,210 @@ merge(Compressor.prototype, {
|
||||
statements = join_consecutive_vars(statements, compressor);
|
||||
}
|
||||
if (compressor.option("collapse_vars")) {
|
||||
statements = collapse_single_use_vars(statements, compressor);
|
||||
statements = collapse(statements, compressor);
|
||||
}
|
||||
} while (CHANGED && max_iter-- > 0);
|
||||
|
||||
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.
|
||||
|
||||
// Search from right to left for assignment-like expressions:
|
||||
// - `var a = x;`
|
||||
// - `a = x;`
|
||||
// - `++a`
|
||||
// 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 stat_index;
|
||||
var prev_stat_index;
|
||||
var def_stat_index;
|
||||
var stat;
|
||||
var var_defs;
|
||||
var var_defs_index;
|
||||
for (stat_index = statements.length; --stat_index >= 0;) {
|
||||
stat = statements[stat_index];
|
||||
// Scan variable definitions from right to left.
|
||||
if (stat instanceof AST_Definitions) {
|
||||
prev_stat_index = stat_index;
|
||||
var_defs = stat.definitions;
|
||||
for (def_stat_index = var_defs.length; --def_stat_index >= 1;) {
|
||||
stat = var_defs[def_stat_index];
|
||||
scan_var_defs(def_stat_index);
|
||||
if (scope.uses_eval || scope.uses_with) return statements;
|
||||
var candidates = [];
|
||||
var stat_index = statements.length;
|
||||
while (--stat_index >= 0) {
|
||||
extract_candidates(statements[stat_index]);
|
||||
while (candidates.length > 0) {
|
||||
var candidate = candidates.pop();
|
||||
var lhs = get_lhs(candidate);
|
||||
if (!lhs || is_lhs_read_only(lhs)) continue;
|
||||
var lvalues = get_lvalues(candidate);
|
||||
if (lhs instanceof AST_SymbolRef) lvalues[lhs.name] = false;
|
||||
var side_effects = value_has_side_effects(candidate);
|
||||
var hit = false, abort = false, replaced = false;
|
||||
var tt = new TreeTransformer(function(node, descend) {
|
||||
if (abort) return node;
|
||||
// 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_Assign && node.operator != "=" && lhs.equivalent_to(node.left)
|
||||
|| 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 (!(node instanceof AST_SymbolDeclaration)
|
||||
&& !is_lhs(node, parent)
|
||||
&& lhs.equivalent_to(node)) {
|
||||
CHANGED = replaced = abort = 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) {
|
||||
var def = candidate.name.definition();
|
||||
if (def.references.length == 1 && (!def.global || compressor.toplevel(def))) {
|
||||
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;
|
||||
}
|
||||
// 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[node.name]
|
||||
|| side_effects && !references_in_scope(node.definition()))
|
||||
|| (sym = lhs_or_def(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) {
|
||||
// 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);
|
||||
if (replaced && !remove_candidate(candidate)) statements.splice(stat_index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
|
||||
function scan_var_defs(end_pos) {
|
||||
var var_names_seen = Object.create(null);
|
||||
var side_effects_encountered = false;
|
||||
var lvalues_encountered = false;
|
||||
var lvalues = Object.create(null);
|
||||
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.
|
||||
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;
|
||||
}
|
||||
function extract_candidates(expr) {
|
||||
if (expr instanceof AST_Assign && !expr.left.has_side_effects(compressor)
|
||||
|| expr instanceof AST_Unary && (expr.operator == "++" || expr.operator == "--")) {
|
||||
candidates.push(expr);
|
||||
} else if (expr instanceof AST_Sequence) {
|
||||
expr.expressions.forEach(extract_candidates);
|
||||
} else if (expr instanceof AST_Definitions) {
|
||||
expr.definitions.forEach(function(var_def) {
|
||||
if (var_def.value) candidates.push(var_def);
|
||||
});
|
||||
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.
|
||||
var unwind = false;
|
||||
var tt = new TreeTransformer(
|
||||
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;
|
||||
}
|
||||
function get_lhs(expr) {
|
||||
if (expr instanceof AST_VarDef) {
|
||||
var def = expr.name.definition();
|
||||
if (def.orig.length > 1
|
||||
|| def.references.length == 1 && (!def.global || compressor.toplevel(def))) {
|
||||
return make_node(AST_SymbolRef, expr.name, expr.name);
|
||||
}
|
||||
} else {
|
||||
return expr[expr instanceof AST_Assign ? "left" : "expression"];
|
||||
}
|
||||
}
|
||||
|
||||
function get_symbol(node) {
|
||||
while (node instanceof AST_PropAccess) node = node.expression;
|
||||
return node;
|
||||
}
|
||||
|
||||
function get_lvalues(expr) {
|
||||
var lvalues = Object.create(null);
|
||||
if (expr instanceof AST_Unary) return lvalues;
|
||||
var scope;
|
||||
var tw = new TreeWalker(function(node, descend) {
|
||||
if (node instanceof AST_Scope) {
|
||||
var save_scope = scope;
|
||||
descend();
|
||||
scope = save_scope;
|
||||
return true;
|
||||
}
|
||||
if (node instanceof AST_SymbolRef || node instanceof AST_PropAccess) {
|
||||
var sym = get_symbol(node);
|
||||
if (sym instanceof AST_SymbolRef) {
|
||||
lvalues[sym.name] = lvalues[sym.name] || is_lhs(node, tw.parent());
|
||||
}
|
||||
);
|
||||
stat.transform(tt);
|
||||
}
|
||||
}
|
||||
|
||||
function is_lvalue(node, parent) {
|
||||
return node instanceof AST_SymbolRef && is_lhs(node, parent);
|
||||
}
|
||||
|
||||
function replace_var(var_decl, 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 = maintain_this_binding(parent, node, var_decl.value);
|
||||
var_decl.value = null;
|
||||
|
||||
var_defs.splice(var_defs_index, 1);
|
||||
def_stat_index--;
|
||||
if (var_defs.length === 0) {
|
||||
statements.splice(prev_stat_index, 1);
|
||||
stat_index--;
|
||||
}
|
||||
// Further optimize statement after substitution.
|
||||
stat.reset_opt_flags(compressor);
|
||||
|
||||
compressor.info("Collapsing {type} {name} [{file}:{line},{col}]", {
|
||||
type: is_constant ? "constant" : "variable",
|
||||
name: var_decl.name.name,
|
||||
file: node.start.file,
|
||||
line: node.start.line,
|
||||
col: node.start.col
|
||||
}
|
||||
});
|
||||
expr[expr instanceof AST_Assign ? "right" : "value"].walk(tw);
|
||||
return lvalues;
|
||||
}
|
||||
|
||||
function lhs_or_def(node) {
|
||||
if (node instanceof AST_VarDef) return node.value && node.name;
|
||||
return is_lhs(node.left, node);
|
||||
}
|
||||
|
||||
function remove_candidate(expr) {
|
||||
var found = false;
|
||||
return statements[stat_index].transform(new TreeTransformer(function(node, descend, in_list) {
|
||||
if (found) return node;
|
||||
if (node === expr) {
|
||||
found = true;
|
||||
if (node instanceof AST_VarDef) {
|
||||
remove(node.name.definition().orig, node.name);
|
||||
}
|
||||
return in_list ? MAP.skip : null;
|
||||
}
|
||||
}, function(node) {
|
||||
if (node instanceof AST_Sequence) switch (node.expressions.length) {
|
||||
case 0: return null;
|
||||
case 1: return node.expressions[0];
|
||||
}
|
||||
if (node instanceof AST_Definitions && node.definitions.length == 0
|
||||
|| node instanceof AST_SimpleStatement && !node.body) {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2022,7 +2062,8 @@ merge(Compressor.prototype, {
|
||||
var var_defs = var_defs_by_id.get(sym.id);
|
||||
if (var_defs.length > 1 && !def.value) {
|
||||
compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", template(def.name));
|
||||
var_defs.splice(var_defs.indexOf(def), 1);
|
||||
remove(var_defs, def);
|
||||
remove(sym.orig, def.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2055,6 +2096,7 @@ merge(Compressor.prototype, {
|
||||
} else {
|
||||
compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", template(def.name));
|
||||
}
|
||||
remove(sym.orig, def.name);
|
||||
}
|
||||
});
|
||||
if (head.length == 0 && tail.length == 1 && tail[0].name instanceof AST_SymbolVar) {
|
||||
@@ -2062,7 +2104,8 @@ merge(Compressor.prototype, {
|
||||
if (var_defs.length > 1) {
|
||||
var def = tail.pop();
|
||||
compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", template(def.name));
|
||||
var_defs.splice(var_defs.indexOf(def), 1);
|
||||
remove(var_defs, def);
|
||||
remove(def.name.definition().orig, def.name);
|
||||
side_effects.unshift(make_node(AST_Assign, def, {
|
||||
operator: "=",
|
||||
left: make_node(AST_SymbolRef, def.name, def.name),
|
||||
@@ -3130,9 +3173,7 @@ merge(Compressor.prototype, {
|
||||
&& (left.operator == "++" || left.operator == "--")) {
|
||||
left = left.expression;
|
||||
} else left = null;
|
||||
if (!left ||
|
||||
left instanceof AST_SymbolRef
|
||||
&& left.definition().orig[0] instanceof AST_SymbolLambda) {
|
||||
if (!left || is_lhs_read_only(left)) {
|
||||
expressions[++i] = cdr;
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user