perf(minifier): stop re-deriving printed numeric spellings (#25103)

The parser cannot produce negative or non-finite NumericLiterals: `-1` parses as unary negation, and codegen prints `Infinity` as `1/0`, which parses as a division. The loop's constant folds re-derive the literal from either spelling and record a change for it, one phantom change per such number on every run over printed output.

Collapse `-` into a negative literal in the Normalize pass — the same treatment `NaN` and `Infinity` identifiers already get — and keep `1/0` and `-1/0` unfolded like the existing `!0` and `void 0` guards: they are the canonical printed spellings of the non-finite numbers.

The guard shifts an inlining decision in lodash.js, making it 20 bytes smaller; all other minsize outputs are unchanged.
This commit is contained in:
Dunqing committed 2026-07-30 10:52:55 +00:00
1 parent 9dc7756729
commit fb402b010c
6 files changed
+30 -6

No files matched your search

@@ -311,8 +311,18 @@ impl<'a> PeepholeOptimizations {
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
#[expect(clippy::float_cmp)]
pub fn fold_binary_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
// Do not fold `1/0` and `-1/0`: codegen prints the non-finite numbers
// in exactly this form, so like `!0` and `void 0` above, folding them
// cannot shrink the output and only records a change on every run.
if e.operator == BinaryOperator::Division
&& matches!(&e.right, Expression::NumericLiteral(r) if r.value == 0.0 && r.value.is_sign_positive())
&& matches!(&e.left, Expression::NumericLiteral(l) if l.value.abs() == 1.0)
{
return;
}
// TODO: tryReduceOperandsForOp
// https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L1136
+10 -1
View File
@@ -2,7 +2,7 @@ use crate::generated::ancestor::Ancestor;
use oxc_allocator::{ArenaVec, ReplaceWith, TakeIn};
use oxc_ast::ast::*;
use oxc_ecmascript::{
constant_evaluation::{DetermineValueType, ValueType},
constant_evaluation::{ConstantValue, DetermineValueType, ValueType},
side_effects::{is_typed_array_constructor, is_valid_regexp},
};
use oxc_semantic::IsGlobalReference;
@@ -144,6 +144,15 @@ impl<'a> Traverse<'a> for Normalize {
Self::fold_void_ident(e, ctx);
None
}
// `-1` parses as unary negation of `1`. Collapsing it into a negative
// literal does not change the output, so do it here instead of letting
// the loop count it as compression progress on every run.
Expression::UnaryExpression(e)
if e.operator == UnaryOperator::UnaryNegation
&& let Expression::NumericLiteral(lit) = &e.argument =>
{
Some(ctx.value_to_expr(e.span, ConstantValue::Number(-lit.value)))
}
Expression::StaticMemberExpression(e) => Self::fold_number_nan_to_nan(e, ctx),
_ => None,
} {
@@ -1062,7 +1062,12 @@ fn test_fold_multiply() {
fn test_fold_division() {
fold("x = Infinity / Infinity", "x = NaN");
fold("x = Infinity / 0", "x = Infinity");
fold("x = 1 / 0", "x = Infinity");
// `1 / 0` is the canonical printed spelling of Infinity and is kept as-is.
fold_same("x = 1 / 0");
fold_same("x = -1 / 0");
// A negative-zero divisor is not canonical and can still be folded.
fold("x = 1 / -0", "x = -Infinity");
fold("x = -1 / -0", "x = Infinity");
fold("x = 0 / 0", "x = NaN");
fold("x = 360 / 360", "x = 1");
fold("x = 10.5 / 0.75", "x = 14");
@@ -113,8 +113,8 @@ fn test_numeric_comparison_edge_cases() {
#[test]
fn test_mathematical_expression_edge_cases() {
// Test operations with special numeric values get optimized
test("return 1 / 0", "return Infinity"); // optimized to Infinity
test("return -1 / 0", "return -Infinity"); // optimized to -Infinity
test_same("return 1 / 0"); // canonical printed spelling of Infinity
test_same("return -1 / 0"); // canonical printed spelling of -Infinity
test("return 0 / 0", "return NaN"); // optimized to NaN
// Test simple arithmetic - these ARE optimized by oxc
+1 -1
View File
@@ -9,7 +9,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
342.15 kB | 116.92 kB | 118.14 kB | 43.17 kB | 44.37 kB | 2 | vue.js
544.10 kB | 70.91 kB | 72.48 kB | 25.73 kB | 26.20 kB | 2 | lodash.js
544.10 kB | 70.89 kB | 72.48 kB | 25.71 kB | 26.20 kB | 2 | lodash.js
555.77 kB | 267.19 kB | 270.13 kB | 87.99 kB | 90.80 kB | 2 | d3.js
@@ -49,7 +49,7 @@ antd.js:
sys deallocs: 1038
sys alloc bytes: 29976031 # 29.98 MB
sys peak growth: 19934725 # 19.93 MB
arena allocs: 371609
arena allocs: 371607
arena reallocs: 76289
arena size: 49109008 # 49.11 MB