merge_machines 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +57 -0
  3. data/Cargo.lock +294 -0
  4. data/Cargo.toml +24 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +252 -0
  7. data/ext/merge_machines_native/core/Cargo.toml +24 -0
  8. data/ext/merge_machines_native/core/src/gcounter.rs +242 -0
  9. data/ext/merge_machines_native/core/src/gset.rs +181 -0
  10. data/ext/merge_machines_native/core/src/hybrid.rs +222 -0
  11. data/ext/merge_machines_native/core/src/lib.rs +221 -0
  12. data/ext/merge_machines_native/core/src/logical.rs +139 -0
  13. data/ext/merge_machines_native/core/src/lww.rs +213 -0
  14. data/ext/merge_machines_native/core/src/orset.rs +334 -0
  15. data/ext/merge_machines_native/core/src/pncounter.rs +165 -0
  16. data/ext/merge_machines_native/extconf.rb +3 -0
  17. data/ext/merge_machines_native/ffi/Cargo.toml +20 -0
  18. data/ext/merge_machines_native/ffi/extconf.rb +21 -0
  19. data/ext/merge_machines_native/ffi/src/data.rs +44 -0
  20. data/ext/merge_machines_native/ffi/src/gcounter.rs +138 -0
  21. data/ext/merge_machines_native/ffi/src/gset.rs +187 -0
  22. data/ext/merge_machines_native/ffi/src/hash.rs +33 -0
  23. data/ext/merge_machines_native/ffi/src/hybrid.rs +110 -0
  24. data/ext/merge_machines_native/ffi/src/lib.rs +221 -0
  25. data/ext/merge_machines_native/ffi/src/logical.rs +89 -0
  26. data/ext/merge_machines_native/ffi/src/lww.rs +360 -0
  27. data/ext/merge_machines_native/ffi/src/names.rs +82 -0
  28. data/ext/merge_machines_native/ffi/src/orset.rs +240 -0
  29. data/ext/merge_machines_native/ffi/src/pncounter.rs +143 -0
  30. data/ext/merge_machines_native/ffi/src/shape.rs +116 -0
  31. data/ext/merge_machines_native/ffi/src/wall.rs +37 -0
  32. data/lib/merge_machines/version.rb +5 -0
  33. data/lib/merge_machines.rb +8 -0
  34. data/sig/merge_machines.rbs +238 -0
  35. metadata +98 -0
