259 lines
11 KiB
Rust
259 lines
11 KiB
Rust
use dever::compiler::{check, model::Program};
|
|
use dever::syntax::{diagnostic::Diagnostic, source::Source};
|
|
|
|
fn linked(text: &str) -> Result<Program, Diagnostic> {
|
|
let sources = sources(text);
|
|
let modules = sources
|
|
.iter()
|
|
.map(dever::syntax::parse)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
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::<Vec<_>>()"), "{generated}");
|
|
for body in [
|
|
"main(text: Text) (answer: List<Bytes>) { 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<Int>) { 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<Text>) (answer: List<Bytes>) { 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<Int>) { 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<Text>) { answer = each(identity, bytes) }",
|
|
"main(bytes: Bytes) (answer: List<Int>) { answer = bytes }",
|
|
"main(values: List<Int>) (answer: Bytes) { answer = values }",
|
|
"main(values: Map<Bytes, Int>) () {}",
|
|
] {
|
|
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<Bytes>) (answer: Bytes) {{ answer = reduce(appendPiece, values, dever.bytes.fromText({literal})) }}"
|
|
));
|
|
assert!(generated.contains("let mut state: Vec<u8>"), "{generated}");
|
|
assert!(!generated.contains("Bytes::from_static"), "{generated}");
|
|
assert!(!generated.contains("Vec::<u8>::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:?}");
|
|
}
|
|
}
|