1use crate::{ProofExpr, ProofTerm};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SuggestedTactic {
11 ModusPonens,
12 UniversalElim,
13 ExistentialIntro,
14 AndIntro,
15 AndElim,
16 OrIntro,
17 OrElim,
18 Induction,
19 Reflexivity,
20 Rewrite,
21 Assumption,
22}
23
24impl SuggestedTactic {
25 pub fn name(&self) -> &'static str {
26 match self {
27 SuggestedTactic::ModusPonens => "Modus Ponens",
28 SuggestedTactic::UniversalElim => "Universal Elimination",
29 SuggestedTactic::ExistentialIntro => "Existential Introduction",
30 SuggestedTactic::AndIntro => "And Introduction",
31 SuggestedTactic::AndElim => "And Elimination",
32 SuggestedTactic::OrIntro => "Or Introduction",
33 SuggestedTactic::OrElim => "Or Elimination (Case Analysis)",
34 SuggestedTactic::Induction => "Induction",
35 SuggestedTactic::Reflexivity => "Reflexivity",
36 SuggestedTactic::Rewrite => "Rewrite",
37 SuggestedTactic::Assumption => "Assumption",
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct SocraticHint {
45 pub text: String,
47 pub suggested_tactic: Option<SuggestedTactic>,
49 pub priority: u8,
51}
52
53impl SocraticHint {
54 pub fn new(text: impl Into<String>, tactic: Option<SuggestedTactic>, priority: u8) -> Self {
55 SocraticHint {
56 text: text.into(),
57 suggested_tactic: tactic,
58 priority,
59 }
60 }
61}
62
63pub fn suggest_hint(
65 goal: &ProofExpr,
66 knowledge_base: &[ProofExpr],
67 failed_tactics: &[SuggestedTactic],
68) -> SocraticHint {
69 let mut hints = Vec::new();
70
71 analyze_goal_structure(goal, &mut hints);
73
74 check_direct_match(goal, knowledge_base, &mut hints);
76
77 check_implications(goal, knowledge_base, &mut hints);
79
80 check_universals(goal, knowledge_base, &mut hints);
82
83 check_connectives(goal, knowledge_base, &mut hints);
85
86 check_equality(goal, knowledge_base, &mut hints);
88
89 hints.retain(|h| {
91 h.suggested_tactic
92 .map(|t| !failed_tactics.contains(&t))
93 .unwrap_or(true)
94 });
95
96 hints.sort_by(|a, b| b.priority.cmp(&a.priority));
98
99 hints.into_iter().next().unwrap_or_else(|| {
101 SocraticHint::new(
102 "What logical structure does your goal have? Try breaking it down into simpler parts.",
103 None,
104 0,
105 )
106 })
107}
108
109fn analyze_goal_structure(goal: &ProofExpr, hints: &mut Vec<SocraticHint>) {
111 match goal {
112 ProofExpr::Implies(_, _) => {
113 hints.push(SocraticHint::new(
114 "Your goal is an implication P \u{2192} Q. To prove it, assume P and then prove Q.",
115 None,
116 7,
117 ));
118 }
119 ProofExpr::ForAll { variable, body } => {
120 hints.push(SocraticHint::new(
121 format!(
122 "Your goal is a universal statement \u{2200}{}. To prove it, consider an arbitrary {} and prove the body.",
123 variable, variable
124 ),
125 None,
126 7,
127 ));
128 }
129 ProofExpr::Exists { variable, body } => {
130 hints.push(SocraticHint::new(
131 format!(
132 "Your goal is an existential statement \u{2203}{}. You need to find a specific witness.",
133 variable
134 ),
135 Some(SuggestedTactic::ExistentialIntro),
136 7,
137 ));
138 }
139 ProofExpr::And(_, _) => {
140 hints.push(SocraticHint::new(
141 "Your goal is a conjunction P \u{2227} Q. You need to prove both P and Q separately.",
142 Some(SuggestedTactic::AndIntro),
143 7,
144 ));
145 }
146 ProofExpr::Or(_, _) => {
147 hints.push(SocraticHint::new(
148 "Your goal is a disjunction P \u{2228} Q. You only need to prove one of them.",
149 Some(SuggestedTactic::OrIntro),
150 7,
151 ));
152 }
153 ProofExpr::Not(_) => {
154 hints.push(SocraticHint::new(
155 "Your goal is a negation \u{00AC}P. Try assuming P and deriving a contradiction.",
156 None,
157 6,
158 ));
159 }
160 ProofExpr::Identity(left, right) => {
161 if left == right {
162 hints.push(SocraticHint::new(
163 "Both sides of the equation are identical. Try reflexivity!",
164 Some(SuggestedTactic::Reflexivity),
165 10,
166 ));
167 } else {
168 hints.push(SocraticHint::new(
169 "Your goal is an equality. Can you rewrite one side to match the other?",
170 Some(SuggestedTactic::Rewrite),
171 6,
172 ));
173 }
174 }
175 ProofExpr::Predicate { name, .. } => {
176 hints.push(SocraticHint::new(
177 format!(
178 "Your goal is {}(...). Do you have this as an assumption, or can you derive it?",
179 name
180 ),
181 None,
182 3,
183 ));
184 }
185 _ => {}
186 }
187}
188
189fn check_direct_match(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
191 for premise in kb {
192 if premise == goal {
193 hints.push(SocraticHint::new(
194 "Look carefully at your assumptions. One of them is exactly what you need!",
195 Some(SuggestedTactic::Assumption),
196 10,
197 ));
198 return;
199 }
200 }
201}
202
203fn check_implications(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
205 for premise in kb {
206 if let ProofExpr::Implies(antecedent, consequent) = premise {
207 if consequent.as_ref() == goal {
209 hints.push(SocraticHint::new(
210 format!(
211 "You have an implication that concludes your goal. Can you prove its antecedent?"
212 ),
213 Some(SuggestedTactic::ModusPonens),
214 9,
215 ));
216 }
217 if consequent.as_ref() == goal && kb.iter().any(|p| p == antecedent.as_ref()) {
219 hints.push(SocraticHint::new(
220 "You have both P and P \u{2192} Q where Q is your goal. Try Modus Ponens!",
221 Some(SuggestedTactic::ModusPonens),
222 10,
223 ));
224 }
225 }
226 }
227}
228
229fn check_universals(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
231 for premise in kb {
232 if let ProofExpr::ForAll { variable, body } = premise {
233 if could_be_instance(goal, body) {
235 hints.push(SocraticHint::new(
236 format!(
237 "You have a universal statement \u{2200}{}. What value should you substitute for {}?",
238 variable, variable
239 ),
240 Some(SuggestedTactic::UniversalElim),
241 8,
242 ));
243 }
244 }
245 }
246}
247
248fn check_connectives(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
250 if let ProofExpr::And(left, right) = goal {
252 let have_left = kb.iter().any(|p| p == left.as_ref());
253 let have_right = kb.iter().any(|p| p == right.as_ref());
254 if have_left && have_right {
255 hints.push(SocraticHint::new(
256 "You have both parts of the conjunction in your assumptions!",
257 Some(SuggestedTactic::AndIntro),
258 10,
259 ));
260 } else if have_left {
261 hints.push(SocraticHint::new(
262 "You have the left part of the conjunction. Now prove the right part.",
263 Some(SuggestedTactic::AndIntro),
264 5,
265 ));
266 } else if have_right {
267 hints.push(SocraticHint::new(
268 "You have the right part of the conjunction. Now prove the left part.",
269 Some(SuggestedTactic::AndIntro),
270 5,
271 ));
272 }
273 }
274
275 for premise in kb {
277 if let ProofExpr::Or(_, _) = premise {
278 hints.push(SocraticHint::new(
279 "You have a disjunction in your assumptions. Consider case analysis!",
280 Some(SuggestedTactic::OrElim),
281 6,
282 ));
283 }
284 }
285
286 for premise in kb {
288 if let ProofExpr::And(left, right) = premise {
289 if left.as_ref() == goal || right.as_ref() == goal {
290 hints.push(SocraticHint::new(
291 "Your goal is part of a conjunction you have. Extract it!",
292 Some(SuggestedTactic::AndElim),
293 9,
294 ));
295 }
296 }
297 }
298}
299
300fn check_equality(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
302 for premise in kb {
304 if let ProofExpr::Identity(left, right) = premise {
305 if term_appears_in_expr(left, goal) || term_appears_in_expr(right, goal) {
307 hints.push(SocraticHint::new(
308 "You have an equation that might help. Try rewriting with it.",
309 Some(SuggestedTactic::Rewrite),
310 7,
311 ));
312 }
313 }
314 }
315
316 if involves_nat(goal) {
318 hints.push(SocraticHint::new(
319 "This involves natural numbers. Have you considered induction?",
320 Some(SuggestedTactic::Induction),
321 6,
322 ));
323 }
324}
325
326fn could_be_instance(goal: &ProofExpr, body: &ProofExpr) -> bool {
328 match (goal, body) {
330 (
331 ProofExpr::Predicate { name: g_name, .. },
332 ProofExpr::Predicate { name: b_name, .. },
333 ) => g_name == b_name,
334 (ProofExpr::Identity(_, _), ProofExpr::Identity(_, _)) => true,
335 _ => false,
336 }
337}
338
339fn term_appears_in_expr(term: &ProofTerm, expr: &ProofExpr) -> bool {
341 match expr {
342 ProofExpr::Predicate { args, .. } => args.iter().any(|a| a == term),
343 ProofExpr::Identity(left, right) => left == term || right == term,
344 ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
345 term_appears_in_expr(term, l) || term_appears_in_expr(term, r)
346 }
347 ProofExpr::Not(inner) => term_appears_in_expr(term, inner),
348 ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
349 term_appears_in_expr(term, body)
350 }
351 _ => false,
352 }
353}
354
355fn involves_nat(expr: &ProofExpr) -> bool {
357 match expr {
358 ProofExpr::Identity(left, right) => is_nat_term(left) || is_nat_term(right),
359 ProofExpr::Predicate { args, .. } => args.iter().any(|a| is_nat_term(a)),
360 ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => involves_nat(body),
361 ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
362 involves_nat(l) || involves_nat(r)
363 }
364 ProofExpr::Not(inner) => involves_nat(inner),
365 _ => false,
366 }
367}
368
369fn is_nat_term(term: &ProofTerm) -> bool {
371 match term {
372 ProofTerm::Constant(s) => s == "Zero" || s == "Nat",
373 ProofTerm::Function(name, _) => name == "Succ" || name == "add" || name == "mul",
374 _ => false,
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn predicate(name: &str, args: Vec<ProofTerm>) -> ProofExpr {
383 ProofExpr::Predicate {
384 name: name.to_string(),
385 args,
386 world: None,
387 }
388 }
389
390 #[test]
391 fn test_direct_match_hint() {
392 let goal = predicate("Human", vec![ProofTerm::Constant("Socrates".to_string())]);
393 let kb = vec![goal.clone()];
394
395 let hint = suggest_hint(&goal, &kb, &[]);
396 assert!(hint.suggested_tactic == Some(SuggestedTactic::Assumption));
397 }
398
399 #[test]
400 fn test_modus_ponens_hint() {
401 let p = predicate("Human", vec![ProofTerm::Constant("Socrates".to_string())]);
402 let q = predicate("Mortal", vec![ProofTerm::Constant("Socrates".to_string())]);
403 let implication = ProofExpr::Implies(Box::new(p.clone()), Box::new(q.clone()));
404
405 let kb = vec![p, implication];
406
407 let hint = suggest_hint(&q, &kb, &[]);
408 assert!(hint.suggested_tactic == Some(SuggestedTactic::ModusPonens));
409 }
410
411 #[test]
412 fn test_conjunction_hint() {
413 let p = predicate("P", vec![]);
414 let q = predicate("Q", vec![]);
415 let goal = ProofExpr::And(Box::new(p.clone()), Box::new(q.clone()));
416
417 let kb = vec![p, q];
418
419 let hint = suggest_hint(&goal, &kb, &[]);
420 assert!(hint.suggested_tactic == Some(SuggestedTactic::AndIntro));
421 }
422
423 #[test]
424 fn test_reflexivity_hint() {
425 let term = ProofTerm::Constant("x".to_string());
426 let goal = ProofExpr::Identity(term.clone(), term);
427
428 let hint = suggest_hint(&goal, &[], &[]);
429 assert!(hint.suggested_tactic == Some(SuggestedTactic::Reflexivity));
430 }
431}