mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +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())))
|
Some(ConstantValue::String(Cow::Borrowed(lit.value.as_str())))
|
||||||
}
|
}
|
||||||
Expression::StaticMemberExpression(e) => self.eval_static_member_expression(e),
|
Expression::StaticMemberExpression(e) => self.eval_static_member_expression(e),
|
||||||
|
Expression::ComputedMemberExpression(e) => self.eval_computed_member_expression(e),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -421,7 +422,30 @@ pub trait ConstantEvaluation<'a> {
|
||||||
match expr.property.name.as_str() {
|
match expr.property.name.as_str() {
|
||||||
"length" => {
|
"length" => {
|
||||||
if let Some(ConstantValue::String(s)) = self.eval_expression(&expr.object) {
|
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()))
|
Some(ConstantValue::Number(s.encode_utf16().count().to_f64().unwrap()))
|
||||||
} else {
|
} else {
|
||||||
if expr.object.may_have_side_effects() {
|
if expr.object.may_have_side_effects() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use oxc_semantic::{ScopeTree, SemanticBuilder, SymbolTable};
|
||||||
use oxc_traverse::ReusableTraverseCtx;
|
use oxc_traverse::ReusableTraverseCtx;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
peephole::{DeadCodeElimination, Normalize, NormalizeOptions, PeepholeOptimizations},
|
peephole::{
|
||||||
|
DeadCodeElimination, LatePeepholeOptimizations, Normalize, NormalizeOptions,
|
||||||
|
PeepholeOptimizations,
|
||||||
|
},
|
||||||
CompressOptions,
|
CompressOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -33,8 +36,8 @@ impl<'a> Compressor<'a> {
|
||||||
let mut ctx = ReusableTraverseCtx::new(scopes, symbols, self.allocator);
|
let mut ctx = ReusableTraverseCtx::new(scopes, symbols, self.allocator);
|
||||||
let normalize_options = NormalizeOptions { convert_while_to_fors: true };
|
let normalize_options = NormalizeOptions { convert_while_to_fors: true };
|
||||||
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
|
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).run_in_loop(program, &mut ctx);
|
||||||
PeepholeOptimizations::new(self.options.target, false).build(program, &mut ctx);
|
LatePeepholeOptimizations::new(self.options.target).build(program, &mut ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dead_code_elimination(self, program: &mut Program<'a>) {
|
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_syntax::identifier::is_identifier_name;
|
||||||
use oxc_traverse::TraverseCtx;
|
use oxc_traverse::TraverseCtx;
|
||||||
|
|
||||||
use super::PeepholeOptimizations;
|
use super::LatePeepholeOptimizations;
|
||||||
use crate::ctx::Ctx;
|
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
|
/// Converts property accesses from quoted string or bracket access syntax to dot or unquoted string
|
||||||
/// syntax, where possible. Dot syntax is more compact.
|
/// syntax, where possible. Dot syntax is more compact.
|
||||||
///
|
///
|
||||||
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/ConvertToDottedProperties.java>
|
/// <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`
|
||||||
/// `foo?.['bar']` -> `foo?.bar`
|
/// `foo?.['bar']` -> `foo?.bar`
|
||||||
fn try_compress_computed_member_expression(
|
pub fn convert_to_dotted_properties(
|
||||||
&mut self,
|
|
||||||
expr: &mut MemberExpression<'a>,
|
expr: &mut MemberExpression<'a>,
|
||||||
ctx: Ctx<'a, '_>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) {
|
) {
|
||||||
let MemberExpression::ComputedMemberExpression(e) = expr else { return };
|
let MemberExpression::ComputedMemberExpression(e) = expr else { return };
|
||||||
let Expression::StringLiteral(s) = &e.expression else { return };
|
let Expression::StringLiteral(s) = &e.expression else { return };
|
||||||
|
|
@ -35,7 +25,6 @@ impl<'a> PeepholeOptimizations {
|
||||||
*expr = MemberExpression::StaticMemberExpression(
|
*expr = MemberExpression::StaticMemberExpression(
|
||||||
ctx.ast.alloc_static_member_expression(e.span, object, property, e.optional),
|
ctx.ast.alloc_static_member_expression(e.span, object, property, e.optional),
|
||||||
);
|
);
|
||||||
self.mark_current_function_as_changed();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let v = s.value.as_str();
|
let v = s.value.as_str();
|
||||||
|
|
@ -43,7 +32,8 @@ impl<'a> PeepholeOptimizations {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(n) = Ctx::string_to_equivalent_number_value(v) {
|
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)),
|
.or_else(|| Self::try_fold_binary_typeof_comparison(e, ctx)),
|
||||||
Expression::UnaryExpression(e) => Self::try_fold_unary_expr(e, ctx),
|
Expression::UnaryExpression(e) => Self::try_fold_unary_expr(e, ctx),
|
||||||
Expression::StaticMemberExpression(e) => Self::try_fold_static_member_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::LogicalExpression(e) => Self::try_fold_logical_expr(e, ctx),
|
||||||
Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx),
|
Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx),
|
||||||
Expression::CallExpression(e) => Self::try_fold_number_constructor(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(
|
fn try_fold_static_member_expr(
|
||||||
static_member_expr: &mut StaticMemberExpression<'a>,
|
e: &mut StaticMemberExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, 'b>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
// TODO: tryFoldObjectPropAccess(n, left, name)
|
// TODO: tryFoldObjectPropAccess(n, left, name)
|
||||||
ctx.eval_static_member_expression(static_member_expr)
|
ctx.eval_static_member_expression(e).map(|value| ctx.value_to_expr(e.span, value))
|
||||||
.map(|value| ctx.value_to_expr(static_member_expr.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(
|
fn try_fold_logical_expr(
|
||||||
|
|
@ -1618,6 +1626,7 @@ mod test {
|
||||||
test("x = [].length", "x = 0");
|
test("x = [].length", "x = 0");
|
||||||
test("x = [1,2,3].length", "x = 3");
|
test("x = [1,2,3].length", "x = 3");
|
||||||
// test("x = [a,b].length", "x = 2");
|
// test("x = [a,b].length", "x = 2");
|
||||||
|
test("x = 'abc'['length']", "x = 3");
|
||||||
|
|
||||||
// Not handled yet
|
// Not handled yet
|
||||||
test("x = [,,1].length", "x = 3");
|
test("x = [,,1].length", "x = 3");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use oxc_allocator::Vec;
|
use oxc_allocator::Vec;
|
||||||
use oxc_ast::{ast::*, NONE};
|
use oxc_ast::{ast::*, NONE};
|
||||||
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType};
|
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType};
|
||||||
|
use oxc_semantic::ReferenceFlags;
|
||||||
use oxc_span::{cmp::ContentEq, GetSpan};
|
use oxc_span::{cmp::ContentEq, GetSpan};
|
||||||
use oxc_traverse::{Ancestor, TraverseCtx};
|
use oxc_traverse::{Ancestor, MaybeBoundIdentifier, TraverseCtx};
|
||||||
|
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
|
|
||||||
|
|
@ -22,15 +23,15 @@ impl<'a> PeepholeOptimizations {
|
||||||
ctx: &mut TraverseCtx<'a>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) {
|
) {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
let mut changed2 = false;
|
loop {
|
||||||
Self::try_replace_if(stmts, &mut changed2, ctx);
|
let mut local_change = false;
|
||||||
while changed2 {
|
Self::try_replace_if(stmts, &mut local_change, ctx);
|
||||||
changed2 = false;
|
if local_change {
|
||||||
Self::try_replace_if(stmts, &mut changed2, ctx);
|
|
||||||
if stmts.iter().any(|stmt| matches!(stmt, Statement::EmptyStatement(_))) {
|
|
||||||
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
|
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
|
||||||
|
changed = local_change;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
changed = changed2;
|
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
self.mark_current_function_as_changed();
|
self.mark_current_function_as_changed();
|
||||||
|
|
@ -77,24 +78,26 @@ impl<'a> PeepholeOptimizations {
|
||||||
expr: &mut Expression<'a>,
|
expr: &mut Expression<'a>,
|
||||||
ctx: &mut TraverseCtx<'a>,
|
ctx: &mut TraverseCtx<'a>,
|
||||||
) {
|
) {
|
||||||
loop {
|
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
if let Expression::ConditionalExpression(logical_expr) = expr {
|
loop {
|
||||||
if let Some(e) = Self::try_minimize_conditional(logical_expr, ctx) {
|
let mut local_change = false;
|
||||||
*expr = e;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Expression::ConditionalExpression(logical_expr) = expr {
|
if let Expression::ConditionalExpression(logical_expr) = expr {
|
||||||
if Self::try_fold_expr_in_boolean_context(&mut logical_expr.test, Ctx(ctx)) {
|
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;
|
||||||
|
local_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if local_change {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
self.mark_current_function_as_changed();
|
self.mark_current_function_as_changed();
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(folded_expr) = match expr {
|
if let Some(folded_expr) = match expr {
|
||||||
|
|
@ -124,6 +127,15 @@ impl<'a> PeepholeOptimizations {
|
||||||
e.operator = e.operator.equality_inverse_operator().unwrap();
|
e.operator = e.operator.equality_inverse_operator().unwrap();
|
||||||
Some(ctx.ast.move_expression(&mut expr.argument))
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,9 +343,13 @@ impl<'a> PeepholeOptimizations {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let return_stmt = return_stmt.unbox();
|
let return_stmt = return_stmt.unbox();
|
||||||
match return_stmt.argument {
|
if let Some(e) = return_stmt.argument {
|
||||||
Some(e) => e,
|
e
|
||||||
None => ctx.ast.void_0(return_stmt.span),
|
} 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 {
|
pub struct PeepholeOptimizations {
|
||||||
target: ESTarget,
|
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.
|
/// Walk the ast in a fixed point loop until no changes are made.
|
||||||
/// `prev_function_changed`, `functions_changed` and `current_function` track changes
|
/// `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
|
/// 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 {
|
impl<'a> PeepholeOptimizations {
|
||||||
pub fn new(target: ESTarget, in_fixed_loop: bool) -> Self {
|
pub fn new(target: ESTarget) -> Self {
|
||||||
Self {
|
Self {
|
||||||
target,
|
target,
|
||||||
in_fixed_loop,
|
|
||||||
iteration: 0,
|
iteration: 0,
|
||||||
prev_functions_changed: FxHashSet::default(),
|
prev_functions_changed: FxHashSet::default(),
|
||||||
functions_changed: FxHashSet::default(),
|
functions_changed: FxHashSet::default(),
|
||||||
|
|
@ -84,7 +79,7 @@ impl<'a> PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_prev_function_changed(&self) -> bool {
|
fn is_prev_function_changed(&self) -> bool {
|
||||||
if !self.in_fixed_loop || self.iteration == 0 {
|
if self.iteration == 0 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if let Some((_, prev_changed, _)) = self.current_function_stack.last() {
|
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);
|
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(
|
fn exit_variable_declaration(
|
||||||
&mut self,
|
&mut self,
|
||||||
decl: &mut VariableDeclaration<'a>,
|
decl: &mut VariableDeclaration<'a>,
|
||||||
|
|
@ -195,17 +183,6 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||||
self.substitute_call_expression(expr, ctx);
|
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>) {
|
fn exit_object_property(&mut self, prop: &mut ObjectProperty<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||||
if !self.is_prev_function_changed() {
|
if !self.is_prev_function_changed() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -263,11 +240,42 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
self.substitute_accessor_property(prop, ctx);
|
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>) {
|
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);
|
self.substitute_catch_clause(catch, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +286,7 @@ pub struct DeadCodeElimination {
|
||||||
|
|
||||||
impl<'a> DeadCodeElimination {
|
impl<'a> DeadCodeElimination {
|
||||||
pub fn new() -> Self {
|
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>) {
|
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 crate::{ctx::Ctx, keep_var::KeepVar};
|
||||||
|
|
||||||
use super::PeepholeOptimizations;
|
use super::{LatePeepholeOptimizations, PeepholeOptimizations};
|
||||||
|
|
||||||
/// Remove Dead Code from the AST.
|
/// 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
|
/// 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>) {
|
fn dead_code_elimination(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: Ctx<'a, 'b>) {
|
||||||
// Remove code after `return` and `throw` statements
|
// 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))
|
(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()));
|
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 crate::ctx::Ctx;
|
||||||
|
|
||||||
use super::PeepholeOptimizations;
|
use super::{LatePeepholeOptimizations, PeepholeOptimizations};
|
||||||
|
|
||||||
/// A peephole optimization that minimizes code by simplifying conditional
|
/// A peephole optimization that minimizes code by simplifying conditional
|
||||||
/// expressions, replacing IFs with HOOKs, replacing object constructors
|
/// expressions, replacing IFs with HOOKs, replacing object constructors
|
||||||
/// with literals, and simplifying returns.
|
/// with literals, and simplifying returns.
|
||||||
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntax.java>
|
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntax.java>
|
||||||
impl<'a, 'b> PeepholeOptimizations {
|
impl<'a> PeepholeOptimizations {
|
||||||
pub fn substitute_catch_clause(
|
|
||||||
&mut self,
|
|
||||||
catch: &mut CatchClause<'a>,
|
|
||||||
ctx: &mut TraverseCtx<'a>,
|
|
||||||
) {
|
|
||||||
self.compress_catch_clause(catch, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn substitute_object_property(
|
pub fn substitute_object_property(
|
||||||
&mut self,
|
&mut self,
|
||||||
prop: &mut ObjectProperty<'a>,
|
prop: &mut ObjectProperty<'a>,
|
||||||
|
|
@ -156,38 +148,6 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
*expr = folded_expr;
|
*expr = folded_expr;
|
||||||
self.mark_current_function_as_changed();
|
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>) {
|
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`
|
/// `() => { return foo })` -> `() => foo`
|
||||||
fn try_compress_arrow_expression(
|
fn try_compress_arrow_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
arrow_expr: &mut ArrowFunctionExpression<'a>,
|
arrow_expr: &mut ArrowFunctionExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) {
|
) {
|
||||||
if !arrow_expr.expression
|
if !arrow_expr.expression
|
||||||
&& arrow_expr.body.directives.is_empty()
|
&& arrow_expr.body.directives.is_empty()
|
||||||
|
|
@ -290,7 +193,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// Enabled by `compress.typeofs`
|
/// Enabled by `compress.typeofs`
|
||||||
fn try_compress_typeof_undefined(
|
fn try_compress_typeof_undefined(
|
||||||
expr: &mut BinaryExpression<'a>,
|
expr: &mut BinaryExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let Expression::UnaryExpression(unary_expr) = &expr.left else { return None };
|
let Expression::UnaryExpression(unary_expr) = &expr.left else { return None };
|
||||||
if !unary_expr.operator.is_typeof() {
|
if !unary_expr.operator.is_typeof() {
|
||||||
|
|
@ -343,7 +246,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// - `document.all == null` is `true`
|
/// - `document.all == null` is `true`
|
||||||
fn try_compress_is_null_or_undefined(
|
fn try_compress_is_null_or_undefined(
|
||||||
expr: &mut LogicalExpression<'a>,
|
expr: &mut LogicalExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let op = expr.operator;
|
let op = expr.operator;
|
||||||
let target_ops = match op {
|
let target_ops = match op {
|
||||||
|
|
@ -389,7 +292,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
right: &mut Expression<'a>,
|
right: &mut Expression<'a>,
|
||||||
span: Span,
|
span: Span,
|
||||||
(find_op, replace_op): (BinaryOperator, BinaryOperator),
|
(find_op, replace_op): (BinaryOperator, BinaryOperator),
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
enum LeftPairValueResult {
|
enum LeftPairValueResult {
|
||||||
Null(Span),
|
Null(Span),
|
||||||
|
|
@ -478,7 +381,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
fn try_compress_logical_expression_to_assignment_expression(
|
fn try_compress_logical_expression_to_assignment_expression(
|
||||||
&self,
|
&self,
|
||||||
expr: &mut LogicalExpression<'a>,
|
expr: &mut LogicalExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
if self.target < ESTarget::ES2020 {
|
if self.target < ESTarget::ES2020 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -509,7 +412,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// `a || (b || c);` -> `(a || b) || c;`
|
/// `a || (b || c);` -> `(a || b) || c;`
|
||||||
fn try_rotate_logical_expression(
|
fn try_rotate_logical_expression(
|
||||||
expr: &mut LogicalExpression<'a>,
|
expr: &mut LogicalExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let Expression::LogicalExpression(right) = &mut expr.right else { return None };
|
let Expression::LogicalExpression(right) = &mut expr.right else { return None };
|
||||||
if right.operator != expr.operator {
|
if right.operator != expr.operator {
|
||||||
|
|
@ -551,7 +454,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
fn has_no_side_effect_for_evaluation_same_target(
|
fn has_no_side_effect_for_evaluation_same_target(
|
||||||
assignment_target: &AssignmentTarget,
|
assignment_target: &AssignmentTarget,
|
||||||
expr: &Expression,
|
expr: &Expression,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match (&assignment_target, &expr) {
|
match (&assignment_target, &expr) {
|
||||||
(
|
(
|
||||||
|
|
@ -642,7 +545,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
left: &Expression<'a>,
|
left: &Expression<'a>,
|
||||||
right: &Expression<'a>,
|
right: &Expression<'a>,
|
||||||
span: Span,
|
span: Span,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
inversed: bool,
|
inversed: bool,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let pair = Self::commutative_pair(
|
let pair = Self::commutative_pair(
|
||||||
|
|
@ -767,7 +670,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
|
|
||||||
fn try_fold_loose_equals_undefined(
|
fn try_fold_loose_equals_undefined(
|
||||||
e: &mut BinaryExpression<'a>,
|
e: &mut BinaryExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
// `foo == void 0` -> `foo == null`, `foo == undefined` -> `foo == null`
|
// `foo == void 0` -> `foo == null`, `foo == undefined` -> `foo == null`
|
||||||
// `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(
|
fn compress_variable_declarator(
|
||||||
&mut self,
|
&mut self,
|
||||||
decl: &mut VariableDeclarator<'a>,
|
decl: &mut VariableDeclarator<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) {
|
) {
|
||||||
// Destructuring Pattern has error throwing side effect.
|
// Destructuring Pattern has error throwing side effect.
|
||||||
if decl.kind.is_const() || decl.id.kind.is_destructuring_pattern() {
|
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(
|
fn try_compress_normal_assignment_to_combined_assignment(
|
||||||
&mut self,
|
&mut self,
|
||||||
expr: &mut AssignmentExpression<'a>,
|
expr: &mut AssignmentExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) {
|
) {
|
||||||
if !matches!(expr.operator, AssignmentOperator::Assign) {
|
if !matches!(expr.operator, AssignmentOperator::Assign) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -867,7 +770,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
fn try_compress_normal_assignment_to_combined_logical_assignment(
|
fn try_compress_normal_assignment_to_combined_logical_assignment(
|
||||||
&mut self,
|
&mut self,
|
||||||
expr: &mut AssignmentExpression<'a>,
|
expr: &mut AssignmentExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) {
|
) {
|
||||||
if self.target < ESTarget::ES2020 {
|
if self.target < ESTarget::ES2020 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -899,7 +802,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
|
|
||||||
fn try_compress_assignment_to_update_expression(
|
fn try_compress_assignment_to_update_expression(
|
||||||
expr: &mut AssignmentExpression<'a>,
|
expr: &mut AssignmentExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let target = expr.left.as_simple_assignment_target_mut()?;
|
let target = expr.left.as_simple_assignment_target_mut()?;
|
||||||
if !matches!(expr.operator, AssignmentOperator::Subtraction) {
|
if !matches!(expr.operator, AssignmentOperator::Subtraction) {
|
||||||
|
|
@ -936,7 +839,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1`
|
/// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1`
|
||||||
fn try_merge_conditional_expression_inside(
|
fn try_merge_conditional_expression_inside(
|
||||||
expr: &mut ConditionalExpression<'a>,
|
expr: &mut ConditionalExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let (
|
let (
|
||||||
Expression::AssignmentExpression(consequent),
|
Expression::AssignmentExpression(consequent),
|
||||||
|
|
@ -979,7 +882,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// `BigInt(1)` -> `1`
|
/// `BigInt(1)` -> `1`
|
||||||
fn try_fold_simple_function_call(
|
fn try_fold_simple_function_call(
|
||||||
call_expr: &mut CallExpression<'a>,
|
call_expr: &mut CallExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
if call_expr.optional || call_expr.arguments.len() >= 2 {
|
if call_expr.optional || call_expr.arguments.len() >= 2 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -1060,7 +963,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fold `Object` or `Array` constructor
|
/// 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 {
|
match callee {
|
||||||
Expression::StaticMemberExpression(e) => {
|
Expression::StaticMemberExpression(e) => {
|
||||||
if !matches!(&e.object, Expression::Identifier(ident) if ident.name == "window") {
|
if !matches!(&e.object, Expression::Identifier(ident) if ident.name == "window") {
|
||||||
|
|
@ -1088,7 +991,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
span: Span,
|
span: Span,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
args: &mut Vec<'a, Argument<'a>>,
|
args: &mut Vec<'a, Argument<'a>>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
match name {
|
match name {
|
||||||
"Object" if args.is_empty() => {
|
"Object" if args.is_empty() => {
|
||||||
|
|
@ -1161,7 +1064,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// `new RegExp()` -> `RegExp()`
|
/// `new RegExp()` -> `RegExp()`
|
||||||
fn try_fold_new_expression(
|
fn try_fold_new_expression(
|
||||||
e: &mut NewExpression<'a>,
|
e: &mut NewExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let Expression::Identifier(ident) = &e.callee else { return None };
|
let Expression::Identifier(ident) = &e.callee else { return None };
|
||||||
let name = ident.name.as_str();
|
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(
|
fn try_compress_chain_call_expression(
|
||||||
&mut self,
|
&mut self,
|
||||||
chain_expr: &mut ChainExpression<'a>,
|
chain_expr: &mut ChainExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, '_>,
|
||||||
) {
|
) {
|
||||||
if let ChainElement::CallExpression(call_expr) = &mut chain_expr.expression {
|
if let ChainElement::CallExpression(call_expr) = &mut chain_expr.expression {
|
||||||
// `window.Object?.()` -> `Object?.()`
|
// `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))
|
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>
|
/// Port from <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntaxTest.java>
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
@ -1437,20 +1401,20 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fold_true_false_comparison() {
|
fn test_fold_true_false_comparison() {
|
||||||
test("x == true", "x == 1");
|
test("x == true", "x == !0");
|
||||||
test("x == false", "x == 0");
|
test("x == false", "x == !1");
|
||||||
test("x != true", "x != 1");
|
test("x != true", "x != !0");
|
||||||
test("x < true", "x < 1");
|
test("x < true", "x < !0");
|
||||||
test("x <= true", "x <= 1");
|
test("x <= true", "x <= !0");
|
||||||
test("x > true", "x > 1");
|
test("x > true", "x > !0");
|
||||||
test("x >= true", "x >= 1");
|
test("x >= true", "x >= !0");
|
||||||
|
|
||||||
test("x instanceof true", "x instanceof !0");
|
test("x instanceof true", "x instanceof !0");
|
||||||
test("x + false", "x + !1");
|
test("x + false", "x + !1");
|
||||||
|
|
||||||
// Order: should perform the nearest.
|
// Order: should perform the nearest.
|
||||||
test("x == x instanceof false", "x == x instanceof !1");
|
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)");
|
test("x == fake(false)", "x == fake(!1)");
|
||||||
|
|
||||||
// The following should not be folded.
|
// The following should not be folded.
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,15 @@ fn integration() {
|
||||||
}",
|
}",
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME
|
test_idempotent(
|
||||||
// test_idempotent(
|
"require('./index.js')(function (e, os) {
|
||||||
// "require('./index.js')(function (e, os) {
|
if (e) return console.log(e)
|
||||||
// if (e) return console.log(e)
|
return console.log(JSON.stringify(os))
|
||||||
// return console.log(JSON.stringify(os))
|
})",
|
||||||
// })",
|
r#"require("./index.js")(function(e, os) {
|
||||||
// r#"require("./index.js")(function(e, os) {
|
return console.log(e || JSON.stringify(os));
|
||||||
// return console.log(e || JSON.stringify(os));
|
});"#,
|
||||||
// });"#,
|
);
|
||||||
// );
|
|
||||||
|
|
||||||
test_idempotent(
|
test_idempotent(
|
||||||
"if (!(foo instanceof Var) || open) {
|
"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
|
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
|
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
|
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
|
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