Skip to main content

ipfrs_tensorlogic/
slice_manager.rs

1//! `TensorSliceManager` — named tensor slice management with copy-on-write semantics,
2//! bounds checking, and overlap detection.
3//!
4//! # Overview
5//!
6//! A [`TensorSliceManager`] owns a [`TensorShape`] that describes the overall tensor
7//! dimensions and a registry of named [`TensorSlice`]s.  Each slice is a contiguous
8//! sub-region described by a [`SliceSpec`] (per-dimension `[start, end)` ranges) and
9//! carries its own flattened `f32` data buffer together with a version counter and a
10//! dirty flag that supports copy-on-write workflows.
11
12use std::collections::HashMap;
13
14// ---------------------------------------------------------------------------
15// TensorShape
16// ---------------------------------------------------------------------------
17
18/// Describes the shape of a multi-dimensional tensor in C (row-major) order.
19///
20/// # Example
21///
22/// ```
23/// use ipfrs_tensorlogic::slice_manager::TensorShape;
24///
25/// let shape = TensorShape { dims: vec![4, 8, 16] };
26/// assert_eq!(shape.total_elements(), 512);
27/// assert_eq!(shape.strides(), vec![128, 16, 1]);
28/// assert_eq!(shape.flat_index(&[1, 2, 3]), Some(1 * 128 + 2 * 16 + 3));
29/// assert_eq!(shape.flat_index(&[4, 0, 0]), None); // out of bounds
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TensorShape {
33    /// Per-dimension sizes; e.g. `[4, 8, 16]` for a 3-D tensor.
34    pub dims: Vec<usize>,
35}
36
37impl TensorShape {
38    /// Returns the total number of elements (product of all dimension sizes).
39    ///
40    /// An empty shape (0-dimensional tensor) returns `1`.
41    #[must_use]
42    pub fn total_elements(&self) -> usize {
43        self.dims.iter().product()
44    }
45
46    /// Returns the C-order (row-major) strides for each dimension.
47    ///
48    /// `strides[i] = dims[i+1] * dims[i+2] * … * dims[rank-1]`.
49    /// The last stride is always `1`.  For a scalar tensor (no dims) an empty
50    /// `Vec` is returned.
51    #[must_use]
52    pub fn strides(&self) -> Vec<usize> {
53        let rank = self.dims.len();
54        let mut strides = vec![1usize; rank];
55        // Fill from the second-to-last dimension backwards.
56        for i in (0..rank.saturating_sub(1)).rev() {
57            strides[i] = strides[i + 1] * self.dims[i + 1];
58        }
59        strides
60    }
61
62    /// Converts a per-dimension index slice into a flat (linear) index.
63    ///
64    /// Returns `None` when:
65    /// - `indices.len() != self.dims.len()`, or
66    /// - any `indices[i] >= self.dims[i]`.
67    #[must_use]
68    pub fn flat_index(&self, indices: &[usize]) -> Option<usize> {
69        if indices.len() != self.dims.len() {
70            return None;
71        }
72        let strides = self.strides();
73        let mut flat = 0usize;
74        for (i, (&idx, &dim)) in indices.iter().zip(self.dims.iter()).enumerate() {
75            if idx >= dim {
76                return None;
77            }
78            flat += idx * strides[i];
79        }
80        Some(flat)
81    }
82}
83
84// ---------------------------------------------------------------------------
85// SliceSpec
86// ---------------------------------------------------------------------------
87
88/// Describes a per-dimension `[start, end)` range that selects a contiguous
89/// sub-region of a tensor.
90///
91/// Both `start` and `end` must have the same length as the enclosing tensor's
92/// rank, and for each dimension `start[i] <= end[i]`.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct SliceSpec {
95    /// Per-dimension start index (inclusive).
96    pub start: Vec<usize>,
97    /// Per-dimension end index (exclusive).
98    pub end: Vec<usize>,
99}
100
101impl SliceSpec {
102    /// Number of elements covered by this slice.
103    ///
104    /// Computed as `∏ (end[i] − start[i])`.  Zero-length dimensions contribute
105    /// a factor of `0`, resulting in an empty slice.
106    #[must_use]
107    pub fn element_count(&self) -> usize {
108        self.start
109            .iter()
110            .zip(self.end.iter())
111            .map(|(&s, &e)| e.saturating_sub(s))
112            .product()
113    }
114
115    /// Returns `true` when `self` and `other` share at least one element.
116    ///
117    /// Two slices overlap iff for **every** dimension `d`:
118    /// `self.start[d] < other.end[d]  &&  other.start[d] < self.end[d]`.
119    ///
120    /// If the two specs have different ranks the function returns `false` rather
121    /// than panicking.
122    #[must_use]
123    pub fn overlaps(&self, other: &SliceSpec) -> bool {
124        if self.start.len() != other.start.len() {
125            return false;
126        }
127        self.start
128            .iter()
129            .zip(self.end.iter())
130            .zip(other.start.iter().zip(other.end.iter()))
131            .all(|((&s, &e), (&os, &oe))| s < oe && os < e)
132    }
133}
134
135// ---------------------------------------------------------------------------
136// TensorSlice
137// ---------------------------------------------------------------------------
138
139/// A named, versioned, copy-on-write slice of a tensor.
140#[derive(Debug, Clone)]
141pub struct TensorSlice {
142    /// Human-readable name used as the registry key.
143    pub name: String,
144    /// The region of the parent tensor this slice covers.
145    pub spec: SliceSpec,
146    /// Flattened element data; length must equal `spec.element_count()`.
147    pub data: Vec<f32>,
148    /// Monotonically increasing write counter; starts at `0`.
149    pub version: u64,
150    /// `true` when the slice has been written since the last [`TensorSliceManager::flush_all`].
151    pub dirty: bool,
152}
153
154// ---------------------------------------------------------------------------
155// SliceManagerStats
156// ---------------------------------------------------------------------------
157
158/// Aggregate statistics for a [`TensorSliceManager`].
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct SliceManagerStats {
161    /// Total number of registered slices.
162    pub total_slices: usize,
163    /// Number of slices whose `dirty` flag is set.
164    pub dirty_slices: usize,
165    /// Sum of all element counts across all slices.
166    pub total_elements: usize,
167}
168
169impl SliceManagerStats {
170    /// Total memory used by all slice data buffers, in bytes.
171    ///
172    /// Each `f32` element occupies 4 bytes.
173    #[must_use]
174    pub fn memory_bytes(&self) -> usize {
175        self.total_elements * 4
176    }
177}
178
179// ---------------------------------------------------------------------------
180// TensorSliceManager
181// ---------------------------------------------------------------------------
182
183/// Registry that manages named slices of a fixed-shape tensor.
184///
185/// # Semantics
186///
187/// * **Bounds checking** — every [`SliceSpec`] is validated against the
188///   manager's [`TensorShape`] on creation.
189/// * **Copy-on-write** — each write increments `version` and marks the slice
190///   `dirty`; calling [`flush_all`](TensorSliceManager::flush_all) clears all
191///   dirty flags (e.g. after persisting to storage).
192/// * **Overlap detection** — [`overlapping_slices`](TensorSliceManager::overlapping_slices)
193///   lists every registered slice that overlaps a given query spec.
194pub struct TensorSliceManager {
195    /// Shape of the underlying tensor.
196    pub shape: TensorShape,
197    /// Registered slices, keyed by name.
198    pub slices: HashMap<String, TensorSlice>,
199}
200
201impl TensorSliceManager {
202    /// Creates a new, empty manager for a tensor of the given `shape`.
203    #[must_use]
204    pub fn new(shape: TensorShape) -> Self {
205        Self {
206            shape,
207            slices: HashMap::new(),
208        }
209    }
210
211    /// Registers a new named slice.
212    ///
213    /// # Errors
214    ///
215    /// Returns `Err` with a descriptive message when:
216    /// - a slice with the same `name` already exists,
217    /// - `spec.end[i] > shape.dims[i]` for any dimension, or
218    /// - `data.len() != spec.element_count()`.
219    pub fn create_slice(
220        &mut self,
221        name: String,
222        spec: SliceSpec,
223        data: Vec<f32>,
224    ) -> Result<(), String> {
225        if self.slices.contains_key(&name) {
226            return Err(format!("slice '{}' already exists", name));
227        }
228
229        // Validate rank.
230        let rank = self.shape.dims.len();
231        if spec.start.len() != rank || spec.end.len() != rank {
232            return Err(format!(
233                "spec rank {} does not match shape rank {}",
234                spec.start.len(),
235                rank
236            ));
237        }
238
239        // Validate per-dimension bounds.
240        for d in 0..rank {
241            if spec.end[d] > self.shape.dims[d] {
242                return Err(format!(
243                    "spec.end[{d}] = {} exceeds shape.dims[{d}] = {}",
244                    spec.end[d], self.shape.dims[d]
245                ));
246            }
247            if spec.start[d] > spec.end[d] {
248                return Err(format!(
249                    "spec.start[{d}] = {} > spec.end[{d}] = {}",
250                    spec.start[d], spec.end[d]
251                ));
252            }
253        }
254
255        // Validate data length.
256        let expected = spec.element_count();
257        if data.len() != expected {
258            return Err(format!(
259                "data.len() = {} but spec.element_count() = {}",
260                data.len(),
261                expected
262            ));
263        }
264
265        self.slices.insert(
266            name.clone(),
267            TensorSlice {
268                name,
269                spec,
270                data,
271                version: 0,
272                dirty: false,
273            },
274        );
275        Ok(())
276    }
277
278    /// Overwrites the data of an existing slice.
279    ///
280    /// On success the slice's `version` is incremented and `dirty` is set to
281    /// `true`.  Returns `false` when the slice does not exist or `data` has the
282    /// wrong length.
283    pub fn write_slice(&mut self, name: &str, data: Vec<f32>) -> bool {
284        match self.slices.get_mut(name) {
285            Some(slice) if slice.data.len() == data.len() => {
286                slice.data = data;
287                slice.version += 1;
288                slice.dirty = true;
289                true
290            }
291            _ => false,
292        }
293    }
294
295    /// Returns an immutable reference to the named slice, or `None`.
296    #[must_use]
297    pub fn read_slice(&self, name: &str) -> Option<&TensorSlice> {
298        self.slices.get(name)
299    }
300
301    /// Clears the `dirty` flag on every registered slice.
302    ///
303    /// Intended to be called after all dirty slices have been flushed to
304    /// persistent storage.
305    pub fn flush_all(&mut self) {
306        for slice in self.slices.values_mut() {
307            slice.dirty = false;
308        }
309    }
310
311    /// Returns the names of all registered slices that overlap `spec`.
312    ///
313    /// The order of the returned names is unspecified (depends on the
314    /// `HashMap` iteration order).
315    #[must_use]
316    pub fn overlapping_slices<'a>(&'a self, spec: &SliceSpec) -> Vec<&'a str> {
317        self.slices
318            .values()
319            .filter(|s| s.spec.overlaps(spec))
320            .map(|s| s.name.as_str())
321            .collect()
322    }
323
324    /// Removes the named slice from the registry.
325    ///
326    /// Returns `true` if the slice existed and was removed, `false` otherwise.
327    pub fn remove_slice(&mut self, name: &str) -> bool {
328        self.slices.remove(name).is_some()
329    }
330
331    /// Computes aggregate statistics over all registered slices.
332    #[must_use]
333    pub fn stats(&self) -> SliceManagerStats {
334        let total_slices = self.slices.len();
335        let dirty_slices = self.slices.values().filter(|s| s.dirty).count();
336        let total_elements = self.slices.values().map(|s| s.data.len()).sum();
337        SliceManagerStats {
338            total_slices,
339            dirty_slices,
340            total_elements,
341        }
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Tests
347// ---------------------------------------------------------------------------
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    // ---- helper constructors ------------------------------------------------
354
355    fn shape_3d() -> TensorShape {
356        TensorShape {
357            dims: vec![4, 8, 16],
358        }
359    }
360
361    fn spec(start: Vec<usize>, end: Vec<usize>) -> SliceSpec {
362        SliceSpec { start, end }
363    }
364
365    fn make_data(n: usize, fill: f32) -> Vec<f32> {
366        vec![fill; n]
367    }
368
369    // ---- TensorShape --------------------------------------------------------
370
371    #[test]
372    fn test_total_elements() {
373        let s = TensorShape {
374            dims: vec![4, 8, 16],
375        };
376        assert_eq!(s.total_elements(), 512);
377    }
378
379    #[test]
380    fn test_total_elements_scalar() {
381        let s = TensorShape { dims: vec![] };
382        assert_eq!(s.total_elements(), 1);
383    }
384
385    #[test]
386    fn test_strides_3d() {
387        let s = shape_3d();
388        assert_eq!(s.strides(), vec![128, 16, 1]);
389    }
390
391    #[test]
392    fn test_strides_1d() {
393        let s = TensorShape { dims: vec![10] };
394        assert_eq!(s.strides(), vec![1]);
395    }
396
397    #[test]
398    fn test_strides_2d() {
399        let s = TensorShape { dims: vec![3, 5] };
400        assert_eq!(s.strides(), vec![5, 1]);
401    }
402
403    #[test]
404    fn test_flat_index_in_bounds() {
405        let s = shape_3d();
406        assert_eq!(s.flat_index(&[1, 2, 3]), Some(128 + 2 * 16 + 3));
407        assert_eq!(s.flat_index(&[0, 0, 0]), Some(0));
408        assert_eq!(s.flat_index(&[3, 7, 15]), Some(3 * 128 + 7 * 16 + 15));
409    }
410
411    #[test]
412    fn test_flat_index_out_of_bounds() {
413        let s = shape_3d();
414        // First dimension out of bounds (4 >= 4)
415        assert_eq!(s.flat_index(&[4, 0, 0]), None);
416        // Last dimension out of bounds
417        assert_eq!(s.flat_index(&[0, 0, 16]), None);
418        // Wrong rank
419        assert_eq!(s.flat_index(&[0, 0]), None);
420    }
421
422    // ---- SliceSpec ----------------------------------------------------------
423
424    #[test]
425    fn test_element_count() {
426        let sp = spec(vec![1, 2, 4], vec![3, 6, 12]);
427        // (3-1) * (6-2) * (12-4) = 2 * 4 * 8 = 64
428        assert_eq!(sp.element_count(), 64);
429    }
430
431    #[test]
432    fn test_element_count_full_slice() {
433        let sp = spec(vec![0, 0, 0], vec![4, 8, 16]);
434        assert_eq!(sp.element_count(), 512);
435    }
436
437    #[test]
438    fn test_overlaps_true() {
439        let a = spec(vec![0, 0], vec![4, 4]);
440        let b = spec(vec![2, 2], vec![6, 6]);
441        assert!(a.overlaps(&b));
442        assert!(b.overlaps(&a));
443    }
444
445    #[test]
446    fn test_overlaps_false_adjacent() {
447        // Adjacent but not overlapping: a ends at 4, b starts at 4.
448        let a = spec(vec![0, 0], vec![4, 4]);
449        let b = spec(vec![4, 0], vec![8, 4]);
450        assert!(!a.overlaps(&b));
451        assert!(!b.overlaps(&a));
452    }
453
454    #[test]
455    fn test_overlaps_false_separated() {
456        let a = spec(vec![0, 0], vec![2, 2]);
457        let b = spec(vec![5, 5], vec![8, 8]);
458        assert!(!a.overlaps(&b));
459    }
460
461    #[test]
462    fn test_overlaps_contained() {
463        let outer = spec(vec![0, 0], vec![8, 8]);
464        let inner = spec(vec![2, 2], vec![4, 4]);
465        assert!(outer.overlaps(&inner));
466        assert!(inner.overlaps(&outer));
467    }
468
469    // ---- TensorSliceManager — create_slice ----------------------------------
470
471    #[test]
472    fn test_create_slice_success() {
473        let mut mgr = TensorSliceManager::new(shape_3d());
474        let sp = spec(vec![0, 0, 0], vec![2, 4, 8]);
475        let n = sp.element_count(); // 2*4*8 = 64
476        let result = mgr.create_slice("a".to_string(), sp, make_data(n, 1.0));
477        assert!(result.is_ok());
478        assert!(mgr.read_slice("a").is_some());
479    }
480
481    #[test]
482    fn test_create_slice_duplicate_name_error() {
483        let mut mgr = TensorSliceManager::new(shape_3d());
484        let sp = spec(vec![0, 0, 0], vec![1, 1, 1]);
485        mgr.create_slice("s".to_string(), sp.clone(), make_data(1, 0.0))
486            .expect("test: should succeed");
487        let err = mgr.create_slice("s".to_string(), sp, make_data(1, 0.0));
488        assert!(err.is_err());
489        let msg = err.unwrap_err();
490        assert!(msg.contains("already exists"), "unexpected message: {msg}");
491    }
492
493    #[test]
494    fn test_create_slice_bounds_check_error() {
495        let mut mgr = TensorSliceManager::new(shape_3d());
496        // end[2] = 17 > shape.dims[2] = 16  → out of bounds
497        let sp = spec(vec![0, 0, 0], vec![1, 1, 17]);
498        let err = mgr.create_slice("bad".to_string(), sp, make_data(17, 0.0));
499        assert!(err.is_err());
500    }
501
502    #[test]
503    fn test_create_slice_wrong_data_length_error() {
504        let mut mgr = TensorSliceManager::new(shape_3d());
505        let sp = spec(vec![0, 0, 0], vec![2, 2, 2]); // element_count = 8
506        let err = mgr.create_slice("bad".to_string(), sp, make_data(5, 0.0)); // 5 ≠ 8
507        assert!(err.is_err());
508    }
509
510    // ---- TensorSliceManager — write_slice -----------------------------------
511
512    #[test]
513    fn test_write_slice_increments_version() {
514        let mut mgr = TensorSliceManager::new(shape_3d());
515        let sp = spec(vec![0, 0, 0], vec![1, 1, 4]);
516        mgr.create_slice("v".to_string(), sp, make_data(4, 0.0))
517            .expect("test: should succeed");
518
519        assert_eq!(
520            mgr.read_slice("v").expect("test: should succeed").version,
521            0
522        );
523
524        mgr.write_slice("v", make_data(4, 1.0));
525        assert_eq!(
526            mgr.read_slice("v").expect("test: should succeed").version,
527            1
528        );
529
530        mgr.write_slice("v", make_data(4, 2.0));
531        assert_eq!(
532            mgr.read_slice("v").expect("test: should succeed").version,
533            2
534        );
535    }
536
537    #[test]
538    fn test_write_slice_sets_dirty_flag() {
539        let mut mgr = TensorSliceManager::new(shape_3d());
540        let sp = spec(vec![0, 0, 0], vec![1, 1, 4]);
541        mgr.create_slice("d".to_string(), sp, make_data(4, 0.0))
542            .expect("test: should succeed");
543
544        assert!(!mgr.read_slice("d").expect("test: should succeed").dirty);
545        mgr.write_slice("d", make_data(4, 9.0));
546        assert!(mgr.read_slice("d").expect("test: should succeed").dirty);
547    }
548
549    #[test]
550    fn test_write_slice_returns_false_missing() {
551        let mut mgr = TensorSliceManager::new(shape_3d());
552        assert!(!mgr.write_slice("nonexistent", make_data(4, 0.0)));
553    }
554
555    #[test]
556    fn test_write_slice_returns_false_wrong_length() {
557        let mut mgr = TensorSliceManager::new(shape_3d());
558        let sp = spec(vec![0, 0, 0], vec![1, 1, 4]); // element_count = 4
559        mgr.create_slice("w".to_string(), sp, make_data(4, 0.0))
560            .expect("test: should succeed");
561        // Supply 5 elements instead of 4.
562        assert!(!mgr.write_slice("w", make_data(5, 0.0)));
563        // Version must not have changed.
564        assert_eq!(
565            mgr.read_slice("w").expect("test: should succeed").version,
566            0
567        );
568    }
569
570    // ---- TensorSliceManager — flush_all -------------------------------------
571
572    #[test]
573    fn test_flush_all_clears_dirty() {
574        let mut mgr = TensorSliceManager::new(shape_3d());
575        for name in ["x", "y", "z"] {
576            let sp = spec(vec![0, 0, 0], vec![1, 1, 2]);
577            mgr.create_slice(name.to_string(), sp, make_data(2, 0.0))
578                .expect("test: should succeed");
579            mgr.write_slice(name, make_data(2, 1.0));
580        }
581        // All three should be dirty.
582        assert_eq!(mgr.stats().dirty_slices, 3);
583
584        mgr.flush_all();
585
586        assert_eq!(mgr.stats().dirty_slices, 0);
587        for name in ["x", "y", "z"] {
588            assert!(!mgr.read_slice(name).expect("test: should succeed").dirty);
589        }
590    }
591
592    // ---- TensorSliceManager — overlapping_slices ----------------------------
593
594    #[test]
595    fn test_overlapping_slices_correct() {
596        let shape = TensorShape { dims: vec![10, 10] };
597        let mut mgr = TensorSliceManager::new(shape);
598
599        // Slice A: rows 0-4, cols 0-4  (element_count = 16)
600        let sa = spec(vec![0, 0], vec![4, 4]);
601        mgr.create_slice("A".to_string(), sa, make_data(16, 1.0))
602            .expect("test: should succeed");
603
604        // Slice B: rows 3-7, cols 3-7  (element_count = 16) — overlaps A
605        let sb = spec(vec![3, 3], vec![7, 7]);
606        mgr.create_slice("B".to_string(), sb, make_data(16, 2.0))
607            .expect("test: should succeed");
608
609        // Slice C: rows 6-10, cols 6-10 (element_count = 16) — does NOT overlap A
610        let sc = spec(vec![6, 6], vec![10, 10]);
611        mgr.create_slice("C".to_string(), sc, make_data(16, 3.0))
612            .expect("test: should succeed");
613
614        // Query against A's region.
615        let query = spec(vec![0, 0], vec![4, 4]);
616        let mut hits = mgr.overlapping_slices(&query);
617        hits.sort_unstable();
618        assert_eq!(hits, vec!["A", "B"]);
619    }
620
621    #[test]
622    fn test_non_overlapping_not_returned() {
623        let shape = TensorShape { dims: vec![10, 10] };
624        let mut mgr = TensorSliceManager::new(shape);
625
626        let sa = spec(vec![0, 0], vec![4, 4]);
627        mgr.create_slice("A".to_string(), sa, make_data(16, 0.0))
628            .expect("test: should succeed");
629
630        // Query in a completely separate region.
631        let query = spec(vec![6, 6], vec![10, 10]);
632        let hits = mgr.overlapping_slices(&query);
633        assert!(hits.is_empty());
634    }
635
636    // ---- TensorSliceManager — read_slice / remove_slice ---------------------
637
638    #[test]
639    fn test_read_slice_returns_correct_data() {
640        let mut mgr = TensorSliceManager::new(shape_3d());
641        let sp = spec(vec![0, 0, 0], vec![1, 1, 4]);
642        let data = vec![1.0_f32, 2.0, 3.0, 4.0];
643        mgr.create_slice("r".to_string(), sp, data.clone())
644            .expect("test: should succeed");
645
646        let slice = mgr.read_slice("r").expect("slice must exist");
647        assert_eq!(slice.data, data);
648        assert_eq!(slice.name, "r");
649    }
650
651    #[test]
652    fn test_read_slice_missing_returns_none() {
653        let mgr = TensorSliceManager::new(shape_3d());
654        assert!(mgr.read_slice("nope").is_none());
655    }
656
657    #[test]
658    fn test_remove_slice_existing() {
659        let mut mgr = TensorSliceManager::new(shape_3d());
660        let sp = spec(vec![0, 0, 0], vec![1, 1, 1]);
661        mgr.create_slice("rm".to_string(), sp, make_data(1, 0.0))
662            .expect("test: should succeed");
663
664        assert!(mgr.remove_slice("rm"));
665        assert!(mgr.read_slice("rm").is_none());
666    }
667
668    #[test]
669    fn test_remove_slice_missing_returns_false() {
670        let mut mgr = TensorSliceManager::new(shape_3d());
671        assert!(!mgr.remove_slice("ghost"));
672    }
673
674    // ---- SliceManagerStats --------------------------------------------------
675
676    #[test]
677    fn test_stats_and_memory_bytes() {
678        let shape = TensorShape { dims: vec![2, 2] };
679        let mut mgr = TensorSliceManager::new(shape);
680
681        let sp1 = spec(vec![0, 0], vec![2, 2]); // 4 elements
682        mgr.create_slice("p".to_string(), sp1, make_data(4, 0.0))
683            .expect("test: should succeed");
684
685        let sp2 = spec(vec![0, 0], vec![1, 2]); // 2 elements
686        mgr.create_slice("q".to_string(), sp2, make_data(2, 0.0))
687            .expect("test: should succeed");
688
689        // Dirty one of them.
690        mgr.write_slice("p", make_data(4, 1.0));
691
692        let st = mgr.stats();
693        assert_eq!(st.total_slices, 2);
694        assert_eq!(st.dirty_slices, 1);
695        assert_eq!(st.total_elements, 6); // 4 + 2
696        assert_eq!(st.memory_bytes(), 24); // 6 * 4
697    }
698}