From 1bb2539d64847a79d0e394841bd6265b8cb53ffe Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Thu, 23 Jan 2025 07:03:42 +0000 Subject: [PATCH] refactor(minifier): move more code into `minimize_conditions` local loop (#8671) --- .../src/peephole/minimize_conditions.rs | 486 +++++++++++++++++- crates/oxc_minifier/src/peephole/mod.rs | 21 + .../peephole/substitute_alternate_syntax.rs | 442 +--------------- 3 files changed, 482 insertions(+), 467 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index 0b7d1cfe6e8fa..3da8da7b67977 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -1,8 +1,12 @@ use oxc_allocator::Vec; use oxc_ast::{ast::*, NONE}; -use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType}; +use oxc_ecmascript::{ + constant_evaluation::{ConstantEvaluation, ValueType}, + ToInt32, +}; use oxc_semantic::ReferenceFlags; use oxc_span::{cmp::ContentEq, GetSpan}; +use oxc_syntax::es_target::ESTarget; use oxc_traverse::{Ancestor, MaybeBoundIdentifier, TraverseCtx}; use crate::ctx::Ctx; @@ -60,7 +64,7 @@ impl<'a> PeepholeOptimizations { }; if let Some(expr) = expr { - Self::try_fold_expr_in_boolean_context(expr, Ctx(ctx)); + Self::try_fold_expr_in_boolean_context(expr, ctx); } if let Some(folded_stmt) = match stmt { @@ -81,15 +85,33 @@ impl<'a> PeepholeOptimizations { let mut changed = false; loop { let mut local_change = false; - if let Expression::ConditionalExpression(logical_expr) = expr { - if Self::try_fold_expr_in_boolean_context(&mut logical_expr.test, Ctx(ctx)) { - local_change = true; + if let Some(folded_expr) = match expr { + Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx), + Expression::BinaryExpression(e) => Self::try_minimize_binary(e, ctx), + Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx) + .or_else(|| { + self.try_compress_logical_expression_to_assignment_expression(e, ctx) + }), + Expression::ConditionalExpression(logical_expr) => { + if Self::try_fold_expr_in_boolean_context(&mut logical_expr.test, ctx) { + local_change = true; + } + Self::try_minimize_conditional(logical_expr, ctx) } - if let Some(e) = Self::try_minimize_conditional(logical_expr, ctx) { - *expr = e; - local_change = true; + Expression::AssignmentExpression(e) => { + if self.try_compress_normal_assignment_to_combined_logical_assignment(e, ctx) { + local_change = true; + } + if Self::try_compress_normal_assignment_to_combined_assignment(e, ctx) { + local_change = true; + } + Self::try_compress_assignment_to_update_expression(e, ctx) } - } + _ => None, + } { + *expr = folded_expr; + local_change = true; + }; if local_change { changed = true; } else { @@ -99,15 +121,12 @@ impl<'a> PeepholeOptimizations { if changed { self.mark_current_function_as_changed(); } + } - if let Some(folded_expr) = match expr { - Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx), - Expression::BinaryExpression(e) => Self::try_minimize_binary(e, ctx), - _ => None, - } { - *expr = folded_expr; - self.mark_current_function_as_changed(); - }; + fn minimize_not(span: Span, expr: Expression<'a>, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + let mut unary = ctx.ast.unary_expression(span, UnaryOperator::LogicalNot, expr); + Self::try_minimize_not(&mut unary, ctx) + .unwrap_or_else(|| Expression::UnaryExpression(ctx.ast.alloc(unary))) } fn try_minimize_not( @@ -716,7 +735,10 @@ impl<'a> PeepholeOptimizations { /// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`. /// /// <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2059> - fn try_fold_expr_in_boolean_context(expr: &mut Expression<'a>, ctx: Ctx<'a, '_>) -> bool { + fn try_fold_expr_in_boolean_context( + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> bool { match expr { // "!!a" => "a" Expression::UnaryExpression(u1) if u1.operator.is_not() => { @@ -752,7 +774,7 @@ impl<'a> PeepholeOptimizations { 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) { + if Ctx(ctx).get_side_free_boolean_value(&e.right) == Some(true) { *expr = ctx.ast.move_expression(&mut e.left); return true; } @@ -762,7 +784,7 @@ impl<'a> PeepholeOptimizations { 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) { + if Ctx(ctx).get_side_free_boolean_value(&e.right) == Some(false) { *expr = ctx.ast.move_expression(&mut e.left); return true; } @@ -771,7 +793,7 @@ impl<'a> PeepholeOptimizations { // "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) { + if let Some(boolean) = Ctx(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 { @@ -780,20 +802,18 @@ impl<'a> PeepholeOptimizations { 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); + let left = Self::minimize_not(left.span(), left, ctx); *expr = ctx.ast.expression_logical(e.span(), left, LogicalOperator::And, right); } return true; } - if let Some(boolean) = ctx.get_side_free_boolean_value(&e.alternate) { + if let Some(boolean) = Ctx(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); + let left = Self::minimize_not(left.span(), left, ctx); *expr = ctx.ast.expression_logical(e.span(), left, LogicalOperator::Or, right); } else { @@ -862,12 +882,328 @@ impl<'a> PeepholeOptimizations { _ => None, } } + + /// Compress `foo === null || foo === undefined` into `foo == null`. + /// + /// `foo === null || foo === undefined` => `foo == null` + /// `foo !== null && foo !== undefined` => `foo != null` + /// + /// Also supports `(a = foo.bar) === null || a === undefined` which commonly happens when + /// optional chaining is lowered. (`(a=foo.bar)==null`) + /// + /// This compression assumes that `document.all` is a normal object. + /// If that assumption does not hold, this compression is not allowed. + /// - `document.all === null || document.all === undefined` is `false` + /// - `document.all == null` is `true` + fn try_compress_is_null_or_undefined( + expr: &mut LogicalExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<Expression<'a>> { + let op = expr.operator; + let target_ops = match op { + LogicalOperator::Or => (BinaryOperator::StrictEquality, BinaryOperator::Equality), + LogicalOperator::And => (BinaryOperator::StrictInequality, BinaryOperator::Inequality), + LogicalOperator::Coalesce => return None, + }; + if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right( + &mut expr.left, + &mut expr.right, + expr.span, + target_ops, + ctx, + ) { + return Some(new_expr); + } + let Expression::LogicalExpression(left) = &mut expr.left else { + return None; + }; + if left.operator != op { + return None; + } + let new_span = Span::new(left.right.span().start, expr.span.end); + Self::try_compress_is_null_or_undefined_for_left_and_right( + &mut left.right, + &mut expr.right, + new_span, + target_ops, + ctx, + ) + .map(|new_expr| { + ctx.ast.expression_logical( + expr.span, + ctx.ast.move_expression(&mut left.left), + expr.operator, + new_expr, + ) + }) + } + + fn try_compress_is_null_or_undefined_for_left_and_right( + left: &mut Expression<'a>, + right: &mut Expression<'a>, + span: Span, + (find_op, replace_op): (BinaryOperator, BinaryOperator), + ctx: &mut TraverseCtx<'a>, + ) -> Option<Expression<'a>> { + enum LeftPairValueResult { + Null(Span), + Undefined, + } + + let ( + Expression::BinaryExpression(left_binary_expr), + Expression::BinaryExpression(right_binary_expr), + ) = (left, right) + else { + return None; + }; + if left_binary_expr.operator != find_op || right_binary_expr.operator != find_op { + return None; + } + + let is_null_or_undefined = |a: &Expression| { + if a.is_null() { + Some(LeftPairValueResult::Null(a.span())) + } else if a.evaluate_to_undefined() { + Some(LeftPairValueResult::Undefined) + } else { + None + } + }; + let is_id_or_assign_to_id = |b: &Expression<'a>| match b { + Expression::Identifier(id) => Some(id.name), + Expression::AssignmentExpression(assign_expr) => { + if assign_expr.operator == AssignmentOperator::Assign { + if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign_expr.left { + return Some(id.name); + } + } + None + } + _ => None, + }; + let (left_value, (left_non_value_expr, left_id_name)) = { + let left_value; + let left_non_value; + if let Some(v) = is_null_or_undefined(&left_binary_expr.left) { + left_value = v; + let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.right)?; + left_non_value = (&mut left_binary_expr.right, left_non_value_id); + } else { + left_value = is_null_or_undefined(&left_binary_expr.right)?; + let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.left)?; + left_non_value = (&mut left_binary_expr.left, left_non_value_id); + } + (left_value, left_non_value) + }; + + let (right_value, right_id) = Self::commutative_pair( + (&right_binary_expr.left, &right_binary_expr.right), + |a| match left_value { + LeftPairValueResult::Null(_) => a.evaluate_to_undefined().then_some(None), + LeftPairValueResult::Undefined => a.is_null().then_some(Some(a.span())), + }, + |b| { + if let Expression::Identifier(id) = b { + Some(id) + } else { + None + } + }, + )?; + + if left_id_name != right_id.name { + return None; + } + + let null_expr_span = match left_value { + LeftPairValueResult::Null(span) => span, + LeftPairValueResult::Undefined => right_value.unwrap(), + }; + Some(ctx.ast.expression_binary( + span, + ctx.ast.move_expression(left_non_value_expr), + replace_op, + ctx.ast.expression_null_literal(null_expr_span), + )) + } + + /// Compress `a = a || b` to `a ||= b` + /// + /// This can only be done for resolved identifiers as this would avoid setting `a` when `a` is truthy. + fn try_compress_normal_assignment_to_combined_logical_assignment( + &mut self, + expr: &mut AssignmentExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> bool { + if self.target < ESTarget::ES2020 { + return false; + } + if !matches!(expr.operator, AssignmentOperator::Assign) { + return false; + } + + let Expression::LogicalExpression(logical_expr) = &mut expr.right else { return false }; + let new_op = logical_expr.operator.to_assignment_operator(); + + let ( + AssignmentTarget::AssignmentTargetIdentifier(write_id_ref), + Expression::Identifier(read_id_ref), + ) = (&expr.left, &logical_expr.left) + else { + return false; + }; + // It should also early return when the reference might refer to a reference value created by a with statement + // when the minifier supports with statements + if write_id_ref.name != read_id_ref.name || Ctx(ctx).is_global_reference(write_id_ref) { + return false; + } + + expr.operator = new_op; + expr.right = ctx.ast.move_expression(&mut logical_expr.right); + true + } + + /// Compress `a || (a = b)` to `a ||= b` + fn try_compress_logical_expression_to_assignment_expression( + &self, + expr: &mut LogicalExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<Expression<'a>> { + if self.target < ESTarget::ES2020 { + return None; + } + + let Expression::AssignmentExpression(assignment_expr) = &mut expr.right else { + return None; + }; + if assignment_expr.operator != AssignmentOperator::Assign { + return None; + } + + let new_op = expr.operator.to_assignment_operator(); + + if !Self::has_no_side_effect_for_evaluation_same_target( + &assignment_expr.left, + &expr.left, + ctx, + ) { + return None; + } + + assignment_expr.span = expr.span; + assignment_expr.operator = new_op; + Some(ctx.ast.move_expression(&mut expr.right)) + } + + /// Compress `a = a + b` to `a += b` + fn try_compress_normal_assignment_to_combined_assignment( + expr: &mut AssignmentExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> bool { + if !matches!(expr.operator, AssignmentOperator::Assign) { + return false; + } + + let Expression::BinaryExpression(binary_expr) = &mut expr.right else { return false }; + let Some(new_op) = binary_expr.operator.to_assignment_operator() else { return false }; + + if !Self::has_no_side_effect_for_evaluation_same_target(&expr.left, &binary_expr.left, ctx) + { + return false; + } + + expr.operator = new_op; + expr.right = ctx.ast.move_expression(&mut binary_expr.right); + true + } + + /// Returns `true` if the assignment target and expression have no side effect for *evaluation* and points to the same reference. + /// + /// Evaluation here means `Evaluation` in the spec. + /// <https://tc39.es/ecma262/multipage/syntax-directed-operations.html#sec-evaluation> + /// + /// Matches the following cases: + /// + /// - `a`, `a` + /// - `a.b`, `a.b` + /// - `a.#b`, `a.#b` + fn has_no_side_effect_for_evaluation_same_target( + assignment_target: &AssignmentTarget, + expr: &Expression, + ctx: &mut TraverseCtx<'a>, + ) -> bool { + match (&assignment_target, &expr) { + ( + AssignmentTarget::AssignmentTargetIdentifier(write_id_ref), + Expression::Identifier(read_id_ref), + ) => write_id_ref.name == read_id_ref.name, + ( + AssignmentTarget::StaticMemberExpression(_), + Expression::StaticMemberExpression(_), + ) + | ( + AssignmentTarget::PrivateFieldExpression(_), + Expression::PrivateFieldExpression(_), + ) => { + let write_expr = assignment_target.to_member_expression(); + let read_expr = expr.to_member_expression(); + let Expression::Identifier(write_expr_object_id) = &write_expr.object() else { + return false; + }; + // It should also return false when the reference might refer to a reference value created by a with statement + // when the minifier supports with statements + !Ctx(ctx).is_global_reference(write_expr_object_id) + && write_expr.content_eq(read_expr) + } + _ => false, + } + } + + /// Compress `a = a + b` to `a += b` + fn try_compress_assignment_to_update_expression( + expr: &mut AssignmentExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option<Expression<'a>> { + let target = expr.left.as_simple_assignment_target_mut()?; + if !matches!(expr.operator, AssignmentOperator::Subtraction) { + return None; + } + match &expr.right { + Expression::NumericLiteral(num) if num.value.to_int_32() == 1 => { + // The `_` will not be placed to the target code. + let target = std::mem::replace( + target, + ctx.ast.simple_assignment_target_identifier_reference(target.span(), "_"), + ); + Some(ctx.ast.expression_update(expr.span, UpdateOperator::Decrement, true, target)) + } + Expression::UnaryExpression(un) + if matches!(un.operator, UnaryOperator::UnaryNegation) => + { + let Expression::NumericLiteral(num) = &un.argument else { return None }; + (num.value.to_int_32() == 1).then(|| { + // The `_` will not be placed to the target code. + let target = std::mem::replace( + target, + ctx.ast.simple_assignment_target_identifier_reference(target.span(), "_"), + ); + ctx.ast.expression_update(expr.span, UpdateOperator::Increment, true, target) + }) + } + _ => None, + } + } } /// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java> #[cfg(test)] mod test { - use crate::tester::{test, test_same}; + use crate::{ + tester::{run, test, test_same}, + CompressOptions, + }; + use oxc_syntax::es_target::ESTarget; /** Check that removing blocks with 1 child works */ #[test] @@ -2083,4 +2419,100 @@ mod test { test_same("x ? a += 0 : a += 1"); test_same("x ? a &&= 0 : a &&= 1"); } + + #[test] + fn test_fold_is_null_or_undefined() { + test("foo === null || foo === undefined", "foo == null"); + test("foo === undefined || foo === null", "foo == null"); + test("foo === null || foo === void 0", "foo == null"); + test("foo === null || foo === void 0 || foo === 1", "foo == null || foo === 1"); + test("foo === 1 || foo === null || foo === void 0", "foo === 1 || foo == null"); + test_same("foo === void 0 || bar === null"); + test_same("foo !== 1 && foo === void 0 || foo === null"); + test_same("foo.a === void 0 || foo.a === null"); // cannot be folded because accessing foo.a might have a side effect + + test("foo !== null && foo !== undefined", "foo != null"); + test("foo !== undefined && foo !== null", "foo != null"); + test("foo !== null && foo !== void 0", "foo != null"); + test("foo !== null && foo !== void 0 && foo !== 1", "foo != null && foo !== 1"); + test("foo !== 1 && foo !== null && foo !== void 0", "foo !== 1 && foo != null"); + test("foo !== 1 || foo !== void 0 && foo !== null", "foo !== 1 || foo != null"); + test_same("foo !== void 0 && bar !== null"); + + test("(_foo = foo) === null || _foo === undefined", "(_foo = foo) == null"); + test("(_foo = foo) === null || _foo === void 0", "(_foo = foo) == null"); + test("(_foo = foo.bar) === null || _foo === undefined", "(_foo = foo.bar) == null"); + test("(_foo = foo) !== null && _foo !== undefined", "(_foo = foo) != null"); + test("(_foo = foo) === undefined || _foo === null", "(_foo = foo) == null"); + test("(_foo = foo) === void 0 || _foo === null", "(_foo = foo) == null"); + test( + "(_foo = foo) === null || _foo === void 0 || _foo === 1", + "(_foo = foo) == null || _foo === 1", + ); + test( + "_foo === 1 || (_foo = foo) === null || _foo === void 0", + "_foo === 1 || (_foo = foo) == null", + ); + test_same("(_foo = foo) === void 0 || bar === null"); + } + + #[test] + fn test_fold_logical_expression_to_assignment_expression() { + test("x || (x = 3)", "x ||= 3"); + test("x && (x = 3)", "x &&= 3"); + test("x ?? (x = 3)", "x ??= 3"); + test("x || (x = g())", "x ||= g()"); + test("x && (x = g())", "x &&= g()"); + test("x ?? (x = g())", "x ??= g()"); + + // `||=`, `&&=`, `??=` sets the name property of the function + // Example case: `let f = false; f || (f = () => {}); console.log(f.name)` + test("x || (x = () => 'a')", "x ||= () => 'a'"); + + test_same("x || (y = 3)"); + + // GetValue(x) has no sideeffect when x is a resolved identifier + test("var x; x.y || (x.y = 3)", "var x; x.y ||= 3"); + test("var x; x.#y || (x.#y = 3)", "var x; x.#y ||= 3"); + test_same("x.y || (x.y = 3)"); + // this can be compressed if `y` does not have side effect + test_same("var x; x[y] || (x[y] = 3)"); + // GetValue(x) has a side effect in this case + // Example case: `var a = { get b() { console.log('b'); return { get c() { console.log('c') } } } }; a.b.c || (a.b.c = 1)` + test_same("var x; x.y.z || (x.y.z = 3)"); + // This case is not supported, since the minifier does not support with statements + // test_same("var x; with (z) { x.y || (x.y = 3) }"); + + // foo() might have a side effect + test_same("foo().a || (foo().a = 3)"); + + let target = ESTarget::ES2019; + let code = "x || (x = 3)"; + assert_eq!( + run(code, Some(CompressOptions { target, ..CompressOptions::default() })), + run(code, None) + ); + } + + #[test] + fn test_compress_normal_assignment_to_combined_logical_assignment() { + test("var x; x = x || 1", "var x; x ||= 1"); + test("var x; x = x && 1", "var x; x &&= 1"); + test("var x; x = x ?? 1", "var x; x ??= 1"); + + // `x` is a global reference and might have a setter + // Example case: `Object.defineProperty(globalThis, 'x', { get: () => true, set: () => console.log('x') }); x = x || 1` + test_same("x = x || 1"); + // setting x.y might have a side effect + test_same("var x; x.y = x.y || 1"); + // This case is not supported, since the minifier does not support with statements + // test_same("var x; with (z) { x = x || 1 }"); + + let target = ESTarget::ES2019; + let code = "var x; x = x || 1"; + assert_eq!( + run(code, Some(CompressOptions { target, ..CompressOptions::default() })), + run(code, None) + ); + } } diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 9868e99ec03d3..303d65b0e120f 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -94,6 +94,27 @@ impl<'a> PeepholeOptimizations { self.functions_changed.insert(scope_id); } } + + pub fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>( + pair: (&'x A, &'x A), + check_a: F, + check_b: G, + ) -> Option<(RetF, RetG)> + where + F: Fn(&'x A) -> Option<RetF>, + G: Fn(&'x A) -> Option<RetG>, + { + if let Some(a) = check_a(pair.0) { + if let Some(b) = check_b(pair.1) { + return Some((a, b)); + } + } else if let Some(a) = check_a(pair.1) { + if let Some(b) = check_b(pair.0) { + return Some((a, b)); + } + } + None + } } impl<'a> Traverse<'a> for PeepholeOptimizations { diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 2726d9d97bfe2..7ad18b1a26c61 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -3,9 +3,8 @@ use oxc_ast::{ast::*, NONE}; use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ValueType}, side_effects::MayHaveSideEffects, - ToInt32, ToJsString, ToNumber, + ToJsString, ToNumber, }; -use oxc_span::cmp::ContentEq; use oxc_span::GetSpan; use oxc_span::SPAN; use oxc_syntax::{ @@ -111,21 +110,12 @@ impl<'a> PeepholeOptimizations { Expression::ArrowFunctionExpression(e) => self.try_compress_arrow_expression(e, ctx), Expression::ChainExpression(e) => self.try_compress_chain_call_expression(e, ctx), Expression::BinaryExpression(e) => Self::swap_binary_expressions(e), - Expression::AssignmentExpression(e) => { - self.try_compress_normal_assignment_to_combined_assignment(e, ctx); - self.try_compress_normal_assignment_to_combined_logical_assignment(e, ctx); - } _ => {} } // Fold if let Some(folded_expr) = match expr { - Expression::AssignmentExpression(e) => { - Self::try_compress_assignment_to_update_expression(e, ctx) - } - Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx) - .or_else(|| Self::try_compress_is_object_and_not_null(e, ctx)) - .or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx)) + Expression::LogicalExpression(e) => Self::try_compress_is_object_and_not_null(e, ctx) .or_else(|| Self::try_rotate_logical_expression(e, ctx)), Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx), Expression::BinaryExpression(e) => Self::try_fold_loose_equals_undefined(e, ctx) @@ -229,183 +219,6 @@ impl<'a> PeepholeOptimizations { Some(ctx.ast.expression_binary(expr.span, left, new_comp_op, right)) } - /// Compress `foo === null || foo === undefined` into `foo == null`. - /// - /// `foo === null || foo === undefined` => `foo == null` - /// `foo !== null && foo !== undefined` => `foo != null` - /// - /// Also supports `(a = foo.bar) === null || a === undefined` which commonly happens when - /// optional chaining is lowered. (`(a=foo.bar)==null`) - /// - /// This compression assumes that `document.all` is a normal object. - /// If that assumption does not hold, this compression is not allowed. - /// - `document.all === null || document.all === undefined` is `false` - /// - `document.all == null` is `true` - fn try_compress_is_null_or_undefined( - expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option<Expression<'a>> { - let op = expr.operator; - let target_ops = match op { - LogicalOperator::Or => (BinaryOperator::StrictEquality, BinaryOperator::Equality), - LogicalOperator::And => (BinaryOperator::StrictInequality, BinaryOperator::Inequality), - LogicalOperator::Coalesce => return None, - }; - if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right( - &mut expr.left, - &mut expr.right, - expr.span, - target_ops, - ctx, - ) { - return Some(new_expr); - } - let Expression::LogicalExpression(left) = &mut expr.left else { - return None; - }; - if left.operator != op { - return None; - } - let new_span = Span::new(left.right.span().start, expr.span.end); - Self::try_compress_is_null_or_undefined_for_left_and_right( - &mut left.right, - &mut expr.right, - new_span, - target_ops, - ctx, - ) - .map(|new_expr| { - ctx.ast.expression_logical( - expr.span, - ctx.ast.move_expression(&mut left.left), - expr.operator, - new_expr, - ) - }) - } - - fn try_compress_is_null_or_undefined_for_left_and_right( - left: &mut Expression<'a>, - right: &mut Expression<'a>, - span: Span, - (find_op, replace_op): (BinaryOperator, BinaryOperator), - ctx: Ctx<'a, '_>, - ) -> Option<Expression<'a>> { - enum LeftPairValueResult { - Null(Span), - Undefined, - } - - let ( - Expression::BinaryExpression(left_binary_expr), - Expression::BinaryExpression(right_binary_expr), - ) = (left, right) - else { - return None; - }; - if left_binary_expr.operator != find_op || right_binary_expr.operator != find_op { - return None; - } - - let is_null_or_undefined = |a: &Expression| { - if a.is_null() { - Some(LeftPairValueResult::Null(a.span())) - } else if a.evaluate_to_undefined() { - Some(LeftPairValueResult::Undefined) - } else { - None - } - }; - let is_id_or_assign_to_id = |b: &Expression| match b { - Expression::Identifier(id) => Some(id.name.clone_in(ctx.ast.allocator)), - Expression::AssignmentExpression(assign_expr) => { - if assign_expr.operator == AssignmentOperator::Assign { - if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign_expr.left { - return Some(id.name.clone_in(ctx.ast.allocator)); - } - } - None - } - _ => None, - }; - let (left_value, (left_non_value_expr, left_id_name)) = { - let left_value; - let left_non_value; - if let Some(v) = is_null_or_undefined(&left_binary_expr.left) { - left_value = v; - let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.right)?; - left_non_value = (&mut left_binary_expr.right, left_non_value_id); - } else { - left_value = is_null_or_undefined(&left_binary_expr.right)?; - let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.left)?; - left_non_value = (&mut left_binary_expr.left, left_non_value_id); - } - (left_value, left_non_value) - }; - - let (right_value, right_id) = Self::commutative_pair( - (&right_binary_expr.left, &right_binary_expr.right), - |a| match left_value { - LeftPairValueResult::Null(_) => a.evaluate_to_undefined().then_some(None), - LeftPairValueResult::Undefined => a.is_null().then_some(Some(a.span())), - }, - |b| { - if let Expression::Identifier(id) = b { - Some(id) - } else { - None - } - }, - )?; - - if left_id_name != right_id.name { - return None; - } - - let null_expr_span = match left_value { - LeftPairValueResult::Null(span) => span, - LeftPairValueResult::Undefined => right_value.unwrap(), - }; - Some(ctx.ast.expression_binary( - span, - ctx.ast.move_expression(left_non_value_expr), - replace_op, - ctx.ast.expression_null_literal(null_expr_span), - )) - } - - /// Compress `a || (a = b)` to `a ||= b` - fn try_compress_logical_expression_to_assignment_expression( - &self, - expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option<Expression<'a>> { - if self.target < ESTarget::ES2020 { - return None; - } - - let Expression::AssignmentExpression(assignment_expr) = &mut expr.right else { - return None; - }; - if assignment_expr.operator != AssignmentOperator::Assign { - return None; - } - - let new_op = expr.operator.to_assignment_operator(); - - if !Self::has_no_side_effect_for_evaluation_same_target( - &assignment_expr.left, - &expr.left, - ctx, - ) { - return None; - } - - assignment_expr.span = expr.span; - assignment_expr.operator = new_op; - Some(ctx.ast.move_expression(&mut expr.right)) - } - /// `a || (b || c);` -> `(a || b) || c;` fn try_rotate_logical_expression( expr: &mut LogicalExpression<'a>, @@ -438,47 +251,6 @@ impl<'a> PeepholeOptimizations { )) } - /// Returns `true` if the assignment target and expression have no side effect for *evaluation* and points to the same reference. - /// - /// Evaluation here means `Evaluation` in the spec. - /// <https://tc39.es/ecma262/multipage/syntax-directed-operations.html#sec-evaluation> - /// - /// Matches the following cases: - /// - /// - `a`, `a` - /// - `a.b`, `a.b` - /// - `a.#b`, `a.#b` - fn has_no_side_effect_for_evaluation_same_target( - assignment_target: &AssignmentTarget, - expr: &Expression, - ctx: Ctx<'a, '_>, - ) -> bool { - match (&assignment_target, &expr) { - ( - AssignmentTarget::AssignmentTargetIdentifier(write_id_ref), - Expression::Identifier(read_id_ref), - ) => write_id_ref.name == read_id_ref.name, - ( - AssignmentTarget::StaticMemberExpression(_), - Expression::StaticMemberExpression(_), - ) - | ( - AssignmentTarget::PrivateFieldExpression(_), - Expression::PrivateFieldExpression(_), - ) => { - let write_expr = assignment_target.to_member_expression(); - let read_expr = expr.to_member_expression(); - let Expression::Identifier(write_expr_object_id) = &write_expr.object() else { - return false; - }; - // It should also return false when the reference might refer to a reference value created by a with statement - // when the minifier supports with statements - !ctx.is_global_reference(write_expr_object_id) && write_expr.content_eq(read_expr) - } - _ => false, - } - } - /// Compress `typeof foo === 'object' && foo !== null` into `typeof foo == 'object' && !!foo`. /// /// - `typeof foo === 'object' && foo !== null` => `typeof foo == 'object' && !!foo` @@ -644,27 +416,6 @@ impl<'a> PeepholeOptimizations { )) } - fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>( - pair: (&'x A, &'x A), - check_a: F, - check_b: G, - ) -> Option<(RetF, RetG)> - where - F: Fn(&'x A) -> Option<RetF>, - G: Fn(&'x A) -> Option<RetG>, - { - if let Some(a) = check_a(pair.0) { - if let Some(b) = check_b(pair.1) { - return Some((a, b)); - } - } else if let Some(a) = check_a(pair.1) { - if let Some(b) = check_b(pair.0) { - return Some((a, b)); - } - } - None - } - fn try_fold_loose_equals_undefined( e: &mut BinaryExpression<'a>, ctx: Ctx<'a, '_>, @@ -738,99 +489,6 @@ impl<'a> PeepholeOptimizations { } } - /// Compress `a = a + b` to `a += b` - fn try_compress_normal_assignment_to_combined_assignment( - &mut self, - expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, '_>, - ) { - if !matches!(expr.operator, AssignmentOperator::Assign) { - return; - } - - let Expression::BinaryExpression(binary_expr) = &mut expr.right else { return }; - let Some(new_op) = binary_expr.operator.to_assignment_operator() else { return }; - - if !Self::has_no_side_effect_for_evaluation_same_target(&expr.left, &binary_expr.left, ctx) - { - return; - } - - expr.operator = new_op; - expr.right = ctx.ast.move_expression(&mut binary_expr.right); - self.mark_current_function_as_changed(); - } - - /// Compress `a = a || b` to `a ||= b` - /// - /// This can only be done for resolved identifiers as this would avoid setting `a` when `a` is truthy. - fn try_compress_normal_assignment_to_combined_logical_assignment( - &mut self, - expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, '_>, - ) { - if self.target < ESTarget::ES2020 { - return; - } - if !matches!(expr.operator, AssignmentOperator::Assign) { - return; - } - - let Expression::LogicalExpression(logical_expr) = &mut expr.right else { return }; - let new_op = logical_expr.operator.to_assignment_operator(); - - let ( - AssignmentTarget::AssignmentTargetIdentifier(write_id_ref), - Expression::Identifier(read_id_ref), - ) = (&expr.left, &logical_expr.left) - else { - return; - }; - // It should also early return when the reference might refer to a reference value created by a with statement - // when the minifier supports with statements - if write_id_ref.name != read_id_ref.name || ctx.is_global_reference(write_id_ref) { - return; - } - - expr.operator = new_op; - expr.right = ctx.ast.move_expression(&mut logical_expr.right); - self.mark_current_function_as_changed(); - } - - fn try_compress_assignment_to_update_expression( - expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option<Expression<'a>> { - let target = expr.left.as_simple_assignment_target_mut()?; - if !matches!(expr.operator, AssignmentOperator::Subtraction) { - return None; - } - match &expr.right { - Expression::NumericLiteral(num) if num.value.to_int_32() == 1 => { - // The `_` will not be placed to the target code. - let target = std::mem::replace( - target, - ctx.ast.simple_assignment_target_identifier_reference(target.span(), "_"), - ); - Some(ctx.ast.expression_update(expr.span, UpdateOperator::Decrement, true, target)) - } - Expression::UnaryExpression(un) - if matches!(un.operator, UnaryOperator::UnaryNegation) => - { - let Expression::NumericLiteral(num) = &un.argument else { return None }; - (num.value.to_int_32() == 1).then(|| { - // The `_` will not be placed to the target code. - let target = std::mem::replace( - target, - ctx.ast.simple_assignment_target_identifier_reference(target.span(), "_"), - ); - ctx.ast.expression_update(expr.span, UpdateOperator::Increment, true, target) - }) - } - _ => None, - } - } - /// Fold `Boolean`, `Number`, `String`, `BigInt` constructors. /// /// `Boolean(a)` -> `!!a` @@ -1443,28 +1101,6 @@ mod test { // test_same("var x; with (z) { x.y || (x.y = 3) }"); } - #[test] - fn test_compress_normal_assignment_to_combined_logical_assignment() { - test("var x; x = x || 1", "var x; x ||= 1"); - test("var x; x = x && 1", "var x; x &&= 1"); - test("var x; x = x ?? 1", "var x; x ??= 1"); - - // `x` is a global reference and might have a setter - // Example case: `Object.defineProperty(globalThis, 'x', { get: () => true, set: () => console.log('x') }); x = x || 1` - test_same("x = x || 1"); - // setting x.y might have a side effect - test_same("var x; x.y = x.y || 1"); - // This case is not supported, since the minifier does not support with statements - // test_same("var x; with (z) { x = x || 1 }"); - - let target = ESTarget::ES2019; - let code = "var x; x = x || 1"; - assert_eq!( - run(code, Some(CompressOptions { target, ..CompressOptions::default() })), - run(code, None) - ); - } - #[test] fn test_fold_subtraction_assignment() { test("x -= 1", "--x"); @@ -1868,42 +1504,6 @@ mod test { test("typeof x.y !== 'undefined'", "typeof x.y < 'u'"); } - #[test] - fn test_fold_is_null_or_undefined() { - test("foo === null || foo === undefined", "foo == null"); - test("foo === undefined || foo === null", "foo == null"); - test("foo === null || foo === void 0", "foo == null"); - test("foo === null || foo === void 0 || foo === 1", "foo == null || foo === 1"); - test("foo === 1 || foo === null || foo === void 0", "foo === 1 || foo == null"); - test_same("foo === void 0 || bar === null"); - test_same("foo !== 1 && foo === void 0 || foo === null"); - test_same("foo.a === void 0 || foo.a === null"); // cannot be folded because accessing foo.a might have a side effect - - test("foo !== null && foo !== undefined", "foo != null"); - test("foo !== undefined && foo !== null", "foo != null"); - test("foo !== null && foo !== void 0", "foo != null"); - test("foo !== null && foo !== void 0 && foo !== 1", "foo != null && foo !== 1"); - test("foo !== 1 && foo !== null && foo !== void 0", "foo !== 1 && foo != null"); - test("foo !== 1 || foo !== void 0 && foo !== null", "foo !== 1 || foo != null"); - test_same("foo !== void 0 && bar !== null"); - - test("(_foo = foo) === null || _foo === undefined", "(_foo = foo) == null"); - test("(_foo = foo) === null || _foo === void 0", "(_foo = foo) == null"); - test("(_foo = foo.bar) === null || _foo === undefined", "(_foo = foo.bar) == null"); - test("(_foo = foo) !== null && _foo !== undefined", "(_foo = foo) != null"); - test("(_foo = foo) === undefined || _foo === null", "(_foo = foo) == null"); - test("(_foo = foo) === void 0 || _foo === null", "(_foo = foo) == null"); - test( - "(_foo = foo) === null || _foo === void 0 || _foo === 1", - "(_foo = foo) == null || _foo === 1", - ); - test( - "_foo === 1 || (_foo = foo) === null || _foo === void 0", - "_foo === 1 || (_foo = foo) == null", - ); - test_same("(_foo = foo) === void 0 || bar === null"); - } - #[test] fn test_fold_is_object_and_not_null() { test( @@ -1953,44 +1553,6 @@ mod test { test_same("var foo; v = typeof foo == 'string' && foo !== null"); } - #[test] - fn test_fold_logical_expression_to_assignment_expression() { - test("x || (x = 3)", "x ||= 3"); - test("x && (x = 3)", "x &&= 3"); - test("x ?? (x = 3)", "x ??= 3"); - test("x || (x = g())", "x ||= g()"); - test("x && (x = g())", "x &&= g()"); - test("x ?? (x = g())", "x ??= g()"); - - // `||=`, `&&=`, `??=` sets the name property of the function - // Example case: `let f = false; f || (f = () => {}); console.log(f.name)` - test("x || (x = () => 'a')", "x ||= () => 'a'"); - - test_same("x || (y = 3)"); - - // GetValue(x) has no sideeffect when x is a resolved identifier - test("var x; x.y || (x.y = 3)", "var x; x.y ||= 3"); - test("var x; x.#y || (x.#y = 3)", "var x; x.#y ||= 3"); - test_same("x.y || (x.y = 3)"); - // this can be compressed if `y` does not have side effect - test_same("var x; x[y] || (x[y] = 3)"); - // GetValue(x) has a side effect in this case - // Example case: `var a = { get b() { console.log('b'); return { get c() { console.log('c') } } } }; a.b.c || (a.b.c = 1)` - test_same("var x; x.y.z || (x.y.z = 3)"); - // This case is not supported, since the minifier does not support with statements - // test_same("var x; with (z) { x.y || (x.y = 3) }"); - - // foo() might have a side effect - test_same("foo().a || (foo().a = 3)"); - - let target = ESTarget::ES2019; - let code = "x || (x = 3)"; - assert_eq!( - run(code, Some(CompressOptions { target, ..CompressOptions::default() })), - run(code, None) - ); - } - #[test] fn test_fold_loose_equals_undefined() { test_same("foo != null");