Push zunrplorkwkt #70

Merged
coissac merged 93 commits from push-zunrplorkwkt into main 2026-08-28 23:15:38 +00:00
9 changed files with 180 additions and 145 deletions
Showing only changes of commit ac38aa759b - Show all commits
+62 -6
View File
@@ -300,7 +300,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af737c6c59cb018ecbe6472cbdf86d39c59d78252febfe311953a991b6e4ed85"
dependencies = [
"common_traits",
"common_traits 0.11.4",
"epserde",
"mem_dbg",
]
@@ -475,7 +475,18 @@ checksum = "fda9ae1f26adcae83adb2e92f69cf59421f2a277a942f49f8e59f2fcbd7cf062"
dependencies = [
"anyhow",
"half",
"impl-tools",
"impl-tools 0.10.3",
]
[[package]]
name = "common_traits"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8e0d7d0c5977b1e87f759d406740c38227125f374f54d81bc197e877f2c5f26"
dependencies = [
"anyhow",
"half",
"impl-tools 0.12.0",
]
[[package]]
@@ -783,7 +794,7 @@ checksum = "c40d342ff20a2ce62d9a85ce406e672dfa137f902ac9670034533184f1533976"
dependencies = [
"anyhow",
"bitflags 2.11.1",
"common_traits",
"common_traits 0.11.4",
"epserde-derive",
"maligned",
"mem_dbg",
@@ -1083,11 +1094,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae95c9095c2f1126d7db785955c73cdc5fc33e7c3fa911bd4a42931672029a7"
dependencies = [
"autocfg",
"impl-tools-lib",
"impl-tools-lib 0.11.4",
"proc-macro-error2",
"syn 2.0.117",
]
[[package]]
name = "impl-tools"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06c8567c75b167cdda5ecee024da9cc9581d41bd52460e2e0e768967972aeef1"
dependencies = [
"autocfg",
"impl-tools-lib 0.12.0",
"proc-macro-error3",
"syn 2.0.117",
]
[[package]]
name = "impl-tools-lib"
version = "0.11.4"
@@ -1100,6 +1123,18 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "impl-tools-lib"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c81eb99ba7c6145aeccb8857ddc0c611cd52440091dcfddd605b6a930c9afbf4"
dependencies = [
"proc-macro-error3",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1519,7 +1554,7 @@ dependencies = [
name = "obicompactvec"
version = "0.1.0"
dependencies = [
"common_traits",
"common_traits 0.13.0",
"memmap2",
"ndarray",
"rayon",
@@ -2009,6 +2044,16 @@ dependencies = [
"quote",
]
[[package]]
name = "proc-macro-error-attr3"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
@@ -2020,6 +2065,17 @@ dependencies = [
"quote",
]
[[package]]
name = "proc-macro-error3"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50"
dependencies = [
"proc-macro-error-attr3",
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -2093,7 +2149,7 @@ dependencies = [
"cacheline-ef",
"clap",
"colored",
"common_traits",
"common_traits 0.11.4",
"epserde",
"epserde-derive",
"fastrand",
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
common_traits = "0.11"
common_traits = "0.13"
memmap2 = "0.9"
ndarray = "0.17"
rayon = "1"
@@ -200,7 +200,7 @@ impl PersistentBitMatrix {
/// buffer, ever — the whole point, see
/// `PersistentSparseBitMatrix::nonzero_iter`); `Columnar`/`Packed`
/// reuse the same sorted-slot batching `fill_sub_matrix` always used
/// (`BitSliceView::nonzero_among_sorted`), just eagerly collected here
/// (`BitSliceView::enumerate_nonzero_slots`), just eagerly collected here
/// rather than filled into a dense buffer — same `get` calls, same
/// mmap locality, no per-format duplication of that logic elsewhere.
/// `Implicit` is trivial (one column, always present).
@@ -228,7 +228,7 @@ impl PersistentBitMatrix {
for c in 0..n_cols {
let view = self.col_view(c);
hits.extend(
view.nonzero_among_sorted(&sorted_slots)
view.enumerate_nonzero_slots(&sorted_slots)
.map(|pos| (perm[pos], c, 1u32)),
);
}
+7 -28
View File
@@ -75,36 +75,15 @@ impl PersistentBitVec {
/// The slots are sorted internally before reading so that accesses to
/// the underlying mmap are as sequential as possible, then the results
/// are reordered to match the input order.
pub fn get_batch(&self, slots: &[usize]) -> Vec<bool> {
let mut out = vec![false; slots.len()];
self.fill_batch(slots, &mut out);
out
#[inline]
pub fn collect_slots_values(&self, slots: &[usize]) -> Vec<bool> {
self.view().collect_slots_values(slots)
}
/// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer.
pub fn fill_batch(&self, slots: &[usize], out: &mut [bool]) {
assert_eq!(slots.len(), out.len());
let n = slots.len();
if n == 0 {
return;
}
let mut perm: Vec<usize> = (0..n).collect();
perm.sort_by_key(|&i| slots[i]);
let sorted: Vec<usize> = perm.iter().map(|&i| slots[i]).collect();
let mut tmp = vec![false; n];
self.fill_batch_sorted(&sorted, &mut tmp);
for (i, &orig_idx) in perm.iter().enumerate() {
out[orig_idx] = tmp[i];
}
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) {
assert_eq!(sorted_slots.len(), out.len());
for (i, &slot) in sorted_slots.iter().enumerate() {
out[i] = self.get(slot);
}
/// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer.
#[inline]
pub fn fill_slots_values(&self, slots: &[usize], out: &mut [bool]) {
self.view().fill_slots_values(slots, out)
}
// SAFETY: mmap is page-aligned, HEADER_SIZE=16 divisible by 8 → u64-aligned.
+4 -4
View File
@@ -372,7 +372,7 @@ impl PersistentCompactIntMatrix {
let mut out: Vec<Vec<u32>> = Vec::with_capacity(n_cols);
for c in 0..n_cols {
let mut col_buf = vec![0u32; slots.len()];
self.col_view(c).fill_batch(slots, &mut col_buf);
self.col_view(c).fill_slots_values(slots, &mut col_buf);
out.push(col_buf);
}
out
@@ -397,7 +397,7 @@ impl PersistentCompactIntMatrix {
for (c, col) in out.iter_mut().enumerate() {
col.resize(n, 0);
let mut tmp = vec![0u32; n];
self.col_view(c).fill_batch_sorted(&sorted_slots, &mut tmp);
self.col_view(c).fill_slots_values_sorted(&sorted_slots, &mut tmp);
for (i, &orig_idx) in perm.iter().enumerate() {
col[orig_idx] = tmp[i];
}
@@ -411,7 +411,7 @@ impl PersistentCompactIntMatrix {
/// design even though no sparse count format exists yet). No native
/// low-effort case here the way `PersistentSparseBitMatrix` has one —
/// both variants reuse the same sorted-slot batching `fill_sub_matrix`
/// already used, via `IntSliceView::nonzero_among_sorted`.
/// already used, via `IntSliceView::enumerate_nonzero_slots`.
pub fn nonzero_iter<'a>(&'a self, slots: &'a [usize]) -> impl Iterator<Item = (usize, usize, u32)> + 'a {
let n = slots.len();
let mut perm: Vec<usize> = (0..n).collect();
@@ -422,7 +422,7 @@ impl PersistentCompactIntMatrix {
for c in 0..n_cols {
hits.extend(
self.col_view(c)
.nonzero_among_sorted(&sorted_slots)
.enumerate_nonzero_slots(&sorted_slots)
.map(|(pos, v)| (perm[pos], c, v)),
);
}
+7 -26
View File
@@ -67,34 +67,15 @@ impl PersistentCompactIntVec {
/// The slots are sorted internally before reading so that accesses to
/// the underlying mmap are as sequential as possible, then the results
/// are reordered to match the input order.
pub fn get_batch(&self, slots: &[usize]) -> Vec<u32> {
let mut out = vec![0u32; slots.len()];
self.fill_batch(slots, &mut out);
out
#[inline]
pub fn collect_slots_values(&self, slots: &[usize]) -> Vec<u32> {
self.view().collect_slots_values(slots)
}
/// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer.
pub fn fill_batch(&self, slots: &[usize], out: &mut [u32]) {
assert_eq!(slots.len(), out.len());
let n = slots.len();
if n == 0 { return; }
let mut perm: Vec<usize> = (0..n).collect();
perm.sort_by_key(|&i| slots[i]);
let sorted: Vec<usize> = perm.iter().map(|&i| slots[i]).collect();
let mut tmp = vec![0u32; n];
self.fill_batch_sorted(&sorted, &mut tmp);
for (i, &orig_idx) in perm.iter().enumerate() {
out[orig_idx] = tmp[i];
}
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) {
assert_eq!(sorted_slots.len(), out.len());
for (i, &slot) in sorted_slots.iter().enumerate() {
out[i] = self.get(slot);
}
/// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer.
#[inline]
pub fn fill_slots_values(&self, slots: &[usize], out: &mut [u32]) {
self.view().fill_slots_values(slots, out)
}
fn overflow_get(&self, slot: usize) -> u32 {
+18 -18
View File
@@ -215,65 +215,65 @@ fn hamming_dist_basic() {
assert_eq!(ra.hamming_dist(&rb), 2);
}
// ── get_batch tests ────────────────────────────────────────────────────────────
// ── collect_slots_values tests ────────────────────────────────────────────────────────────
#[test]
fn bitvec_get_batch_in_order() {
fn bitvec_collect_slots_values_in_order() {
let bits = vec![true, false, true, false, true];
let (_dir, r) = make_bv(&bits);
let got = r.get_batch(&[0, 1, 2, 3, 4]);
let got = r.collect_slots_values(&[0, 1, 2, 3, 4]);
assert_eq!(got, bits);
}
#[test]
fn bitvec_get_batch_out_of_order() {
fn bitvec_collect_slots_values_out_of_order() {
let bits = vec![true, false, true, false, true];
let (_dir, r) = make_bv(&bits);
let got = r.get_batch(&[3, 0, 4, 1]);
let got = r.collect_slots_values(&[3, 0, 4, 1]);
assert_eq!(got, vec![false, true, true, false]);
}
#[test]
fn bitvec_get_batch_with_duplicates() {
fn bitvec_collect_slots_values_with_duplicates() {
let bits = vec![true, false, true];
let (_dir, r) = make_bv(&bits);
let got = r.get_batch(&[0, 2, 0, 1]);
let got = r.collect_slots_values(&[0, 2, 0, 1]);
assert_eq!(got, vec![true, true, true, false]);
}
#[test]
fn bitvec_get_batch_empty() {
fn bitvec_collect_slots_values_empty() {
let (_dir, r) = make_bv(&[true, false]);
let got = r.get_batch(&[]);
let got = r.collect_slots_values(&[]);
assert!(got.is_empty());
}
#[test]
fn bitvec_get_batch_out_of_bounds_returns_false() {
fn bitvec_collect_slots_values_out_of_bounds_returns_false() {
// PersistentBitVec::get does NOT bounds-check — out-of-range slots read
// whatever bit happens to be in the mmap (zero-initialised, so false).
// This is a known gap; get_batch inherits it.
// This is a known gap; collect_slots_values inherits it.
let (_dir, r) = make_bv(&[true, false]);
let got = r.get_batch(&[0, 2]);
let got = r.collect_slots_values(&[0, 2]);
assert_eq!(got, vec![true, false]);
}
// BitSliceView get_batch (same logic, exercised through the view)
// BitSliceView collect_slots_values (same logic, exercised through the view)
#[test]
fn bitslice_view_get_batch() {
fn bitslice_view_collect_slots_values() {
let bits = vec![true, false, true, false, true];
let (_dir, r) = make_bv(&bits);
let view = r.view();
assert_eq!(view.get_batch(&[0, 1, 2]), vec![true, false, true]);
assert_eq!(view.get_batch(&[4, 3]), vec![true, false]);
assert_eq!(view.collect_slots_values(&[0, 1, 2]), vec![true, false, true]);
assert_eq!(view.collect_slots_values(&[4, 3]), vec![true, false]);
}
#[test]
fn bitslice_view_get_batch_out_of_bounds_returns_false() {
fn bitslice_view_collect_slots_values_out_of_bounds_returns_false() {
// BitSliceView::get does NOT bounds-check either.
let bits = vec![true, false];
let (_dir, r) = make_bv(&bits);
let view = r.view();
let got = view.get_batch(&[0, 2]);
let got = view.collect_slots_values(&[0, 2]);
assert_eq!(got, vec![true, false]);
}
+16 -16
View File
@@ -400,7 +400,7 @@ fn partial_relfreq_bray_additive_across_split() {
}
}
// ── get_batch tests ────────────────────────────────────────────────────────────
// ── collect_slots_values tests ────────────────────────────────────────────────────────────
fn make_pciv(counts: &[u32]) -> (tempfile::TempDir, PersistentCompactIntVec) {
let dir = tempdir().unwrap();
@@ -413,49 +413,49 @@ fn make_pciv(counts: &[u32]) -> (tempfile::TempDir, PersistentCompactIntVec) {
}
#[test]
fn pciv_get_batch_in_order() {
fn pciv_collect_slots_values_in_order() {
let counts = vec![10u32, 255, 300, 1000];
let (_dir, v) = make_pciv(&counts);
let got = v.get_batch(&[0, 1, 2, 3]);
let got = v.collect_slots_values(&[0, 1, 2, 3]);
assert_eq!(got, counts);
}
#[test]
fn pciv_get_batch_out_of_order() {
fn pciv_collect_slots_values_out_of_order() {
let counts = vec![10u32, 255, 300, 1000];
let (_dir, v) = make_pciv(&counts);
let got = v.get_batch(&[3, 0, 2, 1]);
let got = v.collect_slots_values(&[3, 0, 2, 1]);
assert_eq!(got, vec![1000, 10, 300, 255]);
}
#[test]
fn pciv_get_batch_with_duplicates() {
fn pciv_collect_slots_values_with_duplicates() {
let counts = vec![10u32, 255, 300];
let (_dir, v) = make_pciv(&counts);
let got = v.get_batch(&[0, 2, 0, 1]);
let got = v.collect_slots_values(&[0, 2, 0, 1]);
assert_eq!(got, vec![10, 300, 10, 255]);
}
#[test]
fn pciv_get_batch_empty() {
fn pciv_collect_slots_values_empty() {
let (_dir, v) = make_pciv(&[10u32, 20]);
let got: Vec<u32> = v.get_batch(&[]);
let got: Vec<u32> = v.collect_slots_values(&[]);
assert!(got.is_empty());
}
#[test]
fn pciv_get_batch_out_of_bounds_panics() {
fn pciv_collect_slots_values_out_of_bounds_panics() {
let (_dir, v) = make_pciv(&[10u32, 20]);
let result = std::panic::catch_unwind(|| v.get_batch(&[0, 2]));
assert!(result.is_err(), "get_batch should panic on out-of-bounds slot");
let result = std::panic::catch_unwind(|| v.collect_slots_values(&[0, 2]));
assert!(result.is_err(), "collect_slots_values should panic on out-of-bounds slot");
}
// IntSliceView get_batch (same logic, exercised through the view)
// IntSliceView collect_slots_values (same logic, exercised through the view)
#[test]
fn intslice_view_get_batch() {
fn intslice_view_collect_slots_values() {
let counts = vec![10u32, 255, 300, 1000];
let (_dir, v) = make_pciv(&counts);
let view = v.view();
assert_eq!(view.get_batch(&[0, 1, 2]), vec![10, 255, 300]);
assert_eq!(view.get_batch(&[3, 1]), vec![1000, 255]);
assert_eq!(view.collect_slots_values(&[0, 1, 2]), vec![10, 255, 300]);
assert_eq!(view.collect_slots_values(&[3, 1]), vec![1000, 255]);
}
+63 -44
View File
@@ -30,13 +30,13 @@ impl<'a> BitSliceView<'a> {
///
/// Slots are sorted internally before reading, then results are reordered
/// to match the input order.
pub fn get_batch(&self, slots: &[usize]) -> Vec<bool> {
pub fn collect_slots_values(&self, slots: &[usize]) -> Vec<bool> {
let mut out = vec![false; slots.len()];
self.fill_batch(slots, &mut out);
self.fill_slots_values(slots, &mut out);
out
}
/// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer.
pub fn fill_batch(&self, slots: &[usize], out: &mut [bool]) {
/// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer.
pub fn fill_slots_values(&self, slots: &[usize], out: &mut [bool]) {
assert_eq!(slots.len(), out.len());
let n = slots.len();
if n == 0 { return; }
@@ -44,32 +44,43 @@ impl<'a> BitSliceView<'a> {
perm.sort_by_key(|&i| slots[i]);
let sorted: Vec<usize> = perm.iter().map(|&i| slots[i]).collect();
let mut tmp = vec![false; n];
self.fill_batch_sorted(&sorted, &mut tmp);
self.fill_slots_values_sorted(&sorted, &mut tmp);
for (i, &orig_idx) in perm.iter().enumerate() {
out[orig_idx] = tmp[i];
}
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) {
assert_eq!(sorted_slots.len(), out.len());
for (i, &slot) in sorted_slots.iter().enumerate() {
out[i] = self.get(slot);
}
}
/// Positions in `sorted_slots` (not the slots themselves) whose bit is
/// set — a `filter` over `get`, not a new traversal. `sorted_slots`
/// need not actually be sorted for correctness, but callers batching
/// several columns over the same slot set sort once and reuse it for
/// cache-friendlier `get` access (see `PersistentBitMatrix::nonzero_iter`).
pub(crate) fn nonzero_among_sorted<'b>(self, sorted_slots: &'b [usize]) -> impl Iterator<Item = usize> + 'b
/// `(position in slots, value)` for every slot. Plain `enumerate` +
/// `get`: order never affects correctness here, only the caller's cache
/// locality (see `enumerate_nonzero_slots`), so this takes `slots` as
/// given.
#[inline]
pub fn enumerate_slots_values<'b>(self, slots: &'b [usize]) -> impl Iterator<Item = (usize, bool)> + 'b
where
Self: 'b,
{
sorted_slots.iter().enumerate()
.filter_map(move |(pos, &slot)| self.get(slot).then_some(pos))
slots.iter().enumerate().map(move |(pos, &slot)| (pos, self.get(slot)))
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_slots_values_sorted(&self, sorted_slots: &[usize], out: &mut [bool]) {
assert_eq!(sorted_slots.len(), out.len());
for (pos, v) in self.enumerate_slots_values(sorted_slots) {
out[pos] = v;
}
}
/// Positions in `slots` (not the slots themselves) whose bit is set.
/// Order of `slots` never matters for correctness; callers batching
/// several columns over the same slot set sort it once beforehand
/// purely for cache-friendlier `get` access (see
/// `PersistentBitMatrix::nonzero_iter`).
pub fn enumerate_nonzero_slots<'b>(self, slots: &'b [usize]) -> impl Iterator<Item = usize> + 'b
where
Self: 'b,
{
self.enumerate_slots_values(slots).filter_map(|(pos, v)| v.then_some(pos))
}
pub fn count_ones(&self) -> u64 {
@@ -184,14 +195,14 @@ impl<'a> IntSliceView<'a> {
///
/// Slots are sorted internally before reading, then results are reordered
/// to match the input order.
pub fn get_batch(&self, slots: &[usize]) -> Vec<u32> {
pub fn collect_slots_values(&self, slots: &[usize]) -> Vec<u32> {
let mut out = vec![0u32; slots.len()];
self.fill_batch(slots, &mut out);
self.fill_slots_values(slots, &mut out);
out
}
/// Like [`get_batch`](Self::get_batch), but fills a caller-provided buffer.
pub fn fill_batch(&self, slots: &[usize], out: &mut [u32]) {
/// Like [`collect_slots_values`](Self::collect_slots_values), but fills a caller-provided buffer.
pub fn fill_slots_values(&self, slots: &[usize], out: &mut [u32]) {
assert_eq!(slots.len(), out.len());
let n = slots.len();
if n == 0 { return; }
@@ -199,32 +210,40 @@ impl<'a> IntSliceView<'a> {
perm.sort_by_key(|&i| slots[i]);
let sorted: Vec<usize> = perm.iter().map(|&i| slots[i]).collect();
let mut tmp = vec![0u32; n];
self.fill_batch_sorted(&sorted, &mut tmp);
self.fill_slots_values_sorted(&sorted, &mut tmp);
for (i, &orig_idx) in perm.iter().enumerate() {
out[orig_idx] = tmp[i];
}
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_batch_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) {
assert_eq!(sorted_slots.len(), out.len());
for (i, &slot) in sorted_slots.iter().enumerate() {
out[i] = self.get(slot);
}
}
/// Like [`BitSliceView::nonzero_among_sorted`]: `(position in
/// sorted_slots, value)` for every nonzero value — a `filter_map` over
/// `get`, not a new traversal.
pub(crate) fn nonzero_among_sorted<'b>(self, sorted_slots: &'b [usize]) -> impl Iterator<Item = (usize, u32)> + 'b
/// `(position in slots, value)` for every slot. See
/// [`BitSliceView::enumerate_slots_values`]: order never affects
/// correctness here.
#[inline]
pub fn enumerate_slots_values<'b>(self, slots: &'b [usize]) -> impl Iterator<Item = (usize, u32)> + 'b
where
Self: 'b,
{
sorted_slots.iter().enumerate().filter_map(move |(pos, &slot)| {
let v = self.get(slot);
(v != 0).then_some((pos, v))
})
slots.iter().enumerate().map(move |(pos, &slot)| (pos, self.get(slot)))
}
/// Fill `out` assuming `sorted_slots` is already in ascending order.
/// Results are written in `sorted_slots` order (no reordering).
pub(crate) fn fill_slots_values_sorted(&self, sorted_slots: &[usize], out: &mut [u32]) {
assert_eq!(sorted_slots.len(), out.len());
for (pos, v) in self.enumerate_slots_values(sorted_slots) {
out[pos] = v;
}
}
/// Like [`BitSliceView::enumerate_nonzero_slots`]: `(position in slots,
/// value)` for every nonzero value — a `filter_map` over `get`, not a
/// new traversal.
pub fn enumerate_nonzero_slots<'b>(self, slots: &'b [usize]) -> impl Iterator<Item = (usize, u32)> + 'b
where
Self: 'b,
{
self.enumerate_slots_values(slots).filter_map(|(pos, v)| (v != 0).then_some((pos, v)))
}
/// Sequential merge scan: yields all n values in slot order.