diff --git a/core/engine/src/module/mod.rs b/core/engine/src/module/mod.rs index 289e41614d..c0cd78f610 100644 --- a/core/engine/src/module/mod.rs +++ b/core/engine/src/module/mod.rs @@ -30,23 +30,26 @@ use std::rc::Rc; use rustc_hash::FxHashSet; use boa_engine::js_string; +use boa_engine::property::PropertyKey; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; use boa_interner::Interner; use boa_parser::source::ReadChar; use boa_parser::{Parser, Source}; use boa_profiler::Profiler; +use boa_string::JsStr; pub use loader::*; pub use namespace::ModuleNamespace; use source::SourceTextModule; pub use synthetic::{SyntheticModule, SyntheticModuleInitializer}; +use crate::object::TypedJsFunction; use crate::{ builtins, builtins::promise::{PromiseCapability, PromiseState}, environments::DeclarativeEnvironment, object::{JsObject, JsPromise}, realm::Realm, - Context, HostDefined, JsError, JsResult, JsString, JsValue, NativeFunction, + Context, HostDefined, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction, }; mod loader; @@ -608,6 +611,34 @@ impl Module { .clone() } + /// Get an exported value from the module. + #[inline] + pub fn get_value(&self, name: K, context: &mut Context) -> JsResult + where + K: Into, + { + let namespace = self.namespace(context); + namespace.get(name, context) + } + + /// Get an exported function, typed, from the module. + #[inline] + pub fn get_typed_fn( + &self, + name: JsStr<'_>, + context: &mut Context, + ) -> JsResult> + where + A: crate::object::TryIntoJsArguments, + R: crate::value::TryFromJs, + { + let func = self.get_value(name, context)?; + let func = func.as_function().ok_or_else(|| { + JsNativeError::typ().with_message(format!("{name:?} is not a function")) + })?; + Ok(func.typed()) + } + /// Returns the path of the module, if it was created from a file or assigned. #[must_use] pub fn path(&self) -> Option<&Path> { diff --git a/core/engine/src/object/builtins/jsfunction.rs b/core/engine/src/object/builtins/jsfunction.rs index 61c23f1f4c..4cc48dba02 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -1,11 +1,74 @@ //! A Rust API wrapper for Boa's `Function` Builtin ECMAScript Object use crate::{ builtins::function::ConstructorKind, native_function::NativeFunctionObject, object::JsObject, - value::TryFromJs, Context, JsNativeError, JsResult, JsValue, NativeFunction, + value::TryFromJs, Context, JsNativeError, JsResult, JsValue, NativeFunction, TryIntoJsResult, }; use boa_gc::{Finalize, Trace}; +use std::marker::PhantomData; use std::ops::Deref; +/// A trait for converting a tuple of Rust values into a vector of `JsValue`, +/// to be used as arguments for a JavaScript function. +pub trait TryIntoJsArguments { + /// Convert a tuple of Rust values into a vector of `JsValue`. + /// This is automatically implemented for tuples that implement + /// `TryIntoJsResult`. + fn into_js_args(self, cx: &mut Context) -> JsResult>; +} + +macro_rules! impl_try_into_js_args { + ($($n: ident: $t: ident),*) => { + impl<$($t),*> TryIntoJsArguments for ($($t,)*) where $($t: TryIntoJsResult),* { + fn into_js_args(self, cx: &mut Context) -> JsResult> { + let ($($n,)*) = self; + Ok(vec![$($n.try_into_js_result(cx)?),*]) + } + } + }; +} + +impl_try_into_js_args!(a: A); +impl_try_into_js_args!(a: A, b: B); +impl_try_into_js_args!(a: A, b: B, c: C); +impl_try_into_js_args!(a: A, b: B, c: C, d: D); +impl_try_into_js_args!(a: A, b: B, c: C, d: D, e: E); + +/// A JavaScript `Function` rust object, typed. This adds types to +/// a JavaScript exported function, allowing for type checking and +/// type conversion in Rust. Those types must convert to a [`JsValue`] +/// but will not be verified at runtime (since JavaScript doesn't +/// actually have strong typing). +/// +/// To create this type, use the [`JsFunction::typed`] method. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct TypedJsFunction { + inner: JsFunction, + _args: PhantomData, + _ret: PhantomData, +} + +impl TypedJsFunction { + /// Transforms this typed function back into a regular `JsFunction`. + #[must_use] + pub fn into_inner(self) -> JsFunction { + self.inner.clone() + } + + /// Call the function with the given arguments. + #[inline] + pub fn call(&self, context: &mut Context, args: A) -> JsResult { + self.call_with_this(&JsValue::undefined(), context, args) + } + + /// Call the function with the given argument and `this`. + #[inline] + pub fn call_with_this(&self, this: &JsValue, context: &mut Context, args: A) -> JsResult { + let arguments = args.into_js_args(context)?; + let result = self.inner.call(this, &arguments, context)?; + R::try_from_js(&result, context) + } +} + /// JavaScript `Function` rust object. #[derive(Debug, Clone, Trace, Finalize)] pub struct JsFunction { @@ -46,6 +109,17 @@ impl JsFunction { .is_callable() .then(|| Self::from_object_unchecked(object)) } + + /// Creates a `TypedJsFunction` from a `JsFunction`. + #[inline] + #[must_use] + pub fn typed(self) -> TypedJsFunction { + TypedJsFunction { + inner: self, + _args: PhantomData, + _ret: PhantomData, + } + } } impl From for JsObject { diff --git a/core/engine/src/object/builtins/jspromise.rs b/core/engine/src/object/builtins/jspromise.rs index 07a5f27802..d8a936e96b 100644 --- a/core/engine/src/object/builtins/jspromise.rs +++ b/core/engine/src/object/builtins/jspromise.rs @@ -1039,6 +1039,79 @@ impl JsPromise { JsFuture { inner: state } } + + /// Run jobs until this promise is resolved or rejected. This could + /// result in an infinite loop if the promise is never resolved or + /// rejected (e.g. with a [`boa_engine::job::JobQueue`] that does + /// not prioritize properly). If you need more control over how + /// the promise handles timing out, consider using + /// [`Context::run_jobs`] directly. + /// + /// Returns [`Result::Ok`] if the promise resolved, or [`Result::Err`] + /// if the promise was rejected. If the promise was already resolved, + /// [`Context::run_jobs`] is guaranteed to not be executed. + /// + /// # Examples + /// + /// ``` + /// # use boa_engine::{Context, JsArgs, JsValue, NativeFunction}; + /// # use boa_engine::object::builtins::{JsFunction, JsPromise}; + /// let context = &mut Context::default(); + /// + /// let p1 = JsPromise::new(|fns, context| { + /// fns.resolve.call(&JsValue::undefined(), &[JsValue::new(1)], context) + /// }, context); + /// let p2 = p1.then( + /// Some( + /// NativeFunction::from_fn_ptr(|_, args, context| { + /// assert_eq!(*args.get_or_undefined(0), JsValue::new(1)); + /// Ok(JsValue::new(2)) + /// }) + /// .to_js_function(context.realm()), + /// ), + /// None, + /// context,); + /// + /// assert_eq!(p2.await_blocking(context), Ok(JsValue::new(2))); + /// ``` + /// + /// This will not panic as `run_jobs()` is not executed. + /// ``` + /// # use boa_engine::{Context, JsValue, NativeFunction}; + /// # use boa_engine::object::builtins::JsPromise; + /// + /// let context = &mut Context::default(); + /// let p1 = JsPromise::new(|fns, context| { + /// fns.resolve.call(&JsValue::Undefined, &[], context) + /// }, context) + /// .then( + /// Some( + /// NativeFunction::from_fn_ptr(|_, _, _| { + /// panic!("This will not happen."); + /// }) + /// .to_js_function(context.realm()) + /// ), + /// None, + /// context, + /// ); + /// let p2 = JsPromise::resolve(1, context); + /// + /// assert_eq!(p2.await_blocking(context), Ok(JsValue::new(1))); + /// // Uncommenting the following line would panic. + /// // context.run_jobs(); + /// ``` + pub fn await_blocking(&self, context: &mut Context) -> Result { + loop { + eprintln!("await_blocking: {:?}", self.state()); + match self.state() { + PromiseState::Pending => { + context.run_jobs(); + } + PromiseState::Fulfilled(f) => break Ok(f), + PromiseState::Rejected(r) => break Err(r), + } + } + } } impl From for JsObject { diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index 57d1af5a2b..56d1d421c0 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -21,6 +21,7 @@ use boa_profiler::Profiler; #[doc(inline)] pub use conversions::convert::Convert; +use crate::object::JsFunction; use crate::{ builtins::{ number::{f64_to_int32, f64_to_uint32}, @@ -173,6 +174,16 @@ impl JsValue { self.as_object().filter(|obj| obj.is_callable()) } + /// Returns a [`JsFunction`] if the value is callable, otherwise `None`. + /// This is equivalent to `JsFunction::from_object(value.as_callable()?)`. + #[inline] + #[must_use] + pub fn as_function(&self) -> Option { + self.as_callable() + .cloned() + .and_then(JsFunction::from_object) + } + /// Returns true if the value is a constructor object. #[inline] #[must_use] diff --git a/core/engine/tests/assets/gcd.js b/core/engine/tests/assets/gcd.js new file mode 100644 index 0000000000..acdce70254 --- /dev/null +++ b/core/engine/tests/assets/gcd.js @@ -0,0 +1,26 @@ +/** + * Calculate the greatest common divisor of two numbers. + * @param {number} a + * @param {number} b + * @returns {number|*} The greatest common divisor of {a} and {b}. + * @throws {TypeError} If either {a} or {b} is not finite. + */ +export function gcd(a, b) { + a = +a; + b = +b; + if (!Number.isFinite(a) || !Number.isFinite(b)) { + throw new TypeError("Invalid input"); + } + + // Euclidean algorithm + function inner_gcd(a, b) { + while (b !== 0) { + let t = b; + b = a % b; + a = t; + } + return a; + } + + return inner_gcd(a, b); +} diff --git a/core/engine/tests/gcd.rs b/core/engine/tests/gcd.rs new file mode 100644 index 0000000000..eae2d16bc5 --- /dev/null +++ b/core/engine/tests/gcd.rs @@ -0,0 +1,36 @@ +#![allow(unused_crate_dependencies)] +//! A test that mimics the GCD example from wasmtime. +//! See: . +//! This is a good point to discuss and improve on the usability +//! of the [`boa_engine`] API. + +// You can execute this example with `cargo run --example gcd` + +use boa_engine::{js_str, Context, Module}; +use boa_parser::Source; +use std::path::PathBuf; + +#[test] +fn gcd() { + let assets_dir = + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("tests/assets"); + + // Create the engine. + let context = &mut Context::default(); + + // Load the JavaScript code. + let gcd_path = assets_dir.join("gcd.js"); + let source = Source::from_filepath(&gcd_path).unwrap(); + let module = Module::parse(source, None, context).unwrap(); + module + .load_link_evaluate(context) + .await_blocking(context) + .unwrap(); + + let js_gcd = module + .get_typed_fn::<(i32, i32), i32>(js_str!("gcd"), context) + .unwrap(); + + assert_eq!(js_gcd.call(context, (6, 9)), Ok(3)); + assert_eq!(js_gcd.call(context, (9, 6)), Ok(3)); +}