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:?}"); }