207 lines
9.8 KiB
Rust
207 lines
9.8 KiB
Rust
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:?}");
|
|
}
|