rem_verification/
verify.rs

1use std::{
2    path::PathBuf,
3    fs::File,
4    io::Write as ioWrite,
5    process::Command,
6    io::BufRead,
7    io::BufReader,
8    collections::HashSet,
9};
10
11use regex::Regex;
12
13use log::{
14    error, 
15    info
16};
17
18/// Returns the paths to:
19/// - The _CoQProject file
20/// - The EquivCheck.v file
21/// - The Primitives.v file
22/// These are used to ensure the files are deleted at the end of the
23/// verification process.
24pub fn coq_verification(
25    original_coq: &PathBuf,
26    refactored_coq: &PathBuf,
27    top_level_function: &String,
28) -> Result<(PathBuf, PathBuf, PathBuf, bool), Box<dyn std::error::Error>> {
29
30    let dir: PathBuf = original_coq.parent().unwrap().to_path_buf(); // The directory where the CoQ files are saved
31
32    // First we need to create the _CoqProject file
33    let coq_path: PathBuf = create_coq_project_file(
34        &original_coq,
35        &refactored_coq,
36        &dir,
37    )?;
38
39    info!("Created _CoqProject file");
40
41    // Create the EquivCheck.v file
42    let equiv_check: PathBuf = create_equiv_check_file(
43        &original_coq,
44        &refactored_coq,
45        &top_level_function,
46        &dir
47    )?;
48
49    info!("Created EquivCheck.v file");
50
51    // Now we need to call CoQ to verify the files
52    let (result, primitives) = verify_coq_files(
53        &original_coq,
54        &refactored_coq,
55        &equiv_check,
56        &dir,
57    )?;
58
59    if result {
60        info!("CoQ verification completed successfully");
61    } else {
62        error!("CoQ verification failed");
63    }
64
65    // Finally cleanup the directory (remove all files other than the CoQ files
66    // and the _CoQ project file)
67    cleanup_directory(&dir)?;
68
69    Ok((
70        coq_path,
71        equiv_check,
72        primitives,
73        result
74    ))
75}
76
77/// Creates the _CoqProject file for the CoQ project. This file is used by coqc
78/// to understand the project structure and dependencies, and helps with
79/// compilation.
80fn create_coq_project_file(
81    original_coq: &PathBuf,
82    refactored_coq: &PathBuf,
83    dir: &PathBuf,
84) -> Result<PathBuf, Box<dyn std::error::Error>> {
85    // The _CoqProject is a text file that contains:
86    // -R . AutoEquiv
87    // Primitives.v
88    // <Original CoQ file>
89    // <Refactored CoQ file>
90    // Any other CoQ files that are needed for the verification
91    // EquivCheck.v
92
93    // Create the file
94    let mut coq_project = File::create(dir.join("_CoqProject"))?;
95    writeln!(coq_project, "-R . AutoEquiv")?;
96    writeln!(coq_project, "")?;
97    writeln!(coq_project, "Primitives.v")?;
98    writeln!(coq_project, "{}", original_coq.file_name().unwrap().to_str().unwrap())?;
99    writeln!(coq_project, "{}", refactored_coq.file_name().unwrap().to_str().unwrap())?;
100    writeln!(coq_project, "EquivCheck.v")?;
101
102    // Return the path to the _CoqProject file
103    let coq_path = dir.join("_CoqProject");
104    Ok(coq_path)
105}
106
107/// Creates the EquivCheck.v file for the CoQ project. This is the most involved
108/// part of the verification process, as it requires writing a CoQ script that
109/// can check the equivalence of two functions.
110/// The script will be saved in the same directory as the original CoQ file.
111fn create_equiv_check_file(
112    original_coq: &PathBuf,
113    refactored_coq: &PathBuf,
114    top_level_function: &String,
115    dir: &PathBuf,
116) -> Result<PathBuf, Box<dyn std::error::Error>> {
117    // The EquivCheck.v file will contain the following:
118    // Require Import Primitives.
119    // Import Primitives.
120    // Require Import <Original CoQ file>.
121    // Require Import <Refactored CoQ file>.
122    // Require Import Coq.ZArith.ZArith.
123    // Local Open Scope Primitives_scope.
124
125    // Theorem equiv_check: forall (args: list nat), <Top level function> args = <Top level function> args.
126    // Proof.
127    //   reflexivity.
128    // Qed.
129
130    // Create the file
131    info!("Directory: {:?}", dir);
132
133    let equiv_check_path = dir.join("EquivCheck.v");
134    let mut equiv_check = File::create(&equiv_check_path)?;
135
136    // Write the header comments
137    writeln!(equiv_check, "(** EquivCheck.v Autogenerated by REM-verification **)")?;
138
139    // Always include the Primitives.v file
140    // These files are removed from the list of imports we collect to ensure
141    // they aren't duplicated.
142    writeln!(equiv_check, "Require Import Primitives.")?;
143    writeln!(equiv_check, "Import Primitives.")?;
144
145    // Also include the original and refactored CoQ files so that we have access
146    // to the functions we need to check.
147    writeln!(
148        equiv_check,
149        "Require Import {}.",
150        original_coq.file_stem().unwrap().to_str().unwrap()
151    )?;
152    writeln!(
153        equiv_check,
154        "Require Import {}.",
155        refactored_coq.file_stem().unwrap().to_str().unwrap()
156    )?;
157
158    // Extract additional import lines from both files.
159    let original_imports: Vec<String> = extract_imports(original_coq)?;
160    let refactored_imports: Vec<String> = extract_imports(refactored_coq)?;
161    let combined_imports: String = combine_imports(original_imports, refactored_imports);
162
163    if !combined_imports.is_empty() {
164        writeln!(
165            equiv_check,
166            "{}",
167            combined_imports
168        )?;
169    }
170
171    writeln!(equiv_check, "Local Open Scope Primitives_scope.")?;
172    writeln!(equiv_check, "\n")?;
173
174    // From here we need to evaluate the Lemma / Theorem needed to check
175    // equivalence. This will depend on the top level function provided.
176    // TODO add in more than just a forall check
177    let proof: String = generate_equiv_check_proof(&top_level_function, &original_coq, &refactored_coq)?;
178    writeln!(equiv_check, "{}", proof)?;
179    info!("Proof generated successfully");
180
181    Ok(dir.join("EquivCheck.v"))
182}
183
184/// Extracts import lines from the beginning of a Coq file.
185/// It collects lines starting with "Require", "Import", or "Require Export"
186/// until it reaches a line that starts with "Local Open Scope" or a non-import line.
187fn extract_imports(coq_path: &PathBuf) -> Result<Vec<String>, Box<dyn std::error::Error>> {
188    let file: File = File::open(coq_path)?;
189    let reader = BufReader::new(file);
190    let mut imports: Vec<String> = Vec::new();
191
192    for line in reader.lines() {
193        let line = line?;
194        let trimmed = line.trim();
195        // Stop if we encounter the "Local Open Scope" directive or a non-import statement.
196        if trimmed.starts_with("Local Open Scope") {
197            break;
198        }
199        // Stop if we encounter the Module declaration.
200        if trimmed.starts_with("Module") {
201            break;
202        }
203        // If the line starts with "Require", "Import", or "Require Export", consider it an import.
204        if trimmed.starts_with("Require") || trimmed.starts_with("Import") {
205            imports.push(trimmed.to_string());
206        } else if !trimmed.is_empty() {
207            // If we hit a non-empty, non-import line, we assume the import section is done.
208            break;
209        }
210    }
211    Ok(imports)
212}
213
214/// Combines two lists of import lines into one, deduplicating identical lines.
215/// Also removes the primitives imports from the output list.
216fn combine_imports(imports1: Vec<String>, imports2: Vec<String>) -> String {
217    let mut set = HashSet::new();
218    // Insert each line into the set (this automatically deduplicates).
219    for line in imports1.into_iter().chain(imports2.into_iter()) {
220        set.insert(line);
221    }
222    // Convert the set into a vector.
223    let mut combined: Vec<_> = set.into_iter().collect::<Vec<String>>();
224    // Remove the Primitives imports.
225    combined.retain(|line| !line.contains("Primitives"));
226    // Combine the set into a single string.
227    combined.join("\n")
228}
229
230
231struct CoqArgument {
232    name: String,
233    ty: String,
234}
235
236/// Generates the proof for the equivalence check.
237fn generate_equiv_check_proof(
238    top_level_function: &str,
239    original_coq: &PathBuf,
240    refactored_coq: &PathBuf,
241) -> Result<String, Box<dyn std::error::Error>> {
242    // Read the original Coq file as a string.
243    let content = std::fs::read_to_string(original_coq)?;
244
245
246    // Use a regex to locate the definition of the function.
247    // We assume the definition looks like:
248    //   Definition top_level_function (arg1 : type1) (arg2 : type2) ... : ...
249    let def_pattern = format!(r"Definition\s+{}\s+((?:\([^)]*\)\s*)+):", top_level_function);
250    let def_re = Regex::new(&def_pattern)?;
251
252    // Find the arguments portion.
253    let caps = def_re.captures(&content)
254        .ok_or("Could not find the function definition in the original Coq file")?;
255    let args_str = caps.get(1).unwrap().as_str();
256
257    // Now extract each argument of the form: (name : type)
258    let arg_re = Regex::new(r"\(\s*([^:\s]+)\s*:\s*([^)]+)\s*\)")?;
259    let mut args: Vec<CoqArgument> = Vec::new();
260    for cap in arg_re.captures_iter(args_str) {
261        let name = cap.get(1).unwrap().as_str().trim().to_string();
262        let ty = cap.get(2).unwrap().as_str().trim().to_string();
263        args.push(CoqArgument { name, ty });
264    }
265
266    // Build the "forall" part of the lemma and the argument list for function application.
267    let forall_part = args.iter()
268        .map(|arg| format!("({} : {})", arg.name, arg.ty))
269        .collect::<Vec<_>>()
270        .join(" ");
271    let args_names = args.iter()
272        .map(|arg| arg.name.clone())
273        .collect::<Vec<_>>()
274        .join(" ");
275
276    // Name the Lemma
277    let lemma_name = format!("{}_equiv_check", top_level_function);
278
279    // Get the module names for the original and refactored CoQ files.
280    let original_module = original_coq
281        .file_stem()
282        .unwrap()
283        .to_str()
284        .unwrap();
285    let refactored_module = refactored_coq
286        .file_stem()
287        .unwrap()
288        .to_str()
289        .unwrap();
290
291    // Generate the proof script as a string.
292    let proof = format!(
293r"Lemma {} : forall {},
294  {}.{} {} = {}.{} {}.
295Proof.
296  intros {}.
297  unfold {}.{}.
298  unfold {}.{}.
299  reflexivity.
300Qed.",
301        lemma_name, forall_part, // Line 1
302        original_module, top_level_function, args_names, // Line 2
303        refactored_module, top_level_function, args_names, // Line 2
304        args_names, // Line 4
305        original_module, top_level_function, // Line 5
306        refactored_module, top_level_function, // Line 6
307    );
308
309    // Log the generated proof script
310    info!("Proof script:\n{}", proof);
311
312    Ok(proof)
313}
314
315
316/// Verifies the CoQ files using CoQ's coqc command.
317/// This function will call coqc on the original and refactored CoQ files, as well
318/// as the EquivCheck.v file. If any of the files fail to compile, the function
319/// will return false.
320/// The function will return true if all files compile successfully, and it has
321/// checked the return value of the EquivCheck.v file.
322/// Verifies the CoQ files using CoQ's coqc command.
323/// This function will call coqc on the original and refactored CoQ files,
324/// as well as the EquivCheck.v file. If any of the files fail to compile,
325/// the function will return false. The function will return true if all
326/// files compile successfully.
327/// Returns the result of the equivalence check and the path to the Primitives.v file.
328fn verify_coq_files(
329    original_coq: &PathBuf,
330    refactored_coq: &PathBuf,
331    equiv_check: &PathBuf,
332    dir: &PathBuf, // This should be the directory that contains Primitives.v.
333) -> Result<(bool, PathBuf), Box<dyn std::error::Error>> {
334    // The mapping: -R <dir> Primitives
335    let dir_str = dir.to_str().ok_or("Invalid directory path")?;
336
337    info!("Directory: {}", dir_str);
338
339    // Start with the Primitives.v file
340    let primitives: PathBuf = dir.join("Primitives.v");
341    let primitives_coq_output = Command::new("coqc")
342        .arg("-R")
343        .arg(dir_str)
344        .arg("Primitives")
345        .arg(&primitives)
346        .output()?;
347    if !primitives_coq_output.status.success() {
348        let stdout = String::from_utf8_lossy(&primitives_coq_output.stdout);
349        let stderr = String::from_utf8_lossy(&primitives_coq_output.stderr);
350        error!("Primitives.v failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
351        return Ok((false, primitives));
352    }
353    info!("Primitives.v compiled successfully");
354
355    // Compile the original Coq file with the same mapping
356    let original_coq_output = Command::new("coqc")
357        .arg("-R")
358        .arg(dir_str)
359        .arg("Primitives")
360        .arg(&original_coq)
361        .output()?;
362    if !original_coq_output.status.success() {
363        let stdout = String::from_utf8_lossy(&original_coq_output.stdout);
364        let stderr = String::from_utf8_lossy(&original_coq_output.stderr);
365        error!("Original Coq file failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
366        return Ok((false, primitives));
367    }
368    info!("Original Coq file compiled successfully");
369
370    // Compile the refactored Coq file
371    let refactored_coq_output = Command::new("coqc")
372        .arg("-R")
373        .arg(dir_str)
374        .arg("Primitives")
375        .arg(&refactored_coq)
376        .output()?;
377    if !refactored_coq_output.status.success() {
378        let stdout = String::from_utf8_lossy(&refactored_coq_output.stdout);
379        let stderr = String::from_utf8_lossy(&refactored_coq_output.stderr);
380        error!("Refactored Coq file failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
381        return Ok((false, primitives));
382    }
383    info!("Refactored Coq file compiled successfully");
384
385    // Compile the EquivCheck.v file
386    let equiv_check_output = Command::new("coqc")
387        .arg("-R")
388        .arg(dir_str)
389        .arg("Primitives")
390        .arg(&equiv_check)
391        .output()?;
392    if !equiv_check_output.status.success() {
393        let stdout = String::from_utf8_lossy(&equiv_check_output.stdout);
394        let stderr = String::from_utf8_lossy(&equiv_check_output.stderr);
395        error!("EquivCheck.v failed to compile:\nstdout: {}\nstderr: {}", stdout, stderr);
396        return Ok((false, primitives));
397    }
398    info!("EquivCheck.v compiled successfully");
399
400    Ok((true, primitives))
401}
402
403/// Cleans up the directory by removing all files other than the CoQ files and
404/// the _CoqProject file.
405/// eqivalent to rm -f *.glob *.vo *.vok *.vos *.aux .*.aux .*.cache, but
406/// platform independent.
407fn cleanup_directory(dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
408    let files = std::fs::read_dir(dir)?;
409    for file in files {
410        let file = file?;
411        let path = file.path();
412        let extension = path.extension();
413        if let Some(extension) = extension {
414            if extension == "glob"
415            || extension == "vo"
416            || extension == "vok"
417            || extension == "vos"
418            || extension == "aux"
419            || extension == "cache"
420            {
421                std::fs::remove_file(path)?;
422            }
423        }
424    }
425
426    Ok(())
427}