feat(minifier): replace Number.*_SAFE_INTEGER/Number.EPSILON (#8682)

The value of `Number.*_SAFE_INTEGER`, `Number.EPSILON` are constants as they cannot be changed. This PR replaces them with `2**53-1` / `-(2**53-1)` / `2**-52` for ES2016+. For ES2015, `Number.EPSILON` is not changed but `Number.*_SAFE_INTEGER`s are replaced with `9007199254740991` / `-9007199254740991`.

**Reference**
- Spec of [`Number.MAX_SAFE_INTEGER`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.max_safe_integer)
- Spec of [`Number.MIN_SAFE_INTEGER`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.min_safe_integer)
- Spec of [`Number.EPSILON`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.epsilon)

### Additional Information
- [`Number.MIN_VALUE`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.min_value) cannot be replaced as the value depends on the runtime
- [`Number.MAX_VALUE`](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.max_value) can be replaced but I didn't come up with a shorter representation that does not lack precision
This commit is contained in:
sapphi-red 2025-01-24 03:47:52 +00:00
parent 0c5bb30859
commit 343690e178
No known key found for this signature in database
GPG key ID: 67631A259A77AC6C
2 changed files with 91 additions and 24 deletions

View file

@ -7,6 +7,8 @@ use oxc_ecmascript::{
constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf,
StringLastIndexOf, StringSubstring, ToInt32, StringLastIndexOf, StringSubstring, ToInt32,
}; };
use oxc_span::SPAN;
use oxc_syntax::es_target::ESTarget;
use oxc_traverse::{Ancestor, TraverseCtx}; use oxc_traverse::{Ancestor, TraverseCtx};
use crate::ctx::Ctx; use crate::ctx::Ctx;
@ -491,10 +493,15 @@ impl<'a> PeepholeOptimizations {
} }
_ => return, _ => return,
}; };
let replacement = match name { let Expression::Identifier(ident) = object else { return };
"POSITIVE_INFINITY" | "NEGATIVE_INFINITY" | "NaN" => {
Self::try_fold_number_constants(object, name, span, ctx) let ctx = &mut Ctx(ctx);
} if !ctx.is_global_reference(ident) {
return;
}
let replacement = match ident.name.as_str() {
"Number" => self.try_fold_number_constants(name, span, ctx),
_ => None, _ => None,
}; };
if let Some(replacement) = replacement { if let Some(replacement) = replacement {
@ -505,28 +512,68 @@ impl<'a> PeepholeOptimizations {
/// replace `Number.*` constants /// replace `Number.*` constants
fn try_fold_number_constants( fn try_fold_number_constants(
object: &Expression<'a>, &self,
name: &str, name: &str,
span: Span, span: Span,
ctx: &mut TraverseCtx<'a>, ctx: &mut Ctx<'a, '_>,
) -> Option<Expression<'a>> { ) -> Option<Expression<'a>> {
let ctx = Ctx(ctx); let num = |span: Span, n: f64| {
let Expression::Identifier(ident) = object else { return None }; ctx.ast.expression_numeric_literal(span, n, None, NumberBase::Decimal)
if ident.name != "Number" || !ctx.is_global_reference(ident) { };
return None; // [neg] base ** exponent [op] a
} let pow_with_expr =
|span: Span, base: f64, exponent: f64, op: BinaryOperator, a: f64| -> Expression<'a> {
ctx.ast.expression_binary(
span,
ctx.ast.expression_binary(
SPAN,
num(SPAN, base),
BinaryOperator::Exponential,
num(SPAN, exponent),
),
op,
num(SPAN, a),
)
};
Some(match name { Some(match name {
"POSITIVE_INFINITY" => { "POSITIVE_INFINITY" => num(span, f64::INFINITY),
ctx.ast.expression_numeric_literal(span, f64::INFINITY, None, NumberBase::Decimal) "NEGATIVE_INFINITY" => num(span, f64::NEG_INFINITY),
"NaN" => num(span, f64::NAN),
"MAX_SAFE_INTEGER" => {
#[allow(clippy::cast_precision_loss)]
if self.target < ESTarget::ES2016 {
num(span, 2.0f64.powf(53.0) - 1.0)
} else {
// 2**53 - 1
pow_with_expr(span, 2.0, 53.0, BinaryOperator::Subtraction, 1.0)
}
}
"MIN_SAFE_INTEGER" => {
#[allow(clippy::cast_precision_loss)]
if self.target < ESTarget::ES2016 {
num(span, -(2.0f64.powf(53.0) - 1.0))
} else {
// -(2**53 - 1)
ctx.ast.expression_unary(
span,
UnaryOperator::UnaryNegation,
pow_with_expr(SPAN, 2.0, 53.0, BinaryOperator::Subtraction, 1.0),
)
}
}
"EPSILON" => {
if self.target < ESTarget::ES2016 {
return None;
}
// 2**-52
ctx.ast.expression_binary(
span,
num(SPAN, 2.0),
BinaryOperator::Exponential,
num(SPAN, -52.0),
)
} }
"NEGATIVE_INFINITY" => ctx.ast.expression_numeric_literal(
span,
f64::NEG_INFINITY,
None,
NumberBase::Decimal,
),
"NaN" => ctx.ast.expression_numeric_literal(span, f64::NAN, None, NumberBase::Decimal),
_ => return None, _ => return None,
}) })
} }
@ -535,7 +582,17 @@ impl<'a> PeepholeOptimizations {
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java> /// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::tester::{test, test_same}; use oxc_syntax::es_target::ESTarget;
use crate::{
tester::{run, test, test_same},
CompressOptions,
};
fn test_es2015(code: &str, expected: &str) {
let opts = CompressOptions { target: ESTarget::ES2015, ..CompressOptions::default() };
assert_eq!(run(code, Some(opts)), run(expected, None));
}
#[test] #[test]
fn test_string_index_of() { fn test_string_index_of() {
@ -1410,9 +1467,19 @@ mod test {
test("v = Number.POSITIVE_INFINITY", "v = Infinity"); test("v = Number.POSITIVE_INFINITY", "v = Infinity");
test("v = Number.NEGATIVE_INFINITY", "v = -Infinity"); test("v = Number.NEGATIVE_INFINITY", "v = -Infinity");
test("v = Number.NaN", "v = NaN"); test("v = Number.NaN", "v = NaN");
test("v = Number.MAX_SAFE_INTEGER", "v = 2**53-1");
test("v = Number.MIN_SAFE_INTEGER", "v = -(2**53-1)");
test("v = Number.EPSILON", "v = 2**-52");
test_same("Number.POSITIVE_INFINITY = 1"); test_same("Number.POSITIVE_INFINITY = 1");
test_same("Number.NEGATIVE_INFINITY = 1"); test_same("Number.NEGATIVE_INFINITY = 1");
test_same("Number.NaN = 1"); test_same("Number.NaN = 1");
test_same("Number.MAX_SAFE_INTEGER = 1");
test_same("Number.MIN_SAFE_INTEGER = 1");
test_same("Number.EPSILON = 1");
test_es2015("v = Number.MAX_SAFE_INTEGER", "v = 9007199254740991");
test_es2015("v = Number.MIN_SAFE_INTEGER", "v = -9007199254740991");
test_es2015("v = Number.EPSILON", "v = Number.EPSILON");
} }
} }

View file

@ -15,13 +15,13 @@ Original | minified | minified | gzip | gzip | Fixture
1.01 MB | 460.16 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js 1.01 MB | 460.16 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js
1.25 MB | 652.85 kB | 646.76 kB | 163.53 kB | 163.73 kB | three.js 1.25 MB | 652.68 kB | 646.76 kB | 163.48 kB | 163.73 kB | three.js
2.14 MB | 723.96 kB | 724.14 kB | 179.91 kB | 181.07 kB | victory.js 2.14 MB | 723.85 kB | 724.14 kB | 179.88 kB | 181.07 kB | victory.js
3.20 MB | 1.01 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js 3.20 MB | 1.01 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js
6.69 MB | 2.31 MB | 2.31 MB | 491.94 kB | 488.28 kB | antd.js 6.69 MB | 2.31 MB | 2.31 MB | 491.91 kB | 488.28 kB | antd.js
10.95 MB | 3.48 MB | 3.49 MB | 905.29 kB | 915.50 kB | typescript.js 10.95 MB | 3.48 MB | 3.49 MB | 905.29 kB | 915.50 kB | typescript.js