From 04a47babddcb05095c66e1fb2ac23f2e6bc8d50b Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 16 Feb 2023 16:19:58 +0100 Subject: [PATCH] Implement traceback helper to use in scheduler --- packages/lib/src/globals/task.rs | 6 +- packages/lib/src/lib.rs | 15 +---- packages/lib/src/lua/create.rs | 93 +++++++++++++++++++++++++++++ packages/lib/src/lua/mod.rs | 4 ++ packages/lib/src/lua/task/result.rs | 5 +- tests/task/defer.luau | 20 +++++++ tests/task/delay.luau | 28 +++++++++ tests/task/spawn.luau | 28 +++++++++ 8 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 packages/lib/src/lua/create.rs diff --git a/packages/lib/src/globals/task.rs b/packages/lib/src/globals/task.rs index 33120f4..126ec76 100644 --- a/packages/lib/src/globals/task.rs +++ b/packages/lib/src/globals/task.rs @@ -98,8 +98,7 @@ pub fn create(lua: &'static Lua) -> LuaResult> { // We want the task scheduler to be transparent, // but it does not return real lua threads, so // we need to override some globals to fake it - let globals = lua.globals(); - let type_original: LuaFunction = globals.get("type")?; + let type_original: LuaFunction = lua.named_registry_value("type")?; let type_proxy = lua.create_function(move |_, value: LuaValue| { if let LuaValue::UserData(u) = &value { if u.is::() { @@ -108,7 +107,7 @@ pub fn create(lua: &'static Lua) -> LuaResult> { } type_original.call(value) })?; - let typeof_original: LuaFunction = globals.get("typeof")?; + let typeof_original: LuaFunction = lua.named_registry_value("typeof")?; let typeof_proxy = lua.create_function(move |_, value: LuaValue| { if let LuaValue::UserData(u) = &value { if u.is::() { @@ -117,6 +116,7 @@ pub fn create(lua: &'static Lua) -> LuaResult> { } typeof_original.call(value) })?; + let globals = lua.globals(); globals.set("type", type_proxy)?; globals.set("typeof", typeof_proxy)?; // Functions in the built-in coroutine library also need to be diff --git a/packages/lib/src/lib.rs b/packages/lib/src/lib.rs index 57b161b..8234428 100644 --- a/packages/lib/src/lib.rs +++ b/packages/lib/src/lib.rs @@ -14,6 +14,7 @@ mod tests; use crate::utils::formatting::pretty_format_luau_error; pub use globals::LuneGlobal; +pub use lua::create_lune_lua; #[derive(Clone, Debug, Default)] pub struct Lune { @@ -88,18 +89,8 @@ impl Lune { script_name: &str, script_contents: &str, ) -> Result { - let lua = Lua::new().into_static(); - // Store original lua global functions in the registry so we can use - // them later without passing them around and dealing with lifetimes - lua.set_named_registry_value("require", lua.globals().get::<_, LuaFunction>("require")?)?; - lua.set_named_registry_value("print", lua.globals().get::<_, LuaFunction>("print")?)?; - lua.set_named_registry_value("error", lua.globals().get::<_, LuaFunction>("error")?)?; - let coroutine: LuaTable = lua.globals().get("coroutine")?; - lua.set_named_registry_value("co.thread", coroutine.get::<_, LuaFunction>("running")?)?; - lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?; - lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?; - let debug: LuaTable = lua.globals().raw_get("debug")?; - lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?; + // Create our special lune-flavored Lua object with extra registry values + let lua = create_lune_lua().expect("Failed to create Lua object"); // Create our task scheduler and schedule the main thread on it let sched = TaskScheduler::new(lua)?.into_static(); lua.set_app_data(sched); diff --git a/packages/lib/src/lua/create.rs b/packages/lib/src/lua/create.rs new file mode 100644 index 0000000..7708c81 --- /dev/null +++ b/packages/lib/src/lua/create.rs @@ -0,0 +1,93 @@ +use mlua::prelude::*; + +/* + - Level 0 is the call to info + - Level 1 is the load call in create() below where we load this into a function + - Level 2 is the call to the scheduler, probably, but we can't know for sure so we start at 2 +*/ +const TRACE_IMPL_LUA: &str = r#" +local lines = {} +for level = 2, 2^8 do + local source, line, name = info(level, "sln") + if source then + if line then + if name and #name > 0 then + push(lines, format(" Script '%s', Line %d - function %s", source, line, name)) + else + push(lines, format(" Script '%s', Line %d", source, line)) + end + elseif name and #name > 0 then + push(lines, format(" Script '%s' - function %s", source, name)) + else + push(lines, format(" Script '%s'", source)) + end + elseif name then + push(lines, format("[Lune] - function %s", source, name)) + else + break + end +end +if #lines > 0 then + push(lines, 1, "Stack Begin") + push(lines, "Stack End") + return concat(lines, "\n") +else + return nil +end +"#; + +/** + Creates a [`mlua::Lua`] object with certain globals stored in the Lua registry. + + These globals can then be modified safely after constructing Lua using this function. + + --- + * `"require"` -> `require` + --- + * `"print"` -> `print` + * `"error"` -> `error` + --- + * `"type"` -> `type` + * `"typeof"` -> `typeof` + --- + * `"co.thread"` -> `coroutine.running` + * `"co.yield"` -> `coroutine.yield` + * `"co.close"` -> `coroutine.close` + --- + * `"dbg.info"` -> `debug.info` + * `"dbg.trace"` -> `debug.traceback` + --- +*/ +pub fn create() -> LuaResult<&'static Lua> { + let lua = Lua::new().into_static(); + let globals = &lua.globals(); + let debug: LuaTable = globals.raw_get("debug")?; + let table: LuaTable = globals.raw_get("table")?; + let string: LuaTable = globals.raw_get("string")?; + let coroutine: LuaTable = globals.get("coroutine")?; + // Store original lua global functions in the registry so we can use + // them later without passing them around and dealing with lifetimes + lua.set_named_registry_value("require", globals.get::<_, LuaFunction>("require")?)?; + lua.set_named_registry_value("print", globals.get::<_, LuaFunction>("print")?)?; + lua.set_named_registry_value("error", globals.get::<_, LuaFunction>("error")?)?; + lua.set_named_registry_value("type", globals.get::<_, LuaFunction>("type")?)?; + lua.set_named_registry_value("typeof", globals.get::<_, LuaFunction>("typeof")?)?; + lua.set_named_registry_value("co.thread", coroutine.get::<_, LuaFunction>("running")?)?; + lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?; + lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?; + lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?; + // Create a trace function that can be called to obtain a full stack trace from + // lua, this is not possible to do from rust when using our manual scheduler + let trace_env = lua.create_table_with_capacity(0, 1)?; + trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?; + trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?; + trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?; + trace_env.set("format", string.get::<_, LuaFunction>("format")?)?; + let trace_fn = lua + .load(TRACE_IMPL_LUA) + .set_environment(trace_env)? + .into_function()?; + lua.set_named_registry_value("dbg.trace", trace_fn)?; + // All done + Ok(lua) +} diff --git a/packages/lib/src/lua/mod.rs b/packages/lib/src/lua/mod.rs index 820f81e..c48ac29 100644 --- a/packages/lib/src/lua/mod.rs +++ b/packages/lib/src/lua/mod.rs @@ -1,3 +1,7 @@ +mod create; + pub mod net; pub mod stdio; pub mod task; + +pub use create::create as create_lune_lua; diff --git a/packages/lib/src/lua/task/result.rs b/packages/lib/src/lua/task/result.rs index 1068a84..895311a 100644 --- a/packages/lib/src/lua/task/result.rs +++ b/packages/lib/src/lua/task/result.rs @@ -94,9 +94,12 @@ impl TaskSchedulerState { Returns `true` if the task scheduler is done, meaning it has no lua threads left to run, and no spawned tasks are running in the background. + + Also returns `true` if a task has requested to exit the process. */ pub fn is_done(&self) -> bool { - self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0 + self.exit_code.is_some() + || (self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0) } } diff --git a/tests/task/defer.luau b/tests/task/defer.luau index 01123f8..c55e3ca 100644 --- a/tests/task/defer.luau +++ b/tests/task/defer.luau @@ -39,6 +39,26 @@ end) task.wait() assert(flag3 == 3, "Defer should run after spawned threads") +-- Delay should be able to be nested + +local flag4: boolean = false +task.delay(0.05, function() + local function nested3() + task.delay(0.05, function() + flag4 = true + end) + end + local function nested2() + task.delay(0.05, nested3) + end + local function nested1() + task.delay(0.05, nested2) + end + nested1() +end) +task.wait(0.25) +assert(flag4, "Defer should work with nesting") + -- Varargs should get passed correctly local fcheck = require("./fcheck") diff --git a/tests/task/delay.luau b/tests/task/delay.luau index 667eb5f..e4bcd00 100644 --- a/tests/task/delay.luau +++ b/tests/task/delay.luau @@ -26,6 +26,34 @@ assert(flag, "Delay should work with yielding (1)") task.wait(0.1) assert(not flag2, "Delay should work with yielding (2)") +-- Defer should be able to be nested + +local flag4: boolean = false +task.defer(function() + local function nested3() + task.defer(function() + task.wait(0.05) + flag4 = true + end) + end + local function nested2() + task.defer(function() + task.wait(0.05) + nested3() + end) + end + local function nested1() + task.defer(function() + task.wait(0.05) + nested2() + end) + end + task.wait(0.05) + nested1() +end) +task.wait(0.25) +assert(flag4, "Defer should work with nesting") + -- Varargs should get passed correctly local fcheck = require("./fcheck") diff --git a/tests/task/spawn.luau b/tests/task/spawn.luau index f01344a..28ec35e 100644 --- a/tests/task/spawn.luau +++ b/tests/task/spawn.luau @@ -31,6 +31,34 @@ end) task.spawn(thread2) assert(flag3, "Spawn should run threads made from coroutine.create") +-- Spawn should be able to be nested + +local flag4: boolean = false +task.spawn(function() + local function nested3() + task.spawn(function() + task.wait(0.05) + flag4 = true + end) + end + local function nested2() + task.spawn(function() + task.wait(0.05) + nested3() + end) + end + local function nested1() + task.spawn(function() + task.wait(0.05) + nested2() + end) + end + task.wait(0.05) + nested1() +end) +task.wait(0.25) +assert(flag4, "Spawn should work with nesting") + -- Varargs should get passed correctly local fcheck = require("./fcheck")