From 343690e1781773a0c3a7813c38e9a4fc95ec544e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:47:52 +0000 Subject: [PATCH] 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 --- .../src/peephole/replace_known_methods.rs | 109 ++++++++++++++---- tasks/minsize/minsize.snap | 6 +- 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index e1463f8a6b831..e377b7b4ac8cc 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -7,6 +7,8 @@ use oxc_ecmascript::{ constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, StringLastIndexOf, StringSubstring, ToInt32, }; +use oxc_span::SPAN; +use oxc_syntax::es_target::ESTarget; use oxc_traverse::{Ancestor, TraverseCtx}; use crate::ctx::Ctx; @@ -491,10 +493,15 @@ impl<'a> PeepholeOptimizations { } _ => return, }; - let replacement = match name { - "POSITIVE_INFINITY" | "NEGATIVE_INFINITY" | "NaN" => { - Self::try_fold_number_constants(object, name, span, ctx) - } + let Expression::Identifier(ident) = object else { return }; + + 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, }; if let Some(replacement) = replacement { @@ -505,28 +512,68 @@ impl<'a> PeepholeOptimizations { /// replace `Number.*` constants fn try_fold_number_constants( - object: &Expression<'a>, + &self, name: &str, span: Span, - ctx: &mut TraverseCtx<'a>, + ctx: &mut Ctx<'a, '_>, ) -> Option> { - let ctx = Ctx(ctx); - let Expression::Identifier(ident) = object else { return None }; - if ident.name != "Number" || !ctx.is_global_reference(ident) { - return None; - } + let num = |span: Span, n: f64| { + ctx.ast.expression_numeric_literal(span, n, None, NumberBase::Decimal) + }; + // [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 { - "POSITIVE_INFINITY" => { - ctx.ast.expression_numeric_literal(span, f64::INFINITY, None, NumberBase::Decimal) + "POSITIVE_INFINITY" => num(span, f64::INFINITY), + "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, }) } @@ -535,7 +582,17 @@ impl<'a> PeepholeOptimizations { /// Port from: #[cfg(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] fn test_string_index_of() { @@ -1410,9 +1467,19 @@ mod test { test("v = Number.POSITIVE_INFINITY", "v = Infinity"); test("v = Number.NEGATIVE_INFINITY", "v = -Infinity"); 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.NEGATIVE_INFINITY = 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"); } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index e8495b1e4244f..315dcfb758597 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -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.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 -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