Latest Results
fix(es/minifier): preserve rest array identity when its reference runs more than once (#12546)
**Description:**
With the default options, the `Pat::Rest` arm of `inline_args_of_iife`
copies the new array literal into the rest parameter's only reference
even when that reference runs more than once (in a loop or in a nested
function). Each evaluation then allocates a new array, so mutations and
identity are lost:
```js
function logAll(...lines) {
for (let i = 0; i < 3; i++) console.log(lines.shift());
}
logAll("a", "b", "c");
```
With `module: true` this becomes `for(let
o=0;o<3;o++)console.log(["a","b","c"].shift());`, which prints `a a a`
instead of `a b c` (`logAll` is used once, so it becomes an IIFE first).
With the default script options, `var g = function (...a) { return () =>
a.push(0); }(1); console.log(g(), g());` prints `2 2` instead of `2 3`.
#12238 made this arm refuse RegExp arguments because they allocate on
each evaluation. The rest array allocates on each evaluation too, but
the arm only checks `ref_count == 1`, which counts occurrences, not
evaluations.
This adds one `continue` to that arm. If the reference can run more than
once (`EXECUTED_MULTIPLE_TIME`, or not `IS_FN_LOCAL`) and the array is
mutated, indexed with a dynamic key, used as a value (`USED_AS_REF`, as
in `({ length: a }).length`), or used through any property other than
`length`, the rest parameter is kept. These are the flags
`store_var_for_inlining` already checks for a single-use RegExp and for
arrays whose element reads are inlined. Reads of `length` and of literal
indices are still inlined: `((...r) => () => r[0])(1)` from #12238 still
becomes `()=>1`, and `((...a) => () => a.length)(1, 2)` still becomes
`()=>2`.
When the `continue` applies, the IIFE keeps its rest parameter and the
arguments it collects as written, like the existing `continue` for
RegExp arguments. The analyzer is unchanged.
The cost: when the reference can run more than once, some forms that
were already correct also keep the rest parameter. These are
non-mutating methods (`a.join(" ")`, `a.includes(x)`), writes such as
`a.length = 0`, `a[i]` with a dynamic `i`, and a rest IIFE that is
itself inside a loop. For example, `function fmt(...p) { return () =>
p.join(" "); } console.log(fmt("a", "b")());` with `module: true` goes
from 19 to 64 bytes.
Performance: a few flag tests, plus a scan of `accessed_props` when the
reference can run more than once, only for a rest parameter that already
passed the existing checks.
**Tests:**
`tests/fixture/issues/12545/`, with `expected.stdout`:
- 8 cases that print a different value without the change:
- `push` in a loop and in a closure;
- `shift` through a function used once (`makeQueue`, `logAll`);
- `a[0]++` in a loop;
- `a[k](v)` in a closure;
- the array escaping through a literal (`[a, h()][0].push(0)`, `{
length: a }.length`).
- 2 controls that keep inlining: `a.length` in a closure and a `push`
that runs once. They and the #12219 fixtures pass before and after.
Checks:
- `cargo test -p swc_ecma_minifier`: 8033 passed, 0 failed (8023 + 10).
No fixture or snapshot output changes, including `benches-full` (`d3`,
`victory`).
- `./scripts/exec.sh`: passes.
- `cargo clippy -p swc_ecma_minifier --all-targets -- -D warnings` and
`cargo fmt --all -- --check`: clean.
- End to end with the `Compiler::minify` pipeline (40 snippets, script
and module): 52 of 80 runs differ without the change, none with it.
**Related issue (if exists):**
Closes #12545
---
Found with a differential fuzzer. Drafted with AI assistance. Latest Branches
+6%
baltasarblanco:fix/fn-decl-loop-body-sloppy -2%
+1%
© 2026 CodSpeed Technology