use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DependencyKind {
TensorInput,
TensorOutput,
RuleImplication,
SharedFact,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DependencyEdge {
pub from_id: u64,
pub to_id: u64,
pub kind: DependencyKind,
pub weight: u32,
}
#[derive(Clone, Debug, Default)]
pub struct DirtySet {
pub dirty: HashSet<u64>,
}
impl DirtySet {
pub fn new() -> Self {
Self {
dirty: HashSet::new(),
}
}
pub fn mark_dirty(&mut self, id: u64) {
self.dirty.insert(id);
}
pub fn is_dirty(&self, id: u64) -> bool {
self.dirty.contains(&id)
}
pub fn clear_dirty(&mut self, id: u64) {
self.dirty.remove(&id);
}
pub fn all_dirty(&self) -> Vec<u64> {
let mut v: Vec<u64> = self.dirty.iter().copied().collect();
v.sort_unstable();
v
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphStats {
pub node_count: usize,
pub edge_count: usize,
pub dirty_count: usize,
pub max_in_degree: usize,
pub max_out_degree: usize,
}
pub struct TensorDependencyGraph {
pub nodes: HashSet<u64>,
pub edges: Vec<DependencyEdge>,
pub dirty: DirtySet,
}
impl TensorDependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashSet::new(),
edges: Vec::new(),
dirty: DirtySet::new(),
}
}
pub fn add_node(&mut self, id: u64) {
self.nodes.insert(id);
}
pub fn add_edge(&mut self, edge: DependencyEdge) {
self.nodes.insert(edge.from_id);
self.nodes.insert(edge.to_id);
self.edges.push(edge);
}
pub fn remove_node(&mut self, id: u64) {
self.nodes.remove(&id);
self.edges.retain(|e| e.from_id != id && e.to_id != id);
self.dirty.clear_dirty(id);
}
pub fn mark_dirty(&mut self, id: u64) {
let mut queue: VecDeque<u64> = VecDeque::new();
self.dirty.mark_dirty(id);
queue.push_back(id);
while let Some(current) = queue.pop_front() {
let successors: Vec<u64> = self
.edges
.iter()
.filter(|e| {
e.from_id == current
&& matches!(
e.kind,
DependencyKind::TensorOutput
| DependencyKind::RuleImplication
| DependencyKind::SharedFact
)
})
.map(|e| e.to_id)
.collect();
for succ in successors {
if !self.dirty.is_dirty(succ) {
self.dirty.mark_dirty(succ);
queue.push_back(succ);
}
}
}
}
pub fn recompute_order(&self) -> Vec<u64> {
let dirty: HashSet<u64> = self.dirty.dirty.clone();
if dirty.is_empty() {
return Vec::new();
}
let mut in_degree: HashMap<u64, usize> = dirty.iter().map(|&n| (n, 0)).collect();
let mut adj: HashMap<u64, Vec<u64>> = dirty.iter().map(|&n| (n, Vec::new())).collect();
for edge in &self.edges {
if dirty.contains(&edge.from_id) && dirty.contains(&edge.to_id) {
adj.entry(edge.from_id).or_default().push(edge.to_id);
*in_degree.entry(edge.to_id).or_insert(0) += 1;
}
}
let mut queue: VecDeque<u64> = in_degree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(&n, _)| n)
.collect();
let mut sorted_queue: Vec<u64> = queue.drain(..).collect();
sorted_queue.sort_unstable();
queue.extend(sorted_queue);
let mut result: Vec<u64> = Vec::with_capacity(dirty.len());
while let Some(node) = queue.pop_front() {
result.push(node);
if let Some(neighbors) = adj.get(&node) {
let mut next_batch: Vec<u64> = Vec::new();
for &neighbor in neighbors {
if let Some(deg) = in_degree.get_mut(&neighbor) {
*deg = deg.saturating_sub(1);
if *deg == 0 {
next_batch.push(neighbor);
}
}
}
next_batch.sort_unstable();
queue.extend(next_batch);
}
}
if result.len() < dirty.len() {
let processed: HashSet<u64> = result.iter().copied().collect();
let mut remaining: Vec<u64> = dirty
.iter()
.filter(|n| !processed.contains(n))
.copied()
.collect();
remaining.sort_unstable();
result.extend(remaining);
}
result
}
pub fn dependents_of(&self, id: u64) -> Vec<u64> {
let mut out: Vec<u64> = self
.edges
.iter()
.filter(|e| e.from_id == id)
.map(|e| e.to_id)
.collect();
out.sort_unstable();
out.dedup();
out
}
pub fn dependencies_of(&self, id: u64) -> Vec<u64> {
let mut out: Vec<u64> = self
.edges
.iter()
.filter(|e| e.to_id == id)
.map(|e| e.from_id)
.collect();
out.sort_unstable();
out.dedup();
out
}
pub fn stats(&self) -> GraphStats {
let node_count = self.nodes.len();
let edge_count = self.edges.len();
let dirty_count = self.dirty.dirty.len();
let mut in_degree: HashMap<u64, usize> = HashMap::new();
let mut out_degree: HashMap<u64, usize> = HashMap::new();
for node in &self.nodes {
in_degree.entry(*node).or_insert(0);
out_degree.entry(*node).or_insert(0);
}
for edge in &self.edges {
*out_degree.entry(edge.from_id).or_insert(0) += 1;
*in_degree.entry(edge.to_id).or_insert(0) += 1;
}
let max_in_degree = in_degree.values().copied().max().unwrap_or(0);
let max_out_degree = out_degree.values().copied().max().unwrap_or(0);
GraphStats {
node_count,
edge_count,
dirty_count,
max_in_degree,
max_out_degree,
}
}
pub fn clear_all_dirty(&mut self) {
self.dirty.dirty.clear();
}
}
impl Default for TensorDependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn edge(from: u64, to: u64, kind: DependencyKind) -> DependencyEdge {
DependencyEdge {
from_id: from,
to_id: to,
kind,
weight: 1,
}
}
fn edge_w(from: u64, to: u64, kind: DependencyKind, weight: u32) -> DependencyEdge {
DependencyEdge {
from_id: from,
to_id: to,
kind,
weight,
}
}
#[test]
fn test_empty_graph_stats() {
let g = TensorDependencyGraph::new();
let s = g.stats();
assert_eq!(s.node_count, 0);
assert_eq!(s.edge_count, 0);
assert_eq!(s.dirty_count, 0);
assert_eq!(s.max_in_degree, 0);
assert_eq!(s.max_out_degree, 0);
}
#[test]
fn test_add_node_idempotent() {
let mut g = TensorDependencyGraph::new();
g.add_node(1);
g.add_node(1);
g.add_node(1);
assert_eq!(g.stats().node_count, 1);
}
#[test]
fn test_add_edge_registers_nodes() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(10, 20, DependencyKind::TensorInput));
assert!(g.nodes.contains(&10));
assert!(g.nodes.contains(&20));
assert_eq!(g.stats().node_count, 2);
}
#[test]
fn test_remove_node_cleans_edges() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(2, 3, DependencyKind::RuleImplication));
g.remove_node(2);
assert_eq!(g.edges.len(), 0);
assert!(!g.nodes.contains(&2));
}
#[test]
fn test_remove_node_clears_dirty() {
let mut g = TensorDependencyGraph::new();
g.add_node(5);
g.dirty.mark_dirty(5);
assert!(g.dirty.is_dirty(5));
g.remove_node(5);
assert!(!g.dirty.is_dirty(5));
}
#[test]
fn test_mark_dirty_propagates_transitively() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(2, 3, DependencyKind::RuleImplication));
g.add_edge(edge(3, 4, DependencyKind::SharedFact));
g.mark_dirty(1);
assert!(g.dirty.is_dirty(1));
assert!(g.dirty.is_dirty(2));
assert!(g.dirty.is_dirty(3));
assert!(g.dirty.is_dirty(4));
}
#[test]
fn test_mark_dirty_no_outgoing() {
let mut g = TensorDependencyGraph::new();
g.add_node(42);
g.mark_dirty(42);
assert!(g.dirty.is_dirty(42));
assert_eq!(g.dirty.dirty.len(), 1);
}
#[test]
fn test_mark_dirty_no_propagate_tensor_input() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorInput));
g.mark_dirty(1);
assert!(g.dirty.is_dirty(1));
assert!(!g.dirty.is_dirty(2));
}
#[test]
fn test_recompute_order_empty_dirty() {
let g = TensorDependencyGraph::new();
assert!(g.recompute_order().is_empty());
}
#[test]
fn test_recompute_order_single_dirty() {
let mut g = TensorDependencyGraph::new();
g.add_node(7);
g.mark_dirty(7);
let order = g.recompute_order();
assert_eq!(order, vec![7]);
}
#[test]
fn test_recompute_order_topo() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(2, 3, DependencyKind::TensorOutput));
g.mark_dirty(1);
let order = g.recompute_order();
let pos = |n: u64| {
order
.iter()
.position(|&x| x == n)
.expect("test: should succeed")
};
assert!(pos(1) < pos(2));
assert!(pos(2) < pos(3));
}
#[test]
fn test_recompute_order_diamond() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(1, 3, DependencyKind::TensorOutput));
g.add_edge(edge(2, 4, DependencyKind::TensorOutput));
g.add_edge(edge(3, 4, DependencyKind::TensorOutput));
g.mark_dirty(1);
let order = g.recompute_order();
let pos = |n: u64| {
order
.iter()
.position(|&x| x == n)
.expect("test: should succeed")
};
assert!(pos(1) < pos(2));
assert!(pos(1) < pos(3));
assert!(pos(2) < pos(4));
assert!(pos(3) < pos(4));
assert_eq!(order.len(), 4);
}
#[test]
fn test_dependents_of() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 3, DependencyKind::TensorOutput));
g.add_edge(edge(1, 2, DependencyKind::RuleImplication));
g.add_edge(edge(5, 1, DependencyKind::SharedFact));
let deps = g.dependents_of(1);
assert_eq!(deps, vec![2, 3]);
}
#[test]
fn test_dependencies_of() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(2, 1, DependencyKind::TensorOutput));
g.add_edge(edge(3, 1, DependencyKind::RuleImplication));
g.add_edge(edge(1, 5, DependencyKind::SharedFact));
let deps = g.dependencies_of(1);
assert_eq!(deps, vec![2, 3]);
}
#[test]
fn test_dirtyset_mark_check_clear() {
let mut ds = DirtySet::new();
assert!(!ds.is_dirty(1));
ds.mark_dirty(1);
assert!(ds.is_dirty(1));
ds.clear_dirty(1);
assert!(!ds.is_dirty(1));
}
#[test]
fn test_dirtyset_all_dirty_sorted() {
let mut ds = DirtySet::new();
ds.mark_dirty(5);
ds.mark_dirty(2);
ds.mark_dirty(8);
ds.mark_dirty(1);
assert_eq!(ds.all_dirty(), vec![1, 2, 5, 8]);
}
#[test]
fn test_stats_node_edge_count() {
let mut g = TensorDependencyGraph::new();
g.add_node(1);
g.add_node(2);
g.add_edge(edge(1, 2, DependencyKind::TensorInput));
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
let s = g.stats();
assert_eq!(s.node_count, 2);
assert_eq!(s.edge_count, 2);
}
#[test]
fn test_max_in_degree() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 3, DependencyKind::TensorOutput));
g.add_edge(edge(2, 3, DependencyKind::TensorOutput));
g.add_edge(edge(4, 3, DependencyKind::TensorOutput));
let s = g.stats();
assert_eq!(s.max_in_degree, 3);
}
#[test]
fn test_max_out_degree() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(1, 3, DependencyKind::TensorOutput));
g.add_edge(edge(1, 4, DependencyKind::TensorOutput));
let s = g.stats();
assert_eq!(s.max_out_degree, 3);
}
#[test]
fn test_clear_all_dirty() {
let mut g = TensorDependencyGraph::new();
g.add_node(1);
g.add_node(2);
g.mark_dirty(1);
g.mark_dirty(2);
assert_eq!(g.dirty.dirty.len(), 2);
g.clear_all_dirty();
assert!(g.dirty.dirty.is_empty());
}
#[test]
fn test_cycle_no_panic() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::RuleImplication));
g.add_edge(edge(2, 1, DependencyKind::RuleImplication));
g.mark_dirty(1);
let order = g.recompute_order();
assert_eq!(order.len(), 2);
assert!(order.contains(&1));
assert!(order.contains(&2));
}
#[test]
fn test_dependency_kind_variants() {
let kinds = [
DependencyKind::TensorInput,
DependencyKind::TensorOutput,
DependencyKind::RuleImplication,
DependencyKind::SharedFact,
];
for k in kinds {
let k2 = k;
assert_eq!(k, k2);
}
}
#[test]
fn test_weight_preserved() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge_w(1, 2, DependencyKind::TensorOutput, 42));
let e = &g.edges[0];
assert_eq!(e.weight, 42);
assert_eq!(e.from_id, 1);
assert_eq!(e.to_id, 2);
}
#[test]
fn test_stats_dirty_count() {
let mut g = TensorDependencyGraph::new();
g.add_node(1);
g.add_node(2);
g.add_node(3);
g.mark_dirty(1);
g.mark_dirty(3);
let s = g.stats();
assert_eq!(s.dirty_count, 2);
}
#[test]
fn test_remove_node_leaves_other_edges() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(3, 4, DependencyKind::TensorOutput));
g.remove_node(1);
assert_eq!(g.edges.len(), 1);
assert_eq!(g.edges[0].from_id, 3);
}
#[test]
fn test_mark_dirty_terminates_with_shared_targets() {
let mut g = TensorDependencyGraph::new();
g.add_edge(edge(1, 2, DependencyKind::TensorOutput));
g.add_edge(edge(1, 2, DependencyKind::TensorOutput)); g.mark_dirty(1);
assert!(g.dirty.is_dirty(2));
}
}