use crate::{Frame, VMResult, Value, VM};
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone)]
pub enum SchedReq {
Go { name_idx: u16, args: Vec<Value> },
Make { cap: usize },
Send { ch: i64, val: Value },
Recv { ch: i64 },
Close { ch: i64 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedError {
Deadlock,
Panic(String),
}
impl std::fmt::Display for SchedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchedError::Deadlock => {
write!(f, "fatal error: all goroutines are asleep - deadlock!")
}
SchedError::Panic(m) => write!(f, "{m}"),
}
}
}
struct Channel {
cap: usize,
buf: VecDeque<Value>,
closed: bool,
send_q: VecDeque<(usize, Value)>,
recv_q: VecDeque<usize>,
}
impl Channel {
fn new(cap: usize) -> Self {
Channel {
cap,
buf: VecDeque::new(),
closed: false,
send_q: VecDeque::new(),
recv_q: VecDeque::new(),
}
}
}
pub struct Scheduler<F: FnMut() -> VM> {
make_vm: F,
vms: Vec<VM>,
ready: VecDeque<usize>,
blocked: HashSet<usize>,
done: HashSet<usize>,
chans: Vec<Channel>,
recv_zero: Value,
}
impl<F: FnMut() -> VM> Scheduler<F> {
pub fn new(make_vm: F) -> Self {
Scheduler {
make_vm,
vms: Vec::new(),
ready: VecDeque::new(),
blocked: HashSet::new(),
done: HashSet::new(),
chans: Vec::new(),
recv_zero: Value::Int(0),
}
}
pub fn with_recv_zero(mut self, zero: Value) -> Self {
self.recv_zero = zero;
self
}
pub fn run(mut self, main_vm: VM) -> Result<(), SchedError> {
self.vms.push(main_vm);
self.ready.push_back(0);
self.drive()
}
pub fn run_capturing(mut self, main_vm: VM, global: &str) -> Result<Option<Value>, SchedError> {
self.vms.push(main_vm);
self.ready.push_back(0);
self.drive()?;
let main = &self.vms[0];
let val = main
.chunk
.names
.iter()
.position(|n| n == global)
.and_then(|i| main.globals.get(i).cloned());
Ok(val)
}
fn drive(&mut self) -> Result<(), SchedError> {
while let Some(gid) = self.ready.pop_front() {
self.vms[gid].clear_halt();
let popped = match self.vms[gid].run() {
VMResult::Error(e) => return Err(SchedError::Panic(e)),
VMResult::Ok(v) => Some(v),
VMResult::Halted => None,
};
match self.vms[gid].take_sched() {
Some(req) => {
if let Some(v) = popped {
self.vms[gid].stack.push(v);
}
self.service(gid, req)?;
}
None => {
self.done.insert(gid);
if gid == 0 {
return Ok(());
}
}
}
if self.ready.is_empty() && !self.blocked.is_empty() {
return Err(SchedError::Deadlock);
}
}
Ok(())
}
fn service(&mut self, gid: usize, req: SchedReq) -> Result<(), SchedError> {
match req {
SchedReq::Make { cap } => {
let id = self.chans.len();
self.chans.push(Channel::new(cap));
self.vms[gid].stack.push(Value::Int(id as i64));
self.ready.push_back(gid);
}
SchedReq::Go { name_idx, args } => {
let ngid = self.vms.len();
let mut vm = (self.make_vm)();
self.position_goroutine(&mut vm, name_idx, &args)?;
self.vms.push(vm);
self.ready.push_back(ngid);
self.ready.push_back(gid);
}
SchedReq::Send { ch, val } => self.send(gid, ch, val)?,
SchedReq::Recv { ch } => self.recv(gid, ch)?,
SchedReq::Close { ch } => {
let c = self.chan_mut(ch)?;
c.closed = true;
let woken: Vec<usize> = c.recv_q.drain(..).collect();
for r in woken {
self.vms[r].stack.push(self.recv_zero.clone());
self.wake(r);
}
self.ready.push_back(gid);
}
}
Ok(())
}
fn send(&mut self, gid: usize, ch: i64, val: Value) -> Result<(), SchedError> {
let (deliver_to, buffered, park) = {
let c = self.chan_mut(ch)?;
if c.closed {
return Err(SchedError::Panic(
"panic: send on closed channel".to_string(),
));
}
if let Some(r) = c.recv_q.pop_front() {
(Some(r), false, false)
} else if c.buf.len() < c.cap {
c.buf.push_back(val.clone());
(None, true, false)
} else {
c.send_q.push_back((gid, val.clone()));
(None, false, true)
}
};
if let Some(r) = deliver_to {
self.vms[r].stack.push(val);
self.wake(r);
self.ready.push_back(gid);
} else if buffered {
self.ready.push_back(gid);
} else if park {
self.blocked.insert(gid);
}
Ok(())
}
fn recv(&mut self, gid: usize, ch: i64) -> Result<(), SchedError> {
enum Outcome {
Value(Value),
WakeSender(usize, Value),
Zero,
Park,
}
let outcome = {
let c = self.chan_mut(ch)?;
if let Some(v) = c.buf.pop_front() {
if let Some((s, sv)) = c.send_q.pop_front() {
c.buf.push_back(sv);
Outcome::WakeSender(s, v)
} else {
Outcome::Value(v)
}
} else if let Some((s, sv)) = c.send_q.pop_front() {
Outcome::WakeSender(s, sv)
} else if c.closed {
Outcome::Zero
} else {
c.recv_q.push_back(gid);
Outcome::Park
}
};
match outcome {
Outcome::Value(v) => {
self.vms[gid].stack.push(v);
self.ready.push_back(gid);
}
Outcome::WakeSender(s, v) => {
self.vms[gid].stack.push(v);
self.wake(s);
self.ready.push_back(gid);
}
Outcome::Zero => {
let z = self.recv_zero.clone();
self.vms[gid].stack.push(z);
self.ready.push_back(gid);
}
Outcome::Park => {
self.blocked.insert(gid);
}
}
Ok(())
}
fn wake(&mut self, gid: usize) {
self.blocked.remove(&gid);
self.ready.push_back(gid);
}
fn chan_mut(&mut self, ch: i64) -> Result<&mut Channel, SchedError> {
self.chans
.get_mut(ch as usize)
.ok_or_else(|| SchedError::Panic(format!("panic: invalid channel {ch}")))
}
fn position_goroutine(
&self,
vm: &mut VM,
name_idx: u16,
args: &[Value],
) -> Result<(), SchedError> {
let entry = vm.chunk.find_sub(name_idx).ok_or_else(|| {
SchedError::Panic(format!("panic: goroutine target sub {name_idx} not found"))
})?;
let base = vm.stack.len();
for a in args {
vm.stack.push(a.clone());
}
vm.frames.push(Frame {
return_ip: vm.chunk.ops.len(), stack_base: base,
slots: Vec::new(),
});
vm.ip = entry;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Chunk, ChunkBuilder, Op};
fn run(chunk: Chunk, global: &str) -> Result<Option<Value>, SchedError> {
let fc = chunk.clone();
Scheduler::new(move || VM::new(fc.clone())).run_capturing(VM::new(chunk), global)
}
#[test]
fn producer_consumer_over_unbuffered_channel() {
let mut b = ChunkBuilder::new();
let prod = b.add_name("producer");
let ch = b.add_name("ch");
let sum = b.add_name("sum");
b.emit(Op::LoadInt(0), 1);
b.emit(Op::ChanMake, 1);
b.emit(Op::SetVar(ch), 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::Go(prod, 1), 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::Add, 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::Add, 1);
b.emit(Op::SetVar(sum), 1);
let skip = b.emit(Op::Jump(0), 1);
let entry = b.current_pos();
b.add_sub_entry(prod, entry);
b.emit(Op::SetSlot(0), 2); for v in 1..=3 {
b.emit(Op::GetSlot(0), 2);
b.emit(Op::LoadInt(v), 2);
b.emit(Op::ChanSend, 2);
}
b.emit(Op::LoadUndef, 2);
b.emit(Op::ReturnValue, 2);
b.patch_jump(skip, b.current_pos());
assert_eq!(run(b.build(), "sum"), Ok(Some(Value::Int(6))));
}
#[test]
fn buffered_channel_holds_values_without_a_receiver() {
let mut b = ChunkBuilder::new();
let ch = b.add_name("ch");
let sum = b.add_name("sum");
b.emit(Op::LoadInt(2), 1);
b.emit(Op::ChanMake, 1);
b.emit(Op::SetVar(ch), 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::LoadInt(10), 1);
b.emit(Op::ChanSend, 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::LoadInt(20), 1);
b.emit(Op::ChanSend, 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::Add, 1);
b.emit(Op::SetVar(sum), 1);
assert_eq!(run(b.build(), "sum"), Ok(Some(Value::Int(30))));
}
#[test]
fn deadlock_is_detected() {
let mut b = ChunkBuilder::new();
let ch = b.add_name("ch");
b.emit(Op::LoadInt(0), 1);
b.emit(Op::ChanMake, 1);
b.emit(Op::SetVar(ch), 1);
b.emit(Op::GetVar(ch), 1);
b.emit(Op::ChanRecv, 1);
b.emit(Op::Pop, 1);
assert_eq!(run(b.build(), "x"), Err(SchedError::Deadlock));
}
}