From 9702501f09aed8ec286654d9eeaf6c115be0722a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 9 Apr 2024 15:13:19 -0700 Subject: [PATCH] Add more utility traits and funtions to boa_interop (#3773) * Add more utility traits and funtions to boa_interop * cargo clippy * Readd the safe version for Fn which also implements Copy * Use a new trait for converting a Rust type to a JsResult * cargo clippy * Add a test for returning result and Fn() * Seal both IntoJsFunction and IntoJsFunctionUnsafe * Try Borrowing and return a nice error instead of panicking * Address comments * Rename into_js_function to into_js_function_copied * Move TryIntoJsResult to boa_engine * Move JsRest to be at the end only of arguments and add JsAll * use from_copy_closure and remove unsafe --- core/engine/src/lib.rs | 16 + core/engine/src/try_into_js_result_impls.rs | 33 ++ core/interop/src/into_js_function_impls.rs | 240 ++++++++++++ core/interop/src/lib.rs | 396 ++++++++++++++++++-- 4 files changed, 647 insertions(+), 38 deletions(-) create mode 100644 core/engine/src/try_into_js_result_impls.rs create mode 100644 core/interop/src/into_js_function_impls.rs diff --git a/core/engine/src/lib.rs b/core/engine/src/lib.rs index c017bfb4c6..713906e9bd 100644 --- a/core/engine/src/lib.rs +++ b/core/engine/src/lib.rs @@ -141,6 +141,22 @@ pub use prelude::*; /// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`) pub type JsResult = StdResult; +/// Create a [`JsResult`] from a Rust value. This trait is used to +/// convert Rust types to JS types, including [`JsResult`] of +/// Rust values and [`JsValue`]s. +/// +/// This trait is implemented for any that can be converted into a [`JsValue`]. +pub trait TryIntoJsResult { + /// Try to convert a Rust value into a `JsResult`. + /// + /// # Errors + /// Any parsing errors that may occur during the conversion, or any + /// error that happened during the call to a function. + fn try_into_js_result(self, context: &mut Context) -> JsResult; +} + +mod try_into_js_result_impls; + /// A utility trait to make working with function arguments easier. pub trait JsArgs { /// Utility function to `get` a parameter from a `[JsValue]` or default to `JsValue::Undefined` diff --git a/core/engine/src/try_into_js_result_impls.rs b/core/engine/src/try_into_js_result_impls.rs new file mode 100644 index 0000000000..e4fe117ec7 --- /dev/null +++ b/core/engine/src/try_into_js_result_impls.rs @@ -0,0 +1,33 @@ +//! Declare implementations of [`TryIntoJsResult`] trait for various types. + +use crate::{Context, JsResult, JsValue, TryIntoJsResult}; + +impl TryIntoJsResult for T +where + T: Into, +{ + fn try_into_js_result(self, _cx: &mut Context) -> JsResult { + Ok(self.into()) + } +} + +impl TryIntoJsResult for Option +where + T: TryIntoJsResult, +{ + fn try_into_js_result(self, cx: &mut Context) -> JsResult { + match self { + Some(value) => value.try_into_js_result(cx), + None => Ok(JsValue::undefined()), + } + } +} + +impl TryIntoJsResult for JsResult +where + T: TryIntoJsResult, +{ + fn try_into_js_result(self, cx: &mut Context) -> JsResult { + self.and_then(|value| value.try_into_js_result(cx)) + } +} diff --git a/core/interop/src/into_js_function_impls.rs b/core/interop/src/into_js_function_impls.rs new file mode 100644 index 0000000000..d0d3093195 --- /dev/null +++ b/core/interop/src/into_js_function_impls.rs @@ -0,0 +1,240 @@ +//! Implementations of the `IntoJsFunction` trait for various function signatures. + +use std::cell::RefCell; + +use boa_engine::{js_string, Context, JsError, NativeFunction, TryIntoJsResult}; + +use crate::private::IntoJsFunctionSealed; +use crate::{IntoJsFunctionCopied, JsRest, TryFromJsArgument, UnsafeIntoJsFunction}; + +/// A token to represent the context argument in the function signature. +/// This should not be used directly and has no external meaning. +#[derive(Debug, Copy, Clone)] +pub struct ContextArgToken; + +macro_rules! impl_into_js_function { + ($($id: ident: $t: ident),*) => { + impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)*), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)*) -> R + 'static + {} + + impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* ContextArgToken,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* &mut Context) -> R + 'static + {} + + impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* JsRest) -> R + 'static + {} + + impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest, ContextArgToken), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* JsRest, &mut Context) -> R + 'static + {} + + impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)*), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)*) -> R + 'static, + { + #[allow(unused_variables)] + unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + match s.try_borrow_mut() { + Ok(mut r) => r( $($id,)* ).try_into_js_result(ctx), + Err(_) => { + Err(JsError::from_opaque(js_string!("recursive calls to this function not supported").into())) + } + } + }) + } + } + } + + impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* JsRest) -> R + 'static, + { + #[allow(unused_variables)] + unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + match s.try_borrow_mut() { + Ok(mut r) => r( $($id,)* rest.into() ).try_into_js_result(ctx), + Err(_) => { + Err(JsError::from_opaque(js_string!("recursive calls to this function not supported").into())) + } + } + }) + } + } + } + + impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* ContextArgToken,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* &mut Context) -> R + 'static, + { + #[allow(unused_variables)] + unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s.borrow_mut()( $($id,)* ctx); + r.try_into_js_result(ctx) + }) + } + } + } + + impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest, ContextArgToken), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: FnMut($($t,)* JsRest, &mut Context) -> R + 'static, + { + #[allow(unused_variables)] + unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s.borrow_mut()( $($id,)* rest.into(), ctx); + r.try_into_js_result(ctx) + }) + } + } + } + + // Safe versions for `Fn(..) -> ...`. + impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)*), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: Fn($($t,)*) -> R + 'static + Copy, + { + #[allow(unused_variables)] + fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction { + let s = self; + NativeFunction::from_copy_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s( $($id,)* ); + r.try_into_js_result(ctx) + }) + } + } + + impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: Fn($($t,)* JsRest) -> R + 'static + Copy, + { + #[allow(unused_variables)] + fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction { + let s = self; + NativeFunction::from_copy_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s( $($id,)* rest.into() ); + r.try_into_js_result(ctx) + }) + } + } + + impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* ContextArgToken,), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: Fn($($t,)* &mut Context) -> R + 'static + Copy, + { + #[allow(unused_variables)] + fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction { + let s = self; + NativeFunction::from_copy_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s( $($id,)* ctx); + r.try_into_js_result(ctx) + }) + } + } + + impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest, ContextArgToken), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: TryIntoJsResult, + T: Fn($($t,)* JsRest, &mut Context) -> R + 'static + Copy, + { + #[allow(unused_variables)] + fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction { + let s = self; + NativeFunction::from_copy_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s( $($id,)* rest.into(), ctx); + r.try_into_js_result(ctx) + }) + } + } + }; +} + +// Currently implemented up to 12 arguments. The empty argument list +// is implemented separately above. +// Consider that JsRest and JsThis are part of this list, but Context +// is not, as it is a special specialization of the template. +impl_into_js_function!(); +impl_into_js_function!(a: A); +impl_into_js_function!(a: A, b: B); +impl_into_js_function!(a: A, b: B, c: C); +impl_into_js_function!(a: A, b: B, c: C, d: D); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L); diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index 0aa8739d6c..1ff4eca8da 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -1,12 +1,18 @@ //! Interop utilities between Boa and its host. -use std::cell::RefCell; - use boa_engine::module::SyntheticModuleInitializer; -use boa_engine::{Context, JsString, JsValue, Module, NativeFunction}; +use boa_engine::value::TryFromJs; +use boa_engine::{Context, JsResult, JsString, JsValue, Module, NativeFunction}; pub mod loaders; +/// Internal module only. +pub(crate) mod private { + /// A sealed trait to prevent users from implementing the `IntoJsModuleFunction` + /// and `IntoJsFunctionUnsafe` traits to their own types. + pub trait IntoJsFunctionSealed {} +} + /// A trait to convert a type into a JS module. pub trait IntoJsModule { /// Converts the type into a JS module. @@ -40,37 +46,271 @@ impl + Clone> IntoJsModule fo /// This trait does not require the implementing type to be `Copy`, which /// can lead to undefined behaviour if it contains Garbage Collected objects. /// -/// # Safety -/// For this trait to be implemented safely, the implementing type must not contain any -/// garbage collected objects (from [`boa_gc`]). -pub unsafe trait IntoJsFunctionUnsafe { +/// This trait is implemented for functions with various signatures. +/// +/// For example: +/// ``` +/// # use boa_engine::{Context, JsValue, NativeFunction}; +/// # use boa_interop::UnsafeIntoJsFunction; +/// # let mut context = Context::default(); +/// let f = |a: i32, b: i32| a + b; +/// let f = unsafe { f.into_js_function_unsafe(&mut context) }; +/// let result = f.call(&JsValue::undefined(), &[JsValue::from(1), JsValue::from(2)], &mut context).unwrap(); +/// assert_eq!(result, JsValue::new(3)); +/// ``` +/// +/// Since the `IntoJsFunctionUnsafe` trait is implemented for `FnMut`, you can +/// also use closures directly: +/// ``` +/// # use boa_engine::{Context, JsValue, NativeFunction}; +/// # use boa_interop::UnsafeIntoJsFunction; +/// # use std::cell::RefCell; +/// # use std::rc::Rc; +/// # let mut context = Context::default(); +/// let mut x = Rc::new(RefCell::new(0)); +/// // Because NativeFunction takes ownership of the closure, +/// // the compiler cannot be certain it won't outlive `x`, so +/// // we need to create a `Rc` and share it. +/// let f = unsafe { +/// let x = x.clone(); +/// move |a: i32| *x.borrow_mut() += a +/// }; +/// let f = unsafe { f.into_js_function_unsafe(&mut context) }; +/// f.call(&JsValue::undefined(), &[JsValue::from(1)], &mut context).unwrap(); +/// f.call(&JsValue::undefined(), &[JsValue::from(4)], &mut context).unwrap(); +/// assert_eq!(*x.borrow(), 5); +/// ``` +pub trait UnsafeIntoJsFunction: private::IntoJsFunctionSealed { /// Converts the type into a JS function. /// /// # Safety /// This function is unsafe to ensure the callee knows the risks of using this trait. /// The implementing type must not contain any garbage collected objects. - unsafe fn into_js_function(self, context: &mut Context) -> NativeFunction; + unsafe fn into_js_function_unsafe(self, context: &mut Context) -> NativeFunction; +} + +/// The safe equivalent of the [`UnsafeIntoJsFunction`] trait. +/// This can only be used on closures that have the `Copy` trait. +/// +/// Since this function is implemented for `Fn(...)` closures, we can use +/// it directly when defining a function: +/// ``` +/// # use boa_engine::{Context, JsValue, NativeFunction}; +/// # use boa_interop::IntoJsFunctionCopied; +/// # let mut context = Context::default(); +/// let f = |a: i32, b: i32| a + b; +/// let f = f.into_js_function_copied(&mut context); +/// let result = f.call( +/// &JsValue::undefined(), +/// &[JsValue::from(1), JsValue::from(2)], +/// &mut context +/// ).unwrap(); +/// assert_eq!(result, JsValue::new(3)); +/// ``` +pub trait IntoJsFunctionCopied: private::IntoJsFunctionSealed + Copy { + /// Converts the type into a JS function. + fn into_js_function_copied(self, context: &mut Context) -> NativeFunction; +} + +/// Create a Rust value from a JS argument. This trait is used to +/// convert arguments from JS to Rust types. It allows support +/// for optional arguments or rest arguments. +pub trait TryFromJsArgument: Sized { + /// Try to convert a JS argument into a Rust value, returning the + /// value and the rest of the arguments to be parsed. + /// + /// # Errors + /// Any parsing errors that may occur during the conversion. + fn try_from_js_argument<'a>( + this: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])>; +} + +impl TryFromJsArgument for T { + fn try_from_js_argument<'a>( + _: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + match rest.split_first() { + Some((first, rest)) => Ok((first.try_js_into(context)?, rest)), + None => T::try_from_js(&JsValue::undefined(), context).map(|v| (v, rest)), + } + } +} + +/// An argument that when used in a JS function will empty the list +/// of JS arguments as `JsValue`s. This can be used for having the +/// rest of the arguments in a function. It should be the last +/// argument of your function, before the `Context` argument if any. +/// +/// For example, +/// ``` +/// # use boa_engine::{Context, JsValue}; +/// # use boa_interop::{IntoJsFunctionCopied, JsRest}; +/// # let mut context = Context::default(); +/// let sums = (|args: JsRest, context: &mut Context| -> i32 { +/// args.iter().map(|i| i.try_js_into::(context).unwrap()).sum::() +/// }).into_js_function_copied(&mut context); +/// +/// let result = sums.call( +/// &JsValue::undefined(), +/// &[JsValue::from(1), JsValue::from(2), JsValue::from(3)], +/// &mut context +/// ).unwrap(); +/// assert_eq!(result, JsValue::new(6)); +/// ``` +#[derive(Debug, Clone)] +pub struct JsRest(pub Vec); + +#[allow(unused)] +impl JsRest { + /// Consumes the `JsRest` and returns the inner list of `JsValue`. + #[must_use] + pub fn into_inner(self) -> Vec { + self.0 + } + + /// Returns an iterator over the inner list of `JsValue`. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Returns a mutable iterator over the inner list of `JsValue`. + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } + + /// Returns the length of the inner list of `JsValue`. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the inner list of `JsValue` is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From<&[JsValue]> for JsRest { + fn from(values: &[JsValue]) -> Self { + Self(values.to_vec()) + } +} + +impl IntoIterator for JsRest { + type Item = JsValue; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.into_inner().into_iter() + } +} + +/// An argument that when used in a JS function will capture all +/// the arguments that can be converted to `T`. The first argument +/// that cannot be converted to `T` will stop the conversion. +/// +/// For example, +/// ``` +/// # use boa_engine::{Context, JsValue}; +/// # use boa_interop::{IntoJsFunctionCopied, JsAll}; +/// # let mut context = Context::default(); +/// let sums = (|args: JsAll, context: &mut Context| -> i32 { +/// args.iter().sum() +/// }).into_js_function_copied(&mut context); +/// +/// let result = sums.call( +/// &JsValue::undefined(), +/// &[JsValue::from(1), JsValue::from(2), JsValue::from(3), JsValue::Boolean(true), JsValue::from(4)], +/// &mut context +/// ).unwrap(); +/// assert_eq!(result, JsValue::new(6)); +/// ``` +#[derive(Debug, Clone)] +pub struct JsAll(pub Vec); + +impl JsAll { + /// Consumes the `JsAll` and returns the inner list of `T`. + #[must_use] + pub fn into_inner(self) -> Vec { + self.0 + } + + /// Returns an iterator over the inner list of `T`. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Returns a mutable iterator over the inner list of `T`. + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } + + /// Returns the length of the inner list of `T`. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the inner list of `T` is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } -unsafe impl IntoJsFunctionUnsafe for T { - unsafe fn into_js_function(self, _context: &mut Context) -> NativeFunction { - let cell = RefCell::new(self); - unsafe { - NativeFunction::from_closure(move |_, _, _| { - cell.borrow_mut()(); - Ok(JsValue::undefined()) - }) +impl TryFromJsArgument for JsAll { + fn try_from_js_argument<'a>( + _this: &'a JsValue, + mut rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + let mut values = Vec::new(); + + while !rest.is_empty() { + match rest[0].try_js_into(context) { + Ok(value) => { + values.push(value); + rest = &rest[1..]; + } + Err(_) => break, + } } + Ok((JsAll(values), rest)) } } +/// Captures the `this` value in a JS function. Although this can be +/// specified multiple times as argument, it will always be filled +/// with clone of the same value. +#[derive(Debug, Clone)] +pub struct JsThis(pub T); + +impl TryFromJsArgument for JsThis { + fn try_from_js_argument<'a>( + this: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + Ok((JsThis(this.try_js_into(context)?), rest)) + } +} + +// Implement `IntoJsFunction` for functions with a various list of +// arguments. +mod into_js_function_impls; + #[test] #[allow(clippy::missing_panics_doc)] pub fn into_js_module() { - use boa_engine::builtins::promise::PromiseState; use boa_engine::{js_string, JsValue, Source}; + use std::cell::RefCell; use std::rc::Rc; - use std::sync::atomic::{AtomicU32, Ordering}; let loader = Rc::new(loaders::HashMapModuleLoader::new()); let mut context = Context::builder() @@ -78,29 +318,58 @@ pub fn into_js_module() { .build() .unwrap(); - let foo_count = Rc::new(AtomicU32::new(0)); - let bar_count = Rc::new(AtomicU32::new(0)); + let foo_count = Rc::new(RefCell::new(0)); + let bar_count = Rc::new(RefCell::new(0)); + let dad_count = Rc::new(RefCell::new(0)); + let result = Rc::new(RefCell::new(JsValue::undefined())); let module = unsafe { vec![ ( js_string!("foo"), - IntoJsFunctionUnsafe::into_js_function( + { + let counter = foo_count.clone(); + move || { + *counter.borrow_mut() += 1; + let result = *counter.borrow(); + result + } + } + .into_js_function_unsafe(&mut context), + ), + ( + js_string!("bar"), + UnsafeIntoJsFunction::into_js_function_unsafe( { - let counter = foo_count.clone(); - move || { - counter.fetch_add(1, Ordering::Relaxed); + let counter = bar_count.clone(); + move |i: i32| { + *counter.borrow_mut() += i; } }, &mut context, ), ), ( - js_string!("bar"), - IntoJsFunctionUnsafe::into_js_function( + js_string!("dad"), + UnsafeIntoJsFunction::into_js_function_unsafe( { - let counter = bar_count.clone(); - move || { - counter.fetch_add(1, Ordering::Relaxed); + let counter = dad_count.clone(); + move |args: JsRest, context: &mut Context| { + *counter.borrow_mut() += args + .into_iter() + .map(|i| i.try_js_into::(context).unwrap()) + .sum::(); + } + }, + &mut context, + ), + ), + ( + js_string!("send"), + UnsafeIntoJsFunction::into_js_function_unsafe( + { + let result = result.clone(); + move |value: JsValue| { + *result.borrow_mut() = value; } }, &mut context, @@ -116,11 +385,15 @@ pub fn into_js_module() { r" import * as test from 'test'; let result = test.foo(); - for (let i = 0; i < 10; i++) { - test.bar(); + test.foo(); + for (let i = 1; i <= 5; i++) { + test.bar(i); + } + for (let i = 1; i < 5; i++) { + test.dad(1, 2, 3); } - result + test.send(result); ", ); let root_module = Module::parse(source, None, &mut context).unwrap(); @@ -129,11 +402,58 @@ pub fn into_js_module() { context.run_jobs(); // Checking if the final promise didn't return an error. - let PromiseState::Fulfilled(v) = promise_result.state() else { - panic!("module didn't execute successfully!") - }; + assert!( + promise_result.state().as_fulfilled().is_some(), + "module didn't execute successfully! Promise: {:?}", + promise_result.state() + ); - assert_eq!(foo_count.load(Ordering::Relaxed), 1); - assert_eq!(bar_count.load(Ordering::Relaxed), 10); - assert_eq!(v, JsValue::undefined()); + assert_eq!(*foo_count.borrow(), 2); + assert_eq!(*bar_count.borrow(), 15); + assert_eq!(*dad_count.borrow(), 24); + assert_eq!(result.borrow().clone().try_js_into(&mut context), Ok(1u32)); +} + +#[test] +fn can_throw_exception() { + use boa_engine::{js_string, JsError, JsValue, Source}; + use std::rc::Rc; + + let loader = Rc::new(loaders::HashMapModuleLoader::new()); + let mut context = Context::builder() + .module_loader(loader.clone()) + .build() + .unwrap(); + + let module = vec![( + js_string!("doTheThrow"), + IntoJsFunctionCopied::into_js_function_copied( + |message: JsValue| -> JsResult<()> { Err(JsError::from_opaque(message)) }, + &mut context, + ), + )] + .into_js_module(&mut context); + + loader.register(js_string!("test"), module); + + let source = Source::from_bytes( + r" + import * as test from 'test'; + try { + test.doTheThrow('javascript'); + } catch(e) { + throw 'from ' + e; + } + ", + ); + let root_module = Module::parse(source, None, &mut context).unwrap(); + + let promise_result = root_module.load_link_evaluate(&mut context); + context.run_jobs(); + + // Checking if the final promise didn't return an error. + assert_eq!( + promise_result.state().as_rejected(), + Some(&JsString::from("from javascript").into()) + ); }