diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index 22ba2b37e9da0..7f69447e1f863 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -188,6 +188,7 @@ pub trait ConstantEvaluation<'a> { Some(ConstantValue::String(Cow::Borrowed(lit.value.as_str()))) } Expression::StaticMemberExpression(e) => self.eval_static_member_expression(e), + Expression::ComputedMemberExpression(e) => self.eval_computed_member_expression(e), _ => None, } } @@ -421,7 +422,30 @@ pub trait ConstantEvaluation<'a> { match expr.property.name.as_str() { "length" => { if let Some(ConstantValue::String(s)) = self.eval_expression(&expr.object) { - // TODO(perf): no need to actually convert, only need the length + Some(ConstantValue::Number(s.encode_utf16().count().to_f64().unwrap())) + } else { + if expr.object.may_have_side_effects() { + return None; + } + + if let Expression::ArrayExpression(arr) = &expr.object { + Some(ConstantValue::Number(arr.elements.len().to_f64().unwrap())) + } else { + None + } + } + } + _ => None, + } + } + + fn eval_computed_member_expression( + &self, + expr: &ComputedMemberExpression<'a>, + ) -> Option> { + match &expr.expression { + Expression::StringLiteral(s) if s.value == "length" => { + if let Some(ConstantValue::String(s)) = self.eval_expression(&expr.object) { Some(ConstantValue::Number(s.encode_utf16().count().to_f64().unwrap())) } else { if expr.object.may_have_side_effects() { diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index 59df444b541db..c9514c26cb5f1 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -4,7 +4,10 @@ use oxc_semantic::{ScopeTree, SemanticBuilder, SymbolTable}; use oxc_traverse::ReusableTraverseCtx; use crate::{ - peephole::{DeadCodeElimination, Normalize, NormalizeOptions, PeepholeOptimizations}, + peephole::{ + DeadCodeElimination, LatePeepholeOptimizations, Normalize, NormalizeOptions, + PeepholeOptimizations, + }, CompressOptions, }; @@ -33,8 +36,8 @@ impl<'a> Compressor<'a> { let mut ctx = ReusableTraverseCtx::new(scopes, symbols, self.allocator); let normalize_options = NormalizeOptions { convert_while_to_fors: true }; Normalize::new(normalize_options, self.options).build(program, &mut ctx); - PeepholeOptimizations::new(self.options.target, true).run_in_loop(program, &mut ctx); - PeepholeOptimizations::new(self.options.target, false).build(program, &mut ctx); + PeepholeOptimizations::new(self.options.target).run_in_loop(program, &mut ctx); + LatePeepholeOptimizations::new(self.options.target).build(program, &mut ctx); } pub fn dead_code_elimination(self, program: &mut Program<'a>) { diff --git a/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs b/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs index f0888e6957947..2e617b645fc49 100644 --- a/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs +++ b/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs @@ -2,30 +2,20 @@ use oxc_ast::ast::*; use oxc_syntax::identifier::is_identifier_name; use oxc_traverse::TraverseCtx; -use super::PeepholeOptimizations; +use super::LatePeepholeOptimizations; use crate::ctx::Ctx; -impl<'a> PeepholeOptimizations { +impl<'a> LatePeepholeOptimizations { /// Converts property accesses from quoted string or bracket access syntax to dot or unquoted string /// syntax, where possible. Dot syntax is more compact. /// /// - pub fn convert_to_dotted_properties( - &mut self, - expr: &mut MemberExpression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - if !self.in_fixed_loop { - self.try_compress_computed_member_expression(expr, Ctx(ctx)); - } - } - + /// /// `foo['bar']` -> `foo.bar` /// `foo?.['bar']` -> `foo?.bar` - fn try_compress_computed_member_expression( - &mut self, + pub fn convert_to_dotted_properties( expr: &mut MemberExpression<'a>, - ctx: Ctx<'a, '_>, + ctx: &mut TraverseCtx<'a>, ) { let MemberExpression::ComputedMemberExpression(e) = expr else { return }; let Expression::StringLiteral(s) = &e.expression else { return }; @@ -35,7 +25,6 @@ impl<'a> PeepholeOptimizations { *expr = MemberExpression::StaticMemberExpression( ctx.ast.alloc_static_member_expression(e.span, object, property, e.optional), ); - self.mark_current_function_as_changed(); return; } let v = s.value.as_str(); @@ -43,7 +32,8 @@ impl<'a> PeepholeOptimizations { return; } if let Some(n) = Ctx::string_to_equivalent_number_value(v) { - e.expression = ctx.ast.expression_numeric_literal(s.span, n, None, NumberBase::Decimal); + e.expression = + Ctx(ctx).ast.expression_numeric_literal(s.span, n, None, NumberBase::Decimal); } } } diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index ef2d403f170ad..c864e9b2a8e16 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -29,6 +29,7 @@ impl<'a, 'b> PeepholeOptimizations { .or_else(|| Self::try_fold_binary_typeof_comparison(e, ctx)), Expression::UnaryExpression(e) => Self::try_fold_unary_expr(e, ctx), Expression::StaticMemberExpression(e) => Self::try_fold_static_member_expr(e, ctx), + Expression::ComputedMemberExpression(e) => Self::try_fold_computed_member_expr(e, ctx), Expression::LogicalExpression(e) => Self::try_fold_logical_expr(e, ctx), Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx), Expression::CallExpression(e) => Self::try_fold_number_constructor(e, ctx), @@ -56,12 +57,19 @@ impl<'a, 'b> PeepholeOptimizations { } fn try_fold_static_member_expr( - static_member_expr: &mut StaticMemberExpression<'a>, + e: &mut StaticMemberExpression<'a>, ctx: Ctx<'a, 'b>, ) -> Option> { // TODO: tryFoldObjectPropAccess(n, left, name) - ctx.eval_static_member_expression(static_member_expr) - .map(|value| ctx.value_to_expr(static_member_expr.span, value)) + ctx.eval_static_member_expression(e).map(|value| ctx.value_to_expr(e.span, value)) + } + + fn try_fold_computed_member_expr( + e: &mut ComputedMemberExpression<'a>, + ctx: Ctx<'a, 'b>, + ) -> Option> { + // TODO: tryFoldObjectPropAccess(n, left, name) + ctx.eval_computed_member_expression(e).map(|value| ctx.value_to_expr(e.span, value)) } fn try_fold_logical_expr( @@ -1618,6 +1626,7 @@ mod test { test("x = [].length", "x = 0"); test("x = [1,2,3].length", "x = 3"); // test("x = [a,b].length", "x = 2"); + test("x = 'abc'['length']", "x = 3"); // Not handled yet test("x = [,,1].length", "x = 3"); diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index c6bf934bc1355..b456e68086012 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -1,8 +1,9 @@ use oxc_allocator::Vec; use oxc_ast::{ast::*, NONE}; use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType}; +use oxc_semantic::ReferenceFlags; use oxc_span::{cmp::ContentEq, GetSpan}; -use oxc_traverse::{Ancestor, TraverseCtx}; +use oxc_traverse::{Ancestor, MaybeBoundIdentifier, TraverseCtx}; use crate::ctx::Ctx; @@ -22,15 +23,15 @@ impl<'a> PeepholeOptimizations { ctx: &mut TraverseCtx<'a>, ) { let mut changed = false; - let mut changed2 = false; - Self::try_replace_if(stmts, &mut changed2, ctx); - while changed2 { - changed2 = false; - Self::try_replace_if(stmts, &mut changed2, ctx); - if stmts.iter().any(|stmt| matches!(stmt, Statement::EmptyStatement(_))) { + loop { + let mut local_change = false; + Self::try_replace_if(stmts, &mut local_change, ctx); + if local_change { stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_))); + changed = local_change; + } else { + break; } - changed = changed2; } if changed { self.mark_current_function_as_changed(); @@ -77,25 +78,27 @@ impl<'a> PeepholeOptimizations { expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>, ) { + let mut changed = false; loop { - let mut changed = false; + let mut local_change = false; if let Expression::ConditionalExpression(logical_expr) = expr { + if Self::try_fold_expr_in_boolean_context(&mut logical_expr.test, Ctx(ctx)) { + local_change = true; + } if let Some(e) = Self::try_minimize_conditional(logical_expr, ctx) { *expr = e; - changed = true; - } - } - if let Expression::ConditionalExpression(logical_expr) = expr { - if Self::try_fold_expr_in_boolean_context(&mut logical_expr.test, Ctx(ctx)) { - changed = true; + local_change = true; } } - if changed { - self.mark_current_function_as_changed(); + if local_change { + changed = true; } else { break; } } + if changed { + self.mark_current_function_as_changed(); + } if let Some(folded_expr) = match expr { Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx), @@ -124,6 +127,15 @@ impl<'a> PeepholeOptimizations { e.operator = e.operator.equality_inverse_operator().unwrap(); Some(ctx.ast.move_expression(&mut expr.argument)) } + Expression::ConditionalExpression(conditional_expr) => { + if let Expression::BinaryExpression(e) = &mut conditional_expr.test { + if e.operator.is_equality() { + e.operator = e.operator.equality_inverse_operator().unwrap(); + return Some(ctx.ast.move_expression(&mut expr.argument)); + } + } + None + } _ => None, } } @@ -331,9 +343,13 @@ impl<'a> PeepholeOptimizations { unreachable!() }; let return_stmt = return_stmt.unbox(); - match return_stmt.argument { - Some(e) => e, - None => ctx.ast.void_0(return_stmt.span), + 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) } } diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index de00649ac2606..72223943c40ca 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -20,10 +20,6 @@ use rustc_hash::FxHashSet; pub struct PeepholeOptimizations { target: ESTarget, - /// `in_fixed_loop`: Do not compress syntaxes that are hard to analyze inside the fixed loop. - /// Opposite of `late` in Closure Compiler. - in_fixed_loop: bool, - /// Walk the ast in a fixed point loop until no changes are made. /// `prev_function_changed`, `functions_changed` and `current_function` track changes /// in top level and each function. No minification code are run if the function is not changed @@ -37,10 +33,9 @@ pub struct PeepholeOptimizations { } impl<'a> PeepholeOptimizations { - pub fn new(target: ESTarget, in_fixed_loop: bool) -> Self { + pub fn new(target: ESTarget) -> Self { Self { target, - in_fixed_loop, iteration: 0, prev_functions_changed: FxHashSet::default(), functions_changed: FxHashSet::default(), @@ -84,7 +79,7 @@ impl<'a> PeepholeOptimizations { } fn is_prev_function_changed(&self) -> bool { - if !self.in_fixed_loop || self.iteration == 0 { + if self.iteration == 0 { return true; } if let Some((_, prev_changed, _)) = self.current_function_stack.last() { @@ -159,13 +154,6 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { self.minimize_exit_points(body, ctx); } - fn exit_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { - if !self.is_prev_function_changed() { - return; - } - self.remove_dead_code_exit_class_body(body, ctx); - } - fn exit_variable_declaration( &mut self, decl: &mut VariableDeclaration<'a>, @@ -195,17 +183,6 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { self.substitute_call_expression(expr, ctx); } - fn exit_member_expression( - &mut self, - expr: &mut MemberExpression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - if !self.is_prev_function_changed() { - return; - } - self.convert_to_dotted_properties(expr, ctx); - } - fn exit_object_property(&mut self, prop: &mut ObjectProperty<'a>, ctx: &mut TraverseCtx<'a>) { if !self.is_prev_function_changed() { return; @@ -263,11 +240,42 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { } self.substitute_accessor_property(prop, ctx); } +} + +/// Changes that do not interfere with optimizations that are run inside the fixed-point loop, +/// which can be done as a last AST pass. +pub struct LatePeepholeOptimizations { + target: ESTarget, +} + +impl<'a> LatePeepholeOptimizations { + pub fn new(target: ESTarget) -> Self { + Self { target } + } + + pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { + traverse_mut_with_ctx(self, program, ctx); + } +} + +impl<'a> Traverse<'a> for LatePeepholeOptimizations { + fn exit_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + Self::convert_to_dotted_properties(expr, ctx); + } + + fn exit_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + Self::remove_dead_code_exit_class_body(body, ctx); + } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + Self::substitute_exit_expression(expr, ctx); + } fn exit_catch_clause(&mut self, catch: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { - if !self.is_prev_function_changed() { - return; - } self.substitute_catch_clause(catch, ctx); } } @@ -278,7 +286,7 @@ pub struct DeadCodeElimination { impl<'a> DeadCodeElimination { pub fn new() -> Self { - Self { inner: PeepholeOptimizations::new(ESTarget::ESNext, false) } + Self { inner: PeepholeOptimizations::new(ESTarget::ESNext) } } pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index 9681cf5d488e1..e3cdbba8feba1 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -9,7 +9,7 @@ use oxc_traverse::{Ancestor, TraverseCtx}; use crate::{ctx::Ctx, keep_var::KeepVar}; -use super::PeepholeOptimizations; +use super::{LatePeepholeOptimizations, PeepholeOptimizations}; /// Remove Dead Code from the AST. /// @@ -74,16 +74,6 @@ impl<'a, 'b> PeepholeOptimizations { } } - pub fn remove_dead_code_exit_class_body( - &mut self, - body: &mut ClassBody<'a>, - _ctx: &mut TraverseCtx<'a>, - ) { - if !self.in_fixed_loop { - Self::remove_empty_class_static_block(body); - } - } - /// Removes dead code thats comes after `return` statements after inlining `if` statements fn dead_code_elimination(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: Ctx<'a, 'b>) { // Remove code after `return` and `throw` statements @@ -581,8 +571,10 @@ impl<'a, 'b> PeepholeOptimizations { }; (params_empty && body_empty).then(|| ctx.ast.statement_empty(e.span)) } +} - fn remove_empty_class_static_block(body: &mut ClassBody<'a>) { +impl<'a> LatePeepholeOptimizations { + pub fn remove_dead_code_exit_class_body(body: &mut ClassBody<'a>, _ctx: &mut TraverseCtx<'a>) { body.body.retain(|e| !matches!(e, ClassElement::StaticBlock(s) if s.body.is_empty())); } } diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 207a2763b8ece..9055e21b148b0 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -18,21 +18,13 @@ use oxc_traverse::{Ancestor, TraverseCtx}; use crate::ctx::Ctx; -use super::PeepholeOptimizations; +use super::{LatePeepholeOptimizations, PeepholeOptimizations}; /// A peephole optimization that minimizes code by simplifying conditional /// expressions, replacing IFs with HOOKs, replacing object constructors /// with literals, and simplifying returns. /// -impl<'a, 'b> PeepholeOptimizations { - pub fn substitute_catch_clause( - &mut self, - catch: &mut CatchClause<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - self.compress_catch_clause(catch, ctx); - } - +impl<'a> PeepholeOptimizations { pub fn substitute_object_property( &mut self, prop: &mut ObjectProperty<'a>, @@ -156,38 +148,6 @@ impl<'a, 'b> PeepholeOptimizations { *expr = folded_expr; self.mark_current_function_as_changed(); } - - // Out of fixed loop syntax changes happen last. - if self.in_fixed_loop { - return; - } - - if let Expression::NewExpression(e) = expr { - self.try_compress_typed_array_constructor(e, ctx); - } - - 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, - } { - *expr = folded_expr; - self.mark_current_function_as_changed(); - } - } - - fn compress_catch_clause(&mut self, catch: &mut CatchClause<'_>, ctx: &mut TraverseCtx<'a>) { - if !self.in_fixed_loop && self.target >= ESTarget::ES2019 { - if let Some(param) = &catch.param { - if let BindingPatternKind::BindingIdentifier(ident) = ¶m.pattern.kind { - if catch.body.body.is_empty() - || ctx.symbols().get_resolved_references(ident.symbol_id()).count() == 0 - { - catch.param = None; - } - }; - } - } } fn swap_binary_expressions(e: &mut BinaryExpression<'a>) { @@ -199,68 +159,11 @@ impl<'a, 'b> PeepholeOptimizations { } } - /// Transforms `undefined` => `void 0` - fn try_compress_undefined( - &self, - ident: &IdentifierReference<'a>, - ctx: Ctx<'a, 'b>, - ) -> Option> { - debug_assert!(!self.in_fixed_loop); - if !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`. - /// Do not compress `true` in `Object.defineProperty(exports, 'Foo', {enumerable: true, ...})`. - fn try_compress_boolean( - &self, - expr: &mut Expression<'a>, - ctx: Ctx<'a, 'b>, - ) -> Option> { - debug_assert!(!self.in_fixed_loop); - let Expression::BooleanLiteral(lit) = expr else { return None }; - let parent = ctx.ancestry.parent(); - let no_unary = { - if let Ancestor::BinaryExpressionRight(u) = parent { - !matches!( - u.operator(), - BinaryOperator::Addition // Other effect, like string concatenation. - | BinaryOperator::Instanceof // Relational operator. - | BinaryOperator::In - | BinaryOperator::StrictEquality // It checks type, so we should not fold. - | BinaryOperator::StrictInequality - ) - } else { - false - } - }; - // XOR: We should use `!neg` when it is not in binary expression. - let num = ctx.ast.expression_numeric_literal( - lit.span, - if lit.value ^ no_unary { 0.0 } else { 1.0 }, - None, - NumberBase::Decimal, - ); - Some(if no_unary { - num - } else { - ctx.ast.expression_unary(lit.span, UnaryOperator::LogicalNot, num) - }) - } - /// `() => { return foo })` -> `() => foo` fn try_compress_arrow_expression( &mut self, arrow_expr: &mut ArrowFunctionExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) { if !arrow_expr.expression && arrow_expr.body.directives.is_empty() @@ -290,7 +193,7 @@ impl<'a, 'b> PeepholeOptimizations { /// Enabled by `compress.typeofs` fn try_compress_typeof_undefined( expr: &mut BinaryExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let Expression::UnaryExpression(unary_expr) = &expr.left else { return None }; if !unary_expr.operator.is_typeof() { @@ -343,7 +246,7 @@ impl<'a, 'b> PeepholeOptimizations { /// - `document.all == null` is `true` fn try_compress_is_null_or_undefined( expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let op = expr.operator; let target_ops = match op { @@ -389,7 +292,7 @@ impl<'a, 'b> PeepholeOptimizations { right: &mut Expression<'a>, span: Span, (find_op, replace_op): (BinaryOperator, BinaryOperator), - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { enum LeftPairValueResult { Null(Span), @@ -478,7 +381,7 @@ impl<'a, 'b> PeepholeOptimizations { fn try_compress_logical_expression_to_assignment_expression( &self, expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { if self.target < ESTarget::ES2020 { return None; @@ -509,7 +412,7 @@ impl<'a, 'b> PeepholeOptimizations { /// `a || (b || c);` -> `(a || b) || c;` fn try_rotate_logical_expression( expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let Expression::LogicalExpression(right) = &mut expr.right else { return None }; if right.operator != expr.operator { @@ -551,7 +454,7 @@ impl<'a, 'b> PeepholeOptimizations { fn has_no_side_effect_for_evaluation_same_target( assignment_target: &AssignmentTarget, expr: &Expression, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> bool { match (&assignment_target, &expr) { ( @@ -642,7 +545,7 @@ impl<'a, 'b> PeepholeOptimizations { left: &Expression<'a>, right: &Expression<'a>, span: Span, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, inversed: bool, ) -> Option> { let pair = Self::commutative_pair( @@ -767,7 +670,7 @@ impl<'a, 'b> PeepholeOptimizations { fn try_fold_loose_equals_undefined( e: &mut BinaryExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { // `foo == void 0` -> `foo == null`, `foo == undefined` -> `foo == null` // `foo != void 0` -> `foo == null`, `foo == undefined` -> `foo == null` @@ -824,7 +727,7 @@ impl<'a, 'b> PeepholeOptimizations { fn compress_variable_declarator( &mut self, decl: &mut VariableDeclarator<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) { // Destructuring Pattern has error throwing side effect. if decl.kind.is_const() || decl.id.kind.is_destructuring_pattern() { @@ -842,7 +745,7 @@ impl<'a, 'b> PeepholeOptimizations { fn try_compress_normal_assignment_to_combined_assignment( &mut self, expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) { if !matches!(expr.operator, AssignmentOperator::Assign) { return; @@ -867,7 +770,7 @@ impl<'a, 'b> PeepholeOptimizations { fn try_compress_normal_assignment_to_combined_logical_assignment( &mut self, expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) { if self.target < ESTarget::ES2020 { return; @@ -899,7 +802,7 @@ impl<'a, 'b> PeepholeOptimizations { fn try_compress_assignment_to_update_expression( expr: &mut AssignmentExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let target = expr.left.as_simple_assignment_target_mut()?; if !matches!(expr.operator, AssignmentOperator::Subtraction) { @@ -936,7 +839,7 @@ impl<'a, 'b> PeepholeOptimizations { /// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1` fn try_merge_conditional_expression_inside( expr: &mut ConditionalExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let ( Expression::AssignmentExpression(consequent), @@ -979,7 +882,7 @@ impl<'a, 'b> PeepholeOptimizations { /// `BigInt(1)` -> `1` fn try_fold_simple_function_call( call_expr: &mut CallExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { if call_expr.optional || call_expr.arguments.len() >= 2 { return None; @@ -1060,7 +963,7 @@ impl<'a, 'b> PeepholeOptimizations { } /// Fold `Object` or `Array` constructor - fn get_fold_constructor_name(callee: &Expression<'a>, ctx: Ctx<'a, 'b>) -> Option<&'a str> { + fn get_fold_constructor_name(callee: &Expression<'a>, ctx: Ctx<'a, '_>) -> Option<&'a str> { match callee { Expression::StaticMemberExpression(e) => { if !matches!(&e.object, Expression::Identifier(ident) if ident.name == "window") { @@ -1088,7 +991,7 @@ impl<'a, 'b> PeepholeOptimizations { span: Span, name: &'a str, args: &mut Vec<'a, Argument<'a>>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { match name { "Object" if args.is_empty() => { @@ -1161,7 +1064,7 @@ impl<'a, 'b> PeepholeOptimizations { /// `new RegExp()` -> `RegExp()` fn try_fold_new_expression( e: &mut NewExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) -> Option> { let Expression::Identifier(ident) = &e.callee else { return None }; let name = ident.name.as_str(); @@ -1214,49 +1117,10 @@ impl<'a, 'b> PeepholeOptimizations { ) } - /// `new Int8Array(0)` -> `new Int8Array()` (also for other TypedArrays) - fn try_compress_typed_array_constructor( - &mut self, - e: &mut NewExpression<'a>, - ctx: Ctx<'a, 'b>, - ) { - debug_assert!(!self.in_fixed_loop); - let Expression::Identifier(ident) = &e.callee else { return }; - let name = ident.name.as_str(); - if !Self::is_typed_array_name(name) || !ctx.is_global_reference(ident) { - return; - } - if e.arguments.len() == 1 - && e.arguments[0].as_expression().is_some_and(Expression::is_number_0) - { - e.arguments.clear(); - } - } - - /// Whether the name matches any TypedArray name. - /// - /// See for the list of TypedArrays. - fn is_typed_array_name(name: &str) -> bool { - matches!( - name, - "Int8Array" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float32Array" - | "Float64Array" - | "BigInt64Array" - | "BigUint64Array" - ) - } - fn try_compress_chain_call_expression( &mut self, chain_expr: &mut ChainExpression<'a>, - ctx: Ctx<'a, 'b>, + ctx: Ctx<'a, '_>, ) { if let ChainElement::CallExpression(call_expr) = &mut chain_expr.expression { // `window.Object?.()` -> `Object?.()` @@ -1273,7 +1137,7 @@ impl<'a, 'b> PeepholeOptimizations { } } - fn try_fold_template_literal(t: &TemplateLiteral, ctx: Ctx<'a, 'b>) -> Option> { + fn try_fold_template_literal(t: &TemplateLiteral, ctx: Ctx<'a, '_>) -> Option> { t.to_js_string().map(|val| ctx.ast.expression_string_literal(t.span(), val, None)) } @@ -1385,6 +1249,106 @@ impl<'a, 'b> PeepholeOptimizations { } } +impl<'a> LatePeepholeOptimizations { + pub fn substitute_exit_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Expression::NewExpression(e) = expr { + Self::try_compress_typed_array_constructor(e, ctx); + } + + 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, + } { + *expr = folded_expr; + } + } + + /// `new Int8Array(0)` -> `new Int8Array()` (also for other TypedArrays) + fn try_compress_typed_array_constructor(e: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) { + let Expression::Identifier(ident) = &e.callee else { return }; + let name = ident.name.as_str(); + if !Self::is_typed_array_name(name) || !Ctx(ctx).is_global_reference(ident) { + return; + } + if e.arguments.len() == 1 + && e.arguments[0].as_expression().is_some_and(Expression::is_number_0) + { + e.arguments.clear(); + } + } + + /// Transforms `undefined` => `void 0` + fn try_compress_undefined( + ident: &IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + 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>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let Expression::BooleanLiteral(lit) = expr else { return None }; + let num = ctx.ast.expression_numeric_literal( + lit.span, + if lit.value { 0.0 } else { 1.0 }, + None, + NumberBase::Decimal, + ); + Some(ctx.ast.expression_unary(lit.span, UnaryOperator::LogicalNot, num)) + } + + pub fn substitute_catch_clause( + &mut self, + catch: &mut CatchClause<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.target >= ESTarget::ES2019 { + if let Some(param) = &catch.param { + if let BindingPatternKind::BindingIdentifier(ident) = ¶m.pattern.kind { + if catch.body.body.is_empty() + || ctx.symbols().get_resolved_references(ident.symbol_id()).count() == 0 + { + catch.param = None; + } + }; + } + } + } + + /// Whether the name matches any TypedArray name. + /// + /// See for the list of TypedArrays. + fn is_typed_array_name(name: &str) -> bool { + matches!( + name, + "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" + ) + } +} + /// Port from #[cfg(test)] mod test { @@ -1437,20 +1401,20 @@ mod test { #[test] fn test_fold_true_false_comparison() { - test("x == true", "x == 1"); - test("x == false", "x == 0"); - test("x != true", "x != 1"); - test("x < true", "x < 1"); - test("x <= true", "x <= 1"); - test("x > true", "x > 1"); - test("x >= true", "x >= 1"); + test("x == true", "x == !0"); + test("x == false", "x == !1"); + test("x != true", "x != !0"); + test("x < true", "x < !0"); + test("x <= true", "x <= !0"); + test("x > true", "x > !0"); + test("x >= true", "x >= !0"); test("x instanceof true", "x instanceof !0"); test("x + false", "x + !1"); // Order: should perform the nearest. test("x == x instanceof false", "x == x instanceof !1"); - test("x in x >> true", "x in x >> 1"); + test("x in x >> true", "x in x >> !0"); test("x == fake(false)", "x == fake(!1)"); // The following should not be folded. diff --git a/crates/oxc_minifier/tests/ast_passes/mod.rs b/crates/oxc_minifier/tests/ast_passes/mod.rs index 9eb056f66da34..d2073a126da43 100644 --- a/crates/oxc_minifier/tests/ast_passes/mod.rs +++ b/crates/oxc_minifier/tests/ast_passes/mod.rs @@ -42,16 +42,15 @@ fn integration() { }", ); - // FIXME - // test_idempotent( - // "require('./index.js')(function (e, os) { - // if (e) return console.log(e) - // return console.log(JSON.stringify(os)) - // })", - // r#"require("./index.js")(function(e, os) { - // return console.log(e || JSON.stringify(os)); - // });"#, - // ); + test_idempotent( + "require('./index.js')(function (e, os) { + if (e) return console.log(e) + return console.log(JSON.stringify(os)) + })", + r#"require("./index.js")(function(e, os) { + return console.log(e || JSON.stringify(os)); + });"#, + ); test_idempotent( "if (!(foo instanceof Var) || open) { diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 46e5a40009af3..d4a288450060d 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -9,19 +9,19 @@ 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.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js +544.10 kB | 71.74 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js 555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js -1.01 MB | 460.18 kB | 458.89 kB | 126.77 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.18 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js 1.25 MB | 652.90 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js -2.14 MB | 724.01 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js +2.14 MB | 724.01 kB | 724.14 kB | 179.93 kB | 181.07 kB | victory.js 3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js -6.69 MB | 2.31 MB | 2.31 MB | 491.99 kB | 488.28 kB | antd.js +6.69 MB | 2.31 MB | 2.31 MB | 491.97 kB | 488.28 kB | antd.js -10.95 MB | 3.48 MB | 3.49 MB | 905.39 kB | 915.50 kB | typescript.js +10.95 MB | 3.48 MB | 3.49 MB | 905.34 kB | 915.50 kB | typescript.js