Skip to main content

tensorlogic_ir/
unification.rs

1//! # First-Order Unification
2//!
3//! This module implements Robinson's unification algorithm for first-order terms.
4//! Unification is a fundamental operation in automated theorem proving, logic programming,
5//! and type inference.
6//!
7//! ## Overview
8//!
9//! **Unification** finds a most general unifier (MGU) that makes two terms syntactically equal:
10//! - Given terms `s` and `t`, find substitution `θ` such that `sθ = tθ`
11//! - The MGU is the most general such substitution (no more specific than necessary)
12//!
13//! ## Examples
14//!
15//! ```rust
16//! use tensorlogic_ir::{Term, Substitution, unify_terms};
17//!
18//! // Unify P(x, f(y)) with P(a, f(b))
19//! let term1 = Term::var("x");
20//! let term2 = Term::constant("a");
21//!
22//! let result = unify_terms(&term1, &term2);
23//! assert!(result.is_ok());
24//! let subst = result.unwrap();
25//! assert_eq!(subst.apply(&term1), term2);
26//! ```
27//!
28//! ## Algorithm
29//!
30//! We use Robinson's unification algorithm with occur-check:
31//! 1. If both terms are identical, return empty substitution
32//! 2. If one is a variable, bind it to the other (with occur-check)
33//! 3. If both are compound terms, recursively unify arguments
34//! 4. Otherwise, unification fails
35//!
36//! ## Applications
37//!
38//! - **Theorem Proving**: Resolution requires unification of complementary literals
39//! - **Logic Programming**: Pattern matching in Prolog-style languages
40//! - **Type Inference**: Hindley-Milner type inference uses unification
41//! - **Sequent Calculus**: Term substitution in quantifier rules
42
43use crate::error::IrError;
44use crate::term::Term;
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47
48/// A substitution maps variables to terms.
49///
50/// A substitution θ = {x₁/t₁, x₂/t₂, ...} represents the simultaneous
51/// replacement of each variable xᵢ with term tᵢ.
52#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
53pub struct Substitution {
54    /// Mapping from variable names to their replacement terms
55    bindings: HashMap<String, Term>,
56}
57
58impl Substitution {
59    /// Create an empty substitution (identity).
60    pub fn empty() -> Self {
61        Substitution {
62            bindings: HashMap::new(),
63        }
64    }
65
66    /// Create a substitution with a single binding.
67    pub fn singleton(var: String, term: Term) -> Self {
68        let mut bindings = HashMap::new();
69        bindings.insert(var, term);
70        Substitution { bindings }
71    }
72
73    /// Create a substitution from a map of bindings.
74    pub fn from_map(bindings: HashMap<String, Term>) -> Self {
75        Substitution { bindings }
76    }
77
78    /// Check if this is the empty substitution.
79    pub fn is_empty(&self) -> bool {
80        self.bindings.is_empty()
81    }
82
83    /// Get the number of bindings.
84    pub fn len(&self) -> usize {
85        self.bindings.len()
86    }
87
88    /// Get the binding for a variable, if it exists.
89    pub fn get(&self, var: &str) -> Option<&Term> {
90        self.bindings.get(var)
91    }
92
93    /// Add a binding to the substitution.
94    pub fn bind(&mut self, var: String, term: Term) {
95        self.bindings.insert(var, term);
96    }
97
98    /// Apply this substitution to a term.
99    ///
100    /// This recursively replaces all occurrences of bound variables
101    /// with their substituted values.
102    pub fn apply(&self, term: &Term) -> Term {
103        match term {
104            Term::Var(name) => {
105                // If variable is bound, return its substitution
106                // Otherwise return the variable unchanged
107                self.bindings
108                    .get(name)
109                    .cloned()
110                    .unwrap_or_else(|| term.clone())
111            }
112            Term::Const(_) => term.clone(),
113            Term::Typed {
114                value,
115                type_annotation,
116            } => Term::Typed {
117                value: Box::new(self.apply(value)),
118                type_annotation: type_annotation.clone(),
119            },
120        }
121    }
122
123    /// Compose two substitutions: (σ ∘ θ)(x) = σ(θ(x))
124    ///
125    /// The composition applies θ first, then σ.
126    pub fn compose(&self, other: &Substitution) -> Substitution {
127        let mut result = HashMap::new();
128
129        // Apply self to all bindings in other
130        for (var, term) in &other.bindings {
131            result.insert(var.clone(), self.apply(term));
132        }
133
134        // Add bindings from self that aren't in other
135        for (var, term) in &self.bindings {
136            if !result.contains_key(var) {
137                result.insert(var.clone(), term.clone());
138            }
139        }
140
141        Substitution::from_map(result)
142    }
143
144    /// Get all bound variables.
145    pub fn domain(&self) -> Vec<String> {
146        self.bindings.keys().cloned().collect()
147    }
148
149    /// Get all terms that variables are bound to.
150    pub fn range(&self) -> Vec<Term> {
151        self.bindings.values().cloned().collect()
152    }
153
154    /// Extend this substitution with a new binding.
155    ///
156    /// Returns an error if the binding conflicts with existing bindings.
157    pub fn extend(&mut self, var: String, term: Term) -> Result<(), IrError> {
158        if let Some(existing) = self.bindings.get(&var) {
159            if existing != &term {
160                return Err(IrError::UnificationFailure {
161                    type1: format!("{:?}", existing),
162                    type2: format!("{:?}", term),
163                });
164            }
165        }
166        self.bindings.insert(var, term);
167        Ok(())
168    }
169
170    /// Try to extend this substitution with all bindings from another substitution.
171    ///
172    /// This is used for subsumption checking where we need to merge substitutions.
173    /// Returns an error if any binding conflicts with existing bindings.
174    pub fn try_extend(&mut self, other: &Substitution) -> Result<(), IrError> {
175        for (var, term) in &other.bindings {
176            if let Some(existing) = self.bindings.get(var) {
177                if existing != term {
178                    return Err(IrError::UnificationFailure {
179                        type1: format!("{:?}", existing),
180                        type2: format!("{:?}", term),
181                    });
182                }
183            } else {
184                self.bindings.insert(var.clone(), term.clone());
185            }
186        }
187        Ok(())
188    }
189}
190
191/// Check if a variable occurs in a term (occur-check).
192///
193/// This is essential for preventing infinite structures in unification.
194/// For example, unifying `x` with `f(x)` would create an infinite term.
195fn occurs_check(var: &str, term: &Term) -> bool {
196    match term {
197        Term::Var(name) => name == var,
198        Term::Const(_) => false,
199        Term::Typed { value, .. } => occurs_check(var, value),
200    }
201}
202
203/// Unify two terms, returning the most general unifier (MGU).
204///
205/// The MGU is a substitution θ such that `θ(term1) = θ(term2)`.
206///
207/// # Errors
208///
209/// Returns `IrError::UnificationFailure` if the terms cannot be unified.
210///
211/// # Examples
212///
213/// ```rust
214/// use tensorlogic_ir::{Term, unify_terms};
215///
216/// let x = Term::var("x");
217/// let a = Term::constant("a");
218///
219/// let mgu = unify_terms(&x, &a).unwrap();
220/// assert_eq!(mgu.apply(&x), a);
221/// ```
222pub fn unify_terms(term1: &Term, term2: &Term) -> Result<Substitution, IrError> {
223    unify_impl(term1, term2, &mut Substitution::empty())
224}
225
226/// Internal unification implementation with accumulating substitution.
227fn unify_impl(
228    term1: &Term,
229    term2: &Term,
230    subst: &mut Substitution,
231) -> Result<Substitution, IrError> {
232    // Apply current substitution to both terms
233    let t1 = subst.apply(term1);
234    let t2 = subst.apply(term2);
235
236    match (&t1, &t2) {
237        // Both are the same variable
238        (Term::Var(n1), Term::Var(n2)) if n1 == n2 => Ok(subst.clone()),
239
240        // t1 is a variable, bind it to t2
241        (Term::Var(name), _) => {
242            if occurs_check(name, &t2) {
243                return Err(IrError::UnificationFailure {
244                    type1: format!("{:?}", t1),
245                    type2: format!("{:?}", t2),
246                });
247            }
248            subst.bind(name.clone(), t2.clone());
249            Ok(subst.clone())
250        }
251
252        // t2 is a variable, bind it to t1
253        (_, Term::Var(name)) => {
254            if occurs_check(name, &t1) {
255                return Err(IrError::UnificationFailure {
256                    type1: format!("{:?}", t1),
257                    type2: format!("{:?}", t2),
258                });
259            }
260            subst.bind(name.clone(), t1.clone());
261            Ok(subst.clone())
262        }
263
264        // Both are constants
265        (Term::Const(v1), Term::Const(v2)) => {
266            if v1 == v2 {
267                Ok(subst.clone())
268            } else {
269                Err(IrError::UnificationFailure {
270                    type1: format!("{:?}", t1),
271                    type2: format!("{:?}", t2),
272                })
273            }
274        }
275
276        // Both are typed terms - unify the inner terms
277        (
278            Term::Typed {
279                value: inner1,
280                type_annotation: ty1,
281            },
282            Term::Typed {
283                value: inner2,
284                type_annotation: ty2,
285            },
286        ) => {
287            // Type annotations must match
288            if ty1 != ty2 {
289                return Err(IrError::UnificationFailure {
290                    type1: format!("{:?}", t1),
291                    type2: format!("{:?}", t2),
292                });
293            }
294            unify_impl(inner1, inner2, subst)
295        }
296
297        // One typed, one not - unify inner term with the other
298        (Term::Typed { value, .. }, other) | (other, Term::Typed { value, .. }) => {
299            unify_impl(value, other, subst)
300        }
301    }
302}
303
304/// Attempt to unify a list of term pairs.
305///
306/// This is useful for unifying predicate arguments in resolution.
307///
308/// # Examples
309///
310/// ```rust
311/// use tensorlogic_ir::{Term, unify_term_list};
312///
313/// let pairs = vec![
314///     (Term::var("x"), Term::constant("a")),
315///     (Term::var("y"), Term::constant("b")),
316/// ];
317///
318/// let mgu = unify_term_list(&pairs).unwrap();
319/// assert_eq!(mgu.len(), 2);
320/// ```
321pub fn unify_term_list(pairs: &[(Term, Term)]) -> Result<Substitution, IrError> {
322    let mut subst = Substitution::empty();
323    for (t1, t2) in pairs {
324        subst = unify_impl(t1, t2, &mut subst)?;
325    }
326    Ok(subst)
327}
328
329/// Check if two terms are unifiable (without computing the unifier).
330pub fn are_unifiable(term1: &Term, term2: &Term) -> bool {
331    unify_terms(term1, term2).is_ok()
332}
333
334/// Rename variables in a term to avoid conflicts.
335///
336/// This is useful when applying quantifier rules in sequent calculus.
337pub fn rename_vars(term: &Term, suffix: &str) -> Term {
338    match term {
339        Term::Var(name) => Term::Var(format!("{}_{}", name, suffix)),
340        Term::Const(_) => term.clone(),
341        Term::Typed {
342            value,
343            type_annotation,
344        } => Term::Typed {
345            value: Box::new(rename_vars(value, suffix)),
346            type_annotation: type_annotation.clone(),
347        },
348    }
349}
350
351// ============================================================================
352// Anti-Unification (Most Specific Generalization)
353// ============================================================================
354
355/// Anti-unification finds the most specific generalization (MSG) of two terms.
356///
357/// While unification finds a substitution that makes two terms equal,
358/// anti-unification finds a term that generalizes both input terms.
359///
360/// ## Examples
361///
362/// ```rust
363/// use tensorlogic_ir::{Term, anti_unify_terms};
364///
365/// // anti_unify(f(a, b), f(c, d)) = f(X, Y)
366/// // where X and Y are fresh variables
367/// let t1 = Term::constant("a");
368/// let t2 = Term::constant("b");
369///
370/// let (gen, _subst1, _subst2) = anti_unify_terms(&t1, &t2);
371/// // gen is a fresh variable that generalizes both a and b
372/// ```
373///
374/// ## Applications
375///
376/// - **Inductive Logic Programming**: Learn patterns from examples
377/// - **Program Synthesis**: Generalize from concrete cases
378/// - **Code Clone Detection**: Find common structure in code
379/// - **Proof Generalization**: Abstract from specific proofs
380pub fn anti_unify_terms(term1: &Term, term2: &Term) -> (Term, Substitution, Substitution) {
381    let mut var_counter = 0;
382    let mut subst1 = Substitution::empty();
383    let mut subst2 = Substitution::empty();
384
385    let gen = anti_unify_impl(term1, term2, &mut var_counter, &mut subst1, &mut subst2);
386    (gen, subst1, subst2)
387}
388
389/// Internal implementation of anti-unification with fresh variable generation.
390fn anti_unify_impl(
391    term1: &Term,
392    term2: &Term,
393    var_counter: &mut usize,
394    subst1: &mut Substitution,
395    subst2: &mut Substitution,
396) -> Term {
397    match (term1, term2) {
398        // If both are the same constant, return the constant
399        (Term::Const(c1), Term::Const(c2)) if c1 == c2 => term1.clone(),
400
401        // If both are the same variable, return the variable
402        (Term::Var(v1), Term::Var(v2)) if v1 == v2 => term1.clone(),
403
404        // For typed terms with same type annotation, anti-unify the inner values
405        (
406            Term::Typed {
407                value: inner1,
408                type_annotation: ty1,
409            },
410            Term::Typed {
411                value: inner2,
412                type_annotation: ty2,
413            },
414        ) if ty1 == ty2 => {
415            let inner_gen = anti_unify_impl(inner1, inner2, var_counter, subst1, subst2);
416            Term::Typed {
417                value: Box::new(inner_gen),
418                type_annotation: ty1.clone(),
419            }
420        }
421
422        // Otherwise, introduce a fresh variable
423        _ => {
424            *var_counter += 1;
425            let fresh_var = Term::Var(format!("_G{}", var_counter));
426
427            // Record the substitutions: fresh_var maps to term1 in subst1, term2 in subst2
428            subst1.bind(format!("_G{}", var_counter), term1.clone());
429            subst2.bind(format!("_G{}", var_counter), term2.clone());
430
431            fresh_var
432        }
433    }
434}
435
436/// Compute the least general generalization (LGG) of a list of terms.
437///
438/// This repeatedly applies anti-unification to find a term that generalizes all inputs.
439///
440/// # Examples
441///
442/// ```rust
443/// use tensorlogic_ir::{Term, lgg_terms};
444///
445/// let terms = vec![
446///     Term::constant("a"),
447///     Term::constant("b"),
448///     Term::constant("c"),
449/// ];
450///
451/// let (gen, substs) = lgg_terms(&terms);
452/// // gen is a fresh variable that generalizes all three constants
453/// assert_eq!(substs.len(), 3);
454/// ```
455pub fn lgg_terms(terms: &[Term]) -> (Term, Vec<Substitution>) {
456    if terms.is_empty() {
457        return (Term::Var("_Empty".to_string()), vec![]);
458    }
459
460    if terms.len() == 1 {
461        return (terms[0].clone(), vec![Substitution::empty()]);
462    }
463
464    // Start with first two terms
465    let (mut gen, subst1, subst2) = anti_unify_terms(&terms[0], &terms[1]);
466    let mut substs = vec![subst1, subst2];
467
468    // Generalize with remaining terms
469    for term in &terms[2..] {
470        let (new_gen, gen_subst, term_subst) = anti_unify_terms(&gen, term);
471        gen = new_gen;
472
473        // Update existing substitutions by composing with gen_subst
474        for s in &mut substs {
475            *s = gen_subst.compose(s);
476        }
477
478        // Add new term's substitution
479        substs.push(term_subst);
480    }
481
482    (gen, substs)
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_empty_substitution() {
491        let subst = Substitution::empty();
492        assert!(subst.is_empty());
493        assert_eq!(subst.len(), 0);
494
495        let term = Term::var("x");
496        assert_eq!(subst.apply(&term), term);
497    }
498
499    #[test]
500    fn test_singleton_substitution() {
501        let subst = Substitution::singleton("x".to_string(), Term::constant("a"));
502        assert_eq!(subst.len(), 1);
503
504        let x = Term::var("x");
505        let a = Term::constant("a");
506        assert_eq!(subst.apply(&x), a);
507    }
508
509    #[test]
510    fn test_substitution_application() {
511        let mut subst = Substitution::empty();
512        subst.bind("x".to_string(), Term::constant("a"));
513        subst.bind("y".to_string(), Term::constant("b"));
514
515        let x = Term::var("x");
516        let y = Term::var("y");
517        let z = Term::var("z");
518
519        assert_eq!(subst.apply(&x), Term::constant("a"));
520        assert_eq!(subst.apply(&y), Term::constant("b"));
521        assert_eq!(subst.apply(&z), z); // Unbound variable unchanged
522    }
523
524    #[test]
525    fn test_unify_var_constant() {
526        let x = Term::var("x");
527        let a = Term::constant("a");
528
529        let mgu = unify_terms(&x, &a).unwrap();
530        assert_eq!(mgu.apply(&x), a);
531    }
532
533    #[test]
534    fn test_unify_same_variable() {
535        let x = Term::var("x");
536        let mgu = unify_terms(&x, &x).unwrap();
537        assert!(mgu.is_empty());
538    }
539
540    #[test]
541    fn test_unify_different_constants() {
542        let a = Term::constant("a");
543        let b = Term::constant("b");
544
545        let result = unify_terms(&a, &b);
546        assert!(result.is_err());
547    }
548
549    #[test]
550    fn test_unify_same_constant() {
551        let a = Term::constant("a");
552        let mgu = unify_terms(&a, &a).unwrap();
553        assert!(mgu.is_empty());
554    }
555
556    #[test]
557    fn test_occur_check() {
558        let x = Term::var("x");
559        assert!(occurs_check("x", &x));
560        assert!(!occurs_check("y", &x));
561
562        let a = Term::constant("a");
563        assert!(!occurs_check("x", &a));
564    }
565
566    #[test]
567    fn test_substitution_composition() {
568        // σ = {x/a}
569        let sigma = Substitution::singleton("x".to_string(), Term::constant("a"));
570        // θ = {y/x}
571        let theta = Substitution::singleton("y".to_string(), Term::var("x"));
572
573        // σ ∘ θ = {x/a, y/a}
574        let composed = sigma.compose(&theta);
575        assert_eq!(composed.len(), 2);
576        assert_eq!(composed.apply(&Term::var("x")), Term::constant("a"));
577        assert_eq!(composed.apply(&Term::var("y")), Term::constant("a"));
578    }
579
580    #[test]
581    fn test_unify_term_list() {
582        let pairs = vec![
583            (Term::var("x"), Term::constant("a")),
584            (Term::var("y"), Term::constant("b")),
585            (Term::var("z"), Term::var("x")),
586        ];
587
588        let mgu = unify_term_list(&pairs).unwrap();
589        assert_eq!(mgu.len(), 3);
590        assert_eq!(mgu.apply(&Term::var("x")), Term::constant("a"));
591        assert_eq!(mgu.apply(&Term::var("y")), Term::constant("b"));
592        assert_eq!(mgu.apply(&Term::var("z")), Term::constant("a"));
593    }
594
595    #[test]
596    fn test_are_unifiable() {
597        let x = Term::var("x");
598        let a = Term::constant("a");
599        let b = Term::constant("b");
600
601        assert!(are_unifiable(&x, &a));
602        assert!(are_unifiable(&a, &a));
603        assert!(!are_unifiable(&a, &b));
604    }
605
606    #[test]
607    fn test_rename_vars() {
608        let x = Term::var("x");
609        let renamed = rename_vars(&x, "1");
610        assert_eq!(renamed, Term::var("x_1"));
611
612        let a = Term::constant("a");
613        let renamed_const = rename_vars(&a, "1");
614        assert_eq!(renamed_const, a); // Constants unchanged
615    }
616
617    #[test]
618    fn test_extend_substitution() {
619        let mut subst = Substitution::empty();
620        assert!(subst.extend("x".to_string(), Term::constant("a")).is_ok());
621        assert!(subst.extend("y".to_string(), Term::constant("b")).is_ok());
622
623        // Extending with same binding is OK
624        assert!(subst.extend("x".to_string(), Term::constant("a")).is_ok());
625
626        // Extending with conflicting binding fails
627        assert!(subst.extend("x".to_string(), Term::constant("b")).is_err());
628    }
629
630    #[test]
631    fn test_typed_term_unification() {
632        use crate::term::TypeAnnotation;
633
634        let x = Term::Typed {
635            value: Box::new(Term::var("x")),
636            type_annotation: TypeAnnotation::new("Int"),
637        };
638        let a = Term::Typed {
639            value: Box::new(Term::constant("5")),
640            type_annotation: TypeAnnotation::new("Int"),
641        };
642
643        let mgu = unify_terms(&x, &a).unwrap();
644        assert_eq!(mgu.len(), 1);
645    }
646
647    // === Anti-Unification Tests ===
648
649    #[test]
650    fn test_anti_unify_same_constant() {
651        // anti_unify(a, a) = a
652        let a1 = Term::constant("a");
653        let a2 = Term::constant("a");
654
655        let (gen, subst1, subst2) = anti_unify_terms(&a1, &a2);
656
657        // Should return the constant itself
658        assert_eq!(gen, a1);
659        assert!(subst1.is_empty());
660        assert!(subst2.is_empty());
661    }
662
663    #[test]
664    fn test_anti_unify_different_constants() {
665        // anti_unify(a, b) = X (fresh variable)
666        let a = Term::constant("a");
667        let b = Term::constant("b");
668
669        let (gen, subst1, subst2) = anti_unify_terms(&a, &b);
670
671        // Should return a fresh variable
672        match gen {
673            Term::Var(name) => assert!(name.starts_with("_G")),
674            _ => panic!("Expected fresh variable"),
675        }
676
677        // Substitutions should map the fresh variable to a and b respectively
678        assert_eq!(subst1.len(), 1);
679        assert_eq!(subst2.len(), 1);
680    }
681
682    #[test]
683    fn test_anti_unify_variable_constant() {
684        // anti_unify(x, a) = X (fresh variable)
685        let x = Term::var("x");
686        let a = Term::constant("a");
687
688        let (gen, _subst1, _subst2) = anti_unify_terms(&x, &a);
689
690        // Should return a fresh variable (since x ≠ a)
691        if let Term::Var(name) = gen {
692            // Could be x if they're the same, or _GN if different
693            assert!(name == "x" || name.starts_with("_G"));
694        }
695
696        // Should have recorded the generalization
697        // (substitutions may be empty or non-empty depending on the terms)
698    }
699
700    #[test]
701    fn test_anti_unify_same_variable() {
702        // anti_unify(x, x) = x
703        let x1 = Term::var("x");
704        let x2 = Term::var("x");
705
706        let (gen, subst1, subst2) = anti_unify_terms(&x1, &x2);
707
708        // Should return the variable itself
709        assert_eq!(gen, x1);
710        assert!(subst1.is_empty());
711        assert!(subst2.is_empty());
712    }
713
714    #[test]
715    fn test_anti_unify_typed_terms() {
716        use crate::term::TypeAnnotation;
717
718        // anti_unify(Int(5), Int(10)) = Int(X)
719        let t1 = Term::Typed {
720            value: Box::new(Term::constant("5")),
721            type_annotation: TypeAnnotation::new("Int"),
722        };
723        let t2 = Term::Typed {
724            value: Box::new(Term::constant("10")),
725            type_annotation: TypeAnnotation::new("Int"),
726        };
727
728        let (gen, _subst1, _subst2) = anti_unify_terms(&t1, &t2);
729
730        // Should return Int(X) where X is fresh
731        match gen {
732            Term::Typed {
733                value,
734                type_annotation,
735            } => {
736                assert_eq!(type_annotation.type_name, "Int");
737                match *value {
738                    Term::Var(name) => assert!(name.starts_with("_G")),
739                    _ => panic!("Expected fresh variable inside typed term"),
740                }
741            }
742            _ => panic!("Expected typed term"),
743        }
744    }
745
746    #[test]
747    fn test_lgg_single_term() {
748        // LGG([a]) = a
749        let terms = vec![Term::constant("a")];
750        let (gen, substs) = lgg_terms(&terms);
751
752        assert_eq!(gen, Term::constant("a"));
753        assert_eq!(substs.len(), 1);
754        assert!(substs[0].is_empty());
755    }
756
757    #[test]
758    fn test_lgg_two_same_terms() {
759        // LGG([a, a]) = a
760        let terms = vec![Term::constant("a"), Term::constant("a")];
761        let (gen, substs) = lgg_terms(&terms);
762
763        assert_eq!(gen, Term::constant("a"));
764        assert_eq!(substs.len(), 2);
765    }
766
767    #[test]
768    fn test_lgg_two_different_terms() {
769        // LGG([a, b]) = X
770        let terms = vec![Term::constant("a"), Term::constant("b")];
771        let (gen, substs) = lgg_terms(&terms);
772
773        // Should be a fresh variable
774        match gen {
775            Term::Var(name) => assert!(name.starts_with("_G")),
776            _ => panic!("Expected fresh variable"),
777        }
778
779        assert_eq!(substs.len(), 2);
780    }
781
782    #[test]
783    fn test_lgg_three_terms() {
784        // LGG([a, b, c]) = X (all different)
785        let terms = vec![
786            Term::constant("a"),
787            Term::constant("b"),
788            Term::constant("c"),
789        ];
790        let (gen, substs) = lgg_terms(&terms);
791
792        // Should be a fresh variable
793        match gen {
794            Term::Var(name) => assert!(name.starts_with("_G")),
795            _ => panic!("Expected fresh variable"),
796        }
797
798        assert_eq!(substs.len(), 3);
799    }
800
801    #[test]
802    fn test_lgg_empty() {
803        // LGG([]) = special empty variable
804        let terms: Vec<Term> = vec![];
805        let (gen, substs) = lgg_terms(&terms);
806
807        match gen {
808            Term::Var(name) => assert_eq!(name, "_Empty"),
809            _ => panic!("Expected _Empty variable"),
810        }
811
812        assert_eq!(substs.len(), 0);
813    }
814
815    #[test]
816    fn test_anti_unify_preserves_structure() {
817        // When generalizing identical structures, structure should be preserved
818        let a1 = Term::constant("a");
819        let a2 = Term::constant("a");
820
821        let (gen, _, _) = anti_unify_terms(&a1, &a2);
822
823        // Should preserve the constant
824        assert_eq!(gen, Term::constant("a"));
825    }
826}