OXYGENHUB
◆ v0.8.0 oxygen / crates / oxy-core / src / graph.rs
66.5 KiB · Rust · last changed oxy resolve --both keeps both sides of an adjacent insertion (closes #121) 9 days ago source blame history raw
1
//! The per-file atom graph (spec §7.3), its materialization, and text conflict
2
//! detection (spec §9.1, §9.5).
3
//!
4
//! The graph is a pure function of the set of changes applied to it: inserts
5
//! add vertices and edges, deletes set a flag, and nothing is ever removed.
6
7
use crate::fasthash::{FastMap as HashMap, FastSet as HashSet};
8
use crate::hash::ChangeHash;
9
use crate::lines::{LineRef, Lines};
10
use crate::objects::AtomRange;
11
use std::collections::{BTreeMap, BTreeSet};
12
13
/// `(introducing change, index within that change for this node)`.
14
pub type AtomKey = (ChangeHash, u32);
15
16
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
17
pub enum Vertex {
18
    Start,
19
    Atom(AtomKey),
20
    End,
21
}
22
23
/// Transitive dependency test between changes; `depends_on(a, a)` is true.
24
pub trait DepOracle {
25
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool;
26
}
27
28
/// Another oracle's answers, each asked once: rendering one file asks about
29
/// the same few pairs over and over (a change deleting a block of lines
30
/// another inserted), and in a long history each answer is a walk through
31
/// thousands of changes (oxygen#96).
32
struct MemoDeps<'a> {
33
    inner: &'a dyn DepOracle,
34
    known: std::cell::RefCell<crate::fasthash::FastMap<(ChangeHash, ChangeHash), bool>>,
35
}
36
37
impl DepOracle for MemoDeps<'_> {
38
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
39
        if let Some(v) = self.known.borrow().get(&(*a, *b)) {
40
            return *v;
41
        }
42
        let v = self.inner.depends_on(a, b);
43
        self.known.borrow_mut().insert((*a, *b), v);
44
        v
45
    }
46
}
47
48
/// No change depends on any other: every pair commutes.
49
pub struct NoDeps;
50
impl DepOracle for NoDeps {
51
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
52
        a == b
53
    }
54
}
55
56
#[derive(Default, Clone, Debug)]
57
struct AtomInfo {
58
    /// `None` until the introducing change has been applied; a line of the
59
    /// insert's shared buffer.
60
    content: Option<LineRef>,
61
    deleted_by: BTreeSet<ChangeHash>,
62
}
63
64
#[derive(Clone, Debug)]
65
pub struct FileGraph {
66
    atoms: HashMap<AtomKey, AtomInfo>,
67
    /// Every vertex, with its direct successors.
68
    succ: HashMap<Vertex, Edges>,
69
    /// The live atoms' order, once known to be the graph's only one.
70
    order: OrderSlot,
71
}
72
73
/// A file graph's live atoms in order, when the graph orders them one way
74
/// only: found by [`FileGraph::materialize`] and brought up to date by
75
/// [`Order::splice`] as a change's edits apply, so a long-lived file
76
/// renders in time that follows its live lines rather than every line it
77
/// ever had. Any other change to the graph drops it.
78
#[derive(Debug)]
79
pub struct Order {
80
    atoms: Vec<AtomKey>,
81
    /// Atoms deleted since the order was last confirmed: it holds while
82
    /// none of them is a ghost (spec §9.1), which only a render can ask,
83
    /// since it needs to know how the changes depend on each other.
84
    unchecked: Vec<AtomKey>,
85
    confirmed: std::sync::atomic::AtomicBool,
86
}
87
88
/// Holds a graph's [`Order`]; filled in by a render, which only reads the
89
/// graph.
90
#[derive(Debug, Default)]
91
struct OrderSlot(std::sync::Mutex<Option<std::sync::Arc<Order>>>);
92
93
impl OrderSlot {
94
    fn get(&self) -> Option<std::sync::Arc<Order>> {
95
        self.0.lock().unwrap_or_else(|e| e.into_inner()).clone()
96
    }
97
    fn set(&self, o: Option<std::sync::Arc<Order>>) {
98
        *self.0.lock().unwrap_or_else(|e| e.into_inner()) = o;
99
    }
100
    fn take(&mut self) -> Option<std::sync::Arc<Order>> {
101
        self.0.get_mut().unwrap_or_else(|e| e.into_inner()).take()
102
    }
103
}
104
105
impl Clone for OrderSlot {
106
    fn clone(&self) -> Self {
107
        OrderSlot(std::sync::Mutex::new(self.get()))
108
    }
109
}
110
111
impl Order {
112
    fn known(atoms: Vec<AtomKey>) -> Order {
113
        Order {
114
            atoms,
115
            unchecked: Vec::new(),
116
            confirmed: std::sync::atomic::AtomicBool::new(true),
117
        }
118
    }
119
120
    /// The order after change `h` inserts the runs `inserts` (after,
121
    /// before, first index, count) and deletes `deleted`, as its edits
122
    /// apply; `None` where that is not certain to be the graph's only
123
    /// order: an anchor not in the order, live lines between an insert's
124
    /// anchors, or two inserts with nothing kept between them.
125
    fn splice(
126
        &self,
127
        h: ChangeHash,
128
        inserts: &[(Vertex, Vertex, u32, u32)],
129
        deleted: &[AtomKey],
130
    ) -> Option<std::sync::Arc<Order>> {
131
        let mut want: HashSet<AtomKey> = deleted.iter().copied().collect();
132
        for (a, b, _, _) in inserts {
133
            for v in [a, b] {
134
                if let Vertex::Atom(k) = v {
135
                    want.insert(*k);
136
                }
137
            }
138
        }
139
        let mut pos: HashMap<AtomKey, usize> = HashMap::default();
140
        for (i, k) in self.atoms.iter().enumerate() {
141
            if want.contains(k) {
142
                pos.insert(*k, i);
143
            }
144
        }
145
        let len = self.atoms.len();
146
        let mut gone = vec![false; len];
147
        let mut unchecked = if self.confirmed.load(std::sync::atomic::Ordering::Relaxed) {
148
            Vec::new()
149
        } else {
150
            self.unchecked.clone()
151
        };
152
        for k in deleted {
153
            if k.0 == h {
154
                return None;
155
            }
156
            if let Some(&i) = pos.get(k) {
157
                if !gone[i] {
158
                    gone[i] = true;
159
                    unchecked.push(*k);
160
                }
161
            }
162
        }
163
        // Each run goes in the gap its anchors leave (slot `i`: before
164
        // `atoms[i]`), which must hold nothing that stays.
165
        let mut slots: Vec<(usize, usize, u32, u32)> = Vec::with_capacity(inserts.len());
166
        for &(a, b, first, count) in inserts {
167
            let from = match a {
168
                Vertex::Start => 0,
169
                Vertex::Atom(k) => pos.get(&k)? + 1,
170
                Vertex::End => return None,
171
            };
172
            let to = match b {
173
                Vertex::End => len,
174
                Vertex::Atom(k) => *pos.get(&k)?,
175
                Vertex::Start => return None,
176
            };
177
            if from > to || gone[from..to].iter().any(|g| !g) {
178
                return None;
179
            }
180
            slots.push((from, to, first, count));
181
        }
182
        // Runs one after another need a kept line between them, or the
183
        // graph would not say which comes first.
184
        slots.sort_unstable();
185
        for w in slots.windows(2) {
186
            let (end, next) = (w[0].1, w[1].0);
187
            if end >= next || !gone[end..next].iter().any(|g| !g) {
188
                return None;
189
            }
190
        }
191
        let added: usize = slots.iter().map(|s| s.3 as usize).sum();
192
        let mut atoms = Vec::with_capacity(len + added);
193
        let mut slots = slots.into_iter().peekable();
194
        let mut runs_at = |i: usize, atoms: &mut Vec<AtomKey>| {
195
            while let Some(&(from, _, first, count)) = slots.peek() {
196
                if from != i {
197
                    break;
198
                }
199
                atoms.extend((first..first + count).map(|j| (h, j)));
200
                slots.next();
201
            }
202
        };
203
        for (i, (k, g)) in self.atoms.iter().zip(&gone).enumerate() {
204
            runs_at(i, &mut atoms);
205
            if !g {
206
                atoms.push(*k);
207
            }
208
        }
209
        runs_at(len, &mut atoms);
210
        let confirmed = unchecked.is_empty();
211
        Some(std::sync::Arc::new(Order {
212
            atoms,
213
            unchecked,
214
            confirmed: std::sync::atomic::AtomicBool::new(confirmed),
215
        }))
216
    }
217
}
218
219
/// A vertex's direct successors, sorted and without repeats. Nearly every
220
/// vertex has one, which is kept inline without an allocation.
221
#[derive(Clone, Debug, Default, PartialEq, Eq)]
222
enum Edges {
223
    #[default]
224
    None,
225
    One(Vertex),
226
    Many(Vec<Vertex>),
227
}
228
229
impl Edges {
230
    fn insert(&mut self, v: Vertex) {
231
        match self {
232
            Edges::None => *self = Edges::One(v),
233
            Edges::One(a) if *a == v => {}
234
            Edges::One(a) => {
235
                let a = *a;
236
                *self = Edges::Many(if a < v { vec![a, v] } else { vec![v, a] });
237
            }
238
            Edges::Many(list) => {
239
                if let Err(i) = list.binary_search(&v) {
240
                    list.insert(i, v);
241
                }
242
            }
243
        }
244
    }
245
246
    fn as_slice(&self) -> &[Vertex] {
247
        match self {
248
            Edges::None => &[],
249
            Edges::One(v) => std::slice::from_ref(v),
250
            Edges::Many(list) => list,
251
        }
252
    }
253
254
    fn iter(&self) -> std::slice::Iter<'_, Vertex> {
255
        self.as_slice().iter()
256
    }
