mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
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:
parent
00dc63f6a5
commit
9953ac7cad
10 changed files with 272 additions and 267 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) = ¶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<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) = ¶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 <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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue