From 35560622138eea8237108ea5a8d094ed63be0c30 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Mon, 14 Oct 2024 07:53:44 +0000 Subject: [PATCH] feat(ecmascript): add `ConstantEvaluation` (#6549) --- .../oxc_ecmascript/src/constant_evaluation.rs | 171 ++++++++++++++++++ crates/oxc_ecmascript/src/lib.rs | 21 ++- crates/oxc_ecmascript/src/to_boolean.rs | 82 +-------- crates/oxc_ecmascript/src/to_number.rs | 27 +-- crates/oxc_minifier/src/ast_passes/mod.rs | 13 -- .../src/ast_passes/peephole_fold_constants.rs | 70 +++---- .../ast_passes/peephole_remove_dead_code.rs | 52 +++--- .../peephole_substitute_alternate_syntax.rs | 38 ++-- crates/oxc_minifier/src/node_util/mod.rs | 105 ++++++----- crates/oxc_minifier/src/tri.rs | 8 - .../tests/ast_passes/dead_code_elimination.rs | 9 +- 11 files changed, 342 insertions(+), 254 deletions(-) create mode 100644 crates/oxc_ecmascript/src/constant_evaluation.rs diff --git a/crates/oxc_ecmascript/src/constant_evaluation.rs b/crates/oxc_ecmascript/src/constant_evaluation.rs new file mode 100644 index 0000000000000..c23dea898d2ed --- /dev/null +++ b/crates/oxc_ecmascript/src/constant_evaluation.rs @@ -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 { +// 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 { + 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 { + 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 { + 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 { + 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 { + 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, + } + } +} diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs index 2adb30453488b..9985429bc5e91 100644 --- a/crates/oxc_ecmascript/src/lib.rs +++ b/crates/oxc_ecmascript/src/lib.rs @@ -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, }; diff --git a/crates/oxc_ecmascript/src/to_boolean.rs b/crates/oxc_ecmascript/src/to_boolean.rs index e8910503b7e20..b210291f94285 100644 --- a/crates/oxc_ecmascript/src/to_boolean.rs +++ b/crates/oxc_ecmascript/src/to_boolean.rs @@ -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 { + // 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, } } diff --git a/crates/oxc_ecmascript/src/to_number.rs b/crates/oxc_ecmascript/src/to_number.rs index 0a405f76b1ce8..8aeacd2555643 100644 --- a/crates/oxc_ecmascript/src/to_number.rs +++ b/crates/oxc_ecmascript/src/to_number.rs @@ -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 { 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::().map_or(Some(f64::NAN), Some) } diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 20a66970bc6e1..8f413b9c0b646 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -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; diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 8a5285b4a321d..8f683528f99cc 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -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> { None } fn try_fold_ctor_cal( _new_expr: &mut NewExpression<'a>, - _ctx: &mut TraverseCtx<'a>, + _ctx: Ctx<'a, '_>, ) -> Option> { 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> { 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> { // None // } fn try_flatten_array_expression( _new_expr: &mut ArrayExpression<'a>, - _ctx: &mut TraverseCtx<'a>, + _ctx: Ctx<'a, 'b>, ) -> Option> { None } fn try_flatten_object_expression( _new_expr: &mut ObjectExpression<'a>, - _ctx: &mut TraverseCtx<'a>, + _ctx: Ctx<'a, 'b>, ) -> Option> { 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> { 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> { 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> { 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> { 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> { 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> { // 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> { // 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> { 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> { 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> { 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> { 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 { } /// - 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 { } /// - 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 { /// #[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> { let left_num = ctx.get_side_free_number_value(left); let right_num = ctx.get_side_free_number_value(right); diff --git a/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs index 0d0aa37acd267..6c81df5be80b7 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs @@ -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> { // 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> { - 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> { // 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> { - 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, } } } diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index 3fbba3e74a40d..f67905f033eb0 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -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> { 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> { // `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> { // `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> { 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) } } diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index ccb8e20025edb..8a51528153f84 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -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; +impl<'a, 'b> Ctx<'a, 'b> { + fn symbols(&self) -> &SymbolTable { + self.0.symbols() + } - 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, - } + /// 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. + /// + pub fn get_boolean_value(self, expr: &Expression<'a>) -> Option { + self.eval_to_boolean(expr) } - fn is_identifier_undefined(&self, ident: &IdentifierReference) -> bool { - if ident.name == "undefined" && ident.is_global_reference(self.symbols()) { - return true; - } - false + /// 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. + /// + pub fn get_number_value(self, expr: &Expression<'a>) -> Option { + 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 { - let value = self.get_number_value(expr); + pub fn get_side_free_number_value(self, expr: &Expression<'a>) -> Option { + 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 { + pub fn get_side_free_bigint_value(self, expr: &Expression<'a>) -> Option { 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> { + pub fn get_side_free_string_value(self, expr: &'a Expression) -> Option> { 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 { - expr.to_number() - } - - fn get_bigint_value(&self, expr: &Expression<'a>) -> Option { + #[expect(clippy::unused_self)] + pub fn get_bigint_value(self, expr: &Expression<'a>) -> Option { 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> { + #[expect(clippy::unused_self)] + pub fn get_string_value(self, expr: &Expression<'a>) -> Option> { 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 { + #[expect(clippy::unused_self)] + pub fn get_string_bigint_value(self, raw_string: &str) -> Option { raw_string.string_to_big_int() } } diff --git a/crates/oxc_minifier/src/tri.rs b/crates/oxc_minifier/src/tri.rs index f5be34ec1b884..f125c05d3f5ae 100644 --- a/crates/oxc_minifier/src/tri.rs +++ b/crates/oxc_minifier/src/tri.rs @@ -45,14 +45,6 @@ impl Tri { Self::from(-self.value() * other.value()) } - pub fn to_option(self) -> Option { - match self { - Self::True => Some(true), - Self::False => Some(false), - Self::Unknown => None, - } - } - pub fn value(self) -> i8 { match self { Self::True => 1, diff --git a/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs index ef2de06c51a27..6b5e777fc29d8 100644 --- a/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs +++ b/crates/oxc_minifier/tests/ast_passes/dead_code_elimination.rs @@ -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 }");