diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 30902b972243e..19289ec02f389 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -504,26 +504,48 @@ impl<'a> PeepholeOptimizations { /// r[a - 1] = arguments[a]; /// ``` fn try_rewrite_arguments_copy_loop(for_stmt: &mut ForStatement<'a>, ctx: &mut Ctx<'a, '_>) { + #[derive(PartialEq, Eq)] + enum VerifyArrayArgResult { + WithOffset, + WithoutOffset, + Invalid, + } + /// Verify whether `arg_expr` is `e > offset ? e - offset : 0` or `e` - fn verify_array_arg(arg_expr: &Expression, name_e: &str, offset: f64) -> bool { + fn verify_array_arg( + arg_expr: &Expression, + name_e: &str, + offset: f64, + ) -> VerifyArrayArgResult { match arg_expr { - Expression::Identifier(id) => offset == 0.0 && id.name == name_e, + Expression::Identifier(id) => { + if offset == 0.0 && id.name == name_e { + VerifyArrayArgResult::WithoutOffset + } else { + VerifyArrayArgResult::Invalid + } + } Expression::ConditionalExpression(cond_expr) => { let Expression::BinaryExpression(test_expr) = &cond_expr.test else { - return false; + return VerifyArrayArgResult::Invalid; }; let Expression::BinaryExpression(cons_expr) = &cond_expr.consequent else { - return false; + return VerifyArrayArgResult::Invalid; }; - test_expr.operator == BinaryOperator::GreaterThan + if test_expr.operator == BinaryOperator::GreaterThan && test_expr.left.is_specific_id(name_e) && matches!(&test_expr.right, Expression::NumericLiteral(n) if n.value == offset) && cons_expr.operator == BinaryOperator::Subtraction && matches!(&cons_expr.left, Expression::Identifier(id) if id.name == name_e) && matches!(&cons_expr.right, Expression::NumericLiteral(n) if n.value == offset) && matches!(&cond_expr.alternate, Expression::NumericLiteral(n) if n.value == 0.0) + { + VerifyArrayArgResult::WithOffset + } else { + VerifyArrayArgResult::Invalid + } } - _ => false, + _ => VerifyArrayArgResult::Invalid, } } @@ -553,6 +575,10 @@ impl<'a> PeepholeOptimizations { assign_expr }; + // reference counts in the for-loop + let mut a_ref_count = 0; + let mut e_ref_count = 0; + let (r_id_name, a_id_name, offset) = { let AssignmentTarget::ComputedMemberExpression(lhs_member_expr) = &body_assign_expr.left @@ -602,6 +628,7 @@ impl<'a> PeepholeOptimizations { } rhs_member_expr_obj }; + a_ref_count += 2; // Parse update: `a++` { @@ -614,10 +641,11 @@ impl<'a> PeepholeOptimizations { if a_id_name != id.name { return; } + a_ref_count += 1; }; // Parse test: `a < e` or `a < arguments.length` - let e_id_name = { + let e_id_info = { let Some(Expression::BinaryExpression(b)) = &for_stmt.test else { return; }; @@ -629,7 +657,10 @@ impl<'a> PeepholeOptimizations { return; } match &b.right { - Expression::Identifier(right) => Some(&right.name), + Expression::Identifier(right) => Some(( + &right.name, + ctx.scoping().get_reference(right.reference_id()).symbol_id(), + )), Expression::StaticMemberExpression(sm) => { let Expression::Identifier(id) = &sm.object else { return; @@ -645,8 +676,12 @@ impl<'a> PeepholeOptimizations { _ => return, } }; + if e_id_info.is_some() { + e_ref_count += 1; + } + a_ref_count += 1; - let init_decl_len = if e_id_name.is_some() { 3 } else { 2 }; + let init_decl_len = if e_id_info.is_some() { 3 } else { 2 }; let Some(init) = &mut for_stmt.init else { return }; let ForStatementInit::VariableDeclaration(var_init) = init else { return }; @@ -664,7 +699,7 @@ impl<'a> PeepholeOptimizations { let mut idx = 0usize; // Check `e = arguments.length` - if let Some(e_id_name) = e_id_name { + if let Some((e_id_name, _)) = e_id_info { let de = var_init .declarations .get(idx) @@ -686,7 +721,7 @@ impl<'a> PeepholeOptimizations { } // Check `a = 0` or `a = k` - { + let a_id_symbol_id = { let de_a = var_init .declarations .get(idx + 1) @@ -698,10 +733,11 @@ impl<'a> PeepholeOptimizations { if !matches!(&de_a.init, Some(Expression::NumericLiteral(n)) if n.value == offset) { return; } - } + de_id.symbol_id() + }; // Check `r = Array(e > 1 ? e - 1 : 0)`, or `r = []` - let r_id_pat = { + let r_id_pat_with_info = { let de_r = var_init .declarations .get_mut(idx) @@ -716,11 +752,13 @@ impl<'a> PeepholeOptimizations { if call.arguments.len() != 1 { return; } - let Some(e_id_name) = e_id_name else { return }; + let Some((e_id_name, _)) = e_id_info else { return }; let Some(arg_expr) = call.arguments[0].as_expression() else { return }; - if !verify_array_arg(arg_expr, e_id_name, offset) { + let result = verify_array_arg(arg_expr, e_id_name, offset); + if result == VerifyArrayArgResult::Invalid { return; } + e_ref_count += if result == VerifyArrayArgResult::WithOffset { 2 } else { 1 }; } Some(Expression::ArrayExpression(arr)) => { if !arr.elements.is_empty() { @@ -733,15 +771,35 @@ impl<'a> PeepholeOptimizations { if de_id.name != r_id_name { return; } + let de_id_symbol_id = de_id.symbol_id(); + (&mut de_r.id, de_id_symbol_id) + }; + + // bail out if `e` or `a` is used outside the for-loop + { + if let Some((_, e_id_symbol_id)) = e_id_info + && e_id_symbol_id.is_none_or(|id| { + ctx.scoping().get_resolved_references(id).count() != e_ref_count + }) + { + return; + } + if ctx.scoping().get_resolved_references(a_id_symbol_id).count() != a_ref_count { + return; + } + } + + // Build `var r = [...arguments]` (with optional `.slice(offset)`) as the only declarator and drop test/update/body. + + let r_id_pat = { + let (r_id, de_id_symbol_id) = r_id_pat_with_info; // `var r = [...arguments]` / `var r = [...arguments].slice(n)` is not needed // if r is not used by other places because `[...arguments]` does not have a sideeffect // `r` is used once in the for-loop (assignment for each index) - (ctx.scoping().get_resolved_references(de_id.symbol_id()).count() > 1) - .then(|| de_r.id.take_in(ctx.ast)) + (ctx.scoping().get_resolved_references(de_id_symbol_id).count() > 1) + .then(|| r_id.take_in(ctx.ast)) }; - // Build `var r = [...arguments]` (with optional `.slice(offset)`) as the only declarator and drop test/update/body. - let base_arr = ctx.ast.expression_array( SPAN, ctx.ast.vec1(ctx.ast.array_expression_element_spread_element( @@ -2367,6 +2425,12 @@ mod test { "function _() { { let _; for (var e = arguments.length, r = Array(e), a = 0; a < e; a++) r[a] = arguments[a]; console.log(r) } }", "function _() { { let _; var r = [...arguments]; console.log(r) } }", ); + test_same( + "function _() { for (var e = arguments.length, r = Array(e), a = 0; a < e; a++) r[a] = arguments[a]; console.log(r, e) }", + ); + test_same( + "function _() { for (var e = arguments.length, r = Array(e), a = 0; a < e; a++) r[a] = arguments[a]; console.log(r, a) }", + ); } #[test]