Skip to content

Commit

Permalink
feat(ecmascript): add ConstantEvaluation (#6549)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Oct 14, 2024
1 parent 67ad08a commit 3556062
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 254 deletions.
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

0 comments on commit 3556062

Please sign in to comment.