Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minifier): minimize if (x) return; return 1 -> return x ? void 0 : 1 #8130

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
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

Loading