427 lines
15 KiB
Rust
427 lines
15 KiB
Rust
|
|
//! Unit tests for tensor-backed chase algorithm
|
||
|
|
|
||
|
|
use geolog::core::{
|
||
|
|
Context, DerivedSort, Formula, RelationStorage, Sequent, Signature, Structure, Term, Theory,
|
||
|
|
};
|
||
|
|
use geolog::query::chase::chase_fixpoint;
|
||
|
|
use geolog::universe::Universe;
|
||
|
|
|
||
|
|
/// Create a simple test theory with one sort and one unary relation
|
||
|
|
fn simple_theory_with_relation() -> Theory {
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
sig.add_sort("V".to_string());
|
||
|
|
sig.add_relation("R".to_string(), DerivedSort::Base(0));
|
||
|
|
Theory {
|
||
|
|
name: "Simple".to_string(),
|
||
|
|
signature: sig,
|
||
|
|
axioms: vec![],
|
||
|
|
axiom_names: vec![],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a preorder-like theory with binary leq relation, reflexivity and transitivity
|
||
|
|
fn preorder_theory() -> Theory {
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
sig.add_sort("X".to_string());
|
||
|
|
|
||
|
|
// Binary relation with product domain: leq : [x: X, y: X] -> Prop
|
||
|
|
let domain = DerivedSort::Product(vec![
|
||
|
|
("x".to_string(), DerivedSort::Base(0)),
|
||
|
|
("y".to_string(), DerivedSort::Base(0)),
|
||
|
|
]);
|
||
|
|
sig.add_relation("leq".to_string(), domain);
|
||
|
|
|
||
|
|
// Reflexivity axiom: forall x : X. |- [x: x, y: x] leq
|
||
|
|
let refl_axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![("x".to_string(), DerivedSort::Base(0))],
|
||
|
|
},
|
||
|
|
premise: Formula::True,
|
||
|
|
conclusion: Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("x".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
("y".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Transitivity axiom: forall x, y, z : X. [x: x, y: y] leq, [x: y, y: z] leq |- [x: x, y: z] leq
|
||
|
|
let trans_axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![
|
||
|
|
("x".to_string(), DerivedSort::Base(0)),
|
||
|
|
("y".to_string(), DerivedSort::Base(0)),
|
||
|
|
("z".to_string(), DerivedSort::Base(0)),
|
||
|
|
],
|
||
|
|
},
|
||
|
|
premise: Formula::Conj(vec![
|
||
|
|
Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("x".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
("y".to_string(), Term::Var("y".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("x".to_string(), Term::Var("y".to_string(), DerivedSort::Base(0))),
|
||
|
|
("y".to_string(), Term::Var("z".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
]),
|
||
|
|
conclusion: Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("x".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
("y".to_string(), Term::Var("z".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
Theory {
|
||
|
|
name: "Preorder".to_string(),
|
||
|
|
signature: sig,
|
||
|
|
axioms: vec![refl_axiom, trans_axiom],
|
||
|
|
axiom_names: vec!["ax/refl".to_string(), "ax/trans".to_string()],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_adds_relation_from_true_premise() {
|
||
|
|
// Axiom: forall x : V. |- R(x)
|
||
|
|
// This should add all elements to R
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
sig.add_sort("V".to_string());
|
||
|
|
sig.add_relation("R".to_string(), DerivedSort::Base(0));
|
||
|
|
|
||
|
|
let axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![("x".to_string(), DerivedSort::Base(0))],
|
||
|
|
},
|
||
|
|
premise: Formula::True,
|
||
|
|
conclusion: Formula::Rel(0, Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
|
||
|
|
// Add some elements
|
||
|
|
let (a, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (c, _) = structure.add_element(&mut universe, 0);
|
||
|
|
|
||
|
|
// Initialize relation
|
||
|
|
structure.init_relations(&[1]);
|
||
|
|
|
||
|
|
// Run chase
|
||
|
|
let iterations = chase_fixpoint(&[axiom], &mut structure, &mut universe, &sig, 100).unwrap();
|
||
|
|
|
||
|
|
// Should add all 3 elements to R
|
||
|
|
assert_eq!(structure.get_relation(0).len(), 3);
|
||
|
|
assert!(structure.query_relation(0, &[a]));
|
||
|
|
assert!(structure.query_relation(0, &[b]));
|
||
|
|
assert!(structure.query_relation(0, &[c]));
|
||
|
|
|
||
|
|
// Should converge in 2 iterations
|
||
|
|
assert_eq!(iterations, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_fixpoint_empty_structure() {
|
||
|
|
let theory = simple_theory_with_relation();
|
||
|
|
|
||
|
|
// Axiom: forall x : V. |- R(x)
|
||
|
|
let axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![("x".to_string(), DerivedSort::Base(0))],
|
||
|
|
},
|
||
|
|
premise: Formula::True,
|
||
|
|
conclusion: Formula::Rel(0, Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
structure.init_relations(&[1]);
|
||
|
|
|
||
|
|
let iterations = chase_fixpoint(&[axiom], &mut structure, &mut universe, &theory.signature, 100).unwrap();
|
||
|
|
|
||
|
|
// Empty structure: no elements, so nothing to add
|
||
|
|
assert_eq!(iterations, 1);
|
||
|
|
assert_eq!(structure.get_relation(0).len(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_preorder_reflexivity() {
|
||
|
|
// Test that chase correctly computes reflexive closure
|
||
|
|
let theory = preorder_theory();
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
|
||
|
|
// Add 3 elements
|
||
|
|
let (a, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (c, _) = structure.add_element(&mut universe, 0);
|
||
|
|
|
||
|
|
// Initialize relation with arity 2
|
||
|
|
structure.init_relations(&[2]);
|
||
|
|
|
||
|
|
// Run chase
|
||
|
|
let iterations = chase_fixpoint(
|
||
|
|
&theory.axioms,
|
||
|
|
&mut structure,
|
||
|
|
&mut universe,
|
||
|
|
&theory.signature,
|
||
|
|
100,
|
||
|
|
).unwrap();
|
||
|
|
|
||
|
|
// Should have exactly 3 reflexive tuples
|
||
|
|
let relation = structure.get_relation(0);
|
||
|
|
assert_eq!(relation.len(), 3, "Should have exactly 3 reflexive tuples");
|
||
|
|
|
||
|
|
// Check reflexive pairs exist
|
||
|
|
assert!(structure.query_relation(0, &[a, a]), "Should have (a,a)");
|
||
|
|
assert!(structure.query_relation(0, &[b, b]), "Should have (b,b)");
|
||
|
|
assert!(structure.query_relation(0, &[c, c]), "Should have (c,c)");
|
||
|
|
|
||
|
|
// Check non-reflexive pairs do NOT exist
|
||
|
|
assert!(!structure.query_relation(0, &[a, b]), "Should NOT have (a,b)");
|
||
|
|
assert!(!structure.query_relation(0, &[a, c]), "Should NOT have (a,c)");
|
||
|
|
assert!(!structure.query_relation(0, &[b, a]), "Should NOT have (b,a)");
|
||
|
|
|
||
|
|
// Should complete in 2 iterations
|
||
|
|
assert_eq!(iterations, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_transitive_closure() {
|
||
|
|
// Test that chase correctly computes transitive closure
|
||
|
|
let theory = preorder_theory();
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
|
||
|
|
// Add 3 elements: a < b < c (chain order)
|
||
|
|
let (a, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (c, _) = structure.add_element(&mut universe, 0);
|
||
|
|
|
||
|
|
// Initialize relation with arity 2
|
||
|
|
structure.init_relations(&[2]);
|
||
|
|
|
||
|
|
// Manually add initial ordering: a ≤ b and b ≤ c
|
||
|
|
structure.get_relation_mut(0).insert(vec![a, b]);
|
||
|
|
structure.get_relation_mut(0).insert(vec![b, c]);
|
||
|
|
|
||
|
|
// Run chase
|
||
|
|
let _iterations = chase_fixpoint(
|
||
|
|
&theory.axioms,
|
||
|
|
&mut structure,
|
||
|
|
&mut universe,
|
||
|
|
&theory.signature,
|
||
|
|
100,
|
||
|
|
).unwrap();
|
||
|
|
|
||
|
|
// Expected: 3 reflexive + 2 initial + 1 transitive (a,c) = 6
|
||
|
|
let relation = structure.get_relation(0);
|
||
|
|
assert_eq!(relation.len(), 6, "Should have 6 tuples");
|
||
|
|
|
||
|
|
// Check reflexive pairs
|
||
|
|
assert!(structure.query_relation(0, &[a, a]));
|
||
|
|
assert!(structure.query_relation(0, &[b, b]));
|
||
|
|
assert!(structure.query_relation(0, &[c, c]));
|
||
|
|
|
||
|
|
// Check initial ordering
|
||
|
|
assert!(structure.query_relation(0, &[a, b]));
|
||
|
|
assert!(structure.query_relation(0, &[b, c]));
|
||
|
|
|
||
|
|
// Check transitive closure!
|
||
|
|
assert!(structure.query_relation(0, &[a, c]), "Should have (a,c) from transitivity");
|
||
|
|
|
||
|
|
// Should NOT have backwards edges
|
||
|
|
assert!(!structure.query_relation(0, &[b, a]));
|
||
|
|
assert!(!structure.query_relation(0, &[c, b]));
|
||
|
|
assert!(!structure.query_relation(0, &[c, a]));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_conjunction_in_conclusion() {
|
||
|
|
// Axiom: forall x : V. |- R(x) ∧ S(x)
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
sig.add_sort("V".to_string());
|
||
|
|
sig.add_relation("R".to_string(), DerivedSort::Base(0));
|
||
|
|
sig.add_relation("S".to_string(), DerivedSort::Base(0));
|
||
|
|
|
||
|
|
let axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![("x".to_string(), DerivedSort::Base(0))],
|
||
|
|
},
|
||
|
|
premise: Formula::True,
|
||
|
|
conclusion: Formula::Conj(vec![
|
||
|
|
Formula::Rel(0, Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
Formula::Rel(1, Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
|
||
|
|
let (a, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, 0);
|
||
|
|
|
||
|
|
structure.init_relations(&[1, 1]);
|
||
|
|
|
||
|
|
let _iterations = chase_fixpoint(&[axiom], &mut structure, &mut universe, &sig, 100).unwrap();
|
||
|
|
|
||
|
|
// Both relations should have both elements
|
||
|
|
assert_eq!(structure.get_relation(0).len(), 2);
|
||
|
|
assert_eq!(structure.get_relation(1).len(), 2);
|
||
|
|
assert!(structure.query_relation(0, &[a]));
|
||
|
|
assert!(structure.query_relation(0, &[b]));
|
||
|
|
assert!(structure.query_relation(1, &[a]));
|
||
|
|
assert!(structure.query_relation(1, &[b]));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_chase_relation_premise() {
|
||
|
|
// Axiom: forall x, y : V. R(x, y) |- S(x, y)
|
||
|
|
// Copy tuples from R to S
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
sig.add_sort("V".to_string());
|
||
|
|
let domain = DerivedSort::Product(vec![
|
||
|
|
("a".to_string(), DerivedSort::Base(0)),
|
||
|
|
("b".to_string(), DerivedSort::Base(0)),
|
||
|
|
]);
|
||
|
|
sig.add_relation("R".to_string(), domain.clone());
|
||
|
|
sig.add_relation("S".to_string(), domain);
|
||
|
|
|
||
|
|
let axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![
|
||
|
|
("x".to_string(), DerivedSort::Base(0)),
|
||
|
|
("y".to_string(), DerivedSort::Base(0)),
|
||
|
|
],
|
||
|
|
},
|
||
|
|
premise: Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("a".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
("b".to_string(), Term::Var("y".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
conclusion: Formula::Rel(
|
||
|
|
1,
|
||
|
|
Term::Record(vec![
|
||
|
|
("a".to_string(), Term::Var("x".to_string(), DerivedSort::Base(0))),
|
||
|
|
("b".to_string(), Term::Var("y".to_string(), DerivedSort::Base(0))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(1);
|
||
|
|
|
||
|
|
let (a, _) = structure.add_element(&mut universe, 0);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, 0);
|
||
|
|
|
||
|
|
structure.init_relations(&[2, 2]);
|
||
|
|
|
||
|
|
// Add some tuples to R
|
||
|
|
structure.get_relation_mut(0).insert(vec![a, b]);
|
||
|
|
structure.get_relation_mut(0).insert(vec![b, a]);
|
||
|
|
|
||
|
|
let _iterations = chase_fixpoint(&[axiom], &mut structure, &mut universe, &sig, 100).unwrap();
|
||
|
|
|
||
|
|
// S should have the same tuples as R
|
||
|
|
assert_eq!(structure.get_relation(1).len(), 2);
|
||
|
|
assert!(structure.query_relation(1, &[a, b]));
|
||
|
|
assert!(structure.query_relation(1, &[b, a]));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Test chase with existential premise (the feature that motivated tensor-backed chase!)
|
||
|
|
#[test]
|
||
|
|
fn test_chase_existential_premise() {
|
||
|
|
// Theory: Graph with reachability
|
||
|
|
// Axiom: forall v0, v1 : V. (exists e : E. src(e) = v0 ∧ tgt(e) = v1) |- reachable(v0, v1)
|
||
|
|
let mut sig = Signature::default();
|
||
|
|
let v_sort = sig.add_sort("V".to_string());
|
||
|
|
let e_sort = sig.add_sort("E".to_string());
|
||
|
|
|
||
|
|
// src, tgt : E -> V
|
||
|
|
sig.add_function("src".to_string(), DerivedSort::Base(e_sort), DerivedSort::Base(v_sort));
|
||
|
|
sig.add_function("tgt".to_string(), DerivedSort::Base(e_sort), DerivedSort::Base(v_sort));
|
||
|
|
|
||
|
|
// reachable : [from: V, to: V] -> Prop
|
||
|
|
let reach_domain = DerivedSort::Product(vec![
|
||
|
|
("from".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
("to".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
]);
|
||
|
|
sig.add_relation("reachable".to_string(), reach_domain);
|
||
|
|
|
||
|
|
// Axiom: (exists e : E. src(e) = v0 ∧ tgt(e) = v1) |- reachable(v0, v1)
|
||
|
|
let axiom = Sequent {
|
||
|
|
context: Context {
|
||
|
|
vars: vec![
|
||
|
|
("v0".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
("v1".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
],
|
||
|
|
},
|
||
|
|
premise: Formula::Exists(
|
||
|
|
"e".to_string(),
|
||
|
|
DerivedSort::Base(e_sort),
|
||
|
|
Box::new(Formula::Conj(vec![
|
||
|
|
Formula::Eq(
|
||
|
|
Term::App(0, Box::new(Term::Var("e".to_string(), DerivedSort::Base(e_sort)))),
|
||
|
|
Term::Var("v0".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
),
|
||
|
|
Formula::Eq(
|
||
|
|
Term::App(1, Box::new(Term::Var("e".to_string(), DerivedSort::Base(e_sort)))),
|
||
|
|
Term::Var("v1".to_string(), DerivedSort::Base(v_sort)),
|
||
|
|
),
|
||
|
|
])),
|
||
|
|
),
|
||
|
|
conclusion: Formula::Rel(
|
||
|
|
0,
|
||
|
|
Term::Record(vec![
|
||
|
|
("from".to_string(), Term::Var("v0".to_string(), DerivedSort::Base(v_sort))),
|
||
|
|
("to".to_string(), Term::Var("v1".to_string(), DerivedSort::Base(v_sort))),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut universe = Universe::new();
|
||
|
|
let mut structure = Structure::new(2); // 2 sorts: V and E
|
||
|
|
|
||
|
|
// Add vertices: a, b, c
|
||
|
|
let (a, _) = structure.add_element(&mut universe, v_sort);
|
||
|
|
let (b, _) = structure.add_element(&mut universe, v_sort);
|
||
|
|
let (c, _) = structure.add_element(&mut universe, v_sort);
|
||
|
|
|
||
|
|
// Add edges: e1 (a->b), e2 (b->c)
|
||
|
|
let (e1, _) = structure.add_element(&mut universe, e_sort);
|
||
|
|
let (e2, _) = structure.add_element(&mut universe, e_sort);
|
||
|
|
|
||
|
|
// Initialize functions and relations
|
||
|
|
structure.init_functions(&[Some(e_sort), Some(e_sort)]); // src, tgt both have domain E
|
||
|
|
structure.init_relations(&[2]); // reachable is binary
|
||
|
|
|
||
|
|
// Define src and tgt
|
||
|
|
structure.define_function(0, e1, a).unwrap(); // src(e1) = a
|
||
|
|
structure.define_function(1, e1, b).unwrap(); // tgt(e1) = b
|
||
|
|
structure.define_function(0, e2, b).unwrap(); // src(e2) = b
|
||
|
|
structure.define_function(1, e2, c).unwrap(); // tgt(e2) = c
|
||
|
|
|
||
|
|
// Run chase
|
||
|
|
let iterations = chase_fixpoint(&[axiom], &mut structure, &mut universe, &sig, 100).unwrap();
|
||
|
|
|
||
|
|
// Should derive reachable(a,b) and reachable(b,c)
|
||
|
|
assert_eq!(structure.get_relation(0).len(), 2, "Should have 2 reachable pairs");
|
||
|
|
assert!(structure.query_relation(0, &[a, b]), "Should have reachable(a,b)");
|
||
|
|
assert!(structure.query_relation(0, &[b, c]), "Should have reachable(b,c)");
|
||
|
|
|
||
|
|
// Should NOT have other pairs
|
||
|
|
assert!(!structure.query_relation(0, &[a, c]), "Should NOT have reachable(a,c) without transitive closure axiom");
|
||
|
|
|
||
|
|
println!("Chase with existential premise completed in {} iterations", iterations);
|
||
|
|
}
|