probabilistic_machines 0.1.1

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.
@@ -0,0 +1,325 @@
1
+ //! Which keys dominate a stream, in `K` counters instead of one per key.
2
+
3
+ use crate::hash::usize_low_u16;
4
+ use crate::{CodecError, TAG_TOPK, read8};
5
+
6
+ const HEADER_LEN: usize = 13;
7
+ const RECORD_LEN: usize = 24;
8
+ /// Where a record's count and overestimate begin, after its key.
9
+ const COUNT_AT: usize = 8;
10
+ const OVERESTIMATE_AT: usize = 16;
11
+
12
+ /// A tracked key, its counter, and how much of that counter it may have
13
+ /// inherited from keys evicted before it or from a merged-in stream that
14
+ /// tracked it no longer.
15
+ ///
16
+ /// The true count lies in `count - overestimate ..= count`.
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18
+ pub struct Hitter<T> {
19
+ pub key: T,
20
+ pub count: u64,
21
+ pub overestimate: u64,
22
+ }
23
+
24
+ /// Space-Saving over `K` counters: whatever is frequent ends up tracked.
25
+ ///
26
+ /// After `n` observations every key that occurred more than `n / K` times is in
27
+ /// the table, and no tracked key's counter is below its true count. Lookups and
28
+ /// insertions scan all `K` counters, which is the trade for needing no heap and
29
+ /// no key ordering: keep `K` in the dozens-to-hundreds range.
30
+ ///
31
+ /// ```
32
+ /// use probabilistic_machines::HeavyHitters;
33
+ ///
34
+ /// let mut traffic = HeavyHitters::<u32, 8>::new();
35
+ /// for request in 0..1_000_u32 {
36
+ /// traffic.add(request % 97);
37
+ /// }
38
+ /// for _ in 0..5_000 {
39
+ /// traffic.add(7);
40
+ /// }
41
+ ///
42
+ /// assert_eq!(traffic.observed(), 6_000);
43
+ /// assert!(traffic.count(&7) >= 5_000);
44
+ /// ```
45
+ #[derive(Debug, Clone, PartialEq, Eq)]
46
+ pub struct HeavyHitters<T, const K: usize> {
47
+ slots: [Option<Hitter<T>>; K],
48
+ observed: u64,
49
+ }
50
+
51
+ impl<T: Copy + Eq, const K: usize> HeavyHitters<T, K> {
52
+ #[must_use]
53
+ pub const fn new() -> Self {
54
+ Self {
55
+ slots: [const { None }; K],
56
+ observed: 0,
57
+ }
58
+ }
59
+
60
+ /// Observations fed in, saturating at [`u64::MAX`].
61
+ #[must_use]
62
+ pub const fn observed(&self) -> u64 {
63
+ self.observed
64
+ }
65
+
66
+ /// Keys currently tracked, at most `K`.
67
+ #[must_use]
68
+ pub fn len(&self) -> usize {
69
+ self.slots.iter().flatten().count()
70
+ }
71
+
72
+ #[must_use]
73
+ pub fn is_empty(&self) -> bool {
74
+ self.slots.iter().all(Option::is_none)
75
+ }
76
+
77
+ /// Every tracked key, in no particular order.
78
+ pub fn hitters(&self) -> impl Iterator<Item = &Hitter<T>> {
79
+ self.slots.iter().flatten()
80
+ }
81
+
82
+ /// Upper bound on how often `key` occurred, 0 when it is not tracked.
83
+ #[must_use]
84
+ pub fn count(&self, key: &T) -> u64 {
85
+ self.hitters()
86
+ .find(|hitter| hitter.key == *key)
87
+ .map_or(0, |hitter| hitter.count)
88
+ }
89
+
90
+ /// The most frequent tracked key.
91
+ #[must_use]
92
+ pub fn top(&self) -> Option<&Hitter<T>> {
93
+ self.hitters().max_by_key(|hitter| hitter.count)
94
+ }
95
+
96
+ pub fn add(&mut self, key: T) {
97
+ self.add_count(key, 1);
98
+ }
99
+
100
+ pub fn add_count(&mut self, key: T, count: u64) {
101
+ self.observed = self.observed.saturating_add(count);
102
+ self.absorb(key, count, 0);
103
+ }
104
+
105
+ /// Folds another table in. Counts of keys tracked by both are exact sums.
106
+ /// A key only one side tracked may have occurred in the other stream too,
107
+ /// up to that side's eviction floor, so it takes the floor on both its
108
+ /// bounds: the counter still covers the true count and the interval
109
+ /// `count - overestimate ..= count` still holds it.
110
+ pub fn merge(&mut self, other: &Self) {
111
+ let floor = other.floor();
112
+ for tracked in self.slots.iter_mut().flatten() {
113
+ if other.hitters().all(|hitter| hitter.key != tracked.key) {
114
+ tracked.count = tracked.count.saturating_add(floor);
115
+ tracked.overestimate = tracked.overestimate.saturating_add(floor);
116
+ }
117
+ }
118
+ for hitter in other.hitters() {
119
+ self.absorb(hitter.key, hitter.count, hitter.overestimate);
120
+ }
121
+ self.observed = self.observed.saturating_add(other.observed);
122
+ }
123
+
124
+ /// How often an untracked key may have occurred: nothing while a counter is
125
+ /// still free, otherwise the weakest counter.
126
+ fn floor(&self) -> u64 {
127
+ if self.slots.iter().any(Option::is_none) {
128
+ return 0;
129
+ }
130
+ self.hitters().map(|hitter| hitter.count).min().unwrap_or(0)
131
+ }
132
+
133
+ fn absorb(&mut self, key: T, count: u64, overestimate: u64) {
134
+ if let Some(tracked) = self
135
+ .slots
136
+ .iter_mut()
137
+ .flatten()
138
+ .find(|hitter| hitter.key == key)
139
+ {
140
+ tracked.count = tracked.count.saturating_add(count);
141
+ tracked.overestimate = tracked.overestimate.saturating_add(overestimate);
142
+ return;
143
+ }
144
+
145
+ if let Some(free) = self.slots.iter_mut().find(|slot| slot.is_none()) {
146
+ *free = Some(Hitter {
147
+ key,
148
+ count,
149
+ overestimate,
150
+ });
151
+ return;
152
+ }
153
+
154
+ let Some(weakest) = self
155
+ .slots
156
+ .iter_mut()
157
+ .flatten()
158
+ .min_by_key(|hitter| hitter.count)
159
+ else {
160
+ return;
161
+ };
162
+ let floor = weakest.count;
163
+ *weakest = Hitter {
164
+ key,
165
+ count: floor.saturating_add(count),
166
+ overestimate: floor.saturating_add(overestimate),
167
+ };
168
+ }
169
+ }
170
+
171
+ impl<const K: usize> HeavyHitters<u64, K> {
172
+ /// `K` as the frame's shape field.
173
+ const COUNTERS: [u8; 2] = {
174
+ assert!(K >= 1, "a table needs at least one counter");
175
+ assert!(K < 1 << 16, "a table needs fewer than 65536 counters");
176
+ usize_low_u16(K).to_le_bytes()
177
+ };
178
+
179
+ /// Length of the [`encode`](Self::encode) frame.
180
+ pub const ENCODED_LEN: usize = HEADER_LEN + K * RECORD_LEN;
181
+
182
+ /// Writes the table into `out`, returning the number of bytes written.
183
+ ///
184
+ /// The frame is the tag, `K`, the observation count, the number of tracked
185
+ /// keys and `K` records of key, count and overestimate. Records past the
186
+ /// tracked count are zero.
187
+ ///
188
+ /// # Errors
189
+ ///
190
+ /// [`CodecError::Truncated`] when `out` is shorter than
191
+ /// [`ENCODED_LEN`](Self::ENCODED_LEN).
192
+ pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
193
+ let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
194
+ return Err(CodecError::Truncated);
195
+ };
196
+ self.write_frame(frame);
197
+ Ok(Self::ENCODED_LEN)
198
+ }
199
+
200
+ /// The table's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
201
+ #[cfg(feature = "alloc")]
202
+ #[must_use]
203
+ pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
204
+ let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
205
+ self.write_frame(&mut frame);
206
+ frame
207
+ }
208
+
209
+ /// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
210
+ fn write_frame(&self, frame: &mut [u8]) {
211
+ frame[0] = TAG_TOPK;
212
+ frame[1..3].copy_from_slice(&Self::COUNTERS);
213
+ frame[3..11].copy_from_slice(&self.observed.to_le_bytes());
214
+ frame[11..13].copy_from_slice(&usize_low_u16(self.len()).to_le_bytes());
215
+ frame[HEADER_LEN..].fill(0);
216
+
217
+ let (slots, _) = frame[HEADER_LEN..].as_chunks_mut::<RECORD_LEN>();
218
+ for (slot, hitter) in slots.iter_mut().zip(self.hitters()) {
219
+ slot[..COUNT_AT].copy_from_slice(&hitter.key.to_le_bytes());
220
+ slot[COUNT_AT..OVERESTIMATE_AT].copy_from_slice(&hitter.count.to_le_bytes());
221
+ slot[OVERESTIMATE_AT..].copy_from_slice(&hitter.overestimate.to_le_bytes());
222
+ }
223
+ }
224
+
225
+ /// Reads a table written by [`encode`](Self::encode).
226
+ ///
227
+ /// # Errors
228
+ ///
229
+ /// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
230
+ /// when it holds another structure, [`CodecError::Parameters`] when it was
231
+ /// written with a different counter count, claims more tracked keys than
232
+ /// `K`, or holds records no stream could have left.
233
+ pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
234
+ let mut table = Self::new();
235
+ table.decode_into(bytes)?;
236
+ Ok(table)
237
+ }
238
+
239
+ /// Reads a frame over this table, for a caller that owns one already and
240
+ /// has no room on its stack for a second.
241
+ ///
242
+ /// The table is untouched when the frame is rejected.
243
+ ///
244
+ /// # Errors
245
+ ///
246
+ /// The same as [`decode`](Self::decode).
247
+ pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
248
+ let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
249
+ return Err(CodecError::Truncated);
250
+ };
251
+ if frame[0] != TAG_TOPK {
252
+ return Err(CodecError::Tag);
253
+ }
254
+ if frame[1..3] != Self::COUNTERS {
255
+ return Err(CodecError::Parameters);
256
+ }
257
+ let tracked = usize::from(u16::from_le_bytes([frame[11], frame[12]]));
258
+ if tracked > K {
259
+ return Err(CodecError::Parameters);
260
+ }
261
+
262
+ let observed = u64::from_le_bytes(read8(&frame[3..11]));
263
+ let (slots, _) = frame[HEADER_LEN..].as_chunks::<RECORD_LEN>();
264
+ let records = &slots[..tracked];
265
+ if !records_are_coherent(records, observed) {
266
+ return Err(CodecError::Parameters);
267
+ }
268
+
269
+ self.observed = observed;
270
+ for (slot, record) in self.slots.iter_mut().zip(records) {
271
+ *slot = Some(hitter(record));
272
+ }
273
+ self.slots[tracked..].fill(None);
274
+ Ok(())
275
+ }
276
+ }
277
+
278
+ /// A record as the hitter it stands for.
279
+ fn hitter(record: &[u8; RECORD_LEN]) -> Hitter<u64> {
280
+ Hitter {
281
+ key: u64::from_le_bytes(read8(&record[..COUNT_AT])),
282
+ count: u64::from_le_bytes(read8(&record[COUNT_AT..OVERESTIMATE_AT])),
283
+ overestimate: u64::from_le_bytes(read8(&record[OVERESTIMATE_AT..])),
284
+ }
285
+ }
286
+
287
+ /// Whether the records are ones Space-Saving could have tracked: a counter
288
+ /// stands for at least `count - overestimate` occurrences, which cannot outrun
289
+ /// the observations they came from, an interval runs upwards, and a key holds
290
+ /// one counter.
291
+ ///
292
+ /// The counter itself may pass the observation count: a merge lifts a key the
293
+ /// other table evicted to that table's eviction floor and widens the
294
+ /// overestimate with it, so the two tables' floors can stack above the two
295
+ /// streams' length while the occurrences the interval claims stay inside it.
296
+ fn records_are_coherent(records: &[[u8; RECORD_LEN]], observed: u64) -> bool {
297
+ records.iter().enumerate().all(|(index, record)| {
298
+ let tracked = hitter(record);
299
+ tracked.overestimate <= tracked.count
300
+ && tracked.count - tracked.overestimate <= observed
301
+ && records[..index]
302
+ .iter()
303
+ .all(|earlier| hitter(earlier).key != tracked.key)
304
+ })
305
+ }
306
+
307
+ impl<T: Copy + Eq, const K: usize> Default for HeavyHitters<T, K> {
308
+ fn default() -> Self {
309
+ Self::new()
310
+ }
311
+ }
312
+
313
+ #[cfg(feature = "alloc")]
314
+ impl<T: Copy + Eq, const K: usize> HeavyHitters<T, K> {
315
+ /// Tracked keys, most frequent first.
316
+ #[must_use]
317
+ pub fn ranked(&self) -> alloc::vec::Vec<Hitter<T>> {
318
+ let mut ranked: alloc::vec::Vec<Hitter<T>> = self.hitters().copied().collect();
319
+ ranked.sort_unstable_by_key(|hitter| core::cmp::Reverse(hitter.count));
320
+ ranked
321
+ }
322
+ }
323
+
324
+ #[cfg(test)]
325
+ mod tests;
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "probabilistic_machines_native"
3
+ version = "0.1.1"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ authors = ["Abdelkader Boudih <oss@seuros.com>"]
7
+ license = "MIT"
8
+ description = "Ruby bindings for the probabilistic-machines sketches"
9
+ publish = false
10
+
11
+ [lib]
12
+ crate-type = ["cdylib"]
13
+
14
+ [lints]
15
+ workspace = true
16
+
17
+ [dependencies]
18
+ probabilistic-machines = { workspace = true, features = ["std"] }
19
+ magnus = { version = ">= 0.9.3", default-features = false, features = ["bytes", "io"] }
20
+ bytes = "1"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'pathname'
5
+ require 'rb_sys/mkmf'
6
+
7
+ create_rust_makefile('probabilistic_machines_native/probabilistic_machines_native') do |r|
8
+ r.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
9
+ r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
10
+ end
11
+
12
+ # rake-compiler builds from tmp/, where the generated relative manifest path no
13
+ # longer resolves.
14
+ makefile = File.join(Dir.pwd, 'Makefile')
15
+ File.write(
16
+ makefile,
17
+ File.read(makefile).gsub(
18
+ /^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
19
+ "RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
20
+ )
21
+ )
@@ -0,0 +1,188 @@
1
+ //! `ProbabilisticMachines::CountMinSketch`: how often each item was seen.
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 probabilistic_machines::{CodecError, CountMinSketch};
10
+
11
+ use crate::{Whole, codec_error, mismatch, optional_kwarg, unsupported};
12
+
13
+ /// Row widths [`Sketch`] is monomorphised for.
14
+ ///
15
+ /// Capped so the widest sketch is 128 KiB. The core returns by value, so a
16
+ /// wider shape could not be built on Ruby's 256 KiB fiber stack without an
17
+ /// in-place constructor, and that needs `unsafe`.
18
+ const WIDTHS: [usize; 2] = [1024, 4096];
19
+ /// Row counts [`Sketch`] is monomorphised for.
20
+ const DEPTHS: [usize; 2] = [4, 8];
21
+
22
+ #[derive(Clone)]
23
+ enum Sketch {
24
+ W1024D4(Box<CountMinSketch<1024, 4>>),
25
+ W1024D8(Box<CountMinSketch<1024, 8>>),
26
+ W4096D4(Box<CountMinSketch<4096, 4>>),
27
+ W4096D8(Box<CountMinSketch<4096, 8>>),
28
+ }
29
+
30
+ /// Runs `body` against whichever variant is held.
31
+ macro_rules! sketch {
32
+ ($sketch:expr, |$inner:ident| $body:expr) => {
33
+ match $sketch {
34
+ Sketch::W1024D4($inner) => $body,
35
+ Sketch::W1024D8($inner) => $body,
36
+ Sketch::W4096D4($inner) => $body,
37
+ Sketch::W4096D8($inner) => $body,
38
+ }
39
+ };
40
+ }
41
+
42
+ #[magnus::wrap(
43
+ class = "ProbabilisticMachines::CountMinSketch",
44
+ free_immediately,
45
+ size
46
+ )]
47
+ struct Wrapper(RefCell<Sketch>);
48
+
49
+ const fn width_of<const W: usize, const D: usize>(_: &CountMinSketch<W, D>) -> usize {
50
+ W
51
+ }
52
+
53
+ const fn depth_of<const W: usize, const D: usize>(_: &CountMinSketch<W, D>) -> usize {
54
+ D
55
+ }
56
+
57
+ /// Each shape is constructed behind its own call frame. Inlined, a debug build
58
+ /// sizes one frame for every arm of the match below at once.
59
+ #[inline(never)]
60
+ fn boxed_new<const W: usize, const D: usize>() -> Box<CountMinSketch<W, D>> {
61
+ Box::new(CountMinSketch::new())
62
+ }
63
+
64
+ #[inline(never)]
65
+ fn boxed_decode<const W: usize, const D: usize>(
66
+ ruby: &Ruby,
67
+ frame: &[u8],
68
+ ) -> Result<Box<CountMinSketch<W, D>>, Error> {
69
+ let mut sketch = boxed_new::<W, D>();
70
+ sketch
71
+ .decode_into(frame)
72
+ .map_err(|error| codec_error(ruby, error))?;
73
+ Ok(sketch)
74
+ }
75
+
76
+ impl Wrapper {
77
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
78
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
79
+ let (Whole(width), Whole(depth)) = get_kwargs::<_, (Whole<usize>, Whole<usize>), (), ()>(
80
+ args.keywords,
81
+ &["width", "depth"],
82
+ &[],
83
+ )?
84
+ .required;
85
+ if !DEPTHS.contains(&depth) {
86
+ return Err(unsupported(ruby, "depth", depth, &DEPTHS));
87
+ }
88
+ let sketch = match (width, depth) {
89
+ (1024, 4) => Sketch::W1024D4(boxed_new::<1024, 4>()),
90
+ (1024, 8) => Sketch::W1024D8(boxed_new::<1024, 8>()),
91
+ (4096, 4) => Sketch::W4096D4(boxed_new::<4096, 4>()),
92
+ (4096, 8) => Sketch::W4096D8(boxed_new::<4096, 8>()),
93
+ _ => return Err(unsupported(ruby, "width", width, &WIDTHS)),
94
+ };
95
+ Ok(Self(RefCell::new(sketch)))
96
+ }
97
+
98
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
99
+ let Some(shape) = blob.get(1..3) else {
100
+ return Err(codec_error(ruby, CodecError::Truncated));
101
+ };
102
+ let sketch = match (shape[0], shape[1]) {
103
+ (10, 4) => Sketch::W1024D4(boxed_decode::<1024, 4>(ruby, &blob)?),
104
+ (10, 8) => Sketch::W1024D8(boxed_decode::<1024, 8>(ruby, &blob)?),
105
+ (12, 4) => Sketch::W4096D4(boxed_decode::<4096, 4>(ruby, &blob)?),
106
+ (12, 8) => Sketch::W4096D8(boxed_decode::<4096, 8>(ruby, &blob)?),
107
+ _ => {
108
+ return Err(Error::new(
109
+ ruby.exception_arg_error(),
110
+ format!("blob was written with a shape outside {WIDTHS:?} x {DEPTHS:?}"),
111
+ ));
112
+ }
113
+ };
114
+ Ok(Self(RefCell::new(sketch)))
115
+ }
116
+
117
+ fn width(&self) -> usize {
118
+ sketch!(&*self.0.borrow(), |sketch| width_of(&**sketch))
119
+ }
120
+
121
+ fn depth(&self) -> usize {
122
+ sketch!(&*self.0.borrow(), |sketch| depth_of(&**sketch))
123
+ }
124
+
125
+ fn add(rb_self: &Self, args: &[Value]) -> Result<(), Error> {
126
+ let args = scan_args::<(Bytes,), (), (), (), RHash, ()>(args)?;
127
+ let (item,) = args.required;
128
+ let count =
129
+ optional_kwarg::<Whole<u32>>(args.keywords, "count")?.map_or(1, |Whole(count)| count);
130
+ sketch!(&mut *rb_self.0.borrow_mut(), |sketch| sketch
131
+ .add_count(&item, count));
132
+ Ok(())
133
+ }
134
+
135
+ fn count(&self, item: Bytes) -> u32 {
136
+ sketch!(&*self.0.borrow(), |sketch| sketch.estimate(&item))
137
+ }
138
+
139
+ fn saturated(&self) -> bool {
140
+ sketch!(&*self.0.borrow(), |sketch| sketch.saturated())
141
+ }
142
+
143
+ fn total(&self) -> u64 {
144
+ sketch!(&*self.0.borrow(), |sketch| sketch.total())
145
+ }
146
+
147
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
148
+ let source = other.0.borrow().clone();
149
+ match (&mut *rb_self.0.borrow_mut(), &source) {
150
+ (Sketch::W1024D4(target), Sketch::W1024D4(source)) => target.merge(source),
151
+ (Sketch::W1024D8(target), Sketch::W1024D8(source)) => target.merge(source),
152
+ (Sketch::W4096D4(target), Sketch::W4096D4(source)) => target.merge(source),
153
+ (Sketch::W4096D8(target), Sketch::W4096D8(source)) => target.merge(source),
154
+ _ => return Err(mismatch(ruby, "sketch")),
155
+ }
156
+ Ok(rb_self)
157
+ }
158
+
159
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
160
+ let frame = sketch!(&*rb_self.0.borrow(), |sketch| sketch.encoded());
161
+ ruby.str_from_slice(&frame)
162
+ }
163
+
164
+ fn inspect(&self) -> String {
165
+ format!(
166
+ "#<ProbabilisticMachines::CountMinSketch width={} depth={} total={}>",
167
+ self.width(),
168
+ self.depth(),
169
+ self.total()
170
+ )
171
+ }
172
+ }
173
+
174
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
175
+ let class = module.define_class("CountMinSketch", ruby.class_object())?;
176
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
177
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
178
+ class.define_method("width", method!(Wrapper::width, 0))?;
179
+ class.define_method("depth", method!(Wrapper::depth, 0))?;
180
+ class.define_method("add", method!(Wrapper::add, -1))?;
181
+ class.define_method("count", method!(Wrapper::count, 1))?;
182
+ class.define_method("total", method!(Wrapper::total, 0))?;
183
+ class.define_method("saturated?", method!(Wrapper::saturated, 0))?;
184
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
185
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
186
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
187
+ Ok(())
188
+ }