use crate::{ProofExpr, ProofTerm};
#[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",
}
}
}
#[derive(Debug, Clone)]
pub struct SocraticHint {
pub text: String,
pub suggested_tactic: Option<SuggestedTactic>,
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,
}
}
}
pub fn suggest_hint(
goal: &ProofExpr,
knowledge_base: &[ProofExpr],
failed_tactics: &[SuggestedTactic],
) -> SocraticHint {
let mut hints = Vec::new();
analyze_goal_structure(goal, &mut hints);
check_direct_match(goal, knowledge_base, &mut hints);
check_implications(goal, knowledge_base, &mut hints);
check_universals(goal, knowledge_base, &mut hints);
check_connectives(goal, knowledge_base, &mut hints);
check_equality(goal, knowledge_base, &mut hints);
hints.retain(|h| {
h.suggested_tactic
.map(|t| !failed_tactics.contains(&t))
.unwrap_or(true)
});
hints.sort_by(|a, b| b.priority.cmp(&a.priority));
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,
)
})
}
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,
));
}
_ => {}
}
}
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;
}
}
}
fn check_implications(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
for premise in kb {
if let ProofExpr::Implies(antecedent, consequent) = premise {
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,
));
}
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,
));
}
}
}
}
fn check_universals(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
for premise in kb {
if let ProofExpr::ForAll { variable, body } = premise {
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,
));
}
}
}
}
fn check_connectives(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
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,
));
}
}
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,
));
}
}
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,
));
}
}
}
}
fn check_equality(goal: &ProofExpr, kb: &[ProofExpr], hints: &mut Vec<SocraticHint>) {
for premise in kb {
if let ProofExpr::Identity(left, right) = premise {
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,
));
}
}
}
if involves_nat(goal) {
hints.push(SocraticHint::new(
"This involves natural numbers. Have you considered induction?",
Some(SuggestedTactic::Induction),
6,
));
}
}
fn could_be_instance(goal: &ProofExpr, body: &ProofExpr) -> bool {
match (goal, body) {
(
ProofExpr::Predicate { name: g_name, .. },
ProofExpr::Predicate { name: b_name, .. },
) => g_name == b_name,
(ProofExpr::Identity(_, _), ProofExpr::Identity(_, _)) => true,
_ => false,
}
}
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,
}
}
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,
}
}
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));
}
}