Browse Source

Implement synthetic modules (#3294)

* Implement synthetic modules

* Add example

* Fix example
pull/3386/head
José Julián Espina 7 months ago committed by GitHub
parent
commit
3177540979
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      boa_engine/src/lib.rs
  2. 223
      boa_engine/src/module/loader.rs
  3. 306
      boa_engine/src/module/mod.rs
  4. 28
      boa_engine/src/module/source.rs
  5. 383
      boa_engine/src/module/synthetic.rs
  6. 2
      boa_examples/src/bin/modules.rs
  7. 180
      boa_examples/src/bin/synthetic.rs

3
boa_engine/src/lib.rs

@ -199,8 +199,7 @@ pub trait JsArgs {
/// `args.get(n).cloned().unwrap_or_default()` or
/// `args.get(n).unwrap_or(&undefined)`.
///
/// This returns a reference for efficiency, in case you only need to call methods of `JsValue`,
/// so try to minimize calling `clone`.
/// This returns a reference for efficiency, in case you only need to call methods of `JsValue`.
fn get_or_undefined(&self, index: usize) -> &JsValue;
}

223
boa_engine/src/module/loader.rs

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

306
boa_engine/src/module/mod.rs

@ -21,18 +21,21 @@
//! [spec]: https://tc39.es/ecma262/#sec-modules
//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records
mod loader;
mod source;
mod synthetic;
pub use loader::*;
use source::SourceTextModule;
pub use synthetic::{SyntheticModule, SyntheticModuleInitializer};
use std::cell::{Cell, RefCell};
use std::hash::Hash;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{collections::HashSet, hash::BuildHasherDefault};
use indexmap::IndexMap;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use rustc_hash::{FxHashSet, FxHasher};
use boa_ast::expression::Identifier;
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
@ -40,233 +43,21 @@ use boa_interner::Sym;
use boa_parser::{Parser, Source};
use boa_profiler::Profiler;
use crate::object::FunctionObjectBuilder;
use crate::script::Script;
use crate::vm::ActiveRunnable;
use crate::{
builtins::promise::{PromiseCapability, PromiseState},
environments::DeclarativeEnvironment,
object::{JsObject, JsPromise, ObjectData},
js_string,
object::{FunctionObjectBuilder, JsObject, JsPromise, ObjectData},
realm::Realm,
Context, JsError, JsResult, JsString, JsValue,
Context, HostDefined, JsError, JsResult, JsString, JsValue, NativeFunction,
};
use crate::{js_string, HostDefined, JsNativeError, NativeFunction};
/// 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);
}
}
/// ECMAScript's [**Abstract module record**][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records
#[derive(Clone, Trace, Finalize)]
pub struct Module {
inner: Gc<Inner>,
inner: Gc<ModuleRepr>,
}
impl std::fmt::Debug for Module {
@ -281,7 +72,7 @@ impl std::fmt::Debug for Module {
}
#[derive(Trace, Finalize)]
struct Inner {
struct ModuleRepr {
realm: Realm,
environment: GcRefCell<Option<Gc<DeclarativeEnvironment>>>,
namespace: GcRefCell<Option<JsObject>>,
@ -295,8 +86,7 @@ pub(crate) enum ModuleKind {
/// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records)
SourceText(SourceTextModule),
/// A [**Synthetic Module Record**](https://tc39.es/proposal-json-modules/#sec-synthetic-module-records)
#[allow(unused)]
Synthetic,
Synthetic(SyntheticModule),
}
/// Return value of the [`Module::resolve_export`] operation.
@ -352,7 +142,6 @@ impl Module {
/// Parses the provided `src` as an ECMAScript module, returning an error if parsing fails.
///
/// [spec]: https://tc39.es/ecma262/#sec-parsemodule
#[inline]
pub fn parse<R: Read>(
src: Source<'_, R>,
realm: Option<Realm>,
@ -363,21 +152,52 @@ impl Module {
parser.set_identifier(context.next_parser_identifier());
let module = parser.parse_module(context.interner_mut())?;
let src = SourceTextModule::new(module);
let inner = Gc::new_cyclic(|weak| {
let src = SourceTextModule::new(module, weak.clone());
let module = Self {
inner: Gc::new(Inner {
ModuleRepr {
realm: realm.unwrap_or_else(|| context.realm().clone()),
environment: GcRefCell::default(),
namespace: GcRefCell::default(),
kind: ModuleKind::SourceText(src.clone()),
kind: ModuleKind::SourceText(src),
host_defined: HostDefined::default(),
}),
};
}
});
src.set_parent(module.clone());
Ok(Self { inner })
}
Ok(module)
/// Abstract operation [`CreateSyntheticModule ( exportNames, evaluationSteps, realm )`][spec].
///
/// Creates a new Synthetic Module from its list of exported names, its evaluation steps and
/// optionally a root realm.
///
/// [spec]: https://tc39.es/proposal-json-modules/#sec-createsyntheticmodule
#[inline]
pub fn synthetic(
export_names: &[JsString],
evaluation_steps: SyntheticModuleInitializer,
realm: Option<Realm>,
context: &mut Context<'_>,
) -> Self {
let names: FxHashSet<Sym> = export_names
.iter()
.map(|string| context.interner_mut().get_or_intern(&**string))
.collect();
let realm = realm.unwrap_or_else(|| context.realm().clone());
let inner = Gc::new_cyclic(|weak| {
let synth = SyntheticModule::new(names, evaluation_steps, weak.clone());
ModuleRepr {
realm,
environment: GcRefCell::default(),
namespace: GcRefCell::default(),
kind: ModuleKind::Synthetic(synth),
host_defined: HostDefined::default(),
}
});
Self { inner }
}
/// Gets the realm of this `Module`.
@ -420,7 +240,7 @@ impl Module {
// Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
//
// [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules
// TODO: 1. If hostDefined is not present, let hostDefined be empty.
// 1. If hostDefined is not present, let hostDefined be empty.
// 2. Let pc be ! NewPromiseCapability(%Promise%).
let pc = PromiseCapability::new(
@ -450,7 +270,7 @@ impl Module {
JsPromise::from_object(pc.promise().clone())
.expect("promise created from the %Promise% intrinsic is always native")
}
ModuleKind::Synthetic => todo!("synthetic.load()"),
ModuleKind::Synthetic(_) => SyntheticModule::load(context),
}
}
@ -505,7 +325,7 @@ impl Module {
fn get_exported_names(&self, export_star_set: &mut Vec<SourceTextModule>) -> FxHashSet<Sym> {
match self.kind() {
ModuleKind::SourceText(src) => src.get_exported_names(export_star_set),
ModuleKind::Synthetic => todo!("synthetic.get_exported_names()"),
ModuleKind::Synthetic(synth) => synth.get_exported_names(),
}
}
@ -528,7 +348,7 @@ impl Module {
) -> Result<ResolvedBinding, ResolveExportError> {
match self.kind() {
ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set),
ModuleKind::Synthetic => todo!("synthetic.resolve_export()"),
ModuleKind::Synthetic(synth) => synth.resolve_export(export_name),
}
}
@ -547,7 +367,10 @@ impl Module {
pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> {
match self.kind() {
ModuleKind::SourceText(src) => src.link(context),
ModuleKind::Synthetic => todo!("synthetic.link()"),
ModuleKind::Synthetic(synth) => {
synth.link(context);
Ok(())
}
}
}
@ -562,11 +385,10 @@ impl Module {
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_link(stack, index, context),
#[allow(unreachable_code)]
// If module is not a Cyclic Module Record, then
ModuleKind::Synthetic => {
ModuleKind::Synthetic(synth) => {
// a. Perform ? module.Link().
todo!("synthetic.link()");
synth.link(context);
// b. Return index.
Ok(index)
}
@ -585,12 +407,11 @@ impl Module {
/// This must only be called if the [`Module::link`] method finished successfully.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(src) => src.evaluate(context),
ModuleKind::Synthetic => todo!("synthetic.evaluate()"),
ModuleKind::Synthetic(synth) => synth.evaluate(context),
}
}
@ -606,10 +427,9 @@ impl Module {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_evaluate(stack, index, None, context),
// 1. If module is not a Cyclic Module Record, then
#[allow(unused, clippy::diverging_sub_expression)]
ModuleKind::Synthetic => {
ModuleKind::Synthetic(synth) => {
// a. Let promise be ! module.Evaluate().
let promise: JsPromise = todo!("module.Evaluate()");
let promise: JsPromise = synth.evaluate(context);
let state = promise.state()?;
match state {
PromiseState::Pending => {

28
boa_engine/src/module/source.rs

@ -15,7 +15,7 @@ use boa_ast::{
ContainsSymbol, LexicallyScopedDeclaration,
},
};
use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace};
use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc};
use boa_interner::Sym;
use indexmap::IndexSet;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
@ -35,7 +35,8 @@ use crate::{
};
use super::{
BindingName, GraphLoadingState, Module, Referrer, ResolveExportError, ResolvedBinding,
BindingName, GraphLoadingState, Module, ModuleRepr, Referrer, ResolveExportError,
ResolvedBinding,
};
/// Information for the [**Depth-first search**] algorithm used in the
@ -270,7 +271,7 @@ impl std::fmt::Debug for SourceTextModule {
#[derive(Trace, Finalize)]
struct Inner {
parent: GcRefCell<Option<Module>>,
parent: WeakGc<ModuleRepr>,
status: GcRefCell<Status>,
loaded_modules: GcRefCell<FxHashMap<Sym, Module>>,
async_parent_modules: GcRefCell<Vec<SourceTextModule>>,
@ -291,18 +292,15 @@ struct ModuleCode {
}
impl SourceTextModule {
/// Sets the parent module of this source module.
pub(super) fn set_parent(&self, parent: Module) {
*self.inner.parent.borrow_mut() = Some(parent);
}
/// Gets the parent module of this source module.
fn parent(&self) -> Module {
self.inner
.parent
.borrow()
.clone()
.expect("parent module must be initialized")
Module {
inner: self
.inner
.parent
.upgrade()
.expect("parent module must be live"),
}
}
/// Creates a new `SourceTextModule` from a parsed `ModuleSource`.
@ -310,7 +308,7 @@ impl SourceTextModule {
/// Contains part of the abstract operation [`ParseModule`][parse].
///
/// [parse]: https://tc39.es/ecma262/#sec-parsemodule
pub(super) fn new(code: boa_ast::Module) -> Self {
pub(super) fn new(code: boa_ast::Module, parent: WeakGc<ModuleRepr>) -> Self {
// 3. Let requestedModules be the ModuleRequests of body.
let requested_modules = code.items().requests();
// 4. Let importEntries be ImportEntries of body.
@ -391,7 +389,7 @@ impl SourceTextModule {
// Most of this can be ignored, since `Status` takes care of the remaining state.
Self {
inner: Gc::new(Inner {
parent: GcRefCell::default(),
parent,
status: GcRefCell::default(),
loaded_modules: GcRefCell::default(),
async_parent_modules: GcRefCell::default(),

383
boa_engine/src/module/synthetic.rs

@ -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(())
}
}

2
boa_examples/src/bin/modules.rs

@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// 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
// 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

180
boa_examples/src/bin/synthetic.rs

@ -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…
Cancel
Save