use clap::{Parser, Subcommand};
use derive_getters::Getters;
use std::path::PathBuf;
#[derive(Debug, Clone, Parser, Getters)]
#[command(name = "elicitation")]
#[command(about = "Type-safe LLM elicitation with formal verification")]
pub struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Verify {
#[command(subcommand)]
action: VerifyAction,
},
Verus {
#[command(subcommand)]
action: VerusAction,
},
Creusot {
#[command(subcommand)]
action: CreusotAction,
},
Graph {
#[command(subcommand)]
action: GraphAction,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum VerifyAction {
List,
Run {
#[arg(short, long, default_value = "kani_verification_results.csv")]
output: PathBuf,
#[arg(short, long, default_value_t = 300)]
timeout: u64,
#[arg(short, long)]
resume: bool,
},
Summary {
#[arg(default_value = "kani_verification_results.csv")]
file: PathBuf,
},
Failed {
#[arg(default_value = "kani_verification_results.csv")]
file: PathBuf,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum VerusAction {
List,
Run {
#[arg(short, long, default_value = "verus_verification_results.csv")]
output: PathBuf,
#[arg(short, long, default_value_t = 600)]
timeout: u64,
#[arg(short, long)]
resume: bool,
#[arg(long)]
verus_path: Option<PathBuf>,
},
Summary {
#[arg(short, long, default_value = "verus_verification_results.csv")]
file: PathBuf,
},
Failed {
#[arg(short, long, default_value = "verus_verification_results.csv")]
file: PathBuf,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum CreusotAction {
List,
Run {
#[arg(short, long, default_value = "creusot_verification_results.csv")]
output: PathBuf,
#[arg(short, long)]
resume: bool,
},
Prove {
#[arg(long, default_value = "creusot_module_results.csv")]
output: PathBuf,
#[arg(long, default_value = "creusot_goal_results.csv")]
goals: PathBuf,
#[arg(long)]
resume: bool,
},
Summary {
#[arg(short, long, default_value = "creusot_verification_results.csv")]
file: PathBuf,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum GraphAction {
List,
Render {
#[arg(short, long)]
root: String,
#[arg(short, long, default_value = "mermaid", value_parser = ["mermaid", "dot"])]
format: String,
#[arg(long)]
include_primitives: bool,
#[arg(short, long)]
output: Option<PathBuf>,
},
}
#[tracing::instrument(skip(cli))]
pub fn execute(cli: Cli) -> anyhow::Result<()> {
tracing::debug!("Executing CLI command");
match cli.command() {
Commands::Verify { action } => crate::verification::runner::handle(action),
Commands::Verus { action } => handle_verus(action),
Commands::Creusot { action } => handle_creusot(action),
Commands::Graph { action } => handle_graph(action),
}
}
#[tracing::instrument(skip(action))]
fn handle_verus(action: &VerusAction) -> anyhow::Result<()> {
tracing::debug!(action = ?action, "Handling Verus command");
match action {
VerusAction::List => list_verus_proofs(),
VerusAction::Run {
output,
timeout,
resume,
verus_path,
} => run_verus_proofs(output, *timeout, *resume, verus_path.as_deref()),
VerusAction::Summary { file } => show_verus_summary(file),
VerusAction::Failed { file } => show_verus_failed(file),
}
}
#[tracing::instrument(skip(action))]
fn handle_creusot(action: &CreusotAction) -> anyhow::Result<()> {
tracing::debug!(action = ?action, "Handling Creusot command");
match action {
CreusotAction::List => crate::verification::creusot_runner::list_modules(),
CreusotAction::Run { output, resume } => {
let summary = crate::verification::creusot_runner::run_all_modules(output, *resume)?;
println!();
println!("✅ Creusot verification complete!");
println!(" Total: {}", summary.total());
println!(" Passed: {}", summary.passed());
println!(" Failed: {}", summary.failed());
Ok(())
}
CreusotAction::Prove {
output,
goals,
resume,
} => {
let workspace_root = std::env::current_dir()?;
let summary = crate::verification::creusot_runner::run_all_modules_prove(
output,
goals,
&workspace_root,
*resume,
)?;
println!();
println!("✅ Creusot prove complete!");
println!(" Total: {}", summary.total());
println!(" Passed: {}", summary.passed());
println!(" Failed: {}", summary.failed());
Ok(())
}
CreusotAction::Summary { file } => crate::verification::creusot_runner::show_summary(file),
}
}
fn list_verus_proofs() -> anyhow::Result<()> {
use crate::verification::verus_runner::VerusProof;
let proofs = VerusProof::all();
println!("📋 Available Verus Proofs ({} total):", proofs.len());
println!();
let mut by_module: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for proof in proofs {
by_module
.entry(proof.module().to_string())
.or_default()
.push(proof.name().to_string());
}
for (module, proofs) in by_module {
println!(" {}:", module);
for proof in proofs {
println!(" - {}", proof);
}
println!();
}
Ok(())
}
fn run_verus_proofs(
output: &std::path::Path,
timeout: u64,
resume: bool,
verus_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
use crate::verification::verus_runner;
let verus_path = if let Some(path) = verus_path {
path.to_path_buf()
} else if let Ok(env_path) = std::env::var("VERUS_PATH") {
std::path::PathBuf::from(shellexpand::tilde(&env_path).to_string())
} else {
let default_path =
shellexpand::tilde("~/repos/verus/source/target-verus/release/verus").to_string();
std::path::PathBuf::from(default_path)
};
if !verus_path.exists() {
anyhow::bail!(
"Verus not found at: {}\nSet VERUS_PATH environment variable or use --verus-path",
verus_path.display()
);
}
verus_runner::run_all_proofs(&verus_path, output, Some(timeout), resume)?;
Ok(())
}
fn show_verus_summary(file: &std::path::Path) -> anyhow::Result<()> {
use crate::verification::verus_runner;
let summary = verus_runner::summarize_results(file)?;
println!("📊 Verus Verification Summary");
println!("============================");
println!();
println!(" Total: {}", summary.total());
println!(" Passed: {} ✅", summary.passed());
println!(" Failed: {} ❌", summary.failed());
println!(" Errors: {} 🔥", summary.errors());
println!();
println!(" Success Rate: {:.1}%", summary.success_rate());
Ok(())
}
fn show_verus_failed(file: &std::path::Path) -> anyhow::Result<()> {
use crate::verification::verus_runner;
let failed = verus_runner::list_failed_proofs(file)?;
if failed.is_empty() {
println!("✅ No failed proofs!");
return Ok(());
}
println!("❌ Failed Verus Proofs ({} total):", failed.len());
println!();
for result in failed {
println!(" {}::{}", result.module(), result.proof());
println!(" Status: {:?}", result.status());
println!(" Time: {}s", result.time_seconds());
if !result.error_message().is_empty() {
println!(
" Error: {}",
result.error_message().lines().next().unwrap_or("")
);
}
println!();
}
Ok(())
}
#[tracing::instrument(skip(action))]
fn handle_graph(action: &GraphAction) -> anyhow::Result<()> {
use crate::type_graph::{
DotRenderer, GraphRenderer, MermaidDirection, MermaidRenderer, TypeGraph,
};
use std::io::Write;
tracing::debug!(action = ?action, "Handling graph command");
match action {
GraphAction::List => {
let types = TypeGraph::registered_types();
if types.is_empty() {
println!("No graphable types registered.");
println!("Enable the `graph` feature and use `#[derive(Elicit)]` on your types.");
} else {
println!("{} registered graphable type(s):\n", types.len());
for name in types {
println!(" {name}");
}
}
Ok(())
}
GraphAction::Render {
root,
format,
include_primitives,
output,
} => {
tracing::debug!(root, format, "Rendering type graph");
let graph = TypeGraph::from_root(root).map_err(|e| anyhow::anyhow!("{e}"))?;
let rendered = match format.as_str() {
"dot" => DotRenderer {
include_primitives: *include_primitives,
..Default::default()
}
.render(&graph),
_ => MermaidRenderer {
direction: MermaidDirection::TopDown,
include_primitives: *include_primitives,
}
.render(&graph),
};
match output {
Some(path) => {
let mut file = std::fs::File::create(path)
.map_err(|e| anyhow::anyhow!("Cannot write to {}: {e}", path.display()))?;
file.write_all(rendered.as_bytes())?;
println!("Written to {}", path.display());
tracing::info!(path = %path.display(), format, "Graph written to file");
}
None => print!("{rendered}"),
}
Ok(())
}
}
}