logicaffeine-proof 0.9.13

Backward-chaining proof engine with Socratic hints
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Socratic Hint Engine - Generates pedagogical hints for proof guidance.
//!
//! Instead of giving direct answers, this module generates leading questions
//! that help users discover the right proof steps themselves.

use crate::{ProofExpr, ProofTerm};

/// Tactics that the user might try
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuggestedTactic {
    ModusPonens,
    UniversalElim,
    ExistentialIntro,
    AndIntro,
    AndElim,
    OrIntro,
    OrElim,
    Induction,
    Reflexivity,
    Rewrite,
    Assumption,
}

impl SuggestedTactic {
    pub fn name(&self) -> &'static str {
        match self {
            SuggestedTactic::ModusPonens => "Modus Ponens",
            SuggestedTactic::UniversalElim => "Universal Elimination",
            SuggestedTactic::ExistentialIntro => "Existential Introduction",
            SuggestedTactic::AndIntro => "And Introduction",
            SuggestedTactic::AndElim => "And Elimination",
            SuggestedTactic::OrIntro => "Or Introduction",
            SuggestedTactic::OrElim => "Or Elimination (Case Analysis)",
            SuggestedTactic::Induction => "Induction",
            SuggestedTactic::Reflexivity => "Reflexivity",
            SuggestedTactic::Rewrite => "Rewrite",
            SuggestedTactic::Assumption => "Assumption",
        }
    }
}

/// A Socratic hint - a leading question to guide the user
#[derive(Debug, Clone)]
pub struct SocraticHint {
    /// The hint text (a question or observation)
    pub text: String,
    /// The tactic this hint is suggesting
    pub suggested_tactic: Option<SuggestedTactic>,
    /// Priority (higher = more relevant)
    pub priority: u8,
}

impl SocraticHint {
    pub fn new(text: impl Into<String>, tactic: Option<SuggestedTactic>, priority: u8) -> Self {
        SocraticHint {
            text: text.into(),
            suggested_tactic: tactic,
            priority,
        }
    }
}

/// Generate a Socratic hint based on the goal and available knowledge
pub fn suggest_hint(
    goal: &ProofExpr,
    knowledge_base: &[ProofExpr],
    failed_tactics: &[SuggestedTactic],
) -> SocraticHint {
    let mut hints = Vec::new();

    // Analyze goal structure
    analyze_goal_structure(goal, &mut hints);

    // Check if goal matches any premise directly
    check_direct_match(goal, knowledge_base, &mut hints);

    // Look for implications that could prove the goal
    check_implications(goal, knowledge_base, &mut hints);

    // Look for universal statements that could be instantiated
    check_universals(goal, knowledge_base, &mut hints);

    // Check for conjunction/disjunction opportunities
    check_connectives(goal, knowledge_base, &mut hints);

    // Check for equality patterns
    check_equality(goal, knowledge_base, &mut hints);

    // Filter out hints for already-tried tactics
    hints.retain(|h| {
        h.suggested_tactic
            .map(|t| !failed_tactics.contains(&t))
            .unwrap_or(true)
    });

    // Sort by priority (highest first)
    hints.sort_by(|a, b| b.priority.cmp(&a.priority));

    // Return the best hint, or a generic one
    hints.into_iter().next().unwrap_or_else(|| {
        SocraticHint::new(
            "What logical structure does your goal have? Try breaking it down into simpler parts.",
            None,
            0,
        )
    })
}

