use super::*;
fn assert_trace_consistent(result: &QueryResult, trace: &ProofTrace) {
let root = &trace.steps[trace.root as usize];
match result {
QueryResult::True => assert!(root.holds, "True verdict but root step holds=false"),
QueryResult::False => assert!(!root.holds, "False verdict but root step holds=true"),
QueryResult::Unknown(_) | QueryResult::ResourceExceeded(_) => {
assert!(
!root.holds,
"non-definitive verdict {result:?} but root step holds=true"
);
assert!(
!matches!(
root.rule,
ProofRule::ForallCounterexample { .. }
| ProofRule::ExistsFailed
| ProofRule::PredicateNotFound { .. }
),
"non-definitive verdict {result:?} but root asserts a decided failure: {:?}",
root.rule
);
}
}
assert_proof_refs_resolve_to_holds_true(trace);
}
#[test]
fn depth_boundary_contract() {
let kb = new_kb();
kb.set_max_chain_depth(3);
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "jmive"));
assert_buf(&kb, make_universal("jmive", "xanlu"));
assert_buf(&kb, make_universal("xanlu", "melbi"));
let verdict = |p: &str| {
kb.query_entailment_inner(make_query("alis", p))
.unwrap_or_else(|e| panic!("query {p}: {e}"))
};
for p in ["gerku", "danlu", "jmive", "xanlu"] {
assert_eq!(
verdict(p),
QueryResult::True,
"chain to {p} is within depth 3"
);
}
assert_eq!(
verdict("melbi"),
QueryResult::ResourceExceeded(ResourceKind::Depth),
"chain needing depth 4 under a depth-3 bound is RESOURCE_EXCEEDED(Depth)"
);
assert_eq!(
verdict("cipni"),
QueryResult::False,
"an unreachable goal is FALSE, not RESOURCE_EXCEEDED"
);
kb.set_max_chain_depth(4);
assert_eq!(verdict("melbi"), QueryResult::True, "depth 4 reaches melbi");
}
#[test]
fn exact_count_with_unresolved_member_bounds() {
let kb = new_kb();
kb.set_materialization(false);
for s in [
"dog(Kim).",
"bird(Kim).",
"all $da: bird($da) -> alive($da).",
"all $da: alive($da) -> animal($da).",
] {
assert_buf(&kb, compile_surface(s));
}
kb.set_max_chain_depth(1);
assert_eq!(
kb.query_entailment_inner(compile_surface("animal(exactly 1 dog)."))
.unwrap(),
QueryResult::ResourceExceeded(ResourceKind::Depth),
"0 satisfying + 1 unresolved vs count 1: non-definitive, not FALSE"
);
assert_eq!(
kb.query_entailment_inner(compile_surface("animal(exactly 2 dog)."))
.unwrap(),
QueryResult::False,
"0 satisfying + 1 unresolved vs count 2: sum bound gives FALSE"
);
let kb2 = new_kb();
kb2.set_materialization(false);
for s in [
"dog(Adam).",
"dog(Bel).",
"dog(Kim).",
"animal(Adam).",
"animal(Bel).",
"bird(Kim).",
"all $da: bird($da) -> alive($da).",
"all $da: alive($da) -> animal($da).",
] {
assert_buf(&kb2, compile_surface(s));
}
kb2.set_max_chain_depth(1);
assert_eq!(
kb2.query_entailment_inner(compile_surface("animal(exactly 1 dog)."))
.unwrap(),
QueryResult::False,
"2 satisfying vs count 1: over-count bound gives FALSE"
);
assert_eq!(
kb2.query_entailment_inner(compile_surface("animal(exactly 2 dog)."))
.unwrap(),
QueryResult::ResourceExceeded(ResourceKind::Depth),
"satisfying == count with an unresolved member: non-definitive, not FALSE"
);
}
fn flat_sum_body(nodes: &mut Vec<LogicNode>) -> u32 {
compute(
nodes,
"sum",
vec![
LogicalTerm::Variable("_v0".to_string()),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
)
}
#[test]
fn flat_forall_and_count_over_compute_batch_is_decisive_over_numeric_members() {
let kb = new_kb();
for n in [4.0, 5.0] {
let mut nodes = Vec::new();
let root = pred(&mut nodes, "namcu", vec![LogicalTerm::Number(n)]);
assert_buf(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
},
);
}
let mut nodes = Vec::new();
let body = flat_sum_body(&mut nodes);
let root = forall(&mut nodes, "_v0", body);
assert_eq!(
query_result(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
}
),
QueryResult::False,
"4 ≠ 2 + 3: the batch finds the numeric counterexample"
);
for (cnt, expected) in [(1u32, QueryResult::True), (2u32, QueryResult::False)] {
let mut nodes = Vec::new();
let body = flat_sum_body(&mut nodes);
let root = count(&mut nodes, "_v0", cnt, body);
assert_eq!(
query_result(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
}
),
expected,
"count {cnt}: exactly one member (5.0) satisfies sum(v, 2, 3)"
);
}
}
#[test]
fn flat_forall_and_count_over_compute_batch_stays_non_definitive() {
let kb = new_kb();
for n in [4.0, 5.0] {
let mut nodes = Vec::new();
let root = pred(&mut nodes, "namcu", vec![LogicalTerm::Number(n)]);
assert_buf(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
},
);
}
assert_buf(&kb, make_assertion("alis", "prenu"));
let mut nodes = Vec::new();
let body = flat_sum_body(&mut nodes);
let root = forall(&mut nodes, "_v0", body);
assert_eq!(
query_result(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
}
),
QueryResult::False,
"the numeric counterexample (4.0) outranks the constant member's Unknown"
);
for cnt in [1u32, 2u32] {
let mut nodes = Vec::new();
let body = flat_sum_body(&mut nodes);
let root = count(&mut nodes, "_v0", cnt, body);
assert_eq!(
query_result(
&kb,
LogicBuffer {
nodes,
roots: vec![root],
}
),
QueryResult::Unknown(UnknownReason::BackendUnavailable),
"count {cnt}: 1 satisfying + 1 unresolved — neither bound decisive, never a guess"
);
}
}
#[test]
fn forall_over_compute_body_batch_path() {
let kb = new_kb();
assert_buf(&kb, compile_surface("dog(Adam)."));
assert_buf(&kb, compile_surface("dog(Bel)."));
assert_eq!(
kb.query_entailment_inner(compile_surface("all $da: sum($da, 2, 3)."))
.unwrap(),
QueryResult::False,
"a compute body false for every member makes the universal FALSE"
);
}
#[test]
fn non_ground_fact_is_dropped_at_the_assert_boundary() {
let kb = new_kb();
let mut inner = kb.inner.borrow_mut();
crate::rules::assert_typed_fact(
StoredFact::Bare(GroundFact {
relation: "gerku".into(),
args: vec![GroundTerm::PatternVar("x__v0".into())],
}),
&mut inner,
);
crate::rules::assert_typed_fact(
StoredFact::Bare(GroundFact {
relation: "mlatu".into(),
args: vec![GroundTerm::SkolemFn(
"sk_9".into(),
Box::new(GroundTerm::PatternVar("y__v1".into())),
)],
}),
&mut inner,
);
crate::rules::assert_typed_fact(
StoredFact::Bare(GroundFact {
relation: "cipni".into(),
args: vec![GroundTerm::SkolemFn(
"sk_10".into(),
Box::new(GroundTerm::DepPair(
Box::new(GroundTerm::Constant("adam".into())),
Box::new(GroundTerm::PatternVar("z__v2".into())),
)),
)],
}),
&mut inner,
);
assert!(
inner.fact_store.is_empty(),
"non-ground facts must be dropped at the boundary, store has: {:?}",
inner.fact_store.all_facts().collect::<Vec<_>>()
);
crate::rules::assert_typed_fact(
StoredFact::Bare(GroundFact {
relation: "gerku".into(),
args: vec![GroundTerm::SkolemFn(
"sk_0".into(),
Box::new(GroundTerm::Constant("adam".into())),
)],
}),
&mut inner,
);
assert_eq!(
inner.fact_store.len(),
1,
"a ground fact must still insert normally"
);
}
#[test]
fn trace_soundness_conformance() {
#[derive(Default)]
struct Exercised {
factax: usize, derived: usize, notfound: usize, ruleblocked: usize, cand_complete: usize, blocker_definitive: usize, }
fn parse_bare_fact_display(s: &str) -> Option<StoredFact> {
let (rel, rest) = s.split_once('(')?;
let inner = rest.strip_suffix(')')?;
if rel.is_empty() || rel.contains(' ') || inner.contains('(') || inner.contains('?') {
return None;
}
let args: Vec<GroundTerm> = if inner.is_empty() {
Vec::new()
} else {
inner
.split(", ")
.map(|a| {
if a == "_" {
GroundTerm::Unspecified
} else if let Ok(v) = a.parse::<f64>() {
GroundTerm::from_f64(v)
} else {
GroundTerm::Constant(a.to_string())
}
})
.collect()
};
Some(StoredFact::Bare(GroundFact {
relation: rel.to_string(),
args,
}))
}
fn validate_cert(
kb: &KnowledgeBase,
trace: &ProofTrace,
ex: &mut Exercised,
) -> Result<(), String> {
let inner = kb.inner.borrow();
let fact_displays: std::collections::HashSet<String> = inner
.fact_store
.all_facts()
.map(|f| f.to_display_string())
.collect();
let rule_labels: std::collections::HashSet<&str> = inner
.universal_rules
.values()
.flatten()
.map(|r| r.label.as_str())
.collect();
fn base_label(l: &str) -> &str {
l.split(" [").next().unwrap_or(l)
}
let holds = |c: u32| trace.steps[c as usize].holds;
for (i, step) in trace.steps.iter().enumerate() {
let all_hold = step.children.iter().all(|&c| holds(c));
let any_hold = step.children.iter().any(|&c| holds(c));
match &step.rule {
ProofRule::Asserted { fact } => {
if !step.holds {
return Err(format!("step #{i} Asserted but holds=false"));
}
if !fact_displays.contains(fact) {
return Err(format!(
"step #{i} Asserted '{fact}' is not in the fact store (factAx bridge)"
));
}
ex.factax += 1;
}
ProofRule::Derived { label, .. } => {
if step.holds && (step.children.is_empty() || !all_hold) {
return Err(format!(
"step #{i} Derived holds=true but a condition child is missing or does not hold"
));
}
if !rule_labels.contains(base_label(label)) {
return Err(format!(
"step #{i} Derived label '{}' is not a registered rule (candOk/ruleClosed bridge)",
base_label(label)
));
}
ex.derived += 1;
}
ProofRule::PredicateNotFound { predicate } => {
if step.holds {
return Err(format!("step #{i} PredicateNotFound but holds=true"));
}
if fact_displays.contains(predicate) {
return Err(format!(
"step #{i} PredicateNotFound '{predicate}' is actually a stored fact (supported bridge)"
));
}
let mut blocked_labels: Vec<&str> = Vec::new();
for &c in &step.children {
match &trace.steps[c as usize].rule {
ProofRule::RuleAttemptFailed { rule_label, .. } => {
if !rule_labels.contains(base_label(rule_label)) {
return Err(format!(
"step #{i} PredicateNotFound records blocked rule '{}' that is not registered (supported bridge)",
base_label(rule_label)
));
}
blocked_labels.push(base_label(rule_label));
ex.ruleblocked += 1;
}
other => {
return Err(format!(
"step #{i} PredicateNotFound child is not RuleAttemptFailed: {other:?}"
));
}
}
}
if let Some(goal) = parse_bare_fact_display(predicate) {
let candidates =
crate::reasoning::matching_rules_typed(&goal, &inner.universal_rules);
for rule in candidates {
let unifying: Vec<_> = rule
.typed_conclusions
.iter()
.filter_map(|c| unify_facts(c, &goal))
.collect();
if unifying.is_empty() {
continue; }
if !blocked_labels.contains(&base_label(&rule.label)) {
return Err(format!(
"step #{i} PredicateNotFound '{predicate}': candidate rule '{}' \
unifies with the goal but is not recorded as blocked \
(Neg candidate-completeness)",
rule.label
));
}
ex.cand_complete += 1;
if rule.negated_exists_groups.is_empty() {
let mut definitive = false;
for bindings in &unifying {
for (ci, ct) in rule.typed_conditions.iter().enumerate() {
let cond_fact = substitute_fact(ct, bindings);
let r = crate::reasoning::check_predicate_in_kb_typed(
&cond_fact,
&inner,
0,
&mut std::collections::HashSet::new(),
);
let negated = rule.negated_condition_indices.contains(&ci);
if (!negated && matches!(r, QueryResult::False))
|| (negated && r.is_true())
{
definitive = true;
}
}
}
if !definitive {
return Err(format!(
"step #{i} PredicateNotFound '{predicate}': blocked \
candidate '{}' has no definitively-refuted positive \
premise and no definitively-holding negated premise \
(Neg blocker-definitiveness)",
rule.label
));
}
ex.blocker_definitive += 1;
}
}
}
ex.notfound += 1;
}
ProofRule::Conjunction => {
if step.holds != all_hold {
return Err(format!(
"step #{i} Conjunction holds={} but all-children-hold={all_hold}",
step.holds
));
}
}
ProofRule::DisjunctionIntro { .. } | ProofRule::DisjunctionCheck { .. } => {
if step.holds && !step.children.is_empty() && !any_hold {
return Err(format!(
"step #{i} Disjunction holds=true but no child holds"
));
}
}
ProofRule::Negation => {
if step.children.is_empty() {
if !step.holds {
return Err(format!("step #{i} Negation leaf but holds=false"));
}
} else if step.holds == holds(step.children[0]) {
return Err(format!(
"step #{i} Negation holds={} but its child holds={}",
step.holds,
holds(step.children[0])
));
}
}
ProofRule::ProofRef { .. } => {
if step.children.is_empty() || step.holds != holds(step.children[0]) {
return Err(format!(
"step #{i} ProofRef holds mismatch with its referent"
));
}
}
ProofRule::RuleAttemptFailed { .. }
| ProofRule::ExistsFailed
| ProofRule::ForallCounterexample { .. } => {
if step.holds {
return Err(format!("step #{i} {:?} but holds=true", step.rule));
}
}
_ => {}
}
}
Ok(())
}
let ex = std::cell::RefCell::new(Exercised::default());
let run_case = |name: &str, kb: &KnowledgeBase, query: LogicBuffer| {
let (result, trace) = kb.query_entailment_with_proof_inner(query).unwrap();
validate_cert(kb, &trace, &mut ex.borrow_mut())
.unwrap_or_else(|e| panic!("invalid certificate on '{name}': {e}\n{trace:?}"));
assert_trace_consistent(&result, &trace);
};
fn conj_query(e: &str, p1: &str, p2: &str) -> LogicBuffer {
let mut nodes = Vec::new();
let a = pred(
&mut nodes,
p1,
vec![LogicalTerm::Constant(e.into()), LogicalTerm::Unspecified],
);
let b = pred(
&mut nodes,
p2,
vec![LogicalTerm::Constant(e.into()), LogicalTerm::Unspecified],
);
let root = and(&mut nodes, a, b);
LogicBuffer {
nodes,
roots: vec![root],
}
}
fn neg_query(e: &str, p: &str) -> LogicBuffer {
let mut nodes = Vec::new();
let inner = pred(
&mut nodes,
p,
vec![LogicalTerm::Constant(e.into()), LogicalTerm::Unspecified],
);
let root = not(&mut nodes, inner);
LogicBuffer {
nodes,
roots: vec![root],
}
}
let mut checked = 0usize;
{
let kb = new_kb();
assert_buf(&kb, make_assertion("alis", "gerku"));
run_case("asserted", &kb, make_query("alis", "gerku"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_universal("gerku", "danlu"));
run_case("derived_single", &kb, make_query("alis", "danlu"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "xanlu"));
run_case("derived_multi", &kb, make_query("alis", "xanlu"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_negated_antecedent_rule());
assert_buf(&kb, make_assertion("alis", "gerku"));
run_case("naf_firing", &kb, make_query("alis", "danlu"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_negated_antecedent_rule());
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_assertion("alis", "mlatu"));
run_case("naf_blocked_false", &kb, make_query("alis", "danlu"));
checked += 1;
}
{
let kb = new_kb();
run_case("predicate_not_found", &kb, make_query("mi", "klama"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_assertion("mi", "klama"));
assert_buf(&kb, make_assertion("mi", "prami"));
run_case("conjunction", &kb, conj_query("mi", "klama", "prami"));
checked += 1;
}
{
let kb = new_kb();
run_case("negation_missing", &kb, neg_query("mi", "klama"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_assertion("bel", "mlatu"));
run_case("two_entities_true", &kb, make_query("alis", "danlu"));
run_case("two_entities_false", &kb, make_query("bel", "danlu"));
checked += 2;
}
{
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu")); run_case("horn_blocked_false", &kb, make_query("adam", "danlu"));
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("mlatu", "danlu"));
run_case(
"two_candidates_both_blocked",
&kb,
make_query("adam", "danlu"),
);
checked += 1;
}
{
let kb = new_kb();
assert_buf(&kb, make_universal("danlu", "xanlu"));
assert_buf(&kb, make_universal("gerku", "danlu"));
run_case("chain_blocked_false", &kb, make_query("adam", "xanlu"));
checked += 1;
}
assert!(
checked >= 12,
"trace-soundness corpus too small ({checked}); gate near-vacuous"
);
let ex = ex.borrow();
assert!(
ex.factax > 0,
"factAx bridge never exercised (no Asserted leaf checked vs the store)"
);
assert!(
ex.derived > 0,
"candOk/ruleClosed bridge never exercised (no Derived step checked vs a rule)"
);
assert!(
ex.notfound > 0,
"supported bridge never exercised (no PredicateNotFound checked)"
);
assert!(
ex.cand_complete >= 2,
"Neg candidate-completeness never exercised on a multi-candidate goal \
(need at least the two-candidate case)"
);
assert!(
ex.blocker_definitive > 0,
"Neg blocker-definitiveness never exercised (no blocked candidate re-derived \
to a definitive premise)"
);
assert!(
ex.ruleblocked > 0,
"supported bridge's blocked-rule check never exercised (no RuleAttemptFailed child validated)"
);
}
#[test]
fn trace_does_not_contradict_unknown_cyclecut() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku"));
let (result, trace) = kb
.query_entailment_with_proof_inner(make_query("alis", "gerku"))
.unwrap();
assert!(matches!(
result,
QueryResult::Unknown(UnknownReason::CycleCut)
));
assert_trace_consistent(&result, &trace);
}
#[test]
fn backchain_cycle_cut_trace_parity() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku"));
let untraced = kb
.query_entailment_inner(make_query("alis", "gerku"))
.unwrap();
assert!(
matches!(untraced, QueryResult::Unknown(UnknownReason::CycleCut)),
"untraced verdict over a cycle should be Unknown(CycleCut), got {untraced:?}"
);
let (traced, trace) = kb
.query_entailment_with_proof_inner(make_query("alis", "gerku"))
.unwrap();
assert!(
matches!(traced, QueryResult::Unknown(UnknownReason::CycleCut)),
"recording-path verdict should match the untraced Unknown(CycleCut), got {traced:?}"
);
assert_trace_consistent(&traced, &trace);
}
#[test]
fn unknown_left_and_evaluates_right_conjunct() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku")); assert_buf(&kb, make_assertion("rex", "mlatu"));
let mut nodes = Vec::new();
let dog_rex = pred(
&mut nodes,
"gerku",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
);
let mlatu_rex = pred(
&mut nodes,
"mlatu",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
);
let root = and(&mut nodes, dog_rex, mlatu_rex);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(
matches!(result, QueryResult::Unknown(UnknownReason::CycleCut)),
"Unknown(CycleCut) ∧ True should be Unknown(CycleCut), got {result:?}"
);
assert_trace_consistent(&result, &trace);
let root_step = &trace.steps[trace.root as usize];
assert!(
matches!(root_step.rule, ProofRule::Conjunction),
"root should be a Conjunction, got {:?}",
root_step.rule
);
assert_eq!(
root_step.children.len(),
2,
"the right conjunct must be evaluated and recorded even though the left is non-definitive"
);
}
#[test]
fn true_and_unknown_right_is_unknown() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku")); assert_buf(&kb, make_assertion("rex", "mlatu"));
let mut nodes = Vec::new();
let mlatu_rex = pred(
&mut nodes,
"mlatu",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
);
let dog_rex = pred(
&mut nodes,
"gerku",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
);
let root = and(&mut nodes, mlatu_rex, dog_rex); let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, _trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(
matches!(result, QueryResult::Unknown(UnknownReason::CycleCut)),
"True ∧ Unknown(CycleCut) should be Unknown(CycleCut), got {result:?}"
);
}
#[test]
fn false_or_unknown_right_is_unknown() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku"));
let mut nodes = Vec::new();
let absent = pred(
&mut nodes,
"blanu",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
); let dog_rex = pred(
&mut nodes,
"gerku",
vec![
LogicalTerm::Constant("rex".into()),
LogicalTerm::Unspecified,
],
);
let root = or(&mut nodes, absent, dog_rex); let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, _trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(
matches!(result, QueryResult::Unknown(UnknownReason::CycleCut)),
"False ∨ Unknown(CycleCut) should be Unknown(CycleCut), got {result:?}"
);
}
#[test]
fn trace_does_not_show_counterexample_under_depth_exceeded() {
let kb = new_kb();
kb.inner.borrow_mut().max_chain_depth = 1;
assert_buf(&kb, make_assertion("alis", "gerku"));
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "xanlu"));
let mut nodes = Vec::new();
let body = pred(
&mut nodes,
"xanlu",
vec![LogicalTerm::Variable("x".into()), LogicalTerm::Unspecified],
);
let root = forall(&mut nodes, "x", body);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(
matches!(result, QueryResult::ResourceExceeded(ResourceKind::Depth)),
"got {result:?}"
);
assert_trace_consistent(&result, &trace);
assert!(
!trace
.steps
.iter()
.any(|s| matches!(s.rule, ProofRule::ForallCounterexample { .. })),
"depth-exceeded ForAll must not display a decided counterexample"
);
}
#[test]
fn trace_naf_flag_false_when_inner_is_unknown() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku"));
let mut nodes = Vec::new();
let inner = pred(
&mut nodes,
"gerku",
vec![
LogicalTerm::Constant("alis".into()),
LogicalTerm::Unspecified,
],
);
let root = not(&mut nodes, inner);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(matches!(result, QueryResult::Unknown(_)), "got {result:?}");
assert!(
!trace.has_naf_dependency(),
"NAF flag must be false when the negated inner is Unknown, not definitively False"
);
}
#[test]
fn negate_unknown_inner_yields_naf_dependent() {
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_universal("danlu", "gerku"));
let mut nodes = Vec::new();
let inner = pred(
&mut nodes,
"gerku",
vec![
LogicalTerm::Constant("alis".into()),
LogicalTerm::Unspecified,
],
);
let root = not(&mut nodes, inner);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(
matches!(result, QueryResult::Unknown(UnknownReason::NafDependent)),
"negating an Unknown sub-goal must yield Unknown(NafDependent), got {result:?}"
);
assert!(!trace.has_naf_dependency());
assert_trace_consistent(&result, &trace);
}
#[test]
fn trace_exists_failed_only_when_all_definitively_false() {
let kb = new_kb();
assert_buf(&kb, make_material_cond("gerku", "danlu", false));
assert_buf(&kb, make_material_cond("danlu", "gerku", false));
let mut nodes = Vec::new();
let body = pred(&mut nodes, "gerku", vec![LogicalTerm::Variable("y".into())]);
let root = exists(&mut nodes, "y", body);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(matches!(result, QueryResult::Unknown(_)), "got {result:?}");
assert_trace_consistent(&result, &trace);
assert!(
!trace
.steps
.iter()
.any(|s| matches!(s.rule, ProofRule::ExistsFailed)),
"Exists over an Unknown candidate must not display a decided ExistsFailed"
);
}
#[test]
fn forall_genuine_counterexample_still_traced() {
let kb = new_kb();
assert_buf(&kb, make_assertion("alis", "mlatu")); assert_buf(&kb, make_assertion("bob", "gerku")); let mut nodes = Vec::new();
let body = pred(
&mut nodes,
"mlatu",
vec![LogicalTerm::Variable("x".into()), LogicalTerm::Unspecified],
);
let root = forall(&mut nodes, "x", body);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
let (result, trace) = kb.query_entailment_with_proof_inner(buf).unwrap();
assert!(matches!(result, QueryResult::False), "got {result:?}");
assert_trace_consistent(&result, &trace);
assert!(
trace
.steps
.iter()
.any(|s| matches!(s.rule, ProofRule::ForallCounterexample { .. })),
"a genuine False member must still be shown as a counterexample"
);
}