use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError};
const FIRST_BUCKET_BITS: u32 = 6;
const BUCKETS: usize = 27;
pub(crate) const MAX_LEN: usize = u32::MAX as usize;
pub(crate) struct AppendVec<T> {
buckets: [OnceLock<Box<[OnceLock<T>]>>; BUCKETS],
len: AtomicUsize,
push_lock: Mutex<()>,
}
impl<T> Default for AppendVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> std::fmt::Debug for AppendVec<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppendVec")
.field("len", &self.len())
.finish_non_exhaustive()
}
}
fn locate(index: usize) -> Option<(usize, usize, usize)> {
if index >= MAX_LEN {
return None;
}
let first = 1u64 << FIRST_BUCKET_BITS;
let biased = u64::try_from(index).ok()?.checked_add(first)?;
let bit = 63u32.checked_sub(biased.leading_zeros())?;
let bucket = bit.checked_sub(FIRST_BUCKET_BITS)?;
let bucket_start = 1u64.checked_shl(bit)?;
let offset = usize::try_from(biased.checked_sub(bucket_start)?).ok()?;
let size = usize::try_from(bucket_start).ok()?;
Some((usize::try_from(bucket).ok()?, offset, size))
}
impl<T> AppendVec<T> {
pub(crate) const fn new() -> Self {
Self {
buckets: [const { OnceLock::new() }; BUCKETS],
len: AtomicUsize::new(0),
push_lock: Mutex::new(()),
}
}
pub(crate) fn len(&self) -> usize {
self.len.load(Ordering::Acquire)
}
pub(crate) fn push(&self, value: T) -> Result<usize, T> {
let _guard = self
.push_lock
.lock()
.unwrap_or_else(PoisonError::into_inner);
let index = self.len.load(Ordering::Acquire);
let Some((bucket, offset, size)) = locate(index) else {
return Err(value);
};
let Some(bucket) = self.buckets.get(bucket) else {
return Err(value);
};
let bucket = bucket.get_or_init(|| (0..size).map(|_| OnceLock::new()).collect());
let Some(slot) = bucket.get(offset) else {
return Err(value);
};
slot.set(value)?;
self.len.store(index.saturating_add(1), Ordering::Release);
Ok(index)
}
pub(crate) fn get(&self, index: usize) -> Option<&T> {
let (bucket, offset, _) = locate(index)?;
self.buckets.get(bucket)?.get()?.get(offset)?.get()
}
}
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)] mod tests {
use super::*;
#[test]
fn locate_covers_bucket_boundaries() {
assert_eq!(locate(0), Some((0, 0, 64)));
assert_eq!(locate(63), Some((0, 63, 64)));
assert_eq!(locate(64), Some((1, 0, 128)));
assert_eq!(locate(191), Some((1, 127, 128)));
assert_eq!(locate(192), Some((2, 0, 256)));
let (bucket, _, _) = locate(MAX_LEN - 1).unwrap();
assert!(bucket < BUCKETS);
assert_eq!(locate(MAX_LEN), None);
}
#[test]
fn references_survive_concurrent_pushes() {
let vec = AppendVec::new();
vec.push(String::from("first")).unwrap();
let first = vec.get(0).unwrap();
std::thread::scope(|scope| {
for thread in 0..4 {
let vec = &vec;
scope.spawn(move || {
for i in 0..500 {
vec.push(format!("{thread}-{i}")).unwrap();
}
});
}
});
assert_eq!(first, "first");
assert_eq!(vec.len(), 2001);
assert!(vec.get(2000).is_some());
assert!(vec.get(2001).is_none());
}
}