#[cfg(not(loom))]
use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
#[cfg(loom)]
use loom::sync::atomic::{AtomicU32, AtomicU64, Ordering};
pub const TAIL: u32 = u32::MAX;
const BACKOFF_SPIN_CAP: u32 = 6;
const _: () = assert!(BACKOFF_SPIN_CAP < 32);
struct Backoff(u32);
impl Backoff {
#[inline]
fn new() -> Self {
Backoff(0)
}
#[inline]
fn spin(&mut self) {
#[cfg(loom)]
BACKOFF_SPIN_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
for _ in 0..(1u32 << self.0) {
core::hint::spin_loop();
}
if self.0 < BACKOFF_SPIN_CAP {
self.0 += 1;
}
}
#[cfg(any(tagged_index_stack_test, loom))]
#[inline]
fn depth(&self) -> u32 {
self.0
}
}
#[inline]
fn note_pop_retry() {
#[cfg(any(tagged_index_stack_test, loom))]
POP_RETRY_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
#[inline]
fn note_push_retry() {
#[cfg(any(tagged_index_stack_test, loom))]
PUSH_RETRY_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
#[cfg(any(tagged_index_stack_test, loom))]
static POP_RETRY_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
#[cfg(loom)]
static BACKOFF_SPIN_COUNT: core::sync::atomic::AtomicUsize =
core::sync::atomic::AtomicUsize::new(0);
#[cfg(any(tagged_index_stack_test, loom))]
static PUSH_RETRY_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
#[cfg(loom)]
#[doc(hidden)]
#[must_use]
pub fn backoff_spin_count_for_test() -> usize {
BACKOFF_SPIN_COUNT.load(core::sync::atomic::Ordering::Relaxed)
}
#[doc(hidden)]
#[must_use]
#[cfg(any(tagged_index_stack_test, loom))]
pub fn retry_counts_for_test() -> (usize, usize) {
(
POP_RETRY_COUNT.load(core::sync::atomic::Ordering::Relaxed),
PUSH_RETRY_COUNT.load(core::sync::atomic::Ordering::Relaxed),
)
}
#[doc(hidden)]
#[must_use]
#[cfg(any(tagged_index_stack_test, loom))]
pub fn backoff_spin_depths_for_test() -> [u32; 9] {
let mut backoff = Backoff::new();
let mut depths = [0; 9];
for depth in &mut depths {
*depth = 1u32 << backoff.depth();
backoff.spin();
}
depths
}
pub enum TaggedIndex<const INDEX_BITS: u32> {}
impl<const INDEX_BITS: u32> TaggedIndex<INDEX_BITS> {
const _CHECK_BITS: () = assert!(
INDEX_BITS >= 1 && INDEX_BITS <= 16,
"INDEX_BITS must be in 1..=16: the tag half must keep at least 48 bits \
(the cache-line-throughput-derived floor against premature tag \
exhaustion/seal — see the crate docs' \"Tag-width budget\" \
section), both halves must be \
non-empty, and every valid index must fit in the shared u32 index \
half (pack/unpack/push_index/empty_index)"
);
pub const INDEX_MASK: u64 = {
let () = Self::_CHECK_BITS;
(1u64 << INDEX_BITS) - 1
};
const INDEX_MASK_U32: u32 = {
let () = Self::_CHECK_BITS;
(1u32 << INDEX_BITS) - 1
};
pub const TAG_BITS: u32 = {
let () = Self::_CHECK_BITS;
64 - INDEX_BITS
};
pub const TAG_MAX: u64 = {
let () = Self::_CHECK_BITS;
(1u64 << Self::TAG_BITS) - 1
};
#[must_use]
pub const fn pack(index: u32, tag: u64) -> Option<u64> {
let () = Self::_CHECK_BITS;
if index >= (1u32 << INDEX_BITS) || tag >= (1u64 << Self::TAG_BITS) {
None
} else {
Some((tag << INDEX_BITS) | (index as u64))
}
}
#[must_use]
pub(crate) const fn pack_truncating(index: u32, tag: u64) -> u64 {
let () = Self::_CHECK_BITS;
debug_assert!(
index as u64 <= Self::INDEX_MASK,
"pack_truncating: index out of range — must be <= INDEX_MASK"
);
debug_assert!(
tag <= Self::TAG_MAX,
"pack_truncating: tag out of range — must be <= TAG_MAX (all \
callers prove this before calling — see this fn's doc)"
);
(tag << INDEX_BITS) | (index as u64)
}
#[must_use]
pub const fn unpack(word: u64) -> (u32, u64) {
((word & Self::INDEX_MASK) as u32, word >> INDEX_BITS)
}
#[must_use]
const fn bootstrap_empty() -> u64 {
Self::pack_truncating(Self::INDEX_MASK_U32, 0)
}
#[must_use]
pub const fn empty_index() -> u32 {
Self::INDEX_MASK_U32
}
#[must_use]
pub const fn is_empty(word: u64) -> bool {
(word & Self::INDEX_MASK) == Self::INDEX_MASK
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TagExhausted;
impl core::fmt::Display for TagExhausted {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"tagged-index-stack: push refused, the head's tag has reached \
TaggedIndex::TAG_MAX; the stack is sealed (pops still work, \
pushes are refused permanently)"
)
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct StackHead<const INDEX_BITS: u32> {
head: AtomicU64,
}
impl<const INDEX_BITS: u32> StackHead<INDEX_BITS> {
#[cfg(not(loom))]
#[must_use]
pub const fn new() -> Self {
Self {
head: AtomicU64::new(TaggedIndex::<INDEX_BITS>::bootstrap_empty()),
}
}
#[cfg(loom)]
#[must_use]
pub fn new() -> Self {
Self {
head: AtomicU64::new(TaggedIndex::<INDEX_BITS>::bootstrap_empty()),
}
}
pub(crate) fn load(&self, ordering: Ordering) -> u64 {
self.head.load(ordering)
}
pub(crate) fn compare_exchange(
&self,
current: u64,
new: u64,
success: Ordering,
failure: Ordering,
) -> Result<u64, u64> {
self.head.compare_exchange(current, new, success, failure)
}
#[must_use]
pub fn is_empty(&self) -> bool {
TaggedIndex::<INDEX_BITS>::is_empty(self.head.load(Ordering::Relaxed))
}
#[must_use]
pub fn pushes_remaining(&self) -> u64 {
let (_, tag) = TaggedIndex::<INDEX_BITS>::unpack(self.head.load(Ordering::Relaxed));
TaggedIndex::<INDEX_BITS>::TAG_MAX - tag
}
#[doc(hidden)]
#[cfg(any(tagged_index_stack_test, loom))]
#[must_use]
pub fn with_tag_for_test(tag: u64) -> Self {
Self {
head: AtomicU64::new(
TaggedIndex::<INDEX_BITS>::pack(TaggedIndex::<INDEX_BITS>::empty_index(), tag)
.expect("with_tag_for_test: tag out of range (tag > TaggedIndex::TAG_MAX)"),
),
}
}
#[doc(hidden)]
#[cfg(any(tagged_index_stack_test, loom))]
#[must_use]
pub fn raw_head(&self) -> u64 {
self.head.load(Ordering::Acquire)
}
#[cfg(loom)]
#[doc(hidden)]
pub fn cas_head_for_test(
&self,
current: u64,
new: u64,
success: Ordering,
failure: Ordering,
) -> Result<u64, u64> {
self.head.compare_exchange(current, new, success, failure)
}
}
impl<const INDEX_BITS: u32> Default for StackHead<INDEX_BITS> {
fn default() -> Self {
Self::new()
}
}
#[allow(unsafe_code)]
pub unsafe trait StackStorage<const INDEX_BITS: u32> {
unsafe fn head(&self) -> &StackHead<INDEX_BITS>;
unsafe fn load_next(&self, index: u32) -> u32;
unsafe fn store_next(&self, index: u32, next: u32);
}
pub trait StackOps<const INDEX_BITS: u32>: StackStorage<INDEX_BITS> {
#[track_caller]
#[allow(unsafe_code)]
unsafe fn push_index(&self, index: u32) -> Result<(), TagExhausted>;
#[must_use = "a popped index is removed from the free-list; discarding it leaks the slot"]
#[track_caller]
fn pop_index(&self) -> Option<u32>;
}
#[allow(unsafe_code)]
pub(crate) trait SealedStorage<const B: u32> {
unsafe fn head(&self) -> &StackHead<B>;
unsafe fn load_next(&self, index: u32) -> u32;
unsafe fn store_next(&self, index: u32, next: u32);
}
#[allow(unsafe_code)]
impl<const B: u32, S: StackStorage<B> + ?Sized> SealedStorage<B> for S {
unsafe fn head(&self) -> &StackHead<B> {
unsafe { StackStorage::head(self) }
}
unsafe fn load_next(&self, index: u32) -> u32 {
unsafe { StackStorage::load_next(self, index) }
}
unsafe fn store_next(&self, index: u32, next: u32) {
unsafe { StackStorage::store_next(self, index, next) }
}
}
#[track_caller]
#[allow(unsafe_code)]
pub(crate) unsafe fn push_index_impl<const B: u32, S: SealedStorage<B> + ?Sized>(
s: &S,
index: u32,
) -> Result<(), TagExhausted> {
let mask = TaggedIndex::<B>::INDEX_MASK;
if u64::from(index) >= mask {
push_index_out_of_range(index, mask);
}
let head_ref: &StackHead<B> = unsafe { s.head() };
let mut head = head_ref.load(Ordering::Relaxed);
let mut backoff = Backoff::new();
loop {
let (cur_idx, tag) = TaggedIndex::<B>::unpack(head);
if tag == TaggedIndex::<B>::TAG_MAX {
return Err(TagExhausted);
}
let next_link = if cur_idx == TaggedIndex::<B>::empty_index() {
TAIL
} else {
cur_idx
};
unsafe {
s.store_next(index, next_link);
}
let new_tag = tag + 1;
let new_head = TaggedIndex::<B>::pack_truncating(index, new_tag);
match head_ref.compare_exchange(head, new_head, Ordering::Release, Ordering::Relaxed) {
Ok(_) => return Ok(()),
Err(actual) => {
note_push_retry();
head = actual;
backoff.spin();
}
}
}
}
#[allow(unsafe_code)]
#[track_caller]
pub(crate) fn pop_index_impl<const B: u32, S: SealedStorage<B> + ?Sized>(s: &S) -> Option<u32> {
let head_ref: &StackHead<B> = unsafe { s.head() };
let mut head = head_ref.load(Ordering::Acquire);
let mut backoff = Backoff::new();
loop {
if TaggedIndex::<B>::is_empty(head) {
return None;
}
let (index, tag) = TaggedIndex::<B>::unpack(head);
let next = unsafe { s.load_next(index) };
let mask = TaggedIndex::<B>::INDEX_MASK;
if next != TAIL && (u64::from(next) >= mask || next == index) {
pop_link_out_of_range(index, next, mask);
}
let new_head = if next == TAIL {
TaggedIndex::<B>::pack_truncating(TaggedIndex::<B>::empty_index(), tag)
} else {
TaggedIndex::<B>::pack_truncating(next, tag)
};
match head_ref.compare_exchange(head, new_head, Ordering::Acquire, Ordering::Acquire) {
Ok(_) => return Some(index),
Err(actual) => {
note_pop_retry();
head = actual;
if !TaggedIndex::<B>::is_empty(actual) {
backoff.spin();
}
}
}
}
}
#[allow(unsafe_code)]
impl<const B: u32, S: StackStorage<B> + ?Sized> StackOps<B> for S {
#[track_caller]
unsafe fn push_index(&self, index: u32) -> Result<(), TagExhausted> {
unsafe { push_index_impl::<B, S>(self, index) }
}
#[track_caller]
fn pop_index(&self) -> Option<u32> {
pop_index_impl::<B, S>(self)
}
}
#[cold]
#[inline(never)]
#[track_caller]
fn push_index_out_of_range(index: u32, mask: u64) -> ! {
panic!(
"index must be < INDEX_MASK (the empty sentinel is reserved), \
got {index} (INDEX_MASK = {mask:#x})"
);
}
#[cold]
#[inline(never)]
#[track_caller]
fn array_links_out_of_range(index: u32, capacity: usize) -> ! {
panic!("ArrayLinks index out of bounds: index {index} >= capacity {capacity}");
}
#[cold]
#[inline(never)]
#[track_caller]
fn pop_link_out_of_range(index: u32, next: u32, mask: u64) -> ! {
if next == index {
panic!(
"load_next({index}) returned {next:#x}, the index's own link points \
back to itself — a self-loop, corrupting the free-list into a cycle: \
pop_index's truncating pack would silently re-issue this same index \
to a second owner"
);
}
let outcome = if (u64::from(next) & mask) == mask {
"the EMPTY SENTINEL, leaking the whole remaining chain"
} else {
"a wrong index, possibly a live one — double-issuing it"
};
panic!(
"load_next({index}) returned {next:#x}, neither TAIL nor \
a valid index (< {mask:#x}): pop_index's truncating pack would silently \
truncate it to {outcome}"
);
}
#[derive(Debug)]
pub struct ArrayIndexStack<const INDEX_BITS: u32, const N: usize> {
head: StackHead<INDEX_BITS>,
links: ArrayLinks<N>,
}
impl<const B: u32, const N: usize> ArrayIndexStack<B, N> {
const _CHECK_N: () = assert!(
N as u64 <= TaggedIndex::<B>::INDEX_MASK,
"ArrayIndexStack capacity N must be <= INDEX_MASK"
);
#[cfg(not(loom))]
#[must_use]
pub const fn new() -> Self {
let () = Self::_CHECK_N;
Self {
head: StackHead::new(),
links: ArrayLinks::new(),
}
}
#[cfg(loom)]
#[must_use]
pub fn new() -> Self {
let () = Self::_CHECK_N;
Self {
head: StackHead::new(),
links: ArrayLinks::new(),
}
}
#[track_caller]
#[allow(unsafe_code)]
pub unsafe fn push(&self, index: u32) -> Result<(), TagExhausted> {
unsafe { push_index_impl::<B, _>(self, index) }
}
#[must_use = "a popped index is removed from the free-list; discarding it leaks the slot"]
#[track_caller]
pub fn pop(&self) -> Option<u32> {
pop_index_impl::<B, _>(self)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.head.is_empty()
}
#[must_use]
pub fn pushes_remaining(&self) -> u64 {
self.head.pushes_remaining()
}
#[doc(hidden)]
#[cfg(any(tagged_index_stack_test, loom))]
#[must_use]
pub fn raw_head(&self) -> u64 {
self.head.raw_head()
}
#[cfg(loom)]
#[doc(hidden)]
pub fn cas_head_for_test(
&self,
current: u64,
new: u64,
success: Ordering,
failure: Ordering,
) -> Result<u64, u64> {
self.head.cas_head_for_test(current, new, success, failure)
}
#[doc(hidden)]
#[cfg(any(tagged_index_stack_test, loom))]
pub fn load_next_for_test(&self, index: u32) -> u32 {
self.links.load_next(index)
}
#[doc(hidden)]
#[cfg(loom)]
#[allow(unsafe_code)]
pub unsafe fn store_next_for_test(&self, index: u32, next: u32) {
self.links.store_next(index, next);
}
#[doc(hidden)]
#[cfg(any(tagged_index_stack_test, loom))]
#[must_use]
pub fn with_tag_for_test(tag: u64) -> Self {
let () = Self::_CHECK_N;
Self {
head: StackHead::with_tag_for_test(tag),
links: ArrayLinks::new(),
}
}
}
impl<const B: u32, const N: usize> Default for ArrayIndexStack<B, N> {
fn default() -> Self {
Self::new()
}
}
#[allow(unsafe_code)]
impl<const B: u32, const N: usize> SealedStorage<B> for ArrayIndexStack<B, N> {
unsafe fn head(&self) -> &StackHead<B> {
&self.head
}
unsafe fn load_next(&self, index: u32) -> u32 {
self.links.load_next(index)
}
unsafe fn store_next(&self, index: u32, next: u32) {
self.links.store_next(index, next)
}
}
#[derive(Debug)]
pub struct ArrayLinks<const N: usize> {
next: [AtomicU32; N],
}
impl<const N: usize> ArrayLinks<N> {
#[cfg(not(loom))]
#[must_use]
pub const fn new() -> Self {
Self {
next: [const { AtomicU32::new(0) }; N],
}
}
#[cfg(loom)]
#[must_use]
pub fn new() -> Self {
Self {
next: core::array::from_fn(|_| AtomicU32::new(0)),
}
}
#[must_use]
#[track_caller]
pub fn load_next(&self, index: u32) -> u32 {
if index as usize >= N {
array_links_out_of_range(index, N);
}
self.next[index as usize].load(Ordering::Acquire)
}
#[track_caller]
pub fn store_next(&self, index: u32, next: u32) {
if index as usize >= N {
array_links_out_of_range(index, N);
}
self.next[index as usize].store(next, Ordering::Release);
}
}
impl<const N: usize> Default for ArrayLinks<N> {
fn default() -> Self {
Self::new()
}
}