use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{ToPrimitive, Zero};
use std::{
collections::HashSet,
fmt::{self, Display},
ops::Sub,
sync::LazyLock,
};
use boa_gc::{Finalize, Trace};
#[doc(inline)]
pub use boa_macros::TryFromJs;
pub use boa_macros::TryIntoJs;
#[doc(inline)]
pub use conversions::convert::Convert;
#[doc(inline)]
pub use conversions::nullable::Nullable;
pub(crate) use self::conversions::IntoOrUndefined;
#[doc(inline)]
pub use self::{
conversions::try_from_js::TryFromJs, conversions::try_into_js::TryIntoJs,
display::ValueDisplay, integer::IntegerOrInfinity, operations::*, r#type::Type,
variant::JsVariant,
};
use crate::builtins::RegExp;
use crate::object::{JsFunction, JsPromise, JsRegExp};
use crate::{
Context, JsBigInt, JsResult, JsString,
builtins::{
Number, Promise,
number::{f64_to_int32, f64_to_uint32},
},
error::JsNativeError,
js_string,
object::JsObject,
property::{PropertyDescriptor, PropertyKey},
symbol::JsSymbol,
};
mod conversions;
pub(crate) mod display;
mod equality;
mod hash;
mod inner;
mod integer;
mod operations;
mod r#type;
mod variant;
#[cfg(test)]
mod tests;
static TWO_E_64: LazyLock<BigInt> = LazyLock::new(|| {
const TWO_E_64: u128 = 2u128.pow(64);
BigInt::from(TWO_E_64)
});
static TWO_E_63: LazyLock<BigInt> = LazyLock::new(|| {
const TWO_E_63: u128 = 2u128.pow(63);
BigInt::from(TWO_E_63)
});
pub use boa_macros::js_object;
pub use boa_macros::js_value;
#[derive(Finalize, Debug, Clone, Trace)]
pub struct JsValue(inner::InnerValue);
impl JsValue {
#[inline]
const fn from_inner(inner: inner::InnerValue) -> Self {
Self(inner)
}
#[inline]
#[must_use]
pub fn new<T>(value: T) -> Self
where
T: Into<Self>,
{
value.into()
}
#[inline]
#[must_use]
pub fn variant(&self) -> JsVariant {
self.0.as_variant()
}
#[inline]
#[must_use]
pub const fn undefined() -> Self {
Self::from_inner(inner::InnerValue::undefined())
}
#[inline]
#[must_use]
pub const fn null() -> Self {
Self::from_inner(inner::InnerValue::null())
}
#[inline]
#[must_use]
pub const fn nan() -> Self {
Self::from_inner(inner::InnerValue::float64(f64::NAN))
}
#[inline]
#[must_use]
pub const fn positive_infinity() -> Self {
Self::from_inner(inner::InnerValue::float64(f64::INFINITY))
}
#[inline]
#[must_use]
pub const fn negative_infinity() -> Self {
Self::from_inner(inner::InnerValue::float64(f64::NEG_INFINITY))
}
#[must_use]
pub fn rational(rational: f64) -> Self {
Self::from_inner(inner::InnerValue::float64(rational))
}
#[inline]
#[must_use]
pub fn is_object(&self) -> bool {
self.0.is_object()
}
#[inline]
#[must_use]
pub fn as_object(&self) -> Option<JsObject> {
self.0.as_object()
}
#[inline]
#[must_use]
pub fn into_object(self) -> Option<JsObject> {
self.0.as_object()
}
#[inline]
#[must_use]
pub fn is_callable(&self) -> bool {
self.as_object().as_ref().is_some_and(JsObject::is_callable)
}
#[inline]
#[must_use]
pub fn as_callable(&self) -> Option<JsObject> {
self.as_object().filter(JsObject::is_callable)
}
#[inline]
#[must_use]
pub fn as_function(&self) -> Option<JsFunction> {
self.as_callable().and_then(JsFunction::from_object)
}
#[inline]
#[must_use]
pub fn is_constructor(&self) -> bool {
self.as_object()
.as_ref()
.is_some_and(JsObject::is_constructor)
}
#[inline]
#[must_use]
pub fn as_constructor(&self) -> Option<JsObject> {
self.as_object().filter(JsObject::is_constructor)
}
#[inline]
#[must_use]
pub fn is_promise(&self) -> bool {
self.as_object().is_some_and(|obj| obj.is::<Promise>())
}
#[inline]
#[must_use]
pub(crate) fn as_promise_object(&self) -> Option<JsObject> {
self.as_object().filter(|obj| obj.is::<Promise>())
}
#[inline]
#[must_use]
pub fn as_promise(&self) -> Option<JsPromise> {
self.as_promise_object()
.and_then(|o| JsPromise::from_object(o).ok())
}
#[inline]
#[must_use]
pub fn is_regexp(&self) -> bool {
self.as_object().is_some_and(|obj| obj.is::<RegExp>())
}
#[inline]
#[must_use]
pub fn as_regexp(&self) -> Option<JsRegExp> {
self.as_object()
.filter(|obj| obj.is::<RegExp>())
.and_then(|o| JsRegExp::from_object(o).ok())
}
#[inline]
#[must_use]
pub fn is_symbol(&self) -> bool {
self.0.is_symbol()
}
#[inline]
#[must_use]
pub fn as_symbol(&self) -> Option<JsSymbol> {
self.0.as_symbol()
}
#[inline]
#[must_use]
pub fn is_undefined(&self) -> bool {
self.0.is_undefined()
}
#[inline]
#[must_use]
pub fn is_null(&self) -> bool {
self.0.is_null()
}
#[inline]
#[must_use]
pub fn is_null_or_undefined(&self) -> bool {
self.is_undefined() || self.is_null()
}
#[inline]
#[must_use]
#[allow(clippy::float_cmp)]
pub fn as_i32(&self) -> Option<i32> {
if let Some(integer) = self.0.as_integer32() {
Some(integer)
} else if let Some(rational) = self.0.as_float64() {
if rational == f64::from(rational as i32) {
Some(rational as i32)
} else {
None
}
} else {
None
}
}
#[inline]
#[must_use]
pub fn is_number(&self) -> bool {
self.0.is_integer32() || self.0.is_float64()
}
#[inline]
#[must_use]
pub fn as_number(&self) -> Option<f64> {
match self.variant() {
JsVariant::Integer32(i) => Some(f64::from(i)),
JsVariant::Float64(f) => Some(f),
_ => None,
}
}
#[inline]
#[must_use]
pub fn is_string(&self) -> bool {
self.0.is_string()
}
#[inline]
#[must_use]
pub fn as_string(&self) -> Option<JsString> {
self.0.as_string()
}
#[inline]
#[must_use]
pub fn is_boolean(&self) -> bool {
self.0.is_bool()
}
#[inline]
#[must_use]
pub fn as_boolean(&self) -> Option<bool> {
self.0.as_bool()
}
#[inline]
#[must_use]
pub fn is_bigint(&self) -> bool {
self.0.is_bigint()
}
#[inline]
#[must_use]
pub fn as_bigint(&self) -> Option<JsBigInt> {
self.0.as_bigint()
}
#[must_use]
pub fn to_boolean(&self) -> bool {
match self.variant() {
JsVariant::Symbol(_) | JsVariant::Object(_) => true,
JsVariant::String(s) if !s.is_empty() => true,
JsVariant::Float64(n) if n != 0.0 && !n.is_nan() => true,
JsVariant::Integer32(n) if n != 0 => true,
JsVariant::BigInt(n) if !n.is_zero() => true,
JsVariant::Boolean(v) => v,
_ => false,
}
}
pub fn to_primitive(
&self,
context: &mut Context,
preferred_type: PreferredType,
) -> JsResult<Self> {
if let Some(input) = self.as_object() {
let exotic_to_prim = input.get_method(JsSymbol::to_primitive(), context)?;
if let Some(exotic_to_prim) = exotic_to_prim {
let hint = match preferred_type {
PreferredType::Default => js_string!("default"),
PreferredType::String => js_string!("string"),
PreferredType::Number => js_string!("number"),
}
.into();
let result = exotic_to_prim.call(self, &[hint], context)?;
return if result.is_object() {
Err(JsNativeError::typ()
.with_message("Symbol.toPrimitive cannot return an object")
.into())
} else {
Ok(result)
};
}
let preferred_type = match preferred_type {
PreferredType::Default | PreferredType::Number => PreferredType::Number,
PreferredType::String => PreferredType::String,
};
return input.ordinary_to_primitive(context, preferred_type);
}
Ok(self.clone())
}
pub fn to_bigint(&self, context: &mut Context) -> JsResult<JsBigInt> {
match self.variant() {
JsVariant::Null => Err(JsNativeError::typ()
.with_message("cannot convert null to a BigInt")
.into()),
JsVariant::Undefined => Err(JsNativeError::typ()
.with_message("cannot convert undefined to a BigInt")
.into()),
JsVariant::String(string) => JsBigInt::from_js_string(&string).map_or_else(
|| {
Err(JsNativeError::syntax()
.with_message(format!(
"cannot convert string '{}' to bigint primitive",
string.to_std_string_escaped()
))
.into())
},
Ok,
),
JsVariant::Boolean(true) => Ok(JsBigInt::one()),
JsVariant::Boolean(false) => Ok(JsBigInt::zero()),
JsVariant::Integer32(_) | JsVariant::Float64(_) => Err(JsNativeError::typ()
.with_message("cannot convert Number to a BigInt")
.into()),
JsVariant::BigInt(b) => Ok(b.clone()),
JsVariant::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::Number)?;
primitive.to_bigint(context)
}
JsVariant::Symbol(_) => Err(JsNativeError::typ()
.with_message("cannot convert Symbol to a BigInt")
.into()),
}
}
#[must_use]
#[inline]
pub const fn display(&self) -> ValueDisplay<'_> {
ValueDisplay {
value: self,
internals: false,
}
}
pub fn to_string(&self, context: &mut Context) -> JsResult<JsString> {
match self.variant() {
JsVariant::Null => Ok(js_string!("null")),
JsVariant::Undefined => Ok(js_string!("undefined")),
JsVariant::Boolean(true) => Ok(js_string!("true")),
JsVariant::Boolean(false) => Ok(js_string!("false")),
JsVariant::Float64(rational) => Ok(JsString::from(rational)),
JsVariant::Integer32(integer) => Ok(JsString::from(integer)),
JsVariant::String(string) => Ok(string.clone()),
JsVariant::Symbol(_) => Err(JsNativeError::typ()
.with_message("can't convert symbol to string")
.into()),
JsVariant::BigInt(bigint) => Ok(bigint.to_string().into()),
JsVariant::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.variant() {
JsVariant::Undefined | JsVariant::Null => Err(JsNativeError::typ()
.with_message("cannot convert 'null' or 'undefined' to object")
.into()),
JsVariant::Boolean(boolean) => Ok(context
.intrinsics()
.templates()
.boolean()
.create(boolean, Vec::default())),
JsVariant::Integer32(integer) => Ok(context
.intrinsics()
.templates()
.number()
.create(f64::from(integer), Vec::default())),
JsVariant::Float64(rational) => Ok(context
.intrinsics()
.templates()
.number()
.create(rational, Vec::default())),
JsVariant::String(string) => Ok(context
.intrinsics()
.templates()
.string()
.create(string.clone(), vec![string.len().into()])),
JsVariant::Symbol(symbol) => Ok(context
.intrinsics()
.templates()
.symbol()
.create(symbol.clone(), Vec::default())),
JsVariant::BigInt(bigint) => Ok(context
.intrinsics()
.templates()
.bigint()
.create(bigint.clone(), Vec::default())),
JsVariant::Object(jsobject) => Ok(jsobject.clone()),
}
}
pub fn to_property_key(&self, context: &mut Context) -> JsResult<PropertyKey> {
Ok(match self.variant() {
JsVariant::String(string) => string.clone().into(),
JsVariant::Symbol(symbol) => symbol.clone().into(),
JsVariant::Integer32(integer) => integer.into(),
JsVariant::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::String)?;
match primitive.variant() {
JsVariant::String(string) => string.clone().into(),
JsVariant::Symbol(symbol) => symbol.clone().into(),
JsVariant::Integer32(integer) => integer.into(),
_ => primitive.to_string(context)?.into(),
}
}
_ => self.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(primitive.to_number(context)?.into())
}
pub fn to_u32(&self, context: &mut Context) -> JsResult<u32> {
if let Some(number) = self.0.as_integer32()
&& let Ok(number) = u32::try_from(number)
{
return Ok(number);
}
let number = self.to_number(context)?;
Ok(f64_to_uint32(number))
}
pub fn to_i32(&self, context: &mut Context) -> JsResult<i32> {
if let Some(number) = self.0.as_integer32() {
return Ok(number);
}
let number = self.to_number(context)?;
Ok(f64_to_int32(number))
}
pub fn to_int8(&self, context: &mut Context) -> JsResult<i8> {
let number = self.to_number(context)?;
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}
let int = number.abs().floor().copysign(number) as i64;
let int_8_bit = int % 2i64.pow(8);
if int_8_bit >= 2i64.pow(7) {
Ok((int_8_bit - 2i64.pow(8)) as i8)
} else {
Ok(int_8_bit as i8)
}
}
pub fn to_uint8(&self, context: &mut Context) -> JsResult<u8> {
let number = self.to_number(context)?;
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}
let int = number.abs().floor().copysign(number) as i64;
let int_8_bit = int % 2i64.pow(8);
Ok(int_8_bit as u8)
}
pub fn to_uint8_clamp(&self, context: &mut Context) -> JsResult<u8> {
let number = self.to_number(context)?;
if number.is_nan() {
return Ok(0);
}
if number <= 0.0 {
return Ok(0);
}
if number >= 255.0 {
return Ok(255);
}
let f = number.floor();
if f + 0.5 < number {
return Ok(f as u8 + 1);
}
if number < f + 0.5 {
return Ok(f as u8);
}
if !(f as u8).is_multiple_of(2) {
return Ok(f as u8 + 1);
}
Ok(f as u8)
}
pub fn to_int16(&self, context: &mut Context) -> JsResult<i16> {
let number = self.to_number(context)?;
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}
let int = number.abs().floor().copysign(number) as i64;
let int_16_bit = int % 2i64.pow(16);
if int_16_bit >= 2i64.pow(15) {
Ok((int_16_bit - 2i64.pow(16)) as i16)
} else {
Ok(int_16_bit as i16)
}
}
pub fn to_uint16(&self, context: &mut Context) -> JsResult<u16> {
let number = self.to_number(context)?;
if number.is_nan() || number.is_zero() || number.is_infinite() {
return Ok(0);
}
let int = number.abs().floor().copysign(number) as i64;
let int_16_bit = int % 2i64.pow(16);
Ok(int_16_bit as u16)
}
pub fn to_big_int64(&self, context: &mut Context) -> JsResult<i64> {
let n = self.to_bigint(context)?;
let int64_bit = n.as_inner().mod_floor(&TWO_E_64);
let value = if int64_bit >= *TWO_E_63 {
int64_bit.sub(&*TWO_E_64)
} else {
int64_bit
};
Ok(value
.to_i64()
.expect("should be within the range of `i64` by the mod operation"))
}
pub fn to_big_uint64(&self, context: &mut Context) -> JsResult<u64> {
let n = self.to_bigint(context)?;
Ok(n.as_inner()
.mod_floor(&TWO_E_64)
.to_u64()
.expect("should be within the range of `u64` by the mod operation"))
}
pub fn to_index(&self, context: &mut Context) -> JsResult<u64> {
if self.is_undefined() {
return Ok(0);
}
let integer = self.to_integer_or_infinity(context)?;
let clamped = integer.clamp_finite(0, Number::MAX_SAFE_INTEGER as i64);
if integer != clamped {
return Err(JsNativeError::range()
.with_message("Index must be between 0 and 2^53 - 1")
.into());
}
debug_assert!(0 <= clamped && clamped <= Number::MAX_SAFE_INTEGER as i64);
Ok(clamped as u64)
}
pub fn to_length(&self, context: &mut Context) -> JsResult<u64> {
Ok(self
.to_integer_or_infinity(context)?
.clamp_finite(0, Number::MAX_SAFE_INTEGER as i64) as u64)
}
pub fn to_integer_or_infinity(&self, context: &mut Context) -> JsResult<IntegerOrInfinity> {
let number = self.to_number(context)?;
Ok(IntegerOrInfinity::from(number))
}
pub fn to_number(&self, context: &mut Context) -> JsResult<f64> {
match self.variant() {
JsVariant::Null => Ok(0.0),
JsVariant::Undefined => Ok(f64::NAN),
JsVariant::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
JsVariant::String(string) => Ok(string.to_number()),
JsVariant::Float64(number) => Ok(number),
JsVariant::Integer32(integer) => Ok(f64::from(integer)),
JsVariant::Symbol(_) => Err(JsNativeError::typ()
.with_message("argument must not be a symbol")
.into()),
JsVariant::BigInt(_) => Err(JsNativeError::typ()
.with_message("argument must not be a bigint")
.into()),
JsVariant::Object(_) => {
let primitive = self.to_primitive(context, PreferredType::Number)?;
primitive.to_number(context)
}
}
}
#[cfg(feature = "float16")]
pub fn to_f16(&self, context: &mut Context) -> JsResult<float16::f16> {
self.to_number(context).map(float16::f16::from_f64)
}
pub fn to_f32(&self, context: &mut Context) -> JsResult<f32> {
self.to_number(context).map(|n| n as f32)
}
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) -> JsResult<&Self> {
if self.is_null_or_undefined() {
Err(JsNativeError::typ()
.with_message("cannot convert null or undefined to Object")
.into())
} else {
Ok(self)
}
}
#[inline]
pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult<PropertyDescriptor> {
self.as_object()
.ok_or_else(|| {
JsNativeError::typ()
.with_message("Cannot construct a property descriptor from a non-object")
.into()
})
.and_then(|obj| obj.to_property_descriptor(context))
}
#[must_use]
pub fn type_of(&self) -> &'static str {
self.variant().type_of()
}
#[must_use]
pub fn js_type_of(&self) -> JsString {
self.variant().js_type_of()
}
#[inline]
#[must_use]
pub fn map<T, F>(&self, f: F) -> Option<T>
where
F: FnOnce(&JsValue) -> T,
{
if self.is_undefined() {
return None;
}
Some(f(self))
}
#[inline]
#[must_use]
pub fn map_or<T, F>(&self, default: T, f: F) -> T
where
F: FnOnce(&JsValue) -> T,
{
if self.is_undefined() {
return default;
}
f(self)
}
pub(crate) fn is_array(&self) -> JsResult<bool> {
self.as_object()
.as_ref()
.map_or(Ok(false), JsObject::is_array_abstract)
}
}
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<f32> for Numeric {
#[inline]
fn from(value: f32) -> Self {
Self::Number(value.into())
}
}
impl From<i64> for Numeric {
#[inline]
fn from(value: i64) -> Self {
Self::BigInt(value.into())
}
}
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<u64> for Numeric {
#[inline]
fn from(value: u64) -> Self {
Self::BigInt(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),
}
}
}