use std::collections::{BTreeMap, BTreeSet};
use xlog_core::{Result, XlogError};
use xlog_ir::{
EirBodyLiteral, EirEpistemicLiteral, EirEpistemicMode, EirEpistemicOp, EirProgram, EirTerm,
EpistemicConstraintPlan, EpistemicExecutablePlan, EpistemicGpuPlan, EpistemicReductionPlan,
EpistemicSolverAssumptionBinding, EpistemicSolverServiceContract,
EpistemicTupleMembershipBinding, EpistemicWcojReductionStatus,
};
use xlog_stats::StatsSnapshot;
use crate::ast::{
Atom, BodyLiteral, CompOp, Comparison, Constraint, EpistemicLiteral, EpistemicMode,
EpistemicOp, Program, Term,
};
use crate::build_eir;
use crate::compile::Compiler;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TruthValue {
True,
False,
}
impl TruthValue {
fn from_bool(value: bool) -> Self {
if value {
TruthValue::True
} else {
TruthValue::False
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum EpistemicTermKey {
Integer(i64),
FloatBits(u64),
String(String),
Symbol(u32),
List(Vec<EpistemicTermKey>),
Cons {
head: Box<EpistemicTermKey>,
tail: Box<EpistemicTermKey>,
},
Compound {
functor: String,
args: Vec<EpistemicTermKey>,
},
PredRef(String),
}
impl EpistemicTermKey {
fn from_term(term: &Term) -> Result<Self> {
Ok(match term {
Term::Integer(value) => Self::Integer(*value),
Term::Float(value) => Self::FloatBits(value.to_bits()),
Term::String(value) => Self::String(value.clone()),
Term::Symbol(value) => Self::Symbol(*value),
Term::List(items) => Self::List(
items
.iter()
.map(Self::from_term)
.collect::<Result<Vec<_>>>()?,
),
Term::Cons { head, tail } => Self::Cons {
head: Box::new(Self::from_term(head)?),
tail: Box::new(Self::from_term(tail)?),
},
Term::Compound { functor, args } => Self::Compound {
functor: functor.clone(),
args: args
.iter()
.map(Self::from_term)
.collect::<Result<Vec<_>>>()?,
},
Term::PredRef(value) => Self::PredRef(value.clone()),
Term::Variable(_) | Term::Anonymous | Term::Aggregate(_) => {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic tuple key".to_string(),
context: "tuple-key epistemic facts require ground terms".to_string(),
});
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum EpistemicAtomKey {
Arity {
predicate: String,
arity: usize,
},
Ground {
predicate: String,
terms: Vec<EpistemicTermKey>,
},
}
impl EpistemicAtomKey {
fn from_arity(predicate: impl Into<String>, arity: usize) -> Self {
Self::Arity {
predicate: predicate.into(),
arity,
}
}
fn from_terms(predicate: impl Into<String>, terms: &[Term]) -> Result<Self> {
Ok(Self::Ground {
predicate: predicate.into(),
terms: terms
.iter()
.map(EpistemicTermKey::from_term)
.collect::<Result<Vec<_>>>()?,
})
}
fn predicate(&self) -> &str {
match self {
Self::Arity { predicate, .. } | Self::Ground { predicate, .. } => predicate,
}
}
fn arity(&self) -> usize {
match self {
Self::Arity { arity, .. } => *arity,
Self::Ground { terms, .. } => terms.len(),
}
}
fn matches_atom(&self, atom: &Atom) -> bool {
if self.predicate() != atom.predicate || self.arity() != atom.arity() {
return false;
}
match self {
Self::Arity { .. } => true,
Self::Ground { terms, .. } => atom
.terms
.iter()
.map(EpistemicTermKey::from_term)
.collect::<Result<Vec<_>>>()
.is_ok_and(|atom_terms| atom_terms == *terms),
}
}
fn overlaps(&self, other: &Self) -> bool {
if self.predicate() != other.predicate() || self.arity() != other.arity() {
return false;
}
matches!(self, Self::Arity { .. }) || matches!(other, Self::Arity { .. }) || self == other
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EpistemicInterpretation {
known: BTreeSet<EpistemicAtomKey>,
possible: BTreeSet<EpistemicAtomKey>,
rejected: BTreeSet<EpistemicAtomKey>,
}
impl EpistemicInterpretation {
pub fn new() -> Self {
Self::default()
}
pub fn with_known(mut self, predicate: impl Into<String>, arity: usize) -> Self {
self.known
.insert(EpistemicAtomKey::from_arity(predicate, arity));
self
}
pub fn with_known_terms(
mut self,
predicate: impl Into<String>,
terms: Vec<Term>,
) -> Result<Self> {
self.known
.insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
Ok(self)
}
pub fn with_possible(mut self, predicate: impl Into<String>, arity: usize) -> Self {
self.possible
.insert(EpistemicAtomKey::from_arity(predicate, arity));
self
}
pub fn with_possible_terms(
mut self,
predicate: impl Into<String>,
terms: Vec<Term>,
) -> Result<Self> {
self.possible
.insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
Ok(self)
}
pub fn with_rejected(mut self, predicate: impl Into<String>, arity: usize) -> Self {
self.rejected
.insert(EpistemicAtomKey::from_arity(predicate, arity));
self
}
pub fn with_rejected_terms(
mut self,
predicate: impl Into<String>,
terms: Vec<Term>,
) -> Result<Self> {
self.rejected
.insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
Ok(self)
}
fn first_contradiction(&self) -> Option<(String, usize)> {
self.known
.iter()
.find(|key| self.rejected.iter().any(|rejected| key.overlaps(rejected)))
.map(|key| (key.predicate().to_string(), key.arity()))
}
fn contains_known(&self, atom: &Atom) -> bool {
self.known.iter().any(|key| key.matches_atom(atom))
}
fn contains_possible(&self, atom: &Atom) -> bool {
self.possible.iter().any(|key| key.matches_atom(atom))
}
fn contains_rejected(&self, atom: &Atom) -> bool {
self.rejected.iter().any(|key| key.matches_atom(atom))
}
fn epistemic_guess_count(&self) -> usize {
self.known.len() + self.possible.len() + self.rejected.len()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EpistemicWorld {
facts: BTreeSet<EpistemicAtomKey>,
}
impl EpistemicWorld {
pub fn new() -> Self {
Self::default()
}
pub fn with_fact(mut self, predicate: impl Into<String>, arity: usize) -> Self {
self.facts
.insert(EpistemicAtomKey::from_arity(predicate, arity));
self
}
pub fn with_fact_terms(
mut self,
predicate: impl Into<String>,
terms: Vec<Term>,
) -> Result<Self> {
self.facts
.insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
Ok(self)
}
fn contains(&self, atom: &Atom) -> bool {
self.facts.iter().any(|fact| fact.matches_atom(atom))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpistemicWorldView {
worlds: Vec<EpistemicWorld>,
}
impl EpistemicWorldView {
pub fn from_worlds(worlds: Vec<EpistemicWorld>) -> Result<Self> {
if worlds.is_empty() {
return Err(xlog_core::XlogError::UnsupportedEpistemicConstruct {
construct: "world view boundary".to_string(),
context: "world view requires at least one stable model".to_string(),
});
}
Ok(Self { worlds })
}
pub fn world_count(&self) -> usize {
self.worlds.len()
}
pub fn evaluate(&self, lit: &EpistemicLiteral) -> TruthValue {
let value = match lit.op {
EpistemicOp::Know => self.worlds.iter().all(|world| world.contains(&lit.atom)),
EpistemicOp::Possible => self.worlds.iter().any(|world| world.contains(&lit.atom)),
};
TruthValue::from_bool(if lit.negated { !value } else { value })
}
}
pub fn plan_epistemic_gpu_execution(program: &Program) -> Result<EpistemicGpuPlan> {
reject_recursive_epistemic_program(program)?;
let eir = build_eir(program)?;
let mut epistemic_literals = Vec::new();
let mut reductions = Vec::new();
let mut tuple_membership_bindings = Vec::new();
let mut solver_assumption_bindings = Vec::new();
for (rule_index, rule) in eir.rules.iter().enumerate() {
let mut rule_epistemic_literals = Vec::new();
let mut relational_body_atoms = 0usize;
let mut has_negated_relational_atom = false;
for lit in &rule.body {
match lit {
EirBodyLiteral::Relational { negated, .. } => {
if *negated {
has_negated_relational_atom = true;
} else {
relational_body_atoms += 1;
}
}
EirBodyLiteral::Epistemic(lit) => {
rule_epistemic_literals.push(lit.clone());
}
EirBodyLiteral::Constraint | EirBodyLiteral::Binding => {}
}
}
if rule_epistemic_literals.is_empty() {
continue;
}
let reduction_index = reductions.len();
for lit in rule_epistemic_literals {
let lit = flatten_epistemic_literal(&lit)?;
let literal_index = epistemic_literals.len();
let augmented_head_terms = augmented_eir_head_terms(rule);
tuple_membership_bindings.push(EpistemicTupleMembershipBinding {
literal_index,
reduction_index,
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity,
key_columns: (0..lit.atom.arity).collect(),
bound_output_columns: bound_output_columns_for_terms(
&lit.atom.terms,
&augmented_head_terms,
),
key_terms: lit.atom.terms.clone(),
op: lit.op,
negated: lit.negated,
});
solver_assumption_bindings.push(EpistemicSolverAssumptionBinding {
literal_index,
reduction_index,
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity,
terms: lit.atom.terms.clone(),
op: lit.op,
negated: lit.negated,
});
epistemic_literals.push(lit);
}
reductions.push(EpistemicReductionPlan {
rule_index,
head_predicate: rule.head.predicate.clone(),
public_head_arity: rule.head.terms.len(),
relational_body_atoms,
wcoj_status: wcoj_status_for_reduction(
relational_body_atoms,
has_negated_relational_atom,
),
});
}
if epistemic_literals.is_empty() {
return Err(xlog_core::XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU execution plan".to_string(),
context: "requires at least one epistemic literal".to_string(),
});
}
let constraints = lower_epistemic_constraints(
&eir,
&mut epistemic_literals,
&reductions,
&mut tuple_membership_bindings,
&mut solver_assumption_bindings,
)?;
let final_output_columns = final_output_columns_for_eir(&eir);
let gpu_plan = EpistemicGpuPlan::new(eir.mode, epistemic_literals, reductions)
.with_tuple_membership_bindings(tuple_membership_bindings)
.with_constraints(constraints)
.with_final_output_columns(final_output_columns)
.with_solver_contract(EpistemicSolverServiceContract::production_default(
solver_assumption_bindings,
));
gpu_plan.validate_tuple_membership_bindings()?;
gpu_plan.validate_solver_contract()?;
gpu_plan.validate_constraints()?;
Ok(gpu_plan)
}
fn lower_epistemic_constraints(
eir: &EirProgram,
epistemic_literals: &mut Vec<EirEpistemicLiteral>,
reductions: &[EpistemicReductionPlan],
tuple_membership_bindings: &mut Vec<EpistemicTupleMembershipBinding>,
solver_assumption_bindings: &mut Vec<EpistemicSolverAssumptionBinding>,
) -> Result<Vec<EpistemicConstraintPlan>> {
let mut constraint_plans = Vec::new();
for (constraint_index, constraint) in eir.constraints.iter().enumerate() {
let has_epistemic = constraint
.body
.iter()
.any(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)));
if !has_epistemic {
continue;
}
if reductions.is_empty() {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU world-view constraint".to_string(),
context: format!(
"constraint[{constraint_index}] is an epistemic integrity constraint but the \
program has no epistemic rule to host its world-view evaluation; add an \
epistemic rule whose reduced model provides the accepted world view, or \
express the constraint over an existing epistemic rule"
),
});
}
let reduction_index = reductions.len() - 1;
let mut flattened_literals = Vec::new();
for lit in &constraint.body {
match lit {
EirBodyLiteral::Epistemic(lit) => {
flattened_literals.push(flatten_epistemic_literal(lit)?);
}
EirBodyLiteral::Relational { .. }
| EirBodyLiteral::Constraint
| EirBodyLiteral::Binding => {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU world-view constraint".to_string(),
context: format!(
"constraint[{constraint_index}] mixes non-epistemic body literals with \
modal literals; world-view integrity constraints currently support \
pure know/possible conjunctions so the constraint can be evaluated \
against accepted world views without an ordinary-RIR rewrite"
),
});
}
}
}
let mut variable_occurrences: std::collections::BTreeMap<String, usize> =
std::collections::BTreeMap::new();
for lit in &flattened_literals {
for term in &lit.atom.terms {
if let EirTerm::Variable(name) = term {
*variable_occurrences.entry(name.clone()).or_insert(0) += 1;
}
}
}
let mut literal_indices = Vec::new();
for lit in flattened_literals {
let mut anonymized_terms = Vec::with_capacity(lit.atom.terms.len());
for term in &lit.atom.terms {
match term {
EirTerm::Integer(_) | EirTerm::Symbol(_) | EirTerm::Anonymous => {
anonymized_terms.push(term.clone());
}
EirTerm::Variable(name) => {
if variable_occurrences.get(name).copied().unwrap_or(0) > 1 {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU world-view constraint".to_string(),
context: format!(
"constraint[{constraint_index}] reuses tuple-key variable \
{name} across literals/positions; shared-variable epistemic \
constraint joins (`:- know p(X), q(X).` / diagonal \
`:- know p(X, X).`) are not yet implemented for GPU world-view \
pruning. Single-occurrence variable keys (`:- know p(X).`) are \
supported and range existentially over the modal relation"
),
});
}
if lit.negated {
return Err(XlogError::Compilation(format!(
"v0.8.5 naf error: unbound variable {name} in negated modal atom \
{}/{} in constraint[{constraint_index}]; bind it before not with \
a positive atom, or use '_' for existential positions",
lit.atom.predicate, lit.atom.arity
)));
}
anonymized_terms.push(EirTerm::Anonymous);
}
other => {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU world-view constraint".to_string(),
context: format!(
"constraint[{constraint_index}] uses {} {}/{} with an unsupported \
tuple-key term {other:?}; headless world-view constraints support \
ground (integer/symbol) and single-occurrence variable/anonymous \
modal atoms",
eir_epistemic_literal_label(&lit),
lit.atom.predicate,
lit.atom.arity
),
});
}
}
}
let mut lit = lit;
lit.atom.terms = anonymized_terms;
let literal_index = epistemic_literals.len();
let bound_output_columns = vec![None; lit.atom.arity];
tuple_membership_bindings.push(EpistemicTupleMembershipBinding {
literal_index,
reduction_index,
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity,
key_columns: (0..lit.atom.arity).collect(),
key_terms: lit.atom.terms.clone(),
bound_output_columns,
op: lit.op,
negated: lit.negated,
});
solver_assumption_bindings.push(EpistemicSolverAssumptionBinding {
literal_index,
reduction_index,
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity,
terms: lit.atom.terms.clone(),
op: lit.op,
negated: lit.negated,
});
epistemic_literals.push(lit);
literal_indices.push(literal_index);
}
constraint_plans.push(EpistemicConstraintPlan {
constraint_index,
literal_indices,
});
}
Ok(constraint_plans)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecursiveEpistemicClass {
NonRecursive,
CaseA,
CaseB,
}
fn reject_recursive_epistemic_program(program: &Program) -> Result<()> {
match classify_recursive_epistemic_program(program) {
Ok(RecursiveEpistemicClass::NonRecursive) => Ok(()),
Ok(RecursiveEpistemicClass::CaseA | RecursiveEpistemicClass::CaseB) => {
Err(recursive_epistemic_rejection(
"an epistemic program contains ordinary recursion; the single-pass epistemic GPU \
planner cannot iterate a recursive fixpoint. Case-A/Case-B recursive epistemic \
programs are executed through the ordinary recursive engine via \
`try_reduce_case_a_recursive_epistemic_program`, not this planner.",
))
}
Err(err) => Err(err),
}
}
pub fn classify_recursive_epistemic_program(program: &Program) -> Result<RecursiveEpistemicClass> {
let has_epistemic = program.rules.iter().any(|rule| {
rule.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
});
if !has_epistemic {
return Ok(RecursiveEpistemicClass::NonRecursive);
}
let mut deps: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for rule in &program.rules {
let entry = deps.entry(rule.head.predicate.as_str()).or_default();
for lit in &rule.body {
if let BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) = lit {
entry.insert(atom.predicate.as_str());
}
}
}
fn reaches<'a>(
start: &'a str,
target: &str,
deps: &BTreeMap<&'a str, BTreeSet<&'a str>>,
seen: &mut BTreeSet<&'a str>,
) -> bool {
let Some(next) = deps.get(start) else {
return false;
};
for &pred in next {
if pred == target {
return true;
}
if seen.insert(pred) && reaches(pred, target, deps, seen) {
return true;
}
}
false
}
let recursive_predicates: BTreeSet<&str> = deps
.keys()
.copied()
.filter(|pred| reaches(pred, pred, &deps, &mut BTreeSet::new()))
.collect();
if recursive_predicates.is_empty() {
return Ok(RecursiveEpistemicClass::NonRecursive);
}
let invariant = InvariantRelations::analyze(program);
let mut saw_case_b = false;
let mut saw_negated_non_invariant_modal = false;
for rule in &program.rules {
for lit in &rule.body {
let BodyLiteral::Epistemic(modal) = lit else {
continue;
};
if invariant.is_invariant(&modal.atom.predicate) {
continue;
}
if modal.negated {
saw_negated_non_invariant_modal = true;
saw_case_b = true;
continue;
}
saw_case_b = true;
}
}
if saw_negated_non_invariant_modal {
let _reduced = reduce_case_a_epistemic_program_to_ordinary(program);
}
let has_epistemic_constraint = program.constraints.iter().any(|constraint| {
constraint
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
});
if has_epistemic_constraint {
return Err(recursive_epistemic_rejection(
"a recursive epistemic program carries an epistemic integrity constraint \
(`:- know ...` / `:- not know ...`). Recursive epistemic programs execute \
through the ordinary semi-naive engine, which does not run the world-view \
constraint kernel, and the recursive reduction would silently DROP the \
modal constraint -- yielding a result that ignores it. To keep results \
sound this fails closed rather than silently dropping the constraint. \
Remove the recursion or express the integrity constraint over a \
non-recursive (single-pass) epistemic relation.",
));
}
if saw_case_b {
Ok(RecursiveEpistemicClass::CaseB)
} else {
Ok(RecursiveEpistemicClass::CaseA)
}
}
fn recursive_epistemic_rejection(context: &str) -> XlogError {
XlogError::UnsupportedEpistemicConstruct {
construct: "recursive epistemic program".to_string(),
context: context.to_string(),
}
}
struct InvariantRelations<'a> {
ordinary_deps: BTreeMap<&'a str, BTreeSet<&'a str>>,
epistemic_heads: BTreeSet<&'a str>,
derived_heads: BTreeSet<&'a str>,
}
impl<'a> InvariantRelations<'a> {
fn analyze(program: &'a Program) -> Self {
let mut ordinary_deps: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
let mut epistemic_heads: BTreeSet<&str> = BTreeSet::new();
let mut derived_heads: BTreeSet<&str> = BTreeSet::new();
for rule in &program.rules {
if rule.body.is_empty() {
continue;
}
let head = rule.head.predicate.as_str();
derived_heads.insert(head);
let entry = ordinary_deps.entry(head).or_default();
for lit in &rule.body {
match lit {
BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
entry.insert(atom.predicate.as_str());
}
BodyLiteral::Epistemic(_) => {
epistemic_heads.insert(head);
}
BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
}
}
}
Self {
ordinary_deps,
epistemic_heads,
derived_heads,
}
}
fn is_invariant(&self, predicate: &str) -> bool {
let mut seen = BTreeSet::new();
self.is_invariant_inner(predicate, &mut seen)
}
fn is_invariant_inner<'b>(&'b self, predicate: &'b str, seen: &mut BTreeSet<&'b str>) -> bool {
if !seen.insert(predicate) {
return false;
}
if !self.derived_heads.contains(predicate) {
return true;
}
if self.epistemic_heads.contains(predicate) {
return false;
}
match self.ordinary_deps.get(predicate) {
None => true,
Some(deps) => deps.iter().all(|dep| self.is_invariant_inner(dep, seen)),
}
}
}
fn eir_epistemic_literal_label(lit: &xlog_ir::EirEpistemicLiteral) -> &'static str {
match (lit.negated, lit.op) {
(false, EirEpistemicOp::Know) => "know",
(false, EirEpistemicOp::Possible) => "possible",
(true, EirEpistemicOp::Know) => "not know",
(true, EirEpistemicOp::Possible) => "not possible",
}
}
fn has_independent_founded_support(eir: &EirProgram, atom: &xlog_ir::EirAtom) -> bool {
if atom.arity > 0 && !atom.terms.iter().all(eir_term_is_ground) {
return false;
}
let mut support_stack = Vec::new();
has_independent_founded_support_inner(eir, atom, &mut support_stack)
}
fn has_tuple_level_independent_founded_support(
eir: &EirProgram,
modal_rule: &xlog_ir::EirRule,
atom: &xlog_ir::EirAtom,
) -> bool {
if atom.arity == 0 {
return false;
}
let modal_domain = positive_relational_body_atoms(modal_rule);
eir.rules.iter().any(|support_rule| {
if !support_rule_head_matches_modal_atom(support_rule, atom) {
return false;
}
let mut support_stack = vec![(atom.predicate.clone(), atom.arity)];
if !eir_rule_has_independent_founded_body(eir, support_rule, &mut support_stack) {
return false;
}
let Some(substitution) = head_substitution_to_atom(&support_rule.head, atom) else {
return false;
};
let support_domain = positive_relational_body_atoms(support_rule);
if support_domain.is_empty() {
return false;
}
let Some(substituted_support_domain) = support_domain
.iter()
.map(|atom| substitute_eir_atom(atom, &substitution))
.collect::<Option<Vec<_>>>()
else {
return false;
};
substituted_support_domain.iter().all(|support_atom| {
modal_domain
.iter()
.any(|modal_atom| modal_atom == support_atom)
})
})
}
fn positive_relational_body_atoms(rule: &xlog_ir::EirRule) -> Vec<xlog_ir::EirAtom> {
rule.body
.iter()
.filter_map(|lit| match lit {
EirBodyLiteral::Relational {
negated: false,
atom,
} => Some(atom.clone()),
_ => None,
})
.collect()
}
fn support_rule_head_matches_modal_atom(rule: &xlog_ir::EirRule, atom: &xlog_ir::EirAtom) -> bool {
rule.head.predicate == atom.predicate
&& rule.head.arity == atom.arity
&& head_substitution_to_atom(&rule.head, atom).is_some()
}
fn head_substitution_to_atom(
head: &xlog_ir::EirAtom,
atom: &xlog_ir::EirAtom,
) -> Option<BTreeMap<String, EirTerm>> {
if head.predicate != atom.predicate || head.arity != atom.arity {
return None;
}
let mut substitution = BTreeMap::new();
for (head_term, atom_term) in head.terms.iter().zip(&atom.terms) {
match head_term {
EirTerm::Variable(name) => match substitution.get(name) {
Some(existing) if existing != atom_term => return None,
Some(_) => {}
None => {
substitution.insert(name.clone(), atom_term.clone());
}
},
EirTerm::Anonymous => return None,
other if other == atom_term => {}
_ => return None,
}
}
Some(substitution)
}
fn substitute_eir_atom(
atom: &xlog_ir::EirAtom,
substitution: &BTreeMap<String, EirTerm>,
) -> Option<xlog_ir::EirAtom> {
let terms = atom
.terms
.iter()
.map(|term| substitute_eir_term(term, substitution))
.collect::<Option<Vec<_>>>()?;
Some(xlog_ir::EirAtom {
predicate: atom.predicate.clone(),
arity: atom.arity,
terms,
})
}
fn substitute_eir_term(
term: &EirTerm,
substitution: &BTreeMap<String, EirTerm>,
) -> Option<EirTerm> {
match term {
EirTerm::Variable(name) => Some(
substitution
.get(name)
.cloned()
.unwrap_or_else(|| term.clone()),
),
EirTerm::Anonymous => None,
EirTerm::List(items) => items
.iter()
.map(|item| substitute_eir_term(item, substitution))
.collect::<Option<Vec<_>>>()
.map(EirTerm::List),
EirTerm::Cons { head, tail } => Some(EirTerm::Cons {
head: Box::new(substitute_eir_term(head, substitution)?),
tail: Box::new(substitute_eir_term(tail, substitution)?),
}),
EirTerm::Compound { functor, args } => Some(EirTerm::Compound {
functor: functor.clone(),
args: args
.iter()
.map(|arg| substitute_eir_term(arg, substitution))
.collect::<Option<Vec<_>>>()?,
}),
EirTerm::Aggregate { .. } => None,
EirTerm::Integer(_)
| EirTerm::FloatBits(_)
| EirTerm::String(_)
| EirTerm::Symbol(_)
| EirTerm::PredRef(_) => Some(term.clone()),
}
}
fn has_independent_founded_support_inner(
eir: &EirProgram,
atom: &xlog_ir::EirAtom,
support_stack: &mut Vec<(String, usize)>,
) -> bool {
if atom.arity > 0 && !atom.terms.iter().all(eir_term_is_ground) {
return false;
}
let key = (atom.predicate.clone(), atom.arity);
if support_stack.iter().any(|ancestor| ancestor == &key) {
return false;
}
support_stack.push(key);
let supported = eir.rules.iter().any(|rule| {
let Some(substitution) = head_substitution_to_atom(&rule.head, atom) else {
return false;
};
eir_rule_has_independent_founded_body_with_substitution(
eir,
rule,
&substitution,
support_stack,
)
});
support_stack.pop();
supported
}
fn eir_rule_has_independent_founded_body(
eir: &EirProgram,
rule: &xlog_ir::EirRule,
support_stack: &mut Vec<(String, usize)>,
) -> bool {
eir_rule_has_independent_founded_body_with_substitution(
eir,
rule,
&BTreeMap::new(),
support_stack,
)
}
fn eir_rule_has_independent_founded_body_with_substitution(
eir: &EirProgram,
rule: &xlog_ir::EirRule,
substitution: &BTreeMap<String, EirTerm>,
support_stack: &mut Vec<(String, usize)>,
) -> bool {
rule.body.iter().all(|lit| match lit {
EirBodyLiteral::Epistemic(_) => false,
EirBodyLiteral::Relational { negated: true, .. } => false,
EirBodyLiteral::Relational {
negated: false,
atom,
} => {
let Some(atom) = substitute_eir_atom(atom, substitution) else {
return false;
};
let dependency_key = (atom.predicate.clone(), atom.arity);
if support_stack
.iter()
.any(|ancestor| ancestor == &dependency_key)
{
return false;
}
if !eir
.rules
.iter()
.any(|rule| head_substitution_to_atom(&rule.head, &atom).is_some())
{
return true;
}
has_independent_founded_support_inner(eir, &atom, support_stack)
}
EirBodyLiteral::Constraint | EirBodyLiteral::Binding => true,
})
}
fn eir_term_is_ground(term: &EirTerm) -> bool {
match term {
EirTerm::Variable(_) | EirTerm::Anonymous | EirTerm::Aggregate { .. } => false,
EirTerm::Integer(_) | EirTerm::FloatBits(_) | EirTerm::String(_) | EirTerm::Symbol(_) => {
true
}
EirTerm::List(items) => items.iter().all(eir_term_is_ground),
EirTerm::Cons { head, tail } => eir_term_is_ground(head) && eir_term_is_ground(tail),
EirTerm::Compound { args, .. } => args.iter().all(eir_term_is_ground),
EirTerm::PredRef(_) => true,
}
}
pub fn compile_epistemic_gpu_execution(program: &Program) -> Result<EpistemicExecutablePlan> {
compile_epistemic_gpu_execution_with_stats_snapshot(program, None)
}
pub fn compile_epistemic_gpu_execution_with_stats_snapshot(
program: &Program,
stats_snapshot: Option<&StatsSnapshot>,
) -> Result<EpistemicExecutablePlan> {
compile_epistemic_gpu_execution_inner(program, stats_snapshot, false)
}
fn compile_epistemic_gpu_execution_inner(
program: &Program,
stats_snapshot: Option<&StatsSnapshot>,
allow_multiple_output_heads: bool,
) -> Result<EpistemicExecutablePlan> {
let gpu_plan = plan_epistemic_gpu_execution(program)?;
if !allow_multiple_output_heads {
require_single_epistemic_output_relation(&gpu_plan)?;
}
let reduced_program = reduce_epistemic_program_to_ordinary(program);
let mut compiler = Compiler::new();
let reduced_runtime_plan =
compiler.compile_program_with_stats_snapshot(&reduced_program, stats_snapshot)?;
let relation_ids = compiler
.rel_ids()
.iter()
.map(|(name, rel)| (name.clone(), *rel))
.collect();
Ok(EpistemicExecutablePlan {
gpu_plan,
relation_ids,
reduced_runtime_plan,
})
}
pub fn try_reduce_case_a_recursive_epistemic_program(program: &Program) -> Result<Option<Program>> {
match classify_recursive_epistemic_program(program)? {
RecursiveEpistemicClass::NonRecursive => Ok(None),
RecursiveEpistemicClass::CaseA | RecursiveEpistemicClass::CaseB => {
Ok(Some(reduce_case_a_epistemic_program_to_ordinary(program)))
}
}
}
fn require_single_epistemic_output_relation(gpu_plan: &EpistemicGpuPlan) -> Result<()> {
let output_relations: BTreeSet<&str> = gpu_plan
.reductions
.iter()
.map(|reduction| reduction.head_predicate.as_str())
.collect();
if output_relations.len() > 1 {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU final output relation".to_string(),
context: format!(
"single-plan GPU execution materializes one final output buffer, but reductions \
target multiple head predicates {:?}; use split GPU execution for independent \
epistemic outputs",
output_relations
),
});
}
Ok(())
}
fn reject_epistemic_constraints(program: &Program) -> Result<()> {
reject_epistemic_constraints_for_boundary(program, "epistemic GPU constraint", "GPU lowering")
}
fn reject_gpt_epistemic_constraints(program: &Program) -> Result<()> {
reject_epistemic_constraints_for_boundary(
program,
"epistemic GPT constraint",
"GPT candidate testing",
)
}
fn reject_epistemic_constraints_for_boundary(
program: &Program,
construct: &str,
boundary: &str,
) -> Result<()> {
for (constraint_index, constraint) in program.constraints.iter().enumerate() {
for lit in &constraint.body {
let BodyLiteral::Epistemic(lit) = lit else {
continue;
};
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: construct.to_string(),
context: format!(
"constraint[{constraint_index}] contains unsupported {} {}/{}; epistemic integrity constraints must be represented explicitly before {boundary}",
epistemic_literal_label(lit),
lit.atom.predicate,
lit.atom.arity()
),
});
}
}
Ok(())
}
fn epistemic_literal_label(lit: &EpistemicLiteral) -> &'static str {
match (lit.negated, lit.op) {
(false, EpistemicOp::Know) => "know",
(false, EpistemicOp::Possible) => "possible",
(true, EpistemicOp::Know) => "not know",
(true, EpistemicOp::Possible) => "not possible",
}
}
fn flatten_epistemic_literal(lit: &EirEpistemicLiteral) -> Result<EirEpistemicLiteral> {
let (arity, terms, _key_columns) =
flatten_structured_key_terms(&lit.atom.predicate, &lit.atom.terms)?;
Ok(EirEpistemicLiteral {
op: lit.op,
negated: lit.negated,
atom: xlog_ir::EirAtom {
predicate: lit.atom.predicate.clone(),
arity,
terms,
},
})
}
fn eir_term_is_scalar_key_element(term: &EirTerm) -> bool {
matches!(
term,
EirTerm::Variable(_)
| EirTerm::Anonymous
| EirTerm::Integer(_)
| EirTerm::FloatBits(_)
| EirTerm::String(_)
| EirTerm::Symbol(_)
)
}
fn flatten_structured_key_terms(
predicate: &str,
terms: &[EirTerm],
) -> Result<(usize, Vec<EirTerm>, Vec<usize>)> {
let mut flattened: Vec<EirTerm> = Vec::with_capacity(terms.len());
for term in terms {
match term {
EirTerm::List(items) => {
flatten_structured_elements(predicate, "list", items, &mut flattened)?;
}
EirTerm::Compound { functor, args } => {
flatten_structured_elements(
predicate,
&format!("compound {functor}/{}", args.len()),
args,
&mut flattened,
)?;
}
EirTerm::Cons { .. } => {
return Err(XlogError::ResourceExhausted {
context: format!(
"modal tuple-key for {predicate} uses a `cons` pattern whose tail length \
is not statically fixed, so it has no finite, typed GPU key-column set; \
bind it to a fixed-arity list literal `[a, b, ...]` instead"
),
estimated_bytes: 0,
budget_bytes: 0,
});
}
EirTerm::PredRef(name) => {
return Err(XlogError::ResourceExhausted {
context: format!(
"modal tuple-key for {predicate} uses predref `{name}`, which has no \
finite, typed GPU key-column encoding"
),
estimated_bytes: 0,
budget_bytes: 0,
});
}
EirTerm::Aggregate { op, variable } => {
return Err(XlogError::ResourceExhausted {
context: format!(
"modal tuple-key for {predicate} uses aggregate `{op}({variable})`, whose \
value is not a finite, typed GPU key-column tuple"
),
estimated_bytes: 0,
budget_bytes: 0,
});
}
scalar => flattened.push(scalar.clone()),
}
}
let arity = flattened.len();
let key_columns = (0..arity).collect();
Ok((arity, flattened, key_columns))
}
fn flatten_structured_elements(
predicate: &str,
shape: &str,
elements: &[EirTerm],
flattened: &mut Vec<EirTerm>,
) -> Result<()> {
for element in elements {
if eir_term_is_scalar_key_element(element) {
flattened.push(element.clone());
} else {
return Err(XlogError::ResourceExhausted {
context: format!(
"modal tuple-key for {predicate} nests a non-scalar element {element:?} inside \
a {shape}; only fixed-arity structures of scalar/Symbol-typed elements have a \
finite, typed GPU key-column encoding"
),
estimated_bytes: 0,
budget_bytes: 0,
});
}
}
Ok(())
}
fn bound_output_columns_for_terms(
key_terms: &[EirTerm],
output_terms: &[EirTerm],
) -> Vec<Option<usize>> {
key_terms
.iter()
.map(|term| match term {
EirTerm::Variable(variable) => output_terms.iter().position(
|head_term| matches!(head_term, EirTerm::Variable(name) if name == variable),
),
_ => None,
})
.collect()
}
fn augmented_eir_head_terms(rule: &xlog_ir::EirRule) -> Vec<EirTerm> {
let mut output_terms = rule.head.terms.clone();
for lit in &rule.body {
let EirBodyLiteral::Epistemic(lit) = lit else {
continue;
};
let key_terms = flatten_structured_key_terms(&lit.atom.predicate, &lit.atom.terms)
.map(|(_, terms, _)| terms)
.unwrap_or_else(|_| lit.atom.terms.clone());
for term in &key_terms {
let EirTerm::Variable(variable) = term else {
continue;
};
if !output_terms
.iter()
.any(|head_term| matches!(head_term, EirTerm::Variable(name) if name == variable))
{
output_terms.push(EirTerm::Variable(variable.clone()));
}
}
}
output_terms
}
fn final_output_columns_for_eir(eir: &EirProgram) -> Option<Vec<usize>> {
let mut final_columns = Vec::new();
let mut needs_projection = false;
for rule in &eir.rules {
if !rule
.body
.iter()
.any(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)))
{
continue;
}
let augmented_len = augmented_eir_head_terms(rule).len();
if augmented_len > rule.head.terms.len() {
needs_projection = true;
}
if final_columns.is_empty() {
final_columns = (0..rule.head.terms.len()).collect();
}
}
if needs_projection {
Some(final_columns)
} else {
None
}
}
fn faeel_unfounded_self_support_rule_indices(program: &Program) -> Vec<usize> {
let Ok(eir) = build_eir(program) else {
return Vec::new();
};
if eir.mode != EirEpistemicMode::Faeel {
return Vec::new();
}
let mut indices = Vec::new();
for (index, (rule, eir_rule)) in program.rules.iter().zip(&eir.rules).enumerate() {
let modal_only_output_variables = modal_only_bound_output_variables(rule);
let drop = eir_rule.body.iter().any(|lit| {
let EirBodyLiteral::Epistemic(modal) = lit else {
return false;
};
if modal.atom.predicate != eir_rule.head.predicate
|| modal.atom.arity != eir_rule.head.arity
{
return false;
}
if has_independent_founded_support(&eir, &modal.atom)
|| has_tuple_level_independent_founded_support(&eir, eir_rule, &modal.atom)
{
return false;
}
if modal
.atom
.terms
.iter()
.any(|term| matches!(term, EirTerm::Variable(name) if modal_only_output_variables.contains(name)))
{
return false;
}
true
});
if drop {
indices.push(index);
}
}
indices
}
pub fn reduce_epistemic_program_to_ordinary(program: &Program) -> Program {
reduce_epistemic_program_to_ordinary_inner(program, &BTreeSet::new())
}
pub fn reduce_epistemic_program_to_ordinary_for_stratified_schema(program: &Program) -> Program {
let determined = EpistemicallyDeterminedPredicates::analyze(program);
reduce_epistemic_program_to_ordinary_inner(program, &determined.determined)
}
fn reduce_epistemic_program_to_ordinary_inner(
program: &Program,
schema_only_determined_resolve: &BTreeSet<String>,
) -> Program {
let mut reduced = program.clone();
for index in faeel_unfounded_self_support_rule_indices(program)
.into_iter()
.rev()
{
reduced.rules.remove(index);
}
let invariant = InvariantRelations::analyze(program);
let mut resolved_augmented_heads: BTreeSet<String> = BTreeSet::new();
for rule in &mut reduced.rules {
let modal_only_output_variables = modal_only_bound_output_variables(rule);
append_body_local_tuple_key_variables_to_head(rule);
let was_fact = rule.body.is_empty();
let had_epistemic_body = rule
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
let mut resolved_here = false;
for lit in &mut rule.body {
if let BodyLiteral::Epistemic(modal) = lit {
let resolvable_target = invariant.is_invariant(&modal.atom.predicate)
|| schema_only_determined_resolve.contains(&modal.atom.predicate);
if !modal.negated
&& resolvable_target
&& modal_atom_binds_output_variable(modal, &modal_only_output_variables)
{
*lit = BodyLiteral::Positive(modal.atom.clone());
resolved_here = true;
}
}
}
if resolved_here {
resolved_augmented_heads.insert(rule.head.predicate.clone());
}
rule.body
.retain(|lit| !matches!(lit, BodyLiteral::Epistemic(_)));
if !was_fact && had_epistemic_body && rule.body.is_empty() {
rule.body.push(BodyLiteral::Comparison(Comparison {
left: Term::Integer(1),
op: CompOp::Eq,
right: Term::Integer(1),
}));
}
}
let augmented_heads =
reconcile_augmented_head_declarations(&mut reduced, &resolved_augmented_heads);
if !augmented_heads.is_empty() {
reduced
.queries
.retain(|query| !augmented_heads.contains(&query.atom.predicate));
}
reduced.constraints.retain(|constraint| {
!constraint
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
});
reduced
}
pub fn reduce_case_a_epistemic_program_to_ordinary(program: &Program) -> Program {
let mut reduced = program.clone();
let mode = program.directives.epistemic_mode_or_default();
let invariant = InvariantRelations::analyze(program);
for rule in &mut reduced.rules {
for lit in &mut rule.body {
if let BodyLiteral::Epistemic(modal) = lit {
*lit = if mode == EpistemicMode::G91
&& modal.op == EpistemicOp::Possible
&& !modal.negated
&& !invariant.is_invariant(&modal.atom.predicate)
{
BodyLiteral::Comparison(Comparison {
left: Term::Integer(1),
op: CompOp::Eq,
right: Term::Integer(1),
})
} else if modal.negated {
BodyLiteral::Negated(modal.atom.clone())
} else {
BodyLiteral::Positive(modal.atom.clone())
};
}
}
}
reduced.constraints.retain(|constraint| {
!constraint
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
});
reduced
}
fn modal_only_bound_output_variables(rule: &crate::ast::Rule) -> BTreeSet<String> {
let mut positively_bound: BTreeSet<&str> = BTreeSet::new();
for lit in &rule.body {
if let BodyLiteral::Positive(atom) = lit {
for term in &atom.terms {
if let Term::Variable(name) = term {
positively_bound.insert(name.as_str());
}
}
}
}
let mut modal_only = BTreeSet::new();
let mut consider = |name: &str| {
if name != "_" && !positively_bound.contains(name) {
modal_only.insert(name.to_string());
}
};
for term in &rule.head.terms {
if let Term::Variable(name) = term {
consider(name);
}
}
for lit in &rule.body {
if let BodyLiteral::Epistemic(lit) = lit {
for term in &lit.atom.terms {
if let Term::Variable(name) = term {
consider(name);
}
}
}
}
modal_only
}
fn modal_atom_binds_output_variable(
modal: &EpistemicLiteral,
modal_only_output_variables: &BTreeSet<String>,
) -> bool {
modal.atom.terms.iter().any(
|term| matches!(term, Term::Variable(name) if modal_only_output_variables.contains(name)),
)
}
fn reconcile_augmented_head_declarations(
reduced: &mut Program,
resolved_augmented_heads: &BTreeSet<String>,
) -> BTreeSet<String> {
use crate::ast::{PredColumn, TypeRef};
let mut augmented_heads = BTreeSet::new();
let mut max_arity: BTreeMap<String, usize> = BTreeMap::new();
let mut inferred_types: BTreeMap<String, Vec<Option<TypeRef>>> = BTreeMap::new();
let declared_types: BTreeMap<String, Vec<TypeRef>> = reduced
.predicates
.iter()
.map(|decl| (decl.name.clone(), decl.types.clone()))
.collect();
for rule in &reduced.rules {
if rule.body.is_empty() {
continue;
}
if !resolved_augmented_heads.contains(&rule.head.predicate) {
continue;
}
let head = rule.head.predicate.as_str();
let arity = rule.head.terms.len();
let entry = max_arity.entry(head.to_string()).or_insert(0);
if arity > *entry {
*entry = arity;
}
let types = inferred_types
.entry(head.to_string())
.or_insert_with(|| vec![None; arity]);
if types.len() < arity {
types.resize(arity, None);
}
for (col, term) in rule.head.terms.iter().enumerate() {
if types[col].is_some() {
continue;
}
let Term::Variable(head_var) = term else {
continue;
};
for lit in &rule.body {
let BodyLiteral::Positive(atom) = lit else {
continue;
};
let Some(pos) = atom
.terms
.iter()
.position(|t| matches!(t, Term::Variable(name) if name == head_var))
else {
continue;
};
if let Some(decl_types) = declared_types.get(&atom.predicate) {
if let Some(typ) = decl_types.get(pos) {
types[col] = Some(typ.clone());
break;
}
}
}
}
}
for decl in &mut reduced.predicates {
let Some(&target_arity) = max_arity.get(&decl.name) else {
continue;
};
if target_arity <= decl.types.len() {
continue;
}
let inferred = inferred_types.get(&decl.name);
for col in decl.types.len()..target_arity {
let typ = inferred
.and_then(|types| types.get(col))
.and_then(|t| t.clone())
.unwrap_or(TypeRef::Scalar(xlog_core::ScalarType::U32));
decl.types.push(typ.clone());
decl.columns.push(PredColumn { name: None, typ });
}
augmented_heads.insert(decl.name.clone());
}
augmented_heads
}
fn append_body_local_tuple_key_variables_to_head(rule: &mut crate::ast::Rule) {
let mut hidden_variables = Vec::new();
for lit in &rule.body {
let BodyLiteral::Epistemic(lit) = lit else {
continue;
};
for term in &lit.atom.terms {
let Term::Variable(variable) = term else {
continue;
};
if variable == "_" {
continue;
}
let already_in_head = rule
.head
.terms
.iter()
.any(|head_term| matches!(head_term, Term::Variable(name) if name == variable));
if !already_in_head && !hidden_variables.iter().any(|name| name == variable) {
hidden_variables.push(variable.clone());
}
}
}
for variable in hidden_variables {
rule.head.terms.push(Term::Variable(variable));
}
}
fn wcoj_status_for_reduction(
relational_body_atoms: usize,
has_negated_relational_atom: bool,
) -> EpistemicWcojReductionStatus {
if relational_body_atoms >= 3 && !has_negated_relational_atom {
EpistemicWcojReductionStatus::RequiresPlannerEligibility
} else {
EpistemicWcojReductionStatus::NotWcojCandidate
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FaeelCandidateResult {
Model,
NoModel(FaeelNoModelReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FaeelNoModelReason {
UnfoundedPossible {
predicate: String,
arity: usize,
},
Contradiction {
predicate: String,
arity: usize,
},
UnsatisfiedLiteral {
predicate: String,
arity: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GeneratePropagateTestConfig {
pub max_candidates: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GeneratePropagateTestTrace {
pub generated: usize,
pub guesses: usize,
pub propagated: usize,
pub pruned: usize,
pub reduced_program_models: usize,
pub tested: usize,
pub accepted: usize,
pub accepted_world_views: usize,
pub rejected: usize,
pub rejection_reasons: Vec<FaeelNoModelReason>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratePropagateTestOutcome {
pub trace: GeneratePropagateTestTrace,
pub accepted_candidate_indices: Vec<usize>,
pub rejected_candidate_indices: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum EpistemicComponentMergeReason {
SharedHeadPredicate {
predicate: String,
},
DerivedPredicate {
predicate: String,
},
SharedModalPredicate {
predicate: String,
},
Constraint {
predicates: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpistemicDependencyComponent {
pub predicates: Vec<String>,
pub rule_indices: Vec<usize>,
pub merge_reasons: Vec<EpistemicComponentMergeReason>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpistemicDependencyGraph {
pub components: Vec<EpistemicDependencyComponent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpistemicSplitPlan {
pub components: Vec<EpistemicDependencyComponent>,
}
impl EpistemicSplitPlan {
pub fn recomposed_rule_indices(&self) -> Vec<usize> {
let mut indices: Vec<usize> = self
.components
.iter()
.flat_map(|component| component.rule_indices.iter().copied())
.collect();
indices.sort_unstable();
indices
}
}
#[derive(Debug, Clone)]
pub struct EpistemicSplitExecutableComponent {
pub component: EpistemicDependencyComponent,
pub executable: EpistemicExecutablePlan,
}
#[derive(Debug, Clone)]
pub struct EpistemicSplitExecutablePlan {
pub split_plan: EpistemicSplitPlan,
pub components: Vec<EpistemicSplitExecutableComponent>,
}
impl EpistemicSplitExecutablePlan {
pub fn recomposed_rule_indices(&self) -> Vec<usize> {
let mut indices: Vec<usize> = self
.components
.iter()
.flat_map(|component| component.component.rule_indices.iter().copied())
.collect();
indices.sort_unstable();
indices
}
pub fn planned_recomposed_rule_indices(&self) -> Vec<usize> {
self.split_plan.recomposed_rule_indices()
}
pub fn recomposed_components(&self) -> Vec<&EpistemicSplitExecutableComponent> {
let mut components: Vec<_> = self.components.iter().collect();
components.sort_by_key(|component| {
component
.component
.rule_indices
.iter()
.copied()
.min()
.unwrap_or(usize::MAX)
});
components
}
}
pub fn evaluate_epistemic_literal(
mode: EpistemicMode,
lit: &EpistemicLiteral,
interpretation: &EpistemicInterpretation,
) -> TruthValue {
let value = match lit.op {
EpistemicOp::Know => interpretation.contains_known(&lit.atom),
EpistemicOp::Possible => match mode {
EpistemicMode::G91 => {
interpretation.contains_known(&lit.atom)
|| interpretation.contains_possible(&lit.atom)
}
EpistemicMode::Faeel => interpretation.contains_known(&lit.atom),
},
};
TruthValue::from_bool(if lit.negated { !value } else { value })
}
pub fn evaluate_faeel_candidate(
program: &Program,
interpretation: &EpistemicInterpretation,
) -> Result<FaeelCandidateResult> {
evaluate_epistemic_candidate(program, interpretation, EpistemicMode::Faeel)
}
pub fn evaluate_epistemic_candidate(
program: &Program,
interpretation: &EpistemicInterpretation,
mode: EpistemicMode,
) -> Result<FaeelCandidateResult> {
reject_gpt_epistemic_constraints(program)?;
if let Some((predicate, arity)) = interpretation.first_contradiction() {
return Ok(FaeelCandidateResult::NoModel(
FaeelNoModelReason::Contradiction { predicate, arity },
));
}
for rule in &program.rules {
for body_lit in &rule.body {
let BodyLiteral::Epistemic(lit) = body_lit else {
continue;
};
if interpretation.contains_known(&lit.atom)
&& interpretation.contains_rejected(&lit.atom)
{
return Ok(FaeelCandidateResult::NoModel(
FaeelNoModelReason::Contradiction {
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity(),
},
));
}
if mode == EpistemicMode::Faeel
&& lit.op == EpistemicOp::Possible
&& interpretation.contains_possible(&lit.atom)
&& !interpretation.contains_known(&lit.atom)
{
return Ok(FaeelCandidateResult::NoModel(
FaeelNoModelReason::UnfoundedPossible {
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity(),
},
));
}
if evaluate_epistemic_literal(mode, lit, interpretation) == TruthValue::False {
return Ok(FaeelCandidateResult::NoModel(
FaeelNoModelReason::UnsatisfiedLiteral {
predicate: lit.atom.predicate.clone(),
arity: lit.atom.arity(),
},
));
}
}
}
Ok(FaeelCandidateResult::Model)
}
pub fn run_generate_propagate_test(
program: &Program,
candidates: Vec<EpistemicInterpretation>,
config: GeneratePropagateTestConfig,
) -> Result<GeneratePropagateTestOutcome> {
run_generate_propagate_test_with_mode(
program,
candidates,
config,
program.directives.epistemic_mode_or_default(),
)
}
pub fn run_generate_propagate_test_with_mode(
program: &Program,
candidates: Vec<EpistemicInterpretation>,
config: GeneratePropagateTestConfig,
mode: EpistemicMode,
) -> Result<GeneratePropagateTestOutcome> {
reject_gpt_epistemic_constraints(program)?;
if candidates.len() > config.max_candidates {
return Err(xlog_core::XlogError::ResourceExhausted {
context: "epistemic GPT candidate guard".to_string(),
estimated_bytes: candidates.len() as u64,
budget_bytes: config.max_candidates as u64,
});
}
let generated = candidates.len();
let guesses = candidates
.iter()
.map(EpistemicInterpretation::epistemic_guess_count)
.sum();
let mut propagated_candidates = Vec::new();
let mut rejection_reasons = Vec::new();
let mut rejected_candidate_indices = Vec::new();
for (idx, candidate) in candidates.into_iter().enumerate() {
if let Some((predicate, arity)) = candidate.first_contradiction() {
rejection_reasons.push(FaeelNoModelReason::Contradiction { predicate, arity });
rejected_candidate_indices.push(idx);
} else {
propagated_candidates.push((idx, candidate));
}
}
let mut trace = GeneratePropagateTestTrace {
generated,
guesses,
propagated: propagated_candidates.len(),
pruned: generated.saturating_sub(propagated_candidates.len()),
reduced_program_models: propagated_candidates.len(),
rejection_reasons,
..GeneratePropagateTestTrace::default()
};
let mut accepted_candidate_indices = Vec::new();
for (idx, candidate) in &propagated_candidates {
trace.tested += 1;
match evaluate_epistemic_candidate(program, candidate, mode)? {
FaeelCandidateResult::Model => {
trace.accepted += 1;
trace.accepted_world_views += 1;
accepted_candidate_indices.push(*idx);
}
FaeelCandidateResult::NoModel(reason) => {
trace.rejected += 1;
trace.rejection_reasons.push(reason);
rejected_candidate_indices.push(*idx);
}
}
}
Ok(GeneratePropagateTestOutcome {
trace,
accepted_candidate_indices,
rejected_candidate_indices,
})
}
pub fn build_epistemic_dependency_graph(program: &Program) -> Result<EpistemicDependencyGraph> {
if program.rules.is_empty() {
return Ok(EpistemicDependencyGraph { components: vec![] });
}
let mut parents: Vec<usize> = (0..program.rules.len()).collect();
let mut rule_predicates = Vec::with_capacity(program.rules.len());
let mut head_owner: BTreeMap<String, usize> = BTreeMap::new();
let mut merge_log: Vec<(usize, EpistemicComponentMergeReason)> = Vec::new();
for (idx, rule) in program.rules.iter().enumerate() {
if rule.body.is_empty() {
continue;
}
if let Some(owner) = head_owner.get(&rule.head.predicate).copied() {
union_components(&mut parents, owner, idx);
merge_log.push((
idx,
EpistemicComponentMergeReason::SharedHeadPredicate {
predicate: rule.head.predicate.clone(),
},
));
} else {
head_owner.insert(rule.head.predicate.clone(), idx);
}
}
let mut modal_owner: BTreeMap<EpistemicAtomKey, usize> = BTreeMap::new();
for (idx, rule) in program.rules.iter().enumerate() {
let mut predicates = BTreeSet::new();
predicates.insert(rule.head.predicate.clone());
for lit in &rule.body {
if let BodyLiteral::Epistemic(lit) = lit {
let key =
EpistemicAtomKey::from_arity(lit.atom.predicate.clone(), lit.atom.arity());
if let Some(owner) = modal_owner.get(&key).copied() {
union_components(&mut parents, owner, idx);
merge_log.push((
idx,
EpistemicComponentMergeReason::SharedModalPredicate {
predicate: format!("{}/{}", lit.atom.predicate, lit.atom.arity()),
},
));
} else {
modal_owner.insert(key, idx);
}
}
if let Some(atom) = lit.atom() {
if let Some(owner) = head_owner.get(&atom.predicate).copied() {
if owner != idx {
union_components(&mut parents, owner, idx);
merge_log.push((
idx,
EpistemicComponentMergeReason::DerivedPredicate {
predicate: atom.predicate.clone(),
},
));
}
}
predicates.insert(atom.predicate.clone());
}
}
rule_predicates.push(predicates);
}
let mut constraint_predicates = Vec::with_capacity(program.constraints.len());
for constraint in &program.constraints {
let predicates = constraint_predicate_set(constraint);
let mut owners = predicates
.iter()
.filter_map(|predicate| head_owner.get(predicate).copied());
if let Some(first_owner) = owners.next() {
let mut coalesced_any = false;
for owner in owners {
if find_component(&mut parents, first_owner) != find_component(&mut parents, owner)
{
coalesced_any = true;
}
union_components(&mut parents, first_owner, owner);
}
if coalesced_any {
let constraint_heads: Vec<String> = predicates
.iter()
.filter(|predicate| head_owner.contains_key(*predicate))
.cloned()
.collect();
merge_log.push((
first_owner,
EpistemicComponentMergeReason::Constraint {
predicates: constraint_heads,
},
));
}
}
constraint_predicates.push(predicates);
}
let mut grouped: BTreeMap<usize, (BTreeSet<String>, Vec<usize>)> = BTreeMap::new();
for (idx, predicates) in rule_predicates.into_iter().enumerate() {
let root = find_component(&mut parents, idx);
let entry = grouped
.entry(root)
.or_insert_with(|| (BTreeSet::new(), vec![]));
entry.0.extend(predicates);
entry.1.push(idx);
}
for predicates in constraint_predicates {
let Some(root) = predicates
.iter()
.filter_map(|predicate| head_owner.get(predicate).copied())
.map(|idx| find_component(&mut parents, idx))
.next()
else {
continue;
};
grouped
.entry(root)
.or_insert_with(|| (BTreeSet::new(), vec![]))
.0
.extend(predicates);
}
let mut reasons_by_root: BTreeMap<usize, BTreeSet<EpistemicComponentMergeReason>> =
BTreeMap::new();
for (touched_idx, reason) in merge_log {
let root = find_component(&mut parents, touched_idx);
reasons_by_root.entry(root).or_default().insert(reason);
}
let mut components: Vec<EpistemicDependencyComponent> = grouped
.into_iter()
.map(|(root, (predicates, mut rule_indices))| {
rule_indices.sort_unstable();
let merge_reasons = reasons_by_root
.remove(&root)
.map(|reasons| reasons.into_iter().collect())
.unwrap_or_default();
EpistemicDependencyComponent {
predicates: predicates.into_iter().collect(),
rule_indices,
merge_reasons,
}
})
.collect();
components.sort_by(|a, b| a.predicates.cmp(&b.predicates));
Ok(EpistemicDependencyGraph { components })
}
fn constraint_predicate_set(constraint: &Constraint) -> BTreeSet<String> {
constraint
.body
.iter()
.filter_map(|lit| lit.atom().map(|atom| atom.predicate.clone()))
.collect()
}
fn find_component(parents: &mut [usize], idx: usize) -> usize {
if parents[idx] != idx {
let root = find_component(parents, parents[idx]);
parents[idx] = root;
}
parents[idx]
}
fn union_components(parents: &mut [usize], left: usize, right: usize) {
let left_root = find_component(parents, left);
let right_root = find_component(parents, right);
if left_root != right_root {
parents[right_root] = left_root;
}
}
#[derive(Debug, Clone)]
pub struct EpistemicStratum {
pub head_predicates: Vec<String>,
pub rule_indices: Vec<usize>,
pub program: Program,
}
#[derive(Debug, Clone)]
pub struct EpistemicStratifiedPlan {
pub strata: Vec<EpistemicStratum>,
}
struct EpistemicallyDeterminedPredicates {
determined: BTreeSet<String>,
}
impl EpistemicallyDeterminedPredicates {
fn analyze(program: &Program) -> Self {
let invariant = InvariantRelations::analyze(program);
let mut derived_heads: BTreeSet<&str> = BTreeSet::new();
for rule in &program.rules {
if !rule.body.is_empty() {
derived_heads.insert(rule.head.predicate.as_str());
}
}
let mut determined: BTreeSet<String> = BTreeSet::new();
let mut changed = true;
while changed {
changed = false;
for head in &derived_heads {
if determined.contains(*head) {
continue;
}
if Self::head_is_determined(program, head, &invariant, &derived_heads, &determined)
{
determined.insert((*head).to_string());
changed = true;
}
}
}
Self { determined }
}
fn head_is_determined(
program: &Program,
head: &str,
invariant: &InvariantRelations,
derived_heads: &BTreeSet<&str>,
determined: &BTreeSet<String>,
) -> bool {
let mut defined = false;
for rule in &program.rules {
if rule.head.predicate != head || rule.body.is_empty() {
continue;
}
defined = true;
for lit in &rule.body {
let referenced = match lit {
BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
atom.predicate.as_str()
}
BodyLiteral::Epistemic(modal) => modal.atom.predicate.as_str(),
BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {
continue
}
};
if referenced == head {
return false;
}
let ok = invariant.is_invariant(referenced)
|| determined.contains(referenced)
|| !derived_heads.contains(referenced);
if !ok {
return false;
}
}
}
defined
}
fn contains(&self, predicate: &str) -> bool {
self.determined.contains(predicate)
}
}
pub fn try_plan_stratified_epistemic_program(
program: &Program,
) -> Result<Option<EpistemicStratifiedPlan>> {
let determined = EpistemicallyDeterminedPredicates::analyze(program);
let mut needs_stratification = false;
for rule in &program.rules {
for lit in &rule.body {
if let BodyLiteral::Epistemic(modal) = lit {
if determined.contains(modal.atom.predicate.as_str())
&& modal.atom.predicate != rule.head.predicate
{
needs_stratification = true;
}
}
}
}
if !needs_stratification {
return Ok(None);
}
let stratum_level = assign_epistemic_strata(program, &determined)?;
let Some(stratum_level) = stratum_level else {
return Ok(None);
};
let mut levels: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for (idx, rule) in program.rules.iter().enumerate() {
let has_epistemic = rule
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
if !has_epistemic {
continue;
}
let Some(level) = stratum_level.get(rule.head.predicate.as_str()) else {
return Ok(None);
};
levels.entry(*level).or_default().push(idx);
}
if levels.len() < 2 {
return Ok(None);
}
let mut strata = Vec::with_capacity(levels.len());
for (_level, rule_indices) in levels {
let head_predicates: Vec<String> = rule_indices
.iter()
.filter_map(|idx| program.rules.get(*idx))
.map(|rule| rule.head.predicate.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let stratum_program =
build_stratum_subprogram(program, &rule_indices, &head_predicates, &stratum_level)?;
strata.push(EpistemicStratum {
head_predicates,
rule_indices,
program: stratum_program,
});
}
Ok(Some(EpistemicStratifiedPlan { strata }))
}
fn assign_epistemic_strata(
program: &Program,
determined: &EpistemicallyDeterminedPredicates,
) -> Result<Option<BTreeMap<String, usize>>> {
let mut epistemic_heads: BTreeSet<&str> = BTreeSet::new();
for rule in &program.rules {
if rule
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
{
epistemic_heads.insert(rule.head.predicate.as_str());
}
}
let mut modal_edges: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for rule in &program.rules {
let head = rule.head.predicate.as_str();
for lit in &rule.body {
if let BodyLiteral::Epistemic(modal) = lit {
let target = modal.atom.predicate.as_str();
if epistemic_heads.contains(target) {
if !determined.contains(target) {
return Ok(None);
}
modal_edges.entry(head).or_default().insert(target);
} else if determined.contains(target) {
let support =
epistemic_support_of_determined_ordinary(program, target, &epistemic_heads);
if support.is_empty() {
return Ok(None);
}
let entry = modal_edges.entry(head).or_default();
for support_head in support {
entry.insert(support_head);
}
}
}
}
}
let mut level: BTreeMap<String, usize> = BTreeMap::new();
fn visit<'a>(
head: &'a str,
modal_edges: &BTreeMap<&'a str, BTreeSet<&'a str>>,
level: &mut BTreeMap<String, usize>,
active: &mut BTreeSet<&'a str>,
) -> Result<usize> {
if let Some(l) = level.get(head) {
return Ok(*l);
}
if !active.insert(head) {
return Err(recursive_epistemic_rejection(
"stratified epistemic planning encountered a modal dependency cycle",
));
}
let mut l = 0;
if let Some(targets) = modal_edges.get(head) {
for target in targets {
let tl = visit(target, modal_edges, level, active)?;
l = l.max(tl + 1);
}
}
active.remove(head);
level.insert(head.to_string(), l);
Ok(l)
}
for head in &epistemic_heads {
visit(head, &modal_edges, &mut level, &mut BTreeSet::new())?;
}
Ok(Some(level))
}
fn epistemic_support_of_determined_ordinary<'a>(
program: &'a Program,
predicate: &'a str,
epistemic_heads: &BTreeSet<&'a str>,
) -> BTreeSet<&'a str> {
let mut support: BTreeSet<&'a str> = BTreeSet::new();
let mut seen: BTreeSet<&'a str> = BTreeSet::new();
let mut stack: Vec<&'a str> = vec![predicate];
while let Some(current) = stack.pop() {
if !seen.insert(current) {
continue;
}
for rule in &program.rules {
if rule.head.predicate != current || rule.body.is_empty() {
continue;
}
for lit in &rule.body {
let referenced = match lit {
BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
atom.predicate.as_str()
}
BodyLiteral::Epistemic(_)
| BodyLiteral::Comparison(_)
| BodyLiteral::IsExpr(_)
| BodyLiteral::Univ(_) => continue,
};
if epistemic_heads.contains(referenced) {
support.insert(referenced);
} else {
stack.push(referenced);
}
}
}
if epistemic_heads.contains(current) && current != predicate {
support.insert(current);
}
}
support
}
fn build_stratum_subprogram(
program: &Program,
rule_indices: &[usize],
head_predicates: &[String],
stratum_level: &BTreeMap<String, usize>,
) -> Result<Program> {
let this_level = head_predicates
.iter()
.filter_map(|h| stratum_level.get(h))
.copied()
.max()
.unwrap_or(0);
let lower_epistemic_heads: BTreeSet<&str> = stratum_level
.iter()
.filter(|(_, level)| **level < this_level)
.map(|(head, _)| head.as_str())
.collect();
let all_epistemic_heads: BTreeSet<&str> = program
.rules
.iter()
.filter(|rule| {
rule.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
})
.map(|rule| rule.head.predicate.as_str())
.collect();
let own_rule_indices: BTreeSet<usize> = rule_indices.iter().copied().collect();
let mut stratum = program.clone();
stratum.rules = program
.rules
.iter()
.enumerate()
.filter_map(|(idx, rule)| {
if own_rule_indices.contains(&idx) {
return Some(rule.clone());
}
if lower_epistemic_heads.contains(rule.head.predicate.as_str()) {
return None;
}
let has_epistemic = rule
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
if has_epistemic && !own_rule_indices.contains(&idx) {
return None;
}
let support = epistemic_support_of_determined_ordinary(
program,
rule.head.predicate.as_str(),
&all_epistemic_heads,
);
if support
.iter()
.any(|h| stratum_level.get(*h).copied().unwrap_or(0) >= this_level)
{
return None;
}
Some(rule.clone())
})
.collect();
let head_set: BTreeSet<&str> = head_predicates.iter().map(String::as_str).collect();
stratum.queries = program
.queries
.iter()
.filter(|query| head_set.contains(query.atom.predicate.as_str()))
.cloned()
.collect();
stratum.constraints = program
.constraints
.iter()
.filter(|constraint| {
constraint_predicate_set(constraint)
.iter()
.all(|p| head_set.contains(p.as_str()) || !is_program_head(program, p))
})
.cloned()
.collect();
Ok(stratum)
}
fn is_program_head(program: &Program, predicate: &str) -> bool {
program
.rules
.iter()
.any(|rule| !rule.body.is_empty() && rule.head.predicate == predicate)
}
pub fn split_epistemic_program(program: &Program) -> Result<EpistemicSplitPlan> {
Ok(EpistemicSplitPlan {
components: build_epistemic_dependency_graph(program)?.components,
})
}
pub fn compile_epistemic_gpu_split_execution(
program: &Program,
) -> Result<EpistemicSplitExecutablePlan> {
compile_epistemic_gpu_split_execution_with_stats_snapshot(program, None)
}
pub fn compile_epistemic_gpu_split_execution_with_stats_snapshot(
program: &Program,
stats_snapshot: Option<&StatsSnapshot>,
) -> Result<EpistemicSplitExecutablePlan> {
reject_epistemic_constraints(program)?;
let split_plan = split_epistemic_program(program)?;
let mut components = Vec::new();
for component in &split_plan.components {
if !component_has_epistemic_rule(program, component) {
continue;
}
let coupling = classify_cross_component_modal_coupling(program, component)?;
let component_program = split_component_program(program, component)?;
let executable = compile_epistemic_gpu_execution_inner(
&component_program,
stats_snapshot,
coupling.allows_multiple_output_heads(),
)?;
components.push(EpistemicSplitExecutableComponent {
component: component.clone(),
executable,
});
}
if components.is_empty() {
return Err(XlogError::UnsupportedEpistemicConstruct {
construct: "epistemic GPU split execution".to_string(),
context: "requires at least one epistemic split component".to_string(),
});
}
Ok(EpistemicSplitExecutablePlan {
split_plan,
components,
})
}
fn component_has_epistemic_rule(
program: &Program,
component: &EpistemicDependencyComponent,
) -> bool {
component
.rule_indices
.iter()
.filter_map(|idx| program.rules.get(*idx))
.any(|rule| {
rule.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
})
}
fn component_epistemic_output_heads(
program: &Program,
component: &EpistemicDependencyComponent,
) -> Vec<String> {
let mut heads: BTreeSet<String> = BTreeSet::new();
for idx in &component.rule_indices {
let Some(rule) = program.rules.get(*idx) else {
continue;
};
let has_epistemic_body = rule
.body
.iter()
.any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
if has_epistemic_body {
heads.insert(rule.head.predicate.clone());
}
}
heads.into_iter().collect()
}
fn format_component_merge_reasons(component: &EpistemicDependencyComponent) -> String {
if component.merge_reasons.is_empty() {
return "no recorded coalesce reason".to_string();
}
component
.merge_reasons
.iter()
.map(|reason| match reason {
EpistemicComponentMergeReason::SharedHeadPredicate { predicate } => {
format!("SharedHeadPredicate({predicate})")
}
EpistemicComponentMergeReason::DerivedPredicate { predicate } => {
format!("DerivedPredicate({predicate})")
}
EpistemicComponentMergeReason::SharedModalPredicate { predicate } => {
format!("SharedModalPredicate({predicate})")
}
EpistemicComponentMergeReason::Constraint { predicates } => {
format!("Constraint({})", predicates.join(", "))
}
})
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CrossComponentCoupling {
JointSolvable,
}
impl CrossComponentCoupling {
fn allows_multiple_output_heads(self) -> bool {
match self {
CrossComponentCoupling::JointSolvable => true,
}
}
}
fn epistemic_tainted_predicates<'a>(
program: &'a Program,
component: &EpistemicDependencyComponent,
epistemic_heads: &'a [String],
) -> BTreeSet<&'a str> {
let mut tainted: BTreeSet<&str> = epistemic_heads.iter().map(String::as_str).collect();
let mut changed = true;
while changed {
changed = false;
for idx in &component.rule_indices {
let Some(rule) = program.rules.get(*idx) else {
continue;
};
if tainted.contains(rule.head.predicate.as_str()) {
continue;
}
let body_touches_tainted = rule.body.iter().any(|lit| {
lit.atom()
.map(|atom| tainted.contains(atom.predicate.as_str()))
.unwrap_or(false)
});
if body_touches_tainted {
tainted.insert(rule.head.predicate.as_str());
changed = true;
}
}
}
tainted
}
fn classify_cross_component_modal_coupling(
program: &Program,
component: &EpistemicDependencyComponent,
) -> Result<CrossComponentCoupling> {
let epistemic_heads = component_epistemic_output_heads(program, component);
if epistemic_heads.len() <= 1 {
return Ok(CrossComponentCoupling::JointSolvable);
}
let tainted = epistemic_tainted_predicates(program, component, &epistemic_heads);
let mut nested_modal_predicates: BTreeSet<String> = BTreeSet::new();
for idx in &component.rule_indices {
let Some(rule) = program.rules.get(*idx) else {
continue;
};
for lit in &rule.body {
if let BodyLiteral::Epistemic(modal) = lit {
if tainted.contains(modal.atom.predicate.as_str()) {
nested_modal_predicates.insert(format!(
"{}/{}",
modal.atom.predicate,
modal.atom.arity()
));
}
}
}
}
if nested_modal_predicates.is_empty() {
return Ok(CrossComponentCoupling::JointSolvable);
}
Err(XlogError::UnsupportedEpistemicConstruct {
construct: "cross-component epistemic coupling".to_string(),
context: format!(
"epistemic output heads {:?} are coupled into a single dependency \
component (reasons: {}) through nested modal literals over \
epistemic-derived predicates {:?}; the modal truth of an \
epistemic-derived head depends on another head's accepted world view, \
so a single joint world-view enumeration would mis-evaluate the \
nested modality and an independent split would be unsound, so this \
fails closed",
epistemic_heads,
format_component_merge_reasons(component),
nested_modal_predicates.into_iter().collect::<Vec<_>>(),
),
})
}
fn split_component_program(
program: &Program,
component: &EpistemicDependencyComponent,
) -> Result<Program> {
let mut component_program = program.clone();
let component_predicates: BTreeSet<&str> =
component.predicates.iter().map(String::as_str).collect();
let component_rule_indices: BTreeSet<usize> = component.rule_indices.iter().copied().collect();
let head_predicates: BTreeSet<&str> = program
.rules
.iter()
.map(|rule| rule.head.predicate.as_str())
.collect();
component_program.rules = program
.rules
.iter()
.enumerate()
.filter_map(|(idx, rule)| {
(component_rule_indices.contains(&idx)
|| (rule.body.is_empty()
&& component_predicates.contains(rule.head.predicate.as_str())))
.then_some(rule.clone())
})
.collect();
component_program.constraints = program
.constraints
.iter()
.filter(|constraint| {
let predicates = constraint_predicate_set(constraint);
let has_component_owned_predicate = predicates
.iter()
.any(|predicate| head_predicates.contains(predicate.as_str()));
!has_component_owned_predicate
|| predicates
.iter()
.all(|predicate| component_predicates.contains(predicate.as_str()))
})
.cloned()
.collect();
Ok(component_program)
}