swc-project / swc

Rust-based platform for the Web
https://swc.rs
Apache License 2.0
31.03k stars 1.22k forks source link

Wrong compress for consecutive switch statements #9619

Open canalun opened 5 hours ago

canalun commented 5 hours ago

Describe the bug

The minifier compresses consecutive switch statements wrongly.

Input code

var a = (() => {
  switch ("production") {
    case "production":
      return "expected";
    default:
      return "unexpected1";
  }

  switch ("production") {
    case "production":
      return "unexpected2";
    default:
      return "unexpected3";
  }
})();

console.log(a);

Config

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es5",
    "loose": false,
    "minify": {
      "compress": {
        "arguments": false,
        "arrows": false,
        "booleans": false,
        "booleans_as_integers": false,
        "collapse_vars": false,
        "comparisons": false,
        "computed_props": false,
        "conditionals": false,
        "dead_code": true,
        "directives": false,
        "drop_console": false,
        "drop_debugger": false,
        "evaluate": false,
        "expression": false,
        "hoist_funs": false,
        "hoist_props": false,
        "hoist_vars": false,
        "if_return": true,
        "join_vars": false,
        "keep_classnames": false,
        "keep_fargs": false,
        "keep_fnames": false,
        "keep_infinity": false,
        "loops": false,
        "negate_iife": false,
        "properties": false,
        "reduce_funcs": false,
        "reduce_vars": false,
        "side_effects": false,
        "switches": true,
        "typeofs": false,
        "unsafe": false,
        "unsafe_arrows": false,
        "unsafe_comps": false,
        "unsafe_Function": false,
        "unsafe_math": false,
        "unsafe_symbols": false,
        "unsafe_methods": false,
        "unsafe_proto": false,
        "unsafe_regexp": false,
        "unsafe_undefined": false,
        "unused": true,
        "const_to_let": false,
        "pristine_globals": false,
        "passes": 1
      },
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": true
}

Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.7.28&code=H4sIAAAAAAAAA6WNQQoCMRAE73lFk1NyEdSby%2FqXMBl1IWSWZKKC7N%2BVoCieBK9dRdc5FASMcM5j3ONmgHqZlE5wdi4SG%2Bkk2fpOAAqV8Ql2fQYKaysZlq8zk3K0QweRD6El%2FbZafnnrLi7m3%2B77cfNrevtML975wRiSXCXxKsnRhcdwB02YIAgaAQAA&config=H4sIAAAAAAAAA32VO3LjMAyGe5%2FCozrNFrtFDrBdzsChSVCmVyQ1BOhYk%2FHdF6IkJ3FAdRY%2BQDB%2BPPRxOB67C5ru9fjBP%2Flh1BkhP57ZglMkfWNLByZoNNmP1L1s9IIzcnpAqKb7QjrSuQeqUfh7de%2BGlBA299UWfPRu%2BprQpDBmQPxiYyu%2FrwSIhN%2FjV5bTuwROKQ2g4x5SGpWPBD1kyc2kYdAjgrrqBg8smcckJplpIbBqzGmUHaL15FNku4AtaKtMsrNolMs35DMY8lcQ4zgdx0XkGlvYwqn0fe31M4erHoomKRRutTf8jwV4Th5JuSJqscCWEAttiOydykAlx58yXJKPrbB%2FAKzCoBGjDqJO1cPxaLXhbqiPjseXJoHzrIuFRuhZWeW9k%2BSd5YFMXkyZwRYDs75mBzfEQG9BgXM8NCJ%2B92TONe2TwjSNkJwUw33WYhULUM21XPm8HTv4L9dJ8qCtHkHTuU1xCqckrtUWDnROdseBu0GpjTMfjdvY5iVa4PkAK7oUrOBJ7XlpSVFSQ72eP%2BeD14RfqfohneSbMfK81z7%2BWm33x7EOOvafB2G514fVoQvJlgrXL8Hc9%2BV%2B%2F%2Bk%2BnbZr%2FcjaeXzbIudaDvf%2Fe2YcFlUGAAA%3D

SWC Info output

No response

Expected behavior

The below result is expected.

console.log("expected");

Actual behavior

I got the below code.

console.log("unexpected2");

Version

1.7.28

Additional context

I investigated it and have submitted a PR. If my approach does not seem too off-track, could you possibly review it? :)

https://github.com/swc-project/swc/pull/9620

approach

Based on the debug log in the "investigation" section, I think it's necessary to process consecutive return statements properly in the place where if_return: Merging returns occurs, that is merge_if_returns().

Specifically, I found the below code, code A, is generated in the process, and then code A is converted into code B by merge_if_returns().

// code A

var a = function() {
  return void "production", void "production", "expected";
  return void "production", void "production", "unexpected2";
}();
console.log(a);

merge_if_returns()

// code B

var a = function() {
  return void "production", void "production", "expected", void "production", void "production", "unexpected2";
}();
console.log(a);

The PR makes merge_if_returns() check if it receives consecutive return statements. If it does, it will do nothing, and dead code elimination works and delete the latter statement.

investigation

---- Input -----
var a = (() => {
  switch ("production") {
    case "production":
      return "expected";
    default:
      return "unexpected1";
  }

  switch ("production") {
    case "production":
      return "unexpected2";
    default:
      return "unexpected3";
  }
})();

