Skip to main content

tiny_diff/
lib.rs

1#![no_std]
2
3//! Keyed tree diffing with FIFO matching for repeated sibling keys.
4//!
5//! Paths identify nodes by child index from the root. Movement parents use new-tree
6//! paths, while movement pairs contain old and new child indices. Removals use old
7//! paths, additions and modifications use new paths. Removal order is unspecified.
8//! The caller applies the resulting diff and owns node lifetimes.
9
10extern crate alloc;
11#[cfg(feature = "std")]
12extern crate std;
13
14mod collections;
15
16use core::hash::Hash;
17
18use crate::collections::{
19    Box,
20    HashMap,
21    Vec,
22    VecDeque,
23};
24
25/// A tree node with stable sibling keys and index-based paths.
26/// Children must expose paths extending their parent's path by their child index.
27pub trait DiffNode: Sized {
28    type Key: Eq + Hash;
29    type Modifications;
30
31    /// Appends changes from `previous` to `self` into `diff`.
32    /// Matching roots must have paths of equal depth and corresponding parents.
33    #[cfg_attr(feature = "hotpath", hotpath::measure)]
34    fn diff(&self, previous: Option<&Self>, diff: &mut Diff<Self::Modifications>) {
35        diff_inner(self, previous, diff, false);
36    }
37
38    fn key(&self) -> &Self::Key;
39    fn path(&self) -> &[u32];
40    fn children(&self) -> &[Self];
41
42    /// Whether these nodes have compatible kinds, independently of their keys.
43    fn compatible(&self, previous: &Self) -> bool;
44
45    /// Returns changes for compatible nodes with matching keys, or `None` if unchanged.
46    fn modifications(&self, previous: &Self) -> Option<Self::Modifications>;
47
48    /// Checks application-specific sibling key rules before matching children in debug builds.
49    #[cfg(debug_assertions)]
50    fn validate_children(&self) {}
51}
52
53/// Changes between two trees, without applying them.
54#[derive(Debug)]
55pub struct Diff<Modifications> {
56    pub added: Vec<Box<[u32]>>,
57    pub modified: Vec<(Box<[u32]>, Modifications)>,
58    pub removed: Vec<Box<[u32]>>,
59    pub moved: HashMap<Box<[u32]>, Vec<(u32, u32)>>,
60}
61
62impl<Modifications> Default for Diff<Modifications> {
63    fn default() -> Self {
64        Self {
65            added: Vec::new(),
66            modified: Vec::new(),
67            removed: Vec::new(),
68            moved: HashMap::default(),
69        }
70    }
71}
72
73fn diff_inner<Node: DiffNode>(
74    current: &Node,
75    previous: Option<&Node>,
76    diff: &mut Diff<Node::Modifications>,
77    parent_removed: bool,
78) {
79    // Add new nodes and their descendants in preorder.
80    let Some(previous) = previous else {
81        diff.added.push(current.path().into());
82
83        for child in current.children() {
84            diff_inner(child, None, diff, false);
85        }
86
87        return;
88    };
89
90    // Replace incompatible node kinds without matching their descendants.
91    if !current.compatible(previous) {
92        diff.removed.push(previous.path().into());
93        diff_inner(current, None, diff, false);
94        return;
95    }
96
97    // Propagate replacement to matched descendants when a key or ancestor changes.
98    let removed_here = current.key() != previous.key() || parent_removed;
99    if removed_here {
100        diff.added.push(current.path().into());
101        diff.removed.push(previous.path().into());
102    } else {
103        if let Some(modifications) = current.modifications(previous) {
104            diff.modified.push((current.path().into(), modifications));
105        }
106
107        // Record index changes under the new parent path, independently of modifications.
108        if let (Some((&new_index, parent)), Some(&old_index)) =
109            (current.path().split_last(), previous.path().last())
110            && new_index != old_index
111        {
112            diff.moved
113                .entry(parent.into())
114                .or_default()
115                .push((old_index, new_index));
116        }
117    }
118
119    #[cfg(debug_assertions)]
120    current.validate_children();
121
122    let current_children = current.children();
123    let previous_children = previous.children();
124
125    // Match the common prefix positionally without building a key map.
126    let mut start = 0;
127    while start < current_children.len() && start < previous_children.len() {
128        if current_children[start].key() != previous_children[start].key() {
129            break;
130        }
131
132        diff_inner(
133            &current_children[start],
134            Some(&previous_children[start]),
135            diff,
136            removed_here,
137        );
138        start += 1;
139    }
140
141    if start < current_children.len() || start < previous_children.len() {
142        // Queue remaining old indices by key to preserve FIFO matching for duplicates.
143        let mut previous_keys = HashMap::<&Node::Key, VecDeque<usize>>::default();
144        for (index, child) in previous_children.iter().enumerate().skip(start) {
145            previous_keys
146                .entry(child.key())
147                .or_default()
148                .push_back(index);
149        }
150
151        // Reuse the earliest unmatched old sibling or add the new node and its descendants.
152        for child in &current_children[start..] {
153            if let Some(old_index) = previous_keys
154                .get_mut(child.key())
155                .and_then(VecDeque::pop_front)
156            {
157                diff_inner(
158                    child,
159                    Some(&previous_children[old_index]),
160                    diff,
161                    removed_here,
162                );
163            } else {
164                diff_inner(child, None, diff, false);
165            }
166        }
167
168        // Remove unmatched branch roots, leaving descendant cleanup to the caller.
169        for indices in previous_keys.values() {
170            for index in indices {
171                diff.removed.push(previous_children[*index].path().into());
172            }
173        }
174    }
175}