Skip to main content

logicaffeine_proof/
engine.rs

1//! Backward chaining proof engine.
2//!
3//! "The machine that crawls backward from the Conclusion to the Axioms."
4//!
5//! This module implements the core proof search algorithm. It takes inference
6//! rules and *hunts* for proofs using backward chaining and unification.
7//!
8//! ## Backward Chaining Strategy
9//!
10//! 1. Start with the goal we want to prove
11//! 2. Find rules whose conclusions unify with our goal
12//! 3. Recursively prove the premises of those rules
13//! 4. Build the derivation tree as we succeed
14//!
15//! ## Example
16//!
17//! ```text
18//! Goal: Mortal(socrates)
19//!
20//! Knowledge Base:
21//!   - Human(socrates)
22//!   - ∀x(Human(x) → Mortal(x))
23//!
24//! Search:
25//!   1. Goal matches conclusion of ∀x(Human(x) → Mortal(x)) with x=socrates
26//!   2. New subgoal: Human(socrates)
27//!   3. Human(socrates) matches knowledge base fact
28//!   4. Build derivation tree: ModusPonens(UniversalInst, PremiseMatch)
29//! ```
30
31use crate::error::{ProofError, ProofResult};
32use crate::unify::{
33    apply_subst_to_expr, apply_subst_to_term, beta_reduce, compose_substitutions, unify_exprs,
34    unify_pattern, unify_terms, Substitution,
35};
36use crate::{DerivationTree, InferenceRule, ProofExpr, ProofGoal, ProofTerm};
37
38/// Default maximum depth for proof search.
39const DEFAULT_MAX_DEPTH: usize = 100;
40
41/// Default node budget for proof search — the total number of `prove_goal` invocations
42/// allowed for one top-level proof. The depth bound alone does NOT guarantee bounded
43/// *time*: with a recursive axiom in scope the branching factor is large, so a depth-100
44/// search can visit `b^100` nodes — effectively a hang. Counting nodes and stopping at the
45/// budget makes every search terminate in bounded time (fail-fast → `verified = false`)
46/// instead of running forever. Generous enough that no legitimate proof reaches it; the
47/// relevance ordering in `try_backward_chain` keeps real proofs far below it.
48const DEFAULT_STEP_BUDGET: usize = 100_000;
49
50/// The backward chaining proof engine.
51///
52/// Searches for proofs by working backwards from the goal, finding rules
53/// whose conclusions match, and recursively proving their premises.
54pub struct BackwardChainer {
55    /// Knowledge base: facts and rules available to the prover.
56    knowledge_base: Vec<ProofExpr>,
57
58    /// Maximum proof depth (prevents infinite loops).
59    max_depth: usize,
60
61    /// Counter for generating fresh variable names.
62    var_counter: usize,
63
64    /// Existentials already opened on the current search branch. Forward
65    /// existential elimination skolemizes `∃x.P(x)` into witness facts; without
66    /// this guard the same existential is re-opened with a fresh constant on
67    /// every recursive `prove_goal`, never adding new reasoning power but
68    /// spinning down to the depth limit (where the oracle, the last resort,
69    /// catches the goal and yields an uncertifiable proof). Recording the
70    /// opened existential collapses that loop to a single elimination.
71    eliminated_existentials: Vec<ProofExpr>,
72
73    /// Loop-detection stack: the canonical keys of the goals currently being
74    /// proved along the active branch (an ancestor chain, pushed on entry to
75    /// `prove_goal` and popped on exit). A *recursive* axiom — one whose
76    /// conclusion shares a predicate with its own antecedent, like Tarski's inner
77    /// transitivity — lets the search re-derive a goal from a fresh instance of
78    /// itself: `Cong(?,?,A,B)` ⇐ `Cong(?',?',A,B)` ⇐ … with new existentials each
79    /// time. The depth bound only stops that after `max_depth` native frames,
80    /// which overflows the stack first. Keying goals modulo their existential
81    /// renaming and refusing to re-enter one already on the branch collapses the
82    /// regress to a finite search — without touching the productive branches,
83    /// which discharge their antecedents against premises/facts, not by re-entry.
84    active_goals: Vec<String>,
85
86    /// Nodes (`prove_goal` invocations) spent on the current top-level proof. Reset when a
87    /// proof starts (depth 0) and incremented on each `prove_goal`; the search aborts once
88    /// it passes `step_budget`. This is the *time* bound the depth limit cannot provide.
89    steps: usize,
90
91    /// The node budget — see [`DEFAULT_STEP_BUDGET`].
92    step_budget: usize,
93}
94
95// =============================================================================
96// HELPER FUNCTIONS
97// =============================================================================
98
99/// Convert a ProofTerm to a ProofExpr for reduction.
100///
101/// Terms embed into expressions as atoms or constructors.
102fn term_to_expr(term: &ProofTerm) -> ProofExpr {
103    match term {
104        ProofTerm::Constant(s) => ProofExpr::Atom(s.clone()),
105        ProofTerm::Variable(s) => ProofExpr::Atom(s.clone()),
106        ProofTerm::BoundVarRef(s) => ProofExpr::Atom(s.clone()),
107        ProofTerm::Function(name, args) => {
108            // Check if this is a known constructor
109            if matches!(name.as_str(), "Zero" | "Succ" | "Nil" | "Cons") {
110                ProofExpr::Ctor {
111                    name: name.clone(),
112                    args: args.iter().map(term_to_expr).collect(),
113                }
114            } else {
115                // Otherwise it's a predicate/function
116                ProofExpr::Predicate {
117                    name: name.clone(),
118                    args: args.clone(),
119                    world: None,
120                }
121            }
122        }
123        ProofTerm::Group(terms) => {
124            // Groups become nested predicates or just the single element
125            if terms.len() == 1 {
126                term_to_expr(&terms[0])
127            } else {
128                // Multi-term groups - convert to predicate
129                ProofExpr::Predicate {
130                    name: "Group".into(),
131                    args: terms.clone(),
132                    world: None,
133                }
134            }
135        }
136    }
137}
138
139/// Whether an expression is falsum (⊥). The engine represents absurdity as the
140/// atom `⊥`; we also accept the spelled forms for robustness across producers.
141fn is_falsum(expr: &ProofExpr) -> bool {
142    matches!(expr, ProofExpr::Atom(s) if s == "⊥" || s == "False" || s == "false")
143}
144
145/// Flatten a (possibly nested) conjunction into its individual conjuncts.
146/// `A ∧ (B ∧ C)` becomes `[A, B, C]`; a non-conjunction becomes a singleton.
147fn flatten_conjuncts(expr: &ProofExpr) -> Vec<ProofExpr> {
148    match expr {
149        ProofExpr::And(left, right) => {
150            let mut conjuncts = flatten_conjuncts(left);
151            conjuncts.extend(flatten_conjuncts(right));
152            conjuncts
153        }
154        other => vec![other.clone()],
155    }
156}
157
158/// Reassemble per-conjunct `proofs` (in `flatten_conjuncts` order) into a proof tree that
159/// mirrors `ante`'s `And` structure with BINARY [`InferenceRule::ConjunctionIntro`] nodes —
160/// the only shape the certifier accepts. Each `And` becomes one binary node over the proofs
161/// of its two sides; each leaf consumes the next proof. Recursing left-then-right matches
162/// `flatten_conjuncts`, so the leaves line up with the proofs without reordering.
163fn build_conjunction_tree(
164    ante: &ProofExpr,
165    subst: &Substitution,
166    proofs: &mut std::vec::IntoIter<DerivationTree>,
167) -> DerivationTree {
168    match ante {
169        ProofExpr::And(left, right) => {
170            let lt = build_conjunction_tree(left, subst, proofs);
171            let rt = build_conjunction_tree(right, subst, proofs);
172            DerivationTree::new(
173                apply_subst_to_expr(ante, subst),
174                InferenceRule::ConjunctionIntro,
175                vec![lt, rt],
176            )
177        }
178        _ => proofs
179            .next()
180            .expect("flatten_conjuncts yields exactly one proof per non-And leaf"),
181    }
182}
183
184/// Whether an expression is a ground/atomic *fact* — the kind a subgoal can be
185/// discharged against directly by unification (as opposed to a rule to chain).
186fn is_atomic_fact(expr: &ProofExpr) -> bool {
187    matches!(
188        expr,
189        ProofExpr::Predicate { .. } | ProofExpr::Identity(_, _) | ProofExpr::Atom(_)
190    )
191}
192
193// =============================================================================
194// CERTIFIABLE CONTRADICTION FINDER (gapless, for verified conflict detection)
195// =============================================================================
196//
197// Every derivation these helpers build certifies end-to-end: each step is an
198// explicit PremiseMatch / UniversalInst / ConjunctionIntro / ModusPonens /
199// Contradiction / CaseAnalysis node. Forward-chaining saturates the known facts;
200// a bounded case-analysis layer handles self-referential paradoxes (e.g. the
201// Barber stated with simple predicates) by splitting on a candidate atom and
202// driving both branches to ⊥ — intuitionistically, so the kernel needs no
203// excluded middle.
204
205/// A Horn-style rule: `ante → cons`, optionally under a `∀ binder`.
206struct CertRule {
207    source: ProofExpr,
208    binder: Option<String>,
209    ante: ProofExpr,
210    cons: ProofExpr,
211}
212
213fn cert_is_rule(e: &ProofExpr) -> bool {
214    matches!(e, ProofExpr::Implies(..))
215        || matches!(e, ProofExpr::ForAll { body, .. }
216            if matches!(body.as_ref(), ProofExpr::Implies(..)))
217}
218
219fn cert_extract_rules(all: &[ProofExpr]) -> Vec<CertRule> {
220    let mut rules = Vec::new();
221    for e in all {
222        match e {
223            ProofExpr::Implies(a, c) => rules.push(CertRule {
224                source: e.clone(),
225                binder: None,
226                ante: (**a).clone(),
227                cons: (**c).clone(),
228            }),
229            ProofExpr::ForAll { variable, body } => {
230                if let ProofExpr::Implies(a, c) = body.as_ref() {
231                    rules.push(CertRule {
232                        source: e.clone(),
233                        binder: Some(variable.clone()),
234                        ante: (**a).clone(),
235                        cons: (**c).clone(),
236                    });
237                }
238            }
239            _ => {}
240        }
241    }
242    rules
243}
244
245/// Seed the non-rule premises as `PremiseMatch` leaves.
246fn cert_seed_facts(all: &[ProofExpr]) -> Vec<(ProofExpr, DerivationTree)> {
247    let mut known: Vec<(ProofExpr, DerivationTree)> = Vec::new();
248    for e in all {
249        if cert_is_rule(e) {
250            continue;
251        }
252        if !known.iter().any(|(p, _)| exprs_structurally_equal(p, e)) {
253            known.push((
254                e.clone(),
255                DerivationTree::leaf(e.clone(), InferenceRule::PremiseMatch),
256            ));
257        }
258    }
259    known
260}
261
262/// Build a certifiable proof that `a = c`, given a directed edge graph in which
263/// each `(b, edge_tree)` in `adj[a]` proves `a = b`. The returned tree chains the
264/// edges along a path from `a` to `c` using `Rewrite` (Leibniz): from a proof of
265/// `a = b` (the running accumulator, `P(b)` for `P = λz. a = z`) and a proof of
266/// `b = d` (the next edge, the equality), `Eq_rec` rewrites `b ↦ d` to yield
267/// `a = d`. A `None` means no path exists. Every node certifies.
268fn cert_eq_path_proof(
269    a: &ProofTerm,
270    c: &ProofTerm,
271    adj: &std::collections::HashMap<String, Vec<(ProofTerm, DerivationTree)>>,
272) -> Option<DerivationTree> {
273    fn term_key(t: &ProofTerm) -> Option<String> {
274        term_skey(t)
275    }
276    let a_key = term_key(a)?;
277    let c_key = term_key(c)?;
278    if a_key == c_key {
279        return None;
280    }
281    // BFS over constants, carrying the running proof of `a = current`.
282    let start_proof = DerivationTree::leaf(
283        ProofExpr::Identity(a.clone(), a.clone()),
284        InferenceRule::Reflexivity,
285    );
286    let mut queue: std::collections::VecDeque<(String, ProofTerm, DerivationTree)> =
287        std::collections::VecDeque::new();
288    queue.push_back((a_key.clone(), a.clone(), start_proof));
289    let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
290    visited.insert(a_key.clone());
291
292    while let Some((cur_key, cur_term, acc_proof)) = queue.pop_front() {
293        if cur_key == c_key {
294            // The accumulator already proves `a = c` once we've stepped onto `c`.
295            // (The start node `a = a` is reflexive and never the target, guarded
296            // above.)
297            return Some(acc_proof);
298        }
299        let Some(edges) = adj.get(&cur_key) else {
300            continue;
301        };
302        for (next_term, edge_tree) in edges {
303            let Some(next_key) = term_key(next_term) else {
304                continue;
305            };
306            if visited.contains(&next_key) {
307                continue;
308            }
309            visited.insert(next_key.clone());
310            // acc_proof : a = cur ;  edge_tree : cur = next.
311            // Rewrite cur ↦ next in `a = cur` to get `a = next`.
312            let new_concl = ProofExpr::Identity(a.clone(), next_term.clone());
313            let step = if cur_key == a_key {
314                // First step: `a = a` rewritten by `a = next` is just the edge.
315                edge_tree.clone()
316            } else {
317                DerivationTree::new(
318                    new_concl,
319                    InferenceRule::Rewrite {
320                        from: cur_term.clone(),
321                        to: next_term.clone(),
322                    },
323                    vec![edge_tree.clone(), acc_proof.clone()],
324                )
325            };
326            queue.push_back((next_key, next_term.clone(), step));
327        }
328    }
329    None
330}
331
332/// Close an equality contradiction: a known `¬(x = y)` paired with a chain of
333/// known equalities that entails `x = y`. The equalities form an undirected
334/// graph (each `a = b` fact also gives `b = a` via `EqualitySymmetry`); if `x`
335/// and `y` lie in one component, a path proof of `x = y` (or `y = x`, then
336/// symmetrised) discharges the negation. Every emitted node certifies.
337fn cert_equality_close(known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
338    use std::collections::HashMap;
339    // Build the symmetric adjacency: each known `a = b` contributes the edge
340    // a→b (its own proof) and b→a (its proof, symmetrised).
341    let mut adj: HashMap<String, Vec<(ProofTerm, DerivationTree)>> = HashMap::new();
342    fn key(t: &ProofTerm) -> Option<String> {
343        match t {
344            ProofTerm::Constant(s) | ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => {
345                Some(s.clone())
346            }
347            _ => None,
348        }
349    }
350    for (prop, tree) in known {
351        if let ProofExpr::Identity(l, r) = prop {
352            let (Some(lk), Some(rk)) = (key(l), key(r)) else {
353                continue;
354            };
355            if lk == rk {
356                continue;
357            }
358            adj.entry(lk).or_default().push((r.clone(), tree.clone()));
359            let sym = DerivationTree::new(
360                ProofExpr::Identity(r.clone(), l.clone()),
361                InferenceRule::EqualitySymmetry,
362                vec![tree.clone()],
363            );
364            adj.entry(rk).or_default().push((l.clone(), sym));
365        }
366    }
367    if adj.is_empty() {
368        return None;
369    }
370
371    for (prop, neg_tree) in known {
372        let ProofExpr::Not(inner) = prop else {
373            continue;
374        };
375        let ProofExpr::Identity(x, y) = inner.as_ref() else {
376            continue;
377        };
378        // Prove the EXACT polarity the negation refutes: ¬(x = y) needs `x = y`.
379        if let Some(eq_proof) = cert_eq_path_proof(x, y, &adj) {
380            return Some(DerivationTree::new(
381                ProofExpr::Atom("⊥".into()),
382                InferenceRule::Contradiction,
383                vec![eq_proof, neg_tree.clone()],
384            ));
385        }
386    }
387    None
388}
389
390/// A structural key for a term — atomic names map to themselves, applications to
391/// `head(arg₀,…,argₙ)` recursively. This lets the equality graph treat compound
392/// terms (e.g. `F(A)`) as first-class nodes, the precondition for congruence.
393fn term_skey(t: &ProofTerm) -> Option<String> {
394    match t {
395        ProofTerm::Constant(s) | ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => {
396            Some(s.clone())
397        }
398        ProofTerm::Function(f, args) => {
399            let mut parts = Vec::with_capacity(args.len());
400            for a in args {
401                parts.push(term_skey(a)?);
402            }
403            Some(format!("{}({})", f, parts.join(",")))
404        }
405        ProofTerm::Group(args) => {
406            let mut parts = Vec::with_capacity(args.len());
407            for a in args {
408                parts.push(term_skey(a)?);
409            }
410            Some(format!("({})", parts.join(",")))
411        }
412        _ => None,
413    }
414}
415
416/// Collect a term and all its subterms (for the congruence universe).
417fn collect_subterms(t: &ProofTerm, out: &mut Vec<ProofTerm>) {
418    out.push(t.clone());
419    if let ProofTerm::Function(_, args) | ProofTerm::Group(args) = t {
420        for a in args {
421            collect_subterms(a, out);
422        }
423    }
424}
425
426/// Build a proof of `head(xs) = head(ys)` from per-argument equality proofs,
427/// rewriting one differing argument at a time. `arg_proofs[i]` is `None` when
428/// `xs[i]` and `ys[i]` are structurally identical, else a proof of `xs[i]=ys[i]`.
429/// Each step is a `Rewrite` (Leibniz / `Eq_rec`): its motive `λz. head(xs)=head(…z…)`
430/// abstracts the i-th argument of the right-hand side, so the source premise is the
431/// running `head(xs)=head(cur)` and the equality premise the argument proof.
432fn build_congruence_proof(
433    head: &str,
434    xs: &[ProofTerm],
435    ys: &[ProofTerm],
436    arg_proofs: &[Option<DerivationTree>],
437) -> DerivationTree {
438    let f = |args: &[ProofTerm]| ProofTerm::Function(head.to_string(), args.to_vec());
439    let mut cur: Vec<ProofTerm> = xs.to_vec();
440    let mut acc = DerivationTree::leaf(
441        ProofExpr::Identity(f(xs), f(xs)),
442        InferenceRule::Reflexivity,
443    );
444    for i in 0..xs.len() {
445        let Some(arg_proof) = &arg_proofs[i] else {
446            continue;
447        };
448        let mut next = cur.clone();
449        next[i] = ys[i].clone();
450        let concl = ProofExpr::Identity(f(xs), f(&next));
451        acc = DerivationTree::new(
452            concl,
453            InferenceRule::Rewrite {
454                from: xs[i].clone(),
455                to: ys[i].clone(),
456            },
457            vec![arg_proof.clone(), acc],
458        );
459        cur = next;
460    }
461    acc
462}
463
464/// Prove `lhs = rhs` by congruence closure over the hypothesis equalities. Build an
465/// equality graph whose nodes are all subterms (compound included), seed it with the
466/// hypotheses (and their symmetrics), then saturate with congruence edges — for any
467/// two present applications `F(xs)`, `F(ys)` whose arguments are already pairwise
468/// connected, add `F(xs)=F(ys)` (its proof built by [`build_congruence_proof`]) — to
469/// a fixpoint. Finally search for a transitive path `lhs → rhs`. Every emitted node
470/// certifies (Reflexivity / EqualitySymmetry / Rewrite / EqualityTransitivity).
471pub(crate) fn cert_congruence_path(
472    lhs: &ProofTerm,
473    rhs: &ProofTerm,
474    hyps: &[(ProofExpr, DerivationTree)],
475) -> Option<DerivationTree> {
476    use std::collections::HashMap;
477    let mut adj: HashMap<String, Vec<(ProofTerm, DerivationTree)>> = HashMap::new();
478
479    // Seed the graph with hypothesis equalities, both directions.
480    for (prop, tree) in hyps {
481        let ProofExpr::Identity(l, r) = prop else {
482            continue;
483        };
484        let (Some(lk), Some(rk)) = (term_skey(l), term_skey(r)) else {
485            continue;
486        };
487        if lk == rk {
488            continue;
489        }
490        adj.entry(lk).or_default().push((r.clone(), tree.clone()));
491        let sym = DerivationTree::new(
492            ProofExpr::Identity(r.clone(), l.clone()),
493            InferenceRule::EqualitySymmetry,
494            vec![tree.clone()],
495        );
496        adj.entry(rk).or_default().push((l.clone(), sym));
497    }
498
499    // The universe of subterms (deduplicated by structural key).
500    let mut universe: Vec<ProofTerm> = Vec::new();
501    for (prop, _) in hyps {
502        if let ProofExpr::Identity(l, r) = prop {
503            collect_subterms(l, &mut universe);
504            collect_subterms(r, &mut universe);
505        }
506    }
507    collect_subterms(lhs, &mut universe);
508    collect_subterms(rhs, &mut universe);
509    let funcs: Vec<ProofTerm> = {
510        let mut seen = std::collections::HashSet::new();
511        universe
512            .into_iter()
513            .filter(|t| matches!(t, ProofTerm::Function(..)))
514            .filter(|t| term_skey(t).is_some_and(|k| seen.insert(k)))
515            .collect()
516    };
517
518    // Saturate congruence edges to a fixpoint.
519    loop {
520        let mut added = false;
521        for i in 0..funcs.len() {
522            for j in 0..funcs.len() {
523                if i == j {
524                    continue;
525                }
526                let (ProofTerm::Function(fi, xs), ProofTerm::Function(fj, ys)) =
527                    (&funcs[i], &funcs[j])
528                else {
529                    continue;
530                };
531                if fi != fj || xs.len() != ys.len() {
532                    continue;
533                }
534                let (ki, kj) = (term_skey(&funcs[i]).unwrap(), term_skey(&funcs[j]).unwrap());
535                let already = adj.get(&ki).is_some_and(|es| {
536                    es.iter().any(|(t, _)| term_skey(t).as_deref() == Some(kj.as_str()))
537                });
538                if already {
539                    continue;
540                }
541                let mut arg_proofs: Vec<Option<DerivationTree>> = Vec::with_capacity(xs.len());
542                let mut ok = true;
543                for (x, y) in xs.iter().zip(ys.iter()) {
544                    if term_skey(x) == term_skey(y) {
545                        arg_proofs.push(None);
546                    } else if let Some(p) = cert_eq_path_proof(x, y, &adj) {
547                        arg_proofs.push(Some(p));
548                    } else {
549                        ok = false;
550                        break;
551                    }
552                }
553                if !ok {
554                    continue;
555                }
556                let cong = build_congruence_proof(fi, xs, ys, &arg_proofs);
557                let sym = DerivationTree::new(
558                    ProofExpr::Identity(funcs[j].clone(), funcs[i].clone()),
559                    InferenceRule::EqualitySymmetry,
560                    vec![cong.clone()],
561                );
562                adj.entry(ki).or_default().push((funcs[j].clone(), cong));
563                adj.entry(kj).or_default().push((funcs[i].clone(), sym));
564                added = true;
565            }
566        }
567        if !added {
568            break;
569        }
570    }
571
572    cert_eq_path_proof(lhs, rhs, &adj)
573}
574
575/// The inequality `a ≤ b`, encoded as the Prop `le(a, b) = true`.
576pub(crate) fn le_eq(a: ProofTerm, b: ProofTerm) -> ProofExpr {
577    ProofExpr::Identity(
578        ProofTerm::Function("le".to_string(), vec![a, b]),
579        ProofTerm::Constant("true".to_string()),
580    )
581}
582
583/// If `e` is an inequality `le(a, b) = true`, return `(a, b)`.
584pub(crate) fn as_le_pair(e: &ProofExpr) -> Option<(ProofTerm, ProofTerm)> {
585    if let ProofExpr::Identity(lhs, rhs) = e {
586        if let (ProofTerm::Function(name, args), ProofTerm::Constant(t)) = (lhs, rhs) {
587            if name == "le" && args.len() == 2 && t == "true" {
588                return Some((args[0].clone(), args[1].clone()));
589            }
590        }
591    }
592    None
593}
594
595/// An integer-literal operand, if this term is one.
596fn as_int_literal(t: &ProofTerm) -> Option<i64> {
597    match t {
598        ProofTerm::Constant(s) => s.parse::<i64>().ok(),
599        _ => None,
600    }
601}
602
603/// Resolve a term through a substitution to a fixpoint, following chains
604/// (`x → y → c`). A witness that is only transitively bound (the existential cut
605/// leaves `a → _G9` while `_G9 → Q` lives elsewhere in the same substitution) is
606/// thereby fully ground.
607fn resolve_term(t: &ProofTerm, subst: &Substitution) -> ProofTerm {
608    let mut cur = t.clone();
609    for _ in 0..256 {
610        let next = apply_subst_to_term(&cur, subst);
611        if next == cur {
612            break;
613        }
614        cur = next;
615    }
616    cur
617}
618
619/// A term with no free variables — a witness ready to instantiate a quantifier.
620fn is_ground_term(t: &ProofTerm) -> bool {
621    match t {
622        ProofTerm::Variable(_) | ProofTerm::BoundVarRef(_) => false,
623        ProofTerm::Constant(_) => true,
624        ProofTerm::Function(_, args) | ProofTerm::Group(args) => args.iter().all(is_ground_term),
625    }
626}
627
628/// The fully-resolved, GROUND witness for `var` under `subst`, or `None` if it is
629/// unbound or only partially determined — in which case the instantiation must be
630/// rejected rather than leak a dangling metavariable into the certificate.
631fn ground_witness(var: &str, subst: &Substitution) -> Option<ProofTerm> {
632    let resolved = resolve_term(subst.get(var)?, subst);
633    is_ground_term(&resolved).then_some(resolved)
634}
635
636/// Collect the free-variable names of `t` in first-occurrence order (no dups),
637/// for the loop-detection key — so two goals that differ only by which fresh
638/// existential names they use produce the same canonical renaming.
639fn collect_vars_ordered_term(t: &ProofTerm, acc: &mut Vec<String>) {
640    match t {
641        ProofTerm::Variable(v) => {
642            if !acc.iter().any(|x| x == v) {
643                acc.push(v.clone());
644            }
645        }
646        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
647            for a in args {
648                collect_vars_ordered_term(a, acc);
649            }
650        }
651        ProofTerm::Constant(_) | ProofTerm::BoundVarRef(_) => {}
652    }
653}
654
655/// Collect the free-variable names of `e` in first-occurrence order (no dups).
656/// Covers every `ProofExpr` variant so the canonical key never silently leaves a
657/// variable un-renamed (which would weaken — never break — loop detection).
658fn collect_vars_ordered_expr(e: &ProofExpr, acc: &mut Vec<String>) {
659    match e {
660        ProofExpr::Predicate { args, .. } => {
661            for a in args {
662                collect_vars_ordered_term(a, acc);
663            }
664        }
665        ProofExpr::Identity(l, r) => {
666            collect_vars_ordered_term(l, acc);
667            collect_vars_ordered_term(r, acc);
668        }
669        ProofExpr::Term(t) => collect_vars_ordered_term(t, acc),
670        ProofExpr::NeoEvent { roles, .. } => {
671            for (_, t) in roles {
672                collect_vars_ordered_term(t, acc);
673            }
674        }
675        ProofExpr::Ctor { args, .. } => {
676            for a in args {
677                collect_vars_ordered_expr(a, acc);
678            }
679        }
680        ProofExpr::Not(p)
681        | ProofExpr::ForAll { body: p, .. }
682        | ProofExpr::Exists { body: p, .. }
683        | ProofExpr::Modal { body: p, .. }
684        | ProofExpr::Temporal { body: p, .. }
685        | ProofExpr::Lambda { body: p, .. }
686        | ProofExpr::Fixpoint { body: p, .. } => collect_vars_ordered_expr(p, acc),
687        ProofExpr::And(l, r)
688        | ProofExpr::Or(l, r)
689        | ProofExpr::Implies(l, r)
690        | ProofExpr::Iff(l, r)
691        | ProofExpr::App(l, r)
692        | ProofExpr::TemporalBinary { left: l, right: r, .. } => {
693            collect_vars_ordered_expr(l, acc);
694            collect_vars_ordered_expr(r, acc);
695        }
696        ProofExpr::Counterfactual { antecedent, consequent } => {
697            collect_vars_ordered_expr(antecedent, acc);
698            collect_vars_ordered_expr(consequent, acc);
699        }
700        ProofExpr::Match { scrutinee, arms } => {
701            collect_vars_ordered_expr(scrutinee, acc);
702            for arm in arms {
703                collect_vars_ordered_expr(&arm.body, acc);
704            }
705        }
706        ProofExpr::Atom(_)
707        | ProofExpr::TypedVar { .. }
708        | ProofExpr::Hole(_)
709        | ProofExpr::Unsupported(_) => {}
710    }
711}
712
713/// Does `var` occur as a free variable anywhere in `expr`?
714fn expr_mentions_var(expr: &ProofExpr, var: &str) -> bool {
715    let mut vs = Vec::new();
716    collect_vars_ordered_expr(expr, &mut vs);
717    vs.iter().any(|v| v == var)
718}
719
720/// Witnesses for the `∀`-bound variables of an axiom being instantiated, in binder
721/// order. Each is its pinned ground binding if it has one; a variable that does NOT
722/// occur in `matrix` is *vacuous* — the matrix (and hence the goal) is independent of
723/// it, so it is soundly instantiated with any in-scope entity (here a sibling's
724/// witness). Returns `None` only when a variable is genuinely underdetermined:
725/// unpinned yet occurring in the matrix, which would leak a metavariable into the
726/// certificate.
727fn instantiation_witnesses(
728    bound_vars: &[String],
729    subst: &Substitution,
730    matrix: &ProofExpr,
731) -> Option<Vec<ProofTerm>> {
732    let fallback = bound_vars.iter().find_map(|v| ground_witness(v, subst));
733    bound_vars
734        .iter()
735        .map(|v| {
736            ground_witness(v, subst).or_else(|| {
737                if expr_mentions_var(matrix, v) {
738                    None
739                } else {
740                    fallback.clone()
741                }
742            })
743        })
744        .collect()
745}
746
747/// Rewrite every occurrence of the eigenconstant `eigen` back to `Variable(var)`
748/// in a term — the generalization step of `∀`-introduction, undoing the opaque
749/// substitution the search ran under so the certifier can abstract `λ(var). …`.
750fn rewrite_const_to_var_term(t: &ProofTerm, eigen: &str, var: &str) -> ProofTerm {
751    match t {
752        ProofTerm::Constant(c) if c == eigen => ProofTerm::Variable(var.to_string()),
753        ProofTerm::Function(n, args) => ProofTerm::Function(
754            n.clone(),
755            args.iter().map(|a| rewrite_const_to_var_term(a, eigen, var)).collect(),
756        ),
757        ProofTerm::Group(args) => {
758            ProofTerm::Group(args.iter().map(|a| rewrite_const_to_var_term(a, eigen, var)).collect())
759        }
760        other => other.clone(),
761    }
762}
763
764/// Rewrite `Constant(eigen)` back to `Variable(var)` throughout an expression.
765fn rewrite_const_to_var_expr(e: &ProofExpr, eigen: &str, var: &str) -> ProofExpr {
766    let re = |x: &ProofExpr| Box::new(rewrite_const_to_var_expr(x, eigen, var));
767    let rt = |x: &ProofTerm| rewrite_const_to_var_term(x, eigen, var);
768    match e {
769        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
770            name: name.clone(),
771            args: args.iter().map(rt).collect(),
772            world: world.clone(),
773        },
774        ProofExpr::Identity(l, r) => ProofExpr::Identity(rt(l), rt(r)),
775        ProofExpr::Atom(s) => ProofExpr::Atom(s.clone()),
776        ProofExpr::And(l, r) => ProofExpr::And(re(l), re(r)),
777        ProofExpr::Or(l, r) => ProofExpr::Or(re(l), re(r)),
778        ProofExpr::Implies(l, r) => ProofExpr::Implies(re(l), re(r)),
779        ProofExpr::Iff(l, r) => ProofExpr::Iff(re(l), re(r)),
780        ProofExpr::Not(p) => ProofExpr::Not(re(p)),
781        ProofExpr::ForAll { variable, body } => {
782            ProofExpr::ForAll { variable: variable.clone(), body: re(body) }
783        }
784        ProofExpr::Exists { variable, body } => {
785            ProofExpr::Exists { variable: variable.clone(), body: re(body) }
786        }
787        ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
788            domain: domain.clone(),
789            force: *force,
790            flavor: flavor.clone(),
791            body: re(body),
792        },
793        ProofExpr::Counterfactual { antecedent, consequent } => ProofExpr::Counterfactual {
794            antecedent: re(antecedent),
795            consequent: re(consequent),
796        },
797        ProofExpr::Temporal { operator, body } => {
798            ProofExpr::Temporal { operator: operator.clone(), body: re(body) }
799        }
800        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
801            operator: operator.clone(),
802            left: re(left),
803            right: re(right),
804        },
805        ProofExpr::Lambda { variable, body } => {
806            ProofExpr::Lambda { variable: variable.clone(), body: re(body) }
807        }
808        ProofExpr::App(l, r) => ProofExpr::App(re(l), re(r)),
809        ProofExpr::NeoEvent { event_var, verb, roles } => ProofExpr::NeoEvent {
810            event_var: event_var.clone(),
811            verb: verb.clone(),
812            roles: roles.iter().map(|(role, t)| (role.clone(), rt(t))).collect(),
813        },
814        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
815            name: name.clone(),
816            args: args.iter().map(|a| rewrite_const_to_var_expr(a, eigen, var)).collect(),
817        },
818        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
819            scrutinee: re(scrutinee),
820            arms: arms
821                .iter()
822                .map(|arm| crate::MatchArm {
823                    ctor: arm.ctor.clone(),
824                    bindings: arm.bindings.clone(),
825                    body: rewrite_const_to_var_expr(&arm.body, eigen, var),
826                })
827                .collect(),
828        },
829        ProofExpr::Fixpoint { name, body } => {
830            ProofExpr::Fixpoint { name: name.clone(), body: re(body) }
831        }
832        ProofExpr::TypedVar { name, typename } => {
833            ProofExpr::TypedVar { name: name.clone(), typename: typename.clone() }
834        }
835        ProofExpr::Hole(s) => ProofExpr::Hole(s.clone()),
836        ProofExpr::Term(t) => ProofExpr::Term(rt(t)),
837        ProofExpr::Unsupported(s) => ProofExpr::Unsupported(s.clone()),
838    }
839}
840
841/// Rewrite `Constant(eigen)` back to `Variable(var)` throughout a whole derivation
842/// tree — its conclusions, the terms its rules carry (instantiation witnesses,
843/// rewrite endpoints, case formulae), and the recorded substitutions. The
844/// generalization that turns an eigenconstant body proof into a `∀`-abstraction.
845fn rewrite_tree_const_to_var(tree: &DerivationTree, eigen: &str, var: &str) -> DerivationTree {
846    let rule = match &tree.rule {
847        InferenceRule::UniversalInst(s) if s == eigen => InferenceRule::UniversalInst(var.to_string()),
848        InferenceRule::ExistentialIntro { witness, witness_type } if witness == eigen => {
849            InferenceRule::ExistentialIntro {
850                witness: var.to_string(),
851                witness_type: witness_type.clone(),
852            }
853        }
854        InferenceRule::ExistentialElim { witness } if witness == eigen => {
855            InferenceRule::ExistentialElim { witness: var.to_string() }
856        }
857        InferenceRule::Rewrite { from, to } => InferenceRule::Rewrite {
858            from: rewrite_const_to_var_term(from, eigen, var),
859            to: rewrite_const_to_var_term(to, eigen, var),
860        },
861        InferenceRule::CaseAnalysis { case_formula } => InferenceRule::CaseAnalysis {
862            case_formula: Box::new(rewrite_const_to_var_expr(case_formula, eigen, var)),
863        },
864        other => other.clone(),
865    };
866    DerivationTree {
867        conclusion: rewrite_const_to_var_expr(&tree.conclusion, eigen, var),
868        rule,
869        premises: tree.premises.iter().map(|p| rewrite_tree_const_to_var(p, eigen, var)).collect(),
870        depth: tree.depth,
871        substitution: tree
872            .substitution
873            .iter()
874            .map(|(k, v)| (k.clone(), rewrite_const_to_var_term(v, eigen, var)))
875            .collect(),
876    }
877}
878
879/// Project `target` out of a conjunctive hypothesis. Given `conj`, a derivation
880/// whose conclusion is a (possibly nested) conjunction, return a derivation of the
881/// matching conjunct — a chain of `ConjunctionElim` steps down to it — paired with
882/// the substitution that unifies that conjunct with `target`. This is forward
883/// ∧-elimination: it lets the search use a conjunct of an assumed `A ∧ B` as a
884/// standalone fact (so the existential middle of a cut pins against it), while the
885/// certifier independently re-derives the projection. Returns `None` if no conjunct
886/// matches.
887fn extract_conjunct(
888    conj: &DerivationTree,
889    target: &ProofExpr,
890) -> Option<(DerivationTree, Substitution)> {
891    if let ProofExpr::And(l, r) = &conj.conclusion {
892        let left =
893            DerivationTree::new((**l).clone(), InferenceRule::ConjunctionElim, vec![conj.clone()]);
894        if let Some(found) = extract_conjunct(&left, target) {
895            return Some(found);
896        }
897        let right =
898            DerivationTree::new((**r).clone(), InferenceRule::ConjunctionElim, vec![conj.clone()]);
899        extract_conjunct(&right, target)
900    } else if let Ok(subst) = unify_exprs(target, &conj.conclusion) {
901        Some((conj.clone(), subst))
902    } else {
903        None
904    }
905}
906
907/// Prove `a ≤ b` by chaining the known `≤` facts. `adj` is the DIRECTED graph of
908/// hypothesis inequalities (`≤` is not symmetric); a path `a → … → b` folds into a
909/// left-nested `le_trans`, and `a = b` closes by `le_refl`. Every node certifies.
910fn cert_le_path(
911    a: &ProofTerm,
912    b: &ProofTerm,
913    adj: &std::collections::HashMap<String, Vec<(ProofTerm, DerivationTree)>>,
914) -> Option<DerivationTree> {
915    let a_key = term_skey(a)?;
916    let b_key = term_skey(b)?;
917    if a_key == b_key {
918        return Some(DerivationTree::leaf(
919            le_eq(a.clone(), a.clone()),
920            InferenceRule::LeRefl,
921        ));
922    }
923    let mut queue: std::collections::VecDeque<(String, Option<DerivationTree>)> =
924        std::collections::VecDeque::new();
925    queue.push_back((a_key.clone(), None));
926    let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
927    visited.insert(a_key);
928    while let Some((cur_key, acc)) = queue.pop_front() {
929        let Some(edges) = adj.get(&cur_key) else {
930            continue;
931        };
932        for (next_term, edge_tree) in edges {
933            let Some(next_key) = term_skey(next_term) else {
934                continue;
935            };
936            if visited.contains(&next_key) {
937                continue;
938            }
939            visited.insert(next_key.clone());
940            // `acc` proves `a ≤ cur`; `edge_tree` proves `cur ≤ next`.
941            let step = match &acc {
942                None => edge_tree.clone(), // cur == a: the edge already proves a ≤ next
943                Some(acc_proof) => DerivationTree::new(
944                    le_eq(a.clone(), next_term.clone()),
945                    InferenceRule::LeTrans,
946                    vec![acc_proof.clone(), edge_tree.clone()],
947                ),
948            };
949            if next_key == b_key {
950                return Some(step);
951            }
952            queue.push_back((next_key, Some(step)));
953        }
954    }
955    None
956}
957
958/// Detect a linear contradiction in the known set: a chain of `≤` facts proving
959/// `le(m, n) = true` for ground `m > n` (impossible — `le m n ⇝ false`). Returns a
960/// `⊥` derivation through the Bool no-confusion discriminator, so contradictory
961/// linear bounds (e.g. `x ≤ 3` with `5 ≤ x`) close any goal by ex falso.
962fn cert_linarith_close(known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
963    use std::collections::HashMap;
964    let mut adj: HashMap<String, Vec<(ProofTerm, DerivationTree)>> = HashMap::new();
965    let mut grounds: Vec<(i64, ProofTerm)> = Vec::new();
966    for (prop, tree) in known {
967        if let Some((x, y)) = as_le_pair(prop) {
968            if let Some(xk) = term_skey(&x) {
969                adj.entry(xk).or_default().push((y.clone(), tree.clone()));
970            }
971            for t in [&x, &y] {
972                if let Some(v) = as_int_literal(t) {
973                    grounds.push((v, t.clone()));
974                }
975            }
976        }
977    }
978    if adj.is_empty() {
979        return None;
980    }
981    for (mv, m) in &grounds {
982        for (nv, n) in &grounds {
983            if mv > nv {
984                if let Some(le_proof) = cert_le_path(m, n, &adj) {
985                    return Some(DerivationTree::new(
986                        ProofExpr::Atom("⊥".into()),
987                        InferenceRule::LinFalse,
988                        vec![le_proof],
989                    ));
990                }
991            }
992        }
993    }
994    None
995}
996
997/// General linear refutation (Farkas). Decide unsatisfiability of the `≤`
998/// hypotheses with Fourier-Motzkin ([`crate::linarith_solve::find_farkas`]), then
999/// RECONSTRUCT a kernel proof from the non-negative multipliers `λᵢ`: each `lᵢ ≤ rᵢ`
1000/// becomes `0 ≤ rᵢ - lᵢ` (`le_sub`), scaled by `λᵢ` (`le_mul_nonneg`), summed
1001/// (`le_add_mono`) into `le(BigL, BigR)`; the variables cancel, so `BigL` ring-reduces
1002/// to `0` and `BigR` to `-d` (`d > 0`), giving the ground-false `le(0, -d)` that the
1003/// discriminator turns into `⊥`. Handles arbitrary coefficients (where the chain
1004/// `cert_linarith_close` cannot).
1005pub(crate) fn cert_farkas(known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
1006    use crate::linarith_solve::{combine, find_farkas, parse_lin};
1007
1008    let mut le_hyps: Vec<(ProofTerm, ProofTerm, DerivationTree)> = Vec::new();
1009    let mut constraints = Vec::new();
1010    for (prop, tree) in known {
1011        if let Some((l, r)) = as_le_pair(prop) {
1012            if let (Some(ll), Some(rl)) = (parse_lin(&l), parse_lin(&r)) {
1013                constraints.push(ll.sub(&rl)); // l - r ≤ 0
1014                le_hyps.push((l, r, tree.clone()));
1015            }
1016        }
1017    }
1018    if le_hyps.is_empty() {
1019        return None;
1020    }
1021    let multipliers = find_farkas(&constraints)?;
1022    let combo = combine(&constraints, &multipliers);
1023    if !combo.is_const() || combo.constant <= 0 {
1024        return None;
1025    }
1026    let d = combo.constant; // Σ λᵢ(lᵢ - rᵢ) = d > 0
1027
1028    let pc = |n: i64| ProofTerm::Constant(n.to_string());
1029    let pf = |op: &str, x: ProofTerm, y: ProofTerm| ProofTerm::Function(op.to_string(), vec![x, y]);
1030
1031    // Per active hypothesis: 0 ≤ rᵢ - lᵢ (le_sub), scaled by λᵢ (le_mul_nonneg).
1032    let mut scaled: Vec<DerivationTree> = Vec::new();
1033    for (&i, &lam) in &multipliers {
1034        if lam <= 0 {
1035            continue;
1036        }
1037        let (l, r, hyp_tree) = &le_hyps[i];
1038        // r + (-1)·l  — the `le_sub` form (sub-free, ring-oracle friendly).
1039        let diff = pf("add", r.clone(), pf("mul", pc(-1), l.clone()));
1040        let sub_i = DerivationTree::new(
1041            le_eq(pc(0), diff.clone()),
1042            InferenceRule::LeSub,
1043            vec![hyp_tree.clone()],
1044        );
1045        let ground = DerivationTree::leaf(le_eq(pc(0), pc(lam)), InferenceRule::Reflexivity);
1046        scaled.push(DerivationTree::new(
1047            le_eq(pf("mul", pc(lam), pc(0)), pf("mul", pc(lam), diff)),
1048            InferenceRule::LeMulNonneg,
1049            vec![ground, sub_i],
1050        ));
1051    }
1052    // Sum them: le(BigL, BigR).
1053    let summed = scaled.into_iter().reduce(|acc, s| {
1054        let (al, ar) = as_le_pair(&acc.conclusion).expect("scaled is an le-fact");
1055        let (sl, sr) = as_le_pair(&s.conclusion).expect("scaled is an le-fact");
1056        DerivationTree::new(
1057            le_eq(pf("add", al, sl), pf("add", ar, sr)),
1058            InferenceRule::LeAddMono,
1059            vec![acc, s],
1060        )
1061    })?;
1062    let (big_l, big_r) = as_le_pair(&summed.conclusion)?;
1063
1064    // Ring-normalize: rewrite BigR → -d, then BigL → 0, giving the ground `le(0, -d)`.
1065    let rw1 = DerivationTree::new(
1066        le_eq(big_l.clone(), pc(-d)),
1067        InferenceRule::Rewrite { from: big_r.clone(), to: pc(-d) },
1068        vec![
1069            DerivationTree::leaf(
1070                ProofExpr::Identity(big_r, pc(-d)),
1071                InferenceRule::ArithDecision,
1072            ),
1073            summed,
1074        ],
1075    );
1076    let rw2 = DerivationTree::new(
1077        le_eq(pc(0), pc(-d)),
1078        InferenceRule::Rewrite { from: big_l.clone(), to: pc(0) },
1079        vec![
1080            DerivationTree::leaf(
1081                ProofExpr::Identity(big_l, pc(0)),
1082                InferenceRule::ArithDecision,
1083            ),
1084            rw1,
1085        ],
1086    );
1087    Some(DerivationTree::new(
1088        ProofExpr::Atom("⊥".into()),
1089        InferenceRule::LinFalse,
1090        vec![rw2],
1091    ))
1092}
1093
1094/// Close a `P` / `¬P` pair in the known set into a `Contradiction` (⊥) node.
1095fn cert_close(known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
1096    for (prop, neg_tree) in known {
1097        if let ProofExpr::Not(inner) = prop {
1098            for (other, pos_tree) in known {
1099                if exprs_structurally_equal(other, inner) {
1100                    return Some(DerivationTree::new(
1101                        ProofExpr::Atom("⊥".into()),
1102                        InferenceRule::Contradiction,
1103                        vec![pos_tree.clone(), neg_tree.clone()],
1104                    ));
1105                }
1106            }
1107        }
1108    }
1109    None
1110}
1111
1112/// The atomic sub-formulas of a (possibly conjunctive / negated) proposition.
1113fn cert_atoms_of(expr: &ProofExpr) -> Vec<ProofExpr> {
1114    match expr {
1115        ProofExpr::And(l, r) => {
1116            let mut v = cert_atoms_of(l);
1117            v.extend(cert_atoms_of(r));
1118            v
1119        }
1120        ProofExpr::Not(inner) => cert_atoms_of(inner),
1121        other => vec![other.clone()],
1122    }
1123}
1124
1125/// Whether a proposition mentions no free variables in its predicate arguments.
1126fn cert_is_ground(expr: &ProofExpr) -> bool {
1127    fn term_ground(t: &ProofTerm) -> bool {
1128        match t {
1129            ProofTerm::Variable(_) | ProofTerm::BoundVarRef(_) => false,
1130            ProofTerm::Function(_, args) | ProofTerm::Group(args) => args.iter().all(term_ground),
1131            ProofTerm::Constant(_) => true,
1132        }
1133    }
1134    match expr {
1135        ProofExpr::Predicate { args, .. } => args.iter().all(term_ground),
1136        ProofExpr::Not(inner) => cert_is_ground(inner),
1137        ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
1138            cert_is_ground(l) && cert_is_ground(r)
1139        }
1140        ProofExpr::Atom(_) => true,
1141        _ => false,
1142    }
1143}
1144
1145/// Collect constant names appearing in predicate arguments of known facts.
1146fn cert_collect_constants(known: &[(ProofExpr, DerivationTree)]) -> Vec<String> {
1147    fn from_term(t: &ProofTerm, out: &mut Vec<String>) {
1148        match t {
1149            ProofTerm::Constant(c) => {
1150                if !out.contains(c) {
1151                    out.push(c.clone());
1152                }
1153            }
1154            ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
1155                for a in args {
1156                    from_term(a, out);
1157                }
1158            }
1159            _ => {}
1160        }
1161    }
1162    fn from_expr(e: &ProofExpr, out: &mut Vec<String>) {
1163        match e {
1164            ProofExpr::Predicate { args, .. } => {
1165                for a in args {
1166                    from_term(a, out);
1167                }
1168            }
1169            ProofExpr::Not(i) => from_expr(i, out),
1170            ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
1171                from_expr(l, out);
1172                from_expr(r, out);
1173            }
1174            _ => {}
1175        }
1176    }
1177    let mut out = Vec::new();
1178    for (p, _) in known {
1179        from_expr(p, &mut out);
1180    }
1181    out
1182}
1183
1184/// Candidate substitutions under which a rule may fire: each binds the rule's
1185/// `∀`-variable to a constant drawn from a known fact that matches one of the
1186/// antecedent's atoms.
1187fn cert_rule_substs(rule: &CertRule, known: &[(ProofExpr, DerivationTree)]) -> Vec<Substitution> {
1188    match &rule.binder {
1189        None => vec![Substitution::new()],
1190        Some(v) => {
1191            let mut substs: Vec<Substitution> = Vec::new();
1192            for atom in cert_atoms_of(&rule.ante) {
1193                for (fact, _) in known {
1194                    if let Ok(s) = unify_exprs(fact, &atom) {
1195                        if s.contains_key(v) && !substs.iter().any(|e| e == &s) {
1196                            substs.push(s);
1197                        }
1198                    }
1199                }
1200            }
1201            substs
1202        }
1203    }
1204}
1205
1206/// Build a gapless proof of `ante` (under `subst`) from the known facts,
1207/// introducing `∧` via `ConjunctionIntro`. Returns `None` if any conjunct is
1208/// not derivable.
1209fn cert_prove_ante(
1210    ante: &ProofExpr,
1211    subst: &Substitution,
1212    known: &[(ProofExpr, DerivationTree)],
1213) -> Option<DerivationTree> {
1214    match ante {
1215        ProofExpr::And(l, r) => {
1216            let lt = cert_prove_ante(l, subst, known)?;
1217            let rt = cert_prove_ante(r, subst, known)?;
1218            let li = apply_subst_to_expr(l, subst);
1219            let ri = apply_subst_to_expr(r, subst);
1220            Some(DerivationTree::new(
1221                ProofExpr::And(Box::new(li), Box::new(ri)),
1222                InferenceRule::ConjunctionIntro,
1223                vec![lt, rt],
1224            ))
1225        }
1226        _ => {
1227            let goal = apply_subst_to_expr(ante, subst);
1228            known
1229                .iter()
1230                .find(|(p, _)| exprs_structurally_equal(p, &goal))
1231                .map(|(_, t)| t.clone())
1232        }
1233    }
1234}
1235
1236/// The negation of a proposition, collapsing double negation: `¬¬P ↦ P`.
1237fn cert_neg(e: &ProofExpr) -> ProofExpr {
1238    match e {
1239        ProofExpr::Not(inner) => (**inner).clone(),
1240        _ => ProofExpr::Not(Box::new(e.clone())),
1241    }
1242}
1243
1244/// A proof of `target` from the known set, by exact structural match.
1245fn cert_lookup(
1246    known: &[(ProofExpr, DerivationTree)],
1247    target: &ProofExpr,
1248) -> Option<DerivationTree> {
1249    known
1250        .iter()
1251        .find(|(p, _)| exprs_structurally_equal(p, target))
1252        .map(|(_, t)| t.clone())
1253}
1254
1255/// The proof of the implication `rule.ante → cons_inst` at this instance — a bare
1256/// `PremiseMatch` for a ground rule, or `UniversalInst` of the source `∀` rule.
1257fn cert_rule_impl_tree(
1258    rule: &CertRule,
1259    subst: &Substitution,
1260    cons_inst: &ProofExpr,
1261) -> Option<DerivationTree> {
1262    match &rule.binder {
1263        None => Some(DerivationTree::leaf(
1264            rule.source.clone(),
1265            InferenceRule::PremiseMatch,
1266        )),
1267        Some(v) => {
1268            let witness = match subst.get(v) {
1269                Some(ProofTerm::Constant(c)) | Some(ProofTerm::Variable(c)) => c.clone(),
1270                _ => return None,
1271            };
1272            let ante_inst = apply_subst_to_expr(&rule.ante, subst);
1273            Some(DerivationTree::new(
1274                ProofExpr::Implies(Box::new(ante_inst), Box::new(cons_inst.clone())),
1275                InferenceRule::UniversalInst(witness),
1276                vec![DerivationTree::leaf(
1277                    rule.source.clone(),
1278                    InferenceRule::PremiseMatch,
1279                )],
1280            ))
1281        }
1282    }
1283}
1284
1285/// n-ary DISJUNCTIVE SYLLOGISM (the heart of unit propagation): peel every refuted
1286/// disjunct off a known disjunction by certified `DisjunctionElim`, returning the
1287/// surviving sub-formula (a single literal once all but one is refuted). `None` if no
1288/// disjunct is refuted (no progress).
1289/// Prove `¬d` when the disjunct `d` is REFUTED by the known set — not only when
1290/// `¬d` is itself known, but when a CONJUNCT of `d` is false (so the whole
1291/// conjunction is), or `d` is a negation of a known fact. This is what lets
1292/// disjunctive syllogism collapse an of-pair clue: its disjuncts are conjunctions
1293/// (`Hunt(x) ∧ In(y,2004) ∧ …`), and a single false conjunct kills the disjunct.
1294/// Every returned tree is certified (`ConjunctionElim` / `Contradiction` / reductio).
1295fn cert_refute(d: &ProofExpr, known: &[(ProofExpr, DerivationTree)]) -> Option<DerivationTree> {
1296    // Directly known negation.
1297    if let Some(t) = cert_lookup(known, &cert_neg(d)) {
1298        return Some(t);
1299    }
1300    match d {
1301        // A conjunction is refuted if either conjunct is — assume the conjunction,
1302        // ∧-eliminate the false conjunct, contradict its refutation, reductio.
1303        ProofExpr::And(l, r) => {
1304            for (side, _other_first) in [(l.as_ref(), true), (r.as_ref(), false)] {
1305                if let Some(neg_side) = cert_refute(side, known) {
1306                    let assume = DerivationTree::leaf(d.clone(), InferenceRule::PremiseMatch);
1307                    let elim = DerivationTree::new(
1308                        side.clone(),
1309                        InferenceRule::ConjunctionElim,
1310                        vec![assume.clone()],
1311                    );
1312                    let contra = DerivationTree::new(
1313                        ProofExpr::Atom("⊥".into()),
1314                        InferenceRule::Contradiction,
1315                        vec![elim, neg_side],
1316                    );
1317                    return Some(DerivationTree::new(
1318                        cert_neg(d),
1319                        InferenceRule::ReductioAdAbsurdum,
1320                        vec![assume, contra],
1321                    ));
1322                }
1323            }
1324            None
1325        }
1326        // `¬x` is refuted when `x` is known — assume `¬x`, contradict `x`, reductio.
1327        ProofExpr::Not(x) => {
1328            let x_tree = cert_lookup(known, x)?;
1329            let assume = DerivationTree::leaf(d.clone(), InferenceRule::PremiseMatch);
1330            let contra = DerivationTree::new(
1331                ProofExpr::Atom("⊥".into()),
1332                InferenceRule::Contradiction,
1333                vec![x_tree, assume.clone()],
1334            );
1335            Some(DerivationTree::new(
1336                cert_neg(d),
1337                InferenceRule::ReductioAdAbsurdum,
1338                vec![assume, contra],
1339            ))
1340        }
1341        _ => None,
1342    }
1343}
1344
1345fn cert_peel_disjunction(
1346    or_tree: &DerivationTree,
1347    known: &[(ProofExpr, DerivationTree)],
1348) -> Option<(ProofExpr, DerivationTree)> {
1349    let ProofExpr::Or(l, r) = &or_tree.conclusion else {
1350        return None;
1351    };
1352    let (l, r) = ((**l).clone(), (**r).clone());
1353    if let Some(neg_l) = cert_refute(&l, known) {
1354        let elim = DerivationTree::new(
1355            r.clone(),
1356            InferenceRule::DisjunctionElim,
1357            vec![or_tree.clone(), neg_l],
1358        );
1359        return Some(cert_peel_disjunction(&elim, known).unwrap_or((r, elim)));
1360    }
1361    if let Some(neg_r) = cert_refute(&r, known) {
1362        let elim = DerivationTree::new(
1363            l.clone(),
1364            InferenceRule::DisjunctionElim,
1365            vec![or_tree.clone(), neg_r],
1366        );
1367        return Some(cert_peel_disjunction(&elim, known).unwrap_or((l, elim)));
1368    }
1369    None
1370}
1371
1372/// Forward UNIT PROPAGATION to a fixpoint — the DPLL inner loop, every step a
1373/// certified inference. Four rules drive a grounded logic grid without any
1374/// case-splitting:
1375///   • **ModusPonens** — a rule whose antecedent is satisfied fires its consequent.
1376///   • **ModusTollens** — a rule whose consequent is refuted refutes its antecedent
1377///     (via reductio: assume the antecedent, derive the consequent, contradict).
1378///   • **Disjunctive syllogism** — a disjunction with all-but-one disjunct refuted
1379///     forces the survivor ([`cert_peel_disjunction`]). This collapses domain
1380///     closures (`In(t,A) ∨ … ∨ In(t,D)`) as the row fills, with NO branching.
1381///   • **Conjunction-negation** — `¬(A ∧ B)` with `A` established refutes `B` (via
1382///     reductio). This is how "exactly one in Florida" (an at-most-one rule, refuted
1383///     by `ModusTollens`) excludes Florida from every other row.
1384/// Together these are the propagation a logic-grid solver runs; case analysis
1385/// ([`cert_derive_falsum`]) is only the rare residual decision.
1386fn cert_saturate(
1387    rules: &[CertRule],
1388    mut known: Vec<(ProofExpr, DerivationTree)>,
1389) -> Vec<(ProofExpr, DerivationTree)> {
1390    let max_rounds = 64;
1391    let mut push_new =
1392        |known: &mut Vec<(ProofExpr, DerivationTree)>, fact: ProofExpr, tree: DerivationTree| -> bool {
1393            if known.iter().any(|(p, _)| exprs_structurally_equal(p, &fact)) {
1394                false
1395            } else {
1396                known.push((fact, tree));
1397                true
1398            }
1399        };
1400    for _ in 0..max_rounds {
1401        let snapshot = known.clone();
1402        let mut added = false;
1403
1404        // (1) ModusPonens — fire a rule whose antecedent holds.
1405        for rule in rules {
1406            for subst in cert_rule_substs(rule, &snapshot) {
1407                let cons_inst = apply_subst_to_expr(&rule.cons, &subst);
1408                if known.iter().any(|(p, _)| exprs_structurally_equal(p, &cons_inst)) {
1409                    continue;
1410                }
1411                let Some(ante_proof) = cert_prove_ante(&rule.ante, &subst, &snapshot) else {
1412                    continue;
1413                };
1414                let Some(impl_tree) = cert_rule_impl_tree(rule, &subst, &cons_inst) else {
1415                    continue;
1416                };
1417                let mp = DerivationTree::new(
1418                    cons_inst.clone(),
1419                    InferenceRule::ModusPonens,
1420                    vec![impl_tree, ante_proof],
1421                );
1422                added |= push_new(&mut known, cons_inst, mp);
1423            }
1424        }
1425
1426        // (2) ModusTollens — a ground rule whose consequent is refuted refutes its
1427        // antecedent. Built from certified primitives: assume the antecedent, derive
1428        // the consequent by ModusPonens, contradict the known negation, reductio.
1429        for rule in rules {
1430            if rule.binder.is_some() {
1431                continue;
1432            }
1433            let neg_cons = cert_neg(&rule.cons);
1434            let Some(nc_tree) = cert_lookup(&snapshot, &neg_cons) else {
1435                continue;
1436            };
1437            let neg_ante = cert_neg(&rule.ante);
1438            if cert_lookup(&known, &neg_ante).is_some() {
1439                continue;
1440            }
1441            let assume = DerivationTree::leaf(rule.ante.clone(), InferenceRule::PremiseMatch);
1442            let rule_tree =
1443                DerivationTree::leaf(rule.source.clone(), InferenceRule::PremiseMatch);
1444            let mp = DerivationTree::new(
1445                rule.cons.clone(),
1446                InferenceRule::ModusPonens,
1447                vec![rule_tree, assume.clone()],
1448            );
1449            let contra = DerivationTree::new(
1450                ProofExpr::Atom("⊥".into()),
1451                InferenceRule::Contradiction,
1452                vec![mp, nc_tree],
1453            );
1454            let tree = DerivationTree::new(
1455                neg_ante.clone(),
1456                InferenceRule::ReductioAdAbsurdum,
1457                vec![assume, contra],
1458            );
1459            added |= push_new(&mut known, neg_ante, tree);
1460        }
1461
1462        // (3) Disjunctive syllogism — peel refuted disjuncts off known disjunctions.
1463        for (p, or_tree) in &snapshot {
1464            if !matches!(p, ProofExpr::Or(..)) {
1465                continue;
1466            }
1467            if let Some((lit, tree)) = cert_peel_disjunction(or_tree, &snapshot) {
1468                added |= push_new(&mut known, lit, tree);
1469            }
1470        }
1471
1472        // (4) Conjunction-negation — `¬(A ∧ B)` with `A` established refutes `B`
1473        // (and symmetrically). Built by reductio: assume B, ∧-introduce A∧B,
1474        // contradict ¬(A∧B).
1475        for (p, neg_tree) in &snapshot {
1476            let ProofExpr::Not(inner) = p else { continue };
1477            let ProofExpr::And(a, b) = inner.as_ref() else {
1478                continue;
1479            };
1480            for (known_is_left, known_side, other) in
1481                [(true, a.as_ref(), b.as_ref()), (false, b.as_ref(), a.as_ref())]
1482            {
1483                let Some(side_proof) = cert_prove_ante(known_side, &Substitution::new(), &snapshot)
1484                else {
1485                    continue;
1486                };
1487                let neg_other = cert_neg(other);
1488                if cert_lookup(&known, &neg_other).is_some() {
1489                    continue;
1490                }
1491                let assume = DerivationTree::leaf(other.clone(), InferenceRule::PremiseMatch);
1492                let (l_proof, r_proof) = if known_is_left {
1493                    (side_proof.clone(), assume.clone())
1494                } else {
1495                    (assume.clone(), side_proof.clone())
1496                };
1497                let conj_intro = DerivationTree::new(
1498                    (**inner).clone(),
1499                    InferenceRule::ConjunctionIntro,
1500                    vec![l_proof, r_proof],
1501                );
1502                let contra = DerivationTree::new(
1503                    ProofExpr::Atom("⊥".into()),
1504                    InferenceRule::Contradiction,
1505                    vec![conj_intro, neg_tree.clone()],
1506                );
1507                let tree = DerivationTree::new(
1508                    neg_other.clone(),
1509                    InferenceRule::ReductioAdAbsurdum,
1510                    vec![assume, contra],
1511                );
1512                added |= push_new(&mut known, neg_other, tree);
1513            }
1514        }
1515
1516        // (5) Conjunction decomposition — a known `A ∧ B` yields `A` and `B`
1517        // (`ConjunctionElim`). When disjunctive syllogism leaves one surviving
1518        // of-pair disjunct (a conjunction), this surfaces its inner either/or as a
1519        // top-level disjunction the case-split can then decide.
1520        for (p, and_tree) in &snapshot {
1521            let ProofExpr::And(a, b) = p else { continue };
1522            for side in [a.as_ref(), b.as_ref()] {
1523                let elim = DerivationTree::new(
1524                    side.clone(),
1525                    InferenceRule::ConjunctionElim,
1526                    vec![and_tree.clone()],
1527                );
1528                added |= push_new(&mut known, side.clone(), elim);
1529            }
1530        }
1531
1532        if !added {
1533            break;
1534        }
1535    }
1536    known
1537}
1538
1539/// Self-referential pivots to case-split on: rule consequent/antecedent atoms
1540/// instantiated at known constants (ground, with negations stripped).
1541fn cert_candidates(known: &[(ProofExpr, DerivationTree)], rules: &[CertRule]) -> Vec<ProofExpr> {
1542    let consts = cert_collect_constants(known);
1543    let mut cands: Vec<ProofExpr> = Vec::new();
1544    let mut push = |c: ProofExpr, cands: &mut Vec<ProofExpr>| {
1545        if cert_is_ground(&c) && !cands.iter().any(|e| exprs_structurally_equal(e, &c)) {
1546            cands.push(c);
1547        }
1548    };
1549    for rule in rules {
1550        let atoms: Vec<ProofExpr> = cert_atoms_of(&rule.cons)
1551            .into_iter()
1552            .chain(cert_atoms_of(&rule.ante))
1553            .collect();
1554        match &rule.binder {
1555            None => {
1556                for a in atoms {
1557                    push(a, &mut cands);
1558                }
1559            }
1560            Some(v) => {
1561                for k in &consts {
1562                    let mut s = Substitution::new();
1563                    s.insert(v.clone(), ProofTerm::Constant(k.clone()));
1564                    for a in &atoms {
1565                        push(apply_subst_to_expr(a, &s), &mut cands);
1566                    }
1567                }
1568            }
1569        }
1570    }
1571    cands
1572}
1573
1574/// Derive ⊥ from `seed` facts: saturate, then (bounded) case-split. Every node
1575/// of the returned tree is certifiable.
1576fn cert_derive_falsum(
1577    rules: &[CertRule],
1578    seed: &[(ProofExpr, DerivationTree)],
1579    depth: usize,
1580    fresh: &mut u32,
1581) -> Option<DerivationTree> {
1582    // First: eliminate an existential premise by introducing a fresh witness.
1583    // `∃x.P(x)` becomes `P(c)` for a fresh constant `c`, and the whole ⊥
1584    // derivation is wrapped in `ExistentialElim` (a `Match` on `Ex`).
1585    if let Some((idx, exist_tree)) = seed.iter().enumerate().find_map(|(i, (p, t))| {
1586        if matches!(p, ProofExpr::Exists { .. }) {
1587            Some((i, t.clone()))
1588        } else {
1589            None
1590        }
1591    }) {
1592        let (variable, body) = match &seed[idx].0 {
1593            ProofExpr::Exists { variable, body } => (variable.clone(), body.as_ref().clone()),
1594            _ => unreachable!(),
1595        };
1596        let c = format!("__sk{}", *fresh);
1597        *fresh += 1;
1598        let mut subst = Substitution::new();
1599        subst.insert(variable, ProofTerm::Constant(c.clone()));
1600        let p_c = apply_subst_to_expr(&body, &subst);
1601
1602        let mut seed2: Vec<(ProofExpr, DerivationTree)> = seed
1603            .iter()
1604            .enumerate()
1605            .filter(|(j, _)| *j != idx)
1606            .map(|(_, x)| x.clone())
1607            .collect();
1608        seed2.push((
1609            p_c.clone(),
1610            DerivationTree::leaf(p_c, InferenceRule::PremiseMatch),
1611        ));
1612
1613        return cert_derive_falsum(rules, &seed2, depth, fresh).map(|body_proof| {
1614            DerivationTree::new(
1615                ProofExpr::Atom("⊥".into()),
1616                InferenceRule::ExistentialElim { witness: c },
1617                vec![exist_tree, body_proof],
1618            )
1619        });
1620    }
1621
1622    let known = cert_saturate(rules, seed.to_vec());
1623    if let Some(t) = cert_close(&known) {
1624        return Some(t);
1625    }
1626    if let Some(t) = cert_equality_close(&known) {
1627        return Some(t);
1628    }
1629    if let Some(t) = cert_linarith_close(&known) {
1630        return Some(t);
1631    }
1632    if let Some(t) = cert_farkas(&known) {
1633        return Some(t);
1634    }
1635    // Integer discreteness: tighten strict `<` hypotheses to `a+1 ≤ b` and retry
1636    // the Farkas refutation — catches the strict contradictions rational
1637    // Fourier-Motzkin reports satisfiable (`x < y ∧ y < x+1`).
1638    if let Some(t) = crate::omega_solve::omega_close(&known) {
1639        return Some(t);
1640    }
1641    if depth == 0 {
1642        return None;
1643    }
1644    // Case-split on a disjunction — the DPLL *decision*, taken only after unit
1645    // propagation (`cert_saturate`) is exhausted. From `A ∨ B` derive ⊥ in BOTH the
1646    // (+A) and (+B) branches; each branch assumes its disjunct's conjuncts, which the
1647    // `DisjunctionCases` certifier binds as local hypotheses.
1648    //
1649    // PRODUCTIVITY GATE (not a structural cap): split a disjunction only when assuming
1650    // SOME disjunct IMMEDIATELY closes — saturates to ⊥ at depth 0, with no further
1651    // splitting. That is the signature of a genuine case decision (an of-pair /
1652    // either-or clue, where one arm is already refuted by what's known). A domain
1653    // CLOSURE has no such disjunct — a row may legitimately sit in any value — so it is
1654    // only ever propagated, never split. This is what stops the search from exploding
1655    // over a grid's many closures while still deciding every real clue branch.
1656    let known_has = |x: &ProofExpr, k: &[(ProofExpr, DerivationTree)]| {
1657        k.iter().any(|(q, _)| exprs_structurally_equal(q, x))
1658    };
1659    let seed_with = |d: &ProofExpr| -> Vec<(ProofExpr, DerivationTree)> {
1660        let mut s = seed.to_vec();
1661        for conj in flatten_conjuncts(d) {
1662            if !s.iter().any(|(q, _)| exprs_structurally_equal(q, &conj)) {
1663                s.push((conj.clone(), DerivationTree::leaf(conj, InferenceRule::PremiseMatch)));
1664            }
1665        }
1666        s
1667    };
1668    let established = |x: &ProofExpr, k: &[(ProofExpr, DerivationTree)]| {
1669        flatten_conjuncts(x).iter().all(|c| known_has(c, k))
1670    };
1671    // DECISION (DPLL): GUESS one undetermined disjunction and require BOTH branches to
1672    // close. Strong in-saturation propagation (disjunctive syllogism, at-most-one
1673    // exclusion, conjunction-negation) prunes each branch; a forced disjunction closes
1674    // one branch immediately. ONE decision per level keeps the tree finite-domain
1675    // bounded. By DPLL completeness, if premises + ¬goal are unsatisfiable then any
1676    // decision's branches both close; if this one's do not, the set is satisfiable and
1677    // the goal is not entailed (so don't fall through to the atom split).
1678    if let Some((p, tree_or)) = known.iter().find_map(|(p, t)| match p {
1679        ProofExpr::Or(a, b) if !(established(a, &known) || established(b, &known)) => {
1680            Some((p.clone(), t.clone()))
1681        }
1682        _ => None,
1683    }) {
1684        let ProofExpr::Or(a, b) = &p else { unreachable!() };
1685        if let (Some(pa), Some(pb)) = (
1686            cert_derive_falsum(rules, &seed_with(a), depth - 1, fresh),
1687            cert_derive_falsum(rules, &seed_with(b), depth - 1, fresh),
1688        ) {
1689            return Some(DerivationTree::new(
1690                ProofExpr::Atom("⊥".into()),
1691                InferenceRule::DisjunctionCases,
1692                vec![tree_or, pa, pb],
1693            ));
1694        }
1695        // A disjunction was available to branch on; by DPLL completeness its failure
1696        // to close means the set is satisfiable. Don't fall through to the atom split
1697        // (that is for disjunction-free paradoxes and would re-explore the grid).
1698        return None;
1699    }
1700    for c in cert_candidates(&known, rules) {
1701        let not_c = ProofExpr::Not(Box::new(c.clone()));
1702        // Skip pivots already settled — splitting on them cannot help.
1703        if known
1704            .iter()
1705            .any(|(p, _)| exprs_structurally_equal(p, &c) || exprs_structurally_equal(p, &not_c))
1706        {
1707            continue;
1708        }
1709        let mut seed_pos = seed.to_vec();
1710        seed_pos.push((c.clone(), DerivationTree::leaf(c.clone(), InferenceRule::PremiseMatch)));
1711        let mut seed_neg = seed.to_vec();
1712        seed_neg.push((
1713            not_c.clone(),
1714            DerivationTree::leaf(not_c.clone(), InferenceRule::PremiseMatch),
1715        ));
1716        if let (Some(p), Some(n)) = (
1717            cert_derive_falsum(rules, &seed_pos, depth - 1, fresh),
1718            cert_derive_falsum(rules, &seed_neg, depth - 1, fresh),
1719        ) {
1720            return Some(DerivationTree::new(
1721                ProofExpr::Atom("⊥".into()),
1722                InferenceRule::CaseAnalysis {
1723                    case_formula: Box::new(c.clone()),
1724                },
1725                vec![p, n],
1726            ));
1727        }
1728    }
1729    None
1730}
1731
1732/// Check if two expressions are structurally equal.
1733///
1734/// This is syntactic equality after normalization - no unification needed.
1735fn exprs_structurally_equal(left: &ProofExpr, right: &ProofExpr) -> bool {
1736    match (left, right) {
1737        (ProofExpr::Atom(a), ProofExpr::Atom(b)) => a == b,
1738
1739        (ProofExpr::Ctor { name: n1, args: a1 }, ProofExpr::Ctor { name: n2, args: a2 }) => {
1740            n1 == n2 && a1.len() == a2.len() && a1.iter().zip(a2).all(|(x, y)| exprs_structurally_equal(x, y))
1741        }
1742
1743        (
1744            ProofExpr::Predicate { name: n1, args: a1, .. },
1745            ProofExpr::Predicate { name: n2, args: a2, .. },
1746        ) => n1 == n2 && a1.len() == a2.len() && a1.iter().zip(a2).all(|(x, y)| terms_structurally_equal(x, y)),
1747
1748        (ProofExpr::Identity(l1, r1), ProofExpr::Identity(l2, r2)) => {
1749            terms_structurally_equal(l1, l2) && terms_structurally_equal(r1, r2)
1750        }
1751
1752        (ProofExpr::And(l1, r1), ProofExpr::And(l2, r2))
1753        | (ProofExpr::Or(l1, r1), ProofExpr::Or(l2, r2))
1754        | (ProofExpr::Implies(l1, r1), ProofExpr::Implies(l2, r2))
1755        | (ProofExpr::Iff(l1, r1), ProofExpr::Iff(l2, r2)) => {
1756            exprs_structurally_equal(l1, l2) && exprs_structurally_equal(r1, r2)
1757        }
1758
1759        (ProofExpr::Not(a), ProofExpr::Not(b)) => exprs_structurally_equal(a, b),
1760
1761        (
1762            ProofExpr::ForAll { variable: v1, body: b1 },
1763            ProofExpr::ForAll { variable: v2, body: b2 },
1764        )
1765        | (
1766            ProofExpr::Exists { variable: v1, body: b1 },
1767            ProofExpr::Exists { variable: v2, body: b2 },
1768        ) => v1 == v2 && exprs_structurally_equal(b1, b2),
1769
1770        (
1771            ProofExpr::Lambda { variable: v1, body: b1 },
1772            ProofExpr::Lambda { variable: v2, body: b2 },
1773        ) => v1 == v2 && exprs_structurally_equal(b1, b2),
1774
1775        (ProofExpr::App(f1, a1), ProofExpr::App(f2, a2)) => {
1776            exprs_structurally_equal(f1, f2) && exprs_structurally_equal(a1, a2)
1777        }
1778
1779        (
1780            ProofExpr::TypedVar { name: n1, typename: t1 },
1781            ProofExpr::TypedVar { name: n2, typename: t2 },
1782        ) => n1 == n2 && t1 == t2,
1783
1784        (
1785            ProofExpr::Fixpoint { name: n1, body: b1 },
1786            ProofExpr::Fixpoint { name: n2, body: b2 },
1787        ) => n1 == n2 && exprs_structurally_equal(b1, b2),
1788
1789        _ => false,
1790    }
1791}
1792
1793/// Check if two terms are structurally equal.
1794fn terms_structurally_equal(left: &ProofTerm, right: &ProofTerm) -> bool {
1795    match (left, right) {
1796        (ProofTerm::Constant(a), ProofTerm::Constant(b)) => a == b,
1797        (ProofTerm::Variable(a), ProofTerm::Variable(b)) => a == b,
1798        (ProofTerm::BoundVarRef(a), ProofTerm::BoundVarRef(b)) => a == b,
1799        (ProofTerm::Function(n1, a1), ProofTerm::Function(n2, a2)) => {
1800            n1 == n2 && a1.len() == a2.len() && a1.iter().zip(a2).all(|(x, y)| terms_structurally_equal(x, y))
1801        }
1802        (ProofTerm::Group(a1), ProofTerm::Group(a2)) => {
1803            a1.len() == a2.len() && a1.iter().zip(a2).all(|(x, y)| terms_structurally_equal(x, y))
1804        }
1805        _ => false,
1806    }
1807}
1808
1809impl BackwardChainer {
1810    /// Create a new proof engine with empty knowledge base.
1811    pub fn new() -> Self {
1812        Self {
1813            knowledge_base: Vec::new(),
1814            max_depth: DEFAULT_MAX_DEPTH,
1815            var_counter: 0,
1816            eliminated_existentials: Vec::new(),
1817            active_goals: Vec::new(),
1818            steps: 0,
1819            step_budget: DEFAULT_STEP_BUDGET,
1820        }
1821    }
1822
1823    /// Set the maximum proof search depth.
1824    pub fn set_max_depth(&mut self, depth: usize) {
1825        self.max_depth = depth;
1826    }
1827
1828    /// Set the node budget — the number of search nodes allowed for one top-level proof
1829    /// before the search aborts. See [`DEFAULT_STEP_BUDGET`].
1830    pub fn set_step_budget(&mut self, budget: usize) {
1831        self.step_budget = budget;
1832    }
1833
1834    /// Charge one node against the budget; abort the search once it is spent. Called at
1835    /// EVERY recursion hub — `prove_goal` AND the `solve_subgoals`/`prove_rule_antecedent`
1836    /// mutual recursion, which does NOT pass through `prove_goal` and so would otherwise
1837    /// run unbounded behind a recursive axiom. The depth bound caps recursion depth; this
1838    /// caps total work, so the search always terminates in bounded time.
1839    fn charge_budget(&mut self) -> ProofResult<()> {
1840        self.steps += 1;
1841        if self.steps > self.step_budget {
1842            Err(ProofError::DepthExceeded)
1843        } else {
1844            Ok(())
1845        }
1846    }
1847
1848    /// Get a reference to the knowledge base (for debugging).
1849    pub fn knowledge_base(&self) -> &[ProofExpr] {
1850        &self.knowledge_base
1851    }
1852
1853    /// Add an axiom/fact/rule to the knowledge base.
1854    ///
1855    /// Event semantics are automatically abstracted to simple predicates for efficient proof search.
1856    pub fn add_axiom(&mut self, expr: ProofExpr) {
1857        // Pre-process: abstract event semantics to simple predicates
1858        let abstracted = self.abstract_all_events(&expr);
1859        // Simplify definite description conjunctions (e.g., butler(butler) ∧ P → P)
1860        let simplified = self.simplify_definite_description_conjunction(&abstracted);
1861        self.knowledge_base.push(simplified);
1862    }
1863
1864    /// Attempt to prove a goal.
1865    ///
1866    /// Returns a derivation tree if successful, explaining how the proof was constructed.
1867    /// Event semantics in the goal are automatically abstracted (but De Morgan is not applied
1868    /// to preserve goal pattern matching for reductio strategies).
1869    pub fn prove(&mut self, goal: ProofExpr) -> ProofResult<DerivationTree> {
1870        // Pre-process: unify definite descriptions across all axioms
1871        // This handles Russell's theory of definite descriptions, where multiple
1872        // "the X" references should refer to the same entity.
1873        self.unify_definite_descriptions();
1874
1875        // Pre-process: abstract event semantics in the goal
1876        // Use abstract_events_only which doesn't apply De Morgan (to preserve ¬∃ pattern)
1877        let abstracted_goal = self.abstract_events_only(&goal);
1878        // Simplify definite description conjunctions
1879        let normalized_goal = self.simplify_definite_description_conjunction(&abstracted_goal);
1880        // The whole prove→certify pipeline runs on a large-stack thread (see
1881        // `verify::on_big_stack`), so the recursive search here needs no thread of its
1882        // own — the native-stack ceiling is already lifted for legitimately deep proofs.
1883        self.prove_goal(ProofGoal::new(normalized_goal), 0)
1884    }
1885
1886    /// Prove a goal with pre-populated context assumptions.
1887    ///
1888    /// This allows proving goals like "x > 5" given assumptions like "x > 10" in the context.
1889    /// The oracle (Z3) will use these context assumptions when verifying.
1890    pub fn prove_with_goal(&mut self, goal: ProofGoal) -> ProofResult<DerivationTree> {
1891        self.unify_definite_descriptions();
1892
1893        // Normalize the target
1894        let abstracted_target = self.abstract_events_only(&goal.target);
1895        let normalized_target = self.simplify_definite_description_conjunction(&abstracted_target);
1896
1897        // Normalize each context assumption
1898        let normalized_context: Vec<ProofExpr> = goal
1899            .context
1900            .iter()
1901            .map(|expr| {
1902                let abstracted = self.abstract_events_only(expr);
1903                self.simplify_definite_description_conjunction(&abstracted)
1904            })
1905            .collect();
1906
1907        let normalized_goal = ProofGoal::with_context(normalized_target, normalized_context);
1908        self.prove_goal(normalized_goal, 0)
1909    }
1910
1911    /// Unify definite descriptions across axioms.
1912    ///
1913    /// When multiple axioms contain the same definite description pattern
1914    /// (e.g., "the barber" creates `∃x ((barber(x) ∧ ∀y (barber(y) → y=x)) ∧ P(x))`),
1915    /// this function:
1916    /// 1. Identifies all axioms with the same defining predicate
1917    /// 2. Extracts the properties attributed to the definite description
1918    /// 3. Replaces them with a unified Skolem constant and extracted properties
1919    fn unify_definite_descriptions(&mut self) {
1920        // Collect definite descriptions by their defining predicate
1921        let mut definite_descs: std::collections::HashMap<String, Vec<(usize, String, ProofExpr)>> = std::collections::HashMap::new();
1922
1923        for (idx, axiom) in self.knowledge_base.iter().enumerate() {
1924            if let Some((pred_name, var_name, property)) = self.extract_definite_description(axiom) {
1925                definite_descs.entry(pred_name).or_default().push((idx, var_name, property));
1926            }
1927        }
1928
1929        // Discourse-telescoped premises reference a description's variable
1930        // FREE ("The barber shaves..." after "The barber is a man." emits
1931        // shave(x, z) with x anaphoric). Those occurrences must be bound to
1932        // the same unified constant, or the telescoped axioms are inert.
1933        let mut anaphor_bindings: Vec<(String, ProofTerm)> = Vec::new();
1934
1935        // For each group of definite descriptions with the same predicate
1936        for (pred_name, descs) in definite_descs {
1937            if descs.is_empty() {
1938                continue;
1939            }
1940
1941            // Create a unified Skolem constant for this definite description
1942            let skolem_name = format!("the_{}", pred_name);
1943            let skolem_const = ProofTerm::Constant(skolem_name.clone());
1944
1945            // Add the defining property: pred(skolem)
1946            let defining_fact = ProofExpr::Predicate {
1947                name: pred_name.clone(),
1948                args: vec![skolem_const.clone()],
1949                world: None,
1950            };
1951            self.knowledge_base.push(defining_fact);
1952
1953            // CRITICAL: Add uniqueness constraint: ∀y (pred(y) → y = skolem)
1954            // This is essential for proofs that assume ∃x pred(x) - they need to
1955            // unify their Skolem constant with our unified constant.
1956            let uniqueness = ProofExpr::ForAll {
1957                variable: "_u".to_string(),
1958                body: Box::new(ProofExpr::Implies(
1959                    Box::new(ProofExpr::Predicate {
1960                        name: pred_name.clone(),
1961                        args: vec![ProofTerm::Variable("_u".to_string())],
1962                        world: None,
1963                    }),
1964                    Box::new(ProofExpr::Identity(
1965                        ProofTerm::Variable("_u".to_string()),
1966                        skolem_const.clone(),
1967                    )),
1968                )),
1969            };
1970            self.knowledge_base.push(uniqueness);
1971
1972            // Replace axioms with the extracted properties
1973            let mut indices_to_remove: Vec<usize> = Vec::new();
1974            for (idx, var_name, property) in descs {
1975                anaphor_bindings.push((var_name.clone(), skolem_const.clone()));
1976                // Substitute the original variable with the Skolem constant
1977                let substituted = self.substitute_term_in_expr(
1978                    &property,
1979                    &ProofTerm::Variable(var_name),
1980                    &skolem_const,
1981                );
1982                // Normalize the property (especially for ∀x ¬(P ∧ Q) → ∀x (P → ¬Q))
1983                let normalized = self.normalize_for_proof(&substituted);
1984                self.knowledge_base.push(normalized);
1985                indices_to_remove.push(idx);
1986            }
1987
1988            // Remove the original existential axioms (in reverse order to preserve indices)
1989            indices_to_remove.sort_unstable_by(|a, b| b.cmp(a));
1990            for idx in indices_to_remove {
1991                self.knowledge_base.remove(idx);
1992            }
1993        }
1994
1995        // Bind free anaphoric occurrences of each description's variable
1996        // throughout the knowledge base (free occurrences only — binders that
1997        // shadow the name keep their bodies untouched).
1998        if !anaphor_bindings.is_empty() {
1999            let kb = std::mem::take(&mut self.knowledge_base);
2000            self.knowledge_base = kb
2001                .into_iter()
2002                .map(|axiom| {
2003                    let mut bound = axiom;
2004                    for (var_name, skolem) in &anaphor_bindings {
2005                        bound = self.substitute_free_var_in_expr(&bound, var_name, skolem);
2006                    }
2007                    bound
2008                })
2009                .collect();
2010        }
2011    }
2012
2013    /// Normalize an expression for proof search.
2014    ///
2015    /// Applies transformations like: ∀x ¬(P ∧ Q) → ∀x (P → ¬Q)
2016    fn normalize_for_proof(&self, expr: &ProofExpr) -> ProofExpr {
2017        match expr {
2018            ProofExpr::ForAll { variable, body } => {
2019                // Check for pattern: ∀x ¬(P ∧ Q) → ∀x (P → ¬Q)
2020                if let ProofExpr::Not(inner) = body.as_ref() {
2021                    if let ProofExpr::And(left, right) = inner.as_ref() {
2022                        return ProofExpr::ForAll {
2023                            variable: variable.clone(),
2024                            body: Box::new(ProofExpr::Implies(
2025                                Box::new(self.normalize_for_proof(left)),
2026                                Box::new(ProofExpr::Not(Box::new(self.normalize_for_proof(right)))),
2027                            )),
2028                        };
2029                    }
2030                }
2031                ProofExpr::ForAll {
2032                    variable: variable.clone(),
2033                    body: Box::new(self.normalize_for_proof(body)),
2034                }
2035            }
2036            ProofExpr::And(left, right) => ProofExpr::And(
2037                Box::new(self.normalize_for_proof(left)),
2038                Box::new(self.normalize_for_proof(right)),
2039            ),
2040            ProofExpr::Or(left, right) => ProofExpr::Or(
2041                Box::new(self.normalize_for_proof(left)),
2042                Box::new(self.normalize_for_proof(right)),
2043            ),
2044            ProofExpr::Implies(left, right) => ProofExpr::Implies(
2045                Box::new(self.normalize_for_proof(left)),
2046                Box::new(self.normalize_for_proof(right)),
2047            ),
2048            ProofExpr::Not(inner) => ProofExpr::Not(Box::new(self.normalize_for_proof(inner))),
2049            ProofExpr::Exists { variable, body } => ProofExpr::Exists {
2050                variable: variable.clone(),
2051                body: Box::new(self.normalize_for_proof(body)),
2052            },
2053            other => other.clone(),
2054        }
2055    }
2056
2057    /// Extract a definite description from an axiom.
2058    ///
2059    /// Pattern: ∃x ((P(x) ∧ ∀y (P(y) → y = x)) ∧ Q(x))
2060    /// Returns: Some((predicate_name, variable_name, Q(x)))
2061    fn extract_definite_description(&self, expr: &ProofExpr) -> Option<(String, String, ProofExpr)> {
2062        // Match: ∃x (body)
2063        let (var, body) = match expr {
2064            ProofExpr::Exists { variable, body } => (variable.clone(), body.as_ref()),
2065            _ => return None,
2066        };
2067
2068        // Match: (defining_part ∧ property)
2069        let (defining_part, property) = match body {
2070            ProofExpr::And(left, right) => (left.as_ref(), right.as_ref().clone()),
2071            _ => return None,
2072        };
2073
2074        // Match defining_part: (P(x) ∧ ∀y (P(y) → y = x))
2075        let (type_pred, uniqueness) = match defining_part {
2076            ProofExpr::And(left, right) => (left.as_ref(), right.as_ref()),
2077            _ => return None,
2078        };
2079
2080        // Extract predicate name from P(x)
2081        let pred_name = match type_pred {
2082            ProofExpr::Predicate { name, args, .. } if args.len() == 1 => {
2083                // Verify the arg is our variable
2084                if let ProofTerm::Variable(v) = &args[0] {
2085                    if v == &var {
2086                        name.clone()
2087                    } else {
2088                        return None;
2089                    }
2090                } else {
2091                    return None;
2092                }
2093            }
2094            _ => return None,
2095        };
2096
2097        // Verify uniqueness constraint: ∀y (P(y) → y = x)
2098        match uniqueness {
2099            ProofExpr::ForAll { variable: _, body: inner_body } => {
2100                match inner_body.as_ref() {
2101                    ProofExpr::Implies(ante, cons) => {
2102                        // Verify antecedent is P(y)
2103                        if let ProofExpr::Predicate { name, .. } = ante.as_ref() {
2104                            if name != &pred_name {
2105                                return None;
2106                            }
2107                        } else {
2108                            return None;
2109                        }
2110                        // Verify consequent is an identity (y = x)
2111                        if !matches!(cons.as_ref(), ProofExpr::Identity(_, _)) {
2112                            return None;
2113                        }
2114                    }
2115                    _ => return None,
2116                }
2117            }
2118            _ => return None,
2119        }
2120
2121        Some((pred_name, var, property))
2122    }
2123
2124    /// Internal proof search with depth tracking.
2125    /// The canonical loop-detection key of a goal: its target with every free
2126    /// variable renamed to a positional placeholder in first-occurrence order, plus
2127    /// a fingerprint of its context. Two goals that differ only by their fresh
2128    /// existential names — the signature of a recursive-axiom regress — map to the
2129    /// same key; a goal proved under a *richer* context (an added hypothesis from
2130    /// →I or ∨-elim) gets a different key, so legitimate re-proofs are never pruned.
2131    fn loop_key(goal: &ProofGoal) -> String {
2132        use std::hash::{Hash, Hasher};
2133        let mut order: Vec<String> = Vec::new();
2134        collect_vars_ordered_expr(&goal.target, &mut order);
2135        let mut subst = Substitution::new();
2136        for (i, v) in order.iter().enumerate() {
2137            subst.insert(v.clone(), ProofTerm::Constant(format!("\u{27ea}{i}\u{27eb}")));
2138        }
2139        let canon = apply_subst_to_expr(&goal.target, &subst);
2140        let mut hasher = std::collections::hash_map::DefaultHasher::new();
2141        goal.context.len().hash(&mut hasher);
2142        for c in &goal.context {
2143            format!("{c:?}").hash(&mut hasher);
2144        }
2145        format!("{canon:?}#{:x}", hasher.finish())
2146    }
2147
2148    fn prove_goal(&mut self, goal: ProofGoal, depth: usize) -> ProofResult<DerivationTree> {
2149        // Node budget: a guarantee of bounded *time*. Reset at the start of a top-level
2150        // proof, then count every node; once the budget is spent the search fails-fast
2151        // (reusing `DepthExceeded` — both are resource bounds) rather than running forever
2152        // on the exponential branching a recursive axiom can induce.
2153        if depth == 0 {
2154            self.steps = 0;
2155        }
2156        self.charge_budget()?;
2157
2158        // Check depth limit
2159        if depth > self.max_depth {
2160            return Err(ProofError::DepthExceeded);
2161        }
2162
2163        // Loop detection: refuse to re-enter a goal already being proved on this
2164        // branch. A recursive axiom can otherwise regress through ever-fresh
2165        // existential instances of the same goal until the native stack overflows
2166        // (the depth bound trips too late). Pruning the re-entry leaves the
2167        // productive branches — which discharge antecedents against premises or
2168        // facts, not by re-deriving the same goal — untouched.
2169        let key = Self::loop_key(&goal);
2170
2171        // Loop detection: refuse to re-enter a goal already being proved on this
2172        // branch. A recursive axiom can otherwise regress through ever-fresh
2173        // existential instances of the same goal until the native stack overflows
2174        // (the depth bound trips too late). Pruning the re-entry leaves the
2175        // productive branches — which discharge antecedents against premises or
2176        // facts, not by re-deriving the same goal — untouched.
2177        if self.active_goals.contains(&key) {
2178            return Err(ProofError::NoProofFound);
2179        }
2180        self.active_goals.push(key);
2181        let result = self.prove_goal_inner(goal, depth);
2182        self.active_goals.pop();
2183        result
2184    }
2185
2186    fn prove_goal_inner(&mut self, goal: ProofGoal, depth: usize) -> ProofResult<DerivationTree> {
2187        // Check depth limit
2188        if depth > self.max_depth {
2189            return Err(ProofError::DepthExceeded);
2190        }
2191
2192        // PRIORITY: Check for inductive goals FIRST
2193        // Goals with TypedVar (e.g., n:Nat) require structural induction,
2194        // not direct unification which would incorrectly ground the variable.
2195        if let Some((_, typename)) = self.find_typed_var(&goal.target) {
2196            // For known inductive types, require induction to succeed
2197            // Falling back to direct matching would incorrectly unify the TypedVar
2198            let is_known_inductive = matches!(typename.as_str(), "Nat" | "List");
2199
2200            if let Some(tree) = self.try_structural_induction(&goal, depth)? {
2201                return Ok(tree);
2202            }
2203
2204            // For known inductive types, if induction fails, the proof fails
2205            // (don't allow incorrect direct unification)
2206            if is_known_inductive {
2207                return Err(ProofError::NoProofFound);
2208            }
2209            // For unknown types, fall through to other strategies
2210        }
2211
2212        // Strategy 0a: Falsum goal — conflict detection.
2213        // A goal of ⊥ asks whether the premises are jointly inconsistent. Search
2214        // the KB + context for a contradiction; the resulting derivation certifies
2215        // to a kernel-checked proof of `False`.
2216        if is_falsum(&goal.target) {
2217            // Prefer the gapless finder (its derivations certify end-to-end);
2218            // fall back to the heuristic one so a contradiction is still surfaced
2219            // for inspection even when it cannot yet be certified.
2220            if let Some(tree) = self.find_certifiable_contradiction(&goal.context, depth)? {
2221                return Ok(tree);
2222            }
2223            if let Some(tree) = self.find_contradiction(&goal.context, depth)? {
2224                return Ok(tree);
2225            }
2226            return Err(ProofError::NoProofFound);
2227        }
2228
2229        // Strategy 0: Reflexivity by computation
2230        // Try to prove a = b by normalizing both sides
2231        if let Some(tree) = self.try_reflexivity(&goal)? {
2232            return Ok(tree);
2233        }
2234
2235        // Strategy 0b: Arithmetic decision over Int (proof-producing oracle).
2236        // Discharges Int equalities (e.g. x+y = y+x) the certifier turns into a
2237        // kernel-checked proof via the ring axioms.
2238        if let Some(tree) = self.try_arithmetic(&goal)? {
2239            return Ok(tree);
2240        }
2241
2242        // Strategy 0c: Linear arithmetic over `Int` — chain `≤` facts by transitivity,
2243        // add inequalities, and decide ground facts by computation, certified by the
2244        // `le_trans`/`le_refl`/`le_add_mono` axioms.
2245        if let Some(tree) = self.try_linarith(&goal, depth)? {
2246            return Ok(tree);
2247        }
2248
2249        // Strategy 1: Direct fact matching
2250        if let Some(tree) = self.try_match_fact(&goal)? {
2251            return Ok(tree);
2252        }
2253
2254        // Strategy 1b: a NEGATION goal in a finite-domain (grid) setting is proved
2255        // fastest and most completely by the certified contradiction finder — unit
2256        // propagation + bounded case analysis. Run it HERE, before the disjunction-
2257        // elimination strategy (5b), which can blow up on a clue's nested either/or.
2258        // Assume the negated proposition and drive it to ⊥. It only short-circuits
2259        // when it actually closes, so a miss leaves the later strategies untouched.
2260        // A DIRECT ModusTollens (a rule whose consequent is refuted) is more specific,
2261        // so prefer it — the grid contradiction finder only fires when it does not apply.
2262        if matches!(goal.target, ProofExpr::Not(_)) {
2263            if let Some(tree) = self.try_modus_tollens(&goal, depth)? {
2264                return Ok(tree);
2265            }
2266            // A double-negation goal `¬¬X` is introduced by `DoubleNegation` (prove X);
2267            // don't let the grid contradiction finder preempt that more specific rule.
2268            let is_double_neg = matches!(&goal.target, ProofExpr::Not(inner) if matches!(inner.as_ref(), ProofExpr::Not(_)));
2269            if !is_double_neg {
2270                if let Some(tree) = self.try_reductio_ad_absurdum(&goal, depth)? {
2271                    return Ok(tree);
2272                }
2273            }
2274        }
2275
2276        // Strategy 2: Introduction rules (structural decomposition)
2277        if let Some(tree) = self.try_intro_rules(&goal, depth)? {
2278            return Ok(tree);
2279        }
2280
2281        // Strategy 3: Backward chaining on implications
2282        if let Some(tree) = self.try_backward_chain(&goal, depth)? {
2283            return Ok(tree);
2284        }
2285
2286        // Strategy 3b: Modus Tollens (from P → Q and ¬Q, derive ¬P)
2287        if let Some(tree) = self.try_modus_tollens(&goal, depth)? {
2288            return Ok(tree);
2289        }
2290
2291        // Strategy 4: Universal instantiation
2292        if let Some(tree) = self.try_universal_inst(&goal, depth)? {
2293            return Ok(tree);
2294        }
2295
2296        // Strategy 5: Existential introduction
2297        if let Some(tree) = self.try_existential_intro(&goal, depth)? {
2298            return Ok(tree);
2299        }
2300
2301        // Strategy 5b: Disjunction elimination (disjunctive syllogism)
2302        if let Some(tree) = self.try_disjunction_elimination(&goal, depth)? {
2303            return Ok(tree);
2304        }
2305
2306        // Strategy 5c: Proof by contradiction (reductio ad absurdum)
2307        // For negation goals, assume the positive and derive contradiction
2308        if let Some(tree) = self.try_reductio_ad_absurdum(&goal, depth)? {
2309            return Ok(tree);
2310        }
2311
2312        // Strategy 5d: Existential elimination from premises
2313        // Extract witnesses from ∃x P(x) premises and add to context
2314        if let Some(tree) = self.try_existential_elimination(&goal, depth)? {
2315            return Ok(tree);
2316        }
2317
2318        // Strategy 6: Equality rewriting (Leibniz's Law)
2319        if let Some(tree) = self.try_equality_rewrite(&goal, depth)? {
2320            return Ok(tree);
2321        }
2322
2323        // Strategy 7: Ex falso quodlibet — a last resort, only at the top level.
2324        // If the premises are jointly contradictory, any goal follows. The
2325        // contradiction is a property of the KB, so one check at depth 0 suffices;
2326        // a consistent KB returns `None` and the goal still fails honestly.
2327        if depth == 0 && !is_falsum(&goal.target) {
2328            if let Some(false_tree) = self.find_certifiable_contradiction(&goal.context, depth)? {
2329                return Ok(DerivationTree::new(
2330                    goal.target.clone(),
2331                    InferenceRule::ExFalso,
2332                    vec![false_tree],
2333                ));
2334            }
2335        }
2336
2337        // Strategy 8: Classical reductio (proof by contradiction) — last resort at the
2338        // top level for a POSITIVE goal G: assume ¬G and search for ⊥; if found, G
2339        // follows via the `dne` axiom. Classical, explicit, kernel-checked.
2340        if depth == 0 && !is_falsum(&goal.target) && !matches!(goal.target, ProofExpr::Not(_)) {
2341            let mut ext = goal.context.clone();
2342            ext.push(ProofExpr::Not(Box::new(goal.target.clone())));
2343            if let Some(false_tree) = self.find_certifiable_contradiction(&ext, depth)? {
2344                return Ok(DerivationTree::new(
2345                    goal.target.clone(),
2346                    InferenceRule::ClassicalReductio,
2347                    vec![false_tree],
2348                ));
2349            }
2350        }
2351
2352        // Strategy 9: Oracle fallback (Z3) — the ABSOLUTE last resort. It is a COMPLETE solver over
2353        // the whole goal + knowledge base, so (a) it runs only at the TOP LEVEL (`depth == 0`):
2354        // invoking it per recursive subgoal is an O(nodes) Z3-call blowup that hangs a deep
2355        // backward-chaining search, and is redundant since a decomposed subgoal is oracle-provable
2356        // only if the goal it came from is; and (b) it runs only AFTER every certifiable kernel
2357        // strategy (incl. ex-falso / reductio above), so a kernel-checkable proof is never lost to a
2358        // non-certifiable Z3 "Verified by Z3" result.
2359        #[cfg(feature = "verification")]
2360        if depth == 0 {
2361            if let Some(tree) = self.try_oracle_fallback(&goal)? {
2362                return Ok(tree);
2363            }
2364        }
2365
2366        // No proof found
2367        Err(ProofError::NoProofFound)
2368    }
2369
2370    // =========================================================================
2371    // STRATEGY 0: REFLEXIVITY BY COMPUTATION
2372    // =========================================================================
2373
2374    /// Try to prove an identity a = b by normalizing both sides.
2375    ///
2376    /// If both sides reduce to structurally identical expressions,
2377    /// the proof is by reflexivity (a = a).
2378    fn try_reflexivity(&self, goal: &ProofGoal) -> ProofResult<Option<DerivationTree>> {
2379        if let ProofExpr::Identity(left_term, right_term) = &goal.target {
2380            // Convert terms to expressions for reduction
2381            let left_expr = term_to_expr(left_term);
2382            let right_expr = term_to_expr(right_term);
2383
2384            // Normalize both sides using full reduction (beta + iota + fix)
2385            let left_normal = beta_reduce(&left_expr);
2386            let right_normal = beta_reduce(&right_expr);
2387
2388            // Check structural equality after normalization
2389            if exprs_structurally_equal(&left_normal, &right_normal) {
2390                return Ok(Some(DerivationTree::leaf(
2391                    goal.target.clone(),
2392                    InferenceRule::Reflexivity,
2393                )));
2394            }
2395        }
2396        Ok(None)
2397    }
2398
2399    /// Strategy 0b: decide an `Int` equality with the proof-producing oracle.
2400    ///
2401    /// The oracle ([`crate::arith::prove_int_eq`]) searches for a real kernel
2402    /// proof (computation + the ring axioms). If it finds one, we emit an
2403    /// `ArithDecision` leaf; the certifier re-runs the oracle to produce the
2404    /// kernel term, which `infer_type` re-checks — so the oracle is never
2405    /// trusted. Non-arithmetic identities are left to other strategies.
2406    fn try_arithmetic(&self, goal: &ProofGoal) -> ProofResult<Option<DerivationTree>> {
2407        if let ProofExpr::Identity(left_term, right_term) = &goal.target {
2408            // Only engage for genuine Int-arithmetic identities (an arithmetic
2409            // operator at the head of a side). This keeps the strategy from
2410            // hijacking unrelated reflexive identities (e.g. f(a)=f(a) over
2411            // non-Int terms), which it would otherwise close with a type-wrong
2412            // `refl Int …` that fails certification.
2413            let is_arith = |t: &ProofTerm| {
2414                matches!(
2415                    t,
2416                    ProofTerm::Function(n, _)
2417                        if matches!(n.as_str(), "add" | "sub" | "mul" | "div" | "mod")
2418                )
2419            };
2420            if !is_arith(left_term) && !is_arith(right_term) {
2421                return Ok(None);
2422            }
2423            let (kl, kr) = match (
2424                crate::certifier::proof_term_to_kernel_term(left_term),
2425                crate::certifier::proof_term_to_kernel_term(right_term),
2426            ) {
2427                (Ok(a), Ok(b)) => (a, b),
2428                _ => return Ok(None),
2429            };
2430
2431            let mut ctx = logicaffeine_kernel::Context::new();
2432            logicaffeine_kernel::prelude::StandardLibrary::register(&mut ctx);
2433
2434            if crate::arith::prove_int_eq(&ctx, &kl, &kr).is_some() {
2435                return Ok(Some(DerivationTree::leaf(
2436                    goal.target.clone(),
2437                    InferenceRule::ArithDecision,
2438                )));
2439            }
2440        }
2441        Ok(None)
2442    }
2443
2444    // =========================================================================
2445    // STRATEGY 1: DIRECT FACT MATCHING
2446    // =========================================================================
2447
2448    /// Try to match the goal directly against a fact in the knowledge base.
2449    fn try_match_fact(&self, goal: &ProofGoal) -> ProofResult<Option<DerivationTree>> {
2450        // Also check local context
2451        for fact in goal.context.iter().chain(self.knowledge_base.iter()) {
2452            if let Ok(subst) = unify_exprs(&goal.target, fact) {
2453                return Ok(Some(
2454                    DerivationTree::leaf(goal.target.clone(), InferenceRule::PremiseMatch)
2455                        .with_substitution(subst),
2456                ));
2457            }
2458        }
2459        // Forward ∧-elimination: a conjunct of an assumed `A ∧ B` is itself a usable
2460        // fact. Project it out with a `ConjunctionElim` chain — this is what lets the
2461        // existential middle of a cut pin against a conjunctive hypothesis instead of
2462        // fanning out into an unbounded search (the difference between milliseconds and
2463        // a blow-up for `(Cong ∧ Cong) → Cong`-shaped theorems).
2464        for fact in goal.context.iter().chain(self.knowledge_base.iter()) {
2465            if matches!(fact, ProofExpr::And(_, _)) {
2466                let leaf = DerivationTree::leaf(fact.clone(), InferenceRule::PremiseMatch);
2467                if let Some((tree, subst)) = extract_conjunct(&leaf, &goal.target) {
2468                    return Ok(Some(tree.with_substitution(subst)));
2469                }
2470            }
2471        }
2472        Ok(None)
2473    }
2474
2475    // =========================================================================
2476    // STRATEGY 2: INTRODUCTION RULES
2477    // =========================================================================
2478
2479    /// Try introduction rules based on the goal's structure.
2480    fn try_intro_rules(
2481        &mut self,
2482        goal: &ProofGoal,
2483        depth: usize,
2484    ) -> ProofResult<Option<DerivationTree>> {
2485        match &goal.target {
2486            // Conjunction Introduction: To prove A ∧ B, prove A and prove B
2487            ProofExpr::And(left, right) => {
2488                let left_goal = ProofGoal::with_context((**left).clone(), goal.context.clone());
2489                let right_goal = ProofGoal::with_context((**right).clone(), goal.context.clone());
2490
2491                // Try to prove both sides
2492                if let (Ok(left_proof), Ok(right_proof)) = (
2493                    self.prove_goal(left_goal, depth + 1),
2494                    self.prove_goal(right_goal, depth + 1),
2495                ) {
2496                    return Ok(Some(DerivationTree::new(
2497                        goal.target.clone(),
2498                        InferenceRule::ConjunctionIntro,
2499                        vec![left_proof, right_proof],
2500                    )));
2501                }
2502            }
2503
2504            // Disjunction Introduction: To prove A ∨ B, prove A or prove B
2505            ProofExpr::Or(left, right) => {
2506                // Try left side first
2507                let left_goal = ProofGoal::with_context((**left).clone(), goal.context.clone());
2508                if let Ok(left_proof) = self.prove_goal(left_goal, depth + 1) {
2509                    return Ok(Some(DerivationTree::new(
2510                        goal.target.clone(),
2511                        InferenceRule::DisjunctionIntro,
2512                        vec![left_proof],
2513                    )));
2514                }
2515
2516                // Try right side
2517                let right_goal = ProofGoal::with_context((**right).clone(), goal.context.clone());
2518                if let Ok(right_proof) = self.prove_goal(right_goal, depth + 1) {
2519                    return Ok(Some(DerivationTree::new(
2520                        goal.target.clone(),
2521                        InferenceRule::DisjunctionIntro,
2522                        vec![right_proof],
2523                    )));
2524                }
2525            }
2526
2527            // Biconditional Introduction (↔I): to prove P ↔ Q, prove both directions
2528            // P → Q and Q → P (each by →I), then combine. Certified as `conj` over the
2529            // two implications (Iff ≡ And (P→Q) (Q→P)).
2530            ProofExpr::Iff(left, right) => {
2531                let pq = ProofExpr::Implies(left.clone(), right.clone());
2532                let qp = ProofExpr::Implies(right.clone(), left.clone());
2533                let pq_goal = ProofGoal::with_context(pq, goal.context.clone());
2534                let qp_goal = ProofGoal::with_context(qp, goal.context.clone());
2535                if let (Ok(pq_proof), Ok(qp_proof)) = (
2536                    self.prove_goal(pq_goal, depth + 1),
2537                    self.prove_goal(qp_goal, depth + 1),
2538                ) {
2539                    return Ok(Some(DerivationTree::new(
2540                        goal.target.clone(),
2541                        InferenceRule::BicondIntro,
2542                        vec![pq_proof, qp_proof],
2543                    )));
2544                }
2545            }
2546
2547            // Universal Introduction (∀I): to prove ∀x.φ(x), prove φ(c) for a FRESH
2548            // EIGENCONSTANT c — an opaque term the search can neither unify nor
2549            // instantiate — then generalize c back to x. Proving the body with x left
2550            // as a `Variable` is unsound for search: a strategy could bind x by
2551            // unification (e.g. match goal `Cong(c,d,a,b)` against hypothesis
2552            // `Cong(a,b,c,d)` by setting a:=c,…), producing a tree the kernel rejects
2553            // but the engine has already committed to. The eigenconstant makes such a
2554            // match fail, forcing the genuinely-arbitrary proof; the certifier then
2555            // abstracts `λ(x:Entity). …` over the generalized body.
2556            ProofExpr::ForAll { variable, body } => {
2557                let eigen = self.fresh_eigenconstant();
2558                let mut subst = Substitution::new();
2559                subst.insert(variable.clone(), ProofTerm::Constant(eigen.clone()));
2560                let body_expr = apply_subst_to_expr(body, &subst);
2561                let body_ctx: Vec<ProofExpr> =
2562                    goal.context.iter().map(|c| apply_subst_to_expr(c, &subst)).collect();
2563                let body_goal = ProofGoal::with_context(body_expr, body_ctx);
2564                if let Ok(body_proof) = self.prove_goal(body_goal, depth + 1) {
2565                    let generalized = rewrite_tree_const_to_var(&body_proof, &eigen, variable);
2566                    return Ok(Some(DerivationTree::new(
2567                        goal.target.clone(),
2568                        InferenceRule::UniversalIntro {
2569                            variable: variable.clone(),
2570                            var_type: "Entity".to_string(),
2571                        },
2572                        vec![generalized],
2573                    )));
2574                }
2575            }
2576
2577            // Implication Introduction (→I): to prove P → Q, assume P (discharge it
2578            // into the local context) and prove Q. The certifier binds P as a local
2579            // hypothesis and wraps the Q-proof in `λ(hp:P). …`.
2580            ProofExpr::Implies(ant, con) => {
2581                let mut ext = goal.context.clone();
2582                ext.push((**ant).clone());
2583                let con_goal = ProofGoal::with_context((**con).clone(), ext);
2584                if let Ok(con_proof) = self.prove_goal(con_goal, depth + 1) {
2585                    return Ok(Some(DerivationTree::new(
2586                        goal.target.clone(),
2587                        InferenceRule::ImpliesIntro,
2588                        vec![con_proof],
2589                    )));
2590                }
2591            }
2592
2593            // Double Negation: To prove ¬¬P, prove P
2594            ProofExpr::Not(inner) => {
2595                if let ProofExpr::Not(core) = &**inner {
2596                    let core_goal = ProofGoal::with_context((**core).clone(), goal.context.clone());
2597                    if let Ok(core_proof) = self.prove_goal(core_goal, depth + 1) {
2598                        return Ok(Some(DerivationTree::new(
2599                            goal.target.clone(),
2600                            InferenceRule::DoubleNegation,
2601                            vec![core_proof],
2602                        )));
2603                    }
2604                }
2605            }
2606
2607            _ => {}
2608        }
2609
2610        Ok(None)
2611    }
2612
2613    // =========================================================================
2614    // STRATEGY 3: BACKWARD CHAINING ON IMPLICATIONS
2615    // =========================================================================
2616
2617    /// Try backward chaining: find P → Goal in KB, then prove P.
2618    fn try_backward_chain(
2619        &mut self,
2620        goal: &ProofGoal,
2621        depth: usize,
2622    ) -> ProofResult<Option<DerivationTree>> {
2623        // Collect every KB implication whose consequent unifies with the goal, and SCORE
2624        // each by how many of its (instantiated) antecedent conjuncts can be discharged
2625        // DIRECTLY against a known fact or hypothesis. Trying the most-pinnable rule first
2626        // finds a direct proof — an axiom whose whole antecedent is already among the
2627        // premises (e.g. Tarski's five-segment, with all seven antecedents as givens) —
2628        // before a recursive axiom (inner transitivity) sends the search into exponential
2629        // re-derivation. This is the relevance signal that lets the prover pick the rule it
2630        // actually needs; it only REORDERS candidates, so nothing that was provable becomes
2631        // unprovable.
2632        struct Candidate {
2633            impl_expr: ProofExpr,
2634            antecedent: ProofExpr,
2635            subst: Substitution,
2636            score: usize,
2637        }
2638        let kb_implications: Vec<ProofExpr> = self
2639            .knowledge_base
2640            .iter()
2641            .filter(|e| matches!(e, ProofExpr::Implies(_, _)))
2642            .cloned()
2643            .collect();
2644        let mut candidates: Vec<Candidate> = Vec::new();
2645        for impl_expr in kb_implications {
2646            let renamed = self.rename_variables(&impl_expr);
2647            if let ProofExpr::Implies(ant, con) = renamed {
2648                if let Ok(subst) = unify_exprs(&goal.target, &con) {
2649                    let antecedent = apply_subst_to_expr(&ant, &subst);
2650                    let score = self.antecedent_pinnability(&antecedent, &goal.context);
2651                    candidates.push(Candidate { impl_expr, antecedent, subst, score });
2652                }
2653            }
2654        }
2655        // Most-pinnable antecedent first; stable, so equal scores keep KB order.
2656        candidates.sort_by(|a, b| b.score.cmp(&a.score));
2657
2658        for cand in candidates {
2659            let ant_goal = ProofGoal::with_context(cand.antecedent, goal.context.clone());
2660            if let Ok(ant_proof) = self.prove_goal(ant_goal, depth + 1) {
2661                let impl_leaf =
2662                    DerivationTree::leaf(cand.impl_expr.clone(), InferenceRule::PremiseMatch);
2663                return Ok(Some(
2664                    DerivationTree::new(
2665                        goal.target.clone(),
2666                        InferenceRule::ModusPonens,
2667                        vec![impl_leaf, ant_proof],
2668                    )
2669                    .with_substitution(cand.subst),
2670                ));
2671            }
2672        }
2673
2674        Ok(None)
2675    }
2676
2677    /// How many of `ant`'s conjuncts can be discharged immediately against a known fact or
2678    /// a conjunct of a conjunctive hypothesis — the relevance score for
2679    /// [`Self::try_backward_chain`]. A rule whose antecedent is entirely pinnable needs no
2680    /// recursive search at all, so it is the one to try first.
2681    fn antecedent_pinnability(&self, ant: &ProofExpr, context: &[ProofExpr]) -> usize {
2682        flatten_conjuncts(ant)
2683            .iter()
2684            .filter(|c| self.subgoal_pinnable(c, context))
2685            .count()
2686    }
2687
2688    /// Relevance of a (possibly `∀`-wrapped) rule to `goal` — how worthwhile it is to try
2689    /// FIRST when backward-chaining. A universal FACT whose matrix unifies with the goal
2690    /// is the best (direct instantiation, no antecedent, terminating). Next, an implication
2691    /// whose consequent unifies with the goal, ranked by how many of its antecedent
2692    /// conjuncts are already discharge-able against hypotheses — so the axiom that needs no
2693    /// recursive search outranks a recursive one. A non-matching rule scores 0. This is a
2694    /// pure ORDERING signal: every rule is still tried, just in a smarter order, so the
2695    /// direct proof is reached before a recursive axiom explodes the search.
2696    fn rule_relevance(&mut self, rule: &ProofExpr, goal: &ProofGoal) -> usize {
2697        let renamed = self.rename_variables(rule);
2698        let mut matrix: &ProofExpr = &renamed;
2699        while let ProofExpr::ForAll { body, .. } = matrix {
2700            matrix = body.as_ref();
2701        }
2702        match matrix {
2703            ProofExpr::Implies(ant, con) => {
2704                if let Ok(subst) = unify_exprs(&goal.target, con) {
2705                    let ant_inst = apply_subst_to_expr(ant, &subst);
2706                    // +2 so any matching implication outranks a non-match; pinnable
2707                    // antecedent conjuncts add on top (a fully-pinned antecedent wins).
2708                    2 + self.antecedent_pinnability(&ant_inst, &goal.context)
2709                } else {
2710                    0
2711                }
2712            }
2713            other => {
2714                // A universal fact that unifies: direct, terminating — rank above any
2715                // implication so it discharges before a (possibly recursive) rule chain.
2716                if unify_exprs(&goal.target, other).is_ok() {
2717                    1000
2718                } else {
2719                    0
2720                }
2721            }
2722        }
2723    }
2724
2725    // =========================================================================
2726    // STRATEGY 3b: MODUS TOLLENS
2727    // =========================================================================
2728
2729    /// Try Modus Tollens: from P → Q and ¬Q, derive ¬P.
2730    ///
2731    /// If the goal is ¬P:
2732    /// 1. Look for implications P → Q in the KB
2733    /// 2. Check if ¬Q is known (in KB or context) OR can be proved
2734    /// 3. If so, derive ¬P by Modus Tollens
2735    fn try_modus_tollens(
2736        &mut self,
2737        goal: &ProofGoal,
2738        depth: usize,
2739    ) -> ProofResult<Option<DerivationTree>> {
2740        // Modus Tollens only applies when goal is a negation: ¬P
2741        let inner_goal = match &goal.target {
2742            ProofExpr::Not(inner) => (**inner).clone(),
2743            _ => return Ok(None),
2744        };
2745
2746        // Collect all implications from KB, including those inside ForAll
2747        let implications: Vec<ProofExpr> = self
2748            .knowledge_base
2749            .iter()
2750            .chain(goal.context.iter())
2751            .flat_map(|expr| {
2752                match expr {
2753                    ProofExpr::Implies(_, _) => vec![expr.clone()],
2754                    ProofExpr::ForAll { body, .. } => {
2755                        // Extract implications from inside universal quantifiers
2756                        if let ProofExpr::Implies(_, _) = body.as_ref() {
2757                            vec![*body.clone()]
2758                        } else {
2759                            vec![]
2760                        }
2761                    }
2762                    _ => vec![],
2763                }
2764            })
2765            .collect();
2766
2767        // Collect all negations from KB and context (for direct matching)
2768        let negations: Vec<ProofExpr> = self
2769            .knowledge_base
2770            .iter()
2771            .chain(goal.context.iter())
2772            .filter_map(|expr| {
2773                if let ProofExpr::Not(inner) = expr {
2774                    Some((**inner).clone())
2775                } else {
2776                    None
2777                }
2778            })
2779            .collect();
2780
2781        // For each implication P → Q
2782        for impl_expr in &implications {
2783            if let ProofExpr::Implies(antecedent, consequent) = impl_expr {
2784                // Check if the antecedent P matches our inner goal (we want to prove ¬P)
2785                if let Ok(subst) = unify_exprs(&inner_goal, antecedent) {
2786                    // Apply substitution to the consequent Q
2787                    let q = apply_subst_to_expr(consequent, &subst);
2788
2789                    // First, check if ¬Q is directly in our known facts
2790                    for negated in &negations {
2791                        if exprs_structurally_equal(negated, &q) {
2792                            // We have P → Q and ¬Q, so we can derive ¬P
2793                            let impl_leaf = DerivationTree::leaf(
2794                                impl_expr.clone(),
2795                                InferenceRule::PremiseMatch,
2796                            );
2797                            let neg_q_leaf = DerivationTree::leaf(
2798                                ProofExpr::Not(Box::new(q.clone())),
2799                                InferenceRule::PremiseMatch,
2800                            );
2801
2802                            return Ok(Some(
2803                                DerivationTree::new(
2804                                    goal.target.clone(),
2805                                    InferenceRule::ModusTollens,
2806                                    vec![impl_leaf, neg_q_leaf],
2807                                )
2808                                .with_substitution(subst),
2809                            ));
2810                        }
2811                    }
2812
2813                    // Second, try to prove ¬Q recursively (for chaining)
2814                    let neg_q_goal = ProofGoal::with_context(
2815                        ProofExpr::Not(Box::new(q.clone())),
2816                        goal.context.clone(),
2817                    );
2818
2819                    if let Ok(neg_q_proof) = self.prove_goal(neg_q_goal, depth + 1) {
2820                        // We proved ¬Q, so we can derive ¬P
2821                        let impl_leaf = DerivationTree::leaf(
2822                            impl_expr.clone(),
2823                            InferenceRule::PremiseMatch,
2824                        );
2825
2826                        return Ok(Some(
2827                            DerivationTree::new(
2828                                goal.target.clone(),
2829                                InferenceRule::ModusTollens,
2830                                vec![impl_leaf, neg_q_proof],
2831                            )
2832                            .with_substitution(subst),
2833                        ));
2834                    }
2835                }
2836            }
2837        }
2838
2839        Ok(None)
2840    }
2841
2842    // =========================================================================
2843    // STRATEGY 4: UNIVERSAL INSTANTIATION
2844    // =========================================================================
2845
2846    /// Try universal instantiation: if KB has ∀x.P(x), try to prove P(t) for some term t.
2847    fn try_universal_inst(
2848        &mut self,
2849        goal: &ProofGoal,
2850        depth: usize,
2851    ) -> ProofResult<Option<DerivationTree>> {
2852        // Look for universal quantifiers in KB, then ORDER them by relevance to the goal:
2853        // the rule whose antecedent is most directly dischargeable (or a universal fact,
2854        // which terminates) is tried first, so a direct proof is found before a recursive
2855        // axiom drives the search into exponential re-derivation. Pure reordering — every
2856        // rule is still tried — so nothing provable becomes unprovable.
2857        let universal_exprs: Vec<ProofExpr> = self
2858            .knowledge_base
2859            .iter()
2860            .filter(|expr| matches!(expr, ProofExpr::ForAll { .. }))
2861            .cloned()
2862            .collect();
2863        let mut scored: Vec<(usize, ProofExpr)> = universal_exprs
2864            .into_iter()
2865            .map(|u| (self.rule_relevance(&u, goal), u))
2866            .collect();
2867        scored.sort_by(|a, b| b.0.cmp(&a.0));
2868        let universals: Vec<ProofExpr> = scored.into_iter().map(|(_, u)| u).collect();
2869
2870        for forall_expr in universals {
2871            // Freshen every variable so instantiation cannot capture variables in
2872            // the goal, then peel *all* leading universal quantifiers. A rule may
2873            // bind several variables (`∀x∀y∀z …`); only peeling one leaves the
2874            // matrix still quantified and unusable — that was the transitivity gap.
2875            let renamed = self.rename_variables(&forall_expr);
2876            let mut body: &ProofExpr = &renamed;
2877            let mut bound_vars: Vec<String> = Vec::new();
2878            while let ProofExpr::ForAll { variable, body: inner } = body {
2879                bound_vars.push(variable.clone());
2880                body = inner.as_ref();
2881            }
2882            if bound_vars.is_empty() {
2883                continue;
2884            }
2885
2886            // Case A: the matrix unifies directly with the goal — a universal fact
2887            // (e.g. ∀x∀y R(x,y) discharging R(a,b)), including one whose matrix is
2888            // itself existential (∀b c d. ∃x. Cong(b,x,c,d) discharging ∃x. Cong(P,x,Q,R)).
2889            if let Ok(subst) = unify_exprs(&goal.target, body) {
2890                // Build NESTED UniversalInst nodes so a multi-binder fact applies to ALL
2891                // witnesses, not just the first. A variable vacuous in the matrix is
2892                // defaulted to an entity; only a genuinely-underdetermined one aborts.
2893                if let Some(witnesses) = instantiation_witnesses(&bound_vars, &subst, body) {
2894                    let mut inst_node =
2895                        DerivationTree::leaf(forall_expr.clone(), InferenceRule::PremiseMatch);
2896                    for witness in &witnesses {
2897                        inst_node = DerivationTree::new(
2898                            goal.target.clone(),
2899                            InferenceRule::UniversalInst(format!("{}", witness)),
2900                            vec![inst_node],
2901                        );
2902                    }
2903                    return Ok(Some(inst_node.with_substitution(subst)));
2904                }
2905            }
2906
2907            // Case B: the matrix is an implication ∀…(P → Q). Unify Q with the
2908            // goal, then prove P. Any bound variable that does NOT appear in Q (a
2909            // "middle term", e.g. the `y` in transitivity) is left open by that
2910            // unification and is resolved while proving P against the KB.
2911            if let ProofExpr::Implies(ant, con) = body {
2912                if let Ok(subst) = unify_exprs(&goal.target, &**con) {
2913                    let ant_inst = apply_subst_to_expr(&**ant, &subst);
2914                    if let Ok((ant_proof, final_subst)) =
2915                        self.prove_rule_antecedent(&ant_inst, &subst, &goal.context, depth + 1)
2916                    {
2917                        // Each bound variable must be pinned by the conclusion
2918                        // unification or the antecedent solve — or be vacuous in the
2919                        // matrix (then defaulted). A genuinely-underdetermined variable
2920                        // aborts this rule rather than leak a metavariable into the cert.
2921                        let witnesses =
2922                            match instantiation_witnesses(&bound_vars, &final_subst, body) {
2923                                Some(w) => w,
2924                                None => continue,
2925                            };
2926                        // Build NESTED UniversalInst nodes — one per bound variable, in
2927                        // binder order — so the certifier applies the universal proof to
2928                        // ALL witnesses (`((forall x) y) z`), fully instantiating
2929                        // ∀x∀y∀z. A single node with one witness left the inner
2930                        // quantifiers unfilled (the "expected Entity, found And" bug).
2931                        let instantiated = apply_subst_to_expr(body, &final_subst);
2932                        let mut inst_node =
2933                            DerivationTree::leaf(forall_expr.clone(), InferenceRule::PremiseMatch);
2934                        for witness in &witnesses {
2935                            inst_node = DerivationTree::new(
2936                                instantiated.clone(),
2937                                InferenceRule::UniversalInst(format!("{}", witness)),
2938                                vec![inst_node],
2939                            );
2940                        }
2941                        return Ok(Some(
2942                            DerivationTree::new(
2943                                goal.target.clone(),
2944                                InferenceRule::ModusPonens,
2945                                vec![inst_node, ant_proof],
2946                            )
2947                            .with_substitution(final_subst),
2948                        ));
2949                    }
2950                }
2951            }
2952        }
2953
2954        Ok(None)
2955    }
2956
2957    /// Prove the antecedent of an instantiated rule, returning both its proof and
2958    /// the bindings it forced. The antecedent may be a conjunction whose conjuncts
2959    /// share a still-open variable (a middle term); [`Self::solve_subgoals`]
2960    /// resolves it by threading bindings left to right.
2961    fn prove_rule_antecedent(
2962        &mut self,
2963        antecedent: &ProofExpr,
2964        subst: &Substitution,
2965        context: &[ProofExpr],
2966        depth: usize,
2967    ) -> ProofResult<(DerivationTree, Substitution)> {
2968        let conjuncts = flatten_conjuncts(antecedent);
2969        let (proofs, final_subst) = self.solve_subgoals(&conjuncts, subst, context, depth)?;
2970        let tree = if proofs.len() == 1 {
2971            proofs.into_iter().next().expect("exactly one antecedent proof")
2972        } else {
2973            // Fold the per-conjunct proofs into a BINARY-nested `ConjunctionIntro` tree
2974            // that mirrors the antecedent's own `And` structure — the certifier's
2975            // `ConjunctionIntro` is binary (`conj P Q · ·`), so a 7-conjunct antecedent
2976            // becomes 6 nested binary nodes, not one illegal flat node. O(n), no extra
2977            // proving: `flatten_conjuncts` and this fold share the same left-to-right
2978            // recursion, so the flat proofs align exactly with the leaves.
2979            let mut proofs = proofs.into_iter();
2980            build_conjunction_tree(antecedent, &final_subst, &mut proofs)
2981        };
2982        Ok((tree, final_subst))
2983    }
2984
2985    /// Whether `g` can be discharged *immediately* by a known fact — either it
2986    /// unifies with an atomic fact, or with a conjunct of a conjunctive hypothesis
2987    /// (forward ∧-elimination). Such a subgoal pins its variables cheaply, so it is
2988    /// preferred over one that would need a recursive rule search.
2989    fn subgoal_pinnable(&self, g: &ProofExpr, context: &[ProofExpr]) -> bool {
2990        context.iter().chain(self.knowledge_base.iter()).any(|fact| {
2991            if is_atomic_fact(fact) {
2992                unify_exprs(g, fact).is_ok()
2993            } else if matches!(fact, ProofExpr::And(_, _)) {
2994                flatten_conjuncts(fact).iter().any(|c| unify_exprs(g, c).is_ok())
2995            } else {
2996                false
2997            }
2998        })
2999    }
3000
3001    /// Choose which subgoal to solve next, cheapest first: a ground subgoal (no
3002    /// branching) over a fact-pinnable one (binds a middle term immediately) over an
3003    /// open one that needs a rule search. Conjunctions are left in place (score
3004    /// highest) — the caller expands them at the head. Returns an index into
3005    /// `subgoals`; ties resolve to the lowest index, keeping the search deterministic.
3006    fn select_subgoal(
3007        &self,
3008        subgoals: &[ProofExpr],
3009        subst: &Substitution,
3010        context: &[ProofExpr],
3011    ) -> usize {
3012        let mut best = 0;
3013        let mut best_score = u8::MAX;
3014        for (i, sg) in subgoals.iter().enumerate() {
3015            let g = apply_subst_to_expr(sg, subst);
3016            let score = if matches!(g, ProofExpr::And(_, _)) {
3017                3
3018            } else if self.collect_variables(&g).is_empty() {
3019                0
3020            } else if self.subgoal_pinnable(&g, context) {
3021                1
3022            } else {
3023                2
3024            };
3025            if score < best_score {
3026                best_score = score;
3027                best = i;
3028                if score == 0 {
3029                    break;
3030                }
3031            }
3032        }
3033        best
3034    }
3035
3036    /// Prove a flat list of subgoals under a shared substitution, threading the
3037    /// bindings discovered for each subgoal into the rest — SLD resolution with
3038    /// backtracking. A subgoal that still mentions a variable after substitution
3039    /// is *open*: its variable is a middle term, bound from the knowledge base by
3040    /// trying facts first (terminating) and then rule consequents (recursive).
3041    fn solve_subgoals(
3042        &mut self,
3043        subgoals: &[ProofExpr],
3044        subst: &Substitution,
3045        context: &[ProofExpr],
3046        depth: usize,
3047    ) -> ProofResult<(Vec<DerivationTree>, Substitution)> {
3048        self.charge_budget()?;
3049        if depth > self.max_depth {
3050            return Err(ProofError::DepthExceeded);
3051        }
3052        if subgoals.is_empty() {
3053            return Ok((Vec::new(), subst.clone()));
3054        }
3055        // A nested conjunction at the head expands into the subgoal list.
3056        let head = apply_subst_to_expr(&subgoals[0], subst);
3057        if matches!(head, ProofExpr::And(_, _)) {
3058            let mut expanded = flatten_conjuncts(&head);
3059            expanded.extend_from_slice(&subgoals[1..]);
3060            return self.solve_subgoals(&expanded, subst, context, depth);
3061        }
3062
3063        // Cheapest-first selection: solve a GROUND subgoal (no branching), or one a
3064        // direct fact / conjunct pins immediately, before an open subgoal that would
3065        // otherwise drive an unbounded rule search. This is constraint propagation —
3066        // a shared middle term gets bound by its easiest sibling first — and is what
3067        // keeps the existential cut behind a recursive axiom from blowing up. The
3068        // chosen subgoal is SOLVED first but its proof is re-INSERTED at its original
3069        // position, so the returned order still matches the (order-sensitive)
3070        // `ConjunctionIntro` reconstruction in `prove_rule_antecedent`.
3071        let idx = self.select_subgoal(subgoals, subst, context);
3072        let first = &subgoals[idx];
3073        let rest: Vec<ProofExpr> = subgoals
3074            .iter()
3075            .enumerate()
3076            .filter(|(i, _)| *i != idx)
3077            .map(|(_, e)| e.clone())
3078            .collect();
3079        let g = apply_subst_to_expr(first, subst);
3080
3081        // Ground subgoal (no open variable): prove it with the full strategy
3082        // suite, so non-Horn antecedents (negations, disjunctions, induction, …)
3083        // stay exactly as provable as a top-level goal. No new bindings arise.
3084        // Recurse at `depth + 1` so a recursive axiom (whose antecedent re-enters the
3085        // chainer) is bounded by `max_depth` instead of overflowing the stack.
3086        if self.collect_variables(&g).is_empty() {
3087            let proof =
3088                self.prove_goal(ProofGoal::with_context(g.clone(), context.to_vec()), depth + 1)?;
3089            let (mut proofs, out) = self.solve_subgoals(&rest, subst, context, depth)?;
3090            proofs.insert(idx, proof);
3091            return Ok((proofs, out));
3092        }
3093
3094        // Open subgoal — Option 1: discharge directly against a known fact, OR against
3095        // a conjunct of a conjunctive hypothesis (forward ∧-elimination), binding the
3096        // middle term; backtrack over the choice if the rest cannot close.
3097        let facts: Vec<ProofExpr> = context
3098            .iter()
3099            .chain(self.knowledge_base.iter())
3100            .filter(|e| is_atomic_fact(e) || matches!(e, ProofExpr::And(_, _)))
3101            .cloned()
3102            .collect();
3103        for fact in &facts {
3104            let matched: Option<(DerivationTree, Substitution)> =
3105                if matches!(fact, ProofExpr::And(_, _)) {
3106                    let src = DerivationTree::leaf(fact.clone(), InferenceRule::PremiseMatch);
3107                    extract_conjunct(&src, &g)
3108                } else if is_atomic_fact(fact) {
3109                    unify_exprs(&g, fact).ok().map(|delta| {
3110                        let leaf = DerivationTree::leaf(
3111                            apply_subst_to_expr(&g, &delta),
3112                            InferenceRule::PremiseMatch,
3113                        );
3114                        (leaf, delta)
3115                    })
3116                } else {
3117                    None
3118                };
3119            if let Some((leaf, delta)) = matched {
3120                let combined = compose_substitutions(subst.clone(), delta.clone());
3121                if let Ok((mut proofs, out)) =
3122                    self.solve_subgoals(&rest, &combined, context, depth)
3123                {
3124                    proofs.insert(idx, leaf.with_substitution(delta));
3125                    return Ok((proofs, out));
3126                }
3127            }
3128        }
3129
3130        // Open subgoal — Option 2: chain through a rule whose consequent unifies
3131        // with the subgoal, recursively proving that rule's antecedent.
3132        let rules: Vec<ProofExpr> = self
3133            .knowledge_base
3134            .iter()
3135            .filter(|e| matches!(e, ProofExpr::ForAll { .. } | ProofExpr::Implies(_, _)))
3136            .cloned()
3137            .collect();
3138        for rule in &rules {
3139            let renamed = self.rename_variables(rule);
3140            let mut matrix: &ProofExpr = &renamed;
3141            while let ProofExpr::ForAll { body, .. } = matrix {
3142                matrix = body.as_ref();
3143            }
3144            if let ProofExpr::Implies(ant, con) = matrix {
3145                if let Ok(delta) = unify_exprs(&g, &**con) {
3146                    let combined = compose_substitutions(subst.clone(), delta.clone());
3147                    let ant_inst = apply_subst_to_expr(&**ant, &combined);
3148                    if let Ok((ant_proof, s_ant)) =
3149                        self.prove_rule_antecedent(&ant_inst, &combined, context, depth + 1)
3150                    {
3151                        // Build NESTED UniversalInst nodes — one per bound variable, in
3152                        // binder order — so a multi-binder rule is fully instantiated.
3153                        // Each variable must be pinned or vacuous in the matrix; a
3154                        // genuinely-underdetermined one aborts rather than leak a metavar.
3155                        let mut bound_vars = Vec::new();
3156                        let mut peel: &ProofExpr = &renamed;
3157                        while let ProofExpr::ForAll { variable, body } = peel {
3158                            bound_vars.push(variable.clone());
3159                            peel = body;
3160                        }
3161                        let witnesses = match instantiation_witnesses(&bound_vars, &s_ant, matrix) {
3162                            Some(w) => w,
3163                            None => continue,
3164                        };
3165                        let resolved = apply_subst_to_expr(&g, &s_ant);
3166                        let instantiated = apply_subst_to_expr(matrix, &s_ant);
3167                        let mut inst_node =
3168                            DerivationTree::leaf(rule.clone(), InferenceRule::PremiseMatch);
3169                        for witness in &witnesses {
3170                            inst_node = DerivationTree::new(
3171                                instantiated.clone(),
3172                                InferenceRule::UniversalInst(format!("{}", witness)),
3173                                vec![inst_node],
3174                            );
3175                        }
3176                        let mp = DerivationTree::new(
3177                            resolved,
3178                            InferenceRule::ModusPonens,
3179                            vec![inst_node, ant_proof],
3180                        );
3181                        if let Ok((mut proofs, out)) =
3182                            self.solve_subgoals(&rest, &s_ant, context, depth)
3183                        {
3184                            proofs.insert(idx, mp);
3185                            return Ok((proofs, out));
3186                        }
3187                    }
3188                }
3189            } else if let Ok(delta) = unify_exprs(&g, matrix) {
3190                // A universally-quantified FACT (no antecedent) — e.g. Tarski A1
3191                // `∀a b. Cong(a,b,b,a)`: instantiate it by unifying its matrix with
3192                // the subgoal directly. Without this only `Implies` rules are chained,
3193                // so a universal fact is unreachable when it must discharge an open
3194                // antecedent (the existential cut behind recursive axioms).
3195                let combined = compose_substitutions(subst.clone(), delta.clone());
3196                let resolved = apply_subst_to_expr(&g, &combined);
3197                // Recover the original ∀-variable order and each variable's witness by
3198                // unifying the ORIGINAL matrix against the resolved (ground) goal, then
3199                // apply the fact to each witness in turn (nested `UniversalInst`).
3200                let mut vars = Vec::new();
3201                let mut orig_body: &ProofExpr = rule;
3202                while let ProofExpr::ForAll { variable, body } = orig_body {
3203                    vars.push(variable.clone());
3204                    orig_body = body;
3205                }
3206                if let Ok(wsubst) = unify_exprs(orig_body, &resolved) {
3207                    let witnesses = match instantiation_witnesses(&vars, &wsubst, orig_body) {
3208                        Some(w) => w,
3209                        None => continue,
3210                    };
3211                    let mut node =
3212                        DerivationTree::leaf(rule.clone(), InferenceRule::PremiseMatch);
3213                    for witness in &witnesses {
3214                        node = DerivationTree::new(
3215                            resolved.clone(),
3216                            InferenceRule::UniversalInst(format!("{}", witness)),
3217                            vec![node],
3218                        );
3219                    }
3220                    if let Ok((mut proofs, out)) =
3221                        self.solve_subgoals(&rest, &combined, context, depth)
3222                    {
3223                        proofs.insert(idx, node);
3224                        return Ok((proofs, out));
3225                    }
3226                }
3227            }
3228        }
3229
3230        Err(ProofError::NoProofFound)
3231    }
3232
3233    // =========================================================================
3234    // STRATEGY 5: EXISTENTIAL INTRODUCTION
3235    // =========================================================================
3236
3237    /// Try existential introduction: to prove ∃x.P(x), find a witness t and prove P(t).
3238    fn try_existential_intro(
3239        &mut self,
3240        goal: &ProofGoal,
3241        depth: usize,
3242    ) -> ProofResult<Option<DerivationTree>> {
3243        if let ProofExpr::Exists { variable, body } = &goal.target {
3244            // We need to find a witness that makes the body true
3245            // Try each constant/ground term in our KB as a potential witness
3246            let witnesses = self.collect_witnesses();
3247
3248            for witness in witnesses {
3249                // Create a substitution mapping the variable to the witness
3250                let mut subst = Substitution::new();
3251                subst.insert(variable.clone(), witness.clone());
3252
3253                // Apply substitution to get the instantiated body
3254                let instantiated = apply_subst_to_expr(body, &subst);
3255
3256                // Try to prove the instantiated body
3257                let inst_goal = ProofGoal::with_context(instantiated, goal.context.clone());
3258
3259                if let Ok(body_proof) = self.prove_goal(inst_goal, depth + 1) {
3260                    let witness_str = format!("{}", witness);
3261                    // Witness type from an explicit TypedVar if present (arithmetic /
3262                    // induction), else the FOL domain `Entity` — matching the rest of
3263                    // the encoding (`∀` ranges over Entity, predicates are Entity→Prop),
3264                    // so an untyped `∃y.P(y)` certifies as `Ex Entity …`, not `Ex Nat …`.
3265                    let witness_type = extract_type_from_exists_body(body)
3266                        .unwrap_or_else(|| "Entity".to_string());
3267                    return Ok(Some(DerivationTree::new(
3268                        goal.target.clone(),
3269                        InferenceRule::ExistentialIntro {
3270                            witness: witness_str,
3271                            witness_type,
3272                        },
3273                        vec![body_proof],
3274                    )));
3275                }
3276            }
3277        }
3278
3279        Ok(None)
3280    }
3281
3282    // =========================================================================
3283    // STRATEGY 5b: DISJUNCTION ELIMINATION (DISJUNCTIVE SYLLOGISM)
3284    // =========================================================================
3285
3286    /// Try disjunction elimination: if KB has A ∨ B and ¬A, conclude B (and vice versa).
3287    ///
3288    /// Disjunctive syllogism:
3289    /// - From A ∨ B and ¬A, derive B
3290    /// - From A ∨ B and ¬B, derive A
3291    fn try_disjunction_elimination(
3292        &mut self,
3293        goal: &ProofGoal,
3294        depth: usize,
3295    ) -> ProofResult<Option<DerivationTree>> {
3296        // Collect disjunctions from KB and context
3297        let disjunctions: Vec<(ProofExpr, ProofExpr, ProofExpr)> = self
3298            .knowledge_base
3299            .iter()
3300            .chain(goal.context.iter())
3301            .filter_map(|expr| {
3302                if let ProofExpr::Or(left, right) = expr {
3303                    Some((expr.clone(), (**left).clone(), (**right).clone()))
3304                } else {
3305                    None
3306                }
3307            })
3308            .collect();
3309
3310        // Collect negations from KB and context
3311        let negations: Vec<ProofExpr> = self
3312            .knowledge_base
3313            .iter()
3314            .chain(goal.context.iter())
3315            .filter_map(|expr| {
3316                if let ProofExpr::Not(inner) = expr {
3317                    Some((**inner).clone())
3318                } else {
3319                    None
3320                }
3321            })
3322            .collect();
3323
3324        // For each disjunction A ∨ B
3325        for (disj_expr, left, right) in &disjunctions {
3326            // Check if ¬left is in KB (so right must be true)
3327            for negated in &negations {
3328                if exprs_structurally_equal(negated, left) {
3329                    // We have A ∨ B and ¬A, so B is true
3330                    // Check if B matches our goal
3331                    if let Ok(subst) = unify_exprs(&goal.target, right) {
3332                        let disj_leaf = DerivationTree::leaf(
3333                            disj_expr.clone(),
3334                            InferenceRule::PremiseMatch,
3335                        );
3336                        let neg_leaf = DerivationTree::leaf(
3337                            ProofExpr::Not(Box::new(left.clone())),
3338                            InferenceRule::PremiseMatch,
3339                        );
3340                        return Ok(Some(
3341                            DerivationTree::new(
3342                                goal.target.clone(),
3343                                InferenceRule::DisjunctionElim,
3344                                vec![disj_leaf, neg_leaf],
3345                            )
3346                            .with_substitution(subst),
3347                        ));
3348                    }
3349                }
3350
3351                // Check if ¬right is in KB (so left must be true)
3352                if exprs_structurally_equal(negated, right) {
3353                    // We have A ∨ B and ¬B, so A is true
3354                    // Check if A matches our goal
3355                    if let Ok(subst) = unify_exprs(&goal.target, left) {
3356                        let disj_leaf = DerivationTree::leaf(
3357                            disj_expr.clone(),
3358                            InferenceRule::PremiseMatch,
3359                        );
3360                        let neg_leaf = DerivationTree::leaf(
3361                            ProofExpr::Not(Box::new(right.clone())),
3362                            InferenceRule::PremiseMatch,
3363                        );
3364                        return Ok(Some(
3365                            DerivationTree::new(
3366                                goal.target.clone(),
3367                                InferenceRule::DisjunctionElim,
3368                                vec![disj_leaf, neg_leaf],
3369                            )
3370                            .with_substitution(subst),
3371                        ));
3372                    }
3373                }
3374            }
3375        }
3376
3377        // No literal disjunction settled the goal. The disjunction the syllogism
3378        // needs is often itself a CONSEQUENCE — "Every color is red or blue" at a
3379        // witness yields `Red(c) ∨ Blue(c)` only after universal instantiation +
3380        // modus ponens. So, for a goal `B` and each known negation `¬A`, try to
3381        // PROVE the disjunction `A ∨ B` (or `B ∨ A`); if it goes through, the
3382        // proven disjunction becomes the first premise of the elimination. This
3383        // is the general elimination step every disjunctive-syllogism chain over
3384        // a closed domain relies on. The conclusion is a single disjunct, never
3385        // itself a disjunction — skipping `Or` goals keeps the recursive
3386        // disjunction proof from chasing ever-larger `A ∨ (A ∨ …)` targets.
3387        if depth < self.max_depth && !matches!(goal.target, ProofExpr::Or(..)) {
3388            for negated in &negations {
3389                if exprs_structurally_equal(negated, &goal.target) {
3390                    continue;
3391                }
3392                for orient_left in [true, false] {
3393                    let disjunction = if orient_left {
3394                        ProofExpr::Or(
3395                            Box::new(negated.clone()),
3396                            Box::new(goal.target.clone()),
3397                        )
3398                    } else {
3399                        ProofExpr::Or(
3400                            Box::new(goal.target.clone()),
3401                            Box::new(negated.clone()),
3402                        )
3403                    };
3404                    // Already covered by a literal disjunction above.
3405                    if disjunctions
3406                        .iter()
3407                        .any(|(d, _, _)| exprs_structurally_equal(d, &disjunction))
3408                    {
3409                        continue;
3410                    }
3411                    let disj_goal =
3412                        ProofGoal::with_context(disjunction.clone(), goal.context.clone());
3413                    // Derive the disjunction from a FACT or a universal rule's
3414                    // consequent only. Routing through full `prove_goal` would
3415                    // hand the `A ∨ B` goal to disjunction INTRODUCTION, which
3416                    // re-proves a disjunct — bouncing back to this very goal in an
3417                    // unbounded `Blue ↦ Red ∨ Blue ↦ Blue` cycle. A closed-domain
3418                    // disjunction is always a consequence, never an introduction.
3419                    let disj_proof = self
3420                        .try_match_fact(&disj_goal)?
3421                        .map(Ok)
3422                        .or_else(|| self.try_universal_inst(&disj_goal, depth + 1).transpose())
3423                        .transpose()?;
3424                    if let Some(disj_proof) = disj_proof {
3425                        let neg_leaf = DerivationTree::leaf(
3426                            ProofExpr::Not(Box::new(negated.clone())),
3427                            InferenceRule::PremiseMatch,
3428                        );
3429                        return Ok(Some(DerivationTree::new(
3430                            goal.target.clone(),
3431                            InferenceRule::DisjunctionElim,
3432                            vec![disj_proof, neg_leaf],
3433                        )));
3434                    }
3435                }
3436            }
3437        }
3438
3439        // The negation the syllogism needs may itself have to be PROVED, not just
3440        // looked up. A closed-domain rule `∀x(ante(x) → (D₁(x) ∨ D₂(x)))` at a
3441        // witness yields `D₁(c) ∨ D₂(c)`; when the goal is one disjunct (say
3442        // `D₂(c)`) and the OTHER disjunct is refutable (`¬D₁(c)` follows from a
3443        // uniqueness/identity constraint), disjunctive syllogism still applies —
3444        // we derive the disjunction from the rule and prove `¬D₁(c)` as a
3445        // sub-goal (which the negation strategies discharge by reductio). This is
3446        // the elimination step a logic grid leans on once it has pinned a cell.
3447        if depth < self.max_depth && !matches!(goal.target, ProofExpr::Or(..)) {
3448            let disjunctive_rules: Vec<(ProofExpr, ProofExpr)> = self
3449                .knowledge_base
3450                .iter()
3451                .chain(goal.context.iter())
3452                .filter_map(|e| match e {
3453                    ProofExpr::ForAll { body, .. } => match body.as_ref() {
3454                        ProofExpr::Implies(_, cons) => match cons.as_ref() {
3455                            ProofExpr::Or(l, r) => Some(((**l).clone(), (**r).clone())),
3456                            _ => None,
3457                        },
3458                        _ => None,
3459                    },
3460                    ProofExpr::Implies(_, cons) => match cons.as_ref() {
3461                        ProofExpr::Or(l, r) => Some(((**l).clone(), (**r).clone())),
3462                        _ => None,
3463                    },
3464                    ProofExpr::Or(l, r) => Some(((**l).clone(), (**r).clone())),
3465                    _ => None,
3466                })
3467                .collect();
3468
3469            for (d_left, d_right) in &disjunctive_rules {
3470                // Match the goal against one disjunct template; the OTHER, under
3471                // the same binding, is the disjunct to refute.
3472                for goal_is_right in [true, false] {
3473                    let (goal_template, other_template) = if goal_is_right {
3474                        (d_right, d_left)
3475                    } else {
3476                        (d_left, d_right)
3477                    };
3478                    let Ok(subst) = unify_exprs(&goal.target, goal_template) else {
3479                        continue;
3480                    };
3481                    let other = apply_subst_to_expr(other_template, &subst);
3482                    if self.contains_quantifier(&other) {
3483                        continue;
3484                    }
3485                    // The disjunction `A ∨ B` (in the rule's orientation) the
3486                    // elimination consumes. Derive it from the rule, never by
3487                    // introduction (which would re-enter this goal).
3488                    let goal_disjunct = apply_subst_to_expr(goal_template, &subst);
3489                    let disjunction = if goal_is_right {
3490                        ProofExpr::Or(Box::new(other.clone()), Box::new(goal_disjunct))
3491                    } else {
3492                        ProofExpr::Or(Box::new(goal_disjunct), Box::new(other.clone()))
3493                    };
3494                    let disj_goal =
3495                        ProofGoal::with_context(disjunction.clone(), goal.context.clone());
3496                    let disj_proof = match self.try_match_fact(&disj_goal)? {
3497                        Some(p) => Some(p),
3498                        None => match self.try_universal_inst(&disj_goal, depth + 1)? {
3499                            Some(p) => Some(p),
3500                            // A GROUNDED rule `ante → (A ∨ B)` yields the disjunction
3501                            // by modus ponens once `ante` is proved. Backward-chain
3502                            // into it: the implication's consequent IS the disjunction,
3503                            // so this derives it without disjunction introduction (no
3504                            // re-proving a single disjunct, no cycle).
3505                            None => self.try_backward_chain(&disj_goal, depth + 1)?,
3506                        },
3507                    };
3508                    let Some(disj_proof) = disj_proof else {
3509                        continue;
3510                    };
3511                    // Prove the negation of the other disjunct as a sub-goal.
3512                    let neg_goal = ProofGoal::with_context(
3513                        ProofExpr::Not(Box::new(other.clone())),
3514                        goal.context.clone(),
3515                    );
3516                    if let Ok(neg_proof) = self.prove_goal(neg_goal, depth + 1) {
3517                        return Ok(Some(DerivationTree::new(
3518                            goal.target.clone(),
3519                            InferenceRule::DisjunctionElim,
3520                            vec![disj_proof, neg_proof],
3521                        )));
3522                    }
3523                }
3524            }
3525        }
3526
3527        // Last resort — general case analysis (intuitionistic ∨-elimination): for a
3528        // disjunction `A ∨ B` in the KB, prove the goal under EACH disjunct. Unlike
3529        // disjunctive syllogism above, no negation is needed. Skip a disjunction
3530        // whose disjunct is already in context (it has been decided — re-splitting
3531        // would loop). The certifier's `DisjunctionCases` binds each disjunct as a
3532        // local hypothesis that the branch proof cites.
3533        if depth < self.max_depth {
3534            for (disj_expr, left, right) in &disjunctions {
3535                let decided = goal.context.iter().any(|c| {
3536                    exprs_structurally_equal(c, left) || exprs_structurally_equal(c, right)
3537                });
3538                if decided {
3539                    continue;
3540                }
3541                let mut left_ctx = goal.context.clone();
3542                left_ctx.push(left.clone());
3543                let left_branch = match self.prove_goal(
3544                    ProofGoal::with_context(goal.target.clone(), left_ctx),
3545                    depth + 1,
3546                ) {
3547                    Ok(t) => t,
3548                    Err(_) => continue,
3549                };
3550                let mut right_ctx = goal.context.clone();
3551                right_ctx.push(right.clone());
3552                let right_branch = match self.prove_goal(
3553                    ProofGoal::with_context(goal.target.clone(), right_ctx),
3554                    depth + 1,
3555                ) {
3556                    Ok(t) => t,
3557                    Err(_) => continue,
3558                };
3559                let disj_leaf =
3560                    DerivationTree::leaf(disj_expr.clone(), InferenceRule::PremiseMatch);
3561                return Ok(Some(DerivationTree::new(
3562                    goal.target.clone(),
3563                    InferenceRule::DisjunctionCases,
3564                    vec![disj_leaf, left_branch, right_branch],
3565                )));
3566            }
3567        }
3568
3569        Ok(None)
3570    }
3571
3572    // =========================================================================
3573    // STRATEGY 5c: PROOF BY CONTRADICTION (REDUCTIO AD ABSURDUM)
3574    // =========================================================================
3575
3576    /// Try proof by contradiction: to prove ¬P, assume P and derive a contradiction.
3577    ///
3578    /// This implements reductio ad absurdum:
3579    /// 1. To prove ¬∃x P(x), assume ∃x P(x), derive contradiction, conclude ¬∃x P(x)
3580    /// 2. To prove ¬P, assume P, derive contradiction, conclude ¬P
3581    ///
3582    /// A contradiction is detected when both Q and ¬Q are derivable.
3583    fn try_reductio_ad_absurdum(
3584        &mut self,
3585        goal: &ProofGoal,
3586        depth: usize,
3587    ) -> ProofResult<Option<DerivationTree>> {
3588        // Only apply to negation goals
3589        let assumed = match &goal.target {
3590            ProofExpr::Not(inner) => (**inner).clone(),
3591            _ => return Ok(None),
3592        };
3593
3594        // Aggressive depth limit - reductio is expensive
3595        if depth > 5 {
3596            return Ok(None);
3597        }
3598
3599        // Special handling for existence negation goals: ¬∃x P(x)
3600        // This is crucial for paradoxes like the Barber Paradox
3601        if let ProofExpr::Exists { .. } = &assumed {
3602            return self.try_existence_negation_proof(&goal, &assumed, depth);
3603        }
3604
3605        // For non-existence goals, skip if they contain other quantifiers
3606        // (to avoid infinite loops with universal instantiation)
3607        if self.contains_quantifier(&assumed) {
3608            return Ok(None);
3609        }
3610
3611        // Create a temporary context with the assumption added
3612        let mut extended_context = goal.context.clone();
3613        extended_context.push(assumed.clone());
3614
3615        // Also Skolemize existentials from the assumption (but be careful)
3616        let skolemized = self.skolemize_existential(&assumed);
3617        for sk in &skolemized {
3618            extended_context.push(sk.clone());
3619        }
3620
3621        // Prefer the gapless certifiable contradiction finder: its ⊥-derivation
3622        // certifies end to end (saturation + equality closure + bounded case
3623        // split), so the reductio it feeds becomes a kernel-checked `¬P`. Fall
3624        // back to the heuristic finder only when the certifiable one cannot close.
3625        if let Some(contradiction_proof) =
3626            self.find_certifiable_contradiction(&extended_context, depth)?
3627        {
3628            let assumption_leaf =
3629                DerivationTree::leaf(assumed.clone(), InferenceRule::PremiseMatch);
3630            return Ok(Some(DerivationTree::new(
3631                goal.target.clone(),
3632                InferenceRule::ReductioAdAbsurdum,
3633                vec![assumption_leaf, contradiction_proof],
3634            )));
3635        }
3636
3637        // Look for contradiction in the extended context + KB
3638        // Note: find_contradiction does NOT call prove_goal recursively
3639        if let Some(contradiction_proof) = self.find_contradiction(&extended_context, depth)? {
3640            // Found a contradiction! Build the reductio proof
3641            let assumption_leaf = DerivationTree::leaf(
3642                assumed.clone(),
3643                InferenceRule::PremiseMatch,
3644            );
3645
3646            return Ok(Some(DerivationTree::new(
3647                goal.target.clone(),
3648                InferenceRule::ReductioAdAbsurdum,
3649                vec![assumption_leaf, contradiction_proof],
3650            )));
3651        }
3652
3653        Ok(None)
3654    }
3655
3656    /// Try to prove ¬∃x P(x) by assuming ∃x P(x) and deriving contradiction.
3657    ///
3658    /// This is the core strategy for existence paradoxes like the Barber Paradox.
3659    /// Steps:
3660    /// 1. Assume ∃x P(x)
3661    /// 2. Skolemize to get P(c) for fresh constant c
3662    /// 3. Skolemize KB existentials (definite descriptions) to extract inner structure
3663    /// 4. Abstract event semantics to simple predicates
3664    /// 5. Instantiate universal premises with the Skolem constant
3665    /// 6. Extract uniqueness constraints and derive equalities
3666    /// 7. Look for contradiction (possibly via case analysis)
3667    fn try_existence_negation_proof(
3668        &mut self,
3669        goal: &ProofGoal,
3670        assumed_existence: &ProofExpr,
3671        depth: usize,
3672    ) -> ProofResult<Option<DerivationTree>> {
3673        // Skolemize the assumed existence: ∃x P(x) → P(c)
3674        let witness_facts = self.skolemize_existential(assumed_existence);
3675
3676        if witness_facts.is_empty() {
3677            return Ok(None);
3678        }
3679
3680        // Build extended context with witness facts
3681        let mut extended_context = goal.context.clone();
3682        extended_context.push(assumed_existence.clone());
3683
3684        // Add witness facts, abstracting events
3685        for fact in &witness_facts {
3686            let abstracted = self.abstract_all_events(fact);
3687            if !extended_context.contains(&abstracted) {
3688                extended_context.push(abstracted);
3689            }
3690            if !extended_context.contains(fact) {
3691                extended_context.push(fact.clone());
3692            }
3693        }
3694
3695        // Extract any Skolem constants from the witness facts
3696        let mut skolem_constants = self.extract_skolem_constants(&witness_facts);
3697
3698        // CRITICAL: Skolemize KB existentials to extract definite description structure.
3699        // Natural language "The barber" creates:
3700        // ∃y ((barber(y) ∧ ∀z (barber(z) → z = y)) ∧ ∀x ...)
3701        // We need to Skolemize these to access the inner universals.
3702        let kb_skolemized = self.skolemize_kb_existentials();
3703        for fact in &kb_skolemized {
3704            let abstracted = self.abstract_all_events(fact);
3705            if !extended_context.contains(&abstracted) {
3706                extended_context.push(abstracted);
3707            }
3708            if !extended_context.contains(fact) {
3709                extended_context.push(fact.clone());
3710            }
3711        }
3712
3713        // Extract additional Skolem constants from KB
3714        let kb_skolems = self.extract_skolem_constants(&kb_skolemized);
3715        for sk in kb_skolems {
3716            if !skolem_constants.contains(&sk) {
3717                skolem_constants.push(sk);
3718            }
3719        }
3720
3721        // Also extract unified definite description constants (e.g., "the_barber")
3722        // These are created by unify_definite_descriptions and should be treated like Skolems
3723        for expr in &self.knowledge_base {
3724            self.collect_unified_constants(expr, &mut skolem_constants);
3725        }
3726
3727        // Instantiate universal premises with Skolem constants
3728        let instantiated = self.instantiate_universals_with_constants(
3729            &extended_context,
3730            &skolem_constants,
3731        );
3732        for inst in &instantiated {
3733            let abstracted = self.abstract_all_events(inst);
3734            if !extended_context.contains(&abstracted) {
3735                extended_context.push(abstracted);
3736            }
3737        }
3738
3739        // Also process KB universals
3740        let kb_instantiated = self.instantiate_kb_universals_with_constants(&skolem_constants);
3741        for inst in &kb_instantiated {
3742            let abstracted = self.abstract_all_events(inst);
3743            if !extended_context.contains(&abstracted) {
3744                extended_context.push(abstracted);
3745            }
3746        }
3747
3748        // CRITICAL: Extract uniqueness constraints from definite descriptions
3749        // and derive equalities between Skolem constants and KB witnesses.
3750        // This handles Russell's definite descriptions: "The barber" creates
3751        // ∃y ((barber(y) ∧ ∀z (barber(z) → z = y)) ∧ ...)
3752        let derived_equalities = self.derive_equalities_from_uniqueness_constraints(
3753            &extended_context,
3754            &skolem_constants,
3755        );
3756
3757        // Add derived equalities to context
3758        for eq in &derived_equalities {
3759            if !extended_context.contains(eq) {
3760                extended_context.push(eq.clone());
3761            }
3762        }
3763
3764        // Apply derived equalities to substitute terms throughout context
3765        // This unifies facts about different barbers (sk_0, y, v) into a single entity
3766        let unified_context = self.apply_equalities_to_context(&extended_context, &derived_equalities);
3767
3768        // Look for direct contradiction first (in unified context)
3769        if let Some(contradiction_proof) = self.find_contradiction(&unified_context, depth)? {
3770            let assumption_leaf = DerivationTree::leaf(
3771                assumed_existence.clone(),
3772                InferenceRule::PremiseMatch,
3773            );
3774
3775            return Ok(Some(DerivationTree::new(
3776                goal.target.clone(),
3777                InferenceRule::ReductioAdAbsurdum,
3778                vec![assumption_leaf, contradiction_proof],
3779            )));
3780        }
3781
3782        // Try case analysis for self-referential structures (like Barber Paradox)
3783        if let Some(case_proof) = self.try_case_analysis_contradiction(&unified_context, &skolem_constants, depth)? {
3784            let assumption_leaf = DerivationTree::leaf(
3785                assumed_existence.clone(),
3786                InferenceRule::PremiseMatch,
3787            );
3788
3789            return Ok(Some(DerivationTree::new(
3790                goal.target.clone(),
3791                InferenceRule::ReductioAdAbsurdum,
3792                vec![assumption_leaf, case_proof],
3793            )));
3794        }
3795
3796        Ok(None)
3797    }
3798
3799    /// Skolemize all existential expressions in the KB.
3800    ///
3801    /// This is essential for definite descriptions from natural language.
3802    /// "The barber" creates `∃y ((barber(y) ∧ ∀z (barber(z) → z = y)) ∧ ...)`.
3803    /// We Skolemize to extract the inner structure.
3804    fn skolemize_kb_existentials(&mut self) -> Vec<ProofExpr> {
3805        let mut results = Vec::new();
3806
3807        for expr in &self.knowledge_base.clone() {
3808            if let ProofExpr::Exists { .. } = expr {
3809                let skolemized = self.skolemize_existential(expr);
3810                results.extend(skolemized);
3811            }
3812        }
3813
3814        results
3815    }
3816
3817    // =========================================================================
3818    // EQUATIONAL REASONING FOR DEFINITE DESCRIPTIONS
3819    // =========================================================================
3820
3821    /// Derive equalities from uniqueness constraints in definite descriptions.
3822    ///
3823    /// Given facts like `barber(sk_0)` and uniqueness constraints like
3824    /// `∀z (barber(z) → z = y)`, derive `sk_0 = y`.
3825    ///
3826    /// This is essential for Russell's definite descriptions where
3827    /// "The barber" creates `∃y ((barber(y) ∧ ∀z (barber(z) → z = y)) ∧ ...)`.
3828    fn derive_equalities_from_uniqueness_constraints(
3829        &self,
3830        context: &[ProofExpr],
3831        skolem_constants: &[String],
3832    ) -> Vec<ProofExpr> {
3833        let mut equalities = Vec::new();
3834
3835        // Collect all uniqueness constraints from KB and context
3836        // Pattern: ∀z (P(z) → z = c) where c is a constant/variable
3837        let uniqueness_constraints = self.extract_uniqueness_constraints(context);
3838
3839        // For each Skolem constant, check if it satisfies predicates
3840        // with uniqueness constraints
3841        for skolem in skolem_constants {
3842            for (predicate_name, unique_entity) in &uniqueness_constraints {
3843                // Check if we have predicate(skolem) in context
3844                let skolem_term = ProofTerm::Constant(skolem.clone());
3845                let skolem_satisfies_predicate = context.iter().any(|expr| {
3846                    self.predicate_matches(expr, predicate_name, &skolem_term)
3847                });
3848
3849                if skolem_satisfies_predicate {
3850                    // Derive: skolem = unique_entity
3851                    let equality = ProofExpr::Identity(
3852                        skolem_term.clone(),
3853                        unique_entity.clone(),
3854                    );
3855                    if !equalities.contains(&equality) {
3856                        equalities.push(equality);
3857                    }
3858
3859                    // Also add the symmetric version for easier matching
3860                    let sym_equality = ProofExpr::Identity(
3861                        unique_entity.clone(),
3862                        skolem_term.clone(),
3863                    );
3864                    if !equalities.contains(&sym_equality) {
3865                        equalities.push(sym_equality);
3866                    }
3867                }
3868            }
3869        }
3870
3871        // Derive transitive equalities: if sk_0 = y and sk_0 = v, then y = v
3872        let mut transitive_equalities = Vec::new();
3873        for eq1 in &equalities {
3874            if let ProofExpr::Identity(t1, t2) = eq1 {
3875                for eq2 in &equalities {
3876                    if let ProofExpr::Identity(t3, t4) = eq2 {
3877                        // If t1 = t2 and t1 = t4, then t2 = t4
3878                        if t1 == t3 && t2 != t4 {
3879                            let trans_eq = ProofExpr::Identity(t2.clone(), t4.clone());
3880                            if !equalities.contains(&trans_eq) && !transitive_equalities.contains(&trans_eq) {
3881                                transitive_equalities.push(trans_eq);
3882                            }
3883                        }
3884                        // If t1 = t2 and t3 = t1, then t2 = t3
3885                        if t1 == t4 && t2 != t3 {
3886                            let trans_eq = ProofExpr::Identity(t2.clone(), t3.clone());
3887                            if !equalities.contains(&trans_eq) && !transitive_equalities.contains(&trans_eq) {
3888                                transitive_equalities.push(trans_eq);
3889                            }
3890                        }
3891                    }
3892                }
3893            }
3894        }
3895        equalities.extend(transitive_equalities);
3896
3897        equalities
3898    }
3899
3900    /// Extract uniqueness constraints from context and KB.
3901    ///
3902    /// Looks for patterns like `∀z (P(z) → z = c)` which establish
3903    /// that c is the unique entity satisfying P.
3904    fn extract_uniqueness_constraints(&self, context: &[ProofExpr]) -> Vec<(String, ProofTerm)> {
3905        let mut constraints = Vec::new();
3906
3907        for expr in context.iter().chain(self.knowledge_base.iter()) {
3908            self.extract_uniqueness_from_expr(expr, &mut constraints);
3909        }
3910
3911        constraints
3912    }
3913
3914    /// Recursively extract uniqueness constraints from an expression.
3915    fn extract_uniqueness_from_expr(&self, expr: &ProofExpr, constraints: &mut Vec<(String, ProofTerm)>) {
3916        match expr {
3917            // Direct uniqueness pattern: ∀z (P(z) → z = c)
3918            ProofExpr::ForAll { variable, body } => {
3919                if let ProofExpr::Implies(ante, cons) = body.as_ref() {
3920                    if let ProofExpr::Identity(left, right) = cons.as_ref() {
3921                        // Check if it's "z = c" where z is the quantified variable
3922                        let var_term = ProofTerm::Variable(variable.clone());
3923                        if left == &var_term {
3924                            // Extract the predicate name from the antecedent
3925                            if let Some(pred_name) = self.extract_unary_predicate_name(ante, variable) {
3926                                // right is the unique entity
3927                                constraints.push((pred_name, right.clone()));
3928                            }
3929                        } else if right == &var_term {
3930                            // Check c = z form
3931                            if let Some(pred_name) = self.extract_unary_predicate_name(ante, variable) {
3932                                constraints.push((pred_name, left.clone()));
3933                            }
3934                        }
3935                    }
3936                }
3937                // Recurse into body for nested structures
3938                self.extract_uniqueness_from_expr(body, constraints);
3939            }
3940
3941            // Conjunction: extract from both sides
3942            ProofExpr::And(left, right) => {
3943                self.extract_uniqueness_from_expr(left, constraints);
3944                self.extract_uniqueness_from_expr(right, constraints);
3945            }
3946
3947            // Existential: extract from body (definite descriptions are wrapped in ∃)
3948            ProofExpr::Exists { body, .. } => {
3949                self.extract_uniqueness_from_expr(body, constraints);
3950            }
3951
3952            _ => {}
3953        }
3954    }
3955
3956    /// Extract the predicate name from a unary predicate application.
3957    ///
3958    /// Given P(z) where z is the variable, returns "P".
3959    fn extract_unary_predicate_name(&self, expr: &ProofExpr, var: &str) -> Option<String> {
3960        match expr {
3961            ProofExpr::Predicate { name, args, .. } => {
3962                if args.len() == 1 {
3963                    if let ProofTerm::Variable(v) = &args[0] {
3964                        if v == var {
3965                            return Some(name.clone());
3966                        }
3967                    }
3968                }
3969                None
3970            }
3971            _ => None,
3972        }
3973    }
3974
3975    /// Check if an expression is a predicate with the given name applied to the term.
3976    fn predicate_matches(&self, expr: &ProofExpr, pred_name: &str, term: &ProofTerm) -> bool {
3977        match expr {
3978            ProofExpr::Predicate { name, args, .. } => {
3979                name == pred_name && args.len() == 1 && &args[0] == term
3980            }
3981            _ => false,
3982        }
3983    }
3984
3985    /// Apply derived equalities to substitute terms throughout context.
3986    ///
3987    /// This unifies facts about different entities (sk_0, y, v) by replacing
3988    /// all occurrences with a canonical representative (the first Skolem constant).
3989    fn apply_equalities_to_context(
3990        &self,
3991        context: &[ProofExpr],
3992        equalities: &[ProofExpr],
3993    ) -> Vec<ProofExpr> {
3994        if equalities.is_empty() {
3995            return context.to_vec();
3996        }
3997
3998        // Build a substitution map from equalities
3999        // Use the first term as the canonical representative
4000        let mut substitutions: Vec<(&ProofTerm, &ProofTerm)> = Vec::new();
4001        for eq in equalities {
4002            if let ProofExpr::Identity(t1, t2) = eq {
4003                // Prefer Skolem constants as canonical (they're from our assumption)
4004                if let ProofTerm::Constant(c) = t1 {
4005                    if c.starts_with("sk_") {
4006                        substitutions.push((t2, t1)); // t2 → t1 (Skolem)
4007                        continue;
4008                    }
4009                }
4010                if let ProofTerm::Constant(c) = t2 {
4011                    if c.starts_with("sk_") {
4012                        substitutions.push((t1, t2)); // t1 → t2 (Skolem)
4013                        continue;
4014                    }
4015                }
4016                // Default: first term is canonical
4017                substitutions.push((t2, t1));
4018            }
4019        }
4020
4021        // Apply substitutions to each expression in context
4022        let mut unified_context = Vec::new();
4023        for expr in context {
4024            let mut unified = expr.clone();
4025            for (from, to) in &substitutions {
4026                unified = self.substitute_term_in_expr(&unified, from, to);
4027            }
4028            // Add abstracted version too
4029            let abstracted = self.abstract_all_events(&unified);
4030            if !unified_context.contains(&unified) {
4031                unified_context.push(unified);
4032            }
4033            if !unified_context.contains(&abstracted) {
4034                unified_context.push(abstracted);
4035            }
4036        }
4037
4038        // Also add implications with substituted terms
4039        // This ensures cyclic implications like P(sk,sk) → ¬P(sk,sk) are in context
4040        for expr in context {
4041            if let ProofExpr::ForAll { variable, body } = expr {
4042                if let ProofExpr::Implies(_, _) = body.as_ref() {
4043                    // Find any Skolem constants and instantiate
4044                    for (from, to) in &substitutions {
4045                        if let ProofTerm::Constant(c) = to {
4046                            if c.starts_with("sk_") {
4047                                // Instantiate this universal with the Skolem constant
4048                                let mut subst = Substitution::new();
4049                                subst.insert(variable.clone(), (*to).clone());
4050                                let instantiated = apply_subst_to_expr(body, &subst);
4051                                let abstracted = self.abstract_all_events(&instantiated);
4052                                if !unified_context.contains(&abstracted) {
4053                                    unified_context.push(abstracted);
4054                                }
4055                            }
4056                        }
4057                    }
4058                }
4059            }
4060        }
4061
4062        unified_context
4063    }
4064
4065    /// Extract Skolem constants from a list of expressions.
4066    fn extract_skolem_constants(&self, exprs: &[ProofExpr]) -> Vec<String> {
4067        let mut constants = Vec::new();
4068        for expr in exprs {
4069            self.collect_skolem_constants_from_expr(expr, &mut constants);
4070        }
4071        constants.sort();
4072        constants.dedup();
4073        constants
4074    }
4075
4076    /// Helper to collect Skolem constants from an expression.
4077    fn collect_skolem_constants_from_expr(&self, expr: &ProofExpr, constants: &mut Vec<String>) {
4078        match expr {
4079            ProofExpr::Predicate { args, .. } => {
4080                for arg in args {
4081                    self.collect_skolem_constants_from_term(arg, constants);
4082                }
4083            }
4084            ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) | ProofExpr::Iff(l, r) => {
4085                self.collect_skolem_constants_from_expr(l, constants);
4086                self.collect_skolem_constants_from_expr(r, constants);
4087            }
4088            ProofExpr::Not(inner) => {
4089                self.collect_skolem_constants_from_expr(inner, constants);
4090            }
4091            ProofExpr::Identity(l, r) => {
4092                self.collect_skolem_constants_from_term(l, constants);
4093                self.collect_skolem_constants_from_term(r, constants);
4094            }
4095            ProofExpr::NeoEvent { roles, .. } => {
4096                for (_, term) in roles {
4097                    self.collect_skolem_constants_from_term(term, constants);
4098                }
4099            }
4100            _ => {}
4101        }
4102    }
4103
4104    /// Helper to collect Skolem constants from a term.
4105    /// Collect unified definite description constants (e.g., "the_barber")
4106    /// These are created by unify_definite_descriptions and start with "the_".
4107    fn collect_unified_constants(&self, expr: &ProofExpr, constants: &mut Vec<String>) {
4108        match expr {
4109            ProofExpr::Predicate { args, .. } => {
4110                for arg in args {
4111                    if let ProofTerm::Constant(name) = arg {
4112                        if name.starts_with("the_") && !constants.contains(name) {
4113                            constants.push(name.clone());
4114                        }
4115                    }
4116                }
4117            }
4118            ProofExpr::And(left, right) | ProofExpr::Or(left, right) |
4119            ProofExpr::Implies(left, right) | ProofExpr::Iff(left, right) => {
4120                self.collect_unified_constants(left, constants);
4121                self.collect_unified_constants(right, constants);
4122            }
4123            ProofExpr::Not(inner) => self.collect_unified_constants(inner, constants),
4124            ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
4125                self.collect_unified_constants(body, constants);
4126            }
4127            ProofExpr::Identity(t1, t2) => {
4128                if let ProofTerm::Constant(name) = t1 {
4129                    if name.starts_with("the_") && !constants.contains(name) {
4130                        constants.push(name.clone());
4131                    }
4132                }
4133                if let ProofTerm::Constant(name) = t2 {
4134                    if name.starts_with("the_") && !constants.contains(name) {
4135                        constants.push(name.clone());
4136                    }
4137                }
4138            }
4139            _ => {}
4140        }
4141    }
4142
4143    fn collect_skolem_constants_from_term(&self, term: &ProofTerm, constants: &mut Vec<String>) {
4144        match term {
4145            ProofTerm::Constant(name) if name.starts_with("sk_") => {
4146                constants.push(name.clone());
4147            }
4148            ProofTerm::Function(_, args) => {
4149                for arg in args {
4150                    self.collect_skolem_constants_from_term(arg, constants);
4151                }
4152            }
4153            ProofTerm::Group(terms) => {
4154                for t in terms {
4155                    self.collect_skolem_constants_from_term(t, constants);
4156                }
4157            }
4158            _ => {}
4159        }
4160    }
4161
4162    /// Instantiate universal quantifiers in the context with given constants.
4163    fn instantiate_universals_with_constants(
4164        &self,
4165        context: &[ProofExpr],
4166        constants: &[String],
4167    ) -> Vec<ProofExpr> {
4168        let mut results = Vec::new();
4169
4170        for expr in context {
4171            if let ProofExpr::ForAll { variable, body } = expr {
4172                for constant in constants {
4173                    let mut subst = Substitution::new();
4174                    subst.insert(variable.clone(), ProofTerm::Constant(constant.clone()));
4175                    let instantiated = apply_subst_to_expr(body, &subst);
4176                    results.push(instantiated);
4177                }
4178            }
4179        }
4180
4181        results
4182    }
4183
4184    /// Instantiate universal quantifiers in KB with given constants.
4185    fn instantiate_kb_universals_with_constants(&self, constants: &[String]) -> Vec<ProofExpr> {
4186        let mut results = Vec::new();
4187
4188        for expr in &self.knowledge_base {
4189            if let ProofExpr::ForAll { variable, body } = expr {
4190                for constant in constants {
4191                    let mut subst = Substitution::new();
4192                    subst.insert(variable.clone(), ProofTerm::Constant(constant.clone()));
4193                    let instantiated = apply_subst_to_expr(body, &subst);
4194                    results.push(instantiated);
4195                }
4196            }
4197        }
4198
4199        results
4200    }
4201
4202    // =========================================================================
4203    // CASE ANALYSIS (TERTIUM NON DATUR)
4204    // =========================================================================
4205
4206    /// Try case analysis to derive a contradiction.
4207    ///
4208    /// For self-referential structures like the Barber Paradox:
4209    /// - Split on a predicate P(c, c) where c is a Skolem constant
4210    /// - Case 1: Assume P(c, c), derive ¬P(c, c) → contradiction
4211    /// - Case 2: Assume ¬P(c, c), derive P(c, c) → contradiction
4212    /// Either way we get contradiction (law of excluded middle).
4213    fn try_case_analysis_contradiction(
4214        &mut self,
4215        context: &[ProofExpr],
4216        skolem_constants: &[String],
4217        depth: usize,
4218    ) -> ProofResult<Option<DerivationTree>> {
4219        // Find candidate predicates for case splitting
4220        // Look for self-referential predicates: P(c, c) where c is a Skolem constant
4221        let candidates = self.find_case_split_candidates(context, skolem_constants);
4222
4223        for candidate in candidates {
4224            // Case 1: Assume the candidate is true
4225            let mut context_with_pos = context.to_vec();
4226            if !context_with_pos.contains(&candidate) {
4227                context_with_pos.push(candidate.clone());
4228            }
4229
4230            // Case 2: Assume the candidate is false
4231            let negated = ProofExpr::Not(Box::new(candidate.clone()));
4232            let mut context_with_neg = context.to_vec();
4233            if !context_with_neg.contains(&negated) {
4234                context_with_neg.push(negated.clone());
4235            }
4236
4237            // Try to derive contradiction in both cases
4238            let case1_contradiction = self.find_contradiction(&context_with_pos, depth)?;
4239            let case2_contradiction = self.find_contradiction(&context_with_neg, depth)?;
4240
4241            // If both cases lead to contradiction, we have a proof
4242            if let (Some(case1_proof), Some(case2_proof)) = (case1_contradiction, case2_contradiction) {
4243                // Build the case analysis proof tree
4244                let case1_tree = DerivationTree::new(
4245                    ProofExpr::Atom("⊥".into()),
4246                    InferenceRule::PremiseMatch,
4247                    vec![case1_proof],
4248                );
4249                let case2_tree = DerivationTree::new(
4250                    ProofExpr::Atom("⊥".into()),
4251                    InferenceRule::PremiseMatch,
4252                    vec![case2_proof],
4253                );
4254
4255                return Ok(Some(DerivationTree::new(
4256                    ProofExpr::Atom("⊥".into()),
4257                    InferenceRule::CaseAnalysis {
4258                        case_formula: Box::new(candidate.clone()),
4259                    },
4260                    vec![case1_tree, case2_tree],
4261                )));
4262            }
4263        }
4264
4265        Ok(None)
4266    }
4267
4268    /// Find candidate predicates for case splitting.
4269    ///
4270    /// Looks for:
4271    /// 1. Self-referential predicates: P(c, c) where c is a Skolem constant
4272    /// 2. Predicates that appear in contradictory implications: P → ¬P and ¬P → P
4273    fn find_case_split_candidates(
4274        &self,
4275        context: &[ProofExpr],
4276        skolem_constants: &[String],
4277    ) -> Vec<ProofExpr> {
4278        let mut candidates = Vec::new();
4279
4280        // Strategy 1: Find self-referential predicates P(c, c)
4281        for expr in context {
4282            if let ProofExpr::Predicate { name, args, world } = expr {
4283                // Check if it's a binary predicate with the same Skolem constant twice
4284                if args.len() == 2 {
4285                    if let (ProofTerm::Constant(c1), ProofTerm::Constant(c2)) = (&args[0], &args[1]) {
4286                        if c1 == c2 && skolem_constants.contains(c1) {
4287                            candidates.push(expr.clone());
4288                        }
4289                    }
4290                }
4291            }
4292        }
4293
4294        // Strategy 2: Find predicates involved in cyclic implications
4295        // Look for patterns like: (P → ¬P) ∧ (¬P → P)
4296        let implications: Vec<(ProofExpr, ProofExpr)> = context.iter()
4297            .chain(self.knowledge_base.iter())
4298            .filter_map(|e| {
4299                if let ProofExpr::Implies(ante, cons) = e {
4300                    Some(((**ante).clone(), (**cons).clone()))
4301                } else {
4302                    None
4303                }
4304            })
4305            .collect();
4306
4307        for (ante, cons) in &implications {
4308            // Check for P → ¬P pattern
4309            if let ProofExpr::Not(inner) = cons {
4310                if exprs_structurally_equal(ante, inner) {
4311                    // Found P → ¬P, check if ¬P → P also exists
4312                    let neg_ante = ProofExpr::Not(Box::new(ante.clone()));
4313                    for (a2, c2) in &implications {
4314                        if exprs_structurally_equal(a2, &neg_ante) && exprs_structurally_equal(c2, ante) {
4315                            // Found the cyclic pair - ante is a good candidate
4316                            if !candidates.contains(ante) {
4317                                candidates.push(ante.clone());
4318                            }
4319                        }
4320                    }
4321                }
4322            }
4323        }
4324
4325        // Strategy 3: Generate self-referential predicates for Skolem constants
4326        // For each Skolem constant sk_N, look for predicates P and create P(sk_N, sk_N)
4327        for const_name in skolem_constants {
4328            // Look for action predicates in implications
4329            for expr in context.iter().chain(self.knowledge_base.iter()) {
4330                if let ProofExpr::Implies(ante, cons) = expr {
4331                    // Extract predicate names from consequences
4332                    self.extract_predicate_template(cons, const_name, &mut candidates);
4333                }
4334            }
4335        }
4336
4337        candidates
4338    }
4339
4340    /// Extract a predicate template and instantiate with a Skolem constant.
4341    fn extract_predicate_template(
4342        &self,
4343        expr: &ProofExpr,
4344        skolem: &str,
4345        candidates: &mut Vec<ProofExpr>,
4346    ) {
4347        match expr {
4348            ProofExpr::Predicate { name, args, world } if args.len() == 2 => {
4349                // Create a self-referential version: P(sk, sk)
4350                let self_ref = ProofExpr::Predicate {
4351                    name: name.clone(),
4352                    args: vec![
4353                        ProofTerm::Constant(skolem.to_string()),
4354                        ProofTerm::Constant(skolem.to_string()),
4355                    ],
4356                    world: world.clone(),
4357                };
4358                if !candidates.contains(&self_ref) {
4359                    candidates.push(self_ref);
4360                }
4361            }
4362            ProofExpr::Not(inner) => {
4363                self.extract_predicate_template(inner, skolem, candidates);
4364            }
4365            ProofExpr::NeoEvent { verb, .. } => {
4366                // Create abstracted predicate version
4367                let self_ref = ProofExpr::Predicate {
4368                    name: verb.to_lowercase(),
4369                    args: vec![
4370                        ProofTerm::Constant(skolem.to_string()),
4371                        ProofTerm::Constant(skolem.to_string()),
4372                    ],
4373                    world: None,
4374                };
4375                if !candidates.contains(&self_ref) {
4376                    candidates.push(self_ref);
4377                }
4378            }
4379            _ => {}
4380        }
4381    }
4382
4383    // =========================================================================
4384    // STRATEGY 5d: EXISTENTIAL ELIMINATION
4385    // =========================================================================
4386
4387    /// Try to eliminate existential quantifiers from premises.
4388    ///
4389    /// For each ∃x P(x) in the KB or context:
4390    /// 1. Generate a fresh Skolem constant c
4391    /// 2. Add P(c) to the context
4392    /// 3. Abstract any event semantics to simple predicates
4393    /// 4. Try to prove the goal with the extended context
4394    fn try_existential_elimination(
4395        &mut self,
4396        goal: &ProofGoal,
4397        depth: usize,
4398    ) -> ProofResult<Option<DerivationTree>> {
4399        // Depth guard to prevent infinite loops
4400        if depth > 8 {
4401            return Ok(None);
4402        }
4403
4404        // Find existential expressions in KB and context
4405        let existentials: Vec<ProofExpr> = self.knowledge_base.iter()
4406            .chain(goal.context.iter())
4407            .filter(|e| matches!(e, ProofExpr::Exists { .. }))
4408            .cloned()
4409            .collect();
4410
4411        if existentials.is_empty() {
4412            return Ok(None);
4413        }
4414
4415        // Try eliminating each existential
4416        for exist_expr in existentials {
4417            // Open each existential at most once per branch — re-opening it with a
4418            // fresh witness adds no facts the first opening did not, and only feeds
4419            // the depth-limit/oracle loop.
4420            if self
4421                .eliminated_existentials
4422                .iter()
4423                .any(|e| exprs_structurally_equal(e, &exist_expr))
4424            {
4425                continue;
4426            }
4427
4428            // Skolemize a single level, keeping the witness constant `c` so the
4429            // certificate's `Ex`-match binds the SAME constant the body is proved
4430            // over (the certifier mirrors this one elimination as one match).
4431            let Some((witness_const, witness_facts)) =
4432                self.skolemize_existential_with_const(&exist_expr)
4433            else {
4434                continue;
4435            };
4436
4437            if witness_facts.is_empty() {
4438                continue;
4439            }
4440
4441            // Abstract event semantics in witness facts
4442            let abstracted_facts: Vec<ProofExpr> = witness_facts.iter()
4443                .map(|f| self.abstract_all_events(f))
4444                .collect();
4445
4446            // Build extended context with witness facts
4447            let mut extended_context = goal.context.clone();
4448            for fact in &abstracted_facts {
4449                if !extended_context.contains(fact) {
4450                    extended_context.push(fact.clone());
4451                }
4452            }
4453
4454            // Also add the original witness facts (in case abstraction changes things)
4455            for fact in &witness_facts {
4456                if !extended_context.contains(fact) {
4457                    extended_context.push(fact.clone());
4458                }
4459            }
4460
4461            // Try to prove the goal with the extended context
4462            let extended_goal = ProofGoal::with_context(goal.target.clone(), extended_context);
4463
4464            // Mark this existential opened for the duration of the inner search,
4465            // then restore so sibling branches may open it themselves.
4466            self.eliminated_existentials.push(exist_expr.clone());
4467            let inner_result = self.prove_goal(extended_goal, depth + 1);
4468            self.eliminated_existentials.pop();
4469
4470            if let Ok(inner_proof) = inner_result {
4471                // The existential premise is discharged from the KB/context, so a
4472                // `PremiseMatch` leaf carrying `∃x.P(x)` is its derivation. The
4473                // certifier consumes `[existential, body]` and produces a single
4474                // `Ex`-match bound to `witness_const`, with the witness body's
4475                // conjuncts available to the body proof as projected hypotheses.
4476                let existential_premise = DerivationTree::leaf(
4477                    exist_expr.clone(),
4478                    InferenceRule::PremiseMatch,
4479                );
4480
4481                return Ok(Some(DerivationTree::new(
4482                    goal.target.clone(),
4483                    InferenceRule::ExistentialElim { witness: witness_const },
4484                    vec![existential_premise, inner_proof],
4485                )));
4486            }
4487        }
4488
4489        Ok(None)
4490    }
4491
4492    /// Check if an expression contains quantifiers.
4493    fn contains_quantifier(&self, expr: &ProofExpr) -> bool {
4494        match expr {
4495            ProofExpr::ForAll { .. } | ProofExpr::Exists { .. } => true,
4496            ProofExpr::And(l, r)
4497            | ProofExpr::Or(l, r)
4498            | ProofExpr::Implies(l, r)
4499            | ProofExpr::Iff(l, r) => self.contains_quantifier(l) || self.contains_quantifier(r),
4500            ProofExpr::Not(inner) => self.contains_quantifier(inner),
4501            _ => false,
4502        }
4503    }
4504
4505    /// Skolemize an existential expression.
4506    ///
4507    /// Given ∃x P(x), introduce a fresh Skolem constant c and return P(c).
4508    /// For nested structures like ∃x((type(x) ∧ unique(x)) ∧ prop(x)),
4509    /// we extract the predicates with the Skolem constant.
4510    fn skolemize_existential(&mut self, expr: &ProofExpr) -> Vec<ProofExpr> {
4511        let mut results = Vec::new();
4512
4513        if let ProofExpr::Exists { variable, body } = expr {
4514            // Generate a fresh Skolem constant
4515            let skolem = format!("sk_{}", self.fresh_var());
4516
4517            // Apply substitution to the body
4518            let mut subst = Substitution::new();
4519            subst.insert(variable.clone(), ProofTerm::Constant(skolem.clone()));
4520
4521            let instantiated = apply_subst_to_expr(body, &subst);
4522
4523            // Flatten conjunctions into separate facts
4524            self.flatten_conjunction(&instantiated, &mut results);
4525
4526            // Handle nested existentials in the result
4527            let mut i = 0;
4528            while i < results.len() {
4529                if let ProofExpr::Exists { .. } = &results[i] {
4530                    let nested = results.remove(i);
4531                    let nested_skolem = self.skolemize_existential(&nested);
4532                    results.extend(nested_skolem);
4533                } else {
4534                    i += 1;
4535                }
4536            }
4537        }
4538
4539        results
4540    }
4541
4542    /// Skolemize a single level of an existential, exposing the witness constant.
4543    ///
4544    /// Given `∃x.P(x)`, introduce a fresh Skolem constant `c` and return
4545    /// `(c, [conjuncts of P(c)])` — the body instantiated at `c` and flattened
4546    /// into its top-level conjuncts. Unlike [`skolemize_existential`], this does
4547    /// NOT recurse into nested existentials in the result: it eliminates exactly
4548    /// one quantifier so the certifier can mirror it with a single `Ex`-match
4549    /// bound to `c`. The returned constant is recorded as the `ExistentialElim`
4550    /// witness so the certificate's Match-bound variable lines up with the facts
4551    /// the body proof references.
4552    fn skolemize_existential_with_const(&mut self, expr: &ProofExpr) -> Option<(String, Vec<ProofExpr>)> {
4553        if let ProofExpr::Exists { variable, body } = expr {
4554            let skolem = format!("sk_{}", self.fresh_var());
4555            let mut subst = Substitution::new();
4556            subst.insert(variable.clone(), ProofTerm::Constant(skolem.clone()));
4557            let instantiated = apply_subst_to_expr(body, &subst);
4558            let mut results = Vec::new();
4559            self.flatten_conjunction(&instantiated, &mut results);
4560            Some((skolem, results))
4561        } else {
4562            None
4563        }
4564    }
4565
4566    /// Flatten a conjunction into a list of its components.
4567    fn flatten_conjunction(&self, expr: &ProofExpr, results: &mut Vec<ProofExpr>) {
4568        match expr {
4569            ProofExpr::And(left, right) => {
4570                self.flatten_conjunction(left, results);
4571                self.flatten_conjunction(right, results);
4572            }
4573            other => results.push(other.clone()),
4574        }
4575    }
4576
4577    // =========================================================================
4578    // DEFINITE DESCRIPTION SIMPLIFICATION
4579    // =========================================================================
4580
4581    /// Check if a predicate is a tautological identity check: name(name)
4582    /// This occurs when parsing "the butler" creates butler(butler)
4583    fn is_tautological_identity(&self, expr: &ProofExpr) -> bool {
4584        if let ProofExpr::Predicate { name, args, .. } = expr {
4585            args.len() == 1 && matches!(
4586                &args[0],
4587                ProofTerm::Constant(c) | ProofTerm::BoundVarRef(c) | ProofTerm::Variable(c) if c == name
4588            )
4589        } else {
4590            false
4591        }
4592    }
4593
4594    /// Simplify conjunction by removing tautological identity predicates.
4595    /// (butler(butler) ∧ P) → P when butler is a constant
4596    fn simplify_definite_description_conjunction(&self, expr: &ProofExpr) -> ProofExpr {
4597        match expr {
4598            ProofExpr::And(left, right) => {
4599                // First simplify children
4600                let left_simplified = self.simplify_definite_description_conjunction(left);
4601                let right_simplified = self.simplify_definite_description_conjunction(right);
4602
4603                // Remove tautological identities from the conjunction
4604                if self.is_tautological_identity(&left_simplified) {
4605                    return right_simplified;
4606                }
4607                if self.is_tautological_identity(&right_simplified) {
4608                    return left_simplified;
4609                }
4610
4611                ProofExpr::And(
4612                    Box::new(left_simplified),
4613                    Box::new(right_simplified),
4614                )
4615            }
4616            ProofExpr::Or(left, right) => ProofExpr::Or(
4617                Box::new(self.simplify_definite_description_conjunction(left)),
4618                Box::new(self.simplify_definite_description_conjunction(right)),
4619            ),
4620            ProofExpr::Implies(left, right) => ProofExpr::Implies(
4621                Box::new(self.simplify_definite_description_conjunction(left)),
4622                Box::new(self.simplify_definite_description_conjunction(right)),
4623            ),
4624            ProofExpr::Iff(left, right) => ProofExpr::Iff(
4625                Box::new(self.simplify_definite_description_conjunction(left)),
4626                Box::new(self.simplify_definite_description_conjunction(right)),
4627            ),
4628            ProofExpr::Not(inner) => ProofExpr::Not(
4629                Box::new(self.simplify_definite_description_conjunction(inner)),
4630            ),
4631            ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
4632                variable: variable.clone(),
4633                body: Box::new(self.simplify_definite_description_conjunction(body)),
4634            },
4635            ProofExpr::Exists { variable, body } => ProofExpr::Exists {
4636                variable: variable.clone(),
4637                body: Box::new(self.simplify_definite_description_conjunction(body)),
4638            },
4639            ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
4640                operator: operator.clone(),
4641                body: Box::new(self.simplify_definite_description_conjunction(body)),
4642            },
4643            ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
4644                operator: operator.clone(),
4645                left: Box::new(self.simplify_definite_description_conjunction(left)),
4646                right: Box::new(self.simplify_definite_description_conjunction(right)),
4647            },
4648            _ => expr.clone(),
4649        }
4650    }
4651
4652    // =========================================================================
4653    // EVENT SEMANTICS ABSTRACTION
4654    // =========================================================================
4655
4656    /// Abstract Neo-Davidsonian event semantics to simple predicates.
4657    ///
4658    /// Converts: ∃e(Shave(e) ∧ Agent(e, x) ∧ Theme(e, y)) → shaves(x, y)
4659    ///
4660    /// This allows the proof engine to reason about events using simpler
4661    /// predicate logic, which is essential for paradoxes like the Barber Paradox.
4662    fn abstract_event_to_predicate(&self, expr: &ProofExpr) -> Option<ProofExpr> {
4663        match expr {
4664            // Direct NeoEvent abstraction
4665            ProofExpr::NeoEvent { verb, roles, .. } => {
4666                // Extract Agent and Theme/Patient roles
4667                let agent = roles.iter()
4668                    .find(|(role, _)| role == "Agent")
4669                    .map(|(_, term)| term.clone());
4670
4671                let theme = roles.iter()
4672                    .find(|(role, _)| role == "Theme" || role == "Patient")
4673                    .map(|(_, term)| term.clone());
4674
4675                // Build a simple predicate: verb(agent, theme) or verb(agent)
4676                let mut args = Vec::new();
4677                if let Some(a) = agent {
4678                    args.push(a);
4679                }
4680                if let Some(t) = theme {
4681                    args.push(t);
4682                }
4683
4684                // Lowercase the verb for predicate naming convention
4685                let pred_name = verb.to_lowercase();
4686
4687                Some(ProofExpr::Predicate {
4688                    name: pred_name,
4689                    args,
4690                    world: None,
4691                })
4692            }
4693
4694            // Handle Exists wrapping an event expression
4695            ProofExpr::Exists { variable, body } => {
4696                // Check if this is an event quantification
4697                if !self.is_event_variable(variable) {
4698                    return None;
4699                }
4700
4701                // Try direct NeoEvent abstraction
4702                if let Some(abstracted) = self.abstract_event_to_predicate(body) {
4703                    return Some(abstracted);
4704                }
4705
4706                // Try to parse conjunction of event predicates
4707                // Pattern: ∃e(Verb(e) ∧ Agent(e, x) ∧ Theme(e, y)) → verb(x, y)
4708                if let Some(abstracted) = self.abstract_event_conjunction(variable, body) {
4709                    return Some(abstracted);
4710                }
4711
4712                None
4713            }
4714
4715            _ => None,
4716        }
4717    }
4718
4719    /// Abstract a conjunction of event predicates to a simple predicate.
4720    ///
4721    /// Handles: Verb(e) ∧ Agent(e, x) ∧ Theme(e, y) → verb(x, y)
4722    fn abstract_event_conjunction(&self, event_var: &str, body: &ProofExpr) -> Option<ProofExpr> {
4723        // Flatten the conjunction to get all components
4724        let mut components = Vec::new();
4725        self.flatten_conjunction(body, &mut components);
4726
4727        // Find verb predicate (single arg that matches event_var)
4728        let mut verb_name: Option<String> = None;
4729        let mut agent: Option<ProofTerm> = None;
4730        let mut theme: Option<ProofTerm> = None;
4731
4732        for comp in &components {
4733            if let ProofExpr::Predicate { name, args, .. } = comp {
4734                // Check if first arg is the event variable
4735                let first_is_event = args.first().map_or(false, |arg| {
4736                    matches!(arg, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == event_var)
4737                });
4738
4739                if !first_is_event && args.len() == 1 {
4740                    // Single arg predicate that's the event var
4741                    if let Some(ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v)) = args.first() {
4742                        if v == event_var {
4743                            verb_name = Some(name.clone());
4744                            continue;
4745                        }
4746                    }
4747                }
4748
4749                if first_is_event {
4750                    match name.as_str() {
4751                        "Agent" if args.len() == 2 => {
4752                            agent = Some(args[1].clone());
4753                        }
4754                        "Theme" | "Patient" if args.len() == 2 => {
4755                            theme = Some(args[1].clone());
4756                        }
4757                        _ if args.len() == 1 && verb_name.is_none() => {
4758                            // This is probably the verb predicate: Verb(e)
4759                            verb_name = Some(name.clone());
4760                        }
4761                        _ => {}
4762                    }
4763                }
4764            }
4765        }
4766
4767        // If we found a verb, construct the simple predicate
4768        if let Some(verb) = verb_name {
4769            let mut args = Vec::new();
4770            if let Some(a) = agent {
4771                args.push(a);
4772            }
4773            if let Some(t) = theme {
4774                args.push(t);
4775            }
4776
4777            return Some(ProofExpr::Predicate {
4778                name: verb.to_lowercase(),
4779                args,
4780                world: None,
4781            });
4782        }
4783
4784        None
4785    }
4786
4787    /// Check if a variable name looks like an event variable.
4788    ///
4789    /// Event variables are typically named "e", "e1", "e2", etc.
4790    fn is_event_variable(&self, var: &str) -> bool {
4791        var == "e" || var.starts_with("e_") ||
4792        (var.starts_with('e') && var.len() == 2 && var.chars().nth(1).map_or(false, |c| c.is_ascii_digit()))
4793    }
4794
4795    /// Recursively abstract all events in an expression.
4796    ///
4797    /// This transforms the entire expression tree, replacing event semantics
4798    /// with simple predicates wherever possible. `pub(crate)` so the
4799    /// verify/certify boundary can register hypotheses and the goal in the
4800    /// SAME abstracted language the search runs in.
4801    pub(crate) fn abstract_all_events(&self, expr: &ProofExpr) -> ProofExpr {
4802        // First try direct abstraction
4803        if let Some(abstracted) = self.abstract_event_to_predicate(expr) {
4804            return abstracted;
4805        }
4806
4807        // Otherwise recurse into the structure
4808        match expr {
4809            ProofExpr::And(left, right) => ProofExpr::And(
4810                Box::new(self.abstract_all_events(left)),
4811                Box::new(self.abstract_all_events(right)),
4812            ),
4813            ProofExpr::Or(left, right) => ProofExpr::Or(
4814                Box::new(self.abstract_all_events(left)),
4815                Box::new(self.abstract_all_events(right)),
4816            ),
4817            ProofExpr::Implies(left, right) => ProofExpr::Implies(
4818                Box::new(self.abstract_all_events(left)),
4819                Box::new(self.abstract_all_events(right)),
4820            ),
4821            ProofExpr::Iff(left, right) => ProofExpr::Iff(
4822                Box::new(self.abstract_all_events(left)),
4823                Box::new(self.abstract_all_events(right)),
4824            ),
4825            ProofExpr::Not(inner) => {
4826                // Apply De Morgan for quantifiers: ¬∃x.P ≡ ∀x.¬P
4827                // This normalization is crucial for efficient proof search
4828                // (Converting negated existentials to universals helps the prover)
4829                if let ProofExpr::Exists { variable, body } = inner.as_ref() {
4830                    return ProofExpr::ForAll {
4831                        variable: variable.clone(),
4832                        body: Box::new(self.abstract_all_events(&ProofExpr::Not(body.clone()))),
4833                    };
4834                }
4835                // Note: We do NOT convert ¬∀x.P to ∃x.¬P because the prover
4836                // works better with universal quantifiers for backward chaining.
4837                ProofExpr::Not(Box::new(self.abstract_all_events(inner)))
4838            }
4839            ProofExpr::ForAll { variable, body } => {
4840                // Check for pattern: ∀x ¬(P ∧ Q) → ∀x (P → ¬Q)
4841                // This converts to implication form for better backward chaining
4842                if let ProofExpr::Not(inner) = body.as_ref() {
4843                    if let ProofExpr::And(left, right) = inner.as_ref() {
4844                        return ProofExpr::ForAll {
4845                            variable: variable.clone(),
4846                            body: Box::new(ProofExpr::Implies(
4847                                Box::new(self.abstract_all_events(left)),
4848                                Box::new(self.abstract_all_events(&ProofExpr::Not(right.clone()))),
4849                            )),
4850                        };
4851                    }
4852                }
4853                ProofExpr::ForAll {
4854                    variable: variable.clone(),
4855                    body: Box::new(self.abstract_all_events(body)),
4856                }
4857            }
4858            ProofExpr::Exists { variable, body } => {
4859                // Check if this is an event quantification that should be abstracted
4860                if self.is_event_variable(variable) {
4861                    if let Some(abstracted) = self.abstract_event_to_predicate(body) {
4862                        return abstracted;
4863                    }
4864                }
4865                // Otherwise keep the existential and recurse
4866                ProofExpr::Exists {
4867                    variable: variable.clone(),
4868                    body: Box::new(self.abstract_all_events(body)),
4869                }
4870            }
4871            // For other expressions, return as-is
4872            other => other.clone(),
4873        }
4874    }
4875
4876    /// Abstract event semantics WITHOUT applying De Morgan transformations.
4877    ///
4878    /// This is used for goals where we want to preserve the ¬∃ pattern
4879    /// for reductio ad absurdum strategies.
4880    fn abstract_events_only(&self, expr: &ProofExpr) -> ProofExpr {
4881        // First try direct abstraction
4882        if let Some(abstracted) = self.abstract_event_to_predicate(expr) {
4883            return abstracted;
4884        }
4885
4886        // Otherwise recurse into the structure
4887        match expr {
4888            ProofExpr::And(left, right) => ProofExpr::And(
4889                Box::new(self.abstract_events_only(left)),
4890                Box::new(self.abstract_events_only(right)),
4891            ),
4892            ProofExpr::Or(left, right) => ProofExpr::Or(
4893                Box::new(self.abstract_events_only(left)),
4894                Box::new(self.abstract_events_only(right)),
4895            ),
4896            ProofExpr::Implies(left, right) => ProofExpr::Implies(
4897                Box::new(self.abstract_events_only(left)),
4898                Box::new(self.abstract_events_only(right)),
4899            ),
4900            ProofExpr::Iff(left, right) => ProofExpr::Iff(
4901                Box::new(self.abstract_events_only(left)),
4902                Box::new(self.abstract_events_only(right)),
4903            ),
4904            ProofExpr::Not(inner) => {
4905                // Just recurse, no De Morgan transformation
4906                ProofExpr::Not(Box::new(self.abstract_events_only(inner)))
4907            }
4908            ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
4909                variable: variable.clone(),
4910                body: Box::new(self.abstract_events_only(body)),
4911            },
4912            ProofExpr::Exists { variable, body } => {
4913                // Check if this is an event quantification that should be abstracted
4914                if self.is_event_variable(variable) {
4915                    if let Some(abstracted) = self.abstract_event_to_predicate(body) {
4916                        return abstracted;
4917                    }
4918                }
4919                // Otherwise keep the existential and recurse
4920                ProofExpr::Exists {
4921                    variable: variable.clone(),
4922                    body: Box::new(self.abstract_events_only(body)),
4923                }
4924            }
4925            // For other expressions, return as-is
4926            other => other.clone(),
4927        }
4928    }
4929
4930    /// Look for a contradiction in the knowledge base and context.
4931    ///
4932    /// A contradiction exists when both P and ¬P are derivable.
4933    /// Find a contradiction in KB + context and return a **gapless** derivation
4934    /// the certifier can turn into a kernel proof of `False`.
4935    ///
4936    /// Unlike the heuristic [`find_contradiction`], every step here is justified
4937    /// by an explicit rule — `PremiseMatch`, `UniversalInst`, `ModusPonens`,
4938    /// `Contradiction` — so the resulting tree certifies end-to-end. It forward-
4939    /// chains over Horn-style rules (`A → C` and `∀x. A(x) → C(x)`) until some
4940    /// proposition `P` and its negation `¬P` are both derivable.
4941    ///
4942    /// This powers verified conflict detection on clean predicate rule sets. The
4943    /// heuristic finder remains for the paradox/reductio path (which needs a
4944    /// separate gapless-emission pass before it can certify).
4945    fn find_certifiable_contradiction(
4946        &self,
4947        context: &[ProofExpr],
4948        _depth: usize,
4949    ) -> ProofResult<Option<DerivationTree>> {
4950        let all: Vec<ProofExpr> = self
4951            .knowledge_base
4952            .iter()
4953            .chain(context.iter())
4954            .cloned()
4955            .collect();
4956        let rules = cert_extract_rules(&all);
4957        let seed = cert_seed_facts(&all);
4958        // `depth` bounds nested case splits / DPLL decisions. A small grid's
4959        // determined cells resolve by propagation within a few decisions; kept
4960        // moderate so the per-cell solve stays fast.
4961        let mut fresh = 0u32;
4962        Ok(cert_derive_falsum(&rules, &seed, 6, &mut fresh))
4963    }
4964
4965    fn find_contradiction(
4966        &mut self,
4967        context: &[ProofExpr],
4968        depth: usize,
4969    ) -> ProofResult<Option<DerivationTree>> {
4970        // Collect all expressions from KB and context
4971        let all_exprs: Vec<ProofExpr> = self.knowledge_base.iter()
4972            .chain(context.iter())
4973            .cloned()
4974            .collect();
4975
4976        // Strategy 1: Look for direct P and ¬P pairs
4977        for expr in &all_exprs {
4978            if let ProofExpr::Not(inner) = expr {
4979                // We have ¬P, check if P exists directly
4980                for other in &all_exprs {
4981                    if exprs_structurally_equal(other, inner) {
4982                        // Found both P and ¬P directly
4983                        let pos_leaf = DerivationTree::leaf(
4984                            (**inner).clone(),
4985                            InferenceRule::PremiseMatch,
4986                        );
4987                        let neg_leaf = DerivationTree::leaf(
4988                            expr.clone(),
4989                            InferenceRule::PremiseMatch,
4990                        );
4991                        return Ok(Some(DerivationTree::new(
4992                            ProofExpr::Atom("⊥".into()),
4993                            InferenceRule::Contradiction,
4994                            vec![pos_leaf, neg_leaf],
4995                        )));
4996                    }
4997                }
4998            }
4999        }
5000
5001        // Strategy 2: Look for implications that derive contradictory results
5002        // Check if context fact P triggers P → ¬P (immediate contradiction)
5003        // Or if P triggers P → Q where ¬Q is also in context
5004        // Note: Extract implications from both top-level and inside ForAll quantifiers
5005        let mut implications: Vec<(ProofExpr, ProofExpr)> = Vec::new();
5006        for e in &all_exprs {
5007            if let ProofExpr::Implies(ante, cons) = e {
5008                implications.push(((**ante).clone(), (**cons).clone()));
5009            }
5010            // Also extract from inside ForAll (important for barber paradox!)
5011            if let ProofExpr::ForAll { body, .. } = e {
5012                if let ProofExpr::Implies(ante, cons) = body.as_ref() {
5013                    implications.push(((**ante).clone(), (**cons).clone()));
5014                }
5015            }
5016        }
5017
5018        // For each fact in the context, see if it triggers contradictory implications
5019        for fact in context {
5020            // Find all implications where fact matches the antecedent
5021            let mut derivable_consequences: Vec<ProofExpr> = Vec::new();
5022
5023            for (ante, cons) in &implications {
5024                // Try to unify the antecedent with the fact
5025                if let Ok(subst) = unify_exprs(fact, ante) {
5026                    let instantiated_cons = apply_subst_to_expr(cons, &subst);
5027                    derivable_consequences.push(instantiated_cons);
5028                }
5029
5030                // Also try matching conjunctive antecedents with multiple facts
5031                if let ProofExpr::And(left, right) = ante {
5032                    // Try to find facts matching both parts of the conjunction
5033                    if let Some(subst) = self.try_match_conjunction_antecedent(
5034                        left, right, &all_exprs
5035                    ) {
5036                        let instantiated_cons = apply_subst_to_expr(cons, &subst);
5037                        if !derivable_consequences.contains(&instantiated_cons) {
5038                            derivable_consequences.push(instantiated_cons);
5039                        }
5040                    }
5041                }
5042            }
5043
5044            // Check if any derived consequence contradicts the triggering fact
5045            for cons in &derivable_consequences {
5046                // Check if cons = ¬fact (the classic barber structure: P → ¬P)
5047                if let ProofExpr::Not(inner) = cons {
5048                    if exprs_structurally_equal(inner, fact) {
5049                        // fact triggered an implication that derives ¬fact
5050                        // This is a contradiction: fact ∧ ¬fact
5051                        let pos_leaf = DerivationTree::leaf(
5052                            fact.clone(),
5053                            InferenceRule::PremiseMatch,
5054                        );
5055                        let neg_leaf = DerivationTree::leaf(
5056                            cons.clone(),
5057                            InferenceRule::ModusPonens,
5058                        );
5059                        return Ok(Some(DerivationTree::new(
5060                            ProofExpr::Atom("⊥".into()),
5061                            InferenceRule::Contradiction,
5062                            vec![pos_leaf, neg_leaf],
5063                        )));
5064                    }
5065                }
5066
5067                // Check if cons contradicts any other fact in context
5068                for other in context {
5069                    if std::ptr::eq(fact as *const _, other as *const _) {
5070                        continue; // Skip the triggering fact itself
5071                    }
5072                    // Check if cons = ¬other
5073                    if let ProofExpr::Not(inner) = cons {
5074                        if exprs_structurally_equal(inner, other) {
5075                            let pos_leaf = DerivationTree::leaf(
5076                                other.clone(),
5077                                InferenceRule::PremiseMatch,
5078                            );
5079                            let neg_leaf = DerivationTree::leaf(
5080                                cons.clone(),
5081                                InferenceRule::ModusPonens,
5082                            );
5083                            return Ok(Some(DerivationTree::new(
5084                                ProofExpr::Atom("⊥".into()),
5085                                InferenceRule::Contradiction,
5086                                vec![pos_leaf, neg_leaf],
5087                            )));
5088                        }
5089                    }
5090                    // Check if other = ¬cons
5091                    if let ProofExpr::Not(inner_other) = other {
5092                        if exprs_structurally_equal(inner_other, cons) {
5093                            let pos_leaf = DerivationTree::leaf(
5094                                cons.clone(),
5095                                InferenceRule::ModusPonens,
5096                            );
5097                            let neg_leaf = DerivationTree::leaf(
5098                                other.clone(),
5099                                InferenceRule::PremiseMatch,
5100                            );
5101                            return Ok(Some(DerivationTree::new(
5102                                ProofExpr::Atom("⊥".into()),
5103                                InferenceRule::Contradiction,
5104                                vec![pos_leaf, neg_leaf],
5105                            )));
5106                        }
5107                    }
5108                }
5109            }
5110
5111            // Check if any pair of consequences contradicts each other
5112            for i in 0..derivable_consequences.len() {
5113                for j in (i + 1)..derivable_consequences.len() {
5114                    let cons1 = &derivable_consequences[i];
5115                    let cons2 = &derivable_consequences[j];
5116
5117                    // Check if cons1 = ¬cons2 or cons2 = ¬cons1
5118                    if let ProofExpr::Not(inner1) = cons1 {
5119                        if exprs_structurally_equal(inner1, cons2) {
5120                            // cons1 = ¬cons2, contradiction!
5121                            let pos_leaf = DerivationTree::leaf(
5122                                cons2.clone(),
5123                                InferenceRule::ModusPonens,
5124                            );
5125                            let neg_leaf = DerivationTree::leaf(
5126                                cons1.clone(),
5127                                InferenceRule::ModusPonens,
5128                            );
5129                            return Ok(Some(DerivationTree::new(
5130                                ProofExpr::Atom("⊥".into()),
5131                                InferenceRule::Contradiction,
5132                                vec![pos_leaf, neg_leaf],
5133                            )));
5134                        }
5135                    }
5136                    if let ProofExpr::Not(inner2) = cons2 {
5137                        if exprs_structurally_equal(inner2, cons1) {
5138                            // cons2 = ¬cons1, contradiction!
5139                            let pos_leaf = DerivationTree::leaf(
5140                                cons1.clone(),
5141                                InferenceRule::ModusPonens,
5142                            );
5143                            let neg_leaf = DerivationTree::leaf(
5144                                cons2.clone(),
5145                                InferenceRule::ModusPonens,
5146                            );
5147                            return Ok(Some(DerivationTree::new(
5148                                ProofExpr::Atom("⊥".into()),
5149                                InferenceRule::Contradiction,
5150                                vec![pos_leaf, neg_leaf],
5151                            )));
5152                        }
5153                    }
5154                }
5155            }
5156        }
5157
5158        // Strategy 3: Try to find self-referential contradictions (like Barber Paradox)
5159        if let Some(proof) = self.find_self_referential_contradiction(context, depth)? {
5160            return Ok(Some(proof));
5161        }
5162
5163        Ok(None)
5164    }
5165
5166    /// Try to match a conjunctive antecedent with facts in the context.
5167    ///
5168    /// For an antecedent like (man(z) ∧ shave(z,z)), we need to find facts
5169    /// that match both parts with consistent variable bindings.
5170    fn try_match_conjunction_antecedent(
5171        &self,
5172        left: &ProofExpr,
5173        right: &ProofExpr,
5174        facts: &[ProofExpr],
5175    ) -> Option<Substitution> {
5176        // Try to find a fact that matches the left part
5177        for fact1 in facts {
5178            if let Ok(subst1) = unify_exprs(fact1, left) {
5179                // Apply this substitution to the right part
5180                let instantiated_right = apply_subst_to_expr(right, &subst1);
5181                // Now look for a fact that matches the instantiated right part
5182                for fact2 in facts {
5183                    if let Ok(subst2) = unify_exprs(fact2, &instantiated_right) {
5184                        // Combine substitutions
5185                        let mut combined = subst1.clone();
5186                        for (k, v) in subst2.iter() {
5187                            combined.insert(k.clone(), v.clone());
5188                        }
5189                        return Some(combined);
5190                    }
5191                }
5192            }
5193        }
5194        // Also try right then left
5195        for fact1 in facts {
5196            if let Ok(subst1) = unify_exprs(fact1, right) {
5197                let instantiated_left = apply_subst_to_expr(left, &subst1);
5198                for fact2 in facts {
5199                    if let Ok(subst2) = unify_exprs(fact2, &instantiated_left) {
5200                        let mut combined = subst1.clone();
5201                        for (k, v) in subst2.iter() {
5202                            combined.insert(k.clone(), v.clone());
5203                        }
5204                        return Some(combined);
5205                    }
5206                }
5207            }
5208        }
5209        None
5210    }
5211
5212    /// Special case: find self-referential contradictions (like the Barber Paradox).
5213    ///
5214    /// Pattern: If we have ∀x(P(x) → Q(b, x)) and ∀x(P(x) → ¬Q(b, x)),
5215    /// then for x = b with P(b), we get Q(b, b) ∧ ¬Q(b, b).
5216    ///
5217    /// This uses direct pattern matching WITHOUT recursive prove_goal calls
5218    /// to avoid infinite recursion.
5219    fn find_self_referential_contradiction(
5220        &mut self,
5221        context: &[ProofExpr],
5222        _depth: usize,
5223    ) -> ProofResult<Option<DerivationTree>> {
5224        // Collect all expressions from KB and context
5225        let all_exprs: Vec<ProofExpr> = self.knowledge_base.iter()
5226            .chain(context.iter())
5227            .cloned()
5228            .collect();
5229
5230        // Look for pairs of universal implications with contradictory conclusions
5231        // that can be instantiated with the same witness
5232        for expr1 in &all_exprs {
5233            if let ProofExpr::ForAll { variable: var1, body: body1 } = expr1 {
5234                if let ProofExpr::Implies(ante1, cons1) = body1.as_ref() {
5235                    for expr2 in &all_exprs {
5236                        if std::ptr::eq(expr1, expr2) {
5237                            continue; // Skip same expression
5238                        }
5239                        if let ProofExpr::ForAll { variable: var2, body: body2 } = expr2 {
5240                            if let ProofExpr::Implies(ante2, cons2) = body2.as_ref() {
5241                                // Check if cons2 = ¬cons1 (structurally)
5242                                if let ProofExpr::Not(neg_cons2) = cons2.as_ref() {
5243                                    // Check if cons1 and neg_cons2 have matching structure
5244                                    // For barber: cons1 = shaves(barber, x), neg_cons2 = shaves(barber, x)
5245
5246                                    // Try instantiating with x = barber (the self-referential case)
5247                                    // We look for constant terms in cons1 that could be witnesses
5248                                    let witnesses = self.extract_constants_from_expr(cons1);
5249
5250                                    for witness_name in &witnesses {
5251                                        let witness = ProofTerm::Constant(witness_name.clone());
5252
5253                                        // Instantiate both antecedents and consequents with this witness
5254                                        let mut subst1 = Substitution::new();
5255                                        subst1.insert(var1.clone(), witness.clone());
5256                                        let ante1_inst = apply_subst_to_expr(ante1, &subst1);
5257                                        let cons1_inst = apply_subst_to_expr(cons1, &subst1);
5258
5259                                        let mut subst2 = Substitution::new();
5260                                        subst2.insert(var2.clone(), witness.clone());
5261                                        let ante2_inst = apply_subst_to_expr(ante2, &subst2);
5262                                        let cons2_inst = apply_subst_to_expr(cons2, &subst2);
5263
5264                                        // Check if cons1_inst and ¬cons2_inst contradict
5265                                        // cons2_inst should be ¬X where X = cons1_inst
5266                                        if let ProofExpr::Not(inner2) = &cons2_inst {
5267                                            if exprs_structurally_equal(&cons1_inst, inner2) {
5268                                                // Now check if both antecedents could hold
5269                                                // ante1 typically is ¬P(x,x) and ante2 is P(x,x)
5270                                                // These are complementary - one must hold
5271                                                // For the paradox, we consider BOTH cases
5272
5273                                                // If ante1 = ¬P(x,x) and ante2 = P(x,x), and x = witness,
5274                                                // we have a tertium non datur case:
5275                                                // - Either P(w,w) holds → cons2_inst = ¬cons1_inst
5276                                                // - Or ¬P(w,w) holds → cons1_inst
5277
5278                                                // Check if ante1 and ante2 are complements
5279                                                if self.are_complements(&ante1_inst, &ante2_inst) {
5280                                                    // By excluded middle, one antecedent holds
5281                                                    // If cons1_inst and cons2_inst = ¬cons1_inst,
5282                                                    // we have a contradiction
5283                                                    let pos_leaf = DerivationTree::leaf(
5284                                                        cons1_inst.clone(),
5285                                                        InferenceRule::ModusPonens,
5286                                                    );
5287                                                    let neg_leaf = DerivationTree::leaf(
5288                                                        cons2_inst,
5289                                                        InferenceRule::ModusPonens,
5290                                                    );
5291                                                    return Ok(Some(DerivationTree::new(
5292                                                        ProofExpr::Atom("⊥".into()),
5293                                                        InferenceRule::Contradiction,
5294                                                        vec![pos_leaf, neg_leaf],
5295                                                    )));
5296                                                }
5297                                            }
5298                                        }
5299                                    }
5300                                }
5301                            }
5302                        }
5303                    }
5304                }
5305            }
5306        }
5307
5308        Ok(None)
5309    }
5310
5311    /// Check if two expressions are complements (one is the negation of the other).
5312    fn are_complements(&self, expr1: &ProofExpr, expr2: &ProofExpr) -> bool {
5313        // Check if expr1 = ¬expr2
5314        if let ProofExpr::Not(inner1) = expr1 {
5315            if exprs_structurally_equal(inner1, expr2) {
5316                return true;
5317            }
5318        }
5319        // Check if expr2 = ¬expr1
5320        if let ProofExpr::Not(inner2) = expr2 {
5321            if exprs_structurally_equal(inner2, expr1) {
5322                return true;
5323            }
5324        }
5325        false
5326    }
5327
5328    /// Extract constant names from an expression.
5329    fn extract_constants_from_expr(&self, expr: &ProofExpr) -> Vec<String> {
5330        let mut constants = Vec::new();
5331        self.extract_constants_recursive(expr, &mut constants);
5332        constants
5333    }
5334
5335    fn extract_constants_recursive(&self, expr: &ProofExpr, constants: &mut Vec<String>) {
5336        match expr {
5337            ProofExpr::Predicate { args, .. } => {
5338                for arg in args {
5339                    self.extract_constants_from_term_recursive(arg, constants);
5340                }
5341            }
5342            ProofExpr::Identity(l, r) => {
5343                self.extract_constants_from_term_recursive(l, constants);
5344                self.extract_constants_from_term_recursive(r, constants);
5345            }
5346            ProofExpr::And(l, r)
5347            | ProofExpr::Or(l, r)
5348            | ProofExpr::Implies(l, r)
5349            | ProofExpr::Iff(l, r) => {
5350                self.extract_constants_recursive(l, constants);
5351                self.extract_constants_recursive(r, constants);
5352            }
5353            ProofExpr::Not(inner) => {
5354                self.extract_constants_recursive(inner, constants);
5355            }
5356            ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
5357                self.extract_constants_recursive(body, constants);
5358            }
5359            _ => {}
5360        }
5361    }
5362
5363    fn extract_constants_from_term_recursive(&self, term: &ProofTerm, constants: &mut Vec<String>) {
5364        match term {
5365            ProofTerm::Constant(name) => {
5366                if !constants.contains(name) {
5367                    constants.push(name.clone());
5368                }
5369            }
5370            ProofTerm::Function(_, args) => {
5371                for arg in args {
5372                    self.extract_constants_from_term_recursive(arg, constants);
5373                }
5374            }
5375            ProofTerm::Group(terms) => {
5376                for t in terms {
5377                    self.extract_constants_from_term_recursive(t, constants);
5378                }
5379            }
5380            _ => {}
5381        }
5382    }
5383
5384    // =========================================================================
5385    // STRATEGY 6: EQUALITY REWRITING (LEIBNIZ'S LAW)
5386    // =========================================================================
5387
5388    /// Try rewriting using equalities in the knowledge base.
5389    ///
5390    /// Leibniz's Law: If a = b and P(a), then P(b).
5391    /// Also handles symmetry (a = b ⊢ b = a) and transitivity (a = b, b = c ⊢ a = c).
5392    fn try_equality_rewrite(
5393        &mut self,
5394        goal: &ProofGoal,
5395        depth: usize,
5396    ) -> ProofResult<Option<DerivationTree>> {
5397        // Collect equalities from KB and context
5398        let equalities: Vec<(ProofTerm, ProofTerm)> = self
5399            .knowledge_base
5400            .iter()
5401            .chain(goal.context.iter())
5402            .filter_map(|expr| {
5403                if let ProofExpr::Identity(l, r) = expr {
5404                    Some((l.clone(), r.clone()))
5405                } else {
5406                    None
5407                }
5408            })
5409            .collect();
5410
5411        if equalities.is_empty() {
5412            return Ok(None);
5413        }
5414
5415        // Handle special case: goal is itself an equality (symmetry/transitivity)
5416        if let ProofExpr::Identity(goal_l, goal_r) = &goal.target {
5417            // Try symmetry: a = b ⊢ b = a
5418            if let Some(tree) = self.try_equality_symmetry(goal_l, goal_r, &equalities, depth)? {
5419                return Ok(Some(tree));
5420            }
5421
5422            // Try transitivity: a = b, b = c ⊢ a = c
5423            if let Some(tree) = self.try_equality_transitivity(goal_l, goal_r, &equalities, depth)? {
5424                return Ok(Some(tree));
5425            }
5426
5427            // Try equational rewriting: use axioms to rewrite LHS step by step
5428            // Only if we have depth budget remaining (prevents infinite recursion)
5429            if depth + 3 < self.max_depth {
5430                if let Some(tree) = self.try_equational_identity_rewrite(goal, goal_l, goal_r, depth)? {
5431                    return Ok(Some(tree));
5432                }
5433            }
5434
5435            // General congruence closure: `F(a)=F(b)` from `a=b`, closed through
5436            // transitivity and nested applications ("equals added to equals").
5437            if let Some(tree) = self.try_congruence(goal)? {
5438                return Ok(Some(tree));
5439            }
5440
5441            return Ok(None);
5442        }
5443
5444        // Try rewriting: substitute one term for another (for non-Identity goals)
5445        for (eq_from, eq_to) in &equalities {
5446            // Try forward: a = b, P(a) ⊢ P(b)
5447            if let Some(tree) = self.try_rewrite_with_equality(
5448                goal, eq_from, eq_to, depth,
5449            )? {
5450                return Ok(Some(tree));
5451            }
5452
5453            // Try backward: a = b, P(b) ⊢ P(a)
5454            if let Some(tree) = self.try_rewrite_with_equality(
5455                goal, eq_to, eq_from, depth,
5456            )? {
5457                return Ok(Some(tree));
5458            }
5459        }
5460
5461        Ok(None)
5462    }
5463
5464    /// Try to prove goal by substituting `from` with `to` in some known fact.
5465    fn try_rewrite_with_equality(
5466        &mut self,
5467        goal: &ProofGoal,
5468        from: &ProofTerm,
5469        to: &ProofTerm,
5470        depth: usize,
5471    ) -> ProofResult<Option<DerivationTree>> {
5472        // Create the "source" expression by substituting `to` with `from` in the goal
5473        // If goal is P(b) and we have a = b, we want to find P(a)
5474        let source_goal = self.substitute_term_in_expr(&goal.target, to, from);
5475
5476        // Check if source_goal differs from the goal (substitution had effect)
5477        if source_goal == goal.target {
5478            return Ok(None);
5479        }
5480
5481        // Try to prove the source goal
5482        let source_proof_goal = ProofGoal::with_context(source_goal.clone(), goal.context.clone());
5483        if let Ok(source_proof) = self.prove_goal(source_proof_goal, depth + 1) {
5484            // Also need a proof of the equality
5485            let equality = ProofExpr::Identity(from.clone(), to.clone());
5486            let eq_proof_goal = ProofGoal::with_context(equality.clone(), goal.context.clone());
5487
5488            if let Ok(eq_proof) = self.prove_goal(eq_proof_goal, depth + 1) {
5489                return Ok(Some(DerivationTree::new(
5490                    goal.target.clone(),
5491                    InferenceRule::Rewrite {
5492                        from: from.clone(),
5493                        to: to.clone(),
5494                    },
5495                    vec![eq_proof, source_proof],
5496                )));
5497            }
5498        }
5499
5500        Ok(None)
5501    }
5502
5503    /// Try equality symmetry: a = b ⊢ b = a
5504    fn try_equality_symmetry(
5505        &mut self,
5506        goal_l: &ProofTerm,
5507        goal_r: &ProofTerm,
5508        equalities: &[(ProofTerm, ProofTerm)],
5509        _depth: usize,
5510    ) -> ProofResult<Option<DerivationTree>> {
5511        // Check if we have r = l in KB (so we can derive l = r)
5512        for (eq_l, eq_r) in equalities {
5513            if eq_l == goal_r && eq_r == goal_l {
5514                // Found r = l, can derive l = r by symmetry
5515                let source = ProofExpr::Identity(goal_r.clone(), goal_l.clone());
5516                return Ok(Some(DerivationTree::new(
5517                    ProofExpr::Identity(goal_l.clone(), goal_r.clone()),
5518                    InferenceRule::EqualitySymmetry,
5519                    vec![DerivationTree::leaf(source, InferenceRule::PremiseMatch)],
5520                )));
5521            }
5522        }
5523        Ok(None)
5524    }
5525
5526    /// Prove an `Identity` goal by congruence closure over the known equalities
5527    /// (knowledge base + local context). Decision and proof reconstruction live in
5528    /// [`cert_congruence_path`]; every emitted node certifies, so a spurious
5529    /// congruence would fail the kernel rather than admit a false equation.
5530    fn try_congruence(&mut self, goal: &ProofGoal) -> ProofResult<Option<DerivationTree>> {
5531        let ProofExpr::Identity(lhs, rhs) = &goal.target else {
5532            return Ok(None);
5533        };
5534        let hyps: Vec<(ProofExpr, DerivationTree)> = self
5535            .knowledge_base
5536            .iter()
5537            .chain(goal.context.iter())
5538            .filter_map(|e| match e {
5539                ProofExpr::Identity(l, r) => Some((
5540                    e.clone(),
5541                    DerivationTree::leaf(
5542                        ProofExpr::Identity(l.clone(), r.clone()),
5543                        InferenceRule::PremiseMatch,
5544                    ),
5545                )),
5546                _ => None,
5547            })
5548            .collect();
5549        if hyps.is_empty() {
5550            return Ok(None);
5551        }
5552        Ok(cert_congruence_path(lhs, rhs, &hyps))
5553    }
5554
5555    /// Prove an inequality goal `a ≤ b` (encoded `le(a, b) = true`) by chaining the
5556    /// known `≤` facts transitively. The directed-graph search and proof
5557    /// reconstruction live in [`cert_le_path`]; every node certifies via the
5558    /// `le_trans`/`le_refl` axioms, so a spurious chain fails the kernel.
5559    fn try_linarith(&mut self, goal: &ProofGoal, depth: usize) -> ProofResult<Option<DerivationTree>> {
5560        let Some((ga, gb)) = as_le_pair(&goal.target) else {
5561            return Ok(None);
5562        };
5563        // Ground case: both operands are literals — decided by computation. `le m n`
5564        // reduces to `true`/`false`, so `m ≤ n` closes by reflexivity exactly when it
5565        // holds (and is left unproven, soundly, when it does not).
5566        if let (Some(av), Some(bv)) = (as_int_literal(&ga), as_int_literal(&gb)) {
5567            return Ok((av <= bv)
5568                .then(|| DerivationTree::leaf(goal.target.clone(), InferenceRule::Reflexivity)));
5569        }
5570        // Addition: `a + c ≤ b + d` from `a ≤ b` and `c ≤ d`, each proved recursively
5571        // (a Farkas primitive — `le_add_mono`).
5572        if let (ProofTerm::Function(fl, la), ProofTerm::Function(fr, ra)) = (&ga, &gb) {
5573            if fl == "add" && fr == "add" && la.len() == 2 && ra.len() == 2 {
5574                let g0 =
5575                    ProofGoal::with_context(le_eq(la[0].clone(), ra[0].clone()), goal.context.clone());
5576                let g1 =
5577                    ProofGoal::with_context(le_eq(la[1].clone(), ra[1].clone()), goal.context.clone());
5578                if let (Ok(p0), Ok(p1)) =
5579                    (self.prove_goal(g0, depth + 1), self.prove_goal(g1, depth + 1))
5580                {
5581                    return Ok(Some(DerivationTree::new(
5582                        goal.target.clone(),
5583                        InferenceRule::LeAddMono,
5584                        vec![p0, p1],
5585                    )));
5586                }
5587            }
5588        }
5589        let mut adj: std::collections::HashMap<String, Vec<(ProofTerm, DerivationTree)>> =
5590            std::collections::HashMap::new();
5591        for e in goal.context.iter().chain(self.knowledge_base.iter()) {
5592            if let Some((x, y)) = as_le_pair(e) {
5593                if let Some(xk) = term_skey(&x) {
5594                    let proof = DerivationTree::leaf(e.clone(), InferenceRule::PremiseMatch);
5595                    adj.entry(xk).or_default().push((y, proof));
5596                }
5597            }
5598        }
5599        Ok(cert_le_path(&ga, &gb, &adj))
5600    }
5601
5602    /// Try equality transitivity: a = b, b = c ⊢ a = c
5603    fn try_equality_transitivity(
5604        &mut self,
5605        goal_l: &ProofTerm,
5606        goal_r: &ProofTerm,
5607        equalities: &[(ProofTerm, ProofTerm)],
5608        _depth: usize,
5609    ) -> ProofResult<Option<DerivationTree>> {
5610        // Look for a = b and b = c where we want a = c
5611        for (eq1_l, eq1_r) in equalities {
5612            if eq1_l == goal_l {
5613                // Found a = b, now look for b = c
5614                for (eq2_l, eq2_r) in equalities {
5615                    if eq2_l == eq1_r && eq2_r == goal_r {
5616                        // Found a = b and b = c, derive a = c
5617                        let premise1 = ProofExpr::Identity(eq1_l.clone(), eq1_r.clone());
5618                        let premise2 = ProofExpr::Identity(eq2_l.clone(), eq2_r.clone());
5619                        return Ok(Some(DerivationTree::new(
5620                            ProofExpr::Identity(goal_l.clone(), goal_r.clone()),
5621                            InferenceRule::EqualityTransitivity,
5622                            vec![
5623                                DerivationTree::leaf(premise1, InferenceRule::PremiseMatch),
5624                                DerivationTree::leaf(premise2, InferenceRule::PremiseMatch),
5625                            ],
5626                        )));
5627                    }
5628                }
5629            }
5630        }
5631        Ok(None)
5632    }
5633
5634    /// Try equational rewriting for Identity goals.
5635    ///
5636    /// For a goal `f(a) = b`, find an axiom `f(x) = g(x)` that matches,
5637    /// rewrite to get `g(a) = b`, and recursively prove that.
5638    fn try_equational_identity_rewrite(
5639        &mut self,
5640        goal: &ProofGoal,
5641        goal_l: &ProofTerm,
5642        goal_r: &ProofTerm,
5643        depth: usize,
5644    ) -> ProofResult<Option<DerivationTree>> {
5645        // First, try congruence: if both sides have the same outermost function/ctor,
5646        // recursively prove the arguments are equal.
5647        if let (
5648            ProofTerm::Function(name_l, args_l),
5649            ProofTerm::Function(name_r, args_r),
5650        ) = (goal_l, goal_r)
5651        {
5652            if name_l == name_r && args_l.len() == args_r.len() {
5653                // Prove each differing argument equal; identical arguments need no
5654                // rewrite. (`None` marks a position left untouched.)
5655                let mut arg_proofs: Vec<Option<DerivationTree>> = Vec::new();
5656                let mut all_ok = true;
5657                for (arg_l, arg_r) in args_l.iter().zip(args_r.iter()) {
5658                    if arg_l == arg_r {
5659                        arg_proofs.push(None);
5660                        continue;
5661                    }
5662                    let arg_goal_expr = ProofExpr::Identity(arg_l.clone(), arg_r.clone());
5663                    let arg_goal = ProofGoal::with_context(arg_goal_expr, goal.context.clone());
5664                    match self.prove_goal(arg_goal, depth + 1) {
5665                        Ok(proof) => arg_proofs.push(Some(proof)),
5666                        Err(_) => {
5667                            all_ok = false;
5668                            break;
5669                        }
5670                    }
5671                }
5672                if all_ok {
5673                    // Congruence: `f(a₀…) = f(b₀…)`, each differing argument rewritten
5674                    // in turn via `Rewrite` (Leibniz) — a real certificate, not a
5675                    // reflexivity placeholder that ignores the argument proofs.
5676                    return Ok(Some(build_congruence_proof(
5677                        name_l, args_l, args_r, &arg_proofs,
5678                    )));
5679                }
5680            }
5681        }
5682        // Collect Identity axioms from KB
5683        let axioms: Vec<(ProofTerm, ProofTerm)> = self
5684            .knowledge_base
5685            .iter()
5686            .filter_map(|e| {
5687                if let ProofExpr::Identity(l, r) = e {
5688                    Some((l.clone(), r.clone()))
5689                } else {
5690                    None
5691                }
5692            })
5693            .collect();
5694
5695        // Try each axiom to rewrite the goal's LHS
5696        for (axiom_l, axiom_r) in &axioms {
5697            // Rename variables in axiom to avoid capture (use same map for both sides!)
5698            let mut var_map = std::collections::HashMap::new();
5699            let renamed_l = self.rename_term_vars_with_map(axiom_l, &mut var_map);
5700            let renamed_r = self.rename_term_vars_with_map(axiom_r, &mut var_map);
5701
5702            // Try to unify axiom LHS with goal LHS
5703            // e.g., unify(Add(Succ(k), n), Add(Succ(Zero), Succ(Zero)))
5704            //       => {k: Zero, n: Succ(Zero)}
5705            if let Ok(subst) = unify_terms(&renamed_l, goal_l) {
5706                // Apply substitution to axiom RHS to get the rewritten term
5707                let rewritten = self.apply_subst_to_term(&renamed_r, &subst);
5708
5709                // First check: does rewritten equal goal_r directly?
5710                if terms_structurally_equal(&rewritten, goal_r) {
5711                    // Direct match! Build the proof
5712                    let axiom_expr = ProofExpr::Identity(axiom_l.clone(), axiom_r.clone());
5713                    return Ok(Some(DerivationTree::new(
5714                        goal.target.clone(),
5715                        InferenceRule::Rewrite {
5716                            from: goal_l.clone(),
5717                            to: rewritten,
5718                        },
5719                        vec![DerivationTree::leaf(axiom_expr, InferenceRule::PremiseMatch)],
5720                    )));
5721                }
5722
5723                // Otherwise, create a new goal with the rewritten LHS
5724                let new_goal_expr = ProofExpr::Identity(rewritten.clone(), goal_r.clone());
5725                let new_goal = ProofGoal::with_context(new_goal_expr.clone(), goal.context.clone());
5726
5727                // Recursively try to prove the new goal
5728                if let Ok(sub_proof) = self.prove_goal(new_goal, depth + 1) {
5729                    // Success! Build the full proof
5730                    let axiom_expr = ProofExpr::Identity(axiom_l.clone(), axiom_r.clone());
5731                    return Ok(Some(DerivationTree::new(
5732                        goal.target.clone(),
5733                        InferenceRule::Rewrite {
5734                            from: goal_l.clone(),
5735                            to: rewritten,
5736                        },
5737                        vec![
5738                            DerivationTree::leaf(axiom_expr, InferenceRule::PremiseMatch),
5739                            sub_proof,
5740                        ],
5741                    )));
5742                }
5743            }
5744        }
5745
5746        Ok(None)
5747    }
5748
5749    /// Rename variables in a term to fresh names (consistently).
5750    fn rename_term_vars(&mut self, term: &ProofTerm) -> ProofTerm {
5751        let mut var_map = std::collections::HashMap::new();
5752        self.rename_term_vars_with_map(term, &mut var_map)
5753    }
5754
5755    fn rename_term_vars_with_map(
5756        &mut self,
5757        term: &ProofTerm,
5758        var_map: &mut std::collections::HashMap<String, String>,
5759    ) -> ProofTerm {
5760        match term {
5761            ProofTerm::Variable(name) => {
5762                // Check if we've already renamed this variable
5763                if let Some(fresh) = var_map.get(name) {
5764                    ProofTerm::Variable(fresh.clone())
5765                } else {
5766                    // Create fresh name and remember it
5767                    let fresh = format!("_v{}", self.var_counter);
5768                    self.var_counter += 1;
5769                    var_map.insert(name.clone(), fresh.clone());
5770                    ProofTerm::Variable(fresh)
5771                }
5772            }
5773            ProofTerm::Function(name, args) => {
5774                ProofTerm::Function(
5775                    name.clone(),
5776                    args.iter().map(|a| self.rename_term_vars_with_map(a, var_map)).collect(),
5777                )
5778            }
5779            ProofTerm::Group(terms) => {
5780                ProofTerm::Group(
5781                    terms.iter().map(|t| self.rename_term_vars_with_map(t, var_map)).collect(),
5782                )
5783            }
5784            other => other.clone(),
5785        }
5786    }
5787
5788    /// Apply a substitution to a term.
5789    fn apply_subst_to_term(&self, term: &ProofTerm, subst: &Substitution) -> ProofTerm {
5790        match term {
5791            ProofTerm::Variable(name) => {
5792                if let Some(replacement) = subst.get(name) {
5793                    replacement.clone()
5794                } else {
5795                    term.clone()
5796                }
5797            }
5798            ProofTerm::Function(name, args) => {
5799                ProofTerm::Function(
5800                    name.clone(),
5801                    args.iter().map(|a| self.apply_subst_to_term(a, subst)).collect(),
5802                )
5803            }
5804            ProofTerm::Group(terms) => {
5805                ProofTerm::Group(terms.iter().map(|t| self.apply_subst_to_term(t, subst)).collect())
5806            }
5807            other => other.clone(),
5808        }
5809    }
5810
5811    /// Substitute a term for another in an expression.
5812    /// Substitute only FREE occurrences of a variable: binders that shadow the
5813    /// name (∀x, ∃x, λx) leave their bodies untouched. Used to bind anaphoric
5814    /// free variables (discourse-telescoped definite descriptions) without
5815    /// capturing unrelated bound variables that share the name.
5816    fn substitute_free_var_in_expr(
5817        &self,
5818        expr: &ProofExpr,
5819        var_name: &str,
5820        to: &ProofTerm,
5821    ) -> ProofExpr {
5822        fn in_term(term: &ProofTerm, var_name: &str, to: &ProofTerm) -> ProofTerm {
5823            match term {
5824                ProofTerm::Variable(v) if v == var_name => to.clone(),
5825                ProofTerm::Function(name, args) => ProofTerm::Function(
5826                    name.clone(),
5827                    args.iter().map(|a| in_term(a, var_name, to)).collect(),
5828                ),
5829                ProofTerm::Group(terms) => ProofTerm::Group(
5830                    terms.iter().map(|t| in_term(t, var_name, to)).collect(),
5831                ),
5832                other => other.clone(),
5833            }
5834        }
5835
5836        match expr {
5837            ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
5838                name: name.clone(),
5839                args: args.iter().map(|a| in_term(a, var_name, to)).collect(),
5840                world: world.clone(),
5841            },
5842            ProofExpr::Identity(l, r) => ProofExpr::Identity(
5843                in_term(l, var_name, to),
5844                in_term(r, var_name, to),
5845            ),
5846            ProofExpr::NeoEvent { event_var, verb, roles } => ProofExpr::NeoEvent {
5847                event_var: event_var.clone(),
5848                verb: verb.clone(),
5849                roles: roles
5850                    .iter()
5851                    .map(|(role, t)| (role.clone(), in_term(t, var_name, to)))
5852                    .collect(),
5853            },
5854            ProofExpr::And(l, r) => ProofExpr::And(
5855                Box::new(self.substitute_free_var_in_expr(l, var_name, to)),
5856                Box::new(self.substitute_free_var_in_expr(r, var_name, to)),
5857            ),
5858            ProofExpr::Or(l, r) => ProofExpr::Or(
5859                Box::new(self.substitute_free_var_in_expr(l, var_name, to)),
5860                Box::new(self.substitute_free_var_in_expr(r, var_name, to)),
5861            ),
5862            ProofExpr::Implies(l, r) => ProofExpr::Implies(
5863                Box::new(self.substitute_free_var_in_expr(l, var_name, to)),
5864                Box::new(self.substitute_free_var_in_expr(r, var_name, to)),
5865            ),
5866            ProofExpr::Iff(l, r) => ProofExpr::Iff(
5867                Box::new(self.substitute_free_var_in_expr(l, var_name, to)),
5868                Box::new(self.substitute_free_var_in_expr(r, var_name, to)),
5869            ),
5870            ProofExpr::Not(inner) => ProofExpr::Not(Box::new(
5871                self.substitute_free_var_in_expr(inner, var_name, to),
5872            )),
5873            ProofExpr::ForAll { variable, body } if variable != var_name => ProofExpr::ForAll {
5874                variable: variable.clone(),
5875                body: Box::new(self.substitute_free_var_in_expr(body, var_name, to)),
5876            },
5877            ProofExpr::Exists { variable, body } if variable != var_name => ProofExpr::Exists {
5878                variable: variable.clone(),
5879                body: Box::new(self.substitute_free_var_in_expr(body, var_name, to)),
5880            },
5881            ProofExpr::Lambda { variable, body } if variable != var_name => ProofExpr::Lambda {
5882                variable: variable.clone(),
5883                body: Box::new(self.substitute_free_var_in_expr(body, var_name, to)),
5884            },
5885            ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
5886                domain: domain.clone(),
5887                force: *force,
5888                flavor: flavor.clone(),
5889                body: Box::new(self.substitute_free_var_in_expr(body, var_name, to)),
5890            },
5891            ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
5892                operator: operator.clone(),
5893                body: Box::new(self.substitute_free_var_in_expr(body, var_name, to)),
5894            },
5895            ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
5896                operator: operator.clone(),
5897                left: Box::new(self.substitute_free_var_in_expr(left, var_name, to)),
5898                right: Box::new(self.substitute_free_var_in_expr(right, var_name, to)),
5899            },
5900            // Shadowing binders (the guards above failed) and atoms pass through.
5901            other => other.clone(),
5902        }
5903    }
5904
5905    fn substitute_term_in_expr(
5906        &self,
5907        expr: &ProofExpr,
5908        from: &ProofTerm,
5909        to: &ProofTerm,
5910    ) -> ProofExpr {
5911        match expr {
5912            ProofExpr::Predicate { name, args, world } => {
5913                let new_args: Vec<_> = args
5914                    .iter()
5915                    .map(|arg| self.substitute_in_term(arg, from, to))
5916                    .collect();
5917                ProofExpr::Predicate {
5918                    name: name.clone(),
5919                    args: new_args,
5920                    world: world.clone(),
5921                }
5922            }
5923            ProofExpr::Identity(l, r) => ProofExpr::Identity(
5924                self.substitute_in_term(l, from, to),
5925                self.substitute_in_term(r, from, to),
5926            ),
5927            ProofExpr::And(l, r) => ProofExpr::And(
5928                Box::new(self.substitute_term_in_expr(l, from, to)),
5929                Box::new(self.substitute_term_in_expr(r, from, to)),
5930            ),
5931            ProofExpr::Or(l, r) => ProofExpr::Or(
5932                Box::new(self.substitute_term_in_expr(l, from, to)),
5933                Box::new(self.substitute_term_in_expr(r, from, to)),
5934            ),
5935            ProofExpr::Implies(l, r) => ProofExpr::Implies(
5936                Box::new(self.substitute_term_in_expr(l, from, to)),
5937                Box::new(self.substitute_term_in_expr(r, from, to)),
5938            ),
5939            ProofExpr::Not(inner) => {
5940                ProofExpr::Not(Box::new(self.substitute_term_in_expr(inner, from, to)))
5941            }
5942            ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
5943                variable: variable.clone(),
5944                body: Box::new(self.substitute_term_in_expr(body, from, to)),
5945            },
5946            ProofExpr::Exists { variable, body } => ProofExpr::Exists {
5947                variable: variable.clone(),
5948                body: Box::new(self.substitute_term_in_expr(body, from, to)),
5949            },
5950            // For other expressions, return as-is
5951            other => other.clone(),
5952        }
5953    }
5954
5955    /// Substitute a term for another in a ProofTerm.
5956    fn substitute_in_term(
5957        &self,
5958        term: &ProofTerm,
5959        from: &ProofTerm,
5960        to: &ProofTerm,
5961    ) -> ProofTerm {
5962        if term == from {
5963            return to.clone();
5964        }
5965        match term {
5966            ProofTerm::Function(name, args) => {
5967                let new_args: Vec<_> = args
5968                    .iter()
5969                    .map(|arg| self.substitute_in_term(arg, from, to))
5970                    .collect();
5971                ProofTerm::Function(name.clone(), new_args)
5972            }
5973            ProofTerm::Group(terms) => {
5974                let new_terms: Vec<_> = terms
5975                    .iter()
5976                    .map(|t| self.substitute_in_term(t, from, to))
5977                    .collect();
5978                ProofTerm::Group(new_terms)
5979            }
5980            other => other.clone(),
5981        }
5982    }
5983
5984    // =========================================================================
5985    // STRATEGY 7: STRUCTURAL INDUCTION
5986    // =========================================================================
5987
5988    /// Try structural induction on inductive types (Nat, List, etc.).
5989    ///
5990    /// First attempts to infer the motive using Miller pattern unification
5991    /// (`?Motive(#n) = Goal` → `?Motive = λn.Goal`). Falls back to crude
5992    /// substitution if pattern unification fails.
5993    ///
5994    /// When the goal contains a TypedVar like `n:Nat`, we split into:
5995    /// - Base case: P(Zero)
5996    /// - Step case: ∀k. P(k) → P(Succ(k))
5997    fn try_structural_induction(
5998        &mut self,
5999        goal: &ProofGoal,
6000        depth: usize,
6001    ) -> ProofResult<Option<DerivationTree>> {
6002        // Look for TypedVar in the goal
6003        if let Some((var_name, typename)) = self.find_typed_var(&goal.target) {
6004            // Try motive inference via pattern unification first
6005            if let Some(motive) = self.try_infer_motive(&goal.target, &var_name) {
6006                match typename.as_str() {
6007                    "Nat" => {
6008                        if let Ok(Some(proof)) =
6009                            self.try_nat_induction_with_motive(goal, &var_name, &motive, depth)
6010                        {
6011                            return Ok(Some(proof));
6012                        }
6013                    }
6014                    "List" => {
6015                        // TODO: Add try_list_induction_with_motive
6016                    }
6017                    _ => {}
6018                }
6019            }
6020
6021            // Fallback: crude substitution approach
6022            match typename.as_str() {
6023                "Nat" => self.try_nat_induction(goal, &var_name, depth),
6024                "List" => self.try_list_induction(goal, &var_name, depth),
6025                _ => Ok(None), // Unknown inductive type
6026            }
6027        } else {
6028            Ok(None)
6029        }
6030    }
6031
6032    /// Try to infer the induction motive using Miller pattern unification.
6033    ///
6034    /// Given a goal like `Add(n:Nat, Zero) = n:Nat`, creates the pattern
6035    /// `?Motive(#n) = Goal` and solves for `?Motive = λn. Goal`.
6036    fn try_infer_motive(&self, goal: &ProofExpr, var_name: &str) -> Option<ProofExpr> {
6037        // Create the pattern: ?Motive(#var_name)
6038        let motive_hole = ProofExpr::Hole("Motive".to_string());
6039        let pattern = ProofExpr::App(
6040            Box::new(motive_hole),
6041            Box::new(ProofExpr::Term(ProofTerm::BoundVarRef(var_name.to_string()))),
6042        );
6043
6044        // The body is the goal itself (with TypedVar replaced by Variable for unification)
6045        let body = self.convert_typed_var_to_variable(goal, var_name);
6046
6047        // Unify: ?Motive(#n) = body
6048        match unify_pattern(&pattern, &body) {
6049            Ok(solution) => solution.get("Motive").cloned(),
6050            Err(_) => None,
6051        }
6052    }
6053
6054    /// Convert TypedVar to regular Variable for pattern unification.
6055    ///
6056    /// Pattern unification expects Variable("n") in the body to match BoundVarRef("n")
6057    /// in the pattern, but our goals have TypedVar { name: "n", typename: "Nat" }.
6058    fn convert_typed_var_to_variable(&self, expr: &ProofExpr, var_name: &str) -> ProofExpr {
6059        match expr {
6060            ProofExpr::TypedVar { name, .. } if name == var_name => {
6061                // Convert to Atom so it becomes a Variable in terms
6062                ProofExpr::Atom(name.clone())
6063            }
6064            ProofExpr::Identity(l, r) => ProofExpr::Identity(
6065                self.convert_typed_var_in_term(l, var_name),
6066                self.convert_typed_var_in_term(r, var_name),
6067            ),
6068            ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
6069                name: name.clone(),
6070                args: args
6071                    .iter()
6072                    .map(|a| self.convert_typed_var_in_term(a, var_name))
6073                    .collect(),
6074                world: world.clone(),
6075            },
6076            ProofExpr::And(l, r) => ProofExpr::And(
6077                Box::new(self.convert_typed_var_to_variable(l, var_name)),
6078                Box::new(self.convert_typed_var_to_variable(r, var_name)),
6079            ),
6080            ProofExpr::Or(l, r) => ProofExpr::Or(
6081                Box::new(self.convert_typed_var_to_variable(l, var_name)),
6082                Box::new(self.convert_typed_var_to_variable(r, var_name)),
6083            ),
6084            ProofExpr::Not(inner) => {
6085                ProofExpr::Not(Box::new(self.convert_typed_var_to_variable(inner, var_name)))
6086            }
6087            _ => expr.clone(),
6088        }
6089    }
6090
6091    /// Convert TypedVar to Variable in a ProofTerm.
6092    fn convert_typed_var_in_term(&self, term: &ProofTerm, var_name: &str) -> ProofTerm {
6093        match term {
6094            ProofTerm::Variable(v) => {
6095                // Check for "name:Type" pattern
6096                if v == var_name || v.starts_with(&format!("{}:", var_name)) {
6097                    ProofTerm::Variable(var_name.to_string())
6098                } else {
6099                    term.clone()
6100                }
6101            }
6102            ProofTerm::Function(name, args) => ProofTerm::Function(
6103                name.clone(),
6104                args.iter()
6105                    .map(|a| self.convert_typed_var_in_term(a, var_name))
6106                    .collect(),
6107            ),
6108            ProofTerm::Group(terms) => ProofTerm::Group(
6109                terms
6110                    .iter()
6111                    .map(|t| self.convert_typed_var_in_term(t, var_name))
6112                    .collect(),
6113            ),
6114            _ => term.clone(),
6115        }
6116    }
6117
6118    /// Perform structural induction on Nat using pattern unification.
6119    ///
6120    /// Uses Miller pattern unification to infer the motive, then applies
6121    /// it to constructors via beta reduction.
6122    ///
6123    /// Base case: P(Zero)
6124    /// Step case: ∀k. P(k) → P(Succ(k))
6125    fn try_nat_induction_with_motive(
6126        &mut self,
6127        goal: &ProofGoal,
6128        var_name: &str,
6129        motive: &ProofExpr,
6130        depth: usize,
6131    ) -> ProofResult<Option<DerivationTree>> {
6132        // Base case: P(Zero)
6133        // Apply the motive lambda to Zero constructor
6134        let zero_ctor = ProofExpr::Ctor {
6135            name: "Zero".into(),
6136            args: vec![],
6137        };
6138        let base_goal_expr = beta_reduce(&ProofExpr::App(
6139            Box::new(motive.clone()),
6140            Box::new(zero_ctor),
6141        ));
6142
6143        let base_goal = ProofGoal::with_context(base_goal_expr, goal.context.clone());
6144        let base_proof = match self.prove_goal(base_goal, depth + 1) {
6145            Ok(proof) => proof,
6146            Err(_) => return Ok(None),
6147        };
6148
6149        // Step case: ∀k. P(k) → P(Succ(k))
6150        let fresh_k = self.fresh_var();
6151        let k_var = ProofExpr::Atom(fresh_k.clone());
6152
6153        // Induction hypothesis: P(k)
6154        let ih = beta_reduce(&ProofExpr::App(
6155            Box::new(motive.clone()),
6156            Box::new(k_var.clone()),
6157        ));
6158
6159        // Step conclusion: P(Succ(k))
6160        let succ_k = ProofExpr::Ctor {
6161            name: "Succ".into(),
6162            args: vec![k_var],
6163        };
6164        let step_goal_expr = beta_reduce(&ProofExpr::App(
6165            Box::new(motive.clone()),
6166            Box::new(succ_k),
6167        ));
6168
6169        // Add IH to context for step case
6170        let mut step_context = goal.context.clone();
6171        step_context.push(ih.clone());
6172
6173        let step_goal = ProofGoal::with_context(step_goal_expr, step_context);
6174        let step_proof = match self.try_step_case_with_equational_reasoning(&step_goal, &ih, depth)
6175        {
6176            Ok(proof) => proof,
6177            Err(_) => return Ok(None),
6178        };
6179
6180        Ok(Some(DerivationTree::new(
6181            goal.target.clone(),
6182            InferenceRule::StructuralInduction {
6183                variable: var_name.to_string(),
6184                ind_type: "Nat".to_string(),
6185                step_var: fresh_k,
6186            },
6187            vec![base_proof, step_proof],
6188        )))
6189    }
6190
6191    /// Perform structural induction on Nat (legacy crude substitution).
6192    ///
6193    /// Base case: P(Zero)
6194    /// Step case: ∀k. P(k) → P(Succ(k))
6195    fn try_nat_induction(
6196        &mut self,
6197        goal: &ProofGoal,
6198        var_name: &str,
6199        depth: usize,
6200    ) -> ProofResult<Option<DerivationTree>> {
6201        // Create Zero constructor
6202        let zero = ProofExpr::Ctor {
6203            name: "Zero".into(),
6204            args: vec![],
6205        };
6206
6207        // Base case: substitute Zero for the induction variable
6208        let base_goal_expr = self.substitute_typed_var(&goal.target, var_name, &zero);
6209        let base_goal = ProofGoal::with_context(base_goal_expr, goal.context.clone());
6210
6211        // Try to prove base case
6212        let base_proof = match self.prove_goal(base_goal, depth + 1) {
6213            Ok(proof) => proof,
6214            Err(_) => return Ok(None), // Can't prove base case
6215        };
6216
6217        // Step case: assume P(k), prove P(Succ(k))
6218        let fresh_k = self.fresh_var();
6219
6220        // Create k as a variable
6221        let k_var = ProofExpr::Atom(fresh_k.clone());
6222
6223        // Create Succ(k)
6224        let succ_k = ProofExpr::Ctor {
6225            name: "Succ".into(),
6226            args: vec![k_var.clone()],
6227        };
6228
6229        // Induction hypothesis: P(k)
6230        let ih = self.substitute_typed_var(&goal.target, var_name, &k_var);
6231
6232        // Step goal: P(Succ(k))
6233        let step_goal_expr = self.substitute_typed_var(&goal.target, var_name, &succ_k);
6234
6235        // Add IH to context for step case
6236        let mut step_context = goal.context.clone();
6237        step_context.push(ih.clone());
6238
6239        let step_goal = ProofGoal::with_context(step_goal_expr, step_context);
6240
6241        // Try to prove step case with IH in context
6242        let step_proof = match self.try_step_case_with_equational_reasoning(&step_goal, &ih, depth)
6243        {
6244            Ok(proof) => proof,
6245            Err(_) => return Ok(None), // Can't prove step case
6246        };
6247
6248        // Build the induction proof tree
6249        Ok(Some(DerivationTree::new(
6250            goal.target.clone(),
6251            InferenceRule::StructuralInduction {
6252                variable: var_name.to_string(),
6253                ind_type: "Nat".to_string(),
6254                step_var: fresh_k,
6255            },
6256            vec![base_proof, step_proof],
6257        )))
6258    }
6259
6260    /// Perform structural induction on List.
6261    ///
6262    /// Base case: P(Nil)
6263    /// Step case: ∀h,t. P(t) → P(Cons(h,t))
6264    fn try_list_induction(
6265        &mut self,
6266        goal: &ProofGoal,
6267        var_name: &str,
6268        depth: usize,
6269    ) -> ProofResult<Option<DerivationTree>> {
6270        // Create Nil constructor
6271        let nil = ProofExpr::Ctor {
6272            name: "Nil".into(),
6273            args: vec![],
6274        };
6275
6276        // Base case: substitute Nil for the induction variable
6277        let base_goal_expr = self.substitute_typed_var(&goal.target, var_name, &nil);
6278        let base_goal = ProofGoal::with_context(base_goal_expr, goal.context.clone());
6279
6280        // Try to prove base case
6281        let base_proof = match self.prove_goal(base_goal, depth + 1) {
6282            Ok(proof) => proof,
6283            Err(_) => return Ok(None),
6284        };
6285
6286        // Step case: assume P(t), prove P(Cons(h, t))
6287        let fresh_h = self.fresh_var();
6288        let fresh_t = self.fresh_var();
6289
6290        let h_var = ProofExpr::Atom(fresh_h);
6291        let t_var = ProofExpr::Atom(fresh_t.clone());
6292
6293        let cons_ht = ProofExpr::Ctor {
6294            name: "Cons".into(),
6295            args: vec![h_var, t_var.clone()],
6296        };
6297
6298        // Induction hypothesis: P(t)
6299        let ih = self.substitute_typed_var(&goal.target, var_name, &t_var);
6300
6301        // Step goal: P(Cons(h, t))
6302        let step_goal_expr = self.substitute_typed_var(&goal.target, var_name, &cons_ht);
6303
6304        let mut step_context = goal.context.clone();
6305        step_context.push(ih.clone());
6306
6307        let step_goal = ProofGoal::with_context(step_goal_expr, step_context);
6308
6309        let step_proof = match self.try_step_case_with_equational_reasoning(&step_goal, &ih, depth)
6310        {
6311            Ok(proof) => proof,
6312            Err(_) => return Ok(None),
6313        };
6314
6315        Ok(Some(DerivationTree::new(
6316            goal.target.clone(),
6317            InferenceRule::StructuralInduction {
6318                variable: var_name.to_string(),
6319                ind_type: "List".to_string(),
6320                step_var: fresh_t,
6321            },
6322            vec![base_proof, step_proof],
6323        )))
6324    }
6325
6326    /// Try to prove the step case, potentially using equational reasoning.
6327    ///
6328    /// The step case often requires:
6329    /// 1. Applying a recursive axiom to simplify the goal
6330    /// 2. Using the induction hypothesis
6331    /// 3. Congruence reasoning (e.g., Succ(x) = Succ(y) if x = y)
6332    fn try_step_case_with_equational_reasoning(
6333        &mut self,
6334        goal: &ProofGoal,
6335        ih: &ProofExpr,
6336        depth: usize,
6337    ) -> ProofResult<DerivationTree> {
6338        // First, try direct proof (might work for simple cases)
6339        if let Ok(proof) = self.prove_goal(goal.clone(), depth + 1) {
6340            return Ok(proof);
6341        }
6342
6343        // For Identity goals, try equational reasoning
6344        if let ProofExpr::Identity(lhs, rhs) = &goal.target {
6345            // Try to rewrite LHS using axioms and see if we can reach RHS
6346            if let Some(proof) = self.try_equational_proof(goal, lhs, rhs, ih, depth)? {
6347                return Ok(proof);
6348            }
6349        }
6350
6351        Err(ProofError::NoProofFound)
6352    }
6353
6354    /// Try equational reasoning: rewrite LHS to match RHS using axioms and IH.
6355    ///
6356    /// For the step case of induction, we need to:
6357    /// 1. Find an axiom that matches the goal's LHS pattern
6358    /// 2. Use the axiom to rewrite LHS
6359    /// 3. Apply the induction hypothesis to simplify
6360    /// 4. Check if the result equals RHS
6361    fn try_equational_proof(
6362        &mut self,
6363        goal: &ProofGoal,
6364        lhs: &ProofTerm,
6365        rhs: &ProofTerm,
6366        ih: &ProofExpr,
6367        _depth: usize,
6368    ) -> ProofResult<Option<DerivationTree>> {
6369        // Find applicable equations from KB (Identity axioms)
6370        let equations: Vec<ProofExpr> = self
6371            .knowledge_base
6372            .iter()
6373            .filter(|e| matches!(e, ProofExpr::Identity(_, _)))
6374            .cloned()
6375            .collect();
6376
6377        // Try each equation to rewrite LHS
6378        for eq_axiom in &equations {
6379            if let ProofExpr::Identity(_, _) = &eq_axiom {
6380                // Rename variables in the axiom to avoid capture
6381                let renamed_axiom = self.rename_variables(&eq_axiom);
6382                if let ProofExpr::Identity(renamed_lhs, renamed_rhs) = renamed_axiom {
6383                    // Unify axiom LHS with goal LHS
6384                    // This binds axiom variables to goal terms
6385                    // e.g., unify(Add(Succ(x), m), Add(Succ(k), Zero)) gives {x->k, m->Zero}
6386                    if let Ok(subst) = unify_terms(&renamed_lhs, lhs) {
6387                        // Apply the substitution to the axiom's RHS
6388                        // This gives us what LHS rewrites to
6389                        let rewritten = self.apply_subst_to_term_with(&renamed_rhs, &subst);
6390
6391                        // Now check if rewritten equals RHS (possibly using IH)
6392                        if self.terms_equal_with_ih(&rewritten, rhs, ih) {
6393                            // Success! Build proof using the axiom and IH
6394                            let axiom_leaf =
6395                                DerivationTree::leaf(eq_axiom.clone(), InferenceRule::PremiseMatch);
6396
6397                            let ih_leaf =
6398                                DerivationTree::leaf(ih.clone(), InferenceRule::PremiseMatch);
6399
6400                            return Ok(Some(DerivationTree::new(
6401                                goal.target.clone(),
6402                                InferenceRule::PremiseMatch, // Equational step
6403                                vec![axiom_leaf, ih_leaf],
6404                            )));
6405                        }
6406                    }
6407                }
6408            }
6409        }
6410
6411        Ok(None)
6412    }
6413
6414    /// Check if two terms are equal, potentially using the induction hypothesis.
6415    fn terms_equal_with_ih(&self, t1: &ProofTerm, t2: &ProofTerm, ih: &ProofExpr) -> bool {
6416        // Direct equality
6417        if t1 == t2 {
6418            return true;
6419        }
6420
6421        // Try using IH: if IH is `x = y`, and t1 contains x, replace with y
6422        if let ProofExpr::Identity(ih_lhs, ih_rhs) = ih {
6423            // Check if t1 can be transformed to t2 using IH
6424            let t1_with_ih = self.rewrite_term_with_equation(t1, ih_lhs, ih_rhs);
6425            if &t1_with_ih == t2 {
6426                return true;
6427            }
6428
6429            // Also try the other direction
6430            let t2_with_ih = self.rewrite_term_with_equation(t2, ih_rhs, ih_lhs);
6431            if t1 == &t2_with_ih {
6432                return true;
6433            }
6434        }
6435
6436        false
6437    }
6438
6439    /// Rewrite occurrences of `from` to `to` in the term.
6440    fn rewrite_term_with_equation(
6441        &self,
6442        term: &ProofTerm,
6443        from: &ProofTerm,
6444        to: &ProofTerm,
6445    ) -> ProofTerm {
6446        // If term matches `from`, return `to`
6447        if term == from {
6448            return to.clone();
6449        }
6450
6451        // Recursively rewrite in subterms
6452        match term {
6453            ProofTerm::Function(name, args) => {
6454                let new_args: Vec<ProofTerm> = args
6455                    .iter()
6456                    .map(|a| self.rewrite_term_with_equation(a, from, to))
6457                    .collect();
6458                ProofTerm::Function(name.clone(), new_args)
6459            }
6460            ProofTerm::Group(terms) => {
6461                let new_terms: Vec<ProofTerm> = terms
6462                    .iter()
6463                    .map(|t| self.rewrite_term_with_equation(t, from, to))
6464                    .collect();
6465                ProofTerm::Group(new_terms)
6466            }
6467            _ => term.clone(),
6468        }
6469    }
6470
6471    /// Apply substitution to a ProofTerm with given substitution.
6472    fn apply_subst_to_term_with(&self, term: &ProofTerm, subst: &Substitution) -> ProofTerm {
6473        match term {
6474            ProofTerm::Variable(v) => subst.get(v).cloned().unwrap_or_else(|| term.clone()),
6475            ProofTerm::Function(name, args) => ProofTerm::Function(
6476                name.clone(),
6477                args.iter()
6478                    .map(|a| self.apply_subst_to_term_with(a, subst))
6479                    .collect(),
6480            ),
6481            ProofTerm::Group(terms) => ProofTerm::Group(
6482                terms
6483                    .iter()
6484                    .map(|t| self.apply_subst_to_term_with(t, subst))
6485                    .collect(),
6486            ),
6487            ProofTerm::Constant(_) => term.clone(),
6488            ProofTerm::BoundVarRef(_) => term.clone(),
6489        }
6490    }
6491
6492    /// Find a TypedVar in the expression.
6493    fn find_typed_var(&self, expr: &ProofExpr) -> Option<(String, String)> {
6494        match expr {
6495            ProofExpr::TypedVar { name, typename } => Some((name.clone(), typename.clone())),
6496            ProofExpr::Identity(l, r) => {
6497                self.find_typed_var_in_term(l).or_else(|| self.find_typed_var_in_term(r))
6498            }
6499            ProofExpr::Predicate { args, .. } => {
6500                for arg in args {
6501                    if let Some(tv) = self.find_typed_var_in_term(arg) {
6502                        return Some(tv);
6503                    }
6504                }
6505                None
6506            }
6507            ProofExpr::And(l, r)
6508            | ProofExpr::Or(l, r)
6509            | ProofExpr::Implies(l, r)
6510            | ProofExpr::Iff(l, r) => self.find_typed_var(l).or_else(|| self.find_typed_var(r)),
6511            ProofExpr::Not(inner) => self.find_typed_var(inner),
6512            ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
6513                self.find_typed_var(body)
6514            }
6515            _ => None,
6516        }
6517    }
6518
6519    /// Find a TypedVar embedded in a ProofTerm.
6520    fn find_typed_var_in_term(&self, term: &ProofTerm) -> Option<(String, String)> {
6521        match term {
6522            ProofTerm::Variable(v) => {
6523                // Check if this variable name is in our KB as a TypedVar
6524                // Actually, TypedVar should be in the expression, not the term
6525                // Let's check if the variable name contains type annotation
6526                if v.contains(':') {
6527                    let parts: Vec<&str> = v.splitn(2, ':').collect();
6528                    if parts.len() == 2 {
6529                        return Some((parts[0].to_string(), parts[1].to_string()));
6530                    }
6531                }
6532                None
6533            }
6534            ProofTerm::Function(_, args) => {
6535                for arg in args {
6536                    if let Some(tv) = self.find_typed_var_in_term(arg) {
6537                        return Some(tv);
6538                    }
6539                }
6540                None
6541            }
6542            ProofTerm::Group(terms) => {
6543                for t in terms {
6544                    if let Some(tv) = self.find_typed_var_in_term(t) {
6545                        return Some(tv);
6546                    }
6547                }
6548                None
6549            }
6550            ProofTerm::Constant(_) => None,
6551            ProofTerm::BoundVarRef(_) => None, // Pattern-level, no TypedVar
6552        }
6553    }
6554
6555    /// Substitute a TypedVar with a given expression throughout the goal.
6556    fn substitute_typed_var(
6557        &self,
6558        expr: &ProofExpr,
6559        var_name: &str,
6560        replacement: &ProofExpr,
6561    ) -> ProofExpr {
6562        match expr {
6563            ProofExpr::TypedVar { name, .. } if name == var_name => replacement.clone(),
6564            ProofExpr::Identity(l, r) => {
6565                let new_l = self.substitute_typed_var_in_term(l, var_name, replacement);
6566                let new_r = self.substitute_typed_var_in_term(r, var_name, replacement);
6567                ProofExpr::Identity(new_l, new_r)
6568            }
6569            ProofExpr::Predicate { name, args, world } => {
6570                let new_args: Vec<ProofTerm> = args
6571                    .iter()
6572                    .map(|a| self.substitute_typed_var_in_term(a, var_name, replacement))
6573                    .collect();
6574                ProofExpr::Predicate {
6575                    name: name.clone(),
6576                    args: new_args,
6577                    world: world.clone(),
6578                }
6579            }
6580            ProofExpr::And(l, r) => ProofExpr::And(
6581                Box::new(self.substitute_typed_var(l, var_name, replacement)),
6582                Box::new(self.substitute_typed_var(r, var_name, replacement)),
6583            ),
6584            ProofExpr::Or(l, r) => ProofExpr::Or(
6585                Box::new(self.substitute_typed_var(l, var_name, replacement)),
6586                Box::new(self.substitute_typed_var(r, var_name, replacement)),
6587            ),
6588            ProofExpr::Implies(l, r) => ProofExpr::Implies(
6589                Box::new(self.substitute_typed_var(l, var_name, replacement)),
6590                Box::new(self.substitute_typed_var(r, var_name, replacement)),
6591            ),
6592            ProofExpr::Iff(l, r) => ProofExpr::Iff(
6593                Box::new(self.substitute_typed_var(l, var_name, replacement)),
6594                Box::new(self.substitute_typed_var(r, var_name, replacement)),
6595            ),
6596            ProofExpr::Not(inner) => {
6597                ProofExpr::Not(Box::new(self.substitute_typed_var(inner, var_name, replacement)))
6598            }
6599            ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
6600                variable: variable.clone(),
6601                body: Box::new(self.substitute_typed_var(body, var_name, replacement)),
6602            },
6603            ProofExpr::Exists { variable, body } => ProofExpr::Exists {
6604                variable: variable.clone(),
6605                body: Box::new(self.substitute_typed_var(body, var_name, replacement)),
6606            },
6607            _ => expr.clone(),
6608        }
6609    }
6610
6611    /// Substitute a TypedVar in a ProofTerm.
6612    fn substitute_typed_var_in_term(
6613        &self,
6614        term: &ProofTerm,
6615        var_name: &str,
6616        replacement: &ProofExpr,
6617    ) -> ProofTerm {
6618        match term {
6619            ProofTerm::Variable(v) => {
6620                // Check for TypedVar pattern "name:Type"
6621                if v == var_name || v.starts_with(&format!("{}:", var_name)) {
6622                    self.expr_to_term(replacement)
6623                } else {
6624                    term.clone()
6625                }
6626            }
6627            ProofTerm::Function(name, args) => ProofTerm::Function(
6628                name.clone(),
6629                args.iter()
6630                    .map(|a| self.substitute_typed_var_in_term(a, var_name, replacement))
6631                    .collect(),
6632            ),
6633            ProofTerm::Group(terms) => ProofTerm::Group(
6634                terms
6635                    .iter()
6636                    .map(|t| self.substitute_typed_var_in_term(t, var_name, replacement))
6637                    .collect(),
6638            ),
6639            ProofTerm::Constant(_) => term.clone(),
6640            ProofTerm::BoundVarRef(_) => term.clone(),
6641        }
6642    }
6643
6644    /// Convert a ProofExpr to a ProofTerm (for use in substitution).
6645    fn expr_to_term(&self, expr: &ProofExpr) -> ProofTerm {
6646        match expr {
6647            ProofExpr::Atom(s) => ProofTerm::Variable(s.clone()),
6648            ProofExpr::Ctor { name, args } => {
6649                ProofTerm::Function(name.clone(), args.iter().map(|a| self.expr_to_term(a)).collect())
6650            }
6651            ProofExpr::TypedVar { name, .. } => ProofTerm::Variable(name.clone()),
6652            _ => ProofTerm::Constant(format!("{}", expr)),
6653        }
6654    }
6655
6656    // =========================================================================
6657    // HELPER METHODS
6658    // =========================================================================
6659
6660    /// Generate a fresh variable name.
6661    fn fresh_var(&mut self) -> String {
6662        self.var_counter += 1;
6663        format!("_G{}", self.var_counter)
6664    }
6665
6666    /// A fresh eigenconstant name for universal introduction: an opaque `Constant`
6667    /// the search cannot unify against (unlike a `Variable`, which it could bind),
6668    /// so the bound variable of a `∀` is proved as a genuinely arbitrary element.
6669    /// The distinctive prefix can never be a numeral or collide with a real symbol.
6670    fn fresh_eigenconstant(&mut self) -> String {
6671        self.var_counter += 1;
6672        format!("__eigen{}", self.var_counter)
6673    }
6674
6675    /// Rename all variables in an expression to fresh names.
6676    fn rename_variables(&mut self, expr: &ProofExpr) -> ProofExpr {
6677        let vars = self.collect_variables(expr);
6678        let mut subst = Substitution::new();
6679
6680        for var in vars {
6681            let fresh = self.fresh_var();
6682            subst.insert(var, ProofTerm::Variable(fresh));
6683        }
6684
6685        apply_subst_to_expr(expr, &subst)
6686    }
6687
6688    /// Collect all variable names in an expression.
6689    fn collect_variables(&self, expr: &ProofExpr) -> Vec<String> {
6690        let mut vars = Vec::new();
6691        self.collect_variables_recursive(expr, &mut vars);
6692        vars
6693    }
6694
6695    fn collect_variables_recursive(&self, expr: &ProofExpr, vars: &mut Vec<String>) {
6696        match expr {
6697            ProofExpr::Predicate { args, .. } => {
6698                for arg in args {
6699                    self.collect_term_variables(arg, vars);
6700                }
6701            }
6702            ProofExpr::Identity(l, r) => {
6703                self.collect_term_variables(l, vars);
6704                self.collect_term_variables(r, vars);
6705            }
6706            ProofExpr::And(l, r)
6707            | ProofExpr::Or(l, r)
6708            | ProofExpr::Implies(l, r)
6709            | ProofExpr::Iff(l, r) => {
6710                self.collect_variables_recursive(l, vars);
6711                self.collect_variables_recursive(r, vars);
6712            }
6713            ProofExpr::Not(inner) => self.collect_variables_recursive(inner, vars),
6714            ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body } => {
6715                if !vars.contains(variable) {
6716                    vars.push(variable.clone());
6717                }
6718                self.collect_variables_recursive(body, vars);
6719            }
6720            ProofExpr::Lambda { variable, body } => {
6721                if !vars.contains(variable) {
6722                    vars.push(variable.clone());
6723                }
6724                self.collect_variables_recursive(body, vars);
6725            }
6726            ProofExpr::App(f, a) => {
6727                self.collect_variables_recursive(f, vars);
6728                self.collect_variables_recursive(a, vars);
6729            }
6730            ProofExpr::NeoEvent { roles, .. } => {
6731                for (_, term) in roles {
6732                    self.collect_term_variables(term, vars);
6733                }
6734            }
6735            _ => {}
6736        }
6737    }
6738
6739    fn collect_term_variables(&self, term: &ProofTerm, vars: &mut Vec<String>) {
6740        match term {
6741            ProofTerm::Variable(v) => {
6742                if !vars.contains(v) {
6743                    vars.push(v.clone());
6744                }
6745            }
6746            ProofTerm::Function(_, args) => {
6747                for arg in args {
6748                    self.collect_term_variables(arg, vars);
6749                }
6750            }
6751            ProofTerm::Group(terms) => {
6752                for t in terms {
6753                    self.collect_term_variables(t, vars);
6754                }
6755            }
6756            ProofTerm::Constant(_) => {}
6757            ProofTerm::BoundVarRef(_) => {} // Pattern-level, no variables
6758        }
6759    }
6760
6761    /// Collect potential witnesses (constants) from the knowledge base.
6762    fn collect_witnesses(&self) -> Vec<ProofTerm> {
6763        let mut witnesses = Vec::new();
6764
6765        for expr in &self.knowledge_base {
6766            self.collect_constants_from_expr(expr, &mut witnesses);
6767        }
6768
6769        witnesses
6770    }
6771
6772    fn collect_constants_from_expr(&self, expr: &ProofExpr, constants: &mut Vec<ProofTerm>) {
6773        match expr {
6774            ProofExpr::Predicate { args, .. } => {
6775                for arg in args {
6776                    self.collect_constants_from_term(arg, constants);
6777                }
6778            }
6779            ProofExpr::Identity(l, r) => {
6780                self.collect_constants_from_term(l, constants);
6781                self.collect_constants_from_term(r, constants);
6782            }
6783            ProofExpr::And(l, r)
6784            | ProofExpr::Or(l, r)
6785            | ProofExpr::Implies(l, r)
6786            | ProofExpr::Iff(l, r) => {
6787                self.collect_constants_from_expr(l, constants);
6788                self.collect_constants_from_expr(r, constants);
6789            }
6790            ProofExpr::Not(inner) => self.collect_constants_from_expr(inner, constants),
6791            ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
6792                self.collect_constants_from_expr(body, constants);
6793            }
6794            ProofExpr::NeoEvent { roles, .. } => {
6795                for (_, term) in roles {
6796                    self.collect_constants_from_term(term, constants);
6797                }
6798            }
6799            _ => {}
6800        }
6801    }
6802
6803    fn collect_constants_from_term(&self, term: &ProofTerm, constants: &mut Vec<ProofTerm>) {
6804        match term {
6805            ProofTerm::Constant(_) => {
6806                if !constants.contains(term) {
6807                    constants.push(term.clone());
6808                }
6809            }
6810            ProofTerm::Function(_, args) => {
6811                // The function application itself could be a witness
6812                if !constants.contains(term) {
6813                    constants.push(term.clone());
6814                }
6815                for arg in args {
6816                    self.collect_constants_from_term(arg, constants);
6817                }
6818            }
6819            ProofTerm::Group(terms) => {
6820                for t in terms {
6821                    self.collect_constants_from_term(t, constants);
6822                }
6823            }
6824            ProofTerm::Variable(_) => {}
6825            ProofTerm::BoundVarRef(_) => {} // Pattern-level, not a constant
6826        }
6827    }
6828
6829    // =========================================================================
6830    // STRATEGY 7: ORACLE FALLBACK (Z3)
6831    // =========================================================================
6832
6833    /// Attempt to prove using Z3 as an oracle.
6834    ///
6835    /// This is the fallback when all structural proof strategies fail.
6836    /// Z3 will verify arithmetic, comparisons, and uninterpreted function reasoning.
6837    #[cfg(feature = "verification")]
6838    fn try_oracle_fallback(&self, goal: &ProofGoal) -> ProofResult<Option<DerivationTree>> {
6839        crate::oracle::try_oracle(goal, &self.knowledge_base)
6840    }
6841}
6842
6843// =============================================================================
6844// HELPER FUNCTIONS
6845// =============================================================================
6846
6847/// Extract the type from an existential body if it contains type information.
6848///
6849/// Looks for TypedVar patterns in the body that might indicate the type
6850/// of the existentially quantified variable. Returns None if no type
6851/// information is found.
6852fn extract_type_from_exists_body(body: &ProofExpr) -> Option<String> {
6853    match body {
6854        // Direct TypedVar in body
6855        ProofExpr::TypedVar { typename, .. } => Some(typename.clone()),
6856
6857        // Recurse into conjunctions
6858        ProofExpr::And(l, r) => {
6859            extract_type_from_exists_body(l).or_else(|| extract_type_from_exists_body(r))
6860        }
6861
6862        // Recurse into disjunctions
6863        ProofExpr::Or(l, r) => {
6864            extract_type_from_exists_body(l).or_else(|| extract_type_from_exists_body(r))
6865        }
6866
6867        // Recurse into nested quantifiers
6868        ProofExpr::Exists { body, .. } | ProofExpr::ForAll { body, .. } => {
6869            extract_type_from_exists_body(body)
6870        }
6871
6872        // No type information found
6873        _ => None,
6874    }
6875}
6876
6877impl Default for BackwardChainer {
6878    fn default() -> Self {
6879        Self::new()
6880    }
6881}