From f9ae70c74ae9842e8046d4489cdf513325b698da Mon Sep 17 00:00:00 2001 From: 7086cmd <54303040+7086cmd@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:41:05 +0000 Subject: [PATCH] feat(minifier): minify basic arithmetic calculations. (#6280) It uses to_string to check which is shorter, which is extremely tough. Waiting for further refactor. --- .../src/ast_passes/peephole_fold_constants.rs | 125 +++++++++++++----- tasks/minsize/minsize.snap | 10 +- 2 files changed, 100 insertions(+), 35 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 825ee6747d07a..bdd7367a10284 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::ops::Neg; use num_bigint::BigInt; +use num_traits::Zero; use oxc_ast::ast::*; use oxc_span::{GetSpan, Span, SPAN}; use oxc_syntax::{ @@ -19,6 +20,9 @@ use crate::{ CompressorPass, }; +static MAX_SAFE_FLOAT: f64 = 9_007_199_254_740_991_f64; +static NEG_MAX_SAFE_FLOAT: f64 = -9_007_199_254_740_991_f64; + /// Constant Folding /// /// @@ -67,6 +71,19 @@ impl<'a> PeepholeFoldConstants { Self { changed: false } } + fn try_get_number_literal_value(&self, expr: &mut Expression<'a>) -> Option { + match expr { + Expression::NumericLiteral(n) => Some(n.value), + Expression::UnaryExpression(unary) + if unary.operator == UnaryOperator::UnaryNegation => + { + let Expression::NumericLiteral(arg) = &mut unary.argument else { return None }; + Some(-arg.value) + } + _ => None, + } + } + fn try_fold_useless_object_dot_define_properties_call( &mut self, _call_expr: &mut CallExpression<'a>, @@ -409,13 +426,9 @@ impl<'a> PeepholeFoldConstants { BinaryOperator::Subtraction | BinaryOperator::Division | BinaryOperator::Remainder - | BinaryOperator::Exponential => { - self.try_fold_arithmetic_op(e.span, &e.left, &e.right, ctx) - } - BinaryOperator::Multiplication - | BinaryOperator::BitwiseAnd - | BinaryOperator::BitwiseOR - | BinaryOperator::BitwiseXOR => { + | BinaryOperator::Multiplication + | BinaryOperator::Exponential => self.try_fold_arithmetic_op(e, ctx), + BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => { // TODO: // self.try_fold_arithmetic_op(e.span, &e.left, &e.right, ctx) // if (result != subtree) { @@ -475,14 +488,65 @@ impl<'a> PeepholeFoldConstants { } } - fn try_fold_arithmetic_op<'b>( + fn try_fold_arithmetic_op( &self, - _span: Span, - _left: &'b Expression<'a>, - _right: &'b Expression<'a>, - _ctx: &mut TraverseCtx<'a>, + operation: &mut BinaryExpression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Option> { - None + fn shorter_than_original(result: f64, left: f64, right: f64) -> bool { + if result > MAX_SAFE_FLOAT + || result < NEG_MAX_SAFE_FLOAT + || result.is_nan() + || result.is_infinite() + { + return false; + } + let result_str = result.to_string().len(); + let original_str = left.to_string().len() + right.to_string().len() + 1; + result_str <= original_str + } + if !operation.operator.is_arithmetic() { + return None; + }; + let left = self.try_get_number_literal_value(&mut operation.left)?; + let right = self.try_get_number_literal_value(&mut operation.right)?; + if !left.is_finite() || !right.is_finite() { + return None; + } + let result = match operation.operator { + BinaryOperator::Addition => left + right, + BinaryOperator::Subtraction => left - right, + BinaryOperator::Multiplication => { + let result = left * right; + if shorter_than_original(result, left, right) { + result + } else { + return None; + } + } + BinaryOperator::Division if !right.is_zero() => { + if right == 0.0 { + return None; + } + let result = left / right; + if shorter_than_original(result, left, right) { + result + } else { + return None; + } + } + BinaryOperator::Remainder if !right.is_zero() && right.is_finite() => left % right, + // TODO BinaryOperator::Exponential if + _ => return None, + }; + let number_base = + if is_exact_int64(result) { NumberBase::Decimal } else { NumberBase::Float }; + Some(ctx.ast.expression_numeric_literal( + operation.span, + result, + result.to_string(), + number_base, + )) } fn try_fold_instanceof<'b>( @@ -870,8 +934,12 @@ impl<'a> PeepholeFoldConstants { /// #[cfg(test)] mod test { + use super::{MAX_SAFE_FLOAT, NEG_MAX_SAFE_FLOAT}; use oxc_allocator::Allocator; + static MAX_SAFE_INT: i64 = 9_007_199_254_740_991_i64; + static NEG_MAX_SAFE_INT: i64 = -9_007_199_254_740_991_i64; + use crate::tester; fn test(source_text: &str, expected: &str) { @@ -1232,18 +1300,14 @@ mod test { test("-1n > -0.9", "false"); // Don't fold unsafely large numbers because there might be floating-point error - let max_safe_int = 9_007_199_254_740_991_i64; - let neg_max_safe_int = -9_007_199_254_740_991_i64; - let max_safe_float = 9_007_199_254_740_991_f64; - let neg_max_safe_float = -9_007_199_254_740_991_f64; - test(&format!("0n > {max_safe_int}"), "false"); - test(&format!("0n < {max_safe_int}"), "true"); - test(&format!("0n > {neg_max_safe_int}"), "true"); - test(&format!("0n < {neg_max_safe_int}"), "false"); - test(&format!("0n > {max_safe_float}"), "false"); - test(&format!("0n < {max_safe_float}"), "true"); - test(&format!("0n > {neg_max_safe_float}"), "true"); - test(&format!("0n < {neg_max_safe_float}"), "false"); + test(&format!("0n > {MAX_SAFE_INT}"), "false"); + test(&format!("0n < {MAX_SAFE_INT}"), "true"); + test(&format!("0n > {NEG_MAX_SAFE_INT}"), "true"); + test(&format!("0n < {NEG_MAX_SAFE_INT}"), "false"); + test(&format!("0n > {MAX_SAFE_FLOAT}"), "false"); + test(&format!("0n < {MAX_SAFE_FLOAT}"), "true"); + test(&format!("0n > {NEG_MAX_SAFE_FLOAT}"), "true"); + test(&format!("0n < {NEG_MAX_SAFE_FLOAT}"), "false"); // comparing with Infinity is allowed test("1n < Infinity", "true"); @@ -1574,7 +1638,6 @@ mod test { } #[test] - #[ignore] fn test_fold_arithmetic() { test("x = 10 + 20", "x = 30"); test("x = 2 / 4", "x = 0.5"); @@ -1586,10 +1649,12 @@ mod test { test("x = 3 % -2", "x = 1"); test("x = -1 % 3", "x = -1"); test_same("x = 1 % 0"); - test("x = 2 ** 3", "x = 8"); - test("x = 2 ** -3", "x = 0.125"); - test_same("x = 2 ** 55"); // backs off folding because 2 ** 55 is too large - test_same("x = 3 ** -1"); // backs off because 3**-1 is shorter than 0.3333333333333333 + // We should not fold this because it's not safe to fold. + test_same(format!("x = {} * {}", MAX_SAFE_INT / 2, MAX_SAFE_INT / 2).as_str()); + // test("x = 2 ** 3", "x = 8"); + // test("x = 2 ** -3", "x = 0.125"); + // test_same("x = 2 ** 55"); // backs off folding because 2 ** 55 is too large + // test_same("x = 3 ** -1"); // backs off because 3**-1 is shorter than 0.3333333333333333 } #[test] diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 769e17f84d398..b57a716d1f04f 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -2,7 +2,7 @@ Original | Minified | esbuild | Gzip | esbuild 72.14 kB | 24.46 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js -173.90 kB | 61.69 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js +173.90 kB | 61.68 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js 287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js @@ -10,17 +10,17 @@ Original | Minified | esbuild | Gzip | esbuild 544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js -555.77 kB | 278.24 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js +555.77 kB | 278.23 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js 1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js -1.25 MB | 670.97 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js +1.25 MB | 670.96 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js 2.14 MB | 756.33 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js -3.20 MB | 1.05 MB | 1.01 MB | 334.10 kB | 331.56 kB | echarts.js +3.20 MB | 1.05 MB | 1.01 MB | 334.07 kB | 331.56 kB | echarts.js -6.69 MB | 2.44 MB | 2.31 MB | 498.86 kB | 488.28 kB | antd.js +6.69 MB | 2.44 MB | 2.31 MB | 498.88 kB | 488.28 kB | antd.js 10.95 MB | 3.59 MB | 3.49 MB | 913.92 kB | 915.50 kB | typescript.js