use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
use crate::canonical::CanonicalRecord;
use crate::clock::ClockTime;
use crate::symbol::SymbolId;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum EdgeKind {
Supersedes,
Corrects,
StaleParent,
Reconfirms,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Edge {
pub kind: EdgeKind,
pub from: SymbolId,
pub to: SymbolId,
pub at: ClockTime,
}
impl Edge {
#[must_use]
pub fn try_from_record(record: &CanonicalRecord) -> Option<Self> {
let (kind, r) = match record {
CanonicalRecord::Supersedes(r) => (EdgeKind::Supersedes, r),
CanonicalRecord::Corrects(r) => (EdgeKind::Corrects, r),
CanonicalRecord::StaleParent(r) => (EdgeKind::StaleParent, r),
CanonicalRecord::Reconfirms(r) => (EdgeKind::Reconfirms, r),
_ => return None,
};
Some(Self {
kind,
from: r.from,
to: r.to,
at: r.at,
})
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum DagError {
#[error("supersession cycle: {kind:?} edge {from:?} -> {to:?} would close a cycle")]
SupersessionCycle {
from: SymbolId,
to: SymbolId,
kind: EdgeKind,
},
#[error("self-edge is forbidden: {kind:?} edge on {memory:?}")]
SelfEdge {
memory: SymbolId,
kind: EdgeKind,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SupersessionDag {
edges: Vec<Edge>,
outgoing: BTreeMap<SymbolId, Vec<usize>>,
incoming: BTreeMap<SymbolId, Vec<usize>>,
}
impl SupersessionDag {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_edge(&mut self, edge: Edge) -> Result<(), DagError> {
if edge.from == edge.to {
return Err(DagError::SelfEdge {
memory: edge.from,
kind: edge.kind,
});
}
if self.reaches(edge.to, edge.from) {
tracing::warn!(
target: "mimir.dag.cycle_rejected",
from = %edge.from,
to = %edge.to,
edge_kind = ?edge.kind,
"supersession cycle rejected",
);
return Err(DagError::SupersessionCycle {
from: edge.from,
to: edge.to,
kind: edge.kind,
});
}
let idx = self.edges.len();
self.outgoing.entry(edge.from).or_default().push(idx);
self.incoming.entry(edge.to).or_default().push(idx);
self.edges.push(edge);
Ok(())
}
fn reaches(&self, start: SymbolId, target: SymbolId) -> bool {
if start == target {
return true;
}
let mut visited = BTreeSet::new();
let mut stack = vec![start];
while let Some(node) = stack.pop() {
if !visited.insert(node) {
continue;
}
if let Some(indices) = self.outgoing.get(&node) {
for &i in indices {
let next = self.edges[i].to;
if next == target {
return true;
}
stack.push(next);
}
}
}
false
}
#[must_use]
pub fn edges(&self) -> &[Edge] {
&self.edges
}
#[must_use]
pub fn len(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
pub fn edges_from(&self, memory: SymbolId) -> impl Iterator<Item = &Edge> {
self.outgoing
.get(&memory)
.into_iter()
.flat_map(move |idxs| idxs.iter().map(move |&i| &self.edges[i]))
}
pub fn edges_to(&self, memory: SymbolId) -> impl Iterator<Item = &Edge> {
self.incoming
.get(&memory)
.into_iter()
.flat_map(move |idxs| idxs.iter().map(move |&i| &self.edges[i]))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn m(n: u64) -> SymbolId {
SymbolId::new(n)
}
fn at(millis: u64) -> ClockTime {
ClockTime::try_from_millis(millis).expect("non-sentinel")
}
fn edge(kind: EdgeKind, from: u64, to: u64) -> Edge {
Edge {
kind,
from: m(from),
to: m(to),
at: at(1_000_000),
}
}
#[test]
fn empty_dag_has_zero_edges() {
let dag = SupersessionDag::new();
assert!(dag.is_empty());
assert_eq!(dag.len(), 0);
assert_eq!(dag.edges(), &[]);
assert!(dag.edges_from(m(0)).next().is_none());
assert!(dag.edges_to(m(0)).next().is_none());
}
#[test]
fn single_edge_adds_and_indexes() {
let mut dag = SupersessionDag::new();
let e = edge(EdgeKind::Supersedes, 1, 2);
dag.add_edge(e).expect("add");
assert_eq!(dag.len(), 1);
assert_eq!(dag.edges(), &[e]);
let out: Vec<_> = dag.edges_from(m(1)).copied().collect();
assert_eq!(out, vec![e]);
let inc: Vec<_> = dag.edges_to(m(2)).copied().collect();
assert_eq!(inc, vec![e]);
assert!(dag.edges_to(m(1)).next().is_none());
assert!(dag.edges_from(m(2)).next().is_none());
}
#[test]
fn self_edge_is_rejected() {
let mut dag = SupersessionDag::new();
let err = dag
.add_edge(edge(EdgeKind::Supersedes, 7, 7))
.expect_err("self edge");
assert_eq!(
err,
DagError::SelfEdge {
memory: m(7),
kind: EdgeKind::Supersedes
}
);
assert!(dag.is_empty(), "failed add must not mutate");
}
#[test]
fn direct_cycle_is_rejected() {
let mut dag = SupersessionDag::new();
dag.add_edge(edge(EdgeKind::Supersedes, 1, 2))
.expect("first");
let err = dag
.add_edge(edge(EdgeKind::Supersedes, 2, 1))
.expect_err("cycle");
assert!(matches!(
err,
DagError::SupersessionCycle {
from, to, kind: EdgeKind::Supersedes
} if from == m(2) && to == m(1)
));
assert_eq!(dag.len(), 1, "failed add must not mutate");
}
#[test]
fn indirect_cycle_across_kinds_is_rejected() {
let mut dag = SupersessionDag::new();
dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("a");
dag.add_edge(edge(EdgeKind::Corrects, 2, 3)).expect("b");
let err = dag
.add_edge(edge(EdgeKind::StaleParent, 3, 1))
.expect_err("3-cycle across kinds");
assert!(matches!(err, DagError::SupersessionCycle { .. }));
assert_eq!(dag.len(), 2);
}
#[test]
fn dag_shaped_additions_all_succeed() {
let mut dag = SupersessionDag::new();
dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("12");
dag.add_edge(edge(EdgeKind::Supersedes, 1, 3)).expect("13");
dag.add_edge(edge(EdgeKind::Supersedes, 2, 4)).expect("24");
dag.add_edge(edge(EdgeKind::Supersedes, 3, 4)).expect("34");
assert_eq!(dag.len(), 4);
assert_eq!(dag.edges_to(m(4)).count(), 2);
assert_eq!(dag.edges_from(m(1)).count(), 2);
}
#[test]
fn edge_try_from_record_matches_every_edge_variant() {
use crate::canonical::{CanonicalRecord, EdgeRecord};
let r = EdgeRecord {
from: m(10),
to: m(11),
at: at(42),
};
let cases = [
(CanonicalRecord::Supersedes(r), EdgeKind::Supersedes),
(CanonicalRecord::Corrects(r), EdgeKind::Corrects),
(CanonicalRecord::StaleParent(r), EdgeKind::StaleParent),
(CanonicalRecord::Reconfirms(r), EdgeKind::Reconfirms),
];
for (record, expected_kind) in cases {
let e = Edge::try_from_record(&record).expect("edge variant");
assert_eq!(e.kind, expected_kind);
assert_eq!(e.from, m(10));
assert_eq!(e.to, m(11));
assert_eq!(e.at, at(42));
}
}
#[test]
fn edge_try_from_record_rejects_non_edge_variants() {
use crate::canonical::{CanonicalRecord, CheckpointRecord};
let cp = CanonicalRecord::Checkpoint(CheckpointRecord {
episode_id: m(1),
at: at(1),
memory_count: 0,
});
assert!(Edge::try_from_record(&cp).is_none());
}
#[test]
fn failed_cycle_add_does_not_corrupt_indices() {
let mut dag = SupersessionDag::new();
dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("a");
let _ = dag
.add_edge(edge(EdgeKind::Supersedes, 2, 1))
.expect_err("cycle");
assert_eq!(dag.len(), 1);
assert_eq!(dag.edges_from(m(1)).count(), 1);
assert_eq!(dag.edges_from(m(2)).count(), 0);
assert_eq!(dag.edges_to(m(1)).count(), 0);
assert_eq!(dag.edges_to(m(2)).count(), 1);
}
}