OXYGENHUB
# 252e8cb23fbc oxygen / crates / oxy-core / src / rewrite.rs
14.1 KiB · Rust · last changed Checkpoints: serialized states per bookmark, automatic every core.checkpoint-interval changes, oxy checkpoint, checkpoint-accelerated load_state; states keep change metadata only 1 month ago source blame history raw
1
//! Rewrite propagation (spec §8.4): when change `old` is replaced by `new`,
2
//! every change that depends on `old` is rewritten with its references
3
//! translated through a correspondence from `old`'s atoms and tree operations
4
//! to `new`'s. Rewritten changes keep their ChangeIds. Where a reference has
5
//! no counterpart, the rewrite fails with the orphaned reference named;
6
//! turning that into a reified conflict is future work.
7
8
use crate::hash::ChangeHash;
9
use crate::objects::{AtomRange, AtomRef, Change, Edit, OpRef, TreeOp};
10
use crate::state::State;
11
use std::collections::{BTreeSet, HashMap};
12
13
/// How `old`'s atoms and tree operations map onto `new`'s.
14
#[derive(Clone, Debug, Default, PartialEq, Eq)]
15
pub struct Correspondence {
16
    /// Old atom index → new atom index, per node the old change edited.
17
    /// Absent means the atom no longer exists.
18
    pub atoms: HashMap<u32, u32>,
19
    /// Old tree-op index → new tree-op index.
20
    pub ops: HashMap<u32, u32>,
21
}
22
23
impl Correspondence {
24
    /// Every atom and operation keeps its index: `new` has the same edits as `old`.
25
    pub fn identity(change: &Change) -> Correspondence {
26
        let mut atoms = HashMap::new();
27
        let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
28
        for e in &change.edits {
29
            if let Edit::Insert { node, lines, .. } = e {
30
                let idx = per_node.entry(*node).or_default();
31
                for _ in lines {
32
                    atoms.insert(*idx, *idx);
33
                    *idx += 1;
34
                }
35
            }
36
        }
37
        // Atom indices are per node, so the identity map is index → index for
38
        // the largest index any node uses.
39
        let max = per_node.values().copied().max().unwrap_or(0);
40
        for i in 0..max {
41
            atoms.insert(i, i);
42
        }
43
        let ops = (0..change.tree_ops.len() as u32).map(|i| (i, i)).collect();
44
        Correspondence { atoms, ops }
45
    }
46
}
47
48
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
49
pub enum RewriteError {
50
    #[error("change {change} references {what} of the rewritten change, which no longer exists")]
51
    Orphaned { change: ChangeHash, what: String },
52
    #[error("change {0} is not in the state")]
53
    Missing(ChangeHash),
54
}
55
56
/// What a rewritten change was replaced by.
57
#[derive(Clone, Debug)]
58
enum Replacement {
59
    Change(ChangeHash),
60
    /// Removed: dependents inherit these dependencies instead.
61
    Removed(Vec<ChangeHash>),
62
}
63
64
/// Rewrite every change in `state` that depends on `old`, given `new` (or
65
/// `None` when `old` is removed) and the correspondence between them.
66
/// Returns `(old hash, rewritten change)` pairs in dependency order, not
67
/// including `old` → `new` itself.
68
///
69
/// `load` fetches a change's full object (a state keeps only metadata).
70
pub fn propagate(state: &State, old: ChangeHash, new: Option<&Change>, corr: &Correspondence, load: &dyn Fn(&ChangeHash) -> Option<Change>) -> Result<Vec<(ChangeHash, Change)>, RewriteError> {
71
    let old_change = state.get(&old).ok_or(RewriteError::Missing(old))?;
72
    let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new();
73
    mapping.insert(old, match new {
74
        Some(n) => Replacement::Change(n.hash()),
75
        None => Replacement::Removed(old_change.deps.clone()),
76
    });
77
    let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new();
78
    corrs.insert(old, corr.clone());
79
80
    // Dependents in topological order: every change whose closure has `old`,
81
    // sorted so that dependencies come first.
82
    let mut dependents: Vec<ChangeHash> = state.changes().filter(|(h, _)| **h != old && state.closure(h).contains(&old)).map(|(h, _)| *h).collect();
83
    dependents.sort_by_key(|h| (state.closure(h).len(), *h));
84
85
    let mut out = Vec::new();
86
    for h in dependents {
87
        let c = load(&h).ok_or(RewriteError::Missing(h))?;
88
        let rewritten = rewrite_one(&h, &c, &mapping, &corrs)?;
89
        let rh = rewritten.hash();
90
        corrs.insert(h, Correspondence::identity(&rewritten));
91
        mapping.insert(h, Replacement::Change(rh));
92
        out.push((h, rewritten));
93
    }
94
    Ok(out)
95
}
96
97
fn rewrite_one(hash: &ChangeHash, c: &Change, mapping: &HashMap<ChangeHash, Replacement>, corrs: &HashMap<ChangeHash, Correspondence>) -> Result<Change, RewriteError> {
98
    let orphan = |what: String| RewriteError::Orphaned { change: *hash, what };
99
    let target = |change: &ChangeHash, what: &str| -> Result<Option<ChangeHash>, RewriteError> {
100
        match mapping.get(change) {
101
            Some(Replacement::Change(nh)) => Ok(Some(*nh)),
102
            Some(Replacement::Removed(_)) => Err(orphan(what.to_string())),
103
            None => Ok(None),
104
        }
105
    };
106
    let map_atom = |r: AtomRef| -> Result<AtomRef, RewriteError> {
107
        match r {
108
            AtomRef::Atom { change, index } => match target(&change, &format!("atom {index}"))? {
109
                Some(nh) => {
110
                    let idx = corrs[&change].atoms.get(&index).ok_or_else(|| orphan(format!("atom {index}")))?;
111
                    Ok(AtomRef::Atom { change: nh, index: *idx })
112
                }
113
                None => Ok(r),
114
            },
115
            other => Ok(other),
116
        }
117
    };
118
    let map_op = |r: OpRef| -> Result<OpRef, RewriteError> {
119
        match target(&r.change, &format!("tree operation {}", r.index))? {
120
            Some(nh) => {
121
                let idx = corrs[&r.change].ops.get(&r.index).ok_or_else(|| orphan(format!("tree operation {}", r.index)))?;
122
                Ok(OpRef { change: nh, index: *idx })
123
            }
124
            None => Ok(r),
125
        }
126
    };
127
    let mut out = c.clone();
128
    let mut deps: BTreeSet<ChangeHash> = BTreeSet::new();
129
    for d in &c.deps {
130
        match mapping.get(d) {
131
            Some(Replacement::Change(nh)) => {
132
                deps.insert(*nh);
133
            }
134
            Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()),
135
            None => {
136
                deps.insert(*d);
137
            }
138
        }
139
    }
