Skip to content

Commit

Permalink
feat(minifier): minimize if(foo) bar -> foo && bar (#8121)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Dec 26, 2024
1 parent 72d9967 commit f8200a8
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 74 deletions.
77 changes: 41 additions & 36 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,45 +66,49 @@ impl<'a> Traverse<'a> for CollapsePass {
// See `latePeepholeOptimizations`
pub struct LatePeepholeOptimizations {
x0_statement_fusion: StatementFusion,
x1_peephole_remove_dead_code: PeepholeRemoveDeadCode,
x1_collapse_variable_declarations: CollapseVariableDeclarations,
x2_peephole_remove_dead_code: PeepholeRemoveDeadCode,
// TODO: MinimizeExitPoints
x2_peephole_minimize_conditions: PeepholeMinimizeConditions,
x3_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax,
x4_peephole_replace_known_methods: PeepholeReplaceKnownMethods,
x5_peephole_fold_constants: PeepholeFoldConstants,
x3_peephole_minimize_conditions: PeepholeMinimizeConditions,
x4_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax,
x5_peephole_replace_known_methods: PeepholeReplaceKnownMethods,
x6_peephole_fold_constants: PeepholeFoldConstants,
}

impl LatePeepholeOptimizations {
pub fn new() -> Self {
let in_fixed_loop = true;
Self {
x0_statement_fusion: StatementFusion::new(),
x1_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(),
x2_peephole_minimize_conditions: PeepholeMinimizeConditions::new(in_fixed_loop),
x3_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax::new(
x1_collapse_variable_declarations: CollapseVariableDeclarations::new(),
x2_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(),
x3_peephole_minimize_conditions: PeepholeMinimizeConditions::new(in_fixed_loop),
x4_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax::new(
in_fixed_loop,
),
x4_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(),
x5_peephole_fold_constants: PeepholeFoldConstants::new(),
x5_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(),
x6_peephole_fold_constants: PeepholeFoldConstants::new(),
}
}

fn reset_changed(&mut self) {
self.x0_statement_fusion.changed = false;
self.x1_peephole_remove_dead_code.changed = false;
self.x2_peephole_minimize_conditions.changed = false;
self.x3_peephole_substitute_alternate_syntax.changed = false;
self.x4_peephole_replace_known_methods.changed = false;
self.x5_peephole_fold_constants.changed = false;
self.x1_collapse_variable_declarations.changed = false;
self.x2_peephole_remove_dead_code.changed = false;
self.x3_peephole_minimize_conditions.changed = false;
self.x4_peephole_substitute_alternate_syntax.changed = false;
self.x5_peephole_replace_known_methods.changed = false;
self.x6_peephole_fold_constants.changed = false;
}

fn changed(&self) -> bool {
self.x0_statement_fusion.changed
|| self.x1_peephole_remove_dead_code.changed
|| self.x2_peephole_minimize_conditions.changed
|| self.x3_peephole_substitute_alternate_syntax.changed
|| self.x4_peephole_replace_known_methods.changed
|| self.x5_peephole_fold_constants.changed
|| self.x1_collapse_variable_declarations.changed
|| self.x2_peephole_remove_dead_code.changed
|| self.x3_peephole_minimize_conditions.changed
|| self.x4_peephole_substitute_alternate_syntax.changed
|| self.x5_peephole_replace_known_methods.changed
|| self.x6_peephole_fold_constants.changed
}

pub fn run_in_loop<'a>(
Expand Down Expand Up @@ -135,54 +139,55 @@ impl<'a> CompressorPass<'a> for LatePeepholeOptimizations {
}

impl<'a> Traverse<'a> for LatePeepholeOptimizations {
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>) {
self.x0_statement_fusion.exit_program(program, ctx);
self.x1_peephole_remove_dead_code.exit_program(program, ctx);
self.x2_peephole_remove_dead_code.exit_program(program, ctx);
}

fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
self.x0_statement_fusion.exit_function_body(body, ctx);
}

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

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

fn exit_block_statement(&mut self, block: &mut BlockStatement<'a>, ctx: &mut TraverseCtx<'a>) {
self.x0_statement_fusion.exit_block_statement(block, 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);
self.x4_peephole_substitute_alternate_syntax.exit_return_statement(stmt, ctx);
}

fn exit_variable_declaration(
&mut self,
decl: &mut VariableDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.x3_peephole_substitute_alternate_syntax.exit_variable_declaration(decl, ctx);
self.x4_peephole_substitute_alternate_syntax.exit_variable_declaration(decl, ctx);
}

fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
self.x1_peephole_remove_dead_code.exit_expression(expr, ctx);
self.x2_peephole_minimize_conditions.exit_expression(expr, ctx);
self.x3_peephole_substitute_alternate_syntax.exit_expression(expr, ctx);
self.x4_peephole_replace_known_methods.exit_expression(expr, ctx);
self.x5_peephole_fold_constants.exit_expression(expr, ctx);
self.x2_peephole_remove_dead_code.exit_expression(expr, ctx);
self.x3_peephole_minimize_conditions.exit_expression(expr, ctx);
self.x4_peephole_substitute_alternate_syntax.exit_expression(expr, ctx);
self.x5_peephole_replace_known_methods.exit_expression(expr, ctx);
self.x6_peephole_fold_constants.exit_expression(expr, ctx);
}

fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) {
self.x3_peephole_substitute_alternate_syntax.enter_call_expression(expr, ctx);
self.x4_peephole_substitute_alternate_syntax.enter_call_expression(expr, ctx);
}

fn exit_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) {
self.x3_peephole_substitute_alternate_syntax.exit_call_expression(expr, ctx);
self.x4_peephole_substitute_alternate_syntax.exit_call_expression(expr, ctx);
}
}

