Skip to content

Commit 94b158d

Browse files
hyperpolymathclaude
andcommitted
feat(cli): wire --diagnose flag into verify and prove subcommands
When --diagnose is set and a proof fails, emit_diagnostic() calls prover.check() (which returns a ProverOutcome) then maps it through diagnose_from_outcome() to produce a DiagnosticReport with failure classification, human-readable explanation, and ordered suggestions. Adds diagnose_from_outcome(prover, outcome) to proof_failure.rs — maps all 8 ProverOutcome variants to FailureKind without a second raw-output prover invocation. The --diagnose flag is available on both `echidna verify` and `echidna prove`. Usage: echidna verify --diagnose myproof.smt2 --prover z3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5aec9d5 commit 94b158d

3 files changed

Lines changed: 113 additions & 31 deletions

File tree

src/rust/diagnostics/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ pub mod proof_failure;
99
pub use corpus_monitor::{CorpusMetrics, CorpusMonitor};
1010
pub use health::{HealthStatus, ProverHealth, ModelHealth, CorpusHealth, DegradationMode};
1111
pub use gnn_training::{GnnTrainingMetrics, load_training_metrics, update_health_with_metrics};
12-
pub use proof_failure::{diagnose, DiagnosticReport, FailureKind, SourceLocation};
12+
pub use proof_failure::{diagnose, diagnose_from_outcome, DiagnosticReport, FailureKind, SourceLocation};

src/rust/diagnostics/proof_failure.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
1818
use std::fmt;
1919

20-
use crate::provers::ProverKind;
20+
use crate::provers::{outcome::ProverOutcome, ProverKind};
2121

2222
/// Taxonomy of proof failure causes.
2323
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -204,6 +204,70 @@ pub fn diagnose(prover: ProverKind, raw_output: &str) -> DiagnosticReport {
204204
}
205205
}
206206

207+
/// Produce a `DiagnosticReport` from a `ProverOutcome` returned by `check()`.
208+
///
209+
/// This avoids a second prover invocation: if the caller already has a
210+
/// `ProverOutcome` (e.g. from `--diagnose` mode which calls `check()` instead
211+
/// of `verify_proof()`), map it directly into a report without re-running.
212+
pub fn diagnose_from_outcome(prover: ProverKind, outcome: &ProverOutcome) -> DiagnosticReport {
213+
let kind = match outcome {
214+
ProverOutcome::Proved { .. } => {
215+
// Should not reach diagnose on success, but handle gracefully.
216+
return DiagnosticReport {
217+
prover,
218+
kind: FailureKind::Unknown { raw_output: "Proved — no failure to diagnose.".into() },
219+
explanation: "The prover succeeded. No diagnostic needed.".into(),
220+
suggestions: vec![],
221+
raw_excerpt: String::new(),
222+
};
223+
},
224+
ProverOutcome::NoProofFound { reason, .. } => FailureKind::UnsolvedGoal {
225+
goal_summary: reason.clone(),
226+
},
227+
ProverOutcome::Timeout { limit_secs } => FailureKind::Timeout {
228+
limit_secs: Some(*limit_secs),
229+
},
230+
ProverOutcome::InvalidInput { reason, location } => FailureKind::SyntaxError {
231+
message: reason.clone(),
232+
location: location.map(|offset| SourceLocation {
233+
file: None,
234+
line: None,
235+
column: Some(offset as u32),
236+
}),
237+
},
238+
ProverOutcome::UnsupportedFeature { feature } => FailureKind::ConfigError {
239+
detail: format!("Unsupported feature: {}", feature),
240+
},
241+
ProverOutcome::InconsistentPremises { detail } => FailureKind::UnsolvedGoal {
242+
goal_summary: Some(
243+
detail.clone().unwrap_or_else(|| {
244+
"Premise set is inconsistent — proof is vacuously true.".into()
245+
}),
246+
),
247+
},
248+
ProverOutcome::ProverError { detail, exit_code } => FailureKind::ProverCrash {
249+
exit_code: *exit_code,
250+
stderr_excerpt: detail.clone(),
251+
},
252+
ProverOutcome::SystemError { detail } => FailureKind::ConfigError {
253+
detail: detail.clone(),
254+
},
255+
};
256+
257+
let (explanation, suggestions) = explain_and_suggest(prover, &kind);
258+
let raw_excerpt = match outcome {
259+
ProverOutcome::NoProofFound { reason, .. } => reason.clone().unwrap_or_default(),
260+
ProverOutcome::ProverError { detail, .. } => detail.clone(),
261+
ProverOutcome::SystemError { detail } => detail.clone(),
262+
ProverOutcome::InvalidInput { reason, .. } => reason.clone(),
263+
ProverOutcome::UnsupportedFeature { feature } => feature.clone(),
264+
ProverOutcome::InconsistentPremises { detail } => detail.clone().unwrap_or_default(),
265+
_ => String::new(),
266+
};
267+
268+
DiagnosticReport { prover, kind, explanation, suggestions, raw_excerpt }
269+
}
270+
207271
// ---------------------------------------------------------------------------
208272
// Per-prover diagnostic parsers
209273
// ---------------------------------------------------------------------------

