Skip to main content

logicaffeine_compile/semantics/
crdt.rs

1//! CRDT runtime values for the tree-walker interpreter.
2//!
3//! The compiled (AOT) tier lowers a `Shared` struct's collection fields to the real
4//! `logicaffeine_data::crdt` types — `ORSet`, `RGA`, `MVRegister` — and runs their full
5//! convergent semantics. For the interpreter to MEAN THE SAME THING as the compiler
6//! (the futamura invariant the cross-tier differential tests lock), it holds those very
7//! same types, not a lookalike `Set`/`List`. Merge is then concurrent-correct by
8//! construction: both tiers call into one `Merge` implementation, so there is no second
9//! copy of the convergence logic that could drift.
10//!
11//! The language's shared collections are homogeneous over a concrete element type
12//! (`SharedSet of Text`, `… of Int`) exactly as the compiled tier's `ORSet<String>` is.
13//! [`CrdtScalar`] is the dynamically-typed element that covers every element type the
14//! surface syntax can name; it is `Hash + Eq + Ord + Clone`, so it satisfies the
15//! data-crate CRDT bounds.
16
17use crate::interpreter::RuntimeValue;
18use logicaffeine_data::crdt::{DeltaCrdt, Merge, MVRegister, ORSet, RemoveWins, ReplicaId, VClock, RGA};
19use std::sync::atomic::{AtomicU64, Ordering};
20
21/// A monotonic source of replica ids for CRDTs the interpreter constructs. Each `new`
22/// shared collection gets a DISTINCT id, so two replicas built in one program (then
23/// merged) carry independent causal histories — the precondition for correct OR-Set /
24/// RGA convergence. The absolute id value never affects observable output (it only tags
25/// internal causal metadata), so a process-global counter keeps every run deterministic.
26static REPLICA_SEQ: AtomicU64 = AtomicU64::new(1);
27
28/// Allocate the next distinct replica id.
29pub fn next_replica_id() -> ReplicaId {
30    REPLICA_SEQ.fetch_add(1, Ordering::Relaxed)
31}
32
33/// A scalar CRDT element. CRDT collections are homogeneous, so a collection only ever
34/// holds one of these variants in practice; the enum exists because the interpreter is
35/// dynamically typed and the element type is not threaded through `struct_defs`.
36#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
37pub enum CrdtScalar {
38    Int(i64),
39    Text(String),
40    Bool(bool),
41}
42
43impl CrdtScalar {
44    /// Project a [`RuntimeValue`] onto a CRDT element. Only the scalar types the surface
45    /// syntax can put in a shared collection are accepted; anything else is a type error
46    /// at the seam (never a silent coercion).
47    pub fn from_runtime(value: &RuntimeValue) -> Result<CrdtScalar, String> {
48        match value {
49            RuntimeValue::Int(n) => Ok(CrdtScalar::Int(*n)),
50            RuntimeValue::Text(s) => Ok(CrdtScalar::Text((**s).clone())),
51            RuntimeValue::Bool(b) => Ok(CrdtScalar::Bool(*b)),
52            other => Err(format!(
53                "a shared collection holds Int, Text, or Bool elements, not {}",
54                other.type_name()
55            )),
56        }
57    }
58
59    /// Reconstitute a [`RuntimeValue`] from a CRDT element.
60    pub fn to_runtime(&self) -> RuntimeValue {
61        match self {
62            CrdtScalar::Int(n) => RuntimeValue::Int(*n),
63            CrdtScalar::Text(s) => RuntimeValue::Text(std::rc::Rc::new(s.clone())),
64            CrdtScalar::Bool(b) => RuntimeValue::Bool(*b),
65        }
66    }
67
68    /// The canonical `Show` rendering of an element — matches how the compiled tier
69    /// prints the same scalar.
70    pub fn render(&self) -> String {
71        match self {
72            CrdtScalar::Int(n) => n.to_string(),
73            CrdtScalar::Text(s) => s.clone(),
74            CrdtScalar::Bool(b) => b.to_string(),
75        }
76    }
77}
78
79/// A live CRDT held by the interpreter. Wraps the actual `logicaffeine_data` type, so its
80/// convergence behaviour is identical to the compiled tier's by construction.
81#[derive(Debug, Clone)]
82pub enum CrdtValue {
83    /// `SharedSet of T` / `SharedSet (AddWins)` — observed-remove set, add-wins on a
84    /// concurrent add/remove of the same element.
85    Set(ORSet<CrdtScalar>),
86    /// `SharedSet (RemoveWins)` — observed-remove set, remove-wins on a concurrent
87    /// add/remove (e.g. an access-revocation / block list).
88    SetRemoveWins(ORSet<CrdtScalar, RemoveWins>),
89    /// `SharedSequence of T` — replicated growable array (`RGA`). Stable, convergent
90    /// insertion order.
91    Seq(RGA<CrdtScalar>),
92    /// `Divergent T` — multi-value register (`MVRegister`). Keeps concurrent writes until
93    /// one is resolved.
94    Register(MVRegister<CrdtScalar>),
95}
96
97impl CrdtValue {
98    pub fn new_set(replica: ReplicaId) -> CrdtValue {
99        CrdtValue::Set(ORSet::new(replica))
100    }
101
102    pub fn new_set_remove_wins(replica: ReplicaId) -> CrdtValue {
103        CrdtValue::SetRemoveWins(ORSet::new(replica))
104    }
105
106    pub fn new_seq(replica: ReplicaId) -> CrdtValue {
107        CrdtValue::Seq(RGA::new(replica))
108    }
109
110    pub fn new_register(replica: ReplicaId) -> CrdtValue {
111        CrdtValue::Register(MVRegister::new(replica))
112    }
113
114    /// The CRDT kind, used for type errors and `type_name`.
115    pub fn kind(&self) -> &'static str {
116        match self {
117            CrdtValue::Set(_) | CrdtValue::SetRemoveWins(_) => "SharedSet",
118            CrdtValue::Seq(_) => "SharedSequence",
119            CrdtValue::Register(_) => "Divergent",
120        }
121    }
122
123    /// `Add element to <set>` — insert into the observed-remove set.
124    pub fn insert(&mut self, value: &RuntimeValue) -> Result<(), String> {
125        let e = CrdtScalar::from_runtime(value)?;
126        match self {
127            CrdtValue::Set(s) => Ok(s.insert(e)),
128            CrdtValue::SetRemoveWins(s) => Ok(s.insert(e)),
129            other => Err(format!("cannot add an element to a {}", other.kind())),
130        }
131    }
132
133    /// `Remove element from <set>` — observed-remove deletion.
134    pub fn remove(&mut self, value: &RuntimeValue) -> Result<(), String> {
135        let e = CrdtScalar::from_runtime(value)?;
136        match self {
137            CrdtValue::Set(s) => Ok(s.remove(&e)),
138            CrdtValue::SetRemoveWins(s) => Ok(s.remove(&e)),
139            other => Err(format!("cannot remove an element from a {}", other.kind())),
140        }
141    }
142
143    /// `<set> contains element` — observed-remove membership.
144    pub fn contains(&self, value: &RuntimeValue) -> Result<bool, String> {
145        let e = CrdtScalar::from_runtime(value)?;
146        match self {
147            CrdtValue::Set(s) => Ok(s.contains(&e)),
148            CrdtValue::SetRemoveWins(s) => Ok(s.contains(&e)),
149            other => Err(format!("a {} has no membership test", other.kind())),
150        }
151    }
152
153    /// `Append element to <sequence>` — RGA append at the end.
154    pub fn append(&mut self, value: &RuntimeValue) -> Result<(), String> {
155        match self {
156            CrdtValue::Seq(s) => {
157                s.append(CrdtScalar::from_runtime(value)?);
158                Ok(())
159            }
160            other => Err(format!("cannot append to a {}", other.kind())),
161        }
162    }
163
164    /// `Set <register> to value` / `Resolve <register> to value` — the divergent register
165    /// takes a value that dominates all concurrent ones.
166    pub fn resolve(&mut self, value: &RuntimeValue) -> Result<(), String> {
167        match self {
168            CrdtValue::Register(r) => {
169                r.resolve(CrdtScalar::from_runtime(value)?);
170                Ok(())
171            }
172            other => Err(format!("cannot resolve a {}", other.kind())),
173        }
174    }
175
176    /// `length of <collection>` — element count (set cardinality or sequence length).
177    pub fn len(&self) -> usize {
178        match self {
179            CrdtValue::Set(s) => s.len(),
180            CrdtValue::SetRemoveWins(s) => s.len(),
181            CrdtValue::Seq(s) => s.len(),
182            CrdtValue::Register(r) => r.values().len(),
183        }
184    }
185
186    pub fn is_empty(&self) -> bool {
187        self.len() == 0
188    }
189
190    /// The elements as runtime values, in convergent order for a sequence and sorted for a
191    /// set (sets have no intrinsic order — sorting makes the rendering deterministic).
192    pub fn to_runtime_vec(&self) -> Vec<RuntimeValue> {
193        match self {
194            CrdtValue::Set(s) => {
195                let mut elems: Vec<&CrdtScalar> = s.iter().collect();
196                elems.sort();
197                elems.into_iter().map(CrdtScalar::to_runtime).collect()
198            }
199            CrdtValue::SetRemoveWins(s) => {
200                let mut elems: Vec<&CrdtScalar> = s.iter().collect();
201                elems.sort();
202                elems.into_iter().map(CrdtScalar::to_runtime).collect()
203            }
204            CrdtValue::Seq(s) => s.to_vec().iter().map(CrdtScalar::to_runtime).collect(),
205            CrdtValue::Register(r) => {
206                let mut vals: Vec<&CrdtScalar> = r.values();
207                vals.sort();
208                vals.into_iter().map(CrdtScalar::to_runtime).collect()
209            }
210        }
211    }
212
213    /// The resolved value of a register — the single current value, or `None` while it is
214    /// empty or still divergent (multiple concurrent values).
215    pub fn register_value(&self) -> Option<RuntimeValue> {
216        match self {
217            CrdtValue::Register(r) => {
218                let vals = r.values();
219                if vals.len() == 1 {
220                    Some(vals[0].to_runtime())
221                } else {
222                    None
223                }
224            }
225            _ => None,
226        }
227    }
228
229    /// Merge another CRDT of the SAME kind into this one (the convergent join). Mismatched
230    /// kinds are a type error rather than a silent no-op.
231    pub fn merge(&mut self, other: &CrdtValue) -> Result<(), String> {
232        match (self, other) {
233            (CrdtValue::Set(a), CrdtValue::Set(b)) => {
234                a.merge(b);
235                Ok(())
236            }
237            (CrdtValue::SetRemoveWins(a), CrdtValue::SetRemoveWins(b)) => {
238                a.merge(b);
239                Ok(())
240            }
241            (CrdtValue::Seq(a), CrdtValue::Seq(b)) => {
242                a.merge(b);
243                Ok(())
244            }
245            (CrdtValue::Register(a), CrdtValue::Register(b)) => {
246                a.merge(b);
247                Ok(())
248            }
249            (a, b) => Err(format!("cannot merge a {} with a {}", a.kind(), b.kind())),
250        }
251    }
252
253    /// The current causal version (vector clock) — every update this replica has observed. The
254    /// δ-CRDT `mergeable` send remembers the version it last shipped to a peer and ships only the
255    /// delta SINCE that version, so a large set/sequence syncs its few NEW entries, not its whole
256    /// state. Idempotent + commutative, so reordering or redelivering deltas still converges.
257    pub fn version(&self) -> VClock {
258        match self {
259            CrdtValue::Set(s) => s.version(),
260            CrdtValue::SetRemoveWins(s) => s.version(),
261            CrdtValue::Seq(s) => s.version(),
262            CrdtValue::Register(s) => s.version(),
263        }
264    }
265
266    /// Serialize the changes since `since` as a KIND-TAGGED delta payload — far smaller than the
267    /// full state when little changed. `None` if the variant can't produce a delta (history
268    /// truncated); the caller falls back to the full-state `crdt_to_wire`.
269    pub fn delta_since_bytes(&self, since: &VClock) -> Option<Vec<u8>> {
270        // bincode, NOT serde_json: a delta's `entries` is a map keyed by the ELEMENT (a `CrdtScalar`
271        // enum), and JSON cannot encode a non-string map key — bincode serializes maps as (key,value)
272        // sequences, so any key type round-trips.
273        fn tagged<D: serde::Serialize>(kind: u8, delta: &D) -> Option<Vec<u8>> {
274            let mut out = vec![kind];
275            out.extend(bincode::serialize(delta).ok()?);
276            Some(out)
277        }
278        match self {
279            CrdtValue::Set(s) => tagged(0, &s.delta_since(since)?),
280            CrdtValue::SetRemoveWins(s) => tagged(1, &s.delta_since(since)?),
281            CrdtValue::Seq(s) => tagged(2, &s.delta_since(since)?),
282            CrdtValue::Register(s) => tagged(3, &s.delta_since(since)?),
283        }
284    }
285
286    /// Apply a kind-tagged delta from `delta_since_bytes` of the SAME kind. Returns false on a
287    /// kind mismatch or malformed bytes, leaving self unchanged — a foreign / garbage delta never
288    /// corrupts the CRDT (the network-edge safety the `mergeable` receive needs).
289    pub fn apply_delta_bytes(&mut self, bytes: &[u8]) -> bool {
290        let Some((&kind, body)) = bytes.split_first() else {
291            return false;
292        };
293        match (self, kind) {
294            (CrdtValue::Set(s), 0) => {
295                match bincode::deserialize::<<ORSet<CrdtScalar> as DeltaCrdt>::Delta>(body) {
296                    Ok(d) => {
297                        s.apply_delta(&d);
298                        true
299                    }
300                    Err(_) => false,
301                }
302            }
303            (CrdtValue::SetRemoveWins(s), 1) => {
304                match bincode::deserialize::<<ORSet<CrdtScalar, RemoveWins> as DeltaCrdt>::Delta>(body) {
305                    Ok(d) => {
306                        s.apply_delta(&d);
307                        true
308                    }
309                    Err(_) => false,
310                }
311            }
312            (CrdtValue::Seq(s), 2) => {
313                match bincode::deserialize::<<RGA<CrdtScalar> as DeltaCrdt>::Delta>(body) {
314                    Ok(d) => {
315                        s.apply_delta(&d);
316                        true
317                    }
318                    Err(_) => false,
319                }
320            }
321            (CrdtValue::Register(s), 3) => {
322                match bincode::deserialize::<<MVRegister<CrdtScalar> as DeltaCrdt>::Delta>(body) {
323                    Ok(d) => {
324                        s.apply_delta(&d);
325                        true
326                    }
327                    Err(_) => false,
328                }
329            }
330            _ => false,
331        }
332    }
333
334    /// `Show` rendering: a set renders in set notation `{…}` (matching a plain `Set`), a
335    /// sequence as an ordered list `[…]`, a register as its resolved value (or, while still
336    /// divergent, the brace-joined set of concurrent values).
337    pub fn render(&self) -> String {
338        let parts = || -> String {
339            self.to_runtime_vec().iter().map(render_runtime).collect::<Vec<_>>().join(", ")
340        };
341        match self {
342            CrdtValue::Register(_) => match self.register_value() {
343                Some(v) => render_runtime(&v),
344                None => format!("{{{}}}", parts()),
345            },
346            CrdtValue::Set(_) | CrdtValue::SetRemoveWins(_) => format!("{{{}}}", parts()),
347            CrdtValue::Seq(_) => format!("[{}]", parts()),
348        }
349    }
350}
351
352/// Structural equality used by the interpreter's `values_equal` for CRDT values: two
353/// CRDTs are equal when they observe the same elements (sequences also compare order).
354pub fn crdt_values_equal(a: &CrdtValue, b: &CrdtValue) -> bool {
355    match (a, b) {
356        (CrdtValue::Set(_), CrdtValue::Set(_))
357        | (CrdtValue::SetRemoveWins(_), CrdtValue::SetRemoveWins(_))
358        | (CrdtValue::Seq(_), CrdtValue::Seq(_))
359        | (CrdtValue::Register(_), CrdtValue::Register(_)) => {
360            a.to_runtime_vec() == b.to_runtime_vec()
361        }
362        _ => false,
363    }
364}
365
366/// Render a runtime scalar the way `Show` does for a CRDT element (text is unquoted).
367fn render_runtime(v: &RuntimeValue) -> String {
368    match v {
369        RuntimeValue::Int(n) => n.to_string(),
370        RuntimeValue::Text(s) => (**s).clone(),
371        RuntimeValue::Bool(b) => b.to_string(),
372        other => other.type_name().to_string(),
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    /// A tiny deterministic PRNG (SplitMix64) so the property tests are exhaustively random
381    /// yet perfectly reproducible — no `rand`, no flakiness.
382    struct Rng(u64);
383    impl Rng {
384        fn next(&mut self) -> u64 {
385            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
386            let mut z = self.0;
387            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
388            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
389            z ^ (z >> 31)
390        }
391        fn below(&mut self, n: u64) -> u64 {
392            self.next() % n
393        }
394    }
395
396    fn int(n: u64) -> RuntimeValue {
397        RuntimeValue::Int(n as i64)
398    }
399
400    /// A random OR-Set: a fresh replica with a random add/remove op stream over a small
401    /// element domain (so collisions and removals actually happen).
402    fn rand_set(rng: &mut Rng, domain: u64, ops: usize) -> CrdtValue {
403        let mut s = CrdtValue::new_set(next_replica_id());
404        for _ in 0..ops {
405            let e = int(rng.below(domain));
406            if rng.below(3) == 0 {
407                let _ = s.remove(&e);
408            } else {
409                let _ = s.insert(&e);
410            }
411        }
412        s
413    }
414
415    /// A random RGA: a fresh replica appending random elements.
416    fn rand_seq(rng: &mut Rng, domain: u64, ops: usize) -> CrdtValue {
417        let mut s = CrdtValue::new_seq(next_replica_id());
418        for _ in 0..ops {
419            let _ = s.append(&int(rng.below(domain)));
420        }
421        s
422    }
423
424    /// `a ⊔ b` as a fresh value (never aliases an operand).
425    fn join(a: &CrdtValue, b: &CrdtValue) -> CrdtValue {
426        let mut out = a.clone();
427        out.merge(b).unwrap();
428        out
429    }
430
431    #[test]
432    fn orset_merge_obeys_the_crdt_laws() {
433        let mut rng = Rng(0xDEAD_BEEF);
434        for _ in 0..500 {
435            let a = rand_set(&mut rng, 6, 8);
436            let b = rand_set(&mut rng, 6, 8);
437            let c = rand_set(&mut rng, 6, 8);
438            assert!(crdt_values_equal(&join(&a, &b), &join(&b, &a)), "commutative");
439            assert!(
440                crdt_values_equal(&join(&join(&a, &b), &c), &join(&a, &join(&b, &c))),
441                "associative"
442            );
443            assert!(crdt_values_equal(&join(&a, &a), &a), "idempotent");
444        }
445    }
446
447    #[test]
448    fn rga_merge_obeys_the_crdt_laws() {
449        let mut rng = Rng(0x0123_4567);
450        for _ in 0..500 {
451            let a = rand_seq(&mut rng, 6, 6);
452            let b = rand_seq(&mut rng, 6, 6);
453            let c = rand_seq(&mut rng, 6, 6);
454            assert!(crdt_values_equal(&join(&a, &b), &join(&b, &a)), "commutative");
455            assert!(
456                crdt_values_equal(&join(&join(&a, &b), &c), &join(&a, &join(&b, &c))),
457                "associative"
458            );
459            assert!(crdt_values_equal(&join(&a, &a), &a), "idempotent");
460        }
461    }
462
463    /// The distinguishing OR-Set property, at the unit level: replica `a` observes-and-
464    /// removes "X", replica `b` adds "X" CONCURRENTLY; after the join "X" survives, because
465    /// `b`'s add carries a tag `a`'s remove never saw. A grow-set-with-one-tombstone fails.
466    #[test]
467    fn orset_add_wins_over_concurrent_remove() {
468        let x = int(42);
469        let mut a = CrdtValue::new_set(next_replica_id());
470        let mut b = CrdtValue::new_set(next_replica_id());
471        a.insert(&x).unwrap();
472        b.insert(&x).unwrap();
473        a.remove(&x).unwrap();
474        let m = join(&a, &b);
475        assert!(m.contains(&x).unwrap(), "the concurrent add must survive the remove");
476    }
477
478    #[test]
479    fn scalar_conversions_round_trip() {
480        for v in [
481            RuntimeValue::Int(-7),
482            RuntimeValue::Text(std::rc::Rc::new("héllo".to_string())),
483            RuntimeValue::Bool(true),
484        ] {
485            let s = CrdtScalar::from_runtime(&v).unwrap();
486            assert_eq!(s.to_runtime(), v);
487        }
488        // A non-scalar is refused at the seam, not silently coerced.
489        assert!(CrdtScalar::from_runtime(&RuntimeValue::Float(1.5)).is_err());
490    }
491}