console.log(a);

   INFO  Done in 125ns, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in inline global defs
    in minify

   INFO  Done in 510.625µs, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in precompress
    in minify

   INFO  Done in 1.145833ms, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in remove dead code
    in minify

  DEBUG  ===== Start =====
var a = (()=>{
    switch("production"){
        case "production":
            return "expected";
        default:
            return "unexpected1";
    }
    switch("production"){
        case "production":
            return "unexpected2";
        default:
            return "unexpected3";
    }
})();
console.log(a);

    at crates/swc_ecma_minifier/src/compress/mod.rs:179
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  compress: Running expression simplifier (pass = 1)
    at crates/swc_ecma_minifier/src/compress/mod.rs:184
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  compress: expr_simplifier took 124.25µs (pass = 1)
    at crates/swc_ecma_minifier/src/compress/mod.rs:212
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  Done in 410.5µs, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in apply pure optimizer
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  Done in 207.542µs, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in analyze
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  switches: Removing a constant switch, kind: "change"
    at crates/swc_ecma_minifier/src/compress/optimize/switches.rs:134
    in visit_mut_stmt with start: "switch(\"production\"){\n    case \"production\":\n        return \"expected\";\n    default:\n        return \"unexpected1\";\n}\n"
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_block_stmt
    in visit_mut_arrow_expr
    in visit_mut_call_expr
    in visit_mut_var_declarator
    in visit_mut_var_declarators
    in visit_mut_var_decl
    in visit_mut_decl
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_script
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  switches: Removing a constant switch, kind: "change"
    at crates/swc_ecma_minifier/src/compress/optimize/switches.rs:134
    in visit_mut_stmt with start: "switch(\"production\"){\n    case \"production\":\n        return \"unexpected2\";\n    default:\n        return \"unexpected3\";\n}\n"
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_block_stmt
    in visit_mut_arrow_expr
    in visit_mut_call_expr
    in visit_mut_var_declarator
    in visit_mut_var_declarators
    in visit_mut_var_decl
    in visit_mut_decl
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_script
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  if_return: Merging returns, kind: "change"
    at crates/swc_ecma_minifier/src/compress/optimize/if_return.rs:279
    in visit_mut_arrow_expr
    in visit_mut_call_expr
    in visit_mut_var_declarator
    in visit_mut_var_declarators
    in visit_mut_var_decl
    in visit_mut_decl
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_script
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  if_return: Merging returns, kind: "change"
    at crates/swc_ecma_minifier/src/compress/optimize/if_return.rs:279
    in visit_mut_arrow_expr
    in visit_mut_call_expr
    in visit_mut_var_declarator
    in visit_mut_var_declarators
    in visit_mut_var_decl
    in visit_mut_decl
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_script
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  if_return: Merging returns, kind: "change"
    at crates/swc_ecma_minifier/src/compress/optimize/if_return.rs:279
    in visit_mut_arrow_expr
    in visit_mut_call_expr
    in visit_mut_var_declarator
    in visit_mut_var_declarators
    in visit_mut_var_decl
    in visit_mut_decl
    in visit_mut_stmt
    in handle_stmt_likes
    in visit_mut_stmts
    in visit_mut_script
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  Done in 1.501458ms, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in apply full optimizer
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  compress: dead_branch_remover took 101µs (pass = 1)
    at crates/swc_ecma_minifier/src/compress/mod.rs:302
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  ===== Removed dead branches =====
var a#2 = ()=>{
    return void "production", void "production", "expected", void "production", void "production", "unexpected2";
}();
console#1.log(a#2);

==== ===== ===== ===== ======
var a#2 = ()=>{
    return "expected", "unexpected2";
}();
console#1.log(a#2);

    at crates/swc_ecma_minifier/src/compress/mod.rs:314
    in optimize with pass: 1
    in compress ast
    in minify

   INFO  Done in 2.994583ms, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in optimize with pass: 1
    in compress ast
    in minify

  DEBUG  ===== Done =====
var a#2 = ()=>{
    return "expected", "unexpected2";
}();
console#1.log(a#2);

    at crates/swc_ecma_minifier/src/compress/mod.rs:146
    in optimize with pass: 2
    in compress ast
    in minify

   INFO  Done in 19.792µs, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in optimize with pass: 2
    in compress ast
    in minify

   INFO  Done in 3.055542ms, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in compress ast
    in minify

  DEBUG  arrows: Optimizing the body of an arrow, kind: "change"
    at crates/swc_ecma_minifier/src/compress/pure/arrows.rs:52
    in postcompress
    in minify

   INFO  Done in 51.542µs, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in postcompress
    in minify

   INFO  Done in 5.809292ms, kind: "perf"
    at crates/swc_timer/src/lib.rs:32
    in minify

   INFO  optimize(/Users/XXXXX/swc/crates/swc_ecma_minifier/tests/fixture/issues/XXXXX/input.js) took 5.907625ms
    at crates/swc_ecma_minifier/tests/compress.rs:254

  DEBUG  Renaming `a#2` to `a`
    at crates/swc_ecma_transforms_base/src/rename/analyzer/scope.rs:161

   INFO  process(/Users/XXXXX/swc/crates/swc_ecma_minifier/tests/fixture/issues/XXXXX/input.js) took 8.09675ms
    at crates/swc_ecma_minifier/tests/compress.rs:267

---- Output -----
var a = (()=>"unexpected2")();
console.log(a);
canalun commented 5 hours ago

I submitted the PR and added the link to it in the above description :)