Browse Source

Add more utility functions around modules and exports (#3937)

* Add more utility functions around modules and exports

* Use import instead of path

* clippies and fmt

* clippies and fmt

* Add JsPromise::await_blocking and remove ell_and_run

* Fix documentation CI job
pull/3956/head
Hans Larsen 4 months ago committed by GitHub
parent
commit
50fabc8387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 33
      core/engine/src/module/mod.rs
  2. 76
      core/engine/src/object/builtins/jsfunction.rs
  3. 73
      core/engine/src/object/builtins/jspromise.rs
  4. 11
      core/engine/src/value/mod.rs
  5. 26
      core/engine/tests/assets/gcd.js
  6. 36
      core/engine/tests/gcd.rs

33
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<K>(&self, name: K, context: &mut Context) -> JsResult<JsValue>
where
K: Into<PropertyKey>,
{
let namespace = self.namespace(context);
namespace.get(name, context)
}
/// Get an exported function, typed, from the module.
#[inline]
pub fn get_typed_fn<A, R>(
&self,
name: JsStr<'_>,
context: &mut Context,
) -> JsResult<TypedJsFunction<A, R>>
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> {

76
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<Vec<JsValue>>;
}
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<Vec<JsValue>> {
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<A: TryIntoJsArguments, R: TryFromJs> {
inner: JsFunction,
_args: PhantomData<A>,
_ret: PhantomData<R>,
}
impl<A: TryIntoJsArguments, R: TryFromJs> TypedJsFunction<A, R> {
/// 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<R> {
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<R> {
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<A: TryIntoJsArguments, R: TryFromJs>(self) -> TypedJsFunction<A, R> {
TypedJsFunction {
inner: self,
_args: PhantomData,
_ret: PhantomData,
}
}
}
impl From<JsFunction> for JsObject {

73
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<JsValue, JsValue> {
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<JsPromise> for JsObject {

11
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<JsFunction> {
self.as_callable()
.cloned()
.and_then(JsFunction::from_object)
}
/// Returns true if the value is a constructor object.
#[inline]
#[must_use]

26
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);
}

36
core/engine/tests/gcd.rs

@ -0,0 +1,36 @@
#![allow(unused_crate_dependencies)]
//! A test that mimics the GCD example from wasmtime.
//! See: <https://docs.wasmtime.dev/examples-rust-gcd.html#gcdrs>.
//! 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));
}
Loading…
Cancel
Save