@@ -0,0 +1,138 @@
1
+ //! `MergeMachines::GCounter`: a counter that only goes up.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use bytes::Bytes;
6
+ use magnus::scan_args::{get_kwargs, scan_args};
7
+ use magnus::typed_data::Obj;
8
+ use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use merge_machines::{GCounter, ReplicaId};
10
+
11
+ use crate::shape::shaped;
12
+ use crate::{
13
+ REPLICAS, Whole, blob_shape, capacity, mismatch, required_kwarg, unsupported, unsupported_blob,
14
+ };
15
+
16
+ /// The counter at a single replica count.
17
+ type Held<const R: usize> = GCounter<R>;
18
+
19
+ shaped!(Counter over Held { R4 = 4, R8 = 8, R16 = 16, R32 = 32 });
20
+
21
+ /// Runs `body` against whichever variant is held.
22
+ macro_rules! counter {
23
+ ($counter:expr, |$inner:ident| $body:expr) => {
24
+ match $counter {
25
+ Counter::R4($inner) => $body,
26
+ Counter::R8($inner) => $body,
27
+ Counter::R16($inner) => $body,
28
+ Counter::R32($inner) => $body,
29
+ }
30
+ };
31
+ }
32
+
33
+ #[magnus::wrap(class = "MergeMachines::GCounter", free_immediately, size)]
34
+ struct Wrapper(RefCell<Counter>);
35
+
36
+ const fn replicas_of<const R: usize>(_: &Held<R>) -> usize {
37
+ R
38
+ }
39
+
40
+ impl Wrapper {
41
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
42
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
43
+ let Whole(replicas): Whole<usize> = required_kwarg(args.keywords, "replicas")?;
44
+ Ok(Self(RefCell::new(dispatch(ruby, replicas, None)?)))
45
+ }
46
+
47
+ /// Reads a blob written by [`dump`](Self::dump).
48
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
49
+ let replicas = blob_shape(ruby, &blob)?;
50
+ Ok(Self(RefCell::new(dispatch(ruby, replicas, Some(&blob))?)))
51
+ }
52
+
53
+ fn replicas(&self) -> usize {
54
+ counter!(&*self.0.borrow(), |counter| replicas_of(&**counter))
55
+ }
56
+
57
+ /// Adds `by:` to `replica:`'s own count.
58
+ fn increment(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
59
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
60
+ let (replica, by) = increment_args(args.keywords)?;
61
+ capacity(
62
+ ruby,
63
+ counter!(&mut *rb_self.0.borrow_mut(), |counter| counter
64
+ .increment(replica, by)),
65
+ )
66
+ }
67
+
68
+ /// What `replica:` has added.
69
+ fn count(&self, args: &[Value]) -> Result<u64, Error> {
70
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
71
+ let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
72
+ Ok(counter!(&*self.0.borrow(), |counter| counter.count(replica)))
73
+ }
74
+
75
+ /// Every replica's count.
76
+ fn value(&self) -> u128 {
77
+ counter!(&*self.0.borrow(), |counter| counter.value())
78
+ }
79
+
80
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
81
+ let source = other.0.borrow().clone();
82
+ let Some(folded) = rb_self.0.borrow_mut().absorb(&source) else {
83
+ return Err(mismatch(ruby, "counter"));
84
+ };
85
+ capacity(ruby, folded)?;
86
+ Ok(rb_self)
87
+ }
88
+
89
+ /// The core frame, tag 0x61.
90
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
91
+ let frame = counter!(&*rb_self.0.borrow(), |counter| counter.encoded());
92
+ ruby.str_from_slice(&frame)
93
+ }
94
+
95
+ fn inspect(&self) -> String {
96
+ format!(
97
+ "#<MergeMachines::GCounter replicas={} value={}>",
98
+ self.replicas(),
99
+ self.value()
100
+ )
101
+ }
102
+ }
103
+
104
+ /// The `replica:` an increment is credited to and the `by:` it adds.
105
+ pub fn increment_args(keywords: RHash) -> Result<(ReplicaId, u64), Error> {
106
+ let keywords = get_kwargs::<_, (Whole<ReplicaId>,), (Option<Whole<u64>>,), ()>(
107
+ keywords,
108
+ &["replica"],
109
+ &["by"],
110
+ )?;
111
+ let (Whole(replica),) = keywords.required;
112
+ let (by,) = keywords.optional;
113
+ Ok((replica, by.map_or(1, |Whole(by)| by)))
114
+ }
115
+
116
+ /// The variant for `replicas`, read from `blob` when there is one.
117
+ fn dispatch(ruby: &Ruby, replicas: usize, blob: Option<&[u8]>) -> Result<Counter, Error> {
118
+ let Some(frame) = blob else {
119
+ return Counter::blank(replicas)
120
+ .ok_or_else(|| unsupported(ruby, "replicas", replicas, &REPLICAS));
121
+ };
122
+ Counter::decoded(ruby, replicas, frame)
123
+ .unwrap_or_else(|| Err(unsupported_blob(ruby, "replica count", &REPLICAS)))
124
+ }
125
+
126
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
127
+ let class = module.define_class("GCounter", ruby.class_object())?;
128
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
129
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
130
+ class.define_method("replicas", method!(Wrapper::replicas, 0))?;
131
+ class.define_method("increment", method!(Wrapper::increment, -1))?;
132
+ class.define_method("count", method!(Wrapper::count, -1))?;
133
+ class.define_method("value", method!(Wrapper::value, 0))?;
134
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
135
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
136
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
137
+ Ok(())
138
+ }
@@ -0,0 +1,187 @@
1
+ //! `MergeMachines::GSet`: a set nothing is ever taken out of.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use bytes::Bytes;
6
+ use magnus::scan_args::scan_args;
7
+ use magnus::typed_data::Obj;
8
+ use magnus::{Error, RArray, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use merge_machines::GSet;
10
+
11
+ use crate::hash::hash64;
12
+ use crate::names::{self, Names};
13
+ use crate::shape::{frame_len_of, shaped};
14
+ use crate::{
15
+ CAPACITIES, Whole, blob_shape, capacity, mismatch, required_kwarg, unsupported,
16
+ unsupported_blob,
17
+ };
18
+
19
+ /// The set at a single capacity, over the 64-bit hash each key becomes.
20
+ type Held<const N: usize> = GSet<u64, N>;
21
+
22
+ shaped!(Set over Held { N64 = 64, N256 = 256, N1024 = 1024 });
23
+
24
+ /// Runs `body` against whichever variant is held.
25
+ macro_rules! set {
26
+ ($set:expr, |$inner:ident| $body:expr) => {
27
+ match $set {
28
+ Set::N64($inner) => $body,
29
+ Set::N256($inner) => $body,
30
+ Set::N1024($inner) => $body,
31
+ }
32
+ };
33
+ }
34
+
35
+ /// The set counts 64-bit key hashes, and `names` holds the string each one
36
+ /// came from.
37
+ #[magnus::wrap(class = "MergeMachines::GSet", free_immediately, size)]
38
+ struct Wrapper {
39
+ set: RefCell<Set>,
40
+ names: RefCell<Names>,
41
+ }
42
+
43
+ const fn capacity_of<const N: usize>(_: &Held<N>) -> usize {
44
+ N
45
+ }
46
+
47
+ impl Wrapper {
48
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
49
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
50
+ let Whole(capacity): Whole<usize> = required_kwarg(args.keywords, "capacity")?;
51
+ Ok(Self {
52
+ set: RefCell::new(dispatch(ruby, capacity, None)?),
53
+ names: RefCell::new(Names::new()),
54
+ })
55
+ }
56
+
57
+ /// Reads a blob written by [`dump`](Self::dump).
58
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
59
+ let set = dispatch(ruby, blob_shape(ruby, &blob)?, Some(&blob))?;
60
+ let frame_len = set!(&set, |inner| frame_len_of(&**inner));
61
+ let names = names::read(ruby, &blob[frame_len..])?;
62
+ let elements = set!(&set, |inner| inner.iter().collect::<Vec<u64>>());
63
+ names::complete(ruby, &names, elements.into_iter())?;
64
+
65
+ let wrapper = Self {
66
+ set: RefCell::new(set),
67
+ names: RefCell::new(names),
68
+ };
69
+ wrapper.prune();
70
+ Ok(wrapper)
71
+ }
72
+
73
+ fn capacity(&self) -> usize {
74
+ set!(&*self.set.borrow(), |set| capacity_of(&**set))
75
+ }
76
+
77
+ /// Adds `key`, which is a no-op if it is already in.
78
+ fn add(ruby: &Ruby, rb_self: &Self, key: String) -> Result<(), Error> {
79
+ let element = hash64(key.as_bytes());
80
+ capacity(
81
+ ruby,
82
+ set!(&mut *rb_self.set.borrow_mut(), |set| set.insert(element)),
83
+ )?;
84
+ rb_self.names.borrow_mut().insert(element, key);
85
+ Ok(())
86
+ }
87
+
88
+ fn includes(&self, key: String) -> bool {
89
+ let element = hash64(key.as_bytes());
90
+ set!(&*self.set.borrow(), |set| set.contains(&element))
91
+ }
92
+
93
+ /// The keys in the set, in no particular order.
94
+ fn members(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
95
+ let elements = set!(&*rb_self.set.borrow(), |set| set
96
+ .iter()
97
+ .collect::<Vec<u64>>());
98
+ names::members(ruby, &rb_self.names.borrow(), &elements)
99
+ }
100
+
101
+ fn size(&self) -> usize {
102
+ set!(&*self.set.borrow(), |set| set.len())
103
+ }
104
+
105
+ fn is_empty(&self) -> bool {
106
+ set!(&*self.set.borrow(), |set| set.is_empty())
107
+ }
108
+
109
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
110
+ let source = other.set.borrow().clone();
111
+ let names = other.names.borrow().clone();
112
+ let Some(folded) = rb_self.set.borrow_mut().absorb(&source) else {
113
+ return Err(mismatch(ruby, "set"));
114
+ };
115
+ rb_self.names.borrow_mut().extend(names);
116
+ rb_self.prune();
117
+ capacity(ruby, folded)?;
118
+ Ok(rb_self)
119
+ }
120
+
121
+ /// The core frame followed by the names of the keys it holds.
122
+ ///
123
+ /// ```text
124
+ /// core frame GSet::<u64, capacity>::ENCODED_LEN bytes, tag 0x81
125
+ /// name record u64 little-endian byte length, then that many UTF-8 bytes
126
+ /// ```
127
+ ///
128
+ /// One record per key, in the core frame's order, filling the rest of the
129
+ /// blob. The frame holds 64-bit hashes, so [`load`](Self::load) rehashes
130
+ /// each name to rebuild what [`members`](Self::members) reads.
131
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
132
+ let (frame, elements) = set!(&*rb_self.set.borrow(), |set| (
133
+ set.encoded(),
134
+ set.iter().collect::<Vec<u64>>()
135
+ ));
136
+ let mut blob = Vec::from(frame);
137
+ names::write(&mut blob, &rb_self.names.borrow(), elements.into_iter());
138
+ ruby.str_from_slice(&blob)
139
+ }
140
+
141
+ fn inspect(&self) -> String {
142
+ format!(
143
+ "#<MergeMachines::GSet capacity={} size={}>",
144
+ self.capacity(),
145
+ self.size()
146
+ )
147
+ }
148
+
149
+ /// Leaves a name for every key the core holds and for nothing else.
150
+ ///
151
+ /// A merge extends the table before the core reports whether everything
152
+ /// fit, because a merge that overran capacity still took whatever it had
153
+ /// room for and those keys need their names. Reconciling afterwards keeps
154
+ /// the names of the keys that landed and drops the ones that did not.
155
+ fn prune(&self) {
156
+ let set = self.set.borrow();
157
+ self.names
158
+ .borrow_mut()
159
+ .retain(|element, _| set!(&*set, |held| held.contains(element)));
160
+ }
161
+ }
162
+
163
+ /// The variant for `capacity`, read from `blob` when there is one.
164
+ fn dispatch(ruby: &Ruby, capacity: usize, blob: Option<&[u8]>) -> Result<Set, Error> {
165
+ let Some(frame) = blob else {
166
+ return Set::blank(capacity)
167
+ .ok_or_else(|| unsupported(ruby, "capacity", capacity, &CAPACITIES));
168
+ };
169
+ Set::decoded(ruby, capacity, frame)
170
+ .unwrap_or_else(|| Err(unsupported_blob(ruby, "capacity", &CAPACITIES)))
171
+ }
172
+
173
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
174
+ let class = module.define_class("GSet", ruby.class_object())?;
175
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
176
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
177
+ class.define_method("capacity", method!(Wrapper::capacity, 0))?;
178
+ class.define_method("add", method!(Wrapper::add, 1))?;
179
+ class.define_method("include?", method!(Wrapper::includes, 1))?;
180
+ class.define_method("members", method!(Wrapper::members, 0))?;
181
+ class.define_method("size", method!(Wrapper::size, 0))?;
182
+ class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
183
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
184
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
185
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
186
+ Ok(())
187
+ }
@@ -0,0 +1,33 @@
1
+ //! The binding's only hash. The core's elements are `Copy + Eq`, so a Ruby
2
+ //! string crosses as a 64-bit hash of its UTF-8 bytes.
3
+
4
+ use crate::{FIELD_LEN, field};
5
+
6
+ const PRIME: u64 = 0x9E37_79B9_7F4A_7C15;
7
+
8
+ /// `splitmix64`'s finalizer: a bijection on [`u64`].
9
+ const fn mix64(value: u64) -> u64 {
10
+ let mut z = value.wrapping_add(PRIME);
11
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
12
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
13
+ z ^ (z >> 31)
14
+ }
15
+
16
+ /// Hashes bytes, folding [`FIELD_LEN`] at a time through [`mix64`], then the
17
+ /// remainder and the length separately.
18
+ pub fn hash64(bytes: &[u8]) -> u64 {
19
+ let (lanes, tail) = bytes.as_chunks::<FIELD_LEN>();
20
+ let mut accumulator = PRIME;
21
+
22
+ for lane in lanes {
23
+ accumulator = (accumulator ^ mix64(u64::from_le_bytes(*lane)))
24
+ .rotate_left(27)
25
+ .wrapping_mul(PRIME);
26
+ }
27
+
28
+ let mut last = [0_u8; FIELD_LEN];
29
+ last[..tail.len()].copy_from_slice(tail);
30
+ let length = u64::from_le_bytes(field(bytes.len()));
31
+
32
+ mix64(mix64(accumulator ^ length.wrapping_mul(PRIME)) ^ mix64(u64::from_le_bytes(last)))
33
+ }
@@ -0,0 +1,110 @@
1
+ //! `MergeMachines::HybridClock`: timestamps near physical time that still
2
+ //! order.
3
+
4
+ use std::cell::RefCell;
5
+
6
+ use bytes::Bytes;
7
+ use magnus::scan_args::{get_kwargs, scan_args};
8
+ use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use merge_machines::{HybridClock, ReplicaId};
10
+
11
+ use crate::data::{stamped, timestamp};
12
+ use crate::{
13
+ Whole, codec_error, exhausted, optional_kwarg, required_kwarg, trailing_replica, wall,
14
+ };
15
+
16
+ /// A clock and the replica whose events it stamps, which the core does not
17
+ /// hold.
18
+ #[magnus::wrap(class = "MergeMachines::HybridClock", free_immediately, size)]
19
+ struct Wrapper {
20
+ clock: RefCell<HybridClock>,
21
+ replica: ReplicaId,
22
+ }
23
+
24
+ impl Wrapper {
25
+ fn new(args: &[Value]) -> Result<Self, Error> {
26
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
27
+ let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
28
+ Ok(Self {
29
+ clock: RefCell::new(HybridClock::new()),
30
+ replica,
31
+ })
32
+ }
33
+
34
+ /// Reads a blob written by [`dump`](Self::dump).
35
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
36
+ let mut clock = HybridClock::new();
37
+ clock
38
+ .decode_into(&blob)
39
+ .map_err(|error| codec_error(ruby, error))?;
40
+
41
+ Ok(Self {
42
+ clock: RefCell::new(clock),
43
+ replica: trailing_replica(ruby, &blob, HybridClock::ENCODED_LEN)?,
44
+ })
45
+ }
46
+
47
+ const fn replica(&self) -> ReplicaId {
48
+ self.replica
49
+ }
50
+
51
+ /// The last timestamp this clock issued.
52
+ fn now(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
53
+ timestamp(ruby, rb_self.clock.borrow().last())
54
+ }
55
+
56
+ /// Stamps a local event at `wall_ns:`, defaulting to the realtime clock.
57
+ fn tick(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
58
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
59
+ let wall_ns = match optional_kwarg::<Whole<u64>>(args.keywords, "wall_ns")? {
60
+ Some(Whole(nanoseconds)) => nanoseconds,
61
+ None => wall::now_ns(ruby)?,
62
+ };
63
+ let issued = rb_self.clock.borrow_mut().tick(wall_ns);
64
+ timestamp(ruby, exhausted(ruby, issued)?)
65
+ }
66
+
67
+ /// Stamps the receipt of the timestamp `wall_ns:` and `counter:` name, at
68
+ /// the realtime clock's reading.
69
+ fn observe(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
70
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
71
+ let (Whole(wall_ns), Whole(counter)) = get_kwargs::<_, (Whole<u64>, Whole<u64>), (), ()>(
72
+ args.keywords,
73
+ &["wall_ns", "counter"],
74
+ &[],
75
+ )?
76
+ .required;
77
+ let now = wall::now_ns(ruby)?;
78
+ let observed = stamped(wall_ns, counter);
79
+ let issued = rb_self.clock.borrow_mut().observe(now, observed);
80
+ timestamp(ruby, exhausted(ruby, issued)?)
81
+ }
82
+
83
+ /// The core frame followed by the replica id, 64-bit little-endian.
84
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
85
+ let mut blob = Vec::from(rb_self.clock.borrow().encoded());
86
+ blob.extend_from_slice(&rb_self.replica.to_le_bytes());
87
+ ruby.str_from_slice(&blob)
88
+ }
89
+
90
+ fn inspect(&self) -> String {
91
+ let last = self.clock.borrow().last();
92
+ format!(
93
+ "#<MergeMachines::HybridClock replica={} wall_ns={} counter={}>",
94
+ self.replica, last.wall_ns, last.counter
95
+ )
96
+ }
97
+ }
98
+
99
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
100
+ let class = module.define_class("HybridClock", ruby.class_object())?;
101
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
102
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
103
+ class.define_method("replica", method!(Wrapper::replica, 0))?;
104
+ class.define_method("now", method!(Wrapper::now, 0))?;
105
+ class.define_method("tick", method!(Wrapper::tick, -1))?;
106
+ class.define_method("observe", method!(Wrapper::observe, -1))?;
107
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
108
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
109
+ Ok(())
110
+ }
@@ -0,0 +1,221 @@
1
+ //! Ruby bindings for the `merge-machines` replicated states.
2
+ //!
3
+ //! Replica counts and set capacities are const generics, so every class
4
+ //! dispatches the number it is given to a fixed set of monomorphised variants
5
+ //! and raises `ArgumentError` for any other number.
6
+ //!
7
+ //! Wall-clock readings are nanoseconds on both sides of this boundary.
8
+ //! `NS_PER_SECOND` is exposed for callers holding seconds.
9
+
10
+ #![allow(clippy::needless_pass_by_value)]
11
+
12
+ mod data;
13
+ mod gcounter;
14
+ mod gset;
15
+ mod hash;
16
+ mod hybrid;
17
+ mod logical;
18
+ mod lww;
19
+ mod names;
20
+ mod orset;
21
+ mod pncounter;
22
+ mod shape;
23
+ mod wall;
24
+
25
+ use std::sync::OnceLock;
26
+
27
+ use magnus::scan_args::get_kwargs;
28
+ use magnus::value::Opaque;
29
+ use magnus::{Error, ExceptionClass, Integer, RHash, RModule, Ruby, TryConvert, Value, prelude::*};
30
+ use merge_machines::{CapacityError, ClockExhausted, CodecError, ReplicaId};
31
+
32
+ /// `MergeMachines::Full`, defined in [`init`].
33
+ static FULL: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
34
+
35
+ /// `MergeMachines::Exhausted`, defined in [`init`].
36
+ static EXHAUSTED: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
37
+
38
+ /// Nanoseconds in a second.
39
+ pub(crate) const NS_PER_SECOND: u32 = 1_000_000_000;
40
+
41
+ /// Replica counts the counters are monomorphised for.
42
+ pub(crate) const REPLICAS: [usize; 4] = [4, 8, 16, 32];
43
+
44
+ /// Capacities the sets are monomorphised for.
45
+ pub(crate) const CAPACITIES: [usize; 3] = [64, 256, 1024];
46
+
47
+ /// Where a core frame names the shape it was written at.
48
+ pub(crate) const SHAPE: core::ops::Range<usize> = 1..3;
49
+
50
+ /// Bytes a 64-bit field occupies in a blob.
51
+ pub(crate) const FIELD_LEN: usize = 8;
52
+
53
+ /// The low [`FIELD_LEN`] bytes of `value`, little-endian.
54
+ pub(crate) fn field(value: usize) -> [u8; FIELD_LEN] {
55
+ let mut bytes = [0_u8; FIELD_LEN];
56
+ for (slot, byte) in bytes.iter_mut().zip(value.to_le_bytes()) {
57
+ *slot = byte;
58
+ }
59
+ bytes
60
+ }
61
+
62
+ /// A whole number of things: a stamp, a replica, a capacity, a counter, a
63
+ /// nanosecond reading.
64
+ ///
65
+ /// magnus reads an integer through Ruby's `NUM2*` family, which truncates a
66
+ /// `Float` towards zero instead of refusing it, so an argument that means an
67
+ /// integer is read as one of these and anything but an `Integer` is turned
68
+ /// away. An `Integer` outside the target width still comes back as the
69
+ /// `RangeError` magnus raises for it.
70
+ pub(crate) struct Whole<T>(pub(crate) T);
71
+
72
+ impl<T: TryConvert> TryConvert for Whole<T> {
73
+ fn try_convert(value: Value) -> Result<Self, Error> {
74
+ if Integer::from_value(value).is_none() {
75
+ return Err(not_whole(value));
76
+ }
77
+ T::try_convert(value).map(Self)
78
+ }
79
+ }
80
+
81
+ fn not_whole(value: Value) -> Error {
82
+ let ruby = Ruby::get_with(value);
83
+ Error::new(
84
+ ruby.exception_type_error(),
85
+ format!(
86
+ "no implicit conversion of {} into Integer: {}",
87
+ value.class(),
88
+ value.inspect()
89
+ ),
90
+ )
91
+ }
92
+
93
+ /// The one required keyword of a constructor.
94
+ pub(crate) fn required_kwarg<T: TryConvert>(keywords: RHash, name: &str) -> Result<T, Error> {
95
+ Ok(get_kwargs::<_, (T,), (), ()>(keywords, &[name], &[])?
96
+ .required
97
+ .0)
98
+ }
99
+
100
+ /// A keyword with a default the caller may leave out.
101
+ pub(crate) fn optional_kwarg<T: TryConvert>(
102
+ keywords: RHash,
103
+ name: &str,
104
+ ) -> Result<Option<T>, Error> {
105
+ Ok(
106
+ get_kwargs::<_, (), (Option<T>,), ()>(keywords, &[], &[name])?
107
+ .optional
108
+ .0,
109
+ )
110
+ }
111
+
112
+ /// The shape a blob was written at, read from its core frame.
113
+ pub(crate) fn blob_shape(ruby: &Ruby, blob: &[u8]) -> Result<usize, Error> {
114
+ let Some(field) = blob.get(SHAPE) else {
115
+ return Err(codec_error(ruby, CodecError::Truncated));
116
+ };
117
+ Ok(usize::from(u16::from_le_bytes([field[0], field[1]])))
118
+ }
119
+
120
+ /// The replica id a clock's blob carries after its core frame.
121
+ pub(crate) fn trailing_replica(
122
+ ruby: &Ruby,
123
+ blob: &[u8],
124
+ frame_len: usize,
125
+ ) -> Result<ReplicaId, Error> {
126
+ let Some((field, _)) = blob
127
+ .get(frame_len..)
128
+ .and_then(<[u8]>::split_first_chunk::<FIELD_LEN>)
129
+ else {
130
+ return Err(codec_error(ruby, CodecError::Truncated));
131
+ };
132
+ Ok(u64::from_le_bytes(*field))
133
+ }
134
+
135
+ pub(crate) fn unsupported(ruby: &Ruby, name: &str, given: usize, supported: &[usize]) -> Error {
136
+ Error::new(
137
+ ruby.exception_arg_error(),
138
+ format!("{name} {given} is not supported, expected one of {supported:?}"),
139
+ )
140
+ }
141
+
142
+ pub(crate) fn unsupported_blob(ruby: &Ruby, name: &str, supported: &[usize]) -> Error {
143
+ Error::new(
144
+ ruby.exception_arg_error(),
145
+ format!("blob was written with a {name} outside {supported:?}"),
146
+ )
147
+ }
148
+
149
+ pub(crate) fn codec_error(ruby: &Ruby, error: CodecError) -> Error {
150
+ Error::new(ruby.exception_arg_error(), format!("codec: {error}"))
151
+ }
152
+
153
+ pub(crate) fn mismatch(ruby: &Ruby, name: &str) -> Error {
154
+ Error::new(
155
+ ruby.exception_arg_error(),
156
+ format!("the other {name} has a different shape"),
157
+ )
158
+ }
159
+
160
+ pub(crate) fn uninitialised(ruby: &Ruby) -> Error {
161
+ Error::new(
162
+ ruby.exception_runtime_error(),
163
+ "the merge_machines extension is not initialised",
164
+ )
165
+ }
166
+
167
+ pub(crate) fn defined_twice(ruby: &Ruby, name: &str) -> Error {
168
+ Error::new(
169
+ ruby.exception_runtime_error(),
170
+ format!("MergeMachines::{name} was initialised twice"),
171
+ )
172
+ }
173
+
174
+ /// Raises `MergeMachines::Full` for a state that ran out of slots.
175
+ pub(crate) fn capacity(ruby: &Ruby, outcome: Result<(), CapacityError>) -> Result<(), Error> {
176
+ outcome.map_err(|error| raised(ruby, &FULL, error.to_string()))
177
+ }
178
+
179
+ /// Raises `MergeMachines::Exhausted` for a clock that ran out of timestamps.
180
+ pub(crate) fn exhausted<T>(ruby: &Ruby, outcome: Result<T, ClockExhausted>) -> Result<T, Error> {
181
+ outcome.map_err(|error| raised(ruby, &EXHAUSTED, error.to_string()))
182
+ }
183
+
184
+ /// Raises `MergeMachines::Exhausted` for a set that ran out of tags.
185
+ pub(crate) fn tags_exhausted(ruby: &Ruby) -> Error {
186
+ raised(ruby, &EXHAUSTED, "no tag left".to_owned())
187
+ }
188
+
189
+ fn raised(ruby: &Ruby, class: &OnceLock<Opaque<ExceptionClass>>, message: String) -> Error {
190
+ class.get().map_or_else(
191
+ || uninitialised(ruby),
192
+ |class| Error::new(ruby.get_inner(*class), message),
193
+ )
194
+ }
195
+
196
+ fn define_errors(ruby: &Ruby, module: RModule) -> Result<(), Error> {
197
+ let base = module.define_error("Error", ruby.exception_standard_error())?;
198
+ let full = module.define_error("Full", base)?;
199
+ FULL.set(full.into())
200
+ .map_err(|_| defined_twice(ruby, "Full"))?;
201
+ let exhausted = module.define_error("Exhausted", base)?;
202
+ EXHAUSTED
203
+ .set(exhausted.into())
204
+ .map_err(|_| defined_twice(ruby, "Exhausted"))
205
+ }
206
+
207
+ #[magnus::init]
208
+ fn init(ruby: &Ruby) -> Result<(), Error> {
209
+ let module = ruby.define_module("MergeMachines")?;
210
+ module.const_set("NS_PER_SECOND", NS_PER_SECOND)?;
211
+ define_errors(ruby, module)?;
212
+ wall::define(ruby)?;
213
+ data::define(ruby, module)?;
214
+ logical::define(ruby, module)?;
215
+ hybrid::define(ruby, module)?;
216
+ gcounter::define(ruby, module)?;
217
+ pncounter::define(ruby, module)?;
218
+ lww::define(ruby, module)?;
219
+ gset::define(ruby, module)?;
220
+ orset::define(ruby, module)
221
+ }