fix(minifier): preserve sibling declarations in arguments copy rewrite (#27140)

## Summary

- Keep sibling `var` declarators when rewriting an `arguments` copy loop to `[...arguments]`. This preserves bindings that later code still uses.
- Leave loops with initialized sibling declarators unchanged, because moving the spread across an initializer could change evaluation order.

Fixes #27138
This commit is contained in:
camc314 committed 2026-09-28 13:36:35 +00:00
1 parent 6531ce925c
commit 4cf9ae275b
3 files changed
+39 -19

No files matched your search

@@ -844,8 +844,12 @@ impl<'a> PeepholeOptimizations {
let Some(init) = &mut for_stmt.init else { return };
let ForStatementInit::VariableDeclaration(var_init) = init else { return };
// Need at least two declarators: r, a (optional `e` may precede them)
if var_init.declarations.len() < init_decl_len {
let Some(siblings) = var_init.declarations.get(init_decl_len..) else { return };
// Sibling declarators may be in the source loop or folded into it by `sequences`.
// Keep uninitialized `var` siblings, but don't move an initializer across the spread.
if !siblings.is_empty()
&& (!var_init.kind.is_var() || siblings.iter().any(|decl| decl.init.is_some()))
{
return;
}
@@ -951,7 +955,7 @@ impl<'a> PeepholeOptimizations {
}
}
// Build `var r = [...arguments]` (with optional `.slice(offset)`) as the only declarator and drop test/update/body.
// Build `var r = [...arguments]` (with optional `.slice(offset)`) and drop test/update/body.
let r_id_pat = {
let (r_id, de_id_symbol_id) = r_id_pat_with_info;
@@ -995,23 +999,21 @@ impl<'a> PeepholeOptimizations {
};
let new_decl = VariableDeclarator::new(SPAN, r_id_pat, None, Some(arr), false, ctx);
// The old declarators (`e`, `a`, and `r`'s original init) are
// replaced wholesale — walk them so refs inside (e.g. `e` in
// `Array(e > 1 ? e - 1 : 0)`) reach `PassChanges`. The moved-out
// `r` binding and `arguments` ident left id-less dummies behind.
for decl in &var_init.declarations {
ctx.drop_variable_declarator(decl);
// Drop only the copy-loop declarators. Siblings remain in the `var` statement.
// Walk removed declarators so refs inside them reach `PassChanges`.
for decl in var_init.declarations.drain(..init_decl_len) {
ctx.drop_variable_declarator(&decl);
}
var_init.declarations = ArenaVec::from_value_in(new_decl, ctx);
var_init.declarations.insert(0, new_decl);
} else {
// `for (var; 0;)` with an empty `VariableDeclaration` is invalid JS when printed and
// makes `try_fold_for` hoist a bogus `var;`. Use `for (; 0;)` instead so dead-code
// folding becomes an empty statement. Walk the dropped
// declarators so their refs reach `PassChanges`.
for decl in &var_init.declarations {
ctx.drop_variable_declarator(decl);
// The copied array is unused, but sibling declarations still need to survive.
for decl in var_init.declarations.drain(..init_decl_len) {
ctx.drop_variable_declarator(&decl);
}
// Avoid invalid `for (var; 0;)` when no siblings remain.
if var_init.declarations.is_empty() {
for_stmt.init = None;
}
for_stmt.init = None;
}
if let Some(old) = for_stmt.test.take() {
ctx.drop_expression(&old);
@@ -1054,6 +1054,24 @@ fn test_rewrite_arguments_copy_loop() {
test_same(
"function _() { for (var e = arguments.length, r = Array(e), a = 0; a < e; a++) r[a] = arguments[a]; console.log(r, a) }",
);
// A previous pass may fold sibling `var` declarations into the loop initializer.
// Those bindings must survive even when the copy loop looks otherwise rewritable.
test(
"function _() { for (var r = [], a = 0, keep; a < arguments.length; a++) r[a] = arguments[a]; use(r, keep) }",
"function _() { var r = [...arguments], keep; use(r, keep) }",
);
test(
"function _() { var items = [], t = 0, i, u, r; for (; t < arguments.length; t++) items[t] = arguments[t]; if (this.list = [], items != null) for (i = 0, u = items; i < u.length; i++) r = u[i], this.list.push(r) }",
"function _() { var items = [...arguments], i, u, r; if (this.list = [], items != null) for (i = 0, u = items; i < u.length; i++) r = u[i], this.list.push(r) }",
);
test(
"function _() { for (var r = [], a = 0, keep; a < arguments.length; a++) r[a] = arguments[a]; use(keep) }",
"function _() { var keep; use(keep) }",
);
test_same(
"function _() { for (var r = [], a = 0, keep = side(); a < arguments.length; a++) r[a] = arguments[a]; use(r, keep) }",
);
}
#[test]
@@ -49,9 +49,9 @@ antd.js:
sys deallocs: 860
sys alloc bytes: 29973448 # 29.97 MB
sys peak growth: 19934725 # 19.93 MB
arena allocs: 181501
arena allocs: 181469
arena reallocs: 34406
arena size: 43041368 # 43.04 MB
arena size: 43039576 # 43.04 MB
binder.ts:
file size: 193077 # 193.08 kB