feat(minifier): add de morgan optimization for void expressions (#27375)

add missing de morgan law optimizations in context of void expression

`(!a || b === c) && b()` -> `a && b !== c || b()`

limitations: due to how nodes are processed, currently simple rewrites
like `!a || b()` are not rewritten to `a && b()` and follow same
limitations as optimizations of `not unary expression`
This commit is contained in:
Armano authored and GitHub committed 2026-10-15 09:38:26 +08:00
1 parent db9f3d69f8
commit 6b5057bb1d
7 files changed
+158 -59

No files matched your search

@@ -119,6 +119,51 @@ impl<'a> PeepholeOptimizations {
e.unbox().argument
}
fn de_morgan_expr_size_delta(
expr: &Expression<'a>,
parent_op: LogicalOperator,
boolean_context: bool,
) -> Option<i32> {
let mut delta = 0;
match expr {
Expression::BinaryExpression(b)
if b.operator.is_equality() || Self::is_typeof_undefined_comparison(b) => {}
Expression::UnaryExpression(u) if u.operator.is_not() => {
delta += if boolean_context { -1 } else { 1 };
}
Expression::LogicalExpression(child) => {
delta += Self::de_morgan_paren_delta(child, boolean_context)?;
// `&&` under `||` prints bare but its inversion (`||` under
// `&&`) needs parens; the reverse drops parens.
match (parent_op, child.operator) {
(LogicalOperator::Or, LogicalOperator::And) => delta += 2,
(LogicalOperator::And, LogicalOperator::Or) => delta -= 2,
_ => {}
}
}
Expression::Identifier(_)
| Expression::ThisExpression(_)
| Expression::NullLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::StringLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::ImportMeta(_)
| Expression::Super(_)
| Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::CallExpression(_)
| Expression::ChainExpression(_)
| Expression::ImportExpression(_)
| Expression::NewExpression(_)
| Expression::AwaitExpression(_) => delta += 1,
Expression::BooleanLiteral(_) => {}
_ => return None,
}
Some(delta)
}
/// Character delta from parentheses added or removed by De Morgan's law
/// (flipping `&&` <-> `||` changes which nested operands need parens), or
/// `None` if some operand cannot invert its operator in place.
@@ -128,42 +173,7 @@ impl<'a> PeepholeOptimizations {
}
let mut delta = 0;
for side in [&e.left, &e.right] {
match side {
Expression::BinaryExpression(b)
if b.operator.is_equality() || Self::is_typeof_undefined_comparison(b) => {}
Expression::UnaryExpression(u) if u.operator.is_not() => {
delta += if boolean_context { -1 } else { 1 }
}
Expression::LogicalExpression(child) => {
delta += Self::de_morgan_paren_delta(child, boolean_context)?;
// `&&` under `||` prints bare but its inversion (`||` under
// `&&`) needs parens; the reverse drops parens.
match (e.operator, child.operator) {
(LogicalOperator::Or, LogicalOperator::And) => delta += 2,
(LogicalOperator::And, LogicalOperator::Or) => delta -= 2,
_ => {}
}
}
Expression::Identifier(_)
| Expression::ThisExpression(_)
| Expression::NullLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::StringLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::ImportMeta(_)
| Expression::Super(_)
| Expression::StaticMemberExpression(_)
| Expression::ComputedMemberExpression(_)
| Expression::CallExpression(_)
| Expression::ChainExpression(_)
| Expression::ImportExpression(_)
| Expression::NewExpression(_)
| Expression::AwaitExpression(_) => delta += 1,
Expression::BooleanLiteral(_) => {}
_ => return None,
}
delta += Self::de_morgan_expr_size_delta(side, e.operator, boolean_context)?;
}
Some(delta)
}
@@ -188,11 +198,7 @@ impl<'a> PeepholeOptimizations {
ctx: &mut TraverseCtx<'a>,
boolean_context: bool,
) {
e.operator = if e.operator == LogicalOperator::And {
LogicalOperator::Or
} else {
LogicalOperator::And
};
e.operator = if e.operator.is_and() { LogicalOperator::Or } else { LogicalOperator::And };
Self::de_morgan_invert(&mut e.left, ctx, boolean_context);
Self::de_morgan_invert(&mut e.right, ctx, boolean_context);
}
@@ -215,4 +221,28 @@ impl<'a> PeepholeOptimizations {
});
}
}
pub fn de_morgan_for_void_expr(expr: &mut LogicalExpression<'a>, ctx: &mut TraverseCtx<'a>) {
if expr.operator.is_coalesce() {
return;
}
let Some(mut delta) = Self::de_morgan_expr_size_delta(&expr.left, expr.operator, true)
else {
return;
};
let inverse =
if expr.operator.is_and() { LogicalOperator::Or } else { LogicalOperator::And };
if let Expression::LogicalExpression(right) = &expr.right {
let parens = |op: LogicalOperator| i32::from(op.is_and() && right.operator.is_or()) * 2;
delta += parens(inverse) - parens(expr.operator);
}
// TODO: use precedence or parent operator to determine if inversion will produce parens
if delta >= -2 {
return;
}
Self::de_morgan_invert(&mut expr.left, ctx, true);
expr.operator = inverse;
ctx.notice_change();
}
}
@@ -240,6 +240,9 @@ impl<'a> PeepholeOptimizations {
}
}
if !ctx.is_tree_shake_only() {
Self::de_morgan_for_void_expr(logical_expr, ctx);
}
false
}
@@ -584,7 +584,7 @@ fn integration() {
",
"
var bar = foo.bar;
(typeof bar != 'object' || !bar) && console.log('foo')
typeof bar == 'object' && bar || console.log('foo')
",
);
}
@@ -298,7 +298,6 @@ fn test_minimize_while_condition() {
}
#[test]
#[ignore = "TODO: De Morgan's law optimization not yet implemented"]
fn test_minimize_demorgan_remove_leading_not() {
test("if(!(!a||!b)&&c) foo()", "((a&&b)&&c)&&foo()");
test("if(!(x&&y)) foo()", "x&&y||foo()");
@@ -326,7 +325,6 @@ fn test_minimize_demorgan2b() {
}
#[test]
#[ignore = "TODO: De Morgan's law optimization not yet implemented"]
fn test_minimize_demorgan3() {
test("if((!a||!b)&&(c||d)) foo()", "(a&&b||!c&&!d)||foo()");
}
@@ -31,6 +31,74 @@ fn test_remove_unused_optional_chain_keeps_base_side_effects() {
test("let log = []; (log.push('base'), null)?.x;", "[].push('base')");
}
#[test]
fn test_de_morgan_for_unused_expr() {
test_same("!a || b()"); // a && b()
test_same("!a && b()"); // a || b()
test_same("!a() || b()"); // a() && b()
test_same("!a() && b()"); // a() || b()
test_same("a && b()");
test_same("a || b()");
test_same("a === b && c()");
test_same("a !== b || c()");
test_same("a && !b && d()");
test_same("a || !b && c()");
test_same("!a && !b && c()"); // a || b || c()
test_same("a && (!b || c())");
// complex
test("a || (!b || c())", "a || !b || c()");
test("(!a || b === c) && b()", "a && b !== c || b()");
test("(!a || b === !0) && (c = { d: !!e })", "a && b !== !0 || (c = { d: !!e })");
test("a && b !== c || d()", "a && b !== c || d()");
test("(!a && !b && !c && !d && !e) || f()", "(a || b || c || d || e) && f()");
test("(!a() || b === c) && d()", "a() && b !== c || d()");
test("u() || ((!a || b === c) && d())", "u() || a && b !== c || d()");
test("u() && ((!a || b === c) && d())", "u() && (!a || b === c) && d()");
test("(!a || b === c) && (d(), e())", "a && b !== c || (d(), e())");
test("a || (!b || d())", "a || !b || d()");
test("(!a || !b) && (c() || d())", "a && b || c() || d()");
test("(!a || !b || !c) && (d() || e())", "a && b && c || d() || e()");
// folding
test("if ((!a || b === c) && d()) e()", "(!a || b === c) && d() && e()");
test("if (!a || b === c) d()", "a && b !== c || d()");
test("if (a && b !== c); else d()", "a && b !== c || d()");
test("if (!a || b()) c()", "(!a || b()) && c()");
test("if (!a && b()) c()", "!a && b() && c()");
// move to
test(
"for ((!a || b === c) && d(); e(); (!f || g === h) && i()) j()",
"for (a && b !== c || d(); e(); f && g !== h || i()) j()",
);
// after removal
test("(!a || b === c) ?? run()", "!a || (b, c)");
}
#[test]
fn test_de_morgan_for_unused_expr_observed() {
// call
test_same("a(!b || c())");
test_same("a(!b && c())");
test_same("a((!a || b === c) && d())");
test_same("a((b(), !c || d()))");
test_same("a((!b || c(), d()))");
test("a(((!b || c === d) && e(), f()))", "a((b && c !== d || e(), f()))");
// return
test_same("function f() { return !a || b() }");
test_same("function f() { return !a && b() }");
test_same("function f() { return (!a || b === c) && d() }");
// assignment
test_same("a = !b || c()");
test_same("a = (!b || c === d) && e()");
// coalesce
test_same("(a() || b()) ?? c()");
test_same("(!a || b()) ?? c()");
test_same("(!a && b()) ?? c()");
test_same("(a < b || c < d) && e()");
test_same("(a ?? b) && e()");
// boolean
test_same("for (!a || b(); c(); !d && e()) f()");
}
#[test]
fn test_remove_unused_this() {
// In a derived class constructor, `this` before `super()` throws a ReferenceError,
+9 -9
View File
@@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
173.90 kB | 55.49 kB | 59.82 kB | 18.78 kB | 19.33 kB | 2 | moment.js
287.63 kB | 88.98 kB | 90.07 kB | 30.85 kB | 31.95 kB | 2 | jquery.js
287.63 kB | 88.94 kB | 90.07 kB | 30.84 kB | 31.95 kB | 2 | jquery.js
342.15 kB | 114.76 kB | 118.14 kB | 42.69 kB | 44.37 kB | 2 | vue.js
342.15 kB | 114.75 kB | 118.14 kB | 42.68 kB | 44.37 kB | 2 | vue.js
544.10 kB | 69.87 kB | 72.48 kB | 25.58 kB | 26.20 kB | 2 | lodash.js
555.77 kB | 262.73 kB | 270.13 kB | 87.66 kB | 90.80 kB | 2 | d3.js
555.77 kB | 262.72 kB | 270.13 kB | 87.65 kB | 90.80 kB | 2 | d3.js
1.01 MB | 435.91 kB | 458.89 kB | 121.78 kB | 126.71 kB | 2 | bundle.min.js
1.01 MB | 435.88 kB | 458.89 kB | 121.78 kB | 126.71 kB | 2 | bundle.min.js
1.25 MB | 634.23 kB | 646.76 kB | 158.43 kB | 163.73 kB | 2 | three.js
1.25 MB | 634.22 kB | 646.76 kB | 158.42 kB | 163.73 kB | 2 | three.js
2.14 MB | 707.61 kB | 724.14 kB | 160.07 kB | 181.07 kB | 2 | victory.js
2.14 MB | 707.58 kB | 724.14 kB | 160.05 kB | 181.07 kB | 2 | victory.js
3.20 MB | 958.47 kB | 1.01 MB | 317.02 kB | 331.56 kB | 2 | echarts.js
3.20 MB | 958.40 kB | 1.01 MB | 316.96 kB | 331.56 kB | 2 | echarts.js
6.69 MB | 2.12 MB | 2.31 MB | 453.40 kB | 488.28 kB | 4 | antd.js
6.69 MB | 2.12 MB | 2.31 MB | 453.41 kB | 488.28 kB | 4 | antd.js
10.95 MB | 3.31 MB | 3.49 MB | 849.87 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.31 MB | 3.49 MB | 849.81 kB | 915.50 kB | 4 | typescript.js
@@ -7,7 +7,7 @@ checker.ts:
sys peak growth: 10169560 # 10.17 MB
arena allocs: 56229
arena reallocs: 5084
arena size: 16501016 # 16.50 MB
arena size: 16501168 # 16.50 MB
App.tsx:
file size: 415342 # 415.34 kB
@@ -16,7 +16,7 @@ App.tsx:
sys deallocs: 61
sys alloc bytes: 2128539 # 2.13 MB
sys peak growth: 1612427 # 1.61 MB
arena allocs: 7726
arena allocs: 7728
arena reallocs: 920
arena size: 2782336 # 2.78 MB
@@ -40,7 +40,7 @@ pdf.mjs:
sys peak growth: 3693355 # 3.69 MB
arena allocs: 19585
arena reallocs: 2574
arena size: 5727216 # 5.73 MB
arena size: 5727264 # 5.73 MB
antd.js:
file size: 6686316 # 6.69 MB
@@ -49,7 +49,7 @@ antd.js:
sys deallocs: 845
sys alloc bytes: 29973448 # 29.97 MB
sys peak growth: 19934725 # 19.93 MB
arena allocs: 181528
arena allocs: 181536
arena reallocs: 34406
arena size: 44087584 # 44.09 MB
@@ -62,7 +62,7 @@ binder.ts:
sys peak growth: 657404 # 657.40 kB
arena allocs: 2930
arena reallocs: 244
arena size: 1092336 # 1.09 MB
arena size: 1092424 # 1.09 MB
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
sys deallocs: 1854
sys alloc bytes: 5304252 # 5.30 MB
sys peak growth: 3707425 # 3.71 MB
arena allocs: 31106
arena allocs: 31111
arena reallocs: 3012
arena size: 8461040 # 8.46 MB