257
}
258
259
impl Default for FileGraph {
260
    fn default() -> Self {
261
        Self::new()
262
    }
263
}
264
265
/// One piece of a materialized file.
266
#[derive(Clone, Debug, PartialEq, Eq)]
267
pub enum Segment {
268
    Lines(Vec<AtomKey>),
269
    /// Order conflict: alternatives that cannot be ordered against each other.
270
    Conflict(Vec<Vec<AtomKey>>),
271
    /// Delete/insert conflict: `inserted` was anchored to `deleted` by a change
272
    /// that commutes with the deletion.
273
    Zombie {
274
        deleted: Vec<AtomKey>,
275
        inserted: Vec<AtomKey>,
276
    },
277
}
278
279
#[derive(Clone, Debug, PartialEq, Eq, Default)]
280
pub struct Materialized {
281
    pub segments: Vec<Segment>,
282
}
283
284
impl Materialized {
285
    pub fn is_conflicted(&self) -> bool {
286
        self.segments
287
            .iter()
288
            .any(|s| !matches!(s, Segment::Lines(_)))
289
    }
290
    pub fn conflict_count(&self) -> usize {
291
        self.segments
292
            .iter()
293
            .filter(|s| !matches!(s, Segment::Lines(_)))
294
            .count()
295
    }
296
    /// Every live atom in materialized order, conflict sides concatenated.
297
    pub fn live_atoms(&self) -> Vec<AtomKey> {
298
        let mut out = Vec::new();
299
        for s in &self.segments {
300
            match s {
301
                Segment::Lines(a) => out.extend(a),
302
                Segment::Conflict(sides) => sides.iter().for_each(|x| out.extend(x)),
303
                Segment::Zombie { inserted, .. } => out.extend(inserted),
304
            }
305
        }
306
        out
307
    }
308
}
309
310
impl FileGraph {
311
    pub fn new() -> Self {
312
        let mut g = FileGraph {
313
            atoms: HashMap::default(),
314
            succ: HashMap::default(),
315
            order: OrderSlot::default(),
316
        };
317
        g.edge(Vertex::Start, Vertex::End);
318
        g
319
    }
320
321
    fn edge(&mut self, a: Vertex, b: Vertex) {
322
        self.succ.entry(a).or_default().insert(b);
323
        self.succ.entry(b).or_default();
324
    }
325
326
    /// Apply an `Insert` by `change` whose first new atom has index `first_index`.
327
    pub fn insert(
328
        &mut self,
329
        change: ChangeHash,
330
        first_index: u32,
331
        after: Vertex,
332
        before: Vertex,
333
        lines: &Lines,
334
    ) {
335
        self.insert_owned(change, first_index, after, before, lines.clone());
336
    }
337
338
    /// [`FileGraph::insert`] taking the lines, so loading a state moves
339
    /// them in rather than copying. Each new atom's entry is made once, as
340
    /// the source of the next edge, and the chain's ends are ensured last.
341
    pub fn insert_owned(
342
        &mut self,
343
        change: ChangeHash,
344
        first_index: u32,
345
        after: Vertex,
346
        before: Vertex,
347
        lines: Lines,
348
    ) {
349
        self.order.take();
350
        self.atoms.reserve(lines.len());
351
        self.succ.reserve(lines.len() + 1);
352
        let mut prev = after;
353
        for i in 0..lines.len() {
354
            let key = (change, first_index + i as u32);
355
            self.atoms.entry(key).or_default().content =
356
                Some(LineRef::new(lines.clone(), i as u32));
357
            let v = Vertex::Atom(key);
358
            self.succ.entry(prev).or_default().insert(v);
359
            prev = v;
360
        }
361
        self.succ.entry(prev).or_default().insert(before);
362
        self.succ.entry(before).or_default();
363
    }
364
365
    /// Apply a `Delete` of `range` by change `by`.
366
    pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) {
367
        self.order.take();
368
        for i in range.from..range.to {
369
            self.atoms
370
                .entry((range.change, i))
371
                .or_default()
372
                .deleted_by
373
                .insert(by);
374
        }
375
    }
376
377
    pub fn is_live(&self, k: &AtomKey) -> bool {
378
        self.atoms
379
            .get(k)
380
            .is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty())
381
    }
382
383
    pub fn content(&self, k: &AtomKey) -> Option<&[u8]> {
384
        self.atoms
385
            .get(k)
386
            .and_then(|a| a.content.as_ref().map(LineRef::bytes))
387
    }
388
389
    pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> {
390
        self.atoms
391
            .get(k)
392
            .into_iter()
393
            .flat_map(|a| a.deleted_by.iter())
394
    }
395
396
    /// Whether the conflict at `m.segments[i]` is an adjacent insertion
397
    /// (oxygen#121): its sides only add lines between two lines that were
398
    /// next to each other, so keeping every side repeats nothing. It is not
399
    /// when a line there was deleted by a change only some sides build on:
400
    /// two changes that each replaced the same line conflict the same way,
401
    /// and keeping both would keep two versions of it. A line deleted in
402
    /// history every side shares does not count.
403
    pub fn adjacent_insertion(&self, m: &Materialized, i: usize, deps: &dyn DepOracle) -> bool {
404
        let Some(Segment::Conflict(sides)) = m.segments.get(i) else {
405
            return false;
406
        };
407
        let before = match i.checked_sub(1).map(|j| &m.segments[j]) {
408
            None => Vertex::Start,
409
            Some(Segment::Lines(a)) => match a.last() {
410
                Some(k) => Vertex::Atom(*k),
411
                None => return false,
412
            },
413
            Some(_) => return false,
414
        };
415
        let on_side: HashSet<AtomKey> = sides.iter().flatten().copied().collect();
416
        let side_changes: Vec<BTreeSet<ChangeHash>> = sides
417
            .iter()
418
            .map(|s| s.iter().map(|a| a.0).collect())
419
            .collect();
420
        let shared = |d: &ChangeHash| {
421
            side_changes
422
                .iter()
423
                .all(|s| s.iter().any(|c| deps.depends_on(c, d)))
424
        };
425
        // From the line before, through the sides' lines and deleted ones,
426
        // up to the next live line.
427
        let mut seen: HashSet<AtomKey> = HashSet::default();
428
        let mut todo = vec![before];
429
        while let Some(v) = todo.pop() {
430
            for &w in self.succ.get(&v).map(Edges::as_slice).unwrap_or(&[]) {
431
                let Vertex::Atom(k) = w else { continue };
432
                if !seen.insert(k) {
433
                    continue;
434
                }
435
                if !on_side.contains(&k) {
436
                    if self.is_live(&k) {
437
                        continue;
438
                    }
439
                    if self.deleters(&k).any(|d| !shared(d)) {
440
                        return false;
441
                    }
442
                }
443
                todo.push(w);
444
            }
445
        }
446
        true
447
    }
448
449
    /// Number of atoms introduced by `change` in this graph (for index bookkeeping).
450
    pub fn atom_count(&self) -> usize {
451
        self.atoms.len()
452
    }
453
454
    /// The changes that inserted a live atom directly after `k`. A change
455
    /// that deletes `k` must depend on them (spec §8.1), or `is_ghost`
456
    /// reads the deletion as concurrent with those insertions.
457
    /// Every atom on some path from `first` to `last`, both included, live
458
    /// or deleted (spec §8): the atoms a range of lines is made of, and
459
    /// those that were deleted from between them. Empty when `last` does
460
    /// not follow `first`.
461
    pub fn between(&self, first: &AtomKey, last: &AtomKey) -> Vec<AtomKey> {
462
        let (from, to) = (Vertex::Atom(*first), Vertex::Atom(*last));
463
        let mut ahead: HashSet<Vertex> = HashSet::default();
464
        let mut stack = vec![from];
465
        while let Some(v) = stack.pop() {
466
            if ahead.insert(v) {
467
                stack.extend(
468
                    self.succ
469
                        .get(&v)
470
                        .map(|e| e.as_slice())
471
                        .unwrap_or(&[])
472
                        .iter()
473
                        .copied(),
474
                );
475
            }
476
        }
477
        if !ahead.contains(&to) {
478
            return vec![];
479
        }
480
        let mut pred: HashMap<Vertex, Vec<Vertex>> = HashMap::default();
481
        for (u, es) in &self.succ {
482
            if ahead.contains(u) {
483
                for w in es.iter() {
484
                    pred.entry(*w).or_default().push(*u);
485
                }
486
            }
487
        }
488
        let mut behind: HashSet<Vertex> = HashSet::default();
489
        let mut stack = vec![to];
490
        while let Some(v) = stack.pop() {
491
            if behind.insert(v) {
492
                stack.extend(pred.get(&v).into_iter().flatten().copied());
493
            }
494
        }
495
        let mut out: Vec<AtomKey> = ahead
496
            .intersection(&behind)
497
            .filter_map(|v| match v {
498
                Vertex::Atom(k) if self.atoms.contains_key(k) => Some(*k),
499
                _ => None,
500
            })
501
            .collect();
502
        out.sort();
503
        out
504
    }