/// Analyze the structure of the goal to suggest relevant tactics
fn analyze_goal_structure(goal: &ProofExpr, hints: &mut Vec<SocraticHint>) {
    match goal {
        ProofExpr::Implies(_, _) => {
            hints.push(SocraticHint::new(
                "Your goal is an implication P \u{2192} Q. To prove it, assume P and then prove Q.",
                None,
                7,
            ));
        }
        ProofExpr::ForAll { variable, body } => {
            hints.push(SocraticHint::new(
                format!(
                    "Your goal is a universal statement \u{2200}{}. To prove it, consider an arbitrary {} and prove the body.",
                    variable, variable
                ),
                None,
                7,
            ));
        }
        ProofExpr::Exists { variable, body } => {
            hints.push(SocraticHint::new(
                format!(
                    "Your goal is an existential statement \u{2203}{}. You need to find a specific witness.",
                    variable
                ),
                Some(SuggestedTactic::ExistentialIntro),
                7,
            ));
        }
        ProofExpr::And(_, _) => {
            hints.push(SocraticHint::new(
                "Your goal is a conjunction P \u{2227} Q. You need to prove both P and Q separately.",
                Some(SuggestedTactic::AndIntro),
                7,
            ));
        }
        ProofExpr::Or(_, _) => {
            hints.push(SocraticHint::new(
                "Your goal is a disjunction P \u{2228} Q. You only need to prove one of them.",
                Some(SuggestedTactic::OrIntro),
                7,
            ));
        }
        ProofExpr::Not(_) => {
            hints.push(SocraticHint::new(
                "Your goal is a negation \u{00AC}P. Try assuming P and deriving a contradiction.",
                None,
                6,
            ));
        }
        ProofExpr::Identity(left, right) => {
            if left == right {
                hints.push(SocraticHint::new(
                    "Both sides of the equation are identical. Try reflexivity!",
                    Some(SuggestedTactic::Reflexivity),
                    10,
                ));
            } else {
                hints.push(SocraticHint::new(
                    "Your goal is an equality. Can you rewrite one side to match the other?",
                    Some(SuggestedTactic::Rewrite),
                    6,
                ));
            }
        }
        ProofExpr::Predicate { name, .. } => {
            hints.push(SocraticHint::new(
                format!(
                    "Your goal is {}(...). Do you have this as an assumption, or can you derive it?",
                    name
                ),
                None,
                3,
            ));
        }
        _ => {}
    }
}

/// Check if the goal matches any premise directly
fn check_direct_match(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
    for premise in kb {
        if premise == goal {
            hints.push(SocraticHint::new(
                "Look carefully at your assumptions. One of them is exactly what you need!",
                Some(SuggestedTactic::Assumption),
                10,
            ));
            return;
        }
    }
}

/// Check for implications P \u{2192} goal in the knowledge base
fn check_implications(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
    for premise in kb {
        if let ProofExpr::Implies(antecedent, consequent) = premise {
            // Check if consequent matches goal
            if consequent.as_ref() == goal {
                hints.push(SocraticHint::new(
                    format!(
                        "You have an implication that concludes your goal. Can you prove its antecedent?"
                    ),
                    Some(SuggestedTactic::ModusPonens),
                    9,
                ));
            }
            // Check if antecedent is also in KB
            if consequent.as_ref() == goal && kb.iter().any(|p| p == antecedent.as_ref()) {
                hints.push(SocraticHint::new(
                    "You have both P and P \u{2192} Q where Q is your goal. Try Modus Ponens!",
                    Some(SuggestedTactic::ModusPonens),
                    10,
                ));
            }
        }
    }
}

/// Check for universal statements that could be instantiated
fn check_universals(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
    for premise in kb {
        if let ProofExpr::ForAll { variable, body } = premise {
            // Check if goal could be an instance of the body
            if could_be_instance(goal, body) {
                hints.push(SocraticHint::new(
                    format!(
                        "You have a universal statement \u{2200}{}. What value should you substitute for {}?",
                        variable, variable
                    ),
                    Some(SuggestedTactic::UniversalElim),
                    8,
                ));
            }
        }
    }
}

/// Check for conjunction/disjunction opportunities
fn check_connectives(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
    // Check if we have both parts of a conjunction goal
    if let ProofExpr::And(left, right) = goal {
        let have_left = kb.iter().any(|p| p == left.as_ref());
        let have_right = kb.iter().any(|p| p == right.as_ref());
        if have_left && have_right {
            hints.push(SocraticHint::new(
                "You have both parts of the conjunction in your assumptions!",
                Some(SuggestedTactic::AndIntro),
                10,
            ));
        } else if have_left {
            hints.push(SocraticHint::new(
                "You have the left part of the conjunction. Now prove the right part.",
                Some(SuggestedTactic::AndIntro),
                5,
            ));
        } else if have_right {
            hints.push(SocraticHint::new(
                "You have the right part of the conjunction. Now prove the left part.",
                Some(SuggestedTactic::AndIntro),
                5,
            ));
        }
    }

    // Check for disjunctions in premises (case analysis)
    for premise in kb {
        if let ProofExpr::Or(_, _) = premise {
            hints.push(SocraticHint::new(
                "You have a disjunction in your assumptions. Consider case analysis!",
                Some(SuggestedTactic::OrElim),
                6,
            ));
        }
    }

    // Check for conjunctions in premises (can extract parts)
    for premise in kb {
        if let ProofExpr::And(left, right) = premise {
            if left.as_ref() == goal || right.as_ref() == goal {
                hints.push(SocraticHint::new(
                    "Your goal is part of a conjunction you have. Extract it!",
                    Some(SuggestedTactic::AndElim),
                    9,
                ));
            }
        }
    }
}