140
    out.deps = deps.into_iter().collect();
141
    for op in &mut out.tree_ops {
142
        match op {
143
            TreeOp::RemoveNode { placement, .. } => *placement = map_op(*placement)?,
144
            TreeOp::Move { old, .. } => *old = map_op(*old)?,
145
            TreeOp::SetMode { old, .. } => *old = map_op(*old)?,
146
            TreeOp::AddNode { .. } => {}
147
        }
148
    }
149
    for e in &mut out.edits {
150
        match e {
151
            Edit::Insert { after, before, .. } => {
152
                *after = map_atom(*after)?;
153
                *before = map_atom(*before)?;
154
            }
155
            Edit::Delete { atoms, .. } => {
156
                let mut mapped: Vec<(ChangeHash, u32)> = Vec::new();
157
                for r in atoms.iter() {
158
                    for i in r.from..r.to {
159
                        match map_atom(AtomRef::Atom { change: r.change, index: i })? {
160
                            AtomRef::Atom { change, index } => mapped.push((change, index)),
161
                            _ => unreachable!(),
162
                        }
163
                    }
164
                }
165
                let mut ranges: Vec<AtomRange> = Vec::new();
166
                for (ch, i) in mapped {
167
                    match ranges.last_mut() {
168
                        Some(r) if r.change == ch && r.to == i => r.to = i + 1,
169
                        _ => ranges.push(AtomRange { change: ch, from: i, to: i + 1 }),
170
                    }
171
                }
172
                *atoms = ranges;
173
            }
174
            Edit::Replace { .. } => {}
175
        }
176
    }
177
    out.normalize();
178
    Ok(out)
179
}
180
181
#[cfg(test)]
182
mod tests {
183
    use super::*;
184
    use crate::hash::ObjectRef;
185
    use crate::ids::{ChangeId, NodeId};
186
    use crate::objects::{ChangeKind, Hlc, Mode, NodeKind};
187
    use crate::state::FileContent;
188
189
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change {
190
        let mut c = Change { change_id: ChangeId::random(), deps, author: ObjectRef::ZERO, hlc: Hlc { wall_ms: 1, logical: 0 }, message: msg.into(), tree_ops, edits, kind: ChangeKind::Normal };
191
        c.normalize();
192
        c
193
    }
194
195
    #[test]
196
    fn describe_propagates_through_dependents() {
197
        let mut st = State::new();
198
        let f = NodeId::random();
199
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one");
200
        let h1 = st.add(c1.clone()).unwrap();
201
        let c2 = change(vec![h1], vec![], vec![Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }, Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two");
202
        let h2 = st.add(c2.clone()).unwrap();
203
        let c3 = change(vec![h2], vec![TreeOp::SetMode { node: f, old: OpRef { change: h1, index: 0 }, mode: Mode { exec: true, opaque: false } }], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h2, index: 0 }, before: AtomRef::End, lines: vec![b"d\n".to_vec()] }], "three");