505
506
    /// The changes that inserted or deleted any of `atoms`.
507
    pub fn touching(&self, atoms: &[AtomKey]) -> BTreeSet<ChangeHash> {
508
        let mut out = BTreeSet::new();
509
        for k in atoms {
510
            out.insert(k.0);
511
            if let Some(info) = self.atoms.get(k) {
512
                out.extend(info.deleted_by.iter().copied());
513
            }
514
        }
515
        out
516
    }
517
518
    pub fn live_successor_changes(&self, k: &AtomKey) -> Vec<ChangeHash> {
519
        self.succ
520
            .get(&Vertex::Atom(*k))
521
            .map(|ss| {
522
                ss.iter()
523
                    .filter_map(|s| match s {
524
                        Vertex::Atom(y) if self.is_live(y) => Some(y.0),
525
                        _ => None,
526
                    })
527
                    .collect()
528
            })
529
            .unwrap_or_default()
530
    }
531
532
    fn is_ghost(&self, k: &AtomKey, info: &AtomInfo, deps: &dyn DepOracle) -> bool {
533
        // A deleted atom is a ghost when a live direct successor was inserted
534
        // by a change that commutes with every deleter: neither depends on
535
        // the other (spec §9.1, delete/insert). If the inserter came after the
536
        // deletion, or the deleter saw the insertion, there is no conflict.
537
        self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| {
538
            ss.iter().any(|s| match s {
539
                Vertex::Atom(y) if self.is_live(y) => !info
540
                    .deleted_by
541
                    .iter()
542
                    .any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)),
543
                _ => false,
544
            })
545
        })
546
    }
547
548
    /// Successors of `v` in the graph with non-kept vertices contracted away.
549
    fn reduced_succ(
550
        &self,
551
        v: Vertex,
552
        kept: &HashSet<Vertex>,
553
        memo: &mut HashMap<Vertex, BTreeSet<Vertex>>,
554
    ) -> BTreeSet<Vertex> {
555
        let direct = self.succ.get(&v).map(Edges::as_slice).unwrap_or_default();
556
        // The common case, a successor that is itself kept: nothing to
557
        // contract.
558
        if direct.iter().all(|s| kept.contains(s)) {
559
            return direct.iter().copied().collect();
560
        }
561
        let mut out = BTreeSet::new();
562
        let mut stack: Vec<Vertex> = direct.to_vec();
563
        let mut seen = HashSet::default();
564
        while let Some(s) = stack.pop() {
565
            if kept.contains(&s) {
566
                out.insert(s);
567
            } else if seen.insert(s) {
568
                if let Some(m) = memo.get(&s) {
569
                    out.extend(m.iter().copied());
570
                } else if let Some(next) = self.succ.get(&s) {
571
                    stack.extend(next.iter().copied());
572
                }
573
            }
574
        }
575
        out
576
    }
577
578
    /// The atoms in order when the graph is one chain from Start to End with
579
    /// nothing deleted: a file only ever inserted into in sequence, as every
580
    /// file of a fresh commit or a clone is. Such a graph has no conflict.
581
    /// An edge to End besides the chain's next atom (Start's own, or an
582
    /// append's anchor) orders nothing, since End is last anyway.
583
    fn as_chain(&self) -> Option<Vec<AtomKey>> {
584
        if self
585
            .atoms
586
            .values()
587
            .any(|a| a.content.is_none() || !a.deleted_by.is_empty())
588
        {
589
            return None;
590
        }
591
        let mut order = Vec::with_capacity(self.atoms.len());
592
        let mut v = Vertex::Start;
593
        loop {
594
            let succ = self.succ.get(&v)?.as_slice();
595
            let mut atoms = succ.iter().filter(|s| **s != Vertex::End);
596
            match (atoms.next(), atoms.next()) {
597
                (None, _) if succ.contains(&Vertex::End) => break,
598
                (Some(Vertex::Atom(k)), None) if order.len() < self.atoms.len() => {
599
                    order.push(*k);
600
                    v = Vertex::Atom(*k);
601
                }
602
                _ => return None,
603
            }
604
        }
605
        (order.len() == self.atoms.len()).then_some(order)
606
    }
607
608
    /// Order the live atoms and locate conflicts.
609
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
610
        let deps = &MemoDeps {
611
            inner: deps,
612
            known: Default::default(),
613
        };
614
        let lines = |order: Vec<AtomKey>| Materialized {
615
            segments: if order.is_empty() {
616
                vec![]
617
            } else {
618
                vec![Segment::Lines(order)]
619
            },
620
        };
621
        use std::sync::atomic::Ordering::Relaxed;
622
        // Which way it went, and how long it took (`OXY_TIMINGS`).
623
        let started = crate::timing::enabled().then(std::time::Instant::now);
624
        let took = |name: &'static str| {
625
            if let Some(t) = started {
626
                crate::timing::add(name, t.elapsed());
627
            }
628
        };
629
        if let Some(o) = self.order.get() {
630
            let holds = o.confirmed.load(Relaxed)
631
                || o.unchecked.iter().all(|k| {
632
                    self.atoms
633
                        .get(k)
634
                        .is_none_or(|info| !self.is_ghost(k, info, deps))
635
                });
636
            if holds {
637
                o.confirmed.store(true, Relaxed);
638
                took("graph: kept order");
639
                return lines(o.atoms.clone());
640
            }
641
            self.order.set(None);
642
        }
643
        // A chain (nothing ever deleted) is quick to walk, and a fresh tree
644
        // is all chains: no order is kept for one.
645
        if let Some(order) = self.as_chain() {
646
            took("graph: chain");
647
            return lines(order);
648
        }
649
        if let Some(order) = self.as_order(deps) {
650
            self.order
651
                .set(Some(std::sync::Arc::new(Order::known(order.clone()))));
652
            took("graph: single order");
653
            return lines(order);
654
        }
655
        let m = self.materialize_general(deps);
656
        took("graph: general");
657
        m
658
    }
659
660
    /// Take the kept order, to [`Order::splice`] it once a change's edits
661
    /// have applied (they drop it).
662
    pub fn take_order(&mut self) -> Option<std::sync::Arc<Order>> {
663
        self.order.take()
664
    }
665
666
    /// Keep `order` (taken before change `h`'s edits applied) spliced by
667
    /// them: `inserts` (after, before, first index, count) and `deleted`.
668
    /// Changes may apply in any order, so a line may arrive already
669
    /// deleted by a change applied before it: then the order is dropped.
670
    pub fn keep_spliced(
671
        &mut self,
672
        order: std::sync::Arc<Order>,
673
        h: ChangeHash,
674
        inserts: &[(Vertex, Vertex, u32, u32)],
675
        deleted: &[AtomKey],
676
    ) {
677
        let live = inserts
678
            .iter()
679
            .all(|&(_, _, first, count)| (first..first + count).all(|i| self.is_live(&(h, i))));
680
        let spliced = if live {
681
            order.splice(h, inserts, deleted)
682
        } else {
683
            None
684
        };
685
        *self.order.0.get_mut().unwrap_or_else(|e| e.into_inner()) = spliced;
686
    }
687
688
    /// The live atoms in order when, with the dead ones contracted away,
689
    /// the graph orders them one way only: no deleted atom is a ghost, and
690
    /// at every step of the topological sort one vertex is next. A file
691
    /// edited without concurrent edits is such a graph, however long its
692
    /// history. `None` otherwise, for [`FileGraph::materialize_general`]
693
    /// to find the conflicts; where this answers, it gives the same order.
