From 85ce9d76b869d75f367bd8298a55ebe718e95091 Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 9 Jan 2025 16:16:47 +0800 Subject: [PATCH] feat(minifier): minimize conditions in boolean context --- crates/oxc_ecmascript/src/to_boolean.rs | 1 + crates/oxc_minifier/src/ast_passes/mod.rs | 30 +- .../peephole_minimize_conditions.rs | 378 +++++++++--------- tasks/minsize/minsize.snap | 22 +- 4 files changed, 219 insertions(+), 212 deletions(-) diff --git a/crates/oxc_ecmascript/src/to_boolean.rs b/crates/oxc_ecmascript/src/to_boolean.rs index d1e051a5de2ad0..8d9338bb1d8ce1 100644 --- a/crates/oxc_ecmascript/src/to_boolean.rs +++ b/crates/oxc_ecmascript/src/to_boolean.rs @@ -46,6 +46,7 @@ impl<'a> ToBoolean<'a> for Expression<'a> { .and_then(|quasi| quasi.value.cooked.as_ref()) .map(|cooked| !cooked.is_empty()) } + Expression::SequenceExpression(e) => e.expressions.last().and_then(Self::to_boolean), _ => None, } } diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 0e0f203945706c..e656fe4514d251 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -44,8 +44,8 @@ pub struct PeepholeOptimizations { x2_exploit_assigns: ExploitAssigns, x3_collapse_variable_declarations: CollapseVariableDeclarations, x4_peephole_fold_constants: PeepholeFoldConstants, - x5_peephole_remove_dead_code: PeepholeRemoveDeadCode, - x6_peephole_minimize_conditions: PeepholeMinimizeConditions, + x6_peephole_remove_dead_code: PeepholeRemoveDeadCode, + x5_peephole_minimize_conditions: PeepholeMinimizeConditions, x7_peephole_replace_known_methods: PeepholeReplaceKnownMethods, x8_convert_to_dotted_properties: ConvertToDottedProperties, x9_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax, @@ -61,8 +61,8 @@ impl PeepholeOptimizations { x2_exploit_assigns: ExploitAssigns::new(), x3_collapse_variable_declarations: CollapseVariableDeclarations::new(), x4_peephole_fold_constants: PeepholeFoldConstants::new(), - x5_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(), - x6_peephole_minimize_conditions: PeepholeMinimizeConditions::new(target), + x5_peephole_minimize_conditions: PeepholeMinimizeConditions::new(target), + x6_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(), x7_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(), x8_convert_to_dotted_properties: ConvertToDottedProperties::new(in_fixed_loop), x9_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax::new( @@ -78,8 +78,8 @@ impl PeepholeOptimizations { self.x2_exploit_assigns.changed = false; self.x3_collapse_variable_declarations.changed = false; self.x4_peephole_fold_constants.changed = false; - self.x5_peephole_remove_dead_code.changed = false; - self.x6_peephole_minimize_conditions.changed = false; + self.x5_peephole_minimize_conditions.changed = false; + self.x6_peephole_remove_dead_code.changed = false; self.x7_peephole_replace_known_methods.changed = false; self.x9_peephole_substitute_alternate_syntax.changed = false; } @@ -90,8 +90,8 @@ impl PeepholeOptimizations { || self.x2_exploit_assigns.changed || self.x3_collapse_variable_declarations.changed || self.x4_peephole_fold_constants.changed - || self.x5_peephole_remove_dead_code.changed - || self.x6_peephole_minimize_conditions.changed + || self.x5_peephole_minimize_conditions.changed + || self.x6_peephole_remove_dead_code.changed || self.x7_peephole_replace_known_methods.changed || self.x9_peephole_substitute_alternate_syntax.changed } @@ -125,7 +125,7 @@ impl<'a> CompressorPass<'a> for PeepholeOptimizations { impl<'a> Traverse<'a> for PeepholeOptimizations { fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { - self.x5_peephole_remove_dead_code.exit_program(program, ctx); + self.x6_peephole_remove_dead_code.exit_program(program, ctx); } fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { @@ -133,13 +133,13 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { self.x1_minimize_exit_points.exit_statements(stmts, ctx); self.x2_exploit_assigns.exit_statements(stmts, ctx); self.x3_collapse_variable_declarations.exit_statements(stmts, ctx); - self.x5_peephole_remove_dead_code.exit_statements(stmts, ctx); - self.x6_peephole_minimize_conditions.exit_statements(stmts, ctx); + self.x5_peephole_minimize_conditions.exit_statements(stmts, ctx); + self.x6_peephole_remove_dead_code.exit_statements(stmts, ctx); } fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - self.x5_peephole_remove_dead_code.exit_statement(stmt, ctx); - self.x6_peephole_minimize_conditions.exit_statement(stmt, ctx); + self.x5_peephole_minimize_conditions.exit_statement(stmt, ctx); + self.x6_peephole_remove_dead_code.exit_statement(stmt, ctx); } fn exit_return_statement(&mut self, stmt: &mut ReturnStatement<'a>, ctx: &mut TraverseCtx<'a>) { @@ -160,8 +160,8 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { self.x4_peephole_fold_constants.exit_expression(expr, ctx); - self.x5_peephole_remove_dead_code.exit_expression(expr, ctx); - self.x6_peephole_minimize_conditions.exit_expression(expr, ctx); + self.x5_peephole_minimize_conditions.exit_expression(expr, ctx); + self.x6_peephole_remove_dead_code.exit_expression(expr, ctx); self.x7_peephole_replace_known_methods.exit_expression(expr, ctx); self.x9_peephole_substitute_alternate_syntax.exit_expression(expr, 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 302b114b736565..9c6c2609ac7d3b 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,11 +1,11 @@ use oxc_allocator::Vec; use oxc_ast::{ast::*, NONE}; -use oxc_ecmascript::constant_evaluation::ValueType; +use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType}; use oxc_span::{cmp::ContentEq, GetSpan, SPAN}; use oxc_syntax::es_target::ESTarget; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx}; -use crate::CompressorPass; +use crate::{ctx::Ctx, CompressorPass}; /// Minimize Conditions /// @@ -43,6 +43,26 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { } fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let expr = match stmt { + Statement::IfStatement(s) => Some(&mut s.test), + Statement::WhileStatement(s) => Some(&mut s.test), + Statement::ForStatement(s) => s.test.as_mut(), + Statement::DoWhileStatement(s) => Some(&mut s.test), + Statement::ExpressionStatement(s) + if !matches!( + ctx.ancestry.ancestor(1), + Ancestor::ArrowFunctionExpressionBody(_) + ) => + { + Some(&mut s.expression) + } + _ => None, + }; + + if let Some(expr) = expr { + self.try_fold_expr_in_boolean_context(expr, Ctx(ctx)); + } + 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), @@ -54,9 +74,12 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { } fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Expression::ConditionalExpression(e) = expr { + self.try_fold_expr_in_boolean_context(&mut e.test, Ctx(ctx)); + } + if let Some(folded_expr) = match expr { Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx), - Expression::LogicalExpression(e) => Self::try_minimize_logical(e, ctx), Expression::BinaryExpression(e) => Self::try_minimize_binary(e, ctx), Expression::ConditionalExpression(e) => self.try_minimize_conditional(e, ctx), _ => None, @@ -72,56 +95,18 @@ impl<'a> PeepholeMinimizeConditions { Self { target, changed: false } } - /// Try to minimize NOT nodes such as `!(x==y)`. fn try_minimize_not( expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { - // TODO: tryMinimizeCondition(node.getFirstChild()); - if !expr.operator.is_not() { - return None; - } - if let Expression::UnaryExpression(e1) = &mut expr.argument { - if e1.operator.is_not() { - // `!!!a` -> `!!a` - if let Expression::UnaryExpression(e2) = &mut e1.argument { - if e2.operator.is_not() { - expr.argument = ctx.ast.move_expression(&mut e2.argument); - return Some(ctx.ast.move_expression(&mut expr.argument)); - } - // `!!delete a.b` -> `delete a.b` - if e2.operator.is_delete() { - return Some(ctx.ast.move_expression(&mut e1.argument)); - } - } - // `!!a` -> `a` // ONLY in boolean contexts - if Self::is_in_boolean_context(ctx) { + if expr.operator.is_not() { + if let Expression::UnaryExpression(e1) = &mut expr.argument { + if e1.operator.is_not() && ValueType::from(&e1.argument).is_boolean() { return Some(ctx.ast.move_expression(&mut e1.argument)); } - if let Expression::BinaryExpression(bin_expr) = &e1.argument { - if matches!( - bin_expr.operator, - BinaryOperator::Equality - | BinaryOperator::Inequality - | BinaryOperator::StrictEquality - | BinaryOperator::StrictInequality - | BinaryOperator::LessThan - | BinaryOperator::LessEqualThan - | BinaryOperator::GreaterThan - | BinaryOperator::GreaterEqualThan - | BinaryOperator::In - | BinaryOperator::Instanceof - ) { - return Some(ctx.ast.move_expression(&mut e1.argument)); - } - } } } - - let Expression::BinaryExpression(binary_expr) = &mut expr.argument else { return None }; - let new_op = binary_expr.operator.equality_inverse_operator()?; - binary_expr.operator = new_op; - Some(ctx.ast.move_expression(&mut expr.argument)) + None } fn try_minimize_if( @@ -296,55 +281,7 @@ impl<'a> PeepholeMinimizeConditions { } } - fn try_minimize_logical( - expr: &mut LogicalExpression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Option> { - // `a && true` -> `a` - // `a && false` -> `false` - if expr.operator == LogicalOperator::And { - if let ( - Expression::Identifier(test_ident), - Expression::BooleanLiteral(consequent_lit), - ) = (&expr.left, &expr.right) - { - if !Self::is_in_boolean_context(ctx) { - return None; - } - if consequent_lit.value { - return Some(ctx.ast.move_expression(&mut expr.left)); - } - if ctx.scopes().find_binding(ctx.current_scope_id(), &test_ident.name).is_some() { - return Some(ctx.ast.expression_boolean_literal(expr.span, false)); - } - return None; - } - } - - // `a || true` -> `true` - // `a || false` -> `a` - if expr.operator == LogicalOperator::Or { - if let ( - Expression::Identifier(test_ident), - Expression::BooleanLiteral(consequent_lit), - ) = (&expr.left, &expr.right) - { - if consequent_lit.value { - if ctx.scopes().find_binding(ctx.current_scope_id(), &test_ident.name).is_some() - { - return Some(ctx.ast.expression_boolean_literal(expr.span, true)); - } - } else { - return Some(ctx.ast.move_expression(&mut expr.left)); - } - return None; - } - } - - None - } - - // based on https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/internal/js_ast/js_ast_helpers.go#L2745 + // https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2745 fn try_minimize_conditional( &self, expr: &mut ConditionalExpression<'a>, @@ -384,6 +321,22 @@ impl<'a> PeepholeMinimizeConditions { } } + // `x != y ? b : c` -> `x == y ? c : b` + if let Expression::BinaryExpression(test_expr) = &mut expr.test { + if matches!( + test_expr.operator, + BinaryOperator::Inequality | BinaryOperator::StrictInequality + ) { + test_expr.operator = test_expr.operator.equality_inverse_operator().unwrap(); + let test = ctx.ast.move_expression(&mut expr.test); + let consequent = ctx.ast.move_expression(&mut expr.consequent); + let alternate = ctx.ast.move_expression(&mut expr.alternate); + return Some( + ctx.ast.expression_conditional(expr.span, test, alternate, consequent), + ); + } + } + // TODO: `/* @__PURE__ */ a() ? b : b` -> `b` // `a ? b : b` -> `a, b` @@ -729,46 +682,115 @@ impl<'a> PeepholeMinimizeConditions { None } - // returns `true` if the current node is in a context in which the return - // value type is coerced to boolean. - // For example `if (condition)` and `return condition` - // inside the `if` stmt, `condition` is coerced to a boolean - // whereas inside the return, it is not - fn is_in_boolean_context(ctx: &mut TraverseCtx<'_>) -> bool { - let mut ancestors = ctx.ancestors().peekable(); - while let Some(ancestor) = ancestors.next() { - match ancestor { - Ancestor::IfStatementTest(_) - | Ancestor::WhileStatementTest(_) - | Ancestor::ForStatementTest(_) - | Ancestor::DoWhileStatementTest(_) - | Ancestor::SequenceExpressionExpressions(_) - | Ancestor::ProgramBody(_) => return true, - // `var k = () => foo`, `foo` is not coerced to a boolean - Ancestor::ExpressionStatementExpression(_) => { - if let Some(next_ancestor) = ancestors.peek() { - match next_ancestor { - Ancestor::FunctionBodyStatements(_) => return false, - _ => return true, - } + /// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`. + /// + /// + fn try_fold_expr_in_boolean_context(&mut self, e: &mut Expression<'a>, ctx: Ctx<'a, '_>) { + loop { + let changed = self.try_fold_expr_in_boolean_context_impl(e, ctx); + if changed { + self.changed = true; + } else { + return; + } + } + } + + fn try_fold_expr_in_boolean_context_impl( + &mut self, + expr: &mut Expression<'a>, + ctx: Ctx<'a, '_>, + ) -> bool { + match expr { + // "!!a" => "a" + Expression::UnaryExpression(u1) if u1.operator.is_not() => { + if let Expression::UnaryExpression(u2) = &mut u1.argument { + if u2.operator.is_not() { + let mut e = ctx.ast.move_expression(&mut u2.argument); + self.try_fold_expr_in_boolean_context(&mut e, ctx); + *expr = e; + return true; } } - Ancestor::CallExpressionArguments(_) - | Ancestor::AssignmentPatternRight(_) - | Ancestor::BindingRestElementArgument(_) - | Ancestor::JSXSpreadAttributeArgument(_) - | Ancestor::NewExpressionArguments(_) - | Ancestor::ObjectPropertyKey(_) - | Ancestor::ObjectPropertyValue(_) - | Ancestor::ReturnStatementArgument(_) - | Ancestor::ThrowStatementArgument(_) - | Ancestor::YieldExpressionArgument(_) - | Ancestor::VariableDeclaratorInit(_) => return false, - _ => continue, } + Expression::BinaryExpression(e) + if e.operator.is_equality() + && matches!(&e.right, Expression::NumericLiteral(lit) if lit.value == 0.0) + && ValueType::from(&e.left).is_number() => + { + let argument = ctx.ast.move_expression(&mut e.left); + *expr = if matches!( + e.operator, + BinaryOperator::StrictInequality | BinaryOperator::Inequality + ) { + // `if ((a | b) !== 0)` -> `if (a | b);` + argument + } else { + // `if ((a | b) === 0);", "if (!(a | b));")` + ctx.ast.expression_unary(e.span, UnaryOperator::LogicalNot, argument) + }; + return true; + } + // "if (!!a && !!b)" => "if (a && b)" + Expression::LogicalExpression(e) if e.operator == LogicalOperator::And => { + self.try_fold_expr_in_boolean_context(&mut e.left, ctx); + self.try_fold_expr_in_boolean_context(&mut e.right, ctx); + // "if (anything && truthyNoSideEffects)" => "if (anything)" + if ctx.get_side_free_boolean_value(&e.right) == Some(true) { + *expr = ctx.ast.move_expression(&mut e.left); + return true; + } + } + // "if (!!a ||!!b)" => "if (a || b)" + Expression::LogicalExpression(e) if e.operator == LogicalOperator::Or => { + self.try_fold_expr_in_boolean_context(&mut e.left, ctx); + self.try_fold_expr_in_boolean_context(&mut e.right, ctx); + // "if (anything || falsyNoSideEffects)" => "if (anything)" + if ctx.get_side_free_boolean_value(&e.right) == Some(false) { + *expr = ctx.ast.move_expression(&mut e.left); + return true; + } + } + Expression::ConditionalExpression(e) => { + // "if (a ? !!b : !!c)" => "if (a ? b : c)" + self.try_fold_expr_in_boolean_context(&mut e.consequent, ctx); + self.try_fold_expr_in_boolean_context(&mut e.alternate, ctx); + if let Some(boolean) = ctx.get_side_free_boolean_value(&e.consequent) { + let right = ctx.ast.move_expression(&mut e.alternate); + let left = ctx.ast.move_expression(&mut e.test); + if boolean { + // "if (anything1 ? truthyNoSideEffects : anything2)" => "if (anything1 || anything2)" + *expr = + ctx.ast.expression_logical(e.span(), left, LogicalOperator::Or, right); + } else { + // "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 || anything2)" + let left = + ctx.ast.expression_unary(left.span(), UnaryOperator::LogicalNot, left); + *expr = + ctx.ast.expression_logical(e.span(), left, LogicalOperator::Or, right); + } + return true; + } + if let Some(boolean) = ctx.get_side_free_boolean_value(&e.alternate) { + let left = ctx.ast.move_expression(&mut e.test); + let right = ctx.ast.move_expression(&mut e.consequent); + if boolean { + // "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)" + let left = + ctx.ast.expression_unary(left.span(), UnaryOperator::LogicalNot, left); + *expr = + ctx.ast.expression_logical(e.span(), left, LogicalOperator::Or, right); + } else { + // "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)" + *expr = + ctx.ast.expression_logical(e.span(), left, LogicalOperator::And, right); + } + return true; + } + } + _ => {} } - - true + false } // `a instanceof b === true` -> `a instanceof b` @@ -819,25 +841,6 @@ impl<'a> PeepholeMinimizeConditions { ctx.ast.expression_unary(e.span, UnaryOperator::LogicalNot, argument) }) } - Expression::NumericLiteral(lit) - if lit.value == 0.0 - && !e.left.is_literal() // let constant folding do the work - && left.is_number() - && Self::is_in_boolean_context(ctx) => - { - match e.operator { - // `x >> y !== 0` -> `x >> y` - BinaryOperator::StrictInequality | BinaryOperator::Inequality => { - Some(ctx.ast.move_expression(&mut e.left)) - } - // `x >> y !== 0` -> `!(x >> y)` - BinaryOperator::StrictEquality | BinaryOperator::Equality => { - let argument = ctx.ast.move_expression(&mut e.left); - Some(ctx.ast.expression_unary(e.span, UnaryOperator::LogicalNot, argument)) - } - _ => None, - } - } _ => None, } } @@ -951,7 +954,6 @@ mod test { // ); } - /** Try to minimize returns */ #[test] fn test_fold_returns() { fold("function f(){if(x)return 1;else return 2}", "function f(){return x?1:2}"); @@ -1146,15 +1148,15 @@ mod test { #[test] fn test_minimize_expr_condition() { - fold("(x ? true : false) && y()", "!!x && y()"); + fold("(x ? true : false) && y()", "x && y()"); fold("(x ? false : true) && y()", "!x && y()"); - fold("(x ? true : y) && y()", "(!!x || y) && y()"); - fold("(x ? y : false) && y()", "(!!x && y) && y()"); + fold("(x ? true : y) && y()", "(x || y) && y();"); + fold("(x ? y : false) && y()", "(x && y) && y()"); fold("var x; (x && true) && y()", "var x; x && y()"); - fold("var x; (x && false) && y()", "var x; false && y()"); + fold("var x; (x && false) && y()", "var x; x && false && y()"); fold("(x && true) && y()", "x && y()"); fold("(x && false) && y()", "x && false && y()"); - fold("var x; (x || true) && y()", "var x; true && y()"); + fold("var x; (x || true) && y()", "var x; (x || true) && y()"); fold("var x; (x || false) && y()", "var x; x && y()"); fold_same("(x || true) && y()"); @@ -1285,6 +1287,7 @@ mod test { fold_same("x?.() ? x?.() : y()"); fold("!x ? foo() : bar()", "x ? bar() : foo()"); + // TODO // fold("while(!(x ? y : z)) foo();", "while(x ? !y : !z) foo();"); // fold("(x ? !y : !z) ? foo() : bar()", "(x ? y : z) ? bar() : foo()"); } @@ -1318,15 +1321,14 @@ mod test { } #[test] - #[ignore] fn test_minimize_for_condition() { // This test uses constant folding logic, so is only here for completeness. // These could be simplified to "for(;;) ..." - fold("for(;!!true;) foo()", "for(;1;) foo()"); + fold("for(;!!true;) foo()", "for(;true;) foo()"); // Verify function deletion tracking. - fold("if(!!true||function(){}) {}", "if(1) {}"); + // fold("if(!!true||function(){}) {}", "if(1) {}"); // Don't bother with FOR inits as there are normalized out. - fold("for(!!true;;) foo()", "for(!0;;) foo()"); + fold("for(!!true;;) foo()", "for(true;;) foo()"); // These test tryMinimizeCondition fold("for(;!!x;) foo()", "for(;x;) foo()"); @@ -1334,16 +1336,15 @@ mod test { fold_same("for(a in b) foo()"); fold_same("for(a in {}) foo()"); fold_same("for(a in []) foo()"); - fold("for(a in !!true) foo()", "for(a in !0) foo()"); + fold("for(a in !!true) foo()", "for(a in true) foo()"); fold_same("for(a of b) foo()"); fold_same("for(a of {}) foo()"); fold_same("for(a of []) foo()"); - fold("for(a of !!true) foo()", "for(a of !0) foo()"); + fold("for(a of !!true) foo()", "for(a of true) foo()"); } #[test] - #[ignore] fn test_minimize_condition_example1() { // Based on a real failing code sample. fold("if(!!(f() > 20)) {foo();foo()}", "if(f() > 20){foo();foo()}"); @@ -1803,7 +1804,6 @@ mod test { #[test] fn test_coercion_substitution_disabled() { - // enableTypeCheck(); test_same("var x = {}; if (x != null) throw 'a';"); test_same("var x = {}; var y = x != null;"); @@ -1813,13 +1813,11 @@ mod test { #[test] fn test_coercion_substitution_boolean_result0() { - // enableTypeCheck(); test_same("var x = {}; var y = x != null;"); } #[test] fn test_coercion_substitution_boolean_result1() { - // enableTypeCheck(); test_same("var x = {}; var y = x == null;"); test_same("var x = {}; var y = x !== null;"); test_same("var x = undefined; var y = x !== null;"); @@ -1834,7 +1832,6 @@ mod test { #[test] fn test_coercion_substitution_if() { - // enableTypeCheck(); test("var x = {};\nif (x != null) throw 'a';\n", "var x={}; if (x!=null) throw 'a'"); test_same("var x = {};\nif (x == null) throw 'a';\n"); test_same("var x = {};\nif (x !== null) throw 'a';\n"); @@ -1859,31 +1856,24 @@ mod test { #[test] fn test_coercion_substitution_expression() { - // enableTypeCheck(); test_same("var x = {}; x != null && alert('b');"); test_same("var x = 1; x != 0 && alert('b');"); } #[test] fn test_coercion_substitution_hook() { - // enableTypeCheck(); - test_same(concat!("var x = {};", "var y = x != null ? 1 : 2;")); - test_same(concat!("var x = 1;", "var y = x != 0 ? 1 : 2;")); + test("var x = {}; var y = x != null ? 1 : 2;", "var x = {}; var y = x == null ? 2 : 1;"); + test("var x = 1; var y = x != 0 ? 1 : 2;", "var x = 1; var y = x == 0 ? 2 : 1;"); } #[test] fn test_coercion_substitution_not() { - // enableTypeCheck(); - test( - "var x = {};\nvar y = !(x != null) ? 1 : 2;\n", - "var x = {};\nvar y = (x == null) ? 1 : 2;\n", - ); - test("var x = 1;\nvar y = !(x != 0) ? 1 : 2;\n", "var x = 1;\nvar y = x == 0 ? 1 : 2;\n"); + test("var x = {}; var y = !(x != null) ? 1 : 2;", "var x = {}; var y = x != null ? 2 : 1;"); + test("var x = 1; var y = !(x != 0) ? 1 : 2; ", "var x = 1; var y = x != 0 ? 2 : 1; "); } #[test] fn test_coercion_substitution_while() { - // enableTypeCheck(); test( "var x = {}; while (x != null) throw 'a';", "var x = {}; for (;x != null;) throw 'a';", @@ -1893,21 +1883,18 @@ mod test { #[test] fn test_coercion_substitution_unknown_type() { - // enableTypeCheck(); test_same("var x = /** @type {?} */ ({});\nif (x != null) throw 'a';\n"); test_same("var x = /** @type {?} */ (1);\nif (x != 0) throw 'a';\n"); } #[test] fn test_coercion_substitution_all_type() { - // enableTypeCheck(); test_same("var x = /** @type {*} */ ({});\nif (x != null) throw 'a';\n"); test_same("var x = /** @type {*} */ (1);\nif (x != 0) throw 'a';\n"); } #[test] fn test_coercion_substitution_primitives_vs_null() { - // enableTypeCheck(); test_same("var x = 0;\nif (x != null) throw 'a';\n"); test_same("var x = '';\nif (x != null) throw 'a';\n"); test_same("var x = false;\nif (x != null) throw 'a';\n"); @@ -1915,7 +1902,6 @@ mod test { #[test] fn test_coercion_substitution_non_number_vs_zero() { - // enableTypeCheck(); test_same("var x = {};\nif (x != 0) throw 'a';\n"); test_same("var x = '';\nif (x != 0) throw 'a';\n"); test_same("var x = false;\nif (x != 0) throw 'a';\n"); @@ -1923,13 +1909,11 @@ mod test { #[test] fn test_coercion_substitution_boxed_number_vs_zero() { - // enableTypeCheck(); test_same("var x = new Number(0);\nif (x != 0) throw 'a';\n"); } #[test] fn test_coercion_substitution_boxed_primitives() { - // enableTypeCheck(); test_same("var x = new Number(); if (x != null) throw 'a';"); test_same("var x = new String(); if (x != null) throw 'a';"); test_same("var x = new Boolean();\nif (x != null) throw 'a';"); @@ -2026,7 +2010,7 @@ mod test { test("!a ? b : c", "a ? c : b"); // test("/* @__PURE__ */ a() ? b : b", "b"); test("a ? b : b", "a, b"); - test("a ? true : false", "!!a"); + test("a ? true : false", "a"); test("a ? false : true", "!a"); test("a ? a : b", "a || b"); test("a ? b : a", "a && b"); @@ -2039,8 +2023,30 @@ mod test { test("var a; a ? b(c, d) : b(e, d)", "var a; b(a ? c : e, d)"); test("var a; a ? b(...c) : b(...e)", "var a; b(...a ? c : e)"); test("var a; a ? b(c) : b(e)", "var a; b(a ? c : e)"); - test("a != null ? a : b", "a ?? b"); - test_same("a() != null ? a() : b"); + // test("a != null ? a : b", "a ?? b"); + test("a() != null ? a() : b", "a() == null ? b : a()"); // test("a != null ? a.b.c[d](e) : undefined", "a?.b.c[d](e)"); } + + #[test] + fn test_try_fold_in_boolean_context() { + test("if (!!a);", "if (a);"); + test("while (!!a);", "for (;a;);"); + test("do; while (!!a);", "do; while (a);"); + test("for (;!!a;);", "for (;a;);"); + test("!!a ? b : c", "a ? b : c"); + test("if (!!!a);", "if (!a);"); + // test("Boolean(!!a)", "Boolean()"); + test("if ((a | b) !== 0);", "if (a | b);"); + test("if ((a | b) === 0);", "if (!(a | b));"); + test("if (!!a && !!b);", "if (a && b);"); + test("if (!!a || !!b);", "if (a || b);"); + test("if (anything || (0, false));", "if (anything);"); + test("if (a ? !!b : !!c);", "if (a ? b : c);"); + test("if (anything1 ? (0, true) : anything2);", "if (anything1 || anything2);"); + test("if (anything1 ? (0, false) : anything2);", "if (!anything1 || anything2);"); + test("if (anything1 ? anything2 : (0, true));", "if (!anything1 || anything2);"); + test("if (anything1 ? anything2 : (0, false));", "if (anything1 && anything2);"); + test_same("if(!![]);"); // removed by constant folding + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index cd52c80b9ca8ed..da530171efb24a 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.70 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js +72.14 kB | 23.71 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 59.77 kB | 59.82 kB | 19.39 kB | 19.33 kB | moment.js +173.90 kB | 59.77 kB | 59.82 kB | 19.40 kB | 19.33 kB | moment.js -287.63 kB | 90.02 kB | 90.07 kB | 32.01 kB | 31.95 kB | jquery.js +287.63 kB | 90.03 kB | 90.07 kB | 32.02 kB | 31.95 kB | jquery.js -342.15 kB | 118.08 kB | 118.14 kB | 44.43 kB | 44.37 kB | vue.js +342.15 kB | 118.10 kB | 118.14 kB | 44.43 kB | 44.37 kB | vue.js -544.10 kB | 71.72 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js +544.10 kB | 71.74 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js -555.77 kB | 272.89 kB | 270.13 kB | 90.85 kB | 90.80 kB | d3.js +555.77 kB | 272.92 kB | 270.13 kB | 90.86 kB | 90.80 kB | d3.js -1.01 MB | 460.13 kB | 458.89 kB | 126.74 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.13 kB | 458.89 kB | 126.75 kB | 126.71 kB | bundle.min.js -1.25 MB | 652.81 kB | 646.76 kB | 163.50 kB | 163.73 kB | three.js +1.25 MB | 652.85 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js -2.14 MB | 725.99 kB | 724.14 kB | 180.05 kB | 181.07 kB | victory.js +2.14 MB | 726.02 kB | 724.14 kB | 180.01 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.68 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.67 kB | 331.56 kB | echarts.js 6.69 MB | 2.32 MB | 2.31 MB | 492.60 kB | 488.28 kB | antd.js -10.95 MB | 3.49 MB | 3.49 MB | 907.25 kB | 915.50 kB | typescript.js +10.95 MB | 3.49 MB | 3.49 MB | 907.27 kB | 915.50 kB | typescript.js