Skip to content

Commit

Permalink
feat(minifier): minimize if (x) return; return 1 -> `return x ? voi…
Browse files Browse the repository at this point in the history
…d 0 : 1` (#8130)
  • Loading branch information
Boshen committed Dec 26, 2024
1 parent 04c7d38 commit f615bfa
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 25 deletions.
12 changes: 7 additions & 5 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ impl<'a> Traverse<'a> for LatePeepholeOptimizations {
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
self.x1_collapse_variable_declarations.exit_statements(stmts, ctx);
self.x2_peephole_remove_dead_code.exit_statements(stmts, ctx);
self.x3_peephole_minimize_conditions.exit_statements(stmts, ctx);
}

fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
Expand Down Expand Up @@ -223,19 +224,20 @@ impl<'a> CompressorPass<'a> for PeepholeOptimizations {
}

impl<'a> Traverse<'a> for PeepholeOptimizations {
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
self.x2_peephole_minimize_conditions.exit_statement(stmt, ctx);
self.x5_peephole_remove_dead_code.exit_statement(stmt, ctx);
}

fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.x5_peephole_remove_dead_code.exit_program(program, ctx);
}

fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
self.x2_peephole_minimize_conditions.exit_statements(stmts, ctx);
self.x5_peephole_remove_dead_code.exit_statements(stmts, ctx);
}

fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
self.x2_peephole_minimize_conditions.exit_statement(stmt, ctx);
self.x5_peephole_remove_dead_code.exit_statement(stmt, ctx);
}

fn exit_return_statement(&mut self, stmt: &mut ReturnStatement<'a>, ctx: &mut TraverseCtx<'a>) {
self.x3_peephole_substitute_alternate_syntax.exit_return_statement(stmt, ctx);
}
Expand Down
124 changes: 116 additions & 8 deletions crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};

Expand Down Expand Up @@ -26,6 +27,21 @@ impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {
}

impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
fn exit_statements(
&mut self,
stmts: &mut oxc_allocator::Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
self.try_replace_if(stmts, ctx);
while self.changed {
self.changed = false;
self.try_replace_if(stmts, ctx);
if stmts.iter().any(|stmt| matches!(stmt, Statement::EmptyStatement(_))) {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
}
}
}

fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(folded_stmt) = match stmt {
// If the condition is a literal, we'll let other optimizations try to remove useless code.
Expand Down Expand Up @@ -115,6 +131,103 @@ impl<'a> PeepholeMinimizeConditions {
_ => None,
}
}

fn try_replace_if(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
for i in 0..stmts.len() {
let Statement::IfStatement(if_stmt) = &stmts[i] else {
continue;
};
let then_branch = &if_stmt.consequent;
let else_branch = &if_stmt.alternate;
let next_node = stmts.get(i + 1);

if next_node.is_some_and(|s| matches!(s, Statement::IfStatement(_)))
&& else_branch.is_none()
&& Self::is_return_block(then_branch)
{
/* TODO */
} else if next_node.is_some_and(Self::is_return_expression)
&& else_branch.is_none()
&& Self::is_return_block(then_branch)
{
// `if (x) return; return 1` -> `return x ? void 0 : 1`
let Statement::IfStatement(if_stmt) = ctx.ast.move_statement(&mut stmts[i]) else {
unreachable!()
};
let mut if_stmt = if_stmt.unbox();
let consequent = Self::get_block_return_expression(&mut if_stmt.consequent, ctx);
let alternate = Self::take_return_argument(&mut stmts[i + 1], ctx);
let argument = ctx.ast.expression_conditional(
if_stmt.span,
if_stmt.test,
consequent,
alternate,
);
stmts[i] = ctx.ast.statement_return(if_stmt.span, Some(argument));
self.changed = true;
break;
} else if else_branch.is_some() && Self::statement_must_exit_parent(then_branch) {
let Statement::IfStatement(if_stmt) = &mut stmts[i] else {
unreachable!();
};
let else_branch = if_stmt.alternate.take().unwrap();
stmts.insert(i + 1, else_branch);
self.changed = true;
}
}
}

fn is_return_block(stmt: &Statement<'a>) -> bool {
match stmt {
Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => {
matches!(block_stmt.body[0], Statement::ReturnStatement(_))
}
Statement::ReturnStatement(_) => true,
_ => false,
}
}

fn is_return_expression(stmt: &Statement<'a>) -> bool {
matches!(stmt, Statement::ReturnStatement(return_stmt) if return_stmt.argument.is_some())
}

fn statement_must_exit_parent(stmt: &Statement<'a>) -> bool {
match stmt {
Statement::ThrowStatement(_) | Statement::ReturnStatement(_) => true,
Statement::BlockStatement(block_stmt) => {
block_stmt.body.last().is_some_and(Self::statement_must_exit_parent)
}
_ => false,
}
}

