399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
mod support;
|
|
|
|
use support::{checked, rejected, sources};
|
|
|
|
#[test]
|
|
fn full_language_contract_package_checks_with_the_official_sources() {
|
|
let prd = include_str!("../../../.trellis/tasks/09-04-dever-language-mvp/prd.md");
|
|
let source = prd
|
|
.split_once("## Complete Package Example\n")
|
|
.unwrap()
|
|
.1
|
|
.split_once("```dever\n")
|
|
.unwrap()
|
|
.1
|
|
.split_once("\n```")
|
|
.unwrap()
|
|
.0;
|
|
let mut sources = dever_core::source::SourceMap::default();
|
|
sources.add("account/user.dever", source);
|
|
checked(&sources);
|
|
assert_eq!(sources.files().len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn clause_domains_are_disjoint_exhaustive_and_order_independent() {
|
|
for clauses in [
|
|
"choose(value: true) (answer: Int) { answer = 1 }\nchoose(value: false) (answer: Int) { answer = 2 }",
|
|
"choose(value: false) (answer: Int) { answer = 2 }\nchoose(value: true) (answer: Int) { answer = 1 }",
|
|
"choose(value: Text) (answer: Int) { answer = 1 }\nchoose(value: null) (answer: Int) { answer = 0 }",
|
|
"choose(value: Int < 0) (answer: Int) { answer = -1 }\nchoose(value: Int >= 0) (answer: Int) { answer = 1 }",
|
|
"choose(value: Decimal < 0.0) (answer: Int) { answer = -1 }\nchoose(value: other) (answer: Int) { answer = 1 }",
|
|
"choose(value: \"yes\") (answer: Int) { answer = 1 }\nchoose(value: other) (answer: Int) { answer = 0 }",
|
|
"choose(value: 1.0e0) (answer: Int) { answer = 1 }\nchoose(value: other) (answer: Int) { answer = 0 }",
|
|
] {
|
|
checked(&sources(clauses));
|
|
}
|
|
for (clauses, message) in [
|
|
("choose(value: true) () {}", "do not cover"),
|
|
(
|
|
"choose(value: Bool) () {}\nchoose(value: true) () {}",
|
|
"overlap",
|
|
),
|
|
(
|
|
"choose(value: true) () {}\nchoose(value: false) () {}\nchoose(value: other) () {}",
|
|
"unreachable",
|
|
),
|
|
(
|
|
"choose(value: Text?) () {}\nchoose(value: null) () {}",
|
|
"overlap",
|
|
),
|
|
(
|
|
"choose(value: null) () {}\nchoose(value: other) () {}",
|
|
"cannot infer",
|
|
),
|
|
("choose(value: Int > 0 and < 1) () {}", "unreachable"),
|
|
(
|
|
"choose(value: Int > 9223372036854775807) () {}",
|
|
"unreachable",
|
|
),
|
|
(
|
|
"choose(value: Decimal > 1.0 and < 1.000000000000000000000000000000001) () {}",
|
|
"unreachable",
|
|
),
|
|
("choose(value: Float > 0.0) () {}", "range bounds"),
|
|
("choose(value: Int > 0 and > 1) () {}", "one lower"),
|
|
(
|
|
"choose(left: true, right: Bool) () {}\nchoose(left: other, right: false) () {}",
|
|
"every input combination",
|
|
),
|
|
("choose(left: Bool, right: other) () {}", "cannot infer"),
|
|
("choose() () {}\nchoose() () {}", "overlap"),
|
|
(
|
|
"choose(value: true) (answer: Int) { answer = 1 }\nchoose(value: false) (different: Int) { different = 2 }",
|
|
"identical ordered output",
|
|
),
|
|
] {
|
|
rejected(clauses, message);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn four_quadrants_cover_a_multi_input_product() {
|
|
checked(&sources(
|
|
"public choose(a: true, b: true) () {}\npublic choose(a: true, b: other) () {}\npublic choose(a: other, b: true) () {}\npublic choose(a: other, b: other) () {}",
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn construction_initialization_and_stable_types_are_checked() {
|
|
let prefix =
|
|
"type User { name: Text\nphone: Text? }\ntype Result { Found(user: User)\nMissing }\n";
|
|
for (body, message) in [
|
|
("main() () { user = User { name = \"a\" } }", "every field"),
|
|
(
|
|
"main() () { user = User { name = \"a\"\nphone = null\nextra = 1 } }",
|
|
"unknown constructor field",
|
|
),
|
|
(
|
|
"main() () { user = User { name = \"a\"\nname = \"b\"\nphone = null } }",
|
|
"duplicate constructor",
|
|
),
|
|
("main() (user: User) { user.name = \"a\" }", "uninitialized"),
|
|
("main() (answer: Int) {}", "uninitialized"),
|
|
("main() () { value = null }", "nullable type context"),
|
|
("main() () { value = 1\nvalue = \"a\" }", "expected Int"),
|
|
("main() () { value = Result.Found() }", "payload arity"),
|
|
("main() () { value = Result.Missing(1) }", "payload arity"),
|
|
("main() () { values = [] }", "type context"),
|
|
("main() () { labels = {} }", "type context"),
|
|
("main() () { values = [1, true] }", "one stable type"),
|
|
("main() () { values = { 1.5 = 1 } }", "Map key"),
|
|
(
|
|
"main() () { values = { \"a\" = 1\n\"a\" = 2 } }",
|
|
"duplicate Map",
|
|
),
|
|
(
|
|
"main() (a: Int, b: Int) { a = 1\nb = 2 }\ninvalid() () { values = [main()] }",
|
|
"multi-output",
|
|
),
|
|
(
|
|
"main() (a: Int, b: Int) { a = 1\nb = 2 }\ninvalid() (same: Bool) { same = main() == main() }",
|
|
"comparable",
|
|
),
|
|
] {
|
|
rejected(&format!("{prefix}{body}"), message);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_rules_and_static_faults_are_enforced() {
|
|
for (expression, message) in [
|
|
("9223372036854775808", "64-bit"),
|
|
("-9223372036854775809", "64-bit"),
|
|
("0.12345678901234567890123456789012345", "34 digits"),
|
|
("9223372036854775807 + 1", "overflow"),
|
|
("-9223372036854775808 // -1", "overflow"),
|
|
("1 // 0", "division by zero"),
|
|
("1 / 0", "division by zero"),
|
|
("1 == 1.0", "does not promote"),
|
|
("1 + 1.0e0", "cannot implicitly mix"),
|
|
("1.0 // 2", "require Int"),
|
|
("1 and true", "expected Bool"),
|
|
] {
|
|
rejected(&format!("main() () {{ value = {expression} }}"), message);
|
|
}
|
|
checked(&sources(
|
|
"public main() (min: Int, exact: Bool, quotient: Decimal, float: Float) { min = -9223372036854775808\nexact = 0.1 + 0.2 == 0.3\nquotient = 5 / 2\nfloat = 1.5 + 2.5 }",
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn graphs_cover_type_references_and_collection_handlers() {
|
|
rejected("type Recursive { next: Recursive? }", "recursive type");
|
|
rejected("type A { b: B }\ntype B { a: List<A> }", "recursive type");
|
|
rejected(
|
|
"repeat_value(value: Int) (answer: Int) { values = each(repeat_value, [value])\nanswer = sum(values) }",
|
|
"recursive function",
|
|
);
|
|
rejected(
|
|
"f(value: true) () {}\ng() () {}\nf(value: false) () {}",
|
|
"contiguous",
|
|
);
|
|
let mut sources = dever_core::source::SourceMap::default();
|
|
sources.add(
|
|
"a.dever",
|
|
"public type A { value: Int }\nstart() () { b.run() }\nfinish() () {}",
|
|
);
|
|
sources.add("b.dever", "public run() () {}\nunused(value: a.A) () {}");
|
|
assert!(
|
|
dever_core::check(&sources)
|
|
.unwrap_err()
|
|
.iter()
|
|
.any(|error| error.message.contains("cyclic package"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_handlers_are_rejected_before_build() {
|
|
for (declarations, call, message) in [
|
|
(
|
|
"handler(value: Int) (a: Int, b: Int) { a = value\nb = value }",
|
|
"each(handler, [1])",
|
|
"invalid outputs",
|
|
),
|
|
(
|
|
"handler(value: Int) (value: Int) { value = 1 }",
|
|
"filter(handler, [1])",
|
|
"duplicate local",
|
|
),
|
|
(
|
|
"handler(value: Int) (answer: Int) { answer = value }",
|
|
"filter(handler, [1])",
|
|
"invalid outputs",
|
|
),
|
|
(
|
|
"handler(value: Int) () {}",
|
|
"sum(handler, [1])",
|
|
"invalid outputs",
|
|
),
|
|
(
|
|
"handler(value: Int) () {}",
|
|
"each(handler, [1], 2)",
|
|
"unknown function",
|
|
),
|
|
] {
|
|
rejected(
|
|
&format!("{declarations}\nmain() () {{ result = {call} }}"),
|
|
message,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unused_writes_are_warnings_not_errors() {
|
|
let sources = sources("public main() () { value = 1\nvalue = 2 }");
|
|
let program = checked(&sources);
|
|
assert_eq!(
|
|
program
|
|
.warnings()
|
|
.iter()
|
|
.filter(|warning| warning.code == "W001")
|
|
.count(),
|
|
2
|
|
);
|
|
assert!(
|
|
program.warnings()[0]
|
|
.render(&sources)
|
|
.contains("warning[W001]")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_scalar_and_choice_keys_are_rejected_when_duplicated() {
|
|
for body in [
|
|
"labels = { Key.First = 1\nKey.First = 2 }",
|
|
"key = Key.First\nlabels = { key = 1\nKey.First = 2 }",
|
|
"key = \"same\"\nlabels = { key = 1\n\"same\" = 2 }",
|
|
] {
|
|
rejected(
|
|
&format!("type Key {{ First\nSecond }}\nmain() () {{ {body} }}"),
|
|
"duplicate Map literal key",
|
|
);
|
|
}
|
|
rejected(
|
|
"invalid(key: Id) () { labels = { key = 1\nkey = 2 } }",
|
|
"duplicate Map literal key",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_numeric_faults_include_local_constants_and_constant_divisors() {
|
|
rejected(
|
|
"invalid(value: Int) (answer: Int) { answer = value // 0 }",
|
|
"division by zero",
|
|
);
|
|
rejected(
|
|
"invalid(value: Decimal) (answer: Decimal) { zero = 0\nanswer = value / zero }",
|
|
"division by zero",
|
|
);
|
|
rejected(
|
|
"invalid() () { value = 9223372036854775807\nanswer = value + 1 }",
|
|
"overflow",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn literal_clause_inference_is_stable_under_permutation() {
|
|
for clauses in [
|
|
"choose(value: 1) () {}\nchoose(value: 2.0) () {}\nchoose(value: other) () {}",
|
|
"choose(value: 2.0) () {}\nchoose(value: other) () {}\nchoose(value: 1) () {}",
|
|
] {
|
|
checked(&sources(clauses));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn names_follow_the_existing_language_conventions() {
|
|
rejected(
|
|
"package App exposes ()",
|
|
"package/exposes declarations were removed",
|
|
);
|
|
for source in [
|
|
"type user { name: Text }",
|
|
"Main() () {}",
|
|
"main() () { bad__name = 1 }",
|
|
] {
|
|
rejected(source, "ASCII");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn float_literal_context_never_retypes_existing_numeric_values() {
|
|
checked(&sources(
|
|
"compare(value: Float) (answer: Bool) { answer = value < 0.0 or 0.0 == value or (-0.0) >= value }",
|
|
));
|
|
rejected(
|
|
"compare(value: Float) (answer: Bool) { decimal = 0.0\nanswer = value == decimal }",
|
|
"same comparable type",
|
|
);
|
|
rejected(
|
|
"compare(value: Float) (answer: Bool) { answer = value == 0 }",
|
|
"same comparable type",
|
|
);
|
|
rejected(
|
|
"invalid() () { decimal = 1.0\nvalue = decimal + 1.0e0 }",
|
|
"cannot implicitly mix",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reduce_bytes_stream_and_static_handler_contracts_are_checked() {
|
|
checked(&sources(
|
|
r#"add(value: Int, total: Int) (next: Int) { next = total + value }
|
|
until(value: Int, total: Int) (next: Int, stop: Bool) { next = total + value
|
|
stop = value == 2 }
|
|
show(value: Int) () {}
|
|
apply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) }
|
|
forward(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = apply(route, value) }
|
|
consume(stream: Stream<Int>) (total: Int) { each(show, stream)
|
|
total = 0 }
|
|
main() () { total = reduce(add, [1, 2], 0)
|
|
partial = reduce_until(until, dever.bytes.from_text("abc"), 0) }"#,
|
|
));
|
|
|
|
for (source, message) in [
|
|
(
|
|
"map(value: Int) (answer: Int) { answer = value }\nbad(stream: Stream<Int>) () { values = each(map, stream) }",
|
|
"each on a stream",
|
|
),
|
|
(
|
|
"bad(value: Int, state: Int) (next: Int, done: Bool) { next = state\ndone = true }\nmain() () { value = reduce_until(bad, [1], 0) }",
|
|
"stop: Bool",
|
|
),
|
|
(
|
|
"apply(route: handler(value: Int) (answer: Int)) (answer: Int) { answer = route }",
|
|
"not runtime values",
|
|
),
|
|
(
|
|
"text(value: Text) (answer: Text) { answer = value }\napply(route: handler(value: Int) (answer: Int)) (answer: Int) { answer = route(1) }\nmain() () { answer = apply(text) }",
|
|
"signature does not match",
|
|
),
|
|
(
|
|
"wrong(value: Int) (result: Int) { result = value }\napply(route: handler(value: Int) (answer: Int)) (answer: Int) { answer = route(1) }\nmain() () { answer = apply(wrong) }",
|
|
"signature does not match",
|
|
),
|
|
(
|
|
"apply(route: handler(value: Int) (answer: Int)) () { values = [route] }",
|
|
"not runtime values",
|
|
),
|
|
(
|
|
"apply(route: handler(value: Int) (answer: Int)) (answer: Bool) { answer = route == route }",
|
|
"not runtime values",
|
|
),
|
|
(
|
|
"loop(next: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = next(value) }\nagain(value: Int) (answer: Int) { answer = loop(again, value) }",
|
|
"specialized handler",
|
|
),
|
|
] {
|
|
rejected(source, message);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn handler_functions_can_cross_packages_and_forward_statically() {
|
|
let mut sources = dever_core::source::SourceMap::default();
|
|
sources.add(
|
|
"worker.dever",
|
|
"public double(value: Int) (answer: Int) { answer = value * 2 }",
|
|
);
|
|
sources.add(
|
|
"main.dever",
|
|
"apply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) }\nforward(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = apply(route, value) }\npublic main() (answer: Int) { answer = forward(worker.double, 3) }",
|
|
);
|
|
checked(&sources);
|
|
}
|
|
|
|
#[test]
|
|
fn handler_signatures_and_streams_reuse_value_type_restrictions() {
|
|
for source in [
|
|
"apply(route: handler(value: Map<List<Int>, Int>) ()) () {}",
|
|
"apply(route: handler() (value: Map<List<Int>, Int>)) () {}",
|
|
"bad(values: Map<Stream<Int>, Int>) () {}",
|
|
] {
|
|
rejected(source, "invalid Map key type");
|
|
}
|
|
rejected(
|
|
"bad(stream: Stream) () {}",
|
|
"incorrect number of type arguments",
|
|
);
|
|
rejected(
|
|
"bad(stream: Stream<Int>) (same: Bool) { same = stream == stream }",
|
|
"same comparable type",
|
|
);
|
|
rejected(
|
|
"bad(values: List<Int>) () { close(values) }",
|
|
"close requires a Stream",
|
|
);
|
|
}
|