use dever::compiler::{check, model::Program}; use dever::syntax::{diagnostic::Diagnostic, source::Source}; fn linked(text: &str) -> Result { let sources = sources(text); let modules = sources .iter() .map(dever::syntax::parse) .collect::, _>>()?; check::check( modules, &["main".into(), "dever.bytes".into(), "text".into()], ) } fn sources(text: &str) -> [Source; 3] { [ Source::new(0, "main.dever", text.into()), Source::new( 1, "lib/dever/bytes.dever", include_str!("../../../lib/dever/bytes.dever").into(), ), Source::new( 2, "lib/text.dever", include_str!("../../../lib/text.dever").into(), ), ] } fn generated_entry(text: &str) -> String { let mut program = linked(text).unwrap(); let entry = program .functions .iter() .position(|function| function.signature.name == "main") .unwrap(); // 改名不改变内建操作身份,防止优化依赖 concat/from_text 的函数名称。 for (index, function) in program.functions.iter_mut().enumerate() { function.signature.name = format!("renamed_{index}"); } let generated = dever::compiler::emit::generate(&program, &sources(text), &[entry].into()); let start = generated.find(&format!("pub fn function{entry}(")).unwrap(); generated[start..] .split("\npub ") .next() .unwrap() .to_owned() } #[test] fn borrowed_bytes_keep_their_shared_owner() { let generated = generated_entry("main(bytes: Bytes) (answer: Bytes) { answer = bytes }"); assert!( generated.contains("arg0: &dever_runtime::Bytes"), "{generated}" ); assert!(generated.contains(".to_owned()"), "{generated}"); assert!(!generated.contains("copy_from_slice"), "{generated}"); } #[test] fn text_byte_views_are_limited_to_non_escaping_iteration() { let generated = generated_entry( "add(item: Int, total: Int) (next: Int) { next = total + item }\nmain(text: Text) (answer: Int) { bytes = dever.bytes.fromText(text)\nanswer = reduce(add, bytes, 0) }", ); assert!(generated.contains(".as_bytes()"), "{generated}"); assert!(!generated.contains("copy_from_slice"), "{generated}"); let generated = generated_entry( "add(item: Int, total: Int) (next: Int) { next = total + item }\nmain(text: Text) (answer: Int) { bytes = dever.bytes.fromText(text)\nanswer = reduce(add, bytes, 0) + length(bytes) }", ); assert!(!generated.contains("copy_from_slice"), "{generated}"); for body in [ "main(text: Text) (answer: Bytes) { bytes = dever.bytes.fromText(text)\nanswer = bytes }", "identity(bytes: Bytes) (answer: Bytes) { answer = bytes }\nmain(text: Text) (answer: Bytes) { bytes = dever.bytes.fromText(text)\nanswer = identity(bytes) }", ] { let generated = generated_entry(body); assert!(generated.contains("copy_from_slice"), "{generated}"); } } #[test] fn empty_separator_parts_are_fused_only_when_fully_concatenated() { let join = "joinPart(item: Bytes, state: Bytes) (answer: Bytes) { answer = dever.bytes.concat(state, item) }\n"; let generated = generated_entry( &(join.to_owned() + "main(text: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\nanswer = reduce(joinPart, parts, seed) }"), ); assert!( generated.contains("state.extend_from_slice(sequence)"), "{generated}" ); assert!(!generated.contains("copy_from_slice"), "{generated}"); assert!(!generated.contains("collect::>()"), "{generated}"); for body in [ "main(text: Text) (answer: List) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\nanswer = parts }", "main(text: Text, seed: Bytes) (answer: Bytes, count: Int) { parts = each(dever.bytes.fromText, text.split(text, \"\"))\ncount = length(parts)\nanswer = reduce(joinPart, parts, seed) }", "main(text: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, \",\"))\nanswer = reduce(joinPart, parts, seed) }", "main(text: Text, separator: Text, seed: Bytes) (answer: Bytes) { parts = each(dever.bytes.fromText, text.split(text, separator))\nanswer = reduce(joinPart, parts, seed) }", ] { let generated = generated_entry(&(join.to_owned() + body)); assert!(generated.contains("copy_from_slice"), "{generated}"); } } #[test] fn literal_text_concat_allocates_only_the_final_buffer() { for literal in [r#""""#, r#""prefix""#, r#""雪\u{0}🙂""#] { let generated = generated_entry(&format!( "main(suffix: Bytes) (answer: Bytes) {{ answer = dever.bytes.concat(dever.bytes.fromText({literal}), suffix) }}" )); assert!( generated.contains("value::bytes::concat(prefix, suffix)"), "{generated}" ); assert!(!generated.contains(".to_vec()"), "{generated}"); assert!(!generated.contains("extend_from_slice"), "{generated}"); } } #[test] fn dynamic_text_and_owned_byte_accumulators_keep_their_existing_paths() { for (source, operation) in [ ( "main(text: Text, suffix: Bytes) (answer: Bytes) { answer = dever.bytes.concat(dever.bytes.fromText(text), suffix) }", "Bytes::copy_from_slice", ), ( "main(suffix: Bytes) (answer: Bytes) { text = \"owned\"\n answer = dever.bytes.concat(dever.bytes.fromText(text), suffix) }", ".into_bytes()", ), ( "main(suffix: Bytes) (answer: Bytes) { bytes = dever.bytes.fromText(\"owned\")\n answer = dever.bytes.concat(bytes, suffix) }", "bytes.extend_from_slice(suffix)", ), ] { let generated = generated_entry(source); assert!(generated.contains(operation), "{generated}"); assert!( !generated.contains("value::bytes::concat(prefix, suffix)"), "{generated}" ); } } #[test] fn bytes_keep_their_declared_collection_contract() { for source in [ "identity(item: Int) (answer: Int) { answer = item }\nmain(bytes: Bytes) (answer: List) { answer = each(identity, bytes) }", "add(item: Int, state: Int) (next: Int) { next = item + state }\nmain(bytes: Bytes) (answer: Int) { answer = reduce(add, bytes, 0) }", "main(values: List) (answer: List) { answer = each(dever.bytes.fromText, values) }", "main(bytes: Bytes?) (answer: Bytes?) { answer = bytes }", ] { let result = linked(source); assert!(result.is_ok(), "{source}: {result:?}"); } for source in [ "main(bytes: Bytes) (answer: Int) { answer = sum(bytes) }", "identity(item: Int) (answer: Int) { answer = item }\nmain(bytes: Bytes) (answer: Int) { answer = sum(identity, bytes) }", "yes(item: Int) (answer: Bool) { answer = true }\nmain(bytes: Bytes) (answer: List) { answer = filter(yes, bytes) }", "yes(item: Int) (answer: Bool) { answer = true }\nmain(bytes: Bytes) (answer: Int?) { answer = find(yes, bytes) }", "identity(item: Text) (answer: Text) { answer = item }\nmain(bytes: Bytes) (answer: List) { answer = each(identity, bytes) }", "main(bytes: Bytes) (answer: List) { answer = bytes }", "main(values: List) (answer: Bytes) { answer = values }", "main(values: Map) () {}", ] { assert_eq!(linked(source).unwrap_err().code, "C005", "{source}"); } } #[test] fn buffer_reductions_append_without_copying_the_accumulator() { for (kind, body, method) in [ ("Text", "state + item", "push_str"), ( "Bytes", "dever.bytes.concat(state, item)", "extend_from_slice", ), ] { let source = format!( "appendPiece(item: {kind}, state: {kind}) (answer: {kind}) {{ answer = {body} }}\nmain(values: List<{kind}>, initial: {kind}) (answer: {kind}) {{ answer = reduce(appendPiece, values, initial) }}" ); let generated = generated_entry(&source); let iteration = generated.split("for item in sequence").nth(1).unwrap(); assert!( iteration.contains(&format!("state.{method}(item)")), "{generated}" ); for copy in [".clone()", ".to_owned()", ".to_vec()"] { assert!(!iteration.contains(copy), "{generated}"); } } } #[test] fn literal_buffer_seeds_do_not_roundtrip_through_immutable_bytes() { for literal in [r#""""#, r#""seed""#, r#""雪\u{0}🙂""#] { let generated = generated_entry(&format!( "appendPiece(item: Bytes, state: Bytes) (answer: Bytes) {{ answer = dever.bytes.concat(state, item) }}\nmain(values: List) (answer: Bytes) {{ answer = reduce(appendPiece, values, dever.bytes.fromText({literal})) }}" )); assert!(generated.contains("let mut state: Vec"), "{generated}"); assert!(!generated.contains("Bytes::from_static"), "{generated}"); assert!(!generated.contains("Vec::::from"), "{generated}"); } } #[test] fn library_helpers_and_intrinsics_are_private() { for source in [ "main() (answer: Bytes) { answer = dever.system.bytesFromText(\"x\") }", "main() (answer: Bytes) { answer = dever.bytes.ready(dever.bytes.BufferResult.Ready(dever.bytes.fromText(\"x\"))) }", "main() (answer: Text) { answer = dever.bytes.decoded(dever.bytes.DecodeResult.Decoded(\"x\")) }", ] { assert_eq!(linked(source).unwrap_err().code, "C006", "{source}"); } } #[test] fn byte_errors_keep_nominal_identity_and_obligations() { assert_eq!( linked("main(bytes: Bytes) () { captured = result(dever.bytes.toText(bytes)) }") .unwrap_err() .code, "C012" ); assert_eq!(linked("type Result { Decoded(text: Text)\n error Failed(message: Text) }\nmain(bytes: Bytes) (answer: Result) { answer = result(dever.bytes.toText(bytes)) }").unwrap_err().code, "C005"); assert!(linked("main(bytes: Bytes) (answer: dever.bytes.DecodeResult) { answer = result(dever.bytes.toText(bytes)) }").is_ok()); assert!(linked("type Result { Ready(text: Text)\n error Failed(message: Text) }\nidentity() (text: Text) { text = \"ok\" }\nmain() (answer: Result) { captured = result(identity())\n answer = captured }").is_ok()); } #[test] fn package_names_resolve_without_exposing_private_types() { let library = Source::new(1, "sample.dever", "type Hidden { value: Int }\npublic type Visible { value: Int }\npublic read(value: Int) (answer: Int) { answer = value }".into()); for (source, accepted) in [ ( "main() (answer: sample.Visible) { answer = sample.Visible { value = sample.read(1) } }", true, ), ("main(value: sample.Hidden) () {}", false), ("main() () { value = sample.Hidden { value = 1 } }", false), ( "main() (answer: Visible) { answer = sample.Visible { value = 1 } }", false, ), ] { let source = Source::new(0, "main.dever", source.into()); let result = check::check( vec![ dever::syntax::parse(&source).unwrap(), dever::syntax::parse(&library).unwrap(), ], &["main".into(), "sample".into()], ); assert_eq!(result.is_ok(), accepted, "{result:?}"); } }