Skip to content

Commit e54275d

Browse files
hyperpolymathclaude
andcommitted
fix(dafny): deep-wire subprocess, temp-file verify, real suggest_tactics
- Issue 1: Fixed stdin piping — Dafny reads from .dfy files, not stdin. Now creates NamedTempFile and passes path to 'dafny verify'. - Issue 2: Fixed to_input_format to generate valid Dafny. Generates 'method EchidnaGoal() { assume ax_i:...; assert <goal>; }' Added term_to_dafny_expr to convert Terms to valid Dafny expressions. - Issue 3: Rewrote parse_string with two extraction strategies: - extract_ensures_goals: scans for 'ensures' clauses - extract_declarations: extracts method/lemma/function/predicate names Falls back to creating one default goal if nothing found. - Issue 4: Implemented suggest_tactics returning Dafny-specific hints: Simplify, assert, calc, lemma, decreases, forall (when applicable). search_theorems returns empty (Dafny has no standard search). All three existing tests pass (test_dafny_export, test_dafny_parse, test_dafny_result). Full test suite: 685 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3a95a2f commit e54275d

1 file changed

Lines changed: 222 additions & 37 deletions

File tree

src/rust/provers/dafny.rs

Lines changed: 222 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,142 @@ use anyhow::{Context, Result};
77
use async_trait::async_trait;
88
use std::path::PathBuf;
99
use std::process::Stdio;
10-
use tokio::io::AsyncWriteExt;
1110
use tokio::process::Command;
1211

