#![allow(clippy::approx_constant)]
use super::smt::{SmtBackend, SmtResult, SmtSolver};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Counterexample {
pub assignments: HashMap<String, Value>,
pub trace: Vec<TraceStep>,
pub failed_assertion: String,
pub explanation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Value {
Int(i64),
Bool(bool),
String(String),
Float(f64),
Array(Vec<Value>),
Tuple(Vec<Value>),
Null,
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int(n) => write!(f, "{n}"),
Self::Bool(b) => write!(f, "{b}"),
Self::String(s) => write!(f, "\"{s}\""),
Self::Float(x) => write!(f, "{x}"),
Self::Array(vs) => {
write!(f, "[")?;
for (i, v) in vs.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v}")?;
}
write!(f, "]")
}
Self::Tuple(vs) => {
write!(f, "(")?;
for (i, v) in vs.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v}")?;
}
write!(f, ")")
}
Self::Null => write!(f, "null"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceStep {
pub step: usize,
pub location: String,
pub operation: String,
pub state: HashMap<String, Value>,
}
impl Counterexample {
pub fn new(failed_assertion: &str) -> Self {
Self {
assignments: HashMap::new(),
trace: Vec::new(),
failed_assertion: failed_assertion.to_string(),
explanation: None,
}
}
pub fn add_assignment(&mut self, var: &str, value: Value) {
self.assignments.insert(var.to_string(), value);
}
pub fn add_trace_step(&mut self, step: TraceStep) {
self.trace.push(step);
}
pub fn set_explanation(&mut self, explanation: &str) {
self.explanation = Some(explanation.to_string());
}
pub fn format_report(&self) -> String {
let mut report = String::new();
report.push_str("=== Counterexample Found ===\n\n");
report.push_str("Failed Assertion:\n");
report.push_str(&format!(" {}\n\n", self.failed_assertion));
if !self.assignments.is_empty() {
report.push_str("Variable Assignments:\n");
for (var, val) in &self.assignments {
report.push_str(&format!(" {var} = {val}\n"));
}
report.push('\n');
}
if !self.trace.is_empty() {
report.push_str("Execution Trace:\n");
for step in &self.trace {
report.push_str(&format!(
" Step {}: {} at {}\n",
step.step, step.operation, step.location
));
if !step.state.is_empty() {
for (var, val) in &step.state {
report.push_str(&format!(" {var} = {val}\n"));
}
}
}
report.push('\n');
}
if let Some(explanation) = &self.explanation {
report.push_str("Explanation:\n");
report.push_str(&format!(" {explanation}\n"));
}
report
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestCase {
pub inputs: HashMap<String, Value>,
pub expected: Option<Value>,
pub property: String,
}
impl TestCase {
pub fn new(property: &str) -> Self {
Self {
inputs: HashMap::new(),
expected: None,
property: property.to_string(),
}
}
pub fn add_input(&mut self, name: &str, value: Value) {
self.inputs.insert(name.to_string(), value);
}
pub fn set_expected(&mut self, value: Value) {
self.expected = Some(value);
}
pub fn to_ruchy_test(&self, test_name: &str) -> String {
let mut code = String::new();
code.push_str(&format!("#[test]\nfun {test_name}() {{\n"));
for (name, value) in &self.inputs {
code.push_str(&format!(
" let {} = {};\n",
name,
Self::value_to_ruchy(value)
));
}
code.push_str(&format!(" assert!({});\n", self.property));
if let Some(expected) = &self.expected {
code.push_str(&format!(
" assert_eq!(result, {});\n",
Self::value_to_ruchy(expected)
));
}
code.push_str("}\n");
code
}
fn value_to_ruchy(value: &Value) -> String {
match value {
Value::Int(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::String(s) => format!("\"{s}\""),
Value::Float(x) => format!("{x:.6}"),
Value::Array(vs) => {
let items: Vec<String> = vs.iter().map(Self::value_to_ruchy).collect();
format!("[{}]", items.join(", "))
}
Value::Tuple(vs) => {
let items: Vec<String> = vs.iter().map(Self::value_to_ruchy).collect();
format!("({})", items.join(", "))
}
Value::Null => "None".to_string(),
}
}
}
pub struct CounterexampleGenerator {
backend: SmtBackend,
max_iterations: usize,
shrinking: bool,
}
impl CounterexampleGenerator {
pub fn new() -> Self {
Self {
backend: SmtBackend::Z3,
max_iterations: 100,
shrinking: true,
}
}
pub fn set_backend(&mut self, backend: SmtBackend) {
self.backend = backend;
}
pub fn set_shrinking(&mut self, enabled: bool) {
self.shrinking = enabled;
}
pub fn generate(
&self,
property: &str,
vars: &[(String, String)],
) -> Result<Option<Counterexample>> {
let mut solver = SmtSolver::new(self.backend);
for (name, sort) in vars {
solver.declare_var(name, sort);
}
solver.assert(&format!("(not {property})"));
match solver.check_sat()? {
SmtResult::Sat => {
let model = solver.get_model()?;
Ok(Some(self.build_counterexample(property, model)))
}
_ => Ok(None),
}
}
fn build_counterexample(
&self,
property: &str,
model: Option<HashMap<String, String>>,
) -> Counterexample {
let mut cex = Counterexample::new(property);
if let Some(assignments) = model {
for (var, val) in assignments {
cex.add_assignment(&var, self.parse_value(&val));
}
}
if self.shrinking {
self.shrink_counterexample(&mut cex);
}
cex
}
fn parse_value(&self, smt_value: &str) -> Value {
if let Ok(n) = smt_value.parse::<i64>() {
Value::Int(n)
} else if smt_value == "true" {
Value::Bool(true)
} else if smt_value == "false" {
Value::Bool(false)
} else if let Ok(x) = smt_value.parse::<f64>() {
Value::Float(x)
} else {
Value::String(smt_value.to_string())
}
}
fn shrink_counterexample(&self, cex: &mut Counterexample) {
let mut changed = true;
let mut iterations = 0;
while changed && iterations < self.max_iterations {
changed = false;
iterations += 1;
for (var, value) in cex.assignments.clone() {
if let Some(shrunk) = self.try_shrink_value(&value) {
cex.assignments.insert(var, shrunk);
changed = true;
}
}
}
}
fn try_shrink_value(&self, value: &Value) -> Option<Value> {
match value {
Value::Int(n) if *n > 0 => Some(Value::Int(n / 2)),
Value::Array(vs) if !vs.is_empty() => {
let mut shrunk = vs.clone();
shrunk.pop();
Some(Value::Array(shrunk))
}
Value::String(s) if s.len() > 1 => Some(Value::String(s[..s.len() - 1].to_string())),
_ => None,
}
}
pub fn generate_test_suite(
&self,
property: &str,
vars: &[(String, String)],
count: usize,
) -> Result<Vec<TestCase>> {
let mut test_cases: Vec<TestCase> = Vec::new();
for i in 0..count {
let mut solver = SmtSolver::new(self.backend);
for (name, sort) in vars {
solver.declare_var(name, sort);
}
for prev_case in &test_cases {
for (var, val) in &prev_case.inputs {
solver.assert(&format!("(not (= {} {}))", var, self.value_to_smt(val)));
}
}
solver.assert(&format!("(not {property})"));
match solver.check_sat()? {
SmtResult::Sat => {
if let Some(model) = solver.get_model()? {
let mut test_case = TestCase::new(property);
for (var, val) in model {
test_case.add_input(&var, self.parse_value(&val));
}
test_cases.push(test_case);
}
}
_ => break,
}
if i >= count {
break;
}
}
Ok(test_cases)
}
fn value_to_smt(&self, value: &Value) -> String {
match value {
Value::Int(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::String(s) => format!("\"{s}\""),
Value::Float(x) => format!("{x:.6}"),
_ => "null".to_string(),
}
}
}
impl Default for CounterexampleGenerator {
fn default() -> Self {
Self::new()
}
}
pub struct SymbolicExecutor {
generator: CounterexampleGenerator,
path_conditions: Vec<String>,
symbolic_state: HashMap<String, String>,
}
impl SymbolicExecutor {
pub fn new() -> Self {
Self {
generator: CounterexampleGenerator::new(),
path_conditions: Vec::new(),
symbolic_state: HashMap::new(),
}
}
pub fn add_condition(&mut self, condition: &str) {
self.path_conditions.push(condition.to_string());
}
pub fn set_symbolic(&mut self, var: &str, symbolic: &str) {
self.symbolic_state
.insert(var.to_string(), symbolic.to_string());
}
pub fn find_error_path(&self, error_condition: &str) -> Result<Option<Counterexample>> {
let mut solver = SmtSolver::new(self.generator.backend);
for var in self.symbolic_state.keys() {
solver.declare_var(var, "Int");
}
for condition in &self.path_conditions {
solver.assert(condition);
}
solver.assert(error_condition);
match solver.check_sat()? {
SmtResult::Sat => {
let model = solver.get_model()?;
Ok(Some(
self.generator.build_counterexample(error_condition, model),
))
}
_ => Ok(None),
}
}
}
impl Default for SymbolicExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counterexample_report() {
let mut cex = Counterexample::new("x > 10");
cex.add_assignment("x", Value::Int(5));
cex.set_explanation("x = 5 does not satisfy x > 10");
let report = cex.format_report();
assert!(report.contains("x > 10"));
assert!(report.contains("x = 5"));
}
#[test]
fn test_test_case_generation() {
let mut test = TestCase::new("x > 0");
test.add_input("x", Value::Int(42));
test.set_expected(Value::Bool(true));
let code = test.to_ruchy_test("test_positive");
assert!(code.contains("let x = 42"));
assert!(code.contains("assert!(x > 0)"));
}
#[test]
fn test_value_display() {
assert_eq!(Value::Int(42).to_string(), "42");
assert_eq!(Value::Bool(true).to_string(), "true");
assert_eq!(Value::String("hello".to_string()).to_string(), "\"hello\"");
assert_eq!(Value::Float(3.15).to_string(), "3.15");
assert_eq!(Value::Null.to_string(), "null");
let array = Value::Array(vec![Value::Int(1), Value::Int(2)]);
assert_eq!(array.to_string(), "[1, 2]");
let tuple = Value::Tuple(vec![Value::Int(1), Value::String("a".to_string())]);
assert_eq!(tuple.to_string(), "(1, \"a\")");
}
#[test]
fn test_trace_step() {
let mut state = HashMap::new();
state.insert("x".to_string(), Value::Int(5));
let step = TraceStep {
step: 1,
location: "line 10".to_string(),
operation: "assignment".to_string(),
state,
};
assert_eq!(step.step, 1);
assert_eq!(step.location, "line 10");
assert_eq!(step.operation, "assignment");
assert!(matches!(step.state.get("x"), Some(Value::Int(5))));
}
#[test]
fn test_counterexample_with_trace() {
let mut cex = Counterexample::new("assertion failed");
cex.add_assignment("x", Value::Int(10));
cex.add_assignment("y", Value::Int(20));
let mut state1 = HashMap::new();
state1.insert("x".to_string(), Value::Int(10));
cex.add_trace_step(TraceStep {
step: 1,
location: "line 5".to_string(),
operation: "init x".to_string(),
state: state1,
});
let mut state2 = HashMap::new();
state2.insert("x".to_string(), Value::Int(10));
state2.insert("y".to_string(), Value::Int(20));
cex.add_trace_step(TraceStep {
step: 2,
location: "line 6".to_string(),
operation: "init y".to_string(),
state: state2,
});
cex.set_explanation("The assertion x + y > 50 failed");
let report = cex.format_report();
assert!(report.contains("assertion failed"));
assert!(report.contains("x = 10"));
assert!(report.contains("y = 20"));
assert!(report.contains("Step 1"));
assert!(report.contains("Step 2"));
assert!(report.contains("The assertion x + y > 50 failed"));
}
#[test]
fn test_empty_counterexample() {
let cex = Counterexample::new("empty");
let report = cex.format_report();
assert!(report.contains("empty"));
assert!(!report.contains("Variable Assignments"));
assert!(!report.contains("Execution Trace"));
assert!(!report.contains("Explanation"));
}
#[test]
fn test_test_case_with_multiple_inputs() {
let mut test = TestCase::new("x + y == z");
test.add_input("x", Value::Int(10));
test.add_input("y", Value::Int(20));
test.add_input("z", Value::Int(30));
test.set_expected(Value::Bool(true));
assert_eq!(test.inputs.len(), 3);
assert!(matches!(test.expected, Some(Value::Bool(true))));
}
#[test]
fn test_counterexample_generator() {
let mut gen = CounterexampleGenerator::new();
gen.set_backend(SmtBackend::Z3);
assert!(matches!(gen.backend, SmtBackend::Z3));
let cex = gen.build_counterexample("test", Some(HashMap::new()));
assert_eq!(cex.failed_assertion, "test");
}
#[test]
fn test_counterexample_from_model() {
let mut gen = CounterexampleGenerator::new();
gen.set_backend(SmtBackend::Z3);
let mut model = HashMap::new();
model.insert("x".to_string(), "42".to_string());
model.insert("y".to_string(), "true".to_string());
let cex = gen.build_counterexample("assertion", Some(model));
assert_eq!(cex.failed_assertion, "assertion");
}
#[test]
fn test_symbolic_executor() {
let mut exec = SymbolicExecutor::new();
exec.add_condition("x > 0");
exec.add_condition("y < 10");
exec.set_symbolic("x", "x_sym");
exec.set_symbolic("y", "y_sym");
assert_eq!(exec.path_conditions.len(), 2);
assert_eq!(exec.symbolic_state.len(), 2);
assert!(exec.path_conditions.contains(&"x > 0".to_string()));
assert!(exec.path_conditions.contains(&"y < 10".to_string()));
}
#[test]
fn test_find_error_path() {
let mut exec = SymbolicExecutor::new();
exec.set_symbolic("x", "x");
exec.add_condition("x > 0");
let result = exec.find_error_path("x < 0");
let _ = result; }
#[test]
fn test_complex_value_display() {
let nested = Value::Array(vec![
Value::Tuple(vec![Value::Int(1), Value::Bool(true)]),
Value::Tuple(vec![Value::Int(2), Value::Bool(false)]),
]);
let display = nested.to_string();
assert!(display.contains("(1, true)"));
assert!(display.contains("(2, false)"));
}
#[test]
fn test_test_case_to_ruchy() {
let mut test = TestCase::new("array.len() > 0");
test.add_input("array", Value::Array(vec![Value::Int(1), Value::Int(2)]));
test.set_expected(Value::Bool(true));
let code = test.to_ruchy_test("test_array_length");
assert!(code.contains("fun test_array_length"));
assert!(code.contains("let array"));
assert!(code.contains("assert!"));
}
#[test]
fn test_counterexample_builder() {
let mut cex = Counterexample::new("invariant violation");
cex.add_assignment("counter", Value::Int(100));
cex.add_assignment("limit", Value::Int(50));
cex.set_explanation("Counter exceeded limit");
assert_eq!(cex.assignments.len(), 2);
assert!(cex.explanation.is_some());
assert_eq!(cex.failed_assertion, "invariant violation");
}
#[test]
fn test_trace_step_display() {
let mut state = HashMap::new();
state.insert("flag".to_string(), Value::Bool(false));
state.insert("count".to_string(), Value::Int(3));
let step = TraceStep {
step: 5,
location: "function foo".to_string(),
operation: "conditional branch".to_string(),
state,
};
assert_eq!(step.step, 5);
assert!(step.location.contains("foo"));
assert!(step.operation.contains("branch"));
assert_eq!(step.state.len(), 2);
}
#[test]
fn test_symbolic_executor_default() {
let exec = SymbolicExecutor::default();
assert!(exec.path_conditions.is_empty());
assert!(exec.symbolic_state.is_empty());
}
#[test]
fn test_value_enum_coverage() {
let _ = Value::Int(42);
let _ = Value::Bool(true);
let _ = Value::String("test".to_string());
let _ = Value::Float(3.15);
let _ = Value::Array(vec![]);
let _ = Value::Tuple(vec![]);
let _ = Value::Null;
}
}
#[cfg(test)]
mod property_tests_counterexample {
use proptest::proptest;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}