694
    fn as_order(&self, deps: &dyn DepOracle) -> Option<Vec<AtomKey>> {
695
        // Kept vertices by dense index: Start, End, then the live atoms.
696
        let mut index: HashMap<Vertex, u32> =
697
            HashMap::with_capacity_and_hasher(self.atoms.len() + 2, Default::default());
698
        let mut verts: Vec<Vertex> = Vec::with_capacity(self.atoms.len() + 2);
699
        verts.extend([Vertex::Start, Vertex::End]);
700
        index.insert(Vertex::Start, 0);
701
        index.insert(Vertex::End, 1);
702
        for (k, info) in &self.atoms {
703
            if info.content.is_none() {
704
                continue;
705
            }
706
            if info.deleted_by.is_empty() {
707
                index.insert(Vertex::Atom(*k), verts.len() as u32);
708
                verts.push(Vertex::Atom(*k));
709
            } else if self.is_ghost(k, info, deps) {
710
                return None;
711
            }
712
        }
713
        // Each kept vertex's successors, contracting the others: what a
714
        // contracted vertex reaches is worked out once (post-order).
715
        let mut reach: HashMap<Vertex, Vec<u32>> = HashMap::default();
716
        let contracted = |v: Vertex, reach: &mut HashMap<Vertex, Vec<u32>>| {
717
            let mut stack: Vec<(Vertex, bool)> = vec![(v, false)];
718
            while let Some((x, done)) = stack.pop() {
719
                if reach.contains_key(&x) {
720
                    continue;
721
                }
722
                let next = self.succ.get(&x).map(Edges::as_slice).unwrap_or_default();
723
                if !done {
724
                    stack.push((x, true));
725
                    for s in next {
726
                        if !index.contains_key(s) && !reach.contains_key(s) {
727
                            stack.push((*s, false));
728
                        }
729
                    }
730
                    continue;
731
                }
732
                let mut out: Vec<u32> = Vec::new();
733
                for s in next {
734
                    match index.get(s) {
735
                        Some(i) => out.push(*i),
736
                        None => out.extend(reach.get(s).into_iter().flatten()),
737
                    }
738
                }
739
                out.sort_unstable();
740
                out.dedup();
741
                reach.insert(x, out);
742
            }
743
        };
744
        // Successors of vertex `i`: `succ[starts[i]..starts[i + 1]]`.
745
        let mut succ: Vec<u32> = Vec::with_capacity(verts.len() * 2);
746
        let mut starts: Vec<usize> = Vec::with_capacity(verts.len() + 1);
747
        let mut indeg: Vec<u32> = vec![0; verts.len()];
748
        for v in &verts {
749
            let from = succ.len();
750
            starts.push(from);
751
            for s in self.succ.get(v).map(Edges::as_slice).unwrap_or_default() {
752
                match index.get(s) {
753
                    Some(i) => succ.push(*i),
754
                    None => {
755
                        contracted(*s, &mut reach);
756
                        succ.extend(reach.get(s).into_iter().flatten());
757
                    }
758
                }
759
            }
760
            if succ.len() - from > 1 {
761
                succ[from..].sort_unstable();
762
                let mut kept = from;
763
                for i in from..succ.len() {
764
                    if i == from || succ[i] != succ[kept - 1] {
765
                        succ[kept] = succ[i];
766
                        kept += 1;
767
                    }
768
                }
769
                succ.truncate(kept);
770
            }
771
            for &w in &succ[from..] {
772
                indeg[w as usize] += 1;
773
            }
774
        }
775
        starts.push(succ.len());
776
        // Kahn's algorithm, giving up where two vertices are ready at once.
777
        let mut ready: Vec<u32> = (0..verts.len() as u32)
778
            .filter(|&i| indeg[i as usize] == 0)
779
            .collect();
780
        let mut order: Vec<AtomKey> = Vec::with_capacity(verts.len());
781
        let mut seen = 0;
782
        while let Some(v) = ready.pop() {
783
            if !ready.is_empty() {
784
                return None;
785
            }
786
            seen += 1;
787
            if let Vertex::Atom(k) = verts[v as usize] {
788
                order.push(k);
789
            }
790
            for &w in &succ[starts[v as usize]..starts[v as usize + 1]] {
791
                indeg[w as usize] -= 1;
792
                if indeg[w as usize] == 0 {
793
                    ready.push(w);
794
                }
795
            }
796
            if ready.len() > 1 {
797
                return None;
798
            }
799
        }
800
        (seen == verts.len()).then_some(order)
801
    }
802
803
    /// [`FileGraph::materialize`] for any graph: contraction, then a
804
    /// topological order with conflict regions.
805
    fn materialize_general(&self, deps: &dyn DepOracle) -> Materialized {
806
        // 1. Vertices that survive contraction: live atoms, ghosts, Start, End.
807
        let mut kept: HashSet<Vertex> = HashSet::default();
808
        let mut ghosts: HashSet<AtomKey> = HashSet::default();
809
        kept.insert(Vertex::Start);
810
        kept.insert(Vertex::End);
811
        for (k, info) in &self.atoms {
812
            if info.content.is_none() {
813
                continue;
814
            }
815
            if info.deleted_by.is_empty() {
816
                kept.insert(Vertex::Atom(*k));
817
            } else if self.is_ghost(k, info, deps) {
818
                kept.insert(Vertex::Atom(*k));
819
                ghosts.insert(*k);
820
            }
821
        }
822
823
        // 2. Reduced graph over kept vertices.
824
        let mut memo = HashMap::default();
825
        let mut rsucc: HashMap<Vertex, BTreeSet<Vertex>> = HashMap::default();
826
        let mut indeg: HashMap<Vertex, usize> = HashMap::default();
827
        for &u in &kept {
828
            indeg.entry(u).or_insert(0);
829
            let rs = self.reduced_succ(u, &kept, &mut memo);
830
            for &w in &rs {
831
                *indeg.entry(w).or_insert(0) += 1;
832
            }
833
            rsucc.insert(u, rs);
834
        }
835
836
        // 3. Kahn's algorithm with side labelling for conflict regions.
837
        let mut ready: BTreeSet<Vertex> = indeg
838
            .iter()
839
            .filter(|(_, &d)| d == 0)
840
            .map(|(v, _)| *v)
841
            .collect();
842
        let mut side: HashMap<Vertex, u32> = HashMap::default();
843
        let mut joins: HashSet<Vertex> = HashSet::default();
844
        let mut next_side = 0u32;
845
        let mut plain: Vec<Vertex> = Vec::new();
846
        let mut conflict: Option<BTreeMap<u32, Vec<Vertex>>> = None;
847
        let mut segments: Vec<Segment> = Vec::new();
848
849
        let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| {
850
            let atoms: Vec<AtomKey> = plain
851
                .iter()
852
                .filter_map(|v| {
853
                    if let Vertex::Atom(k) = v {
854
                        Some(*k)
855
                    } else {
856
                        None
857
                    }
858
                })
859
                .collect();
860
            plain.clear();
861
            if !atoms.is_empty() {
862
                segments.extend(split_zombies(atoms, &ghosts));
863
            }
864
        };
865
        let finish_conflict = |c: BTreeMap<u32, Vec<Vertex>>, segments: &mut Vec<Segment>| {
866
            let sides: Vec<Vec<AtomKey>> = c
867
                .into_values()
868
                .map(|vs| {
869
                    vs.into_iter()
870
                        .filter_map(|v| match v {
871
                            Vertex::Atom(k) if !ghosts.contains(&k) => Some(k),
872
                            _ => None,
873
                        })
874
                        .collect::<Vec<_>>()
875
                })
876
                .filter(|s| !s.is_empty())
877
                .collect();
878
            match sides.len() {
879
                0 => {}
880
                1 => segments.push(Segment::Lines(sides.into_iter().next().unwrap())),
881
                _ => segments.push(Segment::Conflict(sides)),
882
            }
883
        };
884
885
        while !ready.is_empty() {
886
            if ready.len() >= 2 {
887
                if conflict.is_none() {
888
                    flush_plain(&mut plain, &mut segments);
889
                    conflict = Some(BTreeMap::new());
890
                }
891
                for r in &ready {
892
                    if !side.contains_key(r) {
893
                        side.insert(*r, next_side);
894
                        next_side += 1;
895
                    }
896
                }
897
            }
898
            let v = ready.pop_first().unwrap();
899
            let my_side = side.get(&v).copied();
900
            match (&mut conflict, my_side) {
901
                (Some(c), Some(s)) => c.entry(s).or_default().push(v),
902
                (Some(_), None) => {
903
                    let c = conflict.take().unwrap();
904
                    finish_conflict(c, &mut segments);
905
                    plain.push(v);
906
                }
907
                (None, _) => plain.push(v),
908
            }
909
            for &w in &rsucc[&v] {
910
                let d = indeg.get_mut(&w).unwrap();
911
                *d -= 1;
912
                if conflict.is_some() && !joins.contains(&w) {
913
                    match (my_side, side.get(&w).copied()) {
914
                        (Some(s), None) => {
915
                            side.insert(w, s);
916
                        }
917
                        (Some(s), Some(t)) if s != t => {
918
                            side.remove(&w);
919
                            joins.insert(w);
920
                        }
921
                        (None, Some(_)) => {
922
                            side.remove(&w);
923
                            joins.insert(w);
924
                        }
925
                        _ => {}
926
                    }
927
                }
928
                if *d == 0 {
929
                    ready.insert(w);
930
                }
931
            }
932
        }
933
        if let Some(c) = conflict.take() {
934
            finish_conflict(c, &mut segments);
935
        }
936
        flush_plain(&mut plain, &mut segments);
937
        Materialized { segments }
938
    }
939
940
    /// Render a materialization to bytes with conflict markers (spec §9.5).
941
    /// `label` gives the short description of a change for marker headers.
