//! Chase algorithm for computing derived relations. //! //! The chase takes a structure and a set of axioms (sequents) and repeatedly //! applies the axioms until a fixpoint is reached. This is the standard database //! chase algorithm adapted for geometric logic. //! //! # Implementation //! //! This implementation uses the tensor subsystem to evaluate premises: //! 1. Compile premise to TensorExpr (handles existentials, conjunctions, etc.) //! 2. Materialize to get all satisfying variable assignments //! 3. For each assignment, fire the conclusion (add relations, create elements) //! //! This approach is strictly more powerful than query-based chase because //! the tensor system naturally handles existential quantification in premises //! via tensor contraction. //! //! # Supported Axiom Patterns //! //! **Premises** (anything the tensor system can compile): //! - Relations: `R(x,y)` //! - Conjunctions: `R(x,y), S(y,z)` //! - Existentials: `∃e. f(e) = x ∧ g(e) = y` //! - Equalities: `f(x) = y`, `f(x) = g(y)` //! - Disjunctions: `R(x) ∨ S(x)` //! //! **Conclusions**: //! - Relations: `⊢ R(x,y)` — add tuple to relation //! - Existentials: `⊢ ∃b. f(b) = y` — create element with function binding //! - Conjunctions: `⊢ R(x,y), f(x) = z` — multiple effects //! //! # Usage //! //! ```ignore //! use geolog::query::chase::chase_fixpoint; //! //! // Run chase to fixpoint //! let iterations = chase_fixpoint( //! &theory.theory.axioms, //! &mut structure, //! &mut universe, //! &theory.theory.signature, //! 100, //! )?; //! ``` use std::collections::HashMap; use crate::cc::{CongruenceClosure, EquationReason}; use crate::core::{DerivedSort, Formula, RelationStorage, Sequent, Signature, Structure, Term}; use crate::id::{NumericId, Slid}; use crate::tensor::{check_sequent, CheckResult}; use crate::universe::Universe; /// Error type for chase operations #[derive(Debug, Clone)] pub enum ChaseError { /// Unsupported formula in conclusion UnsupportedConclusion(String), /// Variable not bound UnboundVariable(String), /// Function conflict (different values for same input) FunctionConflict(String), /// Chase did not converge MaxIterationsExceeded(usize), /// Tensor compilation failed TensorCompilationFailed(String), } impl std::fmt::Display for ChaseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnsupportedConclusion(s) => write!(f, "Unsupported conclusion: {s}"), Self::UnboundVariable(s) => write!(f, "Unbound variable: {s}"), Self::FunctionConflict(s) => write!(f, "Function conflict: {s}"), Self::MaxIterationsExceeded(n) => write!(f, "Chase did not converge after {n} iterations"), Self::TensorCompilationFailed(s) => write!(f, "Tensor compilation failed: {s}"), } } } impl std::error::Error for ChaseError {} /// Variable binding: maps variable names to Slids pub type Binding = HashMap; /// Execute one step of the chase algorithm. /// /// Iterates over all axioms, evaluates premises using the tensor system, /// and fires conclusions for each satisfying assignment. /// /// Returns `true` if any changes were made. pub fn chase_step( axioms: &[Sequent], structure: &mut Structure, cc: &mut CongruenceClosure, universe: &mut Universe, sig: &Signature, ) -> Result { let mut changed = false; for axiom in axioms { changed |= fire_axiom(axiom, structure, cc, universe, sig)?; } Ok(changed) } /// Fire a single axiom: find violations using tensor system, fire conclusion only for violations. /// /// This is the key to correct chase semantics: we only create fresh elements when /// the tensor system confirms there is NO existing witness for the conclusion. fn fire_axiom( axiom: &Sequent, structure: &mut Structure, cc: &mut CongruenceClosure, universe: &mut Universe, sig: &Signature, ) -> Result { // Check the axiom - if compilation fails due to unsupported patterns, skip silently let violations = match check_sequent(axiom, structure, sig) { Ok(CheckResult::Satisfied) => { // Axiom is already satisfied - nothing to fire return Ok(false); } Ok(CheckResult::Violated(vs)) => vs, Err(_) => { // Tensor compilation failed (unsupported pattern) // Skip this axiom silently return Ok(false); } }; if violations.is_empty() { return Ok(false); } // Build index→Slid lookup for each context variable let index_to_slid: Vec> = axiom.context.vars.iter() .map(|(_, sort)| carrier_to_slid_vec(structure, sort)) .collect(); // Map from variable name to its position in the context let var_to_ctx_idx: HashMap<&str, usize> = axiom.context.vars.iter() .enumerate() .map(|(i, (name, _))| (name.as_str(), i)) .collect(); let mut changed = false; // Fire conclusion ONLY for violations (where premise holds but conclusion doesn't) for violation in violations { // Build binding from violation assignment let binding: Binding = violation.variable_names.iter() .enumerate() .filter_map(|(tensor_idx, var_name)| { let ctx_idx = var_to_ctx_idx.get(var_name.as_str())?; let slid_vec = &index_to_slid[*ctx_idx]; let tensor_val = violation.assignment.get(tensor_idx)?; let slid = slid_vec.get(*tensor_val)?; Some((var_name.clone(), *slid)) }) .collect(); // Fire conclusion with this binding match fire_conclusion(&axiom.conclusion, &binding, structure, cc, universe, sig) { Ok(c) => changed |= c, Err(_) => { // Unsupported conclusion pattern - skip this axiom silently return Ok(false); } } } Ok(changed) } /// Convert a carrier to a Vec of Slids for index→Slid lookup fn carrier_to_slid_vec(structure: &Structure, sort: &DerivedSort) -> Vec { match sort { DerivedSort::Base(sort_id) => { structure.carriers[*sort_id] .iter() .map(|u| Slid::from_usize(u as usize)) .collect() } DerivedSort::Product(_) => { // Product sorts: would need to enumerate all combinations // For now, return empty (these are rare in practice) vec![] } } } /// Fire a conclusion formula given a variable binding. /// Returns true if any changes were made. fn fire_conclusion( formula: &Formula, binding: &Binding, structure: &mut Structure, cc: &mut CongruenceClosure, universe: &mut Universe, sig: &Signature, ) -> Result { match formula { Formula::True => Ok(false), Formula::False => { // Contradiction - this shouldn't happen in valid chase Err(ChaseError::UnsupportedConclusion("False in conclusion".to_string())) } Formula::Rel(rel_id, term) => { // Add tuple to relation let tuple = eval_term_to_tuple(term, binding, structure)?; // Check if already present (using canonical representatives) let canonical_tuple: Vec = tuple.iter() .map(|&s| cc.canonical(s)) .collect(); // Check if a canonically-equivalent tuple exists let exists = structure.relations[*rel_id].iter().any(|existing| { if existing.len() != canonical_tuple.len() { return false; } existing.iter().zip(canonical_tuple.iter()).all(|(e, c)| { cc.canonical(*e) == *c }) }); if exists { return Ok(false); } structure.relations[*rel_id].insert(tuple); Ok(true) } Formula::Conj(formulas) => { let mut changed = false; for f in formulas { changed |= fire_conclusion(f, binding, structure, cc, universe, sig)?; } Ok(changed) } Formula::Disj(formulas) => { // Naive parallel chase: fire all disjuncts // (sound but potentially adds more facts than necessary) let mut changed = false; for f in formulas { changed |= fire_conclusion(f, binding, structure, cc, universe, sig)?; } Ok(changed) } Formula::Eq(left, right) => { fire_equality(left, right, binding, structure, cc, sig) } Formula::Exists(var_name, sort, body) => { fire_existential(var_name, sort, body, binding, structure, cc, universe, sig) } } } /// Evaluate a term to a tuple of Slids (for relation arguments) fn eval_term_to_tuple( term: &Term, binding: &Binding, structure: &Structure, ) -> Result, ChaseError> { match term { Term::Var(name, _) => { let slid = binding.get(name) .ok_or_else(|| ChaseError::UnboundVariable(name.clone()))?; Ok(vec![*slid]) } Term::Record(fields) => { let mut tuple = Vec::new(); for (_, field_term) in fields { tuple.extend(eval_term_to_tuple(field_term, binding, structure)?); } Ok(tuple) } Term::App(_, _) => { // Delegate to eval_term_to_slid which handles function application let result = eval_term_to_slid(term, binding, structure)?; Ok(vec![result]) } Term::Project(_, _) => { Err(ChaseError::UnsupportedConclusion( "Projection in relation argument".to_string() )) } } } /// Evaluate a term to a single Slid fn eval_term_to_slid( term: &Term, binding: &Binding, structure: &Structure, ) -> Result { match term { Term::Var(name, _) => { binding.get(name) .copied() .ok_or_else(|| ChaseError::UnboundVariable(name.clone())) } Term::App(func_idx, arg) => { let arg_slid = eval_term_to_slid(arg, binding, structure)?; let local_id = structure.sort_local_id(arg_slid); structure.get_function(*func_idx, local_id) .ok_or_else(|| ChaseError::UnboundVariable( format!("Function {} undefined at {:?}", func_idx, arg_slid) )) } Term::Project(base, field) => { let _base_slid = eval_term_to_slid(base, binding, structure)?; // Product projection - would need more structure info Err(ChaseError::UnsupportedConclusion( format!("Projection .{} not yet supported in chase", field) )) } Term::Record(_) => { Err(ChaseError::UnsupportedConclusion( "Record term in scalar position".to_string() )) } } } /// Fire an equality in conclusion: f(x) = y, x = y, etc. fn fire_equality( left: &Term, right: &Term, binding: &Binding, structure: &mut Structure, cc: &mut CongruenceClosure, sig: &Signature, ) -> Result { match (left, right) { // f(arg) = value (Term::App(func_idx, arg), value) | (value, Term::App(func_idx, arg)) => { let arg_slid = eval_term_to_slid(arg, binding, structure)?; let local_id = structure.sort_local_id(arg_slid); // Check if dealing with product codomain let func_info = &sig.functions[*func_idx]; match &func_info.codomain { DerivedSort::Base(_) => { // Simple codomain let value_slid = eval_term_to_slid(value, binding, structure)?; // Check if already defined if let Some(existing) = structure.get_function(*func_idx, local_id) { // Check if values are equal (using CC) if cc.are_equal(existing, value_slid) { return Ok(false); // Already set to equivalent value } // Function conflict: add equation to CC instead of error // (this is how we propagate equalities through functions) cc.add_equation(existing, value_slid, EquationReason::FunctionConflict { func_id: *func_idx, domain: arg_slid, }); return Ok(true); // Changed (added equation) } structure.define_function(*func_idx, arg_slid, value_slid) .map_err(|e| ChaseError::FunctionConflict(format!("{:?}", e)))?; Ok(true) } DerivedSort::Product(_fields) => { // Product codomain: f(x) = [field1: v1, ...] if let Term::Record(value_fields) = value { let codomain_values: Vec<(&str, Slid)> = value_fields.iter() .map(|(name, term)| { let slid = eval_term_to_slid(term, binding, structure)?; Ok((name.as_str(), slid)) }) .collect::, ChaseError>>()?; // Check if already defined if let Some(existing) = structure.get_function_product_codomain(*func_idx, local_id) { let all_match = codomain_values.iter().all(|(name, expected)| { existing.iter().any(|(n, v)| n == name && cc.are_equal(*v, *expected)) }); if all_match { return Ok(false); } return Err(ChaseError::FunctionConflict( format!("Function {} already defined at {:?} with different values", func_idx, arg_slid) )); } structure.define_function_product_codomain(*func_idx, arg_slid, &codomain_values) .map_err(|e| ChaseError::FunctionConflict(format!("{:?}", e)))?; Ok(true) } else { Err(ChaseError::UnsupportedConclusion( format!("Expected record for product codomain function, got {:?}", value) )) } } } } // x = y (variable equality) - add to congruence closure! (Term::Var(name1, _), Term::Var(name2, _)) => { let slid1 = binding.get(name1) .ok_or_else(|| ChaseError::UnboundVariable(name1.clone()))?; let slid2 = binding.get(name2) .ok_or_else(|| ChaseError::UnboundVariable(name2.clone()))?; // Check if already equal in CC if cc.are_equal(*slid1, *slid2) { Ok(false) // Already equivalent } else { // Add equation to congruence closure cc.add_equation(*slid1, *slid2, EquationReason::ChaseConclusion); Ok(true) // Changed! } } _ => Err(ChaseError::UnsupportedConclusion( format!("Unsupported equality pattern: {:?} = {:?}", left, right) )) } } /// Check if a formula is satisfied given a variable binding. /// This is used for witness search in existential conclusions. /// Uses CC for canonical relation lookups and equality checks. fn check_formula_satisfied( formula: &Formula, binding: &Binding, structure: &Structure, cc: &mut CongruenceClosure, ) -> bool { match formula { Formula::True => true, Formula::False => false, Formula::Rel(rel_id, term) => { // Check if the tuple is in the relation (using canonical representatives) if let Ok(tuple) = eval_term_to_tuple(term, binding, structure) { let canonical_tuple: Vec = tuple.iter() .map(|&s| cc.canonical(s)) .collect(); // Check if a canonically-equivalent tuple exists structure.relations[*rel_id].iter().any(|existing| { if existing.len() != canonical_tuple.len() { return false; } existing.iter().zip(canonical_tuple.iter()).all(|(e, c)| { cc.canonical(*e) == *c }) }) } else { false // Couldn't evaluate term (unbound variable) } } Formula::Conj(fs) => { fs.iter().all(|f| check_formula_satisfied(f, binding, structure, cc)) } Formula::Disj(fs) => { fs.iter().any(|f| check_formula_satisfied(f, binding, structure, cc)) } Formula::Eq(t1, t2) => { // Check if both terms evaluate to equivalent values (using CC) match (eval_term_to_slid(t1, binding, structure), eval_term_to_slid(t2, binding, structure)) { (Ok(s1), Ok(s2)) => cc.are_equal(s1, s2), _ => false // Couldn't evaluate (unbound variable or undefined function) } } Formula::Exists(inner_var, inner_sort, inner_body) => { // Check if any witness exists in the carrier let DerivedSort::Base(sort_idx) = inner_sort else { return false; // Product sorts not supported }; structure.carriers[*sort_idx].iter().any(|w_u64| { let witness = Slid::from_usize(w_u64 as usize); let mut extended = binding.clone(); extended.insert(inner_var.clone(), witness); check_formula_satisfied(inner_body, &extended, structure, cc) }) } } } /// Fire an existential in conclusion: ∃x:S. body /// This creates a new element if no witness exists. /// /// The algorithm: /// 1. Search the carrier of S for an existing witness w where body[x↦w] holds /// 2. If found, do nothing (witness exists) /// 3. If not found, create a fresh element w and fire body as conclusion with x↦w fn fire_existential( var_name: &str, sort: &DerivedSort, body: &Formula, binding: &Binding, structure: &mut Structure, cc: &mut CongruenceClosure, universe: &mut Universe, sig: &Signature, ) -> Result { let DerivedSort::Base(sort_idx) = sort else { return Err(ChaseError::UnsupportedConclusion( "Existential with product sort not yet supported".to_string() )); }; // Search for existing witness by checking if body is satisfied (using CC for canonical lookups) let carrier = &structure.carriers[*sort_idx]; let witness_found = carrier.iter().any(|elem_u64| { let elem_slid = Slid::from_usize(elem_u64 as usize); let mut extended_binding = binding.clone(); extended_binding.insert(var_name.to_string(), elem_slid); check_formula_satisfied(body, &extended_binding, structure, cc) }); if witness_found { return Ok(false); // Witness already exists, nothing to do } // No witness exists - create a fresh element let (new_elem, _) = structure.add_element(universe, *sort_idx); // Fire body as conclusion with the new element bound to var_name let mut extended_binding = binding.clone(); extended_binding.insert(var_name.to_string(), new_elem); // Use fire_conclusion to make the body true // This handles relations, equalities, conjunctions uniformly fire_conclusion(body, &extended_binding, structure, cc, universe, sig)?; Ok(true) } /// Run the chase algorithm until a fixpoint is reached, with congruence closure. /// /// Repeatedly applies [`chase_step`] and propagates equations until no more changes occur. /// /// # Arguments /// /// * `axioms` - The sequents (axioms) to apply /// * `structure` - The structure to modify /// * `cc` - Congruence closure for equality reasoning /// * `universe` - The universe for element creation /// * `sig` - The signature /// * `max_iterations` - Safety limit to prevent infinite loops /// /// # Returns /// /// The number of iterations taken to reach the fixpoint. pub fn chase_fixpoint_with_cc( axioms: &[Sequent], structure: &mut Structure, cc: &mut CongruenceClosure, universe: &mut Universe, sig: &Signature, max_iterations: usize, ) -> Result { let mut iterations = 0; loop { if iterations >= max_iterations { return Err(ChaseError::MaxIterationsExceeded(max_iterations)); } // Fire axiom conclusions let axiom_changed = chase_step(axioms, structure, cc, universe, sig)?; // Propagate pending equations in CC let eq_changed = propagate_equations(structure, cc, sig); iterations += 1; if !axiom_changed && !eq_changed { break; } } Ok(iterations) } /// Propagate pending equations in the congruence closure. /// /// This merges equivalence classes and detects function conflicts /// (which add new equations via congruence). fn propagate_equations( structure: &Structure, cc: &mut CongruenceClosure, sig: &Signature, ) -> bool { let mut changed = false; while let Some(eq) = cc.pop_pending() { // Merge the equivalence classes if cc.merge(eq.lhs, eq.rhs) { changed = true; // Check for function conflicts (congruence propagation) // If f(a) = x and f(b) = y, and a = b (just merged), then x = y for func_id in 0..sig.functions.len() { if func_id >= structure.functions.len() { continue; } let lhs_local = structure.sort_local_id(eq.lhs); let rhs_local = structure.sort_local_id(eq.rhs); let lhs_val = structure.get_function(func_id, lhs_local); let rhs_val = structure.get_function(func_id, rhs_local); if let (Some(lv), Some(rv)) = (lhs_val, rhs_val) && !cc.are_equal(lv, rv) { // Congruence: f(a) = lv, f(b) = rv, a = b implies lv = rv cc.add_equation(lv, rv, EquationReason::Congruence { func_id }); } } } } changed } /// Canonicalize the structure based on the congruence closure. /// /// After the chase, some elements may have been merged in the CC but the /// structure still contains distinct elements. This function: /// 1. Removes non-canonical elements from carriers /// 2. Replaces relation tuples with their canonical forms fn canonicalize_structure(structure: &mut Structure, cc: &mut CongruenceClosure) { use crate::core::{RelationStorage, VecRelation}; // 1. Canonicalize carriers: keep only canonical representatives for carrier in &mut structure.carriers { let elements: Vec = carrier.iter().collect(); carrier.clear(); for elem in elements { let slid = Slid::from_usize(elem as usize); let canonical = cc.canonical(slid); // Only keep if this element is its own canonical representative if canonical == slid { carrier.insert(elem); } } } // 2. Canonicalize relations: replace tuples with canonical forms for rel in &mut structure.relations { let canonical_tuples: Vec> = rel.iter() .map(|tuple| tuple.iter().map(|&s| cc.canonical(s)).collect()) .collect(); let arity = rel.arity(); let mut new_rel = VecRelation::new(arity); for tuple in canonical_tuples { new_rel.insert(tuple); } *rel = new_rel; } } /// Run the chase algorithm until a fixpoint is reached. /// /// This is a convenience wrapper that creates a fresh congruence closure. /// Use [`chase_fixpoint_with_cc`] if you need to provide your own CC. /// /// # Arguments /// /// * `axioms` - The sequents (axioms) to apply /// * `structure` - The structure to modify /// * `universe` - The universe for element creation /// * `sig` - The signature /// * `max_iterations` - Safety limit to prevent infinite loops /// /// # Returns /// /// The number of iterations taken to reach the fixpoint. pub fn chase_fixpoint( axioms: &[Sequent], structure: &mut Structure, universe: &mut Universe, sig: &Signature, max_iterations: usize, ) -> Result { let mut cc = CongruenceClosure::new(); let iterations = chase_fixpoint_with_cc(axioms, structure, &mut cc, universe, sig, max_iterations)?; // Canonicalize structure to reflect CC merges before returning canonicalize_structure(structure, &mut cc); Ok(iterations) } // Tests are in tests/unit_chase.rs