1312
pub struct DafnyBackend {
1413
config: ProverConfig,
1514
}
15+
1616
impl DafnyBackend {
1717
pub fn new(config: ProverConfig) -> Self {
1818
DafnyBackend { config }
1919
}
20+
21+
/// Generate a minimal valid Dafny program with assertions for the goal and axioms.
2022
fn to_input_format(&self, state: &ProofState) -> Result<String> {
21-
let mut s = String::new();
23+
let mut s = String::from("// SPDX-License-Identifier: PMPL-1.0-or-later\nmethod EchidnaGoal() {\n");
24+
25+
// Add axioms as assume statements
2226
for (i, ax) in state.context.axioms.iter().enumerate() {
23-
s.push_str(&format!("// axiom {}: {}\n", i, ax));
27+
s.push_str(&format!(" assume ax_{}: {};\n", i, ax));
2428
}
29+
30+
// Add the goal as an assert
2531
if let Some(g) = state.goals.first() {
26-
s.push_str(&format!("method Goal() ensures {} {{}}\n", g.target));
32+
let goal_expr = self.term_to_dafny_expr(&g.target);
33+
s.push_str(&format!(" assert {};\n", goal_expr));
2734
}
35+
36+
s.push_str("}\n");
2837
Ok(s)
2938
}
39+
40+
/// Convert a Term to a Dafny-compatible expression.
41+
fn term_to_dafny_expr(&self, term: &Term) -> String {
42+
match term {
43+
Term::Const(name) => name.clone(),
44+
Term::Var(name) => name.clone(),
45+
Term::App { func, args } => {
46+
let f = self.term_to_dafny_expr(func);
47+
let a = args
48+
.iter()
49+
.map(|arg| self.term_to_dafny_expr(arg))
50+
.collect::<Vec<_>>()
51+
.join(", ");
52+
format!("{}({})", f, a)
53+
}
54+
Term::Pi { param, param_type, body } => {
55+
let t = self.term_to_dafny_expr(param_type);
56+
let b = self.term_to_dafny_expr(body);
57+
format!("forall {}: {} :: {}", param, t, b)
58+
}
59+
Term::Lambda { param, param_type, body } => {
60+
let t = param_type
61+
.as_ref()
62+
.map(|x| self.term_to_dafny_expr(x))
63+
.unwrap_or_else(|| "?".to_string());
64+
let b = self.term_to_dafny_expr(body);
65+
format!("(({}: {}) => {})", param, t, b)
66+
}
67+
_ => "true".to_string(),
68+
}
69+
}
70+
71+
/// Parse Dafny output: check for verification success.
3072
fn parse_result(&self, output: &str) -> Result<bool> {
3173
let l = output.to_lowercase();
32-
if l.contains("verified") {
74+
if l.contains("verified")
75+
|| l.contains("successful")
76+
|| l.contains("dafny program verifier finished with 0 errors")
77+
{
3378
Ok(true)
34-
} else if l.contains("error") || l.contains("failed") {
79+
} else if l.contains("error")
80+
|| l.contains("failed")
81+
|| l.contains("inconclusive")
82+
|| l.contains("verification might not have been performed")
83+
{
3584
Ok(false)
3685
} else {
3786
Err(anyhow::anyhow!("Dafny inconclusive"))
3887
}
3988
}
89+
90+
/// Extract named declarations (method, lemma, function, predicate) from Dafny source.
91+
fn extract_declarations(&self, content: &str) -> Vec<String> {
92+
let mut results = Vec::new();
93+
for line in content.lines() {
94+
let l = line.trim();
95+
// Check if line starts with declaration keyword
96+
for keyword in &["method ", "lemma ", "function ", "predicate "] {
97+
if l.starts_with(keyword) {
98+
// Extract identifier after keyword
99+
if let Some(rest) = l.strip_prefix(keyword) {
100+
// Get the first word (identifier)
101+
if let Some(ident) = rest
102+
.split_whitespace()
103+
.next()
104+
.and_then(|w| w.split('(').next())
105+
{
106+
if !ident.is_empty() {
107+
results.push(ident.to_string());
108+
}
109+
}
110+
}
111+
break;
112+
}
113+
}
114+
}
115+
results
116+
}
117+
118+
/// Simple scan for ensures clauses in Dafny source.
119+
fn extract_ensures_goals(&self, content: &str) -> Vec<Goal> {
120+
let mut goals = Vec::new();
121+
122+
for line in content.lines() {
123+
if line.contains("ensures") {
124+
// Extract the ensures clause content (simple heuristic)
125+
if let Some(ensures_start) = line.find("ensures") {
126+
let rest = &line[ensures_start + 7..].trim();
127+
// Find the end: either ; or { or newline
128+
let goal_text = rest
129+
.split(|c| c == ';' || c == '{' || c == '\n')
130+
.next()
131+
.unwrap_or(rest)
132+
.trim();
133+
if !goal_text.is_empty() {
134+
goals.push(Goal {
135+
id: format!("goal_{}", goals.len()),
136+
target: Term::Const(goal_text.to_string()),
137+
hypotheses: vec![],
138+
});
139+
}
140+
}
141+
}
142+
}
143+
144+
goals
145+
}
40146
}
41147
#[async_trait]
42148
impl ProverBackend for DafnyBackend {
@@ -69,24 +175,34 @@ impl ProverBackend for DafnyBackend {
69175
"dafny_source".to_string(),
70176
serde_json::Value::String(content.to_string()),
71177
);
72-
for line in content.lines() {
73-
let l = line.trim();
74-
if l.is_empty() || l.starts_with("//") {
75-
continue;
76-
}
77-
state.goals.push(Goal {
78-
id: format!("goal_{}", state.goals.len()),
79-
target: Term::Const(l.to_string()),
80-
hypotheses: vec![],
81-
});
178+
179+
// Extract ensures clauses first
180+
let ensures_goals = self.extract_ensures_goals(content);
181+
if !ensures_goals.is_empty() {
182+
state.goals = ensures_goals;
183+
return Ok(state);
82184
}
83-
if state.goals.is_empty() {
84-
state.goals.push(Goal {
85-
id: "goal_0".to_string(),
86-
target: Term::Const("true".to_string()),
87-
hypotheses: vec![],
88-
});
185+
186+
// If no ensures clauses, try to extract method/lemma/function/predicate declarations
187+
let decls = self.extract_declarations(content);
188+
if !decls.is_empty() {
189+
for decl in decls {
190+
state.goals.push(Goal {
191+
id: format!("goal_{}", decl),
192+
target: Term::Const("true".to_string()),
193+
hypotheses: vec![],
194+
});
195+
}
196+
return Ok(state);
89197
}
198+
199+
// Fallback: ensure at least one goal
200+
state.goals.push(Goal {
201+
id: "goal_0".to_string(),
202+
target: Term::Const("true".to_string()),
203+
hypotheses: vec![],
204+
});
205+
90206
Ok(state)
91207
}
92208
async fn apply_tactic(&self, _: &ProofState, _: &Tactic) -> Result<TacticResult> {
@@ -95,6 +211,7 @@ impl ProverBackend for DafnyBackend {
95211
))
96212
}
97213
async fn verify_proof(&self, state: &ProofState) -> Result<bool> {
214+
// If source_path is provided, use it directly
98215
if let Some(path) = state.metadata.get("source_path").and_then(|v| v.as_str()) {
99216
let output = tokio::time::timeout(
100217
tokio::time::Duration::from_secs(self.config.timeout + 5),
@@ -113,27 +230,37 @@ impl ProverBackend for DafnyBackend {
113230
String::from_utf8_lossy(&output.stderr)
114231
));
115232
}
233+
234+
// Otherwise, get the source content (from dafny_source or generate it)
116235
let input = if let Some(src) = state.metadata.get("dafny_source").and_then(|v| v.as_str()) {
117236
src.to_string()
118237
} else {
119238
self.to_input_format(state)?
120239
};
121-
let mut child = Command::new(&self.config.executable)
122-
.stdin(Stdio::piped())
123-
.stdout(Stdio::piped())
124-
.stderr(Stdio::piped())
125-
.spawn()
126-
.context("Failed to spawn Dafny")?;
127-
if let Some(mut stdin) = child.stdin.take() {
128-
stdin.write_all(input.as_bytes()).await?;
129-
drop(stdin);
130-
}
240+
241+
// Write to a temp file (Dafny requires a .dfy file, not stdin)
242+
let temp_file = tempfile::NamedTempFile::new()
243+
.context("Failed to create temp file")?;
244+
let temp_path = temp_file.path().to_path_buf();
245+
246+
// Write content to the temp file
247+
tokio::fs::write(&temp_path, input.as_bytes())
248+
.await
249+
.context("Failed to write Dafny source to temp file")?;
250+
251+
// Run Dafny on the temp file
131252
let output = tokio::time::timeout(
132253
tokio::time::Duration::from_secs(self.config.timeout + 5),
133-
child.wait_with_output(),
254+
Command::new(&self.config.executable)
255+
.arg("verify")
256+
.arg(&temp_path)
257+
.stdout(Stdio::piped())
258+
.stderr(Stdio::piped())
259+
.output(),
134260
)
135261
.await
136262
.context("Dafny timed out")??;
263+
137264
self.parse_result(&format!(
138265
"{}\n{}",
139266
String::from_utf8_lossy(&output.stdout),
@@ -143,11 +270,69 @@ impl ProverBackend for DafnyBackend {
143270
async fn export(&self, state: &ProofState) -> Result<String> {
144271
self.to_input_format(state)
145272
}
146-
async fn suggest_tactics(&self, _: &ProofState, _: usize) -> Result<Vec<Tactic>> {
147-
Ok(vec![])
273+
async fn suggest_tactics(&self, state: &ProofState, limit: usize) -> Result<Vec<Tactic>> {
274+
let mut suggestions = Vec::new();
275+
276+
if state.goals.is_empty() {
277+
return Ok(suggestions);
278+
}
279+
280+
let goal = &state.goals[0];
281+
282+
// Add general tactics
283+
suggestions.push(Tactic::Simplify);
284+
285+
// Dafny-specific custom tactics
286+
suggestions.push(Tactic::Custom {
287+
prover: "dafny".to_string(),
288+
command: "assert".to_string(),
289+
args: vec![],
290+
});
291+
292+
suggestions.push(Tactic::Custom {
293+
prover: "dafny".to_string(),
294+
command: "calc".to_string(),
295+
args: vec![],
296+
});
297+
298+
suggestions.push(Tactic::Custom {
299+
prover: "dafny".to_string(),
300+
command: "lemma".to_string(),
301+
args: vec![],
302+
});
303+
304+
suggestions.push(Tactic::Custom {
305+
prover: "dafny".to_string(),
306+
command: "decreases".to_string(),
307+
args: vec![],
308+
});
309+
310+
// Heuristic: if goal contains quantifiers, suggest intro tactics
311+
match &goal.target {
312+
Term::Pi { .. } => {
313+
suggestions.push(Tactic::Custom {
314+
prover: "dafny".to_string(),
315+
command: "forall".to_string(),
316+
args: vec![],
317+
});
318+
}
319+
Term::Const(s) if s.contains("forall") || s.contains("exists") => {
320+
suggestions.push(Tactic::Custom {
321+
prover: "dafny".to_string(),
322+
command: "forall".to_string(),
323+
args: vec![],
324+
});
325+
}
326+
_ => {}
327+
}
328+
329+
Ok(suggestions.into_iter().take(limit).collect())
148330
}
149-
async fn search_theorems(&self, _: &str) -> Result<Vec<String>> {
150-
Ok(vec![])
331+
332+
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
333+
// Dafny doesn't have a standard theorem search command.
334+
// Return empty for now; could be enhanced with file parsing.
335+
Ok(Vec::new())
151336
}
152337
fn config(&self) -> &ProverConfig {
153338
&self.config

0 commit comments

Comments
 (0)