Skip to content

Minifier retargets break and continue when unwrapping do...while(false) #12159

Description

@August-Z

Describe the bug

The loops compressor replaces a do...while(false) statement with its body even when that body contains a direct break or continue.

Those abrupt completions target the do loop. Once the loop is removed, they can target an enclosing loop and silently change program behavior. Without an enclosing target, the optimization can instead emit an illegal jump statement.

The bug does not require source labels. In the downstream Kotlin/JS case, SWC first normalizes a labeled jump such as break inner to an unlabeled break while the loop still exists; a later compression pass then removes that target loop. The unlabeled reproduction below demonstrates the same underlying issue directly.

Input code

function run() {
    const trace = [];

    for (let i = 0; i < 2; i++) {
        do {
            trace.push("body" + i);
            break;
        } while (false);

        trace.push("tail" + i);
    }

    return trace.join(",");
}

console.log(run());

Config

// @swc/core minifySync options
{
    "compress": {
        "dead_code": true,
        "evaluate": true,
        "loops": true,
        "passes": 2
    },
    "mangle": false
}

Link to the code that reproduces this issue

https://github.com/August-Z/swc/tree/aca52cb768faf2f649e0cc076b6a053cebc4b6b1/crates/swc_ecma_minifier/tests/fixture/issues/do-while-false-control-flow

SWC Info output

Operating System:
Platform: darwin
Arch: arm64
Machine Type: arm64
Version: Darwin Kernel Version 25.0.0
CPU: Apple M3 Pro (12 cores)

Binaries:
Node: 26.3.0
npm: 11.16.0
Yarn: 1.22.22
pnpm: 11.19.0

Relevant Packages:
@swc/core: 1.16.1 (verified in an isolated npm install)
@swc/helpers: N/A
@swc/types: N/A

SWC Config:
API: minifySync
.swcrc path: N/A

Expected behavior

The break must continue to target the inner do...while loop. Executing the transformed code should print:

body0,tail0,body1,tail1

Actual behavior

The inner loop is removed and its break is retargeted to the outer for loop. With @swc/core@1.16.1, the output is:

function run(){let trace=[];for(let i=0;i<2;i++){trace.push("body"+i);break}return trace.join(",")}console.log(run());

It prints only body0.

Version

@swc/core 1.16.1; also reproduced on swc main at 5b8c98d

Additional context

This was first observed downstream in web-infra-dev/rspack#15335. Kotlin/JS 1.7.20 emits a labeled single-iteration do...while(false) inside a suspend-function state-machine loop. After compression, the inner jump exits the outer state-machine loop, so later code is removed and the function returns undefined.

The behavior is independently reproducible with @swc/core and does not require Rspack, labels, or mangling. A direct continue has the analogous control-target problem. Without an enclosing loop, function f(){do{break;}while(false);return 42} can be reduced to invalid code equivalent to function f(){break}.

On current main (commit 5b8c98d909b4c5db2730877f0be09182ee470a26, swc_ecma_minifier 61.0.3), the affected path is optimize_loops_with_constant_condition in crates/swc_ecma_minifier/src/compress/pure/loops.rs. It calls should_not_inline_loop_body(&stmt.body, true), allowing the direct jump while removing its target.

Disabling the loops compressor option is the narrow reliable workaround.

SWC previously added a conservative guard for the separate simplify transform in #1618. This report concerns the minifier compressor's independent constant-loop optimization.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions