Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,61 @@
|
||||
use super::super::checked;
|
||||
|
||||
#[test]
|
||||
fn handler_ranges_check_unused_templates_inference_and_forwarding() {
|
||||
let apply = "apply(route: handler(value: Int >= 1) (answer: Int >= 2)) (answer: Int >= 2) { answer = route(1) }\n";
|
||||
checked(apply).unwrap();
|
||||
checked(&(apply.to_owned() + "twice(value: Int) (answer: Int) { answer = 2 }\nmake() (answer: Int >= 2) { answer = apply(twice) }")).unwrap();
|
||||
assert_eq!(checked(&(apply.to_owned() + "identity(value: Int) (answer: Int) { answer = value }\nmake() (answer: Int) { answer = apply(identity) }")).unwrap_err().code, "C011");
|
||||
assert_eq!(
|
||||
checked(
|
||||
"bad(route: handler(value: Int >= 1) (answer: Int)) (answer: Int) { answer = route(0) }"
|
||||
)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
checked(&(apply.to_owned() + "forward(route: handler(value: Int) (answer: Int >= 3)) (answer: Int >= 2) { answer = apply(route) }")).unwrap();
|
||||
for contract in [
|
||||
"handler(value: Int >= 2) (answer: Int >= 3)",
|
||||
"handler(value: Int) (answer: Int >= 1)",
|
||||
] {
|
||||
assert_eq!(
|
||||
checked(&format!(
|
||||
"{apply}forward(route: {contract}) (answer: Int) {{ answer = apply(route) }}"
|
||||
))
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prototype_cache_keeps_distinct_range_contracts() {
|
||||
let prefix = "first(route: handler(value: Int) (answer: Int >= 2)) (answer: Int >= 2) { answer = route(1) }\n";
|
||||
assert_eq!(checked(&(prefix.to_owned() + "second(route: handler(value: Int) (answer: Int >= 1)) (answer: Int >= 2) { answer = route(1) }")).unwrap_err().code, "C011");
|
||||
assert_eq!(checked("first(route: handler(value: Int >= 1) (answer: Int)) (answer: Int) { answer = route(1) }\nsecond(route: handler(value: Int >= 2) (answer: Int)) (answer: Int) { answer = route(1) }").unwrap_err().code, "C011");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collection_handlers_check_items_context_and_repeated_state() {
|
||||
checked("valid(route: handler(value: Int >= 1) (answer: Int >= 2)) (answer: List<Int>) { answer = each(route, [1, 2]) }").unwrap();
|
||||
for body in [
|
||||
"bad(route: handler(value: Int >= 1) (answer: Int), values: List<Int>) (answer: List<Int>) { answer = each(route, values) }",
|
||||
"bad(route: handler(value: Int >= 1) (answer: Int)) (answer: List<Int>) { answer = each(route, [0, 1]) }",
|
||||
"bad(route: handler(value: Int, state: Int >= 1) (answer: Int)) (answer: Int) { answer = reduce(route, [1, 2], 1) }",
|
||||
"bad(route: handler(value: Int, context: Int >= 1) (answer: Int)) (answer: List<Int>) { answer = each(route, [1], 0) }",
|
||||
] {
|
||||
assert_eq!(checked(body).unwrap_err().code, "C011", "{body}");
|
||||
}
|
||||
checked("valid(route: handler(value: Int, state: Int >= 1) (answer: Int >= 1)) (answer: Int >= 1) { answer = reduce(route, [1, 2], 1) }").unwrap();
|
||||
checked("valid(route: handler(value: Int >= 0 and <= 255) (answer: Int), values: Bytes) (answer: List<Int>) { answer = each(route, values) }").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failing_collection_inputs_do_not_produce_invalid_values() {
|
||||
let prefix =
|
||||
"type Problem { error Failed }\nabortOperation() (answer: Int) { fail(Problem.Failed) }\n";
|
||||
checked(&(prefix.to_owned() + "apply(route: handler(value: Int, context: Int >= 1) (answer: Int)) (answer: List<Int>) { answer = each(route, [1], abortOperation()) }")).unwrap();
|
||||
checked("type Problem { error Failed }\nabortOperation(value: Int, state: Int) (answer: Int) { fail(Problem.Failed) }\napply(route: handler(value: Int, state: Int >= 1) (answer: Int >= 1)) (answer: Int >= 1) { answer = reduce(route, [1], 1) }\nentry() (answer: Int >= 1) { answer = apply(abortOperation) }").unwrap();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::checked;
|
||||
|
||||
mod handler_range;
|
||||
mod range;
|
||||
|
||||
#[test]
|
||||
fn decimal_and_float_clauses_cover_their_actual_value_domains() {
|
||||
for text in [
|
||||
"pick(value: Decimal < 0.0) (answer: Int) { answer = -1 }\npick(value: Decimal >= 0.0) (answer: Int) { answer = 1 }",
|
||||
"pick(value: other) (answer: Int) { answer = 1 }\npick(value: -0.0) (answer: Int) { answer = 0 }",
|
||||
"pick(value: null) (answer: Int) { answer = 0 }\npick(value: Decimal < 0.0) (answer: Int) { answer = -1 }\npick(value: other) (answer: Int) { answer = 1 }",
|
||||
"pick(value: 1.0e0) (answer: Int) { answer = 1 }\npick(value: other) (answer: Int) { answer = 0 }",
|
||||
"pick(value: null) (answer: Int) { answer = 0 }\npick(value: 0.0e0) (answer: Int) { answer = 1 }\npick(value: other) (answer: Int) { answer = 2 }",
|
||||
"pick(value: Int <= 9223372036854775807) () {}",
|
||||
"pick(value: Int >= -9223372036854775808) () {}",
|
||||
] {
|
||||
checked(text).unwrap_or_else(|error| panic!("{text}\n{error:?}"));
|
||||
}
|
||||
for text in [
|
||||
"pick(value: Decimal < 0.0) () {}",
|
||||
"pick(value: Decimal <= 0.0) () {}\npick(value: Decimal >= 0.0) () {}",
|
||||
"pick(value: 1.0) () {}\npick(value: 1.00) () {}\npick(value: other) () {}",
|
||||
"pick(value: 0.0e0) () {}\npick(value: -0.0e0) () {}\npick(value: other) () {}",
|
||||
"pick(value: 1.0e0) () {}",
|
||||
"pick(value: Float) () {}\npick(value: other) () {}",
|
||||
"pick(value: Int > 9223372036854775807) () {}\npick(value: other) () {}",
|
||||
] {
|
||||
let error = checked(text).unwrap_err();
|
||||
assert_eq!(error.code, "C009", "{text}: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_partitions_use_exact_successors_at_precision_and_exponent_limits() {
|
||||
for (left, right) in [
|
||||
("1.0", "1.000000000000000000000000000000001"),
|
||||
("0.0", "1e-6176"),
|
||||
("-1e-6176", "0.0"),
|
||||
(
|
||||
"9.999999999999999999999999999999998e6144",
|
||||
"9.999999999999999999999999999999999e6144",
|
||||
),
|
||||
] {
|
||||
let text = format!(
|
||||
"pick(value: Decimal <= {left}) () {{}}\npick(value: Decimal >= {right}) () {{}}"
|
||||
);
|
||||
checked(&text).unwrap_or_else(|error| panic!("{text}: {error:?}"));
|
||||
}
|
||||
for text in [
|
||||
"pick(value: Decimal <= 1.0) () {}\npick(value: Decimal >= 1.000000000000000000000000000000002) () {}",
|
||||
"pick(value: Decimal > 9.999999999999999999999999999999999e6144) () {}\npick(value: other) () {}",
|
||||
"pick(value: Decimal < -9.999999999999999999999999999999999e6144) () {}\npick(value: other) () {}",
|
||||
] {
|
||||
assert_eq!(checked(text).unwrap_err().code, "C009", "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_pattern_validation_rejects_invalid_bounds_and_mixed_types() {
|
||||
for (text, code) in [
|
||||
("pick(value: 9223372036854775808) () {}", "C008"),
|
||||
("pick(value: 1e9999) () {}", "C008"),
|
||||
(
|
||||
"pick(value: Decimal < 0.12345678901234567890123456789012345) () {}",
|
||||
"C008",
|
||||
),
|
||||
("pick(value: Int < 1.5) () {}", "C008"),
|
||||
("pick(value: Float < 1.0e0) () {}", "C005"),
|
||||
("pick(value: Decimal? < 1.0) () {}", "C005"),
|
||||
("pick(value: 1.0) () {}\npick(value: Float) () {}", "C005"),
|
||||
] {
|
||||
assert_eq!(checked(text).unwrap_err().code, code, "{text}");
|
||||
}
|
||||
assert!(checked("pick(value: Int >= 1 and) () {}").is_err());
|
||||
assert!(checked("pick(value: Decimal < 1.0 and) () {}").is_err());
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use super::super::checked;
|
||||
|
||||
const PORT: &str = "type Port { value: Int >= 1 and <= 65535 }\n";
|
||||
|
||||
#[test]
|
||||
fn nonreturning_values_propagate_failure_before_range_boundaries() {
|
||||
let source = PORT.to_owned()
|
||||
+ "type Problem { error Failed }\nabortOperation() (answer: Int) { fail(Problem.Failed) }\nmake() (answer: Port) { answer = Port { value = abortOperation() } }\n";
|
||||
checked(&source).unwrap();
|
||||
checked(&(source + "apply(route: handler() (answer: Int >= 1)) (answer: Int >= 1) { answer = route() }\nentry() (answer: Int >= 1) { answer = apply(abortOperation) }")).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges_check_construction_outputs_and_every_write() {
|
||||
for body in [
|
||||
"make() (answer: Port) { answer = Port { value = 8080 } }",
|
||||
"make(value: Int >= 1 and <= 65535) (answer: Port) { answer = Port { value = value } }\nmake(value: other) (answer: Port) { answer = Port { value = 8080 } }",
|
||||
"make(value: Port) (answer: Int >= 1 and <= 65535) { answer = value.value }",
|
||||
"make(value: Int < 1) (answer: Int >= 1) { answer = 1 }\nmake(value: other) (answer: Int >= 1) { answer = value }",
|
||||
"make(value: Int < 1) (answer: Int >= 1 and <= 65535) { answer = 1 }\nmake(value: Int > 65535) (answer: Int >= 1 and <= 65535) { answer = 65535 }\nmake(value: other) (answer: Int >= 1 and <= 65535) { answer = value }",
|
||||
] {
|
||||
checked(&(PORT.to_owned() + body)).unwrap_or_else(|error| panic!("{body}: {error:?}"));
|
||||
}
|
||||
for body in [
|
||||
"make() (answer: Port) { answer = Port { value = 0 } }",
|
||||
"make(value: Int) (answer: Port) { answer = Port { value = value } }",
|
||||
"make(value: Port) (answer: Port) { value.value = 65536\nanswer = value }",
|
||||
"make(value: Int) (answer: Int >= 1) { answer = value }",
|
||||
"make() (answer: Int >= 1) { answer = 0\nanswer = 1 }",
|
||||
"type Outer { port: Port }\nmake(value: Outer) (answer: Outer) { value.port.value = 0\nanswer = value }",
|
||||
] {
|
||||
assert_eq!(
|
||||
checked(&(PORT.to_owned() + body)).unwrap_err().code,
|
||||
"C011",
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_survive_mutation_without_retaining_stale_facts() {
|
||||
let prefix = "type Row { value: Int }\ntype Pair { left: Row\nright: Row }\n";
|
||||
let body = "row = Row { value = 3 }\npair = Pair { left = row, right = row }\nsaved = pair\npair.left.value = 0\nrow.value = -1\n";
|
||||
for value in ["saved.left.value", "saved.right.value", "pair.right.value"] {
|
||||
checked(&format!(
|
||||
"{prefix}make() (answer: Int >= 1) {{ {body}answer = {value} }}"
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
for value in ["pair.left.value", "row.value"] {
|
||||
assert_eq!(
|
||||
checked(&format!(
|
||||
"{prefix}make() (answer: Int >= 1) {{ {body}answer = {value} }}"
|
||||
))
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
}
|
||||
assert_eq!(checked("make(value: Int >= 1) (answer: Int >= 1) { value = 0\nanswer = value }\nmake(value: other) (answer: Int >= 1) { answer = 1 }").unwrap_err().code, "C011");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choice_payloads_and_captured_outputs_preserve_contracts() {
|
||||
let prefix = "type Result { Ready(value: Int >= 1)\nerror Failed(message: Text) }\n";
|
||||
let consumer = "read(value: Result.Ready(number)) (answer: Int >= 1) recover(\"fallback\") { answer = number }\nread(value: Result.Failed(message)) (answer: Int >= 1) recover(\"fallback\") { answer = 1 }\n";
|
||||
checked(
|
||||
&(prefix.to_owned() + consumer + "make() (answer: Result) { answer = Result.Ready(1) }"),
|
||||
)
|
||||
.unwrap();
|
||||
for value in ["0", "unknown"] {
|
||||
assert_eq!(
|
||||
checked(&format!(
|
||||
"{prefix}make(unknown: Int) (answer: Result) {{ answer = Result.Ready({value}) }}"
|
||||
))
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
}
|
||||
checked(&format!("{prefix}produce() (answer: Int) {{ answer = 3 }}\nmake() (answer: Result) {{ answer = result(produce()) }}")).unwrap();
|
||||
assert_eq!(checked(&format!("{prefix}produce(value: Int) (answer: Int) {{ answer = value }}\nmake(value: Int) (answer: Result) {{ answer = result(produce(value)) }}")).unwrap_err().code, "C011");
|
||||
checked("pair() (small: Int >= 1 and <= 3, large: Int >= 5) { small = 2\nlarge = 8 }\nread() (answer: Int >= 5) { saved = pair()\nanswer = saved.large }").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_proof_uses_checked_int_and_decimal_semantics() {
|
||||
checked(
|
||||
&(PORT.to_owned()
|
||||
+ "step(value: Port) (answer: Int >= 2 and <= 65536) { answer = value.value + 1 }"),
|
||||
)
|
||||
.unwrap();
|
||||
checked("step(value: Int >= -8 and <= 8) (answer: Int >= -16 and <= 16) { answer = value * -2 }\nstep(value: other) (answer: Int >= -16 and <= 16) { answer = 0 }").unwrap();
|
||||
checked("type Rate { value: Decimal >= 0.0 and <= 1.0 }\nscale(value: Rate) (answer: Decimal >= 0.0 and <= 100.0) { answer = value.value * 100.0 }").unwrap();
|
||||
checked("type Tiny { value: Decimal > 0.0 and <= 1e-6176 }\ncopy(value: Tiny) (answer: Decimal > 0.0) { answer = value.value }").unwrap();
|
||||
checked("type Edge { value: Decimal > 1.0 and <= 1.000000000000000000000000000000001 }\ncopy(value: Edge) (answer: Decimal > 1.0) { answer = value.value }").unwrap();
|
||||
checked("convert(value: Int >= 1 and <= 8) (answer: Decimal >= 1.0 and <= 8.0) { answer = value }\nconvert(value: other) (answer: Decimal >= 1.0 and <= 8.0) { answer = 1.0 }").unwrap();
|
||||
for body in [
|
||||
"read(value: Int) (answer: Int >= 0) { answer = value + 1 }",
|
||||
"read(value: Int >= 1) (answer: Int >= 0) { answer = -value }\nread(value: other) (answer: Int >= 0) { answer = 0 }",
|
||||
"read(value: Int >= 0) (answer: Int >= 0) { answer = value + 1 }\nread(value: other) (answer: Int >= 0) { answer = 0 }",
|
||||
] {
|
||||
assert_eq!(checked(body).unwrap_err().code, "C011", "{body}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_empty_and_inconsistent_ranges_are_rejected() {
|
||||
for fields in [
|
||||
"value: Int > 9223372036854775807",
|
||||
"value: Int < -9223372036854775808",
|
||||
"value: Int > 1 and < 2",
|
||||
"value: Decimal > 0.0 and < 1e-6176",
|
||||
] {
|
||||
assert_eq!(
|
||||
checked(&format!("type Invalid {{ {fields} }}"))
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C011"
|
||||
);
|
||||
}
|
||||
for fields in [
|
||||
"value: Float >= 1.0e0",
|
||||
"value: Int? >= 1",
|
||||
"value: Text >= 1",
|
||||
] {
|
||||
assert_eq!(
|
||||
checked(&format!("type Invalid {{ {fields} }}"))
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C005"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
checked("type Invalid { value: Decimal >= 0.12345678901234567890123456789012345 }")
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C008"
|
||||
);
|
||||
assert_eq!(checked("read(value: Int < 0) (answer: Int >= 1) { answer = 1 }\nread(value: other) (answer: Int >= 2) { answer = 2 }").unwrap_err().code, "C013");
|
||||
checked("read(value: Int < 0) (answer: Int > 0) { answer = 1 }\nread(value: other) (answer: Int >= 1) { answer = 2 }").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_proofs_remove_only_redundant_arithmetic_checks() {
|
||||
let text = "type Port { value: Int >= 1 and <= 65535 }\nstep(value: Port) (answer: Int >= 2 and <= 65536) { answer = value.value + 1 }";
|
||||
let mut plain = text.to_owned();
|
||||
for bound in [">= 1 and <= 65535", ">= 2 and <= 65536"] {
|
||||
plain = plain.replace(bound, &" ".repeat(bound.len()));
|
||||
}
|
||||
let source = |text: &str| dever::syntax::source::Source::new(0, "case.dever", text.into());
|
||||
let emit = |text: &str| {
|
||||
dever::compiler::emit::generate(
|
||||
&checked(text).unwrap(),
|
||||
&[source(text)],
|
||||
&std::collections::BTreeSet::from([0]),
|
||||
)
|
||||
};
|
||||
let bounded = emit(text);
|
||||
let unbounded = emit(&plain);
|
||||
assert!(bounded.contains("wrapping_add"));
|
||||
assert!(!bounded.contains("checked_add"));
|
||||
assert!(unbounded.contains("checked_add"));
|
||||
assert!(!unbounded.contains("wrapping_add"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_proofs_follow_field_updates_and_each_specialized_handler() {
|
||||
let text = "type Row { value: Int }\ncopy(value: Int) (answer: Int) { answer = value }\nread(value: Int) (answer: Int) { row = Row { value = 1 }\nsaved = row\nrow.value = value\nknown = saved.value + 1\nunknown = row.value + 1\nanswer = known + unknown }\napply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) + 1 }\nfixed(value: Int) (answer: Int) { answer = 1 }\nfixedCall(value: Int) (answer: Int) { answer = apply(fixed, value) }\nunknownCall(value: Int) (answer: Int) { answer = apply(copy, value) }";
|
||||
let program = checked(text).unwrap();
|
||||
let source = dever::syntax::source::Source::new(0, "case.dever", text.into());
|
||||
let emit = |name: &str| {
|
||||
let index = program
|
||||
.functions
|
||||
.iter()
|
||||
.position(|function| function.signature.name == name)
|
||||
.unwrap();
|
||||
dever::compiler::emit::generate(&program, std::slice::from_ref(&source), &[index].into())
|
||||
};
|
||||
let changed = emit("read");
|
||||
assert!(changed.contains("wrapping_add"));
|
||||
assert!(changed.contains("checked_add"));
|
||||
assert!(emit("fixedCall").contains("wrapping_add"));
|
||||
assert!(!emit("unknownCall").contains("wrapping_add"));
|
||||
assert!(emit("unknownCall").contains("checked_add"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_complement_proof_stops_at_its_work_budget() {
|
||||
let mut source = String::new();
|
||||
for value in 0..100 {
|
||||
source.push_str(&format!(
|
||||
"classify(value: {value}) (answer: Int) {{ answer = value }}\n"
|
||||
));
|
||||
}
|
||||
source.push_str("classify(value: other) (answer: Int) { answer = value }");
|
||||
checked(&source).unwrap();
|
||||
for value in 100..200 {
|
||||
source.push_str(&format!(
|
||||
"\nclassify(value: {value}) (answer: Int) {{ answer = value }}"
|
||||
));
|
||||
}
|
||||
let error = checked(&source).unwrap_err();
|
||||
assert_eq!(error.code, "C015");
|
||||
assert!(error.message.contains("range proof"), "{error:?}");
|
||||
}
|
||||
Reference in new issue
Block a user