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.
T2: CRDTs GCounter/ResettableRegister + hito M1 de punta a punta
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}