Implement task library & test suite, mostly

This commit is contained in:
Filip Tibell 2023-01-21 20:11:17 -05:00
parent 8ab5855ccc
commit f8a2eb79d4
No known key found for this signature in database
13 changed files with 188 additions and 85 deletions

View file

@ -5,7 +5,7 @@ use crate::utils::{
table_builder::ReadonlyTableBuilder, table_builder::ReadonlyTableBuilder,
}; };
pub fn new(lua: &Lua) -> Result<Table> { pub async fn new(lua: &Lua) -> Result<Table> {
let print = |args: &MultiValue, throw: bool| -> Result<()> { let print = |args: &MultiValue, throw: bool| -> Result<()> {
let s = pretty_format_multi_value(args)?; let s = pretty_format_multi_value(args)?;
if throw { if throw {

View file

@ -5,7 +5,7 @@ use tokio::fs;
use crate::utils::table_builder::ReadonlyTableBuilder; use crate::utils::table_builder::ReadonlyTableBuilder;
pub fn new(lua: &Lua) -> Result<Table> { pub async fn new(lua: &Lua) -> Result<Table> {
ReadonlyTableBuilder::new(lua)? ReadonlyTableBuilder::new(lua)?
.with_async_function("readFile", fs_read_file)? .with_async_function("readFile", fs_read_file)?
.with_async_function("readDir", fs_read_dir)? .with_async_function("readDir", fs_read_dir)?

View file

@ -9,5 +9,3 @@ pub use fs::new as new_fs;
pub use net::new as new_net; pub use net::new as new_net;
pub use process::new as new_process; pub use process::new as new_process;
pub use task::new as new_task; pub use task::new as new_task;
pub use task::WaitingThread as WaitingTaskThread;

View file

@ -8,7 +8,7 @@ use reqwest::{
use crate::utils::{net::get_request_user_agent_header, table_builder::ReadonlyTableBuilder}; use crate::utils::{net::get_request_user_agent_header, table_builder::ReadonlyTableBuilder};
pub fn new(lua: &Lua) -> Result<Table> { pub async fn new(lua: &Lua) -> Result<Table> {
ReadonlyTableBuilder::new(lua)? ReadonlyTableBuilder::new(lua)?
.with_function("jsonEncode", net_json_encode)? .with_function("jsonEncode", net_json_encode)?
.with_function("jsonDecode", net_json_decode)? .with_function("jsonDecode", net_json_decode)?

View file

@ -9,7 +9,7 @@ use tokio::process::Command;
use crate::utils::table_builder::ReadonlyTableBuilder; use crate::utils::table_builder::ReadonlyTableBuilder;
pub fn new(lua: &Lua, args_vec: Vec<String>) -> Result<Table> { pub async fn new(lua: &Lua, args_vec: Vec<String>) -> Result<Table> {
// Create readonly args array // Create readonly args array
let inner_args = lua.create_table()?; let inner_args = lua.create_table()?;
for arg in &args_vec { for arg in &args_vec {

View file

@ -1,60 +1,40 @@
use std::{ use std::{thread::sleep, time::Duration};
sync::{Arc, Mutex},
time::Duration,
};
use mlua::{Function, Lua, Result, Table, Thread, Value, Variadic}; use mlua::{Function, Lua, Result, Table, Value};
use tokio::time;
use crate::utils::table_builder::ReadonlyTableBuilder; use crate::utils::table_builder::ReadonlyTableBuilder;
const DEFAULT_SLEEP_DURATION: f32 = 1.0 / 60.0; const DEFAULT_SLEEP_DURATION: f32 = 1.0 / 60.0;
#[allow(dead_code)] const TASK_LIB_LUAU: &str = include_str!("../luau/task.luau");
pub struct WaitingThread<'a> {
is_delayed_for: Option<f32>,
is_deferred: Option<bool>,
thread: Thread<'a>,
args: Variadic<Value<'a>>,
}
pub fn new<'a>(lua: &'a Lua, _threads: &Arc<Mutex<Vec<WaitingThread<'a>>>>) -> Result<Table<'a>> { pub async fn new(lua: &Lua) -> Result<Table> {
// TODO: Figure out how to insert into threads vec let task_lib: Table = lua
.load(TASK_LIB_LUAU)
.set_name("task")?
.eval_async()
.await?;
// FUTURE: Properly implementing the task library in async rust is
// very complicated but should be done at some point, for now we will
// fall back to implementing only task.wait and doing the rest in lua
let task_cancel: Function = task_lib.raw_get("cancel")?;
let task_defer: Function = task_lib.raw_get("defer")?;
let task_delay: Function = task_lib.raw_get("delay")?;
let task_spawn: Function = task_lib.raw_get("spawn")?;
ReadonlyTableBuilder::new(lua)? ReadonlyTableBuilder::new(lua)?
.with_function("cancel", |lua, thread: Thread| { .with_value("cancel", Value::Function(task_cancel))?
thread.reset(lua.create_function(|_, _: ()| Ok(()))?)?; .with_value("defer", Value::Function(task_defer))?
Ok(()) .with_value("delay", Value::Function(task_delay))?
})? .with_value("spawn", Value::Function(task_spawn))?
.with_async_function( .with_function("wait", wait)?
"defer",
|lua, (func, args): (Function, Variadic<Value>)| async move {
let thread = lua.create_thread(func)?;
thread.into_async(args).await?;
Ok(())
},
)?
.with_async_function(
"delay",
|lua, (duration, func, args): (Option<f32>, Function, Variadic<Value>)| async move {
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
time::sleep(Duration::from_secs_f32(secs)).await;
let thread = lua.create_thread(func)?;
thread.into_async(args).await?;
Ok(())
},
)?
.with_async_function(
"spawn",
|lua, (func, args): (Function, Variadic<Value>)| async move {
let thread = lua.create_thread(func)?;
thread.into_async(args).await?;
Ok(())
},
)?
.with_async_function("wait", |_, duration: Option<f32>| async move {
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
time::sleep(Duration::from_secs_f32(secs)).await;
Ok(secs)
})?
.build() .build()
} }
// FIXME: It does seem possible to properly make an async wait
// function with mlua right now, something breaks when using
// async wait functions inside of coroutines
fn wait(_: &Lua, duration: Option<f32>) -> Result<f32> {
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
sleep(Duration::from_secs_f32(secs));
Ok(secs)
}

View file

@ -1,5 +1,3 @@
use std::sync::{Arc, Mutex};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use mlua::Lua; use mlua::Lua;
@ -13,16 +11,15 @@ use crate::{
pub async fn run_lune(name: &str, chunk: &str, args: Vec<String>) -> Result<()> { pub async fn run_lune(name: &str, chunk: &str, args: Vec<String>) -> Result<()> {
let lua = Lua::new(); let lua = Lua::new();
let threads = Arc::new(Mutex::new(Vec::new()));
lua.sandbox(true)?; lua.sandbox(true)?;
// Add in all globals // Add in all globals
{ {
let globals = lua.globals(); let globals = lua.globals();
globals.raw_set("console", new_console(&lua)?)?; globals.raw_set("console", new_console(&lua).await?)?;
globals.raw_set("fs", new_fs(&lua)?)?; globals.raw_set("fs", new_fs(&lua).await?)?;
globals.raw_set("net", new_net(&lua)?)?; globals.raw_set("net", new_net(&lua).await?)?;
globals.raw_set("process", new_process(&lua, args.clone())?)?; globals.raw_set("process", new_process(&lua, args.clone()).await?)?;
globals.raw_set("task", new_task(&lua, &threads)?)?; globals.raw_set("task", new_task(&lua).await?)?;
globals.set_readonly(true); globals.set_readonly(true);
} }
// Run the requested chunk asynchronously // Run the requested chunk asynchronously

112
src/lib/luau/task.luau Normal file
View file

@ -0,0 +1,112 @@
local MINIMUM_DELAY_TIME = 1 / 100
type ThreadOrFunction<A..., R...> = thread | (A...) -> R...
type AnyThreadOrFunction = ThreadOrFunction<...any, ...any>
type WaitingThreadKind = "Normal" | "Deferred" | "Delayed"
type WaitingThread = {
idx: number,
kind: WaitingThreadKind,
thread: thread,
args: { [number]: any, n: number },
}
local waitingThreadCounter = 0
local waitingThreads: { WaitingThread } = {}
local function scheduleWaitingThreads()
-- Grab currently waiting threads and clear the queue but keep capacity
local threadsToResume: { WaitingThread } = table.clone(waitingThreads)
table.clear(waitingThreads)
table.sort(threadsToResume, function(t0, t1)
local k0: WaitingThreadKind = t0.kind
local k1: WaitingThreadKind = t1.kind
if k0 == k1 then
return t0.idx < t1.idx
end
if k0 == "Normal" then
return true
elseif k1 == "Normal" then
return false
elseif k0 == "Deferred" then
return true
else
return false
end
end)
-- Resume threads in order, giving args & waiting if necessary
for _, waitingThread in threadsToResume do
coroutine.resume(
waitingThread.thread,
table.unpack(waitingThread.args, 1, waitingThread.args.n)
)
end
end
local function insertWaitingThread(kind: WaitingThreadKind, tof: AnyThreadOrFunction, ...: any)
if typeof(tof) ~= "thread" and typeof(tof) ~= "function" then
if tof == nil then
error("Expected thread or function, got nil", 3)
end
error(
string.format("Expected thread or function, got %s %s", typeof(tof), tostring(tof)),
3
)
end
local thread = if type(tof) == "function" then coroutine.create(tof) else tof
waitingThreadCounter += 1
local waitingThread: WaitingThread = {
idx = waitingThreadCounter,
kind = kind,
thread = thread,
args = table.pack(...),
}
table.insert(waitingThreads, waitingThread)
return waitingThread
end
local function cancel(thread: unknown)
if typeof(thread) ~= "thread" then
if thread == nil then
error("Expected thread, got nil", 2)
end
error(string.format("Expected thread, got %s %s", typeof(thread), tostring(thread)), 2)
else
coroutine.close(thread)
end
end
local function defer(tof: AnyThreadOrFunction, ...: any): thread
local waiting = insertWaitingThread("Deferred", tof, ...)
local original = waiting.thread
waiting.thread = coroutine.create(function(...)
task.wait(1 / 1_000_000)
coroutine.resume(original, ...)
end)
scheduleWaitingThreads()
return waiting.thread
end
local function delay(delay: number?, tof: AnyThreadOrFunction, ...: any): thread
local waiting = insertWaitingThread("Delayed", tof, ...)
local original = waiting.thread
waiting.thread = coroutine.create(function(...)
task.wait(math.max(MINIMUM_DELAY_TIME, delay or 0))
coroutine.resume(original, ...)
end)
scheduleWaitingThreads()
return waiting.thread
end
local function spawn(tof: AnyThreadOrFunction, ...: any): thread
local waiting = insertWaitingThread("Normal", tof, ...)
scheduleWaitingThreads()
return waiting.thread
end
return {
cancel = cancel,
defer = defer,
delay = delay,
spawn = spawn,
}

View file

@ -18,9 +18,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
Ok(self) Ok(self)
} }
pub fn with_table(self, key: &'static str, value: Table) -> Result<Self> { pub fn with_table(self, key: &'static str, table: Table) -> Result<Self> {
self.tab.raw_set(key, value)?; self.with_value(key, Value::Table(table))
Ok(self)
} }
pub fn with_function<A, R, F>(self, key: &'static str, func: F) -> Result<Self> pub fn with_function<A, R, F>(self, key: &'static str, func: F) -> Result<Self>
@ -29,9 +28,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
R: ToLuaMulti<'lua>, R: ToLuaMulti<'lua>,
F: 'static + Fn(&'lua Lua, A) -> Result<R>, F: 'static + Fn(&'lua Lua, A) -> Result<R>,
{ {
let value = self.lua.create_function(func)?; let f = self.lua.create_function(func)?;
self.tab.raw_set(key, value)?; self.with_value(key, Value::Function(f))
Ok(self)
} }
pub fn with_async_function<A, R, F, FR>(self, key: &'static str, func: F) -> Result<Self> pub fn with_async_function<A, R, F, FR>(self, key: &'static str, func: F) -> Result<Self>
@ -41,9 +39,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
F: 'static + Fn(&'lua Lua, A) -> FR, F: 'static + Fn(&'lua Lua, A) -> FR,
FR: 'lua + Future<Output = Result<R>>, FR: 'lua + Future<Output = Result<R>>,
{ {
let value = self.lua.create_async_function(func)?; let f = self.lua.create_async_function(func)?;
self.tab.raw_set(key, value)?; self.with_value(key, Value::Function(f))
Ok(self)
} }
pub fn build(self) -> Result<Table<'lua>> { pub fn build(self) -> Result<Table<'lua>> {

View file

@ -4,16 +4,20 @@ local flag: boolean = false
task.defer(function() task.defer(function()
flag = true flag = true
end) end)
assert(not flag, "Defer should run after other threads, including the main thread") assert(not flag, "Defer should not run instantly or block")
task.wait(1 / 60)
assert(flag, "Defer should run")
-- Deferred functions should work with yielding -- Deferred functions should work with yielding
local flag2: boolean = false local flag2: boolean = false
task.defer(function() task.defer(function()
task.wait() task.wait(1 / 60)
flag2 = true flag2 = true
end) end)
assert(not flag2, "Defer should work with yielding") assert(not flag2, "Defer should work with yielding (1)")
task.wait(1 / 30)
assert(flag2, "Defer should work with yielding (2)")
-- Deferred functions should run after other spawned threads -- Deferred functions should run after other spawned threads
local flag3: boolean = false local flag3: boolean = false

View file

@ -4,20 +4,22 @@ local flag: boolean = false
task.delay(0, function() task.delay(0, function()
flag = true flag = true
end) end)
assert(not flag, "Delay should never run instantly") assert(not flag, "Delay should not run instantly or block")
task.wait(1 / 60)
assert(flag, "Delay should run after the wanted duration")
-- Delayed functions should work with yielding -- Delayed functions should work with yielding
local flag2: boolean = false local flag2: boolean = false
task.delay(0.2, function() task.delay(0.2, function()
flag2 = true flag2 = true
task.wait(0.2) task.wait(0.4)
flag2 = false flag2 = false
end) end)
task.wait(0.25) task.wait(0.4)
assert(flag2, "Delay should work with yielding") assert(flag, "Delay should work with yielding (1)")
task.wait(0.25) task.wait(0.4)
assert(not flag2, "Delay should work with yielding") assert(not flag2, "Delay should work with yielding (2)")
-- Varargs should get passed correctly -- Varargs should get passed correctly

View file

@ -4,16 +4,18 @@ local flag: boolean = false
task.spawn(function() task.spawn(function()
flag = true flag = true
end) end)
assert(flag, "Spawn should run instantly until yielded") assert(flag, "Spawn should run instantly")
-- Spawned functions should work with yielding -- Spawned functions should work with yielding
local flag2: boolean = false local flag2: boolean = false
task.spawn(function() task.spawn(function()
task.wait() task.wait(0.1)
flag2 = true flag2 = true
end) end)
assert(not flag2, "Spawn should work with yielding") assert(not flag2, "Spawn should work with yielding (1)")
task.wait(0.2)
assert(flag2, "Spawn should work with yielding (2)")
-- Varargs should get passed correctly -- Varargs should get passed correctly

View file

@ -1,3 +1,14 @@
-- Wait should work everywhere
local flag: boolean = false
coroutine.wrap(function()
task.wait(0.1)
flag = true
end)()
assert(flag, "Wait failed while in a coroutine")
-- Wait should be accurate
local DEFAULT = 1 / 60 local DEFAULT = 1 / 60
local EPSILON = 1 / 100 local EPSILON = 1 / 100