/home/noah/src/ruchy/src/proving/prover.rs
Line | Count | Source |
1 | | //! Simplified interactive theorem prover |
2 | | |
3 | | use anyhow::Result; |
4 | | use serde::{Deserialize, Serialize}; |
5 | | use std::collections::HashMap; |
6 | | |
7 | | use super::smt::SmtBackend; |
8 | | use super::tactics::TacticLibrary; |
9 | | |
10 | | /// Interactive prover |
11 | | pub struct InteractiveProver { |
12 | | _backend: SmtBackend, |
13 | | tactics: TacticLibrary, |
14 | | timeout: u64, |
15 | | ml_suggestions: bool, |
16 | | } |
17 | | |
18 | | impl InteractiveProver { |
19 | | /// Create new prover |
20 | 7 | pub fn new(backend: SmtBackend) -> Self { |
21 | 7 | Self { |
22 | 7 | _backend: backend, |
23 | 7 | tactics: TacticLibrary::default(), |
24 | 7 | timeout: 5000, |
25 | 7 | ml_suggestions: false, |
26 | 7 | } |
27 | 7 | } |
28 | | |
29 | | /// Set timeout |
30 | 1 | pub fn set_timeout(&mut self, timeout: u64) { |
31 | 1 | self.timeout = timeout; |
32 | 1 | } |
33 | | |
34 | | /// Enable ML suggestions |
35 | 2 | pub fn set_ml_suggestions(&mut self, enabled: bool) { |
36 | 2 | self.ml_suggestions = enabled; |
37 | 2 | } |
38 | | |
39 | | /// Load proof script |
40 | 1 | pub fn load_script(&mut self, _script: &str) -> Result<()> { |
41 | | // Simplified: just return ok |
42 | 1 | Ok(()) |
43 | 1 | } |
44 | | |
45 | | /// Get available tactics |
46 | 0 | pub fn get_available_tactics(&self) -> Vec<&dyn super::tactics::Tactic> { |
47 | 0 | self.tactics.all_tactics() |
48 | 0 | } |
49 | | |
50 | | /// Apply tactic |
51 | 1 | pub fn apply_tactic(&mut self, session: &mut ProverSession, tactic_name: &str, args: &[&str]) -> Result<ProofResult> { |
52 | 1 | let tactic0 = self.tactics.get_tactic(tactic_name)?; |
53 | | |
54 | 0 | if let Some(goal) = session.current_goal() { |
55 | 0 | let result = tactic.apply(goal, args, &session.context)?; |
56 | 0 | match result { |
57 | | StepResult::Solved => { |
58 | 0 | session.complete_goal(); |
59 | 0 | Ok(ProofResult::Solved) |
60 | | } |
61 | 0 | StepResult::Simplified(new_goal) => { |
62 | 0 | session.update_goal(new_goal); |
63 | 0 | Ok(ProofResult::Progress) |
64 | | } |
65 | 0 | StepResult::Subgoals(subgoals) => { |
66 | 0 | session.replace_with_subgoals(subgoals); |
67 | 0 | Ok(ProofResult::Progress) |
68 | | } |
69 | 0 | StepResult::Failed(msg) => { |
70 | 0 | Ok(ProofResult::Failed(msg)) |
71 | | } |
72 | | } |
73 | | } else { |
74 | 0 | Ok(ProofResult::Failed("No active goal".to_string())) |
75 | | } |
76 | 1 | } |
77 | | |
78 | | /// Process input |
79 | 3 | pub fn process_input(&mut self, session: &mut ProverSession, input: &str) -> Result<ProofResult> { |
80 | | // Try to parse as goal |
81 | 3 | if let Some(goal1 ) = input.strip_prefix("prove ") { |
82 | 1 | session.add_goal(goal.to_string()); |
83 | 1 | return Ok(ProofResult::Progress); |
84 | 2 | } |
85 | | |
86 | | // Try as tactic |
87 | 2 | let parts: Vec<&str> = input.split_whitespace().collect(); |
88 | 2 | if !parts.is_empty() { |
89 | 1 | let tactic_name = parts[0]; |
90 | 1 | let args = &parts[1..]; |
91 | 1 | return self.apply_tactic(session, tactic_name, args); |
92 | 1 | } |
93 | | |
94 | 1 | Ok(ProofResult::Failed("Unknown command".to_string())) |
95 | 3 | } |
96 | | |
97 | | /// Suggest tactics |
98 | 0 | pub fn suggest_tactics(&self, goal: &ProofGoal) -> Result<Vec<super::tactics::TacticSuggestion>> { |
99 | 0 | self.tactics.suggest_tactics(goal, &ProofContext::new()) |
100 | 0 | } |
101 | | } |
102 | | |
103 | | /// Prover session |
104 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
105 | | pub struct ProverSession { |
106 | | goals: Vec<ProofGoal>, |
107 | | context: ProofContext, |
108 | | history: Vec<String>, |
109 | | } |
110 | | |
111 | | impl ProverSession { |
112 | | /// Create new session |
113 | 23 | pub fn new() -> Self { |
114 | 23 | Self { |
115 | 23 | goals: Vec::new(), |
116 | 23 | context: ProofContext::new(), |
117 | 23 | history: Vec::new(), |
118 | 23 | } |
119 | 23 | } |
120 | | |
121 | | /// Add goal |
122 | 13 | pub fn add_goal(&mut self, statement: String) { |
123 | 13 | self.goals.push(ProofGoal { statement }); |
124 | 13 | } |
125 | | |
126 | | /// Get current goal |
127 | 6 | pub fn current_goal(&self) -> Option<&ProofGoal> { |
128 | 6 | self.goals.first() |
129 | 6 | } |
130 | | |
131 | | /// Update current goal |
132 | 3 | pub fn update_goal(&mut self, statement: String) { |
133 | 3 | if !self.goals.is_empty() { |
134 | 2 | self.goals[0].statement = statement; |
135 | 2 | }1 |
136 | 3 | } |
137 | | |
138 | | /// Complete current goal |
139 | 6 | pub fn complete_goal(&mut self) { |
140 | 6 | if !self.goals.is_empty() { |
141 | 5 | self.goals.remove(0); |
142 | 5 | }1 |
143 | 6 | } |
144 | | |
145 | | /// Replace with subgoals |
146 | 3 | pub fn replace_with_subgoals(&mut self, subgoals: Vec<String>) { |
147 | 3 | if !self.goals.is_empty() { |
148 | 2 | self.goals.remove(0); |
149 | 5 | for subgoal in subgoals2 .into_iter2 ().rev2 () { |
150 | 5 | self.goals.insert(0, ProofGoal { statement: subgoal }); |
151 | 5 | } |
152 | 1 | } |
153 | 3 | } |
154 | | |
155 | | /// Get all goals |
156 | 1 | pub fn get_goals(&self) -> &[ProofGoal] { |
157 | 1 | &self.goals |
158 | 1 | } |
159 | | |
160 | | /// Check if complete |
161 | 17 | pub fn is_complete(&self) -> bool { |
162 | 17 | self.goals.is_empty() |
163 | 17 | } |
164 | | |
165 | | /// Export to text |
166 | 5 | pub fn to_text_proof(&self) -> String { |
167 | 5 | let mut proof = String::new(); |
168 | 5 | proof.push_str("Proof:\n"); |
169 | 8 | for line3 in &self.history { |
170 | | use std::fmt::Write; |
171 | 3 | let _ = writeln!(proof, " {line}"); |
172 | | } |
173 | 5 | if self.is_complete() { |
174 | 4 | proof.push_str("Qed.\n"); |
175 | 4 | }1 |
176 | 5 | proof |
177 | 5 | } |
178 | | |
179 | | /// Export to Coq |
180 | 1 | pub fn to_coq_proof(&self) -> String { |
181 | 1 | self.to_text_proof() // Simplified |
182 | 1 | } |
183 | | |
184 | | /// Export to Lean |
185 | 1 | pub fn to_lean_proof(&self) -> String { |
186 | 1 | self.to_text_proof() // Simplified |
187 | 1 | } |
188 | | } |
189 | | |
190 | | impl Default for ProverSession { |
191 | 1 | fn default() -> Self { |
192 | 1 | Self::new() |
193 | 1 | } |
194 | | } |
195 | | |
196 | | /// Proof goal |
197 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
198 | | pub struct ProofGoal { |
199 | | pub statement: String, |
200 | | } |
201 | | |
202 | | /// Proof context |
203 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
204 | | pub struct ProofContext { |
205 | | pub assumptions: Vec<String>, |
206 | | pub definitions: HashMap<String, String>, |
207 | | } |
208 | | |
209 | | impl ProofContext { |
210 | | /// Create new context |
211 | 25 | pub fn new() -> Self { |
212 | 25 | Self { |
213 | 25 | assumptions: Vec::new(), |
214 | 25 | definitions: HashMap::new(), |
215 | 25 | } |
216 | 25 | } |
217 | | } |
218 | | |
219 | | impl Default for ProofContext { |
220 | 1 | fn default() -> Self { |
221 | 1 | Self::new() |
222 | 1 | } |
223 | | } |
224 | | |
225 | | /// Proof result |
226 | | #[derive(Debug)] |
227 | | pub enum ProofResult { |
228 | | Solved, |
229 | | Progress, |
230 | | Failed(String), |
231 | | } |
232 | | |
233 | | /// Step result from tactic application |
234 | | #[derive(Debug, Clone)] |
235 | | pub enum StepResult { |
236 | | /// Goal was solved |
237 | | Solved, |
238 | | |
239 | | /// Goal was simplified to new goal |
240 | | Simplified(String), |
241 | | |
242 | | /// Goal was split into subgoals |
243 | | Subgoals(Vec<String>), |
244 | | |
245 | | /// Tactic failed |
246 | | Failed(String), |
247 | | } |
248 | | |
249 | | #[cfg(test)] |
250 | | mod tests { |
251 | | use super::*; |
252 | | use crate::proving::smt::SmtBackend; |
253 | | |
254 | 7 | fn create_test_prover() -> InteractiveProver { |
255 | 7 | let backend = SmtBackend::Z3; |
256 | 7 | InteractiveProver::new(backend) |
257 | 7 | } |
258 | | |
259 | 22 | fn create_test_session() -> ProverSession { |
260 | 22 | ProverSession::new() |
261 | 22 | } |
262 | | |
263 | | // Test 1: Prover Creation and Basic Configuration |
264 | | #[test] |
265 | 1 | fn test_prover_creation() { |
266 | 1 | let prover = create_test_prover(); |
267 | 1 | assert_eq!(prover.timeout, 5000); |
268 | 1 | assert!(!prover.ml_suggestions); |
269 | 1 | } |
270 | | |
271 | | #[test] |
272 | 1 | fn test_prover_timeout_setting() { |
273 | 1 | let mut prover = create_test_prover(); |
274 | 1 | prover.set_timeout(10000); |
275 | 1 | assert_eq!(prover.timeout, 10000); |
276 | 1 | } |
277 | | |
278 | | #[test] |
279 | 1 | fn test_prover_ml_suggestions_setting() { |
280 | 1 | let mut prover = create_test_prover(); |
281 | 1 | prover.set_ml_suggestions(true); |
282 | 1 | assert!(prover.ml_suggestions); |
283 | | |
284 | 1 | prover.set_ml_suggestions(false); |
285 | 1 | assert!(!prover.ml_suggestions); |
286 | 1 | } |
287 | | |
288 | | #[test] |
289 | 1 | fn test_prover_load_script() { |
290 | 1 | let mut prover = create_test_prover(); |
291 | 1 | let result = prover.load_script("example script"); |
292 | 1 | assert!(result.is_ok()); |
293 | 1 | } |
294 | | |
295 | | // Test 2: Session Management |
296 | | #[test] |
297 | 1 | fn test_session_creation() { |
298 | 1 | let session = create_test_session(); |
299 | 1 | assert!(session.goals.is_empty()); |
300 | 1 | assert!(session.history.is_empty()); |
301 | 1 | assert!(session.is_complete()); |
302 | 1 | } |
303 | | |
304 | | #[test] |
305 | 1 | fn test_session_default() { |
306 | 1 | let session = ProverSession::default(); |
307 | 1 | assert!(session.goals.is_empty()); |
308 | 1 | assert!(session.is_complete()); |
309 | 1 | } |
310 | | |
311 | | #[test] |
312 | 1 | fn test_session_add_goal() { |
313 | 1 | let mut session = create_test_session(); |
314 | 1 | session.add_goal("forall x, x = x".to_string()); |
315 | | |
316 | 1 | assert_eq!(session.goals.len(), 1); |
317 | 1 | assert!(!session.is_complete()); |
318 | | |
319 | 1 | let current_goal = session.current_goal().unwrap(); |
320 | 1 | assert_eq!(current_goal.statement, "forall x, x = x"); |
321 | 1 | } |
322 | | |
323 | | #[test] |
324 | 1 | fn test_session_multiple_goals() { |
325 | 1 | let mut session = create_test_session(); |
326 | 1 | session.add_goal("goal1".to_string()); |
327 | 1 | session.add_goal("goal2".to_string()); |
328 | | |
329 | 1 | assert_eq!(session.goals.len(), 2); |
330 | 1 | let current = session.current_goal().unwrap(); |
331 | 1 | assert_eq!(current.statement, "goal1"); |
332 | 1 | } |
333 | | |
334 | | #[test] |
335 | 1 | fn test_session_complete_goal() { |
336 | 1 | let mut session = create_test_session(); |
337 | 1 | session.add_goal("test goal".to_string()); |
338 | | |
339 | 1 | assert!(!session.is_complete()); |
340 | 1 | session.complete_goal(); |
341 | 1 | assert!(session.is_complete()); |
342 | 1 | } |
343 | | |
344 | | #[test] |
345 | 1 | fn test_session_update_goal() { |
346 | 1 | let mut session = create_test_session(); |
347 | 1 | session.add_goal("original goal".to_string()); |
348 | | |
349 | 1 | session.update_goal("updated goal".to_string()); |
350 | 1 | let current = session.current_goal().unwrap(); |
351 | 1 | assert_eq!(current.statement, "updated goal"); |
352 | 1 | } |
353 | | |
354 | | #[test] |
355 | 1 | fn test_session_replace_with_subgoals() { |
356 | 1 | let mut session = create_test_session(); |
357 | 1 | session.add_goal("main goal".to_string()); |
358 | | |
359 | 1 | let subgoals = vec!["subgoal1".to_string(), "subgoal2".to_string()]; |
360 | 1 | session.replace_with_subgoals(subgoals); |
361 | | |
362 | 1 | assert_eq!(session.goals.len(), 2); |
363 | | // Due to .rev() then inserting at position 0, order is preserved |
364 | 1 | assert_eq!(session.goals[0].statement, "subgoal1"); |
365 | 1 | assert_eq!(session.goals[1].statement, "subgoal2"); |
366 | 1 | } |
367 | | |
368 | | // Test 3: Goal Operations |
369 | | #[test] |
370 | 1 | fn test_get_goals() { |
371 | 1 | let mut session = create_test_session(); |
372 | 1 | session.add_goal("goal1".to_string()); |
373 | 1 | session.add_goal("goal2".to_string()); |
374 | | |
375 | 1 | let goals = session.get_goals(); |
376 | 1 | assert_eq!(goals.len(), 2); |
377 | 1 | assert_eq!(goals[0].statement, "goal1"); |
378 | 1 | assert_eq!(goals[1].statement, "goal2"); |
379 | 1 | } |
380 | | |
381 | | #[test] |
382 | 1 | fn test_session_completion_status() { |
383 | 1 | let mut session = create_test_session(); |
384 | 1 | assert!(session.is_complete()); |
385 | | |
386 | 1 | session.add_goal("test".to_string()); |
387 | 1 | assert!(!session.is_complete()); |
388 | | |
389 | 1 | session.complete_goal(); |
390 | 1 | assert!(session.is_complete()); |
391 | 1 | } |
392 | | |
393 | | // Test 4: Text Export Features |
394 | | #[test] |
395 | 1 | fn test_text_proof_export_empty() { |
396 | 1 | let session = create_test_session(); |
397 | 1 | let text_proof = session.to_text_proof(); |
398 | | |
399 | 1 | assert!(text_proof.contains("Proof:")); |
400 | 1 | assert!(text_proof.contains("Qed.")); |
401 | 1 | } |
402 | | |
403 | | #[test] |
404 | 1 | fn test_text_proof_export_with_history() { |
405 | 1 | let mut session = create_test_session(); |
406 | 1 | session.history.push("apply reflexivity".to_string()); |
407 | 1 | session.history.push("exact H".to_string()); |
408 | | |
409 | 1 | let text_proof = session.to_text_proof(); |
410 | 1 | assert!(text_proof.contains("apply reflexivity")); |
411 | 1 | assert!(text_proof.contains("exact H")); |
412 | 1 | assert!(text_proof.contains("Qed.")); |
413 | 1 | } |
414 | | |
415 | | #[test] |
416 | 1 | fn test_text_proof_export_incomplete() { |
417 | 1 | let mut session = create_test_session(); |
418 | 1 | session.add_goal("incomplete goal".to_string()); |
419 | 1 | session.history.push("started proof".to_string()); |
420 | | |
421 | 1 | let text_proof = session.to_text_proof(); |
422 | 1 | assert!(text_proof.contains("started proof")); |
423 | 1 | assert!(!text_proof.contains("Qed.")); // Should not have Qed for incomplete proof |
424 | 1 | } |
425 | | |
426 | | #[test] |
427 | 1 | fn test_coq_proof_export() { |
428 | 1 | let session = create_test_session(); |
429 | 1 | let coq_proof = session.to_coq_proof(); |
430 | | |
431 | | // Currently simplified to text proof |
432 | 1 | assert!(coq_proof.contains("Proof:")); |
433 | 1 | assert!(coq_proof.contains("Qed.")); |
434 | 1 | } |
435 | | |
436 | | #[test] |
437 | 1 | fn test_lean_proof_export() { |
438 | 1 | let session = create_test_session(); |
439 | 1 | let lean_proof = session.to_lean_proof(); |
440 | | |
441 | | // Currently simplified to text proof |
442 | 1 | assert!(lean_proof.contains("Proof:")); |
443 | 1 | assert!(lean_proof.contains("Qed.")); |
444 | 1 | } |
445 | | |
446 | | // Test 5: Input Processing |
447 | | #[test] |
448 | 1 | fn test_process_prove_command() { |
449 | 1 | let mut prover = create_test_prover(); |
450 | 1 | let mut session = create_test_session(); |
451 | | |
452 | 1 | let result = prover.process_input(&mut session, "prove forall x, x = x").unwrap(); |
453 | | |
454 | 1 | match result { |
455 | | ProofResult::Progress => { |
456 | 1 | assert_eq!(session.goals.len(), 1); |
457 | 1 | assert_eq!(session.current_goal().unwrap().statement, "forall x, x = x"); |
458 | | } |
459 | 0 | _ => panic!("Expected Progress result"), |
460 | | } |
461 | 1 | } |
462 | | |
463 | | #[test] |
464 | 1 | fn test_process_empty_input() { |
465 | 1 | let mut prover = create_test_prover(); |
466 | 1 | let mut session = create_test_session(); |
467 | | |
468 | 1 | let result = prover.process_input(&mut session, "").unwrap(); |
469 | | |
470 | 1 | match result { |
471 | 1 | ProofResult::Failed(msg) => { |
472 | 1 | assert!(msg.contains("Unknown command")); |
473 | | } |
474 | 0 | _ => panic!("Expected Failed result"), |
475 | | } |
476 | 1 | } |
477 | | |
478 | | #[test] |
479 | 1 | fn test_process_unknown_command() { |
480 | 1 | let mut prover = create_test_prover(); |
481 | 1 | let mut session = create_test_session(); |
482 | | |
483 | 1 | let result = prover.process_input(&mut session, "unknown_command arg1 arg2"); |
484 | | |
485 | | // Should try to apply as tactic and fail with error |
486 | 0 | match result { |
487 | 1 | Err(e) => { |
488 | 1 | assert!(e.to_string().contains("Unknown tactic")); |
489 | | } |
490 | 0 | Ok(ProofResult::Failed(_)) => { |
491 | 0 | // This is also acceptable |
492 | 0 | } |
493 | 0 | _ => panic!("Expected error or Failed result"), |
494 | | } |
495 | 1 | } |
496 | | |
497 | | // Test 6: Proof Context |
498 | | #[test] |
499 | 1 | fn test_proof_context_creation() { |
500 | 1 | let context = ProofContext::new(); |
501 | 1 | assert!(context.assumptions.is_empty()); |
502 | 1 | assert!(context.definitions.is_empty()); |
503 | 1 | } |
504 | | |
505 | | #[test] |
506 | 1 | fn test_proof_context_default() { |
507 | 1 | let context = ProofContext::default(); |
508 | 1 | assert!(context.assumptions.is_empty()); |
509 | 1 | assert!(context.definitions.is_empty()); |
510 | 1 | } |
511 | | |
512 | | // Test 7: Proof Goal Structure |
513 | | #[test] |
514 | 1 | fn test_proof_goal_creation() { |
515 | 1 | let goal = ProofGoal { |
516 | 1 | statement: "test statement".to_string(), |
517 | 1 | }; |
518 | 1 | assert_eq!(goal.statement, "test statement"); |
519 | 1 | } |
520 | | |
521 | | // Test 8: Edge Cases |
522 | | #[test] |
523 | 1 | fn test_complete_goal_empty_session() { |
524 | 1 | let mut session = create_test_session(); |
525 | 1 | session.complete_goal(); // Should not panic |
526 | 1 | assert!(session.is_complete()); |
527 | 1 | } |
528 | | |
529 | | #[test] |
530 | 1 | fn test_update_goal_empty_session() { |
531 | 1 | let mut session = create_test_session(); |
532 | 1 | session.update_goal("new goal".to_string()); // Should not panic |
533 | 1 | assert!(session.is_complete()); |
534 | 1 | } |
535 | | |
536 | | #[test] |
537 | 1 | fn test_replace_subgoals_empty_session() { |
538 | 1 | let mut session = create_test_session(); |
539 | 1 | session.replace_with_subgoals(vec!["goal1".to_string()]); // Should not panic |
540 | 1 | assert!(session.is_complete()); |
541 | 1 | } |
542 | | |
543 | | #[test] |
544 | 1 | fn test_current_goal_empty_session() { |
545 | 1 | let session = create_test_session(); |
546 | 1 | assert!(session.current_goal().is_none()); |
547 | 1 | } |
548 | | |
549 | | // Test 9: Serialization (Basic Structure Test) |
550 | | #[test] |
551 | 1 | fn test_session_serialization_structure() { |
552 | 1 | let mut session = create_test_session(); |
553 | 1 | session.add_goal("test goal".to_string()); |
554 | 1 | session.context.assumptions.push("assumption1".to_string()); |
555 | 1 | session.history.push("step1".to_string()); |
556 | | |
557 | | // Test that all fields are accessible for serialization |
558 | 1 | assert_eq!(session.goals.len(), 1); |
559 | 1 | assert_eq!(session.context.assumptions.len(), 1); |
560 | 1 | assert_eq!(session.history.len(), 1); |
561 | 1 | } |
562 | | |
563 | | // Test 10: Multiple Session Operations |
564 | | #[test] |
565 | 1 | fn test_complex_session_workflow() { |
566 | 1 | let mut session = create_test_session(); |
567 | | |
568 | | // Add initial goal |
569 | 1 | session.add_goal("main theorem".to_string()); |
570 | 1 | assert_eq!(session.goals.len(), 1); |
571 | | |
572 | | // Replace with subgoals |
573 | 1 | session.replace_with_subgoals(vec![ |
574 | 1 | "subgoal 1".to_string(), |
575 | 1 | "subgoal 2".to_string(), |
576 | 1 | "subgoal 3".to_string(), |
577 | | ]); |
578 | 1 | assert_eq!(session.goals.len(), 3); |
579 | | |
580 | | // Complete first subgoal |
581 | 1 | session.complete_goal(); |
582 | 1 | assert_eq!(session.goals.len(), 2); |
583 | | |
584 | | // Update current goal |
585 | 1 | session.update_goal("modified subgoal".to_string()); |
586 | 1 | assert_eq!(session.current_goal().unwrap().statement, "modified subgoal"); |
587 | | |
588 | | // Complete remaining goals |
589 | 1 | session.complete_goal(); |
590 | 1 | session.complete_goal(); |
591 | 1 | assert!(session.is_complete()); |
592 | 1 | } |
593 | | } |