Async require now mostly working

This commit is contained in:
Filip Tibell 2023-03-20 14:51:19 +01:00
parent 6318176516
commit 0975a6180b
No known key found for this signature in database

View file

@ -19,18 +19,18 @@ local source = info(1, "s")
if source == '[string "require"]' then if source == '[string "require"]' then
source = info(2, "s") source = info(2, "s")
end end
local absolute, relative = importer:paths(source, ...) local absolute, relative = paths(context, source, ...)
return importer:load(thread(), absolute, relative) return load(context, absolute, relative)
"#; "#;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct Importer<'lua> { struct RequireContext<'lua> {
builtins: HashMap<String, LuaMultiValue<'lua>>, builtins: HashMap<String, LuaMultiValue<'lua>>,
cached: RefCell<HashMap<String, LuaResult<LuaMultiValue<'lua>>>>, cached: RefCell<HashMap<String, LuaResult<LuaMultiValue<'lua>>>>,
pwd: String, pwd: String,
} }
impl<'lua> Importer<'lua> { impl<'lua> RequireContext<'lua> {
pub fn new() -> Self { pub fn new() -> Self {
let mut pwd = current_dir() let mut pwd = current_dir()
.expect("Failed to access current working directory") .expect("Failed to access current working directory")
@ -44,134 +44,136 @@ impl<'lua> Importer<'lua> {
..Default::default() ..Default::default()
} }
} }
}
fn paths(&self, require_source: String, require_path: String) -> LuaResult<(String, String)> { impl<'lua> LuaUserData for RequireContext<'lua> {}
if require_path.starts_with('@') {
return Ok((require_path.clone(), require_path)); fn paths(
} context: RequireContext,
let path_relative_to_pwd = PathBuf::from( require_source: String,
&require_source require_path: String,
.trim_start_matches("[string \"") ) -> LuaResult<(String, String)> {
.trim_end_matches("\"]"), if require_path.starts_with('@') {
) return Ok((require_path.clone(), require_path));
.parent()
.unwrap()
.join(&require_path);
// Try to normalize and resolve relative path segments such as './' and '../'
let file_path = match (
canonicalize(path_relative_to_pwd.with_extension("luau")),
canonicalize(path_relative_to_pwd.with_extension("lua")),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
_ => {
return Err(LuaError::RuntimeError(format!(
"File does not exist at path '{require_path}'"
)))
}
};
let absolute = file_path.to_string_lossy().to_string();
let relative = absolute.trim_start_matches(&self.pwd).to_string();
Ok((absolute, relative))
} }
let path_relative_to_pwd = PathBuf::from(
fn load_builtin(&self, module_name: &str) -> LuaResult<LuaMultiValue> { &require_source
match self.builtins.get(module_name) { .trim_start_matches("[string \"")
Some(module) => Ok(module.clone()), .trim_end_matches("\"]"),
None => Err(LuaError::RuntimeError(format!( )
"No builtin module exists with the name '{}'", .parent()
module_name .unwrap()
))), .join(&require_path);
// Try to normalize and resolve relative path segments such as './' and '../'
let file_path = match (
canonicalize(path_relative_to_pwd.with_extension("luau")),
canonicalize(path_relative_to_pwd.with_extension("lua")),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
_ => {
return Err(LuaError::RuntimeError(format!(
"File does not exist at path '{require_path}'"
)))
} }
} };
let absolute = file_path.to_string_lossy().to_string();
let relative = absolute.trim_start_matches(&context.pwd).to_string();
Ok((absolute, relative))
}
async fn load_file( fn load_builtin<'lua>(
&self, _lua: &'lua Lua,
lua: &'lua Lua, context: RequireContext<'lua>,
absolute_path: String, module_name: String,
relative_path: String, ) -> LuaResult<LuaMultiValue<'lua>> {
) -> LuaResult<LuaMultiValue> { match context.builtins.get(&module_name) {
let cached = { self.cached.borrow().get(&absolute_path).cloned() }; Some(module) => Ok(module.clone()),
match cached { None => Err(LuaError::RuntimeError(format!(
Some(cached) => cached, "No builtin module exists with the name '{}'",
None => { module_name
// Try to read the wanted file, note that we use bytes instead of reading ))),
// to a string since lua scripts are not necessarily valid utf-8 strings
let contents = fs::read(&absolute_path).await.map_err(LuaError::external)?;
// Use a name without extensions for loading the chunk, some
// other code assumes the require path is without extensions
let path_relative_no_extension = relative_path
.trim_end_matches(".lua")
.trim_end_matches(".luau");
// Load the file into a thread
let loaded_func = lua
.load(&contents)
.set_name(path_relative_no_extension)?
.into_function()?;
let loaded_thread = lua.create_thread(loaded_func)?;
// Run the thread and provide a channel that will
// then get its result received when it finishes
let (tx, rx) = oneshot::channel();
{
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
sched.set_task_result_sender(task, tx);
}
// Wait for the thread to finish running, cache + return our result
let rets = rx.await.expect("Sender was dropped during require");
self.cached.borrow_mut().insert(absolute_path, rets.clone());
rets
}
}
} }
}
async fn load( async fn load_file<'lua>(
&self, lua: &'lua Lua,
lua: &'lua Lua, context: RequireContext<'lua>,
absolute_path: String, absolute_path: String,
relative_path: String, relative_path: String,
) -> LuaResult<LuaMultiValue> { ) -> LuaResult<LuaMultiValue<'lua>> {
if absolute_path == relative_path && absolute_path.starts_with('@') { let cached = { context.cached.borrow().get(&absolute_path).cloned() };
if let Some(module_name) = absolute_path.strip_prefix("@lune/") { match cached {
self.load_builtin(module_name) Some(cached) => cached,
} else { None => {
Err(LuaError::RuntimeError( // Try to read the wanted file, note that we use bytes instead of reading
"Require paths prefixed by '@' are not yet supported".to_string(), // to a string since lua scripts are not necessarily valid utf-8 strings
)) let contents = fs::read(&absolute_path).await.map_err(LuaError::external)?;
// Use a name without extensions for loading the chunk, some
// other code assumes the require path is without extensions
let path_relative_no_extension = relative_path
.trim_end_matches(".lua")
.trim_end_matches(".luau");
// Load the file into a thread
let loaded_func = lua
.load(&contents)
.set_name(path_relative_no_extension)?
.into_function()?;
let loaded_thread = lua.create_thread(loaded_func)?;
// Run the thread and provide a channel that will
// then get its result received when it finishes
let (tx, rx) = oneshot::channel();
{
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
sched.set_task_result_sender(task, tx);
} }
} else { // Wait for the thread to finish running, cache + return our result
self.load_file(lua, absolute_path, relative_path).await // FIXME: This waits indefinitely for nested requires for some reason
let rets = rx.await.expect("Sender was dropped during require");
context
.cached
.borrow_mut()
.insert(absolute_path, rets.clone());
rets
} }
} }
} }
impl<'i> LuaUserData for Importer<'i> { async fn load<'lua>(
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { lua: &'lua Lua,
methods.add_method( context: RequireContext<'lua>,
"paths", absolute_path: String,
|_, this, (require_source, require_path): (String, String)| { relative_path: String,
this.paths(require_source, require_path) ) -> LuaResult<LuaMultiValue<'lua>> {
}, if absolute_path == relative_path && absolute_path.starts_with('@') {
); if let Some(module_name) = absolute_path.strip_prefix("@lune/") {
methods.add_method( load_builtin(lua, context, module_name.to_string())
"load", } else {
|lua, this, (thread, absolute_path, relative_path): (LuaThread, String, String)| { Err(LuaError::RuntimeError(
// TODO: Make this work "Require paths prefixed by '@' are not yet supported".to_string(),
// this.load(lua, absolute_path, relative_path) ))
Ok(()) }
}, } else {
); load_file(lua, context, absolute_path, relative_path).await
} }
} }
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> { pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
let require_importer = Importer::new(); let require_context = RequireContext::new();
let require_thread: LuaFunction = lua.named_registry_value("co.thread")?; let require_print: LuaFunction = lua.named_registry_value("print")?;
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?; let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
let require_env = TableBuilder::new(lua)? let require_env = TableBuilder::new(lua)?
.with_value("importer", require_importer)? .with_value("context", require_context)?
.with_value("thread", require_thread)? .with_value("print", require_print)?
.with_value("info", require_info)? .with_value("info", require_info)?
.with_function("paths", |_, (context, require_source, require_path)| {
paths(context, require_source, require_path)
})?
.with_async_function("load", |lua, (context, require_source, require_path)| {
load(lua, context, require_source, require_path)
})?
.build_readonly()?; .build_readonly()?;
let require_fn_lua = lua let require_fn_lua = lua