src/rust/main.rs

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
88
use anyhow::{Context, Result};
99
use clap::{Parser, Subcommand};
10-
use echidna::{ProverConfig, ProverKind};
10+
use echidna::{
11+
diagnostics::proof_failure::diagnose_from_outcome, provers::ProverBackend, ProverConfig,
12+
ProverKind,
13+
};
1114
use indicatif::{ProgressBar, ProgressStyle};
1215
use std::path::PathBuf;
1316
use tracing::{info, warn};
@@ -66,6 +69,10 @@ enum Commands {
6669
/// Library paths
6770
#[arg(long)]
6871
library: Vec<PathBuf>,
72+
73+
/// On failure, run diagnostics and explain why the proof failed
74+
#[arg(long)]
75+
diagnose: bool,
6976
},
7077

7178
/// Verify an existing proof
@@ -88,6 +95,10 @@ enum Commands {
8895
/// Library paths
8996
#[arg(long)]
9097
library: Vec<PathBuf>,
98+
99+
/// On failure, run diagnostics and explain why the proof failed
100+
#[arg(long)]
101+
diagnose: bool,
91102
},
92103

93104
/// Search theorem libraries
@@ -172,9 +183,10 @@ async fn main() -> Result<()> {
172183
neural,
173184
executable,
174185
library,
186+
diagnose,
175187
} => {
176188
prove_command(
177-
file, prover, timeout, neural, executable, library, &formatter,
189+
file, prover, timeout, neural, executable, library, diagnose, &formatter,
178190
)
179191
.await?;
180192
},
@@ -185,8 +197,9 @@ async fn main() -> Result<()> {
185197
timeout,
186198
executable,
187199
library,
200+
diagnose,
188201
} => {
189-
verify_command(file, prover, timeout, executable, library, &formatter).await?;
202+
verify_command(file, prover, timeout, executable, library, diagnose, &formatter).await?;
190203
},
191204

