diff --git a/core/engine/src/value/conversions/convert.rs b/core/engine/src/value/conversions/convert.rs new file mode 100644 index 0000000000..397afa434f --- /dev/null +++ b/core/engine/src/value/conversions/convert.rs @@ -0,0 +1,130 @@ +//! Types and functions for applying JavaScript Convert rules to [`JsValue`] when +//! converting. See (Section 9) for +//! conversion rules of JavaScript types. +//! +//! Some conversions are not specified in the spec (e.g. integer conversions), +//! and we apply rules that make sense (e.g. converting to Number and rounding +//! if necessary). + +use boa_engine::JsNativeError; + +use crate::value::TryFromJs; +use crate::{Context, JsResult, JsString, JsValue}; + +/// A wrapper type that allows converting a `JsValue` to a specific type. +/// This is useful when you want to convert a `JsValue` to a Rust type. +/// +/// # Example +/// Convert a string to number. +/// ``` +/// # use boa_engine::{Context, js_string, JsValue}; +/// # use boa_engine::value::{Convert, TryFromJs}; +/// # let mut context = Context::default(); +/// let value = JsValue::from(js_string!("42")); +/// let Convert(converted): Convert = Convert::try_from_js(&value, &mut context).unwrap(); +/// +/// assert_eq!(converted, 42); +/// ``` +/// +/// Convert a number to a bool. +/// ``` +/// # use boa_engine::{Context, js_string, JsValue}; +/// # use boa_engine::value::{Convert, TryFromJs}; +/// # let mut context = Context::default(); +/// let Convert(conv0): Convert = Convert::try_from_js(&JsValue::Integer(0), &mut context).unwrap(); +/// let Convert(conv5): Convert = Convert::try_from_js(&JsValue::Integer(5), &mut context).unwrap(); +/// let Convert(conv_nan): Convert = Convert::try_from_js(&JsValue::Rational(f64::NAN), &mut context).unwrap(); +/// +/// assert_eq!(conv0, false); +/// assert_eq!(conv5, true); +/// assert_eq!(conv_nan, false); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Convert(pub T); + +impl From for Convert { + fn from(value: T) -> Self { + Self(value) + } +} + +macro_rules! decl_convert_to_int { + ($($ty:ty),*) => { + $( + impl TryFromJs for Convert<$ty> { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + value.to_numeric_number(context).and_then(|num| { + if num.is_finite() { + if num >= f64::from(<$ty>::MAX) { + Err(JsNativeError::typ() + .with_message("cannot convert value to integer, it is too large") + .into()) + } else if num <= f64::from(<$ty>::MIN) { + Err(JsNativeError::typ() + .with_message("cannot convert value to integer, it is too small") + .into()) + // Only round if it differs from the next integer by an epsilon + } else if num.abs().fract() >= (1.0 - f64::EPSILON) { + Ok(Convert(num.round() as $ty)) + } else { + Ok(Convert(num as $ty)) + } + } else if num.is_nan() { + Err(JsNativeError::typ() + .with_message("cannot convert NaN to integer") + .into()) + } else if num.is_infinite() { + Err(JsNativeError::typ() + .with_message("cannot convert Infinity to integer") + .into()) + } else { + Err(JsNativeError::typ() + .with_message("cannot convert non-finite number to integer") + .into()) + } + }) + } + } + )* + }; +} + +decl_convert_to_int!(i8, i16, i32, u8, u16, u32); + +macro_rules! decl_convert_to_float { + ($($ty:ty),*) => { + $( + impl TryFromJs for Convert<$ty> { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + value.to_numeric_number(context).and_then(|num| Ok(Convert(<$ty>::try_from(num).map_err(|_| { + JsNativeError::typ() + .with_message("cannot convert value to float") + })?))) + } + } + )* + }; +} + +decl_convert_to_float!(f64); + +impl TryFromJs for Convert { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + value + .to_string(context) + .and_then(|s| s.to_std_string().map_err(|_| JsNativeError::typ().into())) + .map(Convert) + } +} + +impl TryFromJs for Convert { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + value.to_string(context).map(Convert) + } +} + +impl TryFromJs for Convert { + fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { + Ok(Self(value.to_boolean())) + } +} diff --git a/core/engine/src/value/conversions/mod.rs b/core/engine/src/value/conversions/mod.rs index 0b2d02035d..6d68ea075e 100644 --- a/core/engine/src/value/conversions/mod.rs +++ b/core/engine/src/value/conversions/mod.rs @@ -7,6 +7,8 @@ use super::{JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler}; mod serde_json; pub(super) mod try_from_js; +pub(super) mod convert; + impl From for JsValue { fn from(value: JsString) -> Self { let _timer = Profiler::global().start_event("From", "value"); diff --git a/core/engine/src/value/conversions/try_from_js.rs b/core/engine/src/value/conversions/try_from_js.rs index 07d0fadeb7..a4b5a46560 100644 --- a/core/engine/src/value/conversions/try_from_js.rs +++ b/core/engine/src/value/conversions/try_from_js.rs @@ -2,7 +2,7 @@ use num_bigint::BigInt; -use crate::{js_string, Context, JsBigInt, JsNativeError, JsResult, JsValue}; +use crate::{js_string, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue}; /// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types. pub trait TryFromJs: Sized { @@ -47,6 +47,17 @@ impl TryFromJs for String { } } +impl TryFromJs for JsString { + fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { + match value { + JsValue::String(s) => Ok(s.clone()), + _ => Err(JsNativeError::typ() + .with_message("cannot convert value to a String") + .into()), + } + } +} + impl TryFromJs for Option where T: TryFromJs, @@ -91,6 +102,17 @@ where } } +impl TryFromJs for JsObject { + fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { + match value { + JsValue::Object(o) => Ok(o.clone()), + _ => Err(JsNativeError::typ() + .with_message("cannot convert value to a Object") + .into()), + } + } +} + impl TryFromJs for JsBigInt { fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { match value { diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index a38c9fd8bb..6678e294d6 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -2,16 +2,23 @@ //! //! Javascript values, utility methods and conversion between Javascript values and Rust values. -mod conversions; -pub(crate) mod display; -mod equality; -mod hash; -mod integer; -mod operations; -mod r#type; +use std::{ + collections::HashSet, + fmt::{self, Display}, + ops::Sub, +}; -#[cfg(test)] -mod tests; +use num_bigint::BigInt; +use num_integer::Integer; +use num_traits::{ToPrimitive, Zero}; +use once_cell::sync::Lazy; + +use boa_gc::{custom_trace, Finalize, Trace}; +#[doc(inline)] +pub use boa_macros::TryFromJs; +use boa_profiler::Profiler; +#[doc(inline)] +pub use conversions::convert::Convert; use crate::{ builtins::{ @@ -25,27 +32,24 @@ use crate::{ symbol::JsSymbol, Context, JsBigInt, JsResult, JsString, }; -use boa_gc::{custom_trace, Finalize, Trace}; -use boa_profiler::Profiler; -use num_bigint::BigInt; -use num_integer::Integer; -use num_traits::{ToPrimitive, Zero}; -use once_cell::sync::Lazy; -use std::{ - collections::HashSet, - fmt::{self, Display}, - ops::Sub, -}; +pub(crate) use self::conversions::IntoOrUndefined; #[doc(inline)] pub use self::{ conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity, operations::*, r#type::Type, }; -#[doc(inline)] -pub use boa_macros::TryFromJs; -pub(crate) use self::conversions::IntoOrUndefined; +mod conversions; +pub(crate) mod display; +mod equality; +mod hash; +mod integer; +mod operations; +mod r#type; + +#[cfg(test)] +mod tests; static TWO_E_64: Lazy = Lazy::new(|| { const TWO_E_64: u128 = 2u128.pow(64);