Skip to main content

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}