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::script::Script;
use crate::vm::ActiveRunnable;
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};
#[derive(Debug, Clone)]
pub enum Referrer {
Module(Module),
Realm(Realm),
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),
}
}
}
pub trait ModuleLoader {
#[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<'_>,
);
fn register_module(&self, _specifier: JsString, _module: Module) {}
fn get_module(&self, _specifier: JsString) -> Option<Module> {
None
}
fn init_import_meta(
&self,
_import_meta: &JsObject,
_module: &Module,
_context: &mut Context<'_>,
) {
}
}
#[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,
);
}
}
#[derive(Debug)]
pub struct SimpleModuleLoader {
root: PathBuf,
module_map: GcRefCell<FxHashMap<PathBuf, Module>>,
}
impl SimpleModuleLoader {
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(),
})
}
#[inline]
pub fn insert(&self, path: PathBuf, module: Module) {
self.module_map.borrow_mut().insert(path, module);
}
#[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);
}
}
#[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: (),
}
#[derive(Debug, Trace, Finalize)]
pub(crate) enum ModuleKind {
SourceText(SourceTextModule),
#[allow(unused)]
Synthetic,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedBinding {
module: Module,
binding_name: BindingName,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum BindingName {
Name(Identifier),
Namespace,
}
impl ResolvedBinding {
pub(crate) const fn module(&self) -> &Module {
&self.module
}
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 {
#[inline]
pub fn parse<R: Read>(
src: Source<'_, R>,
realm: Option<Realm>,
context: &mut Context<'_>,
) -> JsResult<Self> {
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 = Self {
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)
}
#[inline]
pub fn realm(&self) -> &Realm {
&self.inner.realm
}
pub(crate) fn kind(&self) -> &ModuleKind {
&self.inner.kind
}
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
self.inner.environment.borrow().clone()
}
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn load(&self, context: &mut Context<'_>) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(_) => {
let pc = PromiseCapability::new(
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect(
"capability creation must always succeed when using the `%Promise%` intrinsic",
);
self.inner_load(
&Rc::new(GraphLoadingState {
capability: pc.clone(),
loading: Cell::new(true),
pending_modules: Cell::new(1),
visited: RefCell::default(),
}),
context,
);
JsPromise::from_object(pc.promise().clone())
.expect("promise created from the %Promise% intrinsic is always native")
}
ModuleKind::Synthetic => todo!("synthetic.load()"),
}
}
fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context<'_>) {
assert!(state.loading.get());
if let ModuleKind::SourceText(src) = self.kind() {
src.inner_load(state, context);
if !state.loading.get() {
return;
}
}
assert!(state.pending_modules.get() >= 1);
state.pending_modules.set(state.pending_modules.get() - 1);
if state.pending_modules.get() == 0 {
state.loading.set(false);
state
.capability
.resolve()
.call(&JsValue::undefined(), &[], context)
.expect("marking a module as loaded should not fail");
}
}
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()"),
}
}
#[allow(clippy::mutable_key_type)]
pub(crate) fn resolve_export(
&self,
export_name: Sym,
resolve_set: &mut FxHashSet<(Self, Sym)>,
) -> Result<ResolvedBinding, ResolveExportError> {
match self.kind() {
ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set),
ModuleKind::Synthetic => todo!("synthetic.resolve_export()"),
}
}
#[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()"),
}
}
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)]
ModuleKind::Synthetic => {
todo!("synthetic.link()");
Ok(index)
}
}
}
#[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()"),
}
}
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),
#[allow(unused, clippy::diverging_sub_expression)]
ModuleKind::Synthetic => {
let promise: JsPromise = todo!("module.Evaluate()");
let state = promise.state()?;
match state {
PromiseState::Pending => {
unreachable!("b. Assert: promise.[[PromiseState]] is not pending.")
}
PromiseState::Fulfilled(_) => Ok(index),
PromiseState::Rejected(err) => Err(JsError::from_opaque(err)),
}
}
}
}
#[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,
)?;
drop(main_timer);
Profiler::global().drop();
Ok(promise)
}
pub fn namespace(&self, context: &mut Context<'_>) -> JsObject {
self.inner
.namespace
.borrow_mut()
.get_or_insert_with(|| {
let exported_names = self.get_exported_names(&mut Vec::default());
let unambiguous_names = exported_names
.into_iter()
.filter_map(|name| {
self.resolve_export(name, &mut HashSet::default())
.ok()
.map(|_| name)
})
.collect();
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);
}
}
#[derive(Debug, Trace, Finalize)]
pub struct ModuleNamespace {
module: Module,
#[unsafe_ignore_trace]
exports: IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>>,
}
impl ModuleNamespace {
pub(crate) fn create(module: Module, names: Vec<Sym>, context: &mut Context<'_>) -> JsObject {
let mut exports = names
.into_iter()
.map(|sym| {
(
context
.interner()
.resolve_expect(sym)
.into_common::<JsString>(false),
sym,
)
})
.collect::<IndexMap<_, _, _>>();
exports.sort_keys();
let namespace = context.intrinsics().templates().namespace().create(
ObjectData::module_namespace(Self { module, exports }),
vec![js_string!("Module").into()],
);
namespace
}
pub(crate) const fn exports(&self) -> &IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>> {
&self.exports
}
pub(crate) const fn module(&self) -> &Module {
&self.module
}
}