use dever::compiler::{check, model::Program}; use dever::syntax::{diagnostic::Diagnostic, source::Source}; mod async_stream; mod channel; mod group; mod parallel; mod select; fn linked(text: &str) -> Result { let inputs = [ ("main", text), ("dever.io", include_str!("../../../../lib/dever/io.dever")), ( "dever.time", include_str!("../../../../lib/dever/time.dever"), ), ( "dever.task", include_str!("../../../../lib/dever/task.dever"), ), ]; let modules = inputs .iter() .enumerate() .map(|(index, (package, text))| { dever::syntax::parse(&Source::new( index, if index == 0 { "main.dever".into() } else { format!("{package}.dever") }, (*text).into(), )) }) .collect::, _>>()?; check::check( modules, &inputs .iter() .map(|(package, _)| (*package).into()) .collect::>(), ) } #[test] fn task_ownership_rejects_aliases_discard_overwrite_and_double_consumption() { let prefix = "work() (value: Int) { value = 42 }\n"; for body in [ "task = run(work())", "run(work())", "task = run(work())\n alias = task\n value = wait(task)", "task = run(work())\n tasks = [task]\n value = wait(task)", "task = run(work())\n task = run(work())\n value = wait(task)", "task = run(work())\n value = wait(task)\n again = wait(task)", ] { let source = format!("{prefix}main() () {{ {body} }}"); let error = linked(&source).unwrap_err(); assert!(matches!(error.code, "C005" | "C013"), "{source}: {error:?}"); } assert!( linked(&format!( "{prefix}main() (answer: Int) {{ task = run(work())\n answer = wait(task) }}" )) .is_ok() ); assert!( linked(&format!( "{prefix}main() (answer: Int) {{ answer = wait(run(work())) }}" )) .is_ok() ); assert_eq!( linked(&format!("{prefix}main() () {{ stop(run(work())) }}")) .unwrap_err() .code, "C005" ); } #[test] fn short_circuit_cannot_conditionally_consume_tasks() { let prefix = "work() (value: Bool) { value = true }\n"; assert_eq!(linked(&format!("{prefix}main(flag: Bool) (answer: Bool) {{ task = run(work())\n answer = flag and wait(task) }}")).unwrap_err().code, "C013"); assert!(linked(&format!("{prefix}main() (answer: Bool) {{ task = run(work())\n unused = false and wait(task)\n answer = wait(task) }}")).is_ok()); assert!( linked(&format!( "{prefix}main() (answer: Bool) {{ task = run(work())\n answer = true and wait(task) }}" )) .is_ok() ); } #[test] fn effects_distinguish_cpu_workers_blocking_calls_and_async_calls() { for source in [ "main() () { dever.task.sleep(0)\n dever.io.println(\"bad\") }", "main() () { dever.task.sleep(0)\n dever.time.sleep(0) }", "work() () {}\nmain() () { parallel(work()) }", "work() () pure {}\nmain() () { blocking(work()) }", "main() () { blocking(dever.task.sleep(0)) }", "main() () pure { stop(run(dever.task.sleep(0))) }", "main() () pure { dever.time.sleep(0) }", ] { assert_eq!(linked(source).unwrap_err().code, "C013", "{source}"); } let program = linked("calc() (value: Int) pure { value = 42 }\nmain() (answer: Int) { answer = parallel(calc())\n blocking(dever.time.sleep(0))\n stop(run(dever.task.sleep(0))) }").unwrap(); let main = program .functions .iter() .find(|function| function.signature.name == "main") .unwrap(); assert!(main.signature.effects.suspend); let calc = program .functions .iter() .find(|function| function.signature.name == "calc") .unwrap(); assert!(!calc.signature.effects.suspend); } #[test] fn nontransferable_values_cannot_escape_into_tasks_or_cross_suspension() { for source in [ "work(stream: Stream) () { close(stream) }\nmain(stream: Stream) () { stop(run(work(stream))) }", "type Box { stream: Stream }\nwork(value: Box) () { close(value.stream) }\nmain(value: Box) () { stop(run(work(value))) }", "main(stream: Stream) () { dever.task.sleep(0)\n close(stream) }", "type Box { stream: Stream }\nmain(value: Box) () { dever.task.sleep(0)\n close(value.stream) }", "work(stream: Stream) () { close(stream)\n dever.task.sleep(0) }\nmain(streams: List>) () { each(work, streams) }", ] { assert_eq!(linked(source).unwrap_err().code, "C013", "{source}"); } assert!(linked("main(stream: Stream) () { close(stream)\n dever.task.sleep(0) }").is_ok()); assert!(linked("main(file: dever.system.File) () { blocking(dever.io.close(file)) }").is_ok()); } #[test] fn task_failure_values_cannot_be_silently_discarded() { let prefix = "type Result { Done(value: Int)\n error Failed(message: Text) }\nwork() (answer: Result) { answer = Result.Failed(\"reason\") }\n"; assert_eq!( linked(&format!( "{prefix}main() () {{ task = run(work())\n lost = wait(task) }}" )) .unwrap_err() .code, "C012" ); assert!( linked(&format!( "{prefix}main() (answer: Result) {{ task = run(work())\n answer = wait(task) }}" )) .is_ok() ); assert_eq!( linked("main() () { lost = result(dever.task.sleep(-1)) }") .unwrap_err() .code, "C012" ); assert!( linked("main() (answer: dever.time.SleepResult) { answer = result(dever.task.sleep(-1)) }") .is_ok() ); } #[test] fn timer_intrinsics_cannot_be_called_by_user_packages() { for name in ["timeSleep", "taskSleep"] { assert_eq!( linked(&format!("main() () {{ value = dever.system.{name}(0) }}")) .unwrap_err() .code, "C006" ); } } #[test] fn cancelling_a_task_does_not_claim_its_input_errors_were_handled() { let prefix = "type Result { Done\n error Failed(message: Text) }\nhandle(value: Result) () recover(\"worker handles an optional error\") {}\n"; let cancelled = format!( "{prefix}main() () {{ pending = run(handle(Result.Failed(\"reason\")))\n stop(pending) }}" ); assert_eq!(linked(&cancelled).unwrap_err().code, "C012"); let waited = format!( "{prefix}main() () {{ pending = run(handle(Result.Failed(\"reason\")))\n wait(pending) }}" ); assert!(linked(&waited).is_ok()); }