diff --git a/radicle-crdt/Cargo.toml b/radicle-crdt/Cargo.toml index a652a8e0..991391c4 100644 --- a/radicle-crdt/Cargo.toml +++ b/radicle-crdt/Cargo.toml @@ -4,12 +4,13 @@ version = "0.1.0" edition = "2021" [features] -test = ["fastrand"] +test = ["fastrand", "quickcheck"] [dependencies] fastrand = { version = "1.8.0", optional = true } num-traits = { version = "0.2.15", default-features = false, features = ["std"] } olpc-cjson = { version = "0.1.1" } +quickcheck = { version = "1", optional = true } serde = { version = "1" } serde_json = { version = "1" } diff --git a/radicle-crdt/src/lib.rs b/radicle-crdt/src/lib.rs index bd09ea88..698b08d8 100644 --- a/radicle-crdt/src/lib.rs +++ b/radicle-crdt/src/lib.rs @@ -1,4 +1,5 @@ #![allow(clippy::collapsible_if)] +#![allow(clippy::bool_assert_comparison)] #![allow(clippy::collapsible_else_if)] #![allow(clippy::type_complexity)] pub mod change; @@ -15,6 +16,12 @@ pub mod test; //////////////////////////////////////////////////////////////////////////////// pub use change::*; +pub use clock::LClock; +pub use lwwmap::LWWMap; +pub use lwwreg::LWWReg; +pub use lwwset::LWWSet; +pub use ord::{Max, Min}; +pub use redactable::Redactable; //////////////////////////////////////////////////////////////////////////////// @@ -70,9 +77,78 @@ impl Semilattice for HashMap { } } +impl Semilattice for Option { + fn merge(&mut self, other: Self) { + match (self, other) { + (this @ None, other @ Some(_)) => { + *this = other; + } + (Some(ref mut a), Some(b)) => { + a.merge(b); + } + (Some(_), None) => {} + (None, None) => {} + } + } +} + +impl Semilattice for () { + fn merge(&mut self, _other: Self) {} +} + +impl Semilattice for bool { + fn merge(&mut self, other: Self) { + match (&self, other) { + (false, true) => *self = true, + (true, false) => *self = true, + (false, false) | (true, true) => {} + } + } +} + pub fn fold(i: impl IntoIterator) -> S where S: Semilattice + Default, { i.into_iter().fold(S::default(), S::join) } + +#[cfg(test)] +mod tests { + use crate::{test, Max, Min, Semilattice}; + use quickcheck_macros::quickcheck; + + #[quickcheck] + fn prop_option_laws(a: Max, b: Max, c: Max) { + test::assert_laws(&a, &b, &c); + } + + #[quickcheck] + fn prop_bool_laws(a: bool, b: bool, c: bool) { + test::assert_laws(&a, &b, &c); + } + + #[test] + fn test_bool() { + assert_eq!(false.join(false), false); + assert_eq!(true.join(true), true); + assert_eq!(true.join(false), true); + assert_eq!(false.join(true), true); + } + + #[test] + fn test_option() { + assert_eq!(None::<()>.join(None), None); + assert_eq!(None::<()>.join(Some(())), Some(())); + assert_eq!(Some(()).join(None), Some(())); + assert_eq!(Some(()).join(Some(())), Some(())); + assert_eq!( + Some(Max::from(0)).join(Some(Max::from(1))), + Some(Max::from(1)) + ); + assert_eq!( + Some(Min::from(0)).join(Some(Min::from(1))), + Some(Min::from(0)) + ); + } +} diff --git a/radicle-crdt/src/lwwmap.rs b/radicle-crdt/src/lwwmap.rs index 6792eb38..20bfa24c 100644 --- a/radicle-crdt/src/lwwmap.rs +++ b/radicle-crdt/src/lwwmap.rs @@ -10,7 +10,7 @@ pub struct LWWMap { inner: BTreeMap, C>>, } -impl LWWMap { +impl LWWMap { pub fn singleton(key: K, value: V, clock: C) -> Self { Self { inner: BTreeMap::from_iter([(key, LWWReg::new(Some(value), clock))]), @@ -66,7 +66,9 @@ impl Default for LWWMap { } } -impl FromIterator<(K, V, C)> for LWWMap { +impl FromIterator<(K, V, C)> + for LWWMap +{ fn from_iter>(iter: I) -> Self { let mut map = LWWMap::default(); for (k, v, c) in iter.into_iter() { @@ -76,7 +78,9 @@ impl FromIterator<(K, V, C)> for LWWM } } -impl Extend<(K, V, C)> for LWWMap { +impl Extend<(K, V, C)> + for LWWMap +{ fn extend>(&mut self, iter: I) { for (k, v, c) in iter.into_iter() { self.insert(k, v, c); @@ -87,7 +91,7 @@ impl Extend<(K, V, C)> for LWWMap Semilattice for LWWMap where K: Ord, - V: PartialOrd + Eq, + V: Semilattice + PartialOrd + Eq, C: Ord + Copy + Default, { fn merge(&mut self, other: Self) { @@ -106,15 +110,17 @@ where #[cfg(test)] mod tests { - use super::*; use quickcheck_macros::quickcheck; + use super::*; + use crate::ord::Max; + #[quickcheck] fn prop_semilattice( - a: Vec<(u8, u8, u16)>, - b: Vec<(u8, u8, u16)>, - c: Vec<(u8, u8, u16)>, - mix: Vec<(u8, u8, u16)>, + a: Vec<(u8, Max, u16)>, + b: Vec<(u8, Max, u16)>, + c: Vec<(u8, Max, u16)>, + mix: Vec<(u8, Max, u16)>, ) { let mut a = LWWMap::from_iter(a); let mut b = LWWMap::from_iter(b); @@ -130,18 +136,18 @@ mod tests { fn test_insert() { let mut map = LWWMap::default(); - map.insert('a', 1, 0); - map.insert('b', 2, 0); - map.insert('c', 3, 0); + map.insert('a', Max::from(1), 0); + map.insert('b', Max::from(2), 0); + map.insert('c', Max::from(3), 0); - assert_eq!(map.get('a'), Some(&1)); - assert_eq!(map.get('b'), Some(&2)); + assert_eq!(map.get('a'), Some(&Max::from(1))); + assert_eq!(map.get('b'), Some(&Max::from(2))); assert_eq!(map.get('?'), None); - let values = map.iter().collect::>(); - assert!(values.contains(&(&'a', &1))); - assert!(values.contains(&(&'b', &2))); - assert!(values.contains(&(&'c', &3))); + let values = map.iter().collect::)>>(); + assert!(values.contains(&(&'a', &Max::from(1)))); + assert!(values.contains(&(&'b', &Max::from(2)))); + assert!(values.contains(&(&'c', &Max::from(3)))); assert_eq!(values.len(), 3); } @@ -149,7 +155,7 @@ mod tests { fn test_insert_remove() { let mut map = LWWMap::default(); - map.insert('a', "alice", 1); + map.insert('a', Max::from("alice"), 1); assert!(map.contains_key('a')); map.remove('a', 0); @@ -168,16 +174,16 @@ mod tests { fn test_remove_insert() { let mut map = LWWMap::default(); - map.insert('a', "alice", 1); - assert_eq!(map.get('a'), Some(&"alice")); + map.insert('a', Max::from("alice"), 1); + assert_eq!(map.get('a'), Some(&Max::from("alice"))); map.remove('a', 2); assert!(!map.contains_key('a')); - map.insert('a', "alice", 1); + map.insert('a', Max::from("alice"), 1); assert!(!map.contains_key('a')); - map.insert('a', "amy", 2); - assert_eq!(map.get('a'), Some(&"amy")); + map.insert('a', Max::from("amy"), 2); + assert_eq!(map.get('a'), Some(&Max::from("amy"))); } } diff --git a/radicle-crdt/src/lwwreg.rs b/radicle-crdt/src/lwwreg.rs index 78a4eff2..86f9aa8c 100644 --- a/radicle-crdt/src/lwwreg.rs +++ b/radicle-crdt/src/lwwreg.rs @@ -3,55 +3,65 @@ use crate::Semilattice; /// Last-Write-Wins Register. /// -/// In case of conflict, biased towards larger values. +/// In case of conflict, uses the [`Semilattice`] instance of `T` to merge. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LWWReg { - /// Nb. the order of the tuple types ensures we bias towards the clock before the value. - inner: Max<(C, T)>, + clock: Max, + value: T, } -impl LWWReg { +impl LWWReg { pub fn new(value: T, clock: C) -> Self { Self { - inner: Max::from((clock, value)), + clock: Max::from(clock), + value, } } - pub fn set(&mut self, value: T, clock: C) { - self.inner.merge(Max::from((clock, value))); + pub fn set(&mut self, value: impl Into, clock: C) { + let clock = Max::from(clock); + let value = value.into(); + + if clock == self.clock { + self.value.merge(value); + } else if clock > self.clock { + self.clock.merge(clock); + self.value = value; + } } pub fn get(&self) -> &T { - &self.inner.get().1 + &self.value } - pub fn clock(&self) -> &C { - &self.inner.get().0 + pub fn clock(&self) -> &Max { + &self.clock } pub fn into_inner(self) -> (T, C) { - let (t, c) = self.inner.into_inner(); - (c, t) + (self.value, self.clock.into_inner()) } } impl Semilattice for LWWReg where - T: PartialOrd + Default, + T: PartialOrd + Default + Semilattice, C: PartialOrd + Default, { fn merge(&mut self, other: Self) { - self.inner.merge(other.inner); + self.set(other.value, other.clock.into_inner()); } } #[cfg(test)] mod tests { - use super::*; use quickcheck_macros::quickcheck; + use super::*; + use crate::Min; + #[quickcheck] - fn prop_semilattice(a: (u8, u16), b: (u8, u16), c: (u8, u16)) { + fn prop_semilattice(a: (Max, u16), b: (Max, u16), c: (Max, u16)) { let a = LWWReg::new(a.0, a.1); let b = LWWReg::new(b.0, b.1); let c = LWWReg::new(c.0, c.1); @@ -59,31 +69,44 @@ mod tests { crate::test::assert_laws(&a, &b, &c); } + #[test] + fn test_merge() { + let a = LWWReg::new(Max::from(0), 0); + let b = LWWReg::new(Max::from(1), 0); + + assert_eq!(a.join(b).get(), &Max::from(1)); + + let a = LWWReg::new(Min::from(0), 0); + let b = LWWReg::new(Min::from(1), 0); + + assert_eq!(a.join(b).get(), &Min::from(0)); + } + #[test] fn test_set_get() { - let mut reg = LWWReg::new(42, 1); - assert_eq!(*reg.get(), 42); + let mut reg = LWWReg::new(Max::from(42), 1); + assert_eq!(*reg.get(), Max::from(42)); reg.set(84, 0); - assert_eq!(*reg.get(), 42); + assert_eq!(*reg.get(), Max::from(42)); reg.set(84, 2); - assert_eq!(*reg.get(), 84); + assert_eq!(*reg.get(), Max::from(84)); // Smaller value, same clock: smaller value loses. reg.set(42, 2); - assert_eq!(*reg.get(), 84); + assert_eq!(*reg.get(), Max::from(84)); // Bigger value, same clock: bigger value wins. reg.set(168, 2); - assert_eq!(*reg.get(), 168); + assert_eq!(*reg.get(), Max::from(168)); // Smaller value, newer clock: smaller value wins. reg.set(42, 3); - assert_eq!(*reg.get(), 42); + assert_eq!(*reg.get(), Max::from(42)); // Same value, newer clock: newer clock is set. reg.set(42, 4); - assert_eq!(*reg.clock(), 4); + assert_eq!(*reg.clock(), Max::from(4)); } } diff --git a/radicle-crdt/src/ord.rs b/radicle-crdt/src/ord.rs index a3109d4a..c6115cdc 100644 --- a/radicle-crdt/src/ord.rs +++ b/radicle-crdt/src/ord.rs @@ -99,3 +99,28 @@ where other.0.partial_cmp(&self.0) } } + +impl Semilattice for Min { + fn merge(&mut self, other: Self) { + if other.0 < self.0 { + self.0 = other.0; + } + } +} + +#[cfg(any(test, feature = "test"))] +mod arbitrary { + use super::*; + + impl quickcheck::Arbitrary for Max { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self::from(T::arbitrary(g)) + } + } + + impl quickcheck::Arbitrary for Min { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self::from(T::arbitrary(g)) + } + } +} diff --git a/radicle-crdt/src/redactable.rs b/radicle-crdt/src/redactable.rs index 46bfaadc..2fc83511 100644 --- a/radicle-crdt/src/redactable.rs +++ b/radicle-crdt/src/redactable.rs @@ -2,15 +2,19 @@ use crate::Semilattice; /// An object that can be either present or removed. /// +/// The "redacted" state is the top-most element and takes precedence +/// over other states. +/// +/// There is no `Default` instance, as it wouldn't obbey semilattice laws. +/// /// Nb. The merge rules are such that if two redactables with different /// values present are merged; the result is redacted. This is the preserve /// the semilattice laws. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Redactable { /// When the object is present. Present(T), /// When the object has been removed. - #[default] Redacted, } diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index f2300b4e..a2143730 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -95,6 +95,7 @@ impl store::FromHistory for Thread { })) } } + impl Semilattice for Thread { fn merge(&mut self, other: Self) { self.comments.merge(other.comments);