Repository: swc-project/swc (rust)

Issue:
Execution order changed after minify
### Describe the bug

When using the `@swc/core` package to minify a source, the execution order is not kept when `collapse_vars` is set to `true`.

### Input code

```typescript
const swc = require('@swc/core')

function source() {
    let c = 0;
    let a = 1;

    c += a;
    a += 5;

    let b = c;

    console.log(a, b, c);
}

swc
    .minify(source.toString(), {})
    .then(output => {
         console.log(output.code)
    })
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es2022",
    "loose": false,
    "minify": {
      "compress": {
        "arguments": false,
        "arrows": false,
        "booleans": false,
        "booleans_as_integers": false,
        "collapse_vars": true,
        "comparisons": false,
        "computed_props": false,
        "conditionals": false,
        "dead_code": false,
        "directives": false,
        "drop_console": false,
        "drop_debugger": false,
        "evaluate": false,
        "expression": false,
        "hoist_funs": false,
        "hoist_props": false,
        "hoist_vars": false,
        "if_return": false,
        "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": false,
        "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": false
      },
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": true,
  "isModule": true
}
```

### Playground link

https://play.swc.rs/?version=1.2.218&code=H4sIAAAAAAAAA0srzUsuyczPUyjOLy1KTtXQVKjmUgCCnNQShWQFWwUDazg3Ecg1tOYC85MVtG0VEiFyiSC2KVQCpDAJqDAZpjA%2Frzg%2FJ1UvJz9dI1FHIUlHIVnTmquWCwC5tCMIeAAAAA%3D%3D&config=H4sIAAAAAAAAA22TTXLDIAyF78I623bRA3TXMzAEhKPW%2FAwSaTyZ3L0i9tjE7g59PMtPQrqrb7Lq466yKQSlnWiKbG7qQ4ENhmzBzOokMkHejASPk2JTBuAmoTe5G1MiWG5PKmBEP7VMNoVcgKid5YsaIDKtOlNK%2Bt3Cc0ojmHgE2pDGyDBA2S5tGkeTCfTVNMqlPmGQMpBS7IUhVwanc0m5x9EhY4oSrtCBcdomt9XisIBlvEInkjwiiiTuXqGDcx2G1sSFwtWM1fAmg9uzH%2FLbFV0SEmtfO8szevU7s7nYBaHXBbiWLdd3wviq%2BQEQs6MhiibAjnt5kz36R4bRy4vytFJ57s5ZhEFK1Ih%2Bq7N5h8LYpSrgqoVWqD3AF8uEDjR4L33v4C%2ByvXTpeMqQ%2FBZL%2F0xnYA71bsQW2kbiAD%2FFGPcPs%2FBg%2BLJnNIVzGg8pAvAluQOWZnDawyLjfMt7WqMDaTa47qLSGsrmBROHde4eDSRXG7g%2FW%2FJcyXf12LZw3gykr0XX4scfPu57mfYDAAA%3D

### Expected behavior

I expect the minify version to have the same execution order.

The original function logs `6 1 1` to the console.

### Actual behavior

The output code is the following:

```js
function a(){let a=0,b=1;b+=5;let c=a+=b;console.log(b,c,a)}
```

When executing the minified the console log is `6 6 6`.

### Version

1.2.218

### Additional context

_No response_

Pull request description:
fix(es/minifier): preserve side effect order in sequences
<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

gcc would optimize this as
```js
export function source() {
    console.log(6, 1, 1);
}
```
which is still a long long way to go

**BREAKING CHANGE:**

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**
closes #5280 

