Skip to content

Commit

Permalink
refactor(minifier): improve constant fold numbers (#8239)
Browse files Browse the repository at this point in the history
Ported esbuild's implementation
  • Loading branch information
Boshen committed Jan 4, 2025
1 parent 39353b2 commit bf0fbce
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 89 deletions.
221 changes: 136 additions & 85 deletions crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,53 +244,94 @@ impl<'a, 'b> PeepholeFoldConstants {
}
}

fn extract_numeric_values(e: &BinaryExpression<'a>) -> Option<(f64, f64)> {
if let (Expression::NumericLiteral(left), Expression::NumericLiteral(right)) =
(&e.left, &e.right)
{
return Some((left.value, right.value));
}
None
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn try_fold_binary_expression(
e: &mut BinaryExpression<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
// TODO: tryReduceOperandsForOp

// https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L1136
// https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L1222
let span = e.span;
match e.operator {
BinaryOperator::ShiftLeft
| BinaryOperator::ShiftRight
| BinaryOperator::ShiftRightZeroFill
| BinaryOperator::Subtraction
| BinaryOperator::Division
| BinaryOperator::Remainder
| BinaryOperator::Multiplication
BinaryOperator::Equality
| BinaryOperator::Inequality
| BinaryOperator::StrictEquality
| BinaryOperator::StrictInequality
| BinaryOperator::LessThan
| BinaryOperator::GreaterThan
| BinaryOperator::LessEqualThan
| BinaryOperator::GreaterEqualThan => Self::try_fold_comparison(e, ctx),
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
ctx.eval_binary(e).or_else(|| Self::try_fold_left_child_op(e, ctx))
}
BinaryOperator::Addition => Self::try_fold_add(e, ctx),
BinaryOperator::Subtraction => {
// Subtraction of small-ish integers can definitely be folded without issues
Self::extract_numeric_values(e)
.filter(|(left, right)| {
left.is_nan()
|| left.is_finite()
|| right.is_nan()
|| right.is_finite()
|| (left.fract() == 0.0
&& right.fract() == 0.0
&& (left.abs() as usize) <= 0xFFFF_FFFF
&& (right.abs() as usize) <= 0xFFFF_FFFF)
})
.and_then(|_| ctx.eval_binary(e))
}
BinaryOperator::Multiplication
| BinaryOperator::Exponential
| BinaryOperator::Instanceof => match (&e.left, &e.right) {
(Expression::NumericLiteral(left), Expression::NumericLiteral(right)) => {
// Do not fold any division unless rhs is 0.
if e.operator == BinaryOperator::Division
&& right.value != 0.0
&& !right.value.is_nan()
&& !right.value.is_infinite()
{
return None;
| BinaryOperator::Remainder => Self::extract_numeric_values(e)
.filter(|(left, right)| {
*left == 0.0
|| left.is_nan()
|| left.is_infinite()
|| *right == 0.0
|| right.is_nan()
|| right.is_infinite()
})
.and_then(|_| ctx.eval_binary(e)),
BinaryOperator::Division => Self::extract_numeric_values(e)
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
.and_then(|_| ctx.eval_binary(e)),
BinaryOperator::ShiftLeft => {
if let Some((left, right)) = Self::extract_numeric_values(e) {
let result = ctx.eval_binary_expression(e)?.into_number()?;
let left_len = Self::approximate_printed_int_char_count(left);
let right_len = Self::approximate_printed_int_char_count(right);
let result_len = Self::approximate_printed_int_char_count(result);
if result_len <= left_len + 2 + right_len {
return Some(ctx.value_to_expr(span, ConstantValue::Number(result)));
}
let value = ctx.eval_binary_expression(e)?;
let ConstantValue::Number(num) = value else { return None };
(num.is_nan()
|| num.is_infinite()
|| (num.abs() <= f64::powf(2.0, 53.0)
&& Self::approximate_printed_int_char_count(num)
<= Self::approximate_printed_int_char_count(left.value)
+ Self::approximate_printed_int_char_count(right.value)
+ e.operator.as_str().len()))
.then_some(value)
}
_ => ctx.eval_binary_expression(e),
None
}
.map(|v| ctx.value_to_expr(e.span, v)),
BinaryOperator::Addition => Self::try_fold_add(e, ctx),
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
if let Some(v) = ctx.eval_binary_expression(e) {
return Some(ctx.value_to_expr(e.span, v));
BinaryOperator::ShiftRightZeroFill => {
if let Some((left, right)) = Self::extract_numeric_values(e) {
let result = ctx.eval_binary_expression(e)?.into_number()?;
let left_len = Self::approximate_printed_int_char_count(left);
let right_len = Self::approximate_printed_int_char_count(right);
let result_len = Self::approximate_printed_int_char_count(result);
if result_len <= left_len + 3 + right_len {
return Some(ctx.value_to_expr(span, ConstantValue::Number(result)));
}
}
Self::try_fold_left_child_op(e, ctx)
None
}
op if op.is_equality() || op.is_compare() => Self::try_fold_comparison(e, ctx),
_ => None,
BinaryOperator::ShiftRight | BinaryOperator::Instanceof => ctx.eval_binary(e),
BinaryOperator::In => None,
}
}

Expand Down Expand Up @@ -574,12 +615,6 @@ mod test {
tester::test(&allocator, source_text, expected, &mut pass);
}

fn test_nospace(source_text: &str, expected: &str) {
let allocator = Allocator::default();
let mut pass = super::PeepholeFoldConstants::new();
tester::test_impl(&allocator, source_text, expected, &mut pass, true);
}

fn test_same(source_text: &str) {
test(source_text, source_text);
}
Expand Down Expand Up @@ -1483,68 +1518,84 @@ mod test {

#[test]
fn test_fold_arithmetic() {
test("x = 10 + 20", "x = 30");
test_same("x = 2 / 4");
test("x = 2.25 * 3", "x = 6.75");
test_same("z = x * y");
test_same("x = y * 5");
test("x = 1 / 0", "x = Infinity");
test("x = 3 % 2", "x = 1");
test("x = 3 % -2", "x = 1");
test("x = -1 % 3", "x = -1");
test("x = 1 % 0", "x = NaN");

test("x = 2 ** 3", "x = 8");
test("x = 2 ** -3", "x = 0.125");
test_same("x = 2 ** 55");
// test_same("x = 3 ** -1"); // backs off because 3**-1 is shorter than 0.3333333333333333
test("1n+ +1n", "1n + +1n");
test("1n- -1n", "1n - -1n");
test("a- -b", "a - -b");
}

test("x = 0 / 0", "x = NaN");
test("x = 0 % 0", "x = NaN");
test("x = (-1) ** 0.5", "x = NaN");
#[test]
fn test_fold_arithmetic_infinity() {
test("x=-Infinity-2", "x=-Infinity");
test("x=Infinity-2", "x=Infinity");
test("x=Infinity*5", "x=Infinity");
test("x = Infinity ** 2", "x = Infinity");
test("x = Infinity ** -2", "x = 0");

test_nospace("1n+ +1n", "1n + +1n");
test_nospace("1n- -1n", "1n - -1n");
test_nospace("a- -b", "a - -b");
test("x = Infinity % Infinity", "x = NaN");
test("x = Infinity % 0", "x = NaN");
}

#[test]
fn test_fold_arithmetic2() {
fn test_fold_add() {
test("x = 10 + 20", "x = 30");
test_same("x = y + 10 + 20");
test_same("x = y / 2 / 4");
// test("x = y * 2.25 * 3", "x = y * 6.75");
test_same("x = y * 2.25 * z * 3");
test("x = 1 + null", "x = 1");
test("x = null + 1", "x = 1");
}

#[test]
fn test_fold_multiply() {
test_same("x = 2.25 * 3");
test_same("z = x * y");
test_same("x = y * 5");
// test("x = null * undefined", "x = NaN");
// test("x = null * 1", "x = 0");
// test("x = (null - 1) * 2", "x = -2");
// test("x = (null + 1) * 2", "x = 2");
// test("x = y + (z * 24 * 60 * 60 * 1000)", "x = y + z * 864E5");
test("x = y + (z & 24 & 60 & 60 & 1000)", "x = y + (z & 8)");
}

#[test]
fn test_fold_arithmetic3() {
test("x = null * undefined", "x = NaN");
test("x = null * 1", "x = 0");
test("x = (null - 1) * 2", "x = -2");
test("x = (null + 1) * 2", "x = 2");
test("x = null ** 0", "x = 1");
test("x = (-0) ** 3", "x = -0");
fn test_fold_division() {
test("x = Infinity / Infinity", "x = NaN");
test("x = Infinity / 0", "x = Infinity");
test("x = 1 / 0", "x = Infinity");
test("x = 0 / 0", "x = NaN");
test_same("x = 2 / 4");
test_same("x = y / 2 / 4");
}

test("x = 1 + null", "x = 1");
test("x = null + 1", "x = 1");
#[test]
fn test_fold_remainder() {
test_same("x = 3 % 2");
test_same("x = 3 % -2");
test_same("x = -1 % 3");
test("x = 1 % 0", "x = NaN");
test("x = 0 % 0", "x = NaN");
}

#[test]
fn test_fold_arithmetic_infinity() {
test("x=-Infinity-2", "x=-Infinity");
test("x=Infinity-2", "x=Infinity");
test("x=Infinity*5", "x=Infinity");
test("x = Infinity ** 2", "x = Infinity");
test("x = Infinity ** -2", "x = 0");
fn test_fold_exponential() {
test_same("x = 2 ** 3");
test_same("x = 2 ** -3");
test_same("x = 2 ** 55");
test_same("x = 3 ** -1");
test_same("x = (-1) ** 0.5");
test("x = (-0) ** 3", "x = -0");
test_same("x = null ** 0");
}

test("x = Infinity / Infinity", "x = NaN");
test("x = Infinity % Infinity", "x = NaN");
test("x = Infinity / 0", "x = Infinity");
test("x = Infinity % 0", "x = NaN");
#[test]
fn test_fold_shift_right_zero_fill() {
test("10 >>> 1", "5");
test_same("-1 >>> 0");
}

#[test]
fn test_fold_shift_left() {
test("1 << 3", "8");
test_same("1 << 24");
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_minifier/src/node_util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ impl<'a> Ctx<'a, '_> {
self.0.symbols()
}

pub fn eval_binary(self, e: &BinaryExpression<'a>) -> Option<Expression<'a>> {
self.eval_binary_expression(e).map(|v| self.value_to_expr(e.span, v))
}

pub fn value_to_expr(self, span: Span, value: ConstantValue<'a>) -> Expression<'a> {
match value {
ConstantValue::Number(n) => {
Expand Down
8 changes: 4 additions & 4 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.68 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js

173.90 kB | 59.86 kB | 59.82 kB | 19.43 kB | 19.33 kB | moment.js
173.90 kB | 59.87 kB | 59.82 kB | 19.43 kB | 19.33 kB | moment.js

287.63 kB | 90.16 kB | 90.07 kB | 32.08 kB | 31.95 kB | jquery.js

342.15 kB | 118.23 kB | 118.14 kB | 44.53 kB | 44.37 kB | vue.js

544.10 kB | 71.81 kB | 72.48 kB | 26.19 kB | 26.20 kB | lodash.js

555.77 kB | 273.19 kB | 270.13 kB | 90.99 kB | 90.80 kB | d3.js
555.77 kB | 273.19 kB | 270.13 kB | 90.98 kB | 90.80 kB | d3.js

1.01 MB | 460.32 kB | 458.89 kB | 126.85 kB | 126.71 kB | bundle.min.js
1.01 MB | 460.33 kB | 458.89 kB | 126.85 kB | 126.71 kB | bundle.min.js

1.25 MB | 652.68 kB | 646.76 kB | 163.53 kB | 163.73 kB | three.js
1.25 MB | 652.70 kB | 646.76 kB | 163.53 kB | 163.73 kB | three.js

2.14 MB | 726.19 kB | 724.14 kB | 180.18 kB | 181.07 kB | victory.js

Expand Down

0 comments on commit bf0fbce

Please sign in to comment.