minerva 0.2.0

Causal ordering for distributed systems
//! The shaped counterexamples ruling R-17 owes this slice: why neither pure
//! kind-law candidate survives, and why no honest either-store exists at
//! all. Each dishonest store here is test-local scaffolding built to
//! *exhibit* its violation; the store-algebra note cites these tests as the
//! executable half of its closure arguments.

use super::super::super::support::dot;
use crate::metis::{Anchor, Dot, DotSet, DotStore, Kind, KindPlurality, Locus};

use super::super::{TestNode, rank};

/// The would-be coproduct store: a state is "a left" or "a right", as a sum
/// type demands. Its merge must return one of the two shapes, and that is
/// the whole problem: when the shapes differ, whichever side it keeps, the
/// other side's uncovered dots are discarded.
#[derive(Clone, Debug, PartialEq, Eq)]
enum Sum {
    Left(DotSet),
    Right(DotSet),
}

impl Default for Sum {
    fn default() -> Self {
        Self::Left(DotSet::new())
    }
}

impl Sum {
    fn set(&self) -> &DotSet {
        match self {
            Self::Left(set) | Self::Right(set) => set,
        }
    }
}

impl DotStore for Sum {
    fn dots(&self) -> impl Iterator<Item = Dot> + '_ {
        DotStore::dots(self.set())
    }

    fn is_bottom(&self) -> bool {
        self.set().is_bottom()
    }

    fn causal_merge(&self, self_context: &DotSet, other: &Self, other_context: &DotSet) -> Self {
        // Matching shapes merge lawfully; differing shapes force the sum to
        // pick one, and this merge keeps self's (any other sum-shaped rule
        // discards symmetrically, which is the point of the exhibit).
        let bottom = DotSet::new();
        let (mine, theirs) = (self.set(), other.set());
        let kept = match (self, other) {
            (Self::Left(_), Self::Left(_)) | (Self::Right(_), Self::Right(_)) => {
                mine.causal_merge(self_context, theirs, other_context)
            }
            (Self::Left(_) | Self::Right(_), _) => {
                mine.causal_merge(self_context, &bottom, other_context)
            }
        };
        match self {
            Self::Left(_) => Self::Left(kept),
            Self::Right(_) => Self::Right(kept),
        }
    }

    fn restrict(&self, roster: impl IntoIterator<Item = u32>) -> Self {
        match self {
            Self::Left(set) => Self::Left(DotStore::restrict(set, roster)),
            Self::Right(set) => Self::Right(DotStore::restrict(set, roster)),
        }
    }

    fn novel_to(&self, context: &DotSet) -> Self {
        match self {
            Self::Left(set) => Self::Left(DotStore::novel_to(set, context)),
            Self::Right(set) => Self::Right(DotStore::novel_to(set, context)),
        }
    }
}

#[test]
fn test_a_sum_shaped_merge_cannot_keep_the_survivor_law() {
    // Two covered pairs of DIFFERENT shapes, each holding a dot the other
    // side's context has never seen. The survivor law (trait law 2) demands
    // both dots survive any merge; a sum-shaped result carries one
    // component, so it discards one of them, whichever side wins. The
    // smallest lawful object containing the coproduct is therefore the
    // product (the store-algebra note's dissolution argument; the shipped
    // node is that product).
    let mut left_set = DotSet::new();
    assert!(left_set.insert(dot(1, 1)));
    let a = Sum::Left(left_set.clone());
    let a_ctx = left_set;

    let mut right_set = DotSet::new();
    assert!(right_set.insert(dot(2, 1)));
    let b = Sum::Right(right_set.clone());
    let b_ctx = right_set;

    let ab = a.causal_merge(&a_ctx, &b, &b_ctx);
    assert!(
        !ab.dots().any(|held| held == dot(2, 1)),
        "the sum kept self's shape and discarded the other side's uncovered dot"
    );
    let ba = b.causal_merge(&b_ctx, &a, &a_ctx);
    assert!(
        !ba.dots().any(|held| held == dot(1, 1)),
        "the mirrored merge discards the mirrored dot"
    );
    assert_ne!(ab, ba, "and no sum-shaped rule is even commutative here");
}

/// The exclusive-kind store, kind-conflict candidate shape 1 taken alone: a
/// node is exactly one kind, and the merge enforces it by picking a winner
/// (here: the greatest tag dot, a last-writer-wins kind) and keeping only
/// the winner's content.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
struct Exclusive {
    /// The winning kind assignment, `(dot, kind)`.
    tag: Option<(Dot, Kind)>,
    /// The winning kind's content dots.
    content: DotSet,
}

