use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use crate::Context;
use crate::cell_family::SourceMap;
use crate::cell_tree::SourceTree;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffOp<K, V> {
Insert { key: K, value: V, index: usize },
Remove { key: K },
Move { key: K, to: usize },
Update { key: K, value: V },
}
pub fn reconcile<K, V>(old: &[(K, V)], new: &[(K, V)]) -> Vec<DiffOp<K, V>>
where
K: Eq + Hash + Clone,
V: Clone + PartialEq,
{
let old_pos: HashMap<&K, usize> = old.iter().enumerate().map(|(i, (k, _))| (k, i)).collect();
let old_val: HashMap<&K, &V> = old.iter().map(|(k, v)| (k, v)).collect();
let new_keys: HashSet<&K> = new.iter().map(|(k, _)| k).collect();
let mut ops = Vec::new();
for (k, _) in old {
if !new_keys.contains(k) {
ops.push(DiffOp::Remove { key: k.clone() });
}
}
let mut common_new_idx: Vec<usize> = Vec::new();
let mut seq: Vec<usize> = Vec::new();
for (i, (k, _)) in new.iter().enumerate() {
if let Some(&oi) = old_pos.get(k) {
common_new_idx.push(i);
seq.push(oi);
}
}
let stable: HashSet<usize> = longest_increasing_subsequence(&seq)
.into_iter()
.map(|j| common_new_idx[j])
.collect();
for (i, (k, v)) in new.iter().enumerate() {
if old_pos.contains_key(k) {
if !stable.contains(&i) {
ops.push(DiffOp::Move {
key: k.clone(),
to: i,
});
}
} else {
ops.push(DiffOp::Insert {
key: k.clone(),
value: v.clone(),
index: i,
});
}
}
for (k, v) in new {
if let Some(&ov) = old_val.get(k)
&& ov != v
{
ops.push(DiffOp::Update {
key: k.clone(),
value: v.clone(),
});
}
}
ops
}
pub fn apply_to_map<K, V>(ctx: &Context, map: &SourceMap<K, V>, ops: &[DiffOp<K, V>])
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
for op in ops {
match op {
DiffOp::Remove { key } => {
map.remove(ctx, key);
}
DiffOp::Insert { key, value, index } => {
map.entry(ctx, key.clone(), value.clone());
map.move_to(ctx, key, *index);
}
DiffOp::Move { key, to } => {
map.move_to(ctx, key, *to);
}
DiffOp::Update { key, value } => {
map.set(ctx, key.clone(), value.clone());
}
}
}
}
pub fn apply_to_tree<K, V>(ctx: &Context, tree: &SourceTree<K, V>, ops: &[DiffOp<K, V>])
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
for op in ops {
match op {
DiffOp::Remove { key } => {
tree.remove_child(ctx, key);
}
DiffOp::Insert { key, value, index } => {
tree.insert_child(ctx, key.clone(), value.clone());
tree.move_child(ctx, key, *index);
}
DiffOp::Move { key, to } => {
tree.move_child(ctx, key, *to);
}
DiffOp::Update { key, value } => {
if let Some(child) = tree.child(key) {
child.set(ctx, value.clone());
}
}
}
}
}
impl<K, V> SourceMap<K, V>
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
pub fn reconcile(&self, ctx: &Context, new: &[(K, V)]) {
let old: Vec<(K, V)> = self
.keys(ctx)
.into_iter()
.filter_map(|k| self.get(ctx, &k).map(|v| (k, v)))
.collect();
let ops = reconcile(&old, new);
apply_to_map(ctx, self, &ops);
}
}
fn longest_increasing_subsequence(seq: &[usize]) -> Vec<usize> {
let n = seq.len();
if n == 0 {
return Vec::new();
}
let mut tails: Vec<usize> = Vec::new();
let mut prev: Vec<usize> = vec![usize::MAX; n];
for i in 0..n {
let mut lo = 0;
let mut hi = tails.len();
while lo < hi {
let mid = (lo + hi) / 2;
if seq[tails[mid]] < seq[i] {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo > 0 {
prev[i] = tails[lo - 1];
}
if lo == tails.len() {
tails.push(i);
} else {
tails[lo] = i;
}
}
let mut res = Vec::new();
let mut k = *tails.last().unwrap();
loop {
res.push(k);
if prev[k] == usize::MAX {
break;
}
k = prev[k];
}
res.reverse();
res
}
#[cfg(test)]
mod tests {
use super::*;
fn apply_ref<K: Eq + Hash + Clone, V: Clone>(
old: &[(K, V)],
ops: &[DiffOp<K, V>],
) -> Vec<(K, V)> {
let mut list: Vec<(K, V)> = old.to_vec();
for op in ops {
match op {
DiffOp::Remove { key } => list.retain(|(k, _)| k != key),
DiffOp::Insert { key, value, index } => {
list.retain(|(k, _)| k != key);
let i = (*index).min(list.len());
list.insert(i, (key.clone(), value.clone()));
}
DiffOp::Move { key, to } => {
if let Some(from) = list.iter().position(|(k, _)| k == key) {
let item = list.remove(from);
let i = (*to).min(list.len());
list.insert(i, item);
}
}
DiffOp::Update { key, value } => {
if let Some(slot) = list.iter_mut().find(|(k, _)| k == key) {
slot.1 = value.clone();
}
}
}
}
list
}
#[test]
fn lis_basic() {
assert_eq!(longest_increasing_subsequence(&[]), Vec::<usize>::new());
assert_eq!(longest_increasing_subsequence(&[2, 0, 1]), vec![1, 2]);
assert_eq!(
longest_increasing_subsequence(&[0, 1, 2, 3]),
vec![0, 1, 2, 3]
);
}
#[test]
fn pure_reorder_emits_only_minimal_moves() {
let old = [("a", 1), ("b", 2), ("c", 3), ("d", 4)];
let new = [("a", 1), ("c", 3), ("b", 2), ("d", 4)];
let ops = reconcile(&old, &new);
assert!(
ops.iter().all(|op| matches!(op, DiffOp::Move { .. })),
"pure reorder must be moves only: {ops:?}"
);
assert_eq!(ops.len(), 1);
assert_eq!(apply_ref(&old, &ops), new.to_vec());
}
#[test]
fn insert_remove_update_combined() {
let old = [("a", 1), ("b", 2), ("c", 3)];
let new = [("c", 3), ("a", 9), ("d", 4)];
let ops = reconcile(&old, &new);
assert!(
ops.iter()
.any(|o| matches!(o, DiffOp::Remove { key } if *key == "b"))
);
assert!(
ops.iter().any(
|o| matches!(o, DiffOp::Insert { key, value, .. } if *key == "d" && *value == 4)
)
);
assert!(
ops.iter()
.any(|o| matches!(o, DiffOp::Update { key, value } if *key == "a" && *value == 9))
);
assert_eq!(apply_ref(&old, &ops), new.to_vec());
}
#[test]
fn full_reversal_is_minimal() {
let old = [(1, 0), (2, 0), (3, 0), (4, 0)];
let new = [(4, 0), (3, 0), (2, 0), (1, 0)];
let ops = reconcile(&old, &new);
let moves = ops
.iter()
.filter(|o| matches!(o, DiffOp::Move { .. }))
.count();
assert_eq!(moves, 3);
assert_eq!(apply_ref(&old, &ops), new.to_vec());
}
#[test]
fn apply_to_map_converges_and_spares_stable_value_cell() {
let ctx = Context::new();
let map: SourceMap<&str, i32> = SourceMap::new(&ctx);
for (k, v) in [("a", 1), ("b", 2), ("c", 3)] {
map.entry(&ctx, k, v);
}
let a_view = ctx.computed({
let map = map.clone();
move |ctx| map.get(ctx, &"a").unwrap_or(0) * 100
});
assert_eq!(ctx.get(&a_view), 100);
map.reconcile(&ctx, &[("a", 1), ("c", 3), ("b", 2)]);
assert_eq!(map.keys(&ctx), vec!["a", "c", "b"]);
assert!(
ctx.is_set(&a_view),
"stable entry's value cell must not be invalidated by a sibling reorder"
);
assert_eq!(ctx.get(&a_view), 100);
}
#[test]
fn apply_to_map_handles_inserts_removes_updates() {
let ctx = Context::new();
let map: SourceMap<&str, i32> = SourceMap::new(&ctx);
for (k, v) in [("a", 1), ("b", 2), ("c", 3)] {
map.entry(&ctx, k, v);
}
let a = map.handle(&"a").unwrap();
map.reconcile(&ctx, &[("c", 3), ("a", 9), ("d", 4)]);
assert_eq!(map.keys(&ctx), vec!["c", "a", "d"]);
assert_eq!(map.get(&ctx, &"a"), Some(9));
assert_eq!(map.get(&ctx, &"d"), Some(4));
assert!(!map.contains_key(&ctx, &"b"));
assert_eq!(map.handle(&"a").unwrap().id, a.id);
}
#[test]
fn apply_to_tree_round_trips_reconcile_ops() {
let ctx = Context::new();
let root = SourceTree::leaf(&ctx, "root", 0);
for (k, v) in [("a", 1), ("b", 2), ("c", 3)] {
root.insert_child(&ctx, k, v);
}
let a = root.child(&"a").unwrap();
a.insert_child(&ctx, "a1", 100);
let old = [("a", 1), ("b", 2), ("c", 3)];
let new = [("c", 3), ("a", 9), ("d", 4)];
let ops = reconcile(&old, &new);
apply_to_tree(&ctx, &root, &ops);
assert_eq!(root.child_ids(&ctx), vec!["c", "a", "d"]);
assert_eq!(root.child(&"a").unwrap().get(&ctx), 9);
assert_eq!(root.child(&"d").unwrap().get(&ctx), 4);
assert!(root.child(&"b").is_none());
assert_eq!(root.child(&"a").unwrap().child_ids(&ctx), vec!["a1"]);
}
#[test]
fn apply_to_tree_pure_move_spares_child_value_reader() {
let ctx = Context::new();
let root = SourceTree::leaf(&ctx, "root", 0);
for (k, v) in [("a", 1), ("b", 2), ("c", 3)] {
root.insert_child(&ctx, k, v);
}
let a = root.child(&"a").unwrap();
let a_view = ctx.computed({
let a = a.clone();
move |ctx| a.get(ctx) * 10
});
assert_eq!(ctx.get(&a_view), 10);
let ops = reconcile(
&[("a", 1), ("b", 2), ("c", 3)],
&[("a", 1), ("c", 3), ("b", 2)],
);
apply_to_tree(&ctx, &root, &ops);
assert_eq!(root.child_ids(&ctx), vec!["a", "c", "b"]);
assert!(
ctx.is_set(&a_view),
"stable child value must not be invalidated by sibling moves"
);
assert_eq!(ctx.get(&a_view), 10);
}
}