Skip to main content

quickjs_oxide/engine/api/core/
module.rs

1//! Module loading: the embedder's
2//! [`ModuleLoader`], the [`ModuleScope`] a host callback receives, and the shims
3//! the core's module host calls.
4
5use std::rc::Rc;
6
7use oxide_core::vm::compile::{CompileError, Compiler};
8
9use super::RuntimeError;
10use super::compile::{
11    ModuleCompileError, compile_module, front_compile_error, normalize_module_name,
12};
13use super::function::{FunctionOptions, HostFunction, host_body};
14use super::runtime::{Preloaded, Shared, current, slot};
15use super::value::{CallableRef, ObjectRef, SymbolRef, Value};
16use crate::engine::api::error::NativeErrorKind;
17use crate::engine::code::module::{ModuleImportAttribute, ModuleImportAttributes};
18use crate::engine::value::JsString;
19
20/// What a loader returns for a module name.
21pub enum ModuleSource {
22    /// JavaScript module source, with the properties its `import.meta`
23    /// starts with.
24    Source {
25        text: Vec<u8>,
26        import_meta: Vec<(String, Value)>,
27    },
28    /// A JSON module.
29    Json(Vec<u8>),
30    /// A JSON5 module (`type: "json5"`).
31    Json5(Vec<u8>),
32    /// A module compiled through [`ModuleScope::compile_module`].
33    Compiled(CompiledModule),
34    /// A module whose exports are these values, each under its name;
35    /// `default` is the default export.
36    Synthetic { exports: Vec<(String, Value)> },
37}
38
39impl ModuleSource {
40    /// Module source with an empty `import.meta`.
41    pub fn source(text: impl Into<Vec<u8>>) -> Self {
42        ModuleSource::Source {
43            text: text.into(),
44            import_meta: Vec::new(),
45        }
46    }
47}
48
49/// A module compiled through a [`ModuleScope`], to return from the loader.
50pub struct CompiledModule {
51    function: ObjectRef,
52    tables: oxide_core::vm::module::ModuleTables,
53    import_meta: Vec<(String, Value)>,
54}
55
56/// The embedder's module loader. Errors are values to throw, made through
57/// the [`ModuleScope`].
58pub trait ModuleLoader {
59    /// The normalized name of `specifier` imported by the module or script
60    /// named `referrer`. The default is QuickJS's: a specifier starting with
61    /// `.` resolves against the referrer's directory.
62    fn normalize(
63        &mut self,
64        scope: &mut ModuleScope<'_>,
65        referrer: &str,
66        specifier: &str,
67        attributes: &ModuleImportAttributes,
68    ) -> Result<String, Value> {
69        let _ = (scope, attributes);
70        Ok(normalize_module_name(referrer, specifier))
71    }
72
73    /// The module named `name` (normalized).
74    fn load(
75        &mut self,
76        scope: &mut ModuleScope<'_>,
77        name: &str,
78        attributes: &ModuleImportAttributes,
79    ) -> Result<ModuleSource, Value>;
80}
81
82/// A module loader's view of the runtime: creating objects, errors, host
83/// functions and host objects, compiling a module source. It is the
84/// compile-time view (no job queue, no running script), so it has these
85/// operations and not those of [`Js`](super::Js). Values made through it are
86/// handles like any other and outlive the callback.
87pub struct ModuleScope<'a> {
88    compiler: Compiler<'a>,
89    shared: &'a Rc<Shared>,
90}
91
92impl ModuleScope<'_> {
93    fn hold(&mut self, value: oxide_core::Value) -> Value {
94        use oxide_core::value::Kind;
95        match value.kind() {
96            Kind::Object => Value::Object(ObjectRef {
97                slot: slot(self.shared, self.compiler.hold(value), value),
98            }),
99            Kind::Symbol => Value::Symbol(SymbolRef {
100                slot: slot(self.shared, self.compiler.hold(value), value),
101            }),
102            Kind::String => {
103                let units: Vec<u16> = self
104                    .compiler
105                    .heap()
106                    .string(value.as_string().expect("a string"))
107                    .repr()
108                    .units()
109                    .collect();
110                JsString::try_from_utf16(units).map_or(Value::Undefined, Value::String)
111            }
112            Kind::Bool => Value::Bool(value.as_bool().unwrap_or(false)),
113            Kind::Int32 => Value::Int(value.as_int32().unwrap_or(0)),
114            Kind::Number => Value::Float(value.as_f64().unwrap_or(f64::NAN)),
115            Kind::Null => Value::Null,
116            _ => Value::Undefined,
117        }
118    }
119
120    /// A new ordinary object.
121    pub fn new_object(&mut self) -> Result<ObjectRef, RuntimeError> {
122        let prototype = self
123            .compiler
124            .intrinsic(oxide_core::Intrinsic::ObjectPrototype);
125        let object = self.compiler.heap_mut().alloc_object(
126            oxide_core::ObjectKind::Ordinary,
127            Some(prototype),
128            4,
129        )?;
130        match self.hold(oxide_core::Value::object(object)) {
131            Value::Object(object) => Ok(object),
132            _ => Err(RuntimeError::Invariant("a new object")),
133        }
134    }
135
136    /// A host function, as [`Js::new_function`](super::Js::new_function)
137    /// makes one.
138    pub fn new_function<Args>(
139        &mut self,
140        options: FunctionOptions,
141        function: impl HostFunction<Args>,
142    ) -> Result<CallableRef, RuntimeError> {
143        let mut data = Vec::with_capacity(options.data.len());
144        for value in &options.data {
145            data.push(self.lower(value));
146        }
147        let body = host_body(self.shared, function);
148        let object = self.compiler.new_host_function(
149            &options.name,
150            options.length,
151            options.constructor,
152            &data,
153            body,
154        )?;
155        match self.hold(oxide_core::Value::object(object)) {
156            Value::Object(object) => Ok(CallableRef(object)),
157            _ => Err(RuntimeError::Invariant("a new function")),
158        }
159    }
160
161    /// A host object, as [`Js::new_host_object`](super::Js::new_host_object)
162    /// makes one.
163    pub fn new_host_object<T: 'static>(
164        &mut self,
165        value: T,
166        prototype: Option<&ObjectRef>,
167    ) -> Result<ObjectRef, RuntimeError> {
168        let prototype = prototype.and_then(|prototype| prototype.core_value().as_object());
169        let object = self
170            .compiler
171            .new_host_object(prototype, std::rc::Rc::new(value))?;
172        match self.hold(oxide_core::Value::object(object)) {
173            Value::Object(object) => Ok(object),
174            _ => Err(RuntimeError::Invariant("a new host object")),
175        }
176    }
177
178    /// A new error object of `kind` with `message`.
179    pub fn new_native_error(
180        &mut self,
181        kind: NativeErrorKind,
182        message: &str,
183    ) -> Result<Value, RuntimeError> {
184        let error = self.compiler.new_error(core_error_kind(kind), message)?;
185        Ok(self.hold(error))
186    }
187
188    /// Compiles `text` as the module `name` (its position tables and
189    /// functions name it). A syntax error is returned as the error to throw.
190    pub fn compile_module(
191        &mut self,
192        name: &str,
193        text: &[u8],
194        import_meta: Vec<(String, Value)>,
195    ) -> Result<CompiledModule, Value> {
196        let debug_info = self.shared.debug_info.get();
197        match compile_module(&mut self.compiler, name, text, debug_info) {
198            Ok((
199                oxide_core::vm::module::ModuleSource::Compiled {
200                    function, tables, ..
201                },
202                _,
203            )) => {
204                let Value::Object(function) = self.hold(oxide_core::Value::object(function)) else {
205                    unreachable!("a module function is an object");
206                };
207                Ok(CompiledModule {
208                    function,
209                    tables,
210                    import_meta,
211                })
212            }
213            Ok(_) => Err(Value::Undefined),
214            Err(error) => {
215                let failure = match error {
216                    ModuleCompileError::Front(error) => front_compile_error(&error, text, name),
217                    ModuleCompileError::Core(error) => error,
218                };
219                let thrown = throw_value(&mut self.compiler, &failure);
220                Err(self.hold(thrown))
221            }
222        }
223    }
224
225    /// The core value of `value`, kept alive until the next entry.
226    fn lower(&mut self, value: &Value) -> oxide_core::Value {
227        let lowered = match value {
228            Value::Undefined => oxide_core::Value::UNDEFINED,
229            Value::Null => oxide_core::Value::NULL,
230            Value::Bool(value) => oxide_core::Value::bool(*value),
231            Value::Int(value) => oxide_core::Value::int32(*value),
232            Value::Float(value) => oxide_core::Value::number(*value),
233            Value::String(text) => {
234                let units: Vec<u16> = text.utf16_units().collect();
235                match self.compiler.heap_mut().alloc_string_units(&units) {
236                    Ok(id) => oxide_core::Value::string(id),
237                    Err(_) => oxide_core::Value::UNDEFINED,
238                }
239            }
240            Value::BigInt(_) => oxide_core::Value::UNDEFINED,
241            Value::Symbol(symbol) => symbol.core_value(),
242            Value::Object(object) => object.core_value(),
243        };
244        // A temporary handle, released by the next entry.
245        if lowered.is_heap() {
246            let handle = self.compiler.hold(lowered);
247            self.shared.release(handle);
248        }
249        lowered
250    }
251}
252
253fn core_error_kind(kind: NativeErrorKind) -> oxide_core::ErrorKind {
254    match kind {
255        NativeErrorKind::Eval => oxide_core::ErrorKind::Eval,
256        NativeErrorKind::Range => oxide_core::ErrorKind::Range,
257        NativeErrorKind::Reference => oxide_core::ErrorKind::Reference,
258        NativeErrorKind::Syntax => oxide_core::ErrorKind::Syntax,
259        NativeErrorKind::Type => oxide_core::ErrorKind::Type,
260        NativeErrorKind::Uri => oxide_core::ErrorKind::Uri,
261        NativeErrorKind::Internal => oxide_core::ErrorKind::Internal,
262        NativeErrorKind::Aggregate => oxide_core::ErrorKind::Aggregate,
263    }
264}
265
266/// The error a failed module compilation throws, as a value (no backtrace:
267/// no frame runs the module yet).
268fn throw_value(compiler: &mut Compiler<'_>, failure: &CompileError) -> oxide_core::Value {
269    let created = match &failure.location {
270        Some((file, line, column)) => {
271            compiler.new_located_error(failure.kind, &failure.message, file, *line, *column)
272        }
273        None => compiler.new_error(failure.kind, &failure.message),
274    };
275    created.unwrap_or(oxide_core::Value::UNDEFINED)
276}
277
278/// The attributes the core passes, in the front end's form.
279fn attributes(pairs: &[(String, String)]) -> ModuleImportAttributes {
280    if pairs.is_empty() {
281        return ModuleImportAttributes::Absent;
282    }
283    let attributes: Vec<ModuleImportAttribute> = pairs
284        .iter()
285        .filter_map(|(key, value)| {
286            Some(ModuleImportAttribute {
287                key: JsString::try_from_utf8(key).ok()?,
288                value: JsString::try_from_utf8(value).ok()?,
289            })
290        })
291        .collect();
292    ModuleImportAttributes::Present(attributes.into_boxed_slice())
293}
294
295/// The core's `normalize` hook: the embedder's loader, or QuickJS's rule.
296pub(crate) fn normalize_shim(
297    compiler: &mut Compiler<'_>,
298    referrer: &str,
299    specifier: &str,
300    pairs: &[(String, String)],
301) -> Result<String, CompileError> {
302    let Some(shared) = current() else {
303        return Ok(normalize_module_name(referrer, specifier));
304    };
305    let Ok(mut loader) = shared.loader.try_borrow_mut() else {
306        return Err(CompileError::new(
307            oxide_core::ErrorKind::Internal,
308            "the module loader was re-entered",
309        ));
310    };
311    let Some(loader) = loader.as_mut() else {
312        return Ok(normalize_module_name(referrer, specifier));
313    };
314    let mut scope = ModuleScope {
315        compiler: compiler.reborrow(),
316        shared: &shared,
317    };
318    loader
319        .normalize(&mut scope, referrer, specifier, &attributes(pairs))
320        .map_err(|thrown| CompileError::thrown(scope.lower(&thrown)))
321}
322
323/// The core's `load` hook: a module `Context::compile_module` compiled, or
324/// the embedder's loader.
325pub(crate) fn load_shim(
326    compiler: &mut Compiler<'_>,
327    name: &str,
328    pairs: &[(String, String)],
329) -> Result<oxide_core::vm::module::ModuleSource, CompileError> {
330    let Some(shared) = current() else {
331        return Err(CompileError::new(
332            oxide_core::ErrorKind::Internal,
333            "unsupported: modules",
334        ));
335    };
336    let preloaded = shared.modules.borrow_mut().remove(name);
337    if let Some(preloaded) = preloaded {
338        let mut scope = ModuleScope {
339            compiler: compiler.reborrow(),
340            shared: &shared,
341        };
342        return Ok(match preloaded {
343            Preloaded::Compiled {
344                function,
345                tables,
346                import_meta,
347            } => compiled_source(&mut scope, function, tables, &import_meta),
348            Preloaded::Synthetic(exports) => synthetic_source(&mut scope, &exports),
349        });
350    }
351    let Ok(mut loader) = shared.loader.try_borrow_mut() else {
352        return Err(CompileError::new(
353            oxide_core::ErrorKind::Internal,
354            "the module loader was re-entered",
355        ));
356    };
357    let Some(loader) = loader.as_mut() else {
358        return Err(CompileError::new(
359            oxide_core::ErrorKind::Reference,
360            format!("could not load module filename '{name}'"),
361        ));
362    };
363    let mut scope = ModuleScope {
364        compiler: compiler.reborrow(),
365        shared: &shared,
366    };
367    let source = loader
368        .load(&mut scope, name, &attributes(pairs))
369        .map_err(|thrown| CompileError::thrown(scope.lower(&thrown)))?;
370    match source {
371        ModuleSource::Source { text, import_meta } => {
372            let module = scope
373                .compile_module(name, &text, import_meta)
374                .map_err(|thrown| CompileError::thrown(scope.lower(&thrown)))?;
375            Ok(compiled_source(
376                &mut scope,
377                module.function,
378                module.tables,
379                &module.import_meta,
380            ))
381        }
382        ModuleSource::Compiled(module) => Ok(compiled_source(
383            &mut scope,
384            module.function,
385            module.tables,
386            &module.import_meta,
387        )),
388        ModuleSource::Synthetic { exports } => Ok(synthetic_source(&mut scope, &exports)),
389        ModuleSource::Json(text) => {
390            Ok(oxide_core::vm::module::ModuleSource::Json(json_text(&text)))
391        }
392        ModuleSource::Json5(text) => Ok(oxide_core::vm::module::ModuleSource::Json5(json_text(
393            &text,
394        ))),
395    }
396}
397
398/// The core's form of a synthetic module: its values, rooted until the next
399/// entry.
400fn synthetic_source(
401    scope: &mut ModuleScope<'_>,
402    exports: &[(String, Value)],
403) -> oxide_core::vm::module::ModuleSource {
404    oxide_core::vm::module::ModuleSource::Synthetic {
405        exports: exports
406            .iter()
407            .map(|(name, value)| (name.clone(), scope.lower(value)))
408            .collect(),
409    }
410}
411
412/// A JSON module's bytes as QuickJS's byte-level parser reads them.
413fn json_text(bytes: &[u8]) -> oxide_core::vm::module::JsonText {
414    oxide_core::vm::module::JsonText::from_bytes(bytes, crate::engine::value::decode_quickjs_utf8)
415}
416
417fn compiled_source(
418    scope: &mut ModuleScope<'_>,
419    function: ObjectRef,
420    tables: oxide_core::vm::module::ModuleTables,
421    import_meta: &[(String, Value)],
422) -> oxide_core::vm::module::ModuleSource {
423    let import_meta = import_meta
424        .iter()
425        .map(|(key, value)| (key.clone(), scope.lower(value)))
426        .collect();
427    let id = function
428        .core_value()
429        .as_object()
430        .expect("a module function");
431    // The function stays rooted by a handle released at the next entry.
432    let _ = scope.lower(&Value::Object(function));
433    oxide_core::vm::module::ModuleSource::Compiled {
434        function: id,
435        tables,
436        import_meta,
437    }
438}