use alloc::collections::{BTreeMap, BTreeSet};
use pr4xis::category::Concept;
use pr4xis::category::quiver::ReachabilityClosure;
use pr4xis::logic::proof::{SimpleCounterexample, SimpleProof, Verdict};
use pr4xis::ontology::meta::{Citation, Label, ModulePath, OntologyName, Provenance};
use crate::address::ContentAddress;
use crate::archive::Archive;
use crate::codec::CodecError;
use crate::definition::EdgeTarget;
extern crate alloc;
#[allow(unused_imports)]
use alloc::{
string::{String, ToString},
vec::Vec,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RelationKind {
Subsumption,
Parthood,
Causation,
}
impl RelationKind {
pub fn as_str(&self) -> &'static str {
match self {
RelationKind::Subsumption => "Subsumption",
RelationKind::Parthood => "Parthood",
RelationKind::Causation => "Causation",
}
}
pub fn from_edge_kind(name: &str) -> Option<Self> {
match name {
"Subsumption" => Some(RelationKind::Subsumption),
"Parthood" => Some(RelationKind::Parthood),
"Causation" => Some(RelationKind::Causation),
_ => None,
}
}
pub fn transitive() -> [RelationKind; 3] {
[
RelationKind::Subsumption,
RelationKind::Parthood,
RelationKind::Causation,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ConceptRef {
pub ontology: OntologyName,
pub name: String,
}
impl ConceptRef {
pub fn new(ontology: OntologyName, name: impl Into<String>) -> Self {
Self {
ontology,
name: name.into(),
}
}
}
impl Concept for ConceptRef {
fn name(&self) -> &'static str {
""
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RuntimeEdge {
pub source: ConceptRef,
pub kind: RelationKind,
pub target: ConceptRef,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MaterializedClosure {
reachable: BTreeMap<RelationKind, ReachabilityClosure<ConceptRef>>,
}
impl MaterializedClosure {
pub fn fold(edges: &[RuntimeEdge]) -> Self {
let mut reachable: BTreeMap<RelationKind, ReachabilityClosure<ConceptRef>> =
BTreeMap::new();
for kind in RelationKind::transitive() {
let closure = ReachabilityClosure::fold(
edges
.iter()
.filter(|e| e.kind == kind)
.map(|e| (e.source.clone(), e.target.clone())),
);
reachable.insert(kind, closure);
}
Self { reachable }
}
pub fn reachable_from(&self, source: &ConceptRef, kind: RelationKind) -> BTreeSet<ConceptRef> {
self.reachable
.get(&kind)
.map(|closure| {
closure
.strict_image(source)
.into_iter()
.map(|(v, _)| v)
.collect()
})
.unwrap_or_default()
}
pub fn reaches(&self, source: &ConceptRef, target: &ConceptRef, kind: RelationKind) -> bool {
self.reachable
.get(&kind)
.is_some_and(|closure| source != target && closure.reaches(source, target))
}
pub fn subsumption_image(&self, c: &ConceptRef) -> Vec<(ConceptRef, u32)> {
self.reachable
.get(&RelationKind::Subsumption)
.map(|closure| closure.strict_image(c))
.unwrap_or_default()
}
pub fn subsumption_meet(&self, a: &ConceptRef, b: &ConceptRef) -> Option<ConceptRef> {
self.reachable
.get(&RelationKind::Subsumption)
.and_then(|closure| {
closure.meet_by(a, b, |c| (c.ontology.as_str().to_string(), c.name.clone()))
})
}
pub fn subsumption_chain(
&self,
child: &ConceptRef,
ancestor: &ConceptRef,
) -> Option<Vec<ConceptRef>> {
let closure = self.reachable.get(&RelationKind::Subsumption)?;
if child != ancestor && !closure.reaches(child, ancestor) {
return None;
}
let mut chain: Vec<(ConceptRef, u32)> = closure
.reflexive_image(child)
.into_iter()
.filter(|(x, _)| x == ancestor || closure.reaches(x, ancestor))
.collect();
chain.sort_unstable_by(|(a, da), (b, db)| {
da.cmp(db)
.then_with(|| a.ontology.as_str().cmp(b.ontology.as_str()))
.then_with(|| a.name.cmp(&b.name))
});
Some(chain.into_iter().map(|(v, _)| v).collect())
}
pub fn union(&mut self, other: &MaterializedClosure) {
for (kind, other_closure) in &other.reachable {
let into = self.reachable.entry(*kind).or_default();
let merged =
ReachabilityClosure::fold(into.edges_iter().chain(other_closure.edges_iter()));
*into = merged;
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MaterializeError {
Root(CodecError),
DanglingEdge(DanglingEdge),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DanglingEdge {
pub source: String,
pub kind: String,
pub orphan: String,
}
impl core::fmt::Display for MaterializeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
MaterializeError::Root(e) => write!(f, "materialize: derive root: {e}"),
MaterializeError::DanglingEdge(d) => write!(
f,
"materialize: dangling edge {}--{}-->{}: target node {:?} is not declared",
d.source, d.kind, d.orphan, d.orphan
),
}
}
}
impl std::error::Error for MaterializeError {}
#[derive(Debug, Clone)]
pub struct RuntimeOntology {
id: OntologyName,
root: ContentAddress,
archive: Archive,
closure: MaterializedClosure,
}
impl PartialEq for RuntimeOntology {
fn eq(&self, other: &Self) -> bool {
self.root == other.root
}
}
impl Eq for RuntimeOntology {}
impl RuntimeOntology {
pub fn id(&self) -> &OntologyName {
&self.id
}
pub fn root(&self) -> ContentAddress {
self.root
}
pub fn archive(&self) -> &Archive {
&self.archive
}
pub fn closure(&self) -> &MaterializedClosure {
&self.closure
}
pub fn concept(&self, name: impl Into<String>) -> ConceptRef {
ConceptRef::new(self.id.clone(), name)
}
pub fn morphisms_from(&self, c: &ConceptRef) -> Vec<RuntimeEdge> {
self.archive
.nodes
.iter()
.filter(|n| n.name == c.name)
.flat_map(|n| {
n.edges.iter().filter_map(move |(kind_name, target)| {
let name = target.local_name()?;
RelationKind::from_edge_kind(kind_name).map(|kind| RuntimeEdge {
source: c.clone(),
kind,
target: ConceptRef::new(self.id.clone(), name.to_string()),
})
})
})
.collect()
}
pub fn reachable_from(&self, c: &ConceptRef, kind: RelationKind) -> BTreeSet<ConceptRef> {
self.closure.reachable_from(c, kind)
}
pub fn is_a(&self, child: &ConceptRef, ancestor: &ConceptRef) -> Verdict {
let holds = self
.closure
.reaches(child, ancestor, RelationKind::Subsumption);
let meta = self.is_a_meta(child, ancestor);
if holds {
Ok(Box::new(SimpleProof::new(meta)))
} else {
Err(Box::new(SimpleCounterexample::new(meta)))
}
}
pub fn lexical(&self, c: &ConceptRef) -> Option<&str> {
self.archive
.nodes
.iter()
.find(|n| n.name == c.name)
.and_then(|n| n.lexical.as_deref())
}
fn is_a_meta(&self, child: &ConceptRef, ancestor: &ConceptRef) -> Provenance {
Provenance {
name: OntologyName::new(alloc::format!(
"IsA[{}/{} ⊑ {}/{}]",
child.ontology,
child.name,
ancestor.ontology,
ancestor.name
)),
description: Label::new(alloc::format!(
"{} is-a {} via the Subsumption transitive closure",
child.name,
ancestor.name
)),
citation: Citation::parse_static(
"Guarino (2009); Smith et al. (2005) OBO Relation Ontology (transitive_over); Mac Lane (1971) CWM II.7",
),
module_path: ModulePath::new_static(module_path!()),
}
}
}
pub fn materialize(
archive: Archive,
id: OntologyName,
) -> Result<RuntimeOntology, MaterializeError> {
let root = archive.root().map_err(MaterializeError::Root)?;
let declared: BTreeSet<&str> = archive.nodes.iter().map(|n| n.name.as_str()).collect();
let mut edges: Vec<RuntimeEdge> = Vec::new();
for node in &archive.nodes {
for (kind_name, target) in &node.edges {
let local = match target {
EdgeTarget::Local(name) => name,
EdgeTarget::Grounded { .. } => continue,
};
if !declared.contains(local.as_str()) {
return Err(MaterializeError::DanglingEdge(DanglingEdge {
source: node.name.clone(),
kind: kind_name.clone(),
orphan: local.clone(),
}));
}
if let Some(kind) = RelationKind::from_edge_kind(kind_name) {
edges.push(RuntimeEdge {
source: ConceptRef::new(id.clone(), node.name.clone()),
kind,
target: ConceptRef::new(id.clone(), local.clone()),
});
}
}
}
let closure = MaterializedClosure::fold(&edges);
Ok(RuntimeOntology {
id,
root,
archive,
closure,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::Definition;
use crate::emit;
pr4xis::ontology! {
name: "Org",
source: "pr4xis-runtime ontology materialize test fixture",
concepts: [Employer, Employee, Person, Agent],
labels: {
Employer: ("en", "Employer", "One who employs."),
Employee: ("en", "Employee", "One who is employed."),
Person: ("en", "Person", "A human being."),
Agent: ("en", "Agent", "One who acts."),
},
is_a: [
(Employer, Person),
(Employee, Person),
(Person, Agent),
],
}
fn org() -> RuntimeOntology {
let archive = emit::emit::<OrgCategory>();
materialize(archive, OntologyName::new_static("Org")).expect("Org materializes")
}
#[test]
fn employer_reaches_agent_via_the_subsumption_closure() {
let onto = org();
let employer = onto.concept("Employer");
let agent = onto.concept("Agent");
let descendants = onto.reachable_from(&employer, RelationKind::Subsumption);
assert!(
descendants.contains(&agent),
"Employer must reach Agent through the Subsumption closure; got {descendants:?}"
);
assert!(descendants.contains(&onto.concept("Person")));
}
#[test]
fn is_a_returns_a_verdict_carrying_the_claim() {
let onto = org();
let employer = onto.concept("Employer");
let agent = onto.concept("Agent");
match onto.is_a(&employer, &agent) {
Ok(proof) => {
let name = proof.meta().name;
assert!(
name.as_str().contains("Employer") && name.as_str().contains("Agent"),
"the proof must name the witnessed Employer ⊑ Agent claim; got {name}"
);
}
Err(c) => panic!("expected Employer is-a Agent to be proven; got {:?}", c),
}
match onto.is_a(&agent, &employer) {
Err(_) => {}
Ok(p) => panic!("Agent is-a Employer must refute; got proof {:?}", p),
}
}
#[test]
fn content_address_identity_equality() {
let archive = emit::emit::<OrgCategory>();
let a = materialize(archive.clone(), OntologyName::new_static("Org")).unwrap();
let b = materialize(archive, OntologyName::new_static("Org")).unwrap();
assert_eq!(a.root(), b.root());
assert_eq!(a, b);
let mut other_archive = emit::emit::<OrgCategory>();
other_archive.nodes.push(Definition {
kind: "Concept".into(),
name: "Stranger".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: None,
});
let other = materialize(other_archive, OntologyName::new_static("Org")).unwrap();
assert_ne!(a, other);
}
#[test]
fn referential_closure_counterexample_on_a_dangling_edge() {
let archive = Archive {
nodes: alloc::vec![Definition {
kind: "Concept".into(),
name: "Employer".into(),
edges: alloc::vec![("Subsumption".into(), "Ghost".into())],
axioms: alloc::vec![],
lexical: None,
}],
connections: alloc::vec![],
};
match materialize(archive, OntologyName::new_static("Broken")) {
Err(MaterializeError::DanglingEdge(d)) => {
assert_eq!(d.source, "Employer");
assert_eq!(d.kind, "Subsumption");
assert_eq!(d.orphan, "Ghost", "the counterexample must name the orphan");
}
other => panic!("expected a DanglingEdge counterexample; got {other:?}"),
}
}
#[test]
fn lexical_lookup_returns_a_nodes_gloss() {
let archive = Archive {
nodes: alloc::vec![Definition {
kind: "Concept".into(),
name: "Employer".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: Some("One who employs.".into()),
}],
connections: alloc::vec![],
};
let onto = materialize(archive, OntologyName::new_static("Org")).unwrap();
assert_eq!(
onto.lexical(&onto.concept("Employer")),
Some("One who employs.")
);
assert_eq!(onto.lexical(&onto.concept("Nobody")), None);
}
#[test]
fn closure_refold_is_idempotent_on_a_prefolded_input() {
let onto = org();
let employer = onto.concept("Employer");
let direct_then_closed = onto.reachable_from(&employer, RelationKind::Subsumption);
let refolded = MaterializedClosure::fold(
&onto
.archive()
.nodes
.iter()
.flat_map(|n| onto.morphisms_from(&onto.concept(n.name.clone())))
.collect::<Vec<_>>(),
);
assert_eq!(
refolded.reachable_from(&employer, RelationKind::Subsumption),
direct_then_closed
);
}
}