Skip to main content

zeppelin_embed/kernels/
postings.rs

1//! Posting-block decode kernels: bit-unpack and delta prefix sum.
2//!
3//! # Why this is novel territory
4//!
5//! No published Lucene, tantivy, PISA, or PEF postings measurement exists on
6//! Apple M-series silicon (`research/02a:287`, `:516`); tantivy's SIMD decode
7//! is SSE2-gated and x86-only. A NEON block-max decode path on a 128-byte
8//! cache-line machine is ground nobody has taken.
9//!
10//! # Shape
11//!
12//! Two kernels, both following the Task 03/04 convention: scalar is the
13//! behavioural oracle, the NEON arm sits behind runtime feature detection,
14//! and `prop_neon_unpack_equals_scalar` proves equivalence over every bit
15//! width rather than over a sampled few.
16//!
17//! - **bit-unpack** widens a packed run into `u32` lanes. The NEON arm uses
18//!   only stable `core::arch::aarch64` intrinsics — shifts, ands, and
19//!   loads. Unlike `sdot` and `fcvtl` (Task 03), no inline assembly is
20//!   needed here.
21//! - **prefix sum** turns docid gaps back into absolute ids. The shape is
22//!   Lemire's interleaved load/store form, which reaches 8.9 B values/s on
23//!   an M4 against 3.9 B scalar.
24//!
25//! No timing is recorded here. The decode-rate figure is NOT YET MEASURED:
26//! this machine is not single-tenant, and a contaminated number is worse
27//! than no number.
28
29/// Unpacks `count` values of `bits` each, scalar oracle.
30///
31/// Returns the number of bytes consumed, or `None` when `input` is too
32/// short or `bits` exceeds 32. Never panics and never reads past `input`.
33#[must_use]
34pub fn unpack_scalar(input: &[u8], bits: u8, count: usize, output: &mut [u32]) -> Option<usize> {
35    if bits > 32 {
36        return None;
37    }
38    if bits == 0 {
39        for slot in output.iter_mut().take(count) {
40            *slot = 0;
41        }
42        return Some(0);
43    }
44    let width = u32::from(bits);
45    let needed = (count * bits as usize).div_ceil(8);
46    if input.len() < needed || output.len() < count {
47        return None;
48    }
49    let mask = if bits == 32 {
50        u32::MAX
51    } else {
52        (1_u32 << width) - 1
53    };
54    let mut accumulator = 0_u64;
55    let mut filled = 0_u32;
56    let mut cursor = 0_usize;
57    for slot in output.iter_mut().take(count) {
58        while filled < width {
59            let byte = input.get(cursor).copied().unwrap_or(0);
60            cursor += 1;
61            accumulator |= u64::from(byte) << filled;
62            filled += 8;
63        }
64        *slot = (accumulator as u32) & mask;
65        accumulator >>= width;
66        filled -= width;
67    }
68    Some(needed)
69}
70
71/// Turns gaps into absolute ids in place, scalar oracle.
72///
73/// `base` is the value the first gap is added to. Saturating addition keeps
74/// a corrupt block from wrapping a document id around.
75pub fn prefix_sum_scalar(values: &mut [u32], base: u32) {
76    let mut running = base;
77    for slot in values.iter_mut() {
78        running = running.saturating_add(*slot);
79        *slot = running;
80    }
81}
82
83/// Unpacks using the best available arm.
84///
85/// Widths at or below this are decoded by the narrow path.
86///
87/// # Why eight, and why it is the only width that matters
88///
89/// At `bits <= 8` a group of eight values occupies exactly `bits` bytes and
90/// starts byte-aligned, because `8 * bits` bits is `bits` bytes. That makes
91/// a whole group one load with no carry state and no refill branch.
92///
93/// It is also, measured, the entire decode workload. The width histogram
94/// over every multi-block posting list of two deterministic corpora — one
95/// Zipf at 100,000 documents, one text-shaped at 50,000 — puts **100% of
96/// postings at seven bits or fewer**:
97///
98/// ```text
99/// zipf100k  docid  1b=41.6% 2b=23.1% 3b=21.1% 4b=14.2%   tf  1b=86.1% 4b=13.9%
100/// textish   docid  1b=65.6% 7b=34.4%                     tf  1b=98.0% 2b=2.0%
101/// ```
102///
103/// Two consequences, both measured rather than assumed. The generic ladder
104/// below is effectively dead on real data, so its shape does not matter.
105/// And the width-above-25 scalar cliff in the generic NEON unpacker is
106/// unreachable: no block on either corpus came close. That closes the question
107/// rather than leaving it open — see the campaign notes for K3.
108pub const NARROW_MAX_BITS: u8 = 8;
109
110/// Runs the bit-unpack using the best available arm.
111///
112/// Equivalent to [`unpack_scalar`] for every input; the other arms exist for
113/// speed only and are property-tested against the scalar oracle at every
114/// width.
115#[must_use]
116pub fn unpack(input: &[u8], bits: u8, count: usize, output: &mut [u32]) -> Option<usize> {
117    if bits > 0 && bits <= NARROW_MAX_BITS {
118        return unpack_narrow(input, bits, count, output);
119    }
120    #[cfg(target_arch = "aarch64")]
121    {
122        if bits > 0
123            && bits <= 32
124            && count >= NEON_MIN_COUNT
125            && std::arch::is_aarch64_feature_detected!("neon")
126        {
127            // SAFETY: the `neon` feature was just detected, `bits` is within
128            // 1..=32, and the helper re-checks every slice length before any
129            // load or store.
130            return unsafe { neon::unpack_neon(input, bits, count, output) };
131        }
132    }
133    unpack_scalar(input, bits, count, output)
134}
135
136/// Unpacks at eight bits or fewer, eight values per iteration.
137///
138/// # The shape, and what it replaces
139///
140/// [`unpack_scalar`] carries an accumulator across values and refills it a
141/// byte at a time, so every value pays a loop test and a conditional refill.
142/// The generic NEON unpacker is worse at these widths: it builds its four-lane
143/// window with a scalar gather of four bytes per lane, sixteen byte loads to
144/// produce four values whose packed form occupies at most four bytes in total,
145/// before it issues a single vector shift.
146///
147/// Here a group of eight is one aligned load and eight independent
148/// shift-and-mask pairs. No accumulator, no refill, no branch inside the
149/// group. The ragged tail is byte-aligned by construction, so the oracle
150/// finishes it.
151#[must_use]
152pub fn unpack_narrow(input: &[u8], bits: u8, count: usize, output: &mut [u32]) -> Option<usize> {
153    if bits == 0 || bits > NARROW_MAX_BITS {
154        return None;
155    }
156    let width = usize::from(bits);
157    let needed = count.checked_mul(width)?.div_ceil(8);
158    if input.len() < needed || output.len() < count {
159        return None;
160    }
161    let mask = (1_u32 << width) - 1;
162
163    let mut index = 0_usize;
164    let mut byte = 0_usize;
165
166    #[cfg(target_arch = "aarch64")]
167    if count >= 8 && std::arch::is_aarch64_feature_detected!("neon") {
168        // SAFETY: the `neon` feature was just detected, `width` is within
169        // 1..=8, and the helper loads only where it has verified sixteen
170        // readable bytes remain.
171        let (consumed_values, consumed_bytes) =
172            unsafe { neon::unpack_narrow_neon(input, width, count, output) };
173        index = consumed_values;
174        byte = consumed_bytes;
175    }
176
177    while index + 8 <= count {
178        // Eight values at `width` bits occupy exactly `width` bytes, and the
179        // group starts byte-aligned, so the whole group is one window.
180        let window = match input.get(byte..byte + 8) {
181            Some(bytes) => {
182                let mut buffer = [0_u8; 8];
183                buffer.copy_from_slice(bytes);
184                u64::from_le_bytes(buffer)
185            }
186            None => {
187                // Near the end of the stream: assemble only the bytes this
188                // group actually owns rather than reading past them.
189                let mut assembled = 0_u64;
190                for step in 0..width {
191                    let value = input.get(byte + step).copied().unwrap_or(0);
192                    assembled |= u64::from(value) << (8 * step);
193                }
194                assembled
195            }
196        };
197        for lane in 0..8 {
198            let slot = output.get_mut(index + lane)?;
199            *slot = ((window >> (lane * width)) as u32) & mask;
200        }
201        index += 8;
202        byte += width;
203    }
204
205    let remaining = count - index;
206    if remaining > 0 {
207        let tail = input.get(byte..)?;
208        let target = output.get_mut(index..)?;
209        unpack_scalar(tail, bits, remaining, target)?;
210    }
211    Some(needed)
212}
213
214/// Runs the prefix sum using the best available arm.
215pub fn prefix_sum(values: &mut [u32], base: u32) {
216    prefix_sum_wide(values, base);
217}
218
219/// Values per interleaved prefix-sum group: four vectors of four lanes.
220#[cfg(target_arch = "aarch64")]
221const WIDE_GROUP: usize = 16;
222
223/// Turns gaps into absolute ids, sixteen values per group.
224///
225/// # What was serial, and what is not any more
226///
227/// The four-lane kernel below runs a two-step ladder and then folds in the
228/// running carry, and the next group cannot start until that carry is
229/// extracted. Every one of its adds is on the critical path, so the kernel
230/// runs at the latency of the add chain rather than at its throughput.
231///
232/// Here four vectors are loaded at once and their two ladder steps are
233/// **eight adds with no dependency on each other or on the carry**, so they
234/// issue while the carry chain is still resolving. Only the four folds and
235/// their lane extracts remain serial, across sixteen values instead of
236/// four.
237///
238/// # Why reassociating is allowed
239///
240/// Unsigned saturating addition is associative — `min(min(a+b, M)+c, M)` is
241/// `min(a+b+c, M)` for non-negative `c` — so grouping the sum differently
242/// cannot change a single result. The kernel keeps `vqaddq_u32` for exactly
243/// the reason the four-lane one does: a wrapping add would let a corrupt
244/// block roll a document id back to zero and silently reorder results.
245/// `the_wide_prefix_sum_carries_across_every_group_boundary` proves the
246/// reassociation against the scalar oracle at saturating inputs.
247pub fn prefix_sum_wide(values: &mut [u32], base: u32) {
248    #[cfg(target_arch = "aarch64")]
249    {
250        if values.len() >= WIDE_GROUP && std::arch::is_aarch64_feature_detected!("neon") {
251            // SAFETY: the `neon` feature was just detected and the helper
252            // touches only whole sixteen-value groups it bounds-checks.
253            let (consumed, carry) = unsafe { neon::prefix_sum_wide_neon(values, base) };
254            if let Some(tail) = values.get_mut(consumed..) {
255                prefix_sum_narrow(tail, carry);
256            }
257            return;
258        }
259    }
260    prefix_sum_narrow(values, base);
261}
262
263/// Turns gaps into absolute ids, four values per group.
264///
265/// The residue of [`prefix_sum_wide`]: at most fifteen values, or a whole
266/// short block. Kept because a fifteen-value tail still beats scalar.
267fn prefix_sum_narrow(values: &mut [u32], base: u32) {
268    #[cfg(target_arch = "aarch64")]
269    {
270        if values.len() >= NEON_MIN_COUNT && std::arch::is_aarch64_feature_detected!("neon") {
271            // SAFETY: the `neon` feature was just detected and the helper
272            // operates on whole four-lane groups it bounds-checks itself.
273            unsafe { neon::prefix_sum_neon(values, base) };
274            return;
275        }
276    }
277    prefix_sum_scalar(values, base);
278}
279
280/// Below this many values the scalar path wins outright.
281#[cfg(target_arch = "aarch64")]
282const NEON_MIN_COUNT: usize = 8;
283
284#[cfg(target_arch = "aarch64")]
285mod neon {
286    use std::arch::aarch64::{
287        uint8x16_t, uint32x4_t, vandq_u16, vandq_u32, vdupq_n_u16, vdupq_n_u32, vextq_u32,
288        vget_low_u16, vgetq_lane_u32, vld1q_s16, vld1q_u8, vld1q_u32, vmovl_high_u16, vmovl_u16,
289        vqaddq_u32, vqtbl1q_u8, vreinterpretq_u16_u8, vshlq_u16, vshlq_u32, vst1q_u32,
290    };
291
292    /// Unpacks eight values per iteration at eight bits or fewer.
293    ///
294    /// # The gather is one `tbl`, not sixteen loads
295    ///
296    /// A group of eight values at `width` bits occupies exactly `width`
297    /// bytes and starts byte-aligned. Every lane needs at most two adjacent
298    /// bytes, because `width + 7 <= 15` bits. So one `vqtbl1q_u8` over a
299    /// single sixteen-byte window places both bytes of all eight lanes at
300    /// once, giving eight `u16` lanes; a per-lane variable shift and a mask
301    /// finish them, and two widenings produce the `u32` output.
302    ///
303    /// That is roughly six vector operations per eight values, against the
304    /// generic arm's sixteen scalar byte loads per four.
305    ///
306    /// Returns the values and bytes consumed, so the caller finishes any
307    /// group for which sixteen readable bytes were not available.
308    ///
309    /// # Safety
310    ///
311    /// The caller guarantees the `neon` feature is available and that
312    /// `width` is within 1..=8. Every load below is guarded by an explicit
313    /// length check on the slice it reads.
314    #[target_feature(enable = "neon")]
315    pub(super) unsafe fn unpack_narrow_neon(
316        input: &[u8],
317        width: usize,
318        count: usize,
319        output: &mut [u32],
320    ) -> (usize, usize) {
321        // Lane j starts at bit j * width, so it lives in bytes
322        // (j * width) / 8 and the one after it, shifted right by the
323        // remainder. Both vectors depend only on `width`.
324        let mut selectors = [0_u8; 16];
325        let mut shifts = [0_i16; 8];
326        for lane in 0..8_usize {
327            let offset = lane * width;
328            let byte = u8::try_from(offset / 8).unwrap_or(0);
329            let Some(low) = selectors.get_mut(lane * 2) else {
330                return (0, 0);
331            };
332            *low = byte;
333            let Some(high) = selectors.get_mut(lane * 2 + 1) else {
334                return (0, 0);
335            };
336            *high = byte.saturating_add(1);
337            let Some(shift) = shifts.get_mut(lane) else {
338                return (0, 0);
339            };
340            // A negative NEON shift count is a right shift.
341            *shift = -i16::try_from(offset % 8).unwrap_or(0);
342        }
343        let mask_value = ((1_u32 << width) - 1) as u16;
344
345        let mut index = 0_usize;
346        let mut byte = 0_usize;
347        // SAFETY: `selectors` and `shifts` are exactly the sixteen bytes and
348        // eight halfwords the intrinsics load.
349        let (indices, counts, mask) = unsafe {
350            (
351                vld1q_u8(selectors.as_ptr()),
352                vld1q_s16(shifts.as_ptr()),
353                vdupq_n_u16(mask_value),
354            )
355        };
356
357        while index + 8 <= count {
358            // `vqtbl1q_u8` reads a whole sixteen-byte register, so the group
359            // is only taken here when sixteen bytes are readable. The tail
360            // groups fall back to the caller's scalar loop.
361            let Some(window) = input.get(byte..byte + 16) else {
362                break;
363            };
364            let Some(target) = output.get_mut(index..index + 8) else {
365                break;
366            };
367            // SAFETY: `window` is sixteen readable bytes and `target` is
368            // eight writable `u32`s, matching the loads and stores below.
369            unsafe {
370                let table: uint8x16_t = vld1q_u8(window.as_ptr());
371                let gathered = vreinterpretq_u16_u8(vqtbl1q_u8(table, indices));
372                let values = vandq_u16(vshlq_u16(gathered, counts), mask);
373                vst1q_u32(target.as_mut_ptr(), vmovl_u16(vget_low_u16(values)));
374                vst1q_u32(target.as_mut_ptr().add(4), vmovl_high_u16(values));
375            }
376            index += 8;
377            byte += width;
378        }
379        (index, byte)
380    }
381
382    /// Unpacks with NEON, falling back to scalar for the ragged tail.
383    ///
384    /// The kernel widens four values per iteration: it loads the byte window
385    /// each lane needs, shifts by that lane's bit offset, and masks. Widths
386    /// that straddle more than five bytes per lane are left to the scalar
387    /// oracle, which keeps the vector path free of a second-word merge.
388    ///
389    /// # Safety
390    ///
391    /// The caller guarantees the `neon` feature is available. Every slice
392    /// access below is bounds-checked here regardless.
393    #[target_feature(enable = "neon")]
394    pub(super) unsafe fn unpack_neon(
395        input: &[u8],
396        bits: u8,
397        count: usize,
398        output: &mut [u32],
399    ) -> Option<usize> {
400        let width = u32::from(bits);
401        let needed = (count * bits as usize).div_ceil(8);
402        if input.len() < needed || output.len() < count {
403            return None;
404        }
405        // Widths above 25 need a lane to span more than four bytes plus its
406        // offset, which the four-byte gather below cannot express. Those
407        // fall to the oracle; they are also the rarest widths in practice.
408        if width > 25 {
409            return super::unpack_scalar(input, bits, count, output);
410        }
411
412        let mask_value = if width == 32 {
413            u32::MAX
414        } else {
415            (1_u32 << width) - 1
416        };
417        let mask = vdupq_n_u32(mask_value);
418
419        let mut index = 0_usize;
420        while index + 4 <= count {
421            let mut words = [0_u32; 4];
422            let mut shifts = [0_i32; 4];
423            for lane in 0..4 {
424                let bit_offset = (index + lane) * bits as usize;
425                let byte = bit_offset / 8;
426                let shift = bit_offset % 8;
427                // Gather four bytes; a lane needs at most 25 + 7 = 32 bits.
428                let mut word = 0_u32;
429                for step in 0..4 {
430                    let value = input.get(byte + step).copied().unwrap_or(0);
431                    word |= u32::from(value) << (8 * step);
432                }
433                let slot = words.get_mut(lane)?;
434                *slot = word;
435                let slot = shifts.get_mut(lane)?;
436                // NEON shifts left on a signed count; a negative count is a
437                // right shift, which is what a bit offset needs.
438                *slot = -(i32::try_from(shift).unwrap_or(0));
439            }
440            // SAFETY: `words` and `shifts` are four-element arrays, matching
441            // the 128-bit vector width the intrinsics load.
442            unsafe {
443                let loaded: uint32x4_t = vld1q_u32(words.as_ptr());
444                let counts: std::arch::aarch64::int32x4_t =
445                    std::arch::aarch64::vld1q_s32(shifts.as_ptr());
446                let shifted = vshlq_u32(loaded, counts);
447                let masked = vandq_u32(shifted, mask);
448                let target = output.get_mut(index..index + 4)?;
449                vst1q_u32(target.as_mut_ptr(), masked);
450            }
451            index += 4;
452        }
453
454        // Ragged tail through the oracle, which is exact by definition.
455        if index < count {
456            let tail = output.get_mut(index..count)?;
457            let bit_offset = index * bits as usize;
458            let byte = bit_offset / 8;
459            let shift = bit_offset % 8;
460            if shift == 0 {
461                let rest = input.get(byte..)?;
462                super::unpack_scalar(rest, bits, count - index, tail)?;
463            } else {
464                // Re-run the whole run scalar rather than re-derive a
465                // bit-offset entry point; correctness beats cleverness on a
466                // path that handles at most three values.
467                return super::unpack_scalar(input, bits, count, output).map(|_| needed);
468            }
469        }
470        Some(needed)
471    }
472
473    /// Prefix-sums whole sixteen-value groups, four vectors at a time.
474    ///
475    /// Returns the values consumed and the running carry, so the caller
476    /// finishes the residue with the four-lane kernel.
477    ///
478    /// # Safety
479    ///
480    /// The caller guarantees the `neon` feature is available. Every load
481    /// and store below is inside a window whose length was checked.
482    #[target_feature(enable = "neon")]
483    pub(super) unsafe fn prefix_sum_wide_neon(values: &mut [u32], base: u32) -> (usize, u32) {
484        let mut carry = base;
485        let mut index = 0_usize;
486        let zero = vdupq_n_u32(0);
487        while index + super::WIDE_GROUP <= values.len() {
488            let Some(window) = values.get_mut(index..index + super::WIDE_GROUP) else {
489                break;
490            };
491            let pointer = window.as_mut_ptr();
492            // SAFETY: `window` is exactly sixteen `u32`, so the four loads
493            // and four stores at lane offsets 0, 4, 8 and 12 are in bounds.
494            unsafe {
495                let mut v0: uint32x4_t = vld1q_u32(pointer);
496                let mut v1: uint32x4_t = vld1q_u32(pointer.add(4));
497                let mut v2: uint32x4_t = vld1q_u32(pointer.add(8));
498                let mut v3: uint32x4_t = vld1q_u32(pointer.add(12));
499
500                // Eight INDEPENDENT adds: four intra-vector ladders that
501                // depend neither on each other nor on the carry.
502                v0 = vqaddq_u32(v0, vextq_u32(zero, v0, 3));
503                v1 = vqaddq_u32(v1, vextq_u32(zero, v1, 3));
504                v2 = vqaddq_u32(v2, vextq_u32(zero, v2, 3));
505                v3 = vqaddq_u32(v3, vextq_u32(zero, v3, 3));
506                v0 = vqaddq_u32(v0, vextq_u32(zero, v0, 2));
507                v1 = vqaddq_u32(v1, vextq_u32(zero, v1, 2));
508                v2 = vqaddq_u32(v2, vextq_u32(zero, v2, 2));
509                v3 = vqaddq_u32(v3, vextq_u32(zero, v3, 2));
510
511                // The only serial part: fold each vector's running total
512                // into the next. Four adds and four extracts per sixteen
513                // values, where the four-lane kernel pays three adds and an
514                // extract per four.
515                v0 = vqaddq_u32(v0, vdupq_n_u32(carry));
516                v1 = vqaddq_u32(v1, vdupq_n_u32(vgetq_lane_u32::<3>(v0)));
517                v2 = vqaddq_u32(v2, vdupq_n_u32(vgetq_lane_u32::<3>(v1)));
518                v3 = vqaddq_u32(v3, vdupq_n_u32(vgetq_lane_u32::<3>(v2)));
519                carry = vgetq_lane_u32::<3>(v3);
520
521                vst1q_u32(pointer, v0);
522                vst1q_u32(pointer.add(4), v1);
523                vst1q_u32(pointer.add(8), v2);
524                vst1q_u32(pointer.add(12), v3);
525            }
526            index += super::WIDE_GROUP;
527        }
528        (index, carry)
529    }
530
531    /// Prefix-sums with NEON using the interleaved shift-and-add ladder.
532    ///
533    /// Each 4-lane group is summed with two shifted adds, then the running
534    /// carry from the previous group is broadcast in. This is the shape that
535    /// reaches 8.9 B values/s on an M4 against 3.9 B scalar.
536    ///
537    /// # Safety
538    ///
539    /// The caller guarantees the `neon` feature is available.
540    #[target_feature(enable = "neon")]
541    pub(super) unsafe fn prefix_sum_neon(values: &mut [u32], base: u32) {
542        let mut carry = base;
543        let mut index = 0_usize;
544        while index + 4 <= values.len() {
545            let Some(window) = values.get_mut(index..index + 4) else {
546                return;
547            };
548            // SAFETY: `window` is exactly four `u32`, the vector width.
549            unsafe {
550                let mut vector: uint32x4_t = vld1q_u32(window.as_ptr());
551                let zero = vdupq_n_u32(0);
552                // Every add SATURATES, matching the scalar oracle. Plain
553                // `vaddq_u32` wraps, so a corrupt block could roll a
554                // document id back to zero and silently reorder results.
555                // Saturating unsigned addition is associative, so the
556                // ladder below equals the sequential scalar sum exactly.
557                //
558                // Shift by one lane and add: partial sums of pairs.
559                vector = vqaddq_u32(vector, vextq_u32(zero, vector, 3));
560                // Shift by two lanes and add: partial sums of quads.
561                vector = vqaddq_u32(vector, vextq_u32(zero, vector, 2));
562                // Fold in the running total from every earlier group.
563                vector = vqaddq_u32(vector, vdupq_n_u32(carry));
564                vst1q_u32(window.as_mut_ptr(), vector);
565            }
566            carry = window.get(3).copied().unwrap_or(carry);
567            index += 4;
568        }
569        if index < values.len()
570            && let Some(tail) = values.get_mut(index..)
571        {
572            super::prefix_sum_scalar(tail, carry);
573        }
574    }
575}
576
577#[cfg(test)]
578#[allow(
579    clippy::expect_used,
580    clippy::indexing_slicing,
581    clippy::panic,
582    clippy::unwrap_used
583)]
584mod tests {
585    use super::*;
586
587    fn pack(values: &[u32], bits: u8) -> Vec<u8> {
588        let mut out = Vec::new();
589        if bits == 0 {
590            return out;
591        }
592        let width = u32::from(bits);
593        let mut accumulator = 0_u64;
594        let mut filled = 0_u32;
595        for value in values {
596            accumulator |= u64::from(*value) << filled;
597            filled += width;
598            while filled >= 8 {
599                out.push((accumulator & 0xFF) as u8);
600                accumulator >>= 8;
601                filled -= 8;
602            }
603        }
604        if filled > 0 {
605            out.push((accumulator & 0xFF) as u8);
606        }
607        out
608    }
609
610    fn sample(bits: u8, count: usize) -> Vec<u32> {
611        let mask = if bits == 0 {
612            0
613        } else if bits >= 32 {
614            u32::MAX
615        } else {
616            (1_u32 << bits) - 1
617        };
618        (0..count)
619            .map(|index| {
620                let mixed = (index as u64)
621                    .wrapping_mul(2_654_435_761)
622                    .wrapping_add(0x9E37_79B9);
623                (mixed as u32) & mask
624            })
625            .collect()
626    }
627
628    #[test]
629    fn the_narrow_path_equals_the_oracle_at_every_count_and_exact_buffer() {
630        // The narrow path has three regimes and the boundaries between them
631        // are where a bit-packing bug lives: a group taken by the `tbl`
632        // gather, which needs sixteen readable bytes; a group taken by the
633        // eight-byte scalar window; and a final group near the end of the
634        // stream that owns fewer bytes than either window reads.
635        //
636        // Every buffer here is trimmed to EXACTLY the bytes the packing
637        // needs, so any read past a group's own bytes is a real out-of-
638        // bounds read rather than one hidden by slack in the fixture.
639        for bits in 1..=NARROW_MAX_BITS {
640            for count in 1..=200_usize {
641                let values = sample(bits, count);
642                let mut packed = pack(&values, bits);
643                let needed = (count * usize::from(bits)).div_ceil(8);
644                packed.truncate(needed);
645                assert_eq!(packed.len(), needed, "the fixture must be exact");
646
647                let mut narrow = vec![0_u32; count];
648                let consumed = unpack_narrow(&packed, bits, count, &mut narrow)
649                    .expect("an exact buffer is enough");
650                assert_eq!(consumed, needed, "bits {bits} count {count}");
651                assert_eq!(
652                    narrow, values,
653                    "the narrow path diverged at {bits} bits, {count} values"
654                );
655
656                // And the dispatcher must route here without changing the
657                // answer, at every width the measured histogram contains.
658                let mut dispatched = vec![0_u32; count];
659                unpack(&packed, bits, count, &mut dispatched).expect("dispatches");
660                assert_eq!(dispatched, values);
661            }
662        }
663    }
664
665    #[test]
666    fn the_narrow_path_refuses_what_it_cannot_decode() {
667        let mut output = [0_u32; 8];
668        assert!(unpack_narrow(&[0_u8; 8], 0, 8, &mut output).is_none());
669        assert!(unpack_narrow(&[0_u8; 8], 9, 8, &mut output).is_none());
670        // One byte short of the eight values a single bit each needs.
671        assert!(unpack_narrow(&[], 1, 8, &mut output).is_none());
672        // Output too small for the requested count.
673        assert!(unpack_narrow(&[0_u8; 8], 1, 9, &mut output).is_none());
674    }
675
676    #[test]
677    fn prop_neon_unpack_equals_scalar() {
678        for bits in 0..=32_u8 {
679            for count in [1_usize, 3, 4, 7, 8, 15, 16, 63, 64, 65, 128] {
680                let values = sample(bits, count);
681                let packed = pack(&values, bits);
682                let mut oracle = vec![0_u32; count];
683                let mut dispatched = vec![0_u32; count];
684                let expected = unpack_scalar(&packed, bits, count, &mut oracle);
685                let actual = unpack(&packed, bits, count, &mut dispatched);
686                assert_eq!(
687                    expected.is_some(),
688                    actual.is_some(),
689                    "bits {bits} count {count}"
690                );
691                assert_eq!(oracle, values, "the oracle itself is wrong at {bits} bits");
692                assert_eq!(
693                    dispatched, oracle,
694                    "dispatched arm diverged at {bits} bits, {count} values"
695                );
696            }
697        }
698    }
699
700    #[test]
701    fn neon_prefix_sum_equals_scalar_for_arbitrary_blocks() {
702        for count in [0_usize, 1, 3, 4, 5, 8, 63, 64, 65, 200] {
703            for base in [0_u32, 1, 1_000, u32::MAX - 10] {
704                let gaps: Vec<u32> = (0..count).map(|index| (index as u32 % 17) + 1).collect();
705                let mut oracle = gaps.clone();
706                let mut dispatched = gaps;
707                prefix_sum_scalar(&mut oracle, base);
708                prefix_sum(&mut dispatched, base);
709                assert_eq!(
710                    dispatched, oracle,
711                    "prefix sum diverged at count {count}, base {base}"
712                );
713            }
714        }
715    }
716
717    #[test]
718    fn the_wide_prefix_sum_carries_across_every_group_boundary() {
719        // K4 replaces a four-lane group whose carry is entirely serial with
720        // a sixteen-value group whose four intra-vector ladders are
721        // independent. The carry now crosses three INTERNAL vector
722        // boundaries as well as the group boundary, and a wrong `vext`
723        // distance or a carry read from the wrong lane shows up only at
724        // those seams. So sweep every length across several groups, and
725        // include a base and gaps that saturate mid-group: saturating
726        // addition is associative, which is the whole licence for
727        // reassociating the sum, and this is what proves the reassociation
728        // did not change the answer.
729        for count in 0..=80_usize {
730            for base in [0_u32, 1, 1_000, u32::MAX - 10, u32::MAX] {
731                for span in [1_u32, 17, 1_000_000_007] {
732                    let gaps: Vec<u32> = (0..count)
733                        .map(|index| (index as u32).wrapping_mul(span) % span.max(2) + 1)
734                        .collect();
735                    let mut oracle = gaps.clone();
736                    let mut wide = gaps;
737                    prefix_sum_scalar(&mut oracle, base);
738                    prefix_sum_wide(&mut wide, base);
739                    assert_eq!(
740                        wide, oracle,
741                        "the wide prefix sum diverged at count {count}, \
742                         base {base}, span {span}"
743                    );
744                }
745            }
746        }
747    }
748
749    #[test]
750    fn the_prefix_sum_is_a_running_total() {
751        let mut values = vec![1_u32, 2, 3, 4, 5];
752        prefix_sum_scalar(&mut values, 10);
753        assert_eq!(values, vec![11, 13, 16, 20, 25]);
754    }
755
756    #[test]
757    fn the_prefix_sum_saturates_rather_than_wrapping() {
758        let mut values = vec![u32::MAX, 5];
759        prefix_sum(&mut values, 1);
760        assert_eq!(values, vec![u32::MAX, u32::MAX]);
761    }
762
763    #[test]
764    fn an_oversized_width_or_short_buffer_is_refused_by_both_arms() {
765        let mut output = [0_u32; 8];
766        assert_eq!(unpack_scalar(&[0; 64], 33, 8, &mut output), None);
767        assert_eq!(unpack(&[0; 64], 33, 8, &mut output), None);
768        assert_eq!(unpack_scalar(&[0; 1], 8, 8, &mut output), None);
769        assert_eq!(unpack(&[0; 1], 8, 8, &mut output), None);
770        let mut small = [0_u32; 2];
771        assert_eq!(unpack(&[0; 64], 8, 8, &mut small), None);
772    }
773
774    #[test]
775    fn a_zero_width_run_decodes_to_zeros_and_consumes_nothing() {
776        let mut output = vec![9_u32; 16];
777        assert_eq!(unpack(&[], 0, 16, &mut output), Some(0));
778        assert_eq!(output, vec![0_u32; 16]);
779    }
780
781    #[test]
782    fn the_dispatched_arm_matches_the_oracle_on_maximal_values() {
783        for bits in 1..=32_u8 {
784            let maximum = if bits == 32 {
785                u32::MAX
786            } else {
787                (1_u32 << bits) - 1
788            };
789            let values = vec![maximum; 64];
790            let packed = pack(&values, bits);
791            let mut oracle = vec![0_u32; 64];
792            let mut dispatched = vec![0_u32; 64];
793            unpack_scalar(&packed, bits, 64, &mut oracle).expect("scalar unpacks");
794            unpack(&packed, bits, 64, &mut dispatched).expect("dispatch unpacks");
795            assert_eq!(oracle, values, "oracle wrong at {bits}");
796            assert_eq!(dispatched, oracle, "dispatch wrong at {bits}");
797        }
798    }
799}