use self::interval::{Interval, IntervalSet};
use interval::IntervalError;
use std::{
cmp::Ordering,
collections::{BTreeMap, btree_map::Entry},
fmt,
num::NonZeroU64,
ops::{BitAnd, Sub},
};
mod interval;
pub const MAX_APPLICATION_ID: u16 = (1 << 12) - 1;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct InvalidPriority(pub u8);
impl fmt::Display for InvalidPriority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid priority {}", self.0)
}
}
impl std::error::Error for InvalidPriority {}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum IdentifierError {
Priority(InvalidPriority),
InvalidBits { field: &'static str, value: u32 },
}
impl fmt::Display for IdentifierError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IdentifierError::Priority(err) => write!(f, "{err}"),
IdentifierError::InvalidBits { field, value } => {
write!(f, "invalid value {value} for field {field}")
}
}
}
}
impl std::error::Error for IdentifierError {}
impl From<InvalidPriority> for IdentifierError {
fn from(value: InvalidPriority) -> Self {
Self::Priority(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
#[repr(u8)] pub enum Priority {
Local = 0,
Low = 2,
Medium = 4,
High = 6,
}
pub const PRIORITY_MAX: Priority = Priority::High;
impl TryFrom<u8> for Priority {
type Error = InvalidPriority;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(match value {
0 => Priority::Local,
2 => Priority::Low,
4 => Priority::Medium,
6 => Priority::High,
_ => return Err(InvalidPriority(value)),
})
}
}
pub const ROOT_APP_ID: u16 = 0;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
pub struct Identifier {
bits: u32,
}
impl std::fmt::Debug for Identifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.priority() {
Priority::High => write!(f, "@{}.{}h", self.node(), self.app()),
Priority::Medium => write!(f, "@{}.{}", self.node(), self.app()),
Priority::Low => write!(f, "@{}.{}l", self.node(), self.app()),
Priority::Local => write!(f, "@{}.{}-", self.node(), self.app()),
}
}
}
impl From<(u8, u16)> for Identifier {
fn from((node, application): (u8, u16)) -> Self {
Identifier::new(node, application)
}
}
impl PartialEq<(u8, u16)> for Identifier {
fn eq(&self, &(node, application): &(u8, u16)) -> bool {
self == &Identifier::new(node, application)
}
}
impl TryFrom<u32> for Identifier {
type Error = IdentifierError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
const BIT_FIELDS: [(u32, u32); 5] = [(0, 7), (8, 19), (20, 20), (21, 23), (24, 31)];
let [_node_id, _app_id, reserved, priority, unused] = BIT_FIELDS.map(|(start, end)| {
let bits_count = end - start + 1;
let mask = (!0u32) >> (u32::BITS - bits_count);
let shift = u32::BITS - end - 1;
(value >> shift) & mask
});
let _priority = Priority::try_from(priority as u8)?;
if reserved != 0 {
return Err(IdentifierError::InvalidBits {
field: "reserved",
value: reserved,
});
}
if unused != 0 {
return Err(IdentifierError::InvalidBits {
field: "unused",
value: unused,
});
}
Ok(Self { bits: value })
}
}
impl Identifier {
pub const fn new(node: u8, application: u16) -> Self {
if application > MAX_APPLICATION_ID {
panic!("application exceeds u12");
}
Self {
bits: ((node as u32) << (12 + 1 + 3 + 8)) | ((application as u32) << (1 + 3 + 8)),
}
.with_priority(Priority::Medium)
}
pub fn checked_successor(self) -> Option<Identifier> {
if self.app() != MAX_APPLICATION_ID {
return Some(Identifier::new(self.node().value(), self.app() + 1));
}
if self.node() != NodeId::MAX {
return Some(Identifier::new(self.node().value() + 1, self.app()));
}
None
}
pub const fn node(&self) -> NodeId {
NodeId {
node_id: (self.bits >> (12 + 1 + 3 + 8)) as u8,
}
}
pub const fn app(&self) -> u16 {
((self.bits >> (1 + 3 + 8)) & 0xfff) as u16
}
pub const fn priority(&self) -> Priority {
let bits = ((self.bits >> 8) & 0b111) as u8;
match bits {
0 if Priority::Local as u8 == 0 => Priority::Local,
2 if Priority::Low as u8 == 2 => Priority::Low,
4 if Priority::Medium as u8 == 4 => Priority::Medium,
6 if Priority::High as u8 == 6 => Priority::High,
_ if cfg!(debug_assertions) => {
panic!("illegal priority")
}
_ => unsafe { std::hint::unreachable_unchecked() },
}
}
pub const fn with_priority(self, priority: Priority) -> Self {
Self {
bits: (self.bits & !(0b111 << 8)) | ((priority as u32) << 8),
}
}
pub const fn bits(&self) -> u32 {
self.bits
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
pub struct NodeId {
node_id: u8,
}
impl NodeId {
pub const MAX: NodeId = NodeId::new(u8::MAX);
pub const fn value(self) -> u8 {
self.node_id
}
pub const fn new(node_id: u8) -> Self {
NodeId { node_id }
}
pub fn identifier(self) -> Identifier {
Identifier::new(self.node_id, ROOT_APP_ID)
}
}
impl std::fmt::Debug for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.node_id)
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.node_id, f)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
pub struct Dot(Identifier, NonZeroU64);
impl std::fmt::Debug for Dot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({:?}, {})", self.0, self.1)
}
}
impl<I> From<(I, NonZeroU64)> for Dot
where
I: Into<Identifier>,
{
fn from((id, seq): (I, NonZeroU64)) -> Self {
Self(id.into(), seq)
}
}
impl Dot {
pub fn successor(&self) -> Dot {
Dot::mint(self.0, self.1.get().wrapping_add(1).max(1))
}
pub const fn mint(id: Identifier, seq: u64) -> Self {
Self(
id,
if let Some(seq) = NonZeroU64::new(seq) {
seq
} else {
panic!("attempted to construct Dot for 0th sequence number");
},
)
}
pub const fn with_priority(self, priority: Priority) -> Self {
Self(self.0.with_priority(priority), self.1)
}
pub fn actor(&self) -> Identifier {
self.0
}
pub fn sequence(&self) -> NonZeroU64 {
self.1
}
}
impl PartialEq<(Identifier, u64)> for Dot {
fn eq(&self, other: &(Identifier, u64)) -> bool {
self.0 == other.0 && self.1.get() == other.1
}
}
impl PartialEq<((u8, u16), u64)> for Dot {
fn eq(&self, other: &((u8, u16), u64)) -> bool {
self.0 == Identifier::from(other.0) && self.1.get() == other.1
}
}
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
pub struct CausalContext {
dots: BTreeMap<Identifier, IntervalSet>,
}
impl std::fmt::Debug for CausalContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("CausalContext").field(&self.dots).finish()
}
}
impl CausalContext {
pub fn new() -> Self {
Self::default()
}
pub fn from_intervals<I, RI>(iter: I) -> Result<Self, IntervalError>
where
I: IntoIterator<Item = (Identifier, RI)>,
RI: IntoIterator<Item = (NonZeroU64, Option<NonZeroU64>)>,
{
let mut dots = BTreeMap::new();
for (id, bounds) in iter {
dots.insert(id, IntervalSet::from_intervals(bounds)?);
}
Ok(Self { dots })
}
pub fn next_dot_for(&self, id: Identifier) -> Dot {
self.next_n_dots_for(1, id)
.next()
.expect("yields as many as are indicated")
}
pub fn unused_identifier(&self) -> Option<Identifier> {
let mut candidate = Identifier::new(0, 0);
for key in self.dots.keys() {
if candidate < *key {
return Some(candidate);
}
candidate = key.checked_successor()?;
}
Some(candidate)
}
pub fn largest_for_node(&self, node: u8) -> impl Iterator<Item = Dot> + '_ {
self.dots
.range(Identifier::new(node, 0)..=Identifier::new(node, MAX_APPLICATION_ID))
.filter_map(|(k, v)| v.last().map(|last_element| Dot(*k, last_element.end())))
}
pub fn next_n_dots_for(
&self,
n: u8,
id: Identifier,
) -> impl Iterator<Item = Dot> + Clone + use<> {
let spans = self.dots.get(&id);
debug_assert!(
spans
.map(|seqs| seqs.len() == 1
&& seqs.first().expect("not empty").start() == NonZeroU64::MIN)
.unwrap_or(true),
"dots for self.id are not sequential and compacted in {:?}",
self.dots
);
let next_seq = spans
.map(IntervalSet::next_after)
.unwrap_or(NonZeroU64::MIN);
(next_seq.get()..next_seq.get() + u64::from(n)).map(move |seq| {
let seq = unsafe { NonZeroU64::new_unchecked(seq) };
Dot(id, seq)
})
}
#[cfg(test)]
pub fn dots_for(&self, id: Identifier) -> impl Iterator<Item = Dot> + '_ {
let dots: Vec<_> = self
.dots
.get(&id)
.iter()
.flat_map(|ivals| ivals.seqs().map(|seq| Dot::mint(id, seq.get())))
.collect();
dots.into_iter()
}
pub fn dots(&self) -> impl Iterator<Item = Dot> + '_ {
self.dots
.iter()
.flat_map(|(id, ivals)| ivals.seqs().map(|seq| (*id, seq).into()))
}
pub fn is_empty(&self) -> bool {
debug_assert!(
self.dots.values().all(|v| !v.is_empty()),
"should not retain empty interval sets"
);
self.dots.is_empty()
}
pub fn size(&self) -> usize {
std::mem::size_of::<Self>()
+ self.dots.len()
* (std::mem::size_of::<Identifier>() + std::mem::size_of::<IntervalSet>())
+ self
.dots
.values()
.map(|ivals| ivals.len() * std::mem::size_of::<Interval>())
.sum::<usize>()
}
#[must_use]
pub fn dot_count(&self) -> u64 {
self.dots
.values()
.map(|ivals| ivals.total_interval_length())
.sum()
}
#[must_use]
pub fn dot_in(&self, dot: Dot) -> bool {
self.dots
.get(&dot.actor())
.is_some_and(|s| s.contains(dot.sequence()))
}
pub fn one(&self) -> Option<Dot> {
self.dots
.iter()
.flat_map(|(id, ivals)| ivals.first().map(|ival| Dot::from((*id, ival.start()))))
.next()
}
pub fn is_compact_for_node(&self, node: u8) -> bool {
self.dots
.range(
Identifier::new(node, 0)
..=Identifier::new(node, MAX_APPLICATION_ID).with_priority(PRIORITY_MAX),
)
.all(|(_, spans)| {
spans.len() == 1 && spans.first().expect("not empty").start() == NonZeroU64::MIN
})
}
pub fn insert_dot(&mut self, dot: Dot) {
self.dots
.entry(dot.actor())
.or_insert_with(IntervalSet::new)
.insert(dot.sequence());
}
pub fn insert_next_dot(&mut self, dot: Dot) {
match self.dots.entry(dot.actor()) {
Entry::Vacant(v) => {
assert_eq!(dot.sequence(), NonZeroU64::MIN);
v.insert(IntervalSet::single(dot.sequence()));
}
Entry::Occupied(mut o) => {
let next = o.get().next_after();
assert_eq!(dot.sequence(), next);
o.get_mut().extend_end_by_one();
}
}
}
pub(crate) fn insert_dots(&mut self, dots: impl IntoIterator<Item = Dot>) {
for dot in dots {
match self.dots.entry(dot.actor()) {
Entry::Vacant(v) => {
v.insert(IntervalSet::single(dot.sequence()));
}
Entry::Occupied(mut o) => {
o.get_mut().insert(dot.sequence());
}
}
}
}
pub fn remove_dot(&mut self, dot: Dot) -> bool {
let Some(ivals) = self.dots.get_mut(&dot.actor()) else {
return false;
};
let removed = ivals.remove(dot.sequence());
if ivals.is_empty() {
self.dots.remove(&dot.actor());
}
removed
}
pub fn remove_dots_in(&mut self, remove: &CausalContext) {
self.dots.retain(|k, v1| {
if let Some(v2) = remove.dots.get(k) {
*v1 = v1.difference(v2);
!v1.is_empty()
} else {
true
}
})
}
pub fn union(&mut self, other: &CausalContext) {
for (k, v1) in &mut self.dots {
if let Some(v2) = other.dots.get(k) {
*v1 = v1.union(v2);
}
}
for (k, v2) in &other.dots {
if !self.dots.contains_key(k) {
self.dots.insert(*k, v2.clone());
}
}
}
pub fn retain_from(&mut self, mut f: impl FnMut(Identifier) -> bool) {
self.dots.retain(|&id, _| f(id));
}
pub fn any_dot_id_with(&self, mut f: impl FnMut(Identifier) -> bool) -> bool {
self.dots.keys().any(|&id| f(id))
}
pub fn any_dot_in(&self, other: &Self) -> bool {
let (smaller_dots, larger_dots) = if self.dots.len() > other.dots.len() {
(&other.dots, &self.dots)
} else {
(&self.dots, &other.dots)
};
for (k, ivals1) in smaller_dots {
if let Some(ivals2) = larger_dots.get(k)
&& ivals1.intersects(ivals2)
{
return true;
}
}
false
}
pub fn intervals(
&self,
) -> impl ExactSizeIterator<
Item = (
Identifier,
impl ExactSizeIterator<Item = (NonZeroU64, Option<NonZeroU64>)> + '_,
),
> + '_ {
self.dots.iter().map(|(id, set)| (*id, set.intervals()))
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub enum ComparisonMode {
#[default]
Full,
IgnoreLocal,
}
impl CausalContext {
pub fn partial_cmp_dots(
&self,
other: &CausalContext,
mode: ComparisonMode,
) -> Option<Ordering> {
let remove_local = |&(id, _): &(&Identifier, _)| {
mode == ComparisonMode::Full || id.priority() != Priority::Local
};
let mut ours = self.dots.iter().filter(remove_local).peekable();
let mut theirs = other.dots.iter().filter(remove_local).peekable();
let (mut o_unique, mut t_unique) = (false, false);
loop {
if o_unique && t_unique {
return None;
}
match (ours.peek(), theirs.peek()) {
(None, None) => {
break;
}
(None, Some(_)) => {
t_unique = true;
break;
}
(Some(_), None) => {
o_unique = true;
break;
}
(Some((o_id, o_ival)), Some((t_id, t_ival))) => {
match o_id.cmp(t_id) {
Ordering::Equal => {
match o_ival.partial_cmp(t_ival) {
Some(Ordering::Equal) => (),
Some(Ordering::Less) => t_unique = true,
Some(Ordering::Greater) => o_unique = true,
None => return None,
}
ours.next();
theirs.next();
}
Ordering::Less => {
o_unique = true;
ours.next();
}
Ordering::Greater => {
t_unique = true;
theirs.next();
}
}
}
}
}
match (o_unique, t_unique) {
(true, true) => None,
(true, false) => Some(Ordering::Greater),
(false, true) => Some(Ordering::Less),
(false, false) => Some(Ordering::Equal),
}
}
pub fn after(&self, other: &CausalContext) -> bool {
self.partial_cmp_dots(other, ComparisonMode::default()) == Some(Ordering::Greater)
}
pub fn happened_before(&self, other: &CausalContext) -> bool {
other.partial_cmp_dots(self, ComparisonMode::default()) == Some(Ordering::Greater)
}
}
impl PartialEq for CausalContext {
fn eq(&self, other: &Self) -> bool {
self.partial_cmp_dots(other, ComparisonMode::default()) == Some(Ordering::Equal)
}
}
impl PartialOrd for CausalContext {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.partial_cmp_dots(other, ComparisonMode::default())
}
}
impl Sub<&CausalContext> for &CausalContext {
type Output = CausalContext;
fn sub(self, rhs: &CausalContext) -> Self::Output {
let mut out = CausalContext::new();
for (id, left) in &self.dots {
if let Some(right) = rhs.dots.get(id) {
let diff = left.difference(right);
if !diff.is_empty() {
out.dots.insert(*id, left.difference(right));
}
} else {
out.dots.insert(*id, left.clone());
}
}
out
}
}
impl BitAnd<&CausalContext> for &CausalContext {
type Output = CausalContext;
fn bitand(self, rhs: &CausalContext) -> Self::Output {
let mut out = CausalContext::new();
for (id, v1) in &self.dots {
if let Some(v2) = rhs.dots.get(id) {
let intersection = v1.intersection(v2);
if !intersection.is_empty() {
out.dots.insert(*id, intersection);
}
}
}
out
}
}
impl FromIterator<Dot> for CausalContext {
fn from_iter<T: IntoIterator<Item = Dot>>(iter: T) -> Self {
let mut cc = CausalContext::default();
cc.insert_dots(iter);
cc
}
}
impl Extend<Dot> for CausalContext {
fn extend<T: IntoIterator<Item = Dot>>(&mut self, iter: T) {
self.insert_dots(iter);
}
}
impl Extend<CausalContext> for CausalContext {
fn extend<T: IntoIterator<Item = CausalContext>>(&mut self, iter: T) {
for cc in iter {
self.union(&cc);
}
}
}
impl<'cc> Extend<&'cc CausalContext> for CausalContext {
fn extend<T: IntoIterator<Item = &'cc CausalContext>>(&mut self, iter: T) {
for cc in iter {
self.union(cc);
}
}
}
#[macro_export]
macro_rules! causal_context(
( { $( @$frac_id:literal : {$( $start:literal $(..=$end:literal)? ),*} ),* } ) => {
{
let tracks = vec![
$(
{
let mut frac_id_str = stringify!($frac_id).splitn(2,'.');
let node = frac_id_str.next().unwrap().parse().expect("node number must be numerical");
let app = frac_id_str.next().expect("missing '.' after node number").parse().expect("app must be numerical");
let id = $crate::Identifier::new(node, app);
let track = vec![
$(
{
#[allow(unused_mut)]
let mut temp = ($start.try_into().unwrap(), None);
$(
temp.1 = Some(
$end.try_into().unwrap()
);
)*
temp
},
)*
];
(id, track)
},
)*
];
$crate::CausalContext::from_intervals(tracks).expect("invalid interval encountered")
}
}
);
#[cfg(test)]
mod tests {
use super::*;
use crate::causal_context;
use std::collections::HashSet;
impl quickcheck::Arbitrary for Priority {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
*g.choose(&[
Priority::Local,
Priority::Low,
Priority::Medium,
Priority::High,
])
.unwrap()
}
}
#[quickcheck]
fn identifier_bits(node: u8, application: u8, priority: Priority) {
let application = u16::from(application);
let id = Identifier::new(node, application).with_priority(priority);
assert_eq!(
(node, application, priority),
(id.node().value(), id.app(), id.priority())
);
let id_2 = Identifier::try_from(id.bits);
assert_eq!(id_2, Ok(id));
}
#[quickcheck]
fn compaction(dots: Vec<Dot>, other_dots: Vec<Dot>) -> bool {
let cc = CausalContext::from_iter(dots.iter().copied());
let has: HashSet<_> = dots.into_iter().collect();
let mut doesnt_have = other_dots.into_iter().filter(|dot| !has.contains(dot));
has.iter().all(|&dot| cc.dot_in(dot)) && doesnt_have.all(|dot| !cc.dot_in(dot))
}
#[test]
fn check_compaction_range() {
let cc = CausalContext::from_iter([
Dot(
Identifier::new(0, MAX_APPLICATION_ID).with_priority(Priority::Medium),
NonZeroU64::new(1).unwrap(),
),
Dot(
Identifier::new(0, MAX_APPLICATION_ID).with_priority(PRIORITY_MAX),
NonZeroU64::new(2).unwrap(),
),
]);
assert!(!cc.is_compact_for_node(0));
}
#[test]
#[expect(clippy::neg_cmp_op_on_partial_ord)]
fn happened_before() {
let id = Identifier::new(0, 0);
let cc1 = CausalContext::from_iter([Dot::mint((0, 0).into(), 1)]);
let mut cc2 = cc1.clone();
cc2.insert_next_dot(cc1.next_dot_for(id));
assert!(cc1.happened_before(&cc2));
assert!(!cc2.happened_before(&cc1));
assert!(!cc1.happened_before(&cc1));
assert!(cc2.after(&cc1));
assert!(!cc1.after(&cc2));
assert!(!cc1.after(&cc1));
assert!(cc2 > cc1);
assert!(!(cc2 < cc1));
}
#[quickcheck]
fn qc_happened_before(dots_both: HashSet<Dot>, mut dots_other: HashSet<Dot>) {
dots_other.retain(|dot| !dots_both.contains(dot));
if dots_other.is_empty() {
return;
}
let mut before = CausalContext::new();
let mut after = before.clone();
for dot in dots_both {
before.insert_dot(dot);
after.insert_dot(dot);
}
for dot in dots_other {
after.insert_dot(dot);
}
assert!(before != after);
assert!(!(before == after));
assert!(before.happened_before(&after));
assert!(!after.happened_before(&before));
assert!(after.after(&before));
assert!(!before.after(&after));
assert!(!before.happened_before(&before));
assert!(!after.happened_before(&after));
assert!(!before.after(&before));
assert!(!after.after(&after));
}
#[quickcheck]
fn qc_equal(dots_both: HashSet<Dot>) {
let mut before = CausalContext::new();
let mut after = before.clone();
for dot in dots_both {
before.insert_dot(dot);
after.insert_dot(dot);
}
assert!(!(before != after));
assert!(before == after);
assert!(!before.happened_before(&after));
assert!(!after.happened_before(&before));
assert!(!after.after(&before));
assert!(!before.after(&after));
assert!(!before.happened_before(&before));
assert!(!after.happened_before(&after));
assert!(!before.after(&before));
assert!(!after.after(&after));
}
#[quickcheck]
fn removal(dots: Vec<(u8, u8)>) {
let dots: HashSet<Dot> = dots
.into_iter()
.map(|(id, seq)| Dot::mint((id, 0).into(), u64::from(seq) + 1))
.collect();
let mut cc = CausalContext::from_iter(dots.iter().copied());
for dot in dots {
assert!(cc.dot_in(dot));
assert!(cc.remove_dot(dot));
assert!(!cc.dot_in(dot));
}
}
#[quickcheck]
fn any_dot_in(a_dots: Vec<(bool, u8)>, b_dots: Vec<(bool, u8)>) {
let a_dots: HashSet<Dot> = a_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let b_dots: HashSet<Dot> = b_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let a_cc = CausalContext::from_iter(a_dots.iter().copied());
let b_cc = CausalContext::from_iter(b_dots.iter().copied());
if !a_dots.is_empty() {
assert!(a_cc.any_dot_in(&a_cc));
}
if !b_dots.is_empty() {
assert!(b_cc.any_dot_in(&b_cc));
}
if a_dots.is_disjoint(&b_dots) {
assert!(!a_cc.any_dot_in(&b_cc));
assert!(!b_cc.any_dot_in(&a_cc));
} else {
assert!(a_cc.any_dot_in(&b_cc));
assert!(b_cc.any_dot_in(&a_cc));
}
}
#[quickcheck]
fn intersection(a_dots: Vec<(bool, u8)>, b_dots: Vec<(bool, u8)>) {
let a_dots: HashSet<Dot> = a_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let b_dots: HashSet<Dot> = b_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let a_cc = CausalContext::from_iter(a_dots.iter().copied());
let b_cc = CausalContext::from_iter(b_dots.iter().copied());
let isect1 = &a_cc & &b_cc;
let isect2 = &b_cc & &a_cc;
for &dot in a_dots.intersection(&b_dots) {
assert!(isect1.dot_in(dot), "a & b does not have {dot:?}");
assert!(isect2.dot_in(dot), "b & a does not have {dot:?}");
}
for &dot in a_dots.symmetric_difference(&b_dots) {
assert!(!isect1.dot_in(dot), "a & b should not have {dot:?}");
assert!(!isect2.dot_in(dot), "b & a should not have {dot:?}");
}
}
#[quickcheck]
fn difference(a_dots: Vec<(bool, u8)>, b_dots: Vec<(bool, u8)>) {
let a_dots: HashSet<Dot> = a_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let b_dots: HashSet<Dot> = b_dots
.into_iter()
.map(|(id, seq)| Dot::mint((<_>::from(id), 0).into(), u64::from(seq) + 1))
.collect();
let a_cc = CausalContext::from_iter(a_dots.iter().copied());
let b_cc = CausalContext::from_iter(b_dots.iter().copied());
let diff = &a_cc - &b_cc;
for &dot in a_dots.difference(&b_dots) {
assert!(diff.dot_in(dot), "a - b does not have {dot:?}");
}
for &dot in a_dots.intersection(&b_dots) {
assert!(
!diff.dot_in(dot),
"a - b should not have {dot:?} which is in both"
);
}
for &dot in b_dots.difference(&a_dots) {
assert!(
!diff.dot_in(dot),
"a - b should not have {dot:?} which is only in b"
);
}
}
#[quickcheck]
fn remove_dots_in(dots: HashSet<(u8, u8)>) {
let dots: HashSet<Dot> = dots
.into_iter()
.map(|(id, seq)| Dot::mint((id, 0).into(), u64::from(seq) + 1))
.collect();
let mut initial = CausalContext::from_iter(dots.iter().copied());
let remove = CausalContext::from_iter(dots.iter().take(dots.len() / 2).copied());
initial.remove_dots_in(&remove);
for dot in dots.iter().take(dots.len() / 2).copied() {
assert!(!initial.dot_in(dot));
}
for dot in dots.iter().skip(dots.len() / 2).copied() {
assert!(initial.dot_in(dot));
}
}
#[test]
fn remove_in_consecutive() {
let id = (0, 0).into();
let dot1 = Dot::mint(id, 1);
let dot2 = Dot::mint(id, 2);
let dot3 = Dot::mint(id, 3);
let dot7 = Dot::mint(id, 7);
let mut cc = CausalContext::from_iter(vec![dot1, dot2, dot3, dot7]);
for dot in [dot1, dot2, dot3] {
assert!(cc.dot_in(dot));
}
assert!(cc.remove_dot(dot3));
assert!(!cc.dot_in(dot3));
assert!(cc.dot_in(dot2));
assert!(cc.dot_in(dot1));
assert!(cc.dot_in(dot7));
for dot in [dot1, dot2] {
assert!(cc.dot_in(dot));
}
assert!(cc.remove_dot(dot1));
assert!(!cc.dot_in(dot1));
assert!(cc.dot_in(dot2));
assert!(cc.dot_in(dot7));
assert!(cc.remove_dot(dot2));
assert!(!cc.dot_in(dot2));
assert!(cc.dot_in(dot7));
}
#[test]
fn try_identifier_from_u32_rejects_invalid_bits() {
let reserved_bit_used = (1u32 << (u32::BITS - 8))
| (1u32 << (u32::BITS - 20))
| (1u32 << (u32::BITS - 21))
| ((Priority::Medium as u32) << (u32::BITS - 24));
assert_eq!(
Identifier::try_from(reserved_bit_used),
Err(IdentifierError::InvalidBits {
field: "reserved",
value: 1
})
);
let unused_bits_used = (1u32 << (u32::BITS - 8))
| (1u32 << (u32::BITS - 20))
| (0u32 << (u32::BITS - 21))
| ((Priority::Medium as u32) << (u32::BITS - 24))
| 0b101;
assert_eq!(
Identifier::try_from(unused_bits_used),
Err(IdentifierError::InvalidBits {
field: "unused",
value: 0b101
})
);
let invalid_priority = (1u32 << (u32::BITS - 8))
| (1u32 << (u32::BITS - 20))
| (0u32 << (u32::BITS - 21))
| (5u32 << (u32::BITS - 24));
assert_eq!(
Identifier::try_from(invalid_priority),
Err(IdentifierError::Priority(InvalidPriority(5)))
);
}
#[test]
fn causal_context_macro_works() {
let cc = causal_context!({@5.1: {1..=4, 6}});
let dots = cc.dots().collect::<Vec<_>>();
let id = Identifier::new(5, 1);
assert_eq!(
dots,
vec![
Dot::mint(id, 1),
Dot::mint(id, 2),
Dot::mint(id, 3),
Dot::mint(id, 4),
Dot::mint(id, 6),
]
)
}
#[test]
fn causal_context_macro_roundtrips_debug_repr() {
let cc: CausalContext = causal_context!({@0.1: {1, 3..=4}, @255.4095: {1, 3..=4}});
assert_eq!(
format!("{cc:?}"),
"CausalContext({@0.1: {1, 3..=4}, @255.4095: {1, 3..=4}})"
);
}
#[expect(clippy::neg_cmp_op_on_partial_ord)]
#[test]
fn repro_miscompare() {
let remote_ctx = causal_context!({@0.1: {6}, @1.1: {1..=3}, @2.1: {1..=4}, @3.1: {1..=2}, @4.1: {1..=4, 6, 8, 10}, @5.1: {1..=4, 6}});
let own_ctx = causal_context!({@0.1: {1..=4, 6}, @1.1: {1..=3}, @2.1: {1..=4}, @3.1: {1..=2}, @4.1: {1..=4, 6, 8, 10}, @5.1: {1..=4, 6}});
assert!(remote_ctx < own_ctx);
assert!(!(remote_ctx > own_ctx));
assert!(!(remote_ctx == own_ctx));
assert!(remote_ctx != own_ctx);
}
#[expect(clippy::neg_cmp_op_on_partial_ord)]
#[test]
fn repro_miscompare_smaller() {
let remote_ctx = causal_context!({@0.1: {6}});
let own_ctx = causal_context!({@0.1: {1..=4, 6}});
assert!(remote_ctx < own_ctx);
assert!(!(remote_ctx > own_ctx));
assert!(!(remote_ctx == own_ctx));
}
#[expect(clippy::neg_cmp_op_on_partial_ord)]
#[test]
fn repro_miscompare_minimal() {
let a = causal_context!({@0.1: {2}});
let b = causal_context!({@0.1: {1}});
assert!(!(a < b));
assert!(!(a > b));
assert!(!(a == b));
}
#[quickcheck]
fn cc_compare(input_lhs: Vec<(u8, u8, u8)>, input_rhs: Vec<(u8, u8, u8)>) {
let create_cc_dots = |mut input: Vec<(u8, u8, u8)>| {
input.truncate(5);
let mut cc_dots = HashSet::new();
for (node, start, count) in input {
let node = node % 2;
for i in 1..count % 4 {
cc_dots.insert(Dot::mint(
Identifier::new(node, 0),
(start % 4).saturating_add(i).into(),
));
}
}
cc_dots
};
let lhs = create_cc_dots(input_lhs);
let rhs = create_cc_dots(input_rhs);
let cc_lhs = CausalContext::from_iter(lhs.iter().copied());
let cc_rhs = CausalContext::from_iter(rhs.iter().copied());
let correct_ord = if lhs.is_subset(&rhs) && rhs.is_subset(&lhs) {
Some(Ordering::Equal)
} else if lhs.is_subset(&rhs) {
Some(Ordering::Less)
} else if rhs.is_subset(&lhs) {
Some(Ordering::Greater)
} else {
None
};
let cc_ord = cc_lhs.partial_cmp(&cc_rhs);
assert_eq!(
cc_ord,
correct_ord,
"{}",
&format!("failed: {cc_lhs:?} cmp {cc_rhs:?}")
);
}
}