mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
feat(minifier): improve negation of expressions in boolean context (#25755)
add `boolean_context` to `minimize_not` so negation folding can happen in-place when possible, reducing extra AST rebuilds and allocations and `!0` / `!1` can now collapse directly to `1` / `0` in boolean context
This commit is contained in:
1 parent
9b7e15362d
commit
83cb5163c0
10 files changed
+97
-75
No files matched your search
@@ -417,13 +417,13 @@ impl<'a> PeepholeOptimizations {
|
||||
) {
|
||||
(Some(true), Some(false)) => {
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
return Some(test);
|
||||
}
|
||||
(Some(false), Some(true)) => {
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
return Some(test);
|
||||
}
|
||||
// "c ? false : x" => "!c && x" (exact for any `c`)
|
||||
@@ -436,7 +436,7 @@ impl<'a> PeepholeOptimizations {
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
let right = expr.alternate.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
@@ -456,7 +456,7 @@ impl<'a> PeepholeOptimizations {
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
let right = expr.consequent.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
@@ -546,8 +546,8 @@ impl<'a> PeepholeOptimizations {
|
||||
// But skip if parens would be needed (e.g., "a+b?1:0" => "+!!(a+b)" is longer)
|
||||
if !needs_parens {
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
return Some(Expression::new_unary_expression(expr.span,
|
||||
UnaryOperator::UnaryPlus,
|
||||
test, ctx));
|
||||
@@ -559,7 +559,7 @@ impl<'a> PeepholeOptimizations {
|
||||
// The `0` must be `+0`: `a ? -0 : 1` would become `+!a`, yielding `+0`, not `-0`.
|
||||
if !consequent.is_sign_negative() && !Self::test_needs_parens(&expr.test) => {
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx, false);
|
||||
return Some(Expression::new_unary_expression(expr.span,
|
||||
UnaryOperator::UnaryPlus,
|
||||
test, ctx));
|
||||
|
||||
@@ -16,14 +16,12 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) {
|
||||
match expr {
|
||||
// "!!a" => "a"
|
||||
Expression::UnaryExpression(u1) if u1.operator.is_not() => {
|
||||
if let Expression::UnaryExpression(u2) = &mut u1.argument
|
||||
&& u2.operator.is_not()
|
||||
{
|
||||
let mut e = u2.argument.take_in(ctx);
|
||||
Self::minimize_expression_in_boolean_context(&mut e, ctx);
|
||||
ctx.replace_expression(expr, e);
|
||||
if Self::try_negate_expression(&mut u1.argument, ctx, true) {
|
||||
Self::minimize_expression_in_boolean_context(&mut u1.argument, ctx);
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
} else {
|
||||
Self::minimize_expression_in_boolean_context(&mut u1.argument, ctx);
|
||||
}
|
||||
}
|
||||
Expression::BinaryExpression(e)
|
||||
@@ -60,7 +58,7 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
}
|
||||
// "if (!!a ||!!b)" => "if (a || b)"
|
||||
Expression::LogicalExpression(e) if e.operator == LogicalOperator::Or => {
|
||||
Expression::LogicalExpression(e) if e.operator.is_or() => {
|
||||
Self::minimize_expression_in_boolean_context(&mut e.left, ctx);
|
||||
Self::minimize_expression_in_boolean_context(&mut e.right, ctx);
|
||||
// "if (anything || falsyNoSideEffects)" => "if (anything)"
|
||||
@@ -82,7 +80,7 @@ impl<'a> PeepholeOptimizations {
|
||||
(LogicalOperator::Or, left)
|
||||
} else {
|
||||
// "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 && anything2)"
|
||||
(LogicalOperator::And, Self::minimize_not(left.span(), left, ctx))
|
||||
(LogicalOperator::And, Self::minimize_not(left.span(), left, ctx, true))
|
||||
};
|
||||
let new_expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
|
||||
ctx.replace_expression(expr, new_expr);
|
||||
@@ -94,7 +92,7 @@ impl<'a> PeepholeOptimizations {
|
||||
let span = e.span;
|
||||
let (op, left) = if boolean {
|
||||
// "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)"
|
||||
(LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx))
|
||||
(LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx, true))
|
||||
} else {
|
||||
// "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)"
|
||||
(LogicalOperator::And, left)
|
||||
|
||||
@@ -42,13 +42,7 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
let Statement::IfStatement(if_stmt) = first else { unreachable!() };
|
||||
let IfStatement { test, alternate, .. } = if_stmt.unbox();
|
||||
|
||||
let expr = match test {
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
unary_expr.unbox().argument
|
||||
}
|
||||
e => Self::minimize_not(e.span(), e, ctx),
|
||||
};
|
||||
let expr = Self::minimize_not(test.span(), test, ctx, true);
|
||||
|
||||
if let Some(test) = &mut for_stmt.test {
|
||||
let left = test.take_in(ctx);
|
||||
|
||||
@@ -40,13 +40,9 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
// `if (!a) {} else x;` => `if (a) x;`
|
||||
// `if (a) {} else x;` => `if (!a) x;`
|
||||
let new_test = match &mut if_stmt.test {
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
unary_expr.argument.take_in(ctx)
|
||||
}
|
||||
_ => Self::minimize_not(if_stmt.test.span(), if_stmt.test.take_in(ctx), ctx),
|
||||
};
|
||||
ctx.replace_expression(&mut if_stmt.test, new_test);
|
||||
ctx.replace_expression_with(&mut if_stmt.test, |old, ctx| {
|
||||
Self::minimize_not(old.span(), old, ctx, true)
|
||||
});
|
||||
ctx.replace_statement(&mut if_stmt.consequent, new_consequent);
|
||||
}
|
||||
|
||||
@@ -71,8 +67,7 @@ impl<'a> PeepholeOptimizations {
|
||||
&& let Expression::UnaryExpression(unary_expr) = &mut if_stmt.test
|
||||
&& unary_expr.operator.is_not()
|
||||
{
|
||||
let new_test = unary_expr.argument.take_in(ctx);
|
||||
ctx.replace_expression(&mut if_stmt.test, new_test);
|
||||
ctx.replace_expression_with(&mut if_stmt.test, Self::unwrap_unary);
|
||||
std::mem::swap(&mut if_stmt.consequent, alternate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,36 +9,34 @@ use super::PeepholeOptimizations;
|
||||
impl<'a> PeepholeOptimizations {
|
||||
pub fn minimize_not(
|
||||
span: Span,
|
||||
expr: Expression<'a>,
|
||||
mut expr: Expression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
boolean_context: bool,
|
||||
) -> Expression<'a> {
|
||||
let mut unary =
|
||||
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, expr, ctx);
|
||||
Self::minimize_unary(&mut unary, ctx);
|
||||
unary
|
||||
if Self::try_negate_expression(&mut expr, ctx, boolean_context) {
|
||||
if boolean_context {
|
||||
Self::minimize_expression_in_boolean_context(&mut expr, ctx);
|
||||
}
|
||||
expr
|
||||
} else {
|
||||
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, expr, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
|
||||
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
let Expression::UnaryExpression(e) = expr else { return };
|
||||
if !e.operator.is_not() {
|
||||
return;
|
||||
}
|
||||
Self::minimize_expression_in_boolean_context(&mut e.argument, ctx);
|
||||
match &mut e.argument {
|
||||
pub fn try_negate_expression(
|
||||
expr: &mut Expression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
boolean_context: bool,
|
||||
) -> bool {
|
||||
match expr {
|
||||
// `!!true` -> `true`
|
||||
// `!!false` -> `false`
|
||||
Expression::UnaryExpression(e)
|
||||
if e.operator.is_not() && e.argument.value_type(ctx).is_boolean() =>
|
||||
if e.operator.is_not()
|
||||
&& (boolean_context || e.argument.value_type(ctx).is_boolean()) =>
|
||||
{
|
||||
// Both discarded `!` wrappers contain no references.
|
||||
ctx.replace_expression_with(expr, |old, _| {
|
||||
let Expression::UnaryExpression(outer) = old else { unreachable!() };
|
||||
let Expression::UnaryExpression(inner) = outer.unbox().argument else {
|
||||
unreachable!()
|
||||
};
|
||||
inner.unbox().argument
|
||||
});
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
true
|
||||
}
|
||||
// `!(a == b)` => `a != b`
|
||||
// `!(a != b)` => `a == b`
|
||||
@@ -46,7 +44,7 @@ impl<'a> PeepholeOptimizations {
|
||||
// `!(a !== b)` => `a === b`
|
||||
Expression::BinaryExpression(binary_expr) if binary_expr.operator.is_equality() => {
|
||||
binary_expr.operator = binary_expr.operator.equality_inverse_operator().unwrap();
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
true
|
||||
}
|
||||
// `!(a == b || c == d)` => `a != b && c != d`
|
||||
// `!(a == b && c == d)` => `a != b || c != d`
|
||||
@@ -60,18 +58,32 @@ impl<'a> PeepholeOptimizations {
|
||||
if Self::de_morgan_paren_delta(logical_expr).is_some_and(|delta| delta <= 0) =>
|
||||
{
|
||||
Self::de_morgan_invert_logical(logical_expr);
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
true
|
||||
}
|
||||
// "!(a, b)" => "a, !b"
|
||||
Expression::SequenceExpression(sequence_expr) => {
|
||||
if let Some(last_expr) = sequence_expr.expressions.last_mut() {
|
||||
ctx.replace_expression_with(last_expr, |old, ctx| {
|
||||
Self::minimize_not(old.span(), old, ctx)
|
||||
Self::minimize_not(old.span(), old, ctx, boolean_context)
|
||||
});
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => {}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
|
||||
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
let Expression::UnaryExpression(e) = expr else { return };
|
||||
if !e.operator.is_not() {
|
||||
return;
|
||||
}
|
||||
Self::minimize_expression_in_boolean_context(&mut e.argument, ctx);
|
||||
|
||||
if Self::try_negate_expression(&mut e.argument, ctx, false) {
|
||||
ctx.replace_expression_with(expr, Self::unwrap_unary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -860,8 +860,7 @@ impl<'a> PeepholeOptimizations {
|
||||
body[0].span()
|
||||
};
|
||||
let test = if_stmt.unbox().test;
|
||||
let mut test = Self::minimize_not(test.span(), test, ctx);
|
||||
Self::minimize_expression_in_boolean_context(&mut test, ctx);
|
||||
let test = Self::minimize_not(test.span(), test, ctx, true);
|
||||
let consequent = if body.len() == 1 {
|
||||
body.remove(0)
|
||||
} else {
|
||||
|
||||
@@ -1107,7 +1107,7 @@ impl<'a> PeepholeOptimizations {
|
||||
Self::minimize_expression_in_boolean_context(&mut arg, ctx);
|
||||
let arg =
|
||||
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, arg, ctx);
|
||||
Some(Self::minimize_not(span, arg, ctx))
|
||||
Some(Self::minimize_not(span, arg, ctx, false))
|
||||
}
|
||||
},
|
||||
"String" => {
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
use crate::test;
|
||||
|
||||
fn test_boolean(source_text: &str, expected: &str) {
|
||||
test(format!("for(;{source_text};);").as_str(), format!("for(;{expected};);").as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimize_expression_in_boolean_context() {
|
||||
test_boolean("!!a", "a");
|
||||
test_boolean("!!a ? b : c", "a ? b : c");
|
||||
test_boolean("!!!a", "!a");
|
||||
test_boolean("Boolean(!!a)", "a");
|
||||
test_boolean("((a | +b) !== 0)", "a | +b");
|
||||
test_boolean("(a | +b) === 0", "!(a | +b)");
|
||||
test_boolean("!!a && !!b", "a && b");
|
||||
test_boolean("!!a || !!b", "a || b");
|
||||
test_boolean("anything || (0, false)", "anything");
|
||||
test_boolean("a ? !!b : !!c", "a ? b : c");
|
||||
test_boolean("foo, !!bar", "foo, bar");
|
||||
test_boolean("anything1 ? (0, true) : anything2", "anything1 || anything2");
|
||||
test_boolean("anything1 ? (0, false) : anything2", "!anything1 && anything2");
|
||||
test_boolean("anything1 ? anything2 : (0, true)", "!anything1 || anything2");
|
||||
test_boolean("anything1 ? anything2 : (0, false)", "anything1 && anything2");
|
||||
test_boolean("+a === 0", "+a == 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_fold_in_boolean_context() {
|
||||
test("if (!!a);", "a");
|
||||
|
||||
@@ -51,7 +51,7 @@ fn integration() {
|
||||
}
|
||||
console.log(c, d);
|
||||
",
|
||||
"if (console.log('effect'), !1) var c, c, d;
|
||||
"if (console.log('effect'), 0) var c, c, d;
|
||||
console.log(c, d);
|
||||
",
|
||||
);
|
||||
|
||||
@@ -5,9 +5,9 @@ checker.ts:
|
||||
sys deallocs: 150
|
||||
sys alloc bytes: 14996600 # 15.00 MB
|
||||
sys peak growth: 10169560 # 10.17 MB
|
||||
arena allocs: 144626
|
||||
arena allocs: 144524
|
||||
arena reallocs: 28468
|
||||
arena size: 19144184 # 19.14 MB
|
||||
arena size: 19141528 # 19.14 MB
|
||||
|
||||
App.tsx:
|
||||
file size: 415342 # 415.34 kB
|
||||
@@ -16,7 +16,7 @@ App.tsx:
|
||||
sys deallocs: 61
|
||||
sys alloc bytes: 2128555 # 2.13 MB
|
||||
sys peak growth: 1612427 # 1.61 MB
|
||||
arena allocs: 15730
|
||||
arena allocs: 15718
|
||||
arena reallocs: 2721
|
||||
arena size: 2882416 # 2.88 MB
|
||||
|
||||
@@ -38,9 +38,9 @@ pdf.mjs:
|
||||
sys deallocs: 2427
|
||||
sys alloc bytes: 5334247 # 5.33 MB
|
||||
sys peak growth: 3693355 # 3.69 MB
|
||||
arena allocs: 44970
|
||||
arena allocs: 44854
|
||||
arena reallocs: 7718
|
||||
arena size: 6311296 # 6.31 MB
|
||||
arena size: 6308176 # 6.31 MB
|
||||
|
||||
antd.js:
|
||||
file size: 6686316 # 6.69 MB
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29976031 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 352722
|
||||
arena allocs: 352497
|
||||
arena reallocs: 76289
|
||||
arena size: 48806072 # 48.81 MB
|
||||
arena size: 48799784 # 48.80 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -60,7 +60,7 @@ binder.ts:
|
||||
sys deallocs: 33
|
||||
sys alloc bytes: 963008 # 963.01 kB
|
||||
sys peak growth: 657404 # 657.40 kB
|
||||
arena allocs: 6847
|
||||
arena allocs: 6841
|
||||
arena reallocs: 842
|
||||
arena size: 1164432 # 1.16 MB
|
||||
|
||||
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 1854
|
||||
sys alloc bytes: 5307512 # 5.31 MB
|
||||
sys peak growth: 3707425 # 3.71 MB
|
||||
arena allocs: 65134
|
||||
arena allocs: 65050
|
||||
arena reallocs: 12252
|
||||
arena size: 9397712 # 9.40 MB
|
||||
arena size: 9395480 # 9.40 MB
|
||||
Reference in new issue
Block a user