use super::*;
use std::hash::{Hash, Hasher};
fn rule_dedup_hash(tag: u8, conditions: &[StoredFact], conclusions: &[StoredFact]) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
tag.hash(&mut hasher);
conditions.hash(&mut hasher);
conclusions.hash(&mut hasher);
hasher.finish()
}
pub(super) fn is_skdep(gt: &GroundTerm) -> bool {
matches!(gt, GroundTerm::PatternVar(s) if s.starts_with(SKDEP_PREFIX))
}
pub(super) fn skdep_base_name(gt: &GroundTerm) -> Option<&str> {
match gt {
GroundTerm::PatternVar(s) => s.strip_prefix(SKDEP_PREFIX),
_ => None,
}
}
pub(super) fn collect_exists_for_skolem(
buffer: &LogicBuffer,
node_id: u32,
subs: &mut HashMap<String, GroundTerm>,
enclosing_universals: &mut Vec<String>,
counter: &mut usize,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::ExistsNode((v, body)) => {
if !subs.contains_key(v.as_str()) {
if enclosing_universals.is_empty() {
let sk = format!("sk_{}", *counter);
*counter += 1;
subs.insert(v.clone(), GroundTerm::Constant(sk));
} else {
let base = format!("sk_{}", *counter);
*counter += 1;
let placeholder = format!("{}{}", SKDEP_PREFIX, base);
subs.insert(v.clone(), GroundTerm::PatternVar(placeholder));
}
}
collect_exists_for_skolem(buffer, *body, subs, enclosing_universals, counter);
}
LogicNode::ForAllNode((v, body)) => {
enclosing_universals.push(v.clone());
collect_exists_for_skolem(buffer, *body, subs, enclosing_universals, counter);
enclosing_universals.pop();
}
LogicNode::AndNode((l, r)) | LogicNode::OrNode((l, r)) => {
collect_exists_for_skolem(buffer, *l, subs, enclosing_universals, counter);
collect_exists_for_skolem(buffer, *r, subs, enclosing_universals, counter);
}
LogicNode::NotNode(inner) => {
collect_exists_for_skolem(buffer, *inner, subs, enclosing_universals, counter);
}
LogicNode::CountNode((v, count, body)) => {
if *count > 0 && !subs.contains_key(v.as_str()) {
if enclosing_universals.is_empty() {
let sk = format!("sk_{}", *counter);
*counter += 1;
subs.insert(v.clone(), GroundTerm::Constant(sk));
} else {
let base = format!("sk_{}", *counter);
*counter += 1;
let placeholder = format!("{}{}", SKDEP_PREFIX, base);
subs.insert(v.clone(), GroundTerm::PatternVar(placeholder));
}
}
collect_exists_for_skolem(buffer, *body, subs, enclosing_universals, counter);
}
LogicNode::Predicate(_) | LogicNode::ComputeNode(_) => {}
LogicNode::PastNode(inner)
| LogicNode::PresentNode(inner)
| LogicNode::FutureNode(inner)
| LogicNode::ObligatoryNode(inner)
| LogicNode::PermittedNode(inner) => {
collect_exists_for_skolem(buffer, *inner, subs, enclosing_universals, counter);
}
}
}
pub(super) fn decompose_implication(buffer: &LogicBuffer, body_id: u32) -> Option<(Vec<u32>, u32)> {
let mut conditions = Vec::new();
let mut current = body_id;
loop {
let Ok(node) = get_node(buffer, current) else {
break;
};
match node {
LogicNode::OrNode((left, right)) => {
let Ok(left_node) = get_node(buffer, *left) else {
break;
};
match left_node {
LogicNode::NotNode(inner) => {
conditions.push(*inner);
current = *right;
}
_ => break,
}
}
_ => break,
}
}
if conditions.is_empty() {
None
} else {
Some((conditions, current))
}
}
fn prenex_flatten(buffer: &LogicBuffer, body_id: u32) -> (Vec<String>, Vec<u32>, u32) {
let mut extra_universals = Vec::new();
let mut conditions = Vec::new();
let mut current = body_id;
loop {
if let Some((conds, rest)) = decompose_implication(buffer, current) {
conditions.extend(conds);
current = rest;
}
if let Ok(LogicNode::ForAllNode((y, inner_body))) = get_node(buffer, current) {
extra_universals.push(y.clone());
current = *inner_body;
continue;
}
break;
}
(extra_universals, conditions, current)
}
pub(super) fn collect_condition_exists(
buffer: &LogicBuffer,
node_id: u32,
exists_vars: &mut HashSet<String>,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::ExistsNode((v, body)) => {
exists_vars.insert(v.clone());
collect_condition_exists(buffer, *body, exists_vars);
}
LogicNode::AndNode((l, r)) => {
collect_condition_exists(buffer, *l, exists_vars);
collect_condition_exists(buffer, *r, exists_vars);
}
LogicNode::PastNode(inner)
| LogicNode::PresentNode(inner)
| LogicNode::FutureNode(inner)
| LogicNode::ObligatoryNode(inner)
| LogicNode::PermittedNode(inner) => {
collect_condition_exists(buffer, *inner, exists_vars);
}
_ => {}
}
}
pub(super) fn flatten_conjuncts_through_exists(
buffer: &LogicBuffer,
node_id: u32,
condition_exists: &HashSet<String>,
tense: Option<&'static str>,
) -> Vec<(u32, Option<&'static str>)> {
let Ok(node) = get_node(buffer, node_id) else {
return vec![(node_id, tense)];
};
match node {
LogicNode::AndNode((l, r)) => {
let mut result = flatten_conjuncts_through_exists(buffer, *l, condition_exists, tense);
result.extend(flatten_conjuncts_through_exists(
buffer,
*r,
condition_exists,
tense,
));
result
}
LogicNode::ExistsNode((v, body)) if condition_exists.contains(v.as_str()) => {
flatten_conjuncts_through_exists(buffer, *body, condition_exists, tense)
}
LogicNode::PastNode(inner) => {
flatten_conjuncts_through_exists(buffer, *inner, condition_exists, Some("Past"))
}
LogicNode::PresentNode(inner) => {
flatten_conjuncts_through_exists(buffer, *inner, condition_exists, Some("Present"))
}
LogicNode::FutureNode(inner) => {
flatten_conjuncts_through_exists(buffer, *inner, condition_exists, Some("Future"))
}
LogicNode::ObligatoryNode(inner) => {
flatten_conjuncts_through_exists(buffer, *inner, condition_exists, Some("Obligatory"))
}
LogicNode::PermittedNode(inner) => {
flatten_conjuncts_through_exists(buffer, *inner, condition_exists, Some("Permitted"))
}
_ => vec![(node_id, tense)],
}
}
pub(super) const MAX_DNF_CLAUSES: usize = 32;
fn dnf_cross_product(
a: Vec<Vec<u32>>,
b: &[Vec<u32>],
cap: usize,
) -> Result<Vec<Vec<u32>>, String> {
let count = a.len().saturating_mul(b.len());
if count > cap {
return Err(format!(
"disjunctive rule antecedent expands to {count} conjunctive clauses, exceeding the \
cap of {cap}; restate with fewer alternations (ja/ga) to keep rule compilation \
bounded."
));
}
let mut out = Vec::with_capacity(count);
for x in &a {
for y in b {
let mut clause = x.clone();
clause.extend(y.iter().copied());
out.push(clause);
}
}
Ok(out)
}
fn dnf_of_node(buffer: &LogicBuffer, id: u32, cap: usize) -> Result<Vec<Vec<u32>>, String> {
match get_node(buffer, id) {
Ok(LogicNode::OrNode((l, r))) => {
let mut out = dnf_of_node(buffer, *l, cap)?;
out.extend(dnf_of_node(buffer, *r, cap)?);
if out.len() > cap {
return Err(format!(
"disjunctive rule antecedent expands to {} conjunctive clauses, exceeding \
the cap of {cap}; restate with fewer alternations (ja/ga).",
out.len()
));
}
Ok(out)
}
Ok(LogicNode::AndNode((l, r))) => {
let lc = dnf_of_node(buffer, *l, cap)?;
let rc = dnf_of_node(buffer, *r, cap)?;
dnf_cross_product(lc, &rc, cap)
}
_ => Ok(vec![vec![id]]),
}
}
pub(super) fn dnf_condition_clauses(
buffer: &LogicBuffer,
condition_ids: &[u32],
cap: usize,
) -> Result<Vec<Vec<u32>>, String> {
let mut clauses: Vec<Vec<u32>> = vec![Vec::new()];
for &cid in condition_ids {
let node_clauses = dnf_of_node(buffer, cid, cap)?;
clauses = dnf_cross_product(clauses, &node_clauses, cap)?;
}
Ok(clauses)
}
fn detect_negated_exists_group(buffer: &LogicBuffer, cond_id: u32) -> Option<(String, Vec<u32>)> {
let LogicNode::NotNode(inner) = get_node(buffer, cond_id).ok()? else {
return None;
};
let LogicNode::ExistsNode((ev, body)) = get_node(buffer, *inner).ok()? else {
return None;
};
let mut leaves = Vec::new();
if !flatten_group_leaves(buffer, *body, ev.as_str(), &mut leaves) || leaves.is_empty() {
return None;
}
Some((ev.clone(), leaves))
}
fn flatten_group_leaves(buffer: &LogicBuffer, id: u32, ev: &str, out: &mut Vec<u32>) -> bool {
match get_node(buffer, id) {
Ok(LogicNode::AndNode((l, r))) => {
flatten_group_leaves(buffer, *l, ev, out) && flatten_group_leaves(buffer, *r, ev, out)
}
Ok(LogicNode::ExistsNode((v, b))) if v.as_str() == ev => {
flatten_group_leaves(buffer, *b, ev, out)
}
Ok(LogicNode::Predicate(_)) | Ok(LogicNode::ComputeNode(_)) => {
out.push(id);
true
}
_ => false,
}
}
fn flatten_consequent(
buffer: &LogicBuffer,
node_id: u32,
skolem_subs: &HashMap<String, GroundTerm>,
tense: Option<&'static str>,
) -> Vec<(u32, Option<&'static str>)> {
let Ok(node) = get_node(buffer, node_id) else {
return vec![(node_id, tense)];
};
match node {
LogicNode::ExistsNode((v, body)) if skolem_subs.contains_key(v.as_str()) => {
flatten_consequent(buffer, *body, skolem_subs, tense)
}
LogicNode::AndNode((l, r)) => {
let mut result = flatten_consequent(buffer, *l, skolem_subs, tense);
result.extend(flatten_consequent(buffer, *r, skolem_subs, tense));
result
}
LogicNode::PastNode(inner) => flatten_consequent(buffer, *inner, skolem_subs, Some("Past")),
LogicNode::PresentNode(inner) => {
flatten_consequent(buffer, *inner, skolem_subs, Some("Present"))
}
LogicNode::FutureNode(inner) => {
flatten_consequent(buffer, *inner, skolem_subs, Some("Future"))
}
LogicNode::ObligatoryNode(inner) => {
flatten_consequent(buffer, *inner, skolem_subs, Some("Obligatory"))
}
LogicNode::PermittedNode(inner) => {
flatten_consequent(buffer, *inner, skolem_subs, Some("Permitted"))
}
_ => vec![(node_id, tense)],
}
}
fn collect_disjunct_branches(buffer: &LogicBuffer, node_id: u32, out: &mut Vec<u32>) {
match get_node(buffer, node_id) {
Ok(LogicNode::OrNode((l, r))) => {
collect_disjunct_branches(buffer, *l, out);
collect_disjunct_branches(buffer, *r, out);
}
_ => out.push(node_id),
}
}
fn build_positive_clause_conditions(
buffer: &LogicBuffer,
clause: &[u32],
pattern_vars: &HashMap<String, String>,
ground_skolems: &HashMap<String, String>,
dependent_skolems: &HashMap<String, (String, Vec<String>)>,
rule_desc: &str,
) -> Result<Vec<StoredFact>, String> {
let mut clause_exists: HashSet<String> = HashSet::new();
for &lid in clause {
collect_condition_exists(buffer, lid, &mut clause_exists);
}
let mut conds = Vec::new();
for &lid in clause {
for (cid, tense) in flatten_conjuncts_through_exists(buffer, lid, &clause_exists, None) {
if detect_negated_exists_group(buffer, cid).is_some() {
return Err(format!(
"cannot represent disjunctive conclusion for {rule_desc}: a negated \
restrictor in the antecedent is unsupported. Rejecting to preserve soundness."
));
}
match build_rule_template_fact_with_negation(
buffer,
cid,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
) {
Some((fact, false)) => conds.push(fact),
Some((_, true)) => {
return Err(format!(
"cannot represent disjunctive conclusion for {rule_desc}: a negated \
antecedent condition is unsupported. Rejecting to preserve soundness."
));
}
None => {
return Err(format!(
"cannot represent disjunctive conclusion for {rule_desc}: an antecedent \
atom is not a flat predicate. Rejecting to preserve soundness."
));
}
}
}
}
Ok(conds)
}
pub(super) fn collect_and_note_constants(
buffer: &LogicBuffer,
node_id: u32,
inner: &mut KnowledgeBaseInner,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::Predicate((_, args)) | LogicNode::ComputeNode((_, args)) => {
for arg in args {
match arg {
LogicalTerm::Constant(c) => inner.note_entity(c),
LogicalTerm::Description(d) => inner.note_description(d),
LogicalTerm::Number(n) => inner.note_number(*n),
_ => {}
}
}
}
LogicNode::AndNode((l, r)) | LogicNode::OrNode((l, r)) => {
collect_and_note_constants(buffer, *l, inner);
collect_and_note_constants(buffer, *r, inner);
}
LogicNode::NotNode(inner_node)
| LogicNode::ExistsNode((_, inner_node))
| LogicNode::ForAllNode((_, inner_node)) => {
collect_and_note_constants(buffer, *inner_node, inner);
}
LogicNode::CountNode((_, _, body)) => {
collect_and_note_constants(buffer, *body, inner);
}
LogicNode::PastNode(inner_node)
| LogicNode::PresentNode(inner_node)
| LogicNode::FutureNode(inner_node)
| LogicNode::ObligatoryNode(inner_node)
| LogicNode::PermittedNode(inner_node) => {
collect_and_note_constants(buffer, *inner_node, inner);
}
}
}
pub(super) fn register_rule(
inner: &mut KnowledgeBaseInner,
label: String,
pattern_var_names: Vec<String>,
typed_conditions: Vec<StoredFact>,
typed_conclusions: Vec<StoredFact>,
negated_condition_indices: Vec<usize>,
negated_exists_groups: Vec<NegatedExistsGroup>,
forward: bool,
) -> Result<(), String> {
if typed_conclusions.is_empty() {
return Err(
"rule with no extractable conclusions — refusing to register an unfireable rule"
.to_string(),
);
}
let group_edge_count: usize = negated_exists_groups
.iter()
.map(|g| g.conditions.len())
.sum();
for concl in &typed_conclusions {
let concl_rel = concl.relation().to_string();
for (idx, cond) in typed_conditions.iter().enumerate() {
let is_neg = negated_condition_indices.contains(&idx);
inner
.pred_dep_graph
.entry(concl_rel.clone())
.or_default()
.push((cond.relation().to_string(), is_neg));
}
for group in &negated_exists_groups {
for cond in &group.conditions {
inner
.pred_dep_graph
.entry(concl_rel.clone())
.or_default()
.push((cond.relation().to_string(), true));
}
}
}
if !inner.rebuilding {
if let Err(e) = check_stratification(&inner.pred_dep_graph) {
for concl in &typed_conclusions {
let concl_rel = concl.relation();
if let Some(edges) = inner.pred_dep_graph.get_mut(concl_rel) {
for _ in 0..(typed_conditions.len() + group_edge_count) {
edges.pop();
}
if edges.is_empty() {
inner.pred_dep_graph.remove(concl_rel);
}
}
}
return Err(e);
}
}
let rule = UniversalRuleRecord {
label,
typed_conditions,
typed_conclusions,
pattern_var_names,
negated_condition_indices,
negated_exists_groups,
forward,
priority: 0, };
let rc = Arc::new(rule);
for concl in &rc.typed_conclusions {
let bucket = inner
.universal_rules
.entry(concl.relation().to_string())
.or_default();
bucket.push(Arc::clone(&rc));
sort_rule_bucket(bucket);
}
if let Some(assertion_id) = inner.current_assertion_id {
let pred_keys: Vec<String> = rc
.typed_conclusions
.iter()
.map(|c| c.relation().to_string())
.collect();
inner
.rule_source_map
.entry(assertion_id)
.or_default()
.extend(pred_keys);
}
invalidate_materialization(inner);
Ok(())
}
pub(super) fn compute_sccs(graph: &HashMap<String, Vec<(String, bool)>>) -> Vec<Vec<String>> {
use std::collections::{BTreeSet, HashSet};
let mut node_set: BTreeSet<&str> = BTreeSet::new();
for (k, edges) in graph {
node_set.insert(k.as_str());
for (dep, _) in edges {
node_set.insert(dep.as_str());
}
}
let nodes: Vec<&str> = node_set.into_iter().collect();
let neighbors = |n: &str| -> Vec<&str> {
match graph.get(n) {
Some(edges) => {
let mut out: Vec<&str> = edges.iter().map(|(d, _)| d.as_str()).collect();
out.sort_unstable();
out
}
None => Vec::new(),
}
};
let mut index_of: HashMap<&str, usize> = HashMap::new();
let mut lowlink: HashMap<&str, usize> = HashMap::new();
let mut on_stack: HashSet<&str> = HashSet::new();
let mut tarjan_stack: Vec<&str> = Vec::new();
let mut next_index = 0usize;
let mut sccs: Vec<Vec<String>> = Vec::new();
for &start in &nodes {
if index_of.contains_key(start) {
continue;
}
index_of.insert(start, next_index);
lowlink.insert(start, next_index);
next_index += 1;
tarjan_stack.push(start);
on_stack.insert(start);
let mut work: Vec<(&str, usize, Vec<&str>)> = vec![(start, 0, neighbors(start))];
while let Some(&(node, _, _)) = work.last() {
let cursor = work.last().unwrap().1;
let nlen = work.last().unwrap().2.len();
if cursor < nlen {
let w = work.last().unwrap().2[cursor];
work.last_mut().unwrap().1 += 1;
if !index_of.contains_key(w) {
index_of.insert(w, next_index);
lowlink.insert(w, next_index);
next_index += 1;
tarjan_stack.push(w);
on_stack.insert(w);
work.push((w, 0, neighbors(w)));
} else if on_stack.contains(w) {
let wi = index_of[w];
let cur = lowlink[node];
lowlink.insert(node, cur.min(wi));
}
} else {
if lowlink[node] == index_of[node] {
let mut comp: Vec<String> = Vec::new();
while let Some(w) = tarjan_stack.pop() {
on_stack.remove(w);
comp.push(w.to_string());
if w == node {
break;
}
}
comp.sort();
sccs.push(comp);
}
work.pop();
if let Some(&(parent, _, _)) = work.last() {
let cur = lowlink[parent];
let child = lowlink[node];
lowlink.insert(parent, cur.min(child));
}
}
}
}
sccs
}
fn check_stratification(graph: &HashMap<String, Vec<(String, bool)>>) -> Result<(), String> {
for scc in compute_sccs(graph) {
let members: std::collections::HashSet<&str> = scc.iter().map(|s| s.as_str()).collect();
for node in &scc {
if let Some(edges) = graph.get(node.as_str()) {
for (dep, is_neg) in edges {
if *is_neg && members.contains(dep.as_str()) {
return Err(format!(
"Unstratifiable negation: strongly-connected component \
containing '{}' -> '{}' (negative)",
node, dep
));
}
}
}
}
}
Ok(())
}
fn term_contains_pattern_var(t: &GroundTerm) -> bool {
match t {
GroundTerm::PatternVar(_) => true,
GroundTerm::SkolemFn(_, dep) => term_contains_pattern_var(dep),
GroundTerm::DepPair(a, b) => term_contains_pattern_var(a) || term_contains_pattern_var(b),
_ => false,
}
}
fn unassert_typed_fact(fact: &StoredFact, inner: &mut KnowledgeBaseInner) {
if !inner.fact_store.remove(fact) {
return;
}
let gf = fact.inner();
for (pos, arg) in gf.args.iter().enumerate() {
if let Some(by_val) = inner
.arg_position_index
.get_mut(&(gf.relation.clone(), pos))
&& let Some(leaf) = by_val.get_mut(arg)
{
leaf.retain(|f| f != fact);
}
}
clear_typed_pred_cache(inner);
invalidate_materialization(inner);
inner.domain_members_dirty = true;
}
pub(super) fn assert_typed_fact(fact: StoredFact, inner: &mut KnowledgeBaseInner) {
if fact.inner().args.iter().any(term_contains_pattern_var) {
eprintln!(
"[Groundness] Dropped non-ground fact '{}' (unbound pattern variable); \
stored facts must be ground.",
fact.to_display_string()
);
return;
}
let rel = fact.relation();
let arity = fact.inner().args.len();
if let Some(sig) = inner.predicate_registry.get(rel) {
if sig.arity != arity && !matches!(sig.source, SignatureSource::Synthetic) {
let source = match sig.source {
SignatureSource::Dictionary => "dictionary",
SignatureSource::Inferred => "inferred from first use",
SignatureSource::Synthetic => "engine-synthetic",
};
if inner.strict && !inner.rebuilding {
let violation = format!(
"arity mismatch: '{}' expects {} args ({}), got {} — fact '{}' rejected",
rel,
sig.arity,
source,
arity,
fact.to_display_string()
);
eprintln!("[Strict] {violation}");
inner.strict_violations.push(violation);
return;
}
eprintln!(
"[Arity Warning] '{}': expected {} args, got {} ({})",
rel, sig.arity, arity, source
);
}
} else {
let source = if crate::kb::is_synthetic_role_predicate(rel) {
SignatureSource::Synthetic
} else if nibli_lexicon::get_arity(rel).is_some() {
SignatureSource::Dictionary
} else {
SignatureSource::Inferred
};
inner.predicate_registry.insert(
rel.to_string(),
PredicateSignature {
arity,
source,
arg_sorts: vec![],
},
);
}
if let Some(sig) = inner.predicate_registry.get(rel) {
if !sig.arg_sorts.is_empty() && !inner.rebuilding {
let gf_check = fact.inner();
for (pos, arg) in gf_check.args.iter().enumerate() {
if pos >= sig.arg_sorts.len() {
break;
}
let expected_sort = &sig.arg_sorts[pos];
if expected_sort.is_empty() {
continue; }
if let GroundTerm::Constant(name) = arg {
if let Some(actual_sort) = inner.entity_sorts.get(name.as_str()) {
if !is_sort_compatible(&inner.sort_hierarchy, actual_sort, expected_sort) {
eprintln!(
"[Sort Warning] '{}' arg {}: entity '{}' has sort '{}', expected '{}'",
rel, pos, name, actual_sort, expected_sort
);
}
}
}
}
}
}
let rel_owned = rel.to_string();
let gf = fact.inner();
let was_new = !inner.fact_store.contains(&fact);
if was_new {
for (pos, arg) in gf.args.iter().enumerate() {
inner
.arg_position_index
.entry((gf.relation.clone(), pos))
.or_default()
.entry(arg.clone())
.or_default()
.push(fact.clone());
}
}
let fact_for_rollback = if inner.strict && !inner.rebuilding {
Some(fact.clone())
} else {
None
};
inner.fact_store.insert(fact);
clear_typed_pred_cache(inner);
invalidate_materialization(inner);
if !inner.integrity_constraints.is_empty() && !inner.rebuilding {
if let Some(violation) = check_constraints_for_predicate(&rel_owned, inner) {
if let Some(rejected) = fact_for_rollback.as_ref() {
let msg = format!(
"integrity constraint violated: {violation} — fact '{}' rejected",
rejected.to_display_string()
);
eprintln!("[Strict] {msg}");
inner.strict_violations.push(msg);
if was_new {
unassert_typed_fact(rejected, inner);
}
return;
}
eprintln!("[Constraint] {}", violation);
}
}
if !inner.rebuilding {
trigger_forward_rules(&rel_owned, inner);
}
}
const MAX_FORWARD_DEPTH: usize = 10;
fn trigger_forward_rules(new_rel: &str, inner: &mut KnowledgeBaseInner) {
if inner.forward_depth >= MAX_FORWARD_DEPTH {
return;
}
let mut forward_rules: Vec<Arc<UniversalRuleRecord>> = inner
.universal_rules
.values()
.flat_map(|v| v.iter())
.filter(|r| {
r.forward
&& r.negated_condition_indices.is_empty()
&& r.negated_exists_groups.is_empty()
&& r.typed_conditions.iter().any(|c| c.relation() == new_rel)
})
.cloned()
.collect();
forward_rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
if forward_rules.is_empty() {
return;
}
inner.forward_depth += 1;
let mut to_derive: Vec<StoredFact> = Vec::new();
for rule in &forward_rules {
for (cond_idx, cond_template) in rule.typed_conditions.iter().enumerate() {
if cond_template.relation() != new_rel {
continue;
}
if rule.negated_condition_indices.contains(&cond_idx) {
continue;
}
let matching_facts: Vec<StoredFact> = inner
.fact_store
.lookup_predicate(new_rel)
.map(|set| set.iter().cloned().collect())
.unwrap_or_default();
for fact in &matching_facts {
let Some(bindings) = unify_facts(cond_template, fact) else {
continue;
};
let all_others = rule
.typed_conditions
.iter()
.enumerate()
.filter(|(i, _)| *i != cond_idx)
.all(|(i, other)| {
let sub = substitute_fact(other, &bindings);
if rule.negated_condition_indices.contains(&i) {
!inner.fact_store.contains(&sub)
} else {
inner.fact_store.contains(&sub)
}
});
if all_others {
for concl in &rule.typed_conclusions {
let derived = substitute_fact(concl, &bindings);
if !inner.fact_store.contains(&derived) {
to_derive.push(derived);
}
}
}
}
}
}
to_derive.sort_by(|a, b| a.to_display_string().cmp(&b.to_display_string()));
for fact in to_derive {
if !inner.rebuilding {
eprintln!("[Forward] Derived: {}", fact.to_display_string());
}
assert_typed_fact(fact, inner);
}
inner.forward_depth -= 1;
}
fn atom_var_args(buffer: &LogicBuffer, node_id: u32) -> Vec<String> {
match get_node(buffer, node_id) {
Ok(LogicNode::Predicate((_, args))) | Ok(LogicNode::ComputeNode((_, args))) => args
.iter()
.filter_map(|t| match t {
LogicalTerm::Variable(v) => Some(v.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
fn register_clause_rule(
buffer: &LogicBuffer,
clause: &[u32],
branch_idx: usize,
clause_count: usize,
universals: &[String],
pattern_vars: &HashMap<String, String>,
pattern_var_names: &[String],
ground_skolems: &HashMap<String, String>,
dependent_skolems: &HashMap<String, (String, Vec<String>)>,
typed_concls: &[StoredFact],
rule_desc: &str,
inner: &mut KnowledgeBaseInner,
) -> Result<(), String> {
let clause_exists: Vec<String> = {
let mut s: HashSet<String> = HashSet::new();
for &lid in clause {
collect_condition_exists(buffer, lid, &mut s);
}
let mut v: Vec<String> = s.into_iter().collect();
v.sort();
v
};
let clause_exists_set: HashSet<String> = clause_exists.iter().cloned().collect();
let all_pattern_var_names: Vec<String> = {
let mut names = pattern_var_names.to_vec();
for var in &clause_exists {
if let Some(pvar) = pattern_vars.get(var) {
names.push(pvar.clone());
}
}
names
};
let mut all_conditions: Vec<(u32, Option<&str>)> = Vec::new();
for &lid in clause {
all_conditions.extend(flatten_conjuncts_through_exists(
buffer,
lid,
&clause_exists_set,
None,
));
}
let mut typed_conds: Vec<StoredFact> = Vec::new();
let mut negated_condition_indices: Vec<usize> = Vec::new();
let mut negated_exists_groups: Vec<NegatedExistsGroup> = Vec::new();
for &(cid, tense) in &all_conditions {
if let Some((ev_var, leaf_ids)) = detect_negated_exists_group(buffer, cid) {
let ev_pvar = format!("ev__{}", ev_var);
let mut group_pattern_vars: HashMap<String, String> = pattern_vars.clone();
group_pattern_vars.insert(ev_var.clone(), ev_pvar.clone());
let mut group_conditions = Vec::new();
for &lid in &leaf_ids {
match build_rule_template_fact(
buffer,
lid,
&group_pattern_vars,
ground_skolems,
dependent_skolems,
tense,
) {
Some(f) => group_conditions.push(f),
None => {
return Err(format!(
"cannot compile negated restrictor group for {rule_desc}: an \
inner atom is not a flat predicate. Rejecting the assertion \
to preserve soundness."
));
}
}
}
negated_exists_groups.push(NegatedExistsGroup {
conditions: group_conditions,
event_var: ev_pvar,
});
continue;
}
match build_rule_template_fact_with_negation(
buffer,
cid,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
) {
Some((fact, is_negated)) => {
if is_negated {
negated_condition_indices.push(typed_conds.len());
}
typed_conds.push(fact);
}
None => {
return Err(format!(
"cannot compile rule antecedent for {rule_desc}: an atom is not a \
flat predicate (tense, nested quantifier, or negated-complex \
antecedents are unsupported). Rejecting the assertion to preserve \
soundness rather than registering an under-conditioned rule."
));
}
}
}
let dedup_key = rule_dedup_hash(0, &typed_conds, typed_concls);
if !inner.known_rules.insert(dedup_key) {
if inner.diag_enabled() {
println!("[Rule] ∀{} already present, skipping", universals.join(","));
}
return Ok(());
}
if inner.diag_enabled() {
println!(
"[Rule] Compiled ∀{} to backward-chaining rule",
universals.join(",")
);
}
let base_label = build_typed_rule_label(&typed_conds, typed_concls);
let label = if clause_count > 1 {
format!(
"[branch {}/{}] {}",
branch_idx + 1,
clause_count,
base_label
)
} else {
base_label
};
if let Err(e) = register_rule(
inner,
label,
all_pattern_var_names,
typed_conds,
typed_concls.to_vec(),
negated_condition_indices,
negated_exists_groups,
false, ) {
eprintln!("[Stratification Error] {}", e);
return Err(e);
}
let is_description_universal =
!universals.is_empty() && universals.iter().all(|v| v.starts_with("_v"));
if branch_idx == 0 && is_description_universal && inner.existential_import {
let mut xp_subs: HashMap<String, GroundTerm> = HashMap::new();
for v in universals {
let xp_name = inner.fresh_skolem();
inner.note_entity(&xp_name);
inner.presupposition_witnesses.insert(xp_name.clone());
xp_subs.insert(v.clone(), GroundTerm::Constant(xp_name));
}
for (k, v) in ground_skolems {
xp_subs
.entry(k.clone())
.or_insert_with(|| GroundTerm::Constant(v.clone()));
}
for var in &clause_exists {
let ev_sk = inner.fresh_skolem();
if var.starts_with("_ev") {
inner.note_event_entity(&ev_sk);
} else {
inner.note_entity(&ev_sk);
}
xp_subs.insert(var.clone(), GroundTerm::Constant(ev_sk));
}
for &(cid, tense) in &all_conditions {
if let Some(fact) = build_stored_fact_from_node(buffer, cid, &xp_subs, tense) {
assert_typed_fact(fact, inner);
}
}
}
Ok(())
}
pub(super) fn compile_forall_to_rule(
buffer: &LogicBuffer,
node_id: u32,
skolem_subs: &HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
) -> Result<(), String> {
let mut universals: Vec<String> = Vec::new();
let mut current = node_id;
loop {
let Ok(node) = get_node(buffer, current) else {
return Ok(());
};
match node {
LogicNode::ForAllNode((v, body)) => {
universals.push(v.clone());
current = *body;
}
LogicNode::PastNode(_)
| LogicNode::PresentNode(_)
| LogicNode::FutureNode(_)
| LogicNode::ObligatoryNode(_)
| LogicNode::PermittedNode(_) => {
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(),
);
}
_ => break,
}
}
let inner_body_id = current;
let (extra_universals, pf_conditions, pf_consequent) = prenex_flatten(buffer, inner_body_id);
universals.extend(extra_universals);
let rule_desc = if universals.is_empty() {
"ground conditional".to_string()
} else {
format!("∀{}", universals.join(","))
};
let mut pattern_vars: HashMap<String, String> = universals
.iter()
.enumerate()
.map(|(i, v)| (v.clone(), format!("x__v{}", i)))
.collect();
let mut ground_skolems: HashMap<String, String> = skolem_subs
.iter()
.filter(|(_, gt)| !is_skdep(gt))
.filter_map(|(k, gt)| {
if let GroundTerm::Constant(s) = gt {
Some((k.clone(), s.clone()))
} else {
None
}
})
.collect();
let pattern_var_names: Vec<String> =
universals.iter().map(|v| pattern_vars[v].clone()).collect();
let mut dependent_skolems: HashMap<String, (String, Vec<String>)> = skolem_subs
.iter()
.filter_map(|(k, gt)| {
skdep_base_name(gt)
.map(|base| (k.clone(), (base.to_string(), pattern_var_names.clone())))
})
.collect();
let implication = if pf_conditions.is_empty() {
None
} else {
Some((pf_conditions, pf_consequent))
};
match implication {
Some((condition_ids, consequent_id)) => {
let clauses = dnf_condition_clauses(buffer, &condition_ids, MAX_DNF_CLAUSES)?;
let mut all_condition_exists: HashSet<String> = HashSet::new();
for clause in &clauses {
for &lid in clause {
collect_condition_exists(buffer, lid, &mut all_condition_exists);
}
}
for var in &all_condition_exists {
dependent_skolems.remove(var);
ground_skolems.remove(var);
let pvar = format!("ev__{}", var);
pattern_vars.insert(var.clone(), pvar);
}
let mut consequent_atoms = flatten_consequent(buffer, consequent_id, skolem_subs, None);
let or_atom_ids: Vec<u32> = consequent_atoms
.iter()
.filter(|&&(aid, _)| matches!(get_node(buffer, aid), Ok(LogicNode::OrNode(_))))
.map(|&(aid, _)| aid)
.collect();
if !or_atom_ids.is_empty() {
for &or_id in &or_atom_ids {
let mut branches = Vec::new();
collect_disjunct_branches(buffer, or_id, &mut branches);
let mut disjuncts: Vec<Vec<StoredFact>> = Vec::new();
for &br in &branches {
let mut leaves = Vec::new();
for (aid, tense) in flatten_consequent(buffer, br, skolem_subs, None) {
match build_rule_template_fact(
buffer,
aid,
&pattern_vars,
&ground_skolems,
&dependent_skolems,
tense,
) {
Some(fact) => leaves.push(fact),
None => {
return Err(format!(
"cannot represent disjunctive conclusion for {rule_desc}: \
a disjunct atom is not a flat predicate. Rejecting to \
preserve soundness."
));
}
}
}
disjuncts.push(leaves);
}
for clause in &clauses {
let conditions = build_positive_clause_conditions(
buffer,
clause,
&pattern_vars,
&ground_skolems,
&dependent_skolems,
&rule_desc,
)?;
let cond_label = conditions
.iter()
.map(|c| c.relation().to_string())
.collect::<Vec<_>>()
.join(" ∧ ");
let disj_label = disjuncts
.iter()
.map(|d| {
d.iter()
.map(|f| f.relation().to_string())
.collect::<Vec<_>>()
.join(" ∧ ")
})
.collect::<Vec<_>>()
.join(" ∨ ");
inner.disjunctive_constraints.push(DisjunctiveConstraint {
label: format!("{cond_label} → {disj_label}"),
conditions,
disjuncts: disjuncts.clone(),
});
}
}
if let Some(aid) = inner.current_assertion_id {
inner.rule_source_map.entry(aid).or_default();
}
if inner.diag_enabled() {
println!(
"[Constraint] Registered disjunctive conclusion {} as ¬(P ∧ ¬Q ∧ ¬R)",
rule_desc
);
}
consequent_atoms
.retain(|&(aid, _)| !matches!(get_node(buffer, aid), Ok(LogicNode::OrNode(_))));
if consequent_atoms.is_empty() {
return Ok(());
}
let retained_vars: HashSet<String> = consequent_atoms
.iter()
.flat_map(|&(aid, _)| atom_var_args(buffer, aid))
.collect();
dependent_skolems.retain(|k, _| retained_vars.contains(k));
}
let mut var_adjacency: HashMap<String, HashSet<String>> = HashMap::new();
for &(aid, _) in &consequent_atoms {
let vars = atom_var_args(buffer, aid);
for a in &vars {
for b in &vars {
if a != b {
var_adjacency
.entry(a.clone())
.or_default()
.insert(b.clone());
}
}
}
}
for (k, val) in dependent_skolems.iter_mut() {
let mut reached: HashSet<String> = HashSet::new();
reached.insert(k.clone());
let mut stack = vec![k.clone()];
while let Some(node) = stack.pop() {
if let Some(neighbors) = var_adjacency.get(&node) {
for n in neighbors {
if reached.insert(n.clone()) {
stack.push(n.clone());
}
}
}
}
let precise: Vec<String> = universals
.iter()
.filter(|u| reached.contains(*u))
.map(|u| pattern_vars[u].clone())
.collect();
val.1 = precise;
}
if !dependent_skolems.is_empty() {
for (_, (base, pvars)) in &dependent_skolems {
if !inner
.skolem_fn_registry
.iter()
.any(|e| e.base_name == *base)
{
inner.skolem_fn_registry.push(SkolemFnEntry {
base_name: base.clone(),
dep_count: pvars.len(),
});
}
}
}
let mut typed_concls: Vec<StoredFact> = Vec::new();
for &(aid, tense) in &consequent_atoms {
match build_rule_template_fact(
buffer,
aid,
&pattern_vars,
&ground_skolems,
&dependent_skolems,
tense,
) {
Some(fact) => typed_concls.push(fact),
None => {
return Err(format!(
"cannot compile rule conclusion for {rule_desc}: a consequent \
atom is not a flat predicate. Rejecting the assertion to \
preserve soundness."
));
}
}
}
let clause_count = clauses.len();
for (branch_idx, clause) in clauses.iter().enumerate() {
register_clause_rule(
buffer,
clause,
branch_idx,
clause_count,
&universals,
&pattern_vars,
&pattern_var_names,
&ground_skolems,
&dependent_skolems,
&typed_concls,
&rule_desc,
inner,
)?;
}
}
None => {
if !dependent_skolems.is_empty() {
for (_, (base, pvars)) in &dependent_skolems {
if !inner
.skolem_fn_registry
.iter()
.any(|e| e.base_name == *base)
{
inner.skolem_fn_registry.push(SkolemFnEntry {
base_name: base.clone(),
dep_count: pvars.len(),
});
}
}
}
let typed_concls: Vec<StoredFact> = match build_rule_template_fact(
buffer,
pf_consequent,
&pattern_vars,
&ground_skolems,
&dependent_skolems,
None, ) {
Some(fact) => vec![fact],
None => {
return Err(format!(
"cannot compile bare universal {rule_desc}: its body is not a flat \
predicate. Rejecting the assertion to preserve soundness."
));
}
};
let dedup_key = rule_dedup_hash(1, &[], &typed_concls);
if !inner.known_rules.insert(dedup_key) {
if inner.diag_enabled() {
println!(
"[Rule] bare ∀{} already present, skipping",
universals.join(",")
);
}
} else {
if inner.diag_enabled() {
println!(
"[Rule] Compiled bare ∀{} backward-chaining rule",
universals.join(",")
);
}
let label = build_typed_rule_label(&[], &typed_concls);
if let Err(e) = register_rule(
inner,
label,
pattern_var_names.clone(),
vec![],
typed_concls,
vec![], vec![], false, ) {
eprintln!("[Stratification Error] {}", e);
return Err(e);
}
}
}
}
Ok(())
}
pub(super) fn generate_count_extra_witnesses(
buffer: &LogicBuffer,
node_id: u32,
skolem_subs: &HashMap<String, GroundTerm>,
inner: &mut KnowledgeBaseInner,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::CountNode((v, count, body)) => {
if *count > 1 {
for _ in 1..*count {
let extra_sk = inner.fresh_skolem();
inner.note_entity(&extra_sk);
let mut typed_extra_subs: HashMap<String, GroundTerm> = skolem_subs
.iter()
.filter(|(_, gt)| !is_skdep(gt))
.map(|(k, gt)| (k.clone(), gt.clone()))
.collect();
typed_extra_subs.insert(v.clone(), GroundTerm::Constant(extra_sk.clone()));
let mut body_exists = HashSet::new();
collect_condition_exists(buffer, *body, &mut body_exists);
for var in &body_exists {
let ev_sk = inner.fresh_skolem();
if var.starts_with("_ev") {
inner.note_event_entity(&ev_sk);
} else {
inner.note_entity(&ev_sk);
}
typed_extra_subs.insert(var.clone(), GroundTerm::Constant(ev_sk));
}
let mut facts = Vec::new();
collect_ground_facts(buffer, *body, &typed_extra_subs, None, &mut facts);
for fact in facts {
assert_typed_fact(fact, inner);
}
}
}
generate_count_extra_witnesses(buffer, *body, skolem_subs, inner);
}
LogicNode::AndNode((l, r)) | LogicNode::OrNode((l, r)) => {
generate_count_extra_witnesses(buffer, *l, skolem_subs, inner);
generate_count_extra_witnesses(buffer, *r, skolem_subs, inner);
}
LogicNode::NotNode(inner_node)
| LogicNode::ExistsNode((_, inner_node))
| LogicNode::ForAllNode((_, inner_node)) => {
generate_count_extra_witnesses(buffer, *inner_node, skolem_subs, inner);
}
LogicNode::PastNode(inner_node)
| LogicNode::PresentNode(inner_node)
| LogicNode::FutureNode(inner_node)
| LogicNode::ObligatoryNode(inner_node)
| LogicNode::PermittedNode(inner_node) => {
generate_count_extra_witnesses(buffer, *inner_node, skolem_subs, inner);
}
LogicNode::Predicate(_) | LogicNode::ComputeNode(_) => {}
}
}
pub(super) fn build_ground_term(
term: &LogicalTerm,
subs: &HashMap<String, GroundTerm>,
) -> GroundTerm {
match term {
LogicalTerm::Variable(v) => {
if let Some(gt) = subs.get(v.as_str()) {
if is_skdep(gt) {
GroundTerm::PatternVar(v.clone())
} else {
gt.clone()
}
} else {
GroundTerm::PatternVar(v.clone())
}
}
LogicalTerm::Constant(c) => GroundTerm::Constant(c.clone()),
LogicalTerm::Description(d) => GroundTerm::Description(d.clone()),
LogicalTerm::Unspecified => GroundTerm::Unspecified,
LogicalTerm::Number(n) => GroundTerm::from_f64(*n),
}
}
pub(super) fn build_stored_fact_from_node(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
tense: Option<&str>,
) -> Option<StoredFact> {
let Ok(node) = get_node(buffer, node_id) else {
return None;
};
match node {
LogicNode::Predicate((rel, args)) | LogicNode::ComputeNode((rel, args)) => {
let ground_args: Vec<GroundTerm> =
args.iter().map(|a| build_ground_term(a, subs)).collect();
let fact = GroundFact::new(rel.clone(), ground_args);
Some(StoredFact::with_tense(fact, tense))
}
LogicNode::ExistsNode((v, body)) => {
if subs.contains_key(v.as_str()) {
build_stored_fact_from_node(buffer, *body, subs, tense)
} else {
None }
}
LogicNode::PastNode(inner) => {
build_stored_fact_from_node(buffer, *inner, subs, Some("Past"))
}
LogicNode::PresentNode(inner) => {
build_stored_fact_from_node(buffer, *inner, subs, Some("Present"))
}
LogicNode::FutureNode(inner) => {
build_stored_fact_from_node(buffer, *inner, subs, Some("Future"))
}
LogicNode::ObligatoryNode(inner) => {
build_stored_fact_from_node(buffer, *inner, subs, Some("Obligatory"))
}
LogicNode::PermittedNode(inner) => {
build_stored_fact_from_node(buffer, *inner, subs, Some("Permitted"))
}
_ => None, }
}
pub(super) fn collect_ground_facts(
buffer: &LogicBuffer,
node_id: u32,
subs: &HashMap<String, GroundTerm>,
tense: Option<&str>,
out: &mut Vec<StoredFact>,
) {
let Ok(node) = get_node(buffer, node_id) else {
return;
};
match node {
LogicNode::AndNode((l, r)) => {
if is_abstraction_marker(buffer, *l) {
collect_ground_facts(buffer, *l, subs, tense, out);
} else {
collect_ground_facts(buffer, *l, subs, tense, out);
collect_ground_facts(buffer, *r, subs, tense, out);
}
}
LogicNode::ExistsNode((v, body)) => {
if subs.contains_key(v.as_str()) {
collect_ground_facts(buffer, *body, subs, tense, out);
}
}
LogicNode::PastNode(inner) => {
collect_ground_facts(buffer, *inner, subs, Some("Past"), out);
}
LogicNode::PresentNode(inner) => {
collect_ground_facts(buffer, *inner, subs, Some("Present"), out);
}
LogicNode::FutureNode(inner) => {
collect_ground_facts(buffer, *inner, subs, Some("Future"), out);
}
LogicNode::ObligatoryNode(inner) => {
collect_ground_facts(buffer, *inner, subs, Some("Obligatory"), out);
}
LogicNode::PermittedNode(inner) => {
collect_ground_facts(buffer, *inner, subs, Some("Permitted"), out);
}
LogicNode::CountNode((v, _, body)) => {
if subs.contains_key(v.as_str()) {
collect_ground_facts(buffer, *body, subs, tense, out);
}
}
_ => {
if let Some(fact) = build_stored_fact_from_node(buffer, node_id, subs, tense) {
out.push(fact);
}
}
}
}
pub(super) fn build_rule_template_fact_with_negation(
buffer: &LogicBuffer,
node_id: u32,
pattern_vars: &HashMap<String, String>,
ground_skolems: &HashMap<String, String>,
dependent_skolems: &HashMap<String, (String, Vec<String>)>,
tense: Option<&str>,
) -> Option<(StoredFact, bool)> {
let Ok(node) = get_node(buffer, node_id) else {
return None;
};
match node {
LogicNode::NotNode(inner_node) => {
build_rule_template_fact(
buffer,
*inner_node,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
)
.map(|fact| (fact, true))
}
_ => build_rule_template_fact(
buffer,
node_id,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
)
.map(|fact| (fact, false)),
}
}
pub(super) fn build_rule_template_fact(
buffer: &LogicBuffer,
node_id: u32,
pattern_vars: &HashMap<String, String>,
ground_skolems: &HashMap<String, String>,
dependent_skolems: &HashMap<String, (String, Vec<String>)>,
tense: Option<&str>,
) -> Option<StoredFact> {
let Ok(node) = get_node(buffer, node_id) else {
return None;
};
match node {
LogicNode::Predicate((rel, args)) | LogicNode::ComputeNode((rel, args)) => {
let ground_args: Vec<GroundTerm> = args
.iter()
.map(|arg| match arg {
LogicalTerm::Variable(v) => {
if let Some(pvar) = pattern_vars.get(v.as_str()) {
GroundTerm::PatternVar(pvar.clone())
} else if let Some(sk) = ground_skolems.get(v.as_str()) {
GroundTerm::Constant(sk.clone())
} else if let Some((base, pvars)) = dependent_skolems.get(v.as_str()) {
let deps: Vec<GroundTerm> = pvars
.iter()
.map(|pv| GroundTerm::PatternVar(pv.clone()))
.collect();
build_skolem_fn_term(base, &deps)
} else {
GroundTerm::PatternVar(v.clone())
}
}
LogicalTerm::Constant(c) => GroundTerm::Constant(c.clone()),
LogicalTerm::Description(d) => GroundTerm::Description(d.clone()),
LogicalTerm::Unspecified => GroundTerm::Unspecified,
LogicalTerm::Number(n) => GroundTerm::from_f64(*n),
})
.collect();
Some(StoredFact::with_tense(
GroundFact::new(rel.clone(), ground_args),
tense,
))
}
LogicNode::ExistsNode((v, body)) => {
if pattern_vars.contains_key(v.as_str())
|| ground_skolems.contains_key(v.as_str())
|| dependent_skolems.contains_key(v.as_str())
{
build_rule_template_fact(
buffer,
*body,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
)
} else {
None
}
}
LogicNode::ObligatoryNode(inner) | LogicNode::PermittedNode(inner) => {
build_rule_template_fact(
buffer,
*inner,
pattern_vars,
ground_skolems,
dependent_skolems,
tense,
)
}
_ => None,
}
}
pub(super) fn build_skolem_fn_term(base_name: &str, deps: &[GroundTerm]) -> GroundTerm {
let dep_term = match deps.len() {
0 => GroundTerm::Unspecified,
1 => deps[0].clone(),
_ => {
let mut acc = deps.last().unwrap().clone();
for dep in deps[..deps.len() - 1].iter().rev() {
acc = GroundTerm::DepPair(Box::new(dep.clone()), Box::new(acc));
}
acc
}
};
GroundTerm::SkolemFn(base_name.to_string(), Box::new(dep_term))
}
#[cfg(test)]
mod stratification_conformance {
use super::*;
use std::collections::{BTreeSet, HashSet};
fn reachable_sets(
graph: &HashMap<String, Vec<(String, bool)>>,
) -> HashMap<String, HashSet<String>> {
let mut nodes: BTreeSet<String> = BTreeSet::new();
for (k, edges) in graph {
nodes.insert(k.clone());
for (d, _) in edges {
nodes.insert(d.clone());
}
}
let mut reach: HashMap<String, HashSet<String>> = nodes
.iter()
.map(|n| (n.clone(), HashSet::from([n.clone()])))
.collect();
let mut changed = true;
while changed {
changed = false;
for u in &nodes {
let Some(edges) = graph.get(u) else { continue };
let mut additions: Vec<String> = Vec::new();
for (v, _) in edges {
if let Some(rv) = reach.get(v) {
additions.extend(rv.iter().cloned());
}
}
let ru = reach.get_mut(u).unwrap();
for w in additions {
if ru.insert(w) {
changed = true;
}
}
}
}
reach
}
fn stratifiable_naive(graph: &HashMap<String, Vec<(String, bool)>>) -> bool {
let reach = reachable_sets(graph);
for (u, edges) in graph {
for (v, is_neg) in edges {
if *is_neg && reach.get(v).is_some_and(|rv| rv.contains(u)) {
return false;
}
}
}
true
}
fn graph_of(edges: &[(&str, &str, bool)]) -> HashMap<String, Vec<(String, bool)>> {
let mut g: HashMap<String, Vec<(String, bool)>> = HashMap::new();
for (u, v, neg) in edges {
g.entry(u.to_string())
.or_default()
.push((v.to_string(), *neg));
}
g
}
fn pseudo_random_graph(seed: u64, num_nodes: usize) -> HashMap<String, Vec<(String, bool)>> {
let mut state = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let mut next = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
state >> 33
};
let names: Vec<String> = (0..num_nodes).map(|i| format!("p{i}")).collect();
let mut g: HashMap<String, Vec<(String, bool)>> = HashMap::new();
for u in 0..num_nodes {
for v in 0..num_nodes {
if next() % 5 < 2 {
let is_neg = next() % 2 == 0;
g.entry(names[u].clone())
.or_default()
.push((names[v].clone(), is_neg));
}
}
}
g
}
fn corpus() -> Vec<(String, HashMap<String, Vec<(String, bool)>>)> {
let mut corpus: Vec<(String, HashMap<String, Vec<(String, bool)>>)> = vec![
("empty".into(), graph_of(&[])),
("neg_self_loop".into(), graph_of(&[("a", "a", true)])),
("pos_self_loop".into(), graph_of(&[("a", "a", false)])),
(
"positive_cycle".into(),
graph_of(&[("a", "b", false), ("b", "c", false), ("c", "a", false)]),
),
(
"neg_cycle".into(),
graph_of(&[("a", "b", true), ("b", "c", true), ("c", "b", false)]),
),
(
"stratified_with_negation".into(),
graph_of(&[("a", "b", true), ("b", "c", false)]),
),
(
"neg_edge_into_cycle_ok".into(),
graph_of(&[("x", "a", true), ("a", "b", false), ("b", "a", false)]),
),
(
"dag".into(),
graph_of(&[("a", "b", true), ("a", "c", false), ("b", "d", true)]),
),
];
for seed in 0u64..300 {
let num_nodes = 2 + (seed as usize % 4); corpus.push((
format!("rand_seed{seed}_n{num_nodes}"),
pseudo_random_graph(seed, num_nodes),
));
}
corpus
}
fn all_nodes(graph: &HashMap<String, Vec<(String, bool)>>) -> BTreeSet<String> {
let mut nodes = BTreeSet::new();
for (k, edges) in graph {
nodes.insert(k.clone());
for (d, _) in edges {
nodes.insert(d.clone());
}
}
nodes
}
#[test]
fn check_stratification_matches_proven_criterion() {
let mut checked = 0usize;
for (name, g) in corpus() {
let check_ok = check_stratification(&g).is_ok();
let naive_ok = stratifiable_naive(&g);
assert_eq!(
check_ok, naive_ok,
"check_stratification disagreed with the proven criterion on '{name}': \
check_ok={check_ok}, naive_ok={naive_ok}, graph={g:?}"
);
checked += 1;
}
assert!(
checked >= 300,
"corpus too small ({checked}); gate near-vacuous"
);
}
#[test]
fn compute_sccs_matches_scc_spec() {
let mut checked = 0usize;
let mut nontrivial_seen = false;
for (name, g) in corpus() {
let sccs = compute_sccs(&g);
let nodes = all_nodes(&g);
let mut seen: BTreeSet<String> = BTreeSet::new();
for scc in &sccs {
if scc.len() > 1 {
nontrivial_seen = true;
}
for node in scc {
assert!(
seen.insert(node.clone()),
"compute_sccs put '{node}' in two SCCs on '{name}': {sccs:?}"
);
assert!(
nodes.contains(node),
"compute_sccs produced out-of-graph node '{node}' on '{name}'"
);
}
}
assert_eq!(
seen, nodes,
"compute_sccs partition does not cover the node set on '{name}': {sccs:?}"
);
let reach = reachable_sets(&g);
let node_vec: Vec<String> = nodes.iter().cloned().collect();
for i in 0..node_vec.len() {
for j in i..node_vec.len() {
let a = &node_vec[i];
let b = &node_vec[j];
let tarjan_same = sccs.iter().any(|scc| scc.contains(a) && scc.contains(b));
let mutually_reachable = reach.get(a).is_some_and(|ra| ra.contains(b))
&& reach.get(b).is_some_and(|rb| rb.contains(a));
assert_eq!(
tarjan_same, mutually_reachable,
"compute_sccs same-SCC({a},{b})={tarjan_same} but \
mutually-reachable={mutually_reachable} on '{name}': {g:?}"
);
}
}
checked += 1;
}
assert!(
checked >= 300,
"corpus too small ({checked}); gate near-vacuous"
);
assert!(
nontrivial_seen,
"no nontrivial SCC (size > 1) anywhere in the corpus — the spec check is near-vacuous"
);
}
}