192205
Commands::Search {
@@ -246,40 +259,29 @@ async fn prove_command(
246259
neural: bool,
247260
executable: Option<PathBuf>,
248261
library: Vec<PathBuf>,
262+
diagnose: bool,
249263
formatter: &OutputFormatter,
250264
) -> Result<()> {
251265
info!("Starting proof for: {}", file.display());
252266

253-
// Detect or use specified prover
254267
let kind = detect_prover(prover_kind, &file)?;
255-
256-
// Create prover configuration
257268
let config = create_config(kind, timeout, neural, executable, library)?;
258-
259-
// Create prover backend
260269
let prover = echidna::provers::ProverFactory::create(kind, config)
261270
.context("Failed to create prover backend")?;
262271

263-
// Show progress
264272
let pb = create_progress_bar("Parsing proof file...");
265-
266-
// Parse the proof file
267273
let state = prover
268274
.parse_file(file.clone())
269275
.await
270276
.context("Failed to parse proof file")?;
271277

272278
pb.set_message("Verifying proof...");
273-
274-
// Verify the proof
275279
let result = prover
276280
.verify_proof(&state)
277281
.await
278282
.context("Failed to verify proof")?;
279-
280283
pb.finish_and_clear();
281284

282-
// Output result
283285
if result {
284286
formatter.success(&format!(
285287
"✓ Proof verified successfully for {}",
@@ -292,57 +294,70 @@ async fn prove_command(
292294
file.display()
293295
))?;
294296
formatter.output_proof_state(&state)?;
297+
if diagnose {
298+
emit_diagnostic(kind, &*prover, &state, formatter).await;
299+
}
295300
std::process::exit(1);
296301
}
297302

298303
Ok(())
299304
}
300305

306+
/// Run `check()` on the prover and display a structured diagnostic report.
307+
///
308+
/// Called after a failed `verify_proof()` when `--diagnose` is active.
309+
/// Uses the richer `ProverOutcome` returned by `check()` to produce an
310+
/// actionable report via `diagnose_from_outcome`.
311+
async fn emit_diagnostic(
312+
kind: ProverKind,
313+
prover: &dyn ProverBackend,
314+
state: &echidna::core::ProofState,
315+
formatter: &OutputFormatter,
316+
) {
317+
let outcome = match prover.check(state).await {
318+
Ok(o) => o,
319+
Err(e) => {
320+
let _ = formatter.error(&format!("[diagnose] could not run check(): {}", e));
321+
return;
322+
},
323+
};
324+
let report = diagnose_from_outcome(kind, &outcome);
325+
let _ = formatter.info("\n--- Proof Failure Diagnostic ---");
326+
let _ = formatter.info(&report.display());
327+
}
328+
301329
/// Verify command implementation
302330
async fn verify_command(
303331
file: PathBuf,
304332
prover_kind: Option<ProverKind>,
305333
timeout: u64,
306334
executable: Option<PathBuf>,
307335
library: Vec<PathBuf>,
336+
diagnose: bool,
308337
formatter: &OutputFormatter,
309338
) -> Result<()> {
310339
info!("Verifying proof: {}", file.display());
311340

312-
// Detect or use specified prover
313341
let kind = detect_prover(prover_kind, &file)?;
314-
315-
// Create prover configuration
316342
let config = create_config(kind, timeout, true, executable, library)?;
317-
318-
// Create prover backend
319343
let prover = echidna::provers::ProverFactory::create(kind, config)
320344
.context("Failed to create prover backend")?;
321345

322-
// Show progress
323346
let pb = create_progress_bar("Parsing proof file...");
324-
325-
// Parse the proof file
326347
let state = prover
327348
.parse_file(file.clone())
328349
.await
329350
.context("Failed to parse proof file")?;
330351

331352
pb.set_message("Verifying proof...");
332-
333-
// Verify the proof
334353
let result = prover
335354
.verify_proof(&state)
336355
.await
337356
.context("Failed to verify proof")?;
338-
339357
pb.finish_and_clear();
340358

341-
// Output result
342359
if result {
343360
formatter.success(&format!("✓ Proof is valid: {}", file.display()))?;
344-
345-
// Show proof statistics
346361
formatter.info(&format!("Goals: {}", state.goals.len()))?;
347362
formatter.info(&format!("Tactics: {}", state.proof_script.len()))?;
348363
formatter.info(&format!(
@@ -351,6 +366,9 @@ async fn verify_command(
351366
))?;
352367
} else {
353368
formatter.error(&format!("✗ Proof is invalid: {}", file.display()))?;
369+
if diagnose {
370+
emit_diagnostic(kind, &*prover, &state, formatter).await;
371+
}
354372
std::process::exit(1);
355373
}
356374

0 commit comments

Comments
 (0)