introduce merge_vars (#4100)

This commit is contained in:
Alex Lam S.L
2020-09-15 03:01:48 +01:00
committed by GitHub
parent d33a3a3253
commit 3ac575f2e8
3 changed files with 415 additions and 5 deletions

View File

@@ -73,6 +73,7 @@ function Compressor(options, false_by_default) {
keep_fnames : false, keep_fnames : false,
keep_infinity : false, keep_infinity : false,
loops : !false_by_default, loops : !false_by_default,
merge_vars : !false_by_default,
negate_iife : !false_by_default, negate_iife : !false_by_default,
objects : !false_by_default, objects : !false_by_default,
passes : 1, passes : 1,
@@ -225,7 +226,8 @@ merge(Compressor.prototype, {
// output and performance. // output and performance.
descend(node, this); descend(node, this);
var opt = node.optimize(this); var opt = node.optimize(this);
if (is_scope && opt === node) { if (is_scope && opt === node && !this.has_directive("use asm") && !opt.pinned()) {
opt.merge_variables(this);
opt.drop_unused(this); opt.drop_unused(this);
descend(opt, this); descend(opt, this);
} }
@@ -4305,11 +4307,185 @@ merge(Compressor.prototype, {
return self; return self;
}); });
AST_Scope.DEFMETHOD("merge_variables", function(compressor) {
if (!compressor.option("merge_vars")) return;
var self = this, segment = self;
var first = [], last = [], index = 0;
var references = Object.create(null);
var prev = Object.create(null);
var tw = new TreeWalker(function(node, descend) {
if (node instanceof AST_Assign) {
var sym = node.left;
if (!(sym instanceof AST_SymbolRef)) return;
node.right.walk(tw);
mark(sym, node.operator == "=");
return true;
}
if (node instanceof AST_Binary) {
if (!lazy_op[node.operator]) return;
node.left.walk(tw);
var save = segment;
segment = node;
node.right.walk(tw);
segment = save;
return true;
}
if (node instanceof AST_Conditional) {
node.condition.walk(tw);
var save = segment;
segment = node;
node.consequent.walk(tw);
node.alternative.walk(tw);
segment = save;
return true;
}
if (node instanceof AST_For) {
if (node.init) node.init.walk(tw);
var save = segment;
segment = node;
if (node.condition) node.condition.walk(tw);
node.body.walk(tw);
if (node.step) node.step.walk(tw);
segment = save;
return true;
}
if (node instanceof AST_ForIn) {
node.object.walk(tw);
var save = segment;
segment = node;
node.init.walk(tw);
node.body.walk(tw);
segment = save;
return true;
}
if (node instanceof AST_If) {
node.condition.walk(tw);
var save = segment;
segment = node;
node.body.walk(tw);
if (node.alternative) node.alternative.walk(tw);
segment = save;
return true;
}
if (node instanceof AST_IterationStatement) {
var save = segment;
segment = node;
descend();
segment = save;
return true;
}
if (node instanceof AST_Scope) {
if (node instanceof AST_Lambda) {
references[node.variables.get("arguments").id] = false;
if (node.name) references[node.name.definition().id] = false;
}
var save = segment;
segment = node;
descend();
segment = save;
return true;
}
if (node instanceof AST_Switch) {
node.expression.walk(tw);
var save = segment;
segment = node;
node.body.forEach(function(branch) {
branch.walk(tw);
});
segment = save;
return true;
}
if (node instanceof AST_SymbolFunarg) {
mark(node, true);
return true;
}
if (node instanceof AST_SymbolRef) {
mark(node);
return true;
}
if (node instanceof AST_Unary) {
if (!unary_arithmetic[node.operator]) return;
var sym = node.expression;
if (!(sym instanceof AST_SymbolRef)) return;
mark(sym);
return true;
}
if (node instanceof AST_VarDef) {
if (!node.value) return true;
node.value.walk(tw);
mark(node.name, true);
return true;
}
});
self.walk(tw);
var merged = Object.create(null);
while (first.length && last.length) {
var head = first.pop();
var def = head.definition;
if (!(def.id in prev)) continue;
if (!references[def.id]) continue;
while (def.id in merged) def = merged[def.id];
do {
var tail = last.pop();
if (!tail) continue;
if (tail.index > head.index) continue;
if (!references[tail.definition.id]) continue;
var orig = [], refs = [];
references[tail.definition.id].forEach(function(sym) {
push(sym);
sym.thedef = def;
sym.name = def.name;
});
references[def.id].forEach(push);
def.orig = orig;
def.refs = refs;
def.eliminated = def.replaced = 0;
def.fixed = tail.definition.fixed && def.fixed;
merged[tail.definition.id] = def;
break;
} while (last.length);
}
function read(def) {
prev[def.id] = last.length;
last.push({
index: index++,
definition: def,
});
}
function mark(sym, write_only) {
var def = sym.definition();
if (segment !== self) references[def.id] = false;
if (def.id in references) {
if (!references[def.id]) return;
references[def.id].push(sym);
if (def.id in prev) last[prev[def.id]] = null;
read(def);
} else if (compressor.exposed(def) || self.variables.get(def.name) !== def) {
references[def.id] = false;
} else {
references[def.id] = [ sym ];
if (!write_only) return read(def);
first.push({
index: index++,
definition: def,
});
}
}
function push(sym) {
if (sym instanceof AST_SymbolRef) {
refs.push(sym);
} else {
orig.push(sym);
}
}
});
AST_Scope.DEFMETHOD("drop_unused", function(compressor) { AST_Scope.DEFMETHOD("drop_unused", function(compressor) {
if (!compressor.option("unused")) return; if (!compressor.option("unused")) return;
if (compressor.has_directive("use asm")) return;
var self = this; var self = this;
if (self.pinned()) return;
var drop_funcs = !(self instanceof AST_Toplevel) || compressor.toplevel.funcs; var drop_funcs = !(self instanceof AST_Toplevel) || compressor.toplevel.funcs;
var drop_vars = !(self instanceof AST_Toplevel) || compressor.toplevel.vars; var drop_vars = !(self instanceof AST_Toplevel) || compressor.toplevel.vars;
var assign_as_unused = /keep_assign/.test(compressor.option("unused")) ? return_false : function(node, props) { var assign_as_unused = /keep_assign/.test(compressor.option("unused")) ? return_false : function(node, props) {
@@ -6451,7 +6627,7 @@ merge(Compressor.prototype, {
if (self.args.length == 0) return make_node(AST_Function, self, { if (self.args.length == 0) return make_node(AST_Function, self, {
argnames: [], argnames: [],
body: [] body: []
}); }).init_scope_vars(exp.scope);
if (all(self.args, function(x) { if (all(self.args, function(x) {
return x instanceof AST_String; return x instanceof AST_String;
})) { })) {
@@ -8739,7 +8915,7 @@ merge(Compressor.prototype, {
self.expression = make_node(AST_Function, self.expression, { self.expression = make_node(AST_Function, self.expression, {
argnames: [], argnames: [],
body: [] body: []
}); }).init_scope_vars(exp.scope);
break; break;
case "Number": case "Number":
self.expression = make_node(AST_Number, self.expression, { self.expression = make_node(AST_Number, self.expression, {

View File

@@ -279,6 +279,7 @@ AST_Lambda.DEFMETHOD("init_scope_vars", function(parent_scope) {
start: this.start, start: this.start,
end: this.end, end: this.end,
})); }));
return this;
}); });
AST_Symbol.DEFMETHOD("mark_enclosed", function(options) { AST_Symbol.DEFMETHOD("mark_enclosed", function(options) {

233
test/compress/merge_vars.js Normal file
View File

@@ -0,0 +1,233 @@
merge: {
options = {
merge_vars: true,
toplevel: false,
}
input: {
var a = "foo";
console.log(a);
function f(b) {
var c;
console.log(b);
c = "bar";
console.log(c);
}
f("baz");
var d = "moo";
console.log(d);
}
expect: {
var a = "foo";
console.log(a);
function f(c) {
var c;
console.log(c);
c = "bar";
console.log(c);
}
f("baz");
var d = "moo";
console.log(d);
}
expect_stdout: [
"foo",
"baz",
"bar",
"moo",
]
}
merge_toplevel: {
options = {
merge_vars: true,
toplevel: true,
}
input: {
var a = "foo";
console.log(a);
function f(b) {
var c;
console.log(b);
c = "bar";
console.log(c);
}
f("baz");
var d = "moo";
console.log(d);
}
expect: {
var d = "foo";
console.log(d);
function f(c) {
var c;
console.log(c);
c = "bar";
console.log(c);
}
f("baz");
var d = "moo";
console.log(d);
}
expect_stdout: [
"foo",
"baz",
"bar",
"moo",
]
}
init_scope_vars: {
options = {
merge_vars: true,
unsafe_proto: true,
}
input: {
Function.prototype.call();
}
expect: {
(function() {}).call();
}
expect_stdout: true
}
binary_branch: {
options = {
merge_vars: true,
}
input: {
console.log(function(a) {
var b = "FAIL", c;
a && (c = b);
return c || "PASS";
}());
}
expect: {
console.log(function(a) {
var b = "FAIL", c;
a && (c = b);
return c || "PASS";
}());
}
expect_stdout: "PASS"
}
conditional_branch: {
options = {
merge_vars: true,
}
input: {
console.log(function(a) {
var b = "FAIL", c;
a ? (c = b) : void 0;
return c || "PASS";
}());
}
expect: {
console.log(function(a) {
var b = "FAIL", c;
a ? (c = b) : void 0;
return c || "PASS";
}());
}
expect_stdout: "PASS"
}
if_branch: {
options = {
merge_vars: true,
}
input: {
console.log(function(a) {
var b = "FAIL", c;
if (a) c = b;
return c || "PASS";
}());
}
expect: {
console.log(function(a) {
var b = "FAIL", c;
if (a) c = b;
return c || "PASS";
}());
}
expect_stdout: "PASS"
}
switch_branch: {
options = {
merge_vars: true,
}
input: {
console.log(function(a) {
var b = "FAIL", c;
switch (a) {
case 1:
c = b;
break;
}
return c || "PASS";
}());
}
expect: {
console.log(function(a) {
var b = "FAIL", c;
switch (a) {
case 1:
c = b;
break;
}
return c || "PASS";
}());
}
expect_stdout: "PASS"
}
read_before_assign_1: {
options = {
inline: true,
merge_vars: true,
sequences: true,
toplevel: true,
}
input: {
var c = 0;
c = 0;
(function() {
var a = console.log(++a);
a;
})();
c;
}
expect: {
var c = 0;
var a;
c = 0,
a = console.log(++a);
}
expect_stdout: "NaN"
}
read_before_assign_2: {
options = {
dead_code: true,
loops: true,
merge_vars: true,
}
input: {
console.log(function(a, a) {
while (b)
return "FAIL";
var b = 1;
return "PASS";
}(0, []));
}
expect: {
console.log(function(a, a) {
if (b)
return "FAIL";
var b = 1;
return "PASS";
}(0, []));
}
expect_stdout: "PASS"
}