Skip to content
Closed
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
feat(minifier): compress single consequence if statement.
  • Loading branch information
7086cmd authored and Boshen committed Oct 3, 2024
commit d507f18ecab8cacd5794d9e1c247130dd88d2fce
154 changes: 114 additions & 40 deletions crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use oxc_ast::ast::*;
use oxc_span::SPAN;
use oxc_traverse::{Traverse, TraverseCtx};

use crate::CompressorPass;
use crate::{CompressOptions, CompressorPass};

/// Minimize Conditions
///
Expand All @@ -11,6 +12,7 @@ use crate::CompressorPass;
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PeepholeMinimizeConditions.java>
pub struct PeepholeMinimizeConditions {
options: CompressOptions,
changed: bool,
}

Expand All @@ -21,7 +23,9 @@ impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {

fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.changed = false;
oxc_traverse::walk_program(self, program, ctx);
if self.options.conditions {
oxc_traverse::walk_program(self, program, ctx);
}
}
}

Expand All @@ -35,11 +39,31 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
self.changed = true;
};
}

fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
match stmt {
Statement::BlockStatement(block) => {
if let Some(folded_stmt) = self.try_fold_block_statement(block, ctx) {
*stmt = folded_stmt;
self.changed = true;
}
}
Statement::IfStatement(if_stmt) => {
if let Some(folded_stmt) =
self.try_fold_single_consequent_without_alternate(if_stmt, ctx)
{
*stmt = folded_stmt;
self.changed = true;
}
}
_ => {}
}
}
}

impl<'a> PeepholeMinimizeConditions {
pub fn new() -> Self {
Self { changed: false }
pub fn new(options: CompressOptions) -> Self {
Self { changed: false, options }
}

/// Try to minimize NOT nodes such as `!(x==y)`.
Expand All @@ -57,18 +81,69 @@ impl<'a> PeepholeMinimizeConditions {
}
None
}

fn try_fold_block_statement(
&self,
stmt: &mut BlockStatement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
// It must have only one statement, so using unwrap is safe.
(stmt.body.len() == 1)
.then(|| {
stmt.body.get_mut(0).and_then(|stmt| {
matches!(stmt, Statement::ExpressionStatement(_))
.then(|| ctx.ast.move_statement(stmt))
})
})
.unwrap_or(None)
}

fn try_fold_single_consequent_without_alternate(
&mut self,
stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
if stmt.alternate.is_some() {
return None;
};
let consequence = match &mut stmt.consequent {
Statement::BlockStatement(block) => {
if block.body.len() != 1 {
return None;
}
block.body.get_mut(0)?
}
_ => &mut stmt.consequent,
};
if matches!(consequence, Statement::ExpressionStatement(_)) && !stmt.test.is_literal() {
let condition = ctx.ast.move_expression(&mut stmt.test);
let consequent = ctx.ast.move_statement(consequence);
let expression = match consequent {
Statement::ExpressionStatement(mut expr) => {
ctx.ast.move_expression(&mut expr.expression)
}
_ => unreachable!(),
};

let new_expr =
ctx.ast.expression_logical(SPAN, condition, LogicalOperator::And, expression);
Some(ctx.ast.statement_expression(SPAN, new_expr))
} else {
None
}
}
}

/// <https://github.com/google/closure-compiler/blob/master/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java>
#[cfg(test)]
mod test {
use oxc_allocator::Allocator;

use crate::tester;
use crate::{tester, CompressOptions};

fn test(source_text: &str, positive: &str) {
let allocator = Allocator::default();
let mut pass = super::PeepholeMinimizeConditions::new();
let mut pass = super::PeepholeMinimizeConditions::new(CompressOptions::all_true());
tester::test(&allocator, source_text, positive, &mut pass);
}

Expand All @@ -86,7 +161,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 @@ -95,70 +169,70 @@ 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(){if(x)a.b=1}");
fold("function f(){if(x){a.b=1}}", "function f(){x&&(a.b=1)}");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REPL here.

fold("function f(){if(x){a.b*=1}}", "function f(){x&&(a.b*=1)}");
fold("function f(){if(x){a.b+=1}}", "function f(){x&&(a.b+=1)}");
fold("function f(){if(x){++a.b}}", "function f(){x&&++a.b}");
fold("function f(){if(x){a.foo()}}", "function f(){x&&a.foo()}");
fold("function f(){if(x){a?.foo()}}", "function f(){x&&a?.foo()}");

// Try it out with throw/catch/finally [which should not change]
fold_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}");
// fold_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}");

