use anyhow::{Context, Result};
use chrono::Utc;
use csv::{Reader, Writer};
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Instant;
#[derive(
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Getters, Default, Serialize, Deserialize,
)]
pub struct CreusotModule {
name: String,
feature: Option<String>,
#[getter(skip)]
unix_only_flag: bool,
}
impl CreusotModule {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
feature: None,
unix_only_flag: false,
}
}
pub fn with_feature(name: impl Into<String>, feature: impl Into<String>) -> Self {
Self {
name: name.into(),
feature: Some(feature.into()),
unix_only_flag: false,
}
}
pub fn unix_only(name: impl Into<String>) -> Self {
Self {
name: name.into(),
feature: None,
unix_only_flag: true,
}
}
pub fn is_unix_only(&self) -> bool {
self.unix_only_flag
}
pub fn all() -> Vec<Self> {
vec![
Self::new("bools"),
Self::new("chars"),
Self::new("collections"),
Self::new("durations"),
Self::new("floats"),
Self::new("integers"),
Self::new("networks"),
Self::new("paths"),
Self::new("strings"),
Self::new("tuples"),
Self::new("ipaddr_bytes"),
Self::new("macaddr"),
Self::new("mechanisms"),
Self::new("socketaddr"),
Self::new("utf8"),
Self::unix_only("pathbytes"),
Self::with_feature("uuids", "uuid"),
Self::with_feature("uuid_bytes", "uuid"),
Self::with_feature("values", "serde_json"),
Self::with_feature("urls", "url"),
Self::with_feature("urlbytes", "url"),
Self::with_feature("regexes", "regex"),
Self::with_feature("regexbytes", "regex"),
Self::with_feature("datetimes_chrono", "chrono"),
Self::with_feature("datetimes_time", "time"),
Self::with_feature("datetimes_jiff", "jiff"),
Self::with_feature("clap_types", "clap-types"),
Self::with_feature("sqlx_types", "sqlx-types"),
Self::with_feature("tokio_types", "tokio-types"),
Self::with_feature("egui_types", "egui-types"),
Self::with_feature("ratatui_types", "ratatui-types"),
Self::with_feature("geo_types", "geo-types"),
Self::with_feature("palette_types", "palette"),
Self::with_feature("ui_types", "ui-types"),
]
}
pub fn is_available(&self) -> bool {
if self.is_unix_only() && !cfg!(unix) {
return false;
}
true
}
}
impl std::fmt::Display for CreusotModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(feature) = &self.feature {
write!(f, "{} (feature: {})", self.name, feature)
} else if self.is_unix_only() {
write!(f, "{} (unix)", self.name)
} else {
write!(f, "{}", self.name)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompilationStatus {
Success,
Failed,
Skipped,
}
#[derive(Debug, Clone, Getters, Serialize, Deserialize)]
pub struct CreusotModuleResult {
module: String,
status: CompilationStatus,
time_seconds: u64,
timestamp: chrono::DateTime<Utc>,
error_message: String,
}
impl CreusotModuleResult {
pub fn new(
module: impl Into<String>,
status: CompilationStatus,
time_seconds: u64,
error_message: impl Into<String>,
) -> Self {
Self {
module: module.into(),
status,
time_seconds,
timestamp: Utc::now(),
error_message: error_message.into(),
}
}
pub fn is_success(&self) -> bool {
self.status == CompilationStatus::Success
}
}
fn creusot_lib_path() -> Option<std::path::PathBuf> {
let version_out = Command::new("cargo")
.args(["creusot", "version"])
.output()
.ok()?;
let version_str = String::from_utf8_lossy(&version_out.stdout);
let channel = version_str
.lines()
.find(|l| l.starts_with("Rust toolchain"))?
.split_whitespace()
.last()?
.to_owned();
let rustup_home = std::env::var("RUSTUP_HOME")
.ok()
.map(std::path::PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".rustup")))?;
let toolchains_dir = rustup_home.join("toolchains");
let entry = std::fs::read_dir(&toolchains_dir).ok()?.find_map(|e| {
let e = e.ok()?;
let name = e.file_name();
let name = name.to_string_lossy();
if name.starts_with(&channel) {
Some(e.path())
} else {
None
}
})?;
let lib_dir = entry.join("lib");
if lib_dir.exists() {
Some(lib_dir)
} else {
None
}
}
pub fn run_creusot_module(module: &CreusotModule) -> Result<CreusotModuleResult> {
let start = Instant::now();
if !module.is_available() {
return Ok(CreusotModuleResult::new(
module.name(),
CompilationStatus::Skipped,
0,
"Module not available on this platform",
));
}
let mut cmd = Command::new("cargo");
cmd.arg("creusot")
.arg("--")
.arg("-p")
.arg("elicitation_creusot");
if let Some(feature) = module.feature() {
cmd.arg("--features").arg(feature);
}
if let Some(lib_path) = creusot_lib_path() {
let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
let new_path = if existing.is_empty() {
lib_path.to_string_lossy().into_owned()
} else {
format!("{}:{}", lib_path.display(), existing)
};
cmd.env("LD_LIBRARY_PATH", new_path);
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = cmd.output().with_context(|| {
format!(
"Failed to execute cargo creusot for module {}",
module.name()
)
})?;
let elapsed = start.elapsed().as_secs();
let stderr = String::from_utf8_lossy(&output.stderr);
let (status, error_message) = if output.status.success() {
(CompilationStatus::Success, String::new())
} else {
(
CompilationStatus::Failed,
format!("Compilation failed:\n{}", stderr),
)
};
Ok(CreusotModuleResult::new(
module.name(),
status,
elapsed,
error_message,
))
}
pub fn run_all_modules(output_csv: &Path, resume: bool) -> Result<CreusotSummary> {
println!("🔬 Running Creusot module compilation checks...");
println!(" Output: {}", output_csv.display());
println!();
let mut completed_modules = std::collections::HashSet::new();
if resume && output_csv.exists() {
println!("📂 Loading existing results...");
let mut reader = Reader::from_path(output_csv)
.with_context(|| format!("Failed to read CSV: {}", output_csv.display()))?;
for result in reader.deserialize::<CreusotModuleResult>().flatten() {
if result.is_success() {
completed_modules.insert(result.module().clone());
}
}
println!(" Found {} completed modules", completed_modules.len());
println!();
}
let mut writer = Writer::from_path(output_csv)
.with_context(|| format!("Failed to create CSV: {}", output_csv.display()))?;
let modules = CreusotModule::all();
let total = modules.len();
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for (i, module) in modules.iter().enumerate() {
if completed_modules.contains(module.name()) {
println!(
"[{:2}/{:2}] ⏭️ Skipping {} (already passed)",
i + 1,
total,
module
);
skipped += 1;
continue;
}
print!("[{:2}/{:2}] 🔬 Checking {}... ", i + 1, total, module);
std::io::stdout().flush().ok();
match run_creusot_module(module) {
Ok(result) => {
match result.status() {
CompilationStatus::Success => {
println!("✅ PASS ({}s)", result.time_seconds());
passed += 1;
}
CompilationStatus::Failed => {
println!("❌ FAIL ({}s)", result.time_seconds());
if !result.error_message().is_empty() {
println!(" Error: {}", result.error_message());
}
failed += 1;
}
CompilationStatus::Skipped => {
println!("⏭️ SKIPPED");
skipped += 1;
}
}
writer
.serialize(&result)
.context("Failed to write result to CSV")?;
writer.flush().context("Failed to flush CSV writer")?;
}
Err(e) => {
println!("🔥 ERROR");
println!(" {:#}", e);
failed += 1;
}
}
}
writer.flush().context("Failed to flush CSV")?;
println!();
println!("📊 Compilation Summary:");
println!(" Total: {}", total);
println!(" Passed: {} ✅", passed);
println!(" Failed: {} ❌", failed);
println!(" Skipped: {} ⏭️", skipped);
Ok(CreusotSummary {
total,
passed,
failed,
skipped,
})
}
#[derive(Debug, Clone, Getters)]
pub struct CreusotSummary {
total: usize,
passed: usize,
failed: usize,
skipped: usize,
}
impl CreusotSummary {
pub fn success_rate(&self) -> f64 {
if self.total == 0 {
0.0
} else {
(self.passed as f64 / (self.total - self.skipped) as f64) * 100.0
}
}
}
pub fn show_summary(csv_path: &Path) -> Result<()> {
let mut reader = Reader::from_path(csv_path)
.with_context(|| format!("Failed to read CSV: {}", csv_path.display()))?;
let mut total = 0;
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for result in reader.deserialize::<CreusotModuleResult>() {
let result = result.context("Failed to parse CSV row")?;
total += 1;
match result.status() {
CompilationStatus::Success => passed += 1,
CompilationStatus::Failed => failed += 1,
CompilationStatus::Skipped => skipped += 1,
}
}
let summary = CreusotSummary {
total,
passed,
failed,
skipped,
};
println!("📊 Creusot Module Compilation Summary");
println!("============================");
println!();
println!(" Total: {}", summary.total);
println!(" Passed: {} ✅", summary.passed);
println!(" Failed: {} ❌", summary.failed);
println!(" Skipped: {} ⏭️", summary.skipped);
println!();
println!(" Success Rate: {:.1}%", summary.success_rate());
Ok(())
}
pub fn list_modules() -> Result<()> {
let modules = CreusotModule::all();
println!("📋 Available Creusot Modules ({} total):", modules.len());
println!();
let core: Vec<_> = modules
.iter()
.filter(|m| m.feature().is_none() && !m.is_unix_only())
.collect();
let unix: Vec<_> = modules.iter().filter(|m| m.is_unix_only()).collect();
let featured: Vec<_> = modules.iter().filter(|m| m.feature().is_some()).collect();
if !core.is_empty() {
println!(" Core modules ({}):", core.len());
for module in core {
println!(" - {}", module.name());
}
println!();
}
if !unix.is_empty() {
println!(" Unix-only modules ({}):", unix.len());
for module in unix {
println!(" - {}", module.name());
}
println!();
}
if !featured.is_empty() {
println!(" Feature-gated modules ({}):", featured.len());
for module in featured {
println!(
" - {} (requires: {})",
module.name(),
module.feature().as_ref().unwrap()
);
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)]
pub enum GoalStatus {
Valid,
Unproved,
}
#[derive(Debug, Clone, Serialize, Deserialize, Getters)]
pub struct ProofGoalRecord {
module: String,
function: String,
goal: String,
prover: String,
proof_time_secs: f64,
status: GoalStatus,
recorded_at: chrono::DateTime<chrono::Utc>,
}
pub fn parse_proof_goals(module: &str, verif_root: &Path) -> Vec<ProofGoalRecord> {
let module_dir = verif_root
.join("verif")
.join("elicitation_creusot_rlib")
.join(module);
if !module_dir.exists() {
tracing::warn!("Verification directory not found: {}", module_dir.display());
return Vec::new();
}
let mut records = Vec::new();
let now = chrono::Utc::now();
let entries = match std::fs::read_dir(&module_dir) {
Ok(e) => e,
Err(e) => {
tracing::warn!(
"Failed to read module directory {}: {}",
module_dir.display(),
e
);
return Vec::new();
}
};
for entry in entries.flatten() {
let fn_dir = entry.path();
if !fn_dir.is_dir() {
continue;
}
let function = fn_dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let proof_json = fn_dir.join("proof.json");
if !proof_json.exists() {
continue;
}
let contents = match std::fs::read_to_string(&proof_json) {
Ok(c) => c,
Err(e) => {
tracing::warn!("Failed to read {}: {}", proof_json.display(), e);
continue;
}
};
let parsed: serde_json::Value = match serde_json::from_str(&contents) {
Ok(v) => v,
Err(e) => {
tracing::warn!("Failed to parse {}: {}", proof_json.display(), e);
continue;
}
};
let proofs = match parsed.get("proofs").and_then(|v| v.as_object()) {
Some(p) => p,
None => continue,
};
for (_theory_name, goals_map) in proofs {
let goals = match goals_map.as_object() {
Some(g) => g,
None => continue,
};
for (goal_name, goal_val) in goals {
let prover = goal_val
.get("prover")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let proof_time_secs = goal_val.get("time").and_then(|v| v.as_f64()).unwrap_or(0.0);
let status = if prover.is_empty() {
GoalStatus::Unproved
} else {
GoalStatus::Valid
};
records.push(ProofGoalRecord {
module: module.to_string(),
function: function.clone(),
goal: goal_name.clone(),
prover,
proof_time_secs,
status,
recorded_at: now,
});
}
}
}
records
}
pub fn run_creusot_module_prove(
module: &CreusotModule,
verif_root: &Path,
) -> Result<(CreusotModuleResult, Vec<ProofGoalRecord>)> {
let start = Instant::now();
if !module.is_available() {
return Ok((
CreusotModuleResult::new(
module.name(),
CompilationStatus::Skipped,
0,
"Module not available on this platform",
),
Vec::new(),
));
}
let mut cmd = Command::new("cargo");
cmd.arg("creusot")
.arg("prove")
.arg("--")
.arg("-p")
.arg("elicitation_creusot");
if let Some(feature) = module.feature() {
cmd.arg("--features").arg(feature);
}
if let Some(lib_path) = creusot_lib_path() {
let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
let new_path = if existing.is_empty() {
lib_path.to_string_lossy().into_owned()
} else {
format!("{}:{}", lib_path.display(), existing)
};
cmd.env("LD_LIBRARY_PATH", new_path);
}
let home = std::env::var("HOME").unwrap_or_default();
let opam_bin = format!("{home}/.opam/default/bin");
let existing_path = std::env::var("PATH").unwrap_or_default();
let new_path = if existing_path.is_empty() {
opam_bin
} else {
format!("{opam_bin}:{existing_path}")
};
cmd.env("PATH", new_path);
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = cmd.output().with_context(|| {
format!(
"Failed to execute cargo creusot prove for module {}",
module.name()
)
})?;
let elapsed = start.elapsed().as_secs();
let stderr = String::from_utf8_lossy(&output.stderr);
let (status, error_message) = if output.status.success() {
(CompilationStatus::Success, String::new())
} else {
(
CompilationStatus::Failed,
format!("Prove failed:\n{stderr}"),
)
};
let module_result = CreusotModuleResult::new(module.name(), status, elapsed, error_message);
let goals = parse_proof_goals(module.name(), verif_root);
Ok((module_result, goals))
}
pub fn run_all_modules_prove(
module_csv: &Path,
goals_csv: &Path,
verif_root: &Path,
resume: bool,
) -> Result<CreusotSummary> {
println!("🔬 Running Creusot prove checks...");
println!(" Module CSV: {}", module_csv.display());
println!(" Goals CSV: {}", goals_csv.display());
println!();
let mut completed_modules = std::collections::HashSet::new();
if resume && module_csv.exists() {
println!("📂 Loading existing results...");
let mut reader = Reader::from_path(module_csv)
.with_context(|| format!("Failed to read CSV: {}", module_csv.display()))?;
for result in reader.deserialize::<CreusotModuleResult>().flatten() {
if result.is_success() {
completed_modules.insert(result.module().clone());
}
}
println!(" Found {} completed modules", completed_modules.len());
println!();
}
let mut module_writer = Writer::from_path(module_csv)
.with_context(|| format!("Failed to create module CSV: {}", module_csv.display()))?;
let goals_file_exists = goals_csv.exists();
let goals_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(goals_csv)
.with_context(|| format!("Failed to open goals CSV: {}", goals_csv.display()))?;
let mut goals_writer = csv::WriterBuilder::new()
.has_headers(!goals_file_exists)
.from_writer(goals_file);
let modules = CreusotModule::all();
let total = modules.len();
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for (i, module) in modules.iter().enumerate() {
if completed_modules.contains(module.name()) {
println!(
"[{:2}/{:2}] ⏭️ Skipping {} (already passed)",
i + 1,
total,
module
);
skipped += 1;
continue;
}
print!("[{:2}/{:2}] 🔬 Proving {}... ", i + 1, total, module);
std::io::stdout().flush().ok();
match run_creusot_module_prove(module, verif_root) {
Ok((result, goals)) => {
let n_proved = goals
.iter()
.filter(|g| g.status() == &GoalStatus::Valid)
.count();
match result.status() {
CompilationStatus::Success => {
println!(
"✅ PASS ({}s) — {} goals proved",
result.time_seconds(),
n_proved
);
passed += 1;
}
CompilationStatus::Failed => {
println!("❌ FAIL ({}s)", result.time_seconds());
if !result.error_message().is_empty() {
println!(" Error: {}", result.error_message());
}
failed += 1;
}
CompilationStatus::Skipped => {
println!("⏭️ SKIPPED");
skipped += 1;
}
}
module_writer
.serialize(&result)
.context("Failed to write module result to CSV")?;
module_writer
.flush()
.context("Failed to flush module CSV writer")?;
for goal in &goals {
goals_writer
.serialize(goal)
.context("Failed to write goal record to CSV")?;
}
goals_writer
.flush()
.context("Failed to flush goals CSV writer")?;
}
Err(e) => {
println!("🔥 ERROR");
println!(" {:#}", e);
failed += 1;
}
}
}
module_writer
.flush()
.context("Failed to flush module CSV")?;
goals_writer.flush().context("Failed to flush goals CSV")?;
println!();
println!("📊 Prove Summary:");
println!(" Total: {}", total);
println!(" Passed: {} ✅", passed);
println!(" Failed: {} ❌", failed);
println!(" Skipped: {} ⏭️", skipped);
Ok(CreusotSummary {
total,
passed,
failed,
skipped,
})
}