fix(ecmascript): fold ** with Number::exponentiate, not IEEE pow (#25644)

`f64::powf` implements IEEE 754 `pow`, which returns `1` for a base of
`1` whatever the exponent.
[`Number::exponentiate`](https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate)
instead returns `NaN` when the exponent is `NaN` (step 1), and when the
base has magnitude `1` and the exponent is infinite (steps 9-10).

Constant folding called `powf` directly, so:

```js
1 ** Infinity      // spec: NaN    oxc folded to: 1
(-1) ** Infinity   // spec: NaN    oxc folded to: 1
1 ** -Infinity     // spec: NaN    oxc folded to: 1
1 ** NaN           // spec: NaN    oxc folded to: 1
```

`1` is truthy and `NaN` is not, so an `if` guarded by such an expression
takes the wrong branch.

The one case the two agree on — an exponent of `0` returning `1`
whatever the base, including `NaN ** 0` and `Infinity ** 0` — is
unchanged, since `powf` already gets it right and the spec checks the
exponent before the base.

`cargo minsize` shows no snapshot change.

Found by the minifier fuzzer (#25594).

---

Written with AI assistance (Claude Code). Reviewed by me, and every
expected value in the added test was checked against Node.js.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kotaro ChikubaandClaude Opus 5 authored and GitHub committed 2026-08-17 12:21:54 +08:00
1 parent b846abc2a4
commit f6000ac141
2 files changed
+38 -1

No files matched your search

@@ -263,7 +263,22 @@ fn binary_operation_evaluate_value_to<'a>(
BinaryOperator::Exponential => {
let lval = left.evaluate_value_to_number(ctx)?;
let rval = right.evaluate_value_to_number(ctx)?;
let result = lval.powf(rval);
// `Number::exponentiate` is not IEEE 754 `pow`, which `f64::powf`
// implements. `pow` returns `1` for a base of `1` whatever the
// exponent, and for an exponent of `0` whatever the base; the
// abstract operation instead returns `NaN` when the exponent is
// `NaN`, and when the base has magnitude `1` and the exponent is
// infinite. Only the exponent-is-zero case is shared, and `powf`
// already agrees there.
// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation>
// <https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate>
#[expect(clippy::float_cmp)]
let base_is_one = lval.abs() == 1.0;
let result = if rval.is_nan() || (base_is_one && rval.is_infinite()) {
f64::NAN
} else {
lval.powf(rval)
};
// For now, ignore the result if it large or has a decimal part
// so that the output does not become bigger than the input.
if result.is_finite() && (result.fract() != 0.0 || result.log10() > 4.0) {
@@ -1139,6 +1139,28 @@ fn test_fold_exponential() {
fold("x = null ** 0", "x = 1");
}
/// `Number::exponentiate` is not IEEE 754 `pow`. It returns `NaN` whenever the
/// exponent is `NaN`, and whenever the base has magnitude `1` and the exponent
/// is infinite — both cases where `pow` is specified to return `1`.
///
/// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation>
#[test]
fn test_fold_exponential_disagrees_with_ieee_pow() {
fold("x = 1 ** Infinity", "x = NaN");
fold("x = 1 ** -Infinity", "x = NaN");
fold("x = (-1) ** Infinity", "x = NaN");
fold("x = (-1) ** -Infinity", "x = NaN");
fold("x = true ** Infinity", "x = NaN");
fold("x = 1 ** NaN", "x = NaN");
fold("x = (-1) ** NaN", "x = NaN");
// The cases `pow` and `Number::exponentiate` agree on.
fold("x = 2 ** Infinity", "x = Infinity");
fold("x = 2 ** NaN", "x = NaN");
fold("x = NaN ** 0", "x = 1");
fold("x = Infinity ** 0", "x = 1");
}
#[test]
fn test_fold_arithmetic_undefined_null_operands() {
// `undefined` has no literal form (it prints as `void 0`), so it never