Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize various switch statements #1044

Merged
merged 11 commits into from Aug 24, 2021
319 changes: 278 additions & 41 deletions lib/compress/index.js
Expand Up @@ -53,7 +53,6 @@ import {
AST_Boolean,
AST_Break,
AST_Call,
AST_Case,
AST_Catch,
AST_Chain,
AST_Class,
Expand Down Expand Up @@ -109,6 +108,7 @@ import {
AST_String,
AST_Sub,
AST_Switch,
AST_SwitchBranch,
AST_Symbol,
AST_SymbolBlockDeclaration,
AST_SymbolCatch,
Expand Down Expand Up @@ -1592,58 +1592,251 @@ def_optimize(AST_Switch, function(self, compressor) {
}
}
}
if (aborts(branch)) {
var prev = body[body.length - 1];
if (aborts(prev) && prev.body.length == branch.body.length
&& make_node(AST_BlockStatement, prev, prev).equivalent_to(make_node(AST_BlockStatement, branch, branch))) {
prev.body = [];
}
}
body.push(branch);
}
while (i < len) eliminate_branch(self.body[i++], body[body.length - 1]);
self.body = body;

for (let i = 0; i < body.length; i++) {
let branch = body[i];
if (branch.body.length === 0) continue;
if (!aborts(branch)) continue;

for (let j = i + 1; j < body.length; i++, j++) {
let next = body[j];
if (next.body.length === 0) continue;
if (
branches_equivalent(next, branch, false)
|| (j === body.length - 1 && branches_equivalent(next, branch, true))
) {
branch.body = [];
branch = next;
continue;
}
break;
}
}

let default_or_exact = default_branch || exact_match;
default_branch = null;
exact_match = null;

// Prune any empty branches at the end of the switch statement.
{
let i = body.length - 1;
for (; i >= 0; i--) {
let bbody = body[i].body;
if (is_break(bbody[bbody.length - 1], compressor)) bbody.pop();
if (!is_inert_body(body[i])) break;
}
// i now points to the index of a branch that contains a body. By incrementing, it's
// pointing to the first branch that's empty.
i++;
if (!default_or_exact || body.indexOf(default_or_exact) >= i) {
// The default behavior is to do nothing. We can take advantage of that to
// remove all case expressions that are side-effect free that also do
// nothing, since they'll default to doing nothing. But we can't remove any
// case expressions before one that would side-effect, since they may cause
// the side-effect to be skipped.
for (let j = body.length - 1; j >= i; j--) {
let branch = body[j];
if (branch === default_or_exact) {
default_or_exact = null;
body.pop();
} else if (!branch.expression.has_side_effects(compressor)) {
body.pop();
} else {
break;
}
}
}
}


// Prune side-effect free branches that fall into default.
if (default_or_exact) {
let default_index = body.indexOf(default_or_exact);
let default_body_index = default_index;
for (; default_body_index < body.length - 1; default_body_index++) {
if (!is_inert_body(body[default_body_index])) break;
}
let side_effect_index = body.length - 1;
for (; side_effect_index >= 0; side_effect_index--) {
let branch = body[side_effect_index];
if (branch === default_or_exact) continue;
if (branch.expression.has_side_effects(compressor)) break;
}
// If the default behavior comes after any side-effect case expressions,
// then we can fold all side-effect free cases into the default branch.
// If the side-effect case is after the default, then any side-effect
// free cases could prevent the side-effect from occurring.
if (default_body_index > side_effect_index) {
let prev_body_index = default_index - 1;
for (; prev_body_index >= 0; prev_body_index--) {
if (!is_inert_body(body[prev_body_index])) break;
}
let before = Math.max(side_effect_index, prev_body_index) + 1;
let after = default_index;
if (side_effect_index > default_index) {
// If the default falls into the same body as a side-effect
// case, then we need preserve that case and only prune the
// cases after it.
after = side_effect_index;
body[side_effect_index].body = body[default_body_index].body;
} else {
// The default will be the last branch.
default_or_exact.body = body[default_body_index].body;
}

// Prune everything after the default (or last side-effect case)
// until the next case with a body.
body.splice(after + 1, default_body_index - after);
// Prune everything before the default that falls into it.
body.splice(before, default_index - before);
}
}