942
    pub fn render(&self, m: &Materialized, label: &dyn Fn(&ChangeHash) -> String) -> Vec<u8> {
943
        let total = m.conflict_count();
944
        let mut n = 0;
945
        let mut out: Vec<u8> = Vec::new();
946
        let push_atoms = |out: &mut Vec<u8>, atoms: &[AtomKey]| {
947
            for a in atoms {
948
                if let Some(c) = self.content(a) {
949
                    out.extend_from_slice(c);
950
                }
951
            }
952
        };
953
        let newline_if_needed = |out: &mut Vec<u8>| {
954
            if !out.is_empty() && *out.last().unwrap() != b'\n' {
955
                out.push(b'\n');
956
            }
957
        };
958
        for seg in &m.segments {
959
            match seg {
960
                Segment::Lines(a) => push_atoms(&mut out, a),
961
                Segment::Conflict(sides) => {
962
                    n += 1;
963
                    let k = sides.len();
964
                    for (i, s) in sides.iter().enumerate() {
965
                        newline_if_needed(&mut out);
966
                        let who = s.first().map(|a| label(&a.0)).unwrap_or_default();
967
                        let marker = if i == 0 { "<<<<<<<" } else { "=======" };
968
                        out.extend_from_slice(
969
                            format!("{marker} oxy conflict {n} of {total}, side {} of {k}: change {who}\n", i + 1).as_bytes(),
970
                        );
971
                        push_atoms(&mut out, s);
972
                    }
973
                    newline_if_needed(&mut out);
974
                    out.extend_from_slice(
975
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
976
                    );
977
                }
978
                Segment::Zombie { deleted, inserted } => {
979
                    n += 1;
980
                    newline_if_needed(&mut out);
981
                    let deleter = deleted
982
                        .first()
983
                        .and_then(|a| self.deleters(a).next().copied())
984
                        .map(|d| label(&d))
985
                        .unwrap_or_default();
986
                    let inserter = inserted.first().map(|a| label(&a.0)).unwrap_or_default();
987
                    out.extend_from_slice(
988
                        format!("<<<<<<< oxy conflict {n} of {total}, side 1 of 2: deleted by change {deleter}\n-------\n").as_bytes(),
989
                    );
990
                    push_atoms(&mut out, deleted);
991
                    newline_if_needed(&mut out);
992
                    out.extend_from_slice(
993
                        format!(
994
                            "======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n"
995
                        )
996
                        .as_bytes(),
997
                    );
998
                    push_atoms(&mut out, inserted);
999
                    newline_if_needed(&mut out);
1000
                    out.extend_from_slice(
1001
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
1002
                    );
1003
                }
1004
            }
1005
        }
1006
        out
1007
    }
1008
}
1009
1010
fn vertex_value(v: &Vertex) -> crate::cbor::Value {
1011
    use crate::cbor::Value;
1012
    match v {
1013
        Vertex::Start => Value::Uint(0),
1014
        Vertex::End => Value::Uint(1),
1015
        Vertex::Atom((c, i)) => {
1016
            Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)])
1017
        }
1018
    }
1019
}
1020
1021
fn vertex_from(v: &crate::cbor::Value) -> Result<Vertex, crate::objects::ObjectError> {
1022
    use crate::cbor::Value;
1023
    use crate::objects::ObjectError;
1024
    match v {
1025
        Value::Uint(0) => Ok(Vertex::Start),
1026
        Value::Uint(1) => Ok(Vertex::End),
1027
        Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((
1028
            crate::objects::oref(&a[0], "vertex")?,
1029
            crate::objects::u32_(&a[1], "vertex")?,
1030
        ))),
1031
        _ => Err(ObjectError::Schema(
1032
            "vertex: expected 0, 1, or [hash, index]".into(),
1033
        )),
1034
    }
1035
}
1036
1037
impl FileGraph {
1038
    /// Serialize for a checkpoint: `{0: atoms [[hash, index, content|null, [deleters]]], 1: edges [[from, to]]}`.
1039
    pub fn to_value(&self) -> crate::cbor::Value {
1040
        use crate::cbor::Value;
1041
        let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect();
1042
        atoms.sort_by_key(|(k, _)| **k);
1043
        let mut edges: Vec<(Vertex, Vertex)> = self
1044
            .succ
1045
            .iter()
1046
            .flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b)))
1047
            .collect();
1048
        edges.sort();
1049
        Value::map([
1050
            (
1051
                0,
1052
                Value::Array(
1053
                    atoms
1054
                        .into_iter()
1055
                        .map(|((c, i), info)| {
1056
                            Value::Array(vec![
1057
                                Value::Bytes(c.0.to_vec()),
1058
                                Value::Uint(*i as u64),
1059
                                info.content
1060
                                    .as_ref()
1061
                                    .map(|b| Value::Bytes(b.bytes().to_vec()))
1062
                                    .unwrap_or(Value::Null),
1063
                                Value::Array(
1064
                                    info.deleted_by
1065
                                        .iter()
1066
                                        .map(|d| Value::Bytes(d.0.to_vec()))
1067
                                        .collect(),
1068
                                ),
1069
                            ])
1070
                        })
1071
                        .collect(),
1072
                ),
1073
            ),
1074
            (
1075
                1,
1076
                Value::Array(
1077
                    edges
1078
                        .iter()
1079
                        .map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)]))
1080
                        .collect(),
1081
                ),
1082
            ),
1083
        ])
1084
    }
1085
1086
    pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> {
1087
        use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError};
1088
        let m = map(v, "file graph")?;
1089
        let mut g = FileGraph {
1090
            atoms: HashMap::default(),
1091
            succ: HashMap::default(),
1092
            order: OrderSlot::default(),
1093
        };
1094
        let atoms = array(field(m, 0, "atoms")?, "atoms")?;
1095
        // Every atom's line in one buffer.
1096
        let mut texts: Vec<&[u8]> = Vec::new();
1097
        for a in atoms {
1098
            let e = array(a, "atom")?;
1099
            if e.len() == 4 && !e[2].is_null() {
1100
                texts.push(bytes(&e[2], "atom content")?);
1101
            }
1102
        }
1103
        let all: Lines = texts.into_iter().collect();
1104
        let mut next = 0u32;
1105
        for a in atoms {
1106
            let e = array(a, "atom")?;
1107
            if e.len() != 4 {
1108
                return Err(ObjectError::Schema("atom: expected 4 fields".into()));
1109
            }
1110
            let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?);
1111
            let content = if e[2].is_null() {
1112
                None
1113
            } else {
1114
                next += 1;
1115
                Some(LineRef::new(all.clone(), next - 1))
1116
            };
1117
            let deleted_by = array(&e[3], "deleters")?
1118
                .iter()
1119
                .map(|d| oref(d, "deleter"))
1120
                .collect::<Result<BTreeSet<_>, _>>()?;
1121
            g.atoms.insert(
1122
                key,
1123
                AtomInfo {
1124
                    content,
1125
                    deleted_by,
1126
                },
1127
            );
1128
        }
1129
        for e in array(field(m, 1, "edges")?, "edges")? {
1130
            let pair = array(e, "edge")?;
1131
            if pair.len() != 2 {
1132
                return Err(ObjectError::Schema("edge: expected [from, to]".into()));
1133
            }
1134
            g.edge(vertex_from(&pair[0])?, vertex_from(&pair[1])?);
1135
        }
1136
        Ok(g)
1137
    }
1138
}
1139
1140
/// Split a plain run into `Lines` and `Zombie` segments around ghost atoms.
1141
fn split_zombies(atoms: Vec<AtomKey>, ghosts: &HashSet<AtomKey>) -> Vec<Segment> {
1142
    let mut out = Vec::new();
1143
    let mut lines: Vec<AtomKey> = Vec::new();
1144
    let mut i = 0;
1145
    while i < atoms.len() {
1146
        if !ghosts.contains(&atoms[i]) {
1147
            lines.push(atoms[i]);
1148
            i += 1;
1149
            continue;
1150
        }
1151
        if !lines.is_empty() {
1152
            out.push(Segment::Lines(std::mem::take(&mut lines)));
1153
        }
1154
        let mut deleted = Vec::new();
1155
        while i < atoms.len() && ghosts.contains(&atoms[i]) {
1156
            deleted.push(atoms[i]);
1157
            i += 1;
1158
        }
1159
        let mut inserted = Vec::new();
1160
        if i < atoms.len() {
1161
            let ch = atoms[i].0;
1162
            while i < atoms.len() && atoms[i].0 == ch && !ghosts.contains(&atoms[i]) {
1163
                inserted.push(atoms[i]);
1164
                i += 1;
1165
            }
1166
        }
1167
        out.push(Segment::Zombie { deleted, inserted });
1168
    }
1169
    if !lines.is_empty() {
1170
        out.push(Segment::Lines(lines));
1171
    }
1172
    out
1173
}
1174
1175
#[cfg(test)]
1176
mod tests {
1177
    use super::*;
1178
    use crate::hash::ObjectRef;
1179
1180
    fn h(n: u8) -> ChangeHash {
1181
        ObjectRef([n; 32])
1182
    }
1183
    fn lines(v: &[&str]) -> Lines {
1184
        v.iter().map(|s| s.as_bytes()).collect()
1185
    }
1186
    fn text(g: &FileGraph, m: &Materialized) -> String {
1187
        String::from_utf8(g.render(m, &|c| c.short(4))).unwrap()
1188
    }
1189
1190
    struct Deps(HashMap<ChangeHash, Vec<ChangeHash>>);
1191
    impl DepOracle for Deps {
1192
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
1193
            if a == b {
1194
                return true;
1195
            }
1196
            let mut stack = vec![*a];
1197
            let mut seen = HashSet::default();
1198
            while let Some(x) = stack.pop() {
1199
                if let Some(ds) = self.0.get(&x) {
1200
                    for d in ds {
1201
                        if d == b {
1202
                            return true;
1203
                        }
1204
                        if seen.insert(*d) {
1205
                            stack.push(*d);
1206
                        }
1207
                    }
1208
                }
1209
            }
1210
            false
1211
        }
1212
    }