Expand Down
96 changes: 70 additions & 26 deletions crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {
}

impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
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.
Statement::IfStatement(s) if !s.test.is_literal() => Self::try_minimize_if(stmt, ctx),
_ => None,
} {
*stmt = folded_stmt;
self.changed = true;
};
}

fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(folded_expr) = match expr {
Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx),
Expand Down Expand Up @@ -56,6 +67,40 @@ impl<'a> PeepholeMinimizeConditions {
binary_expr.operator = new_op;
Some(ctx.ast.move_expression(&mut expr.argument))
}

fn try_minimize_if(
stmt: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
if if_stmt.alternate.is_none() {
// `if(x)foo();` -> `x&&foo();`
if let Some(right) = Self::is_foldable_express_block(&mut if_stmt.consequent, ctx) {
let left = ctx.ast.move_expression(&mut if_stmt.test);
let logical_expr =
ctx.ast.expression_logical(if_stmt.span, left, LogicalOperator::And, right);
return Some(ctx.ast.statement_expression(if_stmt.span, logical_expr));
}
}
None
}

fn is_foldable_express_block(
stmt: &mut Statement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
match stmt {
Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => {
if let Statement::ExpressionStatement(s) = &mut block_stmt.body[0] {
Some(ctx.ast.move_expression(&mut s.expression))
} else {
None
}
}
Statement::ExpressionStatement(s) => Some(ctx.ast.move_expression(&mut s.expression)),
_ => None,
}
}
}

