use super::*;
fn with_sub<T>(
subs: &mut HashMap<String, GroundTerm>,
key: &str,
value: GroundTerm,
f: impl FnOnce(&mut HashMap<String, GroundTerm>) -> T,
) -> T {
let prev = subs.insert(key.to_string(), value);
let result = f(subs);
match prev {
Some(p) => {
subs.insert(key.to_string(), p);
}
None => {
subs.remove(key);
}
}
result
}
fn build_all_candidates(inner: &KnowledgeBaseInner) -> Vec<GroundTerm> {
let members = inner.all_typed_domain_members();
let mut candidates: Vec<GroundTerm> = members.to_vec();
for entry in &inner.skolem_fn_registry {
for combo in GroundTermCartesianProduct::new(members, entry.dep_count) {
candidates.push(build_skolem_fn_term(&entry.base_name, &combo));
}
}
candidates
}
fn ensure_candidates<'a>(
slot: &'a mut Option<Vec<GroundTerm>>,
inner: &KnowledgeBaseInner,
) -> &'a [GroundTerm] {
if slot.is_none() {
*slot = Some(build_all_candidates(inner));
}
slot.as_deref().expect("slot was just filled")
}
fn condition_is_index_decidable(relation: &str, inner: &KnowledgeBaseInner) -> bool {
!nibli_types::relations::is_identity(relation)
&& inner.equivalence_parent.is_empty()
&& !inner.universal_rules.contains_key(relation)
}
fn fact_has_unbound_pattern_var(fact: &StoredFact) -> bool {
fact.inner()
.args
.iter()
.any(|a| matches!(a, GroundTerm::PatternVar(_)))
}
fn bind_join_vars_from_index(
rule: &UniversalRuleRecord,
bindings: &mut HashMap<String, GroundTerm>,
inner: &KnowledgeBaseInner,
) -> Vec<String> {
let mut newly_bound: Vec<String> = Vec::new();
loop {
let mut changed = false;
for ct in &rule.typed_conditions {
let cs = substitute_fact(ct, bindings);
let gf = cs.inner();
let Some(anchor_pos) = gf
.args
.iter()
.position(|a| !matches!(a, GroundTerm::PatternVar(_)))
else {
continue;
};
let has_unbound_individual = gf
.args
.iter()
.any(|a| matches!(a, GroundTerm::PatternVar(s) if s.starts_with("x__")));
if !has_unbound_individual {
continue;
}
let Some(by_val) = inner
.arg_position_index
.get(&(gf.relation.clone(), anchor_pos))
else {
continue;
};
let Some(facts) = by_val.get(&gf.args[anchor_pos]) else {
continue;
};
let matching: Vec<&StoredFact> = facts
.iter()
.filter(|f| {
let fa = &f.inner().args;
fa.len() == gf.args.len()
&& gf
.args
.iter()
.zip(fa.iter())
.all(|(c, a)| matches!(c, GroundTerm::PatternVar(_)) || c == a)
})
.collect();
if matching.len() == 1 {
let fa = &matching[0].inner().args;
for (i, a) in gf.args.iter().enumerate() {
if let GroundTerm::PatternVar(s) = a {
if s.starts_with("x__") && !bindings.contains_key(s) {
bindings.insert(s.clone(), fa[i].clone());
newly_bound.push(s.clone());
changed = true;
}
}
}
}
}
if !changed {
break;
}
}
newly_bound
}
fn prefer_non_definitive(
current: Option<QueryResult>,
candidate: QueryResult,
) -> Option<QueryResult> {
if candidate.is_definitive() {
return current;
}
match (current, candidate) {
(Some(QueryResult::ResourceExceeded(kind)), QueryResult::Unknown(_)) => {
Some(QueryResult::ResourceExceeded(kind))
}
(Some(QueryResult::Unknown(_)), QueryResult::ResourceExceeded(kind)) => {
Some(QueryResult::ResourceExceeded(kind))
}
(Some(existing), _) => Some(existing),
(None, candidate) => Some(candidate),
}
}
pub(crate) fn combine_indeterminate(left: QueryResult, right: QueryResult) -> QueryResult {
match (left, right) {
(QueryResult::ResourceExceeded(kind), _) => QueryResult::ResourceExceeded(kind),
(_, QueryResult::ResourceExceeded(kind)) => QueryResult::ResourceExceeded(kind),
(QueryResult::Unknown(reason), _) => QueryResult::Unknown(reason),
(_, QueryResult::Unknown(reason)) => QueryResult::Unknown(reason),
(l, r) => {
debug_assert!(
false,
"combine_indeterminate reached with two definitive operands: {l:?} / {r:?}"
);
QueryResult::False
}
}
}
fn combine_conjunction(left: QueryResult, right: QueryResult) -> QueryResult {
if left.is_false() || right.is_false() {
QueryResult::False
} else if left.is_true() && right.is_true() {
QueryResult::True
} else {
combine_indeterminate(left, right)
}
}
fn combine_disjunction(left: QueryResult, right: QueryResult) -> QueryResult {
if left.is_true() || right.is_true() {
QueryResult::True
} else if left.is_false() && right.is_false() {
QueryResult::False
} else {
combine_indeterminate(left, right)
}
}
fn negate_result(result: QueryResult) -> QueryResult {
match result {
QueryResult::True => QueryResult::False,
QueryResult::False => QueryResult::True,
QueryResult::Unknown(_) => QueryResult::Unknown(UnknownReason::NafDependent),
QueryResult::ResourceExceeded(kind) => QueryResult::ResourceExceeded(kind),
}
}
fn eval_negated_exists_group(
group: &NegatedExistsGroup,
bindings: &HashMap<String, GroundTerm>,
inner: &KnowledgeBaseInner,
candidates_slot: &mut Option<Vec<GroundTerm>>,
depth: usize,
visited: &mut HashSet<StoredFact>,
tense_fact: Option<&StoredFact>,
) -> QueryResult {
if tense_fact.is_none()
&& inner.materialization
&& let Some(m) = inner.materialized.borrow().as_ref()
&& let Some((rel, tuple)) = crate::materialize::probe_negated_group(group, bindings)
&& m.is_complete_for(&rel, tuple.len())
{
return negate_result(if m.contains(&rel, &tuple) {
QueryResult::True
} else {
QueryResult::False
});
}
let effective: Vec<StoredFact> = match tense_fact {
Some(src) => group
.conditions
.iter()
.map(|c| apply_tense_to_fact(c, src))
.collect(),
None => group.conditions.clone(),
};
let candidates = match collect_group_event_candidates(&effective, &group.event_var, inner) {
Some(narrowed) => narrowed,
None => ensure_candidates(candidates_slot, inner).to_vec(),
};
let mut best: Option<QueryResult> = None;
let mut any_witness = false;
for cand in &candidates {
let mut b = bindings.clone();
b.insert(group.event_var.clone(), cand.clone());
let mut all_inner_true = true;
let mut inner_pending: Option<QueryResult> = None;
for tmpl in &effective {
let cs = substitute_fact(tmpl, &b);
let r = check_predicate_in_kb_typed(&cs, inner, depth + 1, visited);
if r.is_false() {
all_inner_true = false;
inner_pending = None;
break;
}
if !r.is_true() {
all_inner_true = false;
inner_pending = prefer_non_definitive(inner_pending, r);
}
}
if all_inner_true {
any_witness = true;
break;
}
best = prefer_non_definitive(best, inner_pending.unwrap_or(QueryResult::False));
}
let exists_verdict = if any_witness {
QueryResult::True
} else {
best.unwrap_or(QueryResult::False)
};
negate_result(exists_verdict)
}
#[allow(clippy::too_many_arguments)]
fn fold_negated_groups(
rule: &UniversalRuleRecord,
bindings: &HashMap<String, GroundTerm>,
inner: &KnowledgeBaseInner,
candidates_slot: &mut Option<Vec<GroundTerm>>,
depth: usize,
visited: &mut HashSet<StoredFact>,
hold: &mut bool,
pending: &mut Option<QueryResult>,
tense_fact: Option<&StoredFact>,
) {
if rule.negated_exists_groups.is_empty() || (!*hold && pending.is_none()) {
return;
}
for group in &rule.negated_exists_groups {
let gv = eval_negated_exists_group(
group,
bindings,
inner,
candidates_slot,
depth,
visited,
tense_fact,
);
if gv.is_false() {
*hold = false;
*pending = None;
break;
}
if !gv.is_true() {
*hold = false;
*pending = prefer_non_definitive(pending.take(), gv);
}
}
}
#[inline]
pub(super) fn check_cancelled(inner: &KnowledgeBaseInner) -> Result<(), String> {
if inner
.cancel
.as_ref()
.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
{
return Err("reasoning cancelled: deadline exceeded".to_string());
}
Ok(())
}
pub(super) fn check_formula_holds(
buffer: &LogicBuffer,
node_id: u32,
subs: &mut HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
tense: Option<&str>,
) -> Result<QueryResult, String> {
check_cancelled(inner)?;
let (result, _idx) =
check_formula_holds_core::<NoOpSink>(buffer, node_id, subs, inner, tense, &mut NoOpSink)?;
check_cancelled(inner)?;
Ok(result)
}
pub(super) fn check_formula_holds_recording(
buffer: &LogicBuffer,
node_id: u32,
subs: &mut HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
steps: &mut Vec<ProofStep>,
tense: Option<&str>,
memo: &mut HashMap<String, u32>,
) -> Result<(QueryResult, u32), String> {
check_cancelled(inner)?;
let mut sink = RecordingSink { steps, memo };
let result =
check_formula_holds_core::<RecordingSink>(buffer, node_id, subs, inner, tense, &mut sink)?;
check_cancelled(inner)?;
Ok(result)
}
fn check_formula_holds_core<S: TraceSink>(
buffer: &LogicBuffer,
node_id: u32,
subs: &mut HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
tense: Option<&str>,
sink: &mut S,
) -> Result<(QueryResult, u32), String> {
check_cancelled(inner)?;
match get_node(buffer, node_id)? {
LogicNode::AndNode((l, r)) => {
if is_abstraction_marker(buffer, *l) {
return check_formula_holds_core::<S>(buffer, *l, subs, inner, tense, sink);
}
let (lv, li) = check_formula_holds_core::<S>(buffer, *l, subs, inner, tense, sink)?;
if lv.is_false() {
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::Conjunction,
holds: false,
children: vec![li],
})
} else {
0
};
return Ok((QueryResult::False, idx));
}
let (rv, ri) = check_formula_holds_core::<S>(buffer, *r, subs, inner, tense, sink)?;
let verdict = combine_conjunction(lv, rv);
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::Conjunction,
holds: verdict.is_true(),
children: vec![li, ri],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::OrNode((l, r)) => {
let (lv, li) = check_formula_holds_core::<S>(buffer, *l, subs, inner, tense, sink)?;
if lv.is_true() {
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::DisjunctionIntro {
side: "left".to_string(),
},
holds: true,
children: vec![li],
})
} else {
0
};
return Ok((QueryResult::True, idx));
}
let (rv, ri) = check_formula_holds_core::<S>(buffer, *r, subs, inner, tense, sink)?;
let rv_is_true = rv.is_true();
let verdict = combine_disjunction(lv, rv);
let idx = if S::RECORDING {
if rv_is_true {
sink.push(ProofStep {
rule: ProofRule::DisjunctionIntro {
side: "right".to_string(),
},
holds: true,
children: vec![ri],
})
} else {
sink.push(ProofStep {
rule: ProofRule::DisjunctionIntro {
side: "neither".to_string(),
},
holds: verdict.is_true(),
children: vec![li, ri],
})
}
} else {
0
};
Ok((verdict, idx))
}
LogicNode::NotNode(inner_node) => {
let (iv, ii) =
check_formula_holds_core::<S>(buffer, *inner_node, subs, inner, tense, sink)?;
let verdict = negate_result(iv.clone());
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::Negation,
holds: matches!(iv, QueryResult::False),
children: vec![ii],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::PastNode(inner_node) => {
let (verdict, ci) = check_formula_holds_core::<S>(
buffer,
*inner_node,
subs,
inner,
Some("Past"),
sink,
)?;
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ModalPassthrough {
kind: "past".to_string(),
},
holds: verdict.is_true(),
children: vec![ci],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::PresentNode(inner_node) => {
let (verdict, ci) = check_formula_holds_core::<S>(
buffer,
*inner_node,
subs,
inner,
Some("Present"),
sink,
)?;
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ModalPassthrough {
kind: "present".to_string(),
},
holds: verdict.is_true(),
children: vec![ci],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::FutureNode(inner_node) => {
let (verdict, ci) = check_formula_holds_core::<S>(
buffer,
*inner_node,
subs,
inner,
Some("Future"),
sink,
)?;
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ModalPassthrough {
kind: "future".to_string(),
},
holds: verdict.is_true(),
children: vec![ci],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::ObligatoryNode(inner_node) => {
let (verdict, ci) = check_formula_holds_core::<S>(
buffer,
*inner_node,
subs,
inner,
Some("Obligatory"),
sink,
)?;
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ModalPassthrough {
kind: "obligatory".to_string(),
},
holds: verdict.is_true(),
children: vec![ci],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::PermittedNode(inner_node) => {
let (verdict, ci) = check_formula_holds_core::<S>(
buffer,
*inner_node,
subs,
inner,
Some("Permitted"),
sink,
)?;
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ModalPassthrough {
kind: "permitted".to_string(),
},
holds: verdict.is_true(),
children: vec![ci],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::ExistsNode((v, body)) => {
if let Ok(body_node) = get_node(buffer, *body) {
if let LogicNode::ComputeNode((rel, args)) = body_node {
let members = inner.all_typed_domain_members();
if let Some(batch) =
batch_evaluate_compute_for_members(&*inner, rel, args, v, members, subs)
{
let winner = batch
.results
.iter()
.position(|r| *r)
.map(|i| members[i].clone());
for fact in batch.deferred_facts {
assert_typed_fact(fact, inner);
}
if let Some(winning_member) = winner {
let idx = if S::RECORDING {
let body_idx = with_sub(subs, v, winning_member.clone(), |s| {
check_formula_holds_core::<S>(
buffer, *body, s, inner, tense, sink,
)
})?
.1;
sink.push(ProofStep {
rule: ProofRule::ExistsWitness {
var: v.clone(),
term: witness_term_to_logical_term(&winning_member),
},
holds: true,
children: vec![body_idx],
})
} else {
0
};
return Ok((QueryResult::True, idx));
}
}
}
}
if let Some(group) = try_evaluate_numeric_group(&*inner, buffer, v, *body, subs) {
let res = group.verdict.clone();
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ComputeCheck {
method: group.method.to_string(),
detail: group.relation,
},
holds: res.is_true(),
children: vec![],
})
} else {
0
};
return Ok((res, idx));
}
if inner.positive_lookup.get() && tense.is_none() && inner.materialization {
let hit = crate::materialize::probe_positive_group(buffer, *body, v, subs)
.and_then(|(rel, tuple)| {
let m = inner.materialized.borrow();
let m = m.as_ref()?;
m.is_complete_for(&rel, tuple.len())
.then(|| m.contains(&rel, &tuple))
});
if let Some(found) = hit {
return Ok((
if found {
QueryResult::True
} else {
QueryResult::False
},
0,
));
}
}
let candidates: Vec<GroundTerm> =
match collect_entailment_candidates(buffer, *body, v, subs, inner, tense) {
Some(narrowed) => narrowed,
None => {
let members: Vec<GroundTerm> = inner.all_typed_domain_members().to_vec();
let mut all = members.clone();
for entry in &inner.skolem_fn_registry {
for combo in GroundTermCartesianProduct::new(&members, entry.dep_count)
{
all.push(build_skolem_fn_term(&entry.base_name, &combo));
}
}
all
}
};
let mut best_result = None;
for candidate in &candidates {
let result = with_sub(subs, v, candidate.clone(), |s| {
check_formula_holds_core::<NoOpSink>(
buffer,
*body,
s,
inner,
tense,
&mut NoOpSink,
)
})?
.0;
if result.is_true() {
let idx = if S::RECORDING {
let body_idx = with_sub(subs, v, candidate.clone(), |s| {
check_formula_holds_core::<S>(buffer, *body, s, inner, tense, sink)
})?
.1;
sink.push(ProofStep {
rule: ProofRule::ExistsWitness {
var: v.clone(),
term: witness_term_to_logical_term(candidate),
},
holds: true,
children: vec![body_idx],
})
} else {
0
};
return Ok((QueryResult::True, idx));
}
best_result = prefer_non_definitive(best_result, result);
}
let saw_non_definitive = best_result.is_some();
let verdict = best_result.unwrap_or(QueryResult::False);
let idx = if S::RECORDING {
if saw_non_definitive {
sink.push(ProofStep {
rule: ProofRule::PredicateCheck {
method: "indeterminate".to_string(),
detail: "exists".to_string(),
},
holds: false,
children: vec![],
})
} else {
sink.push(ProofStep {
rule: ProofRule::ExistsFailed,
holds: false,
children: vec![],
})
}
} else {
0
};
Ok((verdict, idx))
}
LogicNode::ForAllNode((v, body)) => {
let members: Vec<GroundTerm> = inner.all_non_event_domain_members().to_vec();
if members.is_empty() {
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ForallVacuous,
holds: true,
children: vec![],
})
} else {
0
};
return Ok((QueryResult::True, idx));
}
if let Ok(body_node) = get_node(buffer, *body) {
if let LogicNode::ComputeNode((rel, args)) = body_node {
let members_slice = inner.all_non_event_domain_members();
if let Some(batch) = batch_evaluate_compute_for_members(
&*inner,
rel,
args,
v,
members_slice,
subs,
) {
let fail_member = batch
.results
.iter()
.position(|r| !*r)
.map(|i| members_slice[i].clone());
for fact in batch.deferred_facts {
assert_typed_fact(fact, inner);
}
if let Some(counter) = fail_member {
let idx = if S::RECORDING {
let body_idx = with_sub(subs, v, counter.clone(), |s| {
check_formula_holds_core::<S>(
buffer, *body, s, inner, tense, sink,
)
})?
.1;
sink.push(ProofStep {
rule: ProofRule::ForallCounterexample {
entity: ground_term_to_logical_term(&counter),
},
holds: false,
children: vec![body_idx],
})
} else {
0
};
return Ok((QueryResult::False, idx));
}
let idx = if S::RECORDING {
let (child_indices, entity_terms) = forall_record_all_members::<S>(
buffer, *body, v, &members, subs, inner, tense, sink,
)?;
sink.push(ProofStep {
rule: ProofRule::ForallVerified {
entities: entity_terms,
},
holds: true,
children: child_indices,
})
} else {
0
};
return Ok((QueryResult::True, idx));
}
}
}
let mut best_result = None;
let mut false_member: Option<GroundTerm> = None;
let mut nondef_member: Option<GroundTerm> = None;
for member in &members {
let result = with_sub(subs, v, member.clone(), |s| {
check_formula_holds_core::<NoOpSink>(
buffer,
*body,
s,
inner,
tense,
&mut NoOpSink,
)
})?
.0;
if result.is_false() {
false_member = Some(member.clone());
break;
}
if !result.is_true() {
if nondef_member.is_none() {
nondef_member = Some(member.clone());
}
best_result = prefer_non_definitive(best_result, result);
}
}
if let Some(counter) = false_member {
let idx = if S::RECORDING {
let body_idx = with_sub(subs, v, counter.clone(), |s| {
check_formula_holds_core::<S>(buffer, *body, s, inner, tense, sink)
})?
.1;
sink.push(ProofStep {
rule: ProofRule::ForallCounterexample {
entity: ground_term_to_logical_term(&counter),
},
holds: false,
children: vec![body_idx],
})
} else {
0
};
return Ok((QueryResult::False, idx));
}
if let Some(nd) = nondef_member {
let verdict =
best_result.unwrap_or(QueryResult::Unknown(UnknownReason::IncompleteKnowledge));
let idx = if S::RECORDING {
let body_idx = with_sub(subs, v, nd.clone(), |s| {
check_formula_holds_core::<S>(buffer, *body, s, inner, tense, sink)
})?
.1;
sink.push(ProofStep {
rule: ProofRule::PredicateCheck {
method: "indeterminate".to_string(),
detail: "forall".to_string(),
},
holds: false,
children: vec![body_idx],
})
} else {
0
};
return Ok((verdict, idx));
}
let idx = if S::RECORDING {
let (child_indices, entity_terms) = forall_record_all_members::<S>(
buffer, *body, v, &members, subs, inner, tense, sink,
)?;
sink.push(ProofStep {
rule: ProofRule::ForallVerified {
entities: entity_terms,
},
holds: true,
children: child_indices,
})
} else {
0
};
Ok((QueryResult::True, idx))
}
LogicNode::CountNode((v, count, body)) => {
let members: Vec<GroundTerm> = {
let mut seen = HashSet::new();
let mut out = Vec::new();
for m in inner.all_typed_domain_members() {
if let GroundTerm::Constant(name) = m
&& inner.presupposition_witnesses.contains(name.as_str())
{
continue;
}
let canon = find_canonical_readonly(&inner.equivalence_parent, m);
if seen.insert(canon) {
out.push(m.clone());
}
}
out
};
if let Ok(body_node) = get_node(buffer, *body) {
if let LogicNode::ComputeNode((rel, args)) = body_node {
if let Some(batch) =
batch_evaluate_compute_for_members(&*inner, rel, args, v, &members, subs)
{
for fact in batch.deferred_facts {
assert_typed_fact(fact, inner);
}
let satisfying = batch.results.iter().filter(|r| **r).count() as u32;
let verdict = if satisfying == *count {
QueryResult::True
} else {
QueryResult::False
};
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::CountResult {
expected: *count,
actual: satisfying,
},
holds: verdict.is_true(),
children: vec![],
})
} else {
0
};
return Ok((verdict, idx));
}
}
}
let mut satisfying = 0u32;
let mut unresolved = 0u32;
let mut best_result = None;
for member in &members {
let result = with_sub(subs, v, member.clone(), |s| {
check_formula_holds_core::<NoOpSink>(
buffer,
*body,
s,
inner,
tense,
&mut NoOpSink,
)
})?
.0;
match result {
QueryResult::True => satisfying += 1,
QueryResult::False => {}
other => {
unresolved += 1;
best_result = prefer_non_definitive(best_result, other);
}
}
}
let verdict = if unresolved == 0 {
if satisfying == *count {
QueryResult::True
} else {
QueryResult::False
}
} else if satisfying > *count || satisfying + unresolved < *count {
QueryResult::False
} else {
best_result.unwrap_or(QueryResult::Unknown(UnknownReason::IncompleteKnowledge))
};
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::CountResult {
expected: *count,
actual: satisfying,
},
holds: verdict.is_true(),
children: vec![],
})
} else {
0
};
Ok((verdict, idx))
}
LogicNode::Predicate((rel, args)) => {
if let Some(verdict) = try_numeric_comparison(rel, args, subs) {
let non_finite = matches!(verdict, QueryResult::Unknown(_));
let idx = if S::RECORDING {
let detail = format!(
"{}({}) = {}",
rel,
args.iter()
.map(|a| match a {
LogicalTerm::Number(n) => format!("{}", *n as i64),
LogicalTerm::Variable(var) => subs
.get(var.as_str())
.map(|gt| gt.to_display_string())
.unwrap_or_else(|| var.clone()),
_ => "?".to_string(),
})
.collect::<Vec<_>>()
.join(", "),
if non_finite {
"non-finite".to_string()
} else {
verdict.is_true().to_string()
}
);
sink.push(ProofStep {
rule: ProofRule::PredicateCheck {
method: if non_finite { "non_finite" } else { "numeric" }.to_string(),
detail,
},
holds: verdict.is_true(),
children: vec![],
})
} else {
0
};
return Ok((verdict, idx));
}
if let Some(fact) = build_stored_fact_from_node(buffer, node_id, subs, tense) {
let mut visited = HashSet::new();
let verdict = check_predicate_in_kb_typed(&fact, &*inner, 0, &mut visited);
let idx = if S::RECORDING {
if verdict.is_true() {
sink.trace_child(&fact, &*inner, 0, &mut visited)
} else if !matches!(verdict, QueryResult::False) {
let reason = match &verdict {
QueryResult::Unknown(r) => format!("unknown:{r:?}"),
QueryResult::ResourceExceeded(k) => format!("resource-exceeded:{k:?}"),
_ => "indeterminate".to_string(),
};
sink.push(ProofStep {
rule: ProofRule::PredicateCheck {
method: "indeterminate".to_string(),
detail: reason,
},
holds: false,
children: vec![],
})
} else {
let mut failed_children = Vec::new();
let rules = matching_rules_typed(&fact, &inner.universal_rules);
for rule in rules {
for concl in &rule.typed_conclusions {
if let Some(bindings) = unify_facts(concl, &fact) {
for (ci, ct) in rule.typed_conditions.iter().enumerate() {
let cond_fact = substitute_fact(ct, &bindings);
let cond_result = check_predicate_in_kb_typed(
&cond_fact,
&*inner,
0,
&mut HashSet::new(),
);
let negated = rule.negated_condition_indices.contains(&ci);
let blocked = if negated {
cond_result.is_true()
} else {
!cond_result.is_true()
};
if blocked {
let child_idx = sink.push(ProofStep {
rule: ProofRule::RuleAttemptFailed {
rule_label: rule.label.clone(),
failed_condition: cond_fact.to_display_string(),
},
holds: false,
children: vec![],
});
failed_children.push(child_idx);
break; }
}
}
}
}
sink.push(ProofStep {
rule: ProofRule::PredicateNotFound {
predicate: fact.to_display_string(),
},
holds: false,
children: failed_children,
})
}
} else {
0
};
Ok((verdict, idx))
} else {
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::PredicateCheck {
method: "build_failed".to_string(),
detail: String::new(),
},
holds: false,
children: vec![],
})
} else {
0
};
Ok((QueryResult::False, idx))
}
}
LogicNode::ComputeNode((rel, args)) => {
if let Some(result) = try_arithmetic_evaluation(rel, args, subs) {
if result {
if let Some(fact) = build_stored_fact_from_node(buffer, node_id, subs, tense) {
assert_typed_fact(fact, inner);
}
}
let verdict = if result {
QueryResult::True
} else {
QueryResult::False
};
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ComputeCheck {
method: "arithmetic".to_string(),
detail: rel.clone(),
},
holds: result,
children: vec![],
})
} else {
0
};
return Ok((verdict, idx));
}
let resolved = resolve_args_for_dispatch(args, subs);
if let Ok(result) = dispatch_to_backend(&*inner, rel, &resolved) {
if result {
if let Some(fact) = build_stored_fact_from_node(buffer, node_id, subs, tense) {
assert_typed_fact(fact, inner);
}
}
let verdict = if result {
QueryResult::True
} else {
QueryResult::False
};
let idx = if S::RECORDING {
sink.push(ProofStep {
rule: ProofRule::ComputeCheck {
method: "backend".to_string(),
detail: rel.clone(),
},
holds: result,
children: vec![],
})
} else {
0
};
return Ok((verdict, idx));
}
let verdict =
if let Some(fact) = build_stored_fact_from_node(buffer, node_id, subs, tense) {
let mut visited = HashSet::new();
match check_predicate_in_kb_typed(&fact, &*inner, 0, &mut visited) {
QueryResult::True => QueryResult::True,
_ => QueryResult::Unknown(UnknownReason::BackendUnavailable),
}
} else {
QueryResult::Unknown(UnknownReason::BackendUnavailable)
};
let idx = if S::RECORDING {
let method = match &verdict {
QueryResult::Unknown(UnknownReason::CycleCut) => "cycle_cut",
QueryResult::Unknown(UnknownReason::IncompleteKnowledge) => {
"incomplete_knowledge"
}
QueryResult::Unknown(UnknownReason::NafDependent) => "naf_dependent",
QueryResult::Unknown(UnknownReason::BackendUnavailable) => {
"backend_unavailable"
}
QueryResult::Unknown(UnknownReason::NonFinite) => "non_finite",
QueryResult::ResourceExceeded(ResourceKind::Depth) => "depth_limit",
QueryResult::ResourceExceeded(ResourceKind::Fuel) => "fuel_limit",
QueryResult::ResourceExceeded(ResourceKind::Memory) => "memory_limit",
QueryResult::False | QueryResult::True => "kb",
};
sink.push(ProofStep {
rule: ProofRule::ComputeCheck {
method: method.to_string(),
detail: rel.clone(),
},
holds: verdict.is_true(),
children: vec![],
})
} else {
0
};
Ok((verdict, idx))
}
}
}
#[allow(clippy::too_many_arguments)]
fn forall_record_all_members<S: TraceSink>(
buffer: &LogicBuffer,
body: u32,
v: &str,
members: &[GroundTerm],
subs: &mut HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
tense: Option<&str>,
sink: &mut S,
) -> Result<(Vec<u32>, Vec<LogicalTerm>), String> {
let mut child_indices = Vec::new();
let mut entity_terms = Vec::new();
for member in members {
let body_idx = with_sub(subs, v, member.clone(), |s| {
check_formula_holds_core::<S>(buffer, body, s, inner, tense, sink)
})?
.1;
child_indices.push(body_idx);
entity_terms.push(ground_term_to_logical_term(member));
}
Ok((child_indices, entity_terms))
}
pub(super) fn find_witnesses(
buffer: &LogicBuffer,
node_id: u32,
subs: &mut HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
tense: Option<&str>,
) -> Result<Vec<Vec<(String, GroundTerm)>>, String> {
if inner.find_horizon_hit {
return Ok(Vec::new());
}
match get_node(buffer, node_id)? {
LogicNode::ExistsNode((v, body)) => {
let mut results = Vec::new();
let candidates: Vec<GroundTerm> =
match collect_entailment_candidates(buffer, *body, v, subs, inner, tense) {
Some(narrowed) => narrowed,
None => {
let members: Vec<GroundTerm> = inner.all_typed_domain_members().to_vec();
let mut all = members.clone();
for entry in &inner.skolem_fn_registry {
for combo in GroundTermCartesianProduct::new(&members, entry.dep_count)
{
all.push(build_skolem_fn_term(&entry.base_name, &combo));
}
}
all
}
};
for candidate in candidates {
let mut new_subs = subs.clone();
new_subs.insert(v.clone(), candidate.clone());
let sub_results = find_witnesses(buffer, *body, &mut new_subs, inner, tense)?;
for mut bindings in sub_results {
bindings.push((v.clone(), candidate.clone()));
results.push(bindings);
}
}
Ok(results)
}
LogicNode::PastNode(inner_node) => {
find_witnesses(buffer, *inner_node, subs, inner, Some("Past"))
}
LogicNode::PresentNode(inner_node) => {
find_witnesses(buffer, *inner_node, subs, inner, Some("Present"))
}
LogicNode::FutureNode(inner_node) => {
find_witnesses(buffer, *inner_node, subs, inner, Some("Future"))
}
LogicNode::AndNode((l, r)) => {
let left_results = find_witnesses(buffer, *l, subs, inner, tense)?;
let mut results = Vec::new();
for left_bindings in left_results {
let mut merged_subs = subs.clone();
for (k, v) in &left_bindings {
merged_subs.insert(k.clone(), v.clone());
}
let right_results = find_witnesses(buffer, *r, &mut merged_subs, inner, tense)?;
for right_bindings in right_results {
let mut combined = left_bindings.clone();
combined.extend(right_bindings);
results.push(combined);
}
}
Ok(results)
}
LogicNode::OrNode((l, r)) => {
let mut results = find_witnesses(buffer, *l, subs, inner, tense)?;
results.extend(find_witnesses(buffer, *r, subs, inner, tense)?);
Ok(results)
}
_ => {
let verdict = check_formula_holds(buffer, node_id, subs, inner, tense)?;
if verdict.is_true() {
Ok(vec![vec![]])
} else {
if witness_search_cut(&verdict) {
inner.find_horizon_hit = true;
}
Ok(vec![])
}
}
}
}
fn witness_search_cut(v: &QueryResult) -> bool {
matches!(
v,
QueryResult::ResourceExceeded(_)
| QueryResult::Unknown(UnknownReason::CycleCut)
| QueryResult::Unknown(UnknownReason::IncompleteKnowledge)
| QueryResult::Unknown(UnknownReason::BackendUnavailable)
)
}
pub(super) fn clear_typed_pred_cache(inner: &KnowledgeBaseInner) {
inner.pred_cache.borrow_mut().clear();
inner.depth_cut_table.borrow_mut().clear();
}
pub(super) fn invalidate_materialization(inner: &KnowledgeBaseInner) {
*inner.materialized.borrow_mut() = None;
}
pub(super) fn typed_fact_is_asserted(fact: &StoredFact, inner: &KnowledgeBaseInner) -> bool {
if inner.fact_store.contains(fact) {
return true;
}
if inner.equivalence_parent.is_empty() {
return false;
}
typed_fact_is_asserted_with_equivalence(fact, inner).is_some()
}
fn typed_fact_is_asserted_with_equivalence(
fact: &StoredFact,
inner: &KnowledgeBaseInner,
) -> Option<StoredFact> {
let gf = fact.inner();
let equiv_args: Vec<Vec<GroundTerm>> = gf
.args
.iter()
.map(|arg| {
get_equivalence_class_readonly(
&inner.equivalence_parent,
&inner.equivalence_classes,
arg,
)
})
.collect();
if equiv_args.iter().all(|cls| cls.len() <= 1) {
return None;
}
for combo in CartesianProduct::new(&equiv_args) {
let variant_gf = GroundFact::new(gf.relation.clone(), combo);
let variant = StoredFact::with_tense_from(variant_gf, fact);
if variant != *fact && inner.fact_store.contains(&variant) {
return Some(variant);
}
}
None
}
fn equals_substitution_note(orig: &StoredFact, variant: &StoredFact) -> String {
orig.inner()
.args
.iter()
.zip(variant.inner().args.iter())
.filter(|(o, v)| o != v)
.map(|(o, v)| format!("{} = {}", o.to_display_string(), v.to_display_string()))
.collect::<Vec<_>>()
.join(", ")
}
const DU_VARIANT_BOUND: usize = 256;
struct CartesianProduct<'a> {
sets: &'a [Vec<GroundTerm>],
indices: Vec<usize>,
done: bool,
}
impl<'a> CartesianProduct<'a> {
fn new(sets: &'a [Vec<GroundTerm>]) -> Self {
let done = sets.iter().any(|s| s.is_empty());
Self {
sets,
indices: vec![0; sets.len()],
done,
}
}
}
impl Iterator for CartesianProduct<'_> {
type Item = Vec<GroundTerm>;
fn next(&mut self) -> Option<Vec<GroundTerm>> {
if self.done {
return None;
}
let result: Vec<GroundTerm> = self
.indices
.iter()
.enumerate()
.map(|(i, &idx)| self.sets[i][idx].clone())
.collect();
let mut carry = true;
for i in (0..self.sets.len()).rev() {
if carry {
self.indices[i] += 1;
if self.indices[i] < self.sets[i].len() {
carry = false;
} else {
self.indices[i] = 0;
}
}
}
if carry {
self.done = true;
}
Some(result)
}
}
pub(super) fn sort_rule_bucket(bucket: &mut [Arc<UniversalRuleRecord>]) {
bucket.sort_by_key(|r| std::cmp::Reverse(r.priority));
}
pub(super) fn matching_rules_typed<'a>(
fact: &StoredFact,
universal_rules: &'a HashMap<String, Vec<Arc<UniversalRuleRecord>>>,
) -> &'a [Arc<UniversalRuleRecord>] {
let rules = universal_rules
.get(fact.relation())
.map(Vec::as_slice)
.unwrap_or(&[]);
debug_assert!(
rules.is_sorted_by_key(|r| std::cmp::Reverse(r.priority)),
"universal_rules bucket must be kept descending-sorted by priority"
);
rules
}
pub(super) fn check_predicate_in_kb_typed(
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
) -> QueryResult {
if check_cancelled(inner).is_err() {
return QueryResult::False;
}
let is_traced = inner.traced_predicates.contains(fact.relation());
if is_traced {
let indent = " ".repeat(depth);
eprintln!(
"[Trace] {}depth={} check: {}",
indent,
depth,
fact.to_display_string()
);
}
if fact.relation() == nibli_types::relations::IDENTITY {
let args = &fact.inner().args;
if args.len() == 2 {
if args[0] == args[1] {
return QueryResult::True; }
if !inner.equivalence_parent.is_empty() {
let canon_a = find_canonical_readonly(&inner.equivalence_parent, &args[0]);
let canon_b = find_canonical_readonly(&inner.equivalence_parent, &args[1]);
if canon_a == canon_b {
return QueryResult::True; }
}
}
}
if typed_fact_is_asserted(fact, inner) {
return QueryResult::True;
}
let cached = if inner.pred_cache_enabled.get() {
inner.pred_cache.borrow().get(fact).cloned()
} else {
None
};
if let Some(result) = cached {
return result;
}
let remaining = inner.max_chain_depth.saturating_sub(depth);
if inner.pred_cache_enabled.get()
&& !visited.contains(&cycle_key(fact))
&& inner
.depth_cut_table
.borrow()
.get(fact)
.is_some_and(|&r| r >= remaining)
{
return QueryResult::ResourceExceeded(ResourceKind::Depth);
}
let epoch_before = inner.cycle_cut_epoch.get();
let mut result = try_backward_chain_typed(fact, inner, depth, visited);
if result.is_false()
&& !inner.equivalence_parent.is_empty()
&& fact.relation() != nibli_types::relations::IDENTITY
&& !visited.contains(&cycle_key(fact))
{
let gf = fact.inner();
let equiv_args: Vec<Vec<GroundTerm>> = gf
.args
.iter()
.map(|arg| {
get_equivalence_class_readonly(
&inner.equivalence_parent,
&inner.equivalence_classes,
arg,
)
})
.collect();
if equiv_args.iter().any(|cls| cls.len() > 1) {
let reinserted = visited.insert(cycle_key(fact));
let owns_budget = inner.du_variant_budget.get().is_none();
if owns_budget {
inner.du_variant_budget.set(Some(DU_VARIANT_BOUND));
}
let mut truncated = false;
for combo in CartesianProduct::new(&equiv_args) {
match inner.du_variant_budget.get() {
Some(0) | None => {
truncated = true;
break;
}
Some(b) => inner.du_variant_budget.set(Some(b - 1)),
}
let variant_gf = GroundFact::new(gf.relation.clone(), combo);
let variant = StoredFact::with_tense_from(variant_gf, fact);
if variant != *fact && !visited.contains(&cycle_key(&variant)) {
let variant_result =
check_predicate_in_kb_typed(&variant, inner, depth, visited);
if variant_result.is_true() {
result = QueryResult::True;
break;
}
}
}
if owns_budget {
inner.du_variant_budget.set(None);
}
if reinserted {
visited.remove(&cycle_key(fact));
}
if truncated && result.is_false() {
result = QueryResult::ResourceExceeded(ResourceKind::Depth);
}
}
}
if is_traced {
let indent = " ".repeat(depth);
eprintln!(
"[Trace] {}depth={} result: {} → {}",
indent,
depth,
fact.to_display_string(),
result.status_label()
);
}
if inner.pred_cache_enabled.get() {
if result.is_definitive() {
inner
.pred_cache
.borrow_mut()
.insert(fact.clone(), result.clone());
} else if matches!(result, QueryResult::ResourceExceeded(ResourceKind::Depth))
&& inner.cycle_cut_epoch.get() == epoch_before
{
let mut table = inner.depth_cut_table.borrow_mut();
let entry = table.entry(fact.clone()).or_insert(0);
if remaining > *entry {
*entry = remaining;
}
}
}
result
}
fn stored_fact_contains_var(fact: &StoredFact, var: &str) -> bool {
fn term_contains_var(term: &GroundTerm, var: &str) -> bool {
match term {
GroundTerm::PatternVar(name) => name == var,
GroundTerm::SkolemFn(_, dep) => term_contains_var(dep, var),
GroundTerm::DepPair(a, b) => term_contains_var(a, var) || term_contains_var(b, var),
_ => false,
}
}
fact.inner().args.iter().any(|a| term_contains_var(a, var))
}
fn any_rule_conclusion_unifies(fact: &StoredFact, inner: &KnowledgeBaseInner) -> bool {
let try_goal = |goal: &StoredFact| {
inner
.universal_rules
.get(goal.relation())
.is_some_and(|rules| {
rules.iter().any(|r| {
r.typed_conclusions
.iter()
.any(|c| unify_facts(c, goal).is_some())
})
})
};
try_goal(fact) || strip_tense_from_fact(fact).as_ref().is_some_and(try_goal)
}
#[allow(clippy::too_many_arguments)]
fn filter_event_candidates(
rule: &UniversalRuleRecord,
ev_var: &str,
bindings: &HashMap<String, GroundTerm>,
single_var_cond_indices: &[usize],
candidates: &[GroundTerm],
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
tense_fact: Option<&StoredFact>,
) -> Vec<GroundTerm> {
let (decidable_indices, recursive_indices): (Vec<usize>, Vec<usize>) =
single_var_cond_indices.iter().copied().partition(|&idx| {
condition_is_index_decidable(rule.typed_conditions[idx].relation(), inner)
});
candidates
.iter()
.filter(|candidate| {
let mut tb = bindings.clone();
tb.insert(ev_var.to_string(), (*candidate).clone());
decidable_indices.iter().all(|&idx| {
let bare = substitute_fact(&rule.typed_conditions[idx], &tb);
let cs = match tense_fact {
Some(f) => apply_tense_to_fact(&bare, f),
None => bare,
};
if fact_has_unbound_pattern_var(&cs) {
return true;
}
let asserted = typed_fact_is_asserted(&cs, inner);
if rule.negated_condition_indices.contains(&idx) {
!asserted
} else {
asserted
}
}) && recursive_indices.iter().all(|&idx| {
let bare = substitute_fact(&rule.typed_conditions[idx], &tb);
let cs = match tense_fact {
Some(f) => apply_tense_to_fact(&bare, f),
None => bare,
};
if fact_has_unbound_pattern_var(&cs) {
return true;
}
let result = check_predicate_in_kb_typed(&cs, inner, depth + 1, visited);
let verdict = if rule.negated_condition_indices.contains(&idx) {
negate_result(result)
} else {
result
};
!verdict.is_false()
})
})
.cloned()
.collect()
}
trait TraceSink {
const RECORDING: bool;
fn push(&mut self, step: ProofStep) -> u32;
fn trace_child(
&mut self,
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
) -> u32;
}
struct NoOpSink;
impl TraceSink for NoOpSink {
const RECORDING: bool = false;
#[inline(always)]
fn push(&mut self, _step: ProofStep) -> u32 {
0
}
#[inline(always)]
fn trace_child(
&mut self,
_fact: &StoredFact,
_inner: &KnowledgeBaseInner,
_depth: usize,
_visited: &mut HashSet<StoredFact>,
) -> u32 {
0
}
}
struct RecordingSink<'a> {
steps: &'a mut Vec<ProofStep>,
memo: &'a mut HashMap<String, u32>,
}
impl TraceSink for RecordingSink<'_> {
const RECORDING: bool = true;
fn push(&mut self, step: ProofStep) -> u32 {
let idx = self.steps.len() as u32;
self.steps.push(step);
idx
}
fn trace_child(
&mut self,
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
) -> u32 {
trace_predicate_provenance_typed(fact, inner, self.steps, depth, self.memo, visited)
}
}
#[allow(clippy::too_many_arguments)]
fn emit_derived<S: TraceSink>(
sink: &mut S,
rule: &UniversalRuleRecord,
bindings: &HashMap<String, GroundTerm>,
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
tense_source: Option<&StoredFact>,
tense_label: Option<&str>,
) -> u32 {
let display = fact.to_display_string();
let mut child_indices = Vec::new();
for (idx, cond_template) in rule.typed_conditions.iter().enumerate() {
let bare = substitute_fact(cond_template, bindings);
let cond_fact = match tense_source {
Some(src) => apply_tense_to_fact(&bare, src),
None => bare,
};
if rule.negated_condition_indices.contains(&idx) {
let leaf = sink.push(ProofStep {
rule: ProofRule::Negation,
holds: true,
children: vec![],
});
child_indices.push(leaf);
} else {
let child = sink.trace_child(&cond_fact, inner, depth + 1, visited);
child_indices.push(child);
}
}
for _group in &rule.negated_exists_groups {
let leaf = sink.push(ProofStep {
rule: ProofRule::Negation,
holds: true,
children: vec![],
});
child_indices.push(leaf);
}
let label = match tense_label {
Some(t) => format!("{} [{}]", rule.label, t),
None => rule.label.clone(),
};
sink.push(ProofStep {
rule: ProofRule::Derived {
label,
fact: display,
},
holds: true,
children: child_indices,
})
}
fn tense_label_of(f: &StoredFact) -> &'static str {
match f {
StoredFact::Past(_) => "past",
StoredFact::Present(_) => "present",
StoredFact::Future(_) => "future",
_ => "?",
}
}
fn process_phase<S: TraceSink>(
match_fact: &StoredFact,
tense_fact: Option<&StoredFact>,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
sink: &mut S,
candidates_slot: &mut Option<Vec<GroundTerm>>,
best_result: &mut Option<QueryResult>,
) -> Option<Option<u32>> {
let orig_fact = tense_fact.unwrap_or(match_fact);
let tense_label = tense_fact.map(tense_label_of);
let rules = matching_rules_typed(match_fact, &inner.universal_rules);
for rule in rules {
for typed_concl in &rule.typed_conclusions {
let Some(mut bindings) = unify_facts(typed_concl, match_fact) else {
continue;
};
let unbound_event_vars: Vec<String> = rule
.pattern_var_names
.iter()
.filter(|pv| pv.starts_with("ev__") && !bindings.contains_key(pv.as_str()))
.cloned()
.collect();
if !unbound_event_vars.is_empty() {
let mut per_var_candidates: Vec<Vec<GroundTerm>> = Vec::new();
for ev_var in &unbound_event_vars {
let single_var_cond_indices: Vec<usize> = rule
.typed_conditions
.iter()
.enumerate()
.filter(|(_, ct)| {
stored_fact_contains_var(ct, ev_var)
&& unbound_event_vars.iter().all(|other| {
other == ev_var || !stored_fact_contains_var(ct, other)
})
})
.map(|(i, _)| i)
.collect();
if single_var_cond_indices.is_empty() {
per_var_candidates.push(ensure_candidates(candidates_slot, inner).to_vec());
} else {
let cand = ensure_candidates(candidates_slot, inner).to_vec();
let filtered = filter_event_candidates(
rule,
ev_var,
&bindings,
&single_var_cond_indices,
&cand,
inner,
depth,
visited,
tense_fact,
);
per_var_candidates.push(filtered);
}
}
if per_var_candidates.iter().any(|pvc| pvc.is_empty()) {
continue;
}
let mut found = false;
let mut combo_pending = None;
for combo in TypedMultiCartesian::new(&per_var_candidates, inner.cancel.clone()) {
for (i, ev_var) in unbound_event_vars.iter().enumerate() {
bindings.insert(ev_var.clone(), combo[i].clone());
}
let joined_vars = bind_join_vars_from_index(rule, &mut bindings, inner);
let mut all_hold = true;
let mut pending_here = None;
for (idx, ct) in rule.typed_conditions.iter().enumerate() {
let cs = match tense_fact {
Some(src) => apply_tense_to_fact(&substitute_fact(ct, &bindings), src),
None => substitute_fact(ct, &bindings),
};
let result = check_predicate_in_kb_typed(&cs, inner, depth + 1, visited);
let verdict = if rule.negated_condition_indices.contains(&idx) {
negate_result(result)
} else {
result
};
if verdict.is_false() {
all_hold = false;
pending_here = None;
break;
}
if !verdict.is_true() {
all_hold = false;
pending_here = prefer_non_definitive(pending_here, verdict);
}
}
fold_negated_groups(
rule,
&bindings,
inner,
candidates_slot,
depth,
visited,
&mut all_hold,
&mut pending_here,
tense_fact,
);
if all_hold {
found = true;
break;
}
combo_pending = pending_here.or(combo_pending);
for ev_var in &unbound_event_vars {
bindings.remove(ev_var.as_str());
}
for v in &joined_vars {
bindings.remove(v.as_str());
}
}
if !found {
if let Some(r) = combo_pending {
*best_result = prefer_non_definitive(best_result.take(), r);
}
continue;
}
}
let mut all_conditions_hold = true;
let mut rule_pending = None;
for (idx, ct) in rule.typed_conditions.iter().enumerate() {
let cs = match tense_fact {
Some(src) => apply_tense_to_fact(&substitute_fact(ct, &bindings), src),
None => substitute_fact(ct, &bindings),
};
let result = check_predicate_in_kb_typed(&cs, inner, depth + 1, visited);
let verdict = if rule.negated_condition_indices.contains(&idx) {
negate_result(result)
} else {
result
};
if verdict.is_false() {
all_conditions_hold = false;
rule_pending = None;
break;
}
if !verdict.is_true() {
all_conditions_hold = false;
rule_pending = prefer_non_definitive(rule_pending, verdict);
}
}
fold_negated_groups(
rule,
&bindings,
inner,
candidates_slot,
depth,
visited,
&mut all_conditions_hold,
&mut rule_pending,
tense_fact,
);
if all_conditions_hold {
let root = if S::RECORDING {
Some(emit_derived(
sink,
rule,
&bindings,
orig_fact,
inner,
depth,
visited,
tense_fact,
tense_label,
))
} else {
None
};
return Some(root);
}
if let Some(r) = rule_pending {
*best_result = prefer_non_definitive(best_result.take(), r);
}
}
}
None
}
const CYCLE_SKOLEM_SENTINEL: &str = "\u{1}__cyc_skolem__";
fn is_event_skolem_term(t: &GroundTerm) -> bool {
match t {
GroundTerm::SkolemFn(_, _) => true,
GroundTerm::Constant(s) => s
.strip_prefix("sk_")
.is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())),
_ => false,
}
}
fn cycle_key(fact: &StoredFact) -> StoredFact {
let gf = fact.inner();
if !gf.args.iter().any(is_event_skolem_term) {
return fact.clone();
}
let args = gf
.args
.iter()
.map(|a| {
if is_event_skolem_term(a) {
GroundTerm::Constant(CYCLE_SKOLEM_SENTINEL.to_string())
} else {
a.clone()
}
})
.collect();
StoredFact::with_tense_from(GroundFact::new(gf.relation.clone(), args), fact)
}
fn try_backward_chain_core<S: TraceSink>(
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
sink: &mut S,
) -> (QueryResult, Option<u32>) {
if check_cancelled(inner).is_err() {
return (QueryResult::False, None);
}
if depth >= inner.max_chain_depth {
if fact.relation() != nibli_types::relations::IDENTITY
&& inner.equivalence_parent.is_empty()
&& !any_rule_conclusion_unifies(fact, inner)
{
return (QueryResult::False, None);
}
return (QueryResult::ResourceExceeded(ResourceKind::Depth), None);
}
if !visited.insert(cycle_key(fact)) {
inner.cycle_cut_epoch.set(inner.cycle_cut_epoch.get() + 1);
return (QueryResult::Unknown(UnknownReason::CycleCut), None);
}
let mut candidates_slot: Option<Vec<GroundTerm>> = None;
let rules = matching_rules_typed(fact, &inner.universal_rules);
let is_traced = inner.traced_predicates.contains(fact.relation());
if is_traced && !rules.is_empty() {
let indent = " ".repeat(depth);
eprintln!(
"[Trace] {}depth={} backward-chain: {} ({} rule(s) to try)",
indent,
depth,
fact.to_display_string(),
rules.len()
);
}
let mut best_result = None;
if let Some(root) = process_phase(
fact,
None,
inner,
depth,
visited,
sink,
&mut candidates_slot,
&mut best_result,
) {
visited.remove(&cycle_key(fact));
return (QueryResult::True, root);
}
if let Some(bare_fact) = strip_tense_from_fact(fact) {
if let Some(root) = process_phase(
&bare_fact,
Some(fact),
inner,
depth,
visited,
sink,
&mut candidates_slot,
&mut best_result,
) {
visited.remove(&cycle_key(fact));
return (QueryResult::True, root);
}
}
visited.remove(&cycle_key(fact));
(best_result.unwrap_or(QueryResult::False), None)
}
pub(super) fn try_backward_chain_typed(
fact: &StoredFact,
inner: &KnowledgeBaseInner,
depth: usize,
visited: &mut HashSet<StoredFact>,
) -> QueryResult {
try_backward_chain_core(fact, inner, depth, visited, &mut NoOpSink).0
}
fn strip_tense_from_fact(fact: &StoredFact) -> Option<StoredFact> {
match fact {
StoredFact::Past(f) | StoredFact::Present(f) | StoredFact::Future(f) => {
Some(StoredFact::Bare(f.clone()))
}
_ => None,
}
}
fn apply_tense_to_fact(fact: &StoredFact, source: &StoredFact) -> StoredFact {
match source {
StoredFact::Past(_) => match fact {
StoredFact::Bare(f) => StoredFact::Past(f.clone()),
other => other.clone(),
},
StoredFact::Present(_) => match fact {
StoredFact::Bare(f) => StoredFact::Present(f.clone()),
other => other.clone(),
},
StoredFact::Future(_) => match fact {
StoredFact::Bare(f) => StoredFact::Future(f.clone()),
other => other.clone(),
},
_ => fact.clone(),
}
}
struct TypedMultiCartesian<'a> {
sets: &'a [Vec<GroundTerm>],
indices: Vec<usize>,
done: bool,
cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
}
impl<'a> TypedMultiCartesian<'a> {
fn new(
sets: &'a [Vec<GroundTerm>],
cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
) -> Self {
let done = sets.iter().any(|s| s.is_empty());
Self {
sets,
indices: vec![0; sets.len()],
done,
cancel,
}
}
}
impl<'a> Iterator for TypedMultiCartesian<'a> {
type Item = Vec<GroundTerm>;
fn next(&mut self) -> Option<Self::Item> {
if self
.cancel
.as_ref()
.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
{
return None;
}
if self.done || self.sets.is_empty() {
if self.sets.is_empty() && !self.done {
self.done = true;
return Some(vec![]);
}
return None;
}
let combo: Vec<GroundTerm> = self
.indices
.iter()
.enumerate()
.map(|(set_idx, &item_idx)| self.sets[set_idx][item_idx].clone())
.collect();
let mut carry = true;
for i in (0..self.sets.len()).rev() {
if carry {
self.indices[i] += 1;
if self.indices[i] >= self.sets[i].len() {
self.indices[i] = 0;
} else {
carry = false;
}
}
}
if carry {
self.done = true;
}
Some(combo)
}
}
pub(super) fn trace_predicate_provenance_typed(
fact: &StoredFact,
inner: &KnowledgeBaseInner,
steps: &mut Vec<ProofStep>,
depth: usize,
memo: &mut HashMap<String, u32>,
visited: &mut HashSet<StoredFact>,
) -> u32 {
let display = fact.to_display_string();
if let Some(&cached_idx) = memo.get(&display) {
let idx = steps.len() as u32;
steps.push(ProofStep {
rule: ProofRule::ProofRef { fact: display },
holds: steps[cached_idx as usize].holds,
children: vec![cached_idx],
});
return idx;
}
if inner.fact_store.contains(fact) {
let idx = steps.len() as u32;
steps.push(ProofStep {
rule: ProofRule::Asserted {
fact: display.clone(),
},
holds: true,
children: vec![],
});
memo.insert(display, idx);
return idx;
}
if !inner.equivalence_parent.is_empty() {
if let Some(variant) = typed_fact_is_asserted_with_equivalence(fact, inner) {
let equals_note = equals_substitution_note(fact, &variant);
let substituted = variant.to_display_string();
let child = trace_predicate_provenance_typed(
&variant,
inner,
steps,
depth,
memo,
&mut HashSet::new(),
);
let idx = steps.len() as u32;
steps.push(ProofStep {
rule: ProofRule::EqualitySubstitution {
original: display.clone(),
equality_facts: equals_note,
substituted,
},
holds: true,
children: vec![child],
});
memo.insert(display, idx);
return idx;
}
}
if depth < inner.max_chain_depth {
let (_verdict, root) = try_backward_chain_core::<RecordingSink>(
fact,
inner,
depth,
visited,
&mut RecordingSink { steps, memo },
);
if let Some(idx) = root {
memo.insert(display, idx);
return idx;
}
}
if !inner.equivalence_parent.is_empty() && fact.relation() != nibli_types::relations::IDENTITY {
let gf = fact.inner();
let equiv_args: Vec<Vec<GroundTerm>> = gf
.args
.iter()
.map(|arg| {
get_equivalence_class_readonly(
&inner.equivalence_parent,
&inner.equivalence_classes,
arg,
)
})
.collect();
if equiv_args.iter().any(|cls| cls.len() > 1) {
let mut satisfying: Option<StoredFact> = None;
let owns_budget = inner.du_variant_budget.get().is_none();
if owns_budget {
inner.du_variant_budget.set(Some(DU_VARIANT_BOUND));
}
for combo in CartesianProduct::new(&equiv_args) {
match inner.du_variant_budget.get() {
Some(0) | None => break,
Some(b) => inner.du_variant_budget.set(Some(b - 1)),
}
let variant_gf = GroundFact::new(gf.relation.clone(), combo);
let variant = StoredFact::with_tense_from(variant_gf, fact);
if variant != *fact
&& check_predicate_in_kb_typed(&variant, inner, depth, &mut HashSet::new())
.is_true()
{
satisfying = Some(variant);
break;
}
}
if owns_budget {
inner.du_variant_budget.set(None);
}
if let Some(variant) = satisfying {
let equals_note = equals_substitution_note(fact, &variant);
let substituted = variant.to_display_string();
let child = trace_predicate_provenance_typed(
&variant,
inner,
steps,
depth,
memo,
&mut HashSet::new(),
);
let idx = steps.len() as u32;
steps.push(ProofStep {
rule: ProofRule::EqualitySubstitution {
original: display.clone(),
equality_facts: equals_note,
substituted,
},
holds: true,
children: vec![child],
});
memo.insert(display, idx);
return idx;
}
}
}
let idx = steps.len() as u32;
steps.push(ProofStep {
rule: ProofRule::PredicateNotFound { predicate: display },
holds: false,
children: vec![],
});
idx
}
#[cfg(test)]
mod combiner_tests {
use super::*;
use QueryResult::{False as F, True as T};
fn unk_a() -> QueryResult {
QueryResult::Unknown(UnknownReason::CycleCut)
}
fn unk_b() -> QueryResult {
QueryResult::Unknown(UnknownReason::IncompleteKnowledge)
}
fn re_a() -> QueryResult {
QueryResult::ResourceExceeded(ResourceKind::Fuel)
}
fn re_b() -> QueryResult {
QueryResult::ResourceExceeded(ResourceKind::Depth)
}
#[test]
fn conjunction_truth_table() {
assert_eq!(combine_conjunction(T, T), T);
assert_eq!(combine_conjunction(F, T), F);
assert_eq!(combine_conjunction(T, F), F);
assert_eq!(combine_conjunction(F, unk_a()), F);
assert_eq!(combine_conjunction(unk_a(), F), F);
assert_eq!(combine_conjunction(T, unk_a()), unk_a());
assert_eq!(combine_conjunction(unk_a(), T), unk_a());
assert_eq!(combine_conjunction(T, re_a()), re_a());
assert_eq!(combine_conjunction(re_a(), T), re_a());
assert_eq!(combine_conjunction(unk_a(), unk_b()), unk_a());
assert_eq!(combine_conjunction(unk_a(), re_a()), re_a());
assert_eq!(combine_conjunction(re_a(), unk_a()), re_a());
assert_eq!(combine_conjunction(re_a(), re_b()), re_a());
}
#[test]
fn disjunction_truth_table() {
assert_eq!(combine_disjunction(F, F), F);
assert_eq!(combine_disjunction(T, F), T);
assert_eq!(combine_disjunction(F, T), T);
assert_eq!(combine_disjunction(T, unk_a()), T);
assert_eq!(combine_disjunction(unk_a(), T), T);
assert_eq!(combine_disjunction(F, unk_a()), unk_a());
assert_eq!(combine_disjunction(unk_a(), F), unk_a());
assert_eq!(combine_disjunction(F, re_a()), re_a());
assert_eq!(combine_disjunction(re_a(), F), re_a());
assert_eq!(combine_disjunction(unk_a(), unk_b()), unk_a());
assert_eq!(combine_disjunction(unk_a(), re_a()), re_a());
assert_eq!(combine_disjunction(re_a(), unk_a()), re_a());
assert_eq!(combine_disjunction(re_a(), re_b()), re_a());
}
fn all_results() -> Vec<QueryResult> {
use ResourceKind::*;
use UnknownReason::*;
vec![
T,
F,
QueryResult::Unknown(CycleCut),
QueryResult::Unknown(IncompleteKnowledge),
QueryResult::Unknown(NafDependent),
QueryResult::Unknown(BackendUnavailable),
QueryResult::Unknown(NonFinite),
QueryResult::ResourceExceeded(Depth),
QueryResult::ResourceExceeded(Fuel),
QueryResult::ResourceExceeded(Memory),
]
}
#[test]
fn exhaustive_soundness_matches_lean_model() {
let all = all_results();
for a in &all {
let na = negate_result(a.clone());
let expected_neg = match a {
QueryResult::True => QueryResult::False,
QueryResult::False => QueryResult::True,
QueryResult::Unknown(_) => QueryResult::Unknown(UnknownReason::NafDependent),
QueryResult::ResourceExceeded(k) => QueryResult::ResourceExceeded(k.clone()),
};
assert_eq!(na, expected_neg, "negate_result({a:?})");
assert_eq!(
na.is_definitive(),
a.is_definitive(),
"negation must preserve definiteness for {a:?}"
);
for b in &all {
let conj = combine_conjunction(a.clone(), b.clone());
let disj = combine_disjunction(a.clone(), b.clone());
assert_eq!(
conj.is_true(),
a.is_true() && b.is_true(),
"conj TRUE iff both TRUE: {a:?} ∧ {b:?}"
);
assert_eq!(
conj.is_false(),
a.is_false() || b.is_false(),
"conj FALSE iff some FALSE: {a:?} ∧ {b:?}"
);
assert_eq!(
disj.is_true(),
a.is_true() || b.is_true(),
"disj TRUE iff some TRUE: {a:?} ∨ {b:?}"
);
assert_eq!(
disj.is_false(),
a.is_false() && b.is_false(),
"disj FALSE iff both FALSE: {a:?} ∨ {b:?}"
);
if !a.is_definitive() && !b.is_definitive() {
let re_present = matches!(a, QueryResult::ResourceExceeded(_))
|| matches!(b, QueryResult::ResourceExceeded(_));
assert_eq!(
matches!(conj, QueryResult::ResourceExceeded(_)),
re_present,
"conj RE-precedence: {a:?} ∧ {b:?}"
);
assert_eq!(
matches!(disj, QueryResult::ResourceExceeded(_)),
re_present,
"disj RE-precedence: {a:?} ∨ {b:?}"
);
}
}
}
}
}