Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ecmascript): add ConstantEvaluation #6549

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions crates/oxc_ecmascript/src/constant_evaluation.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
21 changes: 16 additions & 5 deletions crates/oxc_ecmascript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
82 changes: 10 additions & 72 deletions crates/oxc_ecmascript/src/to_boolean.rs
Original file line number Diff line number Diff line change
@@ -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`
///
Expand All @@ -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(_)
Expand All @@ -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,
}
}
Expand Down
27 changes: 1 addition & 26 deletions crates/oxc_ecmascript/src/to_number.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_syntax::operator::UnaryOperator;

use crate::ToBoolean;

/// `ToNumber`
///
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
13 changes: 0 additions & 13 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading