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
197 lines
6.1 KiB
Rust
197 lines
6.1 KiB
Rust
use itertools::Itertools;
|
|
|
|
use super::OpId;
|
|
|
|
/// A wrapper around `Vec<Opid>` which preserves the invariant that the ops are
|
|
/// in ascending order with respect to their counters and actor IDs. In order to
|
|
/// maintain this invariant you must provide a comparator function when adding
|
|
/// ops as the actor indices in an OpId are not sufficient to order the OpIds
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
pub(crate) struct OpIds(Vec<OpId>);
|
|
|
|
impl<'a> IntoIterator for &'a OpIds {
|
|
type Item = &'a OpId;
|
|
type IntoIter = std::slice::Iter<'a, OpId>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.0.iter()
|
|
}
|
|
}
|
|
|
|
impl OpIds {
|
|
pub(crate) fn empty() -> Self {
|
|
Self(Vec::new())
|
|
}
|
|
|
|
pub(crate) fn new<I: Iterator<Item = OpId>, F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
|
|
opids: I,
|
|
cmp: F,
|
|
) -> Self {
|
|
let mut inner = opids.collect::<Vec<_>>();
|
|
inner.sort_by(cmp);
|
|
Self(inner)
|
|
}
|
|
|
|
/// Create a new OpIds if `opids` are sorted with respect to `cmp` and contain no duplicates.
|
|
///
|
|
/// Returns `Some(OpIds)` if `opids` is sorted and has no duplicates, otherwise returns `None`
|
|
pub(crate) fn new_if_sorted<F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
|
|
opids: Vec<OpId>,
|
|
cmp: F,
|
|
) -> Option<Self> {
|
|
if are_sorted_and_unique(opids.iter(), cmp) {
|
|
Some(Self(opids))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Add an op to this set of OpIds. The `comparator` must provide a
|
|
/// consistent ordering between successive calls to `add`.
|
|
pub(crate) fn add<F: Fn(&OpId, &OpId) -> std::cmp::Ordering>(
|
|
&mut self,
|
|
opid: OpId,
|
|
comparator: F,
|
|
) {
|
|
use std::cmp::Ordering::*;
|
|
if self.is_empty() {
|
|
self.0.push(opid);
|
|
return;
|
|
}
|
|
let idx_and_elem = self
|
|
.0
|
|
.iter()
|
|
.find_position(|an_opid| matches!(comparator(an_opid, &opid), Greater | Equal));
|
|
if let Some((idx, an_opid)) = idx_and_elem {
|
|
if comparator(an_opid, &opid) == Equal {
|
|
// nothing to do
|
|
} else {
|
|
self.0.insert(idx, opid);
|
|
}
|
|
} else {
|
|
self.0.push(opid);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn retain<F: Fn(&OpId) -> bool>(&mut self, f: F) {
|
|
self.0.retain(f)
|
|
}
|
|
|
|
pub(crate) fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
pub(crate) fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
|
|
pub(crate) fn iter(&self) -> std::slice::Iter<'_, OpId> {
|
|
self.0.iter()
|
|
}
|
|
|
|
pub(crate) fn contains(&self, op: &OpId) -> bool {
|
|
self.0.contains(op)
|
|
}
|
|
|
|
pub(crate) fn get(&self, idx: usize) -> Option<&OpId> {
|
|
self.0.get(idx)
|
|
}
|
|
}
|
|
|
|
fn are_sorted_and_unique<
|
|
'a,
|
|
I: Iterator<Item = &'a OpId>,
|
|
F: FnMut(&OpId, &OpId) -> std::cmp::Ordering,
|
|
>(
|
|
mut opids: I,
|
|
mut f: F,
|
|
) -> bool {
|
|
use std::cmp::Ordering;
|
|
let mut last = match opids.next() {
|
|
Some(e) => e,
|
|
None => return true,
|
|
};
|
|
|
|
for next in opids {
|
|
if matches!(f(last, next), Ordering::Greater | Ordering::Equal) {
|
|
return false;
|
|
}
|
|
last = next;
|
|
}
|
|
true
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{OpId, OpIds};
|
|
use crate::ActorId;
|
|
use proptest::prelude::*;
|
|
|
|
fn gen_opid(actors: Vec<ActorId>) -> impl Strategy<Value = OpId> {
|
|
(0..actors.len()).prop_flat_map(|actor_idx| {
|
|
(Just(actor_idx), 0..u64::MAX).prop_map(|(actor_idx, counter)| OpId(counter, actor_idx))
|
|
})
|
|
}
|
|
|
|
fn scenario(size: std::ops::Range<usize>) -> impl Strategy<Value = (Vec<ActorId>, Vec<OpId>)> {
|
|
let actors = vec![
|
|
"aaaa".try_into().unwrap(),
|
|
"cccc".try_into().unwrap(),
|
|
"bbbb".try_into().unwrap(),
|
|
];
|
|
proptest::collection::vec(gen_opid(actors.clone()), size)
|
|
.prop_map(move |opids| (actors.clone(), opids))
|
|
}
|
|
|
|
fn duplicate_unsorted_scenario() -> impl Strategy<Value = (Vec<ActorId>, Vec<OpId>)> {
|
|
scenario(1..100).prop_map(|(actors, mut opids)| {
|
|
let mut sorted_opids = opids.clone();
|
|
sorted_opids.sort_by(|left, right| cmp(&actors, left, right));
|
|
sorted_opids.dedup();
|
|
// Unwrap is okay due to the size we pass to `scenario()`
|
|
let last = *sorted_opids.last().unwrap();
|
|
if sorted_opids == opids {
|
|
// Opids are sorted and deduplicated, just copy the last opid and insert it at the
|
|
// front
|
|
opids.insert(0, last);
|
|
}
|
|
(actors, opids)
|
|
})
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_sorted_opids((actors, opids) in scenario(0..100)) {
|
|
let mut sorted_opids = OpIds::default();
|
|
for opid in &opids {
|
|
sorted_opids.add(*opid, |left, right| cmp(&actors, left, right));
|
|
}
|
|
let result = sorted_opids.into_iter().cloned().collect::<Vec<_>>();
|
|
let mut expected = opids;
|
|
expected.sort_by(|left, right| cmp(&actors, left, right));
|
|
expected.dedup();
|
|
assert_eq!(result, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_new_if_sorted((actors, opids) in duplicate_unsorted_scenario()) {
|
|
let mut expected = opids.clone();
|
|
assert_eq!(OpIds::new_if_sorted(opids, |left, right| cmp(&actors, left, right)), None);
|
|
expected.sort_by(|left, right| cmp(&actors, left, right));
|
|
expected.dedup();
|
|
let result = OpIds::new_if_sorted(expected.clone(), |left, right| cmp(&actors, left, right)).unwrap().into_iter().cloned().collect::<Vec<_>>();
|
|
assert_eq!(result, expected)
|
|
}
|
|
}
|
|
|
|
fn cmp(actors: &[ActorId], left: &OpId, right: &OpId) -> std::cmp::Ordering {
|
|
use std::cmp::Ordering;
|
|
match (left, right) {
|
|
(OpId(0, _), OpId(0, _)) => Ordering::Equal,
|
|
(OpId(0, _), OpId(_, _)) => Ordering::Less,
|
|
(OpId(_, _), OpId(0, _)) => Ordering::Greater,
|
|
(OpId(a, x), OpId(b, y)) if a == b => actors[*x].cmp(&actors[*y]),
|
|
(OpId(a, _), OpId(b, _)) => a.cmp(b),
|
|
}
|
|
}
|
|
}
|