Skip to content

Commit 83cb516

Browse files
authored
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
1 parent 9b7e153 commit 83cb516

10 files changed

Lines changed: 97 additions & 75 deletions

‎crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,13 +417,13 @@ impl<'a> PeepholeOptimizations {
417417
) {
418418
(Some(true), Some(false)) => {
419419
let test = expr.test.take_in(ctx);
420-
let test = Self::minimize_not(expr.span, test, ctx);
421-
let test = Self::minimize_not(expr.span, test, ctx);
420+
let test = Self::minimize_not(expr.span, test, ctx, false);
421+
let test = Self::minimize_not(expr.span, test, ctx, false);
422422
return Some(test);
423423
}
424424
(Some(false), Some(true)) => {
425425
let test = expr.test.take_in(ctx);
426-
let test = Self::minimize_not(expr.span, test, ctx);
426+
let test = Self::minimize_not(expr.span, test, ctx, false);
427427
return Some(test);
428428
}
429429
// "c ? false : x" => "!c && x" (exact for any `c`)
@@ -436,7 +436,7 @@ impl<'a> PeepholeOptimizations {
436436
) =>
437437
{
438438
let test = expr.test.take_in(ctx);
439-
let test = Self::minimize_not(expr.span, test, ctx);
439+
let test = Self::minimize_not(expr.span, test, ctx, false);
440440
let right = expr.alternate.take_in(ctx);
441441
return Some(Self::join_with_left_associative_op(
442442
expr.span,
@@ -456,7 +456,7 @@ impl<'a> PeepholeOptimizations {
456456
) =>
457457
{
458458
let test = expr.test.take_in(ctx);
459-
let test = Self::minimize_not(expr.span, test, ctx);
459+
let test = Self::minimize_not(expr.span, test, ctx, false);
460460
let right = expr.consequent.take_in(ctx);
461461
return Some(Self::join_with_left_associative_op(
462462
expr.span,
@@ -546,8 +546,8 @@ impl<'a> PeepholeOptimizations {
546546
// But skip if parens would be needed (e.g., "a+b?1:0" => "+!!(a+b)" is longer)
547547
if !needs_parens {
548548
let test = expr.test.take_in(ctx);
549-
let test = Self::minimize_not(expr.span, test, ctx);
550-
let test = Self::minimize_not(expr.span, test, ctx);
549+
let test = Self::minimize_not(expr.span, test, ctx, false);
550+
let test = Self::minimize_not(expr.span, test, ctx, false);
551551
return Some(Expression::new_unary_expression(expr.span,
552552
UnaryOperator::UnaryPlus,
553553
test, ctx));
@@ -559,7 +559,7 @@ impl<'a> PeepholeOptimizations {
559559
// The `0` must be `+0`: `a ? -0 : 1` would become `+!a`, yielding `+0`, not `-0`.
560560
if !consequent.is_sign_negative() && !Self::test_needs_parens(&expr.test) => {
561561
let test = expr.test.take_in(ctx);
562-
let test = Self::minimize_not(expr.span, test, ctx);
562+
let test = Self::minimize_not(expr.span, test, ctx, false);
563563
return Some(Expression::new_unary_expression(expr.span,
564564
UnaryOperator::UnaryPlus,
565565
test, ctx));

‎crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ impl<'a> PeepholeOptimizations {
1616
ctx: &mut TraverseCtx<'a>,
1717
) {
1818
match expr {
19-
// "!!a" => "a"
2019
Expression::UnaryExpression(u1) if u1.operator.is_not() => {
21-
if let Expression::UnaryExpression(u2) = &mut u1.argument
22-
&& u2.operator.is_not()
23-
{
24-
let mut e = u2.argument.take_in(ctx);
25-
Self::minimize_expression_in_boolean_context(&mut e, ctx);
26-
ctx.replace_expression(expr, e);
20+
if Self::try_negate_expression(&mut u1.argument, ctx, true) {
21+
Self::minimize_expression_in_boolean_context(&mut u1.argument, ctx);
22+
ctx.replace_expression_with(expr, Self::unwrap_unary);
23+
} else {
24+
Self::minimize_expression_in_boolean_context(&mut u1.argument, ctx);
2725
}
2826
}
2927
Expression::BinaryExpression(e)
@@ -60,7 +58,7 @@ impl<'a> PeepholeOptimizations {
6058
}
6159
}
6260
// "if (!!a ||!!b)" => "if (a || b)"
63-
Expression::LogicalExpression(e) if e.operator == LogicalOperator::Or => {
61+
Expression::LogicalExpression(e) if e.operator.is_or() => {
6462
Self::minimize_expression_in_boolean_context(&mut e.left, ctx);
6563
Self::minimize_expression_in_boolean_context(&mut e.right, ctx);
6664
// "if (anything || falsyNoSideEffects)" => "if (anything)"
@@ -82,7 +80,7 @@ impl<'a> PeepholeOptimizations {
8280
(LogicalOperator::Or, left)
8381
} else {
8482
// "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 && anything2)"
85-
(LogicalOperator::And, Self::minimize_not(left.span(), left, ctx))
83+
(LogicalOperator::And, Self::minimize_not(left.span(), left, ctx, true))
8684
};
8785
let new_expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
8886
ctx.replace_expression(expr, new_expr);
@@ -94,7 +92,7 @@ impl<'a> PeepholeOptimizations {
9492
let span = e.span;
9593
let (op, left) = if boolean {
9694
// "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)"
97-
(LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx))
95+
(LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx, true))
9896
} else {
9997
// "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)"
10098
(LogicalOperator::And, left)

‎crates/oxc_minifier/src/peephole/minimize_for_statement.rs‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,7 @@ impl<'a> PeepholeOptimizations {
4242

4343
let Statement::IfStatement(if_stmt) = first else { unreachable!() };
4444
let IfStatement { test, alternate, .. } = if_stmt.unbox();
45-
46-
let expr = match test {
47-
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
48-
unary_expr.unbox().argument
49-
}
50-
e => Self::minimize_not(e.span(), e, ctx),
51-
};
45+
let expr = Self::minimize_not(test.span(), test, ctx, true);
5246

5347
if let Some(test) = &mut for_stmt.test {
5448
let left = test.take_in(ctx);

‎crates/oxc_minifier/src/peephole/minimize_if_statement.rs‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,9 @@ impl<'a> PeepholeOptimizations {
4040

4141
// `if (!a) {} else x;` => `if (a) x;`
4242
// `if (a) {} else x;` => `if (!a) x;`
43-
let new_test = match &mut if_stmt.test {
44-
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
45-
unary_expr.argument.take_in(ctx)
46-
}
47-
_ => Self::minimize_not(if_stmt.test.span(), if_stmt.test.take_in(ctx), ctx),
48-
};
49-
ctx.replace_expression(&mut if_stmt.test, new_test);
43+
ctx.replace_expression_with(&mut if_stmt.test, |old, ctx| {
44+
Self::minimize_not(old.span(), old, ctx, true)
45+
});
5046
ctx.replace_statement(&mut if_stmt.consequent, new_consequent);
5147
}
5248

@@ -71,8 +67,7 @@ impl<'a> PeepholeOptimizations {
7167
&& let Expression::UnaryExpression(unary_expr) = &mut if_stmt.test
7268
&& unary_expr.operator.is_not()
7369
{
74-
let new_test = unary_expr.argument.take_in(ctx);
75-
ctx.replace_expression(&mut if_stmt.test, new_test);
70+
ctx.replace_expression_with(&mut if_stmt.test, Self::unwrap_unary);
7671
std::mem::swap(&mut if_stmt.consequent, alternate);
7772
}
7873
}

‎crates/oxc_minifier/src/peephole/minimize_not_expression.rs‎

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,42 @@ use super::PeepholeOptimizations;
99
impl<'a> PeepholeOptimizations {
1010
pub fn minimize_not(
1111
span: Span,
12-
expr: Expression<'a>,
12+
mut expr: Expression<'a>,
1313
ctx: &mut TraverseCtx<'a>,
14+
boolean_context: bool,
1415
) -> Expression<'a> {
15-
let mut unary =
16-
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, expr, ctx);
17-
Self::minimize_unary(&mut unary, ctx);
18-
unary
16+
if Self::try_negate_expression(&mut expr, ctx, boolean_context) {
17+
if boolean_context {
18+
Self::minimize_expression_in_boolean_context(&mut expr, ctx);
19+
}
20+
expr
21+
} else {
22+
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, expr, ctx)
23+
}
1924
}
2025

21-
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
22-
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
23-
let Expression::UnaryExpression(e) = expr else { return };
24-
if !e.operator.is_not() {
25-
return;
26-
}
27-
Self::minimize_expression_in_boolean_context(&mut e.argument, ctx);
28-
match &mut e.argument {
26+
pub fn try_negate_expression(
27+
expr: &mut Expression<'a>,
28+
ctx: &mut TraverseCtx<'a>,
29+
boolean_context: bool,
30+
) -> bool {
31+
match expr {
2932
// `!!true` -> `true`
3033
// `!!false` -> `false`
3134
Expression::UnaryExpression(e)
32-
if e.operator.is_not() && e.argument.value_type(ctx).is_boolean() =>
35+
if e.operator.is_not()
36+
&& (boolean_context || e.argument.value_type(ctx).is_boolean()) =>
3337
{
34-
// Both discarded `!` wrappers contain no references.
35-
ctx.replace_expression_with(expr, |old, _| {
36-
let Expression::UnaryExpression(outer) = old else { unreachable!() };
37-
let Expression::UnaryExpression(inner) = outer.unbox().argument else {
38-
unreachable!()
39-
};
40-
inner.unbox().argument
41-
});
38+
ctx.replace_expression_with(expr, Self::unwrap_unary);
39+
true
4240
}
4341
// `!(a == b)` => `a != b`
4442
// `!(a != b)` => `a == b`
4543
// `!(a === b)` => `a !== b`
4644
// `!(a !== b)` => `a === b`
4745
Expression::BinaryExpression(binary_expr) if binary_expr.operator.is_equality() => {
4846
binary_expr.operator = binary_expr.operator.equality_inverse_operator().unwrap();
49-
ctx.replace_expression_with(expr, Self::unwrap_unary);
47+
true
5048
}
5149
// `!(a == b || c == d)` => `a != b && c != d`
5250
// `!(a == b && c == d)` => `a != b || c != d`
@@ -60,18 +58,32 @@ impl<'a> PeepholeOptimizations {
6058
if Self::de_morgan_paren_delta(logical_expr).is_some_and(|delta| delta <= 0) =>
6159
{
6260
Self::de_morgan_invert_logical(logical_expr);
63-
ctx.replace_expression_with(expr, Self::unwrap_unary);
61+
true
6462
}
6563
// "!(a, b)" => "a, !b"
6664
Expression::SequenceExpression(sequence_expr) => {
6765
if let Some(last_expr) = sequence_expr.expressions.last_mut() {
6866
ctx.replace_expression_with(last_expr, |old, ctx| {
69-
Self::minimize_not(old.span(), old, ctx)
67+
Self::minimize_not(old.span(), old, ctx, boolean_context)
7068
});
71-
ctx.replace_expression_with(expr, Self::unwrap_unary);
69+
return true;
7270
}
71+
false
7372
}
74-
_ => {}
73+
_ => false,
74+
}
75+
}
76+
77+
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
78+
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
79+
let Expression::UnaryExpression(e) = expr else { return };
80+
if !e.operator.is_not() {
81+
return;
82+
}
83+
Self::minimize_expression_in_boolean_context(&mut e.argument, ctx);
84+
85+
if Self::try_negate_expression(&mut e.argument, ctx, false) {
86+
ctx.replace_expression_with(expr, Self::unwrap_unary);
7587
}
7688
}
7789

‎crates/oxc_minifier/src/peephole/minimize_statements.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -860,8 +860,7 @@ impl<'a> PeepholeOptimizations {
860860
body[0].span()
861861
};
862862
let test = if_stmt.unbox().test;
863-
let mut test = Self::minimize_not(test.span(), test, ctx);
864-
Self::minimize_expression_in_boolean_context(&mut test, ctx);
863+
let test = Self::minimize_not(test.span(), test, ctx, true);
865864
let consequent = if body.len() == 1 {
866865
body.remove(0)
867866
} else {

‎crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1107,7 +1107,7 @@ impl<'a> PeepholeOptimizations {
11071107
Self::minimize_expression_in_boolean_context(&mut arg, ctx);
11081108
let arg =
11091109
Expression::new_unary_expression(span, UnaryOperator::LogicalNot, arg, ctx);
1110-
Some(Self::minimize_not(span, arg, ctx))
1110+
Some(Self::minimize_not(span, arg, ctx, false))
11111111
}
11121112
},
11131113
"String" => {

‎crates/oxc_minifier/tests/peephole/minimize_expression_in_boolean_context.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
use crate::test;
22

3+
fn test_boolean(source_text: &str, expected: &str) {
4+
test(format!("for(;{source_text};);").as_str(), format!("for(;{expected};);").as_str());
5+
}
6+
7+
#[test]
8+
fn test_minimize_expression_in_boolean_context() {
9+
test_boolean("!!a", "a");
10+
test_boolean("!!a ? b : c", "a ? b : c");
11+
test_boolean("!!!a", "!a");
12+
test_boolean("Boolean(!!a)", "a");
13+
test_boolean("((a | +b) !== 0)", "a | +b");
14+
test_boolean("(a | +b) === 0", "!(a | +b)");
15+
test_boolean("!!a && !!b", "a && b");
16+
test_boolean("!!a || !!b", "a || b");
17+
test_boolean("anything || (0, false)", "anything");
18+
test_boolean("a ? !!b : !!c", "a ? b : c");
19+
test_boolean("foo, !!bar", "foo, bar");
20+
test_boolean("anything1 ? (0, true) : anything2", "anything1 || anything2");
21+
test_boolean("anything1 ? (0, false) : anything2", "!anything1 && anything2");
22+
test_boolean("anything1 ? anything2 : (0, true)", "!anything1 || anything2");
23+
test_boolean("anything1 ? anything2 : (0, false)", "anything1 && anything2");
24+
test_boolean("+a === 0", "+a == 0");
25+
}
26+
327
#[test]
428
fn test_try_fold_in_boolean_context() {
529
test("if (!!a);", "a");

‎crates/oxc_minifier/tests/peephole/oxc.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fn integration() {
5151
}
5252
console.log(c, d);
5353
",
54-
"if (console.log('effect'), !1) var c, c, d;
54+
"if (console.log('effect'), 0) var c, c, d;
5555
console.log(c, d);
5656
",
5757
);

‎tasks/track_memory_allocations/allocs_minifier.yaml‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ checker.ts:
55
sys deallocs: 150
66
sys alloc bytes: 14996600 # 15.00 MB
77
sys peak growth: 10169560 # 10.17 MB
8-
arena allocs: 144626
8+
arena allocs: 144524
99
arena reallocs: 28468
10-
arena size: 19144184 # 19.14 MB
10+
arena size: 19141528 # 19.14 MB
1111

1212
App.tsx:
1313
file size: 415342 # 415.34 kB
@@ -16,7 +16,7 @@ App.tsx:
1616
sys deallocs: 61
1717
sys alloc bytes: 2128555 # 2.13 MB
1818
sys peak growth: 1612427 # 1.61 MB
19-
arena allocs: 15730
19+
arena allocs: 15718
2020
arena reallocs: 2721
2121
arena size: 2882416 # 2.88 MB
2222

@@ -38,9 +38,9 @@ pdf.mjs:
3838
sys deallocs: 2427
3939
sys alloc bytes: 5334247 # 5.33 MB
4040
sys peak growth: 3693355 # 3.69 MB
41-
arena allocs: 44970
41+
arena allocs: 44854
4242
arena reallocs: 7718
43-
arena size: 6311296 # 6.31 MB
43+
arena size: 6308176 # 6.31 MB
4444

4545
antd.js:
4646
file size: 6686316 # 6.69 MB
@@ -49,9 +49,9 @@ antd.js:
4949
sys deallocs: 1038
5050
sys alloc bytes: 29976031 # 29.98 MB
5151
sys peak growth: 19934725 # 19.93 MB
52-
arena allocs: 352722
52+
arena allocs: 352497
5353
arena reallocs: 76289
54-
arena size: 48806072 # 48.81 MB
54+
arena size: 48799784 # 48.80 MB
5555

5656
binder.ts:
5757
file size: 193077 # 193.08 kB
@@ -60,7 +60,7 @@ binder.ts:
6060
sys deallocs: 33
6161
sys alloc bytes: 963008 # 963.01 kB
6262
sys peak growth: 657404 # 657.40 kB
63-
arena allocs: 6847
63+
arena allocs: 6841
6464
arena reallocs: 842
6565
arena size: 1164432 # 1.16 MB
6666

@@ -71,6 +71,6 @@ kitchen-sink.tsx:
7171
sys deallocs: 1854
7272
sys alloc bytes: 5307512 # 5.31 MB
7373
sys peak growth: 3707425 # 3.71 MB
74-
arena allocs: 65134
74+
arena allocs: 65050
7575
arena reallocs: 12252
76-
arena size: 9397712 # 9.40 MB
76+
arena size: 9395480 # 9.40 MB

0 commit comments

Comments
 (0)