Skip to main content

luaur_rt/
module.rs

1//! Module registration: [`Lua::register_module`] / [`Lua::unload_module`] and a
2//! minimal `require` builtin that resolves registered `@`-prefixed aliases.
3//!
4//! ## Why luaur-rt ships its own `require`
5//!
6//! Luau's full `require` (path resolution, file system navigation) lives in a
7//! separate `luaur-require` crate and is **not** registered by
8//! `luaL_openlibs` — luaur's base library has no `require` global at all (the
9//! same reason `loadstring`/`collectgarbage` are absent). mlua's
10//! `register_module` populates the require *cache* with a named module so that
11//! `require("@alias")` returns it.
12//!
13//! To make the registered-alias half of that surface work (which is all mlua's
14//! `test_register_module` exercises), luaur-rt keeps its own cache table in the
15//! registry and installs a small Rust `require` function that, given an
16//! `@`-prefixed alias, returns the cached module (and errors otherwise). This is
17//! an original implementation over luaur's C API — it does **not** attempt the
18//! filesystem path resolution of the upstream `require`.
19
20use crate::error::{Error, Result};
21use crate::state::Lua;
22use crate::table::Table;
23use crate::traits::IntoLua;
24use crate::value::Value;
25
26/// The registry key under which the alias -> module cache table is stored.
27const MODULE_CACHE_KEY: &str = "__luaur_rt_modules";
28
29impl Lua {
30    /// Fetch (creating if absent) the registry-stored module cache table, and
31    /// ensure a `require` global is installed that consults it.
32    fn module_cache(&self) -> Result<Table> {
33        // Look up the cache table in the named registry; create it on first use.
34        if let Ok(t) = self.named_registry_value::<Table>(MODULE_CACHE_KEY) {
35            self.ensure_require_installed(&t)?;
36            return Ok(t);
37        }
38        let t = self.create_table();
39        self.set_named_registry_value(MODULE_CACHE_KEY, &t)?;
40        self.ensure_require_installed(&t)?;
41        Ok(t)
42    }
43
44    /// Install the `require` global if it is not already present.
45    fn ensure_require_installed(&self, cache: &Table) -> Result<()> {
46        let globals = self.globals();
47        if globals.contains_key("require")? {
48            return Ok(());
49        }
50        let cache = cache.clone();
51        let require = self.create_function(move |_lua, name: String| {
52            // Try the exact alias first, then a case-insensitive fallback.
53            let exact = cache.get::<Value>(name.as_str())?;
54            let resolved = match exact {
55                Value::Nil => cache.get::<Value>(name.to_ascii_lowercase())?,
56                v => v,
57            };
58            match resolved {
59                Value::Nil => Err(Error::runtime(format!(
60                    "module '{name}' not found: module was not registered"
61                ))),
62                v => Ok(v),
63            }
64        })?;
65        globals.set("require", require)?;
66        Ok(())
67    }
68
69    /// Register `module` under the alias `name` so `require(name)` returns it.
70    /// Mirrors `mlua::Lua::register_module`.
71    ///
72    /// As in Luau, a registered module alias must begin with `'@'`; a name
73    /// without the prefix is rejected with a runtime error. Lookups are
74    /// case-insensitive on the alias (matching Luau's registered-alias rules).
75    pub fn register_module(&self, name: &str, module: impl IntoLua) -> Result<()> {
76        if !name.starts_with('@') {
77            return Err(Error::runtime("module name must begin with '@'"));
78        }
79        let value = module.into_lua(self)?;
80        let cache = self.module_cache()?;
81        // Store under both the exact alias and a lower-cased form so a
82        // case-insensitive `require` resolves either.
83        cache.set(name, value.clone())?;
84        cache.set(name.to_ascii_lowercase(), value)?;
85        Ok(())
86    }
87
88    /// Remove a previously registered module alias. Mirrors
89    /// `mlua::Lua::unload_module`.
90    pub fn unload_module(&self, name: &str) -> Result<()> {
91        let cache = self.module_cache()?;
92        cache.set(name, Value::Nil)?;
93        cache.set(name.to_ascii_lowercase(), Value::Nil)?;
94        Ok(())
95    }
96}