use bitpacking::{BitPacker, BitPacker4x};
use half::f16;
use thiserror::Error;
const BITPACK_GROUP_SIZE: usize = BitPacker4x::BLOCK_LEN;
const DIRECTORY_SENTINEL: u8 = 0xFF;
pub const MAX_BLOCK_ENTRIES: usize = 4096;
const HEADER_SIZE: usize = 16;
const DIRECTORY_ENTRY_SIZE: usize = 8;
fn fill_relative_group(
offsets: &[u32],
min_offset: u32,
last_relative: u32,
out: &mut [u32; BITPACK_GROUP_SIZE],
) {
for (r, val) in out.iter_mut().zip(
offsets
.iter()
.map(|&o| o - min_offset)
.chain(std::iter::repeat(last_relative)),
) {
*r = val;
}
}
pub const DIRECTORY_PREFIX: &str = "~";
#[derive(Debug, Clone, Error)]
pub enum SparsePostingBlockError {
#[error("block must have at least one entry")]
EmptyEntries,
#[error("block has {count} entries, max is {MAX_BLOCK_ENTRIES}")]
TooManyEntries { count: usize },
#[error("directory: max_offsets len ({offsets}) != max_weights len ({weights})")]
MismatchedLengths { offsets: usize, weights: usize },
#[error("expected at least {HEADER_SIZE} header bytes, got {len}")]
TruncatedHeader { len: usize },
#[error("expected {expected} body bytes, got {actual}")]
TruncatedBody { expected: usize, actual: usize },
#[error("invalid bits_per_delta: {value} (expected 0..=32 or 0xFF for directory)")]
InvalidBitsPerDelta { value: u8 },
}
#[derive(Debug, Clone, Copy)]
pub struct PostingBlockHeader {
pub num_entries: u16,
pub bits_per_delta: u8,
pub min_offset: u32,
pub max_offset: u32,
pub max_weight: f32,
}
impl PostingBlockHeader {
pub fn is_directory(&self) -> bool {
self.bits_per_delta == DIRECTORY_SENTINEL
}
}
#[derive(Debug, Clone)]
struct Decompressed {
offsets: Vec<u32>,
values: Vec<f32>,
}
#[derive(Debug, Clone)]
enum PostingBody {
Encoded(Vec<u8>),
Decoded(Decompressed),
}
#[derive(Debug, Clone)]
pub struct SparsePostingBlock {
pub header: PostingBlockHeader,
body: PostingBody,
}
impl SparsePostingBlock {
pub fn from_sorted_entries(entries: &[(u32, f32)]) -> Result<Self, SparsePostingBlockError> {
if entries.is_empty() {
return Err(SparsePostingBlockError::EmptyEntries);
}
if entries.len() > MAX_BLOCK_ENTRIES {
return Err(SparsePostingBlockError::TooManyEntries {
count: entries.len(),
});
}
let n = entries.len();
debug_assert!(
entries.is_sorted_by_key(|e| e.0),
"from_sorted_entries: offsets must be monotonically non-decreasing"
);
let min_offset = entries[0].0;
let max_offset = entries[n - 1].0;
let max_weight = entries
.iter()
.map(|(_, v)| *v)
.fold(0.0f32, f32::max)
.max(f32::MIN_POSITIVE);
let (offsets, values): (Vec<u32>, Vec<f32>) = entries.iter().copied().unzip();
let packer = BitPacker4x::new();
let num_groups = n.div_ceil(BITPACK_GROUP_SIZE);
let last_relative = max_offset - min_offset;
let mut max_bits = 0u8;
let mut rel_group = [0u32; BITPACK_GROUP_SIZE];
for g in 0..num_groups {
let start = g * BITPACK_GROUP_SIZE;
let group_offsets = &offsets[start..n.min(start + BITPACK_GROUP_SIZE)];
fill_relative_group(group_offsets, min_offset, last_relative, &mut rel_group);
let initial = if g == 0 {
0
} else {
offsets[start - 1] - min_offset
};
max_bits = max_bits.max(packer.num_bits_sorted(initial, &rel_group));
}
Ok(SparsePostingBlock {
header: PostingBlockHeader {
min_offset,
max_offset,
max_weight,
num_entries: n as u16,
bits_per_delta: max_bits,
},
body: PostingBody::Decoded(Decompressed { offsets, values }),
})
}
pub fn len(&self) -> usize {
self.header.num_entries as usize
}
pub fn is_empty(&self) -> bool {
self.header.num_entries == 0
}
pub fn decode(&mut self) -> (&[u32], &[f32]) {
if let PostingBody::Encoded(ref raw) = self.body {
if !self.is_directory() {
let decoded = Self::decompress_raw(
raw,
self.header.num_entries as usize,
self.header.bits_per_delta,
self.header.min_offset,
);
self.body = PostingBody::Decoded(decoded);
}
}
match &self.body {
PostingBody::Decoded(d) => (&d.offsets, &d.values),
PostingBody::Encoded(_) => (&[], &[]),
}
}
pub fn offsets(&mut self) -> &[u32] {
self.decode().0
}
pub fn values(&mut self) -> &[f32] {
self.decode().1
}
fn decompress_raw(
raw_body: &[u8],
num_entries: usize,
bits_per_delta: u8,
min_offset: u32,
) -> Decompressed {
let mut offsets = Vec::new();
Self::decompress_offsets_from_body(
raw_body,
num_entries,
bits_per_delta,
min_offset,
&mut offsets,
);
let weight_start = Self::body_weight_offset(num_entries, bits_per_delta);
let weight_bytes = &raw_body[weight_start..weight_start + num_entries * 2];
let values: Vec<f32> = weight_bytes
.chunks_exact(2)
.map(|b| f16::from_le_bytes([b[0], b[1]]).to_f32())
.collect();
Decompressed { offsets, values }
}
fn decompress_offsets_from_body(
raw_body: &[u8],
num_entries: usize,
bits_per_delta: u8,
min_offset: u32,
buf: &mut Vec<u32>,
) {
let packer = BitPacker4x::new();
let num_groups = num_entries.div_ceil(BITPACK_GROUP_SIZE);
let packed_group_bytes = (BITPACK_GROUP_SIZE * (bits_per_delta as usize)).div_ceil(8);
let padded_len = num_groups * BITPACK_GROUP_SIZE;
buf.clear();
buf.resize(padded_len, 0);
let mut byte_offset = 0;
let mut initial = 0u32;
for g in 0..num_groups {
let group_end = byte_offset + packed_group_bytes;
let group = &mut buf[g * BITPACK_GROUP_SIZE..(g + 1) * BITPACK_GROUP_SIZE];
packer.decompress_sorted(
initial,
&raw_body[byte_offset..group_end],
group,
bits_per_delta,
);
initial = group[BITPACK_GROUP_SIZE - 1];
for offset in group.iter_mut() {
*offset += min_offset;
}
byte_offset = group_end;
}
buf.truncate(num_entries);
}
fn body_weight_offset(num_entries: usize, bits_per_delta: u8) -> usize {
let num_groups = num_entries.div_ceil(BITPACK_GROUP_SIZE);
let packed_group_bytes = (BITPACK_GROUP_SIZE * (bits_per_delta as usize)).div_ceil(8);
num_groups * packed_group_bytes
}
pub fn serialize(&self) -> Vec<u8> {
let data = match &self.body {
PostingBody::Encoded(raw) => {
let mut buf = Vec::with_capacity(HEADER_SIZE + raw.len());
self.write_header(&mut buf);
buf.extend_from_slice(raw);
return buf;
}
PostingBody::Decoded(d) => d,
};
let n = data.offsets.len();
let packer = BitPacker4x::new();
let last_relative = self.header.max_offset - self.header.min_offset;
let num_groups = n.div_ceil(BITPACK_GROUP_SIZE);
let packed_group_bytes =
(BITPACK_GROUP_SIZE * (self.header.bits_per_delta as usize)).div_ceil(8);
let mut buf = Vec::with_capacity(self.serialized_size());
self.write_header(&mut buf);
let mut packed = [0u8; BITPACK_GROUP_SIZE * 4];
let mut rel_group = [0u32; BITPACK_GROUP_SIZE];
for g in 0..num_groups {
let start = g * BITPACK_GROUP_SIZE;
let group_offsets = &data.offsets[start..n.min(start + BITPACK_GROUP_SIZE)];
fill_relative_group(
group_offsets,
self.header.min_offset,
last_relative,
&mut rel_group,
);
let initial = if g == 0 {
0
} else {
data.offsets[start - 1] - self.header.min_offset
};
packed[..packed_group_bytes].fill(0);
packer.compress_sorted(
initial,
&rel_group,
&mut packed[..packed_group_bytes],
self.header.bits_per_delta,
);
buf.extend_from_slice(&packed[..packed_group_bytes]);
}
for &v in &data.values {
buf.extend_from_slice(&f16::from_f32(v).to_le_bytes());
}
buf
}
pub fn serialized_size(&self) -> usize {
HEADER_SIZE
+ Self::expected_body_size(self.header.num_entries as usize, self.header.bits_per_delta)
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, SparsePostingBlockError> {
if bytes.len() < HEADER_SIZE {
return Err(SparsePostingBlockError::TruncatedHeader { len: bytes.len() });
}
let num_entries = u16::from_le_bytes([bytes[0], bytes[1]]);
let bits_per_delta = bytes[2];
let min_offset = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
let max_offset = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let max_weight = f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
if bits_per_delta > 32 && bits_per_delta != DIRECTORY_SENTINEL {
return Err(SparsePostingBlockError::InvalidBitsPerDelta {
value: bits_per_delta,
});
}
let expected_body = Self::expected_body_size(num_entries as usize, bits_per_delta);
let actual_body = bytes.len() - HEADER_SIZE;
if actual_body < expected_body {
return Err(SparsePostingBlockError::TruncatedBody {
expected: expected_body,
actual: actual_body,
});
}
Ok(SparsePostingBlock {
header: PostingBlockHeader {
min_offset,
max_offset,
max_weight,
num_entries,
bits_per_delta,
},
body: PostingBody::Encoded(bytes[HEADER_SIZE..HEADER_SIZE + expected_body].to_vec()),
})
}
fn expected_body_size(num_entries: usize, bits_per_delta: u8) -> usize {
if bits_per_delta == DIRECTORY_SENTINEL {
num_entries * DIRECTORY_ENTRY_SIZE
} else {
Self::body_weight_offset(num_entries, bits_per_delta) + num_entries * 2
}
}
fn write_header(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(&self.header.num_entries.to_le_bytes());
buf.push(self.header.bits_per_delta);
buf.push(0); buf.extend_from_slice(&self.header.min_offset.to_le_bytes());
buf.extend_from_slice(&self.header.max_offset.to_le_bytes());
buf.extend_from_slice(&self.header.max_weight.to_le_bytes());
}
pub fn peek_header(bytes: &[u8]) -> Result<PostingBlockHeader, SparsePostingBlockError> {
if bytes.len() < HEADER_SIZE {
return Err(SparsePostingBlockError::TruncatedHeader { len: bytes.len() });
}
Ok(PostingBlockHeader {
num_entries: u16::from_le_bytes([bytes[0], bytes[1]]),
bits_per_delta: bytes[2],
min_offset: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
max_offset: u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
max_weight: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
})
}
pub fn decompress_offsets_into(bytes: &[u8], hdr: &PostingBlockHeader, buf: &mut Vec<u32>) {
debug_assert!(
!hdr.is_directory(),
"decompress_offsets_into called on directory block"
);
Self::decompress_offsets_from_body(
&bytes[HEADER_SIZE..],
hdr.num_entries as usize,
hdr.bits_per_delta,
hdr.min_offset,
buf,
);
}
pub fn raw_weight_bytes<'a>(bytes: &'a [u8], hdr: &PostingBlockHeader) -> &'a [u8] {
debug_assert!(
!hdr.is_directory(),
"raw_weight_bytes called on directory block"
);
let n = hdr.num_entries as usize;
let w_start = Self::weight_byte_offset(hdr);
&bytes[w_start..w_start + n * 2]
}
pub fn read_value_at(bytes: &[u8], hdr: &PostingBlockHeader, index: usize) -> f32 {
debug_assert!(
!hdr.is_directory(),
"read_value_at called on directory block"
);
debug_assert!(index < hdr.num_entries as usize);
let byte_pos = Self::weight_byte_offset(hdr) + index * 2;
f16::from_le_bytes([bytes[byte_pos], bytes[byte_pos + 1]]).to_f32()
}
pub fn decompress_values_into(bytes: &[u8], hdr: &PostingBlockHeader, buf: &mut Vec<f32>) {
debug_assert!(
!hdr.is_directory(),
"decompress_values_into called on directory block"
);
let n = hdr.num_entries as usize;
buf.clear();
buf.resize(n, 0.0);
let w_start = Self::weight_byte_offset(hdr);
let f16_bytes = &bytes[w_start..w_start + n * 2];
convert_f16_to_f32(f16_bytes, buf);
}
fn weight_byte_offset(hdr: &PostingBlockHeader) -> usize {
HEADER_SIZE + Self::body_weight_offset(hdr.num_entries as usize, hdr.bits_per_delta)
}
pub fn is_directory(&self) -> bool {
self.header.bits_per_delta == DIRECTORY_SENTINEL
}
}
#[derive(Debug, Clone)]
pub struct DirectoryBlock(SparsePostingBlock);
impl DirectoryBlock {
pub fn new(max_offsets: &[u32], max_weights: &[f32]) -> Result<Self, SparsePostingBlockError> {
if max_offsets.len() != max_weights.len() {
return Err(SparsePostingBlockError::MismatchedLengths {
offsets: max_offsets.len(),
weights: max_weights.len(),
});
}
if max_offsets.len() > u16::MAX as usize {
return Err(SparsePostingBlockError::TooManyEntries {
count: max_offsets.len(),
});
}
let n = max_offsets.len();
let dim_max = max_weights.iter().copied().fold(0.0f32, f32::max);
let mut raw_body = Vec::with_capacity(n * 8);
for i in 0..n {
raw_body.extend_from_slice(&max_offsets[i].to_le_bytes());
raw_body.extend_from_slice(&max_weights[i].to_le_bytes());
}
Ok(DirectoryBlock(SparsePostingBlock {
header: PostingBlockHeader {
min_offset: max_offsets.first().copied().unwrap_or(0),
max_offset: max_offsets.last().copied().unwrap_or(0),
max_weight: dim_max,
num_entries: n as u16,
bits_per_delta: DIRECTORY_SENTINEL,
},
body: PostingBody::Encoded(raw_body),
}))
}
pub fn from_block(block: SparsePostingBlock) -> Result<Self, SparsePostingBlock> {
if block.is_directory() {
Ok(DirectoryBlock(block))
} else {
Err(block)
}
}
pub fn dim_max_weight(&self) -> f32 {
self.0.header.max_weight
}
pub fn num_blocks(&self) -> usize {
self.0.header.num_entries as usize
}
pub fn entries(&self) -> (Vec<u32>, Vec<f32>) {
let raw = match &self.0.body {
PostingBody::Encoded(raw) => raw.as_slice(),
PostingBody::Decoded(_) => return (Vec::new(), Vec::new()),
};
let n = self.0.header.num_entries as usize;
let mut max_offsets = Vec::with_capacity(n);
let mut max_weights = Vec::with_capacity(n);
for i in 0..n {
let pos = i * 8;
max_offsets.push(u32::from_le_bytes([
raw[pos],
raw[pos + 1],
raw[pos + 2],
raw[pos + 3],
]));
max_weights.push(f32::from_le_bytes([
raw[pos + 4],
raw[pos + 5],
raw[pos + 6],
raw[pos + 7],
]));
}
(max_offsets, max_weights)
}
pub fn into_block(self) -> SparsePostingBlock {
self.0
}
}
#[derive(Debug, Clone)]
pub struct Directory {
max_offsets: Vec<u32>,
max_weights: Vec<f32>,
dim_max_weight: f32,
}
impl Directory {
pub fn new(
max_offsets: Vec<u32>,
max_weights: Vec<f32>,
) -> Result<Self, SparsePostingBlockError> {
if max_offsets.len() != max_weights.len() {
return Err(SparsePostingBlockError::MismatchedLengths {
offsets: max_offsets.len(),
weights: max_weights.len(),
});
}
if max_offsets.is_empty() {
return Err(SparsePostingBlockError::EmptyEntries);
}
let dim_max_weight = max_weights.iter().copied().fold(0.0f32, f32::max);
Ok(Directory {
max_offsets,
max_weights,
dim_max_weight,
})
}
pub fn from_parts(
parts: impl IntoIterator<Item = DirectoryBlock>,
) -> Result<Self, SparsePostingBlockError> {
let mut max_offsets = Vec::new();
let mut max_weights = Vec::new();
for part in parts {
let (o, w) = part.entries();
max_offsets.extend(o);
max_weights.extend(w);
}
Self::new(max_offsets, max_weights)
}
pub fn into_parts(self, max_entries_per_part: usize) -> Vec<DirectoryBlock> {
let cap = max_entries_per_part.max(1).min(u16::MAX as usize);
self.max_offsets
.chunks(cap)
.zip(self.max_weights.chunks(cap))
.map(|(o, w)| DirectoryBlock::new(o, w).expect("chunk from valid directory"))
.collect()
}
pub fn max_offsets(&self) -> &[u32] {
&self.max_offsets
}
pub fn max_weights(&self) -> &[f32] {
&self.max_weights
}
pub fn dim_max_weight(&self) -> f32 {
self.dim_max_weight
}
pub fn num_blocks(&self) -> usize {
self.max_offsets.len()
}
pub fn max_entries_for_block_size(max_block_size_bytes: usize) -> usize {
const ARROW_OVERHEAD_ESTIMATE: usize = 256;
max_block_size_bytes.saturating_sub(HEADER_SIZE + ARROW_OVERHEAD_ESTIMATE)
/ DIRECTORY_ENTRY_SIZE
}
}
pub fn convert_f16_to_f32(f16_bytes: &[u8], out: &mut [f32]) {
#[cfg(target_arch = "aarch64")]
{
convert_f16_to_f32_neon(f16_bytes, out);
return;
}
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx512f") {
unsafe { convert_f16_to_f32_avx512(f16_bytes, out) };
return;
}
if is_x86_feature_detected!("f16c") {
unsafe { convert_f16_to_f32_f16c(f16_bytes, out) };
return;
}
}
#[allow(unreachable_code)]
convert_f16_to_f32_scalar(f16_bytes, out);
}
pub fn convert_f16_to_f32_scalar(f16_bytes: &[u8], out: &mut [f32]) {
for (o, chunk) in out.iter_mut().zip(f16_bytes.chunks_exact(2)) {
*o = f16::from_le_bytes([chunk[0], chunk[1]]).to_f32();
}
}
#[cfg(target_arch = "aarch64")]
fn convert_f16_to_f32_neon(f16_bytes: &[u8], out: &mut [f32]) {
use std::arch::aarch64::*;
let n = out.len();
let chunks = n / 8;
unsafe {
let sign_mask = vdupq_n_u32(0x8000);
let nosign_mask = vdupq_n_u32(0x7FFF);
let bias = vdupq_n_u32(0x3800_0000);
for c in 0..chunks {
let base = c * 8;
let byte_base = base * 2;
let h8 = vld1q_u16(f16_bytes.as_ptr().add(byte_base) as *const u16);
let lo = vmovl_u16(vget_low_u16(h8));
let hi = vmovl_u16(vget_high_u16(h8));
macro_rules! cvt {
($h:expr, $off:expr) => {{
let sign = vshlq_n_u32::<16>(vandq_u32($h, sign_mask));
let nosign = vshlq_n_u32::<13>(vandq_u32($h, nosign_mask));
let bits = vorrq_u32(sign, vaddq_u32(nosign, bias));
vst1q_f32(
out.as_mut_ptr().add(base + $off),
vreinterpretq_f32_u32(bits),
);
}};
}
cvt!(lo, 0);
cvt!(hi, 4);
}
}
let rem_start = chunks * 8;
convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f")]
unsafe fn convert_f16_to_f32_avx512(f16_bytes: &[u8], out: &mut [f32]) {
use std::arch::x86_64::*;
let n = out.len();
let chunks = n / 16;
for c in 0..chunks {
let base = c * 16;
let byte_base = base * 2;
let h16 = _mm256_loadu_si256(f16_bytes.as_ptr().add(byte_base) as *const __m256i);
let f16_out = _mm512_cvtph_ps(h16);
_mm512_storeu_ps(out.as_mut_ptr().add(base), f16_out);
}
let rem_start = chunks * 16;
convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "f16c")]
unsafe fn convert_f16_to_f32_f16c(f16_bytes: &[u8], out: &mut [f32]) {
use std::arch::x86_64::*;
let n = out.len();
let chunks = n / 8;
for c in 0..chunks {
let base = c * 8;
let byte_base = base * 2;
let h8 = _mm_loadu_si128(f16_bytes.as_ptr().add(byte_base) as *const __m128i);
let f8 = _mm256_cvtph_ps(h8);
_mm256_storeu_ps(out.as_mut_ptr().add(base), f8);
}
let rem_start = chunks * 8;
convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
}
#[cfg(test)]
mod tests {
use super::*;
const F16_TOL: f32 = 1e-3;
fn make_block(entries: &[(u32, f32)]) -> SparsePostingBlock {
SparsePostingBlock::from_sorted_entries(entries).expect("make_block: invalid entries")
}
fn sequential_entries(start: u32, step: u32, count: usize, weight: f32) -> Vec<(u32, f32)> {
(0..count)
.map(|i| (start + step * i as u32, weight))
.collect()
}
fn assert_approx(actual: f32, expected: f32, tol: f32) {
assert!(
(actual - expected).abs() <= tol,
"expected {expected} +/- {tol}, got {actual}"
);
}
fn assert_roundtrip_offsets(entries: &[(u32, f32)]) {
let mut block = make_block(entries);
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
assert_eq!(restored.offsets(), block.offsets());
}
fn assert_roundtrip_values(entries: &[(u32, f32)]) {
let mut block = make_block(entries);
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
for (i, (&orig, &rest)) in block
.values()
.iter()
.zip(restored.values().iter())
.enumerate()
{
assert!(
(rest - orig).abs() <= F16_TOL,
"entry {i}: expected {orig} +/- {F16_TOL}, got {rest}"
);
}
}
#[test]
fn roundtrip_at_boundary_sizes() {
for count in [1, 3, 127, 128, 129, 255, 256, 512, MAX_BLOCK_ENTRIES] {
let entries = sequential_entries(0, 1, count, 0.5);
assert_roundtrip_offsets(&entries);
assert_roundtrip_values(&entries);
}
}
#[test]
fn padding_does_not_inflate_bits_per_delta() {
let entries = sequential_entries(0, 1, 129, 0.5);
let block = make_block(&entries);
assert_eq!(block.header.bits_per_delta, 1);
let single = make_block(&[(42, 0.5)]);
assert_eq!(single.header.bits_per_delta, 0);
}
#[test]
fn roundtrip_large_deltas() {
let entries = vec![(0, 0.5), (1_000_000, 0.8), (2_000_000, 0.3)];
assert_roundtrip_offsets(&entries);
assert_roundtrip_values(&entries);
}
#[test]
fn roundtrip_tiny_weights() {
let entries = vec![(0, 0.001), (1, 1.0)];
let mut block = make_block(&entries);
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
assert_eq!(restored.offsets(), block.offsets());
assert_approx(restored.values()[1], 1.0, F16_TOL);
assert!(restored.values()[0] < 0.01);
}
#[test]
fn header_fields() {
let entries = vec![(10, 0.5), (20, 0.9), (30, 0.2)];
let block = make_block(&entries);
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
assert_eq!(restored.header.min_offset, 10);
assert_eq!(restored.header.max_offset, 30);
assert_eq!(restored.header.max_weight, 0.9);
assert_eq!(restored.offsets().len(), 3);
}
#[test]
fn peek_header_matches() {
let entries = sequential_entries(100, 5, 200, 0.42);
let block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
assert_eq!(hdr.num_entries, 200);
assert_eq!(hdr.min_offset, 100);
assert_eq!(hdr.max_offset, 100 + 5 * 199);
}
#[test]
fn raw_weight_bytes_length() {
let entries = sequential_entries(0, 1, 200, 0.5);
let block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let wb = SparsePostingBlock::raw_weight_bytes(&bytes, &hdr);
assert_eq!(wb.len(), 200 * 2);
}
#[test]
fn serialized_size_matches_actual() {
for count in [1, 3, 127, 128, 129, 255, 256, 257, 512, 1024] {
let entries = sequential_entries(0, 1, count, 0.5);
let block = make_block(&entries);
let bytes = block.serialize();
assert_eq!(
block.serialized_size(),
bytes.len(),
"serialized_size mismatch for count={count}"
);
}
}
#[test]
fn directory_block_roundtrip() {
let max_offsets = vec![100, 500, 1000];
let max_weights = vec![0.9, 0.7, 0.5];
let dir = DirectoryBlock::new(&max_offsets, &max_weights).unwrap();
assert_eq!(dir.dim_max_weight(), 0.9);
assert_eq!(dir.num_blocks(), 3);
let block = dir.into_block();
assert!(block.is_directory());
let bytes = block.serialize();
let restored = SparsePostingBlock::deserialize(&bytes).unwrap();
assert!(restored.is_directory());
let dir2 = DirectoryBlock::from_block(restored).unwrap();
let (offsets, weights) = dir2.entries();
assert_eq!(offsets, max_offsets);
assert_eq!(weights, max_weights);
}
#[test]
fn directory_from_block_rejects_posting_block() {
let entries = vec![(0, 1.0), (5, 0.5)];
let block = make_block(&entries);
assert!(!block.is_directory());
let err = DirectoryBlock::from_block(block).unwrap_err();
assert!(!err.is_directory());
}
#[test]
fn deserialize_too_short_returns_err() {
assert!(SparsePostingBlock::deserialize(&[0u8; 15]).is_err());
assert!(SparsePostingBlock::deserialize(&[]).is_err());
}
#[test]
fn deserialize_truncated_body_returns_err() {
let entries = sequential_entries(0, 1, 200, 0.5);
let block = make_block(&entries);
let bytes = block.serialize();
let truncated = &bytes[..bytes.len() - 1];
let err = SparsePostingBlock::deserialize(truncated).unwrap_err();
assert!(
matches!(err, SparsePostingBlockError::TruncatedBody { .. }),
"expected TruncatedBody, got {err:?}"
);
}
#[test]
fn deserialize_truncated_directory_body_returns_err() {
let dir = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
let bytes = dir.into_block().serialize();
let truncated = &bytes[..HEADER_SIZE + 3 * 8 - 1];
let err = SparsePostingBlock::deserialize(truncated).unwrap_err();
assert!(
matches!(err, SparsePostingBlockError::TruncatedBody { .. }),
"expected TruncatedBody, got {err:?}"
);
}
#[test]
fn deserialize_header_only_data_block_returns_err() {
let entries = sequential_entries(0, 1, 200, 0.5);
let block = make_block(&entries);
let bytes = block.serialize();
let err = SparsePostingBlock::deserialize(&bytes[..HEADER_SIZE]).unwrap_err();
assert!(matches!(err, SparsePostingBlockError::TruncatedBody { .. }));
}
#[test]
fn deserialize_extra_trailing_bytes_ignored() {
let entries = sequential_entries(0, 1, 50, 0.5);
let mut block = make_block(&entries);
let mut bytes = block.serialize();
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
assert_eq!(restored.offsets(), block.offsets());
}
#[test]
fn quantization_precision_random() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn cheap_rng(seed: u64, i: usize) -> f32 {
let mut h = DefaultHasher::new();
seed.hash(&mut h);
i.hash(&mut h);
let bits = h.finish();
(bits % 1000) as f32 / 1000.0 * 0.99 + 0.01
}
let entries: Vec<(u32, f32)> = (0..256)
.map(|i| (i as u32 * 7, cheap_rng(12345, i)))
.collect();
let mut block = make_block(&entries);
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
for (i, (&orig, &rest)) in block
.values()
.iter()
.zip(restored.values().iter())
.enumerate()
{
assert!(
(rest - orig).abs() <= F16_TOL,
"entry {i}: expected {orig} +/- {F16_TOL}, got {rest}"
);
}
}
#[test]
fn from_sorted_entries_empty_returns_error() {
let err = SparsePostingBlock::from_sorted_entries(&[]).unwrap_err();
assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
}
#[test]
fn from_sorted_entries_too_many_returns_error() {
let entries: Vec<(u32, f32)> = (0..MAX_BLOCK_ENTRIES + 1)
.map(|i| (i as u32, 0.5))
.collect();
let err = SparsePostingBlock::from_sorted_entries(&entries).unwrap_err();
assert!(
matches!(err, SparsePostingBlockError::TooManyEntries { count } if count == MAX_BLOCK_ENTRIES + 1)
);
}
#[test]
fn from_sorted_entries_at_max_succeeds() {
let entries: Vec<(u32, f32)> = (0..MAX_BLOCK_ENTRIES).map(|i| (i as u32, 0.5)).collect();
let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
assert_eq!(block.len(), MAX_BLOCK_ENTRIES);
}
#[test]
fn directory_new_mismatched_lengths_returns_error() {
let err = DirectoryBlock::new(&[1, 2, 3], &[0.5, 0.5]).unwrap_err();
assert!(matches!(
err,
SparsePostingBlockError::MismatchedLengths {
offsets: 3,
weights: 2,
}
));
}
#[test]
fn directory_block_offsets_values_return_empty() {
let dir = DirectoryBlock::new(&[100], &[0.5]).unwrap();
let mut block = dir.into_block();
assert!(block.is_directory());
assert_eq!(block.offsets(), &[] as &[u32]);
assert_eq!(block.values(), &[] as &[f32]);
}
fn make_dir_data(n: usize) -> (Vec<u32>, Vec<f32>) {
let offsets: Vec<u32> = (0..n).map(|i| (i as u32 + 1) * 100).collect();
let weights: Vec<f32> = (0..n).map(|i| 0.1 + 0.001 * i as f32).collect();
(offsets, weights)
}
#[test]
fn directory_into_parts_single_part() {
let (offsets, weights) = make_dir_data(10);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(100);
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].num_blocks(), 10);
let (o, w) = parts[0].entries();
assert_eq!(o, offsets);
assert_eq!(w, weights);
}
#[test]
fn directory_into_parts_exact_split() {
let (offsets, weights) = make_dir_data(100);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(50);
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].num_blocks(), 50);
assert_eq!(parts[1].num_blocks(), 50);
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_into_parts_uneven_split() {
let (offsets, weights) = make_dir_data(105);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(50);
assert_eq!(parts.len(), 3);
assert_eq!(parts[0].num_blocks(), 50);
assert_eq!(parts[1].num_blocks(), 50);
assert_eq!(parts[2].num_blocks(), 5);
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_into_parts_one_per_part() {
let (offsets, weights) = make_dir_data(5);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(1);
assert_eq!(parts.len(), 5);
for (i, part) in parts.iter().enumerate() {
assert_eq!(part.num_blocks(), 1);
let (o, w) = part.entries();
assert_eq!(o, vec![offsets[i]]);
assert_eq!(w, vec![weights[i]]);
}
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_into_parts_single_entry() {
let dir = Directory::new(vec![42], vec![0.5]).unwrap();
let parts = dir.into_parts(100);
assert_eq!(parts.len(), 1);
let (o, w) = parts[0].entries();
assert_eq!(o, vec![42]);
assert_eq!(w, vec![0.5]);
}
#[test]
fn directory_roundtrip_through_serialize() {
let (offsets, weights) = make_dir_data(250);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(100);
assert_eq!(parts.len(), 3);
let restored_parts: Vec<DirectoryBlock> = parts
.into_iter()
.map(|p| {
let bytes = p.into_block().serialize();
let block = SparsePostingBlock::deserialize(&bytes).unwrap();
assert!(block.is_directory());
DirectoryBlock::from_block(block).unwrap()
})
.collect();
let merged = Directory::from_parts(restored_parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_large_partitioned() {
let n = 10_000;
let (offsets, weights) = make_dir_data(n);
let max_per_part = Directory::max_entries_for_block_size(16384);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(max_per_part);
assert!(parts.len() > 1, "should produce multiple parts at 16KiB");
for part in &parts {
let block = part.clone().into_block();
assert!(
block.serialized_size() <= 16384,
"part serialized size {} exceeds 16KiB",
block.serialized_size()
);
}
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_exceeds_u16_entries() {
let n = 70_000; let (offsets, weights) = make_dir_data(n);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
assert_eq!(dir.num_blocks(), n);
assert_eq!(dir.max_offsets().len(), n);
let parts = dir.into_parts(10_000);
assert_eq!(parts.len(), 7);
let restored: Vec<DirectoryBlock> = parts
.into_iter()
.map(|p| {
let bytes = p.into_block().serialize();
let block = SparsePostingBlock::deserialize(&bytes).unwrap();
assert!(block.is_directory());
DirectoryBlock::from_block(block).unwrap()
})
.collect();
let merged = Directory::from_parts(restored).unwrap();
assert_eq!(merged.num_blocks(), n);
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_into_parts_zero_clamps_to_one() {
let (offsets, weights) = make_dir_data(3);
let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
let parts = dir.into_parts(0);
assert_eq!(parts.len(), 3);
for part in &parts {
assert_eq!(part.num_blocks(), 1);
}
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.max_offsets(), &offsets[..]);
assert_eq!(merged.max_weights(), &weights[..]);
}
#[test]
fn directory_from_parts_preserves_dim_max() {
let parts = vec![
DirectoryBlock::new(&[10, 20], &[0.3, 0.5]).unwrap(),
DirectoryBlock::new(&[30, 40], &[0.9, 0.1]).unwrap(),
DirectoryBlock::new(&[50], &[0.6]).unwrap(),
];
let merged = Directory::from_parts(parts).unwrap();
assert_eq!(merged.dim_max_weight(), 0.9);
assert_eq!(merged.num_blocks(), 5);
}
#[test]
fn directory_max_entries_for_block_size() {
const OVERHEAD: usize = HEADER_SIZE + 256;
assert_eq!(
Directory::max_entries_for_block_size(16384),
(16384 - OVERHEAD) / DIRECTORY_ENTRY_SIZE
);
assert_eq!(
Directory::max_entries_for_block_size(512 * 1024),
(512 * 1024 - OVERHEAD) / DIRECTORY_ENTRY_SIZE
);
assert_eq!(Directory::max_entries_for_block_size(OVERHEAD), 0);
assert_eq!(Directory::max_entries_for_block_size(0), 0);
}
#[test]
fn directory_from_parts_single() {
let part = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
let dir = Directory::from_parts(vec![part]).unwrap();
assert_eq!(dir.max_offsets(), &[10, 20, 30]);
assert_eq!(dir.max_weights(), &[0.5, 0.9, 0.2]);
}
#[test]
fn directory_from_parts_empty_returns_error() {
let err = Directory::from_parts(vec![]).unwrap_err();
assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
}
#[test]
fn directory_new_empty_returns_error() {
let err = Directory::new(vec![], vec![]).unwrap_err();
assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
}
#[test]
fn directory_new_mismatched_returns_error() {
let err = Directory::new(vec![1, 2, 3], vec![0.5]).unwrap_err();
assert!(matches!(
err,
SparsePostingBlockError::MismatchedLengths { .. }
));
}
#[test]
fn directory_prefix_constant() {
assert_eq!(DIRECTORY_PREFIX, "~");
}
#[test]
fn len_and_is_empty() {
let block1 = make_block(&[(0, 1.0)]);
assert_eq!(block1.len(), 1);
assert!(!block1.is_empty());
let block200 = make_block(&sequential_entries(0, 1, 200, 0.5));
assert_eq!(block200.len(), 200);
assert!(!block200.is_empty());
}
#[test]
fn roundtrip_high_offsets() {
let base = u32::MAX - 1000;
let entries: Vec<(u32, f32)> = (0..10).map(|i| (base + i * 100, 0.5)).collect();
assert_roundtrip_offsets(&entries);
assert_roundtrip_values(&entries);
}
#[test]
fn roundtrip_u32_max_single() {
let entries = vec![(u32::MAX, 0.42)];
assert_roundtrip_offsets(&entries);
assert_roundtrip_values(&entries);
}
#[test]
fn roundtrip_varied_deltas() {
let entries = vec![
(0, 0.1),
(1, 0.2),
(100, 0.3),
(101, 0.4),
(10_000, 0.5),
(10_001, 0.6),
(1_000_000, 0.7),
];
assert_roundtrip_offsets(&entries);
assert_roundtrip_values(&entries);
}
#[test]
fn serialize_deserialize_serialize_is_stable() {
for count in [1, 3, 127, 128, 129, 255, 256, 512] {
let entries = sequential_entries(0, 7, count, 0.5);
let block = make_block(&entries);
let bytes1 = block.serialize();
let restored = SparsePostingBlock::deserialize(&bytes1).unwrap();
let bytes2 = restored.serialize();
assert_eq!(
bytes1, bytes2,
"double-serialize mismatch for count={count}"
);
}
}
#[test]
fn raw_weight_bytes_content_correct() {
let entries: Vec<(u32, f32)> = (0..5).map(|i| (i * 10, 0.1 * (i as f32 + 1.0))).collect();
let block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let wb = SparsePostingBlock::raw_weight_bytes(&bytes, &hdr);
assert_eq!(wb.len(), 5 * 2);
for i in 0..5 {
let f = f16::from_le_bytes([wb[i * 2], wb[i * 2 + 1]]).to_f32();
assert_approx(f, entries[i].1, F16_TOL);
}
}
#[test]
fn peek_header_directory_is_directory() {
let dir = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
let bytes = dir.into_block().serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
assert_eq!(hdr.bits_per_delta, DIRECTORY_SENTINEL);
}
#[test]
fn directory_single_entry() {
let dir = DirectoryBlock::new(&[42], &[0.99]).unwrap();
assert_eq!(dir.num_blocks(), 1);
assert_approx(dir.dim_max_weight(), 0.99, 1e-6);
let (offsets, weights) = dir.entries();
assert_eq!(offsets, vec![42]);
assert_eq!(weights, vec![0.99]);
}
#[test]
fn convert_f16_to_f32_empty() {
let mut out = vec![];
convert_f16_to_f32(&[], &mut out);
assert!(out.is_empty());
}
#[test]
fn convert_f16_to_f32_odd_trailing_byte_ignored() {
let val = f16::from_f32(0.5);
let mut input = val.to_le_bytes().to_vec();
input.push(0xAB); let mut out = vec![0.0; 2];
convert_f16_to_f32(&input, &mut out);
assert_approx(out[0], 0.5, F16_TOL);
assert_eq!(out[1], 0.0); }
#[test]
fn convert_f16_simd_matches_scalar() {
for n in [1, 3, 7, 8, 9, 15, 16, 17, 31, 63, 64, 100, 256, 1000] {
let f16_bytes: Vec<u8> = (0..n)
.flat_map(|i| {
let val = 0.01 * (i as f32 + 1.0);
f16::from_f32(val).to_le_bytes()
})
.collect();
let mut scalar_out = vec![0.0f32; n];
let mut simd_out = vec![0.0f32; n];
convert_f16_to_f32_scalar(&f16_bytes, &mut scalar_out);
convert_f16_to_f32(&f16_bytes, &mut simd_out);
for i in 0..n {
assert!(
(scalar_out[i] - simd_out[i]).abs() <= f32::EPSILON,
"mismatch at n={n}, i={i}: scalar={} simd={}",
scalar_out[i],
simd_out[i],
);
}
}
}
#[test]
fn zero_copy_offsets_at_boundary_sizes() {
for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257, 512] {
let entries = sequential_entries(10, 3, count, 0.5);
let mut block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let mut buf = Vec::new();
SparsePostingBlock::decompress_offsets_into(&bytes, &hdr, &mut buf);
assert_eq!(buf.as_slice(), block.offsets(), "count={count}");
}
}
#[test]
fn zero_copy_values_at_boundary_sizes() {
for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257, 512] {
let entries = sequential_entries(0, 1, count, 0.7);
let mut block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let mut buf = Vec::new();
SparsePostingBlock::decompress_values_into(&bytes, &hdr, &mut buf);
for (i, (&a, &b)) in buf.iter().zip(block.values().iter()).enumerate() {
assert!((a - b).abs() <= F16_TOL, "count={count}, i={i}: {a} vs {b}");
}
}
}
#[test]
fn read_value_at_boundary_sizes() {
for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257] {
let entries: Vec<(u32, f32)> = (0..count)
.map(|i| (i as u32 * 5, 0.1 + 0.001 * i as f32))
.collect();
let mut block = make_block(&entries);
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
for i in 0..count {
let v = SparsePostingBlock::read_value_at(&bytes, &hdr, i);
assert_approx(v, block.values()[i], F16_TOL);
}
}
}
}
#[cfg(all(test, feature = "testing"))]
mod proptests {
use super::*;
use proptest::prelude::*;
fn arb_weight() -> impl Strategy<Value = f32> {
(10u16..1000).prop_map(|weight| f32::from(weight) / 1000.0)
}
fn arb_entries(max_count: usize) -> impl Strategy<Value = Vec<(u32, f32)>> {
(1..=max_count)
.prop_flat_map(|n| {
(
proptest::collection::vec(0u32..u32::MAX / 2, n),
proptest::collection::vec(arb_weight(), n),
)
})
.prop_map(|(mut offsets, weights)| {
offsets.sort();
offsets.dedup();
let n = offsets.len().min(weights.len());
offsets.into_iter().zip(weights).take(n).collect::<Vec<_>>()
})
.prop_filter("need at least one entry", |v| !v.is_empty())
}
proptest! {
#[test]
fn serialize_deserialize_serialize_byte_identical(entries in arb_entries(512)) {
let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let bytes1 = block.serialize();
let restored = SparsePostingBlock::deserialize(&bytes1).unwrap();
let bytes2 = restored.serialize();
prop_assert_eq!(&bytes1, &bytes2);
}
#[test]
fn roundtrip_offsets_always_match(entries in arb_entries(512)) {
let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
prop_assert_eq!(restored.offsets(), block.offsets());
}
#[test]
fn roundtrip_values_within_f16_tolerance(entries in arb_entries(512)) {
let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let bytes = block.serialize();
let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
for (i, (&orig, &rest)) in block
.values()
.iter()
.zip(restored.values().iter())
.enumerate()
{
let diff = (orig - rest).abs();
prop_assert!(
diff <= 1e-3,
"entry {}: expected {} ± 1e-3, got {} (diff={})",
i, orig, rest, diff
);
}
}
#[test]
fn zero_copy_matches_lazy_offsets(entries in arb_entries(512)) {
let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let mut buf = Vec::new();
SparsePostingBlock::decompress_offsets_into(&bytes, &hdr, &mut buf);
prop_assert_eq!(buf.as_slice(), block.offsets());
}
#[test]
fn zero_copy_matches_lazy_values(entries in arb_entries(512)) {
let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let bytes = block.serialize();
let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
let mut buf = Vec::new();
SparsePostingBlock::decompress_values_into(&bytes, &hdr, &mut buf);
for (i, (&a, &b)) in buf.iter().zip(block.values().iter()).enumerate() {
let diff = (a - b).abs();
prop_assert!(
diff <= 1e-3,
"entry {}: zero-copy {} vs lazy {} (diff={})",
i, a, b, diff
);
}
}
#[test]
fn serialized_size_always_matches(entries in arb_entries(512)) {
let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let actual = block.serialize().len();
prop_assert_eq!(block.serialized_size(), actual);
}
#[test]
fn serialized_size_survives_roundtrip(entries in arb_entries(512)) {
let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
let size_before = block.serialized_size();
let bytes = block.serialize();
let restored = SparsePostingBlock::deserialize(&bytes).unwrap();
let size_after = restored.serialized_size();
prop_assert_eq!(size_before, bytes.len());
prop_assert_eq!(size_after, bytes.len());
prop_assert_eq!(size_before, size_after);
}
}
}