mirror of https://github.com/boa-dev/boa.git
Browse Source
* Implement draft of module execution * Fix recursion bug * Re-enable JsObject's debug printing * Modify API and document some methods * Add missing documentation * Add newline to module scripts * npx prettier * Apply reviews * Add reference to parent struct on source module * Document more steps on the example * cargo clippy * Revert `BoaGc` changes * Lower `GcRefCell` to `Inner` of `SourceTextModule` * Replace weak ref to module with strong ref * Apply review * Clarify reasoning of manual `Trace` impl * Apply review pt. 2 * Revert gc changespull/2942/head
José Julián Espina
2 years ago
committed by
GitHub
37 changed files with 4239 additions and 264 deletions
@ -0,0 +1,120 @@ |
|||||||
|
use std::cell::Cell; |
||||||
|
|
||||||
|
use boa_ast::expression::Identifier; |
||||||
|
use boa_gc::{Finalize, GcRefCell, Trace}; |
||||||
|
|
||||||
|
use crate::{module::Module, JsValue}; |
||||||
|
|
||||||
|
/// Type of accessor used to access an indirect binding.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
enum BindingAccessor { |
||||||
|
Identifier(Identifier), |
||||||
|
Index(usize), |
||||||
|
} |
||||||
|
|
||||||
|
/// An indirect reference to a binding inside an environment.
|
||||||
|
#[derive(Clone, Debug, Trace, Finalize)] |
||||||
|
struct IndirectBinding { |
||||||
|
module: Module, |
||||||
|
#[unsafe_ignore_trace] |
||||||
|
accessor: Cell<BindingAccessor>, |
||||||
|
} |
||||||
|
|
||||||
|
/// The type of binding a [`ModuleEnvironment`] can contain.
|
||||||
|
#[derive(Clone, Debug, Trace, Finalize)] |
||||||
|
enum BindingType { |
||||||
|
Direct(Option<JsValue>), |
||||||
|
Indirect(IndirectBinding), |
||||||
|
} |
||||||
|
|
||||||
|
/// A [**Module Environment Record**][spec].
|
||||||
|
///
|
||||||
|
/// Module environments allow referencing bindings inside other environments, in addition
|
||||||
|
/// to the usual declarative environment functionality.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-environment-records
|
||||||
|
#[derive(Debug, Trace, Finalize)] |
||||||
|
pub(crate) struct ModuleEnvironment { |
||||||
|
bindings: GcRefCell<Vec<BindingType>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl ModuleEnvironment { |
||||||
|
/// Creates a new `LexicalEnvironment`.
|
||||||
|
pub(crate) fn new(bindings: usize) -> Self { |
||||||
|
Self { |
||||||
|
bindings: GcRefCell::new(vec![BindingType::Direct(None); bindings]), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Get the binding value from the environment by it's index.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the binding value is out of range or not initialized.
|
||||||
|
#[track_caller] |
||||||
|
pub(crate) fn get(&self, index: usize) -> Option<JsValue> { |
||||||
|
let bindings = self.bindings.borrow(); |
||||||
|
|
||||||
|
match &bindings[index] { |
||||||
|
BindingType::Direct(v) => v.clone(), |
||||||
|
BindingType::Indirect(IndirectBinding { module, accessor }) => { |
||||||
|
let env = module.environment()?; |
||||||
|
|
||||||
|
match accessor.get() { |
||||||
|
BindingAccessor::Identifier(name) => { |
||||||
|
let index = env |
||||||
|
.compile_env() |
||||||
|
.borrow() |
||||||
|
.get_binding(name) |
||||||
|
.expect("linking must ensure the binding exists"); |
||||||
|
|
||||||
|
let value = env.get(index.binding_index)?; |
||||||
|
|
||||||
|
accessor.set(BindingAccessor::Index(index.binding_index)); |
||||||
|
|
||||||
|
Some(value) |
||||||
|
} |
||||||
|
BindingAccessor::Index(index) => env.get(index), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Sets the binding value from the environment by index.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the binding value is out of range.
|
||||||
|
#[track_caller] |
||||||
|
pub(crate) fn set(&self, index: usize, value: JsValue) { |
||||||
|
let mut bindings = self.bindings.borrow_mut(); |
||||||
|
|
||||||
|
match &mut bindings[index] { |
||||||
|
BindingType::Direct(v) => *v = Some(value), |
||||||
|
BindingType::Indirect(_) => { |
||||||
|
panic!("cannot modify indirect references to other environments") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates an indirect binding reference to another environment binding.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the binding value is out of range.
|
||||||
|
#[track_caller] |
||||||
|
pub(crate) fn set_indirect( |
||||||
|
&self, |
||||||
|
index: usize, |
||||||
|
target_module: Module, |
||||||
|
target_binding: Identifier, |
||||||
|
) { |
||||||
|
let mut bindings = self.bindings.borrow_mut(); |
||||||
|
|
||||||
|
bindings[index] = BindingType::Indirect(IndirectBinding { |
||||||
|
module: target_module, |
||||||
|
accessor: Cell::new(BindingAccessor::Identifier(target_binding)), |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,737 @@ |
|||||||
|
//! Boa's implementation of the ECMAScript's module system.
|
||||||
|
//!
|
||||||
|
//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module],
|
||||||
|
//! a [`ModuleLoader`] trait for custom module loader implementations, and [`SimpleModuleLoader`],
|
||||||
|
//! the default `ModuleLoader` for [`Context`] which can be used for most simple usecases.
|
||||||
|
//!
|
||||||
|
//! Every module roughly follows the same lifecycle:
|
||||||
|
//! - Parse using [`Module::parse`].
|
||||||
|
//! - Load all its dependencies using [`Module::load`].
|
||||||
|
//! - Link its dependencies together using [`Module::link`].
|
||||||
|
//! - Evaluate the module and its dependencies using [`Module::evaluate`].
|
||||||
|
//!
|
||||||
|
//! The [`ModuleLoader`] trait allows customizing the "load" step on the lifecycle
|
||||||
|
//! of a module, which allows doing things like fetching modules from urls, having multiple
|
||||||
|
//! "modpaths" from where to import modules, or using Rust futures to avoid blocking the main thread
|
||||||
|
//! on loads.
|
||||||
|
//!
|
||||||
|
//! More information:
|
||||||
|
//! - [ECMAScript reference][spec]
|
||||||
|
//!
|
||||||
|
//! [spec]: https://tc39.es/ecma262/#sec-modules
|
||||||
|
//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records
|
||||||
|
|
||||||
|
mod source; |
||||||
|
use source::SourceTextModule; |
||||||
|
|
||||||
|
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 boa_ast::expression::Identifier; |
||||||
|
use boa_gc::{Finalize, Gc, GcRefCell, Trace}; |
||||||
|
use boa_interner::Sym; |
||||||
|
use boa_parser::{Parser, Source}; |
||||||
|
use boa_profiler::Profiler; |
||||||
|
|
||||||
|
use crate::object::FunctionObjectBuilder; |
||||||
|
use crate::{ |
||||||
|
builtins::promise::{PromiseCapability, PromiseState}, |
||||||
|
environments::DeclarativeEnvironment, |
||||||
|
object::{JsObject, JsPromise, ObjectData}, |
||||||
|
realm::Realm, |
||||||
|
Context, JsError, JsResult, JsString, JsValue, |
||||||
|
}; |
||||||
|
use crate::{js_string, JsNativeError, NativeFunction}; |
||||||
|
|
||||||
|
/// The referrer from which a load request of a module originates.
|
||||||
|
#[derive(Debug)] |
||||||
|
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), // TODO: script
|
||||||
|
} |
||||||
|
|
||||||
|
/// 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<'_>, |
||||||
|
); |
||||||
|
|
||||||
|
/// 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 simple module loader that loads modules relative to a root path.
|
||||||
|
#[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> { |
||||||
|
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::error() |
||||||
|
.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>, |
||||||
|
} |
||||||
|
|
||||||
|
impl std::fmt::Debug for Module { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.debug_struct("Module") |
||||||
|
.field("realm", &self.inner.realm.addr()) |
||||||
|
.field("environment", &self.inner.environment) |
||||||
|
.field("namespace", &self.inner.namespace) |
||||||
|
.field("kind", &self.inner.kind) |
||||||
|
.field("host_defined", &self.inner.host_defined) |
||||||
|
.finish() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Trace, Finalize)] |
||||||
|
struct Inner { |
||||||
|
realm: Realm, |
||||||
|
environment: GcRefCell<Option<Gc<DeclarativeEnvironment>>>, |
||||||
|
namespace: GcRefCell<Option<JsObject>>, |
||||||
|
kind: ModuleKind, |
||||||
|
host_defined: (), |
||||||
|
} |
||||||
|
|
||||||
|
/// The kind of a [`Module`].
|
||||||
|
#[derive(Debug, Trace, Finalize)] |
||||||
|
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, |
||||||
|
} |
||||||
|
|
||||||
|
/// Return value of the [`Module::resolve_export`] operation.
|
||||||
|
///
|
||||||
|
/// Indicates how to access a specific export in a module.
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub(crate) struct ResolvedBinding { |
||||||
|
module: Module, |
||||||
|
binding_name: BindingName, |
||||||
|
} |
||||||
|
|
||||||
|
/// The local name of the resolved binding within its containing module.
|
||||||
|
///
|
||||||
|
/// Note that a resolved binding can resolve to a single binding inside a module (`export var a = 1"`)
|
||||||
|
/// or to a whole module namespace (`export * as ns from "mod.js"`).
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub(crate) enum BindingName { |
||||||
|
/// A local binding.
|
||||||
|
Name(Identifier), |
||||||
|
/// The whole namespace of the containing module.
|
||||||
|
Namespace, |
||||||
|
} |
||||||
|
|
||||||
|
impl ResolvedBinding { |
||||||
|
/// Gets the module from which the export resolved.
|
||||||
|
pub(crate) const fn module(&self) -> &Module { |
||||||
|
&self.module |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the binding associated with the resolved export.
|
||||||
|
pub(crate) const fn binding_name(&self) -> BindingName { |
||||||
|
self.binding_name |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
struct GraphLoadingState { |
||||||
|
capability: PromiseCapability, |
||||||
|
loading: Cell<bool>, |
||||||
|
pending_modules: Cell<usize>, |
||||||
|
visited: RefCell<HashSet<SourceTextModule>>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub(crate) enum ResolveExportError { |
||||||
|
NotFound, |
||||||
|
Ambiguous, |
||||||
|
} |
||||||
|
|
||||||
|
impl Module { |
||||||
|
/// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec].
|
||||||
|
///
|
||||||
|
/// 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>, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<Module> { |
||||||
|
let _timer = Profiler::global().start_event("Module parsing", "Main"); |
||||||
|
let mut parser = Parser::new(src); |
||||||
|
parser.set_identifier(context.next_parser_identifier()); |
||||||
|
let module = parser.parse_module(context.interner_mut())?; |
||||||
|
|
||||||
|
let src = SourceTextModule::new(module); |
||||||
|
|
||||||
|
let module = Module { |
||||||
|
inner: Gc::new(Inner { |
||||||
|
realm: realm.unwrap_or_else(|| context.realm().clone()), |
||||||
|
environment: GcRefCell::default(), |
||||||
|
namespace: GcRefCell::default(), |
||||||
|
kind: ModuleKind::SourceText(src.clone()), |
||||||
|
host_defined: (), |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
src.set_parent(module.clone()); |
||||||
|
|
||||||
|
Ok(module) |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the realm of this `Module`.
|
||||||
|
#[inline] |
||||||
|
pub fn realm(&self) -> &Realm { |
||||||
|
&self.inner.realm |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the kind of this `Module`.
|
||||||
|
pub(crate) fn kind(&self) -> &ModuleKind { |
||||||
|
&self.inner.kind |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the environment of this `Module`.
|
||||||
|
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> { |
||||||
|
self.inner.environment.borrow().clone() |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
|
||||||
|
///
|
||||||
|
/// Prepares the module for linking by loading all its module dependencies. Returns a `JsPromise`
|
||||||
|
/// that will resolve when the loading process either completes or fails.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
|
||||||
|
#[allow(clippy::missing_panics_doc)] |
||||||
|
#[inline] |
||||||
|
pub fn load(&self, context: &mut Context<'_>) -> JsPromise { |
||||||
|
match self.kind() { |
||||||
|
ModuleKind::SourceText(_) => { |
||||||
|
// Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
|
||||||
|
//
|
||||||
|
// [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules
|
||||||
|
// TODO: 1. If hostDefined is not present, let hostDefined be empty.
|
||||||
|
|
||||||
|
// 2. Let pc be ! NewPromiseCapability(%Promise%).
|
||||||
|
let pc = PromiseCapability::new( |
||||||
|
&context.intrinsics().constructors().promise().constructor(), |
||||||
|
context, |
||||||
|
) |
||||||
|
.expect( |
||||||
|
"capability creation must always succeed when using the `%Promise%` intrinsic", |
||||||
|
); |
||||||
|
|
||||||
|
// 4. Perform InnerModuleLoading(state, module).
|
||||||
|
self.inner_load( |
||||||
|
// 3. Let state be the GraphLoadingState Record {
|
||||||
|
// [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « »,
|
||||||
|
// [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined
|
||||||
|
// }.
|
||||||
|
&Rc::new(GraphLoadingState { |
||||||
|
capability: pc.clone(), |
||||||
|
loading: Cell::new(true), |
||||||
|
pending_modules: Cell::new(1), |
||||||
|
visited: RefCell::default(), |
||||||
|
}), |
||||||
|
context, |
||||||
|
); |
||||||
|
|
||||||
|
// 5. Return pc.[[Promise]].
|
||||||
|
JsPromise::from_object(pc.promise().clone()) |
||||||
|
.expect("promise created from the %Promise% intrinsic is always native") |
||||||
|
} |
||||||
|
ModuleKind::Synthetic => todo!("synthetic.load()"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`InnerModuleLoading`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading
|
||||||
|
fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context<'_>) { |
||||||
|
// 1. Assert: state.[[IsLoading]] is true.
|
||||||
|
assert!(state.loading.get()); |
||||||
|
|
||||||
|
if let ModuleKind::SourceText(src) = self.kind() { |
||||||
|
// continues on `inner_load
|
||||||
|
src.inner_load(state, context); |
||||||
|
if !state.loading.get() { |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 3. Assert: state.[[PendingModulesCount]] ≥ 1.
|
||||||
|
assert!(state.pending_modules.get() >= 1); |
||||||
|
|
||||||
|
// 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1.
|
||||||
|
state.pending_modules.set(state.pending_modules.get() - 1); |
||||||
|
// 5. If state.[[PendingModulesCount]] = 0, then
|
||||||
|
|
||||||
|
if state.pending_modules.get() == 0 { |
||||||
|
// a. Set state.[[IsLoading]] to false.
|
||||||
|
state.loading.set(false); |
||||||
|
// b. For each Cyclic Module Record loaded of state.[[Visited]], do
|
||||||
|
// i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked.
|
||||||
|
// By default, all modules start on `unlinked`.
|
||||||
|
|
||||||
|
// c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »).
|
||||||
|
state |
||||||
|
.capability |
||||||
|
.resolve() |
||||||
|
.call(&JsValue::undefined(), &[], context) |
||||||
|
.expect("marking a module as loaded should not fail"); |
||||||
|
} |
||||||
|
// 6. Return unused.
|
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract method [`GetExportedNames([exportStarSet])`][spec].
|
||||||
|
///
|
||||||
|
/// Returns a list of all the names exported from this module.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
|
||||||
|
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()"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract method [`ResolveExport(exportName [, resolveSet])`][spec].
|
||||||
|
///
|
||||||
|
/// Returns the corresponding local binding of a binding exported by this module.
|
||||||
|
/// The spec requires that this operation must be idempotent; calling this multiple times
|
||||||
|
/// with the same `export_name` and `resolve_set` should always return the same result.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
|
||||||
|
#[allow(clippy::mutable_key_type)] |
||||||
|
pub(crate) fn resolve_export( |
||||||
|
&self, |
||||||
|
export_name: Sym, |
||||||
|
resolve_set: &mut FxHashSet<(Module, Sym)>, |
||||||
|
) -> Result<ResolvedBinding, ResolveExportError> { |
||||||
|
match self.kind() { |
||||||
|
ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set), |
||||||
|
ModuleKind::Synthetic => todo!("synthetic.resolve_export()"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract method [`Link() `][spec].
|
||||||
|
///
|
||||||
|
/// Prepares this module for evaluation by resolving all its module dependencies and initializing
|
||||||
|
/// its environment.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
|
||||||
|
#[allow(clippy::missing_panics_doc)] |
||||||
|
#[inline] |
||||||
|
pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { |
||||||
|
match self.kind() { |
||||||
|
ModuleKind::SourceText(src) => src.link(context), |
||||||
|
ModuleKind::Synthetic => todo!("synthetic.link()"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
|
||||||
|
fn inner_link( |
||||||
|
&self, |
||||||
|
stack: &mut Vec<SourceTextModule>, |
||||||
|
index: usize, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> 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 => { |
||||||
|
// a. Perform ? module.Link().
|
||||||
|
todo!("synthetic.link()"); |
||||||
|
// b. Return index.
|
||||||
|
Ok(index) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract method [`Evaluate()`][spec].
|
||||||
|
///
|
||||||
|
/// Evaluates this module, returning a promise for the result of the evaluation of this module
|
||||||
|
/// and its dependencies.
|
||||||
|
/// If the promise is rejected, hosts are expected to handle the promise rejection and rethrow
|
||||||
|
/// the evaluation error.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// 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()"), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
|
||||||
|
fn inner_evaluate( |
||||||
|
&self, |
||||||
|
stack: &mut Vec<SourceTextModule>, |
||||||
|
index: usize, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<usize> { |
||||||
|
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 => { |
||||||
|
// a. Let promise be ! module.Evaluate().
|
||||||
|
let promise: JsPromise = todo!("module.Evaluate()"); |
||||||
|
let state = promise.state()?; |
||||||
|
match state { |
||||||
|
PromiseState::Pending => { |
||||||
|
unreachable!("b. Assert: promise.[[PromiseState]] is not pending.") |
||||||
|
} |
||||||
|
// d. Return index.
|
||||||
|
PromiseState::Fulfilled(_) => Ok(index), |
||||||
|
// c. If promise.[[PromiseState]] is rejected, then
|
||||||
|
// i. Return ThrowCompletion(promise.[[PromiseResult]]).
|
||||||
|
PromiseState::Rejected(err) => Err(JsError::from_opaque(err)), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Loads, links and evaluates this module, returning a promise that will resolve after the module
|
||||||
|
/// finishes its lifecycle.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use std::path::Path;
|
||||||
|
/// # use boa_engine::{Context, Source, Module, JsValue};
|
||||||
|
/// # use boa_engine::builtins::promise::PromiseState;
|
||||||
|
/// # use boa_engine::module::{ModuleLoader, SimpleModuleLoader};
|
||||||
|
/// let loader = &SimpleModuleLoader::new(Path::new(".")).unwrap();
|
||||||
|
/// let dyn_loader: &dyn ModuleLoader = loader;
|
||||||
|
/// let mut context = &mut Context::builder().module_loader(dyn_loader).build().unwrap();
|
||||||
|
///
|
||||||
|
/// let source = Source::from_bytes("1 + 3");
|
||||||
|
///
|
||||||
|
/// let module = Module::parse(source, None, context).unwrap();
|
||||||
|
///
|
||||||
|
/// loader.insert(Path::new("main.mjs").to_path_buf(), module.clone());
|
||||||
|
///
|
||||||
|
/// let promise = module.load_link_evaluate(context).unwrap();
|
||||||
|
///
|
||||||
|
/// context.run_jobs();
|
||||||
|
///
|
||||||
|
/// assert_eq!(promise.state().unwrap(), PromiseState::Fulfilled(JsValue::undefined()));
|
||||||
|
/// ```
|
||||||
|
#[allow(clippy::drop_copy)] |
||||||
|
#[inline] |
||||||
|
pub fn load_link_evaluate(&self, context: &mut Context<'_>) -> JsResult<JsPromise> { |
||||||
|
let main_timer = Profiler::global().start_event("Module evaluation", "Main"); |
||||||
|
|
||||||
|
let promise = self |
||||||
|
.load(context) |
||||||
|
.then( |
||||||
|
Some( |
||||||
|
FunctionObjectBuilder::new( |
||||||
|
context, |
||||||
|
NativeFunction::from_copy_closure_with_captures( |
||||||
|
|_, _, module, context| { |
||||||
|
module.link(context)?; |
||||||
|
Ok(JsValue::undefined()) |
||||||
|
}, |
||||||
|
self.clone(), |
||||||
|
), |
||||||
|
) |
||||||
|
.build(), |
||||||
|
), |
||||||
|
None, |
||||||
|
context, |
||||||
|
)? |
||||||
|
.then( |
||||||
|
Some( |
||||||
|
FunctionObjectBuilder::new( |
||||||
|
context, |
||||||
|
NativeFunction::from_copy_closure_with_captures( |
||||||
|
|_, _, module, context| Ok(module.evaluate(context).into()), |
||||||
|
self.clone(), |
||||||
|
), |
||||||
|
) |
||||||
|
.build(), |
||||||
|
), |
||||||
|
None, |
||||||
|
context, |
||||||
|
)?; |
||||||
|
|
||||||
|
// The main_timer needs to be dropped before the Profiler is.
|
||||||
|
drop(main_timer); |
||||||
|
Profiler::global().drop(); |
||||||
|
|
||||||
|
Ok(promise) |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`GetModuleNamespace ( module )`][spec].
|
||||||
|
///
|
||||||
|
/// Gets the [**Module Namespace Object**][ns] that represents this module's exports.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-getmodulenamespace
|
||||||
|
/// [ns]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
|
||||||
|
pub fn namespace(&self, context: &mut Context<'_>) -> JsObject { |
||||||
|
// 1. Assert: If module is a Cyclic Module Record, then module.[[Status]] is not new or unlinked.
|
||||||
|
// 2. Let namespace be module.[[Namespace]].
|
||||||
|
// 3. If namespace is empty, then
|
||||||
|
// 4. Return namespace.
|
||||||
|
self.inner |
||||||
|
.namespace |
||||||
|
.borrow_mut() |
||||||
|
.get_or_insert_with(|| { |
||||||
|
// a. Let exportedNames be module.GetExportedNames().
|
||||||
|
let exported_names = self.get_exported_names(&mut Vec::default()); |
||||||
|
|
||||||
|
// b. Let unambiguousNames be a new empty List.
|
||||||
|
let unambiguous_names = exported_names |
||||||
|
.into_iter() |
||||||
|
// c. For each element name of exportedNames, do
|
||||||
|
.filter_map(|name| { |
||||||
|
// i. Let resolution be module.ResolveExport(name).
|
||||||
|
// ii. If resolution is a ResolvedBinding Record, append name to unambiguousNames.
|
||||||
|
self.resolve_export(name, &mut HashSet::default()) |
||||||
|
.ok() |
||||||
|
.map(|_| name) |
||||||
|
}) |
||||||
|
.collect(); |
||||||
|
|
||||||
|
// d. Set namespace to ModuleNamespaceCreate(module, unambiguousNames).
|
||||||
|
ModuleNamespace::create(self.clone(), unambiguous_names, context) |
||||||
|
}) |
||||||
|
.clone() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl PartialEq for Module { |
||||||
|
#[inline] |
||||||
|
fn eq(&self, other: &Self) -> bool { |
||||||
|
std::ptr::eq(self.inner.as_ref(), other.inner.as_ref()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Eq for Module {} |
||||||
|
|
||||||
|
impl Hash for Module { |
||||||
|
#[inline] |
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
||||||
|
std::ptr::hash(self.inner.as_ref(), state); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Module namespace exotic object.
|
||||||
|
///
|
||||||
|
/// Exposes the bindings exported by a [`Module`] to be accessed from ECMAScript code.
|
||||||
|
#[derive(Debug, Trace, Finalize)] |
||||||
|
pub struct ModuleNamespace { |
||||||
|
module: Module, |
||||||
|
#[unsafe_ignore_trace] |
||||||
|
exports: IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl ModuleNamespace { |
||||||
|
/// Abstract operation [`ModuleNamespaceCreate ( module, exports )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-modulenamespacecreate
|
||||||
|
pub(crate) fn create(module: Module, names: Vec<Sym>, context: &mut Context<'_>) -> JsObject { |
||||||
|
// 1. Assert: module.[[Namespace]] is empty.
|
||||||
|
// ignored since this is ensured by `Module::namespace`.
|
||||||
|
|
||||||
|
// 6. Let sortedExports be a List whose elements are the elements of exports ordered as if an Array of the same values had been sorted using %Array.prototype.sort% using undefined as comparefn.
|
||||||
|
let mut exports = names |
||||||
|
.into_iter() |
||||||
|
.map(|sym| { |
||||||
|
( |
||||||
|
context |
||||||
|
.interner() |
||||||
|
.resolve_expect(sym) |
||||||
|
.into_common::<JsString>(false), |
||||||
|
sym, |
||||||
|
) |
||||||
|
}) |
||||||
|
.collect::<IndexMap<_, _, _>>(); |
||||||
|
exports.sort_keys(); |
||||||
|
|
||||||
|
// 2. Let internalSlotsList be the internal slots listed in Table 32.
|
||||||
|
// 3. Let M be MakeBasicObject(internalSlotsList).
|
||||||
|
// 4. Set M's essential internal methods to the definitions specified in 10.4.6.
|
||||||
|
// 5. Set M.[[Module]] to module.
|
||||||
|
// 7. Set M.[[Exports]] to sortedExports.
|
||||||
|
// 8. Create own properties of M corresponding to the definitions in 28.3.
|
||||||
|
let namespace = context.intrinsics().templates().namespace().create( |
||||||
|
ObjectData::module_namespace(ModuleNamespace { module, exports }), |
||||||
|
vec![js_string!("Module").into()], |
||||||
|
); |
||||||
|
|
||||||
|
// 9. Set module.[[Namespace]] to M.
|
||||||
|
// Ignored because this is done by `Module::namespace`
|
||||||
|
|
||||||
|
// 10. Return M.
|
||||||
|
namespace |
||||||
|
} |
||||||
|
|
||||||
|
/// Gets the export names of the Module Namespace object.
|
||||||
|
pub(crate) const fn exports(&self) -> &IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>> { |
||||||
|
&self.exports |
||||||
|
} |
||||||
|
|
||||||
|
/// Gest the module associated with this Module Namespace object.
|
||||||
|
pub(crate) const fn module(&self) -> &Module { |
||||||
|
&self.module |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@ |
|||||||
|
use crate::{ |
||||||
|
object::{JsObject, JsPrototype}, |
||||||
|
Context, JsResult, |
||||||
|
}; |
||||||
|
|
||||||
|
use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; |
||||||
|
|
||||||
|
/// Definitions of the internal object methods for [**Immutable Prototype Exotic Objects**][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects
|
||||||
|
pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = |
||||||
|
InternalObjectMethods { |
||||||
|
__set_prototype_of__: immutable_prototype_exotic_set_prototype_of, |
||||||
|
..ORDINARY_INTERNAL_METHODS |
||||||
|
}; |
||||||
|
|
||||||
|
/// [`[[SetPrototypeOf]] ( V )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects-setprototypeof-v
|
||||||
|
#[allow(clippy::needless_pass_by_value)] |
||||||
|
pub(crate) fn immutable_prototype_exotic_set_prototype_of( |
||||||
|
obj: &JsObject, |
||||||
|
val: JsPrototype, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. Return ? SetImmutablePrototype(O, V).
|
||||||
|
|
||||||
|
// inlined since other implementations can just use `set_prototype_of` directly.
|
||||||
|
|
||||||
|
// SetImmutablePrototype ( O, V )
|
||||||
|
// <https://tc39.es/ecma262/#sec-set-immutable-prototype>
|
||||||
|
|
||||||
|
// 1. Let current be ? O.[[GetPrototypeOf]]().
|
||||||
|
let current = obj.__get_prototype_of__(context)?; |
||||||
|
|
||||||
|
// 2. If SameValue(V, current) is true, return true.
|
||||||
|
// 3. Return false.
|
||||||
|
Ok(val == current) |
||||||
|
} |
@ -0,0 +1,329 @@ |
|||||||
|
use std::collections::HashSet; |
||||||
|
|
||||||
|
use crate::{ |
||||||
|
module::BindingName, |
||||||
|
object::{JsObject, JsPrototype}, |
||||||
|
property::{PropertyDescriptor, PropertyKey}, |
||||||
|
Context, JsNativeError, JsResult, JsValue, |
||||||
|
}; |
||||||
|
|
||||||
|
use super::{ |
||||||
|
immutable_prototype, ordinary_define_own_property, ordinary_delete, ordinary_get, |
||||||
|
ordinary_get_own_property, ordinary_has_property, ordinary_own_property_keys, |
||||||
|
InternalObjectMethods, ORDINARY_INTERNAL_METHODS, |
||||||
|
}; |
||||||
|
|
||||||
|
/// Definitions of the internal object methods for [**Module Namespace Exotic Objects**][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
|
||||||
|
pub(crate) static MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = |
||||||
|
InternalObjectMethods { |
||||||
|
__get_prototype_of__: module_namespace_exotic_get_prototype_of, |
||||||
|
__set_prototype_of__: module_namespace_exotic_set_prototype_of, |
||||||
|
__is_extensible__: module_namespace_exotic_is_extensible, |
||||||
|
__prevent_extensions__: module_namespace_exotic_prevent_extensions, |
||||||
|
__get_own_property__: module_namespace_exotic_get_own_property, |
||||||
|
__define_own_property__: module_namespace_exotic_define_own_property, |
||||||
|
__has_property__: module_namespace_exotic_has_property, |
||||||
|
__get__: module_namespace_exotic_get, |
||||||
|
__set__: module_namespace_exotic_set, |
||||||
|
__delete__: module_namespace_exotic_delete, |
||||||
|
__own_property_keys__: module_namespace_exotic_own_property_keys, |
||||||
|
..ORDINARY_INTERNAL_METHODS |
||||||
|
}; |
||||||
|
|
||||||
|
/// [`[[GetPrototypeOf]] ( )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof
|
||||||
|
#[allow(clippy::unnecessary_wraps)] |
||||||
|
fn module_namespace_exotic_get_prototype_of( |
||||||
|
_: &JsObject, |
||||||
|
_: &mut Context<'_>, |
||||||
|
) -> JsResult<JsPrototype> { |
||||||
|
// 1. Return null.
|
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[SetPrototypeOf]] ( V )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v
|
||||||
|
#[allow(clippy::unnecessary_wraps)] |
||||||
|
fn module_namespace_exotic_set_prototype_of( |
||||||
|
obj: &JsObject, |
||||||
|
val: JsPrototype, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. Return ! SetImmutablePrototype(O, V).
|
||||||
|
Ok( |
||||||
|
immutable_prototype::immutable_prototype_exotic_set_prototype_of(obj, val, context) |
||||||
|
.expect("this must not fail per the spec"), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[IsExtensible]] ( )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-isextensible
|
||||||
|
#[allow(clippy::unnecessary_wraps)] |
||||||
|
fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult<bool> { |
||||||
|
// 1. Return false.
|
||||||
|
Ok(false) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[PreventExtensions]] ( )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions
|
||||||
|
#[allow(clippy::unnecessary_wraps)] |
||||||
|
fn module_namespace_exotic_prevent_extensions(_: &JsObject, _: &mut Context<'_>) -> JsResult<bool> { |
||||||
|
Ok(true) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[GetOwnProperty]] ( P )`][spec]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p
|
||||||
|
fn module_namespace_exotic_get_own_property( |
||||||
|
obj: &JsObject, |
||||||
|
key: &PropertyKey, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<Option<PropertyDescriptor>> { |
||||||
|
// 1. If P is a Symbol, return OrdinaryGetOwnProperty(O, P).
|
||||||
|
let key = match key { |
||||||
|
PropertyKey::Symbol(_) => return ordinary_get_own_property(obj, key, context), |
||||||
|
PropertyKey::Index(_) => return Ok(None), |
||||||
|
PropertyKey::String(s) => s, |
||||||
|
}; |
||||||
|
|
||||||
|
{ |
||||||
|
let obj = obj.borrow(); |
||||||
|
let obj = obj |
||||||
|
.as_module_namespace() |
||||||
|
.expect("internal method can only be called on module namespace objects"); |
||||||
|
// 2. Let exports be O.[[Exports]].
|
||||||
|
let exports = obj.exports(); |
||||||
|
|
||||||
|
// 3. If exports does not contain P, return undefined.
|
||||||
|
if !exports.contains_key(key) { |
||||||
|
return Ok(None); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 4. Let value be ? O.[[Get]](P, O).
|
||||||
|
let value = obj.get(key.clone(), context)?; |
||||||
|
|
||||||
|
// 5. Return PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false }.
|
||||||
|
Ok(Some( |
||||||
|
PropertyDescriptor::builder() |
||||||
|
.value(value) |
||||||
|
.writable(true) |
||||||
|
.enumerable(true) |
||||||
|
.configurable(false) |
||||||
|
.build(), |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[DefineOwnProperty]] ( P, Desc )`][spec]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc
|
||||||
|
fn module_namespace_exotic_define_own_property( |
||||||
|
obj: &JsObject, |
||||||
|
key: &PropertyKey, |
||||||
|
desc: PropertyDescriptor, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. If P is a Symbol, return ! OrdinaryDefineOwnProperty(O, P, Desc).
|
||||||
|
if let PropertyKey::Symbol(_) = key { |
||||||
|
return ordinary_define_own_property(obj, key, desc, context); |
||||||
|
} |
||||||
|
|
||||||
|
// 2. Let current be ? O.[[GetOwnProperty]](P).
|
||||||
|
let Some(current) = obj.__get_own_property__(key, context)? else { |
||||||
|
// 3. If current is undefined, return false.
|
||||||
|
return Ok(false); |
||||||
|
}; |
||||||
|
|
||||||
|
// 4. If Desc has a [[Configurable]] field and Desc.[[Configurable]] is true, return false.
|
||||||
|
// 5. If Desc has an [[Enumerable]] field and Desc.[[Enumerable]] is false, return false.
|
||||||
|
// 6. If IsAccessorDescriptor(Desc) is true, return false.
|
||||||
|
// 7. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, return false.
|
||||||
|
if desc.configurable() == Some(true) |
||||||
|
|| desc.enumerable() == Some(false) |
||||||
|
|| desc.is_accessor_descriptor() |
||||||
|
|| desc.writable() == Some(false) |
||||||
|
{ |
||||||
|
return Ok(false); |
||||||
|
} |
||||||
|
|
||||||
|
// 8. If Desc has a [[Value]] field, return SameValue(Desc.[[Value]], current.[[Value]]).
|
||||||
|
// 9. Return true.
|
||||||
|
Ok(desc.value().map_or(true, |v| v == current.expect_value())) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[HasProperty]] ( P )`][spec]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p
|
||||||
|
fn module_namespace_exotic_has_property( |
||||||
|
obj: &JsObject, |
||||||
|
key: &PropertyKey, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. If P is a Symbol, return ! OrdinaryHasProperty(O, P).
|
||||||
|
let key = match key { |
||||||
|
PropertyKey::Symbol(_) => return ordinary_has_property(obj, key, context), |
||||||
|
PropertyKey::Index(_) => return Ok(false), |
||||||
|
PropertyKey::String(s) => s, |
||||||
|
}; |
||||||
|
|
||||||
|
let obj = obj.borrow(); |
||||||
|
let obj = obj |
||||||
|
.as_module_namespace() |
||||||
|
.expect("internal method can only be called on module namespace objects"); |
||||||
|
|
||||||
|
// 2. Let exports be O.[[Exports]].
|
||||||
|
let exports = obj.exports(); |
||||||
|
|
||||||
|
// 3. If exports contains P, return true.
|
||||||
|
// 4. Return false.
|
||||||
|
Ok(exports.contains_key(key)) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[Get]] ( P, Receiver )`][spec]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver
|
||||||
|
fn module_namespace_exotic_get( |
||||||
|
obj: &JsObject, |
||||||
|
key: &PropertyKey, |
||||||
|
receiver: JsValue, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<JsValue> { |
||||||
|
// 1. If P is a Symbol, then
|
||||||
|
// a. Return ! OrdinaryGet(O, P, Receiver).
|
||||||
|
let key = match key { |
||||||
|
PropertyKey::Symbol(_) => return ordinary_get(obj, key, receiver, context), |
||||||
|
PropertyKey::Index(_) => return Ok(JsValue::undefined()), |
||||||
|
PropertyKey::String(s) => s, |
||||||
|
}; |
||||||
|
|
||||||
|
let obj = obj.borrow(); |
||||||
|
let obj = obj |
||||||
|
.as_module_namespace() |
||||||
|
.expect("internal method can only be called on module namespace objects"); |
||||||
|
|
||||||
|
// 2. Let exports be O.[[Exports]].
|
||||||
|
let exports = obj.exports(); |
||||||
|
// 3. If exports does not contain P, return undefined.
|
||||||
|
let Some(export_name) = exports.get(key).copied() else { |
||||||
|
return Ok(JsValue::undefined()); |
||||||
|
}; |
||||||
|
|
||||||
|
// 4. Let m be O.[[Module]].
|
||||||
|
let m = obj.module(); |
||||||
|
|
||||||
|
// 5. Let binding be m.ResolveExport(P).
|
||||||
|
let binding = m |
||||||
|
.resolve_export(export_name, &mut HashSet::default()) |
||||||
|
.expect("6. Assert: binding is a ResolvedBinding Record."); |
||||||
|
|
||||||
|
// 7. Let targetModule be binding.[[Module]].
|
||||||
|
// 8. Assert: targetModule is not undefined.
|
||||||
|
let target_module = binding.module(); |
||||||
|
|
||||||
|
// TODO: cache binding resolution instead of doing the whole process on every access.
|
||||||
|
if let BindingName::Name(name) = binding.binding_name() { |
||||||
|
// 10. Let targetEnv be targetModule.[[Environment]].
|
||||||
|
let Some(env) = target_module.environment() else { |
||||||
|
// 11. If targetEnv is empty, throw a ReferenceError exception.
|
||||||
|
let import = context.interner().resolve_expect(export_name); |
||||||
|
return Err(JsNativeError::reference().with_message( |
||||||
|
format!("cannot get import `{import}` from an uninitialized module") |
||||||
|
).into()); |
||||||
|
}; |
||||||
|
|
||||||
|
let locator = env |
||||||
|
.compile_env() |
||||||
|
.borrow() |
||||||
|
.get_binding(name) |
||||||
|
.expect("checked before that the name was reachable"); |
||||||
|
|
||||||
|
// 12. Return ? targetEnv.GetBindingValue(binding.[[BindingName]], true).
|
||||||
|
env.get(locator.binding_index()).ok_or_else(|| { |
||||||
|
let import = context.interner().resolve_expect(export_name); |
||||||
|
|
||||||
|
JsNativeError::reference() |
||||||
|
.with_message(format!("cannot get uninitialized import `{import}`")) |
||||||
|
.into() |
||||||
|
}) |
||||||
|
} else { |
||||||
|
// 9. If binding.[[BindingName]] is namespace, then
|
||||||
|
// a. Return GetModuleNamespace(targetModule).
|
||||||
|
Ok(target_module.namespace(context).into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[Set]] ( P, V, Receiver )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver
|
||||||
|
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] |
||||||
|
fn module_namespace_exotic_set( |
||||||
|
_obj: &JsObject, |
||||||
|
_key: PropertyKey, |
||||||
|
_value: JsValue, |
||||||
|
_receiver: JsValue, |
||||||
|
_context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. Return false.
|
||||||
|
Ok(false) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[Delete]] ( P )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p
|
||||||
|
fn module_namespace_exotic_delete( |
||||||
|
obj: &JsObject, |
||||||
|
key: &PropertyKey, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<bool> { |
||||||
|
// 1. If P is a Symbol, then
|
||||||
|
// a. Return ! OrdinaryDelete(O, P).
|
||||||
|
let key = match key { |
||||||
|
PropertyKey::Symbol(_) => return ordinary_delete(obj, key, context), |
||||||
|
PropertyKey::Index(_) => return Ok(true), |
||||||
|
PropertyKey::String(s) => s, |
||||||
|
}; |
||||||
|
|
||||||
|
let obj = obj.borrow(); |
||||||
|
let obj = obj |
||||||
|
.as_module_namespace() |
||||||
|
.expect("internal method can only be called on module namespace objects"); |
||||||
|
|
||||||
|
// 2. Let exports be O.[[Exports]].
|
||||||
|
let exports = obj.exports(); |
||||||
|
|
||||||
|
// 3. If exports contains P, return false.
|
||||||
|
// 4. Return true.
|
||||||
|
Ok(!exports.contains_key(key)) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`[[OwnPropertyKeys]] ( )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys
|
||||||
|
fn module_namespace_exotic_own_property_keys( |
||||||
|
obj: &JsObject, |
||||||
|
context: &mut Context<'_>, |
||||||
|
) -> JsResult<Vec<PropertyKey>> { |
||||||
|
// 2. Let symbolKeys be OrdinaryOwnPropertyKeys(O).
|
||||||
|
let symbol_keys = ordinary_own_property_keys(obj, context)?; |
||||||
|
|
||||||
|
let obj = obj.borrow(); |
||||||
|
let obj = obj |
||||||
|
.as_module_namespace() |
||||||
|
.expect("internal method can only be called on module namespace objects"); |
||||||
|
|
||||||
|
// 1. Let exports be O.[[Exports]].
|
||||||
|
let exports = obj.exports(); |
||||||
|
|
||||||
|
// 3. Return the list-concatenation of exports and symbolKeys.
|
||||||
|
Ok(exports |
||||||
|
.keys() |
||||||
|
.map(|k| PropertyKey::String(k.clone())) |
||||||
|
.chain(symbol_keys) |
||||||
|
.collect()) |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
function sum(a, b) { |
||||||
|
return a + b; |
||||||
|
} |
||||||
|
|
||||||
|
function sub(a, b) { |
||||||
|
return a - b; |
||||||
|
} |
||||||
|
|
||||||
|
function mult(a, b) { |
||||||
|
return a * b; |
||||||
|
} |
||||||
|
|
||||||
|
function div(a, b) { |
||||||
|
return a / b; |
||||||
|
} |
||||||
|
|
||||||
|
function sqrt(a) { |
||||||
|
return Math.sqrt(a); |
||||||
|
} |
||||||
|
|
||||||
|
export { sum, sub, mult, div, sqrt }; |
@ -0,0 +1,11 @@ |
|||||||
|
import { sum, mult, sqrt } from "./operations.mjs"; |
||||||
|
|
||||||
|
function pyth(a, b) { |
||||||
|
let a2 = mult(a, a); |
||||||
|
let b2 = mult(b, b); |
||||||
|
let a2b2 = sum(a2, b2); |
||||||
|
|
||||||
|
return sqrt(a2b2); |
||||||
|
} |
||||||
|
|
||||||
|
export { pyth }; |
@ -0,0 +1,131 @@ |
|||||||
|
use std::{error::Error, path::Path}; |
||||||
|
|
||||||
|
use boa_engine::{ |
||||||
|
builtins::promise::PromiseState, |
||||||
|
module::{ModuleLoader, SimpleModuleLoader}, |
||||||
|
object::FunctionObjectBuilder, |
||||||
|
Context, JsError, JsNativeError, JsValue, Module, NativeFunction, |
||||||
|
}; |
||||||
|
use boa_parser::Source; |
||||||
|
|
||||||
|
// This example demonstrates how to use Boa's module API
|
||||||
|
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("./boa_examples/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()?; |
||||||
|
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("./boa_examples/scripts/modules") |
||||||
|
.canonicalize()? |
||||||
|
.join("main.mjs"), |
||||||
|
module.clone(), |
||||||
|
); |
||||||
|
|
||||||
|
// The lifecycle of the module is tracked using promises, which can be a bit cumbersome
|
||||||
|
// for simple uses but that use case is better suited by the `Module::load_link_evaluate` method.
|
||||||
|
// This does the full version for demonstration purposes.
|
||||||
|
//
|
||||||
|
// parse -> load -> link -> evaluate
|
||||||
|
let promise_result = module |
||||||
|
// Initial load that recursively loads the module's dependencies.
|
||||||
|
// This returns a `JsPromise` that will be resolved when loading finishes,
|
||||||
|
// which allows async loads and async fetches.
|
||||||
|
.load(context) |
||||||
|
.then( |
||||||
|
Some( |
||||||
|
FunctionObjectBuilder::new( |
||||||
|
context, |
||||||
|
NativeFunction::from_copy_closure_with_captures( |
||||||
|
|_, _, module, context| { |
||||||
|
// After loading, link all modules by resolving the imports
|
||||||
|
// and exports on the full module graph, initializing module
|
||||||
|
// environments. This returns a plain `Err` since all modules
|
||||||
|
// must link at the same time.
|
||||||
|
module.link(context)?; |
||||||
|
Ok(JsValue::undefined()) |
||||||
|
}, |
||||||
|
module.clone(), |
||||||
|
), |
||||||
|
) |
||||||
|
.build(), |
||||||
|
), |
||||||
|
None, |
||||||
|
context, |
||||||
|
)? |
||||||
|
.then( |
||||||
|
Some( |
||||||
|
FunctionObjectBuilder::new( |
||||||
|
context, |
||||||
|
NativeFunction::from_copy_closure_with_captures( |
||||||
|
// Finally, evaluate the root module.
|
||||||
|
// This returns a `JsPromise` since a module could have
|
||||||
|
// top-level await statements, which defers module execution to the
|
||||||
|
// job queue.
|
||||||
|
|_, _, module, context| Ok(module.evaluate(context).into()), |
||||||
|
module.clone(), |
||||||
|
), |
||||||
|
) |
||||||
|
.build(), |
||||||
|
), |
||||||
|
None, |
||||||
|
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("result", context)?; |
||||||
|
|
||||||
|
println!("result = {}", result.display()); |
||||||
|
|
||||||
|
assert_eq!(namespace.get("result", context)?, JsValue::from(5)); |
||||||
|
|
||||||
|
let mix = namespace |
||||||
|
.get("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(()) |
||||||
|
} |
Loading…
Reference in new issue