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