1213
1214
    /// Linear history: each change depends on every earlier one.
1215
    struct Linear;
1216
    impl DepOracle for Linear {
1217
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
1218
            a.0[..2] >= b.0[..2]
1219
        }
1220
    }
1221
1222
    /// A file of `n` lines edited `revs` times, `per` lines replaced each
1223
    /// time, as a long-lived file is: most atoms dead, one live chain.
1224
    fn edited_file(n: usize, revs: usize, per: usize) -> FileGraph {
1225
        let hn = |i: usize| {
1226
            let mut b = [0u8; 32];
1227
            b[..2].copy_from_slice(&(i as u16).to_be_bytes());
1228
            ObjectRef(b)
1229
        };
1230
        let mut g = FileGraph::new();
1231
        let first: Vec<String> = (0..n).map(|i| format!("line {i}\n")).collect();
1232
        let first: Vec<&str> = first.iter().map(String::as_str).collect();
1233
        g.insert(hn(1), 0, Vertex::Start, Vertex::End, &lines(&first));
1234
        for r in 2..revs + 2 {
1235
            let order = g.materialize(&Linear).live_atoms();
1236
            let step = order.len() / per;
1237
            for j in 0..per {
1238
                let at = j * step + (r % step.max(1));
1239
                let k = order[at];
1240
                g.delete(
1241
                    hn(r),
1242
                    &AtomRange {
1243
                        change: k.0,
1244
                        from: k.1,
1245
                        to: k.1 + 1,
1246
                    },
1247
                );
1248
                let after = if at == 0 {
1249
                    Vertex::Start
1250
                } else {
1251
                    Vertex::Atom(order[at - 1])
1252
                };
1253
                let before = order
1254
                    .get(at + 1)
1255
                    .map(|k| Vertex::Atom(*k))
1256
                    .unwrap_or(Vertex::End);
1257
                g.insert(hn(r), j as u32, after, before, &lines(&["new\n"]));
1258
            }
1259
        }
1260
        g
1261
    }
1262
1263
    /// The single-order shortcut gives what the general algorithm gives.
1264
    #[test]
1265
    fn the_order_shortcut_agrees_with_the_general_order() {
1266
        let g = edited_file(500, 10, 20);
1267
        assert!(g.as_chain().is_none());
1268
        let fast = g.as_order(&Linear).expect("a single order");
1269
        assert_eq!(
1270
            Materialized {
1271
                segments: vec![Segment::Lines(fast)]
1272
            },
1273
            g.materialize_general(&Linear)
1274
        );
1275
        // Two changes inserting at one place concurrently: a conflict,
1276
        // which the shortcut leaves to the general algorithm.
1277
        let mut g = FileGraph::new();
1278
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
1279
        let a = Vertex::Atom((h(1), 0));
1280
        g.insert(h(2), 0, a, Vertex::End, &lines(&["b\n"]));
1281
        g.insert(h(3), 0, a, Vertex::End, &lines(&["c\n"]));
1282
        assert!(g.as_order(&NoDeps).is_none());
1283
        assert!(g.materialize(&NoDeps).is_conflicted());
1284
    }
1285
1286
    /// Applied as the state applies a change's edits: the kept order is
1287
    /// spliced by them.
1288
    fn apply_spliced(
1289
        g: &mut FileGraph,
1290
        h: ChangeHash,
1291
        inserts: &[(Vertex, Vertex, usize)],
1292
        deleted: &[AtomKey],
1293
    ) {
1294
        let order = g.take_order();
1295
        let mut runs = Vec::new();
1296
        let mut next = 0u32;
1297
        for (i, &(a, b, n)) in inserts.iter().enumerate() {
1298
            let text: Vec<String> = (0..n).map(|j| format!("{i}.{j}\n")).collect();
1299
            let text: Vec<&str> = text.iter().map(String::as_str).collect();
1300
            g.insert(h, next, a, b, &lines(&text));
1301
            runs.push((a, b, next, n as u32));
1302
            next += n as u32;
1303
        }
1304
        for k in deleted {
1305
            g.delete(
1306
                h,
1307
                &AtomRange {
1308
                    change: k.0,
1309
                    from: k.1,
1310
                    to: k.1 + 1,
1311
                },
1312
            );
1313
        }
1314
        if let Some(o) = order {
1315
            g.keep_spliced(o, h, &runs, deleted);
1316
        }
1317
    }
1318
1319
    /// Edits as a diff makes them from the order `l`: hunks with a kept
1320
    /// line between them, each deleting some lines and inserting some.
1321
    fn random_edits(
1322
        l: &[AtomKey],
1323
        rnd: &mut impl FnMut(usize) -> usize,
1324
    ) -> (Vec<(Vertex, Vertex, usize)>, Vec<AtomKey>) {
1325
        let (mut inserts, mut deleted) = (Vec::new(), Vec::new());
1326
        let mut i = rnd(3);
1327
        while i <= l.len() {
1328
            let del = rnd(3).min(l.len() - i);
1329
            let add = rnd(3);
1330
            let after = if i == 0 {
1331
                Vertex::Start
1332
            } else {
1333
                Vertex::Atom(l[i - 1])
1334
            };
1335
            let before = l.get(i).map(|k| Vertex::Atom(*k)).unwrap_or(Vertex::End);
1336
            deleted.extend(&l[i..i + del]);
1337
            if add > 0 {
1338
                inserts.push((after, before, add));
1339
            }
1340
            i += del + 1 + rnd(6);
1341
        }
1342
        (inserts, deleted)
1343
    }
1344
1345
    /// The kept order, spliced change by change, is what the general
1346
    /// algorithm finds, through sequential edits and through concurrent
1347
    /// ones that leave conflicts and ghosts (where it must stand aside).
1348
    #[test]
1349
    fn the_kept_order_follows_edits() {
1350
        let hn = |i: usize| {
1351
            let mut b = [0u8; 32];
1352
            b[..2].copy_from_slice(&(i as u16).to_be_bytes());
1353
            ObjectRef(b)
1354
        };
1355
        let mut seed = 7u64;
1356
        let mut rnd = move |n: usize| {
1357
            seed = seed
1358
                .wrapping_mul(6364136223846793005)
1359
                .wrapping_add(1442695040888963407);
1360
            ((seed >> 33) as usize) % n.max(1)
1361
        };
1362
        let mut deps = Deps(HashMap::default());
1363
        let mut g = FileGraph::new();
1364
        let first: Vec<String> = (0..40).map(|i| format!("{i}\n")).collect();
1365
        let first: Vec<&str> = first.iter().map(String::as_str).collect();
1366
        g.insert(hn(1), 0, Vertex::Start, Vertex::End, &lines(&first));
1367
        let mut known: Vec<ChangeHash> = vec![hn(1)];
1368
        let mut c = 2;
1369
        let (mut spliced, mut conflicted) = (0, 0);
1370
        for step in 0..400 {
1371
            let l = g.materialize(&deps).live_atoms();
1372
            // Now and then two changes from the same base, neither knowing
1373
            // the other.
1374
            let pair = step % 7 == 3;
1375
            let base = known.clone();
1376
            for _ in 0..if pair { 2 } else { 1 } {
1377
                let h = hn(c);
1378
                c += 1;
1379
                let (inserts, deleted) = random_edits(&l, &mut rnd);
1380
                deps.0.insert(h, base.clone());
1381
                apply_spliced(&mut g, h, &inserts, &deleted);
1382
                known.push(h);
1383
                if g.order.get().is_some() {
1384
                    spliced += 1;
1385
                }
1386
                let m = g.materialize(&deps);
1387
                assert_eq!(m, g.materialize_general(&deps), "step {step}");
1388
                if m.is_conflicted() {
1389
                    conflicted += 1;
1390
                }
1391
            }
1392
            // A change that knows everything settles any conflict by
1393
            // rewriting the file.
1394
            if g.materialize(&deps).is_conflicted() {
1395
                let h = hn(c);
1396
                c += 1;
1397
                let all = g.materialize(&deps).live_atoms();
1398
                deps.0.insert(h, known.clone());
1399
                apply_spliced(&mut g, h, &[(Vertex::Start, Vertex::End, 5)], &all);
1400
                known.push(h);
1401
                assert_eq!(g.materialize(&deps), g.materialize_general(&deps));
1402
            }
1403
        }
1404
        assert!(
1405
            spliced > 100 && conflicted > 0,
1406
            "{spliced} spliced, {conflicted} conflicted"
1407
        );
1408
    }
1409
1410
    /// A change applied before the one whose line it deletes (as a merge
1411
    /// adds the other side, newest first): the line arrives dead, and the
