use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, conc_registry, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::sync::{Mutex, MutexGuard, OnceLock};
fn serial_lock() -> MutexGuard<'static, ()> {
static M: OnceLock<Mutex<()>> = OnceLock::new();
M.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
fn run(src: &str, func: &str, args: Vec<Value>) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = compile_program(&stages);
let handler = DefaultHandler::new(Policy::permissive());
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(func, args).expect("vm call")
}
const SUM_ACTOR_SRC: &str = r#"
import "std.conc" as conc
fn handler(state :: Int, msg :: Int) -> (Int, Int) {
let next := state + msg
(next, next)
}
fn spawn_sum(init :: Int) -> [concurrent] Actor[Int] {
conc.spawn(init, handler)
}
fn reg(a :: Actor[Int], name :: Str) -> [concurrent] Result[Nil, ConcError] {
conc.register(a, name)
}
fn unreg(name :: Str) -> [concurrent] Result[Nil, ConcError] {
conc.unregister(name)
}
fn lk(name :: Str) -> [concurrent] Option[Actor[Int]] {
conc.lookup(name)
}
fn names() -> [concurrent] List[Str] { conc.registered() }
fn ask_via_lookup(name :: Str, msg :: Int) -> [concurrent] Option[Int] {
match conc.lookup(name) {
None => None,
Some(a) => Some(conc.ask(a, msg)),
}
}
fn ask_direct(a :: Actor[Int], msg :: Int) -> [concurrent] Int {
conc.ask(a, msg)
}
"#;
fn unwrap_ok(v: Value) -> Value {
match v {
Value::Variant { name, args } if name == "Ok" && args.len() == 1
=> args.into_iter().next().unwrap(),
other => panic!("expected Ok(_), got {other:?}"),
}
}
fn unwrap_err(v: Value) -> Value {
match v {
Value::Variant { name, args } if name == "Err" && args.len() == 1
=> args.into_iter().next().unwrap(),
other => panic!("expected Err(_), got {other:?}"),
}
}
fn unwrap_some(v: Value) -> Value {
match v {
Value::Variant { name, args } if name == "Some" && args.len() == 1
=> args.into_iter().next().unwrap(),
other => panic!("expected Some(_), got {other:?}"),
}
}
#[test]
fn register_first_time_returns_ok() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let actor = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let r = run(SUM_ACTOR_SRC, "reg", vec![actor, Value::Str("vehicle".into())]);
assert_eq!(unwrap_ok(r), Value::Unit);
let names = run(SUM_ACTOR_SRC, "names", vec![]);
assert_eq!(names, Value::List(vec![Value::Str("vehicle".into())].into()));
}
#[test]
fn register_duplicate_name_returns_already_registered() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let a = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let b = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(99)]);
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![a, Value::Str("dup".into())]));
let r2 = run(SUM_ACTOR_SRC, "reg", vec![b, Value::Str("dup".into())]);
match unwrap_err(r2) {
Value::Variant { name, args }
if name == "AlreadyRegistered" && args.len() == 1 =>
{
match &args[0] {
Value::Str(s) => assert_eq!(s.as_str(), "dup"),
other => panic!("expected Str name, got {other:?}"),
}
}
other => panic!("expected AlreadyRegistered, got {other:?}"),
}
}
#[test]
fn lookup_unregistered_returns_none() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let r = run(SUM_ACTOR_SRC, "lk", vec![Value::Str("nope".into())]);
assert_eq!(r, Value::Variant { name: "None".into(), args: vec![] });
}
#[test]
fn lookup_after_register_returns_same_actor_identity() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let actor = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(10)]);
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![actor.clone(), Value::Str("counter".into())]));
let looked_up = unwrap_some(
run(SUM_ACTOR_SRC, "lk", vec![Value::Str("counter".into())]));
assert_eq!(looked_up, actor);
}
#[test]
fn ask_via_lookup_drives_actor_state() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let a = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![a, Value::Str("acc".into())]));
let r1 = unwrap_some(run(SUM_ACTOR_SRC, "ask_via_lookup",
vec![Value::Str("acc".into()), Value::Int(5)]));
assert_eq!(r1, Value::Int(5));
let r2 = unwrap_some(run(SUM_ACTOR_SRC, "ask_via_lookup",
vec![Value::Str("acc".into()), Value::Int(7)]));
assert_eq!(r2, Value::Int(12), "second ask sees state from the first");
}
#[test]
fn unregister_removes_name_but_existing_handles_still_work() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let a = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![a.clone(), Value::Str("temp".into())]));
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "unreg",
vec![Value::Str("temp".into())]));
let r = run(SUM_ACTOR_SRC, "lk", vec![Value::Str("temp".into())]);
assert_eq!(r, Value::Variant { name: "None".into(), args: vec![] });
let reply = run(SUM_ACTOR_SRC, "ask_direct", vec![a, Value::Int(3)]);
assert_eq!(reply, Value::Int(3),
"unregistered actor's held handle should still accept messages");
}
#[test]
fn unregister_missing_name_returns_not_registered() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let r = run(SUM_ACTOR_SRC, "unreg", vec![Value::Str("missing".into())]);
match unwrap_err(r) {
Value::Variant { name, args }
if name == "NotRegistered" && args.len() == 1 =>
{
match &args[0] {
Value::Str(s) => assert_eq!(s.as_str(), "missing"),
other => panic!("{other:?}"),
}
}
other => panic!("expected NotRegistered, got {other:?}"),
}
}
#[test]
fn registered_lists_names_sorted() {
let _guard = serial_lock();
conc_registry::_reset_for_tests();
let a = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let b = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let c = run(SUM_ACTOR_SRC, "spawn_sum", vec![Value::Int(0)]);
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![a, Value::Str("charlie".into())]));
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![b, Value::Str("alpha".into())]));
let _ = unwrap_ok(run(SUM_ACTOR_SRC, "reg",
vec![c, Value::Str("bravo".into())]));
let names = run(SUM_ACTOR_SRC, "names", vec![]);
assert_eq!(
names,
Value::List(vec![
Value::Str("alpha".into()),
Value::Str("bravo".into()),
Value::Str("charlie".into()),
].into()),
);
}