2026-02-26 11:50:51 +01:00
|
|
|
//! Unit tests for tensor-backed chase algorithm
|
|
|
|
|
|
|
|
|
|
use geolog::core::{
|
|
|
|
|
Context, DerivedSort, Formula, RelationStorage, Sequent, Signature, Structure, Term, Theory,
|
|
|
|
|
};
|
2026-03-19 17:42:04 +01:00
|
|
|
use geolog::query::chase::{chase_fixpoint, chase_fixpoint_with_observer, ChaseEvent, ChaseControl, ChaseObserver};
|
2026-02-26 11:50:51 +01:00
|
|
|
use geolog::universe::Universe;
|
2026-03-19 17:42:04 +01:00
|
|
|
use std::collections::HashMap;
|
2026-02-26 11:50:51 +01:00
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
|
}
|
2026-03-19 17:42:04 +01:00
|
|
|
|
|
|
|
|
/// A test observer that counts events
|
|
|
|
|
struct CountingObserver {
|
|
|
|
|
iteration_starts: usize,
|
|
|
|
|
axiom_firings: usize,
|
|
|
|
|
axiom_fired: usize,
|
|
|
|
|
iteration_ends: usize,
|
|
|
|
|
complete: usize,
|
|
|
|
|
last_binding: Option<HashMap<String, geolog::id::Slid>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CountingObserver {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
iteration_starts: 0,
|
|
|
|
|
axiom_firings: 0,
|
|
|
|
|
axiom_fired: 0,
|
|
|
|
|
iteration_ends: 0,
|
|
|
|
|
complete: 0,
|
|
|
|
|
last_binding: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChaseObserver for CountingObserver {
|
|
|
|
|
fn on_event(&mut self, event: ChaseEvent<'_>) -> ChaseControl {
|
|
|
|
|
match event {
|
|
|
|
|
ChaseEvent::IterationStart { .. } => self.iteration_starts += 1,
|
|
|
|
|
ChaseEvent::AxiomFiring { binding, .. } => {
|
|
|
|
|
self.axiom_firings += 1;
|
|
|
|
|
self.last_binding = Some(binding.clone());
|
|
|
|
|
}
|
|
|
|
|
ChaseEvent::AxiomFired { .. } => self.axiom_fired += 1,
|
|
|
|
|
ChaseEvent::IterationEnd { .. } => self.iteration_ends += 1,
|
|
|
|
|
ChaseEvent::ChaseComplete { .. } => self.complete += 1,
|
|
|
|
|
}
|
|
|
|
|
ChaseControl::Continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_chase_with_observer() {
|
|
|
|
|
// Test that the observer-based chase works and receives events
|
|
|
|
|
let theory = preorder_theory();
|
|
|
|
|
let mut universe = Universe::new();
|
|
|
|
|
let mut structure = Structure::new(1);
|
|
|
|
|
|
|
|
|
|
// Add 2 elements
|
|
|
|
|
structure.add_element(&mut universe, 0);
|
|
|
|
|
structure.add_element(&mut universe, 0);
|
|
|
|
|
|
|
|
|
|
// Initialize relation with arity 2
|
|
|
|
|
structure.init_relations(&[2]);
|
|
|
|
|
|
|
|
|
|
let mut observer = CountingObserver::new();
|
|
|
|
|
|
|
|
|
|
let iterations = chase_fixpoint_with_observer(
|
|
|
|
|
&theory.axioms,
|
|
|
|
|
&theory.axiom_names,
|
|
|
|
|
&mut structure,
|
|
|
|
|
&mut universe,
|
|
|
|
|
&theory.signature,
|
|
|
|
|
100,
|
|
|
|
|
&mut observer,
|
|
|
|
|
).unwrap();
|
|
|
|
|
|
|
|
|
|
// Should complete with events
|
|
|
|
|
assert_eq!(iterations, 2);
|
|
|
|
|
assert!(observer.iteration_starts >= 1, "Should have at least 1 iteration start");
|
|
|
|
|
assert!(observer.axiom_firings >= 2, "Should have at least 2 axiom firings (reflexivity for 2 elements)");
|
|
|
|
|
assert_eq!(observer.complete, 1, "Should have exactly 1 completion event");
|
|
|
|
|
|
|
|
|
|
// Check that reflexive pairs were added
|
|
|
|
|
let relation = structure.get_relation(0);
|
|
|
|
|
assert_eq!(relation.len(), 2, "Should have 2 reflexive tuples");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An observer that stops after a fixed number of firings
|
|
|
|
|
struct StoppingObserver {
|
|
|
|
|
max_firings: usize,
|
|
|
|
|
firing_count: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StoppingObserver {
|
|
|
|
|
fn new(max_firings: usize) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
max_firings,
|
|
|
|
|
firing_count: 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChaseObserver for StoppingObserver {
|
|
|
|
|
fn on_event(&mut self, event: ChaseEvent<'_>) -> ChaseControl {
|
|
|
|
|
if let ChaseEvent::AxiomFiring { .. } = event {
|
|
|
|
|
self.firing_count += 1;
|
|
|
|
|
if self.firing_count >= self.max_firings {
|
|
|
|
|
return ChaseControl::Stop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ChaseControl::Continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_chase_observer_can_stop() {
|
|
|
|
|
// Test that returning ChaseControl::Stop halts the chase
|
|
|
|
|
let theory = preorder_theory();
|
|
|
|
|
let mut universe = Universe::new();
|
|
|
|
|
let mut structure = Structure::new(1);
|
|
|
|
|
|
|
|
|
|
// Add 5 elements - would normally produce 5 reflexive pairs
|
|
|
|
|
for _ in 0..5 {
|
|
|
|
|
structure.add_element(&mut universe, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize relation with arity 2
|
|
|
|
|
structure.init_relations(&[2]);
|
|
|
|
|
|
|
|
|
|
// Stop after just 2 firings
|
|
|
|
|
// Note: The stop happens when the 2nd firing event is received, before the 2nd tuple is added
|
|
|
|
|
// So we expect only 1 tuple (from the first firing)
|
|
|
|
|
let mut observer = StoppingObserver::new(2);
|
|
|
|
|
|
|
|
|
|
let _iterations = chase_fixpoint_with_observer(
|
|
|
|
|
&theory.axioms,
|
|
|
|
|
&theory.axiom_names,
|
|
|
|
|
&mut structure,
|
|
|
|
|
&mut universe,
|
|
|
|
|
&theory.signature,
|
|
|
|
|
100,
|
|
|
|
|
&mut observer,
|
|
|
|
|
).unwrap();
|
|
|
|
|
|
|
|
|
|
// Should have stopped early - only 1 tuple (the 2nd firing was interrupted before execution)
|
|
|
|
|
let relation = structure.get_relation(0);
|
|
|
|
|
assert!(relation.len() < 5, "Should have fewer than 5 tuples after early stop");
|
|
|
|
|
assert_eq!(observer.firing_count, 2, "Should have stopped after exactly 2 firings");
|
|
|
|
|
}
|