use boa_ast::scope::Scope;
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use rustc_hash::FxHashSet;
use crate::{
builtins::promise::ResolvingFunctions,
bytecompiler::ByteCompiler,
environments::{DeclarativeEnvironment, EnvironmentStack},
js_string,
object::JsPromise,
vm::{ActiveRunnable, CallFrame, CodeBlock},
Context, JsNativeError, JsResult, JsString, JsValue, Module,
};
use super::{BindingName, 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,
{
#[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)
}
}
#[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 {
pub fn from_copy_closure<F>(closure: F) -> Self
where
F: Fn(&SyntheticModule, &mut Context) -> JsResult<()> + Copy + 'static,
{
unsafe { Self::from_closure(closure) }
}
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,
{
unsafe { Self::from_closure_with_captures(closure, captures) }
}
pub unsafe fn from_closure<F>(closure: F) -> Self
where
F: Fn(&SyntheticModule, &mut Context) -> JsResult<()> + 'static,
{
unsafe {
Self::from_closure_with_captures(
move |module, (), context| closure(module, context),
(),
)
}
}
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,
{
let ptr = Gc::into_raw(Gc::new(Callback {
f: closure,
captures,
}));
unsafe {
Self {
inner: Gc::from_raw(ptr),
}
}
}
#[inline]
pub(crate) fn call(&self, module: &SyntheticModule, context: &mut Context) -> JsResult<()> {
self.inner.call(module, context)
}
}
#[derive(Debug, Trace, Finalize, Default)]
#[boa_gc(unsafe_no_drop)]
enum ModuleStatus {
#[default]
Unlinked,
Linked {
environment: Gc<DeclarativeEnvironment>,
eval_context: (EnvironmentStack, Gc<CodeBlock>),
},
Evaluated {
environment: Gc<DeclarativeEnvironment>,
promise: JsPromise,
},
}
impl ModuleStatus {
fn transition<F>(&mut self, f: F)
where
F: FnOnce(Self) -> Self,
{
*self = f(std::mem::take(self));
}
}
#[derive(Trace, Finalize)]
pub struct SyntheticModule {
#[unsafe_ignore_trace]
export_names: FxHashSet<JsString>,
eval_steps: SyntheticModuleInitializer,
state: GcRefCell<ModuleStatus>,
}
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.export_names)
.field("eval_steps", &self.eval_steps)
.finish_non_exhaustive()
}
}
impl SyntheticModule {
pub fn set_export(&self, export_name: &JsString, export_value: JsValue) -> JsResult<()> {
let env = self.environment().ok_or_else(|| {
JsNativeError::typ().with_message(format!(
"cannot set name `{}` in an unlinked synthetic module",
export_name.to_std_string_escaped()
))
})?;
let locator = env
.kind()
.as_module()
.expect("must be module environment")
.compile()
.get_binding(export_name)
.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()
))
})?;
env.set(locator.binding_index(), export_value);
Ok(())
}
pub(super) fn new(names: FxHashSet<JsString>, eval_steps: SyntheticModuleInitializer) -> Self {
Self {
export_names: names,
eval_steps,
state: GcRefCell::default(),
}
}
pub(super) fn load(context: &mut Context) -> JsPromise {
JsPromise::resolve(JsValue::undefined(), context)
}
pub(super) fn get_exported_names(&self) -> FxHashSet<JsString> {
self.export_names.clone()
}
#[allow(clippy::mutable_key_type)]
pub(super) fn resolve_export(
&self,
module_self: &Module,
export_name: JsString,
) -> Result<ResolvedBinding, ResolveExportError> {
if self.export_names.contains(&export_name) {
Ok(ResolvedBinding {
module: module_self.clone(),
binding_name: BindingName::Name(export_name),
})
} else {
Err(ResolveExportError::NotFound)
}
}
pub(super) fn link(&self, module_self: &Module, context: &mut Context) {
if !matches!(&*self.state.borrow(), ModuleStatus::Unlinked) {
return;
}
let global_env = module_self.realm().environment().clone();
let global_scope = module_self.realm().scope().clone();
let module_scope = Scope::new(global_scope, true);
let compiler = ByteCompiler::new(
js_string!("<main>"),
true,
false,
module_scope.clone(),
module_scope.clone(),
false,
false,
context.interner_mut(),
false,
);
let exports = self
.export_names
.iter()
.map(|name| {
module_scope.create_mutable_binding(name.clone(), false)
})
.collect::<Vec<_>>();
module_scope.escape_all_bindings();
let cb = Gc::new(compiler.finish());
let mut envs = EnvironmentStack::new(global_env);
envs.push_module(module_scope);
for locator in exports {
envs.put_lexical_value(
locator.scope(),
locator.binding_index(),
JsValue::undefined(),
);
}
let env = envs
.current_declarative_ref()
.cloned()
.expect("should have the module environment");
self.state
.borrow_mut()
.transition(|_| ModuleStatus::Linked {
environment: env,
eval_context: (envs, cb),
});
}
pub(super) fn evaluate(&self, module_self: &Module, context: &mut Context) -> JsPromise {
let (environments, codeblock) = match &*self.state.borrow() {
ModuleStatus::Unlinked => {
let (promise, ResolvingFunctions { reject, .. }) = JsPromise::new_pending(context);
reject
.call(
&JsValue::undefined(),
&[JsNativeError::typ()
.with_message("cannot evaluate unlinked synthetic module")
.to_opaque(context)
.into()],
context,
)
.expect("native resolving functions cannot throw");
return promise;
}
ModuleStatus::Linked { eval_context, .. } => eval_context.clone(),
ModuleStatus::Evaluated { promise, .. } => return promise.clone(),
};
let realm = module_self.realm().clone();
let env_fp = environments.len() as u32;
let callframe = CallFrame::new(
codeblock,
Some(ActiveRunnable::Module(module_self.clone())),
environments,
realm,
)
.with_env_fp(env_fp);
context
.vm
.push_frame_with_stack(callframe, JsValue::undefined(), JsValue::null());
let result = self.eval_steps.call(self, context);
let frame = context.vm.pop_frame().expect("there should be a frame");
frame.restore_stack(&mut context.vm);
let (promise, ResolvingFunctions { resolve, reject }) = JsPromise::new_pending(context);
match result {
Ok(()) => resolve.call(&JsValue::undefined(), &[], context),
Err(err) => reject.call(&JsValue::undefined(), &[err.to_opaque(context)], context),
}
.expect("default resolving functions cannot throw");
self.state.borrow_mut().transition(|state| match state {
ModuleStatus::Linked { environment, .. } => ModuleStatus::Evaluated {
environment,
promise: promise.clone(),
},
_ => unreachable!("checks above ensure the module is linked"),
});
promise
}
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
match &*self.state.borrow() {
ModuleStatus::Unlinked => None,
ModuleStatus::Linked { environment, .. }
| ModuleStatus::Evaluated { environment, .. } => Some(environment.clone()),
}
}
}