dson/dotstores.rs
1// (c) Copyright 2025 Helsing GmbH. All rights reserved.
2//! # Dot Stores
3//!
4//! This module defines the core data structures, known as "dot stores", that underpin the DSON
5//! (JSON CRDT Using Delta-Mutations For Document Stores) implementation. The concepts and data
6//! structures defined here are based on the research paper "[DSON: JSON CRDT Using
7//! Delta-Mutations For Document Stores](dson_paper.txt)".
8//!
9//! ## Overview
10//!
11//! At the heart of DSON is the idea of a **dot store**, a container for data-type-specific
12//! information that stores the state of a delta-based CRDT. Each dot store is paired with a
13//! [`CausalContext`], which tracks the set of observed events (dots) across replicas. This
14//! combination, encapsulated in the [`CausalDotStore`] struct, forms the basis for building
15//! CRDTs.
16//!
17//! The primary dot stores defined in this module are:
18//!
19//! - [`DotFun`]: A map from [`Dot`]s to values, where the set of dots is its keyset. This is used
20//! to implement simple CRDTs like [`MvReg`](crate::crdts::mvreg::MvReg) (multi-value registers).
21//! - [`DotMap`]: A map from an arbitrary key type to a `DotStore`, where the computed dots are the
22//! union of the dots of its values. This is used to implement OR-Maps (Observed-Remove Maps).
23//! - [`DotFunMap`]: A map from [`Dot`]s to `DotStore`s, combining the properties of `DotFun` and
24//! `DotMap`. This is used to implement OR-Arrays (Observed-Remove Arrays).
25//!
26//! These dot stores are designed to be composable, allowing for the construction of arbitrarily
27//! nested JSON-like structures.
28//!
29//! ## Join Operations
30//!
31//! The core of the CRDT logic is the `join` operation, defined in the [`DotStoreJoin`] trait. The
32//! `join` operation merges the state of two `CausalDotStore`s, resolving conflicts in a
33//! deterministic way. The exact semantics of the join operation vary depending on the concrete
34//! dot store type, but the general principle is to keep the most up-to-date values and discard
35//! those that have been causally overwritten.
36//!
37//! ## References
38//!
39//! The theoretical foundations for the dot stores and their join operations are detailed in
40//! the DSON paper. In particular, see the following sections:
41//!
42//! - **Section 3.3**: Introduces the concept of dot stores and defines `DotFun` and `DotMap`.
43//! - **Section 4**: Describes the observed-remove semantics used in DSON.
44//! - **Section 5**: Introduces the `CompDotFun` (here named `DotFunMap`) and the OR-Array
45//! algorithm.
46//!
47//! The original work on delta-based CRDTs can be found in the 2018 paper _Delta state replicated
48//! data types_ by Paulo Sérgio Almeida, Ali Shoker, and Carlos Baquero.
49
50use crate::{
51 CausalContext, Dot, DsonRandomState, create_map, create_map_with_capacity,
52 sentinel::{DummySentinel, KeySentinel, Sentinel, ValueSentinel, Visit},
53};
54use smallvec::SmallVec;
55use std::{borrow::Borrow, collections::HashMap, fmt, hash::Hash, ops::Index};
56
57/// A [`DotStore`] paired with a [`CausalContext`].
58///
59/// This is the fundamental building block of the DSON CRDT. It combines a `DotStore`, which holds
60/// the state of a specific data type, with a `CausalContext`, which tracks the set of observed
61/// events (dots) across replicas. This pairing allows for the implementation of delta-based
62/// CRDTs, where changes can be calculated and transmitted as deltas rather than entire states.
63#[derive(Debug, Clone, PartialEq)]
64#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
65pub struct CausalDotStore<DS> {
66 /// The data-type-specific information.
67 pub store: DS,
68 /// The causal context, tracking observed events.
69 pub context: CausalContext,
70}
71
72impl<'cs, DS> From<&'cs CausalDotStore<DS>> for (&'cs DS, &'cs CausalContext) {
73 fn from(cds: &'cs CausalDotStore<DS>) -> Self {
74 (&cds.store, &cds.context)
75 }
76}
77
78impl<'cs, DS> From<&'cs mut CausalDotStore<DS>> for (&'cs DS, &'cs mut CausalContext) {
79 fn from(cds: &'cs mut CausalDotStore<DS>) -> Self {
80 (&cds.store, &mut cds.context)
81 }
82}
83
84impl<DS> CausalDotStore<DS>
85where
86 DS: DotStore,
87{
88 /// Returns true if this is ⊥ (that is, empty).
89 ///
90 /// NOTE: the DSON paper does not explicitly define what a bottom is for a Causal⟨DotStore⟩, but
91 /// it does provide that "For any 𝑋 ∈ Causal⟨DotStore⟩, 𝑋 ⊔ ⊥ = 𝑋", which constrains it to ⊥ =
92 /// ({}, {}), since that is the only value that satisfies that equation and Equation 4 for any
93 /// arbitrary 𝑋.
94 pub fn is_bottom(&self) -> bool {
95 self.store.is_bottom() && self.context.is_empty()
96 }
97
98 /// Returns a subset-CRDT derived from `self` that allows inflating state at the vector time
99 /// `frontier` to what is in `self`.
100 ///
101 /// Does not include deletions not represented by `self.context - frontier` (that is, deletions of
102 /// already-known store entries); that is left as an exercise for the caller.
103 pub fn subset_for_inflation_from(&self, frontier: &CausalContext) -> CausalDotStore<DS>
104 where
105 DS: Clone + Default,
106 {
107 // our goal here is to produce a CRDT that contains aspects of `self.store` that are _not_
108 // known to `frontier`. this could be additions (the easy case), but could also be deletes.
109 // since deletes are handled via the _absence_ of state plus the _presence_ of a dot, we
110 // need to carefully construct both the (delta) store and (delta) context here.
111 //
112 // the state isn't too bad: we call subset_for_inflation_from recursively all the way
113 // "down" the CRDT, keeping only entries whose dot is not in frontier, or that are on the
114 // path _to_ such a dot.
115 //
116 // the context is trickier: we can't send all of self.context since we're excluding values
117 // that aren't in context. more concretely, consider what happens if (0, 1) => 'a', and the
118 // dot (0, 1) is in both self.context and frontier. we will _not_ include it in the delta
119 // of store (since the node represented by frontier already knows it). if we nevertheless
120 // include (0, 1) in the delta context, the semantics of that is a _deletion_ of (0, 1)
121 // [see DotFun::join], which obviously isn't correct. instead, we produce the delta context
122 //
123 // self.context - frontier + delta_store.dots()
124 //
125 // this indicates that we a) have included anything we know that frontier does not, and b)
126 // acknowledges that some dots are included simply by virtue of being on the path to
127 // new/updated values.
128 let delta_store = self.store.subset_for_inflation_from(frontier);
129 let mut delta_context = &self.context - frontier;
130
131 // NOTE: it _could_ be that nothing bad happens if we don't add in delta_store.dots(),
132 // but that relies on the join at the other end not getting confused by the presence of a
133 // dot in store but not in context. Feels safer to just join them here anyway.
134 delta_store.add_dots_to(&mut delta_context);
135
136 // unfortunately, this doesn't capture deletions. remember from above, the way a delete is
137 // represented is the _presence_ of the dot that inserted a value `context`, coupled with
138 // the _absence_ of its value in `store`.
139 //
140 // consider what happens if A and B are fully synchronized, and both hold, say, just an
141 // MVReg with (1, 1) => 'x' as well as the causal context {(1,1)}. now, A deletes 'x'. this
142 // does not generate a dot, so A's causal context is the same. when A and B next
143 // synchronize, A does not know solely from B's causal context ({(1,1)}) that it is missing
144 // the deletion of (1, 1) => 'x'. the store won't include (1,1) [and shouldn't],
145 // self.context - frontier is empty, and so is delta_store.dots() [since delta_store is
146 // bottom].
147 //
148 // even if we _do_ associate a dot with a deletion (e.g., writing the ALIVE field like we
149 // do for maps and arrays), it doesn't solve this problem. A would then generate (1,2) for
150 // the deletion, and would realize B doesn't have (1,2), *but* it won't know that that
151 // implies that the causal context of the delta it sends to be should therefore include
152 // (1,1). it doesn't know the relationship between (1,2) and (1,1).
153 //
154 // we could keep a "graveyard" that holds
155 //
156 // Dot(A, B) ---(deleted)----> Dot(X, Y)
157 //
158 // and then here _also_ add in Dot(X, Y) for any Dot(A, B) not in frontier, but that raises
159 // the question of how to garbage-collect said graveyard. it's also not clear what happens
160 // for types where a deletion actually implies _multiple_ removed dots.
161 //
162 // for now, we leave making the context reflect deletions as an exercise for the caller.
163
164 CausalDotStore {
165 store: delta_store,
166 context: delta_context,
167 }
168 }
169}
170
171impl<DS> Default for CausalDotStore<DS>
172where
173 DS: Default,
174{
175 fn default() -> Self {
176 Self {
177 store: Default::default(),
178 context: Default::default(),
179 }
180 }
181}
182
183impl<DS> CausalDotStore<DS> {
184 /// Constructs a new empty [`CausalDotStore`].
185 pub fn new() -> Self
186 where
187 DS: Default,
188 {
189 Self::default()
190 }
191}
192
193#[cfg(any(test, feature = "arbitrary"))]
194use crate::dotstores::recording_sentinel::RecordingSentinel;
195
196impl<DS> CausalDotStore<DS> {
197 /// Joins the given [`CausalDotStore`] with this one, and returns the join.
198 ///
199 /// This is a convenience function around [`CausalDotStore::join_with`].
200 pub fn join<S>(mut self, other: Self, sentinel: &mut S) -> Result<CausalDotStore<DS>, S::Error>
201 where
202 S: Sentinel,
203 DS: DotStoreJoin<S> + Default,
204 {
205 self.consume(other, sentinel)?;
206 Ok(CausalDotStore {
207 store: self.store,
208 context: self.context,
209 })
210 }
211
212 // variant of join intended for tests, so it is not built for performance (for example, it clones eagerly
213 // internally to make the interface more convenient) and it exposes internal bits (like
214 // `on_dot_change`)
215 #[cfg(any(test, feature = "arbitrary"))]
216 pub fn test_join<S>(
217 &self,
218 other: &Self,
219 on_dot_change: &mut dyn FnMut(DotChange),
220 sentinel: &mut S,
221 ) -> Result<CausalDotStore<DS>, S::Error>
222 where
223 S: Sentinel,
224 DS: DotStoreJoin<S> + DotStoreJoin<RecordingSentinel> + Default + Clone,
225 {
226 let mut this = self.clone();
227 this.test_join_with_and_track(
228 other.store.clone(),
229 &other.context,
230 on_dot_change,
231 sentinel,
232 )?;
233
234 Ok(CausalDotStore {
235 store: this.store,
236 context: this.context,
237 })
238 }
239
240 #[cfg(any(test, feature = "arbitrary"))]
241 pub fn test_join_with_and_track<S>(
242 &mut self,
243 store: DS,
244 context: &CausalContext,
245 on_dot_change: &mut dyn FnMut(DotChange),
246 sentinel: &mut S,
247 ) -> Result<(), S::Error>
248 where
249 DS: DotStoreJoin<S> + DotStoreJoin<RecordingSentinel> + Clone + Default,
250 S: Sentinel,
251 {
252 #[cfg(debug_assertions)]
253 {
254 // We do a dry_join here first, to ensure that dry-join
255 // and join always result in the same set of calls being
256 // made to Sentinel. This is an invariant that we want to always
257 // hold, so we check it in debug builds for all test cases using this function.
258
259 let mut dry_join_sentinel = RecordingSentinel::new();
260 let dry_result = <DS as DotStoreJoin<RecordingSentinel>>::dry_join(
261 (&self.store, &self.context),
262 (&store, context),
263 &mut dry_join_sentinel,
264 )
265 .expect("RecordingSentinel is infallible");
266
267 let mut full_run_sentinel = RecordingSentinel::new();
268 let full_result = DS::join(
269 (self.store.clone(), &self.context),
270 (store.clone(), context),
271 &mut |_| {},
272 &mut full_run_sentinel,
273 )
274 .expect("RecordingSentinel is infallible");
275
276 assert_eq!(
277 dry_join_sentinel.changes_seen,
278 full_run_sentinel.changes_seen
279 );
280 assert_eq!(dry_result.is_bottom(), full_result.is_bottom());
281 }
282
283 self.join_with_and_track(store, context, on_dot_change, sentinel)?;
284 Ok(())
285 }
286
287 /// Joins the given [`CausalDotStore`] into this one.
288 ///
289 /// This is a convenience function around [`CausalDotStore::join_with`].
290 pub fn consume<S>(
291 &mut self,
292 other: CausalDotStore<DS>,
293 sentinel: &mut S,
294 ) -> Result<(), S::Error>
295 where
296 DS: DotStoreJoin<S> + Default,
297 S: Sentinel,
298 {
299 self.join_with(other.store, &other.context, sentinel)
300 }
301
302 /// Joins the given [`CausalDotStore`] into this one.
303 ///
304 /// This is a convenience function around [`CausalDotStore::join_with`].
305 #[cfg(any(test, feature = "arbitrary"))]
306 pub fn test_consume(&mut self, other: CausalDotStore<DS>)
307 where
308 DS: DotStoreJoin<RecordingSentinel> + Clone + Default,
309 {
310 self.test_join_with(other.store, &other.context)
311 }
312
313 /// Joins or replaces the current [`CausalDotStore`] with the provided one.
314 ///
315 /// If the current value is bottom, it is replaced wholesale, bypassing the
316 /// join. This method does not accept a sentinel as changes cannot always
317 /// be tracked.
318 pub fn join_or_replace_with(&mut self, store: DS, context: &CausalContext)
319 where
320 DS: DotStoreJoin<DummySentinel> + Default,
321 {
322 if self.is_bottom() {
323 *self = CausalDotStore {
324 store,
325 context: context.clone(),
326 };
327 } else {
328 self.join_with(store, context, &mut DummySentinel)
329 .expect("DummySentinel is Infallible");
330 }
331 }
332
333 /// Joins the given [`DotStore`]-[`CausalContext`] pair into those in `self`.
334 ///
335 /// Prefer this method when you need to avoid cloning the [`CausalContext`].
336 pub fn join_with<S>(
337 &mut self,
338 store: DS,
339 context: &CausalContext,
340 sentinel: &mut S,
341 ) -> Result<(), S::Error>
342 where
343 DS: DotStoreJoin<S> + Default,
344 S: Sentinel,
345 {
346 self.join_with_and_track(store, context, &mut |_| (), sentinel)
347 }
348
349 /// Joins the given [`DotStore`]-[`CausalContext`] pair into those in `self`.
350 ///
351 /// Prefer this method when you need to avoid cloning the [`CausalContext`].
352 #[cfg(any(test, feature = "arbitrary"))]
353 pub fn test_join_with(&mut self, store: DS, context: &CausalContext)
354 where
355 DS: DotStoreJoin<RecordingSentinel> + Clone + Default,
356 {
357 self.test_join_with_and_track(store, context, &mut |_| (), &mut RecordingSentinel::new())
358 .expect("RecordingSentinel is infallible");
359 }
360
361 fn join_with_and_track<S>(
362 &mut self,
363 store: DS,
364 context: &CausalContext,
365 on_dot_change: &mut dyn FnMut(DotChange),
366 sentinel: &mut S,
367 ) -> Result<(), S::Error>
368 where
369 DS: DotStoreJoin<S> + Default,
370 S: Sentinel,
371 {
372 let old_store = std::mem::take(&mut self.store);
373 self.store = DS::join(
374 (old_store, &self.context),
375 (store, context),
376 on_dot_change,
377 sentinel,
378 )?;
379 self.context.union(context);
380 Ok(())
381 }
382}
383
384impl<DS> CausalDotStore<DS> {
385 /// Constructs a new [`CausalDotStore`] by applying the given function to the current store.
386 ///
387 /// This method keeps the causal context as-is.
388 pub fn map_store<DS2>(self, m: impl FnOnce(DS) -> DS2) -> CausalDotStore<DS2> {
389 CausalDotStore {
390 store: (m)(self.store),
391 context: self.context,
392 }
393 }
394
395 /// Constructs a new [`CausalDotStore`] by applying the given function to the current context.
396 ///
397 /// This method keeps the store as-is.
398 pub fn map_context(self, m: impl FnOnce(CausalContext) -> CausalContext) -> CausalDotStore<DS> {
399 CausalDotStore {
400 store: self.store,
401 context: (m)(self.context),
402 }
403 }
404
405 /// Calls a function with a reference to the contained store.
406 ///
407 /// Returns the original [`CausalDotStore`] unchanged.
408 pub fn inspect(self, f: impl FnOnce(&DS)) -> CausalDotStore<DS> {
409 f(&self.store);
410 self
411 }
412}
413
414/// A container for data-type specific information that stores the state of a 𝛿-based CRDT.
415///
416/// This trait defines the common interface for all dot stores. It provides methods for querying
417/// the dots contained within the store, checking if the store is empty (i.e., ⊥), and creating a
418/// subset of the store for inflation.
419pub trait DotStore {
420 /// Queries the set of event identifiers (ie, dots) currently stored in the dot store.
421 ///
422 /// Has a default implementation that creates an empty [`CausalContext`] and invokes
423 /// `add_dots_to`.
424 fn dots(&self) -> CausalContext {
425 let mut cc = CausalContext::default();
426 self.add_dots_to(&mut cc);
427 cc
428 }
429
430 /// Add the set of event identifiers (ie, dots) currently stored in the dot store to `other`.
431 ///
432 /// Should not compact the resulting `CausalContext`.
433 fn add_dots_to(&self, other: &mut CausalContext);
434
435 /// Returns true if this dot store is ⊥ (ie, empty).
436 fn is_bottom(&self) -> bool;
437
438 /// Returns a subset-CRDT derived from `self` that allows inflating state at the vector time
439 /// `frontier` to what's in `self`.
440 fn subset_for_inflation_from(&self, frontier: &CausalContext) -> Self;
441}
442
443/// An observed change to a dot store.
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
445pub enum DotChange {
446 /// The given dot was added to the store.
447 Add(Dot),
448 /// The given dot was removed from the store.
449 Remove(Dot),
450}
451
452/// The outcome of performing a dry-join.
453///
454/// When doing a dry-join, we don't perform the join completely.
455/// However, we often need to know whether the join would have
456/// been bottom or not, had it been carried out. This single
457/// bit of information is significantly cheaper to calculate than
458/// a full join.
459#[derive(Debug)]
460pub struct DryJoinOutput {
461 /// True if the output of the dry-join was ⊥ (bottom).
462 is_bottom: bool,
463}
464
465impl DryJoinOutput {
466 /// Create a join-result representing ⊥
467 pub fn bottom() -> Self {
468 Self { is_bottom: true }
469 }
470 /// Update self, setting it to 'not bottom'
471 pub fn set_is_not_bottom(&mut self) {
472 self.is_bottom = false;
473 }
474 pub fn new(is_bottom: bool) -> Self {
475 Self { is_bottom }
476 }
477 /// If parameter is false, set self.is_bottom to false.
478 /// Intuitively this is a join of two dry-join results.
479 pub fn union_with(&mut self, other: DryJoinOutput) {
480 if !other.is_bottom {
481 self.is_bottom = false;
482 }
483 }
484 /// Like `join_with`, but returns the result instead.
485 pub fn union(&self, other: Self) -> Self {
486 Self {
487 is_bottom: self.is_bottom && other.is_bottom,
488 }
489 }
490 /// Returns true if this instance represents ⊥ (bottom)
491 pub fn is_bottom(&self) -> bool {
492 self.is_bottom
493 }
494}
495
496/// A trait for dot stores that can be joined.
497///
498/// This trait defines the `join` and `dry_join` operations, which are the core of the CRDT
499/// logic. The `join` operation merges the state of two dot stores, while `dry_join` simulates a
500/// join without actually modifying the state, which is useful for validation.
501pub trait DotStoreJoin<S>: DotStore {
502 /// Computes the join (⊔) between two CausalDotStores.
503 ///
504 /// Note that for efficiency this does not take a [`CausalDotStore`] directly, but instead
505 /// takes owned [`DotStore`]s and a shared reference to the [`CausalContext`] to avoid
506 /// excessive cloning.
507 ///
508 /// Quoth the DSON paper:
509 ///
510 /// > For any 𝑋 ∈ Causal⟨DotStore⟩, 𝑋 ⊔ ⊥ = 𝑋.
511 /// >
512 /// > For two elements 𝑋1, 𝑋2 ∈ Causal⟨DotStore⟩
513 /// > we say that 𝑋1 < 𝑋2 iff ∃𝑋 ≠ ⊥ ∈ Causal⟨DotStore⟩ such that 𝑋1 ⊔ 𝑋 = 𝑋2.
514 ///
515 /// > An example of a ⊥ value is a merge between two elements of the Causal⟨DotFun⟩
516 /// > semilattice, where the domains are disjoint but all mappings are in the others causal
517 /// > history. Consider for example a write 𝑤1 that precedes a write 𝑤2, i.e., 𝑤1 ≺𝜎 𝑤2, then
518 /// > the dot generated by 𝑤1 is in the causal context of the delta generated by 𝑤2. By the
519 /// > definition of join, the mapping doesn’t “survive” the join, and therefore the old value
520 /// > (written by 𝑤1) is overwritten – it isn’t present in the range of the map after 𝑤2.
521 ///
522 /// The exact semantics of a DotStore's join varies depending on the concrete type used.
523 ///
524 /// # Observing changes
525 /// Join (⊔) operations are commutative, i.e. 𝑋1 ⊔ 𝑋2 = 𝑋2 ⊔ 𝑋1, so the order of arguments
526 /// ds1 and ds2 doesn't matter w.r.t. the final result. However, conventionally we interpret
527 /// ds1 as the current state and ds2 as an incoming delta, so from the perspective of the
528 /// sentinel, changes are applied from ds2 into ds1. The same applies to `on_dot_change`.
529 fn join(
530 ds1: (Self, &CausalContext),
531 ds2: (Self, &CausalContext),
532 on_dot_change: &mut dyn FnMut(DotChange),
533 sentinel: &mut S,
534 ) -> Result<Self, S::Error>
535 where
536 Self: Sized,
537 S: Sentinel;
538
539 // YADR: 2024-10-10 Implementation of DryJoin
540 //
541 // In the context of ensuring the schema-conformance of deltas, we faced the challenge of
542 // how to efficiently validate such deltas prior to merging them into the canonical root
543 // document.
544 //
545 // We decided for adding a "dry" join that duplicates the join logic but doesn't change any
546 // document state as it runs, and neglected alternatives that would require cloning the root
547 // document, keeping an undo log, or allow a sentinel to exit after applying a subset of the
548 // changes (further details at the end of the YADR).
549 //
550 // We did this to achieve minimal performance overhead (cloning), complexity (undo-log), and
551 // surprise factor (partial application) of performing delta validations, accepting the
552 // duplication of code between join and dry-join as well as the maintenance burden this
553 // brings when adding or updating CRDTs.
554 //
555 // We think this is the right trade-off because adding new CRDTs and changing old ones is
556 // uncommon, performance of validation is paramount given its frequency, and debugging
557 // two nearly-identical implementations is likely to be easier than debugging a
558 // join + undo-log combination.
559 // Alternatives that were considered and rejected:
560 //
561 // * Do the regular join, but keep an undo-log. If a validation error is detected,
562 // the delta is undone. This was deemed relatively hard to implement.
563 // * Clone the entire document before the join, then do a regular join. This
564 // does not perform well. Experiments show a cost of ~50ms on fast machines, for reasonably
565 // sized documents (in the 10s of megabytes).
566 // * Do a regular join, but allow the sentinel to veto changes. This was deemed difficult
567 // to implement, and to define the semantics of. It would also result in partial updated,
568 // which is generally undesirable and hard to reason about.
569 // * Implement DryJoin and regular join using the same code. Create abstractions and
570 // use generics as needed to achieve this. A simplified, but functional, prototype using this
571 // approach was written. The complexity was deemed undesirable.
572
573 /// Simulates a [`DotStoreJoin::join`] without constructing the output of the join.
574 ///
575 /// This simulation allows a sentinel to observe a join without committing its result,
576 /// such as to validate a delta prior to joining it.
577 ///
578 /// Since this method does not have to construct the join output, it does not need to take
579 /// ownership of its parameters (ie, it can be run on shared references to the dot stores).
580 ///
581 /// This method returns an indicator determining if the result of the real join would have
582 /// been the bottom type.
583 fn dry_join(
584 ds1: (&Self, &CausalContext),
585 ds2: (&Self, &CausalContext),
586 sentinel: &mut S,
587 ) -> Result<DryJoinOutput, S::Error>
588 where
589 Self: Sized,
590 S: Sentinel;
591}
592
593/// A map from [`Dot`] to `V` whose computed dots is its keyset.
594///
595/// Quoth the DSON paper:
596///
597/// > A join of Causal⟨DotFun⟩ keeps values that exist in both of the mappings and merges their
598/// > respective values, or that exist in either one of the mappings and are “new” to the other in
599/// > the sense that they are not in its causal history.
600///
601/// In practice, this means that a join of two [`DotFun`] will keep only up-to-date elements. In
602/// particular, if instance X1 has observed some [`Dot`] that exists in X2, but that [`Dot`] is not
603/// present in X1, then that [`Dot`] is _not_ preserved (as it has presumably been removed).
604#[derive(Clone, PartialEq, Eq)]
605#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
606pub struct DotFun<V> {
607 // NOTE: the store is explicitly ordered by dot so that self-healing conflicts arising due
608 // to out-of-order delivery of messages can be dealt with by final consumers by just taking
609 // the last value among the conflicts, thus avoiding the need to access the dots directly. This
610 // implicit resolution strategy works as long as the entry is only ever mutated by a single
611 // `Identifier`, as in that case it is guaranteed that later/higher dots will override their
612 // predecessors once all dots have eventually been observed.
613 state: SmallVec<[(Dot, V); 1]>,
614}
615
616impl<V: fmt::Debug> fmt::Debug for DotFun<V> {
617 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618 f.debug_map().entries(self.iter()).finish()
619 }
620}
621
622// manual impl because auto-derive'd `Clone` requires `V: Clone`.
623impl<V> Default for DotFun<V> {
624 fn default() -> Self {
625 Self {
626 state: Default::default(),
627 }
628 }
629}
630
631/// An iterator over the values of a [`DotFun`].
632pub struct DotFunValueIter<'df, V> {
633 it: std::slice::Iter<'df, (Dot, V)>,
634}
635
636impl<'df, V> Iterator for DotFunValueIter<'df, V> {
637 type Item = &'df V;
638
639 fn next(&mut self) -> Option<Self::Item> {
640 self.it.next().map(|(_, v)| v)
641 }
642
643 fn size_hint(&self) -> (usize, Option<usize>) {
644 self.it.size_hint()
645 }
646
647 fn count(self) -> usize
648 where
649 Self: Sized,
650 {
651 self.it.count()
652 }
653
654 fn last(self) -> Option<Self::Item>
655 where
656 Self: Sized,
657 {
658 self.it.last().map(|(_, v)| v)
659 }
660
661 fn nth(&mut self, n: usize) -> Option<Self::Item> {
662 self.it.nth(n).map(|(_, v)| v)
663 }
664}
665impl<V> ExactSizeIterator for DotFunValueIter<'_, V> {}
666impl<V> Clone for DotFunValueIter<'_, V> {
667 fn clone(&self) -> Self {
668 Self {
669 it: self.it.clone(),
670 }
671 }
672}
673impl<V> fmt::Debug for DotFunValueIter<'_, V>
674where
675 V: fmt::Debug,
676{
677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678 self.it.fmt(f)
679 }
680}
681
682impl<V> DotFun<V> {
683 /// Constructs a [`DotFun`] with the given initial capacity.
684 pub fn with_capacity(capacity: usize) -> Self {
685 Self {
686 state: SmallVec::with_capacity(capacity),
687 }
688 }
689
690 /// Produces an iterator over the map's keys and values.
691 pub fn iter(&self) -> impl ExactSizeIterator<Item = (Dot, &V)> {
692 self.state.iter().map(|(k, v)| (*k, v))
693 }
694
695 /// Produces an iterator over the map's keys.
696 pub fn keys(&self) -> impl ExactSizeIterator<Item = Dot> + '_ {
697 self.iter().map(|(k, _)| k)
698 }
699
700 /// Produces an iterator over the map's values.
701 pub fn values(&self) -> DotFunValueIter<'_, V> {
702 DotFunValueIter {
703 it: self.state.iter(),
704 }
705 }
706
707 /// Returns the number of keys in the map.
708 pub fn len(&self) -> usize {
709 self.state.len()
710 }
711
712 /// Returns true if the map is empty.
713 pub fn is_empty(&self) -> bool {
714 self.state.is_empty()
715 }
716
717 fn get_index(&self, dot: &Dot) -> Option<usize> {
718 self.state
719 .as_slice()
720 .binary_search_by_key(dot, |(k, _)| *k)
721 .ok()
722 }
723
724 /// Retrieves the associated value, if any, for the given [`Dot`].
725 pub fn get(&self, dot: &Dot) -> Option<&V> {
726 self.get_index(dot).map(|idx| &self.state[idx].1)
727 }
728
729 /// Retrieves a mutable reference to the associated value, if any, for the given [`Dot`].
730 pub fn get_mut(&mut self, dot: &Dot) -> Option<&mut V> {
731 self.get_index(dot).map(|idx| &mut self.state[idx].1)
732 }
733
734 /// Returns `true` if the given [`Dot`] has a value in this map.
735 pub fn has(&self, dot: &Dot) -> bool {
736 self.get_index(dot).is_some()
737 }
738
739 /// Associates the value with the given [`Dot`].
740 ///
741 /// Returns the previous value if any.
742 pub fn set(&mut self, dot: Dot, value: V) -> Option<V> {
743 if let Some(v) = self.get_mut(&dot) {
744 Some(std::mem::replace(v, value))
745 } else {
746 let idx = self.state.partition_point(|(d, _)| *d < dot);
747 self.state.insert(idx, (dot, value));
748 None
749 }
750 }
751
752 /// Removes and returns the value associated with a [`Dot`], if the dot exists.
753 pub fn remove(&mut self, dot: &Dot) -> Option<V> {
754 if let Some(idx) = self.get_index(dot) {
755 // as tempting as it may be, we shouldn't use swap_remove here as we
756 // want to keep the list sorted
757 Some(self.state.remove(idx).1)
758 } else {
759 None
760 }
761 }
762
763 /// Retains only the values for which a predicate is true.
764 pub fn retain(&mut self, mut f: impl FnMut(&Dot, &mut V) -> bool) {
765 self.state.retain(|(k, v)| f(k, v))
766 }
767}
768
769impl<V> DotStore for DotFun<V>
770where
771 V: PartialEq + fmt::Debug + Clone,
772{
773 fn add_dots_to(&self, other: &mut CausalContext) {
774 other.insert_dots(self.keys());
775 }
776
777 fn is_bottom(&self) -> bool {
778 self.is_empty()
779 }
780
781 fn subset_for_inflation_from(&self, frontier: &CausalContext) -> Self {
782 Self {
783 state: self
784 .state
785 .iter()
786 .filter(|(dot, _)| !frontier.dot_in(*dot))
787 .map(|(dot, v)| (*dot, v.clone()))
788 .collect(),
789 }
790 }
791}
792
793impl<V, S> DotStoreJoin<S> for DotFun<V>
794where
795 S: ValueSentinel<V>,
796 V: PartialEq + fmt::Debug + Clone,
797{
798 /// Formally (Equation 4):
799 /// ```text
800 /// > (𝑚, 𝑐) ⊔ (𝑚′, 𝑐′) =
801 /// > (
802 /// > {𝑑 ↦ 𝑚[𝑑] ⊔ 𝑚′ [𝑑] | 𝑑 ∈ dom 𝑚 ∩ dom 𝑚′}
803 /// > ∪ {(𝑑, 𝑣) ∈ 𝑚 | 𝑑 ∉ 𝑐′}
804 /// > ∪ {(𝑑, 𝑣) ∈ 𝑚′ | 𝑑 ∉ 𝑐}
805 /// > , 𝑐 ∪ 𝑐′
806 /// > )
807 /// ```
808 ///
809 /// Informally:
810 /// - for dots in both stores, join the values
811 /// - for dots in store 1 that haven't been observed by store 2, keep the value
812 /// - for dots in store 2 that haven't been observed by store 1, keep the value
813 /// - don't keep other dots
814 /// - the resulting causal context is the union of the provided causal contexts
815 fn join(
816 (m1, cc1): (Self, &CausalContext),
817 (mut m2, cc2): (Self, &CausalContext),
818 on_dot_change: &mut dyn FnMut(DotChange),
819 sentinel: &mut S,
820 ) -> Result<Self, S::Error>
821 where
822 S: Sentinel,
823 {
824 // NOTE! When making changes to this method, consider if corresponding
825 // changes need to be done to ::dry_join as well!
826
827 let mut res_m = Self::with_capacity(m1.len().max(m2.len()));
828 for (dot, v1) in m1.state {
829 if let Some(v2) = m2.remove(&dot) {
830 // dots are assumed to be unique, so there's no need to join these as they must by
831 // implication be identical.
832 if v1 != v2 {
833 // this should be unreachable, since validation should have caught this
834 unreachable!("duplicate node id detected");
835 }
836
837 res_m.set(dot, v1);
838 } else if !cc2.dot_in(dot) {
839 // m1 has v, m2 does not, but m2 hasn't observed the dot, so we keep v as this can't
840 // be a removal.
841 // TODO(#2): is ds1's author authorized to write this value?
842 res_m.set(dot, v1);
843 } else {
844 // m1 map has a value that m2 does not, _but_ the map that does not have the
845 // value (m2) has already observed the dot in its causal context. So, it must have
846 // intentionally chosen to remove the value, and thus we should not preserve it.
847 // TODO(#2): is ds2's author authorized to clear this value?
848 sentinel.unset(v1)?;
849 on_dot_change(DotChange::Remove(dot));
850 }
851 }
852
853 // m2 has v2, m1 does not, and m1 hasn't observed the dot
854 // meaning v2 should be preserved (it wasn't deleted by m1)
855 // TODO(#2): is ds2's author authorized to write this value?
856 for (dot, v2) in m2.state.into_iter().filter(|(dot, _)| !cc1.dot_in(*dot)) {
857 sentinel.set(&v2)?;
858 res_m.set(dot, v2);
859 on_dot_change(DotChange::Add(dot));
860 }
861
862 Ok(res_m)
863 }
864
865 fn dry_join(
866 (m1, cc1): (&Self, &CausalContext),
867 (m2, cc2): (&Self, &CausalContext),
868 sentinel: &mut S,
869 ) -> Result<DryJoinOutput, S::Error>
870 where
871 Self: Sized,
872 S: Sentinel,
873 {
874 // For explanation of this method, see comments in ::join(..).
875
876 let mut res_m = DryJoinOutput::bottom();
877
878 for (dot, v1) in &m1.state {
879 if let Some(v2) = m2.get(dot) {
880 res_m.set_is_not_bottom();
881 if v1 != v2 {
882 panic!("duplicate node id detected (in crdt join)")
883 }
884 } else if !cc2.dot_in(*dot) {
885 res_m.set_is_not_bottom();
886 } else {
887 sentinel.unset(v1.clone())?;
888 }
889 }
890
891 for (_dot, v2) in m2
892 .state
893 .iter()
894 .filter(|(dot, _)| !m1.has(dot) && !cc1.dot_in(*dot))
895 {
896 res_m.set_is_not_bottom();
897 sentinel.set(v2)?;
898 }
899
900 Ok(res_m)
901 }
902}
903
904/// A map from [`Dot`] to `V: DotStore`, whose computed dots is the union of the dots of its
905/// values.
906///
907/// Quoth the DSON paper:
908///
909/// > We combine the [`DotMap`] and [`DotFun`] to get a dot store which maps dots to dot stores.
910/// > The join operation keeps keys that have not been deleted (as in the [`DotFun`]), or the
911/// > values themselves haven’t been deleted (as in the [`DotMap`]).
912///
913/// Note that this is called `CompDotFun` in the DSON paper (section 5.1), but `DotFunMap` in their
914/// prototype implementation.
915#[derive(Clone, PartialEq, Eq)]
916#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
917#[doc(alias = "CompDotFun")]
918pub struct DotFunMap<V> {
919 state: HashMap<Dot, V, DsonRandomState>,
920}
921
922impl<V: fmt::Debug> fmt::Debug for DotFunMap<V> {
923 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924 f.debug_map().entries(self.state.iter()).finish()
925 }
926}
927
928impl<V> Default for DotFunMap<V> {
929 fn default() -> Self {
930 Self {
931 state: create_map(),
932 }
933 }
934}
935
936impl<V> DotFunMap<V> {
937 /// Constructs a [`DotFunMap`] with the given initial capacity.
938 pub fn with_capacity(capacity: usize) -> Self {
939 Self {
940 state: create_map_with_capacity(capacity),
941 }
942 }
943
944 /// Produces an iterator over the map's keys and values.
945 pub fn iter(&self) -> impl ExactSizeIterator<Item = (Dot, &V)> {
946 self.state.iter().map(|(&k, v)| (k, v))
947 }
948
949 /// Produces an iterator over the map's keys.
950 pub fn keys(&self) -> impl ExactSizeIterator<Item = Dot> + '_ {
951 self.state.keys().copied()
952 }
953
954 /// Produces an iterator over the map's values.
955 pub fn values(&self) -> impl ExactSizeIterator<Item = &V> {
956 self.state.values()
957 }
958
959 /// Returns the number of keys in the map.
960 pub fn len(&self) -> usize {
961 self.state.len()
962 }
963
964 /// Returns true if the map is empty.
965 pub fn is_empty(&self) -> bool {
966 self.state.is_empty()
967 }
968}
969
970impl<V> DotFunMap<V> {
971 /// Retrieves the associated value, if any, for the given [`Dot`].
972 pub fn get(&self, dot: &Dot) -> Option<&V> {
973 self.state.get(dot)
974 }
975
976 /// Retrieves a mutable reference to the associated value, if any, for the given [`Dot`].
977 pub fn get_mut(&mut self, dot: &Dot) -> Option<&mut V> {
978 self.state.get_mut(dot)
979 }
980
981 /// Returns `true` if the given [`Dot`] has a value in this map.
982 pub fn has(&self, dot: &Dot) -> bool {
983 self.state.contains_key(dot)
984 }
985
986 /// Associates the value with the given [`Dot`].
987 ///
988 /// Returns the previous value if any.
989 pub fn set(&mut self, dot: Dot, value: V) -> Option<V> {
990 self.state.insert(dot, value)
991 }
992}
993
994impl<V> FromIterator<(Dot, V)> for DotFunMap<V> {
995 fn from_iter<T: IntoIterator<Item = (Dot, V)>>(iter: T) -> Self {
996 Self {
997 state: HashMap::from_iter(iter),
998 }
999 }
1000}
1001
1002impl<V> DotStore for DotFunMap<V>
1003where
1004 V: DotStore + fmt::Debug,
1005{
1006 fn add_dots_to(&self, other: &mut CausalContext) {
1007 // NOTE: Equation 6 in the paper suggests this should also include self.keys(),
1008 // but the original implementation does not. the text just before eq6 also says
1009 // "Note that the dots method returns the dots in the domain, as well as a union
1010 // on recursive calls of dots on all dot stores in the range", which again,
1011 // doesn't seem true in the implementation. so we do what we think is right.
1012 // This was confirmed by one of the DSON paper authors (Arik Rinberg) by email
1013 // on 2023-08-25.
1014 other.insert_dots(self.keys());
1015 for v in self.values() {
1016 v.add_dots_to(other);
1017 }
1018 }
1019
1020 fn is_bottom(&self) -> bool {
1021 self.state.is_empty()
1022 }
1023
1024 fn subset_for_inflation_from(&self, frontier: &CausalContext) -> Self {
1025 let mut delta = Self {
1026 state: create_map_with_capacity(self.state.len()),
1027 };
1028
1029 for (&dot, v) in &self.state {
1030 let delta_v = v.subset_for_inflation_from(frontier);
1031 if !delta_v.is_bottom() {
1032 // NOTE: we do not consider whether frontier.dot_in(dot), since updates can
1033 // happen _under_ old dots.
1034 delta.state.insert(dot, delta_v);
1035 }
1036 }
1037
1038 delta
1039 }
1040}
1041
1042impl<V, S> DotStoreJoin<S> for DotFunMap<V>
1043where
1044 V: DotStoreJoin<S> + fmt::Debug + Default,
1045 S: Visit<Dot> + KeySentinel,
1046{
1047 /// Formally (Equation 7):
1048 ///
1049 /// > (𝑚, 𝑐) ⊔ (𝑚′, 𝑐′) =
1050 /// > (
1051 /// > {𝑑 ↦ 𝑣(𝑑) | 𝑑 ∈ dom 𝑚 ∩ dom 𝑚′ ∧ 𝑣(𝑑) ≠ ⊥}
1052 /// > ∪ {(𝑑, 𝑣) ∈ 𝑚 | 𝑑 ∉ 𝑐′}
1053 /// > ∪ {(𝑑, 𝑣) ∈ 𝑚′ | 𝑑 ∉ 𝑐}
1054 /// > , 𝑐 ∪ 𝑐′
1055 /// > )
1056 /// > where 𝑣(𝑑) = fst((𝑚(𝑑), 𝑐) ⊔ (𝑚′(𝑑), 𝑐′))
1057 ///
1058 /// Informally:
1059 /// - for dots in both stores, join the values and keep non-bottoms
1060 /// - for dots in store 1 that haven't beeen observed by store 2, keep the value
1061 /// - for dots in store 2 that haven't beeen observed by store 1, keep the value
1062 /// - don't keep other dots
1063 /// - the resulting causal context is the union of the provided causal contexts
1064 fn join(
1065 (m1, cc1): (Self, &CausalContext),
1066 (mut m2, cc2): (Self, &CausalContext),
1067 on_dot_change: &mut dyn FnMut(DotChange),
1068 sentinel: &mut S,
1069 ) -> Result<Self, S::Error>
1070 where
1071 S: Sentinel,
1072 {
1073 // NOTE! When making changes to this method, consider if corresponding
1074 // changes need to be done to ::dry_join as well!
1075
1076 let mut res_m = Self::with_capacity(m1.len().max(m2.len()));
1077 for (dot, v1) in m1.state {
1078 sentinel.enter(&dot)?;
1079 if let Some(v2) = m2.state.remove(&dot) {
1080 let new_v = V::join((v1, cc1), (v2, cc2), on_dot_change, sentinel)?;
1081 if !new_v.is_bottom() {
1082 res_m.set(dot, new_v);
1083 } else {
1084 on_dot_change(DotChange::Remove(dot));
1085 }
1086 } else if !cc2.dot_in(dot) {
1087 // m1 has v, m2 does not, but m2 hasn't observed the dot, so we keep v as this can't
1088 // be a removal.
1089 // TODO(#2): is ds1's author authorized to write this value?
1090 // NOTE: the original implementation does not join the value here, and neither
1091 // do we. this may seem odd since presumably cc2 may not have seen the root dot but
1092 // _may_ have seen updates inside of that dot that we need to take into account.
1093 // however, that is actually impossible due to how dots work -- since the dot is
1094 // the key, and cc2 hasn't seen the key dot, it _cannot_ have seen anything inside
1095 // that dot's value either, as that would mean having seen the key, which is the
1096 // dot. it would be _safe_ to do a join here, we just happen to know it _must_ end
1097 // up returning v1 as-is.
1098 res_m.set(dot, v1);
1099 } else {
1100 // m1 map has a value that m2 does not, _but_ the map that does not have the
1101 // value (m2) has already observed the dot in its causal context. So, it must have
1102 // intentionally chosen to remove the value, and thus we should not preserve it.
1103 // TODO(#2): is ds2's author authorized to clear this value?
1104 // NOTE: there's an important subtlety here compared to DotMap -- if the key
1105 // dot is removed, all nested values are also removed _by implication_. that is, we
1106 // do not join v1 with Default and look for bottom (like we do in DotMap for this
1107 // case), but rather take a removal of a root dot as a subtree removal. this is
1108 // reinforced by the DSON paper:
1109 //
1110 // > As the root is stored in a CompDotFun, once a root is removed it never
1111 // > reappears, contrary to keys in a DotMap which may remain undeleted if there is
1112 // > a concurrent update.
1113 //
1114 // this is awkward for sentinels, because it means they are not notified of
1115 // anything that happens inside of such a nested removed-by-implication value (and
1116 // ditto for `on_dot_change`), so we specifically choose to still join here to
1117 // provide that signal. we just join with Default and cc1's own `CausalContext` so
1118 // that we are sure that everything will be treated as a deletion (which would not
1119 // necessarily be the case if we used cc2's `CausalContext`).
1120 on_dot_change(DotChange::Remove(dot));
1121 let new_v = V::join((v1, cc1), (V::default(), cc1), on_dot_change, sentinel)?;
1122 assert!(new_v.is_bottom());
1123 sentinel.delete_key()?;
1124 }
1125 sentinel.exit()?;
1126 }
1127
1128 // m2 has v2, m1 does not, and m1 hasn't observed the dot
1129 // meaning v2 should be preserved (it wasn't deleted by m1)
1130 // TODO(#2): is ds2's author authorized to write this value?
1131 for (dot, v2) in m2.state {
1132 sentinel.enter(&dot)?;
1133 if !cc1.dot_in(dot) {
1134 on_dot_change(DotChange::Add(dot));
1135 // NOTE: as mentioned in the earlier for loop, the DSON paper does not
1136 // indicate a join should be performed here. however, unlike the inverse case
1137 // (`!cc2.dot_in(dot)`) we do the join anyway here so that `sentinel` and
1138 // `on_dot_change` get information about new stuff from v2. this is only important
1139 // because sentinel and on_dot_change aren't symmetrical with respect to v1 and v2
1140 // as per the "Observing changes" section of the `join` docs.
1141 let new_v = V::join((V::default(), cc1), (v2, cc2), on_dot_change, sentinel)?;
1142 sentinel.create_key()?;
1143 res_m.state.insert(dot, new_v);
1144 }
1145 sentinel.exit()?;
1146 }
1147
1148 Ok(res_m)
1149 }
1150 fn dry_join(
1151 (m1, cc1): (&Self, &CausalContext),
1152 (m2, cc2): (&Self, &CausalContext),
1153 sentinel: &mut S,
1154 ) -> Result<DryJoinOutput, S::Error>
1155 where
1156 S: Sentinel,
1157 {
1158 // For explanation of this method, see comments in ::join(..).
1159
1160 let mut res_m = DryJoinOutput::bottom();
1161 for (dot, v1) in m1.state.iter() {
1162 sentinel.enter(dot)?;
1163 if let Some(v2) = m2.state.get(dot) {
1164 let new_v = V::dry_join((v1, cc1), (v2, cc2), sentinel)?;
1165 if !new_v.is_bottom() {
1166 res_m.set_is_not_bottom();
1167 }
1168 } else if !cc2.dot_in(*dot) {
1169 res_m.set_is_not_bottom();
1170 } else {
1171 let default_v = V::default();
1172 let new_v = V::dry_join((v1, cc1), (&default_v, cc1), sentinel)?;
1173 assert!(new_v.is_bottom());
1174 sentinel.delete_key()?;
1175 }
1176 sentinel.exit()?;
1177 }
1178
1179 for (dot, v2) in m2.state.iter().filter(|(dot, _v2)| !m1.has(dot)) {
1180 sentinel.enter(dot)?;
1181 if !cc1.dot_in(*dot) {
1182 let _new_v = V::dry_join((&V::default(), cc1), (v2, cc2), sentinel)?;
1183 sentinel.create_key()?;
1184 // Just inserting a value to a map makes that map be not-bottom.
1185 res_m.set_is_not_bottom();
1186 }
1187 sentinel.exit()?;
1188 }
1189
1190 Ok(res_m)
1191 }
1192}
1193
1194/// A map from an arbitrary key type to a `V: DotStore`, whose computed dots is the union of the
1195/// dots of its values.
1196///
1197/// This is used to implement OR-Maps (Observed-Remove Maps).
1198///
1199/// Quoth the DSON paper:
1200///
1201/// > The merge in the Causal⟨DotMap⟩ applies the merge recursively on each of the keys in either
1202/// > domains, and keeps all non-⊥ values.
1203#[derive(Clone)]
1204#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
1205pub struct DotMap<K, V> {
1206 #[cfg_attr(
1207 feature = "serde",
1208 serde(bound(
1209 serialize = "K: Hash + Eq + serde::Serialize, V: serde::Serialize",
1210 deserialize = "K: Hash + Eq + serde::Deserialize<'de>, V: serde::Deserialize<'de>"
1211 ))
1212 )]
1213 state: HashMap<K, DotMapValue<V>, DsonRandomState>,
1214}
1215
1216impl<K, V> FromIterator<(K, V)> for DotMap<K, V>
1217where
1218 K: Eq + Hash,
1219{
1220 fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
1221 Self {
1222 state: HashMap::from_iter(iter.into_iter().map(|(key, value)| {
1223 (
1224 key,
1225 DotMapValue {
1226 value,
1227 dots: Default::default(),
1228 },
1229 )
1230 })),
1231 }
1232 }
1233}
1234
1235impl<K, Q, V> Index<&Q> for DotMap<K, V>
1236where
1237 K: Eq + Hash + Borrow<Q>,
1238 Q: Eq + Hash + ?Sized,
1239{
1240 type Output = V;
1241
1242 fn index(&self, index: &Q) -> &Self::Output {
1243 &self.state.index(index).value
1244 }
1245}
1246
1247/// A value in a [`DotMap`], which includes the value itself and a cached set of dots.
1248#[derive(Clone, Default)]
1249#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
1250pub struct DotMapValue<V> {
1251 /// The value stored in the map.
1252 pub(super) value: V,
1253
1254 /// This field, if set, holds a cached version of `value.dots()`.
1255 ///
1256 /// Its purpose is to ensure that calls to `self.dots()` (or analogously `self.add_dots_to()`)
1257 /// do not need to recurse into `value.dots()`. This is hugely important for performance, as it
1258 /// ensures that calls to `.dots()` on map entries are (generally) quite cheap, rather than
1259 /// having to walk the entire subtree of objects. This, in turn, allows `.dots()` to be
1260 /// used to cheaply determine whether a subtree needs to be entered to discover changes based
1261 /// on some other `CausalContext`.
1262 ///
1263 /// The field is read primarily in `impl DotStore`, and updated in `impl DotStoreJoin` using
1264 /// the `on_dot_change` machinery.
1265 #[cfg_attr(feature = "serde", serde(skip))]
1266 pub(super) dots: Option<CausalContext>,
1267}
1268
1269impl<V: fmt::Debug> fmt::Debug for DotMapValue<V> {
1270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1271 f.debug_tuple(if self.dots.is_some() { "V" } else { "v" })
1272 .field(&self.value)
1273 .finish()
1274 }
1275}
1276
1277impl<V> PartialEq for DotMapValue<V>
1278where
1279 V: PartialEq,
1280{
1281 fn eq(&self, other: &Self) -> bool {
1282 self.value == other.value
1283 }
1284}
1285impl<V> Eq for DotMapValue<V> where V: Eq {}
1286
1287impl<V> DotStore for DotMapValue<V>
1288where
1289 V: DotStore,
1290{
1291 fn dots(&self) -> CausalContext {
1292 if let Some(dots) = &self.dots {
1293 debug_assert_eq!(dots, &self.value.dots());
1294 dots.clone()
1295 } else {
1296 self.value.dots()
1297 }
1298 }
1299
1300 fn add_dots_to(&self, other: &mut CausalContext) {
1301 if let Some(dots) = &self.dots {
1302 debug_assert_eq!(dots, &self.value.dots());
1303 other.union(dots);
1304 } else {
1305 self.value.add_dots_to(other);
1306 }
1307 }
1308
1309 fn is_bottom(&self) -> bool {
1310 self.value.is_bottom()
1311 }
1312
1313 fn subset_for_inflation_from(&self, frontier: &CausalContext) -> Self {
1314 Self {
1315 value: self.value.subset_for_inflation_from(frontier),
1316 dots: None,
1317 }
1318 }
1319}
1320
1321impl<S, V> DotStoreJoin<S> for DotMapValue<V>
1322where
1323 V: DotStoreJoin<S> + Default + fmt::Debug,
1324{
1325 fn join(
1326 (s1, ctx1): (Self, &CausalContext),
1327 (s2, ctx2): (Self, &CausalContext),
1328 on_dot_change: &mut dyn FnMut(DotChange),
1329 sentinel: &mut S,
1330 ) -> Result<Self, S::Error>
1331 where
1332 Self: Sized,
1333 S: crate::sentinel::Sentinel,
1334 {
1335 // NOTE! When making changes to this method, consider if corresponding
1336 // changes need to be done to ::dry_join as well!
1337
1338 let Self {
1339 value: s1,
1340 mut dots,
1341 } = s1;
1342 let Self { value: s2, dots: _ } = s2;
1343 let value = V::join(
1344 (s1, ctx1),
1345 (s2, ctx2),
1346 &mut |change| {
1347 if let Some(dots) = &mut dots {
1348 match change {
1349 DotChange::Add(dot) => {
1350 dots.insert_dot(dot);
1351 }
1352 DotChange::Remove(dot) => {
1353 dots.remove_dot(dot);
1354 }
1355 }
1356 }
1357 on_dot_change(change);
1358 },
1359 sentinel,
1360 )?;
1361 dots = Some(
1362 dots.map(|dots| {
1363 debug_assert_eq!(dots, value.dots(), "{value:?}");
1364 dots
1365 })
1366 .unwrap_or_else(|| value.dots()),
1367 );
1368 Ok(Self { value, dots })
1369 }
1370 fn dry_join(
1371 (s1, ctx1): (&Self, &CausalContext),
1372 (s2, ctx2): (&Self, &CausalContext),
1373 sentinel: &mut S,
1374 ) -> Result<DryJoinOutput, S::Error>
1375 where
1376 Self: Sized,
1377 S: Sentinel,
1378 {
1379 let Self { value: s1, dots: _ } = s1;
1380 let Self { value: s2, dots: _ } = s2;
1381 let value = V::dry_join((s1, ctx1), (s2, ctx2), sentinel)?;
1382 Ok(value)
1383 }
1384}
1385
1386impl<K: fmt::Debug, V: fmt::Debug> fmt::Debug for DotMap<K, V> {
1387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1388 f.debug_map().entries(self.state.iter()).finish()
1389 }
1390}
1391
1392impl<K, V> Default for DotMap<K, V> {
1393 fn default() -> Self {
1394 Self {
1395 state: create_map(),
1396 }
1397 }
1398}
1399
1400impl<K, V> PartialEq for DotMap<K, V>
1401where
1402 K: Eq + Hash,
1403 V: PartialEq,
1404{
1405 fn eq(&self, other: &Self) -> bool {
1406 self.state.eq(&other.state)
1407 }
1408}
1409
1410impl<K, V> Eq for DotMap<K, V>
1411where
1412 K: Eq + Hash,
1413 V: Eq,
1414{
1415}
1416
1417impl<K, V> DotMap<K, V> {
1418 /// Constructs a [`DotMap`] with the given initial capacity.
1419 pub fn with_capacity(capacity: usize) -> Self {
1420 Self {
1421 state: create_map_with_capacity(capacity),
1422 }
1423 }
1424
1425 /// Produces an iterator over the map's keys and values.
1426 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&K, &V)> {
1427 self.state.iter().map(|(k, v)| (k, &v.value))
1428 }
1429
1430 // Insert the given key and value into the map.
1431 //
1432 // If the key is already present, its value is overwritten.
1433 //
1434 // Note, this is a low level operation. CRDT types should generally
1435 // not be manipulated directly by user code.
1436 #[doc(hidden)]
1437 pub fn insert(&mut self, key: K, value: V)
1438 where
1439 K: Eq + Hash,
1440 {
1441 self.state.insert(key, DotMapValue { value, dots: None });
1442 }
1443
1444 /// Produces a mutable iterator over the map's keys and values.
1445 ///
1446 /// Invalidates the dots cache for all the map's entries, so calling `.dots()` on this
1447 /// collection after invoking this method may be quite slow (it has to call `.dots()` on all
1448 /// the entries).
1449 pub fn iter_mut_and_invalidate(&mut self) -> impl ExactSizeIterator<Item = (&K, &mut V)> {
1450 self.state.iter_mut().map(|(k, v)| {
1451 // see `get_mut` for why we need this
1452 v.dots = None;
1453 (k, &mut v.value)
1454 })
1455 }
1456
1457 /// Produces an iterator over the map's keys.
1458 pub fn keys(&self) -> impl ExactSizeIterator<Item = &K> + '_ {
1459 self.state.keys()
1460 }
1461
1462 /// Produces an iterator over the map's values.
1463 pub fn values(&self) -> impl ExactSizeIterator<Item = &V> {
1464 self.state.values().map(|v| &v.value)
1465 }
1466
1467 /// Returns the number of keys in the map.
1468 pub fn len(&self) -> usize {
1469 self.state.len()
1470 }
1471
1472 /// Returns true if the map is empty.
1473 pub fn is_empty(&self) -> bool {
1474 self.state.is_empty()
1475 }
1476
1477 #[cfg(any(test, feature = "arbitrary"))]
1478 pub(crate) fn shrink(&self) -> Box<dyn Iterator<Item = Self>>
1479 where
1480 K: Clone + quickcheck::Arbitrary + Hash + Eq,
1481 V: Clone + quickcheck::Arbitrary,
1482 {
1483 Box::new(quickcheck::Arbitrary::shrink(&self.state).map(|state| Self { state }))
1484 }
1485}
1486
1487impl<K, V> DotMap<K, V>
1488where
1489 K: Hash + Eq,
1490{
1491 /// Retrieves the associated value, if any, for the given key.
1492 pub fn get<Q>(&self, key: &Q) -> Option<&V>
1493 where
1494 K: Borrow<Q>,
1495 Q: Hash + Eq + ?Sized,
1496 {
1497 self.state.get(key).map(|v| &v.value)
1498 }
1499
1500 /// Retrieves a mutable reference to the associated value, if any, for the given key.
1501 ///
1502 /// Invalidates the dots cache for the given map entry, so calling `.dots()` on this collection
1503 /// after invoking this method may be slower as it has to call `.dots()` on this entry to
1504 /// re-compute.
1505 pub fn get_mut_and_invalidate<Q>(&mut self, key: &Q) -> Option<&mut V>
1506 where
1507 K: Borrow<Q>,
1508 Q: Hash + Eq + ?Sized,
1509 {
1510 self.state.get_mut(key).map(|v| {
1511 // giving out `&mut v.value` ultimately permits changing `.value`, which in turn might
1512 // change its dots. if `v.value`'s dots change, then `v.dots` also needs to change to
1513 // match, but since we don't here know what that change (if any) might be, all we can
1514 // do is invalidate our cache so that it'll be re-computed at the next read.
1515 //
1516 // this problem (and solution) applies inductively: changing `v.value` changes
1517 // `v.dots`, which also changes `self.dots()`, which may in turn be cached somewhere
1518 // further up. however, since such a cache must _also_ have gone through `get_mut` (or
1519 // `iter_mut`), that cache must already have been invalidated for us to get here in the
1520 // first place.
1521 //
1522 // another option would be to somehow have a handle to those parents here (or
1523 // specifically, `&mut DotMapValue.dots`), but threading that through would be a
1524 // lifetime nightmare.
1525 v.dots = None;
1526 &mut v.value
1527 })
1528 }
1529
1530 /// Returns `true` if the given key has a value in this map.
1531 pub fn has<Q>(&self, key: &Q) -> bool
1532 where
1533 K: Borrow<Q>,
1534 Q: Hash + Eq + ?Sized,
1535 {
1536 self.state.contains_key(key)
1537 }
1538
1539 /// Associates the value with the given key.
1540 ///
1541 /// Returns the previous value if any.
1542 pub fn set(&mut self, key: K, value: V) -> Option<V>
1543 where
1544 V: DotStore,
1545 {
1546 let dots = Some(value.dots());
1547 self.state
1548 .insert(key, DotMapValue { value, dots })
1549 .map(|v| v.value)
1550 }
1551
1552 /// Removes the value with the given key.
1553 ///
1554 /// Returns the previous value if any.
1555 ///
1556 /// Be mindful that removing a key from a `DotMap` also changes its set of dots, but does _not_
1557 /// change the [`CausalContext`] in which the `DotMap` resides. As a result, removing a key in
1558 /// this way will make the CRDT that this `DotMap` represents imply the deletion of, not just
1559 /// the absence of, `key`.
1560 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
1561 where
1562 K: Borrow<Q>,
1563 Q: Hash + Eq + ?Sized,
1564 {
1565 self.state.remove(key).map(|v| v.value)
1566 }
1567
1568 /// Retains only key-value pairs for which `f` returns `true`.
1569 ///
1570 /// See [`DotMap::remove`] for the CRDT implications of removing keys in this way.
1571 ///
1572 /// Note that this does not invalidate the cache, even though it may become inaccurate. For an
1573 /// invalidating alternative, use [`Self::retain_and_invalidate`].
1574 pub fn retain<F>(&mut self, mut f: F)
1575 where
1576 F: FnMut(&K, &mut V) -> bool,
1577 {
1578 self.state.retain(|k, v| f(k, &mut v.value))
1579 }
1580
1581 /// Retains only key-value pairs for which `f` returns `true`.
1582 ///
1583 /// See [`DotMap::remove`] for the CRDT implications of removing keys in this way.
1584 ///
1585 /// Invalidates the dots cache for all entries, so calling `.dots()` on this collection
1586 /// after invoking this method may be slower as it has to call `.dots()` on each entry to
1587 /// re-compute.
1588 pub fn retain_and_invalidate<F>(&mut self, mut f: F)
1589 where
1590 F: FnMut(&K, &mut V) -> bool,
1591 {
1592 self.state.retain(|k, v| {
1593 // see comment in [`Self::get_mut_and_invalidate`]
1594 v.dots = None;
1595 f(k, &mut v.value)
1596 })
1597 }
1598}
1599
1600impl<K, V> DotStore for DotMap<K, V>
1601where
1602 K: Hash + Eq + fmt::Debug + Clone,
1603 V: DotStore,
1604{
1605 fn add_dots_to(&self, other: &mut CausalContext) {
1606 for v in self.values() {
1607 v.add_dots_to(other);
1608 }
1609 }
1610
1611 fn is_bottom(&self) -> bool {
1612 self.state.values().all(DotStore::is_bottom)
1613 }
1614
1615 fn subset_for_inflation_from(&self, frontier: &CausalContext) -> Self {
1616 let mut delta = Self {
1617 state: create_map_with_capacity(self.state.len()),
1618 };
1619
1620 for (k, v) in &self.state {
1621 let delta_v = v.subset_for_inflation_from(frontier);
1622 if !delta_v.is_bottom() {
1623 delta.state.insert(k.clone(), delta_v);
1624 }
1625 }
1626
1627 delta
1628 }
1629}
1630
1631impl<K, V, S> DotStoreJoin<S> for DotMap<K, V>
1632where
1633 K: Hash + Eq + fmt::Debug + Clone,
1634 V: DotStoreJoin<S> + Default + fmt::Debug,
1635 S: Visit<K> + KeySentinel,
1636 // needed for debug assertions of relevant_deletes optimization
1637 V: Clone + PartialEq,
1638{
1639 /// Formally (Equation 5):
1640 ///
1641 /// > (𝑚, 𝑐) ⊔ (𝑚′, 𝑐′) =
1642 /// > (
1643 /// > {𝑘 ↦ 𝑣(𝑘) | 𝑘 ∈ dom 𝑚 ∪ dom 𝑚′ ∧ 𝑣(𝑘) ≠ ⊥}
1644 /// > , 𝑐 ∪ 𝑐′
1645 /// > )
1646 /// > where 𝑣(𝑘) = fst((𝑚(𝑘),𝑐) ⊔ (𝑚′(𝑘), 𝑐′))
1647 ///
1648 /// Informally:
1649 /// - take the union of keys across the two stores:
1650 /// - compute v as the join of the keys' values in the two maps (one may be ⊥)
1651 /// - if v.store is ⊥, skip
1652 /// - otherwise, include the k -> v.store mapping
1653 /// - the resulting causal context is the union of the provided causal contexts
1654 fn join(
1655 (m1, cc1): (Self, &CausalContext),
1656 (mut m2, cc2): (Self, &CausalContext),
1657 on_dot_change: &mut dyn FnMut(DotChange),
1658 sentinel: &mut S,
1659 ) -> Result<Self, S::Error> {
1660 // NOTE! When making changes to this method, consider if corresponding
1661 // changes need to be done to ::dry_join as well!
1662
1663 // NOTE: the original code collects all the keys first, but that would require V: Clone
1664 // (and is unnecessarily inefficient), so we take a different approach that _should_ have
1665 // the exact same semantics: we iterate over m1 first, removing anything that's also in m2,
1666 // and then we iterate over what's left in m2.
1667 // TODO: we really shouldn't allocate a new Self here and then do a tonne of inserts,
1668 // since self can be quite large.
1669 let mut res_m = Self::with_capacity(m1.len().max(m2.len()));
1670 for (k, v1) in m1.state {
1671 sentinel.enter(&k)?;
1672 let v2 = m2.state.remove(&k).unwrap_or_default();
1673 if v2.is_bottom() {
1674 // TODO: We could maybe use something like a bloom filter here
1675 // rather than have to fully compare the dots. I think this would mean keeping a
1676 // bloom filter inside of `DotMapValue` that we also keep up to date, and then
1677 // taking the intersection of the bloom filters here, which should hopefully be
1678 // cheaper than what we currently do. An idea worth exploring at least!
1679 // NOTE: if v1 is also bottom (which _can_ happen), there is nothing to
1680 // delete, so there are definitely no relevant deletes.
1681 let relevant_deletes =
1682 !v1.is_bottom() && v1.dots.as_ref().is_none_or(|dots| dots.any_dot_in(cc2));
1683 if !relevant_deletes {
1684 // since v2 is bottom, if it contains any of the dots in v1, then that means it
1685 // deletes _something_ under k, and so we need to recurse into k. otherwise, we
1686 // can avoid recursing into the CRDT subtree under k entirely!
1687 //
1688 // in debug mode, validate that v1 indeed does not change as a result of
1689 // joining with v2:
1690 if cfg!(debug_assertions) {
1691 let new_v = DotMapValue::<V>::join(
1692 (v1.clone(), cc1),
1693 (v2, cc2),
1694 &mut |_| {},
1695 // TODO: this should arguably be DummySentinel so that tests don't
1696 // rely on the sentinel getting to see these operations just because
1697 // the code is compiled in debug mode. however, doing so would require
1698 // that we add `V: DotStoreJoin<DummySentinel>` as well, which would
1699 // bubble up in a bunch of places.
1700 sentinel,
1701 )?;
1702
1703 if v1.is_bottom() {
1704 // there are multiple ways to be bottom, so don't check strict equality
1705 assert!(new_v.is_bottom(), "{v1:?}\nis bottom, but not\n{new_v:?}");
1706 } else {
1707 assert_eq!(v1, new_v);
1708 }
1709 }
1710 // NOTE: this also preserves v1.dots, ensuring that we never have to walk
1711 // all of v1 as part of the join (which would be bad as v1 could be the entire
1712 // CRDT state not just delta size)!
1713 res_m.state.insert(k, v1);
1714 sentinel.exit()?;
1715 continue;
1716 }
1717 }
1718 let new_v = DotMapValue::<V>::join((v1, cc1), (v2, cc2), on_dot_change, sentinel)?;
1719 if !new_v.is_bottom() {
1720 // Value was in m1, still is alive, so this is an update (but possibly v2 == v1)
1721 // TODO(#2): is ds1's author authorized to write this value?
1722 // TODO(#2): if v2 is Some, is ds2's author authorized to write this value?
1723 res_m.state.insert(k, new_v);
1724 } else {
1725 // If a value was previously bottom, it wouldn't have been in m1.state, as we don't
1726 // store bottom values. This means the value is being removed by ds2.
1727 // TODO(#2): is ds2's author authorized to clear this value?
1728 sentinel.delete_key()?;
1729 }
1730 sentinel.exit()?;
1731 }
1732 // NOTE: this now only contains keys that weren't in m1
1733 for (k, v2) in m2.state {
1734 sentinel.enter(&k)?;
1735 // TODO: implement relevant_deletes optimization here too (v1 and v2 are just
1736 // swapped). didn't do that initially since m1 tends to be the "main document" and m2
1737 // the "delta", so it's more important we avoid exhaustively walking m1 than m2.
1738 //
1739 // NOTE: However, consider the need for sentinels to observe all inserts. If we do
1740 // the optimization described above, a sentinel will no longer observe the contents
1741 // of new trees that are added to the document. We could add an associated const
1742 // to the Sentinel trait, that describes if the optimization above is allowed or not.
1743 let v1 = DotMapValue::<V>::default();
1744 let new_v = DotMapValue::<V>::join((v1, cc1), (v2, cc2), on_dot_change, sentinel)?;
1745 if !new_v.is_bottom() {
1746 // TODO(#2): is ds2's author authorized to write this value?
1747 sentinel.create_key()?;
1748 res_m.state.insert(k, new_v);
1749 } else {
1750 // TODO(#2): is ds2's author authorized to clear this value?
1751 // NOTE: do we even care that ds2 is trying to remove a key that doesn't exist?
1752 }
1753 sentinel.exit()?;
1754 }
1755
1756 Ok(res_m)
1757 }
1758
1759 fn dry_join(
1760 (m1, cc1): (&Self, &CausalContext),
1761 (m2, cc2): (&Self, &CausalContext),
1762 sentinel: &mut S,
1763 ) -> Result<DryJoinOutput, S::Error> {
1764 // For explanation of this method, see comments in ::join(..).
1765
1766 let mut result = DryJoinOutput::bottom();
1767 for (k, v1) in m1.state.iter() {
1768 sentinel.enter(k)?;
1769 let default_v = Default::default();
1770 let v2: &DotMapValue<V> = m2.state.get(k).unwrap_or(&default_v);
1771 if v2.is_bottom() {
1772 let relevant_deletes =
1773 !v1.is_bottom() && v1.dots.as_ref().is_none_or(|dots| dots.any_dot_in(cc2));
1774 if !relevant_deletes {
1775 if cfg!(debug_assertions) {
1776 let _new_v = DotMapValue::<V>::dry_join((v1, cc1), (v2, cc2), sentinel)?;
1777 }
1778 if !v1.is_bottom() {
1779 result.set_is_not_bottom();
1780 }
1781
1782 sentinel.exit()?;
1783 continue;
1784 }
1785 }
1786 let new_v = DotMapValue::<V>::dry_join((v1, cc1), (v2, cc2), sentinel)?;
1787 if !new_v.is_bottom() {
1788 result.set_is_not_bottom();
1789 } else {
1790 sentinel.delete_key()?;
1791 }
1792 sentinel.exit()?;
1793 }
1794
1795 for (k, v2) in m2.state.iter().filter(|(k, _v2)| !m1.has(k)) {
1796 sentinel.enter(k)?;
1797 let v1 = DotMapValue::<V>::default();
1798 let new_v = DotMapValue::<V>::dry_join((&v1, cc1), (v2, cc2), sentinel)?;
1799 if !new_v.is_bottom() {
1800 sentinel.create_key()?;
1801 result.set_is_not_bottom();
1802 }
1803 sentinel.exit()?;
1804 }
1805
1806 Ok(result)
1807 }
1808}
1809
1810impl<K, C> CausalDotStore<crate::crdts::ormap::OrMap<K, C>>
1811where
1812 K: Hash + Eq + fmt::Debug + Clone,
1813 C: crate::ExtensionType + Clone,
1814{
1815 /// Creates a new transaction for this store.
1816 ///
1817 /// The transaction exclusively borrows this store until committed.
1818 ///
1819 /// # Example
1820 ///
1821 /// ```
1822 /// # use dson::{CausalDotStore, Identifier, OrMap};
1823 /// let mut store = CausalDotStore::<OrMap<String>>::default();
1824 /// let id = Identifier::new(0, 0);
1825 /// let tx = store.transact(id);
1826 /// let delta = tx.commit();
1827 /// ```
1828 pub fn transact(
1829 &mut self,
1830 id: crate::Identifier,
1831 ) -> crate::transaction::MapTransaction<'_, K, C> {
1832 crate::transaction::MapTransaction::new(self, id)
1833 }
1834}
1835
1836#[cfg(any(test, feature = "arbitrary"))]
1837pub mod recording_sentinel;
1838
1839#[expect(clippy::wildcard_enum_match_arm)]
1840#[cfg(test)]
1841mod tests {
1842 use std::{
1843 collections::{BTreeMap, HashSet},
1844 num::NonZeroU64,
1845 };
1846
1847 const SEQ_1: NonZeroU64 = NonZeroU64::MIN;
1848 const SEQ_2: NonZeroU64 = NonZeroU64::MIN.saturating_add(1);
1849
1850 mod dotfun {
1851 use super::{super::*, *};
1852 use crate::sentinel::test::{NoChangeValidator, ValueCountingValidator};
1853
1854 #[test]
1855 fn basic() {
1856 let mut map = DotFun::default();
1857 assert!(map.is_empty());
1858 assert!(map.is_bottom());
1859 assert_eq!(map.len(), 0);
1860
1861 let dot = Dot::from(((0, 0), SEQ_1));
1862 assert!(!map.has(&dot));
1863 assert!(!map.dots().dot_in(dot));
1864 assert_eq!(map.get(&dot), None);
1865 assert_eq!(map.get_mut(&dot), None);
1866
1867 assert_eq!(map.set(dot, "foo"), None);
1868 assert!(map.has(&dot));
1869 assert!(map.dots().dot_in(dot));
1870 assert_eq!(map.get(&dot).copied(), Some("foo"));
1871 assert_eq!(map.get_mut(&dot).copied(), Some("foo"));
1872 assert!(!map.is_empty());
1873 assert!(!map.is_bottom());
1874 assert_eq!(map.len(), 1);
1875 }
1876
1877 #[test]
1878 fn join_bottoms() {
1879 let bottom = CausalDotStore::<DotFun<()>>::default();
1880
1881 // We don't expect to see anything created or deleted
1882 // joining bottom with bottom with no causal context
1883 // should produce bottom and an empty causal context
1884 let join = bottom
1885 .test_join(
1886 &bottom,
1887 &mut |_| unreachable!("no dots added or removed"),
1888 &mut NoChangeValidator,
1889 )
1890 .unwrap();
1891 assert_eq!(join.context, Default::default());
1892 assert!(join.store.is_bottom());
1893 }
1894
1895 #[test]
1896 fn join_with_bottom() {
1897 let mut validator = ValueCountingValidator::default();
1898
1899 let mut ds = CausalDotStore::<DotFun<_>>::default();
1900 let bottom = CausalDotStore::<DotFun<_>>::default();
1901
1902 // joining non-bottom x with bottom should produce x (no changes are observed)
1903 let dot = Dot::from(((0, 0), SEQ_1));
1904 ds.store.set(dot, ());
1905 ds.context.insert_next_dot(dot);
1906 let join = ds
1907 .test_join(
1908 &bottom,
1909 &mut |_| unreachable!("no dots added or removed"),
1910 &mut validator,
1911 )
1912 .unwrap();
1913 assert_eq!(join.context, ds.context);
1914 assert!(!join.store.is_bottom());
1915 assert_eq!(join.store.get(&dot).copied(), Some(()));
1916 assert!(validator.added.is_empty()); // we started with something and nothing was added
1917 assert!(validator.removed.is_empty());
1918
1919 // joining bottom with non-bottom x should also produce x (but a change is observed)
1920 let join = bottom
1921 .test_join(
1922 &ds,
1923 &mut |change| assert_eq!(change, DotChange::Add(dot)),
1924 &mut validator,
1925 )
1926 .unwrap();
1927 assert_eq!(join.context, ds.context);
1928 assert!(!join.store.is_bottom());
1929 assert_eq!(join.store.get(&dot).copied(), Some(()));
1930 assert_eq!(validator.added.len(), 1); // we started with nothing and something was added
1931 assert!(validator.removed.is_empty());
1932 }
1933
1934 #[test]
1935 fn join_idempotecy() {
1936 let mut ds = CausalDotStore::<DotFun<_>>::default();
1937 let dot = Dot::from(((0, 0), SEQ_1));
1938 ds.store.set(dot, ());
1939 ds.context.insert_next_dot(dot);
1940 let join = ds
1941 .test_join(
1942 &ds,
1943 &mut |_| unreachable!("self-join means no dot changes"),
1944 &mut NoChangeValidator,
1945 )
1946 .unwrap();
1947 assert_eq!(join.context, ds.context);
1948 assert!(!join.store.is_bottom());
1949 assert_eq!(join.store.get(&dot).copied(), Some(()));
1950 }
1951
1952 #[test]
1953 fn join_keeps_independent() {
1954 let mut validator = ValueCountingValidator::default();
1955
1956 let mut ds1 = CausalDotStore::<DotFun<_>>::default();
1957 let mut ds2 = CausalDotStore::<DotFun<_>>::default();
1958
1959 let dot1 = Dot::from(((0, 0), SEQ_1));
1960 ds1.store.set(dot1, "dot1");
1961 ds1.context.insert_next_dot(dot1);
1962 let dot2 = Dot::from(((1, 0), SEQ_1));
1963 ds2.store.set(dot2, "dot2");
1964 ds2.context.insert_next_dot(dot2);
1965
1966 let mut expected_context = ds1.context.clone();
1967 expected_context.union(&ds2.context);
1968
1969 let join = ds1
1970 .test_join(
1971 &ds2,
1972 &mut |change| assert_eq!(change, DotChange::Add(dot2)),
1973 &mut validator,
1974 )
1975 .unwrap();
1976 assert_eq!(validator.added, BTreeMap::from([("dot2", 1)]));
1977 assert!(validator.removed.is_empty());
1978
1979 assert_eq!(join.context, expected_context);
1980 assert!(!join.store.is_bottom());
1981 assert_eq!(join.store.get(&dot1).copied(), Some("dot1"));
1982 assert_eq!(join.store.get(&dot2).copied(), Some("dot2"));
1983 }
1984
1985 #[test]
1986 fn join_drops_removed() {
1987 let mut validator = ValueCountingValidator::default();
1988
1989 let mut ds1 = CausalDotStore::<DotFun<_>>::default();
1990 let mut ds2 = CausalDotStore::<DotFun<_>>::default();
1991
1992 let dot1 = Dot::from(((0, 0), SEQ_1));
1993 ds1.store.set(dot1, "dot1");
1994 ds1.context.insert_next_dot(dot1);
1995 let dot2 = Dot::from(((1, 0), SEQ_1));
1996 ds2.store.set(dot2, "dot2");
1997 ds2.context.insert_next_dot(dot2);
1998
1999 // make it so that ds1 has seen dot2, but does not have a value for it,
2000 // which implies that ds1 explicitly removed dot2.
2001 ds1.context.insert_next_dot(dot2);
2002
2003 let mut expected_context = ds1.context.clone();
2004 expected_context.union(&ds2.context);
2005
2006 // also check that the join is symmetrical
2007 // (modulo the semantics of on_dot_change and sentinels)
2008 let join = ds1
2009 .test_join(
2010 &ds2,
2011 &mut |_| unreachable!("ds1 has seen all of ds2, so no dot changes"),
2012 &mut validator,
2013 )
2014 .unwrap();
2015
2016 assert_eq!(join.context, expected_context);
2017 assert!(!join.store.is_bottom());
2018 assert_eq!(join.store.get(&dot1).copied(), Some("dot1"));
2019 assert_eq!(join.store.get(&dot2), None);
2020
2021 let mut added = HashSet::new();
2022 let mut removed = HashSet::new();
2023 let join = ds2
2024 .test_join(
2025 &ds1,
2026 &mut |change| match change {
2027 DotChange::Add(d) if d == dot1 => {
2028 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2029 }
2030 DotChange::Remove(d) if d == dot2 => {
2031 assert!(removed.insert(d), "on_dot_change removed {d:?} twice");
2032 }
2033 diff => unreachable!("{diff:?}"),
2034 },
2035 &mut validator,
2036 )
2037 .unwrap();
2038
2039 assert_eq!(join.context, expected_context);
2040 assert!(!join.store.is_bottom());
2041 assert_eq!(join.store.get(&dot1).copied(), Some("dot1"));
2042 assert_eq!(join.store.get(&dot2), None);
2043
2044 assert_eq!(added, HashSet::from_iter([dot1]));
2045 assert_eq!(removed, HashSet::from_iter([dot2]));
2046
2047 assert_eq!(validator.added, BTreeMap::from([("dot1", 1)]));
2048 assert_eq!(validator.removed, BTreeMap::from([("dot2", 1)]));
2049 }
2050 }
2051
2052 mod dotfunmap {
2053 use super::{super::*, *};
2054 use crate::sentinel::{DummySentinel, test::NoChangeValidator};
2055
2056 #[test]
2057 fn basic() {
2058 let mut map = DotFunMap::default();
2059 assert!(map.is_empty());
2060 assert!(map.is_bottom());
2061 assert_eq!(map.len(), 0);
2062
2063 let key = Dot::from(((9, 0), SEQ_1));
2064 let dot = Dot::from(((0, 0), SEQ_1));
2065 assert!(!map.has(&key));
2066 assert!(!map.dots().dot_in(dot));
2067 assert_eq!(map.get(&key), None);
2068 assert_eq!(map.get_mut(&key), None);
2069
2070 assert_eq!(map.set(key, DotFun::default()), None);
2071 assert!(map.has(&key));
2072 assert_ne!(map.get(&key), None);
2073 assert_ne!(map.get_mut(&key), None);
2074 assert!(!map.is_empty());
2075 assert!(!map.is_bottom());
2076 assert_eq!(map.len(), 1);
2077
2078 // since we inserted an empty DotStore, there are still no dots:
2079 assert!(!map.dots().dot_in(dot));
2080 // until we insert one:
2081 map.get_mut(&key).unwrap().set(dot, "bar");
2082 assert!(map.dots().dot_in(dot));
2083 }
2084
2085 #[test]
2086 fn join_bottoms() {
2087 let bottom = CausalDotStore::<DotFunMap<DotFun<()>>>::default();
2088
2089 // joining bottom with bottom with no causal context
2090 // should produce bottom and an empty causal context
2091 let join = bottom
2092 .test_join(
2093 &bottom,
2094 &mut |_| unreachable!("no dots added or removed"),
2095 &mut NoChangeValidator,
2096 )
2097 .unwrap();
2098 assert_eq!(join.context, Default::default());
2099 assert!(join.store.is_bottom());
2100 }
2101
2102 #[test]
2103 fn join_with_bottom() {
2104 let mut ds = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2105 let bottom = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2106
2107 // joining non-bottom x with bottom should produce x
2108 let key = Dot::from(((9, 0), SEQ_1));
2109 let dot = Dot::from(((0, 0), SEQ_1));
2110 let mut v = DotFun::default();
2111 v.set(dot, ());
2112 ds.store.set(key, v.clone());
2113 ds.context.insert_next_dot(dot);
2114 let join = ds
2115 .test_join(
2116 &bottom,
2117 &mut |_| unreachable!("no dots added or removed"),
2118 &mut DummySentinel,
2119 )
2120 .unwrap();
2121 assert_eq!(join.context, ds.context);
2122 assert!(!join.store.is_bottom());
2123 assert_eq!(join.store.get(&key), Some(&v));
2124
2125 // and that should be symmetric
2126 // (modulo the semantics of on_dot_change and sentinels)
2127 let mut added = HashSet::new();
2128 let join = bottom
2129 .test_join(
2130 &ds,
2131 &mut |change| match change {
2132 DotChange::Add(d) if [key, dot].contains(&d) => {
2133 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2134 }
2135 diff => unreachable!("{diff:?}"),
2136 },
2137 &mut DummySentinel,
2138 )
2139 .unwrap();
2140 assert_eq!(join.context, ds.context);
2141 assert!(!join.store.is_bottom());
2142 assert_eq!(join.store.get(&key), Some(&v));
2143 assert_eq!(added, HashSet::from_iter([key, dot]));
2144 }
2145
2146 #[test]
2147 fn join_idempotecy() {
2148 let mut ds = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2149
2150 let key = Dot::from(((9, 0), SEQ_1));
2151 let dot = Dot::from(((0, 0), SEQ_1));
2152 let mut v = DotFun::default();
2153 v.set(dot, ());
2154 ds.store.set(key, v.clone());
2155 ds.context.insert_next_dot(dot);
2156
2157 let join = ds
2158 .test_join(
2159 &ds,
2160 &mut |_| unreachable!("self-join means no dot changes"),
2161 &mut DummySentinel,
2162 )
2163 .unwrap();
2164 assert_eq!(join.context, ds.context);
2165 assert!(!join.store.is_bottom());
2166 assert_eq!(join.store.get(&key), Some(&v));
2167 }
2168
2169 #[test]
2170 fn join_keeps_independent() {
2171 let mut ds1 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2172 let mut ds2 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2173
2174 let key1 = Dot::from(((9, 0), SEQ_1));
2175 let dot1 = Dot::from(((0, 0), SEQ_1));
2176 let mut v1 = DotFun::default();
2177 v1.set(dot1, "dot1");
2178 ds1.store.set(key1, v1.clone());
2179 ds1.context.insert_next_dot(dot1);
2180 let key2 = Dot::from(((8, 0), SEQ_1));
2181 let dot2 = Dot::from(((1, 0), SEQ_1));
2182 let mut v2 = DotFun::default();
2183 v2.set(dot2, "dot2");
2184 ds2.store.set(key2, v2.clone());
2185 ds2.context.insert_next_dot(dot2);
2186
2187 let mut expected_context = ds1.context.clone();
2188 expected_context.union(&ds2.context);
2189
2190 let mut changes = HashMap::new();
2191 let join = ds1
2192 .test_join(
2193 &ds2,
2194 &mut |change| {
2195 *changes.entry(change).or_default() += 1;
2196 },
2197 &mut DummySentinel,
2198 )
2199 .unwrap();
2200
2201 assert_eq!(join.context, expected_context);
2202 assert!(!join.store.is_bottom());
2203 assert_eq!(join.store.get(&key1), Some(&v1));
2204 assert_eq!(join.store.get(&key2), Some(&v2));
2205 assert_eq!(
2206 changes,
2207 HashMap::from_iter([(DotChange::Add(dot2), 1), (DotChange::Add(key2), 1)])
2208 );
2209 }
2210
2211 #[test]
2212 fn join_drops_removed() {
2213 let mut ds1 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2214 let mut ds2 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2215
2216 let key1 = Dot::from(((9, 0), SEQ_1));
2217 let dot1 = Dot::from(((0, 0), SEQ_1));
2218 let mut v1 = DotFun::default();
2219 v1.set(dot1, "dot1");
2220 ds1.store.set(key1, v1.clone());
2221 ds1.context.insert_next_dot(dot1);
2222 let key2 = Dot::from(((8, 0), SEQ_1));
2223 let dot2 = Dot::from(((1, 0), SEQ_1));
2224 let mut v2 = DotFun::default();
2225 v2.set(dot2, "dot2");
2226 ds2.store.set(key2, v2.clone());
2227 ds2.context.insert_next_dot(dot2);
2228
2229 // make it so that ds1 has seen dot2, but does not have a value for key1,
2230 // which implies that ds1 explicitly removed key1.
2231 ds1.context.insert_next_dot(key2);
2232 ds1.context.insert_next_dot(dot2);
2233
2234 let mut expected_context = ds1.context.clone();
2235 expected_context.union(&ds2.context);
2236
2237 // also check that the join is symmetrical
2238 // (modulo the semantics of on_dot_change and sentinels)
2239 let join = ds1
2240 .test_join(
2241 &ds2,
2242 &mut |_| unreachable!("ds1 has seen all of ds2, so no dot changes"),
2243 &mut DummySentinel,
2244 )
2245 .unwrap();
2246
2247 assert_eq!(join.context, expected_context);
2248 assert!(!join.store.is_bottom());
2249 assert_eq!(join.store.get(&key1), Some(&v1));
2250 assert_eq!(join.store.get(&key2), None);
2251
2252 let mut added = HashSet::new();
2253 let mut removed = HashSet::new();
2254 let join = ds2
2255 .test_join(
2256 &ds1,
2257 &mut |change| match change {
2258 DotChange::Add(d) if [dot1, key1].contains(&d) => {
2259 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2260 }
2261 DotChange::Remove(d) if [dot2, key2].contains(&d) => {
2262 assert!(removed.insert(d), "on_dot_change removed {d:?} twice");
2263 }
2264 diff => unreachable!("{diff:?}"),
2265 },
2266 &mut DummySentinel,
2267 )
2268 .unwrap();
2269
2270 assert_eq!(join.context, expected_context);
2271 assert!(!join.store.is_bottom());
2272 assert_eq!(join.store.get(&key1), Some(&v1));
2273 assert_eq!(join.store.get(&key2), None);
2274 assert_eq!(added, HashSet::from_iter([dot1, key1]));
2275 assert_eq!(removed, HashSet::from_iter([dot2, key2]));
2276 }
2277
2278 #[test]
2279 fn nested_join() {
2280 let mut ds1 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2281 let mut ds2 = CausalDotStore::<DotFunMap<DotFun<_>>>::default();
2282
2283 // a single shared key this time so the join will need to join the DotFuns
2284 let key = Dot::from(((9, 0), SEQ_1));
2285
2286 let dot1 = Dot::from(((0, 0), SEQ_1));
2287 let mut v1 = DotFun::default();
2288 v1.set(dot1, "dot1");
2289 ds1.store.set(key, v1.clone());
2290 ds1.context.insert_next_dot(dot1);
2291
2292 // since we're testing nested join here too, make sure that join is _entirely_ obvious
2293 // by make it so that ds1 has seen dot2 but not repeated it, which implies that dot2 is
2294 // removed. we also add an extra value in ds2 for extra spice.
2295 let dot2 = Dot::from(((1, 0), SEQ_1));
2296 let dot3 = Dot::from(((1, 0), SEQ_2));
2297 let mut v2 = DotFun::default();
2298 v2.set(dot2, "dot2");
2299 v2.set(dot3, "dot3");
2300 ds2.store.set(key, v2.clone());
2301 ds2.context.extend([dot2, dot3]);
2302 ds1.context.insert_next_dot(dot2);
2303
2304 let mut expected_context = ds1.context.clone();
2305 expected_context.union(&ds2.context);
2306
2307 let expected_v = DotFun::join(
2308 (v1, &ds1.context),
2309 (v2, &ds2.context),
2310 &mut |change| assert_eq!(change, DotChange::Add(dot3)),
2311 &mut DummySentinel,
2312 )
2313 .unwrap();
2314
2315 // also check that the join is symmetrical
2316 // (modulo the semantics of on_dot_change and sentinels)
2317 let mut added = HashSet::new();
2318 let mut removed = HashSet::new();
2319 let join = ds1
2320 .test_join(
2321 &ds2,
2322 &mut |change| match change {
2323 DotChange::Add(d) if d == dot3 => {
2324 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2325 }
2326 diff => unreachable!("{diff:?}"),
2327 },
2328 &mut DummySentinel,
2329 )
2330 .unwrap();
2331
2332 assert_eq!(join.context, expected_context);
2333 assert!(!join.store.is_bottom());
2334 assert_eq!(join.store.get(&key), Some(&expected_v));
2335 assert_eq!(added, HashSet::from_iter([dot3]));
2336 assert_eq!(removed, HashSet::from_iter([]));
2337
2338 added.clear();
2339 removed.clear();
2340 let join = ds2
2341 .test_join(
2342 &ds1,
2343 &mut |change| match change {
2344 DotChange::Add(d) if d == dot1 => {
2345 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2346 }
2347 DotChange::Remove(d) if d == dot2 => {
2348 assert!(removed.insert(d), "on_dot_change removed {d:?} twice");
2349 }
2350 diff => unreachable!("{diff:?}"),
2351 },
2352 &mut DummySentinel,
2353 )
2354 .unwrap();
2355
2356 assert_eq!(join.context, expected_context);
2357 assert!(!join.store.is_bottom());
2358 assert_eq!(join.store.get(&key), Some(&expected_v));
2359 assert_eq!(added, HashSet::from_iter([dot1]));
2360 assert_eq!(removed, HashSet::from_iter([dot2]));
2361 }
2362 }
2363
2364 mod dotmap {
2365 use super::{super::*, *};
2366 use crate::sentinel::test::{
2367 KeyCountingValidator, NoChangeValidator, ValueCountingValidator,
2368 };
2369
2370 #[test]
2371 fn basic() {
2372 let mut map = DotMap::default();
2373 assert!(map.is_empty());
2374 assert!(map.is_bottom());
2375 assert_eq!(map.len(), 0);
2376
2377 let key = "foo";
2378 let dot = Dot::from(((0, 0), SEQ_1));
2379 assert!(!map.has(key));
2380 assert!(!map.dots().dot_in(dot));
2381 assert_eq!(map.get(key), None);
2382 assert_eq!(map.get_mut_and_invalidate(key), None);
2383
2384 assert_eq!(map.set(key, DotFun::default()), None);
2385 assert!(map.has(key));
2386 assert_ne!(map.get(key), None);
2387 assert_ne!(map.get_mut_and_invalidate(key), None);
2388 assert!(!map.is_empty());
2389 assert!(map.is_bottom(), "map is bottom since all values are bottom");
2390 assert_eq!(map.len(), 1);
2391
2392 // since we inserted an empty DotStore, there are still no dots:
2393 assert!(!map.dots().dot_in(dot));
2394 // until we insert one:
2395 map.get_mut_and_invalidate(key).unwrap().set(dot, "bar");
2396 assert!(map.dots().dot_in(dot));
2397 assert!(!map.is_bottom());
2398 }
2399
2400 #[test]
2401 fn join_bottoms() {
2402 let bottom = CausalDotStore::<DotMap<(), DotFun<()>>>::default();
2403
2404 // joining bottom with bottom with no causal context
2405 // should produce bottom and an empty causal context
2406 let join = bottom
2407 .test_join(
2408 &bottom,
2409 &mut |_| unreachable!("no dots added or removed"),
2410 &mut NoChangeValidator,
2411 )
2412 .unwrap();
2413 assert_eq!(join.context, Default::default());
2414 assert!(join.store.is_bottom());
2415 }
2416
2417 #[test]
2418 fn join_with_bottom() {
2419 let mut validator = KeyCountingValidator::default();
2420
2421 let mut ds = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2422 let bottom = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2423
2424 // joining non-bottom x with bottom should produce x (and no changes are observed)
2425 let key = "foo";
2426 let dot = Dot::from(((0, 0), SEQ_1));
2427 let mut v = DotFun::default();
2428 v.set(dot, ());
2429 ds.store.set(key, v.clone());
2430 ds.context.insert_next_dot(dot);
2431 let join = ds
2432 .test_join(
2433 &bottom,
2434 &mut |_| unreachable!("no dots added or removed"),
2435 &mut validator,
2436 )
2437 .unwrap();
2438 assert_eq!(join.context, ds.context);
2439 assert!(!join.store.is_bottom());
2440 assert_eq!(join.store.get(key), Some(&v));
2441 assert_eq!(validator.added, 0);
2442 assert_eq!(validator.removed, 0);
2443
2444 // joining bottom with non-bottom x should produce x (and a change is observed)
2445 let join = bottom
2446 .test_join(
2447 &ds,
2448 &mut |change| assert_eq!(change, DotChange::Add(dot)),
2449 &mut validator,
2450 )
2451 .unwrap();
2452 assert_eq!(join.context, ds.context);
2453 assert!(!join.store.is_bottom());
2454 assert_eq!(join.store.get(key), Some(&v));
2455 assert_eq!(validator.added, 1);
2456 assert_eq!(validator.removed, 0);
2457 }
2458
2459 #[test]
2460 fn join_idempotecy() {
2461 let mut ds = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2462
2463 let key = "foo";
2464 let dot = Dot::from(((0, 0), SEQ_1));
2465 let mut v = DotFun::default();
2466 v.set(dot, ());
2467 ds.store.set(key, v.clone());
2468 ds.context.insert_next_dot(dot);
2469
2470 let join = ds
2471 .test_join(
2472 &ds,
2473 &mut |_| unreachable!("self-join means no dot changes"),
2474 &mut NoChangeValidator,
2475 )
2476 .unwrap();
2477 assert_eq!(join.context, ds.context);
2478 assert!(!join.store.is_bottom());
2479 assert_eq!(join.store.get(key), Some(&v));
2480 }
2481
2482 #[test]
2483 fn join_keeps_independent() {
2484 let mut validator = KeyCountingValidator::default();
2485
2486 let mut ds1 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2487 let mut ds2 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2488
2489 let key1 = "foo";
2490 let dot1 = Dot::from(((0, 0), SEQ_1));
2491 let mut v1 = DotFun::default();
2492 v1.set(dot1, "dot1");
2493 ds1.store.set(key1, v1.clone());
2494 ds1.context.insert_next_dot(dot1);
2495 let key2 = "bar";
2496 let dot2 = Dot::from(((1, 0), SEQ_1));
2497 let mut v2 = DotFun::default();
2498 v2.set(dot2, "dot2");
2499 ds2.store.set(key2, v2.clone());
2500 ds2.context.insert_next_dot(dot2);
2501
2502 let mut expected_context = ds1.context.clone();
2503 expected_context.union(&ds2.context);
2504
2505 let join = ds1
2506 .test_join(
2507 &ds2,
2508 &mut |change| assert_eq!(change, DotChange::Add(dot2)),
2509 &mut validator,
2510 )
2511 .unwrap();
2512 assert_eq!(validator.added, 1);
2513 assert_eq!(validator.removed, 0);
2514
2515 assert_eq!(join.context, expected_context);
2516 assert!(!join.store.is_bottom());
2517 assert_eq!(join.store.get(key1), Some(&v1));
2518 assert_eq!(join.store.get(key2), Some(&v2));
2519 }
2520
2521 #[test]
2522 fn join_drops_removed() {
2523 let mut validator = KeyCountingValidator::default();
2524
2525 let mut ds1 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2526 let mut ds2 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2527
2528 let key1 = "foo";
2529 let dot1 = Dot::from(((0, 0), SEQ_1));
2530 let mut v1 = DotFun::default();
2531 v1.set(dot1, "dot1");
2532 ds1.store.set(key1, v1.clone());
2533 ds1.context.insert_next_dot(dot1);
2534 let key2 = "bar";
2535 let dot2 = Dot::from(((1, 0), SEQ_1));
2536 let mut v2 = DotFun::default();
2537 v2.set(dot2, "dot2");
2538 ds2.store.set(key2, v2.clone());
2539 ds2.context.insert_next_dot(dot2);
2540
2541 // make it so that ds1 has seen dot2, but does not have a value for key1,
2542 // which implies that ds1 explicitly removed key1.
2543 ds1.context.insert_next_dot(dot2);
2544
2545 let mut expected_context = ds1.context.clone();
2546 expected_context.union(&ds2.context);
2547
2548 // also check that the join is symmetrical
2549 let join = ds1
2550 .test_join(
2551 &ds2,
2552 &mut |_| unreachable!("ds1 has seen all of ds2, so no dot changes"),
2553 &mut validator,
2554 )
2555 .unwrap();
2556
2557 assert_eq!(join.context, expected_context);
2558 assert!(!join.store.is_bottom());
2559 assert_eq!(join.store.get(key1), Some(&v1));
2560 assert_eq!(join.store.get(key2), None);
2561
2562 let mut added = HashSet::new();
2563 let mut removed = HashSet::new();
2564 let join = ds2
2565 .test_join(
2566 &ds1,
2567 &mut |change| match change {
2568 DotChange::Add(d) if d == dot1 => {
2569 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2570 }
2571 DotChange::Remove(d) if d == dot2 => {
2572 assert!(removed.insert(d), "on_dot_change removed {d:?} twice");
2573 }
2574 diff => unreachable!("{diff:?}"),
2575 },
2576 &mut validator,
2577 )
2578 .unwrap();
2579
2580 assert_eq!(join.context, expected_context);
2581 assert!(!join.store.is_bottom());
2582 assert_eq!(join.store.get(key1), Some(&v1));
2583 assert_eq!(join.store.get(key2), None);
2584
2585 assert_eq!(added, HashSet::from_iter([dot1]));
2586 assert_eq!(removed, HashSet::from_iter([dot2]));
2587
2588 assert_eq!(validator.added, 1);
2589 assert_eq!(validator.removed, 1);
2590 }
2591
2592 #[test]
2593 fn nested_join() {
2594 let mut validator = ValueCountingValidator::default();
2595
2596 let mut ds1 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2597 let mut ds2 = CausalDotStore::<DotMap<_, DotFun<_>>>::default();
2598
2599 // a single shared key this time so the join will need to join the DotFuns
2600 let key = "foo";
2601
2602 let dot1 = Dot::from(((0, 0), SEQ_1));
2603 let mut v1 = DotFun::default();
2604 v1.set(dot1, "dot1");
2605 ds1.store.set(key, v1.clone());
2606 ds1.context.insert_next_dot(dot1);
2607
2608 // since we're testing nested join here too, make sure that join is _entirely_ obvious
2609 // by make it so that ds1 has seen dot2 but not repeated it, which implies that dot2 is
2610 // removed. we also add an extra value in ds2 for extra spice.
2611 let dot2 = Dot::from(((1, 0), SEQ_1));
2612 let dot3 = Dot::from(((1, 0), SEQ_2));
2613 let mut v2 = DotFun::default();
2614 v2.set(dot2, "dot2");
2615 v2.set(dot3, "dot3");
2616 ds2.store.set(key, v2.clone());
2617 ds2.context.extend([dot2, dot3]);
2618 ds1.context.insert_next_dot(dot2);
2619
2620 let mut expected_context = ds1.context.clone();
2621 expected_context.union(&ds2.context);
2622
2623 let expected_v = DotFun::join(
2624 (v1, &ds1.context),
2625 (v2, &ds2.context),
2626 &mut |_| {},
2627 &mut validator,
2628 )
2629 .unwrap();
2630 assert_eq!(validator.added, BTreeMap::from([("dot3", 1)]));
2631 assert!(validator.removed.is_empty());
2632
2633 // also check that the join is symmetrical
2634 // (modulo the semantics of on_dot_change and sentinels)
2635 let mut added = HashSet::new();
2636 let mut removed = HashSet::new();
2637 let join = ds1
2638 .test_join(
2639 &ds2,
2640 &mut |change| match change {
2641 DotChange::Add(d) if d == dot3 => {
2642 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2643 }
2644 diff => unreachable!("{diff:?}"),
2645 },
2646 &mut validator,
2647 )
2648 .unwrap();
2649
2650 assert_eq!(join.context, expected_context);
2651 assert!(!join.store.is_bottom());
2652 assert_eq!(join.store.get(key), Some(&expected_v));
2653 assert_eq!(added, HashSet::from_iter([dot3]));
2654 assert_eq!(removed, HashSet::from_iter([]));
2655
2656 added.clear();
2657 removed.clear();
2658 let join = ds2
2659 .test_join(
2660 &ds1,
2661 &mut |change| match change {
2662 DotChange::Add(d) if d == dot1 => {
2663 assert!(added.insert(d), "on_dot_change added {d:?} twice");
2664 }
2665 DotChange::Remove(d) if d == dot2 => {
2666 assert!(removed.insert(d), "on_dot_change removed {d:?} twice");
2667 }
2668 diff => unreachable!("{diff:?}"),
2669 },
2670 &mut validator,
2671 )
2672 .unwrap();
2673
2674 assert_eq!(join.context, expected_context);
2675 assert!(!join.store.is_bottom());
2676 assert_eq!(join.store.get(key), Some(&expected_v));
2677
2678 assert_eq!(added, HashSet::from_iter([dot1]));
2679 assert_eq!(removed, HashSet::from_iter([dot2]));
2680
2681 assert_eq!(validator.added, BTreeMap::from([("dot1", 1), ("dot3", 2)]));
2682 assert_eq!(validator.removed, BTreeMap::from([("dot2", 1)]));
2683 }
2684 }
2685
2686 #[test]
2687 fn causal_dot_store_transact() {
2688 use crate::{CausalDotStore, Identifier, OrMap};
2689 let mut store = CausalDotStore::<OrMap<String>>::default();
2690 let id = Identifier::new(0, 0);
2691 let tx = store.transact(id);
2692 let _delta = tx.commit();
2693 }
2694}