Skip to main content

quickjs_oxide/engine/code/
module.rs

1//! Runtime-independent ECMAScript module drafts.
2//!
3//! QuickJS keeps a `JSModuleDef` beside the compiled module function.  This
4//! module is the Rust ownership boundary for the same information: parsing
5//! produces request/import/export tables whose local binding indices point at
6//! authenticated closure slots on the module root function. The register
7//! lowering (`lower_module_tree_into`) reads the whole draft and turns it into
8//! the core's module source.
9
10use crate::engine::code::function::UnlinkedFunction;
11use crate::engine::value::JsString;
12
13/// Rust spelling of QuickJS's private `JS_ATOM__default_` module binding.
14/// Source text cannot name this cell.
15pub const MODULE_DEFAULT_BINDING_NAME: &str = "<module-default>";
16
17/// Unspellable compiler/runtime cell which caches one module's `import.meta`.
18pub const MODULE_IMPORT_META_BINDING_NAME: &str = "<import.meta>";
19
20/// Source-order index into [`UnlinkedModule::requested_modules`].
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct ModuleRequestIndex(pub u32);
23
24/// One decoded entry from a static import declaration's `with` clause.
25///
26/// QuickJS builds the corresponding null-prototype object in source order.
27/// Keeping the ordered entries structural here preserves that observable
28/// enumeration order without introducing a JavaScript heap object during
29/// parsing.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct ModuleImportAttribute {
32    /// Decoded property key, after identifier and string-literal escapes.
33    pub key: JsString,
34    /// Decoded StringLiteral value.
35    pub value: JsString,
36}
37
38/// Syntactic import-attribute state for one requested module.
39///
40/// `Present([])` deliberately differs from [`Self::Absent`] so compiler and
41/// source tooling can retain whether `with {}` was authored. Pinned QuickJS
42/// does not allocate an attributes object until the first entry, however, so
43/// both states collapse to `None` through [`Self::effective`] at the host
44/// checker and loader boundary.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum ModuleImportAttributes {
47    /// No `with` clause was authored.
48    Absent,
49    /// An authored `with` clause, possibly empty.
50    Present(Box<[ModuleImportAttribute]>),
51}
52
53impl ModuleImportAttributes {
54    /// Attributes exposed to QuickJS-compatible host hooks.
55    #[must_use]
56    pub fn effective(&self) -> Option<&[ModuleImportAttribute]> {
57        match self {
58            Self::Present(attributes) if !attributes.is_empty() => Some(attributes),
59            Self::Absent | Self::Present(_) => None,
60        }
61    }
62
63    /// Entries from an authored `with` clause, including an empty clause.
64    #[must_use]
65    pub fn syntactic(&self) -> Option<&[ModuleImportAttribute]> {
66        match self {
67            Self::Absent => None,
68            Self::Present(attributes) => Some(attributes),
69        }
70    }
71}
72
73/// One normalized-later module specifier from an import or re-export.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ModuleRequest {
76    pub specifier: JsString,
77    pub attributes: ModuleImportAttributes,
78}
79
80/// One imported binding linked to a root closure slot.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ModuleImport {
83    pub request: ModuleRequestIndex,
84    pub import_name: ModuleImportName,
85    pub closure_index: u16,
86}
87
88/// The target selected from one requested module.
89///
90/// QuickJS represents the namespace case with its private `JS_ATOM__star_`
91/// atom. Keeping it outside the JavaScript string domain prevents a real
92/// exported name `"*"` from being mistaken for that implementation sentinel.
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub enum ModuleImportName {
95    Name(JsString),
96    Namespace,
97}
98
99/// Compiler-authored value installed into one hoisted module declaration
100/// during the synthetic link entry.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum ModuleLinkInitializerValue {
103    Undefined,
104    Function {
105        constant: u32,
106        /// Optional root String constant consumed by QuickJS `OP_set_name`
107        /// between closure creation and the declaration-cell write. This is
108        /// present only for an anonymous default function declaration.
109        inferred_name: Option<u32>,
110    },
111}
112
113/// One exact link-entry write to a non-lexical module declaration cell.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct ModuleLinkInitializer {
116    pub closure_index: u16,
117    pub value: ModuleLinkInitializerValue,
118}
119
120/// One pinned QuickJS import/declaration name collision.
121///
122/// QuickJS resolves both records to the import's first closure slot. Ordinary
123/// writes remain read-only, while declaration initialization has a narrowly
124/// authenticated raw-write path.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub struct ModuleImportCollision {
127    pub closure_index: u16,
128    pub declaration: ModuleImportCollisionDeclaration,
129}
130
131/// Declaration flavor sharing an import's effective closure slot.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum ModuleImportCollisionDeclaration {
134    Var,
135    Lexical,
136    Function,
137}
138
139/// Resolved target of one exported name.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub enum ModuleExportTarget {
142    /// Exact root closure slot for a local declaration or imported binding.
143    Local { closure_index: u16 },
144    /// Named re-export resolved through a requested module.
145    Indirect {
146        request: ModuleRequestIndex,
147        import_name: ModuleImportName,
148    },
149}
150
151/// One non-star export. Export names are stored as JavaScript strings because
152/// current ECMAScript permits string-literal names in import/export clauses.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub struct ModuleExport {
155    pub export_name: JsString,
156    pub target: ModuleExportTarget,
157}
158
159/// One `export * from` edge.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub struct ModuleStarExport {
162    pub request: ModuleRequestIndex,
163}
164
165/// Mutable compiler output which has not entered a runtime domain yet.
166#[derive(Debug)]
167pub struct UnlinkedModule {
168    function: UnlinkedFunction,
169    has_top_level_await: bool,
170    declaration_order: Box<[u16]>,
171    link_initializers: Box<[ModuleLinkInitializer]>,
172    import_collisions: Box<[ModuleImportCollision]>,
173    requested_modules: Box<[ModuleRequest]>,
174    imports: Box<[ModuleImport]>,
175    exports: Box<[ModuleExport]>,
176    star_exports: Box<[ModuleStarExport]>,
177}
178
179/// Compiler-owned module tables, sealed into one [`UnlinkedModule`].
180pub struct UnlinkedModuleTables {
181    pub declaration_order: Vec<u16>,
182    pub link_initializers: Vec<ModuleLinkInitializer>,
183    pub import_collisions: Vec<ModuleImportCollision>,
184    pub requested_modules: Vec<ModuleRequest>,
185    pub imports: Vec<ModuleImport>,
186    pub exports: Vec<ModuleExport>,
187    pub star_exports: Vec<ModuleStarExport>,
188}
189
190impl UnlinkedModule {
191    #[must_use]
192    pub fn new(
193        function: UnlinkedFunction,
194        has_top_level_await: bool,
195        tables: UnlinkedModuleTables,
196    ) -> Self {
197        let UnlinkedModuleTables {
198            declaration_order,
199            link_initializers,
200            import_collisions,
201            requested_modules,
202            imports,
203            exports,
204            star_exports,
205        } = tables;
206        Self {
207            function,
208            has_top_level_await,
209            declaration_order: declaration_order.into_boxed_slice(),
210            link_initializers: link_initializers.into_boxed_slice(),
211            import_collisions: import_collisions.into_boxed_slice(),
212            requested_modules: requested_modules.into_boxed_slice(),
213            imports: imports.into_boxed_slice(),
214            exports: exports.into_boxed_slice(),
215            star_exports: star_exports.into_boxed_slice(),
216        }
217    }
218
219    #[must_use]
220    #[cfg_attr(not(test), allow(dead_code))]
221    pub const fn function(&self) -> &UnlinkedFunction {
222        &self.function
223    }
224
225    #[must_use]
226    #[cfg_attr(not(test), allow(dead_code))]
227    pub const fn has_top_level_await(&self) -> bool {
228        self.has_top_level_await
229    }
230
231    #[must_use]
232    #[cfg_attr(not(test), allow(dead_code))]
233    pub const fn declaration_order(&self) -> &[u16] {
234        &self.declaration_order
235    }
236
237    #[must_use]
238    #[cfg_attr(not(test), allow(dead_code))]
239    pub const fn link_initializers(&self) -> &[ModuleLinkInitializer] {
240        &self.link_initializers
241    }
242
243    #[must_use]
244    #[cfg_attr(not(test), allow(dead_code))]
245    pub const fn import_collisions(&self) -> &[ModuleImportCollision] {
246        &self.import_collisions
247    }
248
249    #[must_use]
250    #[cfg_attr(not(test), allow(dead_code))]
251    pub const fn requested_modules(&self) -> &[ModuleRequest] {
252        &self.requested_modules
253    }
254
255    #[must_use]
256    #[cfg_attr(not(test), allow(dead_code))]
257    pub const fn imports(&self) -> &[ModuleImport] {
258        &self.imports
259    }
260
261    #[must_use]
262    #[cfg_attr(not(test), allow(dead_code))]
263    pub const fn exports(&self) -> &[ModuleExport] {
264        &self.exports
265    }
266
267    #[must_use]
268    #[cfg_attr(not(test), allow(dead_code))]
269    pub const fn star_exports(&self) -> &[ModuleStarExport] {
270        &self.star_exports
271    }
272}