mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
feat(minifier): expand fold leading assignments into the var decl (#26142)
expands leading assignment inlining into variable declarations for
`return`, `switch`, `if`, `throw`, `for`, `for in`, `for of`
```js
// return
var a; return a = b(), c;
var a = b(); return c;
// throw
var a; throw a = b(), c;
var a = b(); throw c;
// switch
var a; switch (a = b(), c) {}
var a = b(); switch (c) {}
// for
var a; for (a = b(), c; ;) d;
var a = b(); for (c; ;) d;
// if
var a; if (a = b(), c) d;
var a = b(); if (c) d;
```
fixes #25949
This commit is contained in:
1 parent
4adfb4c949
commit
1e902cc1ad
7 files changed
+197
-74
No files matched your search
@@ -352,13 +352,14 @@ impl<'a> PeepholeOptimizations {
|
||||
fn handle_variable_declaration(
|
||||
mut var_decl: ArenaBox<'a, VariableDeclaration<'a>>,
|
||||
result: &mut ArenaVec<'a, Statement<'a>>,
|
||||
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
if let Some(first_decl) = var_decl.declarations.first_mut()
|
||||
&& let Some(first_decl_init) = first_decl.init.as_mut()
|
||||
{
|
||||
Self::substitute_single_use_symbol_in_statement(first_decl_init, result, ctx, false);
|
||||
// "var a; var b = (a = 1, c);" => "var a = 1; var b = c;"
|
||||
Self::merge_leading_assignments_to_declaration(first_decl_init, false, result, ctx);
|
||||
}
|
||||
Self::substitute_single_use_symbol_within_declaration(
|
||||
var_decl.kind,
|
||||
@@ -455,64 +456,22 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx.drop_statement(&dropped);
|
||||
}
|
||||
// "var a; a = b();" => "var a = b();"
|
||||
match &mut expr_stmt.expression {
|
||||
Expression::AssignmentExpression(assign_expr) => {
|
||||
let merged = Self::merge_assignment_to_declaration(assign_expr, result, ctx);
|
||||
if merged {
|
||||
let dropped = Statement::ExpressionStatement(expr_stmt);
|
||||
ctx.drop_statement(&dropped);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Expression::SequenceExpression(sequence_expr)
|
||||
if result
|
||||
.last()
|
||||
.is_some_and(|stmt| matches!(stmt, Statement::VariableDeclaration(_))) =>
|
||||
{
|
||||
let first_non_merged_index =
|
||||
sequence_expr.expressions.iter_mut().position(|expr| {
|
||||
if let Expression::AssignmentExpression(assign_expr) = expr {
|
||||
!Self::merge_assignment_to_declaration(assign_expr, result, ctx)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let sequence_len = sequence_expr.expressions.len();
|
||||
match first_non_merged_index {
|
||||
None => {
|
||||
// all elements are merged
|
||||
let dropped = Statement::ExpressionStatement(expr_stmt);
|
||||
ctx.drop_statement(&dropped);
|
||||
return;
|
||||
}
|
||||
Some(val) if val == sequence_len - 1 => {
|
||||
// all elements are merged except for the last expression
|
||||
let last_expr = sequence_expr.expressions.pop().unwrap();
|
||||
result.push(Statement::new_expression_statement(
|
||||
last_expr.span(),
|
||||
last_expr,
|
||||
ctx,
|
||||
));
|
||||
let dropped = Statement::ExpressionStatement(expr_stmt);
|
||||
ctx.drop_statement(&dropped);
|
||||
return;
|
||||
}
|
||||
Some(0) => {
|
||||
// no elements are merged
|
||||
}
|
||||
Some(val) => {
|
||||
for dropped in sequence_expr.expressions.drain(0..val) {
|
||||
ctx.drop_expression(&dropped);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if Self::merge_leading_assignments_to_declaration(
|
||||
&mut expr_stmt.expression,
|
||||
true,
|
||||
result,
|
||||
ctx,
|
||||
) {
|
||||
let dropped = Statement::ExpressionStatement(expr_stmt);
|
||||
ctx.drop_statement(&dropped);
|
||||
return;
|
||||
}
|
||||
|
||||
result.push(Statement::ExpressionStatement(expr_stmt));
|
||||
}
|
||||
|
||||
/// Merge a single assignment into a matching uninitialized declarator of the
|
||||
/// variable declaration that immediately precedes it in `result`.
|
||||
fn merge_assignment_to_declaration(
|
||||
assign_expr: &mut AssignmentExpression<'a>,
|
||||
result: &mut ArenaVec<'a, Statement<'a>>,
|
||||
@@ -566,6 +525,60 @@ impl<'a> PeepholeOptimizations {
|
||||
false
|
||||
}
|
||||
|
||||
/// Fold leading assignments in `expr` into the variable declaration.
|
||||
fn merge_leading_assignments_to_declaration(
|
||||
expr: &mut Expression<'a>,
|
||||
value_is_discarded: bool,
|
||||
result: &mut ArenaVec<'a, Statement<'a>>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> bool {
|
||||
let Some(Statement::VariableDeclaration(_)) = result.last() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match expr {
|
||||
// "var a; a = b();" => "var a = b();"
|
||||
Expression::AssignmentExpression(assign_expr) if value_is_discarded => {
|
||||
Self::merge_assignment_to_declaration(assign_expr, result, ctx)
|
||||
}
|
||||
// "var a; return a = b(), c;" => "var a = b(); return c;"
|
||||
Expression::SequenceExpression(sequence_expr) => {
|
||||
// The value of the last expression is observed unless discarded.
|
||||
let limit = sequence_expr.expressions.len() - usize::from(!value_is_discarded);
|
||||
let first_non_merged_index =
|
||||
sequence_expr.expressions.iter_mut().take(limit).position(|expr| match expr {
|
||||
Expression::AssignmentExpression(assign_expr) => {
|
||||
!Self::merge_assignment_to_declaration(assign_expr, result, ctx)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
// `None` means every candidate merged, so the whole prefix is taken.
|
||||
let taken = first_non_merged_index.unwrap_or(limit);
|
||||
if taken == 0 {
|
||||
return false;
|
||||
}
|
||||
match sequence_expr.expressions.len() - taken {
|
||||
// all elements are merged
|
||||
0 => true,
|
||||
// all elements are merged except for the last expression
|
||||
1 => {
|
||||
let only_expr = sequence_expr.expressions.pop().unwrap();
|
||||
ctx.replace_expression(expr, only_expr);
|
||||
false
|
||||
}
|
||||
// The sequence stays in the AST, so remove the targets here.
|
||||
_ => {
|
||||
for dropped in sequence_expr.expressions.drain(0..taken) {
|
||||
ctx.drop_expression(&dropped);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a switch case can be inlined by verifying:
|
||||
/// - The test expression has no side effects
|
||||
/// - All statements can be safely inlined (no unlabeled breaks)
|
||||
@@ -600,6 +613,14 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx.drop_statement(&dropped);
|
||||
}
|
||||
|
||||
// "var a; switch (a = b(), c) {}" => "var a = b(); switch (c) {}"
|
||||
Self::merge_leading_assignments_to_declaration(
|
||||
&mut switch_stmt.discriminant,
|
||||
false,
|
||||
result,
|
||||
ctx,
|
||||
);
|
||||
|
||||
if !ctx.is_tree_shake_only()
|
||||
&& switch_stmt.cases.len() == 1
|
||||
&& Self::can_switch_case_be_inlined(&switch_stmt.cases[0])
|
||||
@@ -664,6 +685,9 @@ impl<'a> PeepholeOptimizations {
|
||||
) -> ControlFlow<()> {
|
||||
Self::substitute_single_use_symbol_in_statement(&mut if_stmt.test, result, ctx, false);
|
||||
|
||||
// "var a; if (a = b(), c) d;" => "var a = b(); if (c) d;"
|
||||
Self::merge_leading_assignments_to_declaration(&mut if_stmt.test, false, result, ctx);
|
||||
|
||||
// Absorb a previous expression statement
|
||||
if ctx.options().sequences {
|
||||
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
|
||||
@@ -828,6 +852,12 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx.replace_expression(argument, new_arg);
|
||||
result.pop();
|
||||
}
|
||||
|
||||
// "var a; return a = b(), c;" => "var a = b(); return c;"
|
||||
if let Some(argument) = &mut ret_stmt.argument {
|
||||
Self::merge_leading_assignments_to_declaration(argument, false, result, ctx);
|
||||
}
|
||||
|
||||
result.push(Statement::ReturnStatement(ret_stmt));
|
||||
}
|
||||
|
||||
@@ -853,6 +883,15 @@ impl<'a> PeepholeOptimizations {
|
||||
let dropped = result.pop().unwrap();
|
||||
ctx.drop_statement(&dropped);
|
||||
}
|
||||
|
||||
// "var a; throw a = b(), c;" => "var a = b(); throw c;"
|
||||
Self::merge_leading_assignments_to_declaration(
|
||||
&mut throw_stmt.argument,
|
||||
false,
|
||||
result,
|
||||
ctx,
|
||||
);
|
||||
|
||||
result.push(Statement::ThrowStatement(throw_stmt));
|
||||
}
|
||||
|
||||
@@ -886,6 +925,12 @@ impl<'a> PeepholeOptimizations {
|
||||
match_expression!(ForStatementInit) => {
|
||||
let init = init.to_expression_mut();
|
||||
Self::substitute_single_use_symbol_in_statement(init, result, ctx, false);
|
||||
// "var a; for (a = b(), c; ;) d;" => "var a = b(); for (c; ;) d;"
|
||||
if Self::merge_leading_assignments_to_declaration(init, true, result, ctx)
|
||||
&& let Some(old_init) = for_stmt.init.take()
|
||||
{
|
||||
ctx.drop_expression(old_init.to_expression());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,6 +1026,15 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx,
|
||||
is_block_scoped_decl,
|
||||
);
|
||||
// "var a; for (b in a = c(), d) e;" => "var a = c(); for (b in d) e;"
|
||||
if !for_in_stmt.left.is_lexical_declaration() {
|
||||
Self::merge_leading_assignments_to_declaration(
|
||||
&mut for_in_stmt.right,
|
||||
false,
|
||||
result,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.options().sequences {
|
||||
@@ -1065,6 +1119,16 @@ impl<'a> PeepholeOptimizations {
|
||||
is_block_scoped_decl,
|
||||
);
|
||||
|
||||
if !for_of_stmt.left.is_lexical_declaration() {
|
||||
// "var a; for (b of a = c(), d) e;" => "var a = c(); for (b of d) e;"
|
||||
Self::merge_leading_assignments_to_declaration(
|
||||
&mut for_of_stmt.right,
|
||||
false,
|
||||
result,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
// "var a; for (a of b) c" => "for (var a of b) c"
|
||||
if let Some(Statement::VariableDeclaration(prev_var_decl)) = result.last_mut()
|
||||
&& let ForStatementLeft::AssignmentTargetIdentifier(id) = &for_of_stmt.left
|
||||
|
||||
@@ -46,7 +46,7 @@ mod join_vars {
|
||||
fn test_aggressive_redeclaration_in_for() {
|
||||
test_same("for(var x = 1; x = 2; x = 3) x = 4");
|
||||
test("for(var x = 1; y = 2; z = 3) {var a = 4}", "for(var x = 1; y = 2; z = 3) var a = 4");
|
||||
test_same("var x; for(x = 1; x = 2; z = 3) x = 4");
|
||||
test("var x; for(x = 1; x = 2; z = 3) x = 4", "for(var x = 1; x = 2; z = 3) x = 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,7 +96,7 @@ mod join_vars {
|
||||
fn test_aggressive_redeclaration_of_let_in_for() {
|
||||
test_same("for(let x = 1; x = 2; x = 3) x = 4");
|
||||
test_same("for(let x = 1; y = 2; z = 3) {let a = 4}");
|
||||
test_same("let x; for(x = 1; x = 2; z = 3) x = 4");
|
||||
test("let x; for(x = 1; x = 2; z = 3) x = 4", "let x = 1; for(; x = 2; z = 3) x = 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -225,7 +225,7 @@ fn test_inline_single_use_variable() {
|
||||
);
|
||||
test(
|
||||
"function wrapper() { var i, x = 1; for (i = x; i < 10; i++) console.log(i) }",
|
||||
"function wrapper() { var i; for (i = 1; i < 10; i++) console.log(i) }",
|
||||
"function wrapper() { for (var i = 1; i < 10; i++) console.log(i) }",
|
||||
);
|
||||
test(
|
||||
"function wrapper() { var x = i; for (var i = x; i < 10; i++) console.log(i) }",
|
||||
@@ -276,12 +276,12 @@ fn test_inline_single_use_variable() {
|
||||
// cannot be compressed to `function wrapper() { var foo; globalThis[console.log(foo)] = (() => { foo = 1 })() }`
|
||||
test(
|
||||
"function wrapper() { var foo; var bar = (() => { foo = 1 })(); globalThis[console.log(foo)] = bar }",
|
||||
"function wrapper() { var foo, bar = (foo = 1, void 0); globalThis[console.log(foo)] = bar }",
|
||||
"function wrapper() { var foo = 1, bar = void 0; globalThis[console.log(foo)] = bar }",
|
||||
);
|
||||
// cannot be compressed to `function wrapper() { let foo; return foo.bar = (() => { foo = {} })(), foo }`
|
||||
test(
|
||||
"function wrapper() { let foo; const bar = (() => { foo = {} })(); foo.bar = bar; return foo }",
|
||||
"function wrapper() { let foo, bar = (foo = {}, void 0); return foo.bar = void 0, foo }",
|
||||
"function wrapper() { let foo = {}, bar; return foo.bar = void 0, foo }",
|
||||
);
|
||||
test(
|
||||
"function wrapper() { let foo = {}; const bar = (() => { console.log() })(); foo.bar = bar; return foo }",
|
||||
|
||||
@@ -46,3 +46,65 @@ fn merge_assignments_to_declarations_other() {
|
||||
test_same("using a = 0; a = 1");
|
||||
test_same("await using a = 0; a = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_leading_assignments_from_statements() {
|
||||
test("function f() { let a; return a = 0, a }", "function f() { let a = 0; return 0 }");
|
||||
test("function f() { var a; return a = b(), a }", "function f() { return b() }");
|
||||
test(
|
||||
"function f() { var a, b; return a = 1, b = 2, c }",
|
||||
"function f() { var a = 1, b = 2; return c }",
|
||||
);
|
||||
// only a contiguous prefix may move, `a = 1` must not be hoisted above `foo()`
|
||||
test_same("function f() { var a; return foo(), a = 1, b }");
|
||||
// the last expression is never taken, its value is needed
|
||||
test_same("function f() { var a; return a = 1 }");
|
||||
|
||||
test("function f() { var a; throw a = 1, e }", "function f() { var a = 1; throw e }");
|
||||
test("function f() { var a; if (a = 1, c) foo() }", "function f() { var a = 1; c && foo() }");
|
||||
test(
|
||||
"function f() { var a; switch (a = 1, c) { case 1: foo() } }",
|
||||
"function f() { var a = 1; c === 1 && foo() }",
|
||||
);
|
||||
test(
|
||||
"function f() { var a; for (a = 1, b(); c; d()) foo() }",
|
||||
"function f() { var a = 1; for (b(); c; d()) foo() }",
|
||||
);
|
||||
test(
|
||||
"function f() { var a; for (a = 1; c; d()) foo() }",
|
||||
"function f() { for (var a = 1; c; d()) foo() }",
|
||||
);
|
||||
test_same("function f() { var a; for (x of a = 1) foo() }");
|
||||
test(
|
||||
"function f() { var a; for (x of (a = 1, b)) foo() }",
|
||||
"function f() { var a = 1; for (x of b) foo() }",
|
||||
);
|
||||
test_same("function f() { var a; for (x in a = 1) foo() }");
|
||||
test(
|
||||
"function f() { var a; for (x in (a = 1, b)) foo() }",
|
||||
"function f() { var a = 1; for (x in b) foo() }",
|
||||
);
|
||||
test("function f() { var a; var b = (a = 1, 2) }", "function f() { var a = 1, b = 2 }");
|
||||
test(
|
||||
"function f() { var a; var b = (a = 1, c) ? d : e }",
|
||||
"function f() { var a = 1, b = c ? d : e }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_leading_assignments_edge_cases() {
|
||||
// `let` may only take a literal, otherwise a TDZ error can be introduced
|
||||
test_same("function f(b) { let a; return a = c(), b }");
|
||||
// Annex B initializer in a for-in head is evaluated before the right hand side
|
||||
test_same("function f() { var a; for (var x = (a = 1) in (a = 2, obj)) foo() }");
|
||||
// loop tests are re-evaluated per iteration, so `a = 1` must stay in place
|
||||
test(
|
||||
"function f() { var a; while (a = 1, c) foo() }",
|
||||
"function f() { for (var a; a = 1, c;) foo() }",
|
||||
);
|
||||
// TDZ: block-scoped loop head shadows an immediately preceding declaration
|
||||
test_same("function f() { var a; for (let a of (a = 1, [])) foo() }");
|
||||
test_same("function f() { var a; for (let a in (a = 1, {})) foo() }");
|
||||
test_same("function f() { var a; for (const a of (a = 1, [])) foo() }");
|
||||
test_same("function f() { var a; for (const a in (a = 1, {})) foo() }");
|
||||
}
|
||||
@@ -252,11 +252,8 @@ fn remove_unused_expressions_in_sequence() {
|
||||
|
||||
#[test]
|
||||
fn remove_unused_expressions_in_for() {
|
||||
test("var i; for (i = 0, 0; i < 10; i++) foo(i);", "var i; for (i = 0; i < 10; i++) foo(i);");
|
||||
test(
|
||||
"var i; for (i = 0; i < 10; 0, i++, 0) foo(i);",
|
||||
"var i; for (i = 0; i < 10; i++) foo(i);",
|
||||
);
|
||||
test("var i; for (i = 0, 0; i < 10; i++) foo(i);", "for (var i = 0; i < 10; i++) foo(i);");
|
||||
test("var i; for (i = 0; i < 10; 0, i++, 0) foo(i);", "for (var i = 0; i < 10; i++) foo(i);");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,23 +3,23 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
-------------------------------------------------------------------------------------
|
||||
72.14 kB | 22.48 kB | 23.70 kB | 8.19 kB | 8.54 kB | 2 | react.development.js
|
||||
|
||||
173.90 kB | 55.53 kB | 59.82 kB | 18.78 kB | 19.33 kB | 2 | moment.js
|
||||
173.90 kB | 55.50 kB | 59.82 kB | 18.77 kB | 19.33 kB | 2 | moment.js
|
||||
|
||||
287.63 kB | 89.01 kB | 90.07 kB | 30.85 kB | 31.95 kB | 2 | jquery.js
|
||||
|
||||
342.15 kB | 114.82 kB | 118.14 kB | 42.72 kB | 44.37 kB | 2 | vue.js
|
||||
342.15 kB | 114.81 kB | 118.14 kB | 42.71 kB | 44.37 kB | 2 | vue.js
|
||||
|
||||
544.10 kB | 69.88 kB | 72.48 kB | 25.58 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 262.76 kB | 270.13 kB | 87.66 kB | 90.80 kB | 2 | d3.js
|
||||
555.77 kB | 262.75 kB | 270.13 kB | 87.66 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 436.04 kB | 458.89 kB | 121.78 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 634.41 kB | 646.76 kB | 158.44 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 634.39 kB | 646.76 kB | 158.43 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 707.66 kB | 724.14 kB | 160.09 kB | 181.07 kB | 2 | victory.js
|
||||
2.14 MB | 707.63 kB | 724.14 kB | 160.08 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 958.93 kB | 1.01 MB | 317.09 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 958.92 kB | 1.01 MB | 317.07 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.44 kB | 488.28 kB | 5 | antd.js
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ pdf.mjs:
|
||||
sys deallocs: 2427
|
||||
sys alloc bytes: 5334247 # 5.33 MB
|
||||
sys peak growth: 3693355 # 3.69 MB
|
||||
arena allocs: 44800
|
||||
arena allocs: 44801
|
||||
arena reallocs: 7718
|
||||
arena size: 6306768 # 6.31 MB
|
||||
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29976031 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 351753
|
||||
arena reallocs: 76289
|
||||
arena size: 48779400 # 48.78 MB
|
||||
arena allocs: 351701
|
||||
arena reallocs: 76248
|
||||
arena size: 48772472 # 48.77 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
|
||||
Reference in new issue
Block a user