use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use rustc_hash::FxHashSet;
use boa_engine::js_string;
use boa_engine::property::PropertyKey;
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use boa_interner::Interner;
use boa_parser::source::ReadChar;
use boa_parser::{Parser, Source};
use boa_profiler::Profiler;
pub use loader::*;
pub use namespace::ModuleNamespace;
use source::SourceTextModule;
pub use synthetic::{SyntheticModule, SyntheticModuleInitializer};
use crate::object::TypedJsFunction;
use crate::{
builtins,
builtins::promise::{PromiseCapability, PromiseState},
environments::DeclarativeEnvironment,
object::{JsObject, JsPromise},
realm::Realm,
Context, HostDefined, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction,
};
mod loader;
mod namespace;
mod source;
mod synthetic;
#[derive(Clone, Trace, Finalize)]
pub struct Module {
inner: Gc<ModuleRepr>,
}
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("namespace", &self.inner.namespace)
.field("kind", &self.inner.kind)
.finish()
}
}
#[derive(Trace, Finalize)]
struct ModuleRepr {
realm: Realm,
namespace: GcRefCell<Option<JsObject>>,
kind: ModuleKind,
host_defined: HostDefined,
path: Option<PathBuf>,
}
#[derive(Debug, Trace, Finalize)]
pub(crate) enum ModuleKind {
SourceText(SourceTextModule),
Synthetic(SyntheticModule),
}
impl ModuleKind {
pub(crate) fn as_source_text(&self) -> Option<&SourceTextModule> {
match self {
ModuleKind::SourceText(src) => Some(src),
ModuleKind::Synthetic(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedBinding {
module: Module,
binding_name: BindingName,
}
#[derive(Debug, Clone)]
pub(crate) enum BindingName {
Name(JsString),
Namespace,
}
impl ResolvedBinding {
pub(crate) const fn module(&self) -> &Module {
&self.module
}
pub(crate) fn binding_name(&self) -> BindingName {
self.binding_name.clone()
}
}
#[derive(Debug, Clone)]
struct GraphLoadingState {
capability: PromiseCapability,
loading: Cell<bool>,
pending_modules: Cell<usize>,
visited: RefCell<HashSet<Module>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ResolveExportError {
NotFound,
Ambiguous,
}
impl Module {
pub fn parse<R: ReadChar>(
src: Source<'_, R>,
realm: Option<Realm>,
context: &mut Context,
) -> JsResult<Self> {
let _timer = Profiler::global().start_event("Module parsing", "Main");
let path = src.path().map(Path::to_path_buf);
let realm = realm.unwrap_or_else(|| context.realm().clone());
let mut parser = Parser::new(src);
parser.set_identifier(context.next_parser_identifier());
let module = parser.parse_module(realm.scope(), context.interner_mut())?;
let src = SourceTextModule::new(module, context.interner());
Ok(Self {
inner: Gc::new(ModuleRepr {
realm,
namespace: GcRefCell::default(),
kind: ModuleKind::SourceText(src),
host_defined: HostDefined::default(),
path,
}),
})
}
#[inline]
pub fn synthetic(
export_names: &[JsString],
evaluation_steps: SyntheticModuleInitializer,
path: Option<PathBuf>,
realm: Option<Realm>,
context: &mut Context,
) -> Self {
let names = export_names.iter().cloned().collect();
let realm = realm.unwrap_or_else(|| context.realm().clone());
let synth = SyntheticModule::new(names, evaluation_steps);
Self {
inner: Gc::new(ModuleRepr {
realm,
namespace: GcRefCell::default(),
kind: ModuleKind::Synthetic(synth),
host_defined: HostDefined::default(),
path,
}),
}
}
pub fn from_value_as_default(value: JsValue, context: &mut Context) -> Self {
Module::synthetic(
&[js_string!("default")],
SyntheticModuleInitializer::from_copy_closure_with_captures(
move |m, value, _ctx| {
m.set_export(&js_string!("default"), value.clone())?;
Ok(())
},
value,
),
None,
None,
context,
)
}
pub fn parse_json(json: JsString, context: &mut Context) -> JsResult<Self> {
let value = builtins::Json::parse(&JsValue::undefined(), &[json.into()], context)?;
Ok(Self::from_value_as_default(value, context))
}
#[inline]
#[must_use]
pub fn realm(&self) -> &Realm {
&self.inner.realm
}
#[inline]
#[must_use]
pub fn host_defined(&self) -> &HostDefined {
&self.inner.host_defined
}
pub(crate) fn kind(&self) -> &ModuleKind {
&self.inner.kind
}
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
match self.kind() {
ModuleKind::SourceText(src) => src.environment(),
ModuleKind::Synthetic(syn) => syn.environment(),
}
}
#[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(_) => SyntheticModule::load(context),
}
}
fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context) {
assert!(state.loading.get());
if let ModuleKind::SourceText(src) = self.kind() {
src.inner_load(self, 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<Module>,
interner: &Interner,
) -> FxHashSet<JsString> {
match self.kind() {
ModuleKind::SourceText(src) => src.get_exported_names(self, export_star_set, interner),
ModuleKind::Synthetic(synth) => synth.get_exported_names(),
}
}
#[allow(clippy::mutable_key_type)]
pub(crate) fn resolve_export(
&self,
export_name: JsString,
resolve_set: &mut FxHashSet<(Self, JsString)>,
interner: &Interner,
) -> Result<ResolvedBinding, ResolveExportError> {
match self.kind() {
ModuleKind::SourceText(src) => {
src.resolve_export(self, &export_name, resolve_set, interner)
}
ModuleKind::Synthetic(synth) => synth.resolve_export(self, export_name),
}
}
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn link(&self, context: &mut Context) -> JsResult<()> {
match self.kind() {
ModuleKind::SourceText(src) => src.link(self, context),
ModuleKind::Synthetic(synth) => {
synth.link(self, context);
Ok(())
}
}
}
fn inner_link(
&self,
stack: &mut Vec<Module>,
index: usize,
context: &mut Context,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_link(self, stack, index, context),
ModuleKind::Synthetic(synth) => {
synth.link(self, context);
Ok(index)
}
}
}
#[inline]
pub fn evaluate(&self, context: &mut Context) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(src) => src.evaluate(self, context),
ModuleKind::Synthetic(synth) => synth.evaluate(self, context),
}
}
fn inner_evaluate(
&self,
stack: &mut Vec<Module>,
index: usize,
context: &mut Context,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_evaluate(self, stack, index, None, context),
ModuleKind::Synthetic(synth) => {
let promise: JsPromise = synth.evaluate(self, context);
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(dropping_copy_types)]
#[inline]
pub fn load_link_evaluate(&self, context: &mut Context) -> JsPromise {
let main_timer = Profiler::global().start_event("Module evaluation", "Main");
let promise = self
.load(context)
.then(
Some(
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| {
module.link(context)?;
Ok(JsValue::undefined())
},
self.clone(),
)
.to_js_function(context.realm()),
),
None,
context,
)
.then(
Some(
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| Ok(module.evaluate(context).into()),
self.clone(),
)
.to_js_function(context.realm()),
),
None,
context,
);
drop(main_timer);
Profiler::global().drop();
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(), context.interner());
let unambiguous_names = exported_names
.into_iter()
.filter_map(|name| {
self.resolve_export(
name.clone(),
&mut HashSet::default(),
context.interner(),
)
.ok()
.map(|_| name)
})
.collect();
ModuleNamespace::create(self.clone(), unambiguous_names, context)
})
.clone()
}
#[inline]
pub fn get_value<K>(&self, name: K, context: &mut Context) -> JsResult<JsValue>
where
K: Into<PropertyKey>,
{
let namespace = self.namespace(context);
namespace.get(name, context)
}
#[inline]
#[allow(clippy::needless_pass_by_value)]
pub fn get_typed_fn<A, R>(
&self,
name: JsString,
context: &mut Context,
) -> JsResult<TypedJsFunction<A, R>>
where
A: crate::object::TryIntoJsArguments,
R: crate::value::TryFromJs,
{
let func = self.get_value(name.clone(), context)?;
let func = func.as_function().ok_or_else(|| {
JsNativeError::typ().with_message(format!("{name:?} is not a function"))
})?;
Ok(func.typed())
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
self.inner.path.as_deref()
}
}
impl PartialEq for Module {
#[inline]
fn eq(&self, other: &Self) -> bool {
Gc::ptr_eq(&self.inner, &other.inner)
}
}
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);
}
}