use std::collections::VecDeque;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NodeId(u32);
impl NodeId {
const NONE: u32 = u32::MAX;
#[must_use]
pub fn from_raw(index: u32) -> Self {
Self(index)
}
#[must_use]
pub fn raw(self) -> u32 {
self.0
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "N{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputKind {
Constraint,
Content,
Style,
}
#[derive(Clone)]
struct DepNode {
generation: u32,
dirty_gen: u32,
constraint_hash: u64,
content_hash: u64,
style_hash: u64,
parent: u32,
}
impl DepNode {
fn new(generation: u32) -> Self {
Self {
generation,
dirty_gen: 0,
constraint_hash: 0,
content_hash: 0,
style_hash: 0,
parent: NodeId::NONE,
}
}
fn is_dirty(&self) -> bool {
self.dirty_gen >= self.generation
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CycleError {
pub from: NodeId,
pub to: NodeId,
}
impl fmt::Display for CycleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"layout cycle detected: {} → {} would create a cycle",
self.from, self.to
)
}
}
impl std::error::Error for CycleError {}
pub struct DepGraph {
nodes: Vec<DepNode>,
fwd_adj: Vec<Vec<NodeId>>,
rev_adj: Vec<Vec<NodeId>>,
current_gen: u32,
pending_dirty: Vec<NodeId>,
free_list: Vec<u32>,
}
impl DepGraph {
#[must_use]
pub fn new() -> Self {
Self {
nodes: Vec::new(),
fwd_adj: Vec::new(),
rev_adj: Vec::new(),
current_gen: 1,
pending_dirty: Vec::new(),
free_list: Vec::new(),
}
}
#[must_use]
pub fn with_capacity(node_cap: usize, _edge_cap: usize) -> Self {
Self {
nodes: Vec::with_capacity(node_cap),
fwd_adj: Vec::with_capacity(node_cap),
rev_adj: Vec::with_capacity(node_cap),
current_gen: 1,
pending_dirty: Vec::new(),
free_list: Vec::new(),
}
}
pub fn add_node(&mut self) -> NodeId {
if let Some(slot) = self.free_list.pop() {
self.nodes[slot as usize] = DepNode::new(self.current_gen);
self.fwd_adj[slot as usize].clear();
self.rev_adj[slot as usize].clear();
NodeId(slot)
} else {
let id = self.nodes.len() as u32;
self.nodes.push(DepNode::new(self.current_gen));
self.fwd_adj.push(Vec::new());
self.rev_adj.push(Vec::new());
NodeId(id)
}
}
pub fn remove_node(&mut self, id: NodeId) {
let idx = id.0 as usize;
if idx < self.nodes.len() && self.nodes[idx].generation != 0 {
for &rev in &self.rev_adj[idx] {
let child_idx = rev.0 as usize;
if child_idx < self.nodes.len() && self.nodes[child_idx].parent == id.0 {
self.nodes[child_idx].parent = NodeId::NONE;
}
}
let fwds = std::mem::take(&mut self.fwd_adj[idx]);
for fwd in fwds {
if let Some(revs) = self.rev_adj.get_mut(fwd.0 as usize) {
revs.retain(|&x| x != id);
}
}
let revs = std::mem::take(&mut self.rev_adj[idx]);
for rev in revs {
if let Some(fwds) = self.fwd_adj.get_mut(rev.0 as usize) {
fwds.retain(|&x| x != id);
}
}
self.nodes[idx].generation = 0;
self.nodes[idx].dirty_gen = 0;
self.free_list.push(id.0);
}
}
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len() - self.free_list.len()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.fwd_adj.iter().map(|v| v.len()).sum()
}
pub fn set_parent(&mut self, child: NodeId, parent: NodeId) {
if (child.0 as usize) < self.nodes.len() {
self.nodes[child.0 as usize].parent = parent.0;
}
}
#[must_use]
pub fn parent(&self, id: NodeId) -> Option<NodeId> {
let node = self.nodes.get(id.0 as usize)?;
if node.parent == NodeId::NONE {
None
} else {
Some(NodeId(node.parent))
}
}
pub fn add_edge(&mut self, from: NodeId, to: NodeId) -> Result<(), CycleError> {
if from == to {
return Err(CycleError { from, to });
}
if self.can_reach(to, from) {
return Err(CycleError { from, to });
}
self.fwd_adj[from.0 as usize].push(to);
self.rev_adj[to.0 as usize].push(from);
Ok(())
}
fn can_reach(&self, from: NodeId, to: NodeId) -> bool {
let mut visited = vec![false; self.nodes.len()];
let mut stack = vec![from];
while let Some(current) = stack.pop() {
if current == to {
return true;
}
let idx = current.0 as usize;
if idx >= self.nodes.len() || visited[idx] {
continue;
}
visited[idx] = true;
if self.nodes[idx].generation == 0 {
continue; }
for &dep in &self.fwd_adj[idx] {
if !visited[dep.0 as usize] {
stack.push(dep);
}
}
}
false
}
pub fn mark_changed(&mut self, id: NodeId, kind: InputKind, new_hash: u64) {
let idx = id.0 as usize;
if idx >= self.nodes.len() {
return;
}
let node = &mut self.nodes[idx];
if node.generation == 0 {
return; }
let old_hash = match kind {
InputKind::Constraint => &mut node.constraint_hash,
InputKind::Content => &mut node.content_hash,
InputKind::Style => &mut node.style_hash,
};
if *old_hash == new_hash {
return; }
*old_hash = new_hash;
node.dirty_gen = self.current_gen;
self.pending_dirty.push(id);
}
pub fn mark_dirty(&mut self, id: NodeId) {
let idx = id.0 as usize;
if idx >= self.nodes.len() {
return;
}
let node = &mut self.nodes[idx];
if node.generation == 0 {
return;
}
node.dirty_gen = self.current_gen;
self.pending_dirty.push(id);
}
pub fn propagate(&mut self) -> Vec<NodeId> {
if self.pending_dirty.is_empty() {
return Vec::new();
}
let mut queue = VecDeque::new();
let mut visited = vec![false; self.nodes.len()];
for &id in &self.pending_dirty {
let idx = id.0 as usize;
if idx < self.nodes.len() && !visited[idx] {
visited[idx] = true;
queue.push_back(id);
}
}
self.pending_dirty.clear();
while let Some(current) = queue.pop_front() {
let idx = current.0 as usize;
if self.nodes[idx].generation == 0 {
continue;
}
self.nodes[idx].dirty_gen = self.current_gen;
for i in 0..self.rev_adj[idx].len() {
let dependent = self.rev_adj[idx][i];
let dep_idx = dependent.0 as usize;
if dep_idx < self.nodes.len() && !visited[dep_idx] {
visited[dep_idx] = true;
queue.push_back(dependent);
}
}
}
self.collect_dirty_dfs_preorder()
}
fn collect_dirty_dfs_preorder(&self) -> Vec<NodeId> {
let mut roots = Vec::new();
for (i, node) in self.nodes.iter().enumerate() {
if node.generation == 0 || !node.is_dirty() {
continue;
}
let has_dirty_dependency = self.fwd_adj[i].iter().any(|&dep_id| {
let dep_idx = dep_id.0 as usize;
dep_idx < self.nodes.len()
&& self.nodes[dep_idx].generation != 0
&& self.nodes[dep_idx].is_dirty()
});
if !has_dirty_dependency {
roots.push(NodeId(i as u32));
}
}
roots.sort();
let mut result = Vec::new();
let mut visited = vec![false; self.nodes.len()];
for root in roots.into_iter().rev() {
self.dfs_postorder(root, &mut result, &mut visited);
}
result.reverse();
result
}
fn dfs_postorder(&self, id: NodeId, result: &mut Vec<NodeId>, visited: &mut [bool]) {
let idx = id.0 as usize;
if idx >= self.nodes.len() || visited[idx] {
return;
}
let node = &self.nodes[idx];
if node.generation == 0 || !node.is_dirty() {
return;
}
visited[idx] = true;
let mut children: Vec<NodeId> = self.rev_adj[idx]
.iter()
.filter(|d| {
let di = d.0 as usize;
di < self.nodes.len()
&& !visited[di]
&& self.nodes[di].generation != 0
&& self.nodes[di].is_dirty()
})
.copied()
.collect();
children.sort();
for child in children.into_iter().rev() {
self.dfs_postorder(child, result, visited);
}
result.push(id);
}
#[must_use]
pub fn is_dirty(&self, id: NodeId) -> bool {
self.nodes
.get(id.0 as usize)
.is_some_and(|n| n.generation != 0 && n.is_dirty())
}
pub fn clean(&mut self, id: NodeId) {
if let Some(node) = self.nodes.get_mut(id.0 as usize) {
node.generation = self.current_gen;
node.dirty_gen = 0;
}
}
pub fn clean_all(&mut self) {
self.current_gen = self.current_gen.wrapping_add(1);
if self.current_gen == 0 {
self.current_gen = 1; }
for node in &mut self.nodes {
if node.generation != 0 {
node.generation = self.current_gen;
node.dirty_gen = 0;
}
}
self.pending_dirty.clear();
}
#[must_use]
pub fn constraint_hash(&self, id: NodeId) -> Option<u64> {
self.nodes
.get(id.0 as usize)
.filter(|n| n.generation != 0)
.map(|n| n.constraint_hash)
}
#[must_use]
pub fn content_hash(&self, id: NodeId) -> Option<u64> {
self.nodes
.get(id.0 as usize)
.filter(|n| n.generation != 0)
.map(|n| n.content_hash)
}
#[must_use]
pub fn style_hash(&self, id: NodeId) -> Option<u64> {
self.nodes
.get(id.0 as usize)
.filter(|n| n.generation != 0)
.map(|n| n.style_hash)
}
pub fn dirty_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
self.nodes
.iter()
.enumerate()
.filter(|(_, n)| n.generation != 0 && n.is_dirty())
.map(|(i, _)| NodeId(i as u32))
}
#[must_use]
pub fn dirty_count(&self) -> usize {
self.nodes
.iter()
.filter(|n| n.generation != 0 && n.is_dirty())
.count()
}
#[must_use]
pub fn dependencies(&self, id: NodeId) -> &[NodeId] {
let idx = id.0 as usize;
if idx >= self.nodes.len() || self.nodes[idx].generation == 0 {
return &[];
}
&self.fwd_adj[idx]
}
#[must_use]
pub fn dependents(&self, id: NodeId) -> &[NodeId] {
let idx = id.0 as usize;
if idx >= self.nodes.len() || self.nodes[idx].generation == 0 {
return &[];
}
&self.rev_adj[idx]
}
pub fn invalidate_all(&mut self) {
for (i, node) in self.nodes.iter_mut().enumerate() {
if node.generation != 0 {
node.dirty_gen = self.current_gen;
self.pending_dirty.push(NodeId(i as u32));
}
}
}
}
impl Default for DepGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_node_returns_sequential_ids() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
assert_eq!(a.raw(), 0);
assert_eq!(b.raw(), 1);
assert_eq!(c.raw(), 2);
assert_eq!(g.node_count(), 3);
}
#[test]
fn remove_node_recycles_slot() {
let mut g = DepGraph::new();
let a = g.add_node();
let _b = g.add_node();
g.remove_node(a);
assert_eq!(g.node_count(), 1);
let c = g.add_node();
assert_eq!(c.raw(), 0); assert_eq!(g.node_count(), 2);
}
#[test]
fn add_edge_creates_dependency() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(a, b).unwrap();
assert_eq!(g.dependencies(a), &[b]);
assert_eq!(g.dependents(b), &[a]);
assert_eq!(g.edge_count(), 1);
}
#[test]
fn self_loop_detected() {
let mut g = DepGraph::new();
let a = g.add_node();
let err = g.add_edge(a, a).unwrap_err();
assert_eq!(err, CycleError { from: a, to: a });
}
#[test]
fn two_node_cycle_detected() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(a, b).unwrap();
let err = g.add_edge(b, a).unwrap_err();
assert_eq!(err, CycleError { from: b, to: a });
}
#[test]
fn three_node_cycle_detected() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.add_edge(a, b).unwrap();
g.add_edge(b, c).unwrap();
let err = g.add_edge(c, a).unwrap_err();
assert_eq!(err, CycleError { from: c, to: a });
}
#[test]
fn dag_allows_diamond() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(a, b).unwrap();
g.add_edge(a, c).unwrap();
g.add_edge(b, d).unwrap();
g.add_edge(c, d).unwrap();
assert_eq!(g.edge_count(), 4);
}
#[test]
fn mark_changed_deduplicates() {
let mut g = DepGraph::new();
let a = g.add_node();
g.mark_changed(a, InputKind::Constraint, 42);
assert!(g.is_dirty(a));
g.clean(a);
g.mark_changed(a, InputKind::Constraint, 42);
assert!(!g.is_dirty(a)); }
#[test]
fn mark_changed_different_hash() {
let mut g = DepGraph::new();
let a = g.add_node();
g.mark_changed(a, InputKind::Constraint, 42);
g.clean(a);
g.mark_changed(a, InputKind::Constraint, 99); assert!(g.is_dirty(a));
}
#[test]
fn propagate_single_node() {
let mut g = DepGraph::new();
let a = g.add_node();
g.mark_dirty(a);
let dirty = g.propagate();
assert_eq!(dirty, vec![a]);
}
#[test]
fn propagate_parent_to_child() {
let mut g = DepGraph::new();
let parent = g.add_node();
let child = g.add_node();
g.add_edge(child, parent).unwrap(); g.set_parent(child, parent);
g.mark_dirty(parent);
let dirty = g.propagate();
assert!(dirty.contains(&parent));
assert!(dirty.contains(&child));
}
#[test]
fn propagate_chain() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.add_edge(b, a).unwrap(); g.add_edge(c, b).unwrap(); g.set_parent(b, a);
g.set_parent(c, b);
g.mark_dirty(a);
let dirty = g.propagate();
assert_eq!(dirty.len(), 3);
assert!(dirty.contains(&a));
assert!(dirty.contains(&b));
assert!(dirty.contains(&c));
}
#[test]
fn propagate_only_affected_subtree() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(b, a).unwrap();
g.add_edge(c, a).unwrap();
g.mark_dirty(a);
let dirty = g.propagate();
assert!(dirty.contains(&a));
assert!(dirty.contains(&b));
assert!(dirty.contains(&c));
assert!(!dirty.contains(&d));
}
#[test]
fn propagate_diamond_deduplicates() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(b, a).unwrap();
g.add_edge(c, a).unwrap();
g.add_edge(d, b).unwrap();
g.add_edge(d, c).unwrap();
g.mark_dirty(a);
let dirty = g.propagate();
assert_eq!(dirty.len(), 4);
let unique: std::collections::HashSet<_> = dirty.iter().collect();
assert_eq!(unique.len(), 4);
}
#[test]
fn clean_all_resets() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(b, a).unwrap();
g.mark_dirty(a);
g.propagate();
assert!(g.is_dirty(a));
assert!(g.is_dirty(b));
g.clean_all();
assert!(!g.is_dirty(a));
assert!(!g.is_dirty(b));
assert_eq!(g.dirty_count(), 0);
}
#[test]
fn invalidate_all_dirties_everything() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.invalidate_all();
let dirty = g.propagate();
assert_eq!(dirty.len(), 3);
assert!(dirty.contains(&a));
assert!(dirty.contains(&b));
assert!(dirty.contains(&c));
}
#[test]
fn parent_child_relationship() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
assert_eq!(g.parent(a), None);
g.set_parent(b, a);
assert_eq!(g.parent(b), Some(a));
}
#[test]
fn input_hashes_stored_independently() {
let mut g = DepGraph::new();
let a = g.add_node();
g.mark_changed(a, InputKind::Constraint, 1);
g.mark_changed(a, InputKind::Content, 2);
g.mark_changed(a, InputKind::Style, 3);
assert_eq!(g.constraint_hash(a), Some(1));
assert_eq!(g.content_hash(a), Some(2));
assert_eq!(g.style_hash(a), Some(3));
}
#[test]
fn dead_node_is_not_dirty() {
let mut g = DepGraph::new();
let a = g.add_node();
g.mark_dirty(a);
g.remove_node(a);
assert!(!g.is_dirty(a));
}
#[test]
fn propagate_skips_dead_nodes() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.add_edge(b, a).unwrap();
g.add_edge(c, b).unwrap();
g.remove_node(b);
g.mark_dirty(a);
let dirty = g.propagate();
assert!(dirty.contains(&a));
assert!(!dirty.contains(&c));
}
#[test]
fn propagate_empty_returns_empty() {
let mut g = DepGraph::new();
let _a = g.add_node();
let dirty = g.propagate();
assert!(dirty.is_empty());
}
#[test]
fn large_tree_propagation() {
let mut g = DepGraph::with_capacity(1000, 1000);
let root = g.add_node();
let mut leaves = Vec::new();
for _ in 0..10 {
let child = g.add_node();
g.add_edge(child, root).unwrap();
g.set_parent(child, root);
for _ in 0..10 {
let grandchild = g.add_node();
g.add_edge(grandchild, child).unwrap();
g.set_parent(grandchild, child);
leaves.push(grandchild);
}
}
assert_eq!(g.node_count(), 111);
g.mark_dirty(root);
let dirty = g.propagate();
assert_eq!(dirty.len(), 111);
g.clean_all();
g.mark_dirty(leaves[42]);
let dirty = g.propagate();
assert_eq!(dirty.len(), 1);
assert_eq!(dirty[0], leaves[42]);
}
#[test]
fn node_size_under_64_bytes() {
assert!(
std::mem::size_of::<DepNode>() <= 64,
"DepNode is {} bytes, exceeds 64-byte budget",
std::mem::size_of::<DepNode>(),
);
}
#[test]
fn node_size_exactly_40_bytes() {
assert_eq!(
std::mem::size_of::<DepNode>(),
40,
"DepNode should be 40 bytes, got {}",
std::mem::size_of::<DepNode>(),
);
}
#[test]
fn multiple_input_changes_single_propagation() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(b, a).unwrap();
g.mark_changed(a, InputKind::Constraint, 10);
g.mark_changed(a, InputKind::Content, 20);
let dirty = g.propagate();
assert_eq!(dirty.len(), 2); }
#[test]
fn propagate_dfs_preorder() {
let mut g = DepGraph::new();
let r = g.add_node(); let a = g.add_node(); let b = g.add_node(); let c = g.add_node(); g.add_edge(a, r).unwrap();
g.add_edge(b, a).unwrap();
g.add_edge(c, a).unwrap();
g.set_parent(a, r);
g.set_parent(b, a);
g.set_parent(c, a);
g.mark_dirty(r);
let dirty = g.propagate();
assert_eq!(dirty.len(), 4);
assert_eq!(dirty[0], r);
assert_eq!(dirty[1], a);
assert_eq!(dirty[2], b);
assert_eq!(dirty[3], c);
}
#[test]
fn dirty_count_accurate() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
assert_eq!(g.dirty_count(), 0);
g.mark_dirty(a);
g.mark_dirty(b);
g.propagate();
assert_eq!(g.dirty_count(), 2);
g.clean(a);
assert_eq!(g.dirty_count(), 1);
g.clean_all();
assert_eq!(g.dirty_count(), 0);
let _ = c;
}
#[test]
fn dependencies_and_dependents_api() {
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
g.add_edge(b, a).unwrap(); g.add_edge(c, a).unwrap();
assert_eq!(g.dependencies(b), &[a]);
assert_eq!(g.dependencies(c), &[a]);
assert_eq!(g.dependents(a).len(), 2);
}
}