204
        let h3 = st.add(c3.clone()).unwrap();
205
        let before = st.materialize();
206
207
        // Rewrite c1's message; everything downstream follows.
208
        let mut n1 = c1.clone();
209
        n1.message = "one, described".into();
210
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())].into_iter().collect();
211
        let load = |h: &ChangeHash| all.get(h).cloned();
212
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load).unwrap();
213
        assert_eq!(rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), vec![h2, h3]);
214
        let mut st2 = State::new();
215
        st2.add(n1.clone()).unwrap();
216
        for (_, c) in &rewritten {
217
            st2.add(c.clone()).unwrap();
218
        }
219
        assert_eq!(st2.len(), 3);
220
        let after = st2.materialize();
221
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
222
        match (&after.files["f"].content, &before.files["f"].content) {
223
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => assert_eq!(a, b),
224
            _ => panic!(),
225
        }
226
        assert!(matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n"));
227
        // ChangeIds survive; hashes change; deps point at the new hashes.
228
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
229
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
230
        assert_ne!(rewritten[0].1.hash(), h2);
231
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
232
    }
233
234
    #[test]
235
    fn missing_counterpart_is_reported() {
236
        let mut st = State::new();
237
        let f = NodeId::random();
238
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one");
239
        let h1 = st.add(c1.clone()).unwrap();
240
        let c2 = change(vec![h1], vec![], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two");
241
        let h2 = st.add(c2.clone()).unwrap();
242
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect();
243
        let load = |h: &ChangeHash| all.get(h).cloned();
244
        // The rewritten c1 drops line "b": atom 1 has no counterpart.
245
        let mut n1 = c1.clone();
246
        n1.edits = vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec()] }];
247
        let corr = Correspondence { atoms: [(0, 0)].into_iter().collect(), ops: [(0, 0)].into_iter().collect() };
248
        let err = propagate(&st, h1, Some(&n1), &corr, &load).unwrap_err();
249
        assert_eq!(err, RewriteError::Orphaned { change: h2, what: "atom 1".into() });
250
        assert_eq!(propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(), RewriteError::Missing(ObjectRef([9; 32])));
251
    }
252
253
    #[test]
254
    fn removal_rewires_dependencies_or_fails_on_references() {
255
        let mut st = State::new();
256
        let f = NodeId::random();
257
        let g = NodeId::random();
258
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![], "one");
259
        let h1 = st.add(c1.clone()).unwrap();
260
        // c2 merely depends on c1 explicitly (touches another file).
261
        let c2 = change(vec![h1], vec![TreeOp::AddNode { node: g, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "g".into() }], vec![], "two");
262
        let h2 = st.add(c2.clone()).unwrap();
263
        let c3 = change(vec![h2], vec![], vec![Edit::Insert { node: g, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }], "three");
264
        let h3 = st.add(c3.clone()).unwrap();
265
        let mut all: HashMap<ChangeHash, Change> = [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect();
266
        let load_all = all.clone();
267
        let load = move |h: &ChangeHash| load_all.get(h).cloned();
268
        // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1.
269
        let out = propagate(&st, h2, None, &Correspondence::default(), &load).unwrap();
270
        assert_eq!(out.len(), 1);
271
        assert_eq!(out[0].0, h3);
272
        assert_eq!(out[0].1.deps, vec![h1]);
273
        // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but
274
        // nothing else does either, so it succeeds and c2 inherits no deps.
275
        let out = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap();
276
        assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty()));
277
        // Removing c2 when c3 references its placement fails.
278
        let c4 = change(vec![h2], vec![TreeOp::RemoveNode { node: g, placement: OpRef { change: h2, index: 0 } }], vec![], "four");
279
        let h4 = st.add(c4.clone()).unwrap();
280
        all.insert(h4, c4);
281
        let load2 = |h: &ChangeHash| all.get(h).cloned();
282
        let err = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap_err();
283
        assert!(matches!(err, RewriteError::Orphaned { change, .. } if change == h4 || change == h3), "{err:?}");
284
    }
285
}