Patch:
diff --git a/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs b/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
index 81c37e32a5..0458e52b3d 100644
--- a/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
+++ b/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
@@ -1226,7 +1226,6 @@ where
             Mergable::Var(..) => {}
             Mergable::Expr(a) => {
                 if let Expr::Seq(a) = a {
-                    //
                     for a in a.exprs.iter_mut().rev() {
                         if self.merge_sequential_expr(&mut Mergable::Expr(a), b)? {
                             return Ok(true);
@@ -1235,6 +1234,10 @@ where
                         if !self.is_skippable_for_seq(None, a) {
                             return Ok(false);
                         }
+
+                        if a.may_have_side_effects(&self.expr_ctx) {
+                            return Ok(false);
+                        }
                     }
 
                     return Ok(false);
@@ -2018,6 +2021,7 @@ impl Visit for UsageCounter<'_> {
     }
 }
 
+#[derive(Debug)]
 enum Mergable<'a> {
     Var(&'a mut VarDeclarator),
     Expr(&'a mut Expr),


Test patch:
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5280/analysis-snapshot.rust-debug b/crates/swc_ecma_minifier/tests/fixture/issues/5280/analysis-snapshot.rust-debug
new file mode 100644
index 0000000000..0ca6371237
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5280/analysis-snapshot.rust-debug
@@ -0,0 +1,200 @@
+TestSnapshot {
+    vars: [
+        (
+            (
+                Atom('a' type=inline),
+                #2,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 3,
+                cond_init: false,
+                declared: true,
+                declared_count: 1,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 1,
+                mutation_by_call_count: 1,
+                usage_count: 3,
+                reassigned_with_assignment: true,
+                reassigned_with_var_decl: false,
+                mutated: true,
+                has_property_access: false,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: false,
+                is_fn_local: true,
+                used_by_nested_fn: true,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: Some(
+                    "let",
+                ),
+                var_initialized: true,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: true,
+                used_as_callee: false,
+                used_as_arg: true,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('b' type=inline),
+                #2,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 1,
+                cond_init: false,
+                declared: true,
+                declared_count: 1,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 1,
+                usage_count: 1,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: true,
+                has_property_access: false,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: false,
+                is_fn_local: true,
+                used_by_nested_fn: true,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: Some(
+                    "let",
+                ),
+                var_initialized: true,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: false,
+                used_as_callee: false,
+                used_as_arg: true,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('c' type=inline),
+                #2,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 3,
+                cond_init: false,
+                declared: true,
+                declared_count: 1,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 1,
+                mutation_by_call_count: 1,
+                usage_count: 3,
+                reassigned_with_assignment: true,
+                reassigned_with_var_decl: false,
+                mutated: true,
+                has_property_access: false,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: false,
+                is_fn_local: true,
+                used_by_nested_fn: true,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: Some(
+                    "let",
+                ),
+                var_initialized: true,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: true,
+                used_as_callee: false,
+                used_as_arg: true,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('console' type=inline),
+                #3,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 1,
+                cond_init: false,
+                declared: false,
+                declared_count: 0,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 0,
+                usage_count: 1,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: false,
+                has_property_access: true,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: true,
+                is_fn_local: true,
+                used_by_nested_fn: true,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: None,
+                var_initialized: false,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: false,
+                used_as_callee: false,
+                used_as_arg: false,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('source' type=inline),
+                #1,
+            ),
+            VarUsageInfo {
+                inline_prevented: true,
+                ref_count: 0,
+                cond_init: false,
+                declared: true,
+                declared_count: 1,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 0,
+                usage_count: 0,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: false,
+                has_property_access: false,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: false,
+                is_fn_local: true,
+                used_by_nested_fn: false,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: None,
+                var_initialized: true,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: true,
+                used_as_callee: false,
+                used_as_arg: false,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+    ],
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5280/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/5280/input.js
new file mode 100644
index 0000000000..959a47faf3
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5280/input.js
@@ -0,0 +1,11 @@
+export function source() {
+    let c = 0;
+    let a = 1;
+
+    c += a;
+    a += 5;
+
+    let b = c;
+
+    console.log(a, b, c);
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.js
new file mode 100644
index 0000000000..12d1aef41f
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.js
@@ -0,0 +1,6 @@
+export function source() {
+    let c = 0, a = 1;
+    c += a, a += 5;
+    let b = c;
+    console.log(a, b, c);
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.mangleOnly.js b/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.mangleOnly.js
new file mode 100644
index 0000000000..1edb4d9417
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5280/output.mangleOnly.js
@@ -0,0 +1,8 @@
+export function source() {
+    let a = 0;
+    let b = 1;
+    a += b;
+    b += 5;
+    let c = a;
+    console.log(b, c, a);
+}