OXYGENHUB
# c07e0e7d2d85 oxygen / crates / oxy-core / src / rewrite.rs
13.3 KiB · Rust · last changed Rewrite propagation: describe and abandon on any change, dependents rewritten with translated references 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
pub fn propagate(state: &State, old: ChangeHash, new: Option<&Change>, corr: &Correspondence) -> Result<Vec<(ChangeHash, Change)>, RewriteError> {
69
    let old_change = state.get(&old).ok_or(RewriteError::Missing(old))?;
70
    let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new();
71
    mapping.insert(old, match new {
72
        Some(n) => Replacement::Change(n.hash()),
73
        None => Replacement::Removed(old_change.deps.clone()),
74
    });
75
    let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new();
76
    corrs.insert(old, corr.clone());
77
78
    // Dependents in topological order: every change whose closure has `old`,
79
    // sorted so that dependencies come first.
80
    let mut dependents: Vec<ChangeHash> = state.changes().filter(|(h, _)| **h != old && state.closure(h).contains(&old)).map(|(h, _)| *h).collect();
81
    dependents.sort_by_key(|h| (state.closure(h).len(), *h));
82
83
    let mut out = Vec::new();
84
    for h in dependents {
85
        let c = state.get(&h).ok_or(RewriteError::Missing(h))?;
86
        let rewritten = rewrite_one(&h, c, &mapping, &corrs)?;
87
        let rh = rewritten.hash();
88
        corrs.insert(h, Correspondence::identity(&rewritten));
89
        mapping.insert(h, Replacement::Change(rh));
90
        out.push((h, rewritten));
91
    }
92
    Ok(out)
93
}
94
95
fn rewrite_one(hash: &ChangeHash, c: &Change, mapping: &HashMap<ChangeHash, Replacement>, corrs: &HashMap<ChangeHash, Correspondence>) -> Result<Change, RewriteError> {
96
    let orphan = |what: String| RewriteError::Orphaned { change: *hash, what };
97
    let target = |change: &ChangeHash, what: &str| -> Result<Option<ChangeHash>, RewriteError> {
98
        match mapping.get(change) {
99
            Some(Replacement::Change(nh)) => Ok(Some(*nh)),
100
            Some(Replacement::Removed(_)) => Err(orphan(what.to_string())),
101
            None => Ok(None),
102
        }
103
    };
104
    let map_atom = |r: AtomRef| -> Result<AtomRef, RewriteError> {
105
        match r {
106
            AtomRef::Atom { change, index } => match target(&change, &format!("atom {index}"))? {
107
                Some(nh) => {
108
                    let idx = corrs[&change].atoms.get(&index).ok_or_else(|| orphan(format!("atom {index}")))?;
109
                    Ok(AtomRef::Atom { change: nh, index: *idx })
110
                }
111
                None => Ok(r),
112
            },
113
            other => Ok(other),
114
        }
115
    };
116
    let map_op = |r: OpRef| -> Result<OpRef, RewriteError> {
117
        match target(&r.change, &format!("tree operation {}", r.index))? {
118
            Some(nh) => {
119
                let idx = corrs[&r.change].ops.get(&r.index).ok_or_else(|| orphan(format!("tree operation {}", r.index)))?;
120
                Ok(OpRef { change: nh, index: *idx })
121
            }
122
            None => Ok(r),
123
        }
124
    };
125
    let mut out = c.clone();
126
    let mut deps: BTreeSet<ChangeHash> = BTreeSet::new();
127
    for d in &c.deps {
128
        match mapping.get(d) {
129
            Some(Replacement::Change(nh)) => {
130
                deps.insert(*nh);
131
            }
132
            Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()),
133
            None => {
134
                deps.insert(*d);
135
            }
136
        }
137
    }
138
    out.deps = deps.into_iter().collect();
139
    for op in &mut out.tree_ops {
140
        match op {
141
            TreeOp::RemoveNode { placement, .. } => *placement = map_op(*placement)?,
142
            TreeOp::Move { old, .. } => *old = map_op(*old)?,
143
            TreeOp::SetMode { old, .. } => *old = map_op(*old)?,
144
            TreeOp::AddNode { .. } => {}
145
        }
146
    }
147
    for e in &mut out.edits {
148
        match e {
149
            Edit::Insert { after, before, .. } => {
150
                *after = map_atom(*after)?;
151
                *before = map_atom(*before)?;
152
            }
153
            Edit::Delete { atoms, .. } => {
154
                let mut mapped: Vec<(ChangeHash, u32)> = Vec::new();
155
                for r in atoms.iter() {
156
                    for i in r.from..r.to {
157
                        match map_atom(AtomRef::Atom { change: r.change, index: i })? {
158
                            AtomRef::Atom { change, index } => mapped.push((change, index)),
159
                            _ => unreachable!(),
160
                        }
161
                    }
162
                }
163
                let mut ranges: Vec<AtomRange> = Vec::new();
164
                for (ch, i) in mapped {
165
                    match ranges.last_mut() {
166
                        Some(r) if r.change == ch && r.to == i => r.to = i + 1,
167
                        _ => ranges.push(AtomRange { change: ch, from: i, to: i + 1 }),
168
                    }
169
                }
170
                *atoms = ranges;
171
            }
172
            Edit::Replace { .. } => {}
173
        }
174
    }
