perf(minifier): add LatePeepholeOptimizations (#8651)

This PR adds a `LatePeepholeOptimizations` pass for minifications that
don't interact with the fixed point loop.

While working on this I found a couple of cases where the previous fixed
point loop is not idempotent.
This commit is contained in:
Boshen 2025-01-22 16:19:06 +08:00 committed by GitHub
parent 00dc63f6a5
commit 9953ac7cad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 272 additions and 267 deletions

View file

@ -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<ConstantValue<'a>> {
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() {

View file

@ -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>) {

View file

@ -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.
///
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/ConvertToDottedProperties.java>
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);
}
}
}

View file

@ -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<Expression<'a>> {
// 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<Expression<'a>> {
// 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");

View file

@ -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;
if let Expression::ConditionalExpression(logical_expr) = expr {
if let Some(e) = Self::try_minimize_conditional(logical_expr, ctx) {
*expr = e;
changed = true;
}
}
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)) {
changed = true;
local_change = true;
}
if let Some(e) = Self::try_minimize_conditional(logical_expr, ctx) {
*expr = e;
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)
}
}

View file

@ -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>) {

View file

@ -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()));
}
}

View file

@ -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.
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntax.java>
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) = &param.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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
// `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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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 <https://tc39.es/ecma262/multipage/indexed-collections.html#sec-typedarray-objects> 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<Expression<'a>> {
fn try_fold_template_literal(t: &TemplateLiteral, ctx: Ctx<'a, '_>) -> Option<Expression<'a>> {
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<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>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
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) = &param.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 <https://tc39.es/ecma262/multipage/indexed-collections.html#sec-typedarray-objects> 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 <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntaxTest.java>
#[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.

View file

@ -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) {

View file

@ -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