impl DotStore for Exclusive {
    fn dots(&self) -> impl Iterator<Item = Dot> + '_ {
        self.tag
            .iter()
            .map(|&(dot, _)| dot)
            .chain(DotStore::dots(&self.content))
    }

    fn is_bottom(&self) -> bool {
        self.tag.is_none() && self.content.is_bottom()
    }

    fn causal_merge(&self, _: &DotSet, other: &Self, _: &DotSet) -> Self {
        // The exclusive rule: the greater tag dot wins outright, and the
        // loser's whole subtree goes with it, contexts never consulted.
        let winner = match (self.tag, other.tag) {
            (Some(mine), Some(theirs)) if theirs.0 > mine.0 => other,
            (None, Some(_)) => other,
            _ => self,
        };
        winner.clone()
    }

    fn restrict(&self, roster: impl IntoIterator<Item = u32>) -> Self {
        Self {
            tag: self.tag,
            content: DotStore::restrict(&self.content, roster),
        }
    }

    fn novel_to(&self, context: &DotSet) -> Self {
        Self {
            tag: self.tag,
            content: DotStore::novel_to(&self.content, context),
        }
    }
}

#[test]
fn test_an_exclusive_kind_merge_discards_a_concurrent_subtree() {
    // Replica A tags the node Register and writes register content; replica
    // B, concurrently, tags it Sequence and writes sequence content. Under
    // the exclusive rule B's later tag dot wins and A's content dot is
    // discarded, though B never saw it: the LWW-kind hazard, the survivor
    // law's own counterexample, and the reason shape 1 alone is refused.
    let mut a_content = DotSet::new();
    assert!(a_content.insert(dot(1, 2)));
    let a = Exclusive {
        tag: Some((dot(1, 1), Kind::Register)),
        content: a_content,
    };
    let mut a_ctx = DotSet::new();
    assert!(a_ctx.insert(dot(1, 1)));
    assert!(a_ctx.insert(dot(1, 2)));

    let mut b_content = DotSet::new();
    assert!(b_content.insert(dot(2, 2)));
    let b = Exclusive {
        tag: Some((dot(2, 1), Kind::Sequence)),
        content: b_content,
    };
    let mut b_ctx = DotSet::new();
    assert!(b_ctx.insert(dot(2, 1)));
    assert!(b_ctx.insert(dot(2, 2)));

    let merged = a.causal_merge(&a_ctx, &b, &b_ctx);
    assert!(
        !merged.dots().any(|held| held == dot(1, 2)),
        "A's concurrent subtree is silently gone, unrecoverably: \
         b's context never covered (1, 2), so the survivor law demanded it live"
    );

    // The shipped node runs the same concurrency and discards nothing: both
    // writes survive, and the conflict is surfaced instead of resolved.
    let mut node_a = TestNode::new();
    assert!(node_a.write_tag(dot(1, 1), Kind::Register));
    assert!(node_a.write_register(dot(1, 2), "a"));
    let mut node_b = TestNode::new();
    assert!(node_b.write_tag(dot(2, 1), Kind::Sequence));
    assert!(node_b.weave(
        dot(2, 2),
        Locus {
            anchor: Anchor::Origin,
            rank: rank(1),
        }
    ));
    let merged = node_a.causal_merge(&a_ctx, &node_b, &b_ctx);
    assert!(merged.dots().any(|held| held == dot(1, 2)));
    assert!(merged.dots().any(|held| held == dot(2, 2)));
    assert_eq!(merged.kinds_present().len(), 2, "surfaced, not resolved");
}

#[test]
fn test_slots_alone_cannot_say_conflict_but_the_node_must() {
    // Kind-conflict candidate shape 2 taken alone (per-kind slots, the
    // replicated-JSON answer): every kind's component merges independently
    // and no conflict ever surfaces, so a node that is honestly TWO kinds
    // reads as innocent through every per-kind window. The probe reproduced
    // that silence across parallel maps (S186). The node keeps the slots
    // semantics in its merge (nothing discarded), and the refusal is what it
    // adds: the checked read cannot hand out either kind as if undisputed.
    let mut node = TestNode::new();
    assert!(node.write_register(dot(1, 1), "register-me"));
    assert!(node.weave(
        dot(2, 1),
        Locus {
            anchor: Anchor::Origin,
            rank: rank(1),
        }
    ));

    // Each per-kind window looks innocent alone, exactly as slots behave.
    assert_eq!(node.register().values().count(), 1);
    assert_eq!(node.sequence().visible_len(), 1);

    // But the two-kind state is unrepresentable as "no conflict": the
    // summary surfaces both kinds, and the checked projection refuses.
    assert_eq!(node.kinds_present().len(), 2);
    assert!(matches!(node.sole(), Err(KindPlurality { .. })));
}