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
303 changes: 262 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,239 @@ 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 (bbody.length > 0) 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 (body[default_body_index].body.length > 0) 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 (default_body_index > side_effect_index) {
let prev_body_index = default_index - 1;
for (; prev_body_index >= 0; prev_body_index--) {
if (body[prev_body_index].body.length > 0) break;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (body[prev_body_index].body.length > 0) break;
const maybe_inert_branch = body[prev_body_index];
if (aborts(maybe_inert_branch) || maybe_inert_branch.has_side_effects(compressor)) break;

If I correctly understand what you're doing here, this should match more "empty" branches safely.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct, and good idea.

This can actually be expanded to the "trim empty last branches", the forward direction when searching for the default branch's body in "trim side effect free branches falling into default", and finding the "default body" in "gut entire switch". I've added test cases for those.

}
let before = Math.max(side_effect_index, prev_body_index) + 1;
let after = default_index;
if (side_effect_index > default_index) {
after = side_effect_index;
body[side_effect_index].body = body[default_body_index].body;
} else {
default_or_exact.body = body[default_body_index].body;
}

body.splice(after + 1, default_body_index - after);
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 => branch.body.length > 0);
let caseBody;
if (i === body.length - 1) {
// There is only one case body. We can extract the body and place it after the switch.
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) {
break DEFAULT;
}

let sideEffect = body.find(branch => {
return (
branch !== default_or_exact
&& branch.expression.has_side_effects(compressor)
);
});
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);
}

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];
if (branch === default_or_exact) {
let statements = [statement(self.expression)];
if (branch.expression) {
statements.push(statement(branch.expression));
}
return make_node(AST_BlockStatement, branch, {
body: statements.concat(branch.body)
}).optimize(compressor);
} else {
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 +1835,46 @@ 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;
}
});

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
9 changes: 2 additions & 7 deletions test/compress/reduce_vars.js
Expand Up @@ -1947,13 +1947,8 @@ issue_1670_6: {
}
expect: {
(function(a) {
switch (1) {
case a = 1:
console.log(a);
break;
default:
console.log(2);
}
if (1 === (a = 1)) console.log(a);
else console.log(2);
})(1);
}
expect_stdout: "1"
Expand Down