mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 21:40:40 +00:00
Implement task library & test suite, mostly
This commit is contained in:
parent
8ab5855ccc
commit
f8a2eb79d4
13 changed files with 188 additions and 85 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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)?
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -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)?
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
112
src/lib/luau/task.luau
Normal 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,
|
||||||
|
}
|
|
@ -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>> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue