automerge/rust/automerge/src/op_tree/iter.rs
Alex Good dd3c6d1303
Move rust workspace into ./rust
After some discussion with PVH I realise that the repo structure in the
last reorg was very rust-centric. In an attempt to put each language on
a level footing move the rust code and project files into ./rust
2022-10-16 19:55:51 +01:00

438 lines
17 KiB
Rust

use std::cmp::Ordering;
use crate::types::Op;
use super::{OpTreeInternal, OpTreeNode};
#[derive(Clone)]
pub(crate) struct OpTreeIter<'a>(Inner<'a>);
impl<'a> OpTreeIter<'a> {
pub(crate) fn new(tree: &'a OpTreeInternal) -> OpTreeIter<'a> {
Self(
tree.root_node
.as_ref()
.map(|root| Inner::NonEmpty {
// This is a guess at the average depth of an OpTree
ancestors: Vec::with_capacity(6),
current: NodeIter {
node: root,
index: 0,
},
cumulative_index: 0,
root_node: root,
})
.unwrap_or(Inner::Empty),
)
}
}
impl<'a> Iterator for OpTreeIter<'a> {
type Item = &'a Op;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.0.nth(n)
}
}
#[derive(Clone)]
enum Inner<'a> {
Empty,
NonEmpty {
// A stack of nodes in the optree which we have descended in to to get to the current
// element.
ancestors: Vec<NodeIter<'a>>,
current: NodeIter<'a>,
// How far through the whole optree we are
cumulative_index: usize,
root_node: &'a OpTreeNode,
},
}
/// A node in the op tree which we are iterating over
#[derive(Clone)]
struct NodeIter<'a> {
/// The node itself
node: &'a OpTreeNode,
/// The index of the next element we will pull from the node. This means something different
/// depending on whether the node is a leaf node or not. If the node is a leaf node then this
/// index is the index in `node.elements` which will be returned on the next call to `next()`.
/// If the node is not an internal node then this index is the index of `children` which we are
/// currently iterating as well as being the index of the next element of `elements` which we
/// will return once we have finished iterating over the child node.
index: usize,
}
impl<'a> Iterator for Inner<'a> {
type Item = &'a Op;
fn next(&mut self) -> Option<Self::Item> {
match self {
Inner::Empty => None,
Inner::NonEmpty {
ancestors,
current,
cumulative_index,
..
} => {
if current.node.is_leaf() {
// If we're in a leaf node and we haven't exhausted it yet we just return the elements
// of the leaf node
if current.index < current.node.len() {
let result = &current.node.elements[current.index];
current.index += 1;
*cumulative_index += 1;
Some(result)
} else {
// We've exhausted the leaf node, we must find the nearest non-exhausted parent (lol)
let node_iter = loop {
if let Some(
node_iter @ NodeIter {
node: parent,
index: parent_index,
},
) = ancestors.pop()
{
// We've exhausted this parent
if parent_index >= parent.elements.len() {
continue;
} else {
// This parent still has elements to process, let's use it!
break node_iter;
}
} else {
// No parents left, we're done
return None;
}
};
// if we've finished the elements in a leaf node and there's a parent node then we
// return the element from the parent node which is one after the index at which we
// descended into the child
*current = node_iter;
let result = &current.node.elements[current.index];
current.index += 1;
*cumulative_index += 1;
Some(result)
}
} else {
// If we're in a non-leaf node then the last iteration returned an element from the
// current nodes `elements`, so we must now descend into a leaf child
ancestors.push(current.clone());
loop {
let child = &current.node.children[current.index];
current.index = 0;
if !child.is_leaf() {
ancestors.push(NodeIter {
node: child,
index: 0,
});
current.node = child
} else {
current.node = child;
break;
}
}
self.next()
}
}
}
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::NonEmpty {
root_node,
cumulative_index,
current,
ancestors,
..
} => {
// Make sure that we don't rewind when calling nth more than once
if n < *cumulative_index {
None
} else if n >= root_node.len() {
*cumulative_index = root_node.len() - 1;
None
} else {
// rather than trying to go back up through the ancestors to find the right
// node we just start at the root.
*current = NodeIter {
node: root_node,
index: n,
};
*cumulative_index = 0;
ancestors.clear();
while !current.node.is_leaf() {
for (child_index, child) in current.node.children.iter().enumerate() {
match (*cumulative_index + child.len()).cmp(&n) {
Ordering::Less => {
*cumulative_index += child.len() + 1;
current.index = child_index + 1;
}
Ordering::Equal => {
*cumulative_index += child.len() + 1;
current.index = child_index + 1;
return Some(&current.node.elements[child_index]);
}
Ordering::Greater => {
current.index = child_index;
let old = std::mem::replace(
current,
NodeIter {
node: child,
index: 0,
},
);
ancestors.push(old);
break;
}
}
}
}
// we're in a leaf node and we kept track of the cumulative index as we went,
let index_in_this_node = n.saturating_sub(*cumulative_index);
current.index = index_in_this_node + 1;
Some(&current.node.elements[index_in_this_node])
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::OpTreeInternal;
use crate::types::{Key, Op, OpId, OpType, ScalarValue};
use proptest::prelude::*;
#[derive(Clone)]
enum Action {
Insert(usize, Op),
Delete(usize),
}
impl std::fmt::Debug for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Insert(index, ..) => write!(f, "Insert({})", index),
Self::Delete(index) => write!(f, "Delete({})", index),
}
}
}
// A struct which impls Debug by only printing the counters of the IDs of the ops it wraps.
// This is useful because the only difference between the ops that we generate is the counter
// of their IDs. Wrapping a Vec<Op> in DebugOps will result in output from assert! etc. which
// only shows the counters. For example, the output of a failing assert_eq! like this
//
// assert_eq!(DebugOps(&ops1), DebugOps(&ops2))
//
// Might look like this
//
// left: `[0,1,2,3]
// right: `[0,1,2,3,4]
//
// i.e. all the other details of the ops are elided
#[derive(PartialEq)]
struct DebugOps<'a>(&'a [Op]);
impl<'a> std::fmt::Debug for DebugOps<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[")?;
for (index, op) in self.0.iter().enumerate() {
if index < self.0.len() - 1 {
write!(f, "{},", op.id.counter())?;
} else {
write!(f, "{}]", op.id.counter())?
}
}
Ok(())
}
}
fn op(counter: u64) -> Op {
Op {
action: OpType::Put(ScalarValue::Uint(counter)),
id: OpId(counter, 0),
key: Key::Map(0),
succ: Default::default(),
pred: Default::default(),
insert: false,
}
}
/// A model for a property based test of the OpTreeIter. We generate a set of actions, each
/// action pertaining to a `model` - which is just a `Vec<Op>`. As we generate each action we
/// apply it to the model and record the action we took. In the property test we replay the
/// same actions against an `OpTree` and check that the iterator returns the same result as the
/// `model`.
#[derive(Clone)]
struct Model {
actions: Vec<Action>,
model: Vec<Op>,
}
impl std::fmt::Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Model")
.field("actions", &self.actions)
.field("model", &DebugOps(&self.model))
.finish()
}
}
impl Model {
fn insert(&self, index: usize, next_op_counter: u64) -> Self {
let mut actions = self.actions.clone();
let op = op(next_op_counter);
actions.push(Action::Insert(index, op.clone()));
let mut model = self.model.clone();
model.insert(index, op);
Self { actions, model }
}
fn delete(&self, index: usize) -> Self {
let mut actions = self.actions.clone();
actions.push(Action::Delete(index));
let mut model = self.model.clone();
model.remove(index);
Self { actions, model }
}
fn next(self, next_op_counter: u64) -> impl Strategy<Value = Model> {
if self.model.is_empty() {
Just(self.insert(0, next_op_counter)).boxed()
} else {
// Note that we have to feed `self` through the `prop_flat_map` using `Just` to
// appease the borrow checker, this is annoying because it does obscure the meaning
// of the code heere which is basically "decide whether the next action should be
// insert, if it is insert choose an index between 0..model.len() + 1 and generate
// an op to insert, otherwise choose an index between 0..model.len() and generate a
// delete action".
//
// 95% chance of inserting to make sure we deal with large lists
(proptest::bool::weighted(0.95), Just(self))
.prop_flat_map(move |(insert, model)| {
if insert {
(0..model.model.len() + 1, Just(model))
.prop_map(move |(index, model)| {
model.insert(index, next_op_counter)
})
.boxed()
} else {
((0..model.model.len()), Just(model))
.prop_map(move |(index, model)| model.delete(index))
.boxed()
}
})
.boxed()
}
}
}
fn model() -> impl Strategy<Value = Model> {
(0_u64..150).prop_flat_map(|num_steps| {
let mut strat = Just((
0,
Model {
actions: Vec::new(),
model: Vec::new(),
},
))
.boxed();
for _ in 0..num_steps {
strat = strat
// Note the counter, which we feed through each `prop_flat_map`, incrementing
// it by one each time. This mean that the generated ops have ascending (but
// not necessarily consecutive because not every `Action` is an `Insert`)
// counters. This makes it easier to debug failures - if we just used a random
// counter it would be much harder to see where things are out of order.
.prop_flat_map(|(counter, model)| {
let next_counter = counter + 1;
model.next(counter).prop_map(move |m| (next_counter, m))
})
.boxed();
}
strat.prop_map(|(_, model)| model)
})
}
fn make_optree(actions: &[Action]) -> super::OpTreeInternal {
let mut optree = OpTreeInternal::new();
for action in actions {
match action {
Action::Insert(index, op) => optree.insert(*index, op.clone()),
Action::Delete(index) => {
optree.remove(*index);
}
}
}
optree
}
/// A model for calls to `nth`. `NthModel::n` is guarnateed to be in `(0..model.len())`
#[derive(Clone)]
struct NthModel {
model: Vec<Op>,
actions: Vec<Action>,
n: usize,
}
impl std::fmt::Debug for NthModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Model")
.field("actions", &self.actions)
.field("model", &DebugOps(&self.model))
.field("n", &self.n)
.finish()
}
}
fn nth_model() -> impl Strategy<Value = NthModel> {
model().prop_flat_map(|model| {
if model.model.is_empty() {
Just(NthModel {
model: model.model,
actions: model.actions,
n: 0,
})
.boxed()
} else {
(0..model.model.len(), Just(model))
.prop_map(|(index, model)| NthModel {
model: model.model,
actions: model.actions,
n: index,
})
.boxed()
}
})
}
proptest! {
#[test]
fn optree_iter_proptest(model in model()) {
let optree = make_optree(&model.actions);
let iter = super::OpTreeIter::new(&optree);
let iterated = iter.cloned().collect::<Vec<_>>();
assert_eq!(DebugOps(&model.model), DebugOps(&iterated))
}
#[test]
fn optree_iter_nth(model in nth_model()) {
let optree = make_optree(&model.actions);
let mut iter = super::OpTreeIter::new(&optree);
let mut model_iter = model.model.iter();
assert_eq!(model_iter.nth(model.n), iter.nth(model.n));
let tail = iter.cloned().collect::<Vec<_>>();
let expected_tail = model_iter.cloned().collect::<Vec<_>>();
assert_eq!(DebugOps(tail.as_slice()), DebugOps(expected_tail.as_slice()));
}
}
}