Skip to content

Commit

Permalink
feat(minifier): implement optimizeImplicitJump
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Feb 9, 2025
1 parent ec601f2 commit 6de5df4
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 124 deletions.
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
18 changes: 6 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,10 @@ 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>,
) {
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
229 changes: 172 additions & 57 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::ops::ControlFlow;

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

use crate::{ctx::Ctx, keep_var::KeepVar};
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,151 @@ impl<'a> PeepholeOptimizations {
false
}

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().skip(1).next().is_some_and(|s| s.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);
// TODO: span
// bodyLoc := s.Yes.Loc
// if len(body) > 0 {
// bodyLoc = body[0].Loc
// }
let test = ctx.ast.move_expression(&mut if_stmt.test);
let mut test = Self::minimize_not(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);
Statement::BlockStatement(
ctx.ast
.alloc(ctx.ast.block_statement_with_scope_id(SPAN, body, scope_id)),
)
};
let mut if_stmt = ctx.ast.if_statement(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(){x++}) }", "");
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 @@ -1123,7 +1123,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

0 comments on commit 6de5df4

Please sign in to comment.