Skip to main content

boa_engine/module/
synthetic.rs

1use boa_ast::scope::Scope;
2use boa_gc::{Finalize, Gc, GcRefCell, Trace};
3use rustc_hash::FxHashSet;
4
5use super::{BindingName, ResolveExportError, ResolvedBinding};
6use crate::{
7    Context, JsExpect, JsNativeError, JsResult, JsString, JsValue, Module, SpannedSourceText,
8    builtins::promise::ResolvingFunctions,
9    bytecompiler::ByteCompiler,
10    class::{Class, ClassBuilder},
11    environments::{DeclarativeEnvironment, EnvironmentStack},
12    js_string,
13    object::JsPromise,
14    vm::{ActiveRunnable, CallFrame, CodeBlock, source_info::SourcePath},
15};
16
17trait TraceableCallback: Trace {
18    fn call(&self, module: &SyntheticModule, context: &mut Context) -> JsResult<()>;
19}
20
21#[derive(Trace, Finalize)]
22struct Callback<F, T>
23where
24    F: Fn(&SyntheticModule, &T, &mut Context) -> JsResult<()>,
25    T: Trace,
26{
27    // SAFETY: `SyntheticModuleInitializer`'s safe API ensures only `Copy` closures are stored; its unsafe API,
28    // on the other hand, explains the invariants to hold in order for this to be safe, shifting
29    // the responsibility to the caller.
30    #[unsafe_ignore_trace]
31    f: F,
32    captures: T,
33}
34
35impl<F, T> TraceableCallback for Callback<F, T>
36where
37    F: Fn(&SyntheticModule, &T, &mut Context) -> JsResult<()>,
38    T: Trace,
39{
40    fn call(&self, module: &SyntheticModule, context: &mut Context) -> JsResult<()> {
41        (self.f)(module, &self.captures, context)
42    }
43}
44
45/// The initializing steps of a [`SyntheticModule`].
46///
47/// # Caveats
48///
49/// By limitations of the Rust language, the garbage collector currently cannot inspect closures
50/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
51/// to use. All other closures can also be stored in a `NativeFunction`, albeit by using an `unsafe`
52/// API, but note that passing closures implicitly capturing traceable types could cause
53/// **Undefined Behaviour**.
54#[derive(Clone, Trace, Finalize)]
55pub struct SyntheticModuleInitializer {
56    inner: Gc<dyn TraceableCallback>,
57}
58
59impl std::fmt::Debug for SyntheticModuleInitializer {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("SyntheticModuleInitializer")
62            .finish_non_exhaustive()
63    }
64}
65
66impl SyntheticModuleInitializer {
67    /// Creates a `SyntheticModuleInitializer` from a [`Copy`] closure.
68    pub fn from_copy_closure<F>(closure: F) -> Self
69    where
70        F: Fn(&SyntheticModule, &mut Context) -> JsResult<()> + Copy + 'static,
71    {
72        // SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
73        unsafe { Self::from_closure(closure) }
74    }
75
76    /// Creates a `SyntheticModuleInitializer` from a [`Copy`] closure and a list of traceable captures.
77    pub fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self
78    where
79        F: Fn(&SyntheticModule, &T, &mut Context) -> JsResult<()> + Copy + 'static,
80        T: Trace + 'static,
81    {
82        // SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
83        unsafe { Self::from_closure_with_captures(closure, captures) }
84    }
85
86    /// Creates a new `SyntheticModuleInitializer` from a closure.
87    ///
88    /// # Safety
89    ///
90    /// Passing a closure that contains a captured variable that needs to be traced by the garbage
91    /// collector could cause an use after free, memory corruption or other kinds of **Undefined
92    /// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
93    /// on why that is the case.
94    pub unsafe fn from_closure<F>(closure: F) -> Self
95    where
96        F: Fn(&SyntheticModule, &mut Context) -> JsResult<()> + 'static,
97    {
98        // SAFETY: The caller must ensure the invariants of the closure hold.
99        unsafe {
100            Self::from_closure_with_captures(
101                move |module, (), context| closure(module, context),
102                (),
103            )
104        }
105    }
106
107    /// Create a new `SyntheticModuleInitializer` from a closure and a list of traceable captures.
108    ///
109    /// # Safety
110    ///
111    /// Passing a closure that contains a captured variable that needs to be traced by the garbage
112    /// collector could cause an use after free, memory corruption or other kinds of **Undefined
113    /// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
114    /// on why that is the case.
115    pub unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self
116    where
117        F: Fn(&SyntheticModule, &T, &mut Context) -> JsResult<()> + 'static,
118        T: Trace + 'static,
119    {
120        // Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the
121        // future: https://github.com/rust-lang/rust/issues/18598
122        let ptr = Gc::into_raw(Gc::new(Callback {
123            f: closure,
124            captures,
125        }));
126
127        // SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object,
128        // meaning this is safe.
129        unsafe {
130            Self {
131                inner: Gc::from_raw(ptr),
132            }
133        }
134    }
135
136    /// Calls this `SyntheticModuleInitializer`, forwarding the arguments to the corresponding function.
137    #[inline]
138    pub(crate) fn call(&self, module: &SyntheticModule, context: &mut Context) -> JsResult<()> {
139        self.inner.call(module, context)
140    }
141}
142
143/// Current status of a [`SyntheticModule`].
144#[derive(Debug, Trace, Finalize, Default)]
145#[boa_gc(unsafe_no_drop)]
146enum ModuleStatus {
147    #[default]
148    Unlinked,
149    Linked {
150        environment: Gc<DeclarativeEnvironment>,
151        eval_context: (EnvironmentStack, Gc<CodeBlock>),
152    },
153    Evaluated {
154        environment: Gc<DeclarativeEnvironment>,
155        promise: JsPromise,
156    },
157}
158
159impl ModuleStatus {
160    /// Transition from one state to another, taking the current state by value to move data
161    /// between states.
162    fn transition<F>(&mut self, f: F)
163    where
164        F: FnOnce(Self) -> Self,
165    {
166        *self = f(std::mem::take(self));
167    }
168}
169
170/// ECMAScript's [**Synthetic Module Records**][spec].
171///
172/// [spec]: https://tc39.es/proposal-json-modules/#sec-synthetic-module-records
173#[derive(Trace, Finalize)]
174pub struct SyntheticModule {
175    #[unsafe_ignore_trace]
176    export_names: FxHashSet<JsString>,
177    eval_steps: SyntheticModuleInitializer,
178    state: GcRefCell<ModuleStatus>,
179}
180
181impl std::fmt::Debug for SyntheticModule {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        f.debug_struct("SyntheticModule")
184            .field("export_names", &self.export_names)
185            .field("eval_steps", &self.eval_steps)
186            .finish_non_exhaustive()
187    }
188}
189
190impl SyntheticModule {
191    /// Abstract operation [`SetSyntheticModuleExport ( module, exportName, exportValue )`][spec].
192    ///
193    /// Sets or changes the exported value for `exportName` in the synthetic module.
194    ///
195    /// # Note
196    ///
197    /// The default export corresponds to the name `"default"`, but note that it needs to
198    /// be passed to the list of exported names in [`Module::synthetic`] beforehand.
199    ///
200    /// [spec]: https://tc39.es/proposal-json-modules/#sec-createsyntheticmodule
201    pub fn set_export(&self, export_name: &JsString, export_value: JsValue) -> JsResult<()> {
202        let env = self.environment().ok_or_else(|| {
203            JsNativeError::typ().with_message(format!(
204                "cannot set name `{}` in an unlinked synthetic module",
205                export_name.to_std_string_escaped()
206            ))
207        })?;
208        let locator = env
209            .kind()
210            .as_module()
211            .js_expect("must be module environment")?
212            .compile()
213            .get_binding(export_name)
214            .ok_or_else(|| {
215                JsNativeError::reference().with_message(format!(
216                    "cannot set name `{}` which was not included in the list of exports",
217                    export_name.to_std_string_escaped()
218                ))
219            })?;
220        env.set(locator.binding_index(), export_value);
221
222        Ok(())
223    }
224
225    /// Sets or changes the exported value for `C::NAME` in the synthetic module
226    /// to the Class's constructor.
227    pub fn export_class<C: Class>(&self, context: &mut Context) -> JsResult<()> {
228        self.export_named_class::<C>(&JsString::from(C::NAME), context)
229    }
230
231    /// Sets or changes the exported value for `export_name` in the synthetic module
232    /// to the Class's constructor.
233    pub fn export_named_class<C: Class>(
234        &self,
235        export_name: &JsString,
236        context: &mut Context,
237    ) -> JsResult<()> {
238        let mut class_builder = ClassBuilder::new::<C>(context);
239        C::init(&mut class_builder)?;
240
241        let class = class_builder.build();
242
243        self.set_export(export_name, class.constructor().into())?;
244        Ok(())
245    }
246
247    /// Creates a new synthetic module.
248    pub(super) fn new(names: FxHashSet<JsString>, eval_steps: SyntheticModuleInitializer) -> Self {
249        Self {
250            export_names: names,
251            eval_steps,
252            state: GcRefCell::default(),
253        }
254    }
255
256    /// Concrete method [`LoadRequestedModules ( )`][spec].
257    ///
258    /// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-LoadRequestedModules
259    pub(super) fn load(context: &mut Context) -> JsPromise {
260        JsPromise::resolve(JsValue::undefined(), context)
261            .expect("default resolve functions cannot throw and must return a promise")
262    }
263
264    /// Concrete method [`GetExportedNames ( [ exportStarSet ] )`][spec].
265    ///
266    /// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-getexportednames
267    pub(super) fn get_exported_names(&self) -> FxHashSet<JsString> {
268        // 1. Return module.[[ExportNames]].
269        self.export_names.clone()
270    }
271
272    /// Concrete method [`ResolveExport ( exportName )`][spec]
273    ///
274    /// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-resolveexport
275    #[allow(clippy::mutable_key_type)]
276    pub(super) fn resolve_export(
277        &self,
278        module_self: &Module,
279        export_name: &JsString,
280    ) -> Result<ResolvedBinding, ResolveExportError> {
281        if self.export_names.contains(export_name) {
282            // 2. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: exportName }.
283            Ok(ResolvedBinding {
284                module: module_self.clone(),
285                binding_name: BindingName::Name(export_name.clone()),
286            })
287        } else {
288            // 1. If module.[[ExportNames]] does not contain exportName, return null.
289            Err(ResolveExportError::NotFound)
290        }
291    }
292
293    /// Concrete method [`Link ( )`][spec].
294    ///
295    /// [spec]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
296    pub(super) fn link(&self, module_self: &Module, context: &mut Context) {
297        if !matches!(&*self.state.borrow(), ModuleStatus::Unlinked) {
298            // Already linked and/or evaluated.
299            return;
300        }
301
302        // 1. Let realm be module.[[Realm]].
303        // 2. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]).
304        // 3. Set module.[[Environment]] to env.
305        let global_env = module_self.realm().environment().clone();
306        let global_scope = module_self.realm().scope().clone();
307        let module_scope = Scope::new(global_scope, true);
308
309        // TODO: A bit of a hack to be able to pass the currently active runnable without an
310        // available codeblock to execute.
311        let compiler = ByteCompiler::new(
312            js_string!("<synthetic>"),
313            true,
314            false,
315            module_scope.clone(),
316            module_scope.clone(),
317            false,
318            false,
319            context.interner_mut(),
320            false,
321            // A synthetic module does not contain `SourceText`
322            SpannedSourceText::new_empty(),
323            SourcePath::None,
324        );
325
326        // 4. For each String exportName in module.[[ExportNames]], do
327        let exports = self
328            .export_names
329            .iter()
330            .map(|name| {
331                //     a. Perform ! env.CreateMutableBinding(exportName, false).
332                module_scope.create_mutable_binding(name.clone(), false)
333            })
334            .collect::<Vec<_>>();
335
336        module_scope.escape_all_bindings();
337
338        let cb = Gc::new(compiler.finish());
339
340        let mut envs = EnvironmentStack::new();
341        envs.push_module(module_scope);
342
343        for locator in exports {
344            //     b. Perform ! env.InitializeBinding(exportName, undefined).
345            envs.put_lexical_value(
346                locator.scope(),
347                locator.binding_index(),
348                JsValue::undefined(),
349                &global_env,
350            );
351        }
352
353        let env = envs
354            .current_declarative_ref(&global_env)
355            .cloned()
356            .expect("should have the module environment");
357
358        self.state
359            .borrow_mut()
360            .transition(|_| ModuleStatus::Linked {
361                environment: env,
362                eval_context: (envs, cb),
363            });
364
365        // 5. Return unused.
366    }
367
368    /// Concrete method [`Evaluate ( )`][spec].
369    ///
370    /// [spec]: https://tc39.es/proposal-json-modules/#sec-smr-Evaluate
371    pub(super) fn evaluate(
372        &self,
373        module_self: &Module,
374        context: &mut Context,
375    ) -> JsResult<JsPromise> {
376        let (environments, codeblock) = match &*self.state.borrow() {
377            ModuleStatus::Unlinked => {
378                let (promise, ResolvingFunctions { reject, .. }) = JsPromise::new_pending(context);
379                reject
380                    .call(
381                        &JsValue::undefined(),
382                        &[JsNativeError::typ()
383                            .with_message("cannot evaluate unlinked synthetic module")
384                            .into_opaque(context)
385                            .into()],
386                        context,
387                    )
388                    .js_expect("native resolving functions cannot throw")?;
389                return Ok(promise);
390            }
391            ModuleStatus::Linked { eval_context, .. } => eval_context.clone(),
392            ModuleStatus::Evaluated { promise, .. } => return Ok(promise.clone()),
393        };
394        // 1. Let moduleContext be a new ECMAScript code execution context.
395
396        let realm = module_self.realm().clone();
397
398        let env_fp = environments.len() as u32;
399        let callframe = CallFrame::new(
400            codeblock,
401            // 4. Set the ScriptOrModule of moduleContext to module.
402            Some(ActiveRunnable::Module(module_self.clone())),
403            // 5. Set the VariableEnvironment of moduleContext to module.[[Environment]].
404            // 6. Set the LexicalEnvironment of moduleContext to module.[[Environment]].
405            environments,
406            // 3. Set the Realm of moduleContext to module.[[Realm]].
407            realm,
408        )
409        .with_env_fp(env_fp);
410
411        // 2. Set the Function of moduleContext to null.
412        // 7. Suspend the currently running execution context.
413        // 8. Push moduleContext on to the execution context stack; moduleContext is now the running execution context.
414        context
415            .vm
416            .push_frame_with_stack(callframe, JsValue::undefined(), JsValue::null());
417
418        // 9. Let steps be module.[[EvaluationSteps]].
419        // 10. Let result be Completion(steps(module)).
420        let result = self.eval_steps.call(self, context);
421
422        // 11. Suspend moduleContext and remove it from the execution context stack.
423        // 12. Resume the context that is now on the top of the execution context stack as the running execution context.
424        let frame = context
425            .vm
426            .pop_frame()
427            .js_expect("there should be a frame")?;
428        context.vm.stack.truncate_to_frame(&frame);
429
430        // 13. Let pc be ! NewPromiseCapability(%Promise%).
431        let (promise, ResolvingFunctions { resolve, reject }) = JsPromise::new_pending(context);
432
433        match result {
434            // 15. Perform ! pc.[[Resolve]](result).
435            Ok(()) => resolve.call(&JsValue::undefined(), &[], context),
436            // 14. IfAbruptRejectPromise(result, pc).
437            Err(err) => reject.call(&JsValue::undefined(), &[err.into_opaque(context)?], context),
438        }
439        .js_expect("default resolving functions cannot throw")?;
440
441        self.state.borrow_mut().transition(|state| match state {
442            ModuleStatus::Linked { environment, .. } => ModuleStatus::Evaluated {
443                environment,
444                promise: promise.clone(),
445            },
446            _ => unreachable!("checks above ensure the module is linked"),
447        });
448
449        // 16. Return pc.[[Promise]].
450        Ok(promise)
451    }
452
453    pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
454        match &*self.state.borrow() {
455            ModuleStatus::Unlinked => None,
456            ModuleStatus::Linked { environment, .. }
457            | ModuleStatus::Evaluated { environment, .. } => Some(environment.clone()),
458        }
459    }
460}