Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/oxc_minifier/src/peephole/minimize_conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,16 +588,16 @@ mod test {
fn test_fold_returns() {
test("function f(){if(x)return 1;else return 2}", "function f(){return x?1:2}");
test("function f(){if(x)return 1;return 2}", "function f(){return x?1:2}");
test("function f(){if(x)return;return 2}", "function f(){return x?void 0:2}");
test("function f(){if(x)return;return 2}", "function f(){if (!x) return 2;}");
test("function f(){if(x)return 1+x;else return 2-x}", "function f(){return x?1+x:2-x}");
test("function f(){if(x)return 1+x;return 2-x}", "function f(){return x?1+x:2-x}");
test(
"function f(){if(x)return y += 1;else return y += 2}",
"function f(){return x?(y+=1):(y+=2)}",
);

test("function f(){if(x)return;else return 2-x}", "function f(){return x?void 0:2-x}");
test("function f(){if(x)return;return 2-x}", "function f(){return x?void 0:2-x}");
test("function f(){if(x)return;else return 2-x}", "function f(){if (!x) return 2 - x;}");
test("function f(){if(x)return;return 2-x}", "function f(){if (!x) return 2 - x;}");
test("function f(){if(x)return x;else return}", "function f(){if(x)return x;}");
test("function f(){if(x)return x;return}", "function f(){if(x)return x}");

Expand Down
19 changes: 7 additions & 12 deletions crates/oxc_minifier/src/peephole/minimize_if_statement.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use oxc_ast::ast::*;

use oxc_semantic::ScopeId;
use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::TraverseCtx;

use crate::ctx::Ctx;

Expand All @@ -13,10 +12,9 @@ impl<'a> PeepholeOptimizations {
pub fn try_minimize_if(
&mut self,
if_stmt: &mut IfStatement<'a>,
traverse_ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Statement<'a>> {
self.wrap_to_avoid_ambiguous_else(if_stmt, traverse_ctx);
let ctx = Ctx(traverse_ctx);
self.wrap_to_avoid_ambiguous_else(if_stmt, ctx);
if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
if if_stmt.alternate.is_none() {
let (op, e) = match &mut if_stmt.test {
Expand Down Expand Up @@ -94,7 +92,7 @@ impl<'a> PeepholeOptimizations {
// "if (!a) return b; else return c;" => "if (a) return c; else return b;"
if_stmt.test = ctx.ast.move_expression(&mut unary_expr.argument);
std::mem::swap(&mut if_stmt.consequent, alternate);
self.wrap_to_avoid_ambiguous_else(if_stmt, traverse_ctx);
self.wrap_to_avoid_ambiguous_else(if_stmt, ctx);
self.mark_current_function_as_changed();
}
}
Expand Down Expand Up @@ -124,14 +122,11 @@ impl<'a> PeepholeOptimizations {

/// Wrap to avoid ambiguous else.
/// `if (foo) if (bar) baz else quaz` -> `if (foo) { if (bar) baz else quaz }`
fn wrap_to_avoid_ambiguous_else(
&mut self,
if_stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a>,
) {
#[expect(clippy::cast_possible_truncation)]
fn wrap_to_avoid_ambiguous_else(&mut self, if_stmt: &mut IfStatement<'a>, ctx: Ctx<'a, '_>) {
if let Statement::IfStatement(if2) = &mut if_stmt.consequent {
if if2.consequent.is_jump_statement() && if2.alternate.is_some() {
let scope_id = ctx.create_child_scope_of_current(ScopeFlags::empty());
let scope_id = ScopeId::new(ctx.scoping.scopes().len() as u32);
if_stmt.consequent = Statement::BlockStatement(ctx.ast.alloc(
ctx.ast.block_statement_with_scope_id(
if_stmt.consequent.span(),
Expand Down
224 changes: 168 additions & 56 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::ops::ControlFlow;

use oxc_allocator::{Box, Vec};
use oxc_ast::{ast::*, Visit};
use oxc_ecmascript::side_effects::MayHaveSideEffects;
use oxc_semantic::ScopeId;
use oxc_span::{ContentEq, GetSpan};
use oxc_traverse::Ancestor;

Expand Down Expand Up @@ -30,15 +33,29 @@ impl<'a> PeepholeOptimizations {
let mut result: Vec<'a, Statement<'a>> = ctx.ast.vec_with_capacity(stmts.len());
let mut is_control_flow_dead = false;
let mut keep_var = KeepVar::new(ctx.ast);
for stmt in ctx.ast.vec_from_iter(stmts.drain(..)) {
let mut new_stmts = ctx.ast.vec_from_iter(stmts.drain(..));
for i in 0..new_stmts.len() {
let stmt = ctx.ast.move_statement(&mut new_stmts[i]);
if is_control_flow_dead
&& !stmt.is_module_declaration()
&& !matches!(stmt.as_declaration(), Some(Declaration::FunctionDeclaration(_)))
{
keep_var.visit_statement(&stmt);
continue;
}
self.minimize_statement(stmt, &mut result, &mut is_control_flow_dead, ctx);
if self
.minimize_statement(
stmt,
i,
&mut new_stmts,
&mut result,
&mut is_control_flow_dead,
ctx,
)
.is_break()
{
break;
};
}
if let Some(stmt) = keep_var.get_variable_declaration_statement() {
result.push(stmt);
Expand Down Expand Up @@ -176,10 +193,12 @@ impl<'a> PeepholeOptimizations {
fn minimize_statement(
&mut self,
stmt: Statement<'a>,
i: usize,
stmts: &mut Vec<'a, Statement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
is_control_flow_dead: &mut bool,
ctx: Ctx<'a, '_>,
) {
) -> ControlFlow<()> {
match stmt {
Statement::EmptyStatement(_) => (),
Statement::BreakStatement(s) => {
Expand Down Expand Up @@ -220,60 +239,10 @@ impl<'a> PeepholeOptimizations {
}
result.push(Statement::SwitchStatement(switch_stmt));
}
Statement::IfStatement(mut if_stmt) => {
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
let a = &mut prev_expr_stmt.expression;
let b = &mut if_stmt.test;
if_stmt.test = Self::join_sequence(a, b, ctx);
result.pop();
self.mark_current_function_as_changed();
Statement::IfStatement(if_stmt) => {
if self.handle_if_statement(i, stmts, result, if_stmt, ctx).is_break() {
return ControlFlow::Break(());
}

if if_stmt.consequent.is_jump_statement() {
// Absorb a previous if statement
if let Some(Statement::IfStatement(prev_if_stmt)) = result.last_mut() {
if prev_if_stmt.alternate.is_none()
&& Self::jump_stmts_look_the_same(
&prev_if_stmt.consequent,
&if_stmt.consequent,
)
{
// "if (a) break c; if (b) break c;" => "if (a || b) break c;"
// "if (a) continue c; if (b) continue c;" => "if (a || b) continue c;"
// "if (a) return c; if (b) return c;" => "if (a || b) return c;"
// "if (a) throw c; if (b) throw c;" => "if (a || b) throw c;"
if_stmt.test = Self::join_with_left_associative_op(
if_stmt.test.span(),
LogicalOperator::Or,
ctx.ast.move_expression(&mut prev_if_stmt.test),
ctx.ast.move_expression(&mut if_stmt.test),
ctx,
);
result.pop();
self.mark_current_function_as_changed();
}
}

if if_stmt.alternate.is_some() {
// "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;"
result.push(Statement::IfStatement(if_stmt));
loop {
if let Some(Statement::IfStatement(if_stmt)) = result.last_mut() {
if if_stmt.consequent.is_jump_statement() {
if let Some(stmt) = if_stmt.alternate.take() {
result.push(stmt);
self.mark_current_function_as_changed();
continue;
}
}
}
break;
}
return;
}
}

result.push(Statement::IfStatement(if_stmt));
}
Statement::ReturnStatement(mut ret_stmt) => {
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
Expand Down Expand Up @@ -443,6 +412,7 @@ impl<'a> PeepholeOptimizations {
Statement::BlockStatement(block_stmt) => self.handle_block(result, block_stmt),
stmt => result.push(stmt),
}
ControlFlow::Continue(())
}

fn join_sequence(
Expand Down Expand Up @@ -475,6 +445,148 @@ impl<'a> PeepholeOptimizations {
false
}

#[expect(clippy::cast_possible_truncation)]
fn handle_if_statement(
&mut self,
i: usize,
stmts: &mut Vec<'a, Statement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
mut if_stmt: Box<'a, IfStatement<'a>>,
ctx: Ctx<'a, '_>,
) -> ControlFlow<()> {
// Absorb a previous expression statement
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
let a = &mut prev_expr_stmt.expression;
let b = &mut if_stmt.test;
if_stmt.test = Self::join_sequence(a, b, ctx);
result.pop();
self.mark_current_function_as_changed();
}

if if_stmt.consequent.is_jump_statement() {
// Absorb a previous if statement
if let Some(Statement::IfStatement(prev_if_stmt)) = result.last_mut() {
if prev_if_stmt.alternate.is_none()
&& Self::jump_stmts_look_the_same(&prev_if_stmt.consequent, &if_stmt.consequent)
{
// "if (a) break c; if (b) break c;" => "if (a || b) break c;"
// "if (a) continue c; if (b) continue c;" => "if (a || b) continue c;"
// "if (a) return c; if (b) return c;" => "if (a || b) return c;"
// "if (a) throw c; if (b) throw c;" => "if (a || b) throw c;"
if_stmt.test = Self::join_with_left_associative_op(
if_stmt.test.span(),
LogicalOperator::Or,
ctx.ast.move_expression(&mut prev_if_stmt.test),
ctx.ast.move_expression(&mut if_stmt.test),
ctx,
);
result.pop();
self.mark_current_function_as_changed();
}
}

let mut optimize_implicit_jump = false;
// "while (x) { if (y) continue; z(); }" => "while (x) { if (!y) z(); }"
// "while (x) { if (y) continue; else z(); w(); }" => "while (x) { if (!y) { z(); w(); } }" => "for (; x;) !y && (z(), w());"
if ctx.ancestors().nth(1).is_some_and(Ancestor::is_for_statement) {
if let Statement::ContinueStatement(continue_stmt) = &if_stmt.consequent {
if continue_stmt.label.is_none() {
optimize_implicit_jump = true;
}
}
}

// "let x = () => { if (y) return; z(); };" => "let x = () => { if (!y) z(); };"
// "let x = () => { if (y) return; else z(); w(); };" => "let x = () => { if (!y) { z(); w(); } };" => "let x = () => { !y && (z(), w()); };"
if ctx.parent().is_function_body() {
if let Statement::ReturnStatement(return_stmt) = &if_stmt.consequent {
if return_stmt.argument.is_none() {
optimize_implicit_jump = true;
}
}
}
if optimize_implicit_jump {
// Don't do this transformation if the branch condition could
// potentially access symbols declared later on on this scope below.
// If so, inverting the branch condition and nesting statements after
// this in a block would break that access which is a behavior change.
//
// // This transformation is incorrect
// if (a()) return; function a() {}
// if (!a()) { function a() {} }
//
// // This transformation is incorrect
// if (a(() => b)) return; let b;
// if (a(() => b)) { let b; }
//
let mut can_move_branch_condition_outside_scope = true;
if let Some(alternate) = &if_stmt.alternate {
if Self::statement_cares_about_scope(alternate) {
can_move_branch_condition_outside_scope = false;
}
}
if let Some(stmts) = stmts.get(i + 1..) {
for stmt in stmts {
if Self::statement_cares_about_scope(stmt) {
can_move_branch_condition_outside_scope = false;
break;
}
}
}

if can_move_branch_condition_outside_scope {
let mut body = ctx.ast.vec();
if let Some(alternate) = if_stmt.alternate.take() {
body.push(alternate);
}
body.extend(stmts.drain(i + 1..));

self.minimize_statements(&mut body, ctx);
let span =
if body.is_empty() { if_stmt.consequent.span() } else { body[0].span() };
let test = ctx.ast.move_expression(&mut if_stmt.test);
let mut test = Self::minimize_not(test.span(), test, ctx);
Self::try_fold_expr_in_boolean_context(&mut test, ctx);
let consequent = if body.len() == 1 {
body.remove(0)
} else {
let scope_id = ScopeId::new(ctx.scopes().len() as u32);
let block_stmt =
ctx.ast.block_statement_with_scope_id(span, body, scope_id);
Statement::BlockStatement(ctx.ast.alloc(block_stmt))
};
let mut if_stmt = ctx.ast.if_statement(test.span(), test, consequent, None);
let if_stmt = self
.try_minimize_if(&mut if_stmt, ctx)
.unwrap_or_else(|| Statement::IfStatement(ctx.ast.alloc(if_stmt)));
result.push(if_stmt);
return ControlFlow::Break(());
}
}

if if_stmt.alternate.is_some() {
// "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;"
result.push(Statement::IfStatement(if_stmt));
loop {
if let Some(Statement::IfStatement(if_stmt)) = result.last_mut() {
if if_stmt.consequent.is_jump_statement() {
if let Some(stmt) = if_stmt.alternate.take() {
result.push(stmt);
self.mark_current_function_as_changed();
continue;
}
}
}
break;
}
return ControlFlow::Continue(());
}
}

result.push(Statement::IfStatement(if_stmt));
ControlFlow::Continue(())
}

/// `appendIfOrLabelBodyPreservingScope`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_parser.go#L9852>
fn handle_block(
&mut self,
Expand Down
11 changes: 6 additions & 5 deletions crates/oxc_minifier/src/peephole/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,20 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
self.minimize_statements(stmts, ctx);
}

fn exit_statement(&mut self, stmt: &mut Statement<'a>, traverse_ctx: &mut TraverseCtx<'a>) {
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
if !self.is_prev_function_changed() {
return;
}
Self::try_fold_stmt_in_boolean_context(stmt, Ctx(traverse_ctx));
self.remove_dead_code_exit_statement(stmt, Ctx(traverse_ctx));
let ctx = Ctx(ctx);
Self::try_fold_stmt_in_boolean_context(stmt, ctx);
self.remove_dead_code_exit_statement(stmt, ctx);
if let Statement::IfStatement(if_stmt) = stmt {
if let Some(folded_stmt) = self.try_minimize_if(if_stmt, traverse_ctx) {
if let Some(folded_stmt) = self.try_minimize_if(if_stmt, ctx) {
*stmt = folded_stmt;
self.mark_current_function_as_changed();
}
}
self.substitute_exit_statement(stmt, Ctx(traverse_ctx));
self.substitute_exit_statement(stmt, ctx);
}

fn exit_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_minifier/src/peephole/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ mod test {
test("{ (function foo(){x++; foo()}) }", "");
test("function f(){return;}", "function f(){}");
test("function f(){return 3;}", "function f(){return 3}");
test("function f(){if(x)return; x=3; return; }", "function f(){if(x)return; x=3; }");
test("function f(){if(x)return; x=3; return; }", "function f(){ x ||= 3; }");
test("{x=3;;;y=2;;;}", "x=3, y=2");

// Cases to test for empty block.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ mod test {
test("function f(){return void 0;}", "function f(){}");
test("function f(){return void foo();}", "function f(){return void foo()}");
test("function f(){return undefined;}", "function f(){}");
test("function f(){if(a()){return undefined;}}", "function f(){if(a())return}");
test("function f(){if(a()){return undefined;}}", "function f(){!a()}");
test_same("function a(undefined) { return undefined; }");
test_same("function f(){return foo()}");

Expand Down
Loading
Loading