feat(ecmascript): add ConstantEvaluation (#6549)

This commit is contained in:
Boshen 2024-10-14 07:53:44 +00:00
parent 67ad08a056
commit 3556062213
11 changed files with 343 additions and 255 deletions

View file

@ -0,0 +1,171 @@
use core::f64;
use std::borrow::Cow;
use num_traits::Zero;
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
pub enum ConstantValue<'a> {
Number(f64),
String(Cow<'a, str>),
Identifier,
Undefined,
}
// impl<'a> ConstantValue<'a> {
// fn to_boolean(&self) -> Option<bool> {
// match self {
// Self::Number(n) => Some(!n.is_zero()),
// Self::String(s) => Some(!s.is_empty()),
// Self::Identifier => None,
// Self::Undefined => Some(false),
// }
// }
// }
pub trait ConstantEvaluation<'a> {
fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool {
matches!(ident.name.as_str(), "undefined" | "NaN" | "Infinity")
}
fn resolve_binding(&self, ident: &IdentifierReference<'a>) -> Option<ConstantValue> {
match ident.name.as_str() {
"undefined" if self.is_global_reference(ident) => Some(ConstantValue::Undefined),
"NaN" if self.is_global_reference(ident) => Some(ConstantValue::Number(f64::NAN)),
"Infinity" if self.is_global_reference(ident) => {
Some(ConstantValue::Number(f64::INFINITY))
}
_ => None,
}
}
fn eval_to_boolean(&self, expr: &Expression<'a>) -> Option<bool> {
match expr {
Expression::Identifier(ident) => match ident.name.as_str() {
"undefined" | "NaN" if self.is_global_reference(ident) => Some(false),
"Infinity" if self.is_global_reference(ident) => Some(true),
_ => None,
},
Expression::LogicalExpression(logical_expr) => {
match logical_expr.operator {
// true && true -> true
// true && false -> false
// a && true -> None
LogicalOperator::And => {
let left = self.eval_to_boolean(&logical_expr.left);
let right = self.eval_to_boolean(&logical_expr.right);
match (left, right) {
(Some(true), Some(true)) => Some(true),
(Some(false), _) | (_, Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
// true || false -> true
// false || false -> false
// a || b -> None
LogicalOperator::Or => {
let left = self.eval_to_boolean(&logical_expr.left);
let right = self.eval_to_boolean(&logical_expr.right);
match (left, right) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
LogicalOperator::Coalesce => None,
}
}
Expression::SequenceExpression(sequence_expr) => {
// For sequence expression, the value is the value of the RHS.
sequence_expr.expressions.last().and_then(|e| self.eval_to_boolean(e))
}
Expression::UnaryExpression(unary_expr) => {
match unary_expr.operator {
UnaryOperator::Void => Some(false),
UnaryOperator::BitwiseNot
| UnaryOperator::UnaryPlus
| UnaryOperator::UnaryNegation => {
// `~0 -> true` `+1 -> true` `+0 -> false` `-0 -> false`
self.eval_to_number(expr).map(|value| !value.is_zero())
}
UnaryOperator::LogicalNot => {
// !true -> false
self.eval_to_boolean(&unary_expr.argument).map(|b| !b)
}
_ => None,
}
}
Expression::AssignmentExpression(assign_expr) => {
match assign_expr.operator {
AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None,
// For ASSIGN, the value is the value of the RHS.
_ => self.eval_to_boolean(&assign_expr.right),
}
}
expr => {
use crate::ToBoolean;
expr.to_boolean()
}
}
}
fn eval_to_number(&self, expr: &Expression<'a>) -> Option<f64> {
match expr {
Expression::Identifier(ident) => match ident.name.as_str() {
"undefined" | "NaN" if self.is_global_reference(ident) => Some(f64::NAN),
"Infinity" if self.is_global_reference(ident) => Some(f64::INFINITY),
_ => None,
},
Expression::UnaryExpression(unary_expr) => match unary_expr.operator {
UnaryOperator::UnaryPlus => self.eval_to_number(&unary_expr.argument),
UnaryOperator::UnaryNegation => {
self.eval_to_number(&unary_expr.argument).map(|v| -v)
}
// UnaryOperator::BitwiseNot => {
// unary_expr.argument.to_number().map(|value| {
// match value {
// NumberValue::Number(num) => NumberValue::Number(f64::from(
// !NumericLiteral::ecmascript_to_int32(num),
// )),
// // ~Infinity -> -1
// // ~-Infinity -> -1
// // ~NaN -> -1
// _ => NumberValue::Number(-1_f64),
// }
// })
// }
UnaryOperator::LogicalNot => {
self.eval_to_boolean(expr).map(|b| if b { 1_f64 } else { 0_f64 })
}
UnaryOperator::Void => Some(f64::NAN),
_ => None,
},
expr => {
use crate::ToNumber;
expr.to_number()
}
}
}
fn eval_expression(&self, expr: &Expression<'a>) -> Option<ConstantValue> {
match expr {
Expression::LogicalExpression(e) => self.eval_logical_expression(e),
Expression::Identifier(ident) => self.resolve_binding(ident),
_ => None,
}
}
fn eval_logical_expression(&self, expr: &LogicalExpression<'a>) -> Option<ConstantValue> {
match expr.operator {
LogicalOperator::And => {
if self.eval_to_boolean(&expr.left) == Some(true) {
self.eval_expression(&expr.right)
} else {
self.eval_expression(&expr.left)
}
}
_ => None,
}
}
}

View file

@ -17,11 +17,22 @@ mod to_int_32;
mod to_number;
mod to_string;
// Constant Evaluation
mod constant_evaluation;
pub use self::{
bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList,
private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName,
string_char_at::StringCharAt, string_index_of::StringIndexOf,
string_last_index_of::StringLastIndexOf, string_to_big_int::StringToBigInt,
to_big_int::ToBigInt, to_boolean::ToBoolean, to_int_32::ToInt32, to_number::ToNumber,
bound_names::BoundNames,
constant_evaluation::{ConstantEvaluation, ConstantValue},
is_simple_parameter_list::IsSimpleParameterList,
private_bound_identifiers::PrivateBoundIdentifiers,
prop_name::PropName,
string_char_at::StringCharAt,
string_index_of::StringIndexOf,
string_last_index_of::StringLastIndexOf,
string_to_big_int::StringToBigInt,
to_big_int::ToBigInt,
to_boolean::ToBoolean,
to_int_32::ToInt32,
to_number::ToNumber,
to_string::ToJsString,
};

View file

@ -1,8 +1,4 @@
use num_traits::Zero;
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use crate::ToNumber;
use oxc_ast::ast::Expression;
/// `ToBoolean`
///
@ -13,7 +9,16 @@ pub trait ToBoolean<'a> {
impl<'a> ToBoolean<'a> for Expression<'a> {
fn to_boolean(&self) -> Option<bool> {
// 1. If argument is a Boolean, return argument.
// 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0, or the empty String, return false.
// 3. NOTE: This step is replaced in section B.3.6.1.
// 4. Return true.
match self {
Expression::Identifier(ident) => match ident.name.as_str() {
"NaN" | "undefined" => Some(false),
"Infinity" => Some(true),
_ => None,
},
Expression::RegExpLiteral(_)
| Expression::ArrayExpression(_)
| Expression::ArrowFunctionExpression(_)
@ -35,73 +40,6 @@ impl<'a> ToBoolean<'a> for Expression<'a> {
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(|cooked| !cooked.is_empty())
}
Expression::Identifier(ident) => match ident.name.as_str() {
"NaN" | "undefined" => Some(false),
"Infinity" => Some(true),
_ => None,
},
Expression::AssignmentExpression(assign_expr) => {
match assign_expr.operator {
AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None,
// For ASSIGN, the value is the value of the RHS.
_ => assign_expr.right.to_boolean(),
}
}
Expression::LogicalExpression(logical_expr) => {
match logical_expr.operator {
// true && true -> true
// true && false -> false
// a && true -> None
LogicalOperator::And => {
let left = logical_expr.left.to_boolean();
let right = logical_expr.right.to_boolean();
match (left, right) {
(Some(true), Some(true)) => Some(true),
(Some(false), _) | (_, Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
// true || false -> true
// false || false -> false
// a || b -> None
LogicalOperator::Or => {
let left = logical_expr.left.to_boolean();
let right = logical_expr.right.to_boolean();
match (left, right) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
LogicalOperator::Coalesce => None,
}
}
Expression::SequenceExpression(sequence_expr) => {
// For sequence expression, the value is the value of the RHS.
sequence_expr.expressions.last().and_then(ToBoolean::to_boolean)
}
Expression::UnaryExpression(unary_expr) => {
if unary_expr.operator == UnaryOperator::Void {
Some(false)
} else if matches!(
unary_expr.operator,
UnaryOperator::BitwiseNot
| UnaryOperator::UnaryPlus
| UnaryOperator::UnaryNegation
) {
// ~0 -> true
// +1 -> true
// +0 -> false
// -0 -> false
self.to_number().map(|value| !value.is_zero())
} else if unary_expr.operator == UnaryOperator::LogicalNot {
// !true -> false
unary_expr.argument.to_boolean().map(|b| !b)
} else {
None
}
}
_ => None,
}
}

View file

@ -1,8 +1,5 @@
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_syntax::operator::UnaryOperator;
use crate::ToBoolean;
/// `ToNumber`
///
@ -15,28 +12,6 @@ impl<'a> ToNumber<'a> for Expression<'a> {
fn to_number(&self) -> Option<f64> {
match self {
Expression::NumericLiteral(number_literal) => Some(number_literal.value),
Expression::UnaryExpression(unary_expr) => match unary_expr.operator {
UnaryOperator::UnaryPlus => unary_expr.argument.to_number(),
UnaryOperator::UnaryNegation => unary_expr.argument.to_number().map(|v| -v),
// UnaryOperator::BitwiseNot => {
// unary_expr.argument.to_number().map(|value| {
// match value {
// NumberValue::Number(num) => NumberValue::Number(f64::from(
// !NumericLiteral::ecmascript_to_int32(num),
// )),
// // ~Infinity -> -1
// // ~-Infinity -> -1
// // ~NaN -> -1
// _ => NumberValue::Number(-1_f64),
// }
// })
// }
UnaryOperator::LogicalNot => {
self.to_boolean().map(|tri| if tri { 1_f64 } else { 0_f64 })
}
UnaryOperator::Void => Some(f64::NAN),
_ => None,
},
Expression::BooleanLiteral(bool_literal) => {
if bool_literal.value {
Some(1.0)
@ -50,7 +25,7 @@ impl<'a> ToNumber<'a> for Expression<'a> {
"NaN" | "undefined" => Some(f64::NAN),
_ => None,
},
// TODO: will be implemented in next PR, just for test pass now.
// TODO: StringToNumber
Expression::StringLiteral(string_literal) => {
string_literal.value.parse::<f64>().map_or(Some(f64::NAN), Some)
}

View file

@ -19,21 +19,8 @@ pub use remove_syntax::RemoveSyntax;
pub use statement_fusion::StatementFusion;
use oxc_ast::ast::Program;
use oxc_semantic::{ScopeTree, SymbolTable};
use oxc_traverse::{Traverse, TraverseCtx};
use crate::node_util::NodeUtil;
impl<'a> NodeUtil<'a> for TraverseCtx<'a> {
fn symbols(&self) -> &SymbolTable {
self.scoping.symbols()
}
fn scopes(&self) -> &ScopeTree {
self.scoping.scopes()
}
}
pub trait CompressorPass<'a>: Traverse<'a> {
fn changed(&self) -> bool;

View file

@ -14,7 +14,7 @@ use oxc_syntax::{
use oxc_traverse::{Ancestor, Traverse, TraverseCtx};
use crate::{
node_util::{is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil},
node_util::{is_exact_int64, Ctx, IsLiteralValue, MayHaveSideEffects},
tri::Tri,
value_type::ValueType,
CompressorPass,
@ -43,6 +43,7 @@ impl<'a> CompressorPass<'a> for PeepholeFoldConstants {
impl<'a> Traverse<'a> for PeepholeFoldConstants {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx);
if let Some(folded_expr) = match expr {
Expression::CallExpression(e) => {
Self::try_fold_useless_object_dot_define_properties_call(e, ctx)
@ -66,21 +67,21 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants {
}
}
impl<'a> PeepholeFoldConstants {
impl<'a, 'b> PeepholeFoldConstants {
pub fn new() -> Self {
Self { changed: false }
}
fn try_fold_useless_object_dot_define_properties_call(
_call_expr: &mut CallExpression<'a>,
_ctx: &mut TraverseCtx<'a>,
_ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
None
}
fn try_fold_ctor_cal(
_new_expr: &mut NewExpression<'a>,
_ctx: &mut TraverseCtx<'a>,
_ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
None
}
@ -90,7 +91,7 @@ impl<'a> PeepholeFoldConstants {
/// `typeof(6) --> "number"`
fn try_fold_type_of(
expr: &mut UnaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
if !expr.argument.is_literal_value(/* include_function */ true) {
return None;
@ -115,21 +116,21 @@ impl<'a> PeepholeFoldConstants {
// fn try_fold_spread(
// &mut self,
// _new_expr: &mut NewExpression<'a>,
// _ctx: &mut TraverseCtx<'a>,
// _ctx: Ctx<'a,'b>,
// ) -> Option<Expression<'a>> {
// None
// }
fn try_flatten_array_expression(
_new_expr: &mut ArrayExpression<'a>,
_ctx: &mut TraverseCtx<'a>,
_ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
None
}
fn try_flatten_object_expression(
_new_expr: &mut ObjectExpression<'a>,
_ctx: &mut TraverseCtx<'a>,
_ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
None
}
@ -137,7 +138,7 @@ impl<'a> PeepholeFoldConstants {
fn try_fold_unary_expression(
&mut self,
expr: &mut UnaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
fn is_valid(x: f64) -> bool {
x.is_finite() && x.fract() == 0.0
@ -154,7 +155,6 @@ impl<'a> PeepholeFoldConstants {
}
}
ctx.get_boolean_value(&expr.argument)
.to_option()
.map(|b| ctx.ast.expression_boolean_literal(SPAN, !b))
}
// `-NaN` -> `NaN`
@ -258,7 +258,7 @@ impl<'a> PeepholeFoldConstants {
fn try_reduce_void(
&mut self,
expr: &mut UnaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if (!expr.argument.is_number() || !expr.argument.is_number_0())
&& !expr.may_have_side_effects()
@ -271,7 +271,7 @@ impl<'a> PeepholeFoldConstants {
fn try_fold_logical_expression(
logical_expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
match logical_expr.operator {
LogicalOperator::And | LogicalOperator::Or => Self::try_fold_and_or(logical_expr, ctx),
@ -284,13 +284,13 @@ impl<'a> PeepholeFoldConstants {
/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/09094b551915a6487a980a783831cba58b5739d1/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L587)
pub fn try_fold_and_or(
logical_expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
let op = logical_expr.operator;
debug_assert!(matches!(op, LogicalOperator::And | LogicalOperator::Or));
let left = &logical_expr.left;
let left_val = ctx.get_boolean_value(left).to_option();
let left_val = ctx.get_boolean_value(left);
if let Some(lval) = left_val {
// Bail `0 && (module.exports = {})` for `cjs-module-lexer`.
@ -332,7 +332,7 @@ impl<'a> PeepholeFoldConstants {
return Some(sequence_expr);
} else if let Expression::LogicalExpression(left_child) = &mut logical_expr.left {
if left_child.operator == logical_expr.operator {
let left_child_right_boolean = ctx.get_boolean_value(&left_child.right).to_option();
let left_child_right_boolean = ctx.get_boolean_value(&left_child.right);
let left_child_op = left_child.operator;
if let Some(right_boolean) = left_child_right_boolean {
if !left_child.right.may_have_side_effects() {
@ -361,7 +361,7 @@ impl<'a> PeepholeFoldConstants {
/// Try to fold a nullish coalesce `foo ?? bar`.
pub fn try_fold_coalesce(
logical_expr: &mut LogicalExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce);
let left = &logical_expr.left;
@ -394,7 +394,7 @@ impl<'a> PeepholeFoldConstants {
fn try_fold_binary_expression(
e: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
// TODO: tryReduceOperandsForOp
match e.operator {
@ -424,11 +424,11 @@ impl<'a> PeepholeFoldConstants {
}
}
fn try_fold_addition<'b>(
fn try_fold_addition(
span: Span,
left: &'b Expression<'a>,
right: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
// skip any potentially dangerous compressions
if left.may_have_side_effects() || right.may_have_side_effects() {
@ -469,7 +469,7 @@ impl<'a> PeepholeFoldConstants {
fn try_fold_arithmetic_op(
operation: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
fn shorter_than_original(
result: f64,
@ -544,7 +544,7 @@ impl<'a> PeepholeFoldConstants {
left: f64,
operator: BinaryOperator,
right: f64,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if left.is_finite() && right.is_finite() || !operator.is_arithmetic() {
return None;
@ -585,21 +585,21 @@ impl<'a> PeepholeFoldConstants {
})
}
fn try_fold_instanceof<'b>(
fn try_fold_instanceof(
_span: Span,
_left: &'b Expression<'a>,
_right: &'b Expression<'a>,
_ctx: &mut TraverseCtx<'a>,
_ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
None
}
fn try_fold_comparison<'b>(
fn try_fold_comparison(
span: Span,
op: BinaryOperator,
left: &'b Expression<'a>,
right: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
let value = match Self::evaluate_comparison(op, left, right, ctx) {
Tri::True => true,
@ -609,11 +609,11 @@ impl<'a> PeepholeFoldConstants {
Some(ctx.ast.expression_boolean_literal(span, value))
}
fn evaluate_comparison<'b>(
fn evaluate_comparison(
op: BinaryOperator,
left: &'b Expression<'a>,
right: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Tri {
if left.may_have_side_effects() || right.may_have_side_effects() {
return Tri::Unknown;
@ -646,10 +646,10 @@ impl<'a> PeepholeFoldConstants {
}
/// <https://tc39.es/ecma262/#sec-abstract-equality-comparison>
fn try_abstract_equality_comparison<'b>(
fn try_abstract_equality_comparison(
left_expr: &'b Expression<'a>,
right_expr: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Tri {
let left = ValueType::from(left_expr);
let right = ValueType::from(right_expr);
@ -737,11 +737,11 @@ impl<'a> PeepholeFoldConstants {
}
/// <https://tc39.es/ecma262/#sec-abstract-relational-comparison>
fn try_abstract_relational_comparison<'b>(
fn try_abstract_relational_comparison(
left_expr: &'b Expression<'a>,
right_expr: &'b Expression<'a>,
will_negative: bool,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Tri {
let left = ValueType::from(left_expr);
let right = ValueType::from(right_expr);
@ -848,10 +848,10 @@ impl<'a> PeepholeFoldConstants {
/// <https://tc39.es/ecma262/#sec-strict-equality-comparison>
#[expect(clippy::float_cmp)]
fn try_strict_equality_comparison<'b>(
fn try_strict_equality_comparison(
left_expr: &'b Expression<'a>,
right_expr: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Tri {
let left = ValueType::from(left_expr);
let right = ValueType::from(right_expr);
@ -922,12 +922,12 @@ impl<'a> PeepholeFoldConstants {
/// ported from [closure-compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L1114-L1162)
#[allow(clippy::cast_possible_truncation)]
fn try_fold_shift<'b>(
fn try_fold_shift(
span: Span,
op: BinaryOperator,
left: &'b Expression<'a>,
right: &'b Expression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
let left_num = ctx.get_side_free_number_value(left);
let right_num = ctx.get_side_free_number_value(right);

View file

@ -1,10 +1,11 @@
use oxc_allocator::Vec;
use oxc_ast::{ast::*, Visit};
use oxc_ecmascript::ConstantEvaluation;
use oxc_span::SPAN;
use oxc_traverse::{Ancestor, Traverse, TraverseCtx};
use crate::node_util::IsLiteralValue;
use crate::{keep_var::KeepVar, node_util::NodeUtil, tri::Tri, CompressorPass};
use crate::node_util::{Ctx, IsLiteralValue};
use crate::{keep_var::KeepVar, CompressorPass};
/// Remove Dead Code from the AST.
///
@ -29,6 +30,7 @@ impl<'a> CompressorPass<'a> for PeepholeRemoveDeadCode {
impl<'a> Traverse<'a> for PeepholeRemoveDeadCode {
fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx);
if let Some(new_stmt) = match stmt {
Statement::IfStatement(if_stmt) => self.try_fold_if(if_stmt, ctx),
Statement::ForStatement(for_stmt) => self.try_fold_for(for_stmt, ctx),
@ -43,17 +45,18 @@ impl<'a> Traverse<'a> for PeepholeRemoveDeadCode {
}
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
self.compress_block(stmt, ctx);
self.compress_block(stmt, Ctx(ctx));
}
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if stmts.iter().any(|stmt| matches!(stmt, Statement::EmptyStatement(_))) {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
}
self.dead_code_elimination(stmts, ctx);
self.dead_code_elimination(stmts, Ctx(ctx));
}
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx);
if let Some(folded_expr) = match expr {
Expression::ConditionalExpression(e) => Self::try_fold_conditional_expression(e, ctx),
_ => None,
@ -64,17 +67,13 @@ impl<'a> Traverse<'a> for PeepholeRemoveDeadCode {
}
}
impl<'a> PeepholeRemoveDeadCode {
impl<'a, 'b> PeepholeRemoveDeadCode {
pub fn new() -> Self {
Self { changed: false }
}
/// 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: &mut TraverseCtx<'a>,
) {
fn dead_code_elimination(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: Ctx<'a, 'b>) {
// Remove code after `return` and `throw` statements
let mut index = None;
'outer: for (i, stmt) in stmts.iter().enumerate() {
@ -134,7 +133,7 @@ impl<'a> PeepholeRemoveDeadCode {
/// Remove block from single line blocks
/// `{ block } -> block`
fn compress_block(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
fn compress_block(&mut self, stmt: &mut Statement<'a>, ctx: Ctx<'a, 'b>) {
if let Statement::BlockStatement(block) = stmt {
// Avoid compressing `if (x) { var x = 1 }` to `if (x) var x = 1` due to different
// semantics according to AnnexB, which lead to different semantics.
@ -156,7 +155,7 @@ impl<'a> PeepholeRemoveDeadCode {
fn try_fold_if(
&mut self,
if_stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Statement<'a>> {
// Descend and remove `else` blocks first.
if let Some(Statement::IfStatement(alternate)) = &mut if_stmt.alternate {
@ -171,11 +170,11 @@ impl<'a> PeepholeRemoveDeadCode {
}
match ctx.get_boolean_value(&if_stmt.test) {
Tri::True => {
Some(true) => {
// self.changed = true;
Some(ctx.ast.move_statement(&mut if_stmt.consequent))
}
Tri::False => {
Some(false) => {
Some(if let Some(alternate) = &mut if_stmt.alternate {
ctx.ast.move_statement(alternate)
} else {
@ -188,19 +187,18 @@ impl<'a> PeepholeRemoveDeadCode {
})
// self.changed = true;
}
Tri::Unknown => None,
None => None,
}
}
fn try_fold_for(
&mut self,
for_stmt: &mut ForStatement<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Statement<'a>> {
let test_boolean =
for_stmt.test.as_ref().map_or(Tri::Unknown, |test| ctx.get_boolean_value(test));
let test_boolean = for_stmt.test.as_ref().and_then(|test| ctx.get_boolean_value(test));
match test_boolean {
Tri::False => {
Some(false) => {
// Remove the entire `for` statement.
// Check vars in statement
let mut keep_var = KeepVar::new(ctx.ast);
@ -211,19 +209,19 @@ impl<'a> PeepholeRemoveDeadCode {
.unwrap_or_else(|| ctx.ast.statement_empty(SPAN)),
)
}
Tri::True => {
Some(true) => {
// Remove the test expression.
for_stmt.test = None;
self.changed = true;
None
}
Tri::Unknown => None,
None => None,
}
}
fn try_fold_expression_stmt(
stmt: &mut ExpressionStatement<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Statement<'a>> {
// We need to check if it is in arrow function with `expression: true`.
// This is the only scenario where we can't remove it even if `ExpressionStatement`.
@ -245,10 +243,10 @@ impl<'a> PeepholeRemoveDeadCode {
/// Try folding conditional expression (?:) if the condition results of the condition is known.
fn try_fold_conditional_expression(
expr: &mut ConditionalExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
match ctx.get_boolean_value(&expr.test) {
Tri::True => {
match ctx.eval_to_boolean(&expr.test) {
Some(true) => {
// Bail `let o = { f() { assert.ok(this !== o); } }; (true ? o.f : false)(); (true ? o.f : false)``;`
let parent = ctx.ancestry.parent();
if parent.is_tagged_template_expression()
@ -258,8 +256,8 @@ impl<'a> PeepholeRemoveDeadCode {
}
Some(ctx.ast.move_expression(&mut expr.consequent))
}
Tri::False => Some(ctx.ast.move_expression(&mut expr.alternate)),
Tri::Unknown => None,
Some(false) => Some(ctx.ast.move_expression(&mut expr.alternate)),
None => None,
}
}
}

View file

@ -9,7 +9,7 @@ use oxc_syntax::{
};
use oxc_traverse::{Ancestor, Traverse, TraverseCtx};
use crate::{node_util::NodeUtil, CompressOptions, CompressorPass};
use crate::{node_util::Ctx, CompressOptions, CompressorPass};
/// A peephole optimization that minimizes code by simplifying conditional
/// expressions, replacing IFs with HOOKs, replacing object constructors
@ -48,7 +48,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
ctx: &mut TraverseCtx<'a>,
) {
for declarator in decl.declarations.iter_mut() {
self.compress_variable_declarator(declarator, ctx);
self.compress_variable_declarator(declarator, Ctx(ctx));
}
}
@ -76,6 +76,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
}
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx);
if let Expression::AssignmentExpression(assignment_expr) = expr {
if let Some(new_expr) = Self::try_compress_assignment_expression(assignment_expr, ctx) {
*expr = new_expr;
@ -88,6 +89,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
}
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx);
match expr {
Expression::NewExpression(new_expr) => {
if let Some(new_expr) = Self::try_fold_new_expression(new_expr, ctx) {
@ -128,11 +130,11 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
expr: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.compress_typeof_undefined(expr, ctx);
self.compress_typeof_undefined(expr, Ctx(ctx));
}
}
impl<'a> PeepholeSubstituteAlternateSyntax {
impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
pub fn new(options: CompressOptions) -> Self {
Self { options, in_define_export: false, changed: false }
}
@ -140,7 +142,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/* Utilities */
/// Transforms `undefined` => `void 0`
fn compress_undefined(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
fn compress_undefined(expr: &mut Expression<'a>, ctx: Ctx<'a, 'b>) -> bool {
if ctx.is_expression_undefined(expr) {
*expr = ctx.ast.void_0(expr.span());
return true;
@ -185,7 +187,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/// Transforms boolean expression `true` => `!0` `false` => `!1`.
/// Enabled by `compress.booleans`.
/// Do not compress `true` in `Object.defineProperty(exports, 'Foo', {enumerable: true, ...})`.
fn compress_boolean(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
fn compress_boolean(&mut self, expr: &mut Expression<'a>, ctx: Ctx<'a, 'b>) -> bool {
let Expression::BooleanLiteral(lit) = expr else { return false };
if self.options.booleans && !self.in_define_export {
let parent = ctx.ancestry.parent();
@ -223,11 +225,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/// Compress `typeof foo == "undefined"` into `typeof foo > "u"`
/// Enabled by `compress.typeofs`
fn compress_typeof_undefined(
&self,
expr: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
fn compress_typeof_undefined(&self, expr: &mut BinaryExpression<'a>, ctx: Ctx<'a, 'b>) {
if !self.options.typeofs {
return;
}
@ -298,7 +296,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn compress_variable_declarator(
&mut self,
decl: &mut VariableDeclarator<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) {
if decl.kind.is_const() {
return;
@ -311,7 +309,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn try_compress_assignment_expression(
expr: &mut AssignmentExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
let target = expr.left.as_simple_assignment_target_mut()?;
if matches!(expr.operator, AssignmentOperator::Subtraction) {
@ -354,7 +352,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn try_fold_new_expression(
new_expr: &mut NewExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
// `new Object` -> `{}`
if new_expr.arguments.is_empty()
@ -413,7 +411,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn try_fold_literal_constructor_call_expression(
call_expr: &mut CallExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
// `Object()` -> `{}`
if call_expr.arguments.is_empty()
@ -467,7 +465,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn try_fold_simple_function_call(
call_expr: &mut CallExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if call_expr.optional || call_expr.arguments.len() != 1 {
return None;
@ -521,7 +519,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
fn try_fold_chain_call_expression(
&mut self,
call_expr: &mut CallExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) {
// `window.Object?.()` -> `Object?.()`
if call_expr.arguments.is_empty() && Self::is_window_object(&call_expr.callee) {
@ -534,7 +532,7 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/// returns an `Array()` constructor call with zero, one, or more arguments, copying from the input
fn array_constructor_call(
arguments: Vec<'a, Argument<'a>>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Expression<'a> {
let callee = ctx.ast.expression_identifier_reference(SPAN, "Array");
ctx.ast.expression_call(SPAN, callee, NONE, arguments, false)
@ -543,13 +541,13 @@ impl<'a> PeepholeSubstituteAlternateSyntax {
/// returns an array literal `[]` of zero, one, or more elements, copying from the input
fn array_literal(
elements: Vec<'a, ArrayExpressionElement<'a>>,
ctx: &mut TraverseCtx<'a>,
ctx: Ctx<'a, 'b>,
) -> Expression<'a> {
ctx.ast.expression_array(SPAN, elements, None)
}
/// returns a new empty array literal expression: `[]`
fn empty_array_literal(ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
fn empty_array_literal(ctx: Ctx<'a, 'b>) -> Expression<'a> {
Self::array_literal(ctx.ast.vec(), ctx)
}
}

View file

@ -3,47 +3,61 @@ mod is_literal_value;
mod may_have_side_effects;
use std::borrow::Cow;
use std::ops::Deref;
use num_bigint::BigInt;
use oxc_ast::ast::*;
use oxc_ecmascript::{StringToBigInt, ToBigInt, ToBoolean, ToJsString, ToNumber};
use oxc_semantic::{IsGlobalReference, ScopeTree, SymbolTable};
use oxc_ecmascript::ConstantEvaluation;
use oxc_ecmascript::{StringToBigInt, ToBigInt, ToJsString};
use oxc_semantic::{IsGlobalReference, SymbolTable};
use oxc_traverse::TraverseCtx;
pub use self::{is_literal_value::IsLiteralValue, may_have_side_effects::MayHaveSideEffects};
use crate::tri::Tri;
#[derive(Clone, Copy)]
pub struct Ctx<'a, 'b>(pub &'b TraverseCtx<'a>);
impl<'a, 'b> Deref for Ctx<'a, 'b> {
type Target = &'b TraverseCtx<'a>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a, 'b> ConstantEvaluation<'a> for Ctx<'a, 'b> {
fn is_global_reference(&self, ident: &oxc_ast::ast::IdentifierReference<'a>) -> bool {
ident.is_global_reference(self.0.symbols())
}
}
pub fn is_exact_int64(num: f64) -> bool {
num.fract() == 0.0
}
pub trait NodeUtil<'a> {
fn symbols(&self) -> &SymbolTable;
#[allow(unused)]
fn scopes(&self) -> &ScopeTree;
fn is_expression_undefined(&self, expr: &Expression) -> bool {
match expr {
Expression::Identifier(ident) if self.is_identifier_undefined(ident) => true,
Expression::UnaryExpression(e) if e.operator.is_void() && e.argument.is_number() => {
true
}
_ => false,
}
impl<'a, 'b> Ctx<'a, 'b> {
fn symbols(&self) -> &SymbolTable {
self.0.symbols()
}
fn is_identifier_undefined(&self, ident: &IdentifierReference) -> bool {
if ident.name == "undefined" && ident.is_global_reference(self.symbols()) {
return true;
}
false
/// Gets the boolean value of a node that represents an expression, or `None` if no
/// such value can be determined by static analysis.
/// This method does not consider whether the node may have side-effects.
/// <https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L109>
pub fn get_boolean_value(self, expr: &Expression<'a>) -> Option<bool> {
self.eval_to_boolean(expr)
}
/// Gets the value of a node as a Number, or None if it cannot be converted.
/// This method does not consider whether `expr` may have side effects.
/// <https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L348>
pub fn get_number_value(self, expr: &Expression<'a>) -> Option<f64> {
self.eval_to_number(expr)
}
/// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L104-L114)
/// Returns the number value of the node if it has one and it cannot have side effects.
fn get_side_free_number_value(&self, expr: &Expression<'a>) -> Option<f64> {
let value = self.get_number_value(expr);
pub fn get_side_free_number_value(self, expr: &Expression<'a>) -> Option<f64> {
let value = self.eval_to_number(expr);
// Calculating the number value, if any, is likely to be faster than calculating side effects,
// and there are only a very few cases where we can compute a number value, but there could
// also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior
@ -55,8 +69,25 @@ pub trait NodeUtil<'a> {
}
}
pub fn is_expression_undefined(self, expr: &Expression) -> bool {
match expr {
Expression::Identifier(ident) if self.is_identifier_undefined(ident) => true,
Expression::UnaryExpression(e) if e.operator.is_void() && e.argument.is_number() => {
true
}
_ => false,
}
}
pub fn is_identifier_undefined(self, ident: &IdentifierReference) -> bool {
if ident.name == "undefined" && ident.is_global_reference(self.symbols()) {
return true;
}
false
}
/// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L121)
fn get_side_free_bigint_value(&self, expr: &Expression<'a>) -> Option<BigInt> {
pub fn get_side_free_bigint_value(self, expr: &Expression<'a>) -> Option<BigInt> {
let value = self.get_bigint_value(expr);
// Calculating the bigint value, if any, is likely to be faster than calculating side effects,
// and there are only a very few cases where we can compute a bigint value, but there could
@ -73,7 +104,7 @@ pub trait NodeUtil<'a> {
/// Gets the value of a node as a String, or `None` if it cannot be converted.
/// This method effectively emulates the `String()` JavaScript cast function when
/// possible and the node has no side effects. Otherwise, it returns `None`.
fn get_side_free_string_value(&self, expr: &'a Expression) -> Option<Cow<'a, str>> {
pub fn get_side_free_string_value(self, expr: &'a Expression) -> Option<Cow<'a, str>> {
let value = self.get_string_value(expr);
// Calculating the string value, if any, is likely to be faster than calculating side effects,
// and there are only a very few cases where we can compute a string value, but there could
@ -85,22 +116,8 @@ pub trait NodeUtil<'a> {
None
}
// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L109)
// Gets the boolean value of a node that represents an expression, or `None` if no
// such value can be determined by static analysis.
// This method does not consider whether the node may have side-effects.
fn get_boolean_value(&self, expr: &Expression<'a>) -> Tri {
Tri::from(expr.to_boolean())
}
/// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L348)
/// Gets the value of a node as a Number, or None if it cannot be converted.
/// This method does not consider whether `expr` may have side effects.
fn get_number_value(&self, expr: &Expression<'a>) -> Option<f64> {
expr.to_number()
}
fn get_bigint_value(&self, expr: &Expression<'a>) -> Option<BigInt> {
#[expect(clippy::unused_self)]
pub fn get_bigint_value(self, expr: &Expression<'a>) -> Option<BigInt> {
expr.to_big_int()
}
@ -108,12 +125,14 @@ pub trait NodeUtil<'a> {
/// Gets the value of a node as a String, or `None` if it cannot be converted. When it returns a
/// String, this method effectively emulates the `String()` JavaScript cast function.
/// This method does not consider whether `expr` may have side effects.
fn get_string_value(&self, expr: &Expression<'a>) -> Option<Cow<'a, str>> {
#[expect(clippy::unused_self)]
pub fn get_string_value(self, expr: &Expression<'a>) -> Option<Cow<'a, str>> {
expr.to_js_string()
}
/// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/NodeUtil.java#L540)
fn get_string_bigint_value(&self, raw_string: &str) -> Option<BigInt> {
#[expect(clippy::unused_self)]
pub fn get_string_bigint_value(self, raw_string: &str) -> Option<BigInt> {
raw_string.string_to_big_int()
}
}

View file

@ -45,14 +45,6 @@ impl Tri {
Self::from(-self.value() * other.value())
}
pub fn to_option(self) -> Option<bool> {
match self {
Self::True => Some(true),
Self::False => Some(false),
Self::Unknown => None,
}
}
pub fn value(self) -> i8 {
match self {
Self::True => 1,

View file

@ -12,9 +12,9 @@ fn test(source_text: &str, expected: &str) {
crate::test(&source_text, expected, options);
}
// fn test_same(source_text: &str) {
// test(source_text, source_text);
// }
fn test_same(source_text: &str) {
test(source_text, source_text);
}
#[test]
fn dce_if_statement() {
@ -63,8 +63,7 @@ fn dce_if_statement() {
// Shadowed `undefined` as a variable should not be erased.
// This is a rollup test.
// FIXME:
// test_same("function foo(undefined) { if (!undefined) { } }");
test_same("function foo(undefined) { if (!undefined) { } }");
test("function foo() { if (undefined) { bar } }", "function foo() { }");
test("function foo() { { bar } }", "function foo() { bar }");