use std::sync::{Arc, Mutex};
use tatara_lisp::{Atom, Span, Spanned, SpannedForm};
use crate::error::{EvalError, Result};
use crate::eval::Interpreter;
use crate::ffi::{Arity, Caller};
use crate::value::Value;
use crate::vm::{Chunk, Vm, compile_program};
#[derive(Debug, Clone, PartialEq)]
pub enum FiberState {
Pending,
Running,
Done,
Errored,
}
pub struct Fiber {
pub chunk: Chunk,
pub thunk: Option<Value>,
pub state: FiberState,
pub result: Option<Value>,
pub error: Option<Value>,
}
impl std::fmt::Debug for Fiber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Fiber")
.field("state", &self.state)
.field("ops_len", &self.chunk.top.ops.len())
.field("has_thunk", &self.thunk.is_some())
.field("has_result", &self.result.is_some())
.finish()
}
}
pub fn make_fiber<H: 'static>(
body: &Spanned,
interp: &mut Interpreter<H>,
host: &mut H,
) -> Result<Value> {
let expanded = interp.fully_expand(body, host)?;
let chunk = compile_program(std::slice::from_ref(&expanded)).map_err(|e| match e {
crate::vm::CompileError::Bad { at, message } => EvalError::bad_form(
Arc::<str>::from("go"),
format!("compile body: {message}"),
at,
),
})?;
let fiber = Fiber {
chunk,
thunk: None,
state: FiberState::Pending,
result: None,
error: None,
};
Ok(Value::Foreign(Arc::new(Mutex::new(fiber))))
}
pub fn run_fiber<H: 'static>(
fiber: &Arc<Mutex<Fiber>>,
interp: &mut Interpreter<H>,
host: &mut H,
) -> Result<Value> {
let chunk = {
let f = fiber.lock().unwrap();
match f.state {
FiberState::Done => return Ok(f.result.clone().unwrap_or(Value::Nil)),
FiberState::Errored => return Ok(f.error.clone().unwrap_or(Value::Nil)),
FiberState::Running => {
return Err(EvalError::native_fn(
Arc::<str>::from("go-run"),
"fiber already running (re-entrant call detected)",
Span::synthetic(),
));
}
_ => {}
}
f.chunk.clone()
};
{
let mut f = fiber.lock().unwrap();
f.state = FiberState::Running;
}
let mut vm = Vm::new();
let result = vm.run(&chunk, interp, host);
let mut f = fiber.lock().unwrap();
match result {
Ok(v) => {
f.state = FiberState::Done;
f.result = Some(v.clone());
Ok(v)
}
Err(e) => {
f.state = FiberState::Errored;
let err_value = match e {
crate::vm::VmError::Eval(inner) => eval_err_to_value(&inner),
other => Value::Error(Arc::new(crate::value::ErrorObj {
tag: Arc::from("fiber-error"),
message: Arc::from(format!("{other}")),
data: Vec::new(),
})),
};
f.error = Some(err_value.clone());
Ok(err_value)
}
}
}
fn eval_err_to_value(e: &EvalError) -> Value {
if let EvalError::User { value, .. } = e {
return value.clone();
}
Value::Error(Arc::new(crate::value::ErrorObj {
tag: Arc::from("fiber-error"),
message: Arc::from(format!("{e}")),
data: Vec::new(),
}))
}
fn expect_fiber(v: &Value, sp: Span) -> Result<Arc<Mutex<Fiber>>> {
match v {
Value::Foreign(any) => any
.clone()
.downcast::<Mutex<Fiber>>()
.map_err(|_| EvalError::type_mismatch("fiber", v.type_name(), sp)),
other => Err(EvalError::type_mismatch("fiber", other.type_name(), sp)),
}
}
pub const FIBER_NAMES: &[&str] = &[
"go",
"go-error",
"go-result",
"go-run",
"go-status",
"go?",
];
pub fn install_fibers<H: 'static>(interp: &mut Interpreter<H>) {
interp.register_fn(
"go",
Arity::Exact(1),
|args: &[Value], _host: &mut H, sp| {
match &args[0] {
Value::Closure(_) | Value::NativeFn(_) | Value::Foreign(_) => {}
other => {
return Err(EvalError::type_mismatch("callable", other.type_name(), sp));
}
}
let fiber = Fiber {
chunk: Chunk::default(),
thunk: Some(args[0].clone()),
state: FiberState::Pending,
result: None,
error: None,
};
Ok(Value::Foreign(Arc::new(Mutex::new(fiber))))
},
);
interp.register_fn(
"go?",
Arity::Exact(1),
|args: &[Value], _h: &mut H, _sp| {
let is = match &args[0] {
Value::Foreign(any) => any.clone().downcast::<Mutex<Fiber>>().is_ok(),
_ => false,
};
Ok(Value::Bool(is))
},
);
interp.register_fn(
"go-status",
Arity::Exact(1),
|args: &[Value], _h: &mut H, sp| {
let f = expect_fiber(&args[0], sp)?;
let g = f.lock().unwrap();
let kw = match g.state {
FiberState::Pending => "pending",
FiberState::Running => "running",
FiberState::Done => "done",
FiberState::Errored => "errored",
};
Ok(Value::Keyword(Arc::from(kw)))
},
);
interp.register_fn(
"go-result",
Arity::Exact(1),
|args: &[Value], _h: &mut H, sp| {
let f = expect_fiber(&args[0], sp)?;
let g = f.lock().unwrap();
Ok(g.result.clone().unwrap_or(Value::Nil))
},
);
interp.register_fn(
"go-error",
Arity::Exact(1),
|args: &[Value], _h: &mut H, sp| {
let f = expect_fiber(&args[0], sp)?;
let g = f.lock().unwrap();
Ok(g.error.clone().unwrap_or(Value::Nil))
},
);
interp.register_higher_order_fn(
"go-run",
Arity::Exact(1),
|args: &[Value], host: &mut H, caller: &Caller<H>, sp: Span| {
let f = expect_fiber(&args[0], sp)?;
let thunk = {
let mut g = f.lock().unwrap();
match g.state {
FiberState::Done => return Ok(g.result.clone().unwrap_or(Value::Nil)),
FiberState::Errored => return Ok(g.error.clone().unwrap_or(Value::Nil)),
FiberState::Running => {
return Err(EvalError::native_fn(
Arc::<str>::from("go-run"),
"fiber already running (re-entrant call detected)",
sp,
));
}
FiberState::Pending => {}
}
g.state = FiberState::Running;
g.thunk.take()
};
let thunk = match thunk {
Some(t) => t,
None => {
let mut g = f.lock().unwrap();
g.state = FiberState::Errored;
let v = Value::Error(Arc::new(crate::value::ErrorObj {
tag: Arc::from("fiber-corrupt"),
message: Arc::from("pending fiber has no thunk"),
data: Vec::new(),
}));
g.error = Some(v.clone());
return Ok(v);
}
};
let result = caller.apply_value(&thunk, vec![], host, sp);
let mut g = f.lock().unwrap();
match result {
Ok(v) => {
g.state = FiberState::Done;
g.result = Some(v.clone());
Ok(v)
}
Err(e) => {
g.state = FiberState::Errored;
let err_value = eval_err_to_value(&e);
g.error = Some(err_value.clone());
Ok(err_value)
}
}
},
);
}
pub fn body_to_fiber<H: 'static>(
body: &Spanned,
interp: &mut Interpreter<H>,
host: &mut H,
) -> Result<Value> {
let lambda = Spanned::new(
body.span,
SpannedForm::List(vec![
Spanned::new(body.span, SpannedForm::Atom(Atom::Symbol("lambda".into()))),
Spanned::new(body.span, SpannedForm::List(Vec::new())),
body.clone(),
]),
);
make_fiber(&lambda, interp, host)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::install_full_stdlib_with;
use crate::Interpreter;
use tatara_lisp::read_spanned;
struct NoHost;
fn run(src: &str) -> Value {
let mut i: Interpreter<NoHost> = Interpreter::new();
install_full_stdlib_with(&mut i, &mut NoHost);
install_fibers(&mut i);
let forms = read_spanned(src).unwrap();
i.eval_program(&forms, &mut NoHost).unwrap()
}
#[test]
fn go_creates_a_fiber_value() {
let v = run("(go (lambda () 42))");
match v {
Value::Foreign(any) => {
assert!(any.downcast::<Mutex<Fiber>>().is_ok());
}
other => panic!("{other:?}"),
}
}
#[test]
fn go_predicate_distinguishes() {
assert!(matches!(run("(go? (go (lambda () 1)))"), Value::Bool(true)));
assert!(matches!(run("(go? 42)"), Value::Bool(false)));
}
#[test]
fn go_status_pending_before_run() {
let v = run("(go-status (go (lambda () (+ 1 2))))");
assert!(matches!(v, Value::Keyword(s) if &*s == "pending"));
}
#[test]
fn go_run_drives_to_done() {
let v = run(
"(let ((f (go (lambda () (* 7 6)))))
(go-run f)
(go-status f))",
);
assert!(matches!(v, Value::Keyword(s) if &*s == "done"));
}
#[test]
fn go_result_after_explicit_run() {
let v = run(
"(let ((f (go (lambda () (* 7 6)))))
(go-run f)
(go-result f))",
);
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn go_result_nil_before_run() {
let v = run("(go-result (go (lambda () 999)))");
assert!(matches!(v, Value::Nil));
}
#[test]
fn go_error_captures_thrown_value() {
let v = run(
"(let ((f (go (lambda () (throw (ex-info \"boom\" (list)))))))
(go-run f)
(go-status f))",
);
assert!(matches!(v, Value::Keyword(s) if &*s == "errored"));
let v = run(
"(let ((f (go (lambda () (throw (ex-info \"boom\" (list)))))))
(go-run f)
(error-message (go-error f)))",
);
assert!(matches!(v, Value::Str(s) if &*s == "boom"));
}
#[test]
fn go_run_returns_body_value() {
let v = run("(go-run (go (lambda () 99)))");
assert!(matches!(v, Value::Int(99)));
}
#[test]
fn go_run_idempotent_after_done() {
let v = run(
"(let ((f (go (lambda () 100))))
(go-run f)
(go-run f)
(go-run f))",
);
assert!(matches!(v, Value::Int(100)));
}
#[test]
fn go_thunk_with_closure_captures() {
let v = run(
"(let ((x 21))
(let ((f (go (lambda () (* x 2)))))
(go-run f)))",
);
assert!(matches!(v, Value::Int(42)));
}
#[test]
fn go_rejects_non_callable() {
let mut i: Interpreter<NoHost> = Interpreter::new();
install_full_stdlib_with(&mut i, &mut NoHost);
install_fibers(&mut i);
let forms = read_spanned("(go 42)").unwrap();
let err = i.eval_program(&forms, &mut NoHost).unwrap_err();
assert!(
matches!(err, EvalError::TypeMismatch { .. }),
"expected TypeMismatch, got {err:?}"
);
}
}