feat(minifier): negate binary comparison for typeof x < 'u' (#26367)

Allow inversion of binary operation when comparing `'u'` string with
`typeof

`!(typeof a < "u")` => `typeof a > "u"`
`!(typeof a > "u")` => `typeof a < "u"`

this is needed to support negation of short version of `typeof x ===
"undefined"` -> `typeof a > "u"` generated by
`substitute_typeof_undefined`

fixes #25294
related #26460
This commit is contained in:
Armano
2026-09-09 03:19:02 +02:00
committed by GitHub
parent d198982c08
commit 80a76a0a2b
4 changed files with 38 additions and 3 deletions
@@ -46,6 +46,14 @@ impl<'a> PeepholeOptimizations {
binary_expr.operator = binary_expr.operator.equality_inverse_operator().unwrap();
true
}
// `!(typeof a < "u")` => `typeof a > "u"`
// `!(typeof a > "u")` => `typeof a < "u"`
Expression::BinaryExpression(binary_expr)
if Self::is_typeof_undefined_comparison(binary_expr) =>
{
binary_expr.operator = binary_expr.operator.compare_inverse_operator().unwrap();
true
}
// `!0` => `1`
// `!1` => `0`
Expression::NumericLiteral(num) if boolean_context => {
@@ -115,7 +123,8 @@ impl<'a> PeepholeOptimizations {
let mut delta = 0;
for side in [&e.left, &e.right] {
match side {
Expression::BinaryExpression(b) if b.operator.is_equality() => {}
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 }
}
@@ -153,6 +162,19 @@ impl<'a> PeepholeOptimizations {
Some(delta)
}
fn is_typeof_undefined_comparison(e: &BinaryExpression<'_>) -> bool {
if !matches!(e.operator, BinaryOperator::LessThan | BinaryOperator::GreaterThan) {
return false;
}
match (&e.left, &e.right) {
(Expression::UnaryExpression(unary), literal)
| (literal, Expression::UnaryExpression(unary)) => {
unary.operator.is_typeof() && literal.is_specific_string_literal("u")
}
_ => false,
}
}
/// Apply De Morgan's law in place. Only called on chains approved by
/// [`Self::de_morgan_paren_delta`].
fn de_morgan_invert_logical(
@@ -29,6 +29,9 @@ fn minimize_nots_with_de_morgan_comparison_chains() {
test("if (!(a === b || c === d)) throw x;", "if (a !== b && c !== d) throw x;");
// `&&` dual.
test("if (!(a == b && c == d)) throw x;", "if (a != b || c != d) throw x;");
// typeof
test("if (!(typeof a < 'u' && !b)) throw x;", "if (typeof a > 'u' || b) throw x;");
test("if (!(typeof a != 'undefined' && !b)) throw x;", "if (typeof a > 'u' || b) throw x;");
// The fold is involutive, so the `if (!x) return` collapse (which negates
// the test again) still reaches its old output.
test(
@@ -55,6 +58,7 @@ fn minimize_nots_with_de_morgan_comparison_chains() {
fn minimize_nots_with_de_morgan_negative_cases() {
// Relational comparisons don't invert freely (NaN), so the chain must stay.
test_same("if (!(a < b || c < d)) throw x;");
test_same("if (!(typeof a < 'u' || c < d)) throw x;");
// A mixed operand would need a bare `!`; that fold is not involutive and can
// regress shapes whose test is negated again later (e.g. branch swaps), so
// it's left alone.
@@ -81,6 +85,9 @@ fn minimize_nots_with_de_morgan_negative_cases() {
#[test]
fn minimize_nots_with_binary_expressions() {
test_same("var v = !(x > 'u');");
test_same("var v = !(x > 0);");
test_same("var v = !(typeof x > typeof y);");
test("!(x === undefined)", "x");
test("!(typeof(x) === 'undefined')", "");
test("!(typeof(x()) === 'undefined')", "x()");
@@ -91,4 +98,10 @@ fn minimize_nots_with_binary_expressions() {
test("var k = !!(foo instanceof bar)", "var k = foo instanceof bar");
test("!(a === 1 ? void 0 : a.b)", "a !== 1 && a.b;");
test("!(a, b)", "a, b");
test("var v = !(typeof x < 'u')", "var v = typeof x > 'u';");
test("var v = !('u' > typeof x)", "var v = 'u' < typeof x;");
test_same("var v = !(typeof x <= 'u');");
test_same("var v = !(typeof x >= 'u');");
test_same("var v = !(typeof x < 'string');");
test_same("var v = !(typeof x > 'string');");
}
+1 -1
View File
@@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
3.20 MB | 958.89 kB | 1.01 MB | 317.07 kB | 331.56 kB | 2 | echarts.js
6.69 MB | 2.12 MB | 2.31 MB | 453.44 kB | 488.28 kB | 5 | antd.js
6.69 MB | 2.12 MB | 2.31 MB | 453.42 kB | 488.28 kB | 5 | antd.js
10.95 MB | 3.31 MB | 3.49 MB | 849.97 kB | 915.50 kB | 4 | typescript.js
@@ -49,7 +49,7 @@ antd.js:
sys deallocs: 1038
sys alloc bytes: 29976375 # 29.98 MB
sys peak growth: 19934749 # 19.93 MB
arena allocs: 207576
arena allocs: 207570
arena reallocs: 75941
arena size: 45456840 # 45.46 MB