use std::{
path::PathBuf,
fs::File,
io::Write as ioWrite,
process::Command,
io::BufRead,
io::BufReader,
collections::HashSet,
};
use regex::Regex;
use log::{
error,
info
};
pub fn coq_verification(
original_coq: &PathBuf,
refactored_coq: &PathBuf,
top_level_function: &String,
) -> Result<(PathBuf, PathBuf, PathBuf, bool), Box<dyn std::error::Error>> {
let dir: PathBuf = original_coq.parent().unwrap().to_path_buf();
let coq_path: PathBuf = create_coq_project_file(
&original_coq,
&refactored_coq,
&dir,
)?;
info!("Created _CoqProject file");
let equiv_check: PathBuf = create_equiv_check_file(
&original_coq,
&refactored_coq,
&top_level_function,
&dir
)?;
info!("Created EquivCheck.v file");
let (result, primitives) = verify_coq_files(
&original_coq,
&refactored_coq,
&equiv_check,
&dir,
)?;
if result {
info!("CoQ verification completed successfully");
} else {
error!("CoQ verification failed");
}
cleanup_directory(&dir)?;
Ok((
coq_path,
equiv_check,
primitives,
result
))
}
fn create_coq_project_file(
original_coq: &PathBuf,
refactored_coq: &PathBuf,
dir: &PathBuf,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut coq_project = File::create(dir.join("_CoqProject"))?;
writeln!(coq_project, "-R . AutoEquiv")?;
writeln!(coq_project, "")?;
writeln!(coq_project, "Primitives.v")?;
writeln!(coq_project, "{}", original_coq.file_name().unwrap().to_str().unwrap())?;
writeln!(coq_project, "{}", refactored_coq.file_name().unwrap().to_str().unwrap())?;
writeln!(coq_project, "EquivCheck.v")?;
let coq_path = dir.join("_CoqProject");
Ok(coq_path)
}
fn create_equiv_check_file(
original_coq: &PathBuf,
refactored_coq: &PathBuf,
top_level_function: &String,
dir: &PathBuf,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
info!("Directory: {:?}", dir);
let equiv_check_path = dir.join("EquivCheck.v");
let mut equiv_check = File::create(&equiv_check_path)?;
writeln!(equiv_check, "(** EquivCheck.v Autogenerated by REM-verification **)")?;
writeln!(equiv_check, "Require Import Primitives.")?;
writeln!(equiv_check, "Import Primitives.")?;
writeln!(
equiv_check,
"Require Import {}.",
original_coq.file_stem().unwrap().to_str().unwrap()
)?;
writeln!(
equiv_check,
"Require Import {}.",
refactored_coq.file_stem().unwrap().to_str().unwrap()
)?;
let original_imports: Vec<String> = extract_imports(original_coq)?;
let refactored_imports: Vec<String> = extract_imports(refactored_coq)?;
let combined_imports: String = combine_imports(original_imports, refactored_imports);
if !combined_imports.is_empty() {
writeln!(
equiv_check,
"{}",
combined_imports
)?;
}
writeln!(equiv_check, "Local Open Scope Primitives_scope.")?;
writeln!(equiv_check, "\n")?;
let proof: String = generate_equiv_check_proof(&top_level_function, &original_coq, &refactored_coq)?;
writeln!(equiv_check, "{}", proof)?;
info!("Proof generated successfully");
Ok(dir.join("EquivCheck.v"))
}
fn extract_imports(coq_path: &PathBuf) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let file: File = File::open(coq_path)?;
let reader = BufReader::new(file);
let mut imports: Vec<String> = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.starts_with("Local Open Scope") {
break;
}
if trimmed.starts_with("Module") {
break;
}
if trimmed.starts_with("Require") || trimmed.starts_with("Import") {
imports.push(trimmed.to_string());
} else if !trimmed.is_empty() {
break;
}
}
Ok(imports)
}
fn combine_imports(imports1: Vec<String>, imports2: Vec<String>) -> String {
let mut set = HashSet::new();
for line in imports1.into_iter().chain(imports2.into_iter()) {
set.insert(line);
}
let mut combined: Vec<_> = set.into_iter().collect::<Vec<String>>();
combined.retain(|line| !line.contains("Primitives"));
combined.join("\n")
}
struct CoqArgument {
name: String,
ty: String,
}
fn generate_equiv_check_proof(
top_level_function: &str,
original_coq: &PathBuf,
refactored_coq: &PathBuf,
) -> Result<String, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(original_coq)?;
let def_pattern = format!(r"Definition\s+{}\s+((?:\([^)]*\)\s*)+):", top_level_function);
let def_re = Regex::new(&def_pattern)?;
let caps = def_re.captures(&content)
.ok_or("Could not find the function definition in the original Coq file")?;
let args_str = caps.get(1).unwrap().as_str();
let arg_re = Regex::new(r"\(\s*([^:\s]+)\s*:\s*([^)]+)\s*\)")?;
let mut args: Vec<CoqArgument> = Vec::new();
for cap in arg_re.captures_iter(args_str) {
let name = cap.get(1).unwrap().as_str().trim().to_string();
let ty = cap.get(2).unwrap().as_str().trim().to_string();
args.push(CoqArgument { name, ty });
}
let forall_part = args.iter()
.map(|arg| format!("({} : {})", arg.name, arg.ty))
.collect::<Vec<_>>()
.join(" ");
let args_names = args.iter()
.map(|arg| arg.name.clone())
.collect::<Vec<_>>()
.join(" ");
let lemma_name = format!("{}_equiv_check", top_level_function);
let original_module = original_coq
.file_stem()
.unwrap()
.to_str()
.unwrap();
let refactored_module = refactored_coq
.file_stem()
.unwrap()
.to_str()
.unwrap();
let proof = format!(
r"Lemma {} : forall {},
{}.{} {} = {}.{} {}.
Proof.
intros {}.
unfold {}.{}.
unfold {}.{}.
reflexivity.
Qed.",
lemma_name, forall_part, original_module, top_level_function, args_names, refactored_module, top_level_function, args_names, args_names, original_module, top_level_function, refactored_module, top_level_function, );
info!("Proof script:\n{}", proof);
Ok(proof)
}
fn verify_coq_files(
original_coq: &PathBuf,
refactored_coq: &PathBuf,
equiv_check: &PathBuf,
dir: &PathBuf, ) -> Result<(bool, PathBuf), Box<dyn std::error::Error>> {
let dir_str = dir.to_str().ok_or("Invalid directory path")?;
info!("Directory: {}", dir_str);
let primitives: PathBuf = dir.join("Primitives.v");
let primitives_coq_output = Command::new("coqc")
.arg("-R")
.arg(dir_str)
.arg("Primitives")
.arg(&primitives)
.output()?;
if !primitives_coq_output.status.success() {
let stdout = String::from_utf8_lossy(&primitives_coq_output.stdout);
let stderr = String::from_utf8_lossy(&primitives_coq_output.stderr);
error!("Primitives.v failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
return Ok((false, primitives));
}
info!("Primitives.v compiled successfully");
let original_coq_output = Command::new("coqc")
.arg("-R")
.arg(dir_str)
.arg("Primitives")
.arg(&original_coq)
.output()?;
if !original_coq_output.status.success() {
let stdout = String::from_utf8_lossy(&original_coq_output.stdout);
let stderr = String::from_utf8_lossy(&original_coq_output.stderr);
error!("Original Coq file failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
return Ok((false, primitives));
}
info!("Original Coq file compiled successfully");
let refactored_coq_output = Command::new("coqc")
.arg("-R")
.arg(dir_str)
.arg("Primitives")
.arg(&refactored_coq)
.output()?;
if !refactored_coq_output.status.success() {
let stdout = String::from_utf8_lossy(&refactored_coq_output.stdout);
let stderr = String::from_utf8_lossy(&refactored_coq_output.stderr);
error!("Refactored Coq file failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
return Ok((false, primitives));
}
info!("Refactored Coq file compiled successfully");
let equiv_check_output = Command::new("coqc")
.arg("-R")
.arg(dir_str)
.arg("Primitives")
.arg(&equiv_check)
.output()?;
if !equiv_check_output.status.success() {
let stdout = String::from_utf8_lossy(&equiv_check_output.stdout);
let stderr = String::from_utf8_lossy(&equiv_check_output.stderr);
error!("EquivCheck.v failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
return Ok((false, primitives));
}
info!("EquivCheck.v compiled successfully");
Ok((true, primitives))
}
fn cleanup_directory(dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let files = std::fs::read_dir(dir)?;
for file in files {
let file = file?;
let path = file.path();
let extension = path.extension();
if let Some(extension) = extension {
if extension == "glob"
|| extension == "vo"
|| extension == "vok"
|| extension == "vos"
|| extension == "aux"
|| extension == "cache"
{
std::fs::remove_file(path)?;
}
}
}
Ok(())
}