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}