From 804e52f47cb5baf319889210661d1bacd10b4e7b Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Mon, 23 Aug 2021 17:53:02 -0400 Subject: [PATCH] Collapse inert bodies --- lib/compress/index.js | 37 ++++++++++++--- test/compress/switch.js | 99 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/lib/compress/index.js b/lib/compress/index.js index feb5df434..ceb43eba0 100644 --- a/lib/compress/index.js +++ b/lib/compress/index.js @@ -1627,7 +1627,7 @@ def_optimize(AST_Switch, function(self, compressor) { for (; i >= 0; i--) { let bbody = body[i].body; if (is_break(bbody[bbody.length - 1], compressor)) bbody.pop(); - if (bbody.length > 0) break; + 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. @@ -1658,7 +1658,7 @@ def_optimize(AST_Switch, function(self, compressor) { 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; + if (!is_inert_body(body[default_body_index])) break; } let side_effect_index = body.length - 1; for (; side_effect_index >= 0; side_effect_index--) { @@ -1666,31 +1666,46 @@ def_optimize(AST_Switch, function(self, compressor) { 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 (body[prev_body_index].body.length > 0) break; + 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 => branch.body.length > 0); + 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) { - // There is only one case body. We can extract the body and place it after the switch. + // All cases fall into the case body. let branch = body[i]; if (has_nested_break(self)) break DEFAULT; @@ -1701,6 +1716,7 @@ def_optimize(AST_Switch, function(self, compressor) { }); branch.body = []; } else if (i !== -1) { + // If there are multiple bodies, then we cannot optimize anything. break DEFAULT; } @@ -1710,6 +1726,7 @@ def_optimize(AST_Switch, function(self, compressor) { && 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( @@ -1720,9 +1737,14 @@ def_optimize(AST_Switch, function(self, compressor) { }).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 @@ -1875,6 +1897,11 @@ def_optimize(AST_Switch, function(self, compressor) { 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) { diff --git a/test/compress/switch.js b/test/compress/switch.js index fcdc01825..abcabee2b 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -2119,17 +2119,50 @@ issue_1680_2: { } expect: { var a = 100, b = 10; + switch (b) { + case a--: + case b: + var c; + case a: + case a--: + } + console.log(a, b); + } + expect_stdout: ["99 10"] +} + +issue_1680_4: { + options = { + dead_code: true, + switches: true, + } + input: { + var a = 10, b = 10; switch (b) { case a--: break; case b: var c; + break; case a: + break; case a--: + break; } console.log(a, b); } - expect_stdout: true + expect: { + var a = 10, b = 10; + switch (b) { + case a--: + case b: + var c; + case a: + case a--: + } + console.log(a, b); + } + expect_stdout: ["9 10"] } issue_1690_1: { @@ -2404,17 +2437,17 @@ collapse_same_branches: { case 1: console.log("PASS"); break - - case 2: + + case 2: console.log("PASS"); break - + } } expect: { switch (id(1)) { case 1: - case 2: + case 2: console.log("PASS"); } } @@ -2431,8 +2464,8 @@ collapse_same_branches_2: { switch (id(1)) { case 1: console.log("PASS"); - - case 2: + + case 2: console.log("PASS"); } } @@ -2440,8 +2473,8 @@ collapse_same_branches_2: { switch (id(1)) { case 1: console.log("PASS"); - - case 2: + + case 2: console.log("PASS"); } } @@ -2462,7 +2495,7 @@ trim_empty_last_branches: { // break should be removed too break case 3: {} - case 4: + case 4: } } expect: { @@ -2485,7 +2518,7 @@ trim_empty_last_branches_2: { case 2: break somewhere_else case 3: {} - case 4: + case 4: } } } @@ -2525,6 +2558,29 @@ trim_side_effect_free_branches_falling_into_default: { } } +trim_side_effect_free_branches_falling_into_default_2: { + options = { + switches: true, + dead_code: true + } + input: { + switch (id(1)) { + default: + case 0: + "no side effect" + case 1: + console.log("PASS default") + case 2: + console.log("PASS 2") + } + } + expect: { + if (2 !== id(1)) + console.log("PASS default"); + console.log("PASS 2") + } +} + gut_entire_switch: { options = { switches: true, @@ -2545,6 +2601,27 @@ gut_entire_switch: { expect_stdout: "PASS" } +gut_entire_switch_2: { + options = { + switches: true, + dead_code: true + } + input: { + switch (id(123)) { + case 1: + "no side effect" + case 1: + // Not here either + default: + console.log("PASS"); + } + } + expect: { + id(123); console.log("PASS"); + } + expect_stdout: "PASS" +} + turn_into_if: { options = { switches: true,