feat(minifier): fold safe integer exponentiation (#25009)

Integer exponentiation remains uncompressed even when its Number result is an exact safe integer and a shorter literal can represent it. This also prevents the existing `Math.pow` rewrite from completing beyond the exponentiation form.

Fold these cases only when both operands have side-effect-free integer-valued 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. Number exponentiation is implementation-approximated, so fractional operands remain unchanged. BigInt, observable coercions, side effects, unsafe results, and size regressions are rejected.

## Example

Input:

```js
use(2 ** 3);
```

Before:

```js
use(2 ** 3);
```

After:

```js
use(8);
```

The minsize corpus reduces Three.js by 2 raw bytes and 1 byte with gzip, below the reported 0.01 kB snapshot precision. The tracked kitchen-sink fixture records six additional arena node allocations from the new replacement; heap metrics and arena size are unchanged.

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

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-28 01:41:03 +00:00
1 parent 73fc747699
commit 70d6ecedb3
5 files changed
+25 -4

No files matched your search

@@ -358,6 +358,11 @@ impl<'a> PeepholeOptimizations {
BinaryOperator::Multiplication => {
Self::try_fold_shorter_numeric_expression(e, ctx)
}
// Number exponentiation is implementation-approximated, so only fold
// integer-valued operands where the result is an exact safe integer.
BinaryOperator::Exponential => Self::extract_numeric_values(e, ctx)
.filter(|(base, exponent)| base.fract() == 0.0 && exponent.fract() == 0.0)
.and_then(|_| Self::try_fold_safe_integer_numeric_expression(e, ctx)),
BinaryOperator::Remainder => {
Self::try_fold_safe_integer_numeric_expression(e, ctx)
}
@@ -934,7 +934,7 @@ fn constant_evaluation_test() {
test("x = 3 * 6", "x = 18;");
test("x = 3 / 6", "x = 3 / 6;");
test("x = 3 % 6", "x = 3;");
test("x = 3 ** 6", "x = 3 ** 6;");
test("x = 3 ** 6", "x = 729;");
test("x = 0 / 0", "x = NaN;");
test("x = 123 / 0", "x = Infinity;");
test("x = 123 / -0", "x = -Infinity;");
@@ -1106,10 +1106,26 @@ fn test_fold_remainder() {
#[test]
fn test_fold_exponential() {
fold_same("x = 2 ** 3");
fold("x = 2 ** 3", "x = 8");
fold("x = 10 ** 4", "x = 1e4");
fold("x = (-2) ** 3", "x = -8");
fold_same("x = 0.5 ** -2");
fold_same("x = 4 ** 0.5");
fold_same("x = (-5e-324) ** 3");
fold_same("x = 2 ** -3");
fold_same("x = 2 ** 50");
fold_same("x = 2 ** 55");
fold_same("x = 1e8 ** 2");
fold_same("x = 3 ** -1");
fold_same("x = f() ** 2");
fold_same("x = 2 ** f()");
fold_same("x = ({ valueOf: f }) ** 2");
fold_same("x = 2n ** 3n");
fold_same("x = 2n ** 3");
fold_same("x = 2 ** 3n");
fold_same("x = (void f()) ** 0");
test_same("function f(Infinity) {\n\treturn Infinity ** 0;\n}");
fold_same("x = (-1) ** 0.5");
fold("x = (-0) ** 3", "x = -0");
fold("x = null ** 0", "x = 1");
@@ -987,7 +987,7 @@ fn test_to_string() {
#[test]
fn test_fold_pow() {
test("v = Math.pow(2, 3)", "v = 2 ** 3");
test("v = Math.pow(2, 3)", "v = 8");
test("v = Math.pow(a, 3)", "v = a ** 3");
test("v = Math.pow(2, b)", "v = 2 ** b");
test("v = Math.pow(a, b)", "v = a ** +b");
@@ -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: 71930
arena allocs: 71936
arena reallocs: 12252
arena size: 9561752 # 9.56 MB