gioser

tawasuyu/hifas

Motor en Rust para decidir, con demostración y no a ojo, cuándo un conjunto de réplicas local-first puede operar sin coordinación y cuándo no — y, cuando la respuesta es "sí, dentro de límites", proveer el mecanismo que lo garantiza.

Rust 24Markdown 15TOML 1358 Definiciones

⎇ main src/cell.rs Rust · 155 líneas · 4.9 KB Historia Quién tocó qué crudo

T2: CRDTs GCounter/ResettableRegister + hito M1 de punta a punta 94482d292 · Sergio ·

1//! `cell` — el dato local que sabe fundirse y proyectarse (§6.1).
2//!
3//! Dos CRDTs con semántica controlada a mano (§10): un monótono (`GCounter`) y
4//! uno no monótono (`ResettableRegister`), que son las dos anclas del hito M1.
5
6use crate::linalg::Scalar;
7
8/// Un pedacito de estado local que sabe cómo intentar fundirse con otro,
9/// y cómo proyectarse al espacio de una arista compartida.
10pub trait Cell: Clone + PartialEq {
11    /// El vector que representa este estado en el espacio de la stalk.
12    fn to_vector(&self) -> Vec<Scalar>;
13
14    /// Fusión de retículo (join). Debe ser conmutativa, asociativa, idempotente.
15    fn join(&self, other: &Self) -> Self;
16
17    /// ¿Esta operación es monótona? (marca para el oráculo CALM)
18    fn is_monotone() -> bool;
19}
20
21/// CRDT mínimo del MVP: cuenta por réplica, fusiona por máximo. Siempre monótono.
22///
23/// Cada réplica solo incrementa su propia casilla; fusionar = máximo casilla por
24/// casilla. Nunca hay conflicto porque nunca se resta. Embebe en `ℤⁿ` (§2.3).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct GCounter {
27    /// Una casilla por réplica.
28    pub slots: Vec<u64>,
29}
30
31impl GCounter {
32    /// G-Counter en cero para `n` réplicas.
33    pub fn zeros(n: usize) -> Self {
34        Self { slots: vec![0; n] }
35    }
36
37    /// La réplica `i` incrementa su propia casilla en `by`.
38    pub fn increment(&mut self, i: usize, by: u64) {
39        self.slots[i] += by;
40    }
41
42    /// Valor total del contador (suma de casillas).
43    pub fn value(&self) -> u64 {
44        self.slots.iter().sum()
45    }
46}
47
48impl Cell for GCounter {
49    fn to_vector(&self) -> Vec<Scalar> {
50        self.slots.iter().map(|&s| s as Scalar).collect()
51    }
52
53    fn join(&self, other: &Self) -> Self {
54        let n = self.slots.len().max(other.slots.len());
55        let slots = (0..n)
56            .map(|i| {
57                let a = self.slots.get(i).copied().unwrap_or(0);
58                let b = other.slots.get(i).copied().unwrap_or(0);
59                a.max(b)
60            })
61            .collect();
62        Self { slots }
63    }
64
65    fn is_monotone() -> bool {
66        true
67    }
68}
69
70/// CRDT no monótono para el caso "difícil": permite reset/borrado.
71///
72/// El registro guarda un valor y un contador de generación. Un `reset`
73/// incrementa la generación y puede **bajar** el valor: eso retracta conocimiento
74/// (§glosario), luego NO es monótono. Sirve para provocar `H¹ ≠ 0` (§8, corrida B).
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ResettableRegister {
77    /// Valor actual del registro.
78    pub value: i64,
79    /// Contador de generación; un reset lo incrementa (rompe la monotonicidad).
80    pub generation: u64,
81}
82
83impl ResettableRegister {
84    /// Registro inicial con `value` en la generación 0.
85    pub fn new(value: i64) -> Self {
86        Self {
87            value,
88            generation: 0,
89        }
90    }
91
92    /// Sobrescribe el valor sin cambiar de generación (operación monótona-ish).
93    pub fn set(&mut self, value: i64) {
94        self.value = value;
95    }
96
97    /// Resetea: nueva generación con un valor que puede ser menor. NO monótono.
98    pub fn reset(&mut self, value: i64) {
99        self.generation += 1;
100        self.value = value;
101    }
102}
103
104impl Cell for ResettableRegister {
105    fn to_vector(&self) -> Vec<Scalar> {
106        vec![self.generation as Scalar, self.value as Scalar]
107    }
108
109    /// Gana la generación más alta; a igual generación, el mayor valor.
110    fn join(&self, other: &Self) -> Self {
111        match self.generation.cmp(&other.generation) {
112            std::cmp::Ordering::Greater => self.clone(),
113            std::cmp::Ordering::Less => other.clone(),
114            std::cmp::Ordering::Equal => Self {
115                generation: self.generation,
116                value: self.value.max(other.value),
117            },
118        }
119    }
120
121    fn is_monotone() -> bool {
122        false
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn gcounter_join_es_maximo_y_monotono() {
132        let a = GCounter { slots: vec![3, 0, 1] };
133        let b = GCounter { slots: vec![1, 5, 1] };
134        let j = a.join(&b);
135        assert_eq!(j.slots, vec![3, 5, 1]);
136        // Idempotente y conmutativo.
137        assert_eq!(a.join(&a), a);
138        assert_eq!(a.join(&b), b.join(&a));
139        // El join nunca baja respecto a ninguno de los dos (monotonía).
140        assert!(j.value() >= a.value() && j.value() >= b.value());
141        assert!(GCounter::is_monotone());
142    }
143
144    #[test]
145    fn resettable_no_es_monotono() {
146        let mut r = ResettableRegister::new(10);
147        let alto = r.clone();
148        r.reset(0); // nueva generación, valor MENOR: retracta conocimiento.
149        let fundido = alto.join(&r);
150        // El reset (generación mayor) gana pese a tener valor menor → no monótono.
151        assert_eq!(fundido.value, 0);
152        assert_eq!(fundido.generation, 1);
153        assert!(!ResettableRegister::is_monotone());
154    }
155}