From ac0d25c426bfe2e57555a43f5dc16759db7555aa Mon Sep 17 00:00:00 2001 From: Ethan Goh <7086cmd@gmail.com> Date: Tue, 26 Nov 2024 22:48:27 +0800 Subject: [PATCH] feat(minifier): minify one child if statement expression (#7230) --- crates/oxc_minifier/src/ast_passes/mod.rs | 1 + .../peephole_minimize_conditions.rs | 92 ++++++++++++++++--- .../tests/ast_passes/dead_code_elimination.rs | 4 +- tasks/minsize/minsize.snap | 24 ++--- 4 files changed, 95 insertions(+), 26 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index b902ded038341..8073a0b41d701 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -80,6 +80,7 @@ impl<'a> Traverse<'a> for EarlyPass { fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { self.x1_peephole_remove_dead_code.exit_statement(stmt, ctx); + self.x2_peephole_minimize_conditions.exit_statement(stmt, ctx); } fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { 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 1b0bb1bbea176..6e40437e9d618 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,4 +1,6 @@ use oxc_ast::ast::*; +use oxc_ecmascript::ToBoolean; +use oxc_span::SPAN; use oxc_traverse::{Traverse, TraverseCtx}; use crate::CompressorPass; @@ -35,6 +37,16 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { self.changed = true; }; } + + fn exit_statement(&mut self, node: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Statement::IfStatement(if_stmt) = node { + self.try_fold_if_block_one(if_stmt, ctx); + if let Some(new_stmt) = Self::try_fold_if_one_child(if_stmt, ctx) { + *node = new_stmt; + self.changed = true; + } + } + } } impl<'a> PeepholeMinimizeConditions { @@ -56,6 +68,63 @@ impl<'a> PeepholeMinimizeConditions { } None } + + /// Duplicate logic to DCE part. + fn try_fold_if_block_one(&mut self, if_stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) { + if let Statement::BlockStatement(block) = &mut if_stmt.consequent { + if block.body.len() == 1 { + self.changed = true; + if_stmt.consequent = ctx.ast.move_statement(block.body.first_mut().unwrap()); + } + } + if let Some(Statement::BlockStatement(block)) = &mut if_stmt.alternate { + if block.body.len() == 1 { + self.changed = true; + if_stmt.alternate = Some(ctx.ast.move_statement(block.body.first_mut().unwrap())); + } + } + } + + fn try_fold_if_one_child( + if_stmt: &mut IfStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if let Statement::ExpressionStatement(expr) = &mut if_stmt.consequent { + // The rest of things for known boolean are tasks for dce instead of here. + (if_stmt.alternate.is_none() && if_stmt.test.to_boolean().is_none()).then(|| { + // Make if (x) y; => x && y; + let (operator, mut test) = match &mut if_stmt.test { + Expression::UnaryExpression(unary) if unary.operator.is_not() => { + let arg = ctx.ast.move_expression(&mut unary.argument); + (LogicalOperator::Or, arg) + } + _ => (LogicalOperator::And, ctx.ast.move_expression(&mut if_stmt.test)), + }; + match &mut test { + Expression::BinaryExpression(bin) if bin.operator.is_equality() => { + if !bin.left.is_literal() && bin.right.is_literal() { + test = ctx.ast.expression_binary( + SPAN, + ctx.ast.move_expression(&mut bin.right), + bin.operator, + ctx.ast.move_expression(&mut bin.left), + ); + } + } + _ => {} + } + let new_expr = ctx.ast.expression_logical( + SPAN, + test, + operator, + ctx.ast.move_expression(&mut expr.expression), + ); + ctx.ast.statement_expression(SPAN, new_expr) + }) + } else { + None + } + } } /// @@ -85,7 +154,6 @@ mod test { /** Check that removing blocks with 1 child works */ #[test] - #[ignore] fn test_fold_one_child_blocks() { // late = false; fold("function f(){if(x)a();x=3}", "function f(){x&&a();x=3}"); @@ -100,7 +168,7 @@ mod test { // Try it out with functions fold("function f(){if(x){foo()}}", "function f(){x&&foo()}"); - fold("function f(){if(x){foo()}else{bar()}}", "function f(){x?foo():bar()}"); + // fold("function f(){if(x){foo()}else{bar()}}", "function f(){x?foo():bar()}"); // Try it out with properties and methods fold("function f(){if(x){a.b=1}}", "function f(){x&&(a.b=1)}"); @@ -117,22 +185,22 @@ mod test { fold_same("function f(){switch(x){case 1:break}}"); // Do while loops stay in a block if that's where they started - fold_same("function f(){if(e1){do foo();while(e2)}else foo2()}"); + // fold_same("function f(){if(e1){do foo();while(e2)}else foo2()}"); // Test an obscure case with do and while - fold("if(x){do{foo()}while(y)}else bar()", "if(x){do foo();while(y)}else bar()"); + // fold("if(x){do{foo()}while(y)}else bar()", "if(x){do foo();while(y)}else bar()"); // Play with nested IFs fold("function f(){if(x){if(y)foo()}}", "function f(){x && (y && foo())}"); - fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); - fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); - fold( - "function f(){if(x){if(y)foo();else bar()}else{baz()}}", - "function f(){x?y?foo():bar():baz()}", - ); + // fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); + // fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); + // fold( + // "function f(){if(x){if(y)foo();else bar()}else{baz()}}", + // "function f(){x?y?foo():bar():baz()}", + // ); - fold("if(e1){while(e2){if(e3){foo()}}}else{bar()}", "if(e1)while(e2)e3&&foo();else bar()"); + // fold("if(e1){while(e2){if(e3){foo()}}}else{bar()}", "if(e1)while(e2)e3&&foo();else bar()"); - fold("if(e1){with(e2){if(e3){foo()}}}else{bar()}", "if(e1)with(e2)e3&&foo();else bar()"); + // fold("if(e1){with(e2){if(e3){foo()}}}else{bar()}", "if(e1)with(e2)e3&&foo();else bar()"); fold("if(a||b){if(c||d){var x;}}", "if(a||b)if(c||d)var x"); fold("if(x){ if(y){var x;}else{var z;} }", "if(x)if(y)var x;else var z"); diff --git a/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs index f366e3fc60ae0..5bc3cf4b60d8b 100644 --- a/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs +++ b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs @@ -49,11 +49,11 @@ fn dce_if_statement() { test("if (!false) { foo }", "foo"); test("if (!true) { foo } else { bar }", "bar"); - test("if (!false && xxx) { foo }", "if (xxx) foo"); + test("if (!false && xxx) { foo }", "xxx && foo"); test("if (!true && yyy) { foo } else { bar }", "bar"); test("if (true || xxx) { foo }", "foo"); - test("if (false || xxx) { foo }", "if (xxx) foo"); + test("if (false || xxx) { foo }", "xxx && foo"); test("if ('production' == 'production') { foo } else { bar }", "foo"); test("if ('development' == 'production') { foo } else { bar }", "bar"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 083cd7b84cfdf..8b91e11f4a5f6 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,26 +1,26 @@ Original | Minified | esbuild | Gzip | esbuild -72.14 kB | 24.12 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js +72.14 kB | 24.06 kB | 23.70 kB | 8.68 kB | 8.54 kB | react.development.js -173.90 kB | 61.67 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js +173.90 kB | 61.54 kB | 59.82 kB | 19.64 kB | 19.33 kB | moment.js -287.63 kB | 92.70 kB | 90.07 kB | 32.26 kB | 31.95 kB | jquery.js +287.63 kB | 92.33 kB | 90.07 kB | 32.45 kB | 31.95 kB | jquery.js -342.15 kB | 121.90 kB | 118.14 kB | 44.59 kB | 44.37 kB | vue.js +342.15 kB | 121.31 kB | 118.14 kB | 44.85 kB | 44.37 kB | vue.js -544.10 kB | 73.48 kB | 72.48 kB | 26.12 kB | 26.20 kB | lodash.js +544.10 kB | 73.40 kB | 72.48 kB | 26.20 kB | 26.20 kB | lodash.js -555.77 kB | 276.48 kB | 270.13 kB | 91.15 kB | 90.80 kB | d3.js +555.77 kB | 276.27 kB | 270.13 kB | 91.49 kB | 90.80 kB | d3.js -1.01 MB | 467.59 kB | 458.89 kB | 126.73 kB | 126.71 kB | bundle.min.js +1.01 MB | 466.71 kB | 458.89 kB | 127.15 kB | 126.71 kB | bundle.min.js -1.25 MB | 662.83 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js +1.25 MB | 662.18 kB | 646.76 kB | 164.73 kB | 163.73 kB | three.js -2.14 MB | 741.55 kB | 724.14 kB | 181.45 kB | 181.07 kB | victory.js +2.14 MB | 741.23 kB | 724.14 kB | 181.84 kB | 181.07 kB | victory.js -3.20 MB | 1.02 MB | 1.01 MB | 332.02 kB | 331.56 kB | echarts.js +3.20 MB | 1.02 MB | 1.01 MB | 333.40 kB | 331.56 kB | echarts.js -6.69 MB | 2.39 MB | 2.31 MB | 496.11 kB | 488.28 kB | antd.js +6.69 MB | 2.39 MB | 2.31 MB | 497.21 kB | 488.28 kB | antd.js -10.95 MB | 3.56 MB | 3.49 MB | 911.20 kB | 915.50 kB | typescript.js +10.95 MB | 3.55 MB | 3.49 MB | 913.93 kB | 915.50 kB | typescript.js