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): improve constant fold numbers #8239

Merged
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
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
Loading