175
    out.normalize();
176
    Ok(out)
177
}
178
179
#[cfg(test)]
180
mod tests {
181
    use super::*;
182
    use crate::hash::ObjectRef;
183
    use crate::ids::{ChangeId, NodeId};
184
    use crate::objects::{ChangeKind, Hlc, Mode, NodeKind};
185
    use crate::state::FileContent;
186
187
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change {
188
        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 };
189
        c.normalize();
190
        c
191
    }
192
193
    #[test]
194
    fn describe_propagates_through_dependents() {
195
        let mut st = State::new();
196
        let f = NodeId::random();
197
        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");
198
        let h1 = st.add(c1.clone()).unwrap();
199
        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");
200
        let h2 = st.add(c2.clone()).unwrap();
201
        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");
202
        let h3 = st.add(c3.clone()).unwrap();
203
        let before = st.materialize();
204
205
        // Rewrite c1's message; everything downstream follows.
206
        let mut n1 = c1.clone();
207
        n1.message = "one, described".into();
208
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1)).unwrap();
209
        assert_eq!(rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), vec![h2, h3]);
210
        let mut st2 = State::new();
211
        st2.add(n1.clone()).unwrap();
212
        for (_, c) in &rewritten {
213
            st2.add(c.clone()).unwrap();
214
        }
215
        assert_eq!(st2.len(), 3);
216
        let after = st2.materialize();
217
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
218
        match (&after.files["f"].content, &before.files["f"].content) {
219
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => assert_eq!(a, b),
220
            _ => panic!(),
221
        }
222
        assert!(matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n"));
223
        // ChangeIds survive; hashes change; deps point at the new hashes.
224
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
225
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
226
        assert_ne!(rewritten[0].1.hash(), h2);
227
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
228
    }
229
230
    #[test]
231
    fn missing_counterpart_is_reported() {
232
        let mut st = State::new();
233
        let f = NodeId::random();
234
        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");
235
        let h1 = st.add(c1.clone()).unwrap();
236
        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");
237
        let h2 = st.add(c2).unwrap();
238
        // The rewritten c1 drops line "b": atom 1 has no counterpart.
239
        let mut n1 = c1.clone();
240
        n1.edits = vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec()] }];
241
        let corr = Correspondence { atoms: [(0, 0)].into_iter().collect(), ops: [(0, 0)].into_iter().collect() };
242
        let err = propagate(&st, h1, Some(&n1), &corr).unwrap_err();
243
        assert_eq!(err, RewriteError::Orphaned { change: h2, what: "atom 1".into() });
244
        assert_eq!(propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr).unwrap_err(), RewriteError::Missing(ObjectRef([9; 32])));
245
    }
246
247
    #[test]
248
    fn removal_rewires_dependencies_or_fails_on_references() {
249
        let mut st = State::new();
250
        let f = NodeId::random();
251
        let g = NodeId::random();
252
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![], "one");
253
        let h1 = st.add(c1).unwrap();
254
        // c2 merely depends on c1 explicitly (touches another file).
255
        let c2 = change(vec![h1], vec![TreeOp::AddNode { node: g, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "g".into() }], vec![], "two");
256
        let h2 = st.add(c2.clone()).unwrap();
257
        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");
258
        let h3 = st.add(c3).unwrap();
259
        // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1.
260
        let out = propagate(&st, h2, None, &Correspondence::default()).unwrap();
261
        assert_eq!(out.len(), 1);
262
        assert_eq!(out[0].0, h3);
263
        assert_eq!(out[0].1.deps, vec![h1]);
264
        // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but
265
        // nothing else does either, so it succeeds and c2 inherits no deps.
266
        let out = propagate(&st, h1, None, &Correspondence::default()).unwrap();
267
        assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty()));
268
        // Removing c2 when c3 references its placement fails.
269
        let c4 = change(vec![h2], vec![TreeOp::RemoveNode { node: g, placement: OpRef { change: h2, index: 0 } }], vec![], "four");
270
        let h4 = st.add(c4).unwrap();
271
        let err = propagate(&st, h2, None, &Correspondence::default()).unwrap_err();
272
        assert!(matches!(err, RewriteError::Orphaned { change, .. } if change == h4 || change == h3), "{err:?}");
273
    }
274
}