/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java>
Expand Down Expand Up @@ -85,7 +130,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}");
Expand All @@ -94,13 +138,13 @@ mod test {
fold("function f(){if(x){a()}x=3}", "function f(){x&&a();x=3}");
fold("function f(){if(x){a?.()}x=3}", "function f(){x&&a?.();x=3}");

fold("function f(){if(x){return 3}}", "function f(){if(x)return 3}");
// fold("function f(){if(x){return 3}}", "function f(){if(x)return 3}");
fold("function f(){if(x){a()}}", "function f(){x&&a()}");
fold("function f(){if(x){throw 1}}", "function f(){if(x)throw 1;}");
// fold("function f(){if(x){throw 1}}", "function f(){if(x)throw 1;}");

// 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)}");
Expand All @@ -122,35 +166,35 @@ mod test {
// 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()}}", "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("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(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");
// 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");

// NOTE - technically we can remove the blocks since both the parent
// and child have elses. But we don't since it causes ambiguities in
// some cases where not all descendent ifs having elses
fold(
"if(x){ if(y){var x;}else{var z;} }else{var w}",
"if(x)if(y)var x;else var z;else var w",
);
fold("if (x) {var x;}else { if (y) { var y;} }", "if(x)var x;else if(y)var y");
// fold(
// "if(x){ if(y){var x;}else{var z;} }else{var w}",
// "if(x)if(y)var x;else var z;else var w",
// );
// fold("if (x) {var x;}else { if (y) { var y;} }", "if(x)var x;else if(y)var y");

// Here's some of the ambiguous cases
fold(
"if(a){if(b){f1();f2();}else if(c){f3();}}else {if(d){f4();}}",
"if(a)if(b){f1();f2()}else c&&f3();else d&&f4()",
);
// fold(
// "if(a){if(b){f1();f2();}else if(c){f3();}}else {if(d){f4();}}",
// "if(a)if(b){f1();f2()}else c&&f3();else d&&f4()",
// );

fold_same("function f(){foo()}");
fold_same("switch(x){case y: foo()}");
Expand All @@ -160,10 +204,10 @@ mod test {
// Lexical declaration cannot appear in a single-statement context.
fold_same("if (foo) { const bar = 1 } else { const baz = 1 }");
fold_same("if (foo) { let bar = 1 } else { let baz = 1 }");
fold(
"if (foo) { var bar = 1 } else { var baz = 1 }",
"if (foo) var bar = 1; else var baz = 1;",
);
// fold(
// "if (foo) { var bar = 1 } else { var baz = 1 }",
// "if (foo) var bar = 1; else var baz = 1;",
// );
}

/** Try to minimize returns */
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.94 kB | 23.70 kB | 8.59 kB | 8.54 kB | react.development.js
72.14 kB | 23.89 kB | 23.70 kB | 8.64 kB | 8.54 kB | react.development.js

173.90 kB | 61.52 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js
173.90 kB | 61.42 kB | 59.82 kB | 19.60 kB | 19.33 kB | moment.js

287.63 kB | 92.42 kB | 90.07 kB | 32.32 kB | 31.95 kB | jquery.js
287.63 kB | 92.09 kB | 90.07 kB | 32.46 kB | 31.95 kB | jquery.js

342.15 kB | 121.31 kB | 118.14 kB | 44.69 kB | 44.37 kB | vue.js
342.15 kB | 120.77 kB | 118.14 kB | 44.86 kB | 44.37 kB | vue.js

544.10 kB | 73.22 kB | 72.48 kB | 26.22 kB | 26.20 kB | lodash.js
544.10 kB | 73.17 kB | 72.48 kB | 26.28 kB | 26.20 kB | lodash.js

555.77 kB | 275.67 kB | 270.13 kB | 91.19 kB | 90.80 kB | d3.js
555.77 kB | 275.52 kB | 270.13 kB | 91.48 kB | 90.80 kB | d3.js

1.01 MB | 466.33 kB | 458.89 kB | 126.76 kB | 126.71 kB | bundle.min.js
1.01 MB | 465.56 kB | 458.89 kB | 127.14 kB | 126.71 kB | bundle.min.js

1.25 MB | 660.39 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js
1.25 MB | 659.76 kB | 646.76 kB | 164.45 kB | 163.73 kB | three.js

2.14 MB | 739.97 kB | 724.14 kB | 181.42 kB | 181.07 kB | victory.js
2.14 MB | 739.69 kB | 724.14 kB | 181.77 kB | 181.07 kB | victory.js

3.20 MB | 1.02 MB | 1.01 MB | 332.30 kB | 331.56 kB | echarts.js
3.20 MB | 1.02 MB | 1.01 MB | 333.18 kB | 331.56 kB | echarts.js

6.69 MB | 2.39 MB | 2.31 MB | 495.67 kB | 488.28 kB | antd.js
6.69 MB | 2.39 MB | 2.31 MB | 496.55 kB | 488.28 kB | antd.js

10.95 MB | 3.54 MB | 3.49 MB | 910.07 kB | 915.50 kB | typescript.js
10.95 MB | 3.54 MB | 3.49 MB | 912.55 kB | 915.50 kB | typescript.js

0 comments on commit f8200a8

Please sign in to comment.