use crate::frozen::{FrozenIndexedDataset, GraphSel, TermId};
use oxrdf::Term;
use shifty_algebra::Path;
use shifty_opt::ClosureKind;
use std::collections::HashSet;
use std::rc::Rc;
#[derive(Clone, PartialEq, Eq, Hash)]
pub(crate) enum ReachStep {
Id,
FwdPred(TermId),
BwdPred(TermId),
Seq(Vec<ReachStep>),
Alt(Vec<ReachStep>),
Star(Box<ReachStep>),
}
pub(crate) fn compile_fwd(path: &Path, ds: &FrozenIndexedDataset) -> ReachStep {
match path {
Path::Id => ReachStep::Id,
Path::Pred(q) => ReachStep::FwdPred(ds.intern(&Term::NamedNode(q.clone()))),
Path::Inverse(p) => compile_bwd(p, ds),
Path::Seq(ps) => ReachStep::Seq(ps.iter().map(|p| compile_fwd(p, ds)).collect()),
Path::Alt(ps) => ReachStep::Alt(ps.iter().map(|p| compile_fwd(p, ds)).collect()),
Path::Star(p) => ReachStep::Star(Box::new(compile_fwd(p, ds))),
}
}
pub(crate) fn compile_bwd(path: &Path, ds: &FrozenIndexedDataset) -> ReachStep {
match path {
Path::Id => ReachStep::Id,
Path::Pred(q) => ReachStep::BwdPred(ds.intern(&Term::NamedNode(q.clone()))),
Path::Inverse(p) => compile_fwd(p, ds),
Path::Seq(ps) => ReachStep::Seq(ps.iter().rev().map(|p| compile_bwd(p, ds)).collect()),
Path::Alt(ps) => ReachStep::Alt(ps.iter().map(|p| compile_bwd(p, ds)).collect()),
Path::Star(p) => ReachStep::Star(Box::new(compile_bwd(p, ds))),
}
}
pub(crate) fn eval_reach(
step: &ReachStep,
node: TermId,
ds: &FrozenIndexedDataset,
g: GraphSel,
) -> HashSet<TermId> {
match step {
ReachStep::Id => HashSet::from([node]),
ReachStep::FwdPred(q) => ds
.scan(Some(node), Some(*q), None, g)
.map(|t| t[2])
.collect(),
ReachStep::BwdPred(q) => ds
.scan(None, Some(*q), Some(node), g)
.map(|t| t[0])
.collect(),
ReachStep::Seq(steps) => {
let mut cursor: HashSet<TermId> = HashSet::from([node]);
for s in steps {
cursor = cursor
.iter()
.flat_map(|&x| eval_reach(s, x, ds, g))
.collect();
}
cursor
}
ReachStep::Alt(steps) => steps
.iter()
.flat_map(|s| eval_reach(s, node, ds, g))
.collect(),
ReachStep::Star(inner) => star_closure(node, inner, ds, g),
}
}
pub(crate) fn star_closure(
node: TermId,
step: &ReachStep,
ds: &FrozenIndexedDataset,
g: GraphSel,
) -> HashSet<TermId> {
let mut result: HashSet<TermId> = HashSet::from([node]);
let mut frontier = vec![node];
while let Some(x) = frontier.pop() {
for y in eval_reach(step, x, ds, g) {
if result.insert(y) {
frontier.push(y);
}
}
}
result
}
pub(crate) fn plus_closure(
node: TermId,
step: &ReachStep,
ds: &FrozenIndexedDataset,
g: GraphSel,
) -> HashSet<TermId> {
let mut result = HashSet::new();
let mut frontier: Vec<TermId> = eval_reach(step, node, ds, g).into_iter().collect();
result.extend(frontier.iter().copied());
while let Some(x) = frontier.pop() {
for y in eval_reach(step, x, ds, g) {
if result.insert(y) {
frontier.push(y);
}
}
}
result
}
pub(crate) fn apply_closure(
node: TermId,
step: &ReachStep,
kind: ClosureKind,
ds: &FrozenIndexedDataset,
g: GraphSel,
) -> Rc<HashSet<TermId>> {
if let Some(cached) = ds.cached_reach(node, step, kind, g) {
return cached;
}
let result = Rc::new(match kind {
ClosureKind::Star => star_closure(node, step, ds, g),
ClosureKind::Plus => plus_closure(node, step, ds, g),
ClosureKind::Opt => {
let mut r = eval_reach(step, node, ds, g);
r.insert(node);
r
}
});
ds.cache_reach(node, step, kind, g, result.clone());
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frozen::FrozenIndexedDataset;
use oxrdf::{Graph, NamedNode, Triple};
use shifty_algebra::Path as AlgPath;
fn nn(iri: &str) -> NamedNode {
NamedNode::new(iri).unwrap()
}
fn t(s: &str, p: &str, o: &str) -> Triple {
Triple::new(nn(s), nn(p), nn(o))
}
fn sample_ds() -> FrozenIndexedDataset {
let mut g = Graph::new();
for tr in [
t("http://ex/a", "http://ex/p", "http://ex/b"),
t("http://ex/b", "http://ex/p", "http://ex/c"),
t("http://ex/c", "http://ex/q", "http://ex/d"),
] {
g.insert(tr.as_ref());
}
FrozenIndexedDataset::from_graph(&g)
}
fn id(iri: &str, ds: &FrozenIndexedDataset) -> TermId {
ds.intern(&Term::NamedNode(nn(iri)))
}
#[test]
fn inverse_folds_to_bwd() {
let ds = sample_ds();
let inv = AlgPath::Inverse(Box::new(AlgPath::Pred(nn("http://ex/p"))));
assert!(matches!(compile_fwd(&inv, &ds), ReachStep::BwdPred(_)));
}
#[test]
fn double_inverse_cancels() {
let ds = sample_ds();
let p = AlgPath::Pred(nn("http://ex/p"));
let inv_inv = AlgPath::Inverse(Box::new(AlgPath::Inverse(Box::new(p))));
assert!(matches!(compile_fwd(&inv_inv, &ds), ReachStep::FwdPred(_)));
}
#[test]
fn seq_bwd_reverses_and_flips() {
let ds = sample_ds();
let p = AlgPath::Pred(nn("http://ex/p"));
let q = AlgPath::Pred(nn("http://ex/q"));
let seq = AlgPath::Seq(vec![p, q]);
let p_id = id("http://ex/p", &ds);
let q_id = id("http://ex/q", &ds);
if let ReachStep::Seq(steps) = compile_bwd(&seq, &ds) {
assert_eq!(steps.len(), 2);
assert!(matches!(steps[0], ReachStep::BwdPred(i) if i == q_id));
assert!(matches!(steps[1], ReachStep::BwdPred(i) if i == p_id));
} else {
panic!("expected Seq");
}
}
#[test]
fn eval_reach_fwd_pred_returns_objects() {
let ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let a = id("http://ex/a", &ds);
let b = id("http://ex/b", &ds);
let result = eval_reach(&ReachStep::FwdPred(p_id), a, &ds, GraphSel::Default);
assert_eq!(result, HashSet::from([b]));
}
#[test]
fn eval_reach_bwd_pred_returns_subjects() {
let ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let b = id("http://ex/b", &ds);
let a = id("http://ex/a", &ds);
let result = eval_reach(&ReachStep::BwdPred(p_id), b, &ds, GraphSel::Default);
assert_eq!(result, HashSet::from([a]));
}
#[test]
fn star_closure_is_reflexive_transitive() {
let ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let a = id("http://ex/a", &ds);
let b = id("http://ex/b", &ds);
let c = id("http://ex/c", &ds);
let result = star_closure(a, &ReachStep::FwdPred(p_id), &ds, GraphSel::Default);
assert!(result.contains(&a)); assert!(result.contains(&b));
assert!(result.contains(&c));
}
#[test]
fn seq_bwd_with_nested_star() {
let ds = sample_ds();
let p = AlgPath::Pred(nn("http://ex/p"));
let q = AlgPath::Pred(nn("http://ex/q"));
let seq = AlgPath::Seq(vec![AlgPath::Star(Box::new(p)), q]);
let p_id = id("http://ex/p", &ds);
let q_id = id("http://ex/q", &ds);
if let ReachStep::Seq(steps) = compile_bwd(&seq, &ds) {
assert_eq!(steps.len(), 2);
assert!(matches!(steps[0], ReachStep::BwdPred(i) if i == q_id));
if let ReachStep::Star(inner) = &steps[1] {
assert!(matches!(inner.as_ref(), ReachStep::BwdPred(i) if *i == p_id));
} else {
panic!("expected Star as second element");
}
} else {
panic!("expected Seq");
}
}
#[test]
fn plus_closure_excludes_start_unless_cyclic() {
let ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let a = id("http://ex/a", &ds);
let b = id("http://ex/b", &ds);
let c = id("http://ex/c", &ds);
let result = plus_closure(a, &ReachStep::FwdPred(p_id), &ds, GraphSel::Default);
assert!(!result.contains(&a)); assert!(result.contains(&b));
assert!(result.contains(&c));
}
#[test]
fn apply_closure_reuses_cached_result() {
let ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let a = id("http://ex/a", &ds);
let step = ReachStep::FwdPred(p_id);
let first = apply_closure(a, &step, ClosureKind::Star, &ds, GraphSel::Default);
let second = apply_closure(a, &step, ClosureKind::Star, &ds, GraphSel::Default);
assert!(Rc::ptr_eq(&first, &second));
}
#[test]
fn extending_dataset_invalidates_cached_closure() {
let mut ds = sample_ds();
let p_id = id("http://ex/p", &ds);
let a = id("http://ex/a", &ds);
let c = id("http://ex/c", &ds);
let step = ReachStep::FwdPred(p_id);
let before = apply_closure(a, &step, ClosureKind::Plus, &ds, GraphSel::Default);
assert!(before.contains(&c));
let d = nn("http://ex/d");
let added = Triple::new(nn("http://ex/c"), nn("http://ex/p"), d);
ds.extend_triples([&added]);
let d_id = id("http://ex/d", &ds);
let after = apply_closure(a, &step, ClosureKind::Plus, &ds, GraphSel::Default);
assert!(after.contains(&d_id));
assert!(!Rc::ptr_eq(&before, &after));
}
}