mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(minifier): avoid longer numeric constant folds (#24875)
Constant evaluation can replace a short numeric expression with a longer exact floating-point literal, increasing minified output. Using the serializer shared by #24876 and optimized by #24889, the minifier compares the folded literal with a conservative lower bound for the original numeric expression before accepting numeric addition, subtraction, and multiplication folds. Parentheses are omitted from the estimate, so an accepted fold cannot make output longer. Measuring the original tree instead of evaluated operand values also preserves profitable outer folds such as `.8` without regressing nested expressions such as `0+(.1+.05)`. ## Example Input: ```js console.log(0.1 + 0.05); console.log(0.1 + 0.2 + 0.1 + 0.4); console.log(0.1 * 1 + 0.05); console.log(0.1 * 1 + 0.7); console.log(0.3 - 0.1); ``` Before: ```js console.log(.15000000000000002),console.log(.7999999999999999),console.log(.15000000000000002),console.log(.8),console.log(.19999999999999998); ``` After: ```js console.log(.1+.05),console.log(.7+.1),console.log(.1+.05),console.log(.8),console.log(.3-.1); ``` Verified with the full `oxc_minifier` test suite, minsize and allocation snapshots, Clippy, and formatting. Closes #24863. AI assistance: Codex was used to implement and verify this change.
This commit is contained in:
1 parent
f7971f181f
commit
46183007c2
4 files changed
+122
-25
No files matched your search
@@ -4,6 +4,7 @@ use oxc_ecmascript::{
|
||||
GlobalContext, ToJsString,
|
||||
constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType, ValueType},
|
||||
side_effects::MayHaveSideEffects,
|
||||
with_number_literal,
|
||||
};
|
||||
use oxc_span::{GetSpan, SPAN};
|
||||
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};
|
||||
@@ -345,26 +346,38 @@ impl<'a> PeepholeOptimizations {
|
||||
&& (left.abs() as usize) <= 0xFFFF_FFFF
|
||||
&& (right.abs() as usize) <= 0xFFFF_FFFF)
|
||||
})
|
||||
.filter(|(left, right)| {
|
||||
Self::folded_numeric_expression_is_shorter(e, left - right) != Some(false)
|
||||
})
|
||||
.and_then(|_| ctx.eval_binary(e))
|
||||
}
|
||||
BinaryOperator::Multiplication
|
||||
| BinaryOperator::Exponential
|
||||
| BinaryOperator::Remainder => Self::extract_numeric_values(e, ctx)
|
||||
.filter(|(left, right)| {
|
||||
*left == 0.0
|
||||
|| left.is_nan()
|
||||
|| left.is_infinite()
|
||||
|| *right == 0.0
|
||||
|| right.is_nan()
|
||||
|| right.is_infinite()
|
||||
// Small number multiplication.
|
||||
|| (e.operator == BinaryOperator::Multiplication
|
||||
&& left.abs() <= 255.0
|
||||
&& left.fract() == 0.0
|
||||
&& right.abs() <= 255.0
|
||||
&& right.fract() == 0.0)
|
||||
| BinaryOperator::Remainder => {
|
||||
let shorter_multiplication = if e.operator == BinaryOperator::Multiplication {
|
||||
Self::try_fold_shorter_numeric_expression(e, ctx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
shorter_multiplication.or_else(|| {
|
||||
Self::extract_numeric_values(e, ctx)
|
||||
.filter(|(left, right)| {
|
||||
*left == 0.0
|
||||
|| left.is_nan()
|
||||
|| left.is_infinite()
|
||||
|| *right == 0.0
|
||||
|| right.is_nan()
|
||||
|| right.is_infinite()
|
||||
// Small number multiplication.
|
||||
|| (e.operator == BinaryOperator::Multiplication
|
||||
&& left.abs() <= 255.0
|
||||
&& left.fract() == 0.0
|
||||
&& right.abs() <= 255.0
|
||||
&& right.fract() == 0.0)
|
||||
})
|
||||
.and_then(|_| ctx.eval_binary(e))
|
||||
})
|
||||
.and_then(|_| ctx.eval_binary(e)),
|
||||
}
|
||||
BinaryOperator::Division => Self::extract_numeric_values(e, ctx)
|
||||
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
|
||||
.and_then(|_| ctx.eval_binary(e)),
|
||||
@@ -412,11 +425,75 @@ impl<'a> PeepholeOptimizations {
|
||||
count
|
||||
}
|
||||
|
||||
/// Lower bound for the minified size of a numeric expression. Parentheses are deliberately
|
||||
/// omitted, so accepting a fold based on this count cannot make the output longer.
|
||||
fn numeric_expression_size_lower_bound(expr: &Expression<'a>) -> Option<usize> {
|
||||
match expr.get_inner_expression() {
|
||||
Expression::NumericLiteral(lit) => Self::number_literal_source_len(lit.value),
|
||||
Expression::UnaryExpression(e)
|
||||
if matches!(
|
||||
e.operator,
|
||||
UnaryOperator::UnaryNegation | UnaryOperator::UnaryPlus
|
||||
) =>
|
||||
{
|
||||
Some(1 + Self::numeric_expression_size_lower_bound(&e.argument)?)
|
||||
}
|
||||
Expression::BinaryExpression(e)
|
||||
if matches!(
|
||||
e.operator,
|
||||
BinaryOperator::Addition
|
||||
| BinaryOperator::Subtraction
|
||||
| BinaryOperator::Multiplication
|
||||
| BinaryOperator::Division
|
||||
| BinaryOperator::Remainder
|
||||
| BinaryOperator::Exponential
|
||||
) =>
|
||||
{
|
||||
Self::binary_numeric_expression_size_lower_bound(e)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_numeric_expression_size_lower_bound(e: &BinaryExpression<'a>) -> Option<usize> {
|
||||
Some(
|
||||
Self::numeric_expression_size_lower_bound(&e.left)?
|
||||
+ e.operator.as_str().len()
|
||||
+ Self::numeric_expression_size_lower_bound(&e.right)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn number_literal_source_len(value: f64) -> Option<usize> {
|
||||
value.is_finite().then(|| {
|
||||
usize::from(value.is_sign_negative()) + with_number_literal(value.abs(), str::len)
|
||||
})
|
||||
}
|
||||
|
||||
fn try_fold_shorter_numeric_expression(
|
||||
e: &BinaryExpression<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> Option<Expression<'a>> {
|
||||
let original_len = Self::binary_numeric_expression_size_lower_bound(e)?;
|
||||
let result = e.evaluate_value(ctx)?.into_number()?;
|
||||
(Self::number_literal_source_len(result)? <= original_len)
|
||||
.then(|| ctx.value_to_expr(e.span, ConstantValue::Number(result)))
|
||||
}
|
||||
|
||||
fn folded_numeric_expression_is_shorter(e: &BinaryExpression<'a>, result: f64) -> Option<bool> {
|
||||
let original_len = Self::binary_numeric_expression_size_lower_bound(e)?;
|
||||
Some(Self::number_literal_source_len(result)? <= original_len)
|
||||
}
|
||||
|
||||
// Simplified version of `tryFoldAdd` from closure compiler.
|
||||
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: &TraverseCtx<'a>) -> Option<Expression<'a>> {
|
||||
if !e.may_have_side_effects(ctx)
|
||||
&& let Some(v) = e.evaluate_value(ctx)
|
||||
{
|
||||
if let ConstantValue::Number(result) = &v
|
||||
&& Self::folded_numeric_expression_is_shorter(e, *result) == Some(false)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return Some(ctx.value_to_expr(e.span, v));
|
||||
}
|
||||
debug_assert_eq!(e.operator, BinaryOperator::Addition);
|
||||
|
||||
@@ -1014,6 +1014,25 @@ fn test_fold_add() {
|
||||
fold("x = null + 1", "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fold_numeric_expression_only_if_shorter() {
|
||||
// https://github.com/oxc-project/oxc/issues/24863
|
||||
fold_same("0.1 + 0.05");
|
||||
fold_same("0.7 + 0.1");
|
||||
fold_same("0.3 - 0.1");
|
||||
fold("0.1 + (0.2 - 0.1) * 0.5", "0.1 + 0.05");
|
||||
fold("0.7 + (0.9 - 0.7) * 0.5", "0.8");
|
||||
fold_same("1e3 + 1e-10");
|
||||
fold("1e12 + 1e12", "2e12");
|
||||
fold("1e12 - 1e12", "0");
|
||||
|
||||
// Compare against the original expression, not the longer value of a nested operand.
|
||||
fold_same("0 + (0.1 + 0.05)");
|
||||
|
||||
// Do not evaluate across an operand with side effects.
|
||||
fold_same("f() + 0.05");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fold_sub() {
|
||||
fold("x = 10 - 20", "x = -10");
|
||||
@@ -1021,8 +1040,9 @@ fn test_fold_sub() {
|
||||
|
||||
#[test]
|
||||
fn test_fold_multiply() {
|
||||
fold_same("x = 2.25 * 3");
|
||||
fold("x = 2.25 * 3", "x = 6.75");
|
||||
fold_same("z = x * y");
|
||||
fold_same("x = f() * 2");
|
||||
fold_same("x = y * 5");
|
||||
fold("x = null * undefined", "x = NaN");
|
||||
fold("x = null * 1", "x = 0");
|
||||
@@ -1035,7 +1055,7 @@ fn test_fold_multiply() {
|
||||
fold("x = 255 * 255", "x = 65025");
|
||||
fold("x = -255 * 255", "x = -65025");
|
||||
fold("x = -255 * -255", "x = 65025");
|
||||
fold_same("x = 256 * 255");
|
||||
fold("x = 256 * 255", "x = 65280");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,17 +11,17 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
544.10 kB | 70.91 kB | 72.48 kB | 25.73 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 267.20 kB | 270.13 kB | 87.98 kB | 90.80 kB | 2 | d3.js
|
||||
555.77 kB | 267.20 kB | 270.13 kB | 87.99 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 439.26 kB | 458.89 kB | 122.03 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 642.45 kB | 646.76 kB | 159.37 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 642.44 kB | 646.76 kB | 159.37 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 710.90 kB | 724.14 kB | 160.38 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.57 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.56 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.79 kB | 488.28 kB | 5 | antd.js
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.80 kB | 488.28 kB | 5 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 852.81 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 852.84 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -49,7 +49,7 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29975903 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 371546
|
||||
arena allocs: 371560
|
||||
arena reallocs: 76271
|
||||
arena size: 49106184 # 49.11 MB
|
||||
|
||||
@@ -71,7 +71,7 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 1858
|
||||
sys alloc bytes: 5337950 # 5.34 MB
|
||||
sys peak growth: 3737991 # 3.74 MB
|
||||
arena allocs: 71838
|
||||
arena allocs: 71882
|
||||
arena reallocs: 12252
|
||||
arena size: 9559192 # 9.56 MB
|
||||
arena size: 9560952 # 9.56 MB
|
||||
|
||||
Reference in new issue
Block a user