Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(minifier): use constant folding unary expression from oxc_ecmascript #6647

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_ecmascript/src/to_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ impl<'a> ToJsString<'a> for IdentifierReference<'a> {

impl<'a> ToJsString<'a> for NumericLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
// 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()))
}
}

Expand Down
202 changes: 13 additions & 189 deletions crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::cmp::Ordering;
use std::ops::Neg;

use num_bigint::BigInt;
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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Expression<'a>> {
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,
Expand All @@ -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<Expression<'a>> {
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<Expression<'a>> {
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>,
Expand Down Expand Up @@ -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))
},
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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");
}
Expand Down
29 changes: 21 additions & 8 deletions crates/oxc_minifier/src/node_util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -123,10 +142,4 @@ impl<'a, 'b> Ctx<'a, 'b> {
pub fn get_string_value(self, expr: &Expression<'a>) -> Option<Cow<'a, str>> {
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<BigInt> {
raw_string.string_to_big_int()
}
}
12 changes: 6 additions & 6 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ 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

342.15 kB | 121.90 kB | 118.14 kB | 44.59 kB | 44.37 kB | vue.js

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

Loading