diff --git a/crates/oxc_minifier/Cargo.toml b/crates/oxc_minifier/Cargo.toml index 64d343ce905be..acdbd7ea1e93b 100644 --- a/crates/oxc_minifier/Cargo.toml +++ b/crates/oxc_minifier/Cargo.toml @@ -39,5 +39,6 @@ num-traits = { workspace = true } [dev-dependencies] oxc_parser = { workspace = true } +cow-utils = { workspace = true } insta = { workspace = true } pico-args = { workspace = true } diff --git a/crates/oxc_minifier/src/ast_passes/fold_constants.rs b/crates/oxc_minifier/src/ast_passes/fold_constants.rs index 752591a5b4862..081637ea5a0cd 100644 --- a/crates/oxc_minifier/src/ast_passes/fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/fold_constants.rs @@ -1,11 +1,7 @@ -//! Constant Folding -//! -//! - use std::{cmp::Ordering, mem}; use num_bigint::BigInt; -use oxc_ast::{ast::*, AstBuilder, Visit}; +use oxc_ast::{ast::*, AstBuilder}; use oxc_span::{GetSpan, Span, SPAN}; use oxc_syntax::{ number::NumberBase, @@ -14,13 +10,15 @@ use oxc_syntax::{ use oxc_traverse::{Traverse, TraverseCtx}; use crate::{ - keep_var::KeepVar, node_util::{is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil, NumberValue}, tri::Tri, ty::Ty, CompressorPass, }; +/// Constant Folding +/// +/// pub struct FoldConstants<'a> { ast: AstBuilder<'a>, evaluate: bool, @@ -29,10 +27,6 @@ pub struct FoldConstants<'a> { impl<'a> CompressorPass<'a> for FoldConstants<'a> {} impl<'a> Traverse<'a> for FoldConstants<'a> { - fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - self.fold_condition(stmt, ctx); - } - fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { self.fold_expression(expr, ctx); } @@ -58,8 +52,6 @@ impl<'a> FoldConstants<'a> { { self.try_fold_and_or(e, ctx) } - // TODO: move to `PeepholeMinimizeConditions` - Expression::ConditionalExpression(e) => self.try_fold_conditional_expression(e, ctx), Expression::UnaryExpression(e) => self.try_fold_unary_expression(e, ctx), _ => None, } { @@ -109,65 +101,6 @@ impl<'a> FoldConstants<'a> { } } - fn fold_expression_and_get_boolean_value( - &self, - expr: &mut Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Option { - self.fold_expression(expr, ctx); - ctx.get_boolean_value(expr).to_option() - } - - fn fold_if_statement(&self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - let Statement::IfStatement(if_stmt) = stmt else { return }; - - // Descend and remove `else` blocks first. - if let Some(alternate) = &mut if_stmt.alternate { - self.fold_if_statement(alternate, ctx); - if matches!(alternate, Statement::EmptyStatement(_)) { - if_stmt.alternate = None; - } - } - - match self.fold_expression_and_get_boolean_value(&mut if_stmt.test, ctx) { - Some(true) => { - *stmt = self.ast.move_statement(&mut if_stmt.consequent); - } - Some(false) => { - *stmt = if let Some(alternate) = &mut if_stmt.alternate { - self.ast.move_statement(alternate) - } else { - // Keep hoisted `vars` from the consequent block. - let mut keep_var = KeepVar::new(self.ast); - keep_var.visit_statement(&if_stmt.consequent); - keep_var - .get_variable_declaration_statement() - .unwrap_or_else(|| self.ast.statement_empty(SPAN)) - }; - } - None => {} - } - } - - fn try_fold_conditional_expression( - &self, - expr: &mut ConditionalExpression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Option> { - match self.fold_expression_and_get_boolean_value(&mut expr.test, ctx) { - Some(true) => { - // Bail `let o = { f() { assert.ok(this !== o); } }; (true ? o.f : false)(); (true ? o.f : false)``;` - let parent = ctx.ancestry.parent(); - if parent.is_tagged_template_expression() || parent.is_call_expression() { - return None; - } - Some(self.ast.move_expression(&mut expr.consequent)) - } - Some(false) => Some(self.ast.move_expression(&mut expr.alternate)), - _ => None, - } - } - fn try_fold_unary_expression( &self, expr: &mut UnaryExpression<'a>, @@ -722,77 +655,4 @@ impl<'a> FoldConstants<'a> { } None } - - pub(crate) fn fold_condition<'b>( - &self, - stmt: &'b mut Statement<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - match stmt { - Statement::WhileStatement(while_stmt) => { - let minimized_expr = self.fold_expression_in_condition(&mut while_stmt.test); - - if let Some(min_expr) = minimized_expr { - while_stmt.test = min_expr; - } - } - Statement::ForStatement(for_stmt) => { - let test_expr = for_stmt.test.as_mut(); - - if let Some(test_expr) = test_expr { - let minimized_expr = self.fold_expression_in_condition(test_expr); - - if let Some(min_expr) = minimized_expr { - for_stmt.test = Some(min_expr); - } - } - } - Statement::IfStatement(_) => { - self.fold_if_statement(stmt, ctx); - } - _ => {} - }; - } - - fn fold_expression_in_condition(&self, expr: &mut Expression<'a>) -> Option> { - let folded_expr = match expr { - Expression::UnaryExpression(unary_expr) => match unary_expr.operator { - UnaryOperator::LogicalNot => { - let should_fold = Self::try_minimize_not(&mut unary_expr.argument); - - if should_fold { - Some(self.ast.move_expression(&mut unary_expr.argument)) - } else { - None - } - } - _ => None, - }, - _ => None, - }; - - folded_expr - } - - /// ported from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PeepholeMinimizeConditions.java#L401-L435) - fn try_minimize_not(expr: &mut Expression<'a>) -> bool { - let span = &mut expr.span(); - - match expr { - Expression::BinaryExpression(binary_expr) => { - let new_op = binary_expr.operator.equality_inverse_operator(); - - match new_op { - Some(new_op) => { - binary_expr.operator = new_op; - binary_expr.span = *span; - - true - } - _ => false, - } - } - _ => false, - } - } } diff --git a/crates/oxc_minifier/src/ast_passes/minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/minimize_conditions.rs new file mode 100644 index 0000000000000..4e613b95ac2e1 --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/minimize_conditions.rs @@ -0,0 +1,70 @@ +use oxc_ast::{ast::*, AstBuilder}; +use oxc_traverse::{Traverse, TraverseCtx}; + +use crate::{node_util::NodeUtil, tri::Tri, CompressorPass}; + +/// Minimize Conditions +/// +/// A peephole optimization that minimizes conditional expressions according to De Morgan's laws. +/// Also rewrites conditional statements as expressions by replacing them +/// with `? :` and short-circuit binary operators. +/// +/// +pub struct MinimizeConditions<'a> { + ast: AstBuilder<'a>, +} + +impl<'a> CompressorPass<'a> for MinimizeConditions<'a> {} + +impl<'a> Traverse<'a> for MinimizeConditions<'a> { + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.fold_expression(expr, ctx); + } +} + +impl<'a> MinimizeConditions<'a> { + pub fn new(ast: AstBuilder<'a>) -> Self { + Self { ast } + } + + fn fold_expression(&self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(folded_expr) = match expr { + Expression::ConditionalExpression(e) => self.try_fold_conditional_expression(e, ctx), + Expression::UnaryExpression(e) if e.operator.is_not() => self.try_minimize_not(e), + _ => None, + } { + *expr = folded_expr; + }; + } + + fn try_fold_conditional_expression( + &self, + expr: &mut ConditionalExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + match ctx.get_boolean_value(&expr.test) { + Tri::True => { + // Bail `let o = { f() { assert.ok(this !== o); } }; (true ? o.f : false)(); (true ? o.f : false)``;` + let parent = ctx.ancestry.parent(); + if parent.is_tagged_template_expression() || parent.is_call_expression() { + return None; + } + Some(self.ast.move_expression(&mut expr.consequent)) + } + Tri::False => Some(self.ast.move_expression(&mut expr.alternate)), + Tri::Unknown => None, + } + } + + /// Try to minimize NOT nodes such as `!(x==y)`. + fn try_minimize_not(&self, expr: &mut UnaryExpression<'a>) -> Option> { + debug_assert!(expr.operator.is_not()); + if let Expression::BinaryExpression(binary_expr) = &mut expr.argument { + if let Some(new_op) = binary_expr.operator.equality_inverse_operator() { + binary_expr.operator = new_op; + return Some(self.ast.move_expression(&mut expr.argument)); + } + } + None + } +} diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 3447e36bc9dd9..d63c41106c10d 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -1,18 +1,21 @@ mod collapse; mod fold_constants; +mod minimize_conditions; mod remove_dead_code; mod remove_syntax; mod substitute_alternate_syntax; pub use collapse::Collapse; pub use fold_constants::FoldConstants; -use oxc_ast::ast::Program; -use oxc_semantic::{ScopeTree, SymbolTable}; -use oxc_traverse::{walk_program, Traverse, TraverseCtx}; +pub use minimize_conditions::MinimizeConditions; pub use remove_dead_code::RemoveDeadCode; pub use remove_syntax::RemoveSyntax; pub use substitute_alternate_syntax::SubstituteAlternateSyntax; +use oxc_ast::ast::Program; +use oxc_semantic::{ScopeTree, SymbolTable}; +use oxc_traverse::{walk_program, Traverse, TraverseCtx}; + use crate::node_util::NodeUtil; impl<'a> NodeUtil for TraverseCtx<'a> { diff --git a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs index a7aaacdf3c5ba..560c90261f025 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs @@ -1,8 +1,9 @@ use oxc_allocator::Vec; use oxc_ast::{ast::*, AstBuilder, Visit}; +use oxc_span::SPAN; use oxc_traverse::{Traverse, TraverseCtx}; -use crate::{keep_var::KeepVar, CompressorPass}; +use crate::{keep_var::KeepVar, node_util::NodeUtil, tri::Tri, CompressorPass}; /// Remove Dead Code from the AST. /// @@ -16,7 +17,11 @@ pub struct RemoveDeadCode<'a> { impl<'a> CompressorPass<'a> for RemoveDeadCode<'a> {} impl<'a> Traverse<'a> for RemoveDeadCode<'a> { - fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) { + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.fold_if_statement(stmt, ctx); + } + + fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) { stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_))); self.dead_code_elimination(stmts); } @@ -76,4 +81,35 @@ impl<'a> RemoveDeadCode<'a> { stmts.push(stmt); } } + + fn fold_if_statement(&self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let Statement::IfStatement(if_stmt) = stmt else { return }; + + // Descend and remove `else` blocks first. + if let Some(alternate) = &mut if_stmt.alternate { + self.fold_if_statement(alternate, ctx); + if matches!(alternate, Statement::EmptyStatement(_)) { + if_stmt.alternate = None; + } + } + + match ctx.get_boolean_value(&if_stmt.test) { + Tri::True => { + *stmt = self.ast.move_statement(&mut if_stmt.consequent); + } + Tri::False => { + *stmt = if let Some(alternate) = &mut if_stmt.alternate { + self.ast.move_statement(alternate) + } else { + // Keep hoisted `vars` from the consequent block. + let mut keep_var = KeepVar::new(self.ast); + keep_var.visit_statement(&if_stmt.consequent); + keep_var + .get_variable_declaration_statement() + .unwrap_or_else(|| self.ast.statement_empty(SPAN)) + }; + } + Tri::Unknown => {} + } + } } diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index 3035c933fe381..5b30c39828688 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -5,7 +5,8 @@ use oxc_traverse::TraverseCtx; use crate::{ ast_passes::{ - Collapse, FoldConstants, RemoveDeadCode, RemoveSyntax, SubstituteAlternateSyntax, + Collapse, FoldConstants, MinimizeConditions, RemoveDeadCode, RemoveSyntax, + SubstituteAlternateSyntax, }, CompressOptions, CompressorPass, }; @@ -37,6 +38,7 @@ impl<'a> Compressor<'a> { // TODO: inline variables self.remove_syntax(program, &mut ctx); self.fold_constants(program, &mut ctx); + self.minimize_conditions(program, &mut ctx); self.remove_dead_code(program, &mut ctx); // TODO: StatementFusion self.substitute_alternate_syntax(program, &mut ctx); @@ -49,6 +51,12 @@ impl<'a> Compressor<'a> { } } + fn minimize_conditions(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.minimize_conditions { + MinimizeConditions::new(ctx.ast).build(program, ctx); + } + } + fn fold_constants(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { if self.options.fold_constants { FoldConstants::new(ctx.ast).with_evaluate(self.options.evaluate).build(program, ctx); diff --git a/crates/oxc_minifier/src/options.rs b/crates/oxc_minifier/src/options.rs index 927d80ef83ada..6a0476cf4ea51 100644 --- a/crates/oxc_minifier/src/options.rs +++ b/crates/oxc_minifier/src/options.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone, Copy)] pub struct CompressOptions { pub remove_syntax: bool, + pub minimize_conditions: bool, pub substitute_alternate_syntax: bool, pub fold_constants: bool, pub remove_dead_code: bool, @@ -42,28 +43,22 @@ pub struct CompressOptions { pub typeofs: bool, } +#[allow(clippy::derivable_impls)] impl Default for CompressOptions { fn default() -> Self { - Self { - remove_syntax: true, - substitute_alternate_syntax: true, - fold_constants: true, - remove_dead_code: true, - collapse: true, - booleans: true, - drop_debugger: true, - drop_console: false, - evaluate: true, - join_vars: true, - loops: true, - typeofs: true, - } + Self { drop_console: false, ..Self::all_true() } } } impl CompressOptions { pub fn all_true() -> Self { Self { + remove_syntax: true, + minimize_conditions: true, + substitute_alternate_syntax: true, + fold_constants: true, + remove_dead_code: true, + collapse: true, booleans: true, drop_debugger: true, drop_console: true, @@ -71,13 +66,13 @@ impl CompressOptions { join_vars: true, loops: true, typeofs: true, - ..Self::default() } } pub fn all_false() -> Self { Self { remove_syntax: false, + minimize_conditions: false, substitute_alternate_syntax: false, fold_constants: false, remove_dead_code: false, @@ -95,6 +90,7 @@ impl CompressOptions { pub fn dead_code_elimination() -> Self { Self { remove_syntax: true, + minimize_conditions: true, fold_constants: true, remove_dead_code: true, ..Self::all_false() diff --git a/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs similarity index 94% rename from crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs rename to crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs index efc75124a1427..fc5e9975cfdac 100644 --- a/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs @@ -1,8 +1,15 @@ +use cow_utils::CowUtils; + use oxc_minifier::CompressOptions; fn test(source_text: &str, expected: &str) { + let t = "('production' == 'production')"; + let f = "('production' == 'development')"; + let source_text = source_text.cow_replace("true", t); + let source_text = source_text.cow_replace("false", f); + let options = CompressOptions::dead_code_elimination(); - crate::test(source_text, expected, options); + crate::test(&source_text, expected, options); } fn test_same(source_text: &str) { @@ -85,11 +92,7 @@ fn dce_if_statement() { // typeof test("if (typeof 1 !== 'number') { REMOVE; }", ""); test("if (typeof false !== 'boolean') { REMOVE; }", ""); - test( - "if (typeof 1 === 'string') { REMOVE; -}", - "", - ); + test("if (typeof 1 === 'string') { REMOVE; }", ""); } #[test] diff --git a/crates/oxc_minifier/tests/ast_passes/minimize_conditions.rs b/crates/oxc_minifier/tests/ast_passes/minimize_conditions.rs new file mode 100644 index 0000000000000..d152d2502dbc9 --- /dev/null +++ b/crates/oxc_minifier/tests/ast_passes/minimize_conditions.rs @@ -0,0 +1,68 @@ +//! + +use oxc_minifier::CompressOptions; + +// TODO: handle negative cases +fn test(source_text: &str, positive: &str, _negative: &str) { + let options = CompressOptions { minimize_conditions: true, ..CompressOptions::all_false() }; + crate::test(source_text, positive, options); +} + +#[test] +#[ignore] +fn try_minimize_cond_simple() { + test("x", "x", "x"); + test("!x", "!x", "!x"); + test("!!x", "x", "x"); + test("!(x && y)", "!x || !y", "!(x && y)"); +} + +#[test] +#[ignore] +fn minimize_demorgan_simple() { + test("!(x&&y)", "!x||!y", "!(x&&y)"); + test("!(x||y)", "!x&&!y", "!(x||y)"); + test("!x||!y", "!x||!y", "!(x&&y)"); + test("!x&&!y", "!x&&!y", "!(x||y)"); + test("!(x && y && z)", "!(x && y && z)", "!(x && y && z)"); + test("(!a||!b)&&c", "(!a||!b)&&c", "!(a&&b||!c)"); + test("(!a||!b)&&(c||d)", "!(a&&b||!c&&!d)", "!(a&&b||!c&&!d)"); +} + +#[test] +#[ignore] +fn minimize_bug8494751() { + test( + "x && (y===2 || !f()) && (y===3 || !h())", + // TODO(tbreisacher): The 'positive' option could be better: + // "x && !((y!==2 && f()) || (y!==3 && h()))", + "!(!x || (y!==2 && f()) || (y!==3 && h()))", + "!(!x || (y!==2 && f()) || (y!==3 && h()))", + ); + + test( + "x && (y===2 || !f?.()) && (y===3 || !h?.())", + "!(!x || (y!==2 && f?.()) || (y!==3 && h?.()))", + "!(!x || (y!==2 && f?.()) || (y!==3 && h?.()))", + ); +} + +#[test] +#[ignore] +fn minimize_complementable_operator() { + test("0===c && (2===a || 1===a)", "0===c && (2===a || 1===a)", "!(0!==c || 2!==a && 1!==a)"); +} + +#[test] +#[ignore] +fn minimize_hook() { + test("!(x ? y : z)", "(x ? !y : !z)", "!(x ? y : z)"); +} + +#[test] +#[ignore] +fn minimize_comma() { + test("!(inc(), test())", "inc(), !test()", "!(inc(), test())"); + test("!(inc?.(), test?.())", "inc?.(), !test?.()", "!(inc?.(), test?.())"); + test("!((x,y)&&z)", "(x,!y)||!z", "!((x,y)&&z)"); +} diff --git a/crates/oxc_minifier/tests/ast_passes/mod.rs b/crates/oxc_minifier/tests/ast_passes/mod.rs index 1ecc67f1b4b3a..98033dab287bb 100644 --- a/crates/oxc_minifier/tests/ast_passes/mod.rs +++ b/crates/oxc_minifier/tests/ast_passes/mod.rs @@ -1,6 +1,7 @@ mod collapse_variable_declarations; +mod dead_code_elimination; mod fold_conditions; mod fold_constants; -mod remove_dead_code; +mod minimize_conditions; mod reorder_constant_expression; mod substitute_alternate_syntax; diff --git a/tasks/coverage/minifier_test262.snap b/tasks/coverage/minifier_test262.snap index 9b320b73fdbda..0a8004b0abf96 100644 --- a/tasks/coverage/minifier_test262.snap +++ b/tasks/coverage/minifier_test262.snap @@ -2,4 +2,13 @@ commit: d62fa93c minifier_test262 Summary: AST Parsed : 43765/43765 (100.00%) -Positive Passed: 43765/43765 (100.00%) +Positive Passed: 43756/43765 (99.98%) +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A3_T2.js +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A3_T3.js +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A3_T4.js +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A4_T2.js +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A4_T3.js +Compress: tasks/coverage/test262/test/language/expressions/conditional/S11.12_A4_T4.js +Compress: tasks/coverage/test262/test/language/statements/if/S12.5_A12_T1.js +Compress: tasks/coverage/test262/test/language/statements/if/S12.5_A12_T3.js +Compress: tasks/coverage/test262/test/language/statements/if/S12.5_A12_T4.js diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 610eececa4468..5bf6aed77c500 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,6 +1,6 @@ Original | Minified | esbuild | Gzip | esbuild -72.14 kB | 24.38 kB | 23.70 kB | 8.73 kB | 8.54 kB | react.development.js +72.14 kB | 24.37 kB | 23.70 kB | 8.73 kB | 8.54 kB | react.development.js 173.90 kB | 61.83 kB | 59.82 kB | 19.59 kB | 19.33 kB | moment.js @@ -18,9 +18,9 @@ Original | Minified | esbuild | Gzip | esbuild 2.14 MB | 751.46 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js -3.20 MB | 1.03 MB | 1.01 MB | 332.61 kB | 331.56 kB | echarts.js +3.20 MB | 1.03 MB | 1.01 MB | 332.60 kB | 331.56 kB | echarts.js -6.69 MB | 2.42 MB | 2.31 MB | 503.23 kB | 488.28 kB | antd.js +6.69 MB | 2.42 MB | 2.31 MB | 503.22 kB | 488.28 kB | antd.js 10.95 MB | 3.60 MB | 3.49 MB | 915.21 kB | 915.50 kB | typescript.js