// See if we can remove the switch entirely if all cases (the default) fall into the same case body.
DEFAULT: if (default_or_exact) {
let i = body.findIndex(branch => !is_inert_body(branch));
let caseBody;
// `i` is equal to one of the following:
// - `-1`, there is no body in the switch statement.
// - `body.length - 1`, all cases fall into the same body.
// - anything else, there are multiple bodies in the switch.
if (i === body.length - 1) {
// All cases fall into the case body.
let branch = body[i];
if (has_nested_break(self)) break DEFAULT;

// This is the last case body, and we've already pruned any breaks, so it's
// safe to hoist.
caseBody = make_node(AST_BlockStatement, branch, {
body: branch.body
});
branch.body = [];
} else if (i !== -1) {
// If there are multiple bodies, then we cannot optimize anything.
break DEFAULT;
}

let sideEffect = body.find(branch => {
return (
branch !== default_or_exact
&& branch.expression.has_side_effects(compressor)
);
});
// If no cases cause a side-effect, we can eliminate the switch entirely.
if (!sideEffect) {
return make_node(AST_BlockStatement, self, {
body: decl.concat(
statement(self.expression),
default_or_exact.expression ? statement(default_or_exact.expression) : [],
caseBody || []
)
}).optimize(compressor);
}

// If we're this far, either there was no body or all cases fell into the same body.
// If there was no body, then we don't need a default branch (because the default is
// do nothing). If there was a body, we'll extract it to after the switch, so the
// switch's new default is to do nothing and we can still prune it.
const default_index = body.indexOf(default_or_exact);
body.splice(default_index, 1);
default_or_exact = null;

if (caseBody) {
// Recurse into switch statement one more time so that we can append the case body
// outside of the switch. This recursion will only happen once since we've pruned
// the default case.
return make_node(AST_BlockStatement, self, {
body: decl.concat(self, caseBody)
}).optimize(compressor);
}
// If we fall here, there is a default branch somewhere, there are no case bodies,
// and there's a side-effect somewhere. Just let the below paths take care of it.
}

if (body.length > 0) {
body[0].body = decl.concat(body[0].body);
}
self.body = body;
while (branch = body[body.length - 1]) {
var stat = branch.body[branch.body.length - 1];
if (stat instanceof AST_Break && compressor.loopcontrol_target(stat) === self)
branch.body.pop();
if (branch.body.length || branch instanceof AST_Case
&& (default_branch || branch.expression.has_side_effects(compressor))) break;
if (body.pop() === default_branch) default_branch = null;
}

