Skip to content

Commit

Permalink
feat(minifier): port esbuild conditional expr minification
Browse files Browse the repository at this point in the history
  • Loading branch information
camc314 committed Jan 8, 2025
1 parent 127772d commit 22bcc3a
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 47 deletions.
303 changes: 267 additions & 36 deletions crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Expression<'a>> {
// `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`
Expand All @@ -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),
Expand Down Expand Up @@ -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), _) =
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)");
}
}
22 changes: 11 additions & 11 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 22bcc3a

Please sign in to comment.