mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
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:
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
|
||||
|
||||
Reference in new issue
Block a user