use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use std::rc::Rc;
use std::time::{Duration, Instant};
use crate::vm::libs::coroutine;
use crate::vm::machine::VM;
use crate::vm::value::{Value, VmTable};
enum Wake {
Elapsed(Instant),
Args(Vec<Value>),
}
#[derive(Default)]
pub struct Scheduler {
deferred: VecDeque<(Value, Vec<Value>)>,
sleeping: BTreeMap<(Instant, u64), (Value, Wake)>,
next_seq: u64,
failed: usize,
}
impl Scheduler {
fn sleep_until(&mut self, at: Instant, thread: Value, wake: Wake) {
let seq = self.next_seq;
self.next_seq += 1;
self.sleeping.insert((at, seq), (thread, wake));
}
fn is_idle(&self) -> bool {
self.deferred.is_empty() && self.sleeping.is_empty()
}
fn forget(&mut self, thread: &Rc<RefCell<VmTable>>) {
let same = |v: &Value| matches!(v, Value::Table(t) if Rc::ptr_eq(t, thread));
self.deferred.retain(|(t, _)| !same(t));
self.sleeping.retain(|_, (t, _)| !same(t));
}
}
pub fn create_task_lib() -> Value {
let t = Rc::new(RefCell::new(VmTable::new()));
let mut b = t.borrow_mut();
b.set_str("spawn", crate::native!("task.spawn", task_spawn));
b.set_str("defer", crate::native!("task.defer", task_defer));
b.set_str("delay", crate::native!("task.delay", task_delay));
b.set_str("wait", crate::native!("task.wait", task_wait));
b.set_str("cancel", crate::native!("task.cancel", task_cancel));
b.frozen = true;
drop(b);
Value::Table(t)
}
fn task_thread(
vm: &mut VM,
arg: Option<&Value>,
func: &str,
index: usize,
) -> Result<Value, String> {
match arg {
Some(f @ (Value::Closure(_) | Value::Native(_))) => {
Ok(coroutine::new_coroutine(vm, f.clone()))
}
Some(t @ Value::Table(_)) if t.type_name() == "thread" => Ok(t.clone()),
_ => Err(format!(
"bad argument #{} to '{}' (function or thread expected)",
index, func
)),
}
}
fn seconds_arg(args: &[Value], index: usize, func: &str) -> Result<f64, String> {
let seconds = match args.get(index) {
None | Some(Value::Nil) => 0.0,
Some(v) => crate::vm::libs::args::number(v).map_err(|_| {
format!(
"bad argument #{} to '{}' (number expected)",
index + 1,
func
)
})?,
};
if seconds.is_nan() || seconds < 0.0 {
return Err(format!(
"bad argument #{} to '{}' (seconds must be a non-negative number)",
index + 1,
func
));
}
Ok(seconds)
}
fn deadline(now: Instant, seconds: f64) -> Instant {
Duration::try_from_secs_f64(seconds)
.ok()
.and_then(|d| now.checked_add(d))
.unwrap_or_else(|| now + Duration::from_secs(60 * 60 * 24 * 365 * 100))
}
fn resume_task(vm: &mut VM, thread: Value, args: Vec<Value>) {
if !coroutine::is_suspended(vm, &thread) {
return;
}
let mut resume_args = Vec::with_capacity(args.len() + 1);
resume_args.push(thread);
resume_args.extend(args);
let failure = match coroutine::coroutine_resume(vm, &resume_args) {
Ok(res) => match res.first() {
Some(Value::Bool(false)) => Some(res.get(1).map(|v| v.to_string()).unwrap_or_default()),
_ => None,
},
Err(err) => Some(err),
};
if let Some(message) = failure {
eprintln!("task error: {}", message);
vm.tasks.failed += 1;
}
}
fn task_spawn(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let thread = task_thread(vm, args.first(), "task.spawn", 1)?;
resume_task(vm, thread.clone(), args[1..].to_vec());
Ok(vec![thread])
}
fn task_defer(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let thread = task_thread(vm, args.first(), "task.defer", 1)?;
vm.tasks
.deferred
.push_back((thread.clone(), args[1..].to_vec()));
Ok(vec![thread])
}
fn task_delay(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let seconds = seconds_arg(args, 0, "task.delay")?;
let thread = task_thread(vm, args.get(1), "task.delay", 2)?;
let rest = args.get(2..).unwrap_or_default().to_vec();
let at = deadline(Instant::now(), seconds);
vm.tasks.sleep_until(at, thread.clone(), Wake::Args(rest));
Ok(vec![thread])
}
fn task_wait(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let seconds = seconds_arg(args, 0, "task.wait")?;
let start = Instant::now();
let at = deadline(start, seconds);
if let Some(handle) = coroutine::yieldable_handle(vm) {
vm.tasks
.sleep_until(at, Value::Table(handle), Wake::Elapsed(start));
return coroutine::coroutine_yield(vm, &[]);
}
run_until(vm, Some(at));
Ok(vec![Value::Float(start.elapsed().as_secs_f64())])
}
fn task_cancel(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Table(thread)) = args.first().filter(|v| v.type_name() == "thread") else {
return Err("bad argument #1 to 'task.cancel' (thread expected)".to_string());
};
vm.tasks.forget(thread);
match coroutine::coroutine_close(vm, &args[..1])?.as_slice() {
[Value::Bool(false), message, ..] => Err(message.to_string()),
_ => Ok(vec![]),
}
}
fn run_pass(vm: &mut VM) -> bool {
let now = Instant::now();
let deferred = std::mem::take(&mut vm.tasks.deferred);
let mut due = Vec::new();
while let Some(entry) = vm.tasks.sleeping.first_entry() {
if entry.key().0 > now {
break;
}
due.push(entry.remove());
}
let ran = !deferred.is_empty() || !due.is_empty();
for (thread, args) in deferred {
resume_task(vm, thread, args);
}
for (thread, wake) in due {
let args = match wake {
Wake::Elapsed(since) => vec![Value::Float(since.elapsed().as_secs_f64())],
Wake::Args(args) => args,
};
resume_task(vm, thread, args);
}
ran
}
fn run_until(vm: &mut VM, until: Option<Instant>) {
loop {
let ran = run_pass(vm);
if until.is_some_and(|u| u <= Instant::now()) {
return;
}
if ran {
continue;
}
let next = vm.tasks.sleeping.first_key_value().map(|(k, _)| k.0);
let wake = match (next, until) {
(Some(n), Some(u)) => n.min(u),
(Some(n), None) => n,
(None, Some(u)) => u,
(None, None) => return,
};
let now = Instant::now();
if wake > now {
std::thread::sleep(wake - now);
}
}
}
pub fn run_until_idle(vm: &mut VM) -> Result<(), String> {
run_until(vm, None);
debug_assert!(vm.tasks.is_idle());
match std::mem::take(&mut vm.tasks.failed) {
0 => Ok(()),
1 => Err("a task raised an error".to_string()),
n => Err(format!("{} tasks raised an error", n)),
}
}
pub fn run_due(vm: &mut VM) {
run_pass(vm);
vm.tasks.failed = 0;
}
pub fn next_due(vm: &VM) -> Option<Instant> {
if !vm.tasks.deferred.is_empty() {
return Some(Instant::now());
}
vm.tasks.sleeping.first_key_value().map(|(k, _)| k.0)
}
#[cfg(test)]
mod tests {
use crate::vm::machine::VM;
use crate::vm::value::Value;
fn run_program(vm: &mut VM, src: &str) -> Result<Value, String> {
let proto = crate::compiler::compile_program(src, "test", crate::compiler::Options::RUN)?;
vm.execute_program(proto)
}
fn global(vm: &VM, name: &str) -> Value {
vm.globals
.get(&crate::vm::value::StrRef::from(name))
.cloned()
.unwrap_or(Value::Nil)
}
#[test]
fn test_tasks_run_after_the_main_chunk() {
let mut vm = VM::new();
run_program(
&mut vm,
"_G.ticks = 0\n\
task.spawn(function()\n\
\u{20} while task.wait(0.001) do\n\
\u{20} _G.ticks += 1\n\
\u{20} if _G.ticks == 3 then break end\n\
\u{20} end\n\
end)\n\
task.delay(0.002, function(v: string) _G.delayed = v end, \"done\")\n\
_G.main_saw = _G.ticks",
)
.expect("program failed");
assert_eq!(global(&vm, "main_saw").to_string(), "0");
assert_eq!(global(&vm, "ticks").to_string(), "3");
assert_eq!(global(&vm, "delayed").to_string(), "done");
assert!(vm.tasks.is_idle());
}
#[test]
fn test_task_errors_fail_the_program_after_the_rest_ran() {
let mut vm = VM::new();
let err = run_program(
&mut vm,
"task.spawn(function() error(\"boom\") end)\n\
task.delay(0.001, function() error(\"again\") end)\n\
task.delay(0.002, function() _G.ran = true end)\n\
_G.main_done = true",
)
.expect_err("a failing task must fail the program");
assert_eq!(err, "2 tasks raised an error");
assert!(matches!(global(&vm, "main_done"), Value::Bool(true)));
assert!(matches!(global(&vm, "ran"), Value::Bool(true)));
}
}