From f7f2d2f93a595c0b5e1ae44e32fe8757331be08a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:31:12 +0000 Subject: [PATCH] feat(minifier): compress `a == null && b` to `a ?? b` when return value is ignored (#8749) Compress `a == null && b` to `a ?? b` when return value is ignored When `a` is `null` or `undefined`, `a == null && b` returns `b`. When `a` is not `null` or `undefined`, `a == null && b` returns `false`. When `a` is `null` or `undefined`, `a ?? b` returns `b`. When `a` is not `null` or `undefined`, `a ?? b` returns `a`. **References** - [Spec of `??`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-binary-logical-operators-runtime-semantics-evaluation) --- crates/oxc_minifier/src/peephole/mod.rs | 1 + .../peephole/substitute_alternate_syntax.rs | 65 +++++++++++++++++++ tasks/minsize/minsize.snap | 22 +++---- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index c3b2c68d5e5a7..6acd9d0fba024 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -151,6 +151,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { let ctx = Ctx(ctx); self.minimize_conditions_exit_statement(stmt, ctx); self.remove_dead_code_exit_statement(stmt, ctx); + self.substitute_exit_statement(stmt, ctx); } fn exit_return_statement(&mut self, stmt: &mut ReturnStatement<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 9eb34c0ed7509..6560157243238 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -844,6 +844,62 @@ impl<'a> PeepholeOptimizations { self.mark_current_function_as_changed(); } } + + pub fn substitute_exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: Ctx<'a, '_>) { + if let Statement::ExpressionStatement(expr_stmt) = stmt { + if let Some(folded_expr) = match &mut expr_stmt.expression { + Expression::LogicalExpression(expr) => { + self.try_compress_is_null_and_to_nullish_coalescing(expr, ctx) + } + _ => None, + } { + expr_stmt.expression = folded_expr; + self.mark_current_function_as_changed(); + } + } + } + + /// Compress `a == null && b` to `a ?? b` + /// + /// - `a == null && b` -> `a ?? b` + /// - `a != null || b` -> `a ?? b` + /// + /// This can be only done when the return value is not used. + /// For example when a = 1, `a == null && b` returns `false` while `a ?? b` returns `1`. + fn try_compress_is_null_and_to_nullish_coalescing( + &self, + expr: &mut LogicalExpression<'a>, + ctx: Ctx<'a, '_>, + ) -> Option> { + if self.target < ESTarget::ES2020 { + return None; + } + let target_op = match expr.operator { + LogicalOperator::And => BinaryOperator::Equality, + LogicalOperator::Or => BinaryOperator::Inequality, + LogicalOperator::Coalesce => return None, + }; + let Expression::BinaryExpression(binary_expr) = &mut expr.left else { + return None; + }; + if binary_expr.operator != target_op { + return None; + } + let new_left_hand_expr = if binary_expr.left.is_null() { + ctx.ast.move_expression(&mut binary_expr.right) + } else if binary_expr.right.is_null() { + ctx.ast.move_expression(&mut binary_expr.left) + } else { + return None; + }; + + Some(ctx.ast.expression_logical( + expr.span, + new_left_hand_expr, + LogicalOperator::Coalesce, + ctx.ast.move_expression(&mut expr.right), + )) + } } impl<'a> LatePeepholeOptimizations { @@ -1645,4 +1701,13 @@ mod test { run(code, None) ); } + + #[test] + fn test_compress_is_null_and_to_nullish_coalescing() { + test("x == null && y", "x ?? y"); + test("x != null || y", "x ?? y"); + test_same("v = x == null && y"); + test_same("v = x != null || y"); + test("void (x == null && y)", "x ?? y"); + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 4d8fbb3143d54..6f46510493f79 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.61 kB | 23.70 kB | 8.55 kB | 8.54 kB | react.development.js +72.14 kB | 23.57 kB | 23.70 kB | 8.55 kB | 8.54 kB | react.development.js -173.90 kB | 59.70 kB | 59.82 kB | 19.26 kB | 19.33 kB | moment.js +173.90 kB | 59.68 kB | 59.82 kB | 19.25 kB | 19.33 kB | moment.js -287.63 kB | 89.58 kB | 90.07 kB | 31.08 kB | 31.95 kB | jquery.js +287.63 kB | 89.54 kB | 90.07 kB | 31.07 kB | 31.95 kB | jquery.js -342.15 kB | 117.72 kB | 118.14 kB | 43.66 kB | 44.37 kB | vue.js +342.15 kB | 117.69 kB | 118.14 kB | 43.66 kB | 44.37 kB | vue.js -544.10 kB | 71.50 kB | 72.48 kB | 25.92 kB | 26.20 kB | lodash.js +544.10 kB | 71.49 kB | 72.48 kB | 25.92 kB | 26.20 kB | lodash.js -555.77 kB | 271.68 kB | 270.13 kB | 88.48 kB | 90.80 kB | d3.js +555.77 kB | 271.48 kB | 270.13 kB | 88.45 kB | 90.80 kB | d3.js -1.01 MB | 457.66 kB | 458.89 kB | 123.79 kB | 126.71 kB | bundle.min.js +1.01 MB | 457.63 kB | 458.89 kB | 123.79 kB | 126.71 kB | bundle.min.js -1.25 MB | 650.64 kB | 646.76 kB | 161.49 kB | 163.73 kB | three.js +1.25 MB | 650.59 kB | 646.76 kB | 161.49 kB | 163.73 kB | three.js -2.14 MB | 718.99 kB | 724.14 kB | 162.41 kB | 181.07 kB | victory.js +2.14 MB | 718.82 kB | 724.14 kB | 162.40 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 325.29 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 325.18 kB | 331.56 kB | echarts.js 6.69 MB | 2.30 MB | 2.31 MB | 469.99 kB | 488.28 kB | antd.js -10.95 MB | 3.37 MB | 3.49 MB | 866.60 kB | 915.50 kB | typescript.js +10.95 MB | 3.37 MB | 3.49 MB | 866.63 kB | 915.50 kB | typescript.js