logicaffeine_proof/
hints.rs

1//! Socratic Hint Engine - Generates pedagogical hints for proof guidance.
2//!
3//! Instead of giving direct answers, this module generates leading questions
4//! that help users discover the right proof steps themselves.
5
6use crate::{ProofExpr, ProofTerm};
7
8/// Tactics that the user might try
9#[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/// A Socratic hint - a leading question to guide the user
43#[derive(Debug, Clone)]
44pub struct SocraticHint {
45    /// The hint text (a question or observation)
46    pub text: String,
47    /// The tactic this hint is suggesting
48    pub suggested_tactic: Option<SuggestedTactic>,
49    /// Priority (higher = more relevant)
50    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
63/// Generate a Socratic hint based on the goal and available knowledge
64pub 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
72    analyze_goal_structure(goal, &mut hints);
73
74    // Check if goal matches any premise directly
75    check_direct_match(goal, knowledge_base, &mut hints);
76
77    // Look for implications that could prove the goal
78    check_implications(goal, knowledge_base, &mut hints);
79
80    // Look for universal statements that could be instantiated
81    check_universals(goal, knowledge_base, &mut hints);
82
83    // Check for conjunction/disjunction opportunities
84    check_connectives(goal, knowledge_base, &mut hints);
85
86    // Check for equality patterns
87    check_equality(goal, knowledge_base, &mut hints);
88
89    // Filter out hints for already-tried tactics
90    hints.retain(|h| {
91        h.suggested_tactic
92            .map(|t| !failed_tactics.contains(&t))
93            .unwrap_or(true)
94    });
95
96    // Sort by priority (highest first)
97    hints.sort_by(|a, b| b.priority.cmp(&a.priority));
98
99    // Return the best hint, or a generic one
100    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
109/// Analyze the structure of the goal to suggest relevant tactics
110fn 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
189/// Check if the goal matches any premise directly
190fn 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
203/// Check for implications P \u{2192} goal in the knowledge base
204fn check_implications(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
205    for premise in kb {
206        if let ProofExpr::Implies(antecedent, consequent) = premise {
207            // Check if consequent matches goal
208            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            // Check if antecedent is also in KB
218            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
229/// Check for universal statements that could be instantiated
230fn check_universals(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
231    for premise in kb {
232        if let ProofExpr::ForAll { variable, body } = premise {
233            // Check if goal could be an instance of the body
234            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
248/// Check for conjunction/disjunction opportunities
249fn check_connectives(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
250    // Check if we have both parts of a conjunction goal
251    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    // Check for disjunctions in premises (case analysis)
276    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    // Check for conjunctions in premises (can extract parts)
287    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
300/// Check for equality-related hints
301fn check_equality(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
302    // Look for equations that could be used to rewrite
303    for premise in kb {
304        if let ProofExpr::Identity(left, right) = premise {
305            // Check if either side of the equation appears in the goal
306            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    // Check for induction opportunities (Nat-related goals)
317    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
326/// Helper: Check if goal could be an instance of body (simple structural check)
327fn could_be_instance(goal: &ProofExpr, body: &ProofExpr) -> bool {
328    // Simplified check - in full implementation, would use unification
329    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
339/// Helper: Check if a term appears in an expression
340fn 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
355/// Helper: Check if expression involves natural numbers (Nat, Zero, Succ)
356fn 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
369/// Helper: Check if a term looks like a Nat
370fn 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}