1412
    /// kept order must not take it in.
1413
    #[test]
1414
    fn the_kept_order_takes_no_line_that_arrives_deleted() {
1415
        let mut deps = Deps(HashMap::default());
1416
        let mut g = FileGraph::new();
1417
        g.insert(
1418
            h(1),
1419
            0,
1420
            Vertex::Start,
1421
            Vertex::End,
1422
            &lines(&["a\n", "b\n", "c\n"]),
1423
        );
1424
        // A deletion, so the order is kept rather than walked as a chain.
1425
        apply_spliced(&mut g, h(2), &[], &[(h(1), 2)]);
1426
        deps.0.insert(h(2), vec![h(1)]);
1427
        let _ = g.materialize(&deps);
1428
        assert!(g.order.get().is_some());
1429
        let (a, b) = (Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)));
1430
        // h(3) inserts x between a and b; h(4) deletes it. h(4) first.
1431
        deps.0.insert(h(3), vec![h(2)]);
1432
        deps.0.insert(h(4), vec![h(3)]);
1433
        apply_spliced(&mut g, h(4), &[], &[(h(3), 0)]);
1434
        apply_spliced(&mut g, h(3), &[(a, b, 1)], &[]);
1435
        let m = g.materialize(&deps);
1436
        assert_eq!(m, g.materialize_general(&deps));
1437
        assert_eq!(text(&g, &m), "a\nb\n");
1438
    }
1439
1440
    /// How long rendering a long-lived file takes (run by hand, in release).
1441
    #[test]
1442
    #[ignore]
1443
    fn bench_materialize_edited_file() {
1444
        let g = edited_file(20_000, 50, 100);
1445
        let t = std::time::Instant::now();
1446
        let mut n = 0;
1447
        for _ in 0..20 {
1448
            n += g.materialize(&Linear).live_atoms().len();
1449
        }
1450
        eprintln!(
1451
            "materialize: {:.2} ms each ({n} atoms)",
1452
            t.elapsed().as_secs_f64() * 1000.0 / 20.0
1453
        );
1454
    }
1455
1456
    /// The chain shortcut agrees with the general algorithm wherever it
1457
    /// applies, and stands aside wherever it does not.
1458
    #[test]
1459
    fn the_chain_shortcut_agrees_with_the_general_order() {
1460
        let mut g = FileGraph::new();
1461
        g.insert(
1462
            h(1),
1463
            0,
1464
            Vertex::Start,
1465
            Vertex::End,
1466
            &lines(&["a\n", "b\n", "c\n"]),
1467
        );
1468
        assert!(g.as_chain().is_some());
1469
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
1470
        // Appended after the last line: still a chain.
1471
        g.insert(
1472
            h(2),
1473
            0,
1474
            Vertex::Atom((h(1), 2)),
1475
            Vertex::End,
1476
            &lines(&["d\n"]),
1477
        );
1478
        assert!(g.as_chain().is_some());
1479
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
1480
        // Inserted between two lines: not a chain (a has two successors).
1481
        g.insert(
1482
            h(4),
1483
            0,
1484
            Vertex::Atom((h(1), 0)),
1485
            Vertex::Atom((h(1), 1)),
1486
            &lines(&["x\n"]),
1487
        );
1488
        assert!(g.as_chain().is_none());
1489
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "a\nx\nb\nc\nd\n");
1490
        // Two inserts at one anchor: a conflict, never a chain.
1491
        let mut c = FileGraph::new();
1492
        c.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
1493
        c.insert(h(2), 0, Vertex::Start, Vertex::End, &lines(&["b\n"]));
1494
        assert!(c.as_chain().is_none());
1495
        assert!(c.materialize(&NoDeps).is_conflicted());
1496
        // A deletion: not a chain.
1497
        let mut d = FileGraph::new();
1498
        d.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1499
        d.delete(
1500
            h(3),
1501
            &AtomRange {
1502
                change: h(1),
1503
                from: 0,
1504
                to: 1,
1505
            },
1506
        );
1507
        assert!(d.as_chain().is_none());
1508
        assert_eq!(d.materialize(&NoDeps), d.materialize_general(&NoDeps));
1509
        // Empty.
1510
        let e = FileGraph::new();
1511
        assert_eq!(e.materialize(&NoDeps), e.materialize_general(&NoDeps));
1512
    }
1513
1514
    #[test]
1515
    fn sequential_inserts_and_deletes() {
1516
        let mut g = FileGraph::new();
1517
        g.insert(
1518
            h(1),
1519
            0,
1520
            Vertex::Start,
1521
            Vertex::End,
1522
            &lines(&["a\n", "b\n", "c\n"]),
1523
        );
1524
        let m = g.materialize(&NoDeps);
1525
        assert!(!m.is_conflicted());
1526
        assert_eq!(text(&g, &m), "a\nb\nc\n");
1527
1528
        // Change 2 depends on 1, inserts between b and c and deletes a.
1529
        g.insert(
1530
            h(2),
1531
            0,
1532
            Vertex::Atom((h(1), 1)),
1533
            Vertex::Atom((h(1), 2)),
1534
            &lines(&["x\n"]),
1535
        );
1536
        g.delete(
1537
            h(2),
1538
            &AtomRange {
1539
                change: h(1),
1540
                from: 0,
1541
                to: 1,
1542
            },
1543
        );
1544
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
1545
        let m = g.materialize(&deps);
1546
        assert!(!m.is_conflicted());
1547
        assert_eq!(text(&g, &m), "b\nx\nc\n");
1548
        assert_eq!(m.live_atoms(), vec![(h(1), 1), (h(2), 0), (h(1), 2)]);
1549
    }
1550
1551
    #[test]
1552
    fn concurrent_inserts_at_same_anchor_conflict() {
1553
        let mut g = FileGraph::new();
1554
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1555
        g.insert(
1556
            h(2),
1557
            0,
1558
            Vertex::Atom((h(1), 0)),
1559
            Vertex::Atom((h(1), 1)),
1560
            &lines(&["from-2\n"]),
1561
        );
1562
        g.insert(
1563
            h(3),
1564
            0,
1565
            Vertex::Atom((h(1), 0)),
1566
            Vertex::Atom((h(1), 1)),
1567
            &lines(&["from-3\n", "more-3\n"]),
1568
        );
1569
        let deps = Deps(
1570
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1571
                .into_iter()
1572
                .collect(),
1573
        );
1574
        let m = g.materialize(&deps);
1575
        assert!(m.is_conflicted());
1576
        assert_eq!(m.conflict_count(), 1);
1577
        assert_eq!(
1578
            m.segments,
1579
            vec![
1580
                Segment::Lines(vec![(h(1), 0)]),
1581
                Segment::Conflict(vec![vec![(h(2), 0)], vec![(h(3), 0), (h(3), 1)]]),
1582
                Segment::Lines(vec![(h(1), 1)]),
1583
            ]
1584
        );
1585
        let t = text(&g, &m);
1586
        assert!(t.starts_with("a\n<<<<<<< oxy conflict 1 of 1, side 1 of 2: change 0202\nfrom-2\n======= oxy conflict 1 of 1, side 2 of 2: change 0303\nfrom-3\nmore-3\n>>>>>>> oxy conflict 1 of 1 end\nb\n"), "{t}");
1587
    }
1588
1589
    #[test]
1590
    fn concurrent_inserts_at_different_anchors_do_not_conflict() {
1591
        let mut g = FileGraph::new();
1592
        g.insert(
1593
            h(1),
1594
            0,
1595
            Vertex::Start,
1596
            Vertex::End,
1597
            &lines(&["a\n", "b\n", "c\n"]),
1598
        );
1599
        g.insert(
1600
            h(2),
1601
            0,
1602
            Vertex::Start,
1603
            Vertex::Atom((h(1), 0)),
1604
            &lines(&["top\n"]),
1605
        );
1606
        g.insert(
1607
            h(3),
1608
            0,
1609
            Vertex::Atom((h(1), 2)),
1610
            Vertex::End,
1611
            &lines(&["bottom\n"]),
1612
        );
1613
        let deps = Deps(
1614
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1615
                .into_iter()
1616
                .collect(),
1617
        );
1618
        let m = g.materialize(&deps);
1619
        assert!(!m.is_conflicted());
1620
        assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n");
1621
    }
1622
1623
    #[test]
1624
    fn concurrent_deletes_of_the_same_line_agree() {
1625
        let mut g = FileGraph::new();
1626
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1627
        g.delete(
1628
            h(2),
1629
            &AtomRange {
1630
                change: h(1),
1631
                from: 0,
1632
                to: 1,
1633
            },
1634
        );
1635
        g.delete(
1636
            h(3),
1637
            &AtomRange {
1638
                change: h(1),
1639
                from: 0,
1640
                to: 1,
1641
            },
1642
        );
1643
        let deps = Deps(
1644
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1645
                .into_iter()
1646
                .collect(),
1647
        );
1648
        let m = g.materialize(&deps);
1649
        assert!(!m.is_conflicted());
1650
        assert_eq!(text(&g, &m), "b\n");
1651
    }
1652
1653
    #[test]
