feat(minifier): fold safe integer division and remainder (#24981)

Finite division and remainder expressions remain uncompressed even when their Number result is an exact safe integer and a shorter literal can represent it. For example, `360 / 360` and `3 % 2` remain as binary expressions.

Fold these cases only when both operands have side-effect-free numeric values, the result is within the Number safe-integer range, and the serialized literal is no longer than a conservative lower bound for the original expression. The existing `NaN` and `Infinity` paths remain unchanged. This preserves `-0` while rejecting BigInt, fractional or unsafe results, observable coercions, and size regressions.

## Example

Input:

```js
use(360 / 360);
```

Before:

```js
use(360 / 360);
```

After:

```js
use(1);
```

The minsize corpus has no change at its reported 0.01 kB precision. The tracked kitchen-sink fixture records four additional arena node allocations from the new replacements; heap metrics and arena size are unchanged.

Verified with the full `oxc_minifier` test suite, `just minsize`, Clippy with warnings denied, and formatting.

Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
This commit is contained in:
Dunqing committed 2026-07-27 15:10:38 +00:00
1 parent fc702c1fa9
commit 5eadfdba26
5 files changed
+59 -14

No files matched your search

@@ -354,12 +354,16 @@ impl<'a> PeepholeOptimizations {
BinaryOperator::Multiplication
| BinaryOperator::Exponential
| BinaryOperator::Remainder => {
let shorter_multiplication = if e.operator == BinaryOperator::Multiplication {
Self::try_fold_shorter_numeric_expression(e, ctx)
} else {
None
let shorter_numeric_expression = match e.operator {
BinaryOperator::Multiplication => {
Self::try_fold_shorter_numeric_expression(e, ctx)
}
BinaryOperator::Remainder => {
Self::try_fold_safe_integer_numeric_expression(e, ctx)
}
_ => None,
};
shorter_multiplication.or_else(|| {
shorter_numeric_expression.or_else(|| {
Self::extract_numeric_values(e, ctx)
.filter(|(left, right)| {
*left == 0.0
@@ -378,9 +382,12 @@ impl<'a> PeepholeOptimizations {
.and_then(|_| ctx.eval_binary(e))
})
}
BinaryOperator::Division => Self::extract_numeric_values(e, ctx)
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
.and_then(|_| ctx.eval_binary(e)),
BinaryOperator::Division => Self::try_fold_safe_integer_numeric_expression(e, ctx)
.or_else(|| {
Self::extract_numeric_values(e, ctx)
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
.and_then(|_| ctx.eval_binary(e))
}),
BinaryOperator::ShiftLeft => {
Self::extract_numeric_values(e, ctx).and_then(|(left, right)| {
let result = e.evaluate_value(ctx)?.into_number()?;
@@ -479,6 +486,17 @@ impl<'a> PeepholeOptimizations {
.then(|| ctx.value_to_expr(e.span, ConstantValue::Number(result)))
}
fn try_fold_safe_integer_numeric_expression(
e: &BinaryExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
Self::extract_numeric_values(e, ctx)?;
let result = e.evaluate_value(ctx)?.into_number()?;
let is_safe_integer = result.fract() == 0.0 && result.abs() <= 9_007_199_254_740_991.0;
(is_safe_integer && Self::folded_numeric_expression_is_shorter(e, result) == Some(true))
.then(|| ctx.value_to_expr(e.span, ConstantValue::Number(result)))
}
fn folded_numeric_expression_is_shorter(e: &BinaryExpression<'a>, result: f64) -> Option<bool> {
let original_len = Self::binary_numeric_expression_size_lower_bound(e)?;
Some(Self::number_literal_source_len(result)? <= original_len)
@@ -933,7 +933,7 @@ fn constant_evaluation_test() {
test("x = 3 - 6", "x = -3;");
test("x = 3 * 6", "x = 18;");
test("x = 3 / 6", "x = 3 / 6;");
test("x = 3 % 6", "x = 3 % 6;");
test("x = 3 % 6", "x = 3;");
test("x = 3 ** 6", "x = 3 ** 6;");
test("x = 0 / 0", "x = NaN;");
test("x = 123 / 0", "x = Infinity;");
@@ -1064,17 +1064,44 @@ fn test_fold_division() {
fold("x = Infinity / 0", "x = Infinity");
fold("x = 1 / 0", "x = Infinity");
fold("x = 0 / 0", "x = NaN");
fold("x = 360 / 360", "x = 1");
fold("x = 10.5 / 0.75", "x = 14");
fold("x = -10.5 / 0.75", "x = -14");
fold("x = 0 / -1", "x = -0");
fold("x = -0 / 1", "x = -0");
fold("x = -5e-324 / 2", "x = -0");
fold("x = 9007199254740992 / 2", "x = 4503599627370496");
fold_same("x = 2 / 4");
fold_same("x = 0.3 / 0.1");
fold_same("x = 1e-323 / 2");
fold_same("x = 1 / 1e-15");
fold_same("x = 9007199254740991 / 0.5");
fold_same("x = f() / 2");
fold_same("x = (void f()) / 1");
fold_same("x = ({ valueOf: f }) / 2");
fold_same("x = 4n / 2n");
fold_same("x = 4n / 2");
fold_same("x = 4n / 0n");
fold_same("x = y / 2 / 4");
}
#[test]
fn test_fold_remainder() {
fold_same("x = 3 % 2");
fold_same("x = 3 % -2");
fold_same("x = -1 % 3");
fold("x = 3 % 2", "x = 1");
fold("x = 3 % -2", "x = 1");
fold("x = -1 % 3", "x = -1");
fold("x = -1 % 1", "x = -0");
fold("x = 5.5 % 1.5", "x = 1");
fold("x = 1 % 0", "x = NaN");
fold("x = 0 % 0", "x = NaN");
fold_same("x = 18014398509481982 % 18014398509481984");
fold_same("x = 0.3 % 0.1");
fold_same("x = f() % 2");
fold_same("x = 1 % f()");
fold_same("x = 5n % 2n");
fold_same("x = 4n % 3n");
}
#[test]
@@ -121,7 +121,7 @@ fn test_mathematical_expression_edge_cases() {
test("return 2 + 3", "return 5");
test("return 10 - 4", "return 6");
test("return 3 * 7", "return 21");
test_same("return 15 / 3"); // division might not be optimized consistently
test("return 15 / 3", "return 5");
// Test cases that are eliminated as dead code (unused expressions)
test("NaN + 1", ""); // eliminated as unused expression
@@ -71,7 +71,7 @@ kitchen-sink.tsx:
sys deallocs: 1858
sys alloc bytes: 5338078 # 5.34 MB
sys peak growth: 3737991 # 3.74 MB
arena allocs: 71926
arena allocs: 71930
arena reallocs: 12252
arena size: 9561752 # 9.56 MB