Skip to main content

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::Counterfactual { antecedent, consequent } => ProofExpr::Counterfactual {
218            antecedent: Box::new(beta_reduce(antecedent)),
219            consequent: Box::new(beta_reduce(consequent)),
220        },
221        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
222            operator: operator.clone(),
223            body: Box::new(beta_reduce(body)),
224        },
225        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
226            operator: operator.clone(),
227            left: Box::new(beta_reduce(left)),
228            right: Box::new(beta_reduce(right)),
229        },
230
231        // Reduce inside Ctor arguments
232        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
233            name: name.clone(),
234            args: args.iter().map(beta_reduce).collect(),
235        },
236
237        // Iota reduction: match (Ctor args) with arms → selected arm body
238        ProofExpr::Match { scrutinee, arms } => {
239            let reduced_scrutinee = beta_reduce(scrutinee);
240
241            // Try iota reduction: if scrutinee is a Ctor, select matching arm
242            if let ProofExpr::Ctor { name: ctor_name, args: ctor_args } = &reduced_scrutinee {
243                for arm in arms {
244                    if &arm.ctor == ctor_name {
245                        // Found matching arm - substitute constructor args for bindings
246                        let mut result = arm.body.clone();
247                        for (binding, arg) in arm.bindings.iter().zip(ctor_args.iter()) {
248                            result = substitute_expr_for_var(&result, binding, arg);
249                        }
250                        // Continue reducing the result
251                        return beta_reduce(&result);
252                    }
253                }
254            }
255
256            // No iota reduction possible - just reduce subexpressions
257            ProofExpr::Match {
258                scrutinee: Box::new(reduced_scrutinee),
259                arms: arms.iter().map(|arm| MatchArm {
260                    ctor: arm.ctor.clone(),
261                    bindings: arm.bindings.clone(),
262                    body: beta_reduce(&arm.body),
263                }).collect(),
264            }
265        }
266
267        // Reduce inside Fixpoint
268        ProofExpr::Fixpoint { name, body } => ProofExpr::Fixpoint {
269            name: name.clone(),
270            body: Box::new(beta_reduce(body)),
271        },
272
273        // Atomic expressions don't reduce
274        ProofExpr::Predicate { .. }
275        | ProofExpr::Identity(_, _)
276        | ProofExpr::Atom(_)
277        | ProofExpr::NeoEvent { .. }
278        | ProofExpr::TypedVar { .. }
279        | ProofExpr::Unsupported(_)
280        | ProofExpr::Hole(_)
281        | ProofExpr::Term(_) => expr.clone(),
282    }
283}
284
285/// Collect the free variables of an expression (complete, binder-aware).
286///
287/// Unlike [`collect_free_vars`], this descends into every binder form (modal,
288/// temporal, event, match, fixpoint, …) so it is sound to use for capture
289/// detection.
290fn free_vars_expr(expr: &ProofExpr, bound: &mut Vec<String>, acc: &mut std::collections::HashSet<String>) {
291    match expr {
292        ProofExpr::Atom(s) => {
293            if !bound.iter().any(|b| b == s) {
294                acc.insert(s.clone());
295            }
296        }
297        ProofExpr::Predicate { args, .. } => {
298            for a in args {
299                free_vars_term(a, bound, acc);
300            }
301        }
302        ProofExpr::Identity(l, r) => {
303            free_vars_term(l, bound, acc);
304            free_vars_term(r, bound, acc);
305        }
306        ProofExpr::And(l, r)
307        | ProofExpr::Or(l, r)
308        | ProofExpr::Implies(l, r)
309        | ProofExpr::Iff(l, r) => {
310            free_vars_expr(l, bound, acc);
311            free_vars_expr(r, bound, acc);
312        }
313        ProofExpr::Not(i) => free_vars_expr(i, bound, acc),
314        ProofExpr::ForAll { variable, body }
315        | ProofExpr::Exists { variable, body }
316        | ProofExpr::Lambda { variable, body } => {
317            bound.push(variable.clone());
318            free_vars_expr(body, bound, acc);
319            bound.pop();
320        }
321        ProofExpr::Modal { body, .. } => free_vars_expr(body, bound, acc),
322        ProofExpr::Counterfactual { antecedent, consequent } => {
323            free_vars_expr(antecedent, bound, acc);
324            free_vars_expr(consequent, bound, acc);
325        }
326        ProofExpr::Temporal { body, .. } => free_vars_expr(body, bound, acc),
327        ProofExpr::TemporalBinary { left, right, .. } => {
328            free_vars_expr(left, bound, acc);
329            free_vars_expr(right, bound, acc);
330        }
331        ProofExpr::App(f, a) => {
332            free_vars_expr(f, bound, acc);
333            free_vars_expr(a, bound, acc);
334        }
335        ProofExpr::NeoEvent { event_var, roles, .. } => {
336            bound.push(event_var.clone());
337            for (_, t) in roles {
338                free_vars_term(t, bound, acc);
339            }
340            bound.pop();
341        }
342        ProofExpr::Ctor { args, .. } => {
343            for a in args {
344                free_vars_expr(a, bound, acc);
345            }
346        }
347        ProofExpr::Match { scrutinee, arms } => {
348            free_vars_expr(scrutinee, bound, acc);
349            for arm in arms {
350                let depth = arm.bindings.len();
351                for b in &arm.bindings {
352                    bound.push(b.clone());
353                }
354                free_vars_expr(&arm.body, bound, acc);
355                for _ in 0..depth {
356                    bound.pop();
357                }
358            }
359        }
360        ProofExpr::Fixpoint { name, body } => {
361            bound.push(name.clone());
362            free_vars_expr(body, bound, acc);
363            bound.pop();
364        }
365        ProofExpr::TypedVar { name, .. } => {
366            if !bound.iter().any(|b| b == name) {
367                acc.insert(name.clone());
368            }
369        }
370        ProofExpr::Hole(_) | ProofExpr::Unsupported(_) => {}
371        ProofExpr::Term(t) => free_vars_term(t, bound, acc),
372    }
373}
374
375fn free_vars_term(term: &ProofTerm, bound: &[String], acc: &mut std::collections::HashSet<String>) {
376    match term {
377        ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => {
378            if !bound.iter().any(|b| b == s) {
379                acc.insert(s.clone());
380            }
381        }
382        ProofTerm::Constant(_) => {}
383        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
384            for a in args {
385                free_vars_term(a, bound, acc);
386            }
387        }
388    }
389}
390
391/// Collect every name appearing anywhere in an expression (bound or free) so a
392/// fresh binder name can be chosen that collides with nothing.
393fn all_names_expr(expr: &ProofExpr, acc: &mut std::collections::HashSet<String>) {
394    match expr {
395        ProofExpr::Atom(s) => {
396            acc.insert(s.clone());
397        }
398        ProofExpr::Predicate { args, .. } => {
399            for a in args {
400                all_names_term(a, acc);
401            }
402        }
403        ProofExpr::Identity(l, r) => {
404            all_names_term(l, acc);
405            all_names_term(r, acc);
406        }
407        ProofExpr::And(l, r)
408        | ProofExpr::Or(l, r)
409        | ProofExpr::Implies(l, r)
410        | ProofExpr::Iff(l, r) => {
411            all_names_expr(l, acc);
412            all_names_expr(r, acc);
413        }
414        ProofExpr::Not(i) => all_names_expr(i, acc),
415        ProofExpr::ForAll { variable, body }
416        | ProofExpr::Exists { variable, body }
417        | ProofExpr::Lambda { variable, body } => {
418            acc.insert(variable.clone());
419            all_names_expr(body, acc);
420        }
421        ProofExpr::Modal { body, .. } => all_names_expr(body, acc),
422        ProofExpr::Counterfactual { antecedent, consequent } => {
423            all_names_expr(antecedent, acc);
424            all_names_expr(consequent, acc);
425        }
426        ProofExpr::Temporal { body, .. } => all_names_expr(body, acc),
427        ProofExpr::TemporalBinary { left, right, .. } => {
428            all_names_expr(left, acc);
429            all_names_expr(right, acc);
430        }
431        ProofExpr::App(f, a) => {
432            all_names_expr(f, acc);
433            all_names_expr(a, acc);
434        }
435        ProofExpr::NeoEvent { event_var, roles, .. } => {
436            acc.insert(event_var.clone());
437            for (_, t) in roles {
438                all_names_term(t, acc);
439            }
440        }
441        ProofExpr::Ctor { args, .. } => {
442            for a in args {
443                all_names_expr(a, acc);
444            }
445        }
446        ProofExpr::Match { scrutinee, arms } => {
447            all_names_expr(scrutinee, acc);
448            for arm in arms {
449                for b in &arm.bindings {
450                    acc.insert(b.clone());
451                }
452                all_names_expr(&arm.body, acc);
453            }
454        }
455        ProofExpr::Fixpoint { name, body } => {
456            acc.insert(name.clone());
457            all_names_expr(body, acc);
458        }
459        ProofExpr::TypedVar { name, .. } => {
460            acc.insert(name.clone());
461        }
462        ProofExpr::Hole(_) | ProofExpr::Unsupported(_) => {}
463        ProofExpr::Term(t) => all_names_term(t, acc),
464    }
465}
466
467fn all_names_term(term: &ProofTerm, acc: &mut std::collections::HashSet<String>) {
468    match term {
469        ProofTerm::Constant(s) | ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => {
470            acc.insert(s.clone());
471        }
472        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
473            for a in args {
474                all_names_term(a, acc);
475            }
476        }
477    }
478}
479
480/// Pick a binder name derived from `base` that collides with nothing in `avoid`.
481fn fresh_proof_name(base: &str, avoid: &std::collections::HashSet<String>) -> String {
482    let mut candidate = format!("{}'", base);
483    let mut n: u32 = 0;
484    while avoid.contains(&candidate) {
485        n += 1;
486        candidate = format!("{}'{}", base, n);
487    }
488    candidate
489}
490
491/// Rename free occurrences of `from` to `to` in an expression, preserving each
492/// occurrence's kind (Atom stays Atom, Variable stays Variable). `to` must be
493/// globally fresh in `expr`, so the rename itself cannot capture. Stops at
494/// binders that re-bind `from`.
495fn alpha_rename_expr(expr: &ProofExpr, from: &str, to: &str) -> ProofExpr {
496    match expr {
497        ProofExpr::Atom(s) if s == from => ProofExpr::Atom(to.to_string()),
498        ProofExpr::Atom(s) => ProofExpr::Atom(s.clone()),
499        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
500            name: name.clone(),
501            args: args.iter().map(|a| alpha_rename_term(a, from, to)).collect(),
502            world: world.clone(),
503        },
504        ProofExpr::Identity(l, r) => ProofExpr::Identity(
505            alpha_rename_term(l, from, to),
506            alpha_rename_term(r, from, to),
507        ),
508        ProofExpr::And(l, r) => ProofExpr::And(
509            Box::new(alpha_rename_expr(l, from, to)),
510            Box::new(alpha_rename_expr(r, from, to)),
511        ),
512        ProofExpr::Or(l, r) => ProofExpr::Or(
513            Box::new(alpha_rename_expr(l, from, to)),
514            Box::new(alpha_rename_expr(r, from, to)),
515        ),
516        ProofExpr::Implies(l, r) => ProofExpr::Implies(
517            Box::new(alpha_rename_expr(l, from, to)),
518            Box::new(alpha_rename_expr(r, from, to)),
519        ),
520        ProofExpr::Iff(l, r) => ProofExpr::Iff(
521            Box::new(alpha_rename_expr(l, from, to)),
522            Box::new(alpha_rename_expr(r, from, to)),
523        ),
524        ProofExpr::Not(i) => ProofExpr::Not(Box::new(alpha_rename_expr(i, from, to))),
525        ProofExpr::ForAll { variable, body } => {
526            if variable == from {
527                expr.clone()
528            } else {
529                ProofExpr::ForAll {
530                    variable: variable.clone(),
531                    body: Box::new(alpha_rename_expr(body, from, to)),
532                }
533            }
534        }
535        ProofExpr::Exists { variable, body } => {
536            if variable == from {
537                expr.clone()
538            } else {
539                ProofExpr::Exists {
540                    variable: variable.clone(),
541                    body: Box::new(alpha_rename_expr(body, from, to)),
542                }
543            }
544        }
545        ProofExpr::Lambda { variable, body } => {
546            if variable == from {
547                expr.clone()
548            } else {
549                ProofExpr::Lambda {
550                    variable: variable.clone(),
551                    body: Box::new(alpha_rename_expr(body, from, to)),
552                }
553            }
554        }
555        ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
556            domain: domain.clone(),
557            force: *force,
558            flavor: flavor.clone(),
559            body: Box::new(alpha_rename_expr(body, from, to)),
560        },
561        ProofExpr::Counterfactual { antecedent, consequent } => ProofExpr::Counterfactual {
562            antecedent: Box::new(alpha_rename_expr(antecedent, from, to)),
563            consequent: Box::new(alpha_rename_expr(consequent, from, to)),
564        },
565        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
566            operator: operator.clone(),
567            body: Box::new(alpha_rename_expr(body, from, to)),
568        },
569        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
570            operator: operator.clone(),
571            left: Box::new(alpha_rename_expr(left, from, to)),
572            right: Box::new(alpha_rename_expr(right, from, to)),
573        },
574        ProofExpr::App(f, a) => ProofExpr::App(
575            Box::new(alpha_rename_expr(f, from, to)),
576            Box::new(alpha_rename_expr(a, from, to)),
577        ),
578        ProofExpr::NeoEvent { event_var, verb, roles } => {
579            if event_var == from {
580                expr.clone()
581            } else {
582                ProofExpr::NeoEvent {
583                    event_var: event_var.clone(),
584                    verb: verb.clone(),
585                    roles: roles.iter().map(|(r, t)| (r.clone(), alpha_rename_term(t, from, to))).collect(),
586                }
587            }
588        }
589        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
590            name: name.clone(),
591            args: args.iter().map(|a| alpha_rename_expr(a, from, to)).collect(),
592        },
593        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
594            scrutinee: Box::new(alpha_rename_expr(scrutinee, from, to)),
595            arms: arms.iter().map(|arm| {
596                if arm.bindings.iter().any(|b| b == from) {
597                    arm.clone()
598                } else {
599                    MatchArm {
600                        ctor: arm.ctor.clone(),
601                        bindings: arm.bindings.clone(),
602                        body: alpha_rename_expr(&arm.body, from, to),
603                    }
604                }
605            }).collect(),
606        },
607        ProofExpr::Fixpoint { name, body } => {
608            if name == from {
609                expr.clone()
610            } else {
611                ProofExpr::Fixpoint {
612                    name: name.clone(),
613                    body: Box::new(alpha_rename_expr(body, from, to)),
614                }
615            }
616        }
617        ProofExpr::TypedVar { name, typename } => {
618            if name == from {
619                ProofExpr::TypedVar { name: to.to_string(), typename: typename.clone() }
620            } else {
621                expr.clone()
622            }
623        }
624        ProofExpr::Hole(_) | ProofExpr::Unsupported(_) => expr.clone(),
625        ProofExpr::Term(t) => ProofExpr::Term(alpha_rename_term(t, from, to)),
626    }
627}
628
629fn alpha_rename_term(term: &ProofTerm, from: &str, to: &str) -> ProofTerm {
630    match term {
631        ProofTerm::Variable(s) if s == from => ProofTerm::Variable(to.to_string()),
632        ProofTerm::BoundVarRef(s) if s == from => ProofTerm::BoundVarRef(to.to_string()),
633        ProofTerm::Variable(s) => ProofTerm::Variable(s.clone()),
634        ProofTerm::BoundVarRef(s) => ProofTerm::BoundVarRef(s.clone()),
635        ProofTerm::Constant(s) => ProofTerm::Constant(s.clone()),
636        ProofTerm::Function(n, args) => {
637            ProofTerm::Function(n.clone(), args.iter().map(|a| alpha_rename_term(a, from, to)).collect())
638        }
639        ProofTerm::Group(args) => {
640            ProofTerm::Group(args.iter().map(|a| alpha_rename_term(a, from, to)).collect())
641        }
642    }
643}
644
645/// Choose a (binder, body) pair for a single-binder form so that substituting
646/// `replacement` into `body` cannot capture a free variable of `replacement`.
647/// If `variable` is free in `replacement`, the binder is alpha-renamed fresh.
648fn rebind_for_subst(
649    variable: &str,
650    inner: &ProofExpr,
651    repl_fvs: &std::collections::HashSet<String>,
652) -> (String, ProofExpr) {
653    if repl_fvs.contains(variable) {
654        let mut avoid = repl_fvs.clone();
655        all_names_expr(inner, &mut avoid);
656        let fresh = fresh_proof_name(variable, &avoid);
657        let renamed = alpha_rename_expr(inner, variable, &fresh);
658        (fresh, renamed)
659    } else {
660        (variable.to_string(), inner.clone())
661    }
662}
663
664/// Substitute an expression for a variable name in another expression.
665///
666/// Used for beta-reduction: (λx. body)(arg) → body[x := arg]
667///
668/// This is capture-avoiding: a binder whose name is free in `replacement` is
669/// alpha-renamed to a fresh name before the substitution descends into it, so a
670/// free variable of the argument is never captured by an inner binder.
671fn substitute_expr_for_var(body: &ProofExpr, var: &str, replacement: &ProofExpr) -> ProofExpr {
672    let mut repl_fvs = std::collections::HashSet::new();
673    free_vars_expr(replacement, &mut Vec::new(), &mut repl_fvs);
674    subst_expr_avoiding(body, var, replacement, &repl_fvs)
675}
676
677fn subst_expr_avoiding(
678    body: &ProofExpr,
679    var: &str,
680    replacement: &ProofExpr,
681    repl_fvs: &std::collections::HashSet<String>,
682) -> ProofExpr {
683    match body {
684        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
685            name: name.clone(),
686            args: args.iter().map(|t| substitute_term_for_var(t, var, replacement)).collect(),
687            world: world.clone(),
688        },
689
690        ProofExpr::Identity(l, r) => ProofExpr::Identity(
691            substitute_term_for_var(l, var, replacement),
692            substitute_term_for_var(r, var, replacement),
693        ),
694
695        ProofExpr::Atom(a) => {
696            // If the atom matches the variable, replace it
697            if a == var {
698                replacement.clone()
699            } else {
700                ProofExpr::Atom(a.clone())
701            }
702        }
703
704        ProofExpr::And(l, r) => ProofExpr::And(
705            Box::new(subst_expr_avoiding(l, var, replacement, repl_fvs)),
706            Box::new(subst_expr_avoiding(r, var, replacement, repl_fvs)),
707        ),
708        ProofExpr::Or(l, r) => ProofExpr::Or(
709            Box::new(subst_expr_avoiding(l, var, replacement, repl_fvs)),
710            Box::new(subst_expr_avoiding(r, var, replacement, repl_fvs)),
711        ),
712        ProofExpr::Implies(l, r) => ProofExpr::Implies(
713            Box::new(subst_expr_avoiding(l, var, replacement, repl_fvs)),
714            Box::new(subst_expr_avoiding(r, var, replacement, repl_fvs)),
715        ),
716        ProofExpr::Iff(l, r) => ProofExpr::Iff(
717            Box::new(subst_expr_avoiding(l, var, replacement, repl_fvs)),
718            Box::new(subst_expr_avoiding(r, var, replacement, repl_fvs)),
719        ),
720        ProofExpr::Not(inner) => ProofExpr::Not(
721            Box::new(subst_expr_avoiding(inner, var, replacement, repl_fvs))
722        ),
723
724        // Quantifiers: shadowing stops substitution; otherwise alpha-rename the
725        // binder away from the replacement's free vars to avoid capture.
726        ProofExpr::ForAll { variable, body: inner } => {
727            if variable == var {
728                body.clone()
729            } else {
730                let (v, b) = rebind_for_subst(variable, inner, repl_fvs);
731                ProofExpr::ForAll {
732                    variable: v,
733                    body: Box::new(subst_expr_avoiding(&b, var, replacement, repl_fvs)),
734                }
735            }
736        }
737        ProofExpr::Exists { variable, body: inner } => {
738            if variable == var {
739                body.clone()
740            } else {
741                let (v, b) = rebind_for_subst(variable, inner, repl_fvs);
742                ProofExpr::Exists {
743                    variable: v,
744                    body: Box::new(subst_expr_avoiding(&b, var, replacement, repl_fvs)),
745                }
746            }
747        }
748
749        ProofExpr::Lambda { variable, body: inner } => {
750            if variable == var {
751                body.clone()
752            } else {
753                let (v, b) = rebind_for_subst(variable, inner, repl_fvs);
754                ProofExpr::Lambda {
755                    variable: v,
756                    body: Box::new(subst_expr_avoiding(&b, var, replacement, repl_fvs)),
757                }
758            }
759        }
760
761        ProofExpr::App(f, a) => ProofExpr::App(
762            Box::new(subst_expr_avoiding(f, var, replacement, repl_fvs)),
763            Box::new(subst_expr_avoiding(a, var, replacement, repl_fvs)),
764        ),
765
766        ProofExpr::Modal { domain, force, flavor, body: inner } => ProofExpr::Modal {
767            domain: domain.clone(),
768            force: *force,
769            flavor: flavor.clone(),
770            body: Box::new(subst_expr_avoiding(inner, var, replacement, repl_fvs)),
771        },
772
773        ProofExpr::Counterfactual { antecedent, consequent } => ProofExpr::Counterfactual {
774            antecedent: Box::new(subst_expr_avoiding(antecedent, var, replacement, repl_fvs)),
775            consequent: Box::new(subst_expr_avoiding(consequent, var, replacement, repl_fvs)),
776        },
777
778        ProofExpr::Temporal { operator, body: inner } => ProofExpr::Temporal {
779            operator: operator.clone(),
780            body: Box::new(subst_expr_avoiding(inner, var, replacement, repl_fvs)),
781        },
782
783        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
784            operator: operator.clone(),
785            left: Box::new(subst_expr_avoiding(left, var, replacement, repl_fvs)),
786            right: Box::new(subst_expr_avoiding(right, var, replacement, repl_fvs)),
787        },
788
789        ProofExpr::NeoEvent { event_var, verb, roles } => {
790            if event_var == var {
791                // event_var shadows var
792                body.clone()
793            } else if repl_fvs.contains(event_var) {
794                // Alpha-rename event_var away from the replacement's free vars.
795                let mut avoid = repl_fvs.clone();
796                for (_, t) in roles {
797                    all_names_term(t, &mut avoid);
798                }
799                let fresh = fresh_proof_name(event_var, &avoid);
800                ProofExpr::NeoEvent {
801                    event_var: fresh.clone(),
802                    verb: verb.clone(),
803                    roles: roles
804                        .iter()
805                        .map(|(r, t)| {
806                            let renamed = alpha_rename_term(t, event_var, &fresh);
807                            (r.clone(), substitute_term_for_var(&renamed, var, replacement))
808                        })
809                        .collect(),
810                }
811            } else {
812                ProofExpr::NeoEvent {
813                    event_var: event_var.clone(),
814                    verb: verb.clone(),
815                    roles: roles
816                        .iter()
817                        .map(|(r, t)| (r.clone(), substitute_term_for_var(t, var, replacement)))
818                        .collect(),
819                }
820            }
821        }
822
823        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
824            name: name.clone(),
825            args: args.iter().map(|a| subst_expr_avoiding(a, var, replacement, repl_fvs)).collect(),
826        },
827
828        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
829            scrutinee: Box::new(subst_expr_avoiding(scrutinee, var, replacement, repl_fvs)),
830            arms: arms.iter().map(|arm| {
831                // Don't substitute if var is bound in this arm
832                if arm.bindings.iter().any(|b| b == var) {
833                    arm.clone()
834                } else {
835                    // Alpha-rename any binding that would capture a free var of
836                    // the replacement before substituting into the arm body.
837                    let mut arm_body = arm.body.clone();
838                    let mut new_bindings = arm.bindings.clone();
839                    let mut avoid = repl_fvs.clone();
840                    all_names_expr(&arm_body, &mut avoid);
841                    for b in &arm.bindings {
842                        avoid.insert(b.clone());
843                    }
844                    for binding in new_bindings.iter_mut() {
845                        if repl_fvs.contains(binding) {
846                            let fresh = fresh_proof_name(binding, &avoid);
847                            arm_body = alpha_rename_expr(&arm_body, binding, &fresh);
848                            avoid.insert(fresh.clone());
849                            *binding = fresh;
850                        }
851                    }
852                    MatchArm {
853                        ctor: arm.ctor.clone(),
854                        bindings: new_bindings,
855                        body: subst_expr_avoiding(&arm_body, var, replacement, repl_fvs),
856                    }
857                }
858            }).collect(),
859        },
860
861        ProofExpr::Fixpoint { name, body: inner } => {
862            if name == var {
863                body.clone()
864            } else {
865                let (v, b) = rebind_for_subst(name, inner, repl_fvs);
866                ProofExpr::Fixpoint {
867                    name: v,
868                    body: Box::new(subst_expr_avoiding(&b, var, replacement, repl_fvs)),
869                }
870            }
871        }
872
873        ProofExpr::TypedVar { .. } | ProofExpr::Unsupported(_) => body.clone(),
874
875        // Holes are meta-variables - don't substitute into them
876        ProofExpr::Hole(_) => body.clone(),
877
878        // Terms: substitute into the inner term
879        ProofExpr::Term(t) => ProofExpr::Term(substitute_term_for_var(t, var, replacement)),
880    }
881}
882
883/// Substitute an expression for a variable in a term.
884///
885/// When a variable in a term matches, convert the replacement expression to a term.
886fn substitute_term_for_var(term: &ProofTerm, var: &str, replacement: &ProofExpr) -> ProofTerm {
887    match term {
888        ProofTerm::Variable(v) if v == var => {
889            // Variable matches, convert replacement to term
890            expr_to_term(replacement)
891        }
892        // BoundVarRef also participates in substitution (for instantiating quantified formulas)
893        ProofTerm::BoundVarRef(v) if v == var => {
894            expr_to_term(replacement)
895        }
896        ProofTerm::Variable(_) | ProofTerm::Constant(_) | ProofTerm::BoundVarRef(_) => term.clone(),
897        ProofTerm::Function(name, args) => ProofTerm::Function(
898            name.clone(),
899            args.iter().map(|a| substitute_term_for_var(a, var, replacement)).collect(),
900        ),
901        ProofTerm::Group(terms) => ProofTerm::Group(
902            terms.iter().map(|t| substitute_term_for_var(t, var, replacement)).collect(),
903        ),
904    }
905}
906
907/// Convert a simple ProofExpr to ProofTerm.
908///
909/// Used during beta-reduction when substituting an expression argument
910/// into a predicate's term position.
911fn expr_to_term(expr: &ProofExpr) -> ProofTerm {
912    match expr {
913        // Atoms become constants
914        ProofExpr::Atom(s) => ProofTerm::Constant(s.clone()),
915
916        // Zero-arity predicates become constants (e.g., "John" as a predicate name)
917        ProofExpr::Predicate { name, args, .. } if args.is_empty() => {
918            ProofTerm::Constant(name.clone())
919        }
920
921        // Predicates with args become functions
922        ProofExpr::Predicate { name, args, .. } => {
923            ProofTerm::Function(name.clone(), args.clone())
924        }
925
926        // Constructors become functions
927        ProofExpr::Ctor { name, args } => {
928            ProofTerm::Function(name.clone(), args.iter().map(expr_to_term).collect())
929        }
930
931        // TypedVar becomes a variable
932        ProofExpr::TypedVar { name, .. } => ProofTerm::Variable(name.clone()),
933
934        // Term is already a term - extract it directly
935        ProofExpr::Term(t) => t.clone(),
936
937        // Fallback: stringify the expression (covers Hole, etc.)
938        _ => ProofTerm::Constant(format!("{}", expr)),
939    }
940}
941
942// =============================================================================
943// TERM-LEVEL UNIFICATION
944// =============================================================================
945
946/// Unify two terms, returning the Most General Unifier (MGU).
947///
948/// The MGU is the smallest substitution that makes both terms identical.
949/// Uses Robinson's algorithm with occurs check.
950///
951/// # Arguments
952///
953/// * `t1` - The first term (often a pattern with variables)
954/// * `t2` - The second term (often a ground term or another pattern)
955///
956/// # Returns
957///
958/// * `Ok(subst)` - A substitution that unifies the terms
959/// * `Err(OccursCheck)` - If unification would create an infinite term
960/// * `Err(SymbolMismatch)` - If function/constant names differ
961/// * `Err(ArityMismatch)` - If argument counts differ
962///
963/// # Example
964///
965/// ```
966/// use logicaffeine_proof::{ProofTerm, unify::unify_terms};
967///
968/// let pattern = ProofTerm::Function(
969///     "Mortal".into(),
970///     vec![ProofTerm::Variable("x".into())]
971/// );
972/// let target = ProofTerm::Function(
973///     "Mortal".into(),
974///     vec![ProofTerm::Constant("Socrates".into())]
975/// );
976///
977/// let subst = unify_terms(&pattern, &target).unwrap();
978/// assert_eq!(
979///     subst.get("x"),
980///     Some(&ProofTerm::Constant("Socrates".into()))
981/// );
982/// ```
983///
984/// # See Also
985///
986/// * [`unify_exprs`] - Unifies expressions (handles quantifiers, connectives)
987/// * [`apply_subst_to_term`] - Applies the resulting substitution
988pub fn unify_terms(t1: &ProofTerm, t2: &ProofTerm) -> ProofResult<Substitution> {
989    let mut subst = Substitution::new();
990    unify_terms_with_subst(t1, t2, &mut subst)?;
991    Ok(subst)
992}
993
994/// Internal unification with accumulating substitution.
995fn unify_terms_with_subst(
996    t1: &ProofTerm,
997    t2: &ProofTerm,
998    subst: &mut Substitution,
999) -> ProofResult<()> {
1000    // Apply current substitution to both terms first
1001    let t1 = apply_subst_to_term(t1, subst);
1002    let t2 = apply_subst_to_term(t2, subst);
1003
1004    match (&t1, &t2) {
1005        // Identical terms unify trivially
1006        (ProofTerm::Constant(c1), ProofTerm::Constant(c2)) if c1 == c2 => Ok(()),
1007
1008        // Different constants cannot unify
1009        (ProofTerm::Constant(c1), ProofTerm::Constant(c2)) => {
1010            Err(ProofError::SymbolMismatch {
1011                left: c1.clone(),
1012                right: c2.clone(),
1013            })
1014        }
1015
1016        // Variable on the left: bind it to the right term
1017        (ProofTerm::Variable(v), t) => {
1018            // Check if they're the same variable
1019            if let ProofTerm::Variable(v2) = t {
1020                if v == v2 {
1021                    return Ok(());
1022                }
1023            }
1024            // Occurs check: prevent infinite types
1025            if occurs(v, t) {
1026                return Err(ProofError::OccursCheck {
1027                    variable: v.clone(),
1028                    term: t.clone(),
1029                });
1030            }
1031            subst.insert(v.clone(), t.clone());
1032            Ok(())
1033        }
1034
1035        // Variable on the right: bind it to the left term
1036        (t, ProofTerm::Variable(v)) => {
1037            // Occurs check
1038            if occurs(v, t) {
1039                return Err(ProofError::OccursCheck {
1040                    variable: v.clone(),
1041                    term: t.clone(),
1042                });
1043            }
1044            subst.insert(v.clone(), t.clone());
1045            Ok(())
1046        }
1047
1048        // BoundVarRef on the left: treat like a variable for unification
1049        // This allows matching quantified formulas like ∀x P(x) against P(butler)
1050        (ProofTerm::BoundVarRef(v), t) => {
1051            if let ProofTerm::BoundVarRef(v2) = t {
1052                if v == v2 {
1053                    return Ok(());
1054                }
1055            }
1056            if occurs(v, t) {
1057                return Err(ProofError::OccursCheck {
1058                    variable: v.clone(),
1059                    term: t.clone(),
1060                });
1061            }
1062            subst.insert(v.clone(), t.clone());
1063            Ok(())
1064        }
1065
1066        // BoundVarRef on the right: bind it to the left term
1067        (t, ProofTerm::BoundVarRef(v)) => {
1068            if occurs(v, t) {
1069                return Err(ProofError::OccursCheck {
1070                    variable: v.clone(),
1071                    term: t.clone(),
1072                });
1073            }
1074            subst.insert(v.clone(), t.clone());
1075            Ok(())
1076        }
1077
1078        // Function unification: same name and arity, unify arguments pairwise
1079        (ProofTerm::Function(f1, args1), ProofTerm::Function(f2, args2)) => {
1080            if f1 != f2 {
1081                return Err(ProofError::SymbolMismatch {
1082                    left: f1.clone(),
1083                    right: f2.clone(),
1084                });
1085            }
1086            if args1.len() != args2.len() {
1087                return Err(ProofError::ArityMismatch {
1088                    expected: args1.len(),
1089                    found: args2.len(),
1090                });
1091            }
1092            for (a1, a2) in args1.iter().zip(args2.iter()) {
1093                unify_terms_with_subst(a1, a2, subst)?;
1094            }
1095            Ok(())
1096        }
1097
1098        // Group unification: same length, unify elements pairwise
1099        (ProofTerm::Group(g1), ProofTerm::Group(g2)) => {
1100            if g1.len() != g2.len() {
1101                return Err(ProofError::ArityMismatch {
1102                    expected: g1.len(),
1103                    found: g2.len(),
1104                });
1105            }
1106            for (t1, t2) in g1.iter().zip(g2.iter()) {
1107                unify_terms_with_subst(t1, t2, subst)?;
1108            }
1109            Ok(())
1110        }
1111
1112        // Any other combination fails
1113        _ => Err(ProofError::UnificationFailed {
1114            left: t1,
1115            right: t2,
1116        }),
1117    }
1118}
1119
1120/// Check if a variable occurs in a term (for occurs check).
1121/// Prevents infinite types like x = f(x).
1122fn occurs(var: &str, term: &ProofTerm) -> bool {
1123    match term {
1124        ProofTerm::Variable(v) => v == var,
1125        ProofTerm::BoundVarRef(v) => v == var, // BoundVarRef participates in occurs check
1126        ProofTerm::Constant(_) => false,
1127        ProofTerm::Function(_, args) => args.iter().any(|a| occurs(var, a)),
1128        ProofTerm::Group(terms) => terms.iter().any(|t| occurs(var, t)),
1129    }
1130}
1131
1132/// Apply a substitution to a term.
1133///
1134/// Replaces all variables in the term according to the substitution mapping.
1135/// Handles transitive chains: if `{x ↦ y, y ↦ z}`, then applying to `x` yields `z`.
1136///
1137/// # Arguments
1138///
1139/// * `term` - The term to transform
1140/// * `subst` - The substitution mapping variables to replacement terms
1141///
1142/// # Returns
1143///
1144/// A new term with all substitutions applied.
1145///
1146/// # Example
1147///
1148/// ```
1149/// use logicaffeine_proof::{ProofTerm, unify::{Substitution, apply_subst_to_term}};
1150/// use std::collections::HashMap;
1151///
1152/// let mut subst = Substitution::new();
1153/// subst.insert("x".into(), ProofTerm::Constant("Socrates".into()));
1154///
1155/// let term = ProofTerm::Function(
1156///     "Mortal".into(),
1157///     vec![ProofTerm::Variable("x".into())]
1158/// );
1159///
1160/// let result = apply_subst_to_term(&term, &subst);
1161/// // result = Mortal(Socrates)
1162/// ```
1163pub fn apply_subst_to_term(term: &ProofTerm, subst: &Substitution) -> ProofTerm {
1164    match term {
1165        ProofTerm::Variable(v) => {
1166            if let Some(replacement) = subst.get(v) {
1167                // Recursively apply to handle chains like {x ↦ y, y ↦ z}
1168                apply_subst_to_term(replacement, subst)
1169            } else {
1170                term.clone()
1171            }
1172        }
1173        // BoundVarRef participates in substitution (for instantiating quantified formulas)
1174        ProofTerm::BoundVarRef(v) => {
1175            if let Some(replacement) = subst.get(v) {
1176                apply_subst_to_term(replacement, subst)
1177            } else {
1178                term.clone()
1179            }
1180        }
1181        ProofTerm::Constant(_) => term.clone(),
1182        ProofTerm::Function(name, args) => {
1183            let new_args = args.iter().map(|a| apply_subst_to_term(a, subst)).collect();
1184            ProofTerm::Function(name.clone(), new_args)
1185        }
1186        ProofTerm::Group(terms) => {
1187            let new_terms = terms.iter().map(|t| apply_subst_to_term(t, subst)).collect();
1188            ProofTerm::Group(new_terms)
1189        }
1190    }
1191}
1192
1193// =============================================================================
1194// EXPRESSION-LEVEL UNIFICATION
1195// =============================================================================
1196
1197/// Unify two expressions, returning the Most General Unifier.
1198///
1199/// Expression unification extends term unification with support for logical
1200/// connectives, quantifiers, and alpha-equivalence for bound variables.
1201///
1202/// # Alpha-Equivalence
1203///
1204/// Bound variable names are considered arbitrary:
1205/// - `∀x P(x)` unifies with `∀y P(y)`
1206/// - `∃e Run(e)` unifies with `∃x Run(x)`
1207///
1208/// This is achieved by substituting fresh constants for bound variables
1209/// before comparing bodies.
1210///
1211/// # Beta-Reduction
1212///
1213/// Both expressions are beta-reduced before comparison, so
1214/// `(λx. P(x))(a)` will unify with `P(a)`.
1215///
1216/// # Arguments
1217///
1218/// * `e1` - The first expression
1219/// * `e2` - The second expression
1220///
1221/// # Returns
1222///
1223/// * `Ok(subst)` - A substitution unifying the expressions
1224/// * `Err(ExprUnificationFailed)` - If expressions cannot be unified
1225///
1226/// # See Also
1227///
1228/// * [`unify_terms`] - Underlying term unification
1229/// * [`beta_reduce`] - Normalization applied before unification
1230/// * [`unify_pattern`] - Higher-order pattern unification for holes
1231pub fn unify_exprs(e1: &ProofExpr, e2: &ProofExpr) -> ProofResult<Substitution> {
1232    let mut subst = Substitution::new();
1233    unify_exprs_with_subst(e1, e2, &mut subst)?;
1234    Ok(subst)
1235}
1236
1237/// Internal expression unification with accumulating substitution.
1238fn unify_exprs_with_subst(
1239    e1: &ProofExpr,
1240    e2: &ProofExpr,
1241    subst: &mut Substitution,
1242) -> ProofResult<()> {
1243    // Beta-reduce both expressions before unification.
1244    // This normalizes lambda applications: (λx. P(x))(a) → P(a)
1245    let e1 = beta_reduce(e1);
1246    let e2 = beta_reduce(e2);
1247
1248    match (&e1, &e2) {
1249        // Atom unification
1250        (ProofExpr::Atom(a1), ProofExpr::Atom(a2)) if a1 == a2 => Ok(()),
1251
1252        // Predicate unification: same name, unify arguments
1253        (
1254            ProofExpr::Predicate { name: n1, args: a1, world: w1 },
1255            ProofExpr::Predicate { name: n2, args: a2, world: w2 },
1256        ) => {
1257            if n1 != n2 {
1258                return Err(ProofError::SymbolMismatch {
1259                    left: n1.clone(),
1260                    right: n2.clone(),
1261                });
1262            }
1263            if a1.len() != a2.len() {
1264                return Err(ProofError::ArityMismatch {
1265                    expected: a1.len(),
1266                    found: a2.len(),
1267                });
1268            }
1269            // Unify worlds if both present
1270            match (w1, w2) {
1271                (Some(w1), Some(w2)) if w1 != w2 => {
1272                    return Err(ProofError::SymbolMismatch {
1273                        left: w1.clone(),
1274                        right: w2.clone(),
1275                    });
1276                }
1277                _ => {}
1278            }
1279            // Unify arguments
1280            for (t1, t2) in a1.iter().zip(a2.iter()) {
1281                unify_terms_with_subst(t1, t2, subst)?;
1282            }
1283            Ok(())
1284        }
1285
1286        // Identity unification
1287        (ProofExpr::Identity(l1, r1), ProofExpr::Identity(l2, r2)) => {
1288            unify_terms_with_subst(l1, l2, subst)?;
1289            unify_terms_with_subst(r1, r2, subst)?;
1290            Ok(())
1291        }
1292
1293        // Binary operators: same operator, unify both sides
1294        (ProofExpr::And(l1, r1), ProofExpr::And(l2, r2))
1295        | (ProofExpr::Or(l1, r1), ProofExpr::Or(l2, r2))
1296        | (ProofExpr::Implies(l1, r1), ProofExpr::Implies(l2, r2))
1297        | (ProofExpr::Iff(l1, r1), ProofExpr::Iff(l2, r2)) => {
1298            unify_exprs_with_subst(l1, l2, subst)?;
1299            unify_exprs_with_subst(r1, r2, subst)?;
1300            Ok(())
1301        }
1302
1303        // Negation
1304        (ProofExpr::Not(inner1), ProofExpr::Not(inner2)) => {
1305            unify_exprs_with_subst(inner1, inner2, subst)
1306        }
1307
1308        // Quantifiers: Alpha-equivalence - bound variable names are arbitrary
1309        // ∃e P(e) ≡ ∃x P(x) because they describe the same logical content
1310        (
1311            ProofExpr::ForAll { variable: v1, body: b1 },
1312            ProofExpr::ForAll { variable: v2, body: b2 },
1313        )
1314        | (
1315            ProofExpr::Exists { variable: v1, body: b1 },
1316            ProofExpr::Exists { variable: v2, body: b2 },
1317        ) => {
1318            // Generate a fresh constant to substitute for both bound variables.
1319            // Using a constant (not a variable) avoids capture issues.
1320            let fresh = fresh_alpha_constant();
1321
1322            // Create substitutions for each bound variable
1323            let subst1: Substitution = [(v1.clone(), fresh.clone())].into_iter().collect();
1324            let subst2: Substitution = [(v2.clone(), fresh)].into_iter().collect();
1325
1326            // Apply substitutions to bodies
1327            let body1_renamed = apply_subst_to_expr(b1, &subst1);
1328            let body2_renamed = apply_subst_to_expr(b2, &subst2);
1329
1330            // Recursively unify the renamed bodies
1331            unify_exprs_with_subst(&body1_renamed, &body2_renamed, subst)
1332        }
1333
1334        // Lambda expressions: Alpha-equivalence - λx.P(x) ≡ λy.P(y)
1335        (
1336            ProofExpr::Lambda { variable: v1, body: b1 },
1337            ProofExpr::Lambda { variable: v2, body: b2 },
1338        ) => {
1339            // Same alpha-renaming technique as quantifiers
1340            let fresh = fresh_alpha_constant();
1341            let subst1: Substitution = [(v1.clone(), fresh.clone())].into_iter().collect();
1342            let subst2: Substitution = [(v2.clone(), fresh)].into_iter().collect();
1343            let body1_renamed = apply_subst_to_expr(b1, &subst1);
1344            let body2_renamed = apply_subst_to_expr(b2, &subst2);
1345            unify_exprs_with_subst(&body1_renamed, &body2_renamed, subst)
1346        }
1347
1348        // Application
1349        (ProofExpr::App(f1, a1), ProofExpr::App(f2, a2)) => {
1350            unify_exprs_with_subst(f1, f2, subst)?;
1351            unify_exprs_with_subst(a1, a2, subst)?;
1352            Ok(())
1353        }
1354
1355        // NeoEvent: Alpha-equivalence for event variables
1356        // ∃e(Run(e) ∧ Agent(e, John)) should unify with ∃x(Run(x) ∧ Agent(x, John))
1357        (
1358            ProofExpr::NeoEvent {
1359                event_var: e1,
1360                verb: v1,
1361                roles: r1,
1362            },
1363            ProofExpr::NeoEvent {
1364                event_var: e2,
1365                verb: v2,
1366                roles: r2,
1367            },
1368        ) => {
1369            // Verb names must match (case-insensitive for robustness)
1370            if v1.to_lowercase() != v2.to_lowercase() {
1371                return Err(ProofError::SymbolMismatch {
1372                    left: v1.clone(),
1373                    right: v2.clone(),
1374                });
1375            }
1376
1377            // Roles must have same length
1378            if r1.len() != r2.len() {
1379                return Err(ProofError::ArityMismatch {
1380                    expected: r1.len(),
1381                    found: r2.len(),
1382                });
1383            }
1384
1385            // Alpha-equivalence: generate fresh constant for event variable
1386            let fresh = fresh_alpha_constant();
1387            let subst1: Substitution = [(e1.clone(), fresh.clone())].into_iter().collect();
1388            let subst2: Substitution = [(e2.clone(), fresh)].into_iter().collect();
1389
1390            // Unify roles pairwise with alpha-renamed event variables
1391            for ((role1, term1), (role2, term2)) in r1.iter().zip(r2.iter()) {
1392                // Role names must match
1393                if role1 != role2 {
1394                    return Err(ProofError::SymbolMismatch {
1395                        left: role1.clone(),
1396                        right: role2.clone(),
1397                    });
1398                }
1399                // Apply alpha-renaming to terms and unify
1400                let t1_renamed = apply_subst_to_term(term1, &subst1);
1401                let t2_renamed = apply_subst_to_term(term2, &subst2);
1402                unify_terms_with_subst(&t1_renamed, &t2_renamed, subst)?;
1403            }
1404            Ok(())
1405        }
1406
1407        // Temporal operators: Past(P), Future(P)
1408        // Same operator required, then unify bodies
1409        (
1410            ProofExpr::Temporal { operator: op1, body: b1 },
1411            ProofExpr::Temporal { operator: op2, body: b2 },
1412        ) => {
1413            if op1 != op2 {
1414                return Err(ProofError::ExprUnificationFailed {
1415                    left: e1.clone(),
1416                    right: e2.clone(),
1417                });
1418            }
1419            unify_exprs_with_subst(b1, b2, subst)
1420        }
1421
1422        // Binary temporal operators: Until(P,Q), Release(P,Q)
1423        // Same operator required, then unify both children
1424        (
1425            ProofExpr::TemporalBinary { operator: op1, left: l1, right: r1 },
1426            ProofExpr::TemporalBinary { operator: op2, left: l2, right: r2 },
1427        ) => {
1428            if op1 != op2 {
1429                return Err(ProofError::ExprUnificationFailed {
1430                    left: e1.clone(),
1431                    right: e2.clone(),
1432                });
1433            }
1434            unify_exprs_with_subst(l1, l2, subst)?;
1435            unify_exprs_with_subst(r1, r2, subst)
1436        }
1437
1438        // Anything else fails
1439        _ => Err(ProofError::ExprUnificationFailed {
1440            left: e1.clone(),
1441            right: e2.clone(),
1442        }),
1443    }
1444}
1445
1446/// Apply a substitution to an expression.
1447///
1448/// Recursively replaces variables in terms within the expression according
1449/// to the substitution mapping. Also handles binder renaming when the
1450/// substitution maps a bound variable name to a new variable.
1451///
1452/// # Arguments
1453///
1454/// * `expr` - The expression to transform
1455/// * `subst` - The substitution mapping variables to replacement terms
1456///
1457/// # Returns
1458///
1459/// A new expression with all substitutions applied to embedded terms.
1460///
1461/// # Binder Handling
1462///
1463/// When a quantifier or lambda binds a variable that maps to another variable
1464/// in the substitution, the binder is renamed:
1465///
1466/// ```text
1467/// apply_subst_to_expr(∀x P(x), {x ↦ y}) = ∀y P(y)
1468/// ```
1469pub fn apply_subst_to_expr(expr: &ProofExpr, subst: &Substitution) -> ProofExpr {
1470    match expr {
1471        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
1472            name: name.clone(),
1473            args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
1474            world: world.clone(),
1475        },
1476        ProofExpr::Identity(l, r) => ProofExpr::Identity(
1477            apply_subst_to_term(l, subst),
1478            apply_subst_to_term(r, subst),
1479        ),
1480        ProofExpr::Atom(a) => ProofExpr::Atom(a.clone()),
1481        ProofExpr::And(l, r) => ProofExpr::And(
1482            Box::new(apply_subst_to_expr(l, subst)),
1483            Box::new(apply_subst_to_expr(r, subst)),
1484        ),
1485        ProofExpr::Or(l, r) => ProofExpr::Or(
1486            Box::new(apply_subst_to_expr(l, subst)),
1487            Box::new(apply_subst_to_expr(r, subst)),
1488        ),
1489        ProofExpr::Implies(l, r) => ProofExpr::Implies(
1490            Box::new(apply_subst_to_expr(l, subst)),
1491            Box::new(apply_subst_to_expr(r, subst)),
1492        ),
1493        ProofExpr::Iff(l, r) => ProofExpr::Iff(
1494            Box::new(apply_subst_to_expr(l, subst)),
1495            Box::new(apply_subst_to_expr(r, subst)),
1496        ),
1497        ProofExpr::Not(inner) => ProofExpr::Not(Box::new(apply_subst_to_expr(inner, subst))),
1498        ProofExpr::ForAll { variable, body } => {
1499            // If the variable is being renamed (maps to a Variable), update the binder
1500            let new_variable = match subst.get(variable) {
1501                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1502                _ => variable.clone(),
1503            };
1504            ProofExpr::ForAll {
1505                variable: new_variable,
1506                body: Box::new(apply_subst_to_expr(body, subst)),
1507            }
1508        }
1509        ProofExpr::Exists { variable, body } => {
1510            // If the variable is being renamed (maps to a Variable), update the binder
1511            let new_variable = match subst.get(variable) {
1512                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1513                _ => variable.clone(),
1514            };
1515            ProofExpr::Exists {
1516                variable: new_variable,
1517                body: Box::new(apply_subst_to_expr(body, subst)),
1518            }
1519        }
1520        ProofExpr::Modal { domain, force, flavor, body } => ProofExpr::Modal {
1521            domain: domain.clone(),
1522            force: *force,
1523            flavor: flavor.clone(),
1524            body: Box::new(apply_subst_to_expr(body, subst)),
1525        },
1526        ProofExpr::Counterfactual { antecedent, consequent } => ProofExpr::Counterfactual {
1527            antecedent: Box::new(apply_subst_to_expr(antecedent, subst)),
1528            consequent: Box::new(apply_subst_to_expr(consequent, subst)),
1529        },
1530        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
1531            operator: operator.clone(),
1532            body: Box::new(apply_subst_to_expr(body, subst)),
1533        },
1534        ProofExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
1535            operator: operator.clone(),
1536            left: Box::new(apply_subst_to_expr(left, subst)),
1537            right: Box::new(apply_subst_to_expr(right, subst)),
1538        },
1539        ProofExpr::Lambda { variable, body } => {
1540            // If the variable is being renamed (maps to a Variable), update the binder
1541            let new_variable = match subst.get(variable) {
1542                Some(ProofTerm::Variable(new_name)) => new_name.clone(),
1543                _ => variable.clone(),
1544            };
1545            ProofExpr::Lambda {
1546                variable: new_variable,
1547                body: Box::new(apply_subst_to_expr(body, subst)),
1548            }
1549        }
1550        ProofExpr::App(f, a) => ProofExpr::App(
1551            Box::new(apply_subst_to_expr(f, subst)),
1552            Box::new(apply_subst_to_expr(a, subst)),
1553        ),
1554        ProofExpr::NeoEvent { event_var, verb, roles } => ProofExpr::NeoEvent {
1555            event_var: event_var.clone(),
1556            verb: verb.clone(),
1557            roles: roles
1558                .iter()
1559                .map(|(r, t)| (r.clone(), apply_subst_to_term(t, subst)))
1560                .collect(),
1561        },
1562        // Peano / Inductive Types
1563        ProofExpr::Ctor { name, args } => ProofExpr::Ctor {
1564            name: name.clone(),
1565            args: args.iter().map(|a| apply_subst_to_expr(a, subst)).collect(),
1566        },
1567        ProofExpr::Match { scrutinee, arms } => ProofExpr::Match {
1568            scrutinee: Box::new(apply_subst_to_expr(scrutinee, subst)),
1569            arms: arms
1570                .iter()
1571                .map(|arm| MatchArm {
1572                    ctor: arm.ctor.clone(),
1573                    bindings: arm.bindings.clone(),
1574                    body: apply_subst_to_expr(&arm.body, subst),
1575                })
1576                .collect(),
1577        },
1578        ProofExpr::Fixpoint { name, body } => ProofExpr::Fixpoint {
1579            name: name.clone(),
1580            body: Box::new(apply_subst_to_expr(body, subst)),
1581        },
1582        ProofExpr::TypedVar { name, typename } => ProofExpr::TypedVar {
1583            name: name.clone(),
1584            typename: typename.clone(),
1585        },
1586        ProofExpr::Unsupported(s) => ProofExpr::Unsupported(s.clone()),
1587        // Holes are meta-variables - term substitution doesn't apply
1588        ProofExpr::Hole(name) => ProofExpr::Hole(name.clone()),
1589        // Terms: apply substitution to the inner term
1590        ProofExpr::Term(t) => ProofExpr::Term(apply_subst_to_term(t, subst)),
1591    }
1592}
1593
1594/// Compose two substitutions: apply s2 after s1.
1595///
1596/// The resulting substitution applies s1 first, then s2. This is the standard
1597/// composition operation for Most General Unifiers.
1598///
1599/// # Semantics
1600///
1601/// For any term t: `apply(compose(s1, s2), t) = apply(s2, apply(s1, t))`
1602///
1603/// # Arguments
1604///
1605/// * `s1` - The first substitution (applied first)
1606/// * `s2` - The second substitution (applied second)
1607///
1608/// # Returns
1609///
1610/// A combined substitution equivalent to applying s1 then s2.
1611///
1612/// # Example
1613///
1614/// ```
1615/// use logicaffeine_proof::{ProofTerm, unify::{Substitution, compose_substitutions}};
1616///
1617/// let mut s1 = Substitution::new();
1618/// s1.insert("x".into(), ProofTerm::Variable("y".into()));
1619///
1620/// let mut s2 = Substitution::new();
1621/// s2.insert("y".into(), ProofTerm::Constant("a".into()));
1622///
1623/// let composed = compose_substitutions(s1, s2);
1624/// // x ↦ a (via y), y ↦ a
1625/// ```
1626pub fn compose_substitutions(s1: Substitution, s2: Substitution) -> Substitution {
1627    let mut result: Substitution = s1
1628        .into_iter()
1629        .map(|(k, v)| (k, apply_subst_to_term(&v, &s2)))
1630        .collect();
1631
1632    // Add bindings from s2 that aren't in s1
1633    for (k, v) in s2 {
1634        result.entry(k).or_insert(v);
1635    }
1636
1637    result
1638}
1639
1640// =============================================================================
1641// HIGHER-ORDER PATTERN UNIFICATION
1642// =============================================================================
1643
1644/// Attempt higher-order pattern unification (Miller patterns).
1645///
1646/// Given `lhs` and `rhs`, if `lhs` is of the form `Hole(h)(args...)` where
1647/// args are distinct `BoundVarRef`s, solves: `h = λargs. rhs`.
1648///
1649/// # Miller Patterns
1650///
1651/// A Miller pattern is a meta-variable applied to distinct bound variables:
1652/// - `?P(x)` is a valid pattern
1653/// - `?F(x, y)` is a valid pattern (x ≠ y)
1654/// - `?G(x, x)` is NOT valid (duplicate variable)
1655/// - `?H(f(x))` is NOT valid (non-variable argument)
1656///
1657/// # Why This Matters
1658///
1659/// Higher-order pattern unification is decidable and has unique most general
1660/// solutions, unlike full higher-order unification. This restriction enables
1661/// automatic motive inference for structural induction.
1662///
1663/// # Returns
1664///
1665/// * `Ok(subst)` - An [`ExprSubstitution`] mapping hole names to lambdas
1666/// * `Err(PatternNotDistinct)` - If pattern has duplicate variables
1667/// * `Err(NotAPattern)` - If arguments aren't bound variable references
1668/// * `Err(ScopeViolation)` - If RHS uses variables not in pattern scope
1669///
1670/// # Example
1671///
1672/// ```text
1673/// unify_pattern(?P(x), Even(x))
1674/// → { "P" ↦ λx. Even(x) }
1675/// ```
1676///
1677/// # See Also
1678///
1679/// * [`ExprSubstitution`] - The result type for hole solutions
1680/// * [`ProofError::PatternNotDistinct`] - Duplicate variable error
1681pub fn unify_pattern(lhs: &ProofExpr, rhs: &ProofExpr) -> ProofResult<ExprSubstitution> {
1682    let mut solution = ExprSubstitution::new();
1683    unify_pattern_internal(lhs, rhs, &mut solution)?;
1684    Ok(solution)
1685}
1686
1687/// Internal pattern unification with accumulating solution.
1688fn unify_pattern_internal(
1689    lhs: &ProofExpr,
1690    rhs: &ProofExpr,
1691    solution: &mut ExprSubstitution,
1692) -> ProofResult<()> {
1693    // Beta-reduce both sides first
1694    let lhs = beta_reduce(lhs);
1695    let rhs = beta_reduce(rhs);
1696
1697    match &lhs {
1698        // Case: Bare hole ?P = rhs
1699        ProofExpr::Hole(h) => {
1700            solution.insert(h.clone(), rhs.clone());
1701            Ok(())
1702        }
1703
1704        // Case: Application - might be Hole(h)(args...)
1705        ProofExpr::App(_, _) => {
1706            // Collect all arguments and find the head
1707            let (head, args) = collect_app_args(&lhs);
1708
1709            if let ProofExpr::Hole(h) = head {
1710                // Check Miller pattern: all args must be distinct BoundVarRefs
1711                let var_args = extract_distinct_vars(&args)?;
1712
1713                // Check that rhs only uses variables from var_args (scope check)
1714                check_scope(&rhs, &var_args)?;
1715
1716                // Construct solution: h = λargs. rhs
1717                // Need to rename variables in rhs to match the BoundVarRef names
1718                let renamed_rhs = rename_vars_to_bound(&rhs, &var_args);
1719                let lambda = build_lambda(var_args, renamed_rhs);
1720                solution.insert(h.clone(), lambda);
1721                Ok(())
1722            } else {
1723                // Not a pattern, try structural equality
1724                if lhs == rhs {
1725                    Ok(())
1726                } else {
1727                    Err(ProofError::ExprUnificationFailed {
1728                        left: lhs.clone(),
1729                        right: rhs.clone(),
1730                    })
1731                }
1732            }
1733        }
1734
1735        // Other cases: structural equality
1736        _ => {
1737            if lhs == rhs {
1738                Ok(())
1739            } else {
1740                Err(ProofError::ExprUnificationFailed {
1741                    left: lhs.clone(),
1742                    right: rhs.clone(),
1743                })
1744            }
1745        }
1746    }
1747}
1748
1749/// Collect f(a)(b)(c) into (f, [a, b, c])
1750fn collect_app_args(expr: &ProofExpr) -> (ProofExpr, Vec<ProofExpr>) {
1751    let mut args = Vec::new();
1752    let mut current = expr.clone();
1753
1754    while let ProofExpr::App(func, arg) = current {
1755        args.push(*arg);
1756        current = *func;
1757    }
1758
1759    args.reverse();
1760    (current, args)
1761}
1762
1763/// Extract distinct variable names from pattern arguments.
1764/// Fails if any arg is not a Term(BoundVarRef) or if duplicates exist.
1765fn extract_distinct_vars(args: &[ProofExpr]) -> ProofResult<Vec<String>> {
1766    let mut vars = Vec::new();
1767    for arg in args {
1768        match arg {
1769            ProofExpr::Term(ProofTerm::BoundVarRef(v)) => {
1770                if vars.contains(v) {
1771                    return Err(ProofError::PatternNotDistinct(v.clone()));
1772                }
1773                vars.push(v.clone());
1774            }
1775            _ => return Err(ProofError::NotAPattern(arg.clone())),
1776        }
1777    }
1778    Ok(vars)
1779}
1780
1781/// Check that all free variables in expr are in the allowed set.
1782fn check_scope(expr: &ProofExpr, allowed: &[String]) -> ProofResult<()> {
1783    let free_vars = collect_free_vars(expr);
1784    for var in free_vars {
1785        if !allowed.contains(&var) {
1786            return Err(ProofError::ScopeViolation {
1787                var,
1788                allowed: allowed.to_vec(),
1789            });
1790        }
1791    }
1792    Ok(())
1793}
1794
1795/// Collect free variables from an expression.
1796fn collect_free_vars(expr: &ProofExpr) -> Vec<String> {
1797    let mut vars = Vec::new();
1798    collect_free_vars_impl(expr, &mut vars, &mut Vec::new());
1799    vars
1800}
1801
1802fn collect_free_vars_impl(expr: &ProofExpr, vars: &mut Vec<String>, bound: &mut Vec<String>) {
1803    match expr {
1804        ProofExpr::Predicate { args, .. } => {
1805            for arg in args {
1806                collect_free_vars_term(arg, vars, bound);
1807            }
1808        }
1809        ProofExpr::Identity(l, r) => {
1810            collect_free_vars_term(l, vars, bound);
1811            collect_free_vars_term(r, vars, bound);
1812        }
1813        ProofExpr::Atom(s) => {
1814            if !bound.contains(s) && !vars.contains(s) {
1815                vars.push(s.clone());
1816            }
1817        }
1818        ProofExpr::And(l, r)
1819        | ProofExpr::Or(l, r)
1820        | ProofExpr::Implies(l, r)
1821        | ProofExpr::Iff(l, r) => {
1822            collect_free_vars_impl(l, vars, bound);
1823            collect_free_vars_impl(r, vars, bound);
1824        }
1825        ProofExpr::Not(inner) => collect_free_vars_impl(inner, vars, bound),
1826        ProofExpr::ForAll { variable, body }
1827        | ProofExpr::Exists { variable, body }
1828        | ProofExpr::Lambda { variable, body } => {
1829            bound.push(variable.clone());
1830            collect_free_vars_impl(body, vars, bound);
1831            bound.pop();
1832        }
1833        ProofExpr::App(f, a) => {
1834            collect_free_vars_impl(f, vars, bound);
1835            collect_free_vars_impl(a, vars, bound);
1836        }
1837        ProofExpr::Term(t) => collect_free_vars_term(t, vars, bound),
1838        ProofExpr::Hole(_) => {} // Holes don't contribute free vars
1839        _ => {} // Other cases don't add free vars
1840    }
1841}
1842
1843fn collect_free_vars_term(term: &ProofTerm, vars: &mut Vec<String>, bound: &[String]) {
1844    match term {
1845        ProofTerm::Variable(v) => {
1846            if !bound.contains(v) && !vars.contains(v) {
1847                vars.push(v.clone());
1848            }
1849        }
1850        ProofTerm::Function(_, args) => {
1851            for arg in args {
1852                collect_free_vars_term(arg, vars, bound);
1853            }
1854        }
1855        ProofTerm::Group(terms) => {
1856            for t in terms {
1857                collect_free_vars_term(t, vars, bound);
1858            }
1859        }
1860        ProofTerm::Constant(_) | ProofTerm::BoundVarRef(_) => {}
1861    }
1862}
1863
1864/// Rename Variable(x) to Variable(x) if x is in the bound vars list.
1865/// This ensures the solution lambda binds the right names.
1866fn rename_vars_to_bound(expr: &ProofExpr, bound_vars: &[String]) -> ProofExpr {
1867    // For the basic case, we don't need to rename since the RHS already uses
1868    // Variable("x") and we want the lambda to bind "x".
1869    // The key is just to use the same names.
1870    expr.clone()
1871}
1872
1873/// Build λx₁.λx₂...λxₙ. body
1874fn build_lambda(vars: Vec<String>, body: ProofExpr) -> ProofExpr {
1875    vars.into_iter().rev().fold(body, |acc, var| {
1876        ProofExpr::Lambda {
1877            variable: var,
1878            body: Box::new(acc),
1879        }
1880    })
1881}
1882
1883// =============================================================================
1884// One-sided pattern matching (pattern → target)
1885// =============================================================================
1886
1887/// One-sided match of a term `pattern` against a `target`: pattern
1888/// `Variable`s bind target subterms; the target is inspected, never bound.
1889/// Repeated pattern variables must bind the same subterm. This is the
1890/// arbiter behind discrimination-tree retrieval (`crate::discrimination`) —
1891/// the tree over-approximates, this decides.
1892pub fn match_term_pattern(pattern: &ProofTerm, target: &ProofTerm) -> Option<Substitution> {
1893    let mut subst = Substitution::new();
1894    let mut bound = Vec::new();
1895    if match_term_into(pattern, target, &mut subst, &mut bound) {
1896        Some(subst)
1897    } else {
1898        None
1899    }
1900}
1901
1902/// One-sided match of an expression `pattern` against a `target` (see
1903/// [`match_term_pattern`]). Quantified subexpressions match name-strictly
1904/// (no alpha-renaming — conservative: an alpha-variant simply fails), and a
1905/// pattern variable never binds a term that mentions a bound variable
1906/// (capture is rejected, not silently permitted).
1907pub fn match_expr_pattern(pattern: &ProofExpr, target: &ProofExpr) -> Option<Substitution> {
1908    let mut subst = Substitution::new();
1909    let mut bound = Vec::new();
1910    if match_expr_into(pattern, target, &mut subst, &mut bound) {
1911        Some(subst)
1912    } else {
1913        None
1914    }
1915}
1916
1917/// Bind `name ↦ value`, enforcing consistency with any existing binding and
1918/// rejecting values that mention an enclosing bound variable (escape check).
1919fn match_bind(
1920    subst: &mut Substitution,
1921    bound: &[String],
1922    name: &str,
1923    value: &ProofTerm,
1924) -> bool {
1925    if let Some(existing) = subst.get(name) {
1926        return existing == value;
1927    }
1928    if term_mentions_any(value, bound) {
1929        return false;
1930    }
1931    subst.insert(name.to_string(), value.clone());
1932    true
1933}
1934
1935fn term_mentions_any(t: &ProofTerm, names: &[String]) -> bool {
1936    match t {
1937        ProofTerm::Variable(n) | ProofTerm::BoundVarRef(n) => names.iter().any(|b| b == n),
1938        ProofTerm::Constant(_) => false,
1939        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
1940            args.iter().any(|a| term_mentions_any(a, names))
1941        }
1942    }
1943}
1944
1945fn match_term_into(
1946    pattern: &ProofTerm,
1947    target: &ProofTerm,
1948    subst: &mut Substitution,
1949    bound: &mut Vec<String>,
1950) -> bool {
1951    match pattern {
1952        ProofTerm::Variable(name) => {
1953            // A pattern variable bound by an enclosing quantifier is rigid: it
1954            // matches exactly itself, never an arbitrary subterm.
1955            if bound.contains(name) {
1956                return matches!(target, ProofTerm::Variable(n) if n == name);
1957            }
1958            match_bind(subst, bound, name, target)
1959        }
1960        ProofTerm::Constant(a) => matches!(target, ProofTerm::Constant(b) if a == b),
1961        ProofTerm::BoundVarRef(a) => matches!(target, ProofTerm::BoundVarRef(b) if a == b),
1962        ProofTerm::Function(name, args) => match target {
1963            ProofTerm::Function(tname, targs) if name == tname && args.len() == targs.len() => {
1964                for (a, b) in args.iter().zip(targs) {
1965                    if !match_term_into(a, b, subst, bound) {
1966                        return false;
1967                    }
1968                }
1969                true
1970            }
1971            _ => false,
1972        },
1973        ProofTerm::Group(args) => match target {
1974            ProofTerm::Group(targs) if args.len() == targs.len() => {
1975                for (a, b) in args.iter().zip(targs) {
1976                    if !match_term_into(a, b, subst, bound) {
1977                        return false;
1978                    }
1979                }
1980                true
1981            }
1982            _ => false,
1983        },
1984    }
1985}
1986
1987fn match_expr_into(
1988    pattern: &ProofExpr,
1989    target: &ProofExpr,
1990    subst: &mut Substitution,
1991    bound: &mut Vec<String>,
1992) -> bool {
1993    match (pattern, target) {
1994        (
1995            ProofExpr::Predicate { name: pn, args: pa, world: pw },
1996            ProofExpr::Predicate { name: tn, args: ta, world: tw },
1997        ) => {
1998            if pn != tn || pa.len() != ta.len() || pw != tw {
1999                return false;
2000            }
2001            for (a, b) in pa.iter().zip(ta) {
2002                if !match_term_into(a, b, subst, bound) {
2003                    return false;
2004                }
2005            }
2006            true
2007        }
2008        (ProofExpr::Identity(pl, pr), ProofExpr::Identity(tl, tr)) => {
2009            match_term_into(pl, tl, subst, bound) && match_term_into(pr, tr, subst, bound)
2010        }
2011        (ProofExpr::Atom(a), ProofExpr::Atom(b)) => a == b,
2012        (ProofExpr::And(pl, pr), ProofExpr::And(tl, tr))
2013        | (ProofExpr::Or(pl, pr), ProofExpr::Or(tl, tr))
2014        | (ProofExpr::Implies(pl, pr), ProofExpr::Implies(tl, tr))
2015        | (ProofExpr::Iff(pl, pr), ProofExpr::Iff(tl, tr)) => {
2016            match_expr_into(pl, tl, subst, bound) && match_expr_into(pr, tr, subst, bound)
2017        }
2018        (ProofExpr::Not(p), ProofExpr::Not(t)) => match_expr_into(p, t, subst, bound),
2019        (
2020            ProofExpr::ForAll { variable: pv, body: pb },
2021            ProofExpr::ForAll { variable: tv, body: tb },
2022        )
2023        | (
2024            ProofExpr::Exists { variable: pv, body: pb },
2025            ProofExpr::Exists { variable: tv, body: tb },
2026        ) => {
2027            if pv != tv {
2028                return false;
2029            }
2030            bound.push(pv.clone());
2031            let ok = match_expr_into(pb, tb, subst, bound);
2032            bound.pop();
2033            ok
2034        }
2035        // Any other variant pair: match only on structural equality — no
2036        // bindings inside shapes this matcher does not walk (conservative).
2037        _ => pattern == target,
2038    }
2039}
2040
2041#[cfg(test)]
2042mod tests {
2043    use super::*;
2044
2045    #[test]
2046    fn test_unify_same_constant() {
2047        let t1 = ProofTerm::Constant("a".into());
2048        let t2 = ProofTerm::Constant("a".into());
2049        let result = unify_terms(&t1, &t2);
2050        assert!(result.is_ok());
2051        assert!(result.unwrap().is_empty());
2052    }
2053
2054    #[test]
2055    fn test_unify_different_constants() {
2056        let t1 = ProofTerm::Constant("a".into());
2057        let t2 = ProofTerm::Constant("b".into());
2058        let result = unify_terms(&t1, &t2);
2059        assert!(result.is_err());
2060    }
2061
2062    #[test]
2063    fn test_unify_var_constant() {
2064        let t1 = ProofTerm::Variable("x".into());
2065        let t2 = ProofTerm::Constant("a".into());
2066        let result = unify_terms(&t1, &t2);
2067        assert!(result.is_ok());
2068        let subst = result.unwrap();
2069        assert_eq!(subst.get("x"), Some(&ProofTerm::Constant("a".into())));
2070    }
2071
2072    #[test]
2073    fn test_occurs_check() {
2074        let t1 = ProofTerm::Variable("x".into());
2075        let t2 = ProofTerm::Function("f".into(), vec![ProofTerm::Variable("x".into())]);
2076        let result = unify_terms(&t1, &t2);
2077        assert!(matches!(result, Err(ProofError::OccursCheck { .. })));
2078    }
2079
2080    #[test]
2081    fn test_compose_substitutions() {
2082        let mut s1 = Substitution::new();
2083        s1.insert("x".into(), ProofTerm::Variable("y".into()));
2084
2085        let mut s2 = Substitution::new();
2086        s2.insert("y".into(), ProofTerm::Constant("a".into()));
2087
2088        let composed = compose_substitutions(s1, s2);
2089
2090        // x should map to a (via y)
2091        assert_eq!(composed.get("x"), Some(&ProofTerm::Constant("a".into())));
2092        // y should also map to a
2093        assert_eq!(composed.get("y"), Some(&ProofTerm::Constant("a".into())));
2094    }
2095
2096    // =========================================================================
2097    // ALPHA-EQUIVALENCE TESTS
2098    // =========================================================================
2099
2100    #[test]
2101    fn test_alpha_equivalence_exists() {
2102        // ∃e P(e) should unify with ∃x P(x)
2103        let e1 = ProofExpr::Exists {
2104            variable: "e".to_string(),
2105            body: Box::new(ProofExpr::Predicate {
2106                name: "run".to_string(),
2107                args: vec![ProofTerm::Variable("e".to_string())],
2108                world: None,
2109            }),
2110        };
2111
2112        let e2 = ProofExpr::Exists {
2113            variable: "x".to_string(),
2114            body: Box::new(ProofExpr::Predicate {
2115                name: "run".to_string(),
2116                args: vec![ProofTerm::Variable("x".to_string())],
2117                world: None,
2118            }),
2119        };
2120
2121        let result = unify_exprs(&e1, &e2);
2122        assert!(
2123            result.is_ok(),
2124            "Alpha-equivalent expressions should unify: {:?}",
2125            result
2126        );
2127    }
2128
2129    #[test]
2130    fn test_alpha_equivalence_forall() {
2131        // ∀x P(x) should unify with ∀y P(y)
2132        let e1 = ProofExpr::ForAll {
2133            variable: "x".to_string(),
2134            body: Box::new(ProofExpr::Predicate {
2135                name: "mortal".to_string(),
2136                args: vec![ProofTerm::Variable("x".to_string())],
2137                world: None,
2138            }),
2139        };
2140
2141        let e2 = ProofExpr::ForAll {
2142            variable: "y".to_string(),
2143            body: Box::new(ProofExpr::Predicate {
2144                name: "mortal".to_string(),
2145                args: vec![ProofTerm::Variable("y".to_string())],
2146                world: None,
2147            }),
2148        };
2149
2150        let result = unify_exprs(&e1, &e2);
2151        assert!(
2152            result.is_ok(),
2153            "Alpha-equivalent universals should unify: {:?}",
2154            result
2155        );
2156    }
2157
2158    #[test]
2159    fn test_alpha_equivalence_nested() {
2160        // ∃e (Run(e) ∧ Agent(e, John)) should unify with ∃x (Run(x) ∧ Agent(x, John))
2161        let e1 = ProofExpr::Exists {
2162            variable: "e".to_string(),
2163            body: Box::new(ProofExpr::And(
2164                Box::new(ProofExpr::Predicate {
2165                    name: "run".to_string(),
2166                    args: vec![ProofTerm::Variable("e".to_string())],
2167                    world: None,
2168                }),
2169                Box::new(ProofExpr::Predicate {
2170                    name: "agent".to_string(),
2171                    args: vec![
2172                        ProofTerm::Variable("e".to_string()),
2173                        ProofTerm::Constant("John".to_string()),
2174                    ],
2175                    world: None,
2176                }),
2177            )),
2178        };
2179
2180        let e2 = ProofExpr::Exists {
2181            variable: "x".to_string(),
2182            body: Box::new(ProofExpr::And(
2183                Box::new(ProofExpr::Predicate {
2184                    name: "run".to_string(),
2185                    args: vec![ProofTerm::Variable("x".to_string())],
2186                    world: None,
2187                }),
2188                Box::new(ProofExpr::Predicate {
2189                    name: "agent".to_string(),
2190                    args: vec![
2191                        ProofTerm::Variable("x".to_string()),
2192                        ProofTerm::Constant("John".to_string()),
2193                    ],
2194                    world: None,
2195                }),
2196            )),
2197        };
2198
2199        let result = unify_exprs(&e1, &e2);
2200        assert!(
2201            result.is_ok(),
2202            "Nested alpha-equivalent expressions should unify: {:?}",
2203            result
2204        );
2205    }
2206}