Repository navigation
es/minifier: conditional constructor folding evaluates the callee before its condition #12216
Description
Describe the bug
Two new expressions with an equal callee and one argument are merged without checking whether the condition changes that callee. The output new F((F=G)?1:2) resolves F before assigning G, whereas the original resolves F after the condition. The sample constructs A1 instead of B1.
Input code
conditional_new_callee:
function A(n){this.value="A"+n}function B(n){this.value="B"+n}function run(F,G){return (F=G)?new F(1):new F(2)}console.log(run(A,B).value);Config
Options passed to minify, equivalent to the exact-source harness settings:
conditional_new_callee:
{
"compress": {
"defaults": false,
"conditionals": true
},
"mangle": false,
"module": false
}Link to the code that reproduces this issue
Primary input and options in SWC Playground
SWC Info output
Source build of swc_ecma_minifier 61.0.5 at 5dada7eaeafc39f7e3ad97ad1a1ea6eed57ceb65, macOS arm64, Rust 1.96.0-nightly (f5eca4fcf 2026-04-09), Node.js v20.20.2.
Observed checkout and freshly fetched authoritative origin/main are the same commit, fetched 2026-09-05T07:51:04Z. Original/emitted execution was independently repeated using an isolated checkout of that commit. The harness follows tests/exec.rs: parse Script, resolve bindings, convert Terser compressor options, optimize, apply hygiene/fixer, emit, then execute each program separately. The playground carries the portable source/settings; the source-build results do not depend on a published release matching main.
Expected behavior
conditional_new_callee: exit 0, stdout:
B1
Actual behavior
conditional_new_callee emits:
function A(n){this.value="A"+n;}function B(n){this.value="B"+n;}function run(F,G){return new F((F=G)?1:2);}console.log(run(A,B).value);Exit 0, stdout:
A1
With default compression (compress: {"conditionals": true}) this input also changes behavior. Observed stdout:
A1
Exit 0.
Version
swc_ecma_minifier 61.0.5, commit 5dada7eaeafc39f7e3ad97ad1a1ea6eed57ceb65, also freshly fetched main.
Additional context
Responsible code: compress_similar_cons_alt Expr::New pair.
Two new expressions with an equal callee and one argument are merged without checking whether the condition changes that callee. The output new F((F=G)?1:2) resolves F before assigning G, whereas the original resolves F after the condition. The sample constructs A1 instead of B1.
No TDZ violations, externally redefined undefined/NaN/Infinity, primitive-coercion side effects, Proxy/watch, frozen or locked objects, function source/stack dependence, callee/caller reflection, or arithmetic exceptions are needed. Local parameter shadowing is distinct from overriding a built-in object or externally redefining a global.
Proposed Scope
Restrict constructor-argument merging to a callee proven stable and side-effect-free across the test; conservatively decline when the test references/modifies that callee or when the callee itself has effects.
Limit the change to this transformation and its regression coverage. No public API, dependency, migration, deployment, or rollout change is required.
Acceptance Criteria and Test Scenarios
- The assigning-condition repro constructs B with value B1.
- A stable constructor and boolean parameter condition can still fold and produce A1.
- Add SWC-owned regression fixtures under
crates/swc_ecma_minifier/tests/fixture/issuesusing the exact inputs and configurations above. Assert the expected execution stdout/exit status as well as valid generated syntax. - Preserve the concrete passing controls below. Use the existing fixture/execution harness, then run
cargo test -p swc_ecma_minifierwithoutUPDATEafter refreshing fixture expectations.
new_control:
function A(n){this.value="A"+n}function run(F,test){return test?new F(1):new F(2)}console.log(run(A,true).value);Config:
{
"compress": {
"defaults": false,
"conditionals": true
},
"mangle": false,
"module": false
}Both original/output exit 0 and stdout is:
A1
Duplicate search
No same-root open duplicate found in bounded searches: conditional minifier.
Out of Scope
Unrelated rewrites, new options, and changing documented minifier semantic assumptions.
This is a message for readers, not the author of this issue.
Please read no +1 before leaving a comment.
Activity