feat(minifier): shorten integer zero comparisons (#24933)

Integer-valued comparisons with zero remain verbose in value contexts, even when the minifier can prove that the left-hand side is an `int32` or `uint32`. Applying the same rewrite to every Number-valued expression would be incorrect for `NaN`, while bitwise expressions with unknown operands may still evaluate as BigInt.

Reuse the existing `IsInt32OrUint32` proof to lower equality with zero to logical negation and inequality with zero to double negation. The optimization remains inside binary-expression compression and does not change statement transforms or DCE.

```js
// before
f((a & 1) != 0);
f((a ^ 2) === 0);

// after
f(!!(a & 1));
f(!(a ^ 2));
```

Verified with `just ready`, `just minsize`, and `just allocs`. On the latest `main`, the minsize snapshot improves Three.js raw output from 642.46 kB to 642.45 kB and TypeScript GZIP output from 852.94 kB to 852.81 kB; allocations and compression-pass iterations are unchanged.

AI disclosure: Codex was used to research, implement, and verify this change.
This commit is contained in:
Dunqing committed 2026-07-26 08:41:45 +00:00
1 parent 2ee6e7a759
commit e9e0f24dca
3 files changed
+37 -5

No files matched your search

@@ -2,7 +2,7 @@ use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_compat::ESFeature;
use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType},
constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType, IsInt32OrUint32},
side_effects::MayHaveSideEffects,
};
use oxc_semantic::ReferenceFlags;
@@ -65,13 +65,29 @@ impl<'a> PeepholeOptimizations {
// `a instanceof b === true` -> `a instanceof b`
// `a instanceof b === false` -> `!(a instanceof b)`
// ^^^^^^^^^^^^^^ `ctx.expression_value_type(&e.left).is_boolean()` is `true`.
// `x >> +y !== 0` -> `x >> +y`
// ^^^^^^^ ctx.expression_value_type(&e.left).is_number()` is `true`.
// `x >> +y !== 0` -> `!!(x >> +y)`
// ^^^^^^^ `e.left.is_int32_or_uint32(ctx)` is `true`.
pub fn minimize_binary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
if !e.operator.is_equality() {
return;
}
// `NaN == 0` and `!NaN` differ, so require proof that the left side is a non-NaN Number.
if e.right.is_number_0() && e.left.is_int32_or_uint32(ctx) {
let argument = e.left.take_in(ctx);
let mut new_expr =
Expression::new_unary_expression(e.span, UnaryOperator::LogicalNot, argument, ctx);
if matches!(e.operator, BinaryOperator::Inequality | BinaryOperator::StrictInequality) {
new_expr = Expression::new_unary_expression(
e.span,
UnaryOperator::LogicalNot,
new_expr,
ctx,
);
}
ctx.replace_expression(expr, new_expr);
return;
}
let left = e.left.value_type(ctx);
let right = e.right.value_type(ctx);
if left.is_undetermined() || right.is_undetermined() {
@@ -1444,6 +1444,22 @@ fn try_minimize_binary() {
test("f(!a === !1)", "f(!!a)");
test("f(!a === true)", "f(!a)");
test("f(!a === false)", "f(!!a)");
test("f((a & 1) != 0)", "f(!!(a & 1))");
test("f((a & 2) == 0)", "f(!(a & 2))");
test("f((a | 1) !== 0)", "f(!!(a | 1))");
test("f((a ^ 2) === 0)", "f(!(a ^ 2))");
test("f((a >>> b) !== 0)", "f(!!(a >>> b))");
test("f(0 === (a & 4))", "f(!(a & 4))");
// Arithmetic can produce `NaN`, whose truthiness differs from comparison with zero.
test_same("f((a + b) != 0)");
test_same("f((a * 2) == 0)");
test("f(+a === 0)", "f(+a == 0)");
// Unknown bitwise operands can be BigInts, so their comparison must remain unchanged.
test_same("f((a & b) !== 0)");
test_same("f((a & 1n) !== 0)");
test_same("f((a & 1) != 1)");
}
#[test]
+2 -2
View File
@@ -15,7 +15,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
1.01 MB | 439.26 kB | 458.89 kB | 122.03 kB | 126.71 kB | 2 | bundle.min.js
1.25 MB | 642.46 kB | 646.76 kB | 159.37 kB | 163.73 kB | 2 | three.js
1.25 MB | 642.45 kB | 646.76 kB | 159.37 kB | 163.73 kB | 2 | three.js
2.14 MB | 710.90 kB | 724.14 kB | 160.38 kB | 181.07 kB | 2 | victory.js
@@ -23,5 +23,5 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
6.69 MB | 2.12 MB | 2.31 MB | 453.79 kB | 488.28 kB | 5 | antd.js
10.95 MB | 3.33 MB | 3.49 MB | 852.94 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.33 MB | 3.49 MB | 852.81 kB | 915.50 kB | 4 | typescript.js