mirror of https://github.com/boa-dev/boa.git
José Julián Espina
1 year ago
committed by
GitHub
7 changed files with 864 additions and 261 deletions
@ -0,0 +1,223 @@
|
||||
use std::path::{Path, PathBuf}; |
||||
|
||||
use rustc_hash::FxHashMap; |
||||
|
||||
use boa_gc::GcRefCell; |
||||
use boa_parser::Source; |
||||
|
||||
use crate::script::Script; |
||||
use crate::{ |
||||
js_string, object::JsObject, realm::Realm, vm::ActiveRunnable, Context, JsError, JsNativeError, |
||||
JsResult, JsString, |
||||
}; |
||||
|
||||
use super::Module; |
||||
|
||||
/// The referrer from which a load request of a module originates.
|
||||
#[derive(Debug, Clone)] |
||||
pub enum Referrer { |
||||
/// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records).
|
||||
Module(Module), |
||||
/// A [**Realm**](https://tc39.es/ecma262/#sec-code-realms).
|
||||
Realm(Realm), |
||||
/// A [**Script Record**](https://tc39.es/ecma262/#sec-script-records)
|
||||
Script(Script), |
||||
} |
||||
|
||||
impl From<ActiveRunnable> for Referrer { |
||||
fn from(value: ActiveRunnable) -> Self { |
||||
match value { |
||||
ActiveRunnable::Script(script) => Self::Script(script), |
||||
ActiveRunnable::Module(module) => Self::Module(module), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Module loading related host hooks.
|
||||
///
|
||||
/// This trait allows to customize the behaviour of the engine on module load requests and
|
||||
/// `import.meta` requests.
|
||||
pub trait ModuleLoader { |
||||
/// Host hook [`HostLoadImportedModule ( referrer, specifier, hostDefined, payload )`][spec].
|
||||
///
|
||||
/// This hook allows to customize the module loading functionality of the engine. Technically,
|
||||
/// this should call the [`FinishLoadingImportedModule`][finish] operation, but this simpler API just provides
|
||||
/// a closure that replaces `FinishLoadingImportedModule`.
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
/// - The host environment must perform `FinishLoadingImportedModule(referrer, specifier, payload, result)`,
|
||||
/// where result is either a normal completion containing the loaded Module Record or a throw
|
||||
/// completion, either synchronously or asynchronously. This is equivalent to calling the `finish_load`
|
||||
/// callback.
|
||||
/// - If this operation is called multiple times with the same `(referrer, specifier)` pair and
|
||||
/// it performs FinishLoadingImportedModule(referrer, specifier, payload, result) where result
|
||||
/// is a normal completion, then it must perform
|
||||
/// `FinishLoadingImportedModule(referrer, specifier, payload, result)` with the same result each
|
||||
/// time.
|
||||
/// - The operation must treat payload as an opaque value to be passed through to
|
||||
/// `FinishLoadingImportedModule`. (can be ignored)
|
||||
///
|
||||
/// [spec]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
|
||||
/// [finish]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
|
||||
#[allow(clippy::type_complexity)] |
||||
fn load_imported_module( |
||||
&self, |
||||
referrer: Referrer, |
||||
specifier: JsString, |
||||
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context<'_>)>, |
||||
context: &mut Context<'_>, |
||||
); |
||||
|
||||
/// Registers a new module into the module loader.
|
||||
///
|
||||
/// This is a convenience method for module loaders caching already parsed modules, since it
|
||||
/// allows registering a new module through the `&dyn ModuleLoader` provided by
|
||||
/// [`Context::module_loader`].
|
||||
///
|
||||
/// Does nothing by default.
|
||||
fn register_module(&self, _specifier: JsString, _module: Module) {} |
||||
|
||||
/// Gets the module associated with the provided specifier.
|
||||
///
|
||||
/// This is a convenience method for module loaders caching already parsed modules, since it
|
||||
/// allows getting a cached module through the `&dyn ModuleLoader` provided by
|
||||
/// [`Context::module_loader`].
|
||||
///
|
||||
/// Returns `None` by default.
|
||||
fn get_module(&self, _specifier: JsString) -> Option<Module> { |
||||
None |
||||
} |
||||
|
||||
/// Host hooks [`HostGetImportMetaProperties ( moduleRecord )`][meta] and
|
||||
/// [`HostFinalizeImportMeta ( importMeta, moduleRecord )`][final].
|
||||
///
|
||||
/// This unifies both APIs into a single hook that can be overriden on both cases.
|
||||
/// The most common usage is to add properties to `import_meta` and return, but this also
|
||||
/// allows modifying the import meta object in more exotic ways before exposing it to ECMAScript
|
||||
/// code.
|
||||
///
|
||||
/// The default implementation of `HostGetImportMetaProperties` is to return a new empty List.
|
||||
///
|
||||
/// [meta]: https://tc39.es/ecma262/#sec-hostgetimportmetaproperties
|
||||
/// [final]: https://tc39.es/ecma262/#sec-hostfinalizeimportmeta
|
||||
fn init_import_meta( |
||||
&self, |
||||
_import_meta: &JsObject, |
||||
_module: &Module, |
||||
_context: &mut Context<'_>, |
||||
) { |
||||
} |
||||
} |
||||
|
||||
/// A module loader that throws when trying to load any modules.
|
||||
///
|
||||
/// Useful to disable the module system on platforms that don't have a filesystem, for example.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct IdleModuleLoader; |
||||
|
||||
impl ModuleLoader for IdleModuleLoader { |
||||
fn load_imported_module( |
||||
&self, |
||||
_referrer: Referrer, |
||||
_specifier: JsString, |
||||
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context<'_>)>, |
||||
context: &mut Context<'_>, |
||||
) { |
||||
finish_load( |
||||
Err(JsNativeError::typ() |
||||
.with_message("module resolution is disabled for this context") |
||||
.into()), |
||||
context, |
||||
); |
||||
} |
||||
} |
||||
|
||||
/// A simple module loader that loads modules relative to a root path.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This loader only works by using the type methods [`SimpleModuleLoader::insert`] and
|
||||
/// [`SimpleModuleLoader::get`]. The utility methods on [`ModuleLoader`] don't work at the moment,
|
||||
/// but we'll unify both APIs in the future.
|
||||
#[derive(Debug)] |
||||
pub struct SimpleModuleLoader { |
||||
root: PathBuf, |
||||
module_map: GcRefCell<FxHashMap<PathBuf, Module>>, |
||||
} |
||||
|
||||
impl SimpleModuleLoader { |
||||
/// Creates a new `SimpleModuleLoader` from a root module path.
|
||||
pub fn new<P: AsRef<Path>>(root: P) -> JsResult<Self> { |
||||
if cfg!(target_family = "wasm") { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("cannot resolve a relative path in WASM targets") |
||||
.into()); |
||||
} |
||||
let root = root.as_ref(); |
||||
let absolute = root.canonicalize().map_err(|e| { |
||||
JsNativeError::typ() |
||||
.with_message(format!("could not set module root `{}`", root.display())) |
||||
.with_cause(JsError::from_opaque(js_string!(e.to_string()).into())) |
||||
})?; |
||||
Ok(Self { |
||||
root: absolute, |
||||
module_map: GcRefCell::default(), |
||||
}) |
||||
} |
||||
|
||||
/// Inserts a new module onto the module map.
|
||||
#[inline] |
||||
pub fn insert(&self, path: PathBuf, module: Module) { |
||||
self.module_map.borrow_mut().insert(path, module); |
||||
} |
||||
|
||||
/// Gets a module from its original path.
|
||||
#[inline] |
||||
pub fn get(&self, path: &Path) -> Option<Module> { |
||||
self.module_map.borrow().get(path).cloned() |
||||
} |
||||
} |
||||
|
||||
impl ModuleLoader for SimpleModuleLoader { |
||||
fn load_imported_module( |
||||
&self, |
||||
_referrer: Referrer, |
||||
specifier: JsString, |
||||
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context<'_>)>, |
||||
context: &mut Context<'_>, |
||||
) { |
||||
let result = (|| { |
||||
let path = specifier |
||||
.to_std_string() |
||||
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; |
||||
let short_path = Path::new(&path); |
||||
let path = self.root.join(short_path); |
||||
let path = path.canonicalize().map_err(|err| { |
||||
JsNativeError::typ() |
||||
.with_message(format!( |
||||
"could not canonicalize path `{}`", |
||||
short_path.display() |
||||
)) |
||||
.with_cause(JsError::from_opaque(js_string!(err.to_string()).into())) |
||||
})?; |
||||
if let Some(module) = self.get(&path) { |
||||
return Ok(module); |
||||
} |
||||
let source = Source::from_filepath(&path).map_err(|err| { |
||||
JsNativeError::typ() |
||||
.with_message(format!("could not open file `{}`", short_path.display())) |
||||
.with_cause(JsError::from_opaque(js_string!(err.to_string()).into())) |
||||
})?; |
||||
let module = Module::parse(source, None, context).map_err(|err| { |
||||
JsNativeError::syntax() |
||||
.with_message(format!("could not parse module `{}`", short_path.display())) |
||||
.with_cause(err) |
||||
})?; |
||||
self.insert(path, module.clone()); |
||||
Ok(module) |
||||
})(); |
||||
|
||||
finish_load(result, context); |
||||
} |
||||
} |
@ -0,0 +1,383 @@
|
||||
use std::rc::Rc; |
||||
|
||||
use boa_ast::expression::Identifier; |
||||
use boa_gc::{Finalize, Gc, GcRefCell, Trace, WeakGc}; |
||||
use boa_interner::Sym; |
||||
use rustc_hash::FxHashSet; |
||||
|
||||
use crate::{ |
||||
builtins::promise::ResolvingFunctions, |
||||
bytecompiler::ByteCompiler, |
||||
environments::{CompileTimeEnvironment, EnvironmentStack}, |
||||
object::JsPromise, |
||||
vm::{ActiveRunnable, CallFrame, CodeBlock}, |
||||
Context, JsNativeError, JsResult, JsString, JsValue, Module, |
||||
}; |
||||
|
||||
use super::{BindingName, ModuleRepr, ResolveExportError, ResolvedBinding}; |
||||
|
||||
trait TraceableCallback: Trace { |
||||
fn call(&self, module: &SyntheticModule, context: &mut Context<'_>) -> JsResult<()>; |
||||
} |
||||
|
||||
#[derive(Trace, Finalize)] |
||||
struct Callback<F, T> |
||||
where |
||||
F: Fn(&SyntheticModule, &T, &mut Context<'_>) -> JsResult<()>, |
||||
T: Trace, |
||||
{ |
||||
// SAFETY: `SyntheticModuleInitializer`'s safe API ensures only `Copy` closures are stored; its unsafe API,
|
||||
// on the other hand, explains the invariants to hold in order for this to be safe, shifting
|
||||
// the responsibility to the caller.
|
||||
#[unsafe_ignore_trace] |
||||
f: F, |
||||
captures: T, |
||||
} |
||||
|
||||
impl<F, T> TraceableCallback for Callback<F, T> |
||||
where |
||||
F: Fn(&SyntheticModule, &T, &mut Context<'_>) -> JsResult<()>, |
||||
T: Trace, |
||||
{ |
||||
fn call(&self, module: &SyntheticModule, context: &mut Context<'_>) -> JsResult<()> { |
||||
(self.f)(module, &self.captures, context) |
||||
} |
||||
} |
||||
|
||||
/// The initializing steps of a [`SyntheticModule`].
|
||||
///
|
||||
/// # Caveats
|
||||
///
|
||||
/// By limitations of the Rust language, the garbage collector currently cannot inspect closures
|
||||
/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
|
||||
/// to use. All other closures can also be stored in a `NativeFunction`, albeit by using an `unsafe`
|
||||
/// API, but note that passing closures implicitly capturing traceable types could cause
|
||||
/// **Undefined Behaviour**.
|
||||
#[derive(Clone, Trace, Finalize)] |
||||
pub struct SyntheticModuleInitializer { |
||||
inner: Gc<dyn TraceableCallback>, |
||||
} |
||||
|
||||
impl std::fmt::Debug for SyntheticModuleInitializer { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.debug_struct("ModuleInitializer").finish_non_exhaustive() |
||||
} |
||||
} |
||||
|
||||
impl SyntheticModuleInitializer { |
||||
/// Creates a `SyntheticModuleInitializer` from a [`Copy`] closure.
|
||||
pub fn from_copy_closure<F>(closure: F) -> Self |
||||
where |
||||
F: Fn(&SyntheticModule, &mut Context<'_>) -> JsResult<()> + Copy + 'static, |
||||
{ |
||||
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
|
||||
unsafe { Self::from_closure(closure) } |
||||
} |
||||
|
||||
/// Creates a `SyntheticModuleInitializer` from a [`Copy`] closure and a list of traceable captures.
|
||||
pub fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self |
||||
where |
||||
F: Fn(&SyntheticModule, &T, &mut Context<'_>) -> JsResult<()> + Copy + 'static, |
||||
T: Trace + 'static, |
||||
{ |
||||
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
|
||||
unsafe { Self::from_closure_with_captures(closure, captures) } |
||||
} |
||||
|
||||
/// Creates a new `SyntheticModuleInitializer` from a closure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
|
||||
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
|
||||
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
|
||||
/// on why that is the case.
|
||||
pub unsafe fn from_closure<F>(closure: F) -> Self |
||||
where |
||||
F: Fn(&SyntheticModule, &mut Context<'_>) -> JsResult<()> + 'static, |
||||
{ |
||||
// SAFETY: The caller must ensure the invariants of the closure hold.
|
||||
unsafe { |
||||
Self::from_closure_with_captures(move |module, _, context| closure(module, context), ()) |
||||
} |
||||
} |
||||
|
||||
/// Create a new `SyntheticModuleInitializer` from a closure and a list of traceable captures.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
|
||||
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
|
||||
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
|
||||
/// on why that is the case.
|
||||
pub unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self |
||||
where |
||||
F: Fn(&SyntheticModule, &T, &mut Context<'_>) -> JsResult<()> + 'static, |
||||
T: Trace + 'static, |
||||
{ |
||||
// Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the
|
||||
// future: https://github.com/rust-lang/rust/issues/18598
|
||||
let ptr = Gc::into_raw(Gc::new(Callback { |
||||
f: closure, |
||||
captures, |
||||
})); |
||||
|
||||
// SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object,
|
||||
// meaning this is safe.
|
||||
unsafe { |
||||
Self { |
||||
inner: Gc::from_raw(ptr), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Calls this `SyntheticModuleInitializer`, forwarding the arguments to the corresponding function.
|
||||
#[inline] |
||||
pub fn call(&self, module: &SyntheticModule, context: &mut Context<'_>) -> JsResult<()> { |
||||
self.inner.call(module, context) |
||||
} |
||||
} |
||||
|
||||
/// ECMAScript's [**Synthetic Module Records**][spec].
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-synthetic-module-records
|
||||
#[derive(Clone, Trace, Finalize)] |
||||
pub struct SyntheticModule { |
||||
inner: Gc<Inner>, |
||||
} |
||||
|
||||
impl std::fmt::Debug for SyntheticModule { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.debug_struct("SyntheticModule") |
||||
.field("export_names", &self.inner.export_names) |
||||
.field("eval_steps", &self.inner.eval_steps) |
||||
.finish_non_exhaustive() |
||||
} |
||||
} |
||||
|
||||
#[derive(Trace, Finalize)] |
||||
struct Inner { |
||||
parent: WeakGc<ModuleRepr>, |
||||
#[unsafe_ignore_trace] |
||||
export_names: FxHashSet<Sym>, |
||||
eval_context: GcRefCell<Option<(EnvironmentStack, Gc<CodeBlock>)>>, |
||||
eval_steps: SyntheticModuleInitializer, |
||||
} |
||||
|
||||
impl SyntheticModule { |
||||
/// Gets the parent module of this source module.
|
||||
fn parent(&self) -> Module { |
||||
Module { |
||||
inner: self |
||||
.inner |
||||
.parent |
||||
.upgrade() |
||||
.expect("parent module must be live"), |
||||
} |
||||
} |
||||
|
||||
/// Creates a new synthetic module.
|
||||
pub(super) fn new( |
||||
names: FxHashSet<Sym>, |
||||
eval_steps: SyntheticModuleInitializer, |
||||
parent: WeakGc<ModuleRepr>, |
||||
) -> Self { |
||||
Self { |
||||
inner: Gc::new(Inner { |
||||
parent, |
||||
export_names: names, |
||||
eval_steps, |
||||
eval_context: GcRefCell::default(), |
||||
}), |
||||
} |
||||
} |
||||
|
||||
/// Concrete method [`LoadRequestedModules ( )`][spec].
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-LoadRequestedModules
|
||||
pub(super) fn load(context: &mut Context<'_>) -> JsPromise { |
||||
// 1. Return ! PromiseResolve(%Promise%, undefined).
|
||||
JsPromise::resolve(JsValue::undefined(), context) |
||||
.expect("creating a promise from the %Promise% constructor must not fail") |
||||
} |
||||
|
||||
/// Concrete method [`GetExportedNames ( [ exportStarSet ] )`][spec].
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-getexportednames
|
||||
pub(super) fn get_exported_names(&self) -> FxHashSet<Sym> { |
||||
// 1. Return module.[[ExportNames]].
|
||||
self.inner.export_names.clone() |
||||
} |
||||
|
||||
/// Concrete method [`ResolveExport ( exportName )`][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-resolveexport
|
||||
#[allow(clippy::mutable_key_type)] |
||||
pub(super) fn resolve_export( |
||||
&self, |
||||
export_name: Sym, |
||||
) -> Result<ResolvedBinding, ResolveExportError> { |
||||
if self.inner.export_names.contains(&export_name) { |
||||
// 2. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: exportName }.
|
||||
Ok(ResolvedBinding { |
||||
module: self.parent(), |
||||
binding_name: BindingName::Name(Identifier::new(export_name)), |
||||
}) |
||||
} else { |
||||
// 1. If module.[[ExportNames]] does not contain exportName, return null.
|
||||
Err(ResolveExportError::NotFound) |
||||
} |
||||
} |
||||
|
||||
/// Concrete method [`Link ( )`][spec].
|
||||
///
|
||||
/// [spec]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
|
||||
pub(super) fn link(&self, context: &mut Context<'_>) { |
||||
let parent = self.parent(); |
||||
// 1. Let realm be module.[[Realm]].
|
||||
// 2. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]).
|
||||
// 3. Set module.[[Environment]] to env.
|
||||
let global_env = parent.realm().environment().clone(); |
||||
let global_compile_env = global_env.compile_env(); |
||||
let module_compile_env = Rc::new(CompileTimeEnvironment::new(global_compile_env, true)); |
||||
|
||||
// TODO: A bit of a hack to be able to pass the currently active runnable without an
|
||||
// available codeblock to execute.
|
||||
let compiler = ByteCompiler::new( |
||||
Sym::MAIN, |
||||
true, |
||||
false, |
||||
module_compile_env.clone(), |
||||
module_compile_env.clone(), |
||||
context, |
||||
); |
||||
|
||||
// 4. For each String exportName in module.[[ExportNames]], do
|
||||
let exports = self |
||||
.inner |
||||
.export_names |
||||
.iter() |
||||
.map(|name| { |
||||
let ident = Identifier::new(*name); |
||||
// a. Perform ! env.CreateMutableBinding(exportName, false).
|
||||
module_compile_env.create_mutable_binding(ident, false) |
||||
}) |
||||
.collect::<Vec<_>>(); |
||||
|
||||
let cb = Gc::new(compiler.finish()); |
||||
|
||||
let mut envs = EnvironmentStack::new(global_env); |
||||
envs.push_module(module_compile_env); |
||||
|
||||
for locator in exports { |
||||
// b. Perform ! env.InitializeBinding(exportName, undefined).
|
||||
envs.put_lexical_value( |
||||
locator.environment_index(), |
||||
locator.binding_index(), |
||||
JsValue::undefined(), |
||||
); |
||||
} |
||||
|
||||
*parent.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); |
||||
|
||||
*self.inner.eval_context.borrow_mut() = Some((envs, cb)); |
||||
|
||||
// 5. Return unused.
|
||||
} |
||||
|
||||
/// Concrete method [`Evaluate ( )`][spec].
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-Evaluate
|
||||
pub(super) fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { |
||||
// 1. Let moduleContext be a new ECMAScript code execution context.
|
||||
|
||||
let parent = self.parent(); |
||||
let mut realm = parent.realm().clone(); |
||||
let (mut environments, codeblock) = self |
||||
.inner |
||||
.eval_context |
||||
.borrow() |
||||
.clone() |
||||
.expect("should have been initialized on `link`"); |
||||
|
||||
let env_fp = environments.len() as u32; |
||||
let callframe = CallFrame::new( |
||||
codeblock, |
||||
// 4. Set the ScriptOrModule of moduleContext to module.
|
||||
Some(ActiveRunnable::Module(parent)), |
||||
// 2. Set the Function of moduleContext to null.
|
||||
None, |
||||
) |
||||
.with_env_fp(env_fp); |
||||
|
||||
// 5. Set the VariableEnvironment of moduleContext to module.[[Environment]].
|
||||
// 6. Set the LexicalEnvironment of moduleContext to module.[[Environment]].
|
||||
std::mem::swap(&mut context.vm.environments, &mut environments); |
||||
// 3. Set the Realm of moduleContext to module.[[Realm]].
|
||||
context.swap_realm(&mut realm); |
||||
// 7. Suspend the currently running execution context.
|
||||
// 8. Push moduleContext on to the execution context stack; moduleContext is now the running execution context.
|
||||
context.vm.push_frame(callframe); |
||||
|
||||
// 9. Let steps be module.[[EvaluationSteps]].
|
||||
// 10. Let result be Completion(steps(module)).
|
||||
let result = self.inner.eval_steps.call(self, context); |
||||
|
||||
// 11. Suspend moduleContext and remove it from the execution context stack.
|
||||
// 12. Resume the context that is now on the top of the execution context stack as the running execution context.
|
||||
std::mem::swap(&mut context.vm.environments, &mut environments); |
||||
context.swap_realm(&mut realm); |
||||
context.vm.pop_frame(); |
||||
|
||||
// 13. Let pc be ! NewPromiseCapability(%Promise%).
|
||||
let (promise, ResolvingFunctions { resolve, reject }) = JsPromise::new_pending(context); |
||||
|
||||
match result { |
||||
// 15. Perform ! pc.[[Resolve]](result).
|
||||
Ok(()) => resolve.call(&JsValue::undefined(), &[], context), |
||||
// 14. IfAbruptRejectPromise(result, pc).
|
||||
Err(err) => reject.call(&JsValue::undefined(), &[err.to_opaque(context)], context), |
||||
} |
||||
.expect("default resolving functions cannot throw"); |
||||
|
||||
// 16. Return pc.[[Promise]].
|
||||
promise |
||||
} |
||||
|
||||
/// Abstract operation [`SetSyntheticModuleExport ( module, exportName, exportValue )`][spec].
|
||||
///
|
||||
/// Sets or changes the exported value for `exportName` in the synthetic module.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The default export corresponds to the name `"default"`, but note that it needs to
|
||||
/// be passed to the list of exported names in [`Module::synthetic`] beforehand.
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-json-modules/#sec-createsyntheticmodule
|
||||
pub fn set_export( |
||||
&self, |
||||
export_name: &JsString, |
||||
export_value: JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<()> { |
||||
let identifier = context.interner_mut().get_or_intern(&**export_name); |
||||
let identifier = Identifier::new(identifier); |
||||
|
||||
let environment = self |
||||
.parent() |
||||
.environment() |
||||
.expect("this must be initialized before evaluating"); |
||||
let locator = environment |
||||
.compile_env() |
||||
.get_binding(identifier) |
||||
.ok_or_else(|| { |
||||
JsNativeError::reference().with_message(format!( |
||||
"cannot set name `{}` which was not included in the list of exports", |
||||
export_name.to_std_string_escaped() |
||||
)) |
||||
})?; |
||||
environment.set(locator.binding_index(), export_value); |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
@ -0,0 +1,180 @@
|
||||
// This example implements a synthetic Rust module that is exposed to JS code.
|
||||
// This mirrors the `modules.rs` example but uses synthetic modules instead.
|
||||
|
||||
use std::path::PathBuf; |
||||
use std::{error::Error, path::Path}; |
||||
|
||||
use boa_engine::builtins::promise::PromiseState; |
||||
use boa_engine::module::{ModuleLoader, SimpleModuleLoader, SyntheticModuleInitializer}; |
||||
use boa_engine::object::FunctionObjectBuilder; |
||||
use boa_engine::{ |
||||
js_string, Context, JsArgs, JsError, JsNativeError, JsValue, Module, NativeFunction, Source, |
||||
}; |
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> { |
||||
// A simple module that we want to compile from Rust code.
|
||||
const MODULE_SRC: &str = r#" |
||||
import { pyth } from "./trig.mjs"; |
||||
import * as ops from "./operations.mjs"; |
||||
|
||||
export let result = pyth(3, 4); |
||||
export function mix(a, b) { |
||||
return ops.sum(ops.mult(a, ops.sub(b, a)), 10); |
||||
} |
||||
"#; |
||||
|
||||
// This can be overriden with any custom implementation of `ModuleLoader`.
|
||||
let loader = &SimpleModuleLoader::new("./scripts/modules")?; |
||||
let dyn_loader: &dyn ModuleLoader = loader; |
||||
|
||||
// Just need to cast to a `ModuleLoader` before passing it to the builder.
|
||||
let context = &mut Context::builder().module_loader(dyn_loader).build()?; |
||||
|
||||
// Now, create the synthetic module and insert it into the loader.
|
||||
let operations = create_operations_module(context); |
||||
loader.insert( |
||||
PathBuf::from("./scripts/modules") |
||||
.canonicalize()? |
||||
.join("operations.mjs"), |
||||
operations, |
||||
); |
||||
|
||||
let source = Source::from_reader(MODULE_SRC.as_bytes(), Some(Path::new("./main.mjs"))); |
||||
|
||||
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
|
||||
let module = Module::parse(source, None, context)?; |
||||
|
||||
// Don't forget to insert the parsed module into the loader itself, since the root module
|
||||
// is not automatically inserted by the `ModuleLoader::load_imported_module` impl.
|
||||
//
|
||||
// Simulate as if the "fake" module is located in the modules root, just to ensure that
|
||||
// the loader won't double load in case someone tries to import "./main.mjs".
|
||||
loader.insert( |
||||
Path::new("./scripts/modules") |
||||
.canonicalize()? |
||||
.join("main.mjs"), |
||||
module.clone(), |
||||
); |
||||
|
||||
// This uses the utility function to load, link and evaluate a module without having to deal
|
||||
// with callbacks. For an example demonstrating the whole lifecycle of a module, see
|
||||
// `modules.rs`
|
||||
let promise_result = module.load_link_evaluate(context)?; |
||||
|
||||
// Very important to push forward the job queue after queueing promises.
|
||||
context.run_jobs(); |
||||
|
||||
// Checking if the final promise didn't return an error.
|
||||
match promise_result.state()? { |
||||
PromiseState::Pending => return Err("module didn't execute!".into()), |
||||
PromiseState::Fulfilled(v) => { |
||||
assert_eq!(v, JsValue::undefined()) |
||||
} |
||||
PromiseState::Rejected(err) => { |
||||
return Err(JsError::from_opaque(err).try_native(context)?.into()) |
||||
} |
||||
} |
||||
|
||||
// We can access the full namespace of the module with all its exports.
|
||||
let namespace = module.namespace(context); |
||||
let result = namespace.get(js_string!("result"), context)?; |
||||
|
||||
println!("result = {}", result.display()); |
||||
|
||||
assert_eq!( |
||||
namespace.get(js_string!("result"), context)?, |
||||
JsValue::from(5) |
||||
); |
||||
|
||||
let mix = namespace |
||||
.get(js_string!("mix"), context)? |
||||
.as_callable() |
||||
.cloned() |
||||
.ok_or_else(|| JsNativeError::typ().with_message("mix export wasn't a function!"))?; |
||||
let result = mix.call(&JsValue::undefined(), &[5.into(), 10.into()], context)?; |
||||
|
||||
println!("mix(5, 10) = {}", result.display()); |
||||
|
||||
assert_eq!(result, 35.into()); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
// Creates the synthetic equivalent to the `./modules/operations.mjs` file.
|
||||
fn create_operations_module(context: &mut Context<'_>) -> Module { |
||||
// We first create the function objects that will be exported by the module. More
|
||||
// on that below.
|
||||
let sum = FunctionObjectBuilder::new( |
||||
context.realm(), |
||||
NativeFunction::from_fn_ptr(|_, args, ctx| { |
||||
args.get_or_undefined(0).add(args.get_or_undefined(1), ctx) |
||||
}), |
||||
) |
||||
.length(2) |
||||
.name(js_string!("sum")) |
||||
.build(); |
||||
let sub = FunctionObjectBuilder::new( |
||||
context.realm(), |
||||
NativeFunction::from_fn_ptr(|_, args, ctx| { |
||||
args.get_or_undefined(0).sub(args.get_or_undefined(1), ctx) |
||||
}), |
||||
) |
||||
.length(2) |
||||
.name(js_string!("sub")) |
||||
.build(); |
||||
let mult = FunctionObjectBuilder::new( |
||||
context.realm(), |
||||
NativeFunction::from_fn_ptr(|_, args, ctx| { |
||||
args.get_or_undefined(0).mul(args.get_or_undefined(1), ctx) |
||||
}), |
||||
) |
||||
.length(2) |
||||
.name(js_string!("mult")) |
||||
.build(); |
||||
let div = FunctionObjectBuilder::new( |
||||
context.realm(), |
||||
NativeFunction::from_fn_ptr(|_, args, ctx| { |
||||
args.get_or_undefined(0).div(args.get_or_undefined(1), ctx) |
||||
}), |
||||
) |
||||
.length(2) |
||||
.name(js_string!("div")) |
||||
.build(); |
||||
let sqrt = FunctionObjectBuilder::new( |
||||
context.realm(), |
||||
NativeFunction::from_fn_ptr(|_, args, ctx| { |
||||
let a = args.get_or_undefined(0).to_number(ctx)?; |
||||
Ok(JsValue::from(a.sqrt())) |
||||
}), |
||||
) |
||||
.length(1) |
||||
.name(js_string!("sqrt")) |
||||
.build(); |
||||
|
||||
Module::synthetic( |
||||
// Make sure to list all exports beforehand.
|
||||
&[ |
||||
js_string!("sum"), |
||||
js_string!("sub"), |
||||
js_string!("mult"), |
||||
js_string!("div"), |
||||
js_string!("sqrt"), |
||||
], |
||||
// The initializer is evaluated every time a module imports this synthetic module,
|
||||
// so we avoid creating duplicate objects by capturing and cloning them instead.
|
||||
SyntheticModuleInitializer::from_copy_closure_with_captures( |
||||
|module, fns, context| { |
||||
println!("Running initializer!"); |
||||
module.set_export(&js_string!("sum"), fns.0.clone().into(), context)?; |
||||
module.set_export(&js_string!("sub"), fns.1.clone().into(), context)?; |
||||
module.set_export(&js_string!("mult"), fns.2.clone().into(), context)?; |
||||
module.set_export(&js_string!("div"), fns.3.clone().into(), context)?; |
||||
module.set_export(&js_string!("sqrt"), fns.4.clone().into(), context)?; |
||||
Ok(()) |
||||
}, |
||||
(sum, sub, mult, div, sqrt), |
||||
), |
||||
None, |
||||
context, |
||||
) |
||||
} |
Loading…
Reference in new issue