fn get_block_return_expression(
stmt: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
match stmt {
Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => {
if let Statement::ReturnStatement(_) = &mut block_stmt.body[0] {
Self::take_return_argument(stmt, ctx)
} else {
unreachable!()
}
}
Statement::ReturnStatement(_) => Self::take_return_argument(stmt, ctx),
_ => unreachable!(),
}
}

fn take_return_argument(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
let Statement::ReturnStatement(return_stmt) = ctx.ast.move_statement(stmt) else {
unreachable!()
};
let return_stmt = return_stmt.unbox();
match return_stmt.argument {
Some(e) => e,
None => ctx.ast.void_0(return_stmt.span),
}
}
}

/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java>
Expand Down Expand Up @@ -226,7 +339,6 @@ mod test {

/** Try to minimize returns */
#[test]
#[ignore]
fn test_fold_returns() {
fold("function f(){if(x)return 1;else return 2}", "function f(){return x?1:2}");
fold("function f(){if(x)return 1;return 2}", "function f(){return x?1:2}");
Expand All @@ -238,10 +350,10 @@ mod test {
"function f(){return x?(y+=1):(y+=2)}",
);

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

fold_same("function f(){for(var x in y) { return x.y; } return k}");
}
Expand Down Expand Up @@ -347,7 +459,6 @@ mod test {
}

#[test]
#[ignore]
fn test_fold_returns_integration2() {
// late = true;
// disableNormalize();
Expand All @@ -359,7 +470,6 @@ mod test {
}

#[test]
#[ignore]
fn test_dont_remove_duplicate_statements_without_normalization() {
// In the following test case, we can't remove the duplicate "alert(x);" lines since each "x"
// refers to a different variable.
Expand Down Expand Up @@ -516,13 +626,11 @@ mod test {
}

#[test]
#[ignore]
fn test_preserve_if() {
fold_same("if(!a&&!b)for(;f(););");
}

#[test]
#[ignore]
fn test_no_swap_with_dangling_else() {
fold_same("if(!x) {for(;;)foo(); for(;;)bar()} else if(y) for(;;) f()");
fold_same("if(!a&&!b) {for(;;)foo(); for(;;)bar()} else if(y) for(;;) f()");
Expand Down
24 changes: 12 additions & 12 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.89 kB | 23.70 kB | 8.64 kB | 8.54 kB | react.development.js
72.14 kB | 23.85 kB | 23.70 kB | 8.64 kB | 8.54 kB | react.development.js

173.90 kB | 61.39 kB | 59.82 kB | 19.60 kB | 19.33 kB | moment.js
173.90 kB | 60.60 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js

287.63 kB | 92.05 kB | 90.07 kB | 32.44 kB | 31.95 kB | jquery.js
287.63 kB | 91.40 kB | 90.07 kB | 32.36 kB | 31.95 kB | jquery.js

342.15 kB | 120.72 kB | 118.14 kB | 44.86 kB | 44.37 kB | vue.js
342.15 kB | 120.07 kB | 118.14 kB | 44.79 kB | 44.37 kB | vue.js

544.10 kB | 73.15 kB | 72.48 kB | 26.28 kB | 26.20 kB | lodash.js
544.10 kB | 72.91 kB | 72.48 kB | 26.27 kB | 26.20 kB | lodash.js

555.77 kB | 275.48 kB | 270.13 kB | 91.48 kB | 90.80 kB | d3.js
555.77 kB | 275.31 kB | 270.13 kB | 91.45 kB | 90.80 kB | d3.js

1.01 MB | 465.45 kB | 458.89 kB | 127.12 kB | 126.71 kB | bundle.min.js
1.01 MB | 462.98 kB | 458.89 kB | 127.06 kB | 126.71 kB | bundle.min.js

1.25 MB | 659.74 kB | 646.76 kB | 164.45 kB | 163.73 kB | three.js
1.25 MB | 658.98 kB | 646.76 kB | 164.40 kB | 163.73 kB | three.js

2.14 MB | 739.59 kB | 724.14 kB | 181.75 kB | 181.07 kB | victory.js
2.14 MB | 736.98 kB | 724.14 kB | 181.33 kB | 181.07 kB | victory.js

3.20 MB | 1.02 MB | 1.01 MB | 332.98 kB | 331.56 kB | echarts.js
3.20 MB | 1.02 MB | 1.01 MB | 332.77 kB | 331.56 kB | echarts.js

6.69 MB | 2.39 MB | 2.31 MB | 496.49 kB | 488.28 kB | antd.js
6.69 MB | 2.38 MB | 2.31 MB | 495.91 kB | 488.28 kB | antd.js

10.95 MB | 3.54 MB | 3.49 MB | 912.49 kB | 915.50 kB | typescript.js
10.95 MB | 3.52 MB | 3.49 MB | 911.59 kB | 915.50 kB | typescript.js

0 comments on commit f615bfa

Please sign in to comment.