1654
    fn delete_insert_conflict_is_a_zombie() {
1655
        let mut g = FileGraph::new();
1656
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1657
        // 2 deletes a; 3 (concurrent) inserts after a.
1658
        g.delete(
1659
            h(2),
1660
            &AtomRange {
1661
                change: h(1),
1662
                from: 0,
1663
                to: 1,
1664
            },
1665
        );
1666
        g.insert(
1667
            h(3),
1668
            0,
1669
            Vertex::Atom((h(1), 0)),
1670
            Vertex::Atom((h(1), 1)),
1671
            &lines(&["after-a\n"]),
1672
        );
1673
        let deps = Deps(
1674
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1675
                .into_iter()
1676
                .collect(),
1677
        );
1678
        let m = g.materialize(&deps);
1679
        assert!(m.is_conflicted());
1680
        assert_eq!(
1681
            m.segments,
1682
            vec![
1683
                Segment::Zombie {
1684
                    deleted: vec![(h(1), 0)],
1685
                    inserted: vec![(h(3), 0)]
1686
                },
1687
                Segment::Lines(vec![(h(1), 1)]),
1688
            ]
1689
        );
1690
        let t = text(&g, &m);
1691
        assert!(t.contains("-------\na\n======="), "{t}");
1692
        assert!(t.contains("after-a\n>>>>>>>"), "{t}");
1693
1694
        // If the deleter saw the insert (depends on 3), there is no conflict.
1695
        let deps = Deps(
1696
            [(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])]
1697
                .into_iter()
1698
                .collect(),
1699
        );
1700
        let m = g.materialize(&deps);
1701
        assert!(!m.is_conflicted());
1702
        assert_eq!(text(&g, &m), "after-a\nb\n");
1703
    }
1704
1705
    #[test]
1706
    fn insert_after_a_line_deleted_by_a_dependency_is_not_a_conflict() {
1707
        // 1 inserts a,b; 2 deletes b (depends on 1); 3 depends on 2 yet anchors
1708
        // after the dead b (a rewritten change may do this): no conflict.
1709
        let mut g = FileGraph::new();
1710
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1711
        g.delete(
1712
            h(2),
1713
            &AtomRange {
1714
                change: h(1),
1715
                from: 1,
1716
                to: 2,
1717
            },
1718
        );
1719
        g.insert(
1720
            h(3),
1721
            0,
1722
            Vertex::Atom((h(1), 1)),
1723
            Vertex::End,
1724
            &lines(&["c\n"]),
1725
        );
1726
        let deps = Deps(
1727
            [(h(2), vec![h(1)]), (h(3), vec![h(2)])]
1728
                .into_iter()
1729
                .collect(),
1730
        );
1731
        let m = g.materialize(&deps);
1732
        assert!(!m.is_conflicted(), "{:?}", m.segments);
1733
        assert_eq!(text(&g, &m), "a\nc\n");
1734
        // A change that deletes a line it introduced itself, with a dependent
1735
        // anchoring after it, is likewise fine.
1736
        let mut g = FileGraph::new();
1737
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"]));
1738
        g.delete(
1739
            h(1),
1740
            &AtomRange {
1741
                change: h(1),
1742
                from: 0,
1743
                to: 1,
1744
            },
1745
        );
1746
        g.insert(
1747
            h(2),
1748
            0,
1749
            Vertex::Atom((h(1), 0)),
1750
            Vertex::End,
1751
            &lines(&["y\n"]),
1752
        );
1753
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
1754
        let m = g.materialize(&deps);
1755
        assert!(!m.is_conflicted());
1756
        assert_eq!(text(&g, &m), "y\n");
1757
    }
1758
1759
    #[test]
1760
    fn resolution_collapses_a_conflict() {
1761
        let mut g = FileGraph::new();
1762
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
1763
        g.insert(
1764
            h(2),
1765
            0,
1766
            Vertex::Start,
1767
            Vertex::Atom((h(1), 0)),
1768
            &lines(&["x\n"]),
1769
        );
1770
        g.insert(
1771
            h(3),
1772
            0,
1773
            Vertex::Start,
1774
            Vertex::Atom((h(1), 0)),
1775
            &lines(&["y\n"]),
1776
        );
1777
        let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1778
            .into_iter()
1779
            .collect();
1780
        assert!(g.materialize(&Deps(deps.clone())).is_conflicted());
1781
        // Resolution keeps y, drops x.
1782
        g.delete(
1783
            h(4),
1784
            &AtomRange {
1785
                change: h(2),
1786
                from: 0,
1787
                to: 1,
1788
            },
1789
        );
1790
        deps.insert(h(4), vec![h(2), h(3)]);
1791
        let m = g.materialize(&Deps(deps));
1792
        assert!(!m.is_conflicted());
1793
        assert_eq!(text(&g, &m), "y\na\n");
1794
    }
1795
1796
    #[test]
1797
    fn empty_and_unterminated_files() {
1798
        let g = FileGraph::new();
1799
        let m = g.materialize(&NoDeps);
1800
        assert_eq!(m.segments, vec![]);
1801
        assert_eq!(text(&g, &m), "");
1802
        let mut g = FileGraph::new();
1803
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["no newline"]));
1804
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "no newline");
1805
    }
1806
1807
    #[test]
1808
    fn serialization_round_trip() {
1809
        let mut g = FileGraph::new();
1810
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1811
        g.insert(
1812
            h(2),
1813
            0,
1814
            Vertex::Atom((h(1), 0)),
1815
            Vertex::Atom((h(1), 1)),
1816
            &lines(&["x\n"]),
1817
        );
1818
        g.delete(
1819
            h(3),
1820
            &AtomRange {
1821
                change: h(1),
1822
                from: 1,
1823
                to: 2,
1824
            },
1825
        );
1826
        let v = g.to_value();
1827
        let bytes = crate::cbor::encode(&v);
1828
        let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
1829
        let deps = Deps(
1830
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1831
                .into_iter()
1832
                .collect(),
1833
        );
1834
        assert_eq!(back.materialize(&deps), g.materialize(&deps));
1835
        assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n");
1836
        assert_eq!(
1837
            crate::cbor::encode(&back.to_value()),
1838
            bytes,
1839
            "deterministic"
1840
        );
1841
    }
1842
1843
    /// Spec §7.3: the graph is a function of the set of changes, not their order.
1844
    #[test]
1845
    fn application_order_is_irrelevant() {
1846
        use rand::seq::SliceRandom;
1847
        type Op = (
1848
            ChangeHash,
1849
            Vec<(u32, Vertex, Vertex, Lines)>,
1850
            Vec<AtomRange>,
1851
        );
1852
        let ops: Vec<Op> = vec![
1853
            (
1854
                h(1),
1855
                vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))],
1856
                vec![],
1857
            ),
1858
            (
1859
                h(2),
1860
                vec![(
1861
                    0,
1862
                    Vertex::Atom((h(1), 0)),
1863
                    Vertex::Atom((h(1), 1)),
1864
                    lines(&["p\n"]),
1865
                )],
1866
                vec![],
1867
            ),
1868
            (
1869
                h(3),
1870
                vec![(
1871
                    0,
1872
                    Vertex::Atom((h(1), 0)),
1873
                    Vertex::Atom((h(1), 1)),
1874
                    lines(&["q\n", "r\n"]),
1875
                )],
1876
                vec![AtomRange {
1877
                    change: h(1),
1878
                    from: 2,
1879
                    to: 3,
1880
                }],
1881
            ),
1882
            (
1883
                h(4),
1884
                vec![(
1885
                    0,
1886
                    Vertex::Atom((h(3), 1)),
1887
                    Vertex::Atom((h(1), 1)),
1888
                    lines(&["s\n"]),
1889
                )],
1890
                vec![AtomRange {
1891
                    change: h(1),
1892
                    from: 1,
1893
                    to: 2,
1894
                }],
1895
            ),
1896
            (
1897
                h(5),
1898
                vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))],
1899
                vec![],
1900
            ),
1901
        ];
1902
        let deps = Deps(
1903
            [
1904
                (h(2), vec![h(1)]),
1905
                (h(3), vec![h(1)]),
1906
                (h(4), vec![h(3), h(1)]),
1907
                (h(5), vec![h(1)]),
1908
            ]
1909
            .into_iter()
1910
            .collect(),
1911
        );
1912
        let build = |order: &[usize]| {
1913
            let mut g = FileGraph::new();
1914
            for &i in order {
1915
                let (c, ins, dels) = &ops[i];
1916
                for (idx, a, b, l) in ins {
1917
                    g.insert(*c, *idx, *a, *b, l);
1918
                }
1919
                for d in dels {
1920
                    g.delete(*c, d);
1921
                }
1922
            }
1923
            let m = g.materialize(&deps);
1924
            (m.clone(), text(&g, &m))
1925
        };
1926
        let reference = build(&[0, 1, 2, 3, 4]);
1927
        assert!(reference.0.is_conflicted());
1928
        let mut rng = rand::thread_rng();
1929
        let mut order: Vec<usize> = (0..ops.len()).collect();
1930
        for _ in 0..50 {
1931
            order.shuffle(&mut rng);
1932
            assert_eq!(build(&order), reference, "order {order:?}");
1933
        }
1934
    }
1935
}