Skip to main content

boa_engine/module/
namespace.rs

1use std::hash::BuildHasherDefault;
2
3use indexmap::IndexSet;
4use rustc_hash::{FxHashMap, FxHasher};
5
6use boa_gc::{Finalize, Trace};
7
8use crate::object::internal_methods::immutable_prototype::immutable_prototype_exotic_set_prototype_of;
9use crate::object::internal_methods::{
10    InternalMethodPropertyContext, InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
11    ordinary_define_own_property, ordinary_delete, ordinary_get, ordinary_get_own_property,
12    ordinary_has_property, ordinary_own_property_keys, ordinary_try_get,
13};
14use crate::object::{JsData, JsPrototype};
15use crate::property::{PropertyDescriptor, PropertyKey};
16use crate::{Context, JsExpect, JsResult, JsString, JsValue, js_string, object::JsObject};
17use crate::{JsNativeError, Module};
18
19use super::{BindingName, ResolvedBinding};
20
21/// Module namespace exotic object.
22///
23/// Exposes the bindings exported by a [`Module`] to be accessed from ECMAScript code.
24#[derive(Debug, Trace, Finalize)]
25pub struct ModuleNamespace {
26    module: Module,
27    #[unsafe_ignore_trace]
28    exports: IndexSet<JsString, BuildHasherDefault<FxHasher>>,
29    /// Cached binding resolutions for each export name.
30    /// Populated once during namespace creation; bindings are immutable after linking.
31    ///
32    /// SAFETY: Every `Module` inside a `ResolvedBinding` is a transitive dependency
33    /// of the parent `module` field (which IS traced). Those modules are reachable
34    /// through `SourceTextModule::loaded_modules` / `SyntheticModule`, so tracing
35    /// them again here would be redundant. Skipping the trace avoids walking the
36    /// entire hashmap on every GC cycle. This is a performance optimization:
37    /// our GC already ignores pointers that were already traced, but this avoids
38    /// a lookup to check if the pointer is alive. The logic is correct either way.
39    #[unsafe_ignore_trace]
40    resolved_bindings: FxHashMap<JsString, ResolvedBinding>,
41}
42
43impl JsData for ModuleNamespace {
44    fn internal_methods(&self) -> &'static InternalObjectMethods {
45        static METHODS: InternalObjectMethods = InternalObjectMethods {
46            __get_prototype_of__: module_namespace_exotic_get_prototype_of,
47            __set_prototype_of__: module_namespace_exotic_set_prototype_of,
48            __is_extensible__: module_namespace_exotic_is_extensible,
49            __prevent_extensions__: module_namespace_exotic_prevent_extensions,
50            __get_own_property__: module_namespace_exotic_get_own_property,
51            __define_own_property__: module_namespace_exotic_define_own_property,
52            __has_property__: module_namespace_exotic_has_property,
53            __try_get__: module_namespace_exotic_try_get,
54            __get__: module_namespace_exotic_get,
55            __set__: module_namespace_exotic_set,
56            __delete__: module_namespace_exotic_delete,
57            __own_property_keys__: module_namespace_exotic_own_property_keys,
58            ..ORDINARY_INTERNAL_METHODS
59        };
60
61        &METHODS
62    }
63}
64
65impl ModuleNamespace {
66    /// Abstract operation [`ModuleNamespaceCreate ( module, exports )`][spec].
67    ///
68    /// [spec]: https://tc39.es/ecma262/#sec-modulenamespacecreate
69    pub(crate) fn create(module: Module, names: Vec<JsString>, context: &mut Context) -> JsObject {
70        // 1. Assert: module.[[Namespace]] is empty.
71        // ignored since this is ensured by `Module::namespace`.
72
73        // 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.
74        let mut exports = names.into_iter().collect::<IndexSet<_, _>>();
75        exports.sort();
76
77        // Pre-resolve all export bindings and cache them.
78        // After linking, ResolveExport results are stable (per spec),
79        // so we can safely cache them to avoid repeated graph traversals.
80        let mut resolved_bindings = FxHashMap::default();
81        for name in &exports {
82            if let Ok(binding) = module.resolve_export(
83                name,
84                &mut rustc_hash::FxHashSet::default(),
85                context.interner(),
86            ) {
87                resolved_bindings.insert(name.clone(), binding);
88            }
89        }
90
91        // 2. Let internalSlotsList be the internal slots listed in Table 32.
92        // 3. Let M be MakeBasicObject(internalSlotsList).
93        // 4. Set M's essential internal methods to the definitions specified in 10.4.6.
94        // 5. Set M.[[Module]] to module.
95        // 7. Set M.[[Exports]] to sortedExports.
96        // 8. Create own properties of M corresponding to the definitions in 28.3.
97
98        // 9. Set module.[[Namespace]] to M.
99        // Ignored because this is done by `Module::namespace`
100
101        // 10. Return M.
102        context.intrinsics().templates().namespace().create(
103            Self {
104                module,
105                exports,
106                resolved_bindings,
107            },
108            vec![js_string!("Module").into()],
109        )
110    }
111
112    /// Gets the export names of the Module Namespace object.
113    pub(crate) const fn exports(&self) -> &IndexSet<JsString, BuildHasherDefault<FxHasher>> {
114        &self.exports
115    }
116
117    /// Gets the module associated with this namespace.
118    pub(crate) const fn module(&self) -> &Module {
119        &self.module
120    }
121
122    /// Gets a cached resolved binding for the given export name.
123    pub(crate) fn get_resolved_binding(&self, name: &JsString) -> Option<&ResolvedBinding> {
124        self.resolved_bindings.get(name)
125    }
126}
127
128/// [`[[GetPrototypeOf]] ( )`][spec].
129///
130/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof
131#[allow(clippy::unnecessary_wraps)]
132fn module_namespace_exotic_get_prototype_of(
133    _: &JsObject,
134    _: &mut Context,
135) -> JsResult<JsPrototype> {
136    // 1. Return null.
137    Ok(None)
138}
139
140/// [`[[SetPrototypeOf]] ( V )`][spec].
141///
142/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v
143#[allow(clippy::unnecessary_wraps)]
144fn module_namespace_exotic_set_prototype_of(
145    obj: &JsObject,
146    val: JsPrototype,
147    context: &mut Context,
148) -> JsResult<bool> {
149    // 1. Return ! SetImmutablePrototype(O, V).
150    Ok(
151        immutable_prototype_exotic_set_prototype_of(obj, val, context)
152            .js_expect("this must not fail per the spec")?,
153    )
154}
155
156/// [`[[IsExtensible]] ( )`][spec].
157///
158/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-isextensible
159#[allow(clippy::unnecessary_wraps)]
160fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context) -> JsResult<bool> {
161    // 1. Return false.
162    Ok(false)
163}
164
165/// [`[[PreventExtensions]] ( )`][spec].
166///
167/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions
168#[allow(clippy::unnecessary_wraps)]
169fn module_namespace_exotic_prevent_extensions(_: &JsObject, _: &mut Context) -> JsResult<bool> {
170    Ok(true)
171}
172
173/// [`[[GetOwnProperty]] ( P )`][spec]
174///
175/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p
176fn module_namespace_exotic_get_own_property(
177    obj: &JsObject,
178    key: &PropertyKey,
179    context: &mut InternalMethodPropertyContext<'_>,
180) -> JsResult<Option<PropertyDescriptor>> {
181    // 1. If P is a Symbol, return OrdinaryGetOwnProperty(O, P).
182    let key = match key {
183        PropertyKey::Symbol(_) => return ordinary_get_own_property(obj, key, context),
184        PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
185        PropertyKey::String(s) => s.clone(),
186    };
187
188    {
189        let obj = obj
190            .downcast_ref::<ModuleNamespace>()
191            .js_expect("internal method can only be called on module namespace objects")?;
192        // 2. Let exports be O.[[Exports]].
193        let exports = obj.exports();
194
195        // 3. If exports does not contain P, return undefined.
196        if !exports.contains(&key) {
197            return Ok(None);
198        }
199    }
200
201    // 4. Let value be ? O.[[Get]](P, O).
202    let value = obj.get(key, context)?;
203
204    // 5. Return PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false }.
205    Ok(Some(
206        PropertyDescriptor::builder()
207            .value(value)
208            .writable(true)
209            .enumerable(true)
210            .configurable(false)
211            .build(),
212    ))
213}
214
215/// [`[[DefineOwnProperty]] ( P, Desc )`][spec]
216///
217/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc
218fn module_namespace_exotic_define_own_property(
219    obj: &JsObject,
220    key: &PropertyKey,
221    desc: PropertyDescriptor,
222    context: &mut InternalMethodPropertyContext<'_>,
223) -> JsResult<bool> {
224    // 1. If P is a Symbol, return ! OrdinaryDefineOwnProperty(O, P, Desc).
225    if let PropertyKey::Symbol(_) = key {
226        return ordinary_define_own_property(obj, key, desc, context);
227    }
228
229    // 2. Let current be ? O.[[GetOwnProperty]](P).
230    let Some(current) = obj.__get_own_property__(key, context)? else {
231        // 3. If current is undefined, return false.
232        return Ok(false);
233    };
234
235    // 4. If Desc has a [[Configurable]] field and Desc.[[Configurable]] is true, return false.
236    // 5. If Desc has an [[Enumerable]] field and Desc.[[Enumerable]] is false, return false.
237    // 6. If IsAccessorDescriptor(Desc) is true, return false.
238    // 7. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, return false.
239    if desc.configurable() == Some(true)
240        || desc.enumerable() == Some(false)
241        || desc.is_accessor_descriptor()
242        || desc.writable() == Some(false)
243    {
244        return Ok(false);
245    }
246
247    // 8. If Desc has a [[Value]] field, return SameValue(Desc.[[Value]], current.[[Value]]).
248    // 9. Return true.
249    Ok(desc.value().is_none_or(|v| v == current.expect_value()))
250}
251
252/// [`[[HasProperty]] ( P )`][spec]
253///
254/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p
255fn module_namespace_exotic_has_property(
256    obj: &JsObject,
257    key: &PropertyKey,
258    context: &mut InternalMethodPropertyContext<'_>,
259) -> JsResult<bool> {
260    // 1. If P is a Symbol, return ! OrdinaryHasProperty(O, P).
261    let key = match key {
262        PropertyKey::Symbol(_) => return ordinary_has_property(obj, key, context),
263        PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
264        PropertyKey::String(s) => s.clone(),
265    };
266
267    let obj = obj
268        .downcast_ref::<ModuleNamespace>()
269        .js_expect("internal method can only be called on module namespace objects")?;
270
271    // 2. Let exports be O.[[Exports]].
272    let exports = obj.exports();
273
274    // 3. If exports contains P, return true.
275    // 4. Return false.
276    Ok(exports.contains(&key))
277}
278
279/// Internal optimization method for `Module Namespace` exotic objects.
280///
281/// This method combines the internal methods `[[HasProperty]]` and `[[Get]]`.
282///
283/// More information:
284///  - [ECMAScript reference HasProperty][spec0]
285///  - [ECMAScript reference Get][spec1]
286///
287/// [spec0]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p
288/// [spec1]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver
289fn module_namespace_exotic_try_get(
290    obj: &JsObject,
291    key: &PropertyKey,
292    receiver: JsValue,
293    context: &mut InternalMethodPropertyContext<'_>,
294) -> JsResult<Option<JsValue>> {
295    // 1. If P is a Symbol, then
296    //     a. Return ! OrdinaryGet(O, P, Receiver).
297    let key = match key {
298        PropertyKey::Symbol(_) => return ordinary_try_get(obj, key, receiver, context),
299        PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
300        PropertyKey::String(s) => s.clone(),
301    };
302
303    let obj = obj
304        .downcast_ref::<ModuleNamespace>()
305        .js_expect("internal method can only be called on module namespace objects")?;
306
307    // 2. Let exports be O.[[Exports]].
308    let exports = obj.exports();
309
310    // 3. If exports does not contain P, return undefined.
311    let Some(export_name) = exports.get(&key).cloned() else {
312        return Ok(None);
313    };
314
315    // 4. Let m be O.[[Module]].
316    let module = obj.module();
317
318    // 5. Let binding be m.ResolveExport(P).
319    // 6. Assert: binding is a ResolvedBinding Record.
320    // Use the pre-resolved cache when available; fall back to a fresh
321    // ResolveExport call on cache miss for robustness.
322    let fallback;
323    let binding = if let Some(b) = obj.get_resolved_binding(&export_name) {
324        b
325    } else {
326        fallback = module
327            .resolve_export(
328                &export_name,
329                &mut rustc_hash::FxHashSet::default(),
330                context.interner(),
331            )
332            .expect("6. Assert: binding is a ResolvedBinding Record.");
333        &fallback
334    };
335
336    // 7. Let targetModule be binding.[[Module]].
337    // 8. Assert: targetModule is not undefined.
338    let target_module = binding.module();
339
340    if let BindingName::Name(name) = binding.binding_name() {
341        // 10. Let targetEnv be targetModule.[[Environment]].
342        let Some(env) = target_module.environment() else {
343            // 11. If targetEnv is empty, throw a ReferenceError exception.
344            let import = export_name.to_std_string_escaped();
345            return Err(JsNativeError::reference()
346                .with_message(format!(
347                    "cannot get import `{import}` from an uninitialized module"
348                ))
349                .into());
350        };
351
352        let locator = env
353            .kind()
354            .as_module()
355            .js_expect("must be module environment")?
356            .compile()
357            .get_binding(name)
358            .js_expect("checked before that the name was reachable")?;
359
360        // 12. Return ? targetEnv.GetBindingValue(binding.[[BindingName]], true).
361        env.get(locator.binding_index()).map(Some).ok_or_else(|| {
362            let import = export_name.to_std_string_escaped();
363
364            JsNativeError::reference()
365                .with_message(format!("cannot get uninitialized import `{import}`"))
366                .into()
367        })
368    } else {
369        // 9. If binding.[[BindingName]] is namespace, then
370        //     a. Return GetModuleNamespace(targetModule).
371        Ok(Some(target_module.namespace(context).into()))
372    }
373}
374
375/// [`[[Get]] ( P, Receiver )`][spec]
376///
377/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver
378fn module_namespace_exotic_get(
379    obj: &JsObject,
380    key: &PropertyKey,
381    receiver: JsValue,
382    context: &mut InternalMethodPropertyContext<'_>,
383) -> JsResult<JsValue> {
384    // 1. If P is a Symbol, then
385    //     a. Return ! OrdinaryGet(O, P, Receiver).
386    let key = match key {
387        PropertyKey::Symbol(_) => return ordinary_get(obj, key, receiver, context),
388        PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
389        PropertyKey::String(s) => s.clone(),
390    };
391
392    let obj = obj
393        .downcast_ref::<ModuleNamespace>()
394        .js_expect("internal method can only be called on module namespace objects")?;
395
396    // 2. Let exports be O.[[Exports]].
397    let exports = obj.exports();
398    // 3. If exports does not contain P, return undefined.
399    let Some(export_name) = exports.get(&key).cloned() else {
400        return Ok(JsValue::undefined());
401    };
402
403    // 4. Let m be O.[[Module]].
404    let module = obj.module();
405
406    // 5. Let binding be m.ResolveExport(P).
407    // 6. Assert: binding is a ResolvedBinding Record.
408    // Use the pre-resolved cache when available; fall back to a fresh
409    // ResolveExport call on cache miss for robustness.
410    let fallback;
411    let binding = if let Some(b) = obj.get_resolved_binding(&export_name) {
412        b
413    } else {
414        fallback = module
415            .resolve_export(
416                &export_name,
417                &mut rustc_hash::FxHashSet::default(),
418                context.interner(),
419            )
420            .expect("6. Assert: binding is a ResolvedBinding Record.");
421        &fallback
422    };
423
424    // 7. Let targetModule be binding.[[Module]].
425    // 8. Assert: targetModule is not undefined.
426    let target_module = binding.module();
427
428    if let BindingName::Name(name) = binding.binding_name() {
429        // 10. Let targetEnv be targetModule.[[Environment]].
430        let Some(env) = target_module.environment() else {
431            // 11. If targetEnv is empty, throw a ReferenceError exception.
432            let import = export_name.to_std_string_escaped();
433            return Err(JsNativeError::reference()
434                .with_message(format!(
435                    "cannot get import `{import}` from an uninitialized module"
436                ))
437                .into());
438        };
439
440        let locator = env
441            .kind()
442            .as_module()
443            .js_expect("must be module environment")?
444            .compile()
445            .get_binding(name)
446            .js_expect("checked before that the name was reachable")?;
447
448        // 12. Return ? targetEnv.GetBindingValue(binding.[[BindingName]], true).
449        env.get(locator.binding_index()).ok_or_else(|| {
450            let import = export_name.to_std_string_escaped();
451
452            JsNativeError::reference()
453                .with_message(format!("cannot get uninitialized import `{import}`"))
454                .into()
455        })
456    } else {
457        // 9. If binding.[[BindingName]] is namespace, then
458        //     a. Return GetModuleNamespace(targetModule).
459        Ok(target_module.namespace(context).into())
460    }
461}
462
463/// [`[[Set]] ( P, V, Receiver )`][spec].
464///
465/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver
466#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
467fn module_namespace_exotic_set(
468    _obj: &JsObject,
469    _key: PropertyKey,
470    _value: JsValue,
471    _receiver: JsValue,
472    _context: &mut InternalMethodPropertyContext<'_>,
473) -> JsResult<bool> {
474    // 1. Return false.
475    Ok(false)
476}
477
478/// [`[[Delete]] ( P )`][spec].
479///
480/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p
481fn module_namespace_exotic_delete(
482    obj: &JsObject,
483    key: &PropertyKey,
484    context: &mut InternalMethodPropertyContext<'_>,
485) -> JsResult<bool> {
486    // 1. If P is a Symbol, then
487    //     a. Return ! OrdinaryDelete(O, P).
488    let key = match key {
489        PropertyKey::Symbol(_) => return ordinary_delete(obj, key, context),
490        PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
491        PropertyKey::String(s) => s.clone(),
492    };
493
494    let obj = obj
495        .downcast_ref::<ModuleNamespace>()
496        .js_expect("internal method can only be called on module namespace objects")?;
497
498    // 2. Let exports be O.[[Exports]].
499    let exports = obj.exports();
500
501    // 3. If exports contains P, return false.
502    // 4. Return true.
503    Ok(!exports.contains(&key))
504}
505
506/// [`[[OwnPropertyKeys]] ( )`][spec].
507///
508/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys
509fn module_namespace_exotic_own_property_keys(
510    obj: &JsObject,
511    context: &mut Context,
512) -> JsResult<Vec<PropertyKey>> {
513    // 2. Let symbolKeys be OrdinaryOwnPropertyKeys(O).
514    let symbol_keys = ordinary_own_property_keys(obj, context)?;
515
516    let obj = obj
517        .downcast_ref::<ModuleNamespace>()
518        .js_expect("internal method can only be called on module namespace objects")?;
519
520    // 1. Let exports be O.[[Exports]].
521    let exports = obj.exports();
522
523    // 3. Return the list-concatenation of exports and symbolKeys.
524    Ok(exports
525        .iter()
526        .map(|k| PropertyKey::String(k.clone()))
527        .chain(symbol_keys)
528        .collect())
529}