perf(minifier): normalize undefined to void 0 before everything else (#8699)

so subsequent code don't need to lookup `undefined`.
This commit is contained in:
Boshen 2025-01-25 11:50:08 +08:00 committed by GitHub
parent 34d3d72b64
commit 8587965e45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 61 additions and 58 deletions

View file

@ -27,7 +27,7 @@ fn main() -> std::io::Result<()> {
let mut allocator = Allocator::default();
let printed = minify(&allocator, &source_text, source_type, mangle, nospace);
println!("{printed}");
// println!("{printed}");
if twice {
allocator.reset();

View file

@ -62,6 +62,7 @@ impl<'a> Ctx<'a, '_> {
}
}
#[inline]
pub fn is_identifier_undefined(self, ident: &IdentifierReference) -> bool {
if ident.name == "undefined" && ident.is_global_reference(self.symbols()) {
return true;
@ -69,6 +70,7 @@ impl<'a> Ctx<'a, '_> {
false
}
#[inline]
pub fn is_identifier_infinity(self, ident: &IdentifierReference) -> bool {
if ident.name == "Infinity" && ident.is_global_reference(self.symbols()) {
return true;
@ -76,6 +78,7 @@ impl<'a> Ctx<'a, '_> {
false
}
#[inline]
pub fn is_identifier_nan(self, ident: &IdentifierReference) -> bool {
if ident.name == "NaN" && ident.is_global_reference(self.symbols()) {
return true;

View file

@ -573,34 +573,34 @@ impl<'a, 'b> PeepholeOptimizations {
if e.arguments.len() != 1 {
return None;
}
Some(ctx.value_to_expr(
e.span,
ConstantValue::Number(match &e.arguments[0] {
// `Number(undefined)` -> `NaN`
Argument::Identifier(ident) if ctx.is_identifier_undefined(ident) => f64::NAN,
// `Number(null)` -> `0`
Argument::NullLiteral(_) => 0.0,
// `Number(true)` -> `1` `Number(false)` -> `0`
Argument::BooleanLiteral(b) => f64::from(b.value),
// `Number(100)` -> `100`
Argument::NumericLiteral(n) => n.value,
// `Number("a")` -> `+"a"` -> `NaN`
// `Number("1")` -> `+"1"` -> `1`
Argument::StringLiteral(n) => {
let argument = ctx.ast.expression_string_literal(n.span, n.value, n.raw);
if let Some(n) = ctx.eval_to_number(&argument) {
n
} else {
return Some(ctx.ast.expression_unary(
e.span,
UnaryOperator::UnaryPlus,
argument,
));
}
let arg = e.arguments[0].as_expression()?;
let value = ConstantValue::Number(match arg {
// `Number(undefined)` -> `NaN`
Expression::Identifier(ident) if ctx.is_identifier_undefined(ident) => f64::NAN,
// `Number(null)` -> `0`
Expression::NullLiteral(_) => 0.0,
// `Number(true)` -> `1` `Number(false)` -> `0`
Expression::BooleanLiteral(b) => f64::from(b.value),
// `Number(100)` -> `100`
Expression::NumericLiteral(n) => n.value,
// `Number("a")` -> `+"a"` -> `NaN`
// `Number("1")` -> `+"1"` -> `1`
Expression::StringLiteral(n) => {
let argument = ctx.ast.expression_string_literal(n.span, n.value, n.raw);
if let Some(n) = ctx.eval_to_number(&argument) {
n
} else {
return Some(ctx.ast.expression_unary(
e.span,
UnaryOperator::UnaryPlus,
argument,
));
}
_ => return None,
}),
))
}
e if e.is_void_0() => f64::NAN,
_ => return None,
});
Some(ctx.value_to_expr(e.span, value))
}
fn try_fold_binary_typeof_comparison(
@ -1744,6 +1744,7 @@ mod test {
#[test]
fn test_number_constructor() {
test("Number(undefined)", "NaN");
test("Number(void 0)", "NaN");
test("Number(null)", "0");
test("Number(true)", "1");
test("Number(false)", "0");

View file

@ -4,10 +4,9 @@ use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ValueType},
ToInt32,
};
use oxc_semantic::ReferenceFlags;
use oxc_span::{cmp::ContentEq, GetSpan};
use oxc_syntax::es_target::ESTarget;
use oxc_traverse::{Ancestor, MaybeBoundIdentifier, TraverseCtx};
use oxc_traverse::{Ancestor, TraverseCtx};
use crate::ctx::Ctx;
@ -366,13 +365,9 @@ impl<'a> PeepholeOptimizations {
unreachable!()
};
let return_stmt = return_stmt.unbox();
if let Some(e) = return_stmt.argument {
e
} else {
let name = "undefined";
let symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), name);
let ident = MaybeBoundIdentifier::new(Atom::from(name), symbol_id);
ident.create_expression(ReferenceFlags::read(), ctx)
match return_stmt.argument {
Some(e) => e,
None => ctx.ast.void_0(return_stmt.span),
}
}

View file

@ -3,7 +3,7 @@ use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::ConstantEvaluation;
use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::{ctx::Ctx, CompressOptions};
@ -22,6 +22,7 @@ pub struct NormalizeOptions {
/// * convert `Infinity` to `f64::INFINITY`
/// * convert `NaN` to `f64::NaN`
/// * convert `var x; void x` to `void 0`
/// * convert `undefined` to `void 0`
///
/// Also
///
@ -62,7 +63,10 @@ impl<'a> Traverse<'a> for Normalize {
*expr = ctx.ast.move_expression(&mut paren_expr.expression);
}
match expr {
Expression::Identifier(_) => {
Expression::Identifier(ident) => {
if let Some(e) = Self::try_compress_undefined(ident, ctx) {
*expr = e;
}
Self::convert_infinity_or_nan_into_number(expr, ctx);
}
Expression::UnaryExpression(e) if e.operator.is_void() => {
@ -154,6 +158,24 @@ impl<'a> Normalize {
}
e.argument = ctx.ast.expression_numeric_literal(ident.span, 0.0, None, NumberBase::Decimal);
}
/// Transforms `undefined` => `void 0`
/// So subsequent passes don't need to look up whether `undefined` is shadowed or not.
fn try_compress_undefined(
ident: &IdentifierReference<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if !Ctx(ctx).is_identifier_undefined(ident) {
return None;
}
// `delete undefined` returns `false`
// `delete void 0` returns `true`
if matches!(ctx.parent(), Ancestor::UnaryExpressionArgument(e) if e.operator().is_delete())
{
return None;
}
Some(ctx.ast.void_0(ident.span))
}
}
#[cfg(test)]

View file

@ -871,7 +871,6 @@ impl<'a> LatePeepholeOptimizations {
}
if let Some(folded_expr) = match expr {
Expression::Identifier(ident) => Self::try_compress_undefined(ident, ctx),
Expression::BooleanLiteral(_) => Self::try_compress_boolean(expr, ctx),
_ => None,
} {
@ -893,23 +892,6 @@ impl<'a> LatePeepholeOptimizations {
}
}
/// Transforms `undefined` => `void 0`
fn try_compress_undefined(
ident: &IdentifierReference<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if !Ctx(ctx).is_identifier_undefined(ident) {
return None;
}
// `delete undefined` returns `false`
// `delete void 0` returns `true`
if matches!(ctx.parent(), Ancestor::UnaryExpressionArgument(e) if e.operator().is_delete())
{
return None;
}
Some(ctx.ast.void_0(ident.span))
}
/// Transforms boolean expression `true` => `!0` `false` => `!1`.
fn try_compress_boolean(
expr: &mut Expression<'a>,

View file

@ -9,7 +9,7 @@ Original | minified | minified | gzip | gzip | Fixture
342.15 kB | 118.19 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js
544.10 kB | 71.73 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js
544.10 kB | 71.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js