Skip to content

Commit

Permalink
refactor(minifier): use constant folding unary expression from `oxc_e…
Browse files Browse the repository at this point in the history
…cmascript` (#6647)
  • Loading branch information
Boshen committed Oct 17, 2024
1 parent 1d3d256 commit f4cdc56
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 206 deletions.
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

0 comments on commit f4cdc56

Please sign in to comment.