mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
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:
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,
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user