mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 14:49:36 +00:00
feat(minifier): expand de morgan's optimization to allow move of ! (#25930)
Expand negation to include move of `!` operator to inner children ```js !(a == b && c == 3 && a) a != b || c != 3 | !a ``` delta calculation is dependent on boolean context - if we expect truthly/falsly value negation of `!a` will be converted to `a` (-1 delta) - in ambiguous context `!a` will be converted to `!!a` (+1 delta) ```js a = !!(!b || !c); // ^ ^ ^ -3 a = !(b || c) // ^ ^ ^ +1 - if we could detect if parens can be removed it would be -1 // ^^^ ^^ -1 - if we would know that we can remove parens ```
This commit is contained in:
1 parent
baf4b1e00a
commit
3ed4f6ad90
6 files changed
+90
-39
No files matched your search
@@ -54,10 +54,15 @@ impl<'a> PeepholeOptimizations {
|
||||
// The fold is exact and involutive: a later `minimize_not` restores
|
||||
// the original chain at no cost, so shapes that consume the `!` for
|
||||
// free (branch swaps, `!!` collapses) are unaffected.
|
||||
//
|
||||
// delta: we do not know the parent precedence, so we must conservatively
|
||||
// assume negated operands may still need ().
|
||||
// delta == 0 it does assume the parent will not drop parens.
|
||||
Expression::LogicalExpression(logical_expr)
|
||||
if Self::de_morgan_paren_delta(logical_expr).is_some_and(|delta| delta <= 0) =>
|
||||
if Self::de_morgan_paren_delta(logical_expr, boolean_context)
|
||||
.is_some_and(|delta| delta <= 0) =>
|
||||
{
|
||||
Self::de_morgan_invert_logical(logical_expr);
|
||||
Self::de_morgan_invert_logical(logical_expr, ctx, boolean_context);
|
||||
true
|
||||
}
|
||||
// "!(a, b)" => "a, !b"
|
||||
@@ -97,7 +102,7 @@ impl<'a> PeepholeOptimizations {
|
||||
/// 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.
|
||||
fn de_morgan_paren_delta(e: &LogicalExpression<'a>) -> Option<i32> {
|
||||
fn de_morgan_paren_delta(e: &LogicalExpression<'a>, boolean_context: bool) -> Option<i32> {
|
||||
if !matches!(e.operator, LogicalOperator::And | LogicalOperator::Or) {
|
||||
return None;
|
||||
}
|
||||
@@ -105,8 +110,11 @@ impl<'a> PeepholeOptimizations {
|
||||
for side in [&e.left, &e.right] {
|
||||
match side {
|
||||
Expression::BinaryExpression(b) if b.operator.is_equality() => {}
|
||||
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)?;
|
||||
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) {
|
||||
@@ -115,6 +123,24 @@ impl<'a> PeepholeOptimizations {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -123,23 +149,36 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
/// Apply De Morgan's law in place. Only called on chains approved by
|
||||
/// [`Self::de_morgan_paren_delta`].
|
||||
fn de_morgan_invert_logical(e: &mut LogicalExpression<'a>) {
|
||||
fn de_morgan_invert_logical(
|
||||
e: &mut LogicalExpression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
boolean_context: bool,
|
||||
) {
|
||||
e.operator = if e.operator == LogicalOperator::And {
|
||||
LogicalOperator::Or
|
||||
} else {
|
||||
LogicalOperator::And
|
||||
};
|
||||
Self::de_morgan_invert(&mut e.left);
|
||||
Self::de_morgan_invert(&mut e.right);
|
||||
Self::de_morgan_invert(&mut e.left, ctx, boolean_context);
|
||||
Self::de_morgan_invert(&mut e.right, ctx, boolean_context);
|
||||
}
|
||||
|
||||
fn de_morgan_invert(expr: &mut Expression<'a>) {
|
||||
match expr {
|
||||
Expression::BinaryExpression(e) => {
|
||||
e.operator = e.operator.equality_inverse_operator().unwrap();
|
||||
}
|
||||
Expression::LogicalExpression(e) => Self::de_morgan_invert_logical(e),
|
||||
_ => unreachable!(),
|
||||
fn de_morgan_invert(
|
||||
expr: &mut Expression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
boolean_context: bool,
|
||||
) {
|
||||
if let Expression::LogicalExpression(e) = expr
|
||||
&& !e.operator.is_coalesce()
|
||||
{
|
||||
Self::de_morgan_invert_logical(e, ctx, boolean_context);
|
||||
return;
|
||||
}
|
||||
|
||||
if !Self::try_negate_expression(expr, ctx, boolean_context) {
|
||||
ctx.replace_expression_with(expr, |expr, ctx| {
|
||||
Expression::new_unary_expression(expr.span(), UnaryOperator::LogicalNot, expr, ctx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,14 +405,14 @@ fn js_parser_test() {
|
||||
test("a = !!(b && c)", "a = !!(b && c);");
|
||||
test("a = !!(b || c)", "a = !!(b || c);");
|
||||
test("a = !!(b ?? c)", "a = !!(b ?? c);");
|
||||
test("a = !!(!b && c)", "a = !!(!b && c);");
|
||||
test("a = !!(!b || c)", "a = !!(!b || c);");
|
||||
test("a = !!(!b && c)", "a = !(b || !c);");
|
||||
test("a = !!(!b || c)", "a = !(b && !c);");
|
||||
test("a = !!(!b ?? c)", "a = !b;");
|
||||
test("a = !!(b && !c)", "a = !!(b && !c);");
|
||||
test("a = !!(b || !c)", "a = !!(b || !c);");
|
||||
test("a = !!(b && !c)", "a = !(!b || c);");
|
||||
test("a = !!(b || !c)", "a = !(!b && c);");
|
||||
test("a = !!(b ?? !c)", "a = !!(b ?? !c);");
|
||||
test("a = !!(!b && !c)", "a = !b && !c;");
|
||||
test("a = !!(!b || !c)", "a = !b || !c;");
|
||||
test("a = !!(!b && !c)", "a = !(b || c);");
|
||||
test("a = !!(!b || !c)", "a = !(b && c);");
|
||||
test("a = !!(!b ?? !c)", "a = !b;");
|
||||
test("a = !!(b, c)", "a = (b, !!c);");
|
||||
test("a = Boolean(b); var Boolean", "a = Boolean(b);var Boolean;");
|
||||
|
||||
@@ -356,9 +356,8 @@ fn test_minimize_demorgan20a() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: De Morgan's law optimization not yet implemented"]
|
||||
fn test_minimize_demorgan20b() {
|
||||
test("if (0!==c || 2!==a && 1!==a) g(); else f()", "(0!==c || 2!==a && 1!==a) ? g() : f()");
|
||||
test("if (0!==c || 2!==a && 1!==a) g(); else f()", "c!==0 || a!==2 && a!==1 ? g() : f()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -57,6 +57,19 @@ fn minimize_nots_with_de_morgan_negative_cases() {
|
||||
// Existing shapes that consume the `!` for free must not regress.
|
||||
test("var v = !!(a == b || c == d);", "var v = a == b || c == d;");
|
||||
test("if (!(a == b && c == d)) x(); else y();", "a != b || c != d ? x() : y();");
|
||||
// mixed unary-not leaves in both boolean and value contexts.
|
||||
test("if (!(!a || b)) x();", "a && !b && x();");
|
||||
test_same("var v = !(!a || b);");
|
||||
test("if (!(a == b || !c && d == e)) x();", "a == b || !c && d == e || x();");
|
||||
// parenthesis-size guard.
|
||||
test_same("if (!(a == b || fn1())) throw x;");
|
||||
test_same("if (!(a == b && fn1())) throw x;");
|
||||
test("var v = !!(!a || b);", "var v = !(a && !b);");
|
||||
test("var v = !!(a && !b);", "var v = !(!a || b);");
|
||||
test_same("if (!(a < b || !c)) throw x;");
|
||||
test_same("if (!(a in b || c == d)) throw x;");
|
||||
test("if (!(a === b && (a ?? b))) throw x;", "if (!(a === b && (a ?? b))) throw x;");
|
||||
test("if (!(a === b && !a)) throw x;", "if (a !== b || a) throw x;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+11
-11
@@ -1,27 +1,27 @@
|
||||
| Oxc props | ESBuild | Oxc props | ESBuild |
|
||||
Original | minified | minified | gzip | gzip | Iterations | File
|
||||
-------------------------------------------------------------------------------------
|
||||
72.14 kB | 22.48 kB | 23.70 kB | 8.20 kB | 8.54 kB | 2 | react.development.js
|
||||
72.14 kB | 22.48 kB | 23.70 kB | 8.19 kB | 8.54 kB | 2 | react.development.js
|
||||
|
||||
173.90 kB | 55.53 kB | 59.82 kB | 18.78 kB | 19.33 kB | 2 | moment.js
|
||||
|
||||
287.63 kB | 89.02 kB | 90.07 kB | 30.87 kB | 31.95 kB | 2 | jquery.js
|
||||
287.63 kB | 89.01 kB | 90.07 kB | 30.85 kB | 31.95 kB | 2 | jquery.js
|
||||
|
||||
342.15 kB | 114.85 kB | 118.14 kB | 42.73 kB | 44.37 kB | 2 | vue.js
|
||||
342.15 kB | 114.82 kB | 118.14 kB | 42.72 kB | 44.37 kB | 2 | vue.js
|
||||
|
||||
544.10 kB | 69.88 kB | 72.48 kB | 25.59 kB | 26.20 kB | 2 | lodash.js
|
||||
544.10 kB | 69.88 kB | 72.48 kB | 25.58 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 262.77 kB | 270.13 kB | 87.67 kB | 90.80 kB | 2 | d3.js
|
||||
555.77 kB | 262.76 kB | 270.13 kB | 87.66 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 436.06 kB | 458.89 kB | 121.79 kB | 126.71 kB | 2 | bundle.min.js
|
||||
1.01 MB | 436.04 kB | 458.89 kB | 121.78 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 634.41 kB | 646.76 kB | 158.45 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 634.41 kB | 646.76 kB | 158.44 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 707.71 kB | 724.14 kB | 160.11 kB | 181.07 kB | 2 | victory.js
|
||||
2.14 MB | 707.66 kB | 724.14 kB | 160.09 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 959.19 kB | 1.01 MB | 317.26 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 958.93 kB | 1.01 MB | 317.09 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.48 kB | 488.28 kB | 5 | antd.js
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.44 kB | 488.28 kB | 5 | antd.js
|
||||
|
||||
10.95 MB | 3.31 MB | 3.49 MB | 850.30 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.31 MB | 3.49 MB | 850.04 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -5,7 +5,7 @@ checker.ts:
|
||||
sys deallocs: 150
|
||||
sys alloc bytes: 14996600 # 15.00 MB
|
||||
sys peak growth: 10169560 # 10.17 MB
|
||||
arena allocs: 144012
|
||||
arena allocs: 144024
|
||||
arena reallocs: 28468
|
||||
arena size: 19127248 # 19.13 MB
|
||||
|
||||
@@ -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: 15670
|
||||
arena allocs: 15676
|
||||
arena reallocs: 2721
|
||||
arena size: 2880752 # 2.88 MB
|
||||
|
||||
@@ -38,7 +38,7 @@ pdf.mjs:
|
||||
sys deallocs: 2427
|
||||
sys alloc bytes: 5334247 # 5.33 MB
|
||||
sys peak growth: 3693355 # 3.69 MB
|
||||
arena allocs: 44802
|
||||
arena allocs: 44800
|
||||
arena reallocs: 7718
|
||||
arena size: 6306768 # 6.31 MB
|
||||
|
||||
@@ -49,7 +49,7 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29976031 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 351769
|
||||
arena allocs: 351753
|
||||
arena reallocs: 76289
|
||||
arena size: 48779400 # 48.78 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: 64966
|
||||
arena allocs: 64963
|
||||
arena reallocs: 12252
|
||||
arena size: 9394240 # 9.39 MB
|
||||
arena size: 9393120 # 9.39 MB
|
||||
Reference in new issue
Block a user