/// Check for equality-related hints
fn check_equality(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
    // Look for equations that could be used to rewrite
    for premise in kb {
        if let ProofExpr::Identity(left, right) = premise {
            // Check if either side of the equation appears in the goal
            if term_appears_in_expr(left, goal) || term_appears_in_expr(right, goal) {
                hints.push(SocraticHint::new(
                    "You have an equation that might help. Try rewriting with it.",
                    Some(SuggestedTactic::Rewrite),
                    7,
                ));
            }
        }
    }

    // Check for induction opportunities (Nat-related goals)
    if involves_nat(goal) {
        hints.push(SocraticHint::new(
            "This involves natural numbers. Have you considered induction?",
            Some(SuggestedTactic::Induction),
            6,
        ));
    }
}

/// Helper: Check if goal could be an instance of body (simple structural check)
fn could_be_instance(goal: &ProofExpr, body: &ProofExpr) -> bool {
    // Simplified check - in full implementation, would use unification
    match (goal, body) {
        (
            ProofExpr::Predicate { name: g_name, .. },
            ProofExpr::Predicate { name: b_name, .. },
        ) => g_name == b_name,
        (ProofExpr::Identity(_, _), ProofExpr::Identity(_, _)) => true,
        _ => false,
    }
}

/// Helper: Check if a term appears in an expression
fn term_appears_in_expr(term: &ProofTerm, expr: &ProofExpr) -> bool {
    match expr {
        ProofExpr::Predicate { args, .. } => args.iter().any(|a| a == term),
        ProofExpr::Identity(left, right) => left == term || right == term,
        ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
            term_appears_in_expr(term, l) || term_appears_in_expr(term, r)
        }
        ProofExpr::Not(inner) => term_appears_in_expr(term, inner),
        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
            term_appears_in_expr(term, body)
        }
        _ => false,
    }
}

/// Helper: Check if expression involves natural numbers (Nat, Zero, Succ)
fn involves_nat(expr: &ProofExpr) -> bool {
    match expr {
        ProofExpr::Identity(left, right) => is_nat_term(left) || is_nat_term(right),
        ProofExpr::Predicate { args, .. } => args.iter().any(|a| is_nat_term(a)),
        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => involves_nat(body),
        ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
            involves_nat(l) || involves_nat(r)
        }
        ProofExpr::Not(inner) => involves_nat(inner),
        _ => false,
    }
}

/// Helper: Check if a term looks like a Nat
fn is_nat_term(term: &ProofTerm) -> bool {
    match term {
        ProofTerm::Constant(s) => s == "Zero" || s == "Nat",
        ProofTerm::Function(name, _) => name == "Succ" || name == "add" || name == "mul",
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn predicate(name: &str, args: Vec<ProofTerm>) -> ProofExpr {
        ProofExpr::Predicate {
            name: name.to_string(),
            args,
            world: None,
        }
    }

    #[test]
    fn test_direct_match_hint() {
        let goal = predicate("Human", vec![ProofTerm::Constant("Socrates".to_string())]);
        let kb = vec![goal.clone()];

        let hint = suggest_hint(&goal, &kb, &[]);
        assert!(hint.suggested_tactic == Some(SuggestedTactic::Assumption));
    }

    #[test]
    fn test_modus_ponens_hint() {
        let p = predicate("Human", vec![ProofTerm::Constant("Socrates".to_string())]);
        let q = predicate("Mortal", vec![ProofTerm::Constant("Socrates".to_string())]);
        let implication = ProofExpr::Implies(Box::new(p.clone()), Box::new(q.clone()));

        let kb = vec![p, implication];

        let hint = suggest_hint(&q, &kb, &[]);
        assert!(hint.suggested_tactic == Some(SuggestedTactic::ModusPonens));
    }

    #[test]
    fn test_conjunction_hint() {
        let p = predicate("P", vec![]);
        let q = predicate("Q", vec![]);
        let goal = ProofExpr::And(Box::new(p.clone()), Box::new(q.clone()));

        let kb = vec![p, q];

        let hint = suggest_hint(&goal, &kb, &[]);
        assert!(hint.suggested_tactic == Some(SuggestedTactic::AndIntro));
    }

    #[test]
    fn test_reflexivity_hint() {
        let term = ProofTerm::Constant("x".to_string());
        let goal = ProofExpr::Identity(term.clone(), term);

        let hint = suggest_hint(&goal, &[], &[]);
        assert!(hint.suggested_tactic == Some(SuggestedTactic::Reflexivity));
    }
}