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:
Armano authored and GitHub committed 2026-08-19 13:21:35 +08:00
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");
+1 -1
View File
@@ -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