use super::*;
use std::cell::{Cell, RefCell};
#[derive(Clone, Debug)]
pub enum SignatureSource {
Dictionary,
Inferred,
Synthetic,
}
pub(super) fn is_synthetic_role_predicate(rel: &str) -> bool {
match rel.rsplit_once("_x") {
Some((base, suffix)) => {
!base.is_empty() && !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_digit())
}
None => false,
}
}
#[derive(Clone, Debug)]
pub struct PredicateSignature {
pub arity: usize,
pub source: SignatureSource,
pub arg_sorts: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct IntegrityConstraint {
pub label: String,
pub conjuncts: Vec<StoredFact>,
pub predicates: Vec<String>,
}
#[derive(Clone, Debug)]
pub(super) struct DisjunctiveConstraint {
pub(super) label: String,
pub(super) conditions: Vec<StoredFact>,
pub(super) disjuncts: Vec<Vec<StoredFact>>,
}
pub(super) fn check_constraints_for_predicate(
rel: &str,
inner: &KnowledgeBaseInner,
) -> Option<String> {
for constraint in &inner.integrity_constraints {
if !constraint.predicates.iter().any(|p| p == rel) {
continue;
}
let all_hold = constraint
.conjuncts
.iter()
.all(|c| inner.fact_store.contains(c));
if all_hold {
let facts: Vec<String> = constraint
.conjuncts
.iter()
.map(|c| c.to_display_string())
.collect();
return Some(format!(
"Integrity violation '{}': {} all hold simultaneously",
constraint.label,
facts.join(" ∧ ")
));
}
}
None
}
pub(super) fn get_node(buffer: &LogicBuffer, node_id: u32) -> Result<&LogicNode, String> {
buffer.nodes.get(node_id as usize).ok_or_else(|| {
format!(
"invalid node index {} (buffer has {} nodes)",
node_id,
buffer.nodes.len()
)
})
}
pub(super) const ABSTRACTION_MARKER_PREFIX: &str = "__abs_";
pub(super) fn is_abstraction_marker(buffer: &LogicBuffer, node_id: u32) -> bool {
matches!(
get_node(buffer, node_id),
Ok(LogicNode::Predicate((rel, _))) if rel.starts_with(ABSTRACTION_MARKER_PREFIX)
)
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GroundTerm {
Constant(String),
Number(u64),
Description(String),
Unspecified,
SkolemFn(String, Box<GroundTerm>),
DepPair(Box<GroundTerm>, Box<GroundTerm>),
PatternVar(String),
}
impl GroundTerm {
pub fn from_f64(v: f64) -> Self {
GroundTerm::Number(v.to_bits())
}
pub fn as_f64(&self) -> Option<f64> {
match self {
GroundTerm::Number(bits) => Some(f64::from_bits(*bits)),
_ => None,
}
}
pub fn to_display_string(&self) -> String {
match self {
GroundTerm::Constant(s) => s.clone(),
GroundTerm::Number(bits) => {
let v = f64::from_bits(*bits);
if v == v.floor() && v.abs() < 1e15 {
format!("{}", v as i64)
} else {
format!("{v}")
}
}
GroundTerm::Description(s) => format!("the {s}"),
GroundTerm::Unspecified => "_".to_string(),
GroundTerm::SkolemFn(name, dep) => {
format!("{name}({})", dep.to_display_string())
}
GroundTerm::DepPair(a, b) => {
format!("({}, {})", a.to_display_string(), b.to_display_string())
}
GroundTerm::PatternVar(s) => format!("?{s}"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GroundFact {
pub relation: String,
pub args: Vec<GroundTerm>,
}
impl GroundFact {
pub fn new(relation: impl Into<String>, args: Vec<GroundTerm>) -> Self {
GroundFact {
relation: relation.into(),
args,
}
}
pub fn to_display_string(&self) -> String {
if self.args.is_empty() {
self.relation.clone()
} else {
let args_str: Vec<String> = self.args.iter().map(|a| a.to_display_string()).collect();
format!("{}({})", self.relation, args_str.join(", "))
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StoredFact {
Bare(GroundFact),
Past(GroundFact),
Present(GroundFact),
Future(GroundFact),
Obligatory(GroundFact),
Permitted(GroundFact),
}
impl StoredFact {
pub fn relation(&self) -> &str {
match self {
StoredFact::Bare(f)
| StoredFact::Past(f)
| StoredFact::Present(f)
| StoredFact::Future(f)
| StoredFact::Obligatory(f)
| StoredFact::Permitted(f) => &f.relation,
}
}
pub fn inner(&self) -> &GroundFact {
match self {
StoredFact::Bare(f)
| StoredFact::Past(f)
| StoredFact::Present(f)
| StoredFact::Future(f)
| StoredFact::Obligatory(f)
| StoredFact::Permitted(f) => f,
}
}
pub fn with_tense_from(fact: GroundFact, source: &StoredFact) -> Self {
match source {
StoredFact::Bare(_) => StoredFact::Bare(fact),
StoredFact::Past(_) => StoredFact::Past(fact),
StoredFact::Present(_) => StoredFact::Present(fact),
StoredFact::Future(_) => StoredFact::Future(fact),
StoredFact::Obligatory(_) => StoredFact::Obligatory(fact),
StoredFact::Permitted(_) => StoredFact::Permitted(fact),
}
}
pub fn with_tense(fact: GroundFact, tense: Option<&str>) -> Self {
match tense {
Some("Past") => StoredFact::Past(fact),
Some("Present") => StoredFact::Present(fact),
Some("Future") => StoredFact::Future(fact),
Some("Obligatory") => StoredFact::Obligatory(fact),
Some("Permitted") => StoredFact::Permitted(fact),
_ => StoredFact::Bare(fact),
}
}
pub fn to_display_string(&self) -> String {
match self {
StoredFact::Bare(f) => f.to_display_string(),
StoredFact::Past(f) => format!("Past({})", f.to_display_string()),
StoredFact::Present(f) => format!("Present({})", f.to_display_string()),
StoredFact::Future(f) => format!("Future({})", f.to_display_string()),
StoredFact::Obligatory(f) => format!("Obligatory({})", f.to_display_string()),
StoredFact::Permitted(f) => format!("Permitted({})", f.to_display_string()),
}
}
}
pub fn unify_facts(
template: &StoredFact,
concrete: &StoredFact,
) -> Option<HashMap<String, GroundTerm>> {
let (t_inner, c_inner) = match (template, concrete) {
(StoredFact::Bare(t), StoredFact::Bare(c)) => (t, c),
(StoredFact::Past(t), StoredFact::Past(c)) => (t, c),
(StoredFact::Present(t), StoredFact::Present(c)) => (t, c),
(StoredFact::Future(t), StoredFact::Future(c)) => (t, c),
(StoredFact::Obligatory(t), StoredFact::Obligatory(c)) => (t, c),
(StoredFact::Permitted(t), StoredFact::Permitted(c)) => (t, c),
_ => return None,
};
if t_inner.relation != c_inner.relation {
return None;
}
if t_inner.args.len() != c_inner.args.len() {
return None;
}
let mut bindings = HashMap::new();
for (t_arg, c_arg) in t_inner.args.iter().zip(c_inner.args.iter()) {
if !unify_terms(t_arg, c_arg, &mut bindings) {
return None;
}
}
Some(bindings)
}
fn unify_terms(
template: &GroundTerm,
concrete: &GroundTerm,
bindings: &mut HashMap<String, GroundTerm>,
) -> bool {
match template {
GroundTerm::PatternVar(name) => {
if let Some(existing) = bindings.get(name) {
existing == concrete
} else {
bindings.insert(name.clone(), concrete.clone());
true
}
}
GroundTerm::Constant(a) => matches!(concrete, GroundTerm::Constant(b) if a == b),
GroundTerm::Number(a) => matches!(concrete, GroundTerm::Number(b) if a == b),
GroundTerm::Description(a) => matches!(concrete, GroundTerm::Description(b) if a == b),
GroundTerm::Unspecified => matches!(concrete, GroundTerm::Unspecified),
GroundTerm::SkolemFn(name_a, dep_a) => {
if let GroundTerm::SkolemFn(name_b, dep_b) = concrete {
name_a == name_b && unify_terms(dep_a, dep_b, bindings)
} else {
false
}
}
GroundTerm::DepPair(a1, a2) => {
if let GroundTerm::DepPair(b1, b2) = concrete {
unify_terms(a1, b1, bindings) && unify_terms(a2, b2, bindings)
} else {
false
}
}
}
}
pub fn substitute_fact(
template: &StoredFact,
bindings: &HashMap<String, GroundTerm>,
) -> StoredFact {
let sub_inner = |f: &GroundFact| -> GroundFact {
GroundFact {
relation: f.relation.clone(),
args: f
.args
.iter()
.map(|a| substitute_term(a, bindings).into_owned())
.collect(),
}
};
match template {
StoredFact::Bare(f) => StoredFact::Bare(sub_inner(f)),
StoredFact::Past(f) => StoredFact::Past(sub_inner(f)),
StoredFact::Present(f) => StoredFact::Present(sub_inner(f)),
StoredFact::Future(f) => StoredFact::Future(sub_inner(f)),
StoredFact::Obligatory(f) => StoredFact::Obligatory(sub_inner(f)),
StoredFact::Permitted(f) => StoredFact::Permitted(sub_inner(f)),
}
}
pub fn substitute_term<'a>(
term: &'a GroundTerm,
bindings: &HashMap<String, GroundTerm>,
) -> Cow<'a, GroundTerm> {
match term {
GroundTerm::PatternVar(name) => match bindings.get(name) {
Some(replacement) => Cow::Owned(replacement.clone()),
None => Cow::Borrowed(term),
},
GroundTerm::SkolemFn(name, dep) => {
let new_dep = substitute_term(dep, bindings);
match new_dep {
Cow::Borrowed(_) => Cow::Borrowed(term),
Cow::Owned(d) => Cow::Owned(GroundTerm::SkolemFn(name.clone(), Box::new(d))),
}
}
GroundTerm::DepPair(a, b) => {
let new_a = substitute_term(a, bindings);
let new_b = substitute_term(b, bindings);
match (&new_a, &new_b) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => Cow::Borrowed(term),
_ => Cow::Owned(GroundTerm::DepPair(
Box::new(new_a.into_owned()),
Box::new(new_b.into_owned()),
)),
}
}
_ => Cow::Borrowed(term),
}
}
#[derive(Clone)]
pub(super) struct SkolemFnEntry {
pub(super) base_name: String,
pub(super) dep_count: usize,
}
pub(super) const SKDEP_PREFIX: &str = "__skdep__";
pub(super) fn build_typed_rule_label(
conditions: &[StoredFact],
conclusions: &[StoredFact],
) -> String {
let conds: Vec<String> = conditions
.iter()
.map(|f| f.relation().to_string())
.collect();
let concls: Vec<String> = conclusions
.iter()
.map(|f| f.relation().to_string())
.collect();
if conds.is_empty() {
concls.join(" ∧ ")
} else {
format!("{} → {}", conds.join(" ∧ "), concls.join(" ∧ "))
}
}
#[derive(Clone)]
pub(super) struct NegatedExistsGroup {
pub(super) conditions: Vec<StoredFact>,
pub(super) event_var: String,
}
#[derive(Clone)]
pub(super) struct UniversalRuleRecord {
pub(super) label: String,
pub(super) typed_conditions: Vec<StoredFact>,
pub(super) typed_conclusions: Vec<StoredFact>,
pub(super) pattern_var_names: Vec<String>,
pub(super) negated_condition_indices: Vec<usize>,
pub(super) negated_exists_groups: Vec<NegatedExistsGroup>,
pub(super) forward: bool,
pub(super) priority: u32,
}
#[derive(Clone)]
pub(super) struct FactRecord {
pub(super) id: u64,
pub(super) buffer: LogicBuffer,
pub(super) label: String,
pub(super) retracted: bool,
}
pub(super) struct KnowledgeBaseInner {
pub(super) skolem_counter: usize,
pub(super) known_entities: HashSet<String>,
pub(super) known_event_entities: HashSet<String>,
pub(super) known_descriptions: HashSet<String>,
pub(super) known_numbers: HashSet<u64>,
pub(super) known_rules: HashSet<u64>,
pub(super) skolem_fn_registry: Vec<SkolemFnEntry>,
pub(super) fact_store: Box<dyn crate::fact_store::FactStore>,
pub(super) universal_rules: HashMap<String, Vec<Arc<UniversalRuleRecord>>>,
pub(super) fact_counter: u64,
pub(super) fact_registry: HashMap<u64, FactRecord>,
pub(super) rebuilding: bool,
pub(super) typed_domain_members_cache: Vec<GroundTerm>,
pub(super) typed_non_event_members_cache: Vec<GroundTerm>,
pub(super) domain_members_dirty: bool,
pub(super) max_chain_depth: usize,
pub(super) pred_dep_graph: HashMap<String, Vec<(String, bool)>>,
pub(super) equivalence_parent: HashMap<GroundTerm, GroundTerm>,
pub(super) equivalence_classes: HashMap<GroundTerm, Vec<GroundTerm>>,
pub(super) predicate_registry: HashMap<String, PredicateSignature>,
pub(super) integrity_constraints: Vec<IntegrityConstraint>,
pub(super) arg_position_index: HashMap<(String, usize), HashMap<GroundTerm, Vec<StoredFact>>>,
pub(super) rule_source_map: HashMap<u64, Vec<String>>,
pub(super) current_assertion_id: Option<u64>,
pub(super) forward_depth: usize,
pub(super) sort_hierarchy: HashMap<String, HashSet<String>>,
pub(super) entity_sorts: HashMap<String, String>,
pub(super) traced_predicates: HashSet<String>,
pub(super) negative_facts: HashSet<Vec<StoredFact>>,
pub(super) disjunctive_constraints: Vec<DisjunctiveConstraint>,
pub(super) cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
pub(super) compute_eval: Option<crate::compute::EvalFn>,
pub(super) compute_batch_eval: Option<crate::compute::BatchEvalFn>,
pub(super) pred_cache: RefCell<HashMap<StoredFact, QueryResult>>,
pub(super) pred_cache_enabled: Cell<bool>,
pub(super) depth_cut_table: RefCell<HashMap<StoredFact, usize>>,
pub(super) cycle_cut_epoch: Cell<u64>,
pub(super) du_variant_budget: Cell<Option<usize>>,
pub(super) verbose: bool,
pub(super) strict: bool,
pub(super) existential_import: bool,
pub(super) derived_only: HashSet<String>,
pub(super) admitted: HashSet<String>,
pub(super) presupposition_witnesses: HashSet<String>,
pub(super) strict_violations: Vec<String>,
pub(super) materialized: RefCell<Option<crate::materialize::Materialized>>,
pub(super) materialization: bool,
pub(super) positive_lookup: Cell<bool>,
pub(super) find_horizon_hit: bool,
}
impl Clone for KnowledgeBaseInner {
fn clone(&self) -> Self {
Self {
skolem_counter: self.skolem_counter,
known_entities: self.known_entities.clone(),
known_event_entities: self.known_event_entities.clone(),
known_descriptions: self.known_descriptions.clone(),
known_numbers: self.known_numbers.clone(),
known_rules: self.known_rules.clone(),
skolem_fn_registry: self.skolem_fn_registry.clone(),
fact_store: self.fact_store.clone_box(),
universal_rules: self.universal_rules.clone(),
fact_counter: self.fact_counter,
fact_registry: self.fact_registry.clone(),
rebuilding: false,
typed_domain_members_cache: self.typed_domain_members_cache.clone(),
typed_non_event_members_cache: self.typed_non_event_members_cache.clone(),
domain_members_dirty: self.domain_members_dirty,
max_chain_depth: self.max_chain_depth,
pred_dep_graph: self.pred_dep_graph.clone(),
equivalence_parent: self.equivalence_parent.clone(),
equivalence_classes: self.equivalence_classes.clone(),
predicate_registry: self.predicate_registry.clone(),
integrity_constraints: self.integrity_constraints.clone(),
arg_position_index: self.arg_position_index.clone(),
rule_source_map: self.rule_source_map.clone(),
current_assertion_id: None,
forward_depth: 0,
sort_hierarchy: self.sort_hierarchy.clone(),
entity_sorts: self.entity_sorts.clone(),
traced_predicates: self.traced_predicates.clone(),
negative_facts: self.negative_facts.clone(),
disjunctive_constraints: self.disjunctive_constraints.clone(),
cancel: self.cancel.clone(),
compute_eval: self.compute_eval,
compute_batch_eval: self.compute_batch_eval,
pred_cache: RefCell::new(HashMap::new()),
pred_cache_enabled: Cell::new(false),
depth_cut_table: RefCell::new(HashMap::new()),
cycle_cut_epoch: Cell::new(0),
du_variant_budget: Cell::new(None),
verbose: self.verbose,
strict: self.strict,
existential_import: self.existential_import,
derived_only: self.derived_only.clone(),
admitted: self.admitted.clone(),
strict_violations: Vec::new(),
presupposition_witnesses: self.presupposition_witnesses.clone(),
materialized: RefCell::new(None),
materialization: self.materialization,
positive_lookup: Cell::new(true),
find_horizon_hit: false,
}
}
}
impl KnowledgeBaseInner {
pub(super) fn new() -> Self {
Self {
skolem_counter: 0,
known_entities: HashSet::new(),
known_event_entities: HashSet::new(),
known_descriptions: HashSet::new(),
known_numbers: HashSet::new(),
known_rules: HashSet::new(),
skolem_fn_registry: Vec::new(),
fact_store: Box::new(crate::fact_store::InMemoryFactStore::new()),
universal_rules: HashMap::new(),
fact_counter: 0,
fact_registry: HashMap::new(),
rebuilding: false,
typed_domain_members_cache: Vec::new(),
typed_non_event_members_cache: Vec::new(),
domain_members_dirty: true,
max_chain_depth: 10,
pred_dep_graph: HashMap::new(),
equivalence_parent: HashMap::new(),
equivalence_classes: HashMap::new(),
predicate_registry: HashMap::new(),
integrity_constraints: Vec::new(),
arg_position_index: HashMap::new(),
rule_source_map: HashMap::new(),
current_assertion_id: None,
forward_depth: 0,
sort_hierarchy: HashMap::new(),
entity_sorts: HashMap::new(),
traced_predicates: HashSet::new(),
negative_facts: HashSet::new(),
disjunctive_constraints: Vec::new(),
cancel: None,
compute_eval: None,
compute_batch_eval: None,
pred_cache: RefCell::new(HashMap::new()),
pred_cache_enabled: Cell::new(false),
depth_cut_table: RefCell::new(HashMap::new()),
cycle_cut_epoch: Cell::new(0),
du_variant_budget: Cell::new(None),
verbose: false,
strict: false,
existential_import: true,
derived_only: HashSet::new(),
admitted: HashSet::new(),
strict_violations: Vec::new(),
presupposition_witnesses: HashSet::new(),
materialized: RefCell::new(None),
materialization: true,
positive_lookup: Cell::new(true),
find_horizon_hit: false,
}
}
pub(super) fn reset(&mut self) {
self.skolem_counter = 0;
self.known_entities.clear();
self.known_event_entities.clear();
self.known_descriptions.clear();
self.known_numbers.clear();
self.known_rules.clear();
self.skolem_fn_registry.clear();
self.fact_store.clear();
self.universal_rules.clear();
self.fact_counter = 0;
self.fact_registry.clear();
self.rebuilding = false;
self.typed_domain_members_cache.clear();
self.typed_non_event_members_cache.clear();
self.domain_members_dirty = true;
self.pred_dep_graph.clear();
self.equivalence_parent.clear();
self.equivalence_classes.clear();
self.predicate_registry.clear();
self.arg_position_index.clear();
self.rule_source_map.clear();
self.current_assertion_id = None;
self.forward_depth = 0;
self.negative_facts.clear();
self.disjunctive_constraints.clear();
self.presupposition_witnesses.clear();
self.derived_only.clear();
self.admitted.clear();
self.pred_cache.borrow_mut().clear();
self.pred_cache_enabled.set(false);
self.depth_cut_table.borrow_mut().clear();
*self.materialized.borrow_mut() = None;
}
#[inline]
pub(super) fn diag_enabled(&self) -> bool {
!self.rebuilding && self.verbose
}
pub(super) fn fresh_fact_id(&mut self) -> u64 {
let id = self.fact_counter;
self.fact_counter += 1;
id
}
pub(super) fn fresh_skolem(&mut self) -> String {
let sk = format!("sk_{}", self.skolem_counter);
self.skolem_counter += 1;
sk
}
pub(super) fn note_entity(&mut self, name: &str) {
if self.known_entities.insert(name.to_string()) {
self.domain_members_dirty = true;
}
}
pub(super) fn note_event_entity(&mut self, name: &str) {
if self.known_event_entities.insert(name.to_string()) {
self.domain_members_dirty = true;
}
}
pub(super) fn note_description(&mut self, name: &str) {
if self.known_descriptions.insert(name.to_string()) {
self.domain_members_dirty = true;
}
}
pub(super) fn note_number(&mut self, value: f64) {
if value.is_finite() && self.known_numbers.insert(value.to_bits()) {
self.domain_members_dirty = true;
}
}
pub(super) fn ensure_domain_members_cached(&mut self) {
if !self.domain_members_dirty {
return;
}
let mut typed_members = Vec::new();
let mut non_event_members = Vec::new();
for e in &self.known_entities {
let t = GroundTerm::Constant(e.clone());
typed_members.push(t.clone());
non_event_members.push(t);
}
for e in &self.known_event_entities {
typed_members.push(GroundTerm::Constant(e.clone()));
}
for d in &self.known_descriptions {
let t = GroundTerm::Description(d.clone());
typed_members.push(t.clone());
non_event_members.push(t);
}
for bits in &self.known_numbers {
let t = GroundTerm::Number(*bits);
typed_members.push(t.clone());
non_event_members.push(t);
}
typed_members.sort();
non_event_members.sort();
self.typed_domain_members_cache = typed_members;
self.typed_non_event_members_cache = non_event_members;
self.domain_members_dirty = false;
}
pub(super) fn all_typed_domain_members(&self) -> &[GroundTerm] {
&self.typed_domain_members_cache
}
pub(super) fn all_non_event_domain_members(&self) -> &[GroundTerm] {
&self.typed_non_event_members_cache
}
}
pub(super) fn find_canonical(
parent: &mut HashMap<GroundTerm, GroundTerm>,
term: &GroundTerm,
) -> GroundTerm {
let p = match parent.get(term) {
Some(p) => p.clone(),
None => return term.clone(),
};
if &p == term {
return p;
}
let root = find_canonical(parent, &p);
parent.insert(term.clone(), root.clone());
root
}
pub(super) fn find_canonical_readonly(
parent: &HashMap<GroundTerm, GroundTerm>,
term: &GroundTerm,
) -> GroundTerm {
let mut current = term.clone();
loop {
match parent.get(¤t) {
Some(p) if p != ¤t => current = p.clone(),
_ => return current,
}
}
}
pub(super) fn union_terms(inner: &mut KnowledgeBaseInner, a: &GroundTerm, b: &GroundTerm) {
let root_a = find_canonical(&mut inner.equivalence_parent, a);
let root_b = find_canonical(&mut inner.equivalence_parent, b);
if root_a == root_b {
return; }
let size_a = inner
.equivalence_classes
.get(&root_a)
.map_or(1, |c| c.len());
let size_b = inner
.equivalence_classes
.get(&root_b)
.map_or(1, |c| c.len());
let (winner, loser) = if size_a >= size_b {
(root_a, root_b)
} else {
(root_b, root_a)
};
inner
.equivalence_parent
.insert(loser.clone(), winner.clone());
let loser_class = inner
.equivalence_classes
.remove(&loser)
.unwrap_or_else(|| vec![loser.clone()]);
let winner_class = inner
.equivalence_classes
.entry(winner.clone())
.or_insert_with(|| vec![winner.clone()]);
winner_class.extend(loser_class);
}
pub(super) fn get_equivalence_class_readonly(
parent: &HashMap<GroundTerm, GroundTerm>,
classes: &HashMap<GroundTerm, Vec<GroundTerm>>,
term: &GroundTerm,
) -> Vec<GroundTerm> {
let canon = find_canonical_readonly(parent, term);
classes
.get(&canon)
.cloned()
.unwrap_or_else(|| vec![term.clone()])
}
pub(super) fn is_sort_compatible(
hierarchy: &HashMap<String, HashSet<String>>,
actual: &str,
expected: &str,
) -> bool {
if actual == expected {
return true;
}
let mut visited = HashSet::new();
let mut stack = vec![actual.to_string()];
while let Some(current) = stack.pop() {
if !visited.insert(current.clone()) {
continue;
}
if let Some(parents) = hierarchy.get(¤t) {
for parent in parents {
if parent == expected {
return true;
}
stack.push(parent.clone());
}
}
}
false
}
#[cfg_attr(
all(not(target_arch = "wasm32"), target_has_atomic = "ptr"),
doc = "WARNING: This type uses RefCell for interior mutability. \
It is NOT thread-safe. Use Arc<Mutex<KnowledgeBase>> for multi-threaded contexts."
)]
pub struct KnowledgeBase {
pub(super) inner: RefCell<KnowledgeBaseInner>,
}
pub(super) struct GroundTermCartesianProduct<'a> {
terms: &'a [GroundTerm],
dep_count: usize,
indices: Vec<usize>,
done: bool,
}
impl<'a> GroundTermCartesianProduct<'a> {
pub(super) fn new(terms: &'a [GroundTerm], dep_count: usize) -> Self {
let done = dep_count > 0 && terms.is_empty();
Self {
terms,
dep_count,
indices: vec![0; dep_count],
done,
}
}
}
impl<'a> Iterator for GroundTermCartesianProduct<'a> {
type Item = Vec<GroundTerm>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
if self.dep_count == 0 {
self.done = true;
return Some(vec![]);
}
let combo: Vec<GroundTerm> = self
.indices
.iter()
.map(|&i| self.terms[i].clone())
.collect();
let mut carry = true;
for i in (0..self.dep_count).rev() {
if carry {
self.indices[i] += 1;
if self.indices[i] >= self.terms.len() {
self.indices[i] = 0;
} else {
carry = false;
}
}
}
if carry {
self.done = true;
}
Some(combo)
}
}
pub(super) fn clear_and_enable_pred_cache(inner: &KnowledgeBaseInner) {
clear_typed_pred_cache(inner);
inner.pred_cache_enabled.set(true);
}
pub(super) fn enable_pred_cache(inner: &KnowledgeBaseInner) {
inner.pred_cache_enabled.set(true);
}
pub(super) fn invalidate_pred_cache(inner: &KnowledgeBaseInner) {
clear_typed_pred_cache(inner);
invalidate_materialization(inner);
inner.pred_cache_enabled.set(false);
}
fn tensed_body_hides_conditional(buffer: &LogicBuffer, node_id: u32) -> bool {
let Ok(node) = get_node(buffer, node_id) else {
return false;
};
match node {
LogicNode::ExistsNode((_, body)) => tensed_body_hides_conditional(buffer, *body),
LogicNode::PastNode(n)
| LogicNode::PresentNode(n)
| LogicNode::FutureNode(n)
| LogicNode::ObligatoryNode(n)
| LogicNode::PermittedNode(n) => tensed_body_hides_conditional(buffer, *n),
LogicNode::AndNode((l, r)) => {
tensed_body_hides_conditional(buffer, *l) || tensed_body_hides_conditional(buffer, *r)
}
LogicNode::OrNode((l, r)) => {
matches!(get_node(buffer, *l), Ok(LogicNode::NotNode(_)))
|| matches!(get_node(buffer, *r), Ok(LogicNode::NotNode(_)))
}
_ => false,
}
}
pub(super) fn register_ground_material_conditional(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
) -> Result<bool, String> {
let Ok(node) = get_node(buffer, node_id) else {
return Ok(false);
};
let registered = match node {
LogicNode::ExistsNode((v, body)) if subs.contains_key(v.as_str()) => {
register_ground_material_conditional(buffer, *body, subs, inner)?
}
LogicNode::PastNode(n)
| LogicNode::PresentNode(n)
| LogicNode::FutureNode(n)
| LogicNode::ObligatoryNode(n)
| LogicNode::PermittedNode(n) => {
if tensed_body_hides_conditional(buffer, *n) {
return Err(
"cannot register a tense (past/now/future) or deontic (must/may) \
wrapping a ground material conditional: a timeless backward-chaining \
rule cannot carry whole-rule tense or modality without over-claiming \
on untensed facts. Rejecting the assertion to preserve soundness; \
restate the temporal/deontic scope on the relevant predicate instead."
.to_string(),
);
}
register_ground_material_conditional(buffer, *n, subs, inner)?
}
LogicNode::AndNode((l, r)) => {
let left = register_ground_material_conditional(buffer, *l, subs, inner)?;
let right = register_ground_material_conditional(buffer, *r, subs, inner)?;
left || right
}
LogicNode::OrNode((l, r)) => {
if matches!(get_node(buffer, *l), Ok(LogicNode::NotNode(_))) {
compile_forall_to_rule(buffer, node_id, subs, inner)?;
true
}
else if matches!(get_node(buffer, *r), Ok(LogicNode::NotNode(_))) {
let mut swapped = buffer.clone();
swapped.nodes.push(LogicNode::OrNode((*r, *l)));
let swapped_id = (swapped.nodes.len() - 1) as u32;
compile_forall_to_rule(&swapped, swapped_id, subs, inner)?;
true
} else {
false
}
}
_ => false,
};
Ok(registered)
}
fn contains_count_node(buffer: &LogicBuffer, node_id: u32) -> bool {
let Ok(node) = get_node(buffer, node_id) else {
return false;
};
match node {
LogicNode::CountNode(_) => true,
LogicNode::AndNode((l, r)) | LogicNode::OrNode((l, r)) => {
contains_count_node(buffer, *l) || contains_count_node(buffer, *r)
}
LogicNode::NotNode(n)
| LogicNode::ExistsNode((_, n))
| LogicNode::ForAllNode((_, n))
| LogicNode::PastNode(n)
| LogicNode::PresentNode(n)
| LogicNode::FutureNode(n)
| LogicNode::ObligatoryNode(n)
| LogicNode::PermittedNode(n) => contains_count_node(buffer, *n),
_ => false,
}
}
fn root_reduces_to_negation(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) -> bool {
let Ok(node) = get_node(buffer, node_id) else {
return false;
};
match node {
LogicNode::NotNode(_) => true,
LogicNode::PastNode(n)
| LogicNode::PresentNode(n)
| LogicNode::FutureNode(n)
| LogicNode::ObligatoryNode(n)
| LogicNode::PermittedNode(n) => root_reduces_to_negation(buffer, *n, subs),
LogicNode::ExistsNode((v, body)) if subs.contains_key(v.as_str()) => {
root_reduces_to_negation(buffer, *body, subs)
}
_ => false,
}
}
fn all_conjuncts_reduce_to_negation(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) -> bool {
let Ok(node) = get_node(buffer, node_id) else {
return false;
};
match node {
LogicNode::AndNode((l, r)) if !is_abstraction_marker(buffer, *l) => {
all_conjuncts_reduce_to_negation(buffer, *l, subs)
&& all_conjuncts_reduce_to_negation(buffer, *r, subs)
}
LogicNode::ExistsNode((v, body)) if subs.contains_key(v.as_str()) => {
all_conjuncts_reduce_to_negation(buffer, *body, subs)
}
_ => match find_negation_body(buffer, node_id, subs, None) {
Some((body, _)) => negation_body_purely_representable(buffer, body, subs),
None => false,
},
}
}
fn negation_body_purely_representable(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) -> bool {
let Ok(node) = get_node(buffer, node_id) else {
return false;
};
match node {
LogicNode::AndNode((l, r)) => {
if is_abstraction_marker(buffer, *l) {
true
} else {
negation_body_purely_representable(buffer, *l, subs)
&& negation_body_purely_representable(buffer, *r, subs)
}
}
LogicNode::ExistsNode((v, body)) => {
subs.contains_key(v.as_str()) && negation_body_purely_representable(buffer, *body, subs)
}
LogicNode::PastNode(n)
| LogicNode::PresentNode(n)
| LogicNode::FutureNode(n)
| LogicNode::ObligatoryNode(n)
| LogicNode::PermittedNode(n) => negation_body_purely_representable(buffer, *n, subs),
LogicNode::Predicate(_) => true,
_ => false,
}
}
fn record_negative_conjuncts(
inner: &mut KnowledgeBaseInner,
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::AndNode((l, r)) => {
if !is_abstraction_marker(buffer, *l) {
record_negative_conjuncts(inner, buffer, *l, subs);
record_negative_conjuncts(inner, buffer, *r, subs);
}
}
LogicNode::ExistsNode((v, body)) if subs.contains_key(v.as_str()) => {
record_negative_conjuncts(inner, buffer, *body, subs)
}
_ => {
if root_reduces_to_negation(buffer, node_id, subs) {
record_negative_ground_fact(inner, buffer, node_id, subs);
}
}
}
}
fn find_negation_body(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
tense: Option<&'static str>,
) -> Option<(u32, Option<&'static str>)> {
let node = get_node(buffer, node_id).ok()?;
match node {
LogicNode::NotNode(body) => Some((*body, tense)),
LogicNode::PastNode(n) => find_negation_body(buffer, *n, subs, Some("Past")),
LogicNode::PresentNode(n) => find_negation_body(buffer, *n, subs, Some("Present")),
LogicNode::FutureNode(n) => find_negation_body(buffer, *n, subs, Some("Future")),
LogicNode::ObligatoryNode(n) | LogicNode::PermittedNode(n) => {
find_negation_body(buffer, *n, subs, tense)
}
LogicNode::ExistsNode((v, body)) if subs.contains_key(v.as_str()) => {
find_negation_body(buffer, *body, subs, tense)
}
_ => None,
}
}
pub(super) fn record_negative_ground_fact(
inner: &mut KnowledgeBaseInner,
buffer: &LogicBuffer,
root_id: u32,
skolem_subs: &HashMap<String, GroundTerm>,
) {
let Some((body_id, tense)) = find_negation_body(buffer, root_id, skolem_subs, None) else {
return;
};
if !negation_body_purely_representable(buffer, body_id, skolem_subs) {
return;
}
let mut leaves = Vec::new();
collect_ground_facts(buffer, body_id, skolem_subs, tense, &mut leaves);
if leaves.is_empty() {
return;
}
let mut event_var_map: HashMap<String, String> = HashMap::new();
let templates: Vec<StoredFact> = leaves
.iter()
.map(|f| generalize_event_args(f, &inner.known_event_entities, &mut event_var_map))
.collect();
inner.negative_facts.insert(templates);
}
fn generalize_event_args(
fact: &StoredFact,
event_entities: &HashSet<String>,
event_var_map: &mut HashMap<String, String>,
) -> StoredFact {
let gf = fact.inner();
let args: Vec<GroundTerm> = gf
.args
.iter()
.map(|arg| match arg {
GroundTerm::Constant(c) if event_entities.contains(c.as_str()) => {
let next_idx = event_var_map.len();
let pvar = event_var_map
.entry(c.clone())
.or_insert_with(|| format!("__neg_ev{next_idx}"))
.clone();
GroundTerm::PatternVar(pvar)
}
other => other.clone(),
})
.collect();
StoredFact::with_tense_from(GroundFact::new(gf.relation.clone(), args), fact)
}
pub(super) fn negative_group_holds(
templates: &[StoredFact],
store: &dyn crate::fact_store::FactStore,
) -> bool {
fn solve(
templates: &[StoredFact],
idx: usize,
bindings: &HashMap<String, GroundTerm>,
store: &dyn crate::fact_store::FactStore,
) -> bool {
let Some(template) = templates.get(idx) else {
return true; };
let bound = substitute_fact(template, bindings);
let Some(candidates) = store.lookup_predicate(bound.relation()) else {
return false;
};
for fact in candidates {
if let Some(new_bindings) = unify_facts(&bound, fact) {
let mut merged = bindings.clone();
merged.extend(new_bindings);
if solve(templates, idx + 1, &merged, store) {
return true;
}
}
}
false
}
solve(templates, 0, &HashMap::new(), store)
}
pub(super) fn solve_group_bindings(
templates: &[StoredFact],
store: &dyn crate::fact_store::FactStore,
) -> Vec<HashMap<String, GroundTerm>> {
fn solve(
templates: &[StoredFact],
idx: usize,
bindings: &HashMap<String, GroundTerm>,
store: &dyn crate::fact_store::FactStore,
out: &mut Vec<HashMap<String, GroundTerm>>,
) {
let Some(template) = templates.get(idx) else {
out.push(bindings.clone());
return;
};
let bound = substitute_fact(template, bindings);
let Some(candidates) = store.lookup_predicate(bound.relation()) else {
return;
};
for fact in candidates {
if let Some(new_bindings) = unify_facts(&bound, fact) {
let mut merged = bindings.clone();
merged.extend(new_bindings);
solve(templates, idx + 1, &merged, store, out);
}
}
}
let mut out = Vec::new();
solve(templates, 0, &HashMap::new(), store, &mut out);
out
}
fn neg_group_covers(templates: &[StoredFact], facts: &[StoredFact]) -> bool {
fn solve(
templates: &[StoredFact],
idx: usize,
bindings: &HashMap<String, GroundTerm>,
facts: &[StoredFact],
) -> bool {
let Some(template) = templates.get(idx) else {
return true;
};
let bound = substitute_fact(template, bindings);
for fact in facts {
if bound.relation() != fact.relation() {
continue;
}
if let Some(new_bindings) = unify_facts(&bound, fact) {
let mut merged = bindings.clone();
merged.extend(new_bindings);
if solve(templates, idx + 1, &merged, facts) {
return true;
}
}
}
false
}
solve(templates, 0, &HashMap::new(), facts)
}
pub(super) fn disjunct_explicitly_denied(
disjunct: &[StoredFact],
negative_facts: &HashSet<Vec<StoredFact>>,
) -> bool {
negative_facts
.iter()
.any(|neg_group| neg_group_covers(neg_group, disjunct))
}
fn node_is_forall_through_tense(buffer: &LogicBuffer, node_id: u32) -> bool {
let mut current = node_id;
loop {
match get_node(buffer, current) {
Ok(LogicNode::ForAllNode(_)) => return true,
Ok(LogicNode::PastNode(n))
| Ok(LogicNode::PresentNode(n))
| Ok(LogicNode::FutureNode(n))
| Ok(LogicNode::ObligatoryNode(n))
| Ok(LogicNode::PermittedNode(n)) => current = *n,
_ => return false,
}
}
}
fn leading_skolemized_exists_over_forall(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) -> Option<u32> {
let mut current = node_id;
let mut peeled = false;
loop {
match get_node(buffer, current) {
Ok(LogicNode::ExistsNode((v, body))) => match subs.get(v.as_str()) {
Some(gt) if !is_skdep(gt) => {
peeled = true;
current = *body;
}
_ => return None,
},
Ok(LogicNode::ForAllNode(_)) if peeled => return Some(current),
_ => return None,
}
}
}
fn tense_wraps_skolemized_exists_over_forall(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
) -> bool {
let mut current = node_id;
let mut saw_tense = false;
loop {
match get_node(buffer, current) {
Ok(LogicNode::PastNode(n))
| Ok(LogicNode::PresentNode(n))
| Ok(LogicNode::FutureNode(n))
| Ok(LogicNode::ObligatoryNode(n))
| Ok(LogicNode::PermittedNode(n)) => {
saw_tense = true;
current = *n;
}
_ => {
return saw_tense
&& leading_skolemized_exists_over_forall(buffer, current, subs).is_some();
}
}
}
}
pub(super) fn process_assertion(
inner: &mut KnowledgeBaseInner,
logic: &LogicBuffer,
) -> Result<(), String> {
inner.strict_violations.clear();
for &root_id in &logic.roots {
if root_id as usize >= logic.nodes.len() {
eprintln!(
"[Warning] skipping invalid root index {} (buffer has {} nodes)",
root_id,
logic.nodes.len()
);
continue;
}
let mut skolem_subs = HashMap::new();
let mut enclosing_universals = Vec::new();
collect_exists_for_skolem(
logic,
root_id,
&mut skolem_subs,
&mut enclosing_universals,
&mut inner.skolem_counter,
);
if inner.diag_enabled() && !skolem_subs.is_empty() {
let mut entries: Vec<(&String, &GroundTerm)> = skolem_subs.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
let mapping: Vec<String> = entries
.iter()
.map(|(v, gt)| {
if let Some(base) = skdep_base_name(gt) {
format!("{} ↦ {}(∀-dependent)", v, base)
} else {
format!("{} ↦ {}", v, gt.to_display_string())
}
})
.collect();
println!(
"[Skolem] {} variable(s) → {}",
skolem_subs.len(),
mapping.join(", ")
);
}
for (var, gt) in &skolem_subs {
if !is_skdep(gt) {
if let GroundTerm::Constant(sk) = gt {
if var.starts_with("_ev") {
inner.note_event_entity(sk);
} else {
inner.note_entity(sk);
}
}
}
}
collect_and_note_constants(logic, root_id, inner);
let is_forall = node_is_forall_through_tense(logic, root_id);
if is_forall {
compile_forall_to_rule(logic, root_id, &skolem_subs, inner)?;
} else if let Some(inner_forall_id) =
leading_skolemized_exists_over_forall(logic, root_id, &skolem_subs)
{
compile_forall_to_rule(logic, inner_forall_id, &skolem_subs, inner)?;
} else if tense_wraps_skolemized_exists_over_forall(logic, root_id, &skolem_subs) {
return Err(
"cannot compile a tense (past/now/future) or deontic (must/may) \
wrapping a whole universal/conditional rule: a timeless \
backward-chaining rule cannot carry whole-rule tense or \
modality without over-claiming on untensed facts. Rejecting \
the assertion to preserve soundness; restate the \
temporal/deontic scope on the relevant predicate instead."
.to_string(),
);
} else {
let mut typed_leaves = Vec::new();
collect_ground_facts(logic, root_id, &skolem_subs, None, &mut typed_leaves);
if let Some(rel) = asserted_numeric_comparison(&typed_leaves) {
return Err(format!(
"`{rel}` over numeric literals is a computed comparison, not an \
assertable fact: the engine evaluates it at query time and the \
computed value always wins, so an asserted fact could never be \
consulted. (A non-numeric comparison like `la .alis. cu zmadu \
la .bob.` is a relational fact and asserts normally.)"
));
}
if let Some(rel) = asserted_derived_only(&typed_leaves, &inner.derived_only) {
return Err(format!(
"`{rel}` is declared derived-only (`derived_only(\"{rel}\")`): it can be \
concluded by a rule but never asserted directly. Assert the facts its \
rule derives from instead — or, if this relation really should be a base \
fact in this model, remove its `derived_only` declaration (a visible, \
reviewable edit, which is the point)."
));
}
if let Some(rel) = asserted_unadmitted(&typed_leaves, &inner.admitted) {
return Err(format!(
"`{rel}` is not admitted vocabulary: this knowledge base declared its \
base vocabulary closed with `admits(\"…\")`, and `{rel}` is not in it. \
Add `admits(\"{rel}\")` ABOVE the first `{rel}` assertion if this \
relation really belongs in the record — a visible, reviewable edit, \
which is the point."
));
}
let nothing_collected = typed_leaves.is_empty();
let mut pending_declarations: Vec<String> = Vec::new();
let mut pending_admits: Vec<String> = Vec::new();
for fact in &typed_leaves {
if let StoredFact::Bare(gf) = fact {
if gf.relation == nibli_types::relations::IDENTITY && gf.args.len() == 2 {
union_terms(inner, &gf.args[0], &gf.args[1]);
}
}
if let StoredFact::Bare(gf) = fact
&& gf.relation == DERIVED_ONLY_ROLE
&& let Some(GroundTerm::Constant(rel)) = gf.args.get(1)
{
pending_declarations.push(rel.clone());
}
if let StoredFact::Bare(gf) = fact
&& gf.relation == ADMITS_ROLE
&& let Some(GroundTerm::Constant(rel)) = gf.args.get(1)
{
pending_admits.push(rel.clone());
}
}
for rel in &pending_declarations {
if inner.derived_only.contains(rel) {
continue; }
if inner
.fact_store
.lookup_predicate(rel)
.is_some_and(|s| !s.is_empty())
{
return Err(format!(
"`derived_only(\"{rel}\")` comes too late: `{rel}` has already been \
asserted in this knowledge base, so the declaration would silently \
protect nothing. Move it ABOVE the first `{rel}` assertion — or, if \
those assertions are the mistake, remove them."
));
}
}
for rel in pending_declarations {
inner.derived_only.insert(rel);
}
if !pending_admits.is_empty() && inner.admitted.is_empty() {
if let Some(rel) = first_non_declaration_relation(inner) {
return Err(format!(
"`admits(\"{}\")` comes too late: `{rel}` was already asserted, so \
closing the vocabulary here would silently admit it along with \
everything else above. Move the whole `admits` block ABOVE the \
first ordinary assertion.",
pending_admits[0]
));
}
}
for rel in pending_admits {
inner.admitted.insert(rel);
}
for fact in typed_leaves {
assert_typed_fact(fact, inner);
}
let registered =
register_ground_material_conditional(logic, root_id, &skolem_subs, inner)?;
if nothing_collected
&& !registered
&& !all_conjuncts_reduce_to_negation(logic, root_id, &skolem_subs)
&& !contains_count_node(logic, root_id)
{
return Err(
"assertion has no representable content: a bare disjunction, an \
exclusive-or, or a negation whose body is not a plain conjunction \
of positive facts ingests no facts and registers no rules. \
Rejecting to preserve soundness rather than reporting it as \
asserted (querying it back would return False)."
.to_string(),
);
}
record_negative_conjuncts(inner, logic, root_id, &skolem_subs);
}
generate_count_extra_witnesses(logic, root_id, &skolem_subs, inner);
}
if !inner.strict_violations.is_empty() {
let joined = inner.strict_violations.drain(..).collect::<Vec<_>>();
return Err(format!("strict mode rejected: {}", joined.join("; ")));
}
Ok(())
}
pub(super) const DERIVED_ONLY: &str = "derived_only";
pub(super) const DERIVED_ONLY_ROLE: &str = "derived_only_x1";
pub(super) const ADMITS: &str = "admits";
pub(super) const ADMITS_ROLE: &str = "admits_x1";
fn asserted_derived_only(leaves: &[StoredFact], closed: &HashSet<String>) -> Option<String> {
if closed.is_empty() {
return None;
}
leaves.iter().find_map(|f| {
let gf = f.inner();
(gf.relation != DERIVED_ONLY && closed.contains(gf.relation.as_str()))
.then(|| gf.relation.clone())
})
}
fn asserted_unadmitted(leaves: &[StoredFact], admitted: &HashSet<String>) -> Option<String> {
if admitted.is_empty() {
return None;
}
leaves.iter().find_map(|f| {
let gf = f.inner();
let rel = gf.relation.as_str();
(rel != ADMITS
&& rel != DERIVED_ONLY
&& !admitted.contains(rel)
&& crate::materialize::surface_relation(rel) == rel)
.then(|| gf.relation.clone())
})
}
fn first_non_declaration_relation(inner: &KnowledgeBaseInner) -> Option<String> {
let mut found: Option<String> = None;
for f in inner.fact_store.all_facts() {
let rel = f.inner().relation.clone();
let base = crate::materialize::surface_relation(&rel).to_string();
if base == ADMITS || base == DERIVED_ONLY {
continue;
}
if found.as_deref().is_none_or(|cur| base.as_str() < cur) {
found = Some(base);
}
}
found
}
fn asserted_numeric_comparison(leaves: &[StoredFact]) -> Option<&'static str> {
const CMP: [&str; 3] = ["greater", "less", "num_equal"];
let is_num = |t: &GroundTerm| matches!(t, GroundTerm::Number(_));
for f in leaves {
let gf = f.inner();
if gf.args.len() >= 2 && is_num(&gf.args[0]) && is_num(&gf.args[1]) {
if let Some(rel) = CMP.iter().copied().find(|&c| c == gf.relation.as_str()) {
return Some(rel);
}
}
}
for &base in &CMP {
let (x1, x2) = (format!("{base}_x1"), format!("{base}_x2"));
for a in leaves {
let ga = a.inner();
if ga.relation == x1
&& ga.args.len() == 2
&& is_num(&ga.args[1])
&& leaves.iter().any(|b| {
let gb = b.inner();
gb.relation == x2
&& gb.args.len() == 2
&& gb.args[0] == ga.args[0]
&& is_num(&gb.args[1])
})
{
return Some(base);
}
}
}
None
}
pub(super) fn collect_entailment_candidates(
buffer: &LogicBuffer,
body_id: u32,
var_name: &str,
subs: &HashMap<String, GroundTerm>,
inner: &KnowledgeBaseInner,
tense: Option<&str>,
) -> Option<Vec<GroundTerm>> {
let mut anchors = Vec::new();
collect_mandatory_anchors(buffer, body_id, var_name, subs, tense, &mut anchors);
let mut compute_heads: HashSet<&str> = HashSet::new();
collect_compute_heads(buffer, body_id, &mut compute_heads);
if !compute_heads.is_empty() {
anchors
.retain(|a| !compute_heads.contains(crate::materialize::surface_relation(&a.relation)));
}
if anchors.is_empty() {
return None;
}
let members: Vec<GroundTerm> = inner.all_typed_domain_members().to_vec();
let mut best: Option<HashSet<GroundTerm>> = None;
for anchor in &anchors {
let mut candidates = HashSet::new();
extract_from_index(anchor, inner, &mut candidates);
extract_rule_candidates_for_entailment(anchor, inner, &members, &mut candidates);
candidates.remove(&GroundTerm::Unspecified);
match &best {
None => best = Some(candidates),
Some(prev) if candidates.len() < prev.len() => best = Some(candidates),
_ => {}
}
}
best.map(|set| {
let mut candidates: Vec<GroundTerm> = set.into_iter().collect();
candidates.sort();
candidates
})
}
pub(crate) fn is_non_indexable_relation(rel: &str) -> bool {
let base = crate::materialize::surface_relation(rel);
nibli_types::relations::is_identity(base)
|| nibli_types::relations::is_numeric_comparison(base)
|| nibli_types::relations::is_builtin_arithmetic(base)
}
pub(super) fn collect_group_event_candidates(
conditions: &[StoredFact],
event_var: &str,
inner: &KnowledgeBaseInner,
) -> Option<Vec<GroundTerm>> {
let members: Vec<GroundTerm> = inner.all_typed_domain_members().to_vec();
let mut best: Option<HashSet<GroundTerm>> = None;
for cond in conditions {
let gf = cond.inner();
if is_non_indexable_relation(&gf.relation) {
continue;
}
let positions: Vec<usize> = gf
.args
.iter()
.enumerate()
.filter(|(_, a)| matches!(a, GroundTerm::PatternVar(s) if s == event_var))
.map(|(i, _)| i)
.collect();
if positions.len() != 1 {
continue;
}
let anchor = PredicateAnchor {
relation: gf.relation.clone(),
var_position: positions[0],
args: vec![LogicalTerm::Unspecified; gf.args.len()],
tense: match cond {
StoredFact::Past(_) => Some("Past"),
StoredFact::Present(_) => Some("Present"),
StoredFact::Future(_) => Some("Future"),
_ => None,
},
};
let mut candidates = HashSet::new();
extract_from_index(&anchor, inner, &mut candidates);
extract_rule_candidates_for_entailment(&anchor, inner, &members, &mut candidates);
candidates.remove(&GroundTerm::Unspecified);
match &best {
None => best = Some(candidates),
Some(prev) if candidates.len() < prev.len() => best = Some(candidates),
_ => {}
}
}
best.map(|set| {
let mut v: Vec<GroundTerm> = set.into_iter().collect();
v.sort();
v
})
}
fn collect_mandatory_anchors(
buffer: &LogicBuffer,
node_id: u32,
var_name: &str,
subs: &HashMap<String, GroundTerm>,
tense: Option<&str>,
anchors: &mut Vec<PredicateAnchor>,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::Predicate((rel, args)) => {
if is_non_indexable_relation(rel) {
return;
}
if let Some(pos) = find_var_position(args, var_name, subs) {
anchors.push(PredicateAnchor {
relation: rel.clone(),
var_position: pos,
args: args.clone(),
tense: tense_to_static(tense),
});
}
}
LogicNode::AndNode((l, r)) => {
collect_mandatory_anchors(buffer, *l, var_name, subs, tense, anchors);
collect_mandatory_anchors(buffer, *r, var_name, subs, tense, anchors);
}
LogicNode::PastNode(inner_id) => {
collect_mandatory_anchors(buffer, *inner_id, var_name, subs, Some("Past"), anchors);
}
LogicNode::PresentNode(inner_id) => {
collect_mandatory_anchors(buffer, *inner_id, var_name, subs, Some("Present"), anchors);
}
LogicNode::FutureNode(inner_id) => {
collect_mandatory_anchors(buffer, *inner_id, var_name, subs, Some("Future"), anchors);
}
LogicNode::ObligatoryNode(inner_id) | LogicNode::PermittedNode(inner_id) => {
collect_mandatory_anchors(buffer, *inner_id, var_name, subs, tense, anchors);
}
LogicNode::ExistsNode((_, body)) | LogicNode::ForAllNode((_, body)) => {
collect_mandatory_anchors(buffer, *body, var_name, subs, tense, anchors);
}
_ => {}
}
}
fn collect_compute_heads<'b>(buffer: &'b LogicBuffer, node_id: u32, heads: &mut HashSet<&'b str>) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::ComputeNode((rel, _)) => {
heads.insert(rel.as_str());
}
LogicNode::Predicate(_) => {}
LogicNode::AndNode((l, r)) | LogicNode::OrNode((l, r)) => {
collect_compute_heads(buffer, *l, heads);
collect_compute_heads(buffer, *r, heads);
}
LogicNode::NotNode(id)
| LogicNode::ExistsNode((_, id))
| LogicNode::ForAllNode((_, id))
| LogicNode::PastNode(id)
| LogicNode::PresentNode(id)
| LogicNode::FutureNode(id)
| LogicNode::ObligatoryNode(id)
| LogicNode::PermittedNode(id) => {
collect_compute_heads(buffer, *id, heads);
}
LogicNode::CountNode((_, _, body)) => {
collect_compute_heads(buffer, *body, heads);
}
}
}
fn extract_rule_candidates_for_entailment(
anchor: &PredicateAnchor,
inner: &KnowledgeBaseInner,
members: &[GroundTerm],
candidates: &mut HashSet<GroundTerm>,
) {
let rules = match inner.universal_rules.get(anchor.relation.as_str()) {
Some(r) => r,
None => return,
};
for rule in rules {
for conclusion in &rule.typed_conclusions {
if conclusion.relation() != anchor.relation {
continue;
}
let conc_args = &conclusion.inner().args;
if conc_args.len() != anchor.args.len() {
continue;
}
match &conc_args[anchor.var_position] {
GroundTerm::PatternVar(_) => {
candidates.extend(members.iter().cloned());
for entry in &inner.skolem_fn_registry {
for combo in GroundTermCartesianProduct::new(members, entry.dep_count) {
candidates.insert(build_skolem_fn_term(&entry.base_name, &combo));
}
}
}
GroundTerm::SkolemFn(base, _) => {
let dep_count = inner
.skolem_fn_registry
.iter()
.find(|e| e.base_name == *base)
.map(|e| e.dep_count)
.unwrap_or(1);
for combo in GroundTermCartesianProduct::new(members, dep_count) {
candidates.insert(build_skolem_fn_term(base, &combo));
}
}
other => {
candidates.insert(other.clone());
}
}
}
}
}
struct PredicateAnchor {
relation: String,
var_position: usize,
args: Vec<LogicalTerm>,
tense: Option<&'static str>,
}
fn find_var_position(
args: &[LogicalTerm],
var_name: &str,
subs: &HashMap<String, GroundTerm>,
) -> Option<usize> {
let mut var_pos = None;
for (i, arg) in args.iter().enumerate() {
if let LogicalTerm::Variable(v) = arg {
if v == var_name {
if var_pos.is_some() {
return None; }
var_pos = Some(i);
} else if !subs.contains_key(v) {
}
}
}
var_pos
}
fn tense_to_static(tense: Option<&str>) -> Option<&'static str> {
match tense {
Some("Past") => Some("Past"),
Some("Present") => Some("Present"),
Some("Future") => Some("Future"),
Some("Obligatory") => Some("Obligatory"),
Some("Permitted") => Some("Permitted"),
_ => None,
}
}
fn extract_from_index(
anchor: &PredicateAnchor,
inner: &KnowledgeBaseInner,
candidates: &mut HashSet<GroundTerm>,
) {
let facts = match inner.fact_store.lookup_predicate(anchor.relation.as_str()) {
Some(f) => f,
None => return,
};
for stored_fact in facts {
let tense_matches = match (anchor.tense, stored_fact) {
(None, StoredFact::Bare(_)) => true,
(Some("Past"), StoredFact::Past(_)) => true,
(Some("Present"), StoredFact::Present(_)) => true,
(Some("Future"), StoredFact::Future(_)) => true,
(Some("Obligatory"), StoredFact::Obligatory(_)) => true,
(Some("Permitted"), StoredFact::Permitted(_)) => true,
_ => false,
};
if !tense_matches {
continue;
}
let fact_args = &stored_fact.inner().args;
if fact_args.len() != anchor.args.len() {
continue;
}
let direct = fact_args[anchor.var_position].clone();
candidates.insert(direct.clone());
if !inner.equivalence_parent.is_empty() {
for equiv in get_equivalence_class_readonly(
&inner.equivalence_parent,
&inner.equivalence_classes,
&direct,
) {
candidates.insert(equiv);
}
}
}
}