logicaffeine_proof/
unify.rs

1//! First-order unification for proof search.
2//!
3//! Implements Robinson's Unification Algorithm with occurs check. This is the
4//! core pattern-matching engine that enables logical reasoning.
5//!
6//! ## What Unification Does
7//!
8//! Unification finds a substitution that makes two terms identical:
9//!
10//! | Pattern | Target | Substitution |
11//! |---------|--------|--------------|
12//! | `Mortal(x)` | `Mortal(Socrates)` | `{x ↦ Socrates}` |
13//! | `Add(Succ(n), 0)` | `Add(Succ(Zero), 0)` | `{n ↦ Zero}` |
14//! | `f(x, x)` | `f(a, b)` | Fails (x can't be both a and b) |
15//!
16//! ## Occurs Check
17//!
18//! The occurs check prevents infinite terms. `x = f(x)` has no finite solution
19//! since it would require `x = f(f(f(...)))`. We reject such unifications.
20//!
21//! ## Alpha-Equivalence
22//!
23//! Bound variable names are arbitrary: `∃e P(e) ≡ ∃x P(x)`. We handle this
24//! by substituting fresh constants for bound variables when unifying
25//! quantified expressions.
26//!
27//! # See Also
28//!
29//! * [`beta_reduce`] - Normalizes lambda applications before unification
30//! * [`ProofTerm`] - The term representation being unified
31//! * [`ProofExpr`] - The expression representation for higher-order unification
32
33use std::collections::HashMap;
34use std::sync::atomic::{AtomicUsize, Ordering};
35
36use crate::error::{ProofError, ProofResult};
37use crate::{MatchArm, ProofExpr, ProofTerm};
38
39/// A substitution mapping variable names to terms.
40///
41/// Substitutions are the output of unification. Applying a substitution
42/// to both sides of a unification problem yields identical terms.
43///
44/// # Example
45///
46/// After unifying `Mortal(x)` with `Mortal(Socrates)`:
47///
48/// ```text
49/// { "x" ↦ Constant("Socrates") }
50/// ```
51///
52/// # Operations
53///
54/// * [`unify_terms`] - Creates substitutions from term unification
55/// * [`apply_subst_to_term`] - Applies substitution to a term
56/// * [`compose_substitutions`] - Combines two substitutions
57pub type Substitution = HashMap<String, ProofTerm>;
58
59/// Substitution for expression-level meta-variables (holes).
60///
61/// Maps hole names to their solutions, typically lambda abstractions
62/// inferred during higher-order pattern unification.
63///
64/// # Example
65///
66/// Solving `?P(x) = Even(x)` yields:
67///
68/// ```text
69/// { "P" ↦ Lambda { variable: "x", body: Even(x) } }
70/// ```
71///
72/// # See Also
73///
74/// * [`unify_pattern`] - Creates expression substitutions via Miller pattern unification
75pub type ExprSubstitution = HashMap<String, ProofExpr>;
76
77// =============================================================================
78// ALPHA-EQUIVALENCE SUPPORT
79// =============================================================================
80
81/// Global counter for generating fresh constants during alpha-renaming.
82static ALPHA_COUNTER: AtomicUsize = AtomicUsize::new(0);
83
84/// Generate a fresh constant for alpha-renaming.
85/// Uses a prefix that cannot appear in user input to avoid collisions.
86fn fresh_alpha_constant() -> ProofTerm {
87    let id = ALPHA_COUNTER.fetch_add(1, Ordering::SeqCst);
88    ProofTerm::Constant(format!("#α{}", id))
89}
90
91// =============================================================================
92// BETA-REDUCTION
93// =============================================================================
94
95/// Check if an expression is a constructor form (safe for fix unfolding).
96/// Only constructors are safe guards against non-termination.
97fn is_constructor_form(expr: &ProofExpr) -> bool {
98    matches!(expr, ProofExpr::Ctor { .. })
99}
100
101/// Beta-reduce an expression to Weak Head Normal Form (WHNF).
102///
103/// Normalizes lambda applications and match expressions, enabling unification
104/// to work on structurally equivalent terms.
105///
106/// # Reduction Rules
107///
108/// | Redex | Reduction |
109/// |-------|-----------|
110/// | `(λx. body)(arg)` | `body[x := arg]` (beta) |
111/// | `(fix f. body) (Ctor ...)` | `body[f := fix f. body] (Ctor ...)` (fix unfolding) |
112/// | `match (Ctor args) { ... }` | Selected arm with args substituted (iota) |
113///
114/// # Weak Head Normal Form
115///
116/// WHNF stops at the outermost constructor or lambda. It does not reduce
117/// under binders, making it efficient for unification purposes.
118///
119/// # Termination
120///
121/// Fix unfolding only occurs when the argument is a constructor (`Ctor`),
122/// ensuring termination for well-typed terms.
123///
124/// # Example
125///
126/// ```text
127/// beta_reduce((λx. P(x))(Socrates))
128/// → P(Socrates)
129///
130/// beta_reduce(match Zero { Zero => True, Succ(k) => False })
131/// → True
132/// ```
133///
134/// # See Also
135///
136/// * [`unify_exprs`] - Calls beta_reduce before comparing expressions
137/// * [`certifier::certify`](crate::certifier::certify) - Uses normalized terms for kernel conversion
138pub fn beta_reduce(expr: &ProofExpr) -> ProofExpr {
139    match expr {
140        // Beta-reduction and Fix unfolding
141        ProofExpr::App(func, arg) => {
142            // First, reduce both function and argument
143            let func_reduced = beta_reduce(func);
144            let arg_reduced = beta_reduce(arg);
145
146            match func_reduced {
147                // Beta-reduction: (λx. body)(arg) → body[x := arg]
148                ProofExpr::Lambda { variable, body } => {
149                    let result = substitute_expr_for_var(&body, &variable, &arg_reduced);
150                    // Recursively reduce the result (handle nested redexes)
151                    beta_reduce(&result)
152                }
153
154                // Fix unfolding: (fix f. body) arg → body[f := fix f. body] arg
155                // Only unfold when arg is a constructor (guard against non-termination)
156                ProofExpr::Fixpoint { ref name, ref body } if is_constructor_form(&arg_reduced) => {
157                    // Substitute fix for f in body
158                    let fix_expr = ProofExpr::Fixpoint {
159                        name: name.clone(),
160                        body: body.clone(),
161                    };
162                    let unfolded = substitute_expr_for_var(body, name, &fix_expr);
163                    // Apply unfolded body to arg and reduce
164                    let applied = ProofExpr::App(Box::new(unfolded), Box::new(arg_reduced));
165                    beta_reduce(&applied)
166                }
167
168                _ => {
169                    // No reduction possible, return normalized application
170                    ProofExpr::App(Box::new(func_reduced), Box::new(arg_reduced))
171                }
172            }
173        }
174
175        // Reduce inside binary connectives
176        ProofExpr::And(l, r) => ProofExpr::And(
177            Box::new(beta_reduce(l)),
178            Box::new(beta_reduce(r)),
179        ),
180        ProofExpr::Or(l, r) => ProofExpr::Or(
181            Box::new(beta_reduce(l)),
182            Box::new(beta_reduce(r)),
183        ),
184        ProofExpr::Implies(l, r) => ProofExpr::Implies(
185            Box::new(beta_reduce(l)),
186            Box::new(beta_reduce(r)),
187        ),
188        ProofExpr::Iff(l, r) => ProofExpr::Iff(
189            Box::new(beta_reduce(l)),
190            Box::new(beta_reduce(r)),
191        ),
192        ProofExpr::Not(inner) => ProofExpr::Not(Box::new(beta_reduce(inner))),
193
194        // Reduce inside quantifiers
195        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
196            variable: variable.clone(),
197            body: Box::new(beta_reduce(body)),
198        },
199        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
200            variable: variable.clone(),
201            body: Box::new(beta_reduce(body)),
202        },
203
204        // Reduce inside lambda bodies (but not the lambda itself - that's a value)
205        ProofExpr::Lambda { variable, body } => ProofExpr::Lambda {
206            variable: variable.clone(),
207            body: Box::new(beta_reduce(body)),
208        },
209
210        // Reduce inside modal and temporal operators
211        ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
212            domain: domain.clone(),
213            force: *force,
214            flavor: flavor.clone(),
215            body: Box::new(beta_reduce(body)),
216        },
217        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
218            operator: operator.clone(),
219            body: Box::new(beta_reduce(body)),
220        },
221        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
222            operator: operator.clone(),
223            left: Box::new(beta_reduce(left)),
224            right: Box::new(beta_reduce(right)),
225        },
226
227        // Reduce inside Ctor arguments
228        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
229            name: name.clone(),
230            args: args.iter().map(beta_reduce).collect(),
231        },
232
233        // Iota reduction: match (Ctor args) with arms → selected arm body
234        ProofExpr::Match { scrutinee, arms } => {
235            let reduced_scrutinee = beta_reduce(scrutinee);
236
237            // Try iota reduction: if scrutinee is a Ctor, select matching arm
238            if let ProofExpr::Ctor { name: ctor_name, args: ctor_args } = &reduced_scrutinee {
239                for arm in arms {
240                    if &arm.ctor == ctor_name {
241                        // Found matching arm - substitute constructor args for bindings
242                        let mut result = arm.body.clone();
243                        for (binding, arg) in arm.bindings.iter().zip(ctor_args.iter()) {
244                            result = substitute_expr_for_var(&result, binding, arg);
245                        }
246                        // Continue reducing the result
247                        return beta_reduce(&result);
248                    }
249                }
250            }
251
252            // No iota reduction possible - just reduce subexpressions
253            ProofExpr::Match {
254                scrutinee: Box::new(reduced_scrutinee),
255                arms: arms.iter().map(|arm| MatchArm {
256                    ctor: arm.ctor.clone(),
257                    bindings: arm.bindings.clone(),
258                    body: beta_reduce(&arm.body),
259                }).collect(),
260            }
261        }
262
263        // Reduce inside Fixpoint
264        ProofExpr::Fixpoint { name, body } => ProofExpr::Fixpoint {
265            name: name.clone(),
266            body: Box::new(beta_reduce(body)),
267        },
268
269        // Atomic expressions don't reduce
270        ProofExpr::Predicate { .. }
271        | ProofExpr::Identity(_, _)
272        | ProofExpr::Atom(_)
273        | ProofExpr::NeoEvent { .. }
274        | ProofExpr::TypedVar { .. }
275        | ProofExpr::Unsupported(_)
276        | ProofExpr::Hole(_)
277        | ProofExpr::Term(_) => expr.clone(),
278    }
279}
280
281/// Substitute an expression for a variable name in another expression.
282///
283/// Used for beta-reduction: (λx. body)(arg) → body[x := arg]
284/// Handles variable capture by not substituting inside shadowing binders.
285fn substitute_expr_for_var(body: &ProofExpr, var: &str, replacement: &ProofExpr) -> ProofExpr {
286    match body {
287        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
288            name: name.clone(),
289            args: args.iter().map(|t| substitute_term_for_var(t, var, replacement)).collect(),
290            world: world.clone(),
291        },
292
293        ProofExpr::Identity(l, r) => ProofExpr::Identity(
294            substitute_term_for_var(l, var, replacement),
295            substitute_term_for_var(r, var, replacement),
296        ),
297
298        ProofExpr::Atom(a) => {
299            // If the atom matches the variable, replace it
300            if a == var {
301                replacement.clone()
302            } else {
303                ProofExpr::Atom(a.clone())
304            }
305        }
306
307        ProofExpr::And(l, r) => ProofExpr::And(
308            Box::new(substitute_expr_for_var(l, var, replacement)),
309            Box::new(substitute_expr_for_var(r, var, replacement)),
310        ),
311        ProofExpr::Or(l, r) => ProofExpr::Or(
312            Box::new(substitute_expr_for_var(l, var, replacement)),
313            Box::new(substitute_expr_for_var(r, var, replacement)),
314        ),
315        ProofExpr::Implies(l, r) => ProofExpr::Implies(
316            Box::new(substitute_expr_for_var(l, var, replacement)),
317            Box::new(substitute_expr_for_var(r, var, replacement)),
318        ),
319        ProofExpr::Iff(l, r) => ProofExpr::Iff(
320            Box::new(substitute_expr_for_var(l, var, replacement)),
321            Box::new(substitute_expr_for_var(r, var, replacement)),
322        ),
323        ProofExpr::Not(inner) => ProofExpr::Not(
324            Box::new(substitute_expr_for_var(inner, var, replacement))
325        ),
326
327        // Quantifiers: don't substitute if the variable is shadowed
328        ProofExpr::ForAll { variable, body: inner } => {
329            if variable == var {
330                // Variable is shadowed, don't substitute in body
331                body.clone()
332            } else {
333                ProofExpr::ForAll {
334                    variable: variable.clone(),
335                    body: Box::new(substitute_expr_for_var(inner, var, replacement)),
336                }
337            }
338        }
339        ProofExpr::Exists { variable, body: inner } => {
340            if variable == var {
341                body.clone()
342            } else {
343                ProofExpr::Exists {
344                    variable: variable.clone(),
345                    body: Box::new(substitute_expr_for_var(inner, var, replacement)),
346                }
347            }
348        }
349
350        // Lambda: don't substitute if the variable is shadowed
351        ProofExpr::Lambda { variable, body: inner } => {
352            if variable == var {
353                body.clone()
354            } else {
355                ProofExpr::Lambda {
356                    variable: variable.clone(),
357                    body: Box::new(substitute_expr_for_var(inner, var, replacement)),
358                }
359            }
360        }
361
362        ProofExpr::App(f, a) => ProofExpr::App(
363            Box::new(substitute_expr_for_var(f, var, replacement)),
364            Box::new(substitute_expr_for_var(a, var, replacement)),
365        ),
366
367        ProofExpr::Modal { domain, force, flavor, body: inner } => ProofExpr::Modal {
368            domain: domain.clone(),
369            force: *force,
370            flavor: flavor.clone(),
371            body: Box::new(substitute_expr_for_var(inner, var, replacement)),
372        },
373
374        ProofExpr::Temporal { operator, body: inner } => ProofExpr::Temporal {
375            operator: operator.clone(),
376            body: Box::new(substitute_expr_for_var(inner, var, replacement)),
377        },
378
379        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
380            operator: operator.clone(),
381            left: Box::new(substitute_expr_for_var(left, var, replacement)),
382            right: Box::new(substitute_expr_for_var(right, var, replacement)),
383        },
384
385        ProofExpr::NeoEvent { event_var, verb, roles } => {
386            // Substitute in roles, but not the event_var itself
387            ProofExpr::NeoEvent {
388                event_var: event_var.clone(),
389                verb: verb.clone(),
390                roles: roles.iter().map(|(r, t)| {
391                    (r.clone(), substitute_term_for_var(t, var, replacement))
392                }).collect(),
393            }
394        }
395
396        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
397            name: name.clone(),
398            args: args.iter().map(|a| substitute_expr_for_var(a, var, replacement)).collect(),
399        },
400
401        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
402            scrutinee: Box::new(substitute_expr_for_var(scrutinee, var, replacement)),
403            arms: arms.iter().map(|arm| {
404                // Don't substitute if var is bound in this arm
405                if arm.bindings.contains(&var.to_string()) {
406                    arm.clone()
407                } else {
408                    MatchArm {
409                        ctor: arm.ctor.clone(),
410                        bindings: arm.bindings.clone(),
411                        body: substitute_expr_for_var(&arm.body, var, replacement),
412                    }
413                }
414            }).collect(),
415        },
416
417        ProofExpr::Fixpoint { name, body: inner } => {
418            if name == var {
419                body.clone()
420            } else {
421                ProofExpr::Fixpoint {
422                    name: name.clone(),
423                    body: Box::new(substitute_expr_for_var(inner, var, replacement)),
424                }
425            }
426        }
427
428        ProofExpr::TypedVar { .. } | ProofExpr::Unsupported(_) => body.clone(),
429
430        // Holes are meta-variables - don't substitute into them
431        ProofExpr::Hole(_) => body.clone(),
432
433        // Terms: substitute into the inner term
434        ProofExpr::Term(t) => ProofExpr::Term(substitute_term_for_var(t, var, replacement)),
435    }
436}
437
438/// Substitute an expression for a variable in a term.
439///
440/// When a variable in a term matches, convert the replacement expression to a term.
441fn substitute_term_for_var(term: &ProofTerm, var: &str, replacement: &ProofExpr) -> ProofTerm {
442    match term {
443        ProofTerm::Variable(v) if v == var => {
444            // Variable matches, convert replacement to term
445            expr_to_term(replacement)
446        }
447        // BoundVarRef also participates in substitution (for instantiating quantified formulas)
448        ProofTerm::BoundVarRef(v) if v == var => {
449            expr_to_term(replacement)
450        }
451        ProofTerm::Variable(_) | ProofTerm::Constant(_) | ProofTerm::BoundVarRef(_) => term.clone(),
452        ProofTerm::Function(name, args) => ProofTerm::Function(
453            name.clone(),
454            args.iter().map(|a| substitute_term_for_var(a, var, replacement)).collect(),
455        ),
456        ProofTerm::Group(terms) => ProofTerm::Group(
457            terms.iter().map(|t| substitute_term_for_var(t, var, replacement)).collect(),
458        ),
459    }
460}
461
462/// Convert a simple ProofExpr to ProofTerm.
463///
464/// Used during beta-reduction when substituting an expression argument
465/// into a predicate's term position.
466fn expr_to_term(expr: &ProofExpr) -> ProofTerm {
467    match expr {
468        // Atoms become constants
469        ProofExpr::Atom(s) => ProofTerm::Constant(s.clone()),
470
471        // Zero-arity predicates become constants (e.g., "John" as a predicate name)
472        ProofExpr::Predicate { name, args, .. } if args.is_empty() => {
473            ProofTerm::Constant(name.clone())
474        }
475
476        // Predicates with args become functions
477        ProofExpr::Predicate { name, args, .. } => {
478            ProofTerm::Function(name.clone(), args.clone())
479        }
480
481        // Constructors become functions
482        ProofExpr::Ctor { name, args } => {
483            ProofTerm::Function(name.clone(), args.iter().map(expr_to_term).collect())
484        }
485
486        // TypedVar becomes a variable
487        ProofExpr::TypedVar { name, .. } => ProofTerm::Variable(name.clone()),
488
489        // Term is already a term - extract it directly
490        ProofExpr::Term(t) => t.clone(),
491
492        // Fallback: stringify the expression (covers Hole, etc.)
493        _ => ProofTerm::Constant(format!("{}", expr)),
494    }
495}
496
497// =============================================================================
498// TERM-LEVEL UNIFICATION
499// =============================================================================
500
501/// Unify two terms, returning the Most General Unifier (MGU).
502///
503/// The MGU is the smallest substitution that makes both terms identical.
504/// Uses Robinson's algorithm with occurs check.
505///
506/// # Arguments
507///
508/// * `t1` - The first term (often a pattern with variables)
509/// * `t2` - The second term (often a ground term or another pattern)
510///
511/// # Returns
512///
513/// * `Ok(subst)` - A substitution that unifies the terms
514/// * `Err(OccursCheck)` - If unification would create an infinite term
515/// * `Err(SymbolMismatch)` - If function/constant names differ
516/// * `Err(ArityMismatch)` - If argument counts differ
517///
518/// # Example
519///
520/// ```
521/// use logicaffeine_proof::{ProofTerm, unify::unify_terms};
522///
523/// let pattern = ProofTerm::Function(
524///     "Mortal".into(),
525///     vec![ProofTerm::Variable("x".into())]
526/// );
527/// let target = ProofTerm::Function(
528///     "Mortal".into(),
529///     vec![ProofTerm::Constant("Socrates".into())]
530/// );
531///
532/// let subst = unify_terms(&pattern, &target).unwrap();
533/// assert_eq!(
534///     subst.get("x"),
535///     Some(&ProofTerm::Constant("Socrates".into()))
536/// );
537/// ```
538///
539/// # See Also
540///
541/// * [`unify_exprs`] - Unifies expressions (handles quantifiers, connectives)
542/// * [`apply_subst_to_term`] - Applies the resulting substitution
543pub fn unify_terms(t1: &ProofTerm, t2: &ProofTerm) -> ProofResult<Substitution> {
544    let mut subst = Substitution::new();
545    unify_terms_with_subst(t1, t2, &mut subst)?;
546    Ok(subst)
547}
548
549/// Internal unification with accumulating substitution.
550fn unify_terms_with_subst(
551    t1: &ProofTerm,
552    t2: &ProofTerm,
553    subst: &mut Substitution,
554) -> ProofResult<()> {
555    // Apply current substitution to both terms first
556    let t1 = apply_subst_to_term(t1, subst);
557    let t2 = apply_subst_to_term(t2, subst);
558
559    match (&t1, &t2) {
560        // Identical terms unify trivially
561        (ProofTerm::Constant(c1), ProofTerm::Constant(c2)) if c1 == c2 => Ok(()),
562
563        // Different constants cannot unify
564        (ProofTerm::Constant(c1), ProofTerm::Constant(c2)) => {
565            Err(ProofError::SymbolMismatch {
566                left: c1.clone(),
567                right: c2.clone(),
568            })
569        }
570
571        // Variable on the left: bind it to the right term
572        (ProofTerm::Variable(v), t) => {
573            // Check if they're the same variable
574            if let ProofTerm::Variable(v2) = t {
575                if v == v2 {
576                    return Ok(());
577                }
578            }
579            // Occurs check: prevent infinite types
580            if occurs(v, t) {
581                return Err(ProofError::OccursCheck {
582                    variable: v.clone(),
583                    term: t.clone(),
584                });
585            }
586            subst.insert(v.clone(), t.clone());
587            Ok(())
588        }
589
590        // Variable on the right: bind it to the left term
591        (t, ProofTerm::Variable(v)) => {
592            // Occurs check
593            if occurs(v, t) {
594                return Err(ProofError::OccursCheck {
595                    variable: v.clone(),
596                    term: t.clone(),
597                });
598            }
599            subst.insert(v.clone(), t.clone());
600            Ok(())
601        }
602
603        // BoundVarRef on the left: treat like a variable for unification
604        // This allows matching quantified formulas like ∀x P(x) against P(butler)
605        (ProofTerm::BoundVarRef(v), t) => {
606            if let ProofTerm::BoundVarRef(v2) = t {
607                if v == v2 {
608                    return Ok(());
609                }
610            }
611            if occurs(v, t) {
612                return Err(ProofError::OccursCheck {
613                    variable: v.clone(),
614                    term: t.clone(),
615                });
616            }
617            subst.insert(v.clone(), t.clone());
618            Ok(())
619        }
620
621        // BoundVarRef on the right: bind it to the left term
622        (t, ProofTerm::BoundVarRef(v)) => {
623            if occurs(v, t) {
624                return Err(ProofError::OccursCheck {
625                    variable: v.clone(),
626                    term: t.clone(),
627                });
628            }
629            subst.insert(v.clone(), t.clone());
630            Ok(())
631        }
632
633        // Function unification: same name and arity, unify arguments pairwise
634        (ProofTerm::Function(f1, args1), ProofTerm::Function(f2, args2)) => {
635            if f1 != f2 {
636                return Err(ProofError::SymbolMismatch {
637                    left: f1.clone(),
638                    right: f2.clone(),
639                });
640            }
641            if args1.len() != args2.len() {
642                return Err(ProofError::ArityMismatch {
643                    expected: args1.len(),
644                    found: args2.len(),
645                });
646            }
647            for (a1, a2) in args1.iter().zip(args2.iter()) {
648                unify_terms_with_subst(a1, a2, subst)?;
649            }
650            Ok(())
651        }
652
653        // Group unification: same length, unify elements pairwise
654        (ProofTerm::Group(g1), ProofTerm::Group(g2)) => {
655            if g1.len() != g2.len() {
656                return Err(ProofError::ArityMismatch {
657                    expected: g1.len(),
658                    found: g2.len(),
659                });
660            }
661            for (t1, t2) in g1.iter().zip(g2.iter()) {
662                unify_terms_with_subst(t1, t2, subst)?;
663            }
664            Ok(())
665        }
666
667        // Any other combination fails
668        _ => Err(ProofError::UnificationFailed {
669            left: t1,
670            right: t2,
671        }),
672    }
673}
674
675/// Check if a variable occurs in a term (for occurs check).
676/// Prevents infinite types like x = f(x).
677fn occurs(var: &str, term: &ProofTerm) -> bool {
678    match term {
679        ProofTerm::Variable(v) => v == var,
680        ProofTerm::BoundVarRef(v) => v == var, // BoundVarRef participates in occurs check
681        ProofTerm::Constant(_) => false,
682        ProofTerm::Function(_, args) => args.iter().any(|a| occurs(var, a)),
683        ProofTerm::Group(terms) => terms.iter().any(|t| occurs(var, t)),
684    }
685}
686
687/// Apply a substitution to a term.
688///
689/// Replaces all variables in the term according to the substitution mapping.
690/// Handles transitive chains: if `{x ↦ y, y ↦ z}`, then applying to `x` yields `z`.
691///
692/// # Arguments
693///
694/// * `term` - The term to transform
695/// * `subst` - The substitution mapping variables to replacement terms
696///
697/// # Returns
698///
699/// A new term with all substitutions applied.
700///
701/// # Example
702///
703/// ```
704/// use logicaffeine_proof::{ProofTerm, unify::{Substitution, apply_subst_to_term}};
705/// use std::collections::HashMap;
706///
707/// let mut subst = Substitution::new();
708/// subst.insert("x".into(), ProofTerm::Constant("Socrates".into()));
709///
710/// let term = ProofTerm::Function(
711///     "Mortal".into(),
712///     vec![ProofTerm::Variable("x".into())]
713/// );
714///
715/// let result = apply_subst_to_term(&term, &subst);
716/// // result = Mortal(Socrates)
717/// ```
718pub fn apply_subst_to_term(term: &ProofTerm, subst: &Substitution) -> ProofTerm {
719    match term {
720        ProofTerm::Variable(v) => {
721            if let Some(replacement) = subst.get(v) {
722                // Recursively apply to handle chains like {x ↦ y, y ↦ z}
723                apply_subst_to_term(replacement, subst)
724            } else {
725                term.clone()
726            }
727        }
728        // BoundVarRef participates in substitution (for instantiating quantified formulas)
729        ProofTerm::BoundVarRef(v) => {
730            if let Some(replacement) = subst.get(v) {
731                apply_subst_to_term(replacement, subst)
732            } else {
733                term.clone()
734            }
735        }
736        ProofTerm::Constant(_) => term.clone(),
737        ProofTerm::Function(name, args) => {
738            let new_args = args.iter().map(|a| apply_subst_to_term(a, subst)).collect();
739            ProofTerm::Function(name.clone(), new_args)
740        }
741        ProofTerm::Group(terms) => {
742            let new_terms = terms.iter().map(|t| apply_subst_to_term(t, subst)).collect();
743            ProofTerm::Group(new_terms)
744        }
745    }
746}
747
748// =============================================================================
749// EXPRESSION-LEVEL UNIFICATION
750// =============================================================================
751
752/// Unify two expressions, returning the Most General Unifier.
753///
754/// Expression unification extends term unification with support for logical
755/// connectives, quantifiers, and alpha-equivalence for bound variables.
756///
757/// # Alpha-Equivalence
758///
759/// Bound variable names are considered arbitrary:
760/// - `∀x P(x)` unifies with `∀y P(y)`
761/// - `∃e Run(e)` unifies with `∃x Run(x)`
762///
763/// This is achieved by substituting fresh constants for bound variables
764/// before comparing bodies.
765///
766/// # Beta-Reduction
767///
768/// Both expressions are beta-reduced before comparison, so
769/// `(λx. P(x))(a)` will unify with `P(a)`.
770///
771/// # Arguments
772///
773/// * `e1` - The first expression
774/// * `e2` - The second expression
775///
776/// # Returns
777///
778/// * `Ok(subst)` - A substitution unifying the expressions
779/// * `Err(ExprUnificationFailed)` - If expressions cannot be unified
780///
781/// # See Also
782///
783/// * [`unify_terms`] - Underlying term unification
784/// * [`beta_reduce`] - Normalization applied before unification
785/// * [`unify_pattern`] - Higher-order pattern unification for holes
786pub fn unify_exprs(e1: &ProofExpr, e2: &ProofExpr) -> ProofResult<Substitution> {
787    let mut subst = Substitution::new();
788    unify_exprs_with_subst(e1, e2, &mut subst)?;
789    Ok(subst)
790}
791
792/// Internal expression unification with accumulating substitution.
793fn unify_exprs_with_subst(
794    e1: &ProofExpr,
795    e2: &ProofExpr,
796    subst: &mut Substitution,
797) -> ProofResult<()> {
798    // Beta-reduce both expressions before unification.
799    // This normalizes lambda applications: (λx. P(x))(a) → P(a)
800    let e1 = beta_reduce(e1);
801    let e2 = beta_reduce(e2);
802
803    match (&e1, &e2) {
804        // Atom unification
805        (ProofExpr::Atom(a1), ProofExpr::Atom(a2)) if a1 == a2 => Ok(()),
806
807        // Predicate unification: same name, unify arguments
808        (
809            ProofExpr::Predicate { name: n1, args: a1, world: w1 },
810            ProofExpr::Predicate { name: n2, args: a2, world: w2 },
811        ) => {
812            if n1 != n2 {
813                return Err(ProofError::SymbolMismatch {
814                    left: n1.clone(),
815                    right: n2.clone(),
816                });
817            }
818            if a1.len() != a2.len() {
819                return Err(ProofError::ArityMismatch {
820                    expected: a1.len(),
821                    found: a2.len(),
822                });
823            }
824            // Unify worlds if both present
825            match (w1, w2) {
826                (Some(w1), Some(w2)) if w1 != w2 => {
827                    return Err(ProofError::SymbolMismatch {
828                        left: w1.clone(),
829                        right: w2.clone(),
830                    });
831                }
832                _ => {}
833            }
834            // Unify arguments
835            for (t1, t2) in a1.iter().zip(a2.iter()) {
836                unify_terms_with_subst(t1, t2, subst)?;
837            }
838            Ok(())
839        }
840
841        // Identity unification
842        (ProofExpr::Identity(l1, r1), ProofExpr::Identity(l2, r2)) => {
843            unify_terms_with_subst(l1, l2, subst)?;
844            unify_terms_with_subst(r1, r2, subst)?;
845            Ok(())
846        }
847
848        // Binary operators: same operator, unify both sides
849        (ProofExpr::And(l1, r1), ProofExpr::And(l2, r2))
850        | (ProofExpr::Or(l1, r1), ProofExpr::Or(l2, r2))
851        | (ProofExpr::Implies(l1, r1), ProofExpr::Implies(l2, r2))
852        | (ProofExpr::Iff(l1, r1), ProofExpr::Iff(l2, r2)) => {
853            unify_exprs_with_subst(l1, l2, subst)?;
854            unify_exprs_with_subst(r1, r2, subst)?;
855            Ok(())
856        }
857
858        // Negation
859        (ProofExpr::Not(inner1), ProofExpr::Not(inner2)) => {
860            unify_exprs_with_subst(inner1, inner2, subst)
861        }
862
863        // Quantifiers: Alpha-equivalence - bound variable names are arbitrary
864        // ∃e P(e) ≡ ∃x P(x) because they describe the same logical content
865        (
866            ProofExpr::ForAll { variable: v1, body: b1 },
867            ProofExpr::ForAll { variable: v2, body: b2 },
868        )
869        | (
870            ProofExpr::Exists { variable: v1, body: b1 },
871            ProofExpr::Exists { variable: v2, body: b2 },
872        ) => {
873            // Generate a fresh constant to substitute for both bound variables.
874            // Using a constant (not a variable) avoids capture issues.
875            let fresh = fresh_alpha_constant();
876
877            // Create substitutions for each bound variable
878            let subst1: Substitution = [(v1.clone(), fresh.clone())].into_iter().collect();
879            let subst2: Substitution = [(v2.clone(), fresh)].into_iter().collect();
880
881            // Apply substitutions to bodies
882            let body1_renamed = apply_subst_to_expr(b1, &subst1);
883            let body2_renamed = apply_subst_to_expr(b2, &subst2);
884
885            // Recursively unify the renamed bodies
886            unify_exprs_with_subst(&body1_renamed, &body2_renamed, subst)
887        }
888
889        // Lambda expressions: Alpha-equivalence - λx.P(x) ≡ λy.P(y)
890        (
891            ProofExpr::Lambda { variable: v1, body: b1 },
892            ProofExpr::Lambda { variable: v2, body: b2 },
893        ) => {
894            // Same alpha-renaming technique as quantifiers
895            let fresh = fresh_alpha_constant();
896            let subst1: Substitution = [(v1.clone(), fresh.clone())].into_iter().collect();
897            let subst2: Substitution = [(v2.clone(), fresh)].into_iter().collect();
898            let body1_renamed = apply_subst_to_expr(b1, &subst1);
899            let body2_renamed = apply_subst_to_expr(b2, &subst2);
900            unify_exprs_with_subst(&body1_renamed, &body2_renamed, subst)
901        }
902
903        // Application
904        (ProofExpr::App(f1, a1), ProofExpr::App(f2, a2)) => {
905            unify_exprs_with_subst(f1, f2, subst)?;
906            unify_exprs_with_subst(a1, a2, subst)?;
907            Ok(())
908        }
909
910        // NeoEvent: Alpha-equivalence for event variables
911        // ∃e(Run(e) ∧ Agent(e, John)) should unify with ∃x(Run(x) ∧ Agent(x, John))
912        (
913            ProofExpr::NeoEvent {
914                event_var: e1,
915                verb: v1,
916                roles: r1,
917            },
918            ProofExpr::NeoEvent {
919                event_var: e2,
920                verb: v2,
921                roles: r2,
922            },
923        ) => {
924            // Verb names must match (case-insensitive for robustness)
925            if v1.to_lowercase() != v2.to_lowercase() {
926                return Err(ProofError::SymbolMismatch {
927                    left: v1.clone(),
928                    right: v2.clone(),
929                });
930            }
931
932            // Roles must have same length
933            if r1.len() != r2.len() {
934                return Err(ProofError::ArityMismatch {
935                    expected: r1.len(),
936                    found: r2.len(),
937                });
938            }
939
940            // Alpha-equivalence: generate fresh constant for event variable
941            let fresh = fresh_alpha_constant();
942            let subst1: Substitution = [(e1.clone(), fresh.clone())].into_iter().collect();
943            let subst2: Substitution = [(e2.clone(), fresh)].into_iter().collect();
944
945            // Unify roles pairwise with alpha-renamed event variables
946            for ((role1, term1), (role2, term2)) in r1.iter().zip(r2.iter()) {
947                // Role names must match
948                if role1 != role2 {
949                    return Err(ProofError::SymbolMismatch {
950                        left: role1.clone(),
951                        right: role2.clone(),
952                    });
953                }
954                // Apply alpha-renaming to terms and unify
955                let t1_renamed = apply_subst_to_term(term1, &subst1);
956                let t2_renamed = apply_subst_to_term(term2, &subst2);
957                unify_terms_with_subst(&t1_renamed, &t2_renamed, subst)?;
958            }
959            Ok(())
960        }
961
962        // Temporal operators: Past(P), Future(P)
963        // Same operator required, then unify bodies
964        (
965            ProofExpr::Temporal { operator: op1, body: b1 },
966            ProofExpr::Temporal { operator: op2, body: b2 },
967        ) => {
968            if op1 != op2 {
969                return Err(ProofError::ExprUnificationFailed {
970                    left: e1.clone(),
971                    right: e2.clone(),
972                });
973            }
974            unify_exprs_with_subst(b1, b2, subst)
975        }
976
977        // Binary temporal operators: Until(P,Q), Release(P,Q)
978        // Same operator required, then unify both children
979        (
980            ProofExpr::TemporalBinary { operator: op1, left: l1, right: r1 },
981            ProofExpr::TemporalBinary { operator: op2, left: l2, right: r2 },
982        ) => {
983            if op1 != op2 {
984                return Err(ProofError::ExprUnificationFailed {
985                    left: e1.clone(),
986                    right: e2.clone(),
987                });
988            }
989            unify_exprs_with_subst(l1, l2, subst)?;
990            unify_exprs_with_subst(r1, r2, subst)
991        }
992
993        // Anything else fails
994        _ => Err(ProofError::ExprUnificationFailed {
995            left: e1.clone(),
996            right: e2.clone(),
997        }),
998    }
999}
1000
1001/// Apply a substitution to an expression.
1002///
1003/// Recursively replaces variables in terms within the expression according
1004/// to the substitution mapping. Also handles binder renaming when the
1005/// substitution maps a bound variable name to a new variable.
1006///
1007/// # Arguments
1008///
1009/// * `expr` - The expression to transform
1010/// * `subst` - The substitution mapping variables to replacement terms
1011///
1012/// # Returns
1013///
1014/// A new expression with all substitutions applied to embedded terms.
1015///
1016/// # Binder Handling
1017///
1018/// When a quantifier or lambda binds a variable that maps to another variable
1019/// in the substitution, the binder is renamed:
1020///
1021/// ```text
1022/// apply_subst_to_expr(∀x P(x), {x ↦ y}) = ∀y P(y)
1023/// ```
1024pub fn apply_subst_to_expr(expr: &ProofExpr, subst: &Substitution) -> ProofExpr {
1025    match expr {
1026        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
1027            name: name.clone(),
1028            args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
1029            world: world.clone(),
1030        },
1031        ProofExpr::Identity(l, r) => ProofExpr::Identity(
1032            apply_subst_to_term(l, subst),
1033            apply_subst_to_term(r, subst),
1034        ),
1035        ProofExpr::Atom(a) => ProofExpr::Atom(a.clone()),
1036        ProofExpr::And(l, r) => ProofExpr::And(
1037            Box::new(apply_subst_to_expr(l, subst)),
1038            Box::new(apply_subst_to_expr(r, subst)),
1039        ),
1040        ProofExpr::Or(l, r) => ProofExpr::Or(
1041            Box::new(apply_subst_to_expr(l, subst)),
1042            Box::new(apply_subst_to_expr(r, subst)),
1043        ),
1044        ProofExpr::Implies(l, r) => ProofExpr::Implies(
1045            Box::new(apply_subst_to_expr(l, subst)),
1046            Box::new(apply_subst_to_expr(r, subst)),
1047        ),
1048        ProofExpr::Iff(l, r) => ProofExpr::Iff(
1049            Box::new(apply_subst_to_expr(l, subst)),
1050            Box::new(apply_subst_to_expr(r, subst)),
1051        ),
1052        ProofExpr::Not(inner) => ProofExpr::Not(Box::new(apply_subst_to_expr(inner, subst))),
1053        ProofExpr::ForAll { variable, body } => {
1054            // If the variable is being renamed (maps to a Variable), update the binder
1055            let new_variable = match subst.get(variable) {
1056                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1057                _ => variable.clone(),
1058            };
1059            ProofExpr::ForAll {
1060                variable: new_variable,
1061                body: Box::new(apply_subst_to_expr(body, subst)),
1062            }
1063        }
1064        ProofExpr::Exists { variable, body } => {
1065            // If the variable is being renamed (maps to a Variable), update the binder
1066            let new_variable = match subst.get(variable) {
1067                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1068                _ => variable.clone(),
1069            };
1070            ProofExpr::Exists {
1071                variable: new_variable,
1072                body: Box::new(apply_subst_to_expr(body, subst)),
1073            }
1074        }
1075        ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
1076            domain: domain.clone(),
1077            force: *force,
1078            flavor: flavor.clone(),
1079            body: Box::new(apply_subst_to_expr(body, subst)),
1080        },
1081        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
1082            operator: operator.clone(),
1083            body: Box::new(apply_subst_to_expr(body, subst)),
1084        },
1085        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
1086            operator: operator.clone(),
1087            left: Box::new(apply_subst_to_expr(left, subst)),
1088            right: Box::new(apply_subst_to_expr(right, subst)),
1089        },
1090        ProofExpr::Lambda { variable, body } => {
1091            // If the variable is being renamed (maps to a Variable), update the binder
1092            let new_variable = match subst.get(variable) {
1093                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1094                _ => variable.clone(),
1095            };
1096            ProofExpr::Lambda {
1097                variable: new_variable,
1098                body: Box::new(apply_subst_to_expr(body, subst)),
1099            }
1100        }
1101        ProofExpr::App(f, a) => ProofExpr::App(
1102            Box::new(apply_subst_to_expr(f, subst)),
1103            Box::new(apply_subst_to_expr(a, subst)),
1104        ),
1105        ProofExpr::NeoEvent { event_var, verb, roles } => ProofExpr::NeoEvent {
1106            event_var: event_var.clone(),
1107            verb: verb.clone(),
1108            roles: roles
1109                .iter()
1110                .map(|(r, t)| (r.clone(), apply_subst_to_term(t, subst)))
1111                .collect(),
1112        },
1113        // Peano / Inductive Types
1114        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
1115            name: name.clone(),
1116            args: args.iter().map(|a| apply_subst_to_expr(a, subst)).collect(),
1117        },
1118        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
1119            scrutinee: Box::new(apply_subst_to_expr(scrutinee, subst)),
1120            arms: arms
1121                .iter()
1122                .map(|arm| MatchArm {
1123                    ctor: arm.ctor.clone(),
1124                    bindings: arm.bindings.clone(),
1125                    body: apply_subst_to_expr(&arm.body, subst),
1126                })
1127                .collect(),
1128        },
1129        ProofExpr::Fixpoint { name, body } => ProofExpr::Fixpoint {
1130            name: name.clone(),
1131            body: Box::new(apply_subst_to_expr(body, subst)),
1132        },
1133        ProofExpr::TypedVar { name, typename } => ProofExpr::TypedVar {
1134            name: name.clone(),
1135            typename: typename.clone(),
1136        },
1137        ProofExpr::Unsupported(s) => ProofExpr::Unsupported(s.clone()),
1138        // Holes are meta-variables - term substitution doesn't apply
1139        ProofExpr::Hole(name) => ProofExpr::Hole(name.clone()),
1140        // Terms: apply substitution to the inner term
1141        ProofExpr::Term(t) => ProofExpr::Term(apply_subst_to_term(t, subst)),
1142    }
1143}
1144
1145/// Compose two substitutions: apply s2 after s1.
1146///
1147/// The resulting substitution applies s1 first, then s2. This is the standard
1148/// composition operation for Most General Unifiers.
1149///
1150/// # Semantics
1151///
1152/// For any term t: `apply(compose(s1, s2), t) = apply(s2, apply(s1, t))`
1153///
1154/// # Arguments
1155///
1156/// * `s1` - The first substitution (applied first)
1157/// * `s2` - The second substitution (applied second)
1158///
1159/// # Returns
1160///
1161/// A combined substitution equivalent to applying s1 then s2.
1162///
1163/// # Example
1164///
1165/// ```
1166/// use logicaffeine_proof::{ProofTerm, unify::{Substitution, compose_substitutions}};
1167///
1168/// let mut s1 = Substitution::new();
1169/// s1.insert("x".into(), ProofTerm::Variable("y".into()));
1170///
1171/// let mut s2 = Substitution::new();
1172/// s2.insert("y".into(), ProofTerm::Constant("a".into()));
1173///
1174/// let composed = compose_substitutions(s1, s2);
1175/// // x ↦ a (via y), y ↦ a
1176/// ```
1177pub fn compose_substitutions(s1: Substitution, s2: Substitution) -> Substitution {
1178    let mut result: Substitution = s1
1179        .into_iter()
1180        .map(|(k, v)| (k, apply_subst_to_term(&v, &s2)))
1181        .collect();
1182
1183    // Add bindings from s2 that aren't in s1
1184    for (k, v) in s2 {
1185        result.entry(k).or_insert(v);
1186    }
1187
1188    result
1189}
1190
1191// =============================================================================
1192// HIGHER-ORDER PATTERN UNIFICATION
1193// =============================================================================
1194
1195/// Attempt higher-order pattern unification (Miller patterns).
1196///
1197/// Given `lhs` and `rhs`, if `lhs` is of the form `Hole(h)(args...)` where
1198/// args are distinct `BoundVarRef`s, solves: `h = λargs. rhs`.
1199///
1200/// # Miller Patterns
1201///
1202/// A Miller pattern is a meta-variable applied to distinct bound variables:
1203/// - `?P(x)` is a valid pattern
1204/// - `?F(x, y)` is a valid pattern (x ≠ y)
1205/// - `?G(x, x)` is NOT valid (duplicate variable)
1206/// - `?H(f(x))` is NOT valid (non-variable argument)
1207///
1208/// # Why This Matters
1209///
1210/// Higher-order pattern unification is decidable and has unique most general
1211/// solutions, unlike full higher-order unification. This restriction enables
1212/// automatic motive inference for structural induction.
1213///
1214/// # Returns
1215///
1216/// * `Ok(subst)` - An [`ExprSubstitution`] mapping hole names to lambdas
1217/// * `Err(PatternNotDistinct)` - If pattern has duplicate variables
1218/// * `Err(NotAPattern)` - If arguments aren't bound variable references
1219/// * `Err(ScopeViolation)` - If RHS uses variables not in pattern scope
1220///
1221/// # Example
1222///
1223/// ```text
1224/// unify_pattern(?P(x), Even(x))
1225/// → { "P" ↦ λx. Even(x) }
1226/// ```
1227///
1228/// # See Also
1229///
1230/// * [`ExprSubstitution`] - The result type for hole solutions
1231/// * [`ProofError::PatternNotDistinct`] - Duplicate variable error
1232pub fn unify_pattern(lhs: &ProofExpr, rhs: &ProofExpr) -> ProofResult<ExprSubstitution> {
1233    let mut solution = ExprSubstitution::new();
1234    unify_pattern_internal(lhs, rhs, &mut solution)?;
1235    Ok(solution)
1236}
1237
1238/// Internal pattern unification with accumulating solution.
1239fn unify_pattern_internal(
1240    lhs: &ProofExpr,
1241    rhs: &ProofExpr,
1242    solution: &mut ExprSubstitution,
1243) -> ProofResult<()> {
1244    // Beta-reduce both sides first
1245    let lhs = beta_reduce(lhs);
1246    let rhs = beta_reduce(rhs);
1247
1248    match &lhs {
1249        // Case: Bare hole ?P = rhs
1250        ProofExpr::Hole(h) => {
1251            solution.insert(h.clone(), rhs.clone());
1252            Ok(())
1253        }
1254
1255        // Case: Application - might be Hole(h)(args...)
1256        ProofExpr::App(_, _) => {
1257            // Collect all arguments and find the head
1258            let (head, args) = collect_app_args(&lhs);
1259
1260            if let ProofExpr::Hole(h) = head {
1261                // Check Miller pattern: all args must be distinct BoundVarRefs
1262                let var_args = extract_distinct_vars(&args)?;
1263
1264                // Check that rhs only uses variables from var_args (scope check)
1265                check_scope(&rhs, &var_args)?;
1266
1267                // Construct solution: h = λargs. rhs
1268                // Need to rename variables in rhs to match the BoundVarRef names
1269                let renamed_rhs = rename_vars_to_bound(&rhs, &var_args);
1270                let lambda = build_lambda(var_args, renamed_rhs);
1271                solution.insert(h.clone(), lambda);
1272                Ok(())
1273            } else {
1274                // Not a pattern, try structural equality
1275                if lhs == rhs {
1276                    Ok(())
1277                } else {
1278                    Err(ProofError::ExprUnificationFailed {
1279                        left: lhs.clone(),
1280                        right: rhs.clone(),
1281                    })
1282                }
1283            }
1284        }
1285
1286        // Other cases: structural equality
1287        _ => {
1288            if lhs == rhs {
1289                Ok(())
1290            } else {
1291                Err(ProofError::ExprUnificationFailed {
1292                    left: lhs.clone(),
1293                    right: rhs.clone(),
1294                })
1295            }
1296        }
1297    }
1298}
1299
1300/// Collect f(a)(b)(c) into (f, [a, b, c])
1301fn collect_app_args(expr: &ProofExpr) -> (ProofExpr, Vec<ProofExpr>) {
1302    let mut args = Vec::new();
1303    let mut current = expr.clone();
1304
1305    while let ProofExpr::App(func, arg) = current {
1306        args.push(*arg);
1307        current = *func;
1308    }
1309
1310    args.reverse();
1311    (current, args)
1312}
1313
1314/// Extract distinct variable names from pattern arguments.
1315/// Fails if any arg is not a Term(BoundVarRef) or if duplicates exist.
1316fn extract_distinct_vars(args: &[ProofExpr]) -> ProofResult<Vec<String>> {
1317    let mut vars = Vec::new();
1318    for arg in args {
1319        match arg {
1320            ProofExpr::Term(ProofTerm::BoundVarRef(v)) => {
1321                if vars.contains(v) {
1322                    return Err(ProofError::PatternNotDistinct(v.clone()));
1323                }
1324                vars.push(v.clone());
1325            }
1326            _ => return Err(ProofError::NotAPattern(arg.clone())),
1327        }
1328    }
1329    Ok(vars)
1330}
1331
1332/// Check that all free variables in expr are in the allowed set.
1333fn check_scope(expr: &ProofExpr, allowed: &[String]) -> ProofResult<()> {
1334    let free_vars = collect_free_vars(expr);
1335    for var in free_vars {
1336        if !allowed.contains(&var) {
1337            return Err(ProofError::ScopeViolation {
1338                var,
1339                allowed: allowed.to_vec(),
1340            });
1341        }
1342    }
1343    Ok(())
1344}
1345
1346/// Collect free variables from an expression.
1347fn collect_free_vars(expr: &ProofExpr) -> Vec<String> {
1348    let mut vars = Vec::new();
1349    collect_free_vars_impl(expr, &mut vars, &mut Vec::new());
1350    vars
1351}
1352
1353fn collect_free_vars_impl(expr: &ProofExpr, vars: &mut Vec<String>, bound: &mut Vec<String>) {
1354    match expr {
1355        ProofExpr::Predicate { args, .. } => {
1356            for arg in args {
1357                collect_free_vars_term(arg, vars, bound);
1358            }
1359        }
1360        ProofExpr::Identity(l, r) => {
1361            collect_free_vars_term(l, vars, bound);
1362            collect_free_vars_term(r, vars, bound);
1363        }
1364        ProofExpr::Atom(s) => {
1365            if !bound.contains(s) && !vars.contains(s) {
1366                vars.push(s.clone());
1367            }
1368        }
1369        ProofExpr::And(l, r)
1370        | ProofExpr::Or(l, r)
1371        | ProofExpr::Implies(l, r)
1372        | ProofExpr::Iff(l, r) => {
1373            collect_free_vars_impl(l, vars, bound);
1374            collect_free_vars_impl(r, vars, bound);
1375        }
1376        ProofExpr::Not(inner) => collect_free_vars_impl(inner, vars, bound),
1377        ProofExpr::ForAll { variable, body }
1378        | ProofExpr::Exists { variable, body }
1379        | ProofExpr::Lambda { variable, body } => {
1380            bound.push(variable.clone());
1381            collect_free_vars_impl(body, vars, bound);
1382            bound.pop();
1383        }
1384        ProofExpr::App(f, a) => {
1385            collect_free_vars_impl(f, vars, bound);
1386            collect_free_vars_impl(a, vars, bound);
1387        }
1388        ProofExpr::Term(t) => collect_free_vars_term(t, vars, bound),
1389        ProofExpr::Hole(_) => {} // Holes don't contribute free vars
1390        _ => {} // Other cases don't add free vars
1391    }
1392}
1393
1394fn collect_free_vars_term(term: &ProofTerm, vars: &mut Vec<String>, bound: &[String]) {
1395    match term {
1396        ProofTerm::Variable(v) => {
1397            if !bound.contains(v) && !vars.contains(v) {
1398                vars.push(v.clone());
1399            }
1400        }
1401        ProofTerm::Function(_, args) => {
1402            for arg in args {
1403                collect_free_vars_term(arg, vars, bound);
1404            }
1405        }
1406        ProofTerm::Group(terms) => {
1407            for t in terms {
1408                collect_free_vars_term(t, vars, bound);
1409            }
1410        }
1411        ProofTerm::Constant(_) | ProofTerm::BoundVarRef(_) => {}
1412    }
1413}
1414
1415/// Rename Variable(x) to Variable(x) if x is in the bound vars list.
1416/// This ensures the solution lambda binds the right names.
1417fn rename_vars_to_bound(expr: &ProofExpr, bound_vars: &[String]) -> ProofExpr {
1418    // For the basic case, we don't need to rename since the RHS already uses
1419    // Variable("x") and we want the lambda to bind "x".
1420    // The key is just to use the same names.
1421    expr.clone()
1422}
1423
1424/// Build λx₁.λx₂...λxₙ. body
1425fn build_lambda(vars: Vec<String>, body: ProofExpr) -> ProofExpr {
1426    vars.into_iter().rev().fold(body, |acc, var| {
1427        ProofExpr::Lambda {
1428            variable: var,
1429            body: Box::new(acc),
1430        }
1431    })
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436    use super::*;
1437
1438    #[test]
1439    fn test_unify_same_constant() {
1440        let t1 = ProofTerm::Constant("a".into());
1441        let t2 = ProofTerm::Constant("a".into());
1442        let result = unify_terms(&t1, &t2);
1443        assert!(result.is_ok());
1444        assert!(result.unwrap().is_empty());
1445    }
1446
1447    #[test]
1448    fn test_unify_different_constants() {
1449        let t1 = ProofTerm::Constant("a".into());
1450        let t2 = ProofTerm::Constant("b".into());
1451        let result = unify_terms(&t1, &t2);
1452        assert!(result.is_err());
1453    }
1454
1455    #[test]
1456    fn test_unify_var_constant() {
1457        let t1 = ProofTerm::Variable("x".into());
1458        let t2 = ProofTerm::Constant("a".into());
1459        let result = unify_terms(&t1, &t2);
1460        assert!(result.is_ok());
1461        let subst = result.unwrap();
1462        assert_eq!(subst.get("x"), Some(&ProofTerm::Constant("a".into())));
1463    }
1464
1465    #[test]
1466    fn test_occurs_check() {
1467        let t1 = ProofTerm::Variable("x".into());
1468        let t2 = ProofTerm::Function("f".into(), vec![ProofTerm::Variable("x".into())]);
1469        let result = unify_terms(&t1, &t2);
1470        assert!(matches!(result, Err(ProofError::OccursCheck { .. })));
1471    }
1472
1473    #[test]
1474    fn test_compose_substitutions() {
1475        let mut s1 = Substitution::new();
1476        s1.insert("x".into(), ProofTerm::Variable("y".into()));
1477
1478        let mut s2 = Substitution::new();
1479        s2.insert("y".into(), ProofTerm::Constant("a".into()));
1480
1481        let composed = compose_substitutions(s1, s2);
1482
1483        // x should map to a (via y)
1484        assert_eq!(composed.get("x"), Some(&ProofTerm::Constant("a".into())));
1485        // y should also map to a
1486        assert_eq!(composed.get("y"), Some(&ProofTerm::Constant("a".into())));
1487    }
1488
1489    // =========================================================================
1490    // ALPHA-EQUIVALENCE TESTS
1491    // =========================================================================
1492
1493    #[test]
1494    fn test_alpha_equivalence_exists() {
1495        // ∃e P(e) should unify with ∃x P(x)
1496        let e1 = ProofExpr::Exists {
1497            variable: "e".to_string(),
1498            body: Box::new(ProofExpr::Predicate {
1499                name: "run".to_string(),
1500                args: vec![ProofTerm::Variable("e".to_string())],
1501                world: None,
1502            }),
1503        };
1504
1505        let e2 = ProofExpr::Exists {
1506            variable: "x".to_string(),
1507            body: Box::new(ProofExpr::Predicate {
1508                name: "run".to_string(),
1509                args: vec![ProofTerm::Variable("x".to_string())],
1510                world: None,
1511            }),
1512        };
1513
1514        let result = unify_exprs(&e1, &e2);
1515        assert!(
1516            result.is_ok(),
1517            "Alpha-equivalent expressions should unify: {:?}",
1518            result
1519        );
1520    }
1521
1522    #[test]
1523    fn test_alpha_equivalence_forall() {
1524        // ∀x P(x) should unify with ∀y P(y)
1525        let e1 = ProofExpr::ForAll {
1526            variable: "x".to_string(),
1527            body: Box::new(ProofExpr::Predicate {
1528                name: "mortal".to_string(),
1529                args: vec![ProofTerm::Variable("x".to_string())],
1530                world: None,
1531            }),
1532        };
1533
1534        let e2 = ProofExpr::ForAll {
1535            variable: "y".to_string(),
1536            body: Box::new(ProofExpr::Predicate {
1537                name: "mortal".to_string(),
1538                args: vec![ProofTerm::Variable("y".to_string())],
1539                world: None,
1540            }),
1541        };
1542
1543        let result = unify_exprs(&e1, &e2);
1544        assert!(
1545            result.is_ok(),
1546            "Alpha-equivalent universals should unify: {:?}",
1547            result
1548        );
1549    }
1550
1551    #[test]
1552    fn test_alpha_equivalence_nested() {
1553        // ∃e (Run(e) ∧ Agent(e, John)) should unify with ∃x (Run(x) ∧ Agent(x, John))
1554        let e1 = ProofExpr::Exists {
1555            variable: "e".to_string(),
1556            body: Box::new(ProofExpr::And(
1557                Box::new(ProofExpr::Predicate {
1558                    name: "run".to_string(),
1559                    args: vec![ProofTerm::Variable("e".to_string())],
1560                    world: None,
1561                }),
1562                Box::new(ProofExpr::Predicate {
1563                    name: "agent".to_string(),
1564                    args: vec![
1565                        ProofTerm::Variable("e".to_string()),
1566                        ProofTerm::Constant("John".to_string()),
1567                    ],
1568                    world: None,
1569                }),
1570            )),
1571        };
1572
1573        let e2 = ProofExpr::Exists {
1574            variable: "x".to_string(),
1575            body: Box::new(ProofExpr::And(
1576                Box::new(ProofExpr::Predicate {
1577                    name: "run".to_string(),
1578                    args: vec![ProofTerm::Variable("x".to_string())],
1579                    world: None,
1580                }),
1581                Box::new(ProofExpr::Predicate {
1582                    name: "agent".to_string(),
1583                    args: vec![
1584                        ProofTerm::Variable("x".to_string()),
1585                        ProofTerm::Constant("John".to_string()),
1586                    ],
1587                    world: None,
1588                }),
1589            )),
1590        };
1591
1592        let result = unify_exprs(&e1, &e2);
1593        assert!(
1594            result.is_ok(),
1595            "Nested alpha-equivalent expressions should unify: {:?}",
1596            result
1597        );
1598    }
1599}