use dever::compiler::{check, model::Program}; use dever::syntax::{diagnostic::Diagnostic, source::Source}; fn linked(text: &str, identity: &str) -> Result { let sources = [ Source::new(0, "main.dever", text.into()), Source::new( 1, "lib/dever/io.dever", include_str!("../../../../lib/dever/io.dever").into(), ), ]; let modules = sources .iter() .map(dever::syntax::parse) .collect::, _>>()?; check::check(modules, &[identity.into(), "dever.io".into()]) } #[test] fn pure_rejects_direct_transitive_captured_and_handler_io() { let prefix = "write(value: Text) (answer: Text) { dever.io.println(value)\n answer = value }\napply(route: handler(value: Text) (answer: Text), value: Text) (answer: Text) { answer = route(value) }\n"; for source in [ "main(path: Text) () pure { file = dever.io.open(path) }".into(), "main(path: Text) (answer: dever.io.OpenResult) pure { answer = result(dever.io.open(path)) }".into(), "main() () pure { dever.io.println(\"hello\") }".into(), prefix.to_owned() + "main() (answer: Text) pure { answer = write(\"hello\") }", prefix.to_owned() + "main() (answer: Text) pure { answer = apply(write, \"hello\") }", prefix.to_owned() + "main() (answer: List) pure { answer = each(write, [\"hello\"]) }", "unused(route: handler() ()) () pure { route() }".into(), ] { let error = linked(&source, "main").unwrap_err(); assert_eq!(error.code, "C013", "{source}: {error:?}"); } let source = "identity(value: Text) (answer: Text) { answer = value }\napply(route: handler(value: Text) (answer: Text), value: Text) (answer: Text) { answer = route(value) }\nmain() (answer: Text) pure { answer = apply(identity, \"hello\") }"; assert!(linked(source, "main").is_ok()); } #[test] fn resource_equality_cannot_hide_inside_value_containers() { for source in [ "main(file: dever.system.File) (answer: Bool) { answer = file == file }", "main(file: dever.system.File) (answer: Bool) { answer = [file] == [file] }", "type Box { file: dever.system.File }\nmain(value: Box) (answer: Bool) { answer = value == value }", "type Box { Some(file: dever.system.File)\n None }\nmain(value: Box) (answer: Bool) { answer = value == value }", "main(file: dever.system.File?) (answer: Bool) { answer = file == null }", "main(files: Map) () {}", ] { assert_eq!(linked(source, "main").unwrap_err().code, "C005", "{source}"); } assert!(linked("type Box { files: List }\nmain(value: Box) (answer: Box) { answer = value }", "main").is_ok()); } #[test] fn file_errors_keep_nominal_identity_and_handling_obligations() { let source = "main(path: Text) (answer: dever.io.OpenResult) { answer = result(dever.io.open(path)) }"; assert!(linked(source, "main").is_ok()); assert_eq!( linked( "main(path: Text) () { lost = result(dever.io.open(path)) }", "main" ) .unwrap_err() .code, "C012" ); assert_eq!(linked("type OpenResult { Opened(file: dever.system.File)\n error Failed(message: Text) }\nmain(path: Text) (answer: OpenResult) { answer = result(dever.io.open(path)) }", "main").unwrap_err().code, "C005"); } #[test] fn raw_file_intrinsics_and_unmediated_app_effects_are_rejected() { assert_eq!( linked( "main() () { value = dever.system.fileOpen(\"file\") }", "main" ) .unwrap_err() .code, "C006" ); for identity in ["app.docs.app.files", "app.docs.domain.files"] { let source = "helper(path: Text) (file: dever.system.File) { file = dever.io.open(path) }\nmain(path: Text) (file: dever.system.File) { file = helper(path) }"; assert_eq!(linked(source, identity).unwrap_err().code, "C013"); } } #[test] fn stream_operations_remain_lazy_and_effectful() { let prefix = "count(item: Int, state: Int) (next: Int) { next = state + item }\n"; for source in [ "main(stream: Stream) () pure { close(stream) }".into(), prefix.to_owned() + "main(stream: Stream) (answer: Int) pure { answer = reduce(count, stream, 0) }", ] { assert_eq!( linked(&source, "main").unwrap_err().code, "C013", "{source}" ); } for source in [ "same(item: Int) (answer: Int) { answer = item }\nmain(stream: Stream) (answer: List) { answer = each(same, stream) }", "yes(item: Int) (answer: Bool) { answer = true }\nmain(stream: Stream) (answer: Int?) { answer = find(yes, stream) }", "main(stream: Stream) (answer: Int) { answer = length(stream) }", "main(stream: Stream) (answer: Bool) { answer = stream == stream }", "main(file: dever.system.File) () { close(file) }", ] { assert_eq!(linked(source, "main").unwrap_err().code, "C005", "{source}"); } assert!(linked("main(file: dever.system.File) () { stream = dever.io.chunks(file, 1)\n close(stream) }", "main").is_ok()); } #[test] fn stream_events_cannot_drop_failures_or_leak_private_types() { for source in [ "ignore(event: dever.io.ReadEvent) () {}\nmain(stream: Stream) () { each(ignore, stream) }", "ignore(event: dever.io.ReadEvent, state: Int) (next: Int) { next = state }\nmain(stream: Stream) (answer: Int) { answer = reduce(ignore, stream, 0) }", "main(file: dever.system.File) () { lost = result(dever.io.chunks(file, 0)) }", ] { assert_eq!(linked(source, "main").unwrap_err().code, "C012", "{source}"); } assert_eq!( linked( "type Hidden { value: Int }\npublic type Visible { stream: Stream }", "main" ) .unwrap_err() .code, "C006" ); assert_eq!( linked("type Cycle { stream: Stream }", "main") .unwrap_err() .code, "C007" ); }