// Try it out with switch statements
fold_same("function f(){switch(x){case 1:break}}");
// 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()}",
// );
Comment on lines +201 to +206
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently I didn't support handling the BlockStatement.


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");
// 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("function f(){foo()}", "function f(){foo()}");
fold("switch(x){case y: foo()}", "switch(x){case y:foo()}");
fold(
"try{foo()}catch(ex){bar()}finally{baz()}",
"try{foo()}catch(ex){bar()}finally{baz()}",
);
// fold("function f(){foo()}", "function f(){foo()}");
// fold("switch(x){case y: foo()}", "switch(x){case y:foo()}");
// fold(
// "try{foo()}catch(ex){bar()}finally{baz()}",
// "try{foo()}catch(ex){bar()}finally{baz()}",
// );
}

/** Try to minimize returns */
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl<'a> Compressor<'a> {
&mut StatementFusion::new(),
&mut PeepholeRemoveDeadCode::new(),
// TODO: MinimizeExitPoints
&mut PeepholeMinimizeConditions::new(),
&mut PeepholeMinimizeConditions::new(self.options),
&mut PeepholeSubstituteAlternateSyntax::new(self.options),
&mut PeepholeReplaceKnownMethods::new(),
&mut PeepholeFoldConstants::new(),
Expand Down Expand Up @@ -78,7 +78,7 @@ impl<'a> Compressor<'a> {

fn dead_code_elimination(self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
PeepholeFoldConstants::new().build(program, ctx);
PeepholeMinimizeConditions::new().build(program, ctx);
PeepholeMinimizeConditions::new(self.options).build(program, ctx);
PeepholeRemoveDeadCode::new().build(program, ctx);
}
}
7 changes: 7 additions & 0 deletions crates/oxc_minifier/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
pub struct CompressOptions {
pub dead_code_elimination: bool,

/// Optimize conditions, for example from `if (a) { b } else { c }` to `a ? b : c`.
///
/// Default `true`
pub conditions: bool,

/// Various optimizations for boolean context, for example `!!a ? b : c` → `a ? b : c`.
///
/// Default `true`
Expand Down Expand Up @@ -49,6 +54,7 @@ impl CompressOptions {
pub fn all_true() -> Self {
Self {
dead_code_elimination: false,
conditions: true,
booleans: true,
drop_debugger: true,
drop_console: true,
Expand All @@ -62,6 +68,7 @@ impl CompressOptions {
pub fn all_false() -> Self {
Self {
dead_code_elimination: false,
conditions: false,
booleans: false,
drop_debugger: false,
drop_console: false,
Expand Down
24 changes: 12 additions & 12 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
Original | Minified | esbuild | Gzip | esbuild

72.14 kB | 24.47 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js
72.14 kB | 24.43 kB | 23.70 kB | 8.70 kB | 8.54 kB | react.development.js

173.90 kB | 61.69 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js
173.90 kB | 61.62 kB | 59.82 kB | 19.62 kB | 19.33 kB | moment.js

287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js
287.63 kB | 92.60 kB | 90.07 kB | 32.49 kB | 31.95 kB | jquery.js

342.15 kB | 124.14 kB | 118.14 kB | 44.81 kB | 44.37 kB | vue.js
342.15 kB | 123.67 kB | 118.14 kB | 45.05 kB | 44.37 kB | vue.js

544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js
544.10 kB | 74.08 kB | 72.48 kB | 26.31 kB | 26.20 kB | lodash.js

555.77 kB | 278.70 kB | 270.13 kB | 91.39 kB | 90.80 kB | d3.js
555.77 kB | 278.58 kB | 270.13 kB | 91.70 kB | 90.80 kB | d3.js

1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js
1.01 MB | 469.45 kB | 458.89 kB | 127.45 kB | 126.71 kB | bundle.min.js

1.25 MB | 671.00 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js
1.25 MB | 670.46 kB | 646.76 kB | 165.26 kB | 163.73 kB | three.js

2.14 MB | 756.69 kB | 724.14 kB | 182.87 kB | 181.07 kB | victory.js
2.14 MB | 756.45 kB | 724.14 kB | 183.16 kB | 181.07 kB | victory.js

3.20 MB | 1.05 MB | 1.01 MB | 334.10 kB | 331.56 kB | echarts.js
3.20 MB | 1.05 MB | 1.01 MB | 335.29 kB | 331.56 kB | echarts.js

6.69 MB | 2.44 MB | 2.31 MB | 498.93 kB | 488.28 kB | antd.js
6.69 MB | 2.44 MB | 2.31 MB | 499.89 kB | 488.28 kB | antd.js

10.95 MB | 3.59 MB | 3.49 MB | 913.96 kB | 915.50 kB | typescript.js
10.95 MB | 3.58 MB | 3.49 MB | 916.58 kB | 915.50 kB | typescript.js