diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index b6ffc5bb52dc1..d92458fd6724d 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -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>) { @@ -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); } diff --git a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs index 62eec660ef192..b31990be346a1 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,3 +1,4 @@ +use oxc_allocator::Vec; use oxc_ast::ast::*; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; @@ -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. @@ -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), + } + } } /// @@ -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}"); @@ -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}"); } @@ -347,7 +459,6 @@ mod test { } #[test] - #[ignore] fn test_fold_returns_integration2() { // late = true; // disableNormalize(); @@ -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. @@ -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()"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index b9f1059afe8d7..7c52fdae17486 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -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