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

Add a new type Convert<> to convert values #3786

Merged
merged 6 commits into from
Apr 10, 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
130 changes: 130 additions & 0 deletions core/engine/src/value/conversions/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Types and functions for applying JavaScript Convert rules to [`JsValue`] when
//! converting. See <https://262.ecma-international.org/5.1/#sec-9> (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<i32> = 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<bool> = Convert::try_from_js(&JsValue::Integer(0), &mut context).unwrap();
/// let Convert(conv5): Convert<bool> = Convert::try_from_js(&JsValue::Integer(5), &mut context).unwrap();
/// let Convert(conv_nan): Convert<bool> = 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<T: TryFromJs>(pub T);

impl<T: TryFromJs> From<T> for Convert<T> {
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<Self> {
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<Self> {
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<String> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value
.to_string(context)
.and_then(|s| s.to_std_string().map_err(|_| JsNativeError::typ().into()))
.map(Convert)
}
}

impl TryFromJs for Convert<JsString> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value.to_string(context).map(Convert)
}
}

impl TryFromJs for Convert<bool> {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
Ok(Self(value.to_boolean()))
}
}
2 changes: 2 additions & 0 deletions core/engine/src/value/conversions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsString> for JsValue {
fn from(value: JsString) -> Self {
let _timer = Profiler::global().start_event("From<JsString>", "value");
Expand Down
24 changes: 23 additions & 1 deletion core/engine/src/value/conversions/try_from_js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,6 +47,17 @@ impl TryFromJs for String {
}
}

impl TryFromJs for JsString {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {
JsValue::String(s) => Ok(s.clone()),
_ => Err(JsNativeError::typ()
.with_message("cannot convert value to a String")
.into()),
}
}
}

impl<T> TryFromJs for Option<T>
where
T: TryFromJs,
Expand Down Expand Up @@ -91,6 +102,17 @@ where
}
}

impl TryFromJs for JsObject {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
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<Self> {
match value {
Expand Down
50 changes: 27 additions & 23 deletions core/engine/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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<BigInt> = Lazy::new(|| {
const TWO_E_64: u128 = 2u128.pow(64);
Expand Down
Loading