use std::collections::HashMap;
use std::path::PathBuf;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Confidence {
High,
#[default]
Medium,
Low,
}
impl std::fmt::Display for Confidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::High => write!(f, "high"),
Self::Medium => write!(f, "medium"),
Self::Low => write!(f, "low"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Condition {
pub variable: String,
pub constraint: String,
pub source_line: u32,
pub confidence: Confidence,
}
impl Condition {
pub fn high(variable: impl Into<String>, constraint: impl Into<String>, line: u32) -> Self {
Self {
variable: variable.into(),
constraint: constraint.into(),
source_line: line,
confidence: Confidence::High,
}
}
pub fn medium(variable: impl Into<String>, constraint: impl Into<String>, line: u32) -> Self {
Self {
variable: variable.into(),
constraint: constraint.into(),
source_line: line,
confidence: Confidence::Medium,
}
}
pub fn low(variable: impl Into<String>, constraint: impl Into<String>, line: u32) -> Self {
Self {
variable: variable.into(),
constraint: constraint.into(),
source_line: line,
confidence: Confidence::Low,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvariantKind {
Type,
NonNull,
NonNegative,
Positive,
Range,
Relation,
Length,
}
impl std::fmt::Display for InvariantKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Type => write!(f, "type"),
Self::NonNull => write!(f, "non_null"),
Self::NonNegative => write!(f, "non_negative"),
Self::Positive => write!(f, "positive"),
Self::Range => write!(f, "range"),
Self::Relation => write!(f, "relation"),
Self::Length => write!(f, "length"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Invariant {
pub variable: String,
pub kind: InvariantKind,
pub expression: String,
pub confidence: Confidence,
pub observations: u32,
pub counterexample_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InputOutputSpec {
pub function: String,
pub inputs: Vec<serde_json::Value>,
pub output: serde_json::Value,
pub test_function: String,
pub line: u32,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExceptionSpec {
pub function: String,
pub inputs: Vec<serde_json::Value>,
pub exception_type: String,
pub match_pattern: Option<String>,
pub test_function: String,
pub line: u32,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PropertySpec {
pub function: String,
pub property_type: String,
pub constraint: String,
pub test_function: String,
pub line: u32,
pub confidence: Confidence,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Interval {
pub lo: f64,
pub hi: f64,
}
impl Serialize for Interval {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Interval", 2)?;
if self.lo == f64::NEG_INFINITY {
state.serialize_field("lo", "-inf")?;
} else if self.lo == f64::INFINITY {
state.serialize_field("lo", "+inf")?;
} else if self.lo.is_nan() {
state.serialize_field("lo", "NaN")?;
} else {
state.serialize_field("lo", &self.lo)?;
}
if self.hi == f64::NEG_INFINITY {
state.serialize_field("hi", "-inf")?;
} else if self.hi == f64::INFINITY {
state.serialize_field("hi", "+inf")?;
} else if self.hi.is_nan() {
state.serialize_field("hi", "NaN")?;
} else {
state.serialize_field("hi", &self.hi)?;
}
state.end()
}
}
impl<'de> Deserialize<'de> for Interval {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct IntervalHelper {
lo: serde_json::Value,
hi: serde_json::Value,
}
let helper = IntervalHelper::deserialize(deserializer)?;
fn parse_bound(v: serde_json::Value) -> Result<f64, String> {
match v {
serde_json::Value::Number(n) => {
n.as_f64().ok_or_else(|| "invalid number".to_string())
}
serde_json::Value::String(s) => match s.as_str() {
"-inf" | "-Infinity" => Ok(f64::NEG_INFINITY),
"+inf" | "inf" | "Infinity" => Ok(f64::INFINITY),
"NaN" => Ok(f64::NAN),
_ => s.parse::<f64>().map_err(|e| e.to_string()),
},
serde_json::Value::Null => Ok(f64::INFINITY), _ => Err("expected number or string".to_string()),
}
}
let lo = parse_bound(helper.lo).map_err(serde::de::Error::custom)?;
let hi = parse_bound(helper.hi).map_err(serde::de::Error::custom)?;
Ok(Interval { lo, hi })
}
}
impl Interval {
pub fn const_val(n: f64) -> Self {
Self { lo: n, hi: n }
}
pub fn top() -> Self {
Self {
lo: f64::NEG_INFINITY,
hi: f64::INFINITY,
}
}
pub fn bottom() -> Self {
Self {
lo: f64::INFINITY,
hi: f64::NEG_INFINITY,
}
}
pub fn is_bottom(&self) -> bool {
self.lo > self.hi
}
pub fn is_top(&self) -> bool {
self.lo == f64::NEG_INFINITY && self.hi == f64::INFINITY
}
pub fn contains(&self, n: f64) -> bool {
!self.is_bottom() && self.lo <= n && n <= self.hi
}
pub fn contains_zero(&self) -> bool {
self.contains(0.0)
}
pub fn join(&self, other: &Self) -> Self {
if self.is_bottom() {
return *other;
}
if other.is_bottom() {
return *self;
}
Self {
lo: self.lo.min(other.lo),
hi: self.hi.max(other.hi),
}
}
pub fn meet(&self, other: &Self) -> Self {
if self.is_bottom() || other.is_bottom() {
return Self::bottom();
}
Self {
lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi),
}
}
pub fn widen(&self, other: &Self) -> Self {
if self.is_bottom() {
return *other;
}
if other.is_bottom() {
return *self;
}
Self {
lo: if other.lo < self.lo {
f64::NEG_INFINITY
} else {
self.lo
},
hi: if other.hi > self.hi {
f64::INFINITY
} else {
self.hi
},
}
}
pub fn add(&self, other: &Self) -> Self {
if self.is_bottom() || other.is_bottom() {
return Self::bottom();
}
Self {
lo: self.lo + other.lo,
hi: self.hi + other.hi,
}
}
pub fn sub(&self, other: &Self) -> Self {
if self.is_bottom() || other.is_bottom() {
return Self::bottom();
}
Self {
lo: self.lo - other.hi,
hi: self.hi - other.lo,
}
}
pub fn mul(&self, other: &Self) -> Self {
if self.is_bottom() || other.is_bottom() {
return Self::bottom();
}
let products = [
self.lo * other.lo,
self.lo * other.hi,
self.hi * other.lo,
self.hi * other.hi,
];
Self {
lo: products.iter().cloned().fold(f64::INFINITY, f64::min),
hi: products.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
}
}
pub fn div(&self, other: &Self) -> (Self, bool) {
if self.is_bottom() || other.is_bottom() {
return (Self::bottom(), false);
}
let may_div_zero = other.contains_zero();
if other.lo == 0.0 && other.hi == 0.0 {
return (Self::bottom(), true);
}
if may_div_zero {
return (Self::top(), true);
}
let products = [
self.lo / other.lo,
self.lo / other.hi,
self.hi / other.lo,
self.hi / other.hi,
];
(
Self {
lo: products.iter().cloned().fold(f64::INFINITY, f64::min),
hi: products.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
},
false,
)
}
pub fn neg(&self) -> Self {
if self.is_bottom() {
return Self::bottom();
}
Self {
lo: -self.hi,
hi: -self.lo,
}
}
}
impl Default for Interval {
fn default() -> Self {
Self::top()
}
}
impl std::fmt::Display for Interval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_bottom() {
write!(f, "bottom")
} else if self.is_top() {
write!(f, "(-inf, +inf)")
} else if self.lo == self.hi {
write!(f, "[{}]", self.lo)
} else {
let lo_str = if self.lo == f64::NEG_INFINITY {
"-inf".to_string()
} else {
self.lo.to_string()
};
let hi_str = if self.hi == f64::INFINITY {
"+inf".to_string()
} else {
self.hi.to_string()
};
write!(f, "[{}, {}]", lo_str, hi_str)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeadStore {
pub variable: String,
pub ssa_name: String,
pub line: u32,
pub block_id: u32,
pub is_phi: bool,
}
impl DeadStore {
pub fn to_dict(&self) -> serde_json::Value {
serde_json::json!({
"variable": self.variable,
"ssa_name": self.ssa_name,
"line": self.line,
"block_id": self.block_id,
"is_phi": self.is_phi,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChopResult {
pub lines: Vec<u32>,
pub count: u32,
pub source_line: u32,
pub target_line: u32,
pub path_exists: bool,
pub function: String,
pub explanation: Option<String>,
}
impl ChopResult {
pub fn same_line(line: u32, function: impl Into<String>) -> Self {
Self {
lines: vec![line],
count: 1,
source_line: line,
target_line: line,
path_exists: true,
function: function.into(),
explanation: Some(format!("Source and target are the same line ({}).", line)),
}
}
pub fn no_path(source: u32, target: u32, function: impl Into<String>) -> Self {
Self {
lines: vec![],
count: 0,
source_line: source,
target_line: target,
path_exists: false,
function: function.into(),
explanation: Some(format!(
"No dependency path from line {} to line {}. \
The source line does not affect the target line.",
source, target
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntervalWarning {
pub line: u32,
pub kind: String,
pub variable: String,
pub bounds: Interval,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContractsReport {
pub function: String,
pub file: PathBuf,
pub preconditions: Vec<Condition>,
pub postconditions: Vec<Condition>,
pub invariants: Vec<Condition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionInvariants {
pub function_name: String,
pub preconditions: Vec<Invariant>,
pub postconditions: Vec<Invariant>,
pub observation_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InvariantsSummary {
pub total_observations: u32,
pub total_invariants: u32,
pub by_kind: HashMap<String, u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InvariantsReport {
pub functions: Vec<FunctionInvariants>,
pub summary: InvariantsSummary,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionSpecs {
pub function_name: String,
pub summary: String,
pub test_count: u32,
pub input_output_specs: Vec<InputOutputSpec>,
pub exception_specs: Vec<ExceptionSpec>,
pub property_specs: Vec<PropertySpec>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpecsByType {
pub input_output: u32,
pub exception: u32,
pub property: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpecsSummary {
pub total_specs: u32,
pub by_type: SpecsByType,
pub test_functions_scanned: u32,
pub test_files_scanned: u32,
pub functions_found: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpecsReport {
pub functions: Vec<FunctionSpecs>,
pub summary: SpecsSummary,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BoundsResult {
pub function: String,
pub bounds: HashMap<u32, HashMap<String, Interval>>,
pub warnings: Vec<IntervalWarning>,
pub converged: bool,
pub iterations: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeadStoresReport {
pub function: String,
pub file: PathBuf,
pub dead_stores_ssa: Vec<DeadStore>,
pub count: u32,
pub dead_stores_live_vars: Option<Vec<DeadStore>>,
pub live_vars_count: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubAnalysisStatus {
Success,
Partial,
Failed,
Skipped,
}
impl SubAnalysisStatus {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success | Self::Partial)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubAnalysisResult {
pub name: String,
pub status: SubAnalysisStatus,
pub items_found: u32,
pub elapsed_ms: u64,
pub error: Option<String>,
pub data: Option<serde_json::Value>,
}
impl SubAnalysisResult {
pub fn success(&self) -> bool {
self.status.is_success()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoverageInfo {
pub constrained_functions: u32,
pub total_functions: u32,
pub coverage_pct: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VerifySummary {
pub spec_count: u32,
pub invariant_count: u32,
pub contract_count: u32,
pub annotated_count: u32,
pub behavioral_count: u32,
pub pattern_count: u32,
pub pattern_high_confidence: u32,
pub coverage: CoverageInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VerifyReport {
pub path: PathBuf,
pub sub_results: HashMap<String, SubAnalysisResult>,
pub summary: VerifySummary,
pub total_elapsed_ms: u64,
pub files_analyzed: u32,
pub files_failed: u32,
pub partial_results: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Json,
Text,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json => write!(f, "json"),
Self::Text => write!(f, "text"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confidence_enum_serialization() {
assert_eq!(
serde_json::to_string(&Confidence::High).unwrap(),
"\"high\""
);
assert_eq!(
serde_json::to_string(&Confidence::Medium).unwrap(),
"\"medium\""
);
assert_eq!(serde_json::to_string(&Confidence::Low).unwrap(), "\"low\"");
}
#[test]
fn test_confidence_enum_deserialization() {
assert_eq!(
serde_json::from_str::<Confidence>("\"high\"").unwrap(),
Confidence::High
);
assert_eq!(
serde_json::from_str::<Confidence>("\"medium\"").unwrap(),
Confidence::Medium
);
assert_eq!(
serde_json::from_str::<Confidence>("\"low\"").unwrap(),
Confidence::Low
);
}
#[test]
fn test_confidence_default() {
assert_eq!(Confidence::default(), Confidence::Medium);
}
#[test]
fn test_condition_struct_fields() {
let cond = Condition::high("x", "x >= 0", 10);
assert_eq!(cond.variable, "x");
assert_eq!(cond.constraint, "x >= 0");
assert_eq!(cond.source_line, 10);
assert_eq!(cond.confidence, Confidence::High);
}
#[test]
fn test_condition_serialization() {
let cond = Condition::high("x", "x >= 0", 10);
let json = serde_json::to_string(&cond).unwrap();
assert!(json.contains("\"variable\":\"x\""));
assert!(json.contains("\"confidence\":\"high\""));
}
#[test]
fn test_interval_const_val() {
let i = Interval::const_val(5.0);
assert_eq!(i.lo, 5.0);
assert_eq!(i.hi, 5.0);
assert!(i.contains(5.0));
assert!(!i.contains(4.0));
assert!(!i.contains(6.0));
}
#[test]
fn test_interval_basic_operations() {
let i = Interval { lo: 0.0, hi: 10.0 };
assert!(i.contains(0.0));
assert!(i.contains(5.0));
assert!(i.contains(10.0));
assert!(!i.contains(-1.0));
assert!(!i.contains(11.0));
}
#[test]
fn test_interval_bottom_top_detection() {
assert!(Interval::bottom().is_bottom());
assert!(!Interval::top().is_bottom());
assert!(Interval::top().is_top());
assert!(!Interval::bottom().is_top());
assert!(!Interval::const_val(5.0).is_bottom());
assert!(!Interval::const_val(5.0).is_top());
}
#[test]
fn test_interval_contains_zero() {
assert!(Interval { lo: -5.0, hi: 5.0 }.contains_zero());
assert!(Interval { lo: 0.0, hi: 10.0 }.contains_zero());
assert!(Interval { lo: -10.0, hi: 0.0 }.contains_zero());
assert!(!Interval { lo: 1.0, hi: 10.0 }.contains_zero());
assert!(!Interval {
lo: -10.0,
hi: -1.0
}
.contains_zero());
}
#[test]
fn test_interval_join() {
let a = Interval { lo: 0.0, hi: 5.0 };
let b = Interval { lo: 3.0, hi: 10.0 };
let joined = a.join(&b);
assert_eq!(joined.lo, 0.0);
assert_eq!(joined.hi, 10.0);
}
#[test]
fn test_interval_meet() {
let a = Interval { lo: 0.0, hi: 5.0 };
let b = Interval { lo: 3.0, hi: 10.0 };
let met = a.meet(&b);
assert_eq!(met.lo, 3.0);
assert_eq!(met.hi, 5.0);
}
#[test]
fn test_interval_add() {
let a = Interval { lo: 1.0, hi: 5.0 };
let b = Interval { lo: 2.0, hi: 3.0 };
let sum = a.add(&b);
assert_eq!(sum.lo, 3.0);
assert_eq!(sum.hi, 8.0);
}
#[test]
fn test_interval_sub() {
let a = Interval { lo: 5.0, hi: 10.0 };
let b = Interval { lo: 1.0, hi: 3.0 };
let diff = a.sub(&b);
assert_eq!(diff.lo, 2.0); assert_eq!(diff.hi, 9.0); }
#[test]
fn test_interval_mul() {
let a = Interval { lo: 2.0, hi: 3.0 };
let b = Interval { lo: 4.0, hi: 5.0 };
let prod = a.mul(&b);
assert_eq!(prod.lo, 8.0);
assert_eq!(prod.hi, 15.0);
}
#[test]
fn test_interval_mul_negative() {
let a = Interval { lo: -2.0, hi: 3.0 };
let b = Interval { lo: -1.0, hi: 2.0 };
let prod = a.mul(&b);
assert_eq!(prod.lo, -4.0);
assert_eq!(prod.hi, 6.0);
}
#[test]
fn test_interval_div() {
let a = Interval { lo: 10.0, hi: 20.0 };
let b = Interval { lo: 2.0, hi: 5.0 };
let (quot, div_zero) = a.div(&b);
assert!(!div_zero);
assert_eq!(quot.lo, 2.0); assert_eq!(quot.hi, 10.0); }
#[test]
fn test_interval_div_by_zero() {
let a = Interval { lo: 10.0, hi: 20.0 };
let b = Interval { lo: -1.0, hi: 1.0 }; let (_, div_zero) = a.div(&b);
assert!(div_zero);
}
#[test]
fn test_interval_widen() {
let a = Interval { lo: 0.0, hi: 10.0 };
let b = Interval { lo: -5.0, hi: 15.0 };
let widened = a.widen(&b);
assert_eq!(widened.lo, f64::NEG_INFINITY);
assert_eq!(widened.hi, f64::INFINITY);
}
#[test]
fn test_dead_store_struct() {
let ds = DeadStore {
variable: "x".to_string(),
ssa_name: "x_2".to_string(),
line: 10,
block_id: 1,
is_phi: false,
};
assert_eq!(ds.variable, "x");
assert_eq!(ds.ssa_name, "x_2");
assert_eq!(ds.line, 10);
assert!(!ds.is_phi);
}
#[test]
fn test_dead_store_serialization() {
let ds = DeadStore {
variable: "x".to_string(),
ssa_name: "x_2".to_string(),
line: 10,
block_id: 1,
is_phi: false,
};
let json = serde_json::to_string(&ds).unwrap();
assert!(json.contains("\"variable\":\"x\""));
assert!(json.contains("\"ssa_name\":\"x_2\""));
}
#[test]
fn test_chop_result_struct() {
let result = ChopResult {
lines: vec![2, 3, 4],
count: 3,
source_line: 2,
target_line: 4,
path_exists: true,
function: "example".to_string(),
explanation: Some("Found path".to_string()),
};
assert_eq!(result.count, 3);
assert!(result.path_exists);
}
#[test]
fn test_chop_result_same_line() {
let result = ChopResult::same_line(5, "test_func");
assert_eq!(result.lines, vec![5]);
assert_eq!(result.count, 1);
assert!(result.path_exists);
}
#[test]
fn test_chop_result_no_path() {
let result = ChopResult::no_path(2, 10, "test_func");
assert!(result.lines.is_empty());
assert_eq!(result.count, 0);
assert!(!result.path_exists);
}
#[test]
fn test_contracts_report_struct() {
let report = ContractsReport {
function: "process_data".to_string(),
file: PathBuf::from("test.py"),
preconditions: vec![Condition::high("x", "x >= 0", 10)],
postconditions: vec![],
invariants: vec![],
};
assert_eq!(report.function, "process_data");
assert_eq!(report.preconditions.len(), 1);
}
#[test]
fn test_invariant_kind_serialization() {
assert_eq!(
serde_json::to_string(&InvariantKind::Type).unwrap(),
"\"type\""
);
assert_eq!(
serde_json::to_string(&InvariantKind::NonNull).unwrap(),
"\"non_null\""
);
assert_eq!(
serde_json::to_string(&InvariantKind::NonNegative).unwrap(),
"\"non_negative\""
);
}
#[test]
fn test_output_format_default() {
assert_eq!(OutputFormat::default(), OutputFormat::Json);
}
#[test]
fn test_output_format_display() {
assert_eq!(OutputFormat::Json.to_string(), "json");
assert_eq!(OutputFormat::Text.to_string(), "text");
}
}