if (body.length == 0) {
return make_node(AST_BlockStatement, self, {
body: decl.concat(make_node(AST_SimpleStatement, self.expression, {
body: self.expression
}))
body: decl.concat(statement(self.expression))
}).optimize(compressor);
}
if (body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) {
var has_break = false;
var tw = new TreeWalker(function(node) {
if (has_break
|| node instanceof AST_Lambda
|| node instanceof AST_SimpleStatement) return true;
if (node instanceof AST_Break && tw.loopcontrol_target(node) === self)
has_break = true;
});
self.walk(tw);
if (!has_break) {
var statements = body[0].body.slice();
var exp = body[0].expression;
if (exp) statements.unshift(make_node(AST_SimpleStatement, exp, {
body: exp
}));
statements.unshift(make_node(AST_SimpleStatement, self.expression, {
body:self.expression
}));
return make_node(AST_BlockStatement, self, {
body: statements
if (body.length == 1 && !has_nested_break(self)) {
// This is the last case body, and we've already pruned any breaks, so it's
// safe to hoist.
let branch = body[0];
return make_node(AST_If, self, {
condition: make_node(AST_Binary, self, {
operator: "===",
left: self.expression,
right: branch.expression,
}),
body: make_node(AST_BlockStatement, branch, {
body: branch.body
}),
alternative: null
}).optimize(compressor);
}
if (body.length === 2 && default_or_exact && !has_nested_break(self)) {
let branch = body[0] === default_or_exact ? body[1] : body[0];
let exact_exp = default_or_exact.expression && statement(default_or_exact.expression);
if (aborts(body[0])) {
// Only the first branch body could have a break (at the last statement)
let first = body[0];
if (is_break(first.body[first.body.length - 1], compressor)) {
first.body.pop();
}
return make_node(AST_If, self, {
condition: make_node(AST_Binary, self, {
operator: "===",
left: self.expression,
right: branch.expression,
}),
body: make_node(AST_BlockStatement, branch, {
body: branch.body
}),
alternative: make_node(AST_BlockStatement, default_or_exact, {
body: [].concat(
exact_exp || [],
default_or_exact.body
)
})
}).optimize(compressor);
}
let operator = "===";
let consequent = make_node(AST_BlockStatement, branch, {
body: branch.body,
});
let always = make_node(AST_BlockStatement, default_or_exact, {
body: [].concat(
exact_exp || [],
default_or_exact.body
)
});
if (body[0] === default_or_exact) {
operator = "!==";
let tmp = always;
always = consequent;
consequent = tmp;
}
return make_node(AST_BlockStatement, self, {
body: [
make_node(AST_If, self, {
condition: make_node(AST_Binary, self, {
operator: operator,
left: self.expression,
right: branch.expression,
}),
body: consequent,
alternative: null
})
].concat(always)
}).optimize(compressor);
}
return self;

Expand All @@ -1654,6 +1847,50 @@ def_optimize(AST_Switch, function(self, compressor) {
trim_unreachable_code(compressor, branch, decl);
}
}
function branches_equivalent(branch, prev, insertBreak) {
let bbody = branch.body;
let pbody = prev.body;
if (insertBreak) {
bbody = bbody.concat(make_node(AST_Break));
}
if (bbody.length !== pbody.length) return false;
let bblock = make_node(AST_BlockStatement, branch, { body: bbody });
let pblock = make_node(AST_BlockStatement, prev, { body: pbody });
return bblock.equivalent_to(pblock);
}
function statement(expression) {
return make_node(AST_SimpleStatement, expression, {
body: expression
});
}
function has_nested_break(root) {
let has_break = false;
let tw = new TreeWalker(node => {
if (has_break) return true;
if (node instanceof AST_Lambda) return true;
if (node instanceof AST_SimpleStatement) return true;
if (!is_break(node, tw)) return;
let parent = tw.parent();
if (
parent instanceof AST_SwitchBranch
&& parent.body[parent.body.length - 1] === node
) {
return;
}
has_break = true;
});
root.walk(tw);
return has_break;
}
function is_break(node, stack) {
return node instanceof AST_Break
&& stack.loopcontrol_target(node) === self;
}
function is_inert_body(branch) {
return !aborts(branch) && !make_node(AST_BlockStatement, branch, {
body: branch.body
}).has_side_effects(compressor)
}
});

def_optimize(AST_Try, function(self, compressor) {
Expand Down
4 changes: 1 addition & 3 deletions test/compress/functions.js
Expand Up @@ -1328,9 +1328,7 @@ issue_2620_4: {
expect: {
var c = "FAIL";
!function() {
switch (NaN) {
case void (c = "PASS"):
}
if (NaN === void (c = "PASS"));
}();
console.log(c);
}
Expand Down
10 changes: 2 additions & 8 deletions test/compress/issue-1750.js
Expand Up @@ -16,10 +16,7 @@ case_1: {
}
expect: {
var a = 0, b = 1;
switch (true) {
case a || true:
b = 2;
}
if (true === (a || true)) b = 2;
console.log(a, b);
}
expect_stdout: "0 2"
Expand All @@ -44,10 +41,7 @@ case_2: {
}
expect: {
var a = 0, b = 1;
switch (0) {
case a:
a = 3;
}
if (0 === a) a = 3;
console.log(a, b);
}
expect_stdout: "3 1"
Expand Down