diff --git a/core/engine/src/value/conversions/mod.rs b/core/engine/src/value/conversions/mod.rs index b247db10f9..3fa3841212 100644 --- a/core/engine/src/value/conversions/mod.rs +++ b/core/engine/src/value/conversions/mod.rs @@ -7,6 +7,7 @@ use super::{JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler}; mod either; mod serde_json; pub(super) mod try_from_js; +pub(super) mod try_into_js; pub(super) mod convert; diff --git a/core/engine/src/value/conversions/try_into_js.rs b/core/engine/src/value/conversions/try_into_js.rs new file mode 100644 index 0000000000..ce24903c5d --- /dev/null +++ b/core/engine/src/value/conversions/try_into_js.rs @@ -0,0 +1,289 @@ +use crate::{Context, JsNativeError, JsResult, JsValue}; +use boa_string::JsString; + +/// This trait adds a conversions from a Rust Type into [`JsValue`]. +pub trait TryIntoJs: Sized { + /// This function tries to convert a `Self` into [`JsValue`]. + fn try_into_js(&self, context: &mut Context) -> JsResult; +} + +impl TryIntoJs for bool { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + JsResult::Ok(JsValue::Boolean(*self)) + } +} + +impl TryIntoJs for &str { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + JsResult::Ok(JsValue::String(JsString::from(*self))) + } +} +impl TryIntoJs for String { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + JsResult::Ok(JsValue::String(JsString::from(self.as_str()))) + } +} + +macro_rules! impl_try_into_js_by_from { + ($t:ty) => { + impl TryIntoJs for $t { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + JsResult::Ok(JsValue::from(self.clone())) + } + } + }; + [$($ts:ty),+] => { + $(impl_try_into_js_by_from!($ts);)+ + } +} +impl_try_into_js_by_from![i8, u8, i16, u16, i32, u32, f32, f64]; +impl_try_into_js_by_from![ + JsValue, + JsString, + crate::JsBigInt, + crate::JsObject, + crate::JsSymbol, + crate::object::JsArray, + crate::object::JsArrayBuffer, + crate::object::JsDataView, + crate::object::JsDate, + crate::object::JsFunction, + crate::object::JsGenerator, + crate::object::JsMapIterator, + crate::object::JsMap, + crate::object::JsSetIterator, + crate::object::JsSet, + crate::object::JsSharedArrayBuffer, + crate::object::JsInt8Array, + crate::object::JsInt16Array, + crate::object::JsInt32Array, + crate::object::JsUint8Array, + crate::object::JsUint16Array, + crate::object::JsUint32Array, + crate::object::JsFloat32Array, + crate::object::JsFloat64Array +]; + +const MAX_SAFE_INTEGER_I64: i64 = (1 << 53) - 1; +const MIN_SAFE_INTEGER_I64: i64 = -MAX_SAFE_INTEGER_I64; + +fn err_outside_safe_range() -> crate::JsError { + JsNativeError::typ() + .with_message("cannot convert value into JsValue: the value is outside the safe range") + .into() +} +fn convert_safe_i64(value: i64) -> JsValue { + i32::try_from(value).map_or(JsValue::Rational(value as f64), JsValue::Integer) +} + +impl TryIntoJs for i64 { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + let value = *self; + #[allow(clippy::manual_range_contains)] + if value < MIN_SAFE_INTEGER_I64 || MAX_SAFE_INTEGER_I64 < value { + JsResult::Err(err_outside_safe_range()) + } else { + JsResult::Ok(convert_safe_i64(value)) + } + } +} +impl TryIntoJs for u64 { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + let value = *self; + if (MAX_SAFE_INTEGER_I64 as u64) < value { + JsResult::Err(err_outside_safe_range()) + } else { + JsResult::Ok(convert_safe_i64(value as i64)) + } + } +} +impl TryIntoJs for i128 { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + let value = *self; + if value < i128::from(MIN_SAFE_INTEGER_I64) || i128::from(MAX_SAFE_INTEGER_I64) < value { + JsResult::Err(err_outside_safe_range()) + } else { + JsResult::Ok(convert_safe_i64(value as i64)) + } + } +} +impl TryIntoJs for u128 { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + let value = *self; + if (MAX_SAFE_INTEGER_I64 as u128) < value { + JsResult::Err(err_outside_safe_range()) + } else { + JsResult::Ok(convert_safe_i64(value as i64)) + } + } +} + +impl TryIntoJs for Option +where + T: TryIntoJs, +{ + fn try_into_js(&self, context: &mut Context) -> JsResult { + match self { + Some(x) => x.try_into_js(context), + None => JsResult::Ok(JsValue::Null), + } + } +} + +impl TryIntoJs for Vec +where + T: TryIntoJs, +{ + fn try_into_js(&self, context: &mut Context) -> JsResult { + let arr = crate::object::JsArray::new(context); + for value in self { + let value = value.try_into_js(context)?; + arr.push(value, context)?; + } + JsResult::Ok(arr.into()) + } +} + +macro_rules! impl_try_into_js_for_tuples { + ($($names:ident : $ts:ident),+) => { + impl<$($ts: TryIntoJs,)+> TryIntoJs for ($($ts,)+) { + fn try_into_js(&self, context: &mut Context) -> JsResult { + let ($($names,)+) = self; + let arr = crate::object::JsArray::new(context); + $(arr.push($names.try_into_js(context)?, context)?;)+ + JsResult::Ok(arr.into()) + } + } + }; +} + +impl_try_into_js_for_tuples!(a: A); +impl_try_into_js_for_tuples!(a: A, b: B); +impl_try_into_js_for_tuples!(a: A, b: B, c: C); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J); +impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K); + +impl TryIntoJs for () { + fn try_into_js(&self, _context: &mut Context) -> JsResult { + JsResult::Ok(JsValue::Null) + } +} + +impl TryIntoJs for std::collections::HashSet +where + T: TryIntoJs, +{ + fn try_into_js(&self, context: &mut Context) -> JsResult { + let set = crate::object::JsSet::new(context); + for value in self { + let value = value.try_into_js(context)?; + set.add(value, context)?; + } + JsResult::Ok(set.into()) + } +} + +impl TryIntoJs for std::collections::HashMap +where + K: TryIntoJs, + V: TryIntoJs, +{ + fn try_into_js(&self, context: &mut Context) -> JsResult { + let map = crate::object::JsMap::new(context); + for (key, value) in self { + let key = key.try_into_js(context)?; + let value = value.try_into_js(context)?; + map.set(key, value, context)?; + } + JsResult::Ok(map.into()) + } +} + +#[cfg(test)] +mod try_into_js_tests { + use crate::value::{TryFromJs, TryIntoJs}; + use crate::{Context, JsResult}; + + #[test] + fn big_int_err() { + fn assert(int: &T, context: &mut Context) { + let expect_err = int.try_into_js(context); + assert!(expect_err.is_err()); + } + + let mut context = Context::default(); + let context = &mut context; + + let int = (1 << 55) + 17i64; + assert(&int, context); + + let int = (1 << 55) + 17u64; + assert(&int, context); + + let int = (1 << 55) + 17u128; + assert(&int, context); + + let int = (1 << 55) + 17i128; + assert(&int, context); + } + + #[test] + fn int_tuple() -> JsResult<()> { + let mut context = Context::default(); + let context = &mut context; + + let tuple_initial = ( + -42i8, + 42u8, + 1764i16, + 7641u16, + -((1 << 27) + 13), + (1 << 27) + 72u32, + (1 << 49) + 1793i64, + (1 << 49) + 1793u64, + -((1 << 49) + 7193i128), + (1 << 49) + 9173u128, + ); + + // it will rewrite without reading, so it's just for auto type resolving. + #[allow(unused_assignments)] + let mut tuple_after_transform = tuple_initial; + + let js_value = tuple_initial.try_into_js(context)?; + tuple_after_transform = TryFromJs::try_from_js(&js_value, context)?; + + assert_eq!(tuple_initial, tuple_after_transform); + Ok(()) + } + + #[test] + fn string() -> JsResult<()> { + let mut context = Context::default(); + let context = &mut context; + + let s_init = "String".to_string(); + let js_value = s_init.try_into_js(context)?; + let s: String = TryFromJs::try_from_js(&js_value, context)?; + assert_eq!(s_init, s); + Ok(()) + } + + #[test] + fn vec() -> JsResult<()> { + let mut context = Context::default(); + let context = &mut context; + + let vec_init = vec![(-4i64, 2u64), (15, 15), (32, 23)]; + let js_value = vec_init.try_into_js(context)?; + println!("JsValue: {}", js_value.display()); + let vec: Vec<(i64, u64)> = TryFromJs::try_from_js(&js_value, context)?; + assert_eq!(vec_init, vec); + Ok(()) + } +} diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index 34e8e27377..f98c12607c 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -17,6 +17,7 @@ use once_cell::sync::Lazy; use boa_gc::{custom_trace, Finalize, Trace}; #[doc(inline)] pub use boa_macros::TryFromJs; +pub use boa_macros::TryIntoJs; use boa_profiler::Profiler; #[doc(inline)] pub use conversions::convert::Convert; @@ -24,8 +25,8 @@ pub use conversions::convert::Convert; pub(crate) use self::conversions::IntoOrUndefined; #[doc(inline)] pub use self::{ - conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity, - operations::*, r#type::Type, + conversions::try_from_js::TryFromJs, conversions::try_into_js::TryIntoJs, + display::ValueDisplay, integer::IntegerOrInfinity, operations::*, r#type::Type, }; use crate::builtins::RegExp; use crate::object::{JsFunction, JsPromise, JsRegExp}; diff --git a/core/macros/src/lib.rs b/core/macros/src/lib.rs index 0807d9a0a8..a5bb12afef 100644 --- a/core/macros/src/lib.rs +++ b/core/macros/src/lib.rs @@ -497,3 +497,110 @@ fn to_compile_errors(errors: Vec) -> proc_macro2::TokenStream { let compile_errors = errors.iter().map(syn::Error::to_compile_error); quote!(#(#compile_errors)*) } + +/// Derives the `TryIntoJs` trait, with the `#[boa()]` attribute. +/// +/// # Panics +/// +/// It will panic if the user tries to derive the `TryIntoJs` trait in an `enum` or a tuple struct. +#[proc_macro_derive(TryIntoJs, attributes(boa))] +pub fn derive_try_into_js(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + let Data::Struct(data) = input.data else { + panic!("you can only derive TryFromJs for structs"); + }; + // TODO: Enums ? + + let Fields::Named(fields) = data.fields else { + panic!("you can only derive TryFromJs for named-field structs") + }; + + let props = generate_obj_properties(fields) + .map_err(|err| vec![err]) + .unwrap_or_else(to_compile_errors); + + let type_name = input.ident; + + // Build the output, possibly using quasi-quotation + let expanded = quote! { + impl ::boa_engine::value::TryIntoJs for #type_name { + fn try_into_js(&self, context: &mut boa_engine::Context) -> boa_engine::JsResult { + let obj = boa_engine::JsObject::default(); + #props + boa_engine::JsResult::Ok(obj.into()) + } + } + }; + + // Hand the output tokens back to the compiler + expanded.into() +} + +/// Generates property creation for object. +fn generate_obj_properties(fields: FieldsNamed) -> Result { + use syn::spanned::Spanned; + + let mut prop_ctors = Vec::with_capacity(fields.named.len()); + + for field in fields.named { + let span = field.span(); + let name = field.ident.ok_or_else(|| { + syn::Error::new( + span, + "you can only derive `TryIntoJs` for named-field structs", + ) + })?; + + let mut into_js_with = None; + let mut prop_key = format!("{name}"); + let mut skip = false; + + for attr in field + .attrs + .into_iter() + .filter(|attr| attr.path().is_ident("boa")) + { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("into_js_with") { + let value = meta.value()?; + into_js_with = Some(value.parse::()?); + Ok(()) + } else if meta.path.is_ident("rename") { + let value = meta.value()?; + prop_key = value.parse::()?.value(); + Ok(()) + } else if meta.path.is_ident("skip") & meta.input.is_empty() { + skip = true; + Ok(()) + } else { + Err(meta.error( + "invalid syntax in the `#[boa()]` attribute. \ + Note that this attribute only accepts the following syntax: \ + \n* `#[boa(into_js_with = \"fully::qualified::path\")]`\ + \n* `#[boa(rename = \"jsPropertyName\")]` \ + \n* `#[boa(skip)]` \ + ", + )) + } + })?; + } + + if skip { + continue; + } + + let value = if let Some(into_js_with) = into_js_with { + let into_js_with = Ident::new(&into_js_with.value(), into_js_with.span()); + quote! { #into_js_with(&self.#name, context)? } + } else { + quote! { boa_engine::value::TryIntoJs::try_into_js(&self.#name, context)? } + }; + prop_ctors.push(quote! { + obj.create_data_property_or_throw(boa_engine::js_str!(#prop_key), #value, context)?; + }); + } + + Ok(quote! { #(#prop_ctors)* }) +} diff --git a/examples/src/bin/try_into_js_derive.rs b/examples/src/bin/try_into_js_derive.rs new file mode 100644 index 0000000000..55de96e87a --- /dev/null +++ b/examples/src/bin/try_into_js_derive.rs @@ -0,0 +1,96 @@ +use boa_engine::{ + js_string, + value::{TryFromJs, TryIntoJs}, + Context, JsResult, JsValue, Source, +}; + +#[derive(TryIntoJs)] +struct Test { + x: i32, + #[boa(rename = "y")] + y_point: i32, + #[allow(unused)] + #[boa(skip)] + tuple: (i32, u8, String), + #[boa(rename = "isReadable")] + #[boa(into_js_with = "readable_into_js")] + is_readable: i8, +} + +#[derive(TryFromJs, Debug, PartialEq, Eq)] +struct ResultVerifier { + x: i32, + y: i32, + #[boa(rename = "isReadable")] + is_readable: bool, +} + +fn main() -> JsResult<()> { + let js_code = r#" + function pointShift(pointA, pointB) { + if (pointA.isReadable === true && pointB.isReadable === true) { + return { + x: pointA.x + pointB.x, + y: pointA.y + pointB.y, + isReadable: true, + } + } + return undefined + } + "#; + + let mut context = Context::default(); + let context = &mut context; + + context.eval(Source::from_bytes(js_code))?; + + let point_shift = context + .global_object() + .get(js_string!("pointShift"), context)?; + let point_shift = point_shift.as_callable().unwrap(); + + let a = Test { + x: 10, + y_point: 20, + tuple: (30, 40, "no matter".into()), + is_readable: 1, + }; + let b = Test { + x: 2, + y_point: 1, + tuple: (30, 40, "no matter".into()), + is_readable: 2, + }; + let c = Test { + x: 2, + y_point: 1, + tuple: (30, 40, "no matter".into()), + is_readable: 0, + }; + + let result = point_shift.call( + &JsValue::Undefined, + &[a.try_into_js(context)?, b.try_into_js(context)?], + context, + )?; + let verifier = ResultVerifier::try_from_js(&result, context)?; + let expect = ResultVerifier { + x: 10 + 2, + y: 20 + 1, + is_readable: true, + }; + assert_eq!(verifier, expect); + + let result = point_shift.call( + &JsValue::Undefined, + &[a.try_into_js(context)?, c.try_into_js(context)?], + context, + )?; + assert!(result.is_undefined()); + + Ok(()) +} + +fn readable_into_js(value: &i8, _context: &mut Context) -> JsResult { + Ok(JsValue::Boolean(*value != 0)) +}