From f4cdc565779397905123716e18e5d91404178c43 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:30:38 +0000 Subject: [PATCH] refactor(minifier): use constant folding unary expression from `oxc_ecmascript` (#6647) --- crates/oxc_codegen/src/gen.rs | 2 +- crates/oxc_ecmascript/src/to_string.rs | 4 +- .../src/ast_passes/peephole_fold_constants.rs | 202 ++---------------- crates/oxc_minifier/src/node_util/mod.rs | 29 ++- tasks/minsize/minsize.snap | 12 +- 5 files changed, 43 insertions(+), 206 deletions(-) diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 138efb858a3b5..560f94466b9aa 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -1124,7 +1124,7 @@ impl<'a> GenExpr for NumericLiteral<'a> { if p.options.minify { p.print_str("1/0"); } else { - p.print_str("1 / 0"); + p.print_str("Infinity"); } }); } else if value.is_sign_positive() { diff --git a/crates/oxc_ecmascript/src/to_string.rs b/crates/oxc_ecmascript/src/to_string.rs index 8dcca1540f7b6..f48824974b999 100644 --- a/crates/oxc_ecmascript/src/to_string.rs +++ b/crates/oxc_ecmascript/src/to_string.rs @@ -62,8 +62,8 @@ impl<'a> ToJsString<'a> for IdentifierReference<'a> { impl<'a> ToJsString<'a> for NumericLiteral<'a> { fn to_js_string(&self) -> Option> { - // FIXME: to js number string - Some(Cow::Owned(self.value.to_string())) + use oxc_syntax::number::ToJsString; + Some(Cow::Owned(self.value.to_js_string())) } } 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 ed4614b6afe4f..7afac0db09558 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -1,5 +1,4 @@ use std::cmp::Ordering; -use std::ops::Neg; use num_bigint::BigInt; use num_traits::Zero; @@ -7,7 +6,7 @@ use num_traits::Zero; use oxc_ast::ast::*; use oxc_ecmascript::ToInt32; use oxc_ecmascript::{ - constant_evaluation::{IsLiteralValue, ValueType}, + constant_evaluation::{ConstantEvaluation, ValueType}, side_effects::MayHaveSideEffects, }; use oxc_span::{GetSpan, Span, SPAN}; @@ -57,7 +56,9 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants { Expression::ArrayExpression(e) => Self::try_flatten_array_expression(e, ctx), Expression::ObjectExpression(e) => Self::try_flatten_object_expression(e, ctx), Expression::BinaryExpression(e) => Self::try_fold_binary_expression(e, ctx), - Expression::UnaryExpression(e) => self.try_fold_unary_expression(e, ctx), + Expression::UnaryExpression(e) => { + ctx.eval_unary_expression(e).map(|v| ctx.value_to_expr(e.span, v)) + } // TODO: return tryFoldGetProp(subtree); Expression::LogicalExpression(e) => Self::try_fold_logical_expression(e, ctx), // TODO: tryFoldGetElem @@ -89,32 +90,6 @@ impl<'a, 'b> PeepholeFoldConstants { None } - /// Folds 'typeof(foo)' if foo is a literal, e.g. - /// `typeof("bar") --> "string"` - /// `typeof(6) --> "number"` - fn try_fold_type_of( - expr: &mut UnaryExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option> { - if !expr.argument.is_literal_value(/* include_function */ true) { - return None; - } - let s = match &mut expr.argument { - Expression::FunctionExpression(_) => "function", - Expression::StringLiteral(_) => "string", - Expression::NumericLiteral(_) => "number", - Expression::BooleanLiteral(_) => "boolean", - Expression::NullLiteral(_) - | Expression::ObjectExpression(_) - | Expression::ArrayExpression(_) => "object", - Expression::UnaryExpression(e) if e.operator == UnaryOperator::Void => "undefined", - Expression::BigIntLiteral(_) => "bigint", - Expression::Identifier(ident) if ctx.is_identifier_undefined(ident) => "undefined", - _ => return None, - }; - Some(ctx.ast.expression_string_literal(SPAN, s)) - } - // TODO // fn try_fold_spread( // &mut self, @@ -138,146 +113,6 @@ impl<'a, 'b> PeepholeFoldConstants { None } - fn try_fold_unary_expression( - &mut self, - expr: &mut UnaryExpression<'a>, - ctx: Ctx<'a, 'b>, - ) -> Option> { - fn is_valid(x: f64) -> bool { - x.is_finite() && x.fract() == 0.0 - } - match expr.operator { - UnaryOperator::Void => self.try_reduce_void(expr, ctx), - UnaryOperator::Typeof => Self::try_fold_type_of(expr, ctx), - // TODO: tryReduceOperandsForOp - #[allow(clippy::float_cmp)] - UnaryOperator::LogicalNot => { - if let Expression::NumericLiteral(n) = &expr.argument { - if n.value == 0.0 || n.value == 1.0 { - return None; - } - } - ctx.get_boolean_value(&expr.argument) - .map(|b| ctx.ast.expression_boolean_literal(expr.span, !b)) - } - // `-NaN` -> `NaN` - UnaryOperator::UnaryNegation if expr.argument.is_nan() => { - Some(ctx.ast.move_expression(&mut expr.argument)) - } - // `--1` -> `1` - UnaryOperator::UnaryNegation => match &mut expr.argument { - Expression::UnaryExpression(unary) - if matches!(unary.operator, UnaryOperator::UnaryNegation) => - { - Some(ctx.ast.move_expression(&mut unary.argument)) - } - Expression::NumericLiteral(n) => Some(ctx.ast.expression_numeric_literal( - expr.span, - -n.value, - "", - NumberBase::Decimal, - )), - _ => None, - }, - // `+1` -> `1` - UnaryOperator::UnaryPlus => match &expr.argument { - Expression::UnaryExpression(unary) => { - matches!(unary.operator, UnaryOperator::UnaryNegation) - .then(|| ctx.ast.move_expression(&mut expr.argument)) - } - Expression::Identifier(id) if id.name == "Infinity" => { - Some(ctx.ast.move_expression(&mut expr.argument)) - } - // `+NaN` -> `NaN` - _ if expr.argument.is_nan() => Some(ctx.ast.move_expression(&mut expr.argument)), - _ if expr.argument.is_number() => Some(ctx.ast.move_expression(&mut expr.argument)), - _ => None, - }, - UnaryOperator::BitwiseNot => match &mut expr.argument { - Expression::BigIntLiteral(n) => { - let value = ctx.get_string_bigint_value(n.raw.as_str().trim_end_matches('n')); - value.map(|value| { - let value = !value; - ctx.ast.expression_big_int_literal( - expr.span, - value.to_string() + "n", - BigintBase::Decimal, - ) - }) - } - Expression::NumericLiteral(n) => is_valid(n.value).then(|| { - let value = !n.value.to_int_32(); - ctx.ast.expression_numeric_literal( - expr.span, - value.into(), - value.to_string(), - NumberBase::Decimal, - ) - }), - Expression::UnaryExpression(un) => { - match un.operator { - UnaryOperator::BitwiseNot if un.argument.is_number() => { - // Return the un-bitten value - Some(ctx.ast.move_expression(&mut un.argument)) - } - UnaryOperator::UnaryNegation if un.argument.is_big_int_literal() => { - // `~-1n` -> `0n` - if let Expression::BigIntLiteral(n) = &mut un.argument { - let value = ctx - .get_string_bigint_value(n.raw.as_str().trim_end_matches('n')); - value.and_then(|value| value.checked_sub(&BigInt::from(1))).map( - |value| { - ctx.ast.expression_big_int_literal( - expr.span, - value.neg().to_string() + "n", - BigintBase::Decimal, - ) - }, - ) - } else { - None - } - } - UnaryOperator::UnaryNegation if un.argument.is_number() => { - // `-~1` -> `2` - if let Expression::NumericLiteral(n) = &mut un.argument { - is_valid(n.value).then(|| { - let value = !n.value.to_int_32().wrapping_neg(); - ctx.ast.expression_numeric_literal( - expr.span, - value.into(), - value.to_string(), - NumberBase::Decimal, - ) - }) - } else { - None - } - } - _ => None, - } - } - _ => None, - }, - UnaryOperator::Delete => None, - } - } - - /// `void 1` -> `void 0` - fn try_reduce_void( - &mut self, - expr: &mut UnaryExpression<'a>, - ctx: Ctx<'a, 'b>, - ) -> Option> { - if (!expr.argument.is_number() || !expr.argument.is_number_0()) - && !expr.may_have_side_effects() - { - expr.argument = ctx.ast.number_0(); - self.changed = true; - } - None - } - fn try_fold_logical_expression( logical_expr: &mut LogicalExpression<'a>, ctx: Ctx<'a, 'b>, @@ -455,7 +290,6 @@ impl<'a, 'b> PeepholeFoldConstants { // at the beginning let left_string = ctx.get_string_value(left)?; let right_string = ctx.get_string_value(right)?; - // let value = left_string.to_owned(). let value = left_string + right_string; Some(ctx.ast.expression_string_literal(span, value)) }, @@ -577,21 +411,12 @@ impl<'a, 'b> PeepholeFoldConstants { BinaryOperator::Exponential => left.powf(right), _ => unreachable!(), }; - Some(match result { - f64::INFINITY => ctx.ast.expression_identifier_reference(SPAN, "Infinity"), - f64::NEG_INFINITY => ctx.ast.expression_unary( - SPAN, - UnaryOperator::UnaryNegation, - ctx.ast.expression_identifier_reference(SPAN, "Infinity"), - ), - _ if result.is_nan() => ctx.ast.expression_identifier_reference(SPAN, "NaN"), - _ => ctx.ast.expression_numeric_literal( - SPAN, - result, - result.to_js_string(), - if is_exact_int64(result) { NumberBase::Decimal } else { NumberBase::Float }, - ), - }) + Some(ctx.ast.expression_numeric_literal( + SPAN, + result, + result.to_js_string(), + if is_exact_int64(result) { NumberBase::Decimal } else { NumberBase::Float }, + )) } fn try_fold_instanceof( @@ -1512,13 +1337,12 @@ mod test { test("a = ~1", "a = -2"); test("a = ~101", "a = -102"); - // More tests added by Ethan, which aligns with Google Closure Compiler's behavior - test_same("a = ~1.1"); // By default, we don't fold floating-point numbers. + test("a = ~1.1", "a = -2"); test("a = ~0x3", "a = -4"); // Hexadecimal number test("a = ~9", "a = -10"); // Despite `-10` is longer than `~9`, the compiler still folds it. test_same("a = ~b"); - test_same("a = ~NaN"); - test_same("a = ~-Infinity"); + test("a = ~NaN", "a = -1"); + test("a = ~-Infinity", "a = -1"); test("x = ~2147483658.0", "x = 2147483637"); test("x = ~-2147483658", "x = -2147483639"); } diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index 2786e4c0898d4..b609455bcf8e6 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -3,8 +3,11 @@ use std::ops::Deref; use num_bigint::BigInt; use oxc_ast::ast::*; -use oxc_ecmascript::{constant_evaluation::ConstantEvaluation, side_effects::MayHaveSideEffects}; -use oxc_ecmascript::{StringToBigInt, ToBigInt, ToJsString}; +use oxc_ecmascript::{ + constant_evaluation::{ConstantEvaluation, ConstantValue}, + side_effects::MayHaveSideEffects, +}; +use oxc_ecmascript::{ToBigInt, ToJsString}; use oxc_semantic::{IsGlobalReference, SymbolTable}; use oxc_traverse::TraverseCtx; @@ -33,6 +36,22 @@ impl<'a, 'b> Ctx<'a, 'b> { self.0.symbols() } + pub fn value_to_expr(self, span: Span, value: ConstantValue<'a>) -> Expression<'a> { + match value { + ConstantValue::Number(n) => { + let number_base = + if is_exact_int64(n) { NumberBase::Decimal } else { NumberBase::Float }; + self.ast.expression_numeric_literal(span, n, "", number_base) + } + ConstantValue::BigInt(n) => { + self.ast.expression_big_int_literal(span, n.to_string() + "n", BigintBase::Decimal) + } + ConstantValue::String(s) => self.ast.expression_string_literal(span, s), + ConstantValue::Boolean(b) => self.ast.expression_boolean_literal(span, b), + ConstantValue::Undefined => self.ast.void_0(span), + } + } + /// Gets the boolean value of a node that represents an expression, or `None` if no /// such value can be determined by static analysis. /// This method does not consider whether the node may have side-effects. @@ -123,10 +142,4 @@ impl<'a, 'b> Ctx<'a, 'b> { pub fn get_string_value(self, expr: &Expression<'a>) -> Option> { expr.to_js_string() } - - /// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/NodeUtil.java#L540) - #[expect(clippy::unused_self)] - pub fn get_string_bigint_value(self, raw_string: &str) -> Option { - raw_string.string_to_big_int() - } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 040390a36234b..916260df40723 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -2,7 +2,7 @@ Original | Minified | esbuild | Gzip | esbuild 72.14 kB | 24.12 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js -173.90 kB | 61.68 kB | 59.82 kB | 19.55 kB | 19.33 kB | moment.js +173.90 kB | 61.67 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js 287.63 kB | 92.70 kB | 90.07 kB | 32.27 kB | 31.95 kB | jquery.js @@ -10,17 +10,17 @@ Original | Minified | esbuild | Gzip | esbuild 544.10 kB | 73.49 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js -555.77 kB | 276.31 kB | 270.13 kB | 91.09 kB | 90.80 kB | d3.js +555.77 kB | 276.27 kB | 270.13 kB | 91.09 kB | 90.80 kB | d3.js 1.01 MB | 467.63 kB | 458.89 kB | 126.75 kB | 126.71 kB | bundle.min.js -1.25 MB | 662.90 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js +1.25 MB | 662.73 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js -2.14 MB | 741.42 kB | 724.14 kB | 181.41 kB | 181.07 kB | victory.js +2.14 MB | 741.37 kB | 724.14 kB | 181.41 kB | 181.07 kB | victory.js -3.20 MB | 1.02 MB | 1.01 MB | 331.95 kB | 331.56 kB | echarts.js +3.20 MB | 1.02 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js 6.69 MB | 2.39 MB | 2.31 MB | 496.10 kB | 488.28 kB | antd.js -10.95 MB | 3.56 MB | 3.49 MB | 911.24 kB | 915.50 kB | typescript.js +10.95 MB | 3.56 MB | 3.49 MB | 911.23 kB | 915.50 kB | typescript.js