@@ -7,36 +7,142 @@ use anyhow::{Context, Result};
77use async_trait:: async_trait;
88use std:: path:: PathBuf ;
99use std:: process:: Stdio ;
10- use tokio:: io:: AsyncWriteExt ;
1110use tokio:: process:: Command ;
1211
1312pub struct DafnyBackend {
1413 config : ProverConfig ,
1514}
15+
1616impl 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\n method 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]
42148impl 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