use super::{
accumulator::{compute_ancestry_from_dag, ComparisonResult, EventAccumulator},
frontier::Frontier,
relation::AbstractCausalRelation,
};
use crate::error::RetrievalError;
use crate::retrieval::GetEvents;
use ankurah_proto::{Clock, EventId};
use std::collections::{BTreeSet, HashMap};
pub(crate) async fn compare<E: GetEvents>(
event_getter: E,
subject: &Clock,
comparison: &Clock,
budget: usize,
) -> Result<ComparisonResult<E>, RetrievalError> {
if subject.as_slice() == comparison.as_slice() {
let accumulator = EventAccumulator::new(event_getter);
return Ok(ComparisonResult::new(AbstractCausalRelation::Equal, accumulator));
}
if subject.as_slice().is_empty() || comparison.as_slice().is_empty() {
let accumulator = EventAccumulator::new(event_getter);
return Ok(ComparisonResult::new(
AbstractCausalRelation::DivergedSince {
meet: vec![],
subject: vec![],
other: vec![],
subject_chain: vec![],
other_chain: vec![],
},
accumulator,
));
}
let mut accumulator = EventAccumulator::new(event_getter);
{
let comparison_set: BTreeSet<EventId> = comparison.as_slice().iter().cloned().collect();
let mut all_parents: BTreeSet<EventId> = BTreeSet::new();
let mut applicable = true;
for id in subject.as_slice() {
match accumulator.get_event(id).await {
Ok(event) => {
accumulator.accumulate(&event);
let parents = event.parent.as_slice();
if parents.is_empty() || !parents.iter().all(|p| comparison_set.contains(p)) {
applicable = false;
break;
}
all_parents.extend(parents.iter().cloned());
}
Err(_) => {
applicable = false;
break;
}
}
}
if applicable && comparison_set.is_subset(&all_parents) {
let chain: Vec<EventId> = subject.as_slice().to_vec();
return Ok(ComparisonResult::new(AbstractCausalRelation::StrictDescends { chain }, accumulator));
}
}
let initial_budget = budget;
let max_budget = initial_budget * 4;
let mut current_budget = initial_budget;
loop {
let mut comp = Comparison::new(&mut accumulator, subject, comparison, current_budget);
let relation = loop {
if let Some(relation) = comp.step().await? {
break relation;
}
};
match &relation {
AbstractCausalRelation::BudgetExceeded { .. } if current_budget < max_budget => {
current_budget = (current_budget * 4).min(max_budget);
continue;
}
_ => {
return Ok(ComparisonResult::new(relation, accumulator));
}
}
}
}
struct Comparison<'a, E: GetEvents> {
accumulator: &'a mut EventAccumulator<E>,
subject: Side,
comparison: Side,
states: HashMap<EventId, NodeState>,
meet_candidates: BTreeSet<EventId>,
any_common: bool,
remaining_budget: usize,
}
struct Side {
original: BTreeSet<EventId>,
frontier: Frontier<EventId>,
processed: BTreeSet<EventId>,
opposite_heads_seen: BTreeSet<EventId>,
visited: Vec<EventId>,
root: Option<EventId>,
}
impl Side {
fn new(heads: BTreeSet<EventId>) -> Self {
Self {
frontier: Frontier::new(heads.clone()),
original: heads,
processed: BTreeSet::new(),
opposite_heads_seen: BTreeSet::new(),
visited: Vec::new(),
root: None,
}
}
fn absorb(&mut self, id: &EventId, parents: &[EventId], opposite_original: &BTreeSet<EventId>) {
self.visited.push(id.clone());
if parents.is_empty() && self.root.is_none() {
self.root = Some(id.clone());
}
if self.processed.insert(id.clone()) {
self.frontier.extend(parents.iter().filter(|p| !self.processed.contains(*p)).cloned());
if opposite_original.contains(id) {
self.opposite_heads_seen.insert(id.clone());
}
}
}
fn covers(&self, opposite: &Side) -> bool { self.opposite_heads_seen.len() == opposite.original.len() }
}
#[derive(Clone)]
struct NodeState {
seen_from_subject: bool,
seen_from_comparison: bool,
common_child_count: usize,
subject_children: Vec<EventId>, other_children: Vec<EventId>, }
impl NodeState {
fn new() -> Self {
Self {
seen_from_subject: false,
seen_from_comparison: false,
common_child_count: 0,
subject_children: Vec::new(),
other_children: Vec::new(),
}
}
fn is_common(&self) -> bool { self.seen_from_subject && self.seen_from_comparison }
fn mark_seen_from(&mut self, from_subject: bool, from_comparison: bool) {
if from_subject {
self.seen_from_subject = true;
}
if from_comparison {
self.seen_from_comparison = true;
}
}
fn add_child(&mut self, child: EventId, from_subject: bool, from_comparison: bool) {
if from_subject {
self.subject_children.push(child.clone());
}
if from_comparison {
self.other_children.push(child);
}
}
}
impl<'a, E: GetEvents> Comparison<'a, E> {
fn new(accumulator: &'a mut EventAccumulator<E>, subject: &Clock, comparison: &Clock, budget: usize) -> Self {
Self {
accumulator,
subject: Side::new(subject.as_slice().iter().cloned().collect()),
comparison: Side::new(comparison.as_slice().iter().cloned().collect()),
states: HashMap::new(),
meet_candidates: BTreeSet::new(),
any_common: false,
remaining_budget: budget,
}
}
async fn step(&mut self) -> Result<Option<AbstractCausalRelation<EventId>>, RetrievalError> {
let mut all_frontier_ids = Vec::new();
all_frontier_ids.extend(self.subject.frontier.ids.iter().cloned());
all_frontier_ids.extend(self.comparison.frontier.ids.iter().cloned());
for id in &all_frontier_ids {
if !self.subject.frontier.ids.contains(id) && !self.comparison.frontier.ids.contains(id) {
continue;
}
let event = match self.accumulator.get_event(id).await {
Ok(event) => event,
Err(RetrievalError::EventNotFound(_)) => {
if self.subject.frontier.ids.contains(id) && self.comparison.frontier.ids.contains(id) {
self.process_event(id.clone(), &[]);
continue;
}
return Err(RetrievalError::EventNotFound(id.clone()));
}
Err(e) => return Err(e),
};
self.accumulator.accumulate(&event);
self.remaining_budget = self.remaining_budget.saturating_sub(1);
self.process_event(event.id(), event.parent.as_slice());
}
Ok(self.check_result())
}
fn process_event(&mut self, id: EventId, parents: &[EventId]) {
let from_subject = self.subject.frontier.remove(&id);
let from_comparison = self.comparison.frontier.remove(&id);
let is_common = {
let state = self.states.entry(id.clone()).or_insert_with(NodeState::new);
state.mark_seen_from(from_subject, from_comparison);
state.is_common()
};
if is_common && self.meet_candidates.insert(id.clone()) {
self.any_common = true;
for parent in parents {
self.states.entry(parent.clone()).or_insert_with(NodeState::new).common_child_count += 1;
}
}
for parent in parents {
self.states.entry(parent.clone()).or_insert_with(NodeState::new).add_child(id.clone(), from_subject, from_comparison);
}
if from_subject {
self.subject.absorb(&id, parents, &self.comparison.original);
}
if from_comparison {
self.comparison.absorb(&id, parents, &self.subject.original);
}
}
fn build_forward_chain(visited: &[EventId], meet: &BTreeSet<EventId>) -> Vec<EventId> {
let mut chain: Vec<_> = visited.iter().rev().cloned().collect();
if !meet.is_empty() {
if let Some(pos) = chain.iter().position(|id| meet.contains(id)) {
chain = chain.into_iter().skip(pos + 1).collect();
}
}
chain
}
fn collect_immediate_children(&self, meet: &[EventId]) -> (Vec<EventId>, Vec<EventId>) {
let mut subject_immediate = BTreeSet::new();
let mut other_immediate = BTreeSet::new();
for meet_id in meet {
if let Some(state) = self.states.get(meet_id) {
for child in &state.subject_children {
subject_immediate.insert(child.clone());
}
for child in &state.other_children {
other_immediate.insert(child.clone());
}
}
}
let meet_set: BTreeSet<_> = meet.iter().cloned().collect();
let common: BTreeSet<_> = subject_immediate.intersection(&other_immediate).cloned().collect();
subject_immediate.retain(|id| !common.contains(id) && !meet_set.contains(id));
other_immediate.retain(|id| !common.contains(id) && !meet_set.contains(id));
(subject_immediate.into_iter().collect(), other_immediate.into_iter().collect())
}
fn check_result(&mut self) -> Option<AbstractCausalRelation<EventId>> {
if self.subject.covers(&self.comparison) {
let dag = self.accumulator.dag();
let comparison_heads: Vec<EventId> = self.comparison.original.iter().cloned().collect();
let comparison_ancestry = compute_ancestry_from_dag(dag, &comparison_heads);
let roots_grounded = self
.subject
.processed
.iter()
.filter(|id| dag.get(*id).is_some_and(|parents| parents.is_empty()))
.all(|root| comparison_ancestry.contains(root));
let frontier_grounded = self.subject.frontier.ids.iter().all(|id| comparison_ancestry.contains(id));
if roots_grounded && frontier_grounded {
let chain: Vec<_> =
self.subject.visited.iter().rev().filter(|id| !self.comparison.original.contains(id)).cloned().collect();
return Some(AbstractCausalRelation::StrictDescends { chain });
}
}
if self.comparison.covers(&self.subject) {
return Some(AbstractCausalRelation::StrictAscends);
}
if self.subject.frontier.is_empty() && self.comparison.frontier.is_empty() {
if !self.any_common {
if let (Some(subject_root), Some(other_root)) = (&self.subject.root, &self.comparison.root) {
if subject_root != other_root {
return Some(AbstractCausalRelation::Disjoint {
gca: None,
subject_root: subject_root.clone(),
other_root: other_root.clone(),
});
}
}
return Some(AbstractCausalRelation::DivergedSince {
meet: vec![],
subject: vec![],
other: vec![],
subject_chain: vec![],
other_chain: vec![],
});
}
let dag = self.accumulator.dag();
let meet_candidates = &self.meet_candidates;
let all_heads_accounted = self
.comparison
.original
.iter()
.all(|head| compute_ancestry_from_dag(dag, std::slice::from_ref(head)).iter().any(|id| meet_candidates.contains(id)));
if !all_heads_accounted {
return Some(AbstractCausalRelation::DivergedSince {
meet: vec![],
subject: vec![],
other: vec![],
subject_chain: vec![],
other_chain: vec![],
});
}
let meet: Vec<_> =
self.meet_candidates.iter().filter(|id| self.states.get(*id).map_or(0, |s| s.common_child_count) == 0).cloned().collect();
let meet_set: BTreeSet<_> = meet.iter().cloned().collect();
let subject_chain = Self::build_forward_chain(&self.subject.visited, &meet_set);
let other_chain = Self::build_forward_chain(&self.comparison.visited, &meet_set);
let (subject_immediate, other_immediate) = self.collect_immediate_children(&meet);
Some(AbstractCausalRelation::DivergedSince {
meet,
subject: subject_immediate,
other: other_immediate,
subject_chain,
other_chain,
})
} else if self.remaining_budget == 0 {
Some(AbstractCausalRelation::BudgetExceeded {
subject: self.subject.frontier.ids.clone(),
other: self.comparison.frontier.ids.clone(),
})
} else {
None
}
}
}