#[cfg(test)]
mod tests;
use crate::{
builtins::{
number::{f64_to_int32, f64_to_uint32},
Number,
},
object::{JsObject, Object, ObjectData},
property::{PropertyDescriptor, PropertyKey},
symbol::{JsSymbol, WellKnownSymbols},
BoaProfiler, Context, JsBigInt, JsResult, JsString,
};
use gc::{Finalize, Trace};
use std::{
collections::HashSet,
convert::TryFrom,
fmt::{self, Display},
str::FromStr,
};
mod conversions;
pub(crate) mod display;
mod equality;
mod hash;
mod operations;
mod r#type;
pub use conversions::*;
pub use display::ValueDisplay;
pub use equality::*;
pub use hash::*;
pub use operations::*;
pub use r#type::Type;
#[derive(Trace, Finalize, Debug, Clone)]
pub enum JsValue {
Null,
Undefined,
Boolean(bool),
String(JsString),
Rational(f64),
Integer(i32),
BigInt(JsBigInt),
Object(JsObject),
Symbol(JsSymbol),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegerOrInfinity {
Integer(i64),
PositiveInfinity,
NegativeInfinity,
}
impl JsValue {
#[inline]
pub fn new<T>(value: T) -> Self
where
T: Into<Self>,
{
value.into()
}
#[inline]
pub fn undefined() -> Self {
Self::Undefined
}
#[inline]
pub fn null() -> Self {
Self::Null
}
#[inline]
pub fn nan() -> Self {
Self::Rational(f64::NAN)
}
#[inline]
pub fn positive_inifnity() -> Self {
Self::Rational(f64::INFINITY)
}
#[inline]
pub fn negative_inifnity() -> Self {
Self::Rational(f64::NEG_INFINITY)
}
pub(crate) fn new_object(context: &Context) -> Self {
let _timer = BoaProfiler::global().start_event("new_object", "value");
context.construct_object().into()
}
#[inline]
pub fn is_object(&self) -> bool {
matches!(self, Self::Object(_))
}
#[inline]
pub fn as_object(&self) -> Option<JsObject> {
match *self {
Self::Object(ref o) => Some(o.clone()),
_ => None,
}
}
#[inline]
pub fn is_symbol(&self) -> bool {
matches!(self, Self::Symbol(_))
}
pub fn as_symbol(&self) -> Option<JsSymbol> {
match self {
Self::Symbol(symbol) => Some(symbol.clone()),
_ => None,
}
}
#[inline]
pub fn is_function(&self) -> bool {
matches!(self, Self::Object(o) if o.is_function())
}
#[inline]
pub fn is_undefined(&self) -> bool {
matches!(self, Self::Undefined)
}
#[inline]
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[inline]
pub fn is_null_or_undefined(&self) -> bool {
matches!(self, Self::Null | Self::Undefined)
}
#[inline]
pub fn is_double(&self) -> bool {
matches!(self, Self::Rational(_))
}
#[inline]
#[allow(clippy::float_cmp)]
pub fn is_integer(&self) -> bool {
let is_racional_intiger = |n: f64| n == ((n as i32) as f64);
match *self {
Self::Integer(_) => true,
Self::Rational(n) if is_racional_intiger(n) => true,
_ => false,
}
}
#[inline]
pub fn is_number(&self) -> bool {
matches!(self, Self::Rational(_) | Self::Integer(_))
}
#[inline]
pub fn as_number(&self) -> Option<f64> {
match *self {
Self::Integer(integer) => Some(integer.into()),
Self::Rational(rational) => Some(rational),
_ => None,
}
}
#[inline]
pub fn is_string(&self) -> bool {
matches!(self, Self::String(_))
}
#[inline]
pub fn as_string(&self) -> Option<&JsString> {
match self {
Self::String(ref string) => Some(string),
_ => None,
}
}
#[inline]
pub fn is_boolean(&self) -> bool {
matches!(self, Self::Boolean(_))
}
#[inline]
pub fn as_boolean(&self) -> Option<bool> {
match self {
Self::Boolean(boolean) => Some(*boolean),
_ => None,
}
}
#[inline]
pub fn is_bigint(&self) -> bool {
matches!(self, Self::BigInt(_))
}
#[inline]
pub fn as_bigint(&self) -> Option<&JsBigInt> {
match self {
Self::BigInt(bigint) => Some(bigint),
_ => None,
}
}
pub fn to_boolean(&self) -> bool {
match *self {
Self::Undefined | Self::Null => false,
Self::Symbol(_) | Self::Object(_) => true,
Self::String(ref s) if !s.is_empty() => true,
Self::Rational(n) if n != 0.0 && !n.is_nan() => true,
Self::Integer(n) if n != 0 => true,
Self::BigInt(ref n) if !n.is_zero() => true,
Self::Boolean(v) => v,
_ => false,
}
}
pub(crate) fn get_property<Key>(&self, key: Key) -> Option<PropertyDescriptor>
where
Key: Into<PropertyKey>,
{
let key = key.into();
let _timer = BoaProfiler::global().start_event("Value::get_property", "value");
match self {
Self::Object(ref object) => {
let property = object.borrow().properties().get(&key).cloned();
if property.is_some() {
return property;
}
object.borrow().prototype_instance().get_property(key)
}
_ => None,
}
}
pub(crate) fn get_field<K>(&self, key: K, context: &mut Context) -> JsResult<Self>
where
K: Into<PropertyKey>,
{
let _timer = BoaProfiler::global().start_event("Value::get_field", "value");
if let Self::Object(ref obj) = *self {
obj.clone()
.__get__(&key.into(), obj.clone().into(), context)
} else {
Ok(JsValue::undefined())
}
}
#[inline]
pub(crate) fn set_field<K, V>(
&self,
key: K,
value: V,
throw: bool,
context: &mut Context,
) -> JsResult<JsValue>
where
K: Into<PropertyKey>,
V: Into<JsValue>,
{
let key = key.into();
let value = value.into();
let _timer = BoaProfiler::global().start_event("Value::set_field", "value");
if let Self::Object(ref obj) = *self {
let success = obj
.clone()
.__set__(key, value.clone(), obj.clone().into(), context)?;
if !success && throw {
return Err(context.construct_type_error("Cannot assign value to property"));
} else {
return Ok(value);
}
}
Ok(value)
}
#[inline]
pub fn set_data(&self, data: ObjectData) {
if let Self::Object(ref obj) = *self {
obj.borrow_mut().data = data;
}
}
#[inline]
pub(crate) fn set_property<K, P>(&self, key: K, property: P)
where
K: Into<PropertyKey>,
P: Into<PropertyDescriptor>,
{
if let Some(object) = self.as_object() {
object.insert(key.into(), property.into());
}
}
pub fn to_primitive(
&self,
context: &mut Context,
preferred_type: PreferredType,
) -> JsResult<JsValue> {
if let JsValue::Object(obj) = self {
if let Some(exotic_to_prim) =
obj.get_method(context, WellKnownSymbols::to_primitive())?
{
let hint = match preferred_type {
PreferredType::String => "string",
PreferredType::Number => "number",
PreferredType::Default => "default",
}
.into();
let result = exotic_to_prim.call(self, &[hint], context)?;
return if result.is_object() {
Err(context.construct_type_error("Symbol.toPrimitive cannot return an object"))
} else {
Ok(result)
};
}
let mut hint = preferred_type;
if hint == PreferredType::Default {
hint = PreferredType::Number;
};
obj.ordinary_to_primitive(context, hint)
} else {
Ok(self.clone())
}
}
pub fn to_bigint(&self, context: &mut Context) -> JsResult<JsBigInt> {
match self {
JsValue::Null => Err(context.construct_type_error("cannot convert null to a BigInt")),
JsValue::Undefined => {
Err(context.construct_type_error("cannot convert undefined to a BigInt"))
}
JsValue::String(ref string) => {
if let Some(value) = JsBigInt::from_string(string) {
Ok(value)
} else {
Err(context.construct_syntax_error(format!(
"cannot convert string '{}' to bigint primitive",
string
)))
}
}
JsValue::Boolean(true) => Ok(JsBigInt::one()),
JsValue::Boolean(false) => Ok(JsBigInt::zero()),
JsValue::Integer(num) => Ok(JsBigInt::new(*num)),
JsValue::Rational(num) => {
if let Ok(bigint) = JsBigInt::try_from(*num) {
return Ok(bigint);
}
Err(context.construct_type_error(format!(
"The number {} cannot be converted to a BigInt because it is not an integer",
num
)))
}
JsValue::BigInt(b) => Ok(b.clone()),
JsValue::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::Number)?;
primitive.to_bigint(context)
}
JsValue::Symbol(_) => {
Err(context.construct_type_error("cannot convert Symbol to a BigInt"))
}
}
}
#[inline]
pub fn display(&self) -> ValueDisplay<'_> {
ValueDisplay { value: self }
}
pub fn to_string(&self, context: &mut Context) -> JsResult<JsString> {
match self {
JsValue::Null => Ok("null".into()),
JsValue::Undefined => Ok("undefined".into()),
JsValue::Boolean(boolean) => Ok(boolean.to_string().into()),
JsValue::Rational(rational) => Ok(Number::to_native_string(*rational).into()),
JsValue::Integer(integer) => Ok(integer.to_string().into()),
JsValue::String(string) => Ok(string.clone()),
JsValue::Symbol(_) => {
Err(context.construct_type_error("can't convert symbol to string"))
}
JsValue::BigInt(ref bigint) => Ok(bigint.to_string().into()),
JsValue::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::String)?;
primitive.to_string(context)
}
}
}
pub fn to_object(&self, context: &mut Context) -> JsResult<JsObject> {
match self {
JsValue::Undefined | JsValue::Null => {
Err(context.construct_type_error("cannot convert 'null' or 'undefined' to object"))
}
JsValue::Boolean(boolean) => {
let prototype = context.standard_objects().boolean_object().prototype();
Ok(JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::boolean(*boolean),
)))
}
JsValue::Integer(integer) => {
let prototype = context.standard_objects().number_object().prototype();
Ok(JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::number(f64::from(*integer)),
)))
}
JsValue::Rational(rational) => {
let prototype = context.standard_objects().number_object().prototype();
Ok(JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::number(*rational),
)))
}
JsValue::String(ref string) => {
let prototype = context.standard_objects().string_object().prototype();
let object = JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::string(string.clone()),
));
object.insert_property(
"length",
PropertyDescriptor::builder()
.value(string.encode_utf16().count())
.writable(false)
.enumerable(false)
.configurable(false),
);
Ok(object)
}
JsValue::Symbol(ref symbol) => {
let prototype = context.standard_objects().symbol_object().prototype();
Ok(JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::symbol(symbol.clone()),
)))
}
JsValue::BigInt(ref bigint) => {
let prototype = context.standard_objects().bigint_object().prototype();
Ok(JsObject::new(Object::with_prototype(
prototype.into(),
ObjectData::big_int(bigint.clone()),
)))
}
JsValue::Object(jsobject) => Ok(jsobject.clone()),
}
}
pub fn to_property_key(&self, context: &mut Context) -> JsResult<PropertyKey> {
Ok(match self {
JsValue::String(string) => string.clone().into(),
JsValue::Symbol(symbol) => symbol.clone().into(),
_ => match self.to_primitive(context, PreferredType::String)? {
JsValue::String(ref string) => string.clone().into(),
JsValue::Symbol(ref symbol) => symbol.clone().into(),
primitive => primitive.to_string(context)?.into(),
},
})
}
pub fn to_numeric(&self, context: &mut Context) -> JsResult<Numeric> {
let primitive = self.to_primitive(context, PreferredType::Number)?;
if let Some(bigint) = primitive.as_bigint() {
return Ok(bigint.clone().into());
}
Ok(self.to_number(context)?.into())
}
pub fn to_u32(&self, context: &mut Context) -> JsResult<u32> {
if let JsValue::Integer(number) = *self {
return Ok(number as u32);
}
let number = self.to_number(context)?;
Ok(f64_to_uint32(number))
}
pub fn to_i32(&self, context: &mut Context) -> JsResult<i32> {
if let JsValue::Integer(number) = *self {
return Ok(number);
}
let number = self.to_number(context)?;
Ok(f64_to_int32(number))
}
pub fn to_index(&self, context: &mut Context) -> JsResult<usize> {
if self.is_undefined() {
return Ok(0);
}
let integer_index = self.to_integer(context)?;
if integer_index < 0.0 {
return Err(context.construct_range_error("Integer index must be >= 0"));
}
if integer_index > Number::MAX_SAFE_INTEGER {
return Err(
context.construct_range_error("Integer index must be less than 2**(53) - 1")
);
}
Ok(integer_index as usize)
}
pub fn to_length(&self, context: &mut Context) -> JsResult<usize> {
let len = self.to_integer(context)?;
if len < 0.0 {
return Ok(0);
}
Ok(len.min(Number::MAX_SAFE_INTEGER) as usize)
}
pub fn to_integer(&self, context: &mut Context) -> JsResult<f64> {
let number = self.to_number(context)?;
if !number.is_finite() {
if number.is_nan() {
return Ok(0.0);
}
return Ok(number);
}
Ok(number.trunc() + 0.0) }
pub fn to_number(&self, context: &mut Context) -> JsResult<f64> {
match *self {
JsValue::Null => Ok(0.0),
JsValue::Undefined => Ok(f64::NAN),
JsValue::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
JsValue::String(ref string) => Ok(string.string_to_number()),
JsValue::Rational(number) => Ok(number),
JsValue::Integer(integer) => Ok(f64::from(integer)),
JsValue::Symbol(_) => {
Err(context.construct_type_error("argument must not be a symbol"))
}
JsValue::BigInt(_) => {
Err(context.construct_type_error("argument must not be a bigint"))
}
JsValue::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::Number)?;
primitive.to_number(context)
}
}
}
pub fn to_numeric_number(&self, context: &mut Context) -> JsResult<f64> {
let primitive = self.to_primitive(context, PreferredType::Number)?;
if let Some(bigint) = primitive.as_bigint() {
return Ok(bigint.to_f64());
}
primitive.to_number(context)
}
#[inline]
pub fn require_object_coercible(&self, context: &mut Context) -> JsResult<&JsValue> {
if self.is_null_or_undefined() {
Err(context.construct_type_error("cannot convert null or undefined to Object"))
} else {
Ok(self)
}
}
#[inline]
pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult<PropertyDescriptor> {
match self {
JsValue::Object(ref obj) => obj.to_property_descriptor(context),
_ => Err(context
.construct_type_error("Cannot construct a property descriptor from a non-object")),
}
}
pub fn to_integer_or_infinity(&self, context: &mut Context) -> JsResult<IntegerOrInfinity> {
let number = self.to_number(context)?;
if number.is_nan() || number == 0.0 || number == -0.0 {
Ok(IntegerOrInfinity::Integer(0))
} else if number.is_infinite() && number.is_sign_positive() {
Ok(IntegerOrInfinity::PositiveInfinity)
} else if number.is_infinite() && number.is_sign_negative() {
Ok(IntegerOrInfinity::NegativeInfinity)
} else {
let integer = number.abs().floor();
let integer = integer.min(Number::MAX_SAFE_INTEGER) as i64;
if number < 0.0 {
Ok(IntegerOrInfinity::Integer(-integer))
} else {
Ok(IntegerOrInfinity::Integer(integer))
}
}
}
pub fn type_of(&self) -> JsString {
match *self {
Self::Rational(_) | Self::Integer(_) => "number",
Self::String(_) => "string",
Self::Boolean(_) => "boolean",
Self::Symbol(_) => "symbol",
Self::Null => "object",
Self::Undefined => "undefined",
Self::BigInt(_) => "bigint",
Self::Object(ref object) => {
if object.is_callable() {
"function"
} else {
"object"
}
}
}
.into()
}
pub(crate) fn is_array(&self, _context: &mut Context) -> JsResult<bool> {
if let Some(object) = self.as_object() {
Ok(object.is_array())
} else {
Ok(false)
}
}
}
impl Default for JsValue {
fn default() -> Self {
Self::Undefined
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PreferredType {
String,
Number,
Default,
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Numeric {
Number(f64),
BigInt(JsBigInt),
}
impl From<f64> for Numeric {
#[inline]
fn from(value: f64) -> Self {
Self::Number(value)
}
}
impl From<i32> for Numeric {
#[inline]
fn from(value: i32) -> Self {
Self::Number(value.into())
}
}
impl From<i16> for Numeric {
#[inline]
fn from(value: i16) -> Self {
Self::Number(value.into())
}
}
impl From<i8> for Numeric {
#[inline]
fn from(value: i8) -> Self {
Self::Number(value.into())
}
}
impl From<u32> for Numeric {
#[inline]
fn from(value: u32) -> Self {
Self::Number(value.into())
}
}
impl From<u16> for Numeric {
#[inline]
fn from(value: u16) -> Self {
Self::Number(value.into())
}
}
impl From<u8> for Numeric {
#[inline]
fn from(value: u8) -> Self {
Self::Number(value.into())
}
}
impl From<JsBigInt> for Numeric {
#[inline]
fn from(value: JsBigInt) -> Self {
Self::BigInt(value)
}
}
impl From<Numeric> for JsValue {
fn from(value: Numeric) -> Self {
match value {
Numeric::Number(number) => Self::new(number),
Numeric::BigInt(bigint) => Self::new(bigint),
}
}
}