From 22bcc3a1969c1ceb63b0caae060509c47bf54ab5 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 8 Jan 2025 16:53:16 +0000 Subject: [PATCH] feat(minifier): port esbuild conditional expr minification --- .../peephole_minimize_conditions.rs | 303 +++++++++++++++--- tasks/minsize/minsize.snap | 22 +- 2 files changed, 278 insertions(+), 47 deletions(-) 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 48f98979b045da..3549d6920ba181 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,5 +1,5 @@ use oxc_allocator::Vec; -use oxc_ast::ast::*; +use oxc_ast::{ast::*, NONE}; use oxc_ecmascript::constant_evaluation::ValueType; use oxc_span::{cmp::ContentEq, GetSpan, SPAN}; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx}; @@ -342,41 +342,31 @@ impl<'a> PeepholeMinimizeConditions { None } + // based on https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/internal/js_ast/js_ast_helpers.go#L2745 fn try_minimize_conditional( expr: &mut ConditionalExpression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { - // `a ? a : b` -> `a || b` - if let (Expression::Identifier(test_ident), Expression::Identifier(consequent_ident)) = - (&expr.test, &expr.consequent) - { - if test_ident.name == consequent_ident.name { - let ident = ctx.ast.move_expression(&mut expr.test); - - return Some(ctx.ast.expression_logical( - expr.span, - ident, - LogicalOperator::Or, - ctx.ast.move_expression(&mut expr.alternate), - )); - } - } - - // `foo ? bar : foo` -> `foo && bar` - if let (Expression::Identifier(test_ident), Expression::Identifier(alternate_ident)) = - (&expr.test, &expr.alternate) - { - if test_ident.name == alternate_ident.name { - return Some(ctx.ast.expression_logical( - expr.span, + // `(a, b) ? c : d` -> `a, b ? c : d` + if let Expression::SequenceExpression(sequence_expr) = &expr.test { + if sequence_expr.expressions.len() > 1 { + let mut sequence = ctx.ast.move_expression(&mut expr.test); + let Expression::SequenceExpression(ref mut sequence_expr) = &mut sequence else { + unreachable!() + }; + let test = sequence_expr.expressions.pop().expect("sequence_expr.expressions"); + expr.test = test; + sequence_expr.expressions.push(ctx.ast.expression_conditional( + SPAN, ctx.ast.move_expression(&mut expr.test), - LogicalOperator::And, ctx.ast.move_expression(&mut expr.consequent), + ctx.ast.move_expression(&mut expr.alternate), )); + return Some(sequence); } } - // `!a ? b() : c()` -> `a ? c() : b()` + // `!a ? b : c` -> `a ? c : b` if let Expression::UnaryExpression(test_expr) = &mut expr.test { if test_expr.operator.is_not() // Skip `!!!a` @@ -391,8 +381,19 @@ impl<'a> PeepholeMinimizeConditions { } } - // `a ? false : true` -> `!a` + // TODO: `/* @__PURE__ */ a() ? b : b` -> `b` + + // `a ? b : b` -> `a, b` + if expr.alternate.content_eq(&expr.consequent) { + let expressions = ctx.ast.vec_from_array([ + ctx.ast.move_expression(&mut expr.test), + ctx.ast.move_expression(&mut expr.consequent), + ]); + return Some(ctx.ast.expression_sequence(expr.span, expressions)); + } + // `a ? true : false` -> `!!a` + // `a ? false : true` -> `!a` if let ( Expression::Identifier(_), Expression::BooleanLiteral(consequent_lit), @@ -420,6 +421,222 @@ impl<'a> PeepholeMinimizeConditions { } } + // `a ? a : b` -> `a || b` + if let (Expression::Identifier(test_ident), Expression::Identifier(consequent_ident)) = + (&expr.test, &expr.consequent) + { + if test_ident.name == consequent_ident.name { + let ident = ctx.ast.move_expression(&mut expr.test); + + return Some(ctx.ast.expression_logical( + expr.span, + ident, + LogicalOperator::Or, + ctx.ast.move_expression(&mut expr.alternate), + )); + } + } + // `a ? b : a` -> `a && b` + if let (Expression::Identifier(test_ident), Expression::Identifier(alternate_ident)) = + (&expr.test, &expr.alternate) + { + if test_ident.name == alternate_ident.name { + return Some(ctx.ast.expression_logical( + expr.span, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::And, + ctx.ast.move_expression(&mut expr.consequent), + )); + } + } + + // `a ? b ? c : d : d` -> `a && b ? c : d` + if let Expression::ConditionalExpression(consequent) = &mut expr.consequent { + if consequent.alternate.content_eq(&expr.alternate) { + return Some(ctx.ast.expression_conditional( + SPAN, + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::And, + ctx.ast.move_expression(&mut consequent.test), + ), + ctx.ast.move_expression(&mut consequent.consequent), + ctx.ast.move_expression(&mut consequent.alternate), + )); + } + } + + // `a ? b : c ? b : d` -> `a || c ? b : d` + if let Expression::ConditionalExpression(alternate) = &mut expr.alternate { + if alternate.consequent.content_eq(&expr.consequent) { + return Some(ctx.ast.expression_conditional( + SPAN, + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::Or, + ctx.ast.move_expression(&mut alternate.test), + ), + ctx.ast.move_expression(&mut expr.consequent), + ctx.ast.move_expression(&mut alternate.alternate), + )); + } + } + + // `a ? c : (b, c)` -> `(a || b), c` + if let Expression::SequenceExpression(alternate) = &mut expr.alternate { + if alternate.expressions.len() == 2 + && alternate.expressions[1].content_eq(&expr.consequent) + { + return Some(ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_array([ + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::Or, + ctx.ast.move_expression(&mut alternate.expressions[0]), + ), + ctx.ast.move_expression(&mut expr.consequent), + ]), + )); + } + } + + // `a ? (b, c) : c` -> `(a && b), c` + if let Expression::SequenceExpression(consequent) = &mut expr.consequent { + if consequent.expressions.len() == 2 + && consequent.expressions[1].content_eq(&expr.alternate) + { + return Some(ctx.ast.expression_sequence( + SPAN, + ctx.ast.vec_from_array([ + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::And, + ctx.ast.move_expression(&mut consequent.expressions[0]), + ), + ctx.ast.move_expression(&mut expr.alternate), + ]), + )); + } + } + + // `a ? b || c : c` => "(a && b) || c" + if let Expression::LogicalExpression(logical_expr) = &mut expr.consequent { + if logical_expr.operator == LogicalOperator::Or + && logical_expr.right.content_eq(&expr.alternate) + { + return Some(ctx.ast.expression_logical( + SPAN, + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::And, + ctx.ast.move_expression(&mut logical_expr.left), + ), + LogicalOperator::Or, + ctx.ast.move_expression(&mut expr.alternate), + )); + } + } + + // `a ? c : b && c` -> `(a || b) && c`` + if let Expression::LogicalExpression(logical_expr) = &mut expr.alternate { + if logical_expr.operator == LogicalOperator::And + && logical_expr.right.content_eq(&expr.consequent) + { + return Some(ctx.ast.expression_logical( + SPAN, + ctx.ast.expression_logical( + SPAN, + ctx.ast.move_expression(&mut expr.test), + LogicalOperator::Or, + ctx.ast.move_expression(&mut logical_expr.left), + ), + LogicalOperator::And, + ctx.ast.move_expression(&mut expr.consequent), + )); + } + } + + // `a ? b(c, d) : b(e, d)` -> `b(a ? c : e, d)`` + if let ( + Expression::Identifier(test), + Expression::CallExpression(consequent), + Expression::CallExpression(alternate), + ) = (&expr.test, &mut expr.consequent, &mut expr.alternate) + { + if consequent.callee.content_eq(&alternate.callee) + && consequent.arguments.len() == alternate.arguments.len() + && ctx.scopes().find_binding(ctx.current_scope_id(), &test.name).is_some() + && consequent + .arguments + .iter() + .zip(&alternate.arguments) + .skip(1) + .all(|(a, b)| a.content_eq(b)) + { + // `a ? b(...c) : b(...e)` -> `b(...a ? c : e)`` + if matches!(consequent.arguments[0], Argument::SpreadElement(_)) + && matches!(alternate.arguments[0], Argument::SpreadElement(_)) + { + let callee = ctx.ast.move_expression(&mut consequent.callee); + let consequent_first_arg = { + let Argument::SpreadElement(ref mut el) = &mut consequent.arguments[0] + else { + unreachable!() + }; + ctx.ast.move_expression(&mut el.argument) + }; + let alternate_first_arg = { + let Argument::SpreadElement(ref mut el) = &mut alternate.arguments[0] + else { + unreachable!() + }; + ctx.ast.move_expression(&mut el.argument) + }; + let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec()); + args[0] = ctx.ast.argument_spread_element( + SPAN, + ctx.ast.expression_conditional( + SPAN, + ctx.ast.move_expression(&mut expr.test), + consequent_first_arg, + alternate_first_arg, + ), + ); + + return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false)); + } + // `a ? b(c) : b(e)` -> `b(a ? c : e)`` + if !matches!(consequent.arguments[0], Argument::SpreadElement(_)) + && !matches!(alternate.arguments[0], Argument::SpreadElement(_)) + { + let callee = ctx.ast.move_expression(&mut consequent.callee); + + let consequent_first_arg = + ctx.ast.move_expression(consequent.arguments[0].to_expression_mut()); + let alternate_first_arg = + ctx.ast.move_expression(alternate.arguments[0].to_expression_mut()); + let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec()); + args[0] = Argument::from(ctx.ast.expression_conditional( + SPAN, + ctx.ast.move_expression(&mut expr.test), + consequent_first_arg, + alternate_first_arg, + )); + return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false)); + } + } + } + + // TODO: Try using the "??" or "?." operators + + // Non esbuild optimizations + // `x ? true : y` -> `x || y` // `x ? false : y` -> `!x && y` if let (Expression::Identifier(_), Expression::BooleanLiteral(consequent_lit), _) = @@ -474,15 +691,6 @@ impl<'a> PeepholeMinimizeConditions { )); } - // `foo() ? bar : bar` -> `foo(), bar` - if expr.alternate.content_eq(&expr.consequent) { - let expressions = ctx.ast.vec_from_array([ - ctx.ast.move_expression(&mut expr.test), - ctx.ast.move_expression(&mut expr.consequent), - ]); - return Some(ctx.ast.expression_sequence(expr.span, expressions)); - } - None } @@ -1775,4 +1983,27 @@ mod test { test("!!!!delete x.y", "delete x.y"); test("var k = !!(foo instanceof bar)", "var k = foo instanceof bar"); } + + #[test] + fn minimize_conditional_exprs_esbuild() { + test("(a, b) ? c : d", "a, b ? c : d"); + 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 ? false : true", "!a"); + test("a ? a : b", "a || b"); + test("a ? b : a", "a && b"); + test("a ? b ? c : d : d", "a && b ? c : d"); + test("a ? b : c ? b : d", "a || c ? b : d"); + test("a ? c : (b, c)", "(a || b), c"); + test("a ? (b, c) : c", "(a && b), c"); + test("a ? b || c : c", "(a && b) || c"); + test("a ? c : b && c", "(a || b) && c"); + 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("a != null ? a.b.c[d](e) : undefined", "a?.b.c[d](e)"); + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index c3a775666c230f..827137a019df6b 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.71 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js +72.14 kB | 23.70 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js 173.90 kB | 59.80 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js -287.63 kB | 90.14 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js +287.63 kB | 90.13 kB | 90.07 kB | 32.05 kB | 31.95 kB | jquery.js -342.15 kB | 118.38 kB | 118.14 kB | 44.53 kB | 44.37 kB | vue.js +342.15 kB | 118.36 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js -544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js +544.10 kB | 71.74 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js -555.77 kB | 273.20 kB | 270.13 kB | 90.93 kB | 90.80 kB | d3.js +555.77 kB | 273.19 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js -1.01 MB | 460.62 kB | 458.89 kB | 126.89 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.47 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js -1.25 MB | 653.02 kB | 646.76 kB | 163.55 kB | 163.73 kB | three.js +1.25 MB | 652.88 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js -2.14 MB | 726.50 kB | 724.14 kB | 180.19 kB | 181.07 kB | victory.js +2.14 MB | 726.28 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.93 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.75 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.68 kB | 488.28 kB | antd.js -10.95 MB | 3.50 MB | 3.49 MB | 909.08 kB | 915.50 kB | typescript.js +10.95 MB | 3.50 MB | 3.49 MB | 908.82 kB | 915.50 kB | typescript.js