Skip to content

Commit

Permalink
feat(minifier): minify basic arithmetic calculations. (#6280)
Browse files Browse the repository at this point in the history
It uses to_string to check which is shorter, which is extremely tough. Waiting for further refactor.
  • Loading branch information
7086cmd committed Oct 7, 2024
1 parent 2bcd12a commit f9ae70c
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 35 deletions.
125 changes: 95 additions & 30 deletions crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::cmp::Ordering;
use std::ops::Neg;

use num_bigint::BigInt;
use num_traits::Zero;
use oxc_ast::ast::*;
use oxc_span::{GetSpan, Span, SPAN};
use oxc_syntax::{
Expand All @@ -19,6 +20,9 @@ use crate::{
CompressorPass,
};

static MAX_SAFE_FLOAT: f64 = 9_007_199_254_740_991_f64;
static NEG_MAX_SAFE_FLOAT: f64 = -9_007_199_254_740_991_f64;

/// Constant Folding
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PeepholeFoldConstants.java>
Expand Down Expand Up @@ -67,6 +71,19 @@ impl<'a> PeepholeFoldConstants {
Self { changed: false }
}

fn try_get_number_literal_value(&self, expr: &mut Expression<'a>) -> Option<f64> {
match expr {
Expression::NumericLiteral(n) => Some(n.value),
Expression::UnaryExpression(unary)
if unary.operator == UnaryOperator::UnaryNegation =>
{
let Expression::NumericLiteral(arg) = &mut unary.argument else { return None };
Some(-arg.value)
}
_ => None,
}
}

fn try_fold_useless_object_dot_define_properties_call(
&mut self,
_call_expr: &mut CallExpression<'a>,
Expand Down Expand Up @@ -409,13 +426,9 @@ impl<'a> PeepholeFoldConstants {
BinaryOperator::Subtraction
| BinaryOperator::Division
| BinaryOperator::Remainder
| BinaryOperator::Exponential => {
self.try_fold_arithmetic_op(e.span, &e.left, &e.right, ctx)
}
BinaryOperator::Multiplication
| BinaryOperator::BitwiseAnd
| BinaryOperator::BitwiseOR
| BinaryOperator::BitwiseXOR => {
| BinaryOperator::Multiplication
| BinaryOperator::Exponential => self.try_fold_arithmetic_op(e, ctx),
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
// TODO:
// self.try_fold_arithmetic_op(e.span, &e.left, &e.right, ctx)
// if (result != subtree) {
Expand Down Expand Up @@ -475,14 +488,65 @@ impl<'a> PeepholeFoldConstants {
}
}

fn try_fold_arithmetic_op<'b>(
fn try_fold_arithmetic_op(
&self,
_span: Span,
_left: &'b Expression<'a>,
_right: &'b Expression<'a>,
_ctx: &mut TraverseCtx<'a>,
operation: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
None
fn shorter_than_original(result: f64, left: f64, right: f64) -> bool {
if result > MAX_SAFE_FLOAT
|| result < NEG_MAX_SAFE_FLOAT
|| result.is_nan()
|| result.is_infinite()
{
return false;
}
let result_str = result.to_string().len();
let original_str = left.to_string().len() + right.to_string().len() + 1;
result_str <= original_str
}
if !operation.operator.is_arithmetic() {
return None;
};
let left = self.try_get_number_literal_value(&mut operation.left)?;
let right = self.try_get_number_literal_value(&mut operation.right)?;
if !left.is_finite() || !right.is_finite() {
return None;
}
let result = match operation.operator {
BinaryOperator::Addition => left + right,
BinaryOperator::Subtraction => left - right,
BinaryOperator::Multiplication => {
let result = left * right;
if shorter_than_original(result, left, right) {
result
} else {
return None;
}
}
BinaryOperator::Division if !right.is_zero() => {
if right == 0.0 {
return None;
}
let result = left / right;
if shorter_than_original(result, left, right) {
result
} else {
return None;
}
}
BinaryOperator::Remainder if !right.is_zero() && right.is_finite() => left % right,
// TODO BinaryOperator::Exponential if
_ => return None,
};
let number_base =
if is_exact_int64(result) { NumberBase::Decimal } else { NumberBase::Float };
Some(ctx.ast.expression_numeric_literal(
operation.span,
result,
result.to_string(),
number_base,
))
}

fn try_fold_instanceof<'b>(
Expand Down Expand Up @@ -870,8 +934,12 @@ impl<'a> PeepholeFoldConstants {
/// <https://github.com/google/closure-compiler/blob/master/test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java>
#[cfg(test)]
mod test {
use super::{MAX_SAFE_FLOAT, NEG_MAX_SAFE_FLOAT};
use oxc_allocator::Allocator;

static MAX_SAFE_INT: i64 = 9_007_199_254_740_991_i64;
static NEG_MAX_SAFE_INT: i64 = -9_007_199_254_740_991_i64;

use crate::tester;

fn test(source_text: &str, expected: &str) {
Expand Down Expand Up @@ -1232,18 +1300,14 @@ mod test {
test("-1n > -0.9", "false");

// Don't fold unsafely large numbers because there might be floating-point error
let max_safe_int = 9_007_199_254_740_991_i64;
let neg_max_safe_int = -9_007_199_254_740_991_i64;
let max_safe_float = 9_007_199_254_740_991_f64;
let neg_max_safe_float = -9_007_199_254_740_991_f64;
test(&format!("0n > {max_safe_int}"), "false");
test(&format!("0n < {max_safe_int}"), "true");
test(&format!("0n > {neg_max_safe_int}"), "true");
test(&format!("0n < {neg_max_safe_int}"), "false");
test(&format!("0n > {max_safe_float}"), "false");
test(&format!("0n < {max_safe_float}"), "true");
test(&format!("0n > {neg_max_safe_float}"), "true");
test(&format!("0n < {neg_max_safe_float}"), "false");
test(&format!("0n > {MAX_SAFE_INT}"), "false");
test(&format!("0n < {MAX_SAFE_INT}"), "true");
test(&format!("0n > {NEG_MAX_SAFE_INT}"), "true");
test(&format!("0n < {NEG_MAX_SAFE_INT}"), "false");
test(&format!("0n > {MAX_SAFE_FLOAT}"), "false");
test(&format!("0n < {MAX_SAFE_FLOAT}"), "true");
test(&format!("0n > {NEG_MAX_SAFE_FLOAT}"), "true");
test(&format!("0n < {NEG_MAX_SAFE_FLOAT}"), "false");

// comparing with Infinity is allowed
test("1n < Infinity", "true");
Expand Down Expand Up @@ -1574,7 +1638,6 @@ mod test {
}

#[test]
#[ignore]
fn test_fold_arithmetic() {
test("x = 10 + 20", "x = 30");
test("x = 2 / 4", "x = 0.5");
Expand All @@ -1586,10 +1649,12 @@ mod test {
test("x = 3 % -2", "x = 1");
test("x = -1 % 3", "x = -1");
test_same("x = 1 % 0");
test("x = 2 ** 3", "x = 8");
test("x = 2 ** -3", "x = 0.125");
test_same("x = 2 ** 55"); // backs off folding because 2 ** 55 is too large
test_same("x = 3 ** -1"); // backs off because 3**-1 is shorter than 0.3333333333333333
// We should not fold this because it's not safe to fold.
test_same(format!("x = {} * {}", MAX_SAFE_INT / 2, MAX_SAFE_INT / 2).as_str());
// test("x = 2 ** 3", "x = 8");
// test("x = 2 ** -3", "x = 0.125");
// test_same("x = 2 ** 55"); // backs off folding because 2 ** 55 is too large
// test_same("x = 3 ** -1"); // backs off because 3**-1 is shorter than 0.3333333333333333
}

#[test]
Expand Down
10 changes: 5 additions & 5 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.46 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js

173.90 kB | 61.69 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js
173.90 kB | 61.68 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js

287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js

342.15 kB | 124.11 kB | 118.14 kB | 44.80 kB | 44.37 kB | vue.js

544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js

555.77 kB | 278.24 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js
555.77 kB | 278.23 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js

1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js

1.25 MB | 670.97 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js
1.25 MB | 670.96 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js

2.14 MB | 756.33 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js

3.20 MB | 1.05 MB | 1.01 MB | 334.10 kB | 331.56 kB | echarts.js
3.20 MB | 1.05 MB | 1.01 MB | 334.07 kB | 331.56 kB | echarts.js

6.69 MB | 2.44 MB | 2.31 MB | 498.86 kB | 488.28 kB | antd.js
6.69 MB | 2.44 MB | 2.31 MB | 498.88 kB | 488.28 kB | antd.js

10.95 MB | 3.59 MB | 3.49 MB | 913.92 kB | 915.50 kB | typescript.js

0 comments on commit f9ae70c

Please sign in to comment.