tagged_index_stack/imp.rs
1//! The tagged-index-stack implementation, gated as one unit by the crate
2//! root's valid-configuration `#[cfg]`.
3//!
4//! `compile_error!` does not stop name-resolution of sibling items, so invalid
5//! configurations must fail with only the named error: `lib.rs` cfgs this
6//! module OUT under every invalid configuration and re-exports it (`pub use
7//! imp::*`) under every valid one, so public paths are unchanged. The whole
8//! body in one module is this single-responsibility crate's established file
9//! structure.
10
11// The atomics are aliased so loom can shadow the REAL stack type: under
12// `--cfg loom` they are built on `loom::sync::atomic`, so the shipped loom
13// tests exercise the actual code; otherwise `core::sync::atomic`, keeping the
14// crate zero-non-std-dep.
15#[cfg(not(loom))]
16use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
17#[cfg(loom)]
18use loom::sync::atomic::{AtomicU32, AtomicU64, Ordering};
19
20/// The "no next" sentinel stored in a slot's link to denote the BOTTOM of the
21/// stack (the first index pushed onto an empty stack chains to this).
22/// `u32::MAX`.
23///
24/// `TAIL` (`u32::MAX`) terminates a link chain; the head's empty sentinel is
25/// `INDEX_MASK` (at most `0xFFFF`). Push and pop translate between them.
26pub const TAIL: u32 = u32::MAX;
27
28/// Exponential-backoff cap for `push_index`/`pop_index`'s CAS-retry arms: the
29/// Kth lost CAS within one call, counting from 0, spins `1 << K` times via
30/// [`core::hint::spin_loop`] before retrying (K = 0 spins once, up to
31/// `1 << BACKOFF_SPIN_CAP` at the cap). The cap is enforced by
32/// [`Backoff`]`::spin`'s saturation — `K` never exceeds it — and is a
33/// per-call local, reset on every fresh `push_index`/`pop_index`; backoff
34/// happens within one call's retry loop, never across calls. `pop_index`
35/// skips the backoff when the lost CAS reveals the stack just went empty
36/// (documented at [`pop_index`](StackOps::pop_index)).
37///
38/// The shipped cap is a deliberate fairness-vs-throughput compromise, not a
39/// low-contention optimum: caps 8/10 give more aggregate throughput but
40/// measurably worse per-thread fairness under oversubscription, while caps
41/// 0/4 are fairer but slower. Measurements and the full fairness/throughput
42/// tables are in `docs/perf/TIS_BACKOFF_CAP_SWEEP_GATE.md` (a repository
43/// file). `spin_loop` is a processor hint, so one unit is not a portable time
44/// unit; the measured trade is host- and microarchitecture-specific. Lock-free
45/// is not starvation-free — see the crate-root doc's "Lock-freedom and
46/// starvation" section for the measured trade.
47const BACKOFF_SPIN_CAP: u32 = 6;
48
49// Keep the shift bound compile-time checked so overflow cannot become a release-build bug.
50const _: () = assert!(BACKOFF_SPIN_CAP < 32);
51
52/// Per-call exponential-backoff state for the CAS-retry arms: wraps the retry
53/// counter (`K`, starting at 0) that drives the spin-loop depth of
54/// [`Backoff::spin`]. Starts fresh every call, never persisted.
55struct Backoff(u32);
56
57impl Backoff {
58 /// Inline into generic retry-loop instantiations; otherwise this private
59 /// non-generic helper could remain an out-of-line call.
60 #[inline]
61 fn new() -> Self {
62 Backoff(0)
63 }
64
65 /// Exponential backoff before retrying (BACKOFF_SPIN_CAP): spins
66 /// `1 << K` times, letting the winning thread's Release CAS drain off
67 /// the head cache line instead of every loser re-hammering it
68 /// immediately. `K` grows only within one call.
69 ///
70 ///
71 /// Capped, not unconditional: saturation keeps `K <= BACKOFF_SPIN_CAP`,
72 /// so `1u32 << K` can never overflow (`K` = 32 would, after only 32
73 /// consecutive lost CASes in one call — an ordinary event under the
74 /// contention this crate is built for, not a remote one). The
75 /// saturation also guarantees `self.0 <= BACKOFF_SPIN_CAP` at the
76 /// shift, so no `.min` guard is needed on the shift expression.
77 #[inline]
78 fn spin(&mut self) {
79 #[cfg(loom)]
80 BACKOFF_SPIN_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
81 for _ in 0..(1u32 << self.0) {
82 core::hint::spin_loop();
83 }
84 if self.0 < BACKOFF_SPIN_CAP {
85 self.0 += 1;
86 }
87 }
88
89 #[cfg(any(tagged_index_stack_test, loom))]
90 #[inline]
91 fn depth(&self) -> u32 {
92 self.0
93 }
94}
95
96// Test/loom instrumentation is kept beside the state it observes. The note
97// functions are unconditional so retry sites have one cfg boundary only.
98#[inline]
99fn note_pop_retry() {
100 #[cfg(any(tagged_index_stack_test, loom))]
101 POP_RETRY_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
102}
103
104#[inline]
105fn note_push_retry() {
106 #[cfg(any(tagged_index_stack_test, loom))]
107 PUSH_RETRY_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
108}
109
110#[cfg(any(tagged_index_stack_test, loom))]
111static POP_RETRY_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
112#[cfg(loom)]
113static BACKOFF_SPIN_COUNT: core::sync::atomic::AtomicUsize =
114 core::sync::atomic::AtomicUsize::new(0);
115#[cfg(any(tagged_index_stack_test, loom))]
116static PUSH_RETRY_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
117
118#[cfg(loom)]
119#[doc(hidden)]
120#[must_use]
121pub fn backoff_spin_count_for_test() -> usize {
122 BACKOFF_SPIN_COUNT.load(core::sync::atomic::Ordering::Relaxed)
123}
124
125#[doc(hidden)]
126#[must_use]
127#[cfg(any(tagged_index_stack_test, loom))]
128pub fn retry_counts_for_test() -> (usize, usize) {
129 (
130 POP_RETRY_COUNT.load(core::sync::atomic::Ordering::Relaxed),
131 PUSH_RETRY_COUNT.load(core::sync::atomic::Ordering::Relaxed),
132 )
133}
134
135#[doc(hidden)]
136#[must_use]
137#[cfg(any(tagged_index_stack_test, loom))]
138pub fn backoff_spin_depths_for_test() -> [u32; 9] {
139 let mut backoff = Backoff::new();
140 let mut depths = [0; 9];
141 for depth in &mut depths {
142 *depth = 1u32 << backoff.depth();
143 backoff.spin();
144 }
145 depths
146}
147
148/// A packed `(index | tag)` word with a compile-time-chosen index width.
149///
150/// The low `INDEX_BITS` bits carry a slot index; the high `64 - INDEX_BITS`
151/// bits carry a strictly monotonic generation ABA tag that SEALS at
152/// [`TAG_MAX`](Self::TAG_MAX) rather than wrapping. The all-ones index value
153/// ([`empty_index`](Self::empty_index)) is reserved as the empty-stack sentinel,
154/// so valid indices are `0 .. (1 << INDEX_BITS) - 1`.
155///
156/// This is a namespace of `const fn` bit operations, not a value type — no
157/// state, no memory, no `unsafe`, strict-provenance-clean by construction (it
158/// packs a plain integer index, never a pointer/address). Declared as an
159/// UNINHABITED `enum` (zero variants) rather than a unit `struct`: a unit
160/// struct is freely constructible, and closing that off later with a private
161/// field would be a breaking change once published, whereas an uninhabited
162/// `enum` has no constructor at all from the start.
163pub enum TaggedIndex<const INDEX_BITS: u32> {}
164
165impl<const INDEX_BITS: u32> TaggedIndex<INDEX_BITS> {
166 /// Compile-time guard: `INDEX_BITS` must be in `1..=16` so both halves
167 /// are non-empty, every valid index fits the `u32` that the whole
168 /// index-carrying surface takes ([`push_index`](StackOps::push_index),
169 /// [`pack`](Self::pack)'s parameter, [`unpack`](Self::unpack)'s index
170 /// half, [`empty_index`](Self::empty_index) — all `u32`), with no
171 /// casts, and the tag half keeps a minimum of 48 bits — the seal-time
172 /// floor below which a head's pushes-until-sealed lifetime comes within
173 /// reach of an ordinary long-running process (see the crate docs'
174 /// "Tag-width budget" section). At
175 /// every legal width `INDEX_MASK <= 0xFFFF`, so `INDEX_MASK != TAIL`
176 /// and `index == TAIL` can never silently pass the runtime guard.
177 ///
178 /// This `const` is forced to evaluate from every associated item of
179 /// `TaggedIndex<INDEX_BITS>`: [`pack`](Self::pack) forces it directly
180 /// with a `let () = Self::_CHECK_BITS;` statement, `INDEX_MASK` and
181 /// [`TAG_BITS`](Self::TAG_BITS) evaluate it in their own initializers,
182 /// and [`unpack`](Self::unpack), [`empty_index`](Self::empty_index),
183 /// [`is_empty`](Self::is_empty), and the
184 /// crate-private `pack_truncating` all route through `INDEX_MASK` — so
185 /// an out-of-range `INDEX_BITS` cannot reach any associated item
186 /// without tripping this guard.
187 const _CHECK_BITS: () = assert!(
188 INDEX_BITS >= 1 && INDEX_BITS <= 16,
189 "INDEX_BITS must be in 1..=16: the tag half must keep at least 48 bits \
190 (the cache-line-throughput-derived floor against premature tag \
191 exhaustion/seal — see the crate docs' \"Tag-width budget\" \
192 section), both halves must be \
193 non-empty, and every valid index must fit in the shared u32 index \
194 half (pack/unpack/push_index/empty_index)"
195 );
196
197 /// Bit-mask for the low `INDEX_BITS` (the index half), e.g. `0xFFFF`
198 /// for `INDEX_BITS = 16`. Its `u32`-typed form is the
199 /// [`empty_index`](Self::empty_index) value.
200 ///
201 /// Forces `_CHECK_BITS` to evaluate here too — see `_CHECK_BITS`'s doc.
202 pub const INDEX_MASK: u64 = {
203 let () = Self::_CHECK_BITS;
204 (1u64 << INDEX_BITS) - 1
205 };
206
207 /// The `u32` form of [`Self::INDEX_MASK`] — identical value. The index
208 /// half is `u32`-typed end to end ([`pack`](Self::pack)'s parameter,
209 /// [`unpack`](Self::unpack)'s first element, [`empty_index`](Self::empty_index));
210 /// this mirror exists so those surfaces need no cast: `INDEX_BITS <= 16`
211 /// (`_CHECK_BITS`), so `(1u32 << INDEX_BITS) - 1` derives it directly.
212 const INDEX_MASK_U32: u32 = {
213 let () = Self::_CHECK_BITS;
214 (1u32 << INDEX_BITS) - 1
215 };
216
217 /// Number of bits carrying the tag (`64 - INDEX_BITS`). The tag is
218 /// strictly monotonic and SEALS at [`TAG_MAX`](Self::TAG_MAX) — it does
219 /// not wrap.
220 pub const TAG_BITS: u32 = {
221 let () = Self::_CHECK_BITS;
222 64 - INDEX_BITS
223 };
224
225 /// Largest tag a head word can carry: `2^TAG_BITS - 1`. A push that
226 /// observes this tag on the current head is refused
227 /// (`Err(`[`TagExhausted`]`)`) instead of bumping it to `2^TAG_BITS`,
228 /// which would wrap back to 0 and re-issue a `(index, tag)` head word
229 /// that a popper parked since the previous cycle may still hold as its
230 /// stale CAS expectation — see
231 /// [`push_index`](StackOps::push_index)'s `# Errors` section and the
232 /// crate-root docs' "Tag-width budget" section.
233 /// [`pack`](Self::pack)`(_, TAG_MAX)` is `Some`; `pack(_, TAG_MAX + 1)`
234 /// is `None`.
235 pub const TAG_MAX: u64 = {
236 let () = Self::_CHECK_BITS;
237 (1u64 << Self::TAG_BITS) - 1
238 };
239
240 /// Pack `(index, tag)` into one `u64`, CHECKED: `Some(word)` for an
241 /// in-range pair, `None` when either half is out of range — `index >=
242 /// 2^INDEX_BITS` over the `u32` index parameter (which unchecked masking
243 /// would silently turn into a
244 /// DIFFERENT, valid-looking index, or into the
245 /// [empty sentinel](Self::empty_index) if the low bits happen to be all
246 /// ones) or `tag >= 2^TAG_BITS` (whose high bits a `tag << INDEX_BITS`
247 /// shift would silently drop). The index parameter is `u32`; the tag half
248 /// is `u64`. For an accepted pair the word is exactly
249 /// `(index | tag << INDEX_BITS)`: both halves are already within their
250 /// bit budgets, so no masking takes place and `unpack` recovers both
251 /// halves exactly.
252 ///
253 /// Note the two bounds in this crate are deliberately different ranges:
254 /// `< 2^INDEX_BITS` is this function's acceptance boundary, while
255 /// [`push_index`](StackOps::push_index)'s `< INDEX_MASK`
256 /// (`INDEX_MASK == 2^INDEX_BITS - 1`) is stricter because it also
257 /// excludes the reserved empty sentinel. Packing the empty index with a
258 /// tag IS accepted here — the legitimate tag-preserving empty transition
259 /// ([`empty_index`](Self::empty_index)).
260 ///
261 /// `push_index`/`pop_index` do NOT call this function on the hot path:
262 /// their inputs are already proven within range by the crate's own
263 /// guards, so they pack through the crate-private truncating fast path
264 /// `pack_truncating` purely to skip this function's redundant range
265 /// re-check (see its doc).
266 #[must_use]
267 pub const fn pack(index: u32, tag: u64) -> Option<u64> {
268 // Forced here too: a const eval taking the short-circuit branch
269 // would otherwise skip both const paths.
270 let () = Self::_CHECK_BITS;
271 if index >= (1u32 << INDEX_BITS) || tag >= (1u64 << Self::TAG_BITS) {
272 None
273 } else {
274 Some((tag << INDEX_BITS) | (index as u64))
275 }
276 }
277
278 /// Truncating fast path: `(tag << INDEX_BITS) | index`. TRUSTS ITS
279 /// PRECONDITION — the name is the contract: this silently produces a
280 /// VALID-LOOKING word from invalid input, and no masking takes place:
281 /// an over-wide index ORs its high bits across the index/tag boundary
282 /// into the tag half, corrupting BOTH halves at once (a different
283 /// index AND a different tag — nothing rounds invalid input to a
284 /// benign value); an over-wide tag loses its high bits. If you cannot
285 /// prove your halves are in range, use [`pack`](Self::pack), which
286 /// rejects instead (see its doc for the checked semantics). The range
287 /// proof is additionally tripped by a `debug_assert!` in the body —
288 /// a debug-build check, never a release-build guarantee.
289 ///
290 /// Crate-private so the sharp edges stay in-crate; the only callers are
291 /// [`push_index`](StackOps::push_index), [`pop_index`](StackOps::pop_index),
292 /// and the bootstrap constructor. All three prove `tag <= TAG_MAX` before
293 /// calling — push's seal check refuses an already-[`TAG_MAX`](Self::TAG_MAX)
294 /// tag before its `tag + 1` bump — so truncation never actually discards
295 /// a bit on this path, and the push caller's plain `+` bump can never
296 /// overflow at any legal width (`TAG_MAX <= 2^63 - 1`): there is nothing
297 /// for a wrapping or saturating operator to guard.
298 ///
299 /// This helper is NOT a wrap-on-truncate mechanism: it does not wrap
300 /// the tag back to 0, and must never be made to — wrap-on-truncation
301 /// would reopen the exact stale-CAS double-issue the seal
302 /// ([`TAG_MAX`](Self::TAG_MAX) + [`TagExhausted`]) exists to close
303 /// (see the crate-root docs' "Tag-width budget" section).
304 #[must_use]
305 pub(crate) const fn pack_truncating(index: u32, tag: u64) -> u64 {
306 let () = Self::_CHECK_BITS;
307 debug_assert!(
308 index as u64 <= Self::INDEX_MASK,
309 "pack_truncating: index out of range — must be <= INDEX_MASK"
310 );
311 debug_assert!(
312 tag <= Self::TAG_MAX,
313 "pack_truncating: tag out of range — must be <= TAG_MAX (all \
314 callers prove this before calling — see this fn's doc)"
315 );
316 (tag << INDEX_BITS) | (index as u64)
317 }
318
319 /// Split a packed word back into `(u32 index, u64 tag)`.
320 #[must_use]
321 pub const fn unpack(word: u64) -> (u32, u64) {
322 // INDEX_MASK <= 0xFFFF at every legal width per `_CHECK_BITS`, so the
323 // AND result is <= 0xFFFF and the cast is lossless by construction.
324 ((word & Self::INDEX_MASK) as u32, word >> INDEX_BITS)
325 }
326
327 /// Bootstrap empty-stack word: index =
328 /// [`empty_index`](Self::empty_index), tag = 0. A freshly-constructed
329 /// [`StackHead`] is this.
330 ///
331 /// **Only bootstrap-time emptiness uses tag 0 unconditionally.** A RUNTIME
332 /// empty transition (a pop that drains the last element) MUST preserve the
333 /// running tag — see [`empty_index`](Self::empty_index); resetting to 0
334 /// there reopens the ABA window (see the crate docs' tag-preservation note).
335 ///
336 /// Crate-private bootstrap helper. Runtime empty transitions must use the
337 /// observed tag instead; this word is only valid for construction.
338 #[must_use]
339 const fn bootstrap_empty() -> u64 {
340 Self::pack_truncating(Self::INDEX_MASK_U32, 0)
341 }
342
343 /// The empty sentinel's index half: the `u32` form of `INDEX_MASK`, for
344 /// packing it with a
345 /// NON-zero, caller-supplied RUNNING tag (`pack(empty_index(), running_tag)`)
346 /// instead of the crate-private bootstrap helper, which always zeroes the
347 /// tag.
348 ///
349 /// **Empty-transition tag preservation:** the transition in [`pop_index`](StackOps::pop_index)
350 /// uses this, packing the tag it just observed on the popped head, so the
351 /// ABA tag keeps counting forward across the empty→non-empty churn cycle.
352 /// [`is_empty`](Self::is_empty) inspects only the index half, so a non-zero
353 /// tag here is still unambiguously "empty".
354 #[must_use]
355 pub const fn empty_index() -> u32 {
356 Self::INDEX_MASK_U32
357 }
358
359 /// Whether a packed word denotes the empty stack (index half == the empty
360 /// sentinel), REGARDLESS of the tag half.
361 #[must_use]
362 pub const fn is_empty(word: u64) -> bool {
363 (word & Self::INDEX_MASK) == Self::INDEX_MASK
364 }
365}
366
367/// A push was refused because the head's running tag is already
368/// [`TaggedIndex::TAG_MAX`]: bumping it would wrap to 0 and re-issue a
369/// `(index, tag)` head word that a popper parked since the previous cycle
370/// may still hold as its CAS expectation — the exact stale-CAS double-issue
371/// the tag exists to prevent (see the crate-root docs' "The tag is strictly
372/// monotonic" section). The stack is then SEALED: every further
373/// [`push_index`](StackOps::push_index) is refused the same way,
374/// permanently; [`pop_index`](StackOps::pop_index) is unaffected and drains
375/// the remaining chain normally. The refused index was never published and
376/// remains owned by the caller — nothing leaked by this error alone.
377///
378/// No `core::error::Error` impl: this crate's declared MSRV
379/// (`Cargo.toml`'s `rust-version`) is 1.79, and `core::error::Error`
380/// stabilized in 1.81. Deferred, not silently skipped — add the impl in a
381/// future change once the MSRV floor moves past 1.81.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
383pub struct TagExhausted;
384
385impl core::fmt::Display for TagExhausted {
386 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
387 write!(
388 f,
389 "tagged-index-stack: push refused, the head's tag has reached \
390 TaggedIndex::TAG_MAX; the stack is sealed (pops still work, \
391 pushes are refused permanently)"
392 )
393 }
394}
395
396/// The head word of a tagged Treiber free-list: a single `AtomicU64` packing an
397/// `(index | tag)` pair (see [`TaggedIndex`]). Owned by exactly one
398/// [`StackStorage`] implementor value at a time, and bound to one link
399/// backing for its WHOLE life — the binding between this head and its links
400/// is established by that impl, not re-asserted per call; sharing one head
401/// between implementor values (clause 1) or rebinding a live head across time
402/// are hazards — see the [`StackStorage`] trait's `# Safety` contract. The
403/// stack operations themselves live
404/// on [`StackOps`] (blanket-implemented by the crate), not here; this type
405/// is the bare atomic embedders inherit a cache line through.
406///
407/// # Layout note — no cache-line isolation
408///
409/// This type is a bare `AtomicU64` with no padding or alignment of its own —
410/// `#[repr(transparent)]` makes that a compiler-enforced guarantee (layout,
411/// size, and ABI identical to the single `head` field), not an incidental
412/// property of the current definition — so it inherits the cache line of
413/// whatever struct embeds it. If it lands adjacent
414/// to another frequently-modified atomic, the two fields false-share — each
415/// write invalidates the other core's copy of the line, and contending cores
416/// ping-pong the line even though the atomics are logically independent. That
417/// costs throughput, never correctness, and only matters when the line is
418/// genuinely hot. Fix it at the embedding site when a profile shows it — wrap
419/// this stack in a `#[repr(align(64))]` newtype or interpose padding — rather
420/// than paying for blanket alignment inside the crate, which would waste most
421/// of a cache line for every embedder that does not need the isolation.
422///
423/// # Sealing is permanent — no reset
424///
425/// Once [`pushes_remaining`](Self::pushes_remaining) reaches 0 (the tag is
426/// [`TaggedIndex::TAG_MAX`]), this head is sealed and stays sealed: there is
427/// no reset/rotation API, and none will be added. An in-place reset would be
428/// a plain `store` on `head` — breaking the release-sequence invariant
429/// documented on the private field below — AND would restore tag 0,
430/// reintroducing the exact full-wrap collision this seal exists to close
431/// (see the crate-root docs' tag-preservation note on why a naive tag reset is unsound in
432/// general). A sealed head cannot be reset. A replacement must be a
433/// distinct [`StackHead`] object; if it reuses the same link cells and index
434/// population, the sealed head must first be fully drained
435/// ([`pop_index`](StackOps::pop_index) → `None` repeatedly until empty) and
436/// must outlive every popper that may still reference it — for a `'static`
437/// head, forever.
438#[repr(transparent)]
439#[derive(Debug)]
440pub struct StackHead<const INDEX_BITS: u32> {
441 /// INVARIANT (release sequence): every modification of `head` MUST be a
442 /// compare_exchange (an RMW). Today both writers are —
443 /// [`push_index`](StackOps::push_index)'s `Release` CAS and
444 /// [`pop_index`](StackOps::pop_index)'s `Acquire` CAS (plus the loom-only
445 /// `cas_head_for_test`, also a CAS; constructing the atomic in `new` is
446 /// initialization, not a modification, and `raw_head` only loads). Per
447 /// the release-sequence rule, a release sequence continues through every
448 /// subsequent RMW to the same location regardless of those RMWs' own
449 /// orderings, so with every write here an RMW the release sequence headed
450 /// by any push's `Release` CAS stays UNBROKEN across all later
451 /// modifications. That is what lets `pop_index`'s successful CAS be plain
452 /// `Acquire` instead of `AcqRel`: any later `Acquire` read of a value
453 /// this pop wrote still lands inside that push's release sequence, so the
454 /// happens-before edge back to the link-writing push survives
455 /// transitively.
456 ///
457 /// Do NOT add a plain `store` to this field (e.g. a hypothetical
458 /// `clear()`/`reset()`, or a `Drop` impl zeroing it). A non-RMW write
459 /// severs every release sequence it follows; after that, `pop_index`'s
460 /// `Acquire`-only success ordering can silently un-publish links on
461 /// weakly-ordered targets — no compile error, and likely no test failure
462 /// on x86. If such an API is ever genuinely needed, promote `pop_index`'s
463 /// success ordering to `AcqRel` in the same change. (Plain loads are
464 /// harmless: they modify nothing, so they break no sequence.)
465 head: AtomicU64,
466}
467
468impl<const INDEX_BITS: u32> StackHead<INDEX_BITS> {
469 /// A fresh, EMPTY stack head (the bootstrap empty sentinel, tag 0). Under
470 /// `--cfg loom` this cannot be `const` (loom's atomics have no `const` ctor).
471 #[cfg(not(loom))]
472 #[must_use]
473 pub const fn new() -> Self {
474 Self {
475 head: AtomicU64::new(TaggedIndex::<INDEX_BITS>::bootstrap_empty()),
476 }
477 }
478
479 /// A fresh, EMPTY stack head (loom build — non-`const`).
480 #[cfg(loom)]
481 #[must_use]
482 pub fn new() -> Self {
483 Self {
484 head: AtomicU64::new(TaggedIndex::<INDEX_BITS>::bootstrap_empty()),
485 }
486 }
487
488 /// Thin wrapper over the head atomic's load — for the [`StackOps`] blanket
489 /// impl.
490 pub(crate) fn load(&self, ordering: Ordering) -> u64 {
491 self.head.load(ordering)
492 }
493
494 /// Thin wrapper over the head atomic's strong compare_exchange — for the
495 /// [`StackOps`] blanket impl.
496 pub(crate) fn compare_exchange(
497 &self,
498 current: u64,
499 new: u64,
500 success: Ordering,
501 failure: Ordering,
502 ) -> Result<u64, u64> {
503 self.head.compare_exchange(current, new, success, failure)
504 }
505
506 /// Whether the stack is currently empty. Advisory only — a concurrent
507 /// push or pop can make the answer stale the instant this returns, in
508 /// either direction — so use it for diagnostics/monitoring, not for
509 /// correctness decisions ([`pop_index`](StackOps::pop_index)'s `None` is
510 /// the authoritative empty check).
511 ///
512 /// A `Relaxed` load is sufficient here because the result is explicitly
513 /// racy: no ordering is being promised, and a plain load touches nothing,
514 /// so the release-sequence invariant documented on the private `head`
515 /// field is untouched.
516 #[must_use]
517 pub fn is_empty(&self) -> bool {
518 TaggedIndex::<INDEX_BITS>::is_empty(self.head.load(Ordering::Relaxed))
519 }
520
521 /// Successful pushes this head can still accept before
522 /// [`push_index`](StackOps::push_index) starts refusing with
523 /// `Err(`[`TagExhausted`]`)`: `TaggedIndex::TAG_MAX - tag`. `0` means
524 /// sealed — every future push is refused (see "Sealing is permanent"
525 /// above).
526 ///
527 /// Advisory `Relaxed` load, same posture as [`is_empty`](Self::is_empty):
528 /// a concurrent push can make this stale the instant it returns. A
529 /// plain load touches nothing, so the release-sequence invariant on the
530 /// private `head` field (see its doc) is untouched.
531 #[must_use]
532 pub fn pushes_remaining(&self) -> u64 {
533 let (_, tag) = TaggedIndex::<INDEX_BITS>::unpack(self.head.load(Ordering::Relaxed));
534 TaggedIndex::<INDEX_BITS>::TAG_MAX - tag
535 }
536
537 /// **test-only** constructor seeding a specific tag, for a tiny-tag
538 /// regression oracle at the REAL tag width — never via a
539 /// `TAG_BITS`-reducing cfg (this crate's Option-4 tiny-tag oracle
540 /// convention). Builds fresh atomic storage directly
541 /// (`AtomicU64::new(..)`): this is INITIALISATION, not a plain `store`
542 /// on a live head, so the release-sequence invariant documented on the
543 /// private `head` field is untouched.
544 ///
545 /// `#[doc(hidden)]` + gated: same test-only-forwarder convention as
546 /// [`raw_head`](Self::raw_head) — see its rationale.
547 ///
548 /// Uses the CHECKED [`TaggedIndex::pack`], not the crate-private
549 /// truncating fast path: a test passing `tag > TAG_MAX` must get a loud
550 /// failure here, not a silently truncated (and therefore wrong) starting
551 /// tag that would make a test oracle pass or fail for the wrong reason.
552 ///
553 /// # Panics
554 /// Panics if `tag > `[`TaggedIndex::TAG_MAX`].
555 #[doc(hidden)]
556 #[cfg(any(tagged_index_stack_test, loom))]
557 #[must_use]
558 pub fn with_tag_for_test(tag: u64) -> Self {
559 Self {
560 head: AtomicU64::new(
561 TaggedIndex::<INDEX_BITS>::pack(TaggedIndex::<INDEX_BITS>::empty_index(), tag)
562 .expect("with_tag_for_test: tag out of range (tag > TaggedIndex::TAG_MAX)"),
563 ),
564 }
565 }
566
567 /// The raw packed head word (`Acquire`) — for this crate's own diagnostics
568 /// and tests only. The index half is a live top-of-stack index or
569 /// [`empty_index`](TaggedIndex::empty_index); the high bits are the running
570 /// tag. `Acquire` so a loom test that splits a pop's read from its CAS (to
571 /// open the ABA window) still forms the same happens-before edge the real
572 /// `pop_index`'s `Acquire` head load does.
573 ///
574 /// `#[doc(hidden)]` + gated (this project's established test-only surface
575 /// convention — every other `#[doc(hidden)]` item in this crate points
576 /// here for the generic rationale): this is a `pub` item only so
577 /// An internal test harness can reach it. Gated: compiled only under the
578 /// repository test cfg or a loom build — a default build (a downstream consumer, the docs.rs
579 /// render) does not contain this item at all, so unlike `#[doc(hidden)]`
580 /// alone the gate makes it genuinely unnameable from safe downstream
581 /// code, not merely hidden from rustdoc navigation. It is not exercised
582 /// by any production caller. It is an unstable repository-test surface;
583 /// enabling the test cfg is not a semver promise for these probes.
584 #[doc(hidden)]
585 #[cfg(any(tagged_index_stack_test, loom))]
586 #[must_use]
587 pub fn raw_head(&self) -> u64 {
588 self.head.load(Ordering::Acquire)
589 }
590
591 /// **loom-test-only** raw CAS on the head word, exposed so the loom model
592 /// can split a pop's head-load from its CAS —
593 /// opening the ABA window the real `pop_index` closes internally — and
594 /// drive the buggy-drain counterfactual, all against the REAL head atomic.
595 /// Not part of the stable API: it is compiled only under `--cfg loom`.
596 ///
597 /// `#[doc(hidden)]`: see [`raw_head`](StackHead::raw_head)'s
598 /// rationale. This item carries the strictly narrower `#[cfg(loom)]`
599 /// gate (vs `raw_head`'s test-cfg-or-loom gate), so it does not exist
600 /// at all outside a `--cfg loom` build.
601 ///
602 /// # Errors
603 ///
604 /// Forwards `AtomicU64::compare_exchange`'s `Err(actual)` on CAS failure.
605 #[cfg(loom)]
606 #[doc(hidden)]
607 pub fn cas_head_for_test(
608 &self,
609 current: u64,
610 new: u64,
611 success: Ordering,
612 failure: Ordering,
613 ) -> Result<u64, u64> {
614 self.head.compare_exchange(current, new, success, failure)
615 }
616}
617
618impl<const INDEX_BITS: u32> Default for StackHead<INDEX_BITS> {
619 fn default() -> Self {
620 Self::new()
621 }
622}
623
624/// One implementor supplies a [`StackHead`] and atomic per-index links. The
625/// head↔links binding is fixed by the implementor; links may be slot-resident
626/// or fused in [`ArrayIndexStack`]. The implementation must be non-blocking if
627/// the resulting [`StackOps`] operations are to remain lock-free.
628///
629/// # Safety
630///
631/// Implementing this trait is a soundness commitment. The implementor must:
632///
633/// 1. Bind each head to exactly one live implementor and one link backing for
634/// its whole life; never share or rebind it while an index is reachable.
635/// 2. Use one stable index↔cell mapping. After an `Acquire` observation of a
636/// head published by a `Release` push, `load_next` must observe that push's
637/// `store_next` or a later write in the cell's modification order, never an
638/// earlier write. A link cell may be mutated only by the stack-algorithm
639/// push that is about to publish its index through the binding currently
640/// receiving a valid, unique publish/recycle authority. That authority may
641/// be freshly issued or legitimately transferred by a successful pop from
642/// another binding sharing the cells. Later legitimate pop+repush operations
643/// may therefore write the same cell again. Out-of-band storage-owner,
644/// payload, direct, or forged writes are forbidden, even when they leave an
645/// acyclic, in-range chain.
646/// 3. Keep reachable index populations disjoint across bindings sharing link
647/// cells. Sharing cells with disjoint populations is allowed, and a popped
648/// index may be transferred from one binding to another before the receiving
649/// binding publishes it.
650/// 4. Return only [`TAIL`] or a valid index from a dedicated, non-payload-
651/// aliased link cell.
652/// 5. Return the same logical head from [`head`](Self::head) on every call.
653/// 6. Document a fixed link domain, a subset of `0 .. INDEX_MASK`, with a
654/// dedicated cell for every member. Hooks must be memory-safe in that
655/// domain; unchecked access outside it is permitted only because the
656/// caller contract guarantees the algorithm never supplies such an index.
657/// 7. Make every link-cell access atomic; a stale popper may read while a
658/// concurrent push writes and then lose its head CAS.
659///
660/// # Shared-storage hazard class: detection boundary
661///
662/// The inventory counts binding relationships, not storage values. Shared cells
663/// are valid when reachable populations stay disjoint; a successful pop may
664/// transfer an index's authority to another binding, which may then publish it.
665/// The forbidden shapes are:
666///
667/// | Shape | Forbidden arrangement | Detector |
668/// |---|---|---|
669/// | 1 | One binding reads and writes different backings. | May eventually trip the self-loop guard. |
670/// | 2 | Two live bindings share one head but use different backings. | May trip the self-loop guard; not structural. |
671/// | 3 | Bindings share cells while an index is reachable from both. | Can remain acyclic and silently double-issue. |
672/// | 4 | A live head is rebound over time to a different backing. | Can leak first, then trip the self-loop guard. |
673///
674/// These are implementor obligations, not a complete runtime detector; direct
675/// forged writes and deeper acyclic corruption can pass every guard.
676///
677/// # Ordering contract
678///
679/// `load_next` must use `Acquire` (or stronger), and `store_next` must use
680/// `Release` (or stronger). Head publication already carries the link's
681/// visibility, but these link orderings are deliberate defence-in-depth and
682/// keep an implementation independent of the stack's internal head orderings;
683/// see `docs/perf/TIS_LINK_ORDERING_WEAK_CAS_GATE.md` for the measured status.
684///
685/// The three hooks are `unsafe fn`: the compiler requires an unsafe call site.
686/// Their caller-side contracts are stated on the methods below; the
687/// implementation obligations above remain the unsafe impl's responsibility.
688/// The complete design rationale is in
689/// `docs/adr/2026-09-01-tagged-index-stack-storage-binding-closure.md`.
690///
691/// # Stability
692///
693/// This trait is intentionally open for external slot-resident implementations;
694/// future methods will have default bodies or require a major release.
695#[allow(unsafe_code)]
696// Unsafe: implementors bind one head to stable atomic link storage.
697pub unsafe trait StackStorage<const INDEX_BITS: u32> {
698 /// The stack's head word.
699 ///
700 /// # Safety
701 ///
702 /// The caller may use the returned reference only as this binding's head
703 /// and must not create a competing head↔links binding around it.
704 unsafe fn head(&self) -> &StackHead<INDEX_BITS>;
705
706 /// Load `index`'s next link with `Acquire` ordering.
707 ///
708 /// # Safety
709 ///
710 /// `index` must have been pushed through this exact binding at least once,
711 /// so its link cell was initialized by `store_next`. It need not remain
712 /// reachable: a concurrent popper may win before this caller's CAS.
713 unsafe fn load_next(&self, index: u32) -> u32;
714
715 /// Store `index`'s next link with `Release` ordering. This is the only
716 /// stack write to link storage, and it is lazy: links are written only
717 /// during a push immediately before that push's publishing CAS.
718 ///
719 /// # Safety
720 ///
721 /// The caller must be in the CAS-valid push phase: `index` satisfies
722 /// [`StackOps::push_index`]'s three caller obligations, including a unique
723 /// authority legitimately transferred from any binding sharing the cells;
724 /// `next` is [`TAIL`] or the index observed as this binding's head; and this
725 /// call is made by the stack algorithm immediately before the CAS that
726 /// publishes `index`. No storage-owner, payload, direct, or forged write
727 /// may substitute for this call.
728 unsafe fn store_next(&self, index: u32, next: u32);
729}
730
731/// The stack operations — [`push_index`](Self::push_index) /
732/// [`pop_index`](Self::pop_index) — blanket-implemented by the crate for every
733/// [`StackStorage`] implementor. Downstream impls are impossible (trait
734/// coherence: a second impl would conflict with this blanket), so the
735/// CAS-retry-loop bodies cannot be overridden or drifted from; an implementor
736/// controls only `head`/`load_next`/`store_next`.
737pub trait StackOps<const INDEX_BITS: u32>: StackStorage<INDEX_BITS> {
738 /// Push `index` onto the stack (classic Treiber push with a tag bump).
739 /// The current head index (or [`TAIL`] when empty) is stored in `index`'s
740 /// link with `Release`, then the head is CASed to `(index, tag + 1)`.
741 /// The numeric guard below is unconditional because an invalid index would
742 /// corrupt the free-list and could double-issue a slot.
743 ///
744 /// # Safety
745 ///
746 /// The caller must uphold all three clauses:
747 ///
748 /// 1. `index` is in this implementor's documented link domain: it has a
749 /// dedicated cell and the hooks are memory-safe for it. The runtime
750 /// `index < INDEX_MASK` guard checks only the packed-word range, not
751 /// this possibly narrower domain.
752 /// 2. `index` is not reachable through any binding whose hooks touch the
753 /// same link cells. It was never pushed, or its latest push was followed
754 /// by a successful [`pop_index`](Self::pop_index) returning it from any
755 /// such binding. A stale popper that observed the index but lost its CAS
756 /// did not pop it and does not block this push; the tag makes that stale
757 /// CAS fail, and this push overwrites the old link before publishing the
758 /// index through the receiving binding.
759 /// 3. This call owns a unique, unconsumed publish/recycle authority: either
760 /// a fresh index or one legitimately transferred by a specific
761 /// successful pop, including a pop through another binding sharing the
762 /// cells. The authority is consumed at this call's successful head CAS,
763 /// not at physical return, so a later popper may republish the index with
764 /// its own authority before this call returns. Two pushes may not consume
765 /// the same authority without an intervening successful pop. These
766 /// liveness and authority obligations are not runtime-checked; violating
767 /// them can create a cycle or double-issue an index.
768 ///
769 /// # Errors
770 ///
771 /// Returns `Err(`[`TagExhausted`]`)` without publishing when the current tag
772 /// is [`TaggedIndex::TAG_MAX`]. The head is then permanently sealed; the
773 /// refused index remains the caller's. A retry may have left stale link
774 /// contents, which the next successful push overwrites before publishing.
775 ///
776 /// # Panics
777 ///
778 /// Panics if `index >= INDEX_MASK` (the empty sentinel is reserved), in both
779 /// debug and release. The implementor's link hooks may enforce a narrower
780 /// bound; callers must satisfy that bound too.
781 #[track_caller]
782 #[allow(unsafe_code)]
783 // Single documented reason to hold `unsafe`: this method carries the
784 // caller-side three-clause unsafe contract (link domain + liveness +
785 // exclusive ownership), relied on for memory safety by allocator
786 // consumers; see the `# Safety` section above.
787 unsafe fn push_index(&self, index: u32) -> Result<(), TagExhausted>;
788
789 /// Pop the top index off the stack, or `None` if empty.
790 /// Loads the tagged head, reads its next link, and CASes the head to that
791 /// link without changing the tag. A popper that loses its CAS retries with
792 /// an `Acquire` observation; the monotonic, sealing tag prevents ABA.
793 ///
794 /// When the popped element is the last one (`next == TAIL`), the empty
795 /// sentinel keeps the observed running tag rather than resetting to zero.
796 /// On a lost CAS whose `actual` head is already empty, retry backoff is
797 /// skipped because the next iteration returns `None` (see the configured
798 /// backoff cap).
799 ///
800 /// `load_next` is reached through this implementor's binding, whose head is
801 /// read once for the whole CAS loop; see [`StackStorage`].
802 ///
803 /// # Panics
804 ///
805 /// Panics if `load_next` returns neither [`TAIL`] nor an index below
806 /// `INDEX_MASK`, or returns the popped index itself. The release-active
807 /// guard prevents `pack_truncating` from turning an invalid value into a
808 /// wrong live index or the empty sentinel. A self-loop indicates a caller
809 /// contract violation; the guard is a detector, not a repair. Dedicated
810 /// link storage is required because a stale popper may read a link after
811 /// another thread has popped the index, and payload-aliasing could produce
812 /// an arbitrary in-range value that passes this guard silently.
813 ///
814 /// The implementor's `load_next` may also panic on its own narrower domain
815 /// bound; see [`StackOps::push_index`]'s `# Panics`.
816 #[must_use = "a popped index is removed from the free-list; discarding it leaks the slot"]
817 #[track_caller]
818 fn pop_index(&self) -> Option<u32>;
819}
820
821/// The crate-INTERNAL accessor shape — the head plus the link hooks — that
822/// the shared CAS-retry algorithm (`push_index_impl`/`pop_index_impl` below)
823/// is written against: `head()` + `load_next`/`store_next`, the same three
824/// signatures as [`StackStorage`]'s.
825///
826/// Sealed BY CONSTRUCTION: `pub(crate)` means this trait can be neither named
827/// nor implemented outside this crate, so no downstream impl can ever exist.
828/// This is NOT an extension point — the public extension point remains
829/// [`StackStorage`]. Its in-crate implementors are [`ArrayIndexStack`]
830/// (directly, below) and every [`StackStorage`] implementor (via the blanket
831/// bridge impl). The whole point: [`ArrayIndexStack`] stops implementing the
832/// PUBLIC trait — so its head becomes unreachable from outside — while the
833/// algorithm body stays written exactly once.
834///
835/// All three hooks are `unsafe fn`: the bridge forwards them verbatim, and
836/// the algorithm supplies the caller-side proofs at its call sites.
837// Unsafe: internal hooks may rely on caller-proved link-domain invariants.
838#[allow(unsafe_code)]
839pub(crate) trait SealedStorage<const B: u32> {
840 /// # Safety
841 ///
842 /// Same contract as [`StackStorage::head`].
843 unsafe fn head(&self) -> &StackHead<B>;
844 /// # Safety
845 ///
846 /// Same contract as [`StackStorage::load_next`].
847 unsafe fn load_next(&self, index: u32) -> u32;
848 /// # Safety
849 ///
850 /// Same contract as [`StackStorage::store_next`]'s `# Safety` — see
851 /// there (one normative location; this crate cross-references it).
852 unsafe fn store_next(&self, index: u32, next: u32);
853}
854
855/// Bridge every public [`StackStorage`] implementor to the internal
856/// [`SealedStorage`] algorithm. Calls are qualified because both traits
857/// declare methods with the same shapes.
858// Unsafe: forwards the public storage hooks without changing their contracts.
859#[allow(unsafe_code)]
860impl<const B: u32, S: StackStorage<B> + ?Sized> SealedStorage<B> for S {
861 unsafe fn head(&self) -> &StackHead<B> {
862 // SAFETY: `S: StackStorage<B>` means an `unsafe impl` asserted the
863 // implementor contract for this binding. The stack algorithm calls
864 // `head()` exactly once per operation and uses the returned
865 // reference only as THIS binding's head — never building a second,
866 // competing binding around it — discharging
867 // [`StackStorage::head`]'s caller-side contract.
868 unsafe { StackStorage::head(self) }
869 }
870 unsafe fn load_next(&self, index: u32) -> u32 {
871 // SAFETY: the pop algorithm calls this only on an index unpacked
872 // from a head word observed through THIS binding's `head()`; such
873 // an index was pushed through this binding at least once (the push
874 // that published it initialised its link cell via `store_next`),
875 // which is exactly [`StackStorage::load_next`]'s caller-side
876 // contract — it does NOT require the index to still be reachable,
877 // and it may not be (a concurrent popper can win the CAS first;
878 // this caller's CAS then fails and retries).
879 unsafe { StackStorage::load_next(self, index) }
880 }
881 unsafe fn store_next(&self, index: u32, next: u32) {
882 // SAFETY: the proof lives at the sole caller, `push_index_impl` —
883 // this bridge cannot locally verify the push phase/liveness
884 // obligations.
885 unsafe { StackStorage::store_next(self, index, next) }
886 }
887}
888
889/// The push CAS-retry algorithm, written once against [`SealedStorage`] —
890/// the body of [`StackOps::push_index`], which remains the documented public
891/// surface (see its doc for the algorithm, its `# Safety` section (the caller-side
892/// contract) and `# Panics`). [`ArrayIndexStack`]'s inherent `push` calls this directly,
893/// off the public trait plumbing.
894///
895/// # Safety
896///
897/// Same caller-side contract as [`StackOps::push_index`]'s `# Safety` —
898/// the normative location, which this crate cross-references here. This
899/// function is the shared body behind both [`StackOps::push_index`] and
900/// [`ArrayIndexStack::push`]; its caller must discharge the link-domain,
901/// liveness, and exclusive-ownership clauses.
902#[track_caller]
903#[allow(unsafe_code)]
904// Unsafe: the caller supplies exclusive publish authority for `index`.
905pub(crate) unsafe fn push_index_impl<const B: u32, S: SealedStorage<B> + ?Sized>(
906 s: &S,
907 index: u32,
908) -> Result<(), TagExhausted> {
909 let mask = TaggedIndex::<B>::INDEX_MASK;
910 if u64::from(index) >= mask {
911 push_index_out_of_range(index, mask);
912 }
913 // `head()` is read exactly once per operation — see StackStorage's clause
914 // 5: use the same logical head for the operation.
915 // SAFETY: this operation uses one stable binding's head exactly once;
916 // the caller forwarded `StackStorage::head`'s binding contract.
917 let head_ref: &StackHead<B> = unsafe { s.head() };
918 // `Relaxed`, not `Acquire`: push uses the observed word ONLY as
919 // `(index, tag)` values — it never follows a link through it. Whatever
920 // a concurrent popper must observe of this push is published by the
921 // Release SUCCESS CAS and recovered by the popper's OWN Acquire head
922 // observation, never by anything this load orders. `pop_index`'s
923 // initial load below MUST stay `Acquire` — pop DOES follow a link from
924 // the observed word. The loom suite passes with exactly this ordering.
925 let mut head = head_ref.load(Ordering::Relaxed);
926 let mut backoff = Backoff::new();
927 loop {
928 // Unpack the current head once: the index half chains this push to
929 // the top of the stack (below), the tag half feeds the ABA bump.
930 let (cur_idx, tag) = TaggedIndex::<B>::unpack(head);
931 // Seal check: refuse rather than wrap the tag to 0 (a wrapped tag
932 // would re-issue a head word a parked popper may still hold as its
933 // CAS expectation) — BEFORE any side effect, so a first-attempt
934 // refusal touches nothing; see `StackOps::push_index`'s `# Errors`.
935 if tag == TaggedIndex::<B>::TAG_MAX {
936 return Err(TagExhausted);
937 }
938 // The link this index chains to: the current head's index, or TAIL
939 // if the stack is empty. On an empty head the observed index half
940 // IS the sentinel `INDEX_MASK` (which `_CHECK_BITS` keeps <= 0xFFFF
941 // at every legal width), NOT `TAIL` — so the branch mapping it to
942 // `TAIL` is semantically REQUIRED, not a readability choice:
943 // without it an empty-head push would store `INDEX_MASK` as the
944 // link, and the next pop would panic on it at the clause-4 guard
945 // below (neither TAIL nor a valid index).
946 let next_link = if cur_idx == TaggedIndex::<B>::empty_index() {
947 TAIL
948 } else {
949 cur_idx
950 };
951 // Write the link under Release so a concurrent pop's Acquire read of
952 // this slot's link (after observing it as head) sees it. This is the
953 // ONLY link write — never an eager init — and it may run
954 // more than once: on a CAS failure the NEXT iteration recomputes
955 // `next_link` from the fresh head and OVERWRITES this same link
956 // cell before its own CAS. The stale write from the failed
957 // iteration is never observable in the stack's read-set, for two
958 // disjoint reasons — the normative retry-overwrite proof (the
959 // SAFETY comment below cross-references it instead of repeating):
960 //
961 // (a) An ordinary pop cannot select `index`'s link cell at all
962 // while this push is mid-retry. Under the liveness and
963 // exclusive-ownership clauses of the caller-side push
964 // contract, the `index` this push is publishing is
965 // UNREACHABLE — part of no live chain — until this push's CAS
966 // successfully publishes it, so no pop's traversal reaches
967 // `index` before publication, stale-read or not. On the
968 // losing-CAS path described here, this push's CAS has
969 // displaced nothing.
970 //
971 // (b) A pop that DOES read `index`'s link cell mid-retry must be a
972 // stale popper from a PRIOR push/pop lifecycle of this same
973 // `index` (not this push). That stale popper's own CAS
974 // expectation is the old `(index, tag)` head word from that
975 // prior cycle — already displaced by whichever pop won the
976 // head CAS and transferred ownership of `index` to ITS
977 // caller. The tag is strictly monotonic (it never wraps — see
978 // the crate-root docs' "Tag-width budget" section), so that
979 // stale expected value can never be
980 // reinstalled: the stale popper's own CAS is guaranteed to
981 // fail regardless of what THIS push stores to the link cell
982 // or does with the head word.
983 //
984 // SAFETY: `next_link` is `TAIL` or the observed head index, and the
985 // publishing CAS follows this store. The caller forwarded the
986 // link-domain, liveness, and exclusive-ownership proof; the retry
987 // overwrite argument above covers stale writes.
988 unsafe {
989 s.store_next(index, next_link);
990 }
991 // Plain `+`: the seal check above ran before this bump, so
992 // tag < TAG_MAX and the addition cannot overflow.
993 let new_tag = tag + 1;
994 let new_head = TaggedIndex::<B>::pack_truncating(index, new_tag);
995 // Release on success so a pop's Acquire sees the link we wrote.
996 // Relaxed on failure is sound HERE, and the asymmetry with pop is
997 // deliberate: a failed CAS sends push around the loop with the
998 // value it read used ONLY as a value — push never follows a link
999 // through that read, so the read carries no ordering burden. pop
1000 // is NOT symmetric: its retry's re-read names the index whose link
1001 // load_next will consult next, so pop's failure ordering MUST
1002 // stay Acquire (the loom counterfactual
1003 // `counterfactual_relaxed_cas_failure_corrupts_free_list` proves
1004 // Relaxed corrupts; the end-to-end guard is
1005 // `pop_retry_after_failed_cas_sees_concurrent_pushs_link_real_type`).
1006 // The happens-before edge a popper needs from this push is carried
1007 // by the Release success CAS's own release sequence — extended by
1008 // every later head RMW (see the `head` field's INVARIANT) — never
1009 // by anything push's failed-CAS reads observe.
1010 // Strong, not weak: codegen-identical on every measured lowering —
1011 // see `TIS_LINK_ORDERING_WEAK_CAS_GATE.md`, "Codegen matrix and observations".
1012 match head_ref.compare_exchange(head, new_head, Ordering::Release, Ordering::Relaxed) {
1013 Ok(_) => return Ok(()),
1014 Err(actual) => {
1015 note_push_retry();
1016 head = actual;
1017 backoff.spin();
1018 }
1019 }
1020 }
1021}
1022
1023/// The pop CAS-retry algorithm, written once against [`SealedStorage`] —
1024/// the body of [`StackOps::pop_index`], which remains the documented public
1025/// surface (see its doc for the algorithm and `# Panics`).
1026/// [`ArrayIndexStack`]'s inherent `pop` calls this directly, off the public
1027/// trait plumbing.
1028// Unsafe: the head observation proves the link hook's read precondition.
1029#[allow(unsafe_code)]
1030#[track_caller]
1031pub(crate) fn pop_index_impl<const B: u32, S: SealedStorage<B> + ?Sized>(s: &S) -> Option<u32> {
1032 // `head()` is read exactly once per operation — see StackStorage's clause
1033 // 5: use the same logical head for the operation.
1034 // SAFETY: this operation uses one stable binding's head exactly once;
1035 // the caller forwarded `StackStorage::head`'s binding contract.
1036 let head_ref: &StackHead<B> = unsafe { s.head() };
1037 let mut head = head_ref.load(Ordering::Acquire);
1038 let mut backoff = Backoff::new();
1039 loop {
1040 if TaggedIndex::<B>::is_empty(head) {
1041 return None;
1042 }
1043 let (index, tag) = TaggedIndex::<B>::unpack(head);
1044 // Read the next link Before the CAS (the push stored it under
1045 // Release; our Acquire observation of head — whether from the
1046 // initial load OR from a retry CAS failure — synchronizes with it).
1047 // SAFETY: `index` came from this binding's Acquire head observation;
1048 // its link was initialized by the publishing push, even if this
1049 // operation later loses its CAS.
1050 let next = unsafe { s.load_next(index) };
1051 // Unconditional guard (release-active, mirroring push's
1052 // `index < INDEX_MASK` check) for clause 4 of the StackStorage
1053 // implementor contract: pack_truncating() below would silently
1054 // truncate a bad value to a wrong (possibly still-live) index or
1055 // to the empty sentinel. The guard is not separately measured.
1056 // See `# Panics` above.
1057 // The self-loop and truncation meanings are defined in `# Panics`
1058 // above; the shared-storage catch boundary is in `StackStorage`.
1059 let mask = TaggedIndex::<B>::INDEX_MASK;
1060 if next != TAIL && (u64::from(next) >= mask || next == index) {
1061 pop_link_out_of_range(index, next, mask);
1062 }
1063 let new_head = if next == TAIL {
1064 // Preserve the running tag across the empty transition.
1065 TaggedIndex::<B>::pack_truncating(TaggedIndex::<B>::empty_index(), tag)
1066 } else {
1067 TaggedIndex::<B>::pack_truncating(next, tag)
1068 };
1069 // Acquire on success with NO Release half is sound ONLY because
1070 // every write to `head` is an RMW: this CAS stays inside the
1071 // release sequence headed by the push that `Release`d the link
1072 // being handed out, so our own write need not head one. See the
1073 // INVARIANT on the `head` field — a plain `store` there would
1074 // sever that sequence and make this ordering unsound.
1075 // The success/failure asymmetry with push's `Release`/`Relaxed`
1076 // CAS is deliberate and explained from push's side in
1077 // `push_index_impl`'s CAS comment (why push's failure ordering is
1078 // `Relaxed` while pop's must stay `Acquire` — pop follows a link
1079 // on retry, push does not).
1080 // Strong CAS over `compare_exchange_weak` — measured
1081 // codegen-identical on aarch64 (see push's CAS note:
1082 // `docs/perf/TIS_LINK_ORDERING_WEAK_CAS_GATE.md`'s
1083 // "Codegen matrix and observations" section).
1084 match head_ref.compare_exchange(head, new_head, Ordering::Acquire, Ordering::Acquire) {
1085 Ok(_) => return Some(index),
1086 Err(actual) => {
1087 note_pop_retry();
1088 head = actual;
1089 // Skipped when the lost CAS reveals the stack just went
1090 // empty: the top-of-loop `is_empty` check returns `None`
1091 // next iteration regardless, so spinning here is pure
1092 // wasted latency; which outcome a call eventually returns
1093 // is unchanged, only how fast it gets there.
1094 if !TaggedIndex::<B>::is_empty(actual) {
1095 backoff.spin();
1096 }
1097 }
1098 }
1099 }
1100}
1101
1102// Unsafe: exposes and forwards `push_index`'s caller-side contract.
1103#[allow(unsafe_code)]
1104impl<const B: u32, S: StackStorage<B> + ?Sized> StackOps<B> for S {
1105 #[track_caller]
1106 unsafe fn push_index(&self, index: u32) -> Result<(), TagExhausted> {
1107 // SAFETY: this fn's own caller-side contract (link domain +
1108 // liveness + exclusive ownership, `push_index`'s `# Safety` above)
1109 // is forwarded verbatim
1110 // to `push_index_impl`'s identical `# Safety` contract — not
1111 // discharged locally, just passed through.
1112 unsafe { push_index_impl::<B, S>(self, index) }
1113 }
1114
1115 #[track_caller]
1116 fn pop_index(&self) -> Option<u32> {
1117 pop_index_impl::<B, S>(self)
1118 }
1119}
1120
1121/// Cold panic path for [`StackOps::push_index`]'s `index < INDEX_MASK`
1122/// caller-contract guard, split out of `push_index` itself so the panic and
1123/// its message formatting can never land in the hot loop's body (`#[cold]` +
1124/// `#[inline(never)]`). `#[track_caller]` here — combined with
1125/// `#[track_caller]` on `push_index` — forwards `push_index`'s received
1126/// caller location down, so a consumer pushing from many call sites learns
1127/// WHICH one violated the contract.
1128#[cold]
1129#[inline(never)]
1130#[track_caller]
1131fn push_index_out_of_range(index: u32, mask: u64) -> ! {
1132 panic!(
1133 "index must be < INDEX_MASK (the empty sentinel is reserved), \
1134 got {index} (INDEX_MASK = {mask:#x})"
1135 );
1136}
1137
1138/// Cold panic path for [`ArrayLinks`] index bounds, with a crate-owned
1139/// diagnostic shared by its load and store methods.
1140#[cold]
1141#[inline(never)]
1142#[track_caller]
1143fn array_links_out_of_range(index: u32, capacity: usize) -> ! {
1144 panic!("ArrayLinks index out of bounds: index {index} >= capacity {capacity}");
1145}
1146
1147/// Cold panic path for [`StackOps::pop_index`]'s clause-4 guard, split out of
1148/// `pop_index` itself — same `#[cold]` + `#[inline(never)]` +
1149/// `#[track_caller]` shape and caller-location-chaining rationale as
1150/// [`push_index_out_of_range`] above. Reports which of the three caught
1151/// shapes the caller's [`load_next`](StackStorage::load_next) answer has
1152/// — a self-loop or one of the two truncation outcomes an over-wide value
1153/// would silently produce. See `pop_index`'s `# Panics` for the self-loop
1154/// causes.
1155#[cold]
1156#[inline(never)]
1157#[track_caller]
1158fn pop_link_out_of_range(index: u32, next: u32, mask: u64) -> ! {
1159 if next == index {
1160 panic!(
1161 "load_next({index}) returned {next:#x}, the index's own link points \
1162 back to itself — a self-loop, corrupting the free-list into a cycle: \
1163 pop_index's truncating pack would silently re-issue this same index \
1164 to a second owner"
1165 );
1166 }
1167 let outcome = if (u64::from(next) & mask) == mask {
1168 "the EMPTY SENTINEL, leaking the whole remaining chain"
1169 } else {
1170 "a wrong index, possibly a live one — double-issuing it"
1171 };
1172 panic!(
1173 "load_next({index}) returned {next:#x}, neither TAIL nor \
1174 a valid index (< {mask:#x}): pop_index's truncating pack would silently \
1175 truncate it to {outcome}"
1176 );
1177}
1178
1179/// An owned standalone stack: head and links fused into one object. A
1180/// lock-free LIFO free-list of indices with a STRICTLY MONOTONIC generation
1181/// tag packed into the head word that ELIMINATES ABA outright at every
1182/// permitted `INDEX_BITS` — it never wraps; a push that would need to bump
1183/// the tag past [`TaggedIndex::TAG_MAX`] is refused instead
1184/// (`Err(`[`TagExhausted`]`)`), sealing the stack (pops are unaffected and
1185/// keep draining). The pushes-until-sealed lifetime is derived in the
1186/// crate-root docs' "Tag-width budget" section. Const-generic over the
1187/// index width `INDEX_BITS` and the link capacity `N`.
1188///
1189/// Fusion is ALSO the structural closure of the shared-head hazard: this
1190/// type deliberately does NOT implement the public [`StackStorage`] trait
1191/// (its head↔links binding is served by a crate-internal sealed accessor
1192/// instead), its `head` field is private, and no trait impl hands out a
1193/// `&StackHead` for it — so the public API cannot construct a competing
1194/// binding around a standalone `ArrayIndexStack`. The seal is
1195/// instantiation-independent: the private head and the crate's coherence
1196/// boundary enforce it for every `INDEX_BITS, N`. The remaining binding
1197/// obligations apply to custom [`StackStorage`] implementors and are stated
1198/// in that trait's `# Safety` contract.
1199///
1200/// The simple [`push`](Self::push)/[`pop`](Self::pop) inherent methods exist
1201/// for standalone callers (`push` is an `unsafe fn`, carrying
1202/// [`StackOps::push_index`]'s `# Safety` contract); a fresh stack is EMPTY (lazy links) — the
1203/// caller pushes indices as they become free. Custom implementors with
1204/// slot-resident links do not use this type: they implement [`StackStorage`]
1205/// instead and call the [`StackOps`] methods. `N` is constrained at
1206/// construction to `N <= TaggedIndex::<INDEX_BITS>::INDEX_MASK`; link access
1207/// still checks `index < N`, so a caller must stay inside the owned domain.
1208#[derive(Debug)]
1209pub struct ArrayIndexStack<const INDEX_BITS: u32, const N: usize> {
1210 head: StackHead<INDEX_BITS>,
1211 links: ArrayLinks<N>,
1212}
1213
1214impl<const B: u32, const N: usize> ArrayIndexStack<B, N> {
1215 const _CHECK_N: () = assert!(
1216 N as u64 <= TaggedIndex::<B>::INDEX_MASK,
1217 "ArrayIndexStack capacity N must be <= INDEX_MASK"
1218 );
1219
1220 /// A fresh, EMPTY stack (head = the bootstrap empty sentinel, tag 0; every
1221 /// link at `0`). Under `--cfg loom` this cannot be `const` (loom's atomics
1222 /// have no `const` ctor).
1223 #[cfg(not(loom))]
1224 #[must_use]
1225 pub const fn new() -> Self {
1226 let () = Self::_CHECK_N;
1227 Self {
1228 head: StackHead::new(),
1229 links: ArrayLinks::new(),
1230 }
1231 }
1232
1233 /// A fresh, EMPTY stack (loom build — non-`const`).
1234 #[cfg(loom)]
1235 #[must_use]
1236 pub fn new() -> Self {
1237 let () = Self::_CHECK_N;
1238 Self {
1239 head: StackHead::new(),
1240 links: ArrayLinks::new(),
1241 }
1242 }
1243
1244 /// Push `index` onto the stack, driving the crate-internal CAS-retry
1245 /// algorithm (`push_index_impl`) directly. This type deliberately does
1246 /// NOT implement the public [`StackStorage`] trait (see the type doc), so
1247 /// it does not go through [`StackOps::push_index`]'s blanket impl — the
1248 /// identical algorithm body is crate-internal. See
1249 /// [`StackOps::push_index`]'s doc for the algorithm, its `# Safety`
1250 /// section (the caller contract) and `# Panics`.
1251 ///
1252 /// # Safety
1253 ///
1254 /// Same contract as [`StackOps::push_index`]'s `# Safety` — see there
1255 /// (one normative location; this crate cross-references it).
1256 ///
1257 /// # Errors
1258 ///
1259 /// Same as [`StackOps::push_index`]'s `# Errors` — see there.
1260 // `#[track_caller]` chains the caller location through the forwarder down
1261 // to `push_index_impl` and its `#[cold]` panic helper, so diagnostics through
1262 // the owned type name the user's call site exactly as the trait method does.
1263 #[track_caller]
1264 #[allow(unsafe_code)]
1265 // Single documented reason to hold `unsafe`: forwards
1266 // `StackOps::push_index`'s caller-side unsafe contract (link domain +
1267 // liveness + exclusive ownership) to the shared body `push_index_impl`.
1268 pub unsafe fn push(&self, index: u32) -> Result<(), TagExhausted> {
1269 // SAFETY: forwards this fn's own caller-side contract (link domain +
1270 // liveness + exclusive ownership, same as `StackOps::push_index`'s
1271 // `# Safety`) verbatim to
1272 // `push_index_impl` — not discharged locally, just passed through.
1273 unsafe { push_index_impl::<B, _>(self, index) }
1274 }
1275
1276 /// Pop the top index off the stack, or `None` if empty — driving the
1277 /// crate-internal CAS-retry algorithm (`pop_index_impl`) directly. This
1278 /// type deliberately does NOT implement the public [`StackStorage`] trait
1279 /// (see the type doc), so it does not go through
1280 /// [`StackOps::pop_index`]'s blanket impl — the identical algorithm body
1281 /// is crate-internal. See [`StackOps::pop_index`]'s doc for the
1282 /// algorithm and `# Panics`.
1283 #[must_use = "a popped index is removed from the free-list; discarding it leaks the slot"]
1284 // `#[track_caller]` chains the caller location through the forwarder down
1285 // to `pop_index_impl` and its `#[cold]` panic helper, so diagnostics through
1286 // the owned type name the user's call site exactly as the trait method does.
1287 #[track_caller]
1288 pub fn pop(&self) -> Option<u32> {
1289 pop_index_impl::<B, _>(self)
1290 }
1291
1292 /// Whether the stack is currently empty. Advisory `Relaxed` check — see
1293 /// [`StackHead::is_empty`].
1294 #[must_use]
1295 pub fn is_empty(&self) -> bool {
1296 self.head.is_empty()
1297 }
1298
1299 /// Successful pushes this head can still accept before `push` starts
1300 /// refusing with `Err(`[`TagExhausted`]`)` — forwarder to
1301 /// [`StackHead::pushes_remaining`].
1302 #[must_use]
1303 pub fn pushes_remaining(&self) -> u64 {
1304 self.head.pushes_remaining()
1305 }
1306
1307 /// The raw packed head word (`Acquire`) — forwarder to
1308 /// [`StackHead::raw_head`] (the internal model and unit harnesses need it).
1309 ///
1310 /// Gated: same test-cfg/loom gate as [`StackHead::raw_head`] —
1311 /// it does not exist in a default build.
1312 #[doc(hidden)]
1313 #[cfg(any(tagged_index_stack_test, loom))]
1314 #[must_use]
1315 pub fn raw_head(&self) -> u64 {
1316 self.head.raw_head()
1317 }
1318
1319 /// **loom-test-only** raw CAS on the head word — forwarder to
1320 /// [`StackHead::cas_head_for_test`].
1321 ///
1322 /// # Errors
1323 ///
1324 /// Forwards `AtomicU64::compare_exchange`'s `Err(actual)` on CAS failure.
1325 #[cfg(loom)]
1326 #[doc(hidden)]
1327 pub fn cas_head_for_test(
1328 &self,
1329 current: u64,
1330 new: u64,
1331 success: Ordering,
1332 failure: Ordering,
1333 ) -> Result<u64, u64> {
1334 self.head.cas_head_for_test(current, new, success, failure)
1335 }
1336
1337 /// **test-only** read-only link forwarder — loads index `index`'s
1338 /// link cell (`Acquire`), forwarding to [`ArrayLinks::load_next`].
1339 /// Internal model and unit harnesses read links directly from the REAL
1340 /// [`ArrayIndexStack`] (including a split pop and a never-pushed link probe):
1341 /// this type does not implement the public [`StackStorage`] trait, so
1342 /// [`StackStorage::load_next`] cannot reach its links.
1343 /// `#[doc(hidden)]` per the crate's established test-only-forwarder
1344 /// rationale (see [`raw_head`] and [`cas_head_for_test`]): not part of the
1345 /// stable API. Gated: same test-cfg/loom gate as [`StackHead::raw_head`]
1346 /// — it does not exist in a default build. Read-only — it exposes no
1347 /// `&StackHead` and no link write, so it reopens none of the sealed
1348 /// hazard.
1349 #[doc(hidden)]
1350 #[cfg(any(tagged_index_stack_test, loom))]
1351 pub fn load_next_for_test(&self, index: u32) -> u32 {
1352 self.links.load_next(index)
1353 }
1354
1355 /// **test-only** write-side twin of [`load_next_for_test`] — stores
1356 /// `next` into `index`'s link cell directly
1357 /// ([`ArrayLinks::store_next`], `Release`), bypassing the stack
1358 /// algorithm entirely. Needed for a hand-inlined counterfactual that
1359 /// reproduces the tag-wrap behaviour the seal forbids, without going
1360 /// through the real [`push`](Self::push) — the loom-only tiny-tag
1361 /// counterfactual uses it.
1362 ///
1363 /// `#[doc(hidden)]` per this crate's established test-only-forwarder
1364 /// rationale (see [`raw_head`]). Gated: `loom` only. This is a raw
1365 /// link-cell write that bypasses the stack algorithm and is therefore
1366 /// unsafe; it exists only to construct the seal counterfactual's wrapped
1367 /// publish. It is not a legal storage operation.
1368 ///
1369 /// # Safety
1370 ///
1371 /// The caller must provide an in-domain `index` and use this only as the
1372 /// loom counterfactual's deliberate stand-in for a stack-algorithm push,
1373 /// immediately followed by the matching raw head CAS. It must not be used
1374 /// by production code or to forge a live link outside that model.
1375 #[doc(hidden)]
1376 #[cfg(loom)]
1377 #[allow(unsafe_code)]
1378 pub unsafe fn store_next_for_test(&self, index: u32, next: u32) {
1379 self.links.store_next(index, next);
1380 }
1381
1382 /// **test-only** constructor seeding a specific tag — forwarder to
1383 /// [`StackHead::with_tag_for_test`] (see its doc, including its `# Panics`
1384 /// contract for an out-of-range tag).
1385 ///
1386 /// `#[doc(hidden)]` + gated: same test-only-forwarder convention as
1387 /// [`raw_head`].
1388 ///
1389 /// # Panics
1390 /// Panics if `tag > `[`TaggedIndex::TAG_MAX`].
1391 #[doc(hidden)]
1392 #[cfg(any(tagged_index_stack_test, loom))]
1393 #[must_use]
1394 pub fn with_tag_for_test(tag: u64) -> Self {
1395 let () = Self::_CHECK_N;
1396 Self {
1397 head: StackHead::with_tag_for_test(tag),
1398 links: ArrayLinks::new(),
1399 }
1400 }
1401}
1402
1403impl<const B: u32, const N: usize> Default for ArrayIndexStack<B, N> {
1404 fn default() -> Self {
1405 Self::new()
1406 }
1407}
1408
1409// Unsafe: the owned head and links form one fixed in-domain binding.
1410#[allow(unsafe_code)]
1411impl<const B: u32, const N: usize> SealedStorage<B> for ArrayIndexStack<B, N> {
1412 unsafe fn head(&self) -> &StackHead<B> {
1413 &self.head
1414 }
1415 unsafe fn load_next(&self, index: u32) -> u32 {
1416 self.links.load_next(index)
1417 }
1418 unsafe fn store_next(&self, index: u32, next: u32) {
1419 self.links.store_next(index, next)
1420 }
1421}
1422
1423/// An owned `[AtomicU32; N]` link backing (used inside the fused
1424/// [`ArrayIndexStack`]; slot-resident implementors host their own links
1425/// instead). Every link starts at `0` — matching OS-zeroed backing — and is
1426/// only ever written by a push (no eager free-list chaining).
1427///
1428/// # Layout note — link-array false sharing
1429///
1430/// Each link is a 4-byte `AtomicU32`, so 16 consecutive indices share one
1431/// 64-byte cache line. If indices from the same 16-index group are handed to
1432/// different threads under contention, this array becomes a SECOND contended
1433/// surface alongside the stack's own head — contended by accident of index
1434/// numbering, not by design. Fix it at the CALLER when a profile shows it:
1435/// wrap the index-to-link mapping so contended indices land in different
1436/// groups, use a `#[repr(align(64))]` newtype per link, or — this crate's own
1437/// README's recommendation for production — host links slot-resident inside a
1438/// larger per-slot struct. Do NOT pad `ArrayLinks` itself to one link per
1439/// cache line: that would multiply its footprint 16x for every
1440/// single-threaded (or contention-indifferent) caller.
1441#[derive(Debug)]
1442pub struct ArrayLinks<const N: usize> {
1443 next: [AtomicU32; N],
1444}
1445
1446impl<const N: usize> ArrayLinks<N> {
1447 /// Construct `N` links, every one at `0`. NOT a bulk free-list init — links
1448 /// only become meaningful once their index is pushed. Under
1449 /// `--cfg loom` this cannot be `const` (loom's atomics have no `const` ctor).
1450 #[cfg(not(loom))]
1451 #[must_use]
1452 pub const fn new() -> Self {
1453 Self {
1454 next: [const { AtomicU32::new(0) }; N],
1455 }
1456 }
1457
1458 /// Construct `N` links, every one at `0` (loom build — non-`const`).
1459 #[cfg(loom)]
1460 #[must_use]
1461 pub fn new() -> Self {
1462 Self {
1463 next: core::array::from_fn(|_| AtomicU32::new(0)),
1464 }
1465 }
1466
1467 /// Load the "next" link for `index` with `Acquire` ordering.
1468 ///
1469 /// This `Acquire` — and [`store_next`](Self::store_next)'s `Release` — is
1470 /// deliberately retained rather than weakened to `Relaxed`, which the
1471 /// stack's own head-publication proof would permit: defence-in-depth, see
1472 /// [`StackStorage`]'s "Ordering contract".
1473 ///
1474 /// # Panics
1475 ///
1476 /// Panics if `index >= N`; the owned stack also checks this backing bound
1477 /// after its head-word range check.
1478 #[must_use]
1479 #[track_caller]
1480 pub fn load_next(&self, index: u32) -> u32 {
1481 if index as usize >= N {
1482 array_links_out_of_range(index, N);
1483 }
1484 self.next[index as usize].load(Ordering::Acquire)
1485 }
1486
1487 /// Store the "next" link for `index` with `Release` ordering. This is the
1488 /// ONLY write the stack makes to link storage, and only during a push — the
1489 /// lazy-link discipline: link storage is never eagerly initialised.
1490 /// Like [`load_next`](Self::load_next)'s `Acquire`, this `Release` is
1491 /// deliberate defence-in-depth, not a stack-proof requirement — see
1492 /// [`StackStorage`]'s "Ordering contract".
1493 ///
1494 /// # Panics
1495 ///
1496 /// Panics if `index >= N` — the same bound as
1497 /// [`load_next`](Self::load_next).
1498 #[track_caller]
1499 pub fn store_next(&self, index: u32, next: u32) {
1500 if index as usize >= N {
1501 array_links_out_of_range(index, N);
1502 }
1503 self.next[index as usize].store(next, Ordering::Release);
1504 }
1505}
1506
1507impl<const N: usize> Default for ArrayLinks<N> {
1508 fn default() -> Self {
1509 Self::new()
1510 }
1511}