once_ptr_cell/imp.rs
1use core::marker::PhantomData;
2use core::ptr::NonNull;
3
4// The atomics are aliased so loom can shadow the REAL `OncePtrCell` type: under
5// `--cfg loom` the cell is built on `loom::sync::atomic`, so the shipped loom
6// tests (in `tests/`) model-check the actual implementation, not a hand-copied
7// transcription. Under normal builds it is `core::sync::atomic`, keeping the
8// crate `no_std` and allocation-free.
9//
10// CONSUMER HAZARD: `--cfg loom` is a global `RUSTFLAGS` cfg — it applies to
11// every crate in the build, not only the one whose loom suite you meant to
12// run. Under it `OncePtrCell::new` is NOT `const` (see its doc), so a
13// `static CELL: OncePtrCell<T> = OncePtrCell::new();` anywhere in the build
14// fails to compile. Scope the flag (`cargo test -p <crate> ...`), or supply a
15// `#[cfg(loom)]` const-capable stand-in in your own crate — see
16// `src/registry/bootstrap.rs`'s `loom_shim` in the `sefer-alloc` repository
17// this crate is extracted from, for a worked example.
18#[cfg(not(loom))]
19use core::sync::atomic::{AtomicPtr, Ordering};
20#[cfg(loom)]
21use loom::sync::atomic::{AtomicPtr, Ordering};
22
23/// The loser spin-wait hint. In a normal build this is [`core::hint::spin_loop`]
24/// (a PAUSE/YIELD CPU hint, no scheduler involvement). Under `--cfg loom` the
25/// real busy-spin is opaque to loom's model executor and would exhaust its
26/// branch budget ("processor must make progress"); there we yield to loom's
27/// fair scheduler instead, so it can advance the winner thread to its publish.
28/// Same happens-before semantics either way (a hint/yield synchronises nothing);
29/// only the scheduling nudge differs.
30#[cfg(loom)]
31#[inline]
32fn spin_hint() {
33 loom::thread::yield_now();
34}
35#[cfg(not(loom))]
36#[inline]
37fn spin_hint() {
38 core::hint::spin_loop();
39}
40
41/// The `INITIALIZING` sentinel address: a non-null, non-real marker meaning
42/// "one thread won the CAS and is currently running the init closure". Never
43/// dereferenced — only compared for pointer equality against the cell's stored
44/// value. An *aligned* pointer to `T` can never equal this address
45/// (`align_of::<T>() >= 2` is asserted at construction); a *misaligned or
46/// synthesised* pointer at this address is reachable from safe code and is
47/// rejected by a release-active `assert!` in
48/// [`OncePtrCell::get_or_try_init`], not by this constant alone.
49const SENTINEL_INITIALIZING: usize = 1;
50
51/// The outcome of [`OncePtrCell::dbg_rollback_reenterable`] — exactly the two
52/// answers that probe can give, and no third one it could never produce.
53///
54/// In particular there is no "rollback is broken" variant: the probe cannot
55/// distinguish that from "another thread legitimately owns the cell now",
56/// because both make its postcondition CAS fail identically. See the
57/// method's own docs for the full argument.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum RollbackProbe {
60 /// The rollback provably cleared the sentinel: the probe's postcondition
61 /// CAS re-won the cell afterwards, so no future winner or spinning loser
62 /// can be wedged by it. The cell is restored to `UNINIT` before
63 /// returning.
64 Proven,
65 /// The probe could not run its check, and this is NOT evidence that
66 /// rollback is broken. Either the cell was not `UNINIT` when the probe
67 /// entered (already `READY`, or owned by another thread at that
68 /// instant) — in which case the probe never touched it at all — or a
69 /// real `get_or_try_init` caller re-won the cell during the probe's own
70 /// rollback-then-reCAS window, in which case the probe still does not
71 /// touch it, but the cell is no longer necessarily `UNINIT`: the real
72 /// caller may already be running `init`, or may have published `READY`,
73 /// by the time this returns. Either way the probe never clobbers a
74 /// state it does not own.
75 NotApplicable,
76}
77
78/// A lazy, CAS-published pointer cell: `UNINIT -> INITIALIZING -> READY` over a
79/// single `AtomicPtr<T>`, with fallible init (OOM rolls back and losers
80/// re-race). See the [crate-level docs](crate) for the full state machine, the
81/// anti-livelock loser-spin rule, and the "usable inside a
82/// `#[global_allocator]`" niche.
83///
84/// The cell never drops, frees, or reads through the pointee — it only
85/// publishes and hands back the `*mut T` the init closure produced.
86///
87/// `#[repr(transparent)]`: the "one `AtomicPtr`"/"one word" claims made
88/// throughout this crate's docs are a LAYOUT GUARANTEE, not an
89/// implementation detail that happens to be true on the current compiler.
90/// `PhantomData<*mut T>` is the only other field; it is always zero-sized
91/// with alignment 1, which is exactly what `repr(transparent)` requires of
92/// every field beyond the one real one.
93#[repr(transparent)]
94pub struct OncePtrCell<T> {
95 /// The one word driving the state machine: `null` = `UNINIT`,
96 /// [`SENTINEL_INITIALIZING`] = `INITIALIZING`, any other value = `READY`
97 /// (a real published pointer).
98 ptr: AtomicPtr<T>,
99 /// `OncePtrCell<T>` behaves like it holds a `*mut T` it hands out; the
100 /// marker documents the relationship without owning a `T`.
101 _marker: PhantomData<*mut T>,
102}
103
104// The cell is `Send + Sync` UNCONDITIONALLY, exactly like the `AtomicPtr<T>` it
105// wraps — and for the same reason. The cell never dereferences `T` or hands out
106// a `&T`; it only stores and returns a RAW `*mut T` / `NonNull<T>`. Whether the
107// pointee is safe to *access* from another thread is the CALLER's contract (the
108// `get`/`get_or_try_init` accessors return raw pointers, and reading through
109// them is `unsafe`), not this type's — precisely the `AtomicPtr` model, which is
110// `Send + Sync` for every `T`. This is what lets the cell hold a pointer to a
111// `!Sync` payload (e.g. a per-thread heap) whose actual access the caller guards
112// by its own single-writer/`&mut` discipline. The `PhantomData<*mut T>` (present
113// only to document the "holds a `*mut T`" relationship and pin variance) is what
114// removes the auto-impls, so we restore them here.
115//
116// SAFETY: `ptr` is an `AtomicPtr`, so all concurrent access to the cell's own
117// state is race-free; the only value crossing a thread boundary through the cell
118// is a raw `*mut T`, which is `Send`/`Sync`-neutral (raw pointers carry no
119// sharing obligation — the obligation is on the caller's later deref). Identical
120// to `AtomicPtr<T>`'s own unconditional `Send + Sync`.
121unsafe impl<T> Send for OncePtrCell<T> {}
122// SAFETY: see the `Send` impl above.
123unsafe impl<T> Sync for OncePtrCell<T> {}
124
125/// RAII rollback guard held across the init closure: if `init`
126/// unwinds instead of returning, the winner thread's stack unwinds through
127/// this guard's `Drop`, which stores `null` with `Release` — exactly the
128/// same rollback the explicit OOM path performs. Without this, an unwinding
129/// `init` leaves the `INITIALIZING` sentinel stuck forever: every concurrent
130/// loser busy-spins at 100% CPU indefinitely (they spin on `==
131/// INITIALIZING`, which never changes), and every future
132/// `get_or_try_init`/`get` caller observes permanent `INITIALIZING` — a
133/// silent whole-process livelock, and a strictly worse outcome than the
134/// `OnceLock` equivalent, which leaves its cell uninitialised and lets the
135/// next caller retry.
136///
137/// Defused (via [`RollbackGuard::defuse`]) on both non-unwinding exits — the
138/// successful publish and the explicit `None`/OOM rollback — so the normal
139/// paths are unaffected; this guard only ever fires on the unwind path.
140///
141/// Test coverage note: `tests/cell_unit.rs`'s
142/// `panicking_init_rolls_back_and_subsequent_call_succeeds` proves a
143/// strictly weaker property than the one described above — that a
144/// SUBSEQUENT call on an already-quiescent cell succeeds after a panicking
145/// init unwound and rolled back. The same file's
146/// `concurrent_get_or_try_init_started_before_unwind_completes_still_succeeds`
147/// goes further: a real concurrent caller, whose own `get_or_try_init` call
148/// is issued no later than the point where it observes the winner already
149/// holds the sentinel, is never lost — either it observes the live sentinel
150/// and spins until the rollback wakes it, or it observes the already-rolled-
151/// back cell and wins the CAS itself directly. Both interleavings are
152/// possible depending on scheduling, and the test only guarantees success
153/// across whichever one actually happens; it does NOT deterministically
154/// force the spin-and-wake path specifically (that would need a hook inside
155/// the CAS/spin loop itself, which this crate does not have). A future
156/// change that made the rollback conditional (e.g. skipping it when no
157/// loser is observed waiting) would still very likely reintroduce a
158/// livelock this test would time out on, just not with airtight certainty
159/// that the spin branch itself was exercised on every run. Not closed by a
160/// loom test: loom's deterministic scheduling model and
161/// `std::panic::catch_unwind` do not compose cleanly (loom needs to replay
162/// every interleaving of an unwind path, which its own docs do not treat as
163/// a first-class supported pattern).
164struct RollbackGuard<'a, T> {
165 ptr: &'a AtomicPtr<T>,
166 defused: bool,
167}
168
169impl<'a, T> RollbackGuard<'a, T> {
170 #[inline]
171 fn new(ptr: &'a AtomicPtr<T>) -> Self {
172 Self {
173 ptr,
174 defused: false,
175 }
176 }
177
178 /// Disarm the guard: its `Drop` becomes a no-op. Call once the caller has
179 /// itself handled the `INITIALIZING` state (published `READY`, or
180 /// performed the explicit `None`/OOM rollback).
181 #[inline]
182 fn defuse(&mut self) {
183 self.defused = true;
184 }
185}
186
187impl<T> Drop for RollbackGuard<'_, T> {
188 #[inline]
189 fn drop(&mut self) {
190 if !self.defused {
191 // Same ordering rationale as the explicit OOM rollback in
192 // `get_or_try_init`: `Release` pairs with the retrying thread's
193 // later CAS `Acquire`; there is no partially-initialised state to
194 // synchronise (init never published), only the "cell is free
195 // again" fact.
196 self.ptr.store(core::ptr::null_mut(), Ordering::Release);
197 }
198 }
199}
200
201impl<T> OncePtrCell<T> {
202 /// Construct a fresh `UNINIT` cell (null pointer).
203 ///
204 /// **Not `const` under `--cfg loom`** (loom's atomics have no const
205 /// constructor); on normal builds it is `const` so the cell can live in a
206 /// `static`. Because `--cfg loom` is a global `RUSTFLAGS` cfg, this
207 /// applies to every crate in a build that sets it, not only crates that
208 /// mean to run loom against `OncePtrCell` itself — a
209 /// `static CELL: OncePtrCell<T> = OncePtrCell::new();` anywhere in such a
210 /// build fails to compile. Scope the flag to this crate
211 /// (`cargo test -p once-ptr-cell ...`), or supply your own
212 /// `#[cfg(loom)]` const-capable stand-in if you need the flag
213 /// workspace-wide.
214 ///
215 /// # Panics
216 ///
217 /// Panics if `align_of::<T>() == 1`. The `INITIALIZING` sentinel is encoded
218 /// as the address `1` (see the crate-level "Sentinel encoding" docs); that
219 /// encoding needs a spare low bit, which requires every valid aligned
220 /// address of `T` to be even — i.e. `align_of::<T>() >= 2`. In the
221 /// documented `static CELL: OncePtrCell<T> = OncePtrCell::new();` usage
222 /// this `assert!` is evaluated at compile time (a const-eval failure, not
223 /// a runtime panic); called from a non-const context (e.g. inside a
224 /// function, or via `OncePtrCell::<T>::default()`) with a `T` whose
225 /// alignment is 1, it panics at runtime instead.
226 #[cfg(not(loom))]
227 #[must_use]
228 pub const fn new() -> Self {
229 // Compile-time guard: the sentinel address (1) must not be a valid
230 // aligned address for `T`, or it could collide with a real pointer.
231 // Every `T` used behind this cell must have alignment >= 2.
232 assert!(
233 core::mem::align_of::<T>() >= 2,
234 "OncePtrCell<T> requires align_of::<T>() >= 2 so the INITIALIZING \
235 sentinel (address 1) can never collide with a real published pointer"
236 );
237 OncePtrCell {
238 ptr: AtomicPtr::new(core::ptr::null_mut()),
239 _marker: PhantomData,
240 }
241 }
242
243 /// Construct a fresh `UNINIT` cell (loom build — non-`const`).
244 ///
245 /// # Panics
246 ///
247 /// Panics if `align_of::<T>() == 1` — see the non-loom [`OncePtrCell::new`]
248 /// doc above for why (identical condition; this build cannot be `const` so
249 /// the check always runs at runtime here).
250 #[cfg(loom)]
251 #[must_use]
252 pub fn new() -> Self {
253 assert!(
254 core::mem::align_of::<T>() >= 2,
255 "OncePtrCell<T> requires align_of::<T>() >= 2"
256 );
257 OncePtrCell {
258 ptr: AtomicPtr::new(core::ptr::null_mut()),
259 _marker: PhantomData,
260 }
261 }
262
263 /// The `INITIALIZING` sentinel as a `*mut T` — a bare marker, never
264 /// dereferenced, constructed WITHOUT provenance (strict-provenance-clean).
265 #[inline]
266 fn sentinel() -> *mut T {
267 core::ptr::without_provenance_mut::<T>(SENTINEL_INITIALIZING)
268 }
269
270 /// `true` iff `p` is a real published pointer (non-null AND non-sentinel).
271 #[inline]
272 fn is_ready(p: *mut T) -> bool {
273 let a = p.addr();
274 a != 0 && a != SENTINEL_INITIALIZING
275 }
276
277 /// Return the published pointer if the cell is `READY`, else `None`.
278 ///
279 /// A pure `Acquire` load: no CAS, no init, no spin. `None` means the cell is
280 /// `UNINIT` or `INITIALIZING` right now (neither the sentinel nor null is
281 /// ever returned as `Some`).
282 ///
283 /// The returned pointer is the exact value the init closure produced; the
284 /// `Acquire` load pairs with the winner's `Release` publish, so any read the
285 /// caller performs through the pointer sees the fully initialised pointee.
286 #[inline]
287 #[must_use]
288 pub fn get(&self) -> Option<NonNull<T>> {
289 let p = self.ptr.load(Ordering::Acquire);
290 if Self::is_ready(p) {
291 // SAFETY: `is_ready(p)` just proved `p` is non-null (neither null
292 // nor the sentinel).
293 Some(unsafe { NonNull::new_unchecked(p) })
294 } else {
295 None
296 }
297 }
298
299 /// Get the published pointer, or run `init` to produce it — with the full
300 /// `UNINIT -> INITIALIZING -> READY` protocol, OOM rollback, and loser
301 /// re-race.
302 ///
303 /// Contract:
304 /// - **Fast path**: if the cell is already `READY`, returns the published
305 /// pointer with one `Acquire` load; `init` is not called.
306 /// - **Winner**: the thread that CASes `null -> sentinel` calls `init`
307 /// exactly once. `init` returns `Some(ptr)` on success (the cell
308 /// publishes it with `Release` and returns it — `ptr` is leaked for the
309 /// process lifetime, the cell never frees it), or `None` on OOM (the cell
310 /// rolls the sentinel back to `null` and returns `None`; a later call may
311 /// retry).
312 /// - **Loser**: a thread that loses the CAS spins with `Acquire` loads
313 /// **only while the state is `INITIALIZING`**. When the winner publishes,
314 /// the loser returns the same pointer. When the winner rolls back after
315 /// OOM (state returns to `null`), the loser falls out of the spin and
316 /// **re-races the CAS itself** — it does not wait for a `READY` that will
317 /// never come.
318 ///
319 /// Returns `Some(published pointer)` (same value for all threads across a
320 /// successful lifetime) or `None` if `init` reported OOM on this thread's
321 /// winning attempt. The returned pointer is never null and never the
322 /// sentinel.
323 ///
324 /// `init` is [`FnOnce`], not `FnMut`, because ONE call to this method
325 /// invokes it **at most once**: whichever way the winner arm exits
326 /// (publish, OOM rollback, or unwind) it leaves the method, and the
327 /// loser arm never calls `init` at all — a loser that falls out of the
328 /// spin on a rollback re-races the CAS and, if it wins, is making its
329 /// own first and only call. `FnOnce` is therefore the accurate bound,
330 /// and it lets you pass a closure that consumes what it captures.
331 ///
332 /// `init` must be reentrancy-safe with respect to whatever the cell guards:
333 /// it runs while this thread holds the `INITIALIZING` sentinel, so it must
334 /// not itself call back into `get_or_try_init` on the SAME cell (that would
335 /// spin forever — the current thread is the only one able to publish).
336 ///
337 /// The restriction is **transitive, and multiple cells form a lock-order
338 /// graph**: `init` must not wait, through any chain of calls, on a cell
339 /// whose own initialiser can wait on this one. Two cells are enough for a
340 /// deadlock with no direct self-recursion anywhere — thread 1 wins `A` and
341 /// its `init` initialises `B`, while thread 2 wins `B` and its `init`
342 /// initialises `A`; both spin forever at 100% CPU. Acquire multiple cells
343 /// in a fixed global order, exactly as you would locks.
344 ///
345 /// `init` must also be fast and non-blocking: every loser thread spins for
346 /// exactly as long as the winner's `init` call takes (see the module docs'
347 /// "spin-wait" section) — there is no bounded-latency guarantee from the
348 /// cell itself, only from the caller keeping `init` short.
349 ///
350 /// Calling this from inside a `#[global_allocator]` adds further hard
351 /// obligations on `init` (no allocation, no unwind) — see the crate docs'
352 /// ["Using this inside a `#[global_allocator]`"](crate#using-this-inside-a-global_allocator)
353 /// section.
354 ///
355 /// # Panics
356 ///
357 /// Panics if the winning `init` call returns `Some(ptr)` where `ptr`'s
358 /// address is the reserved `INITIALIZING` sentinel (`1`) — a safe `init`
359 /// closure can construct and return this exact address, and publishing it
360 /// unguarded would make every reader (this thread's own fast path
361 /// included) misclassify the cell as still-initializing forever. This
362 /// check is release-active, not `debug_assert!`-gated.
363 ///
364 /// If `init` itself panics (unwinds) instead of returning, the panic
365 /// propagates out of `get_or_try_init` and the cell is left in `UNINIT`
366 /// (not wedged in `INITIALIZING`) — a later call, on any thread, may
367 /// retry `init`. This mirrors the OOM/`None` rollback above; the only
368 /// difference is how the winner exits. Note what this does and does not
369 /// buy: it keeps the CELL consistent, but it does not make the unwind
370 /// itself sound when the frame below is a `GlobalAlloc` method, where
371 /// unwinding is undefined behaviour regardless of this cell's state.
372 #[must_use = "`None` means `init` reported OOM and the cell was rolled \
373 back to UNINIT — it is NOT initialised, and discarding \
374 this hides the failure"]
375 #[inline]
376 pub fn get_or_try_init<F>(&self, init: F) -> Option<NonNull<T>>
377 where
378 F: FnOnce() -> Option<NonNull<T>>,
379 {
380 // Fast path, and nothing else: one `Acquire` load plus the readiness
381 // test. Everything the already-published case does NOT need — the
382 // claim CAS, the rollback guard, the release-active `assert!`, the
383 // loser spin, the re-race loop — lives in `init_slow`, which is
384 // `#[cold] #[inline(never)]` so none of it is inlined into a
385 // caller that only ever hits this branch.
386 let p = self.ptr.load(Ordering::Acquire);
387 if Self::is_ready(p) {
388 // SAFETY: `is_ready(p)` just proved `p` is non-null (neither the
389 // `null` UNINIT value nor the `SENTINEL_INITIALIZING` marker), so
390 // `p` is a real published pointer.
391 return Some(unsafe { NonNull::new_unchecked(p) });
392 }
393 self.init_slow(init)
394 }
395
396 /// The full `UNINIT -> INITIALIZING -> READY` protocol: claim CAS, init
397 /// closure, publish/rollback, loser spin, re-race. Split out of
398 /// [`OncePtrCell::get_or_try_init`] so the already-READY fast path stays
399 /// small enough to inline on its own; correctness is unchanged, and the
400 /// re-checked fast path at the top of the loop below is still needed
401 /// here (a re-racing loser re-enters it after a rollback).
402 ///
403 /// Note this does NOT deduplicate monomorphised code: the slow path is
404 /// still generic over `F`, so one copy exists per closure type. It only
405 /// keeps that copy out of the caller's hot path.
406 #[cold]
407 #[inline(never)]
408 fn init_slow<F>(&self, init: F) -> Option<NonNull<T>>
409 where
410 F: FnOnce() -> Option<NonNull<T>>,
411 {
412 loop {
413 // Re-checked fast path: a loser that fell out of the spin on a
414 // rollback, or lost the CAS to a winner that has since
415 // published, lands here.
416 let p = self.ptr.load(Ordering::Acquire);
417 if Self::is_ready(p) {
418 // SAFETY: `is_ready(p)` just proved `p` is non-null (neither
419 // the `null` UNINIT value nor the `SENTINEL_INITIALIZING`
420 // marker), so `p` is a real published pointer.
421 return Some(unsafe { NonNull::new_unchecked(p) });
422 }
423
424 // Slow path: race to become the initialising winner.
425 match self.ptr.compare_exchange(
426 core::ptr::null_mut(),
427 Self::sentinel(),
428 // Success `Acquire`: synchronises-with whichever `Release`
429 // store last returned the cell to `null` — the explicit OOM
430 // rollback in this function's own winner arm, the unwind
431 // guard's `Drop`, or either of `dbg_rollback_reenterable`'s
432 // two null-stores. It says nothing
433 // about the publish this thread is about to perform: an
434 // acquire cannot pair with a release that has not happened
435 // yet. The load-bearing pair for the pointee is this winner's
436 // own `Release` publish below against every reader's
437 // `Acquire` load.
438 //
439 // Whether `Relaxed` would suffice here — a rollback leaves no
440 // payload state for a new winner to acquire — remains an open
441 // question, and is DELIBERATELY not acted on. Weakening it
442 // needs BOTH a loom counterfactual proving the weaker form
443 // sound and a measurement showing it is worth anything, and
444 // the second half is unobtainable on the hardware this crate
445 // is developed on: `Acquire` on x86-64 is a plain load, so a
446 // local A/B can only ever report noise. The same applies to
447 // the loser spin's per-iteration `Acquire` below. Both stay
448 // as they are — over-strong, never under-strong — until
449 // someone can measure them on a weakly-ordered target
450 // (AArch64/ARM) with a model to back the change.
451 Ordering::Acquire,
452 // Failure `Relaxed`: we re-load in the spin loop below.
453 Ordering::Relaxed,
454 ) {
455 Ok(_) => {
456 // ── Winner ──────────────────────────────────────────────
457 // We hold the INITIALIZING sentinel; we are the sole
458 // initialiser. Hold a rollback guard across `init()` so an
459 // UNWINDING init (a panic in caller code, or the `assert!`
460 // below firing) also rolls the sentinel back — see
461 // `RollbackGuard`'s own doc for why this is load-bearing.
462 let mut guard = RollbackGuard::new(&self.ptr);
463 match init() {
464 Some(ptr) => {
465 let raw = ptr.as_ptr();
466 // Release-active `assert!`, not `debug_assert!`
467 // a SAFE init closure can construct
468 // `NonNull::new(without_provenance_mut(1))` and
469 // hand back the very SENTINEL address this cell
470 // uses to mean "still initialising". In release,
471 // a `debug_assert!` here compiles out, so the
472 // sentinel would get published as if it were
473 // READY — every current loser and every future
474 // caller then spins forever, since the published
475 // value reads back as `INITIALIZING`, not `READY`
476 // (`is_ready`'s own definition), with no
477 // diagnostic anywhere. Two integer compares on a
478 // once-per-cell cold path is a negligible cost
479 // for closing a violation of this method's own
480 // documented "never null, never the sentinel"
481 // guarantee that is reachable from 100% safe
482 // code — exactly the class `debug_assert!` is
483 // NOT meant for. If this fires, the rollback
484 // guard above unwinds it cleanly.
485 assert!(
486 Self::is_ready(raw),
487 "OncePtrCell: init returned the null/sentinel address"
488 );
489 // Publish with `Release` so every subsequent
490 // `Acquire` load (fast path here, plus every loser's
491 // spin-load) sees the fully constructed pointee.
492 // This is THE ordering the Relaxed-publish
493 // counterfactual breaks.
494 self.ptr.store(raw, Ordering::Release);
495 // Defuse: the guard's rollback must NOT fire now
496 // that the real pointer is published.
497 guard.defuse();
498 return Some(ptr);
499 }
500 None => {
501 // OOM: roll the sentinel back to null so losers
502 // spinning on `== INITIALIZING` fall out and
503 // re-race, and future callers can retry. `Release`
504 // pairs with the retrying thread's later CAS
505 // `Acquire`: there is no partially-initialised state
506 // to synchronise (init never published), only the
507 // "cell is free again" fact. Explicit here (rather
508 // than relying on the guard's Drop) to keep this
509 // path's ordering self-documenting; defuse first so
510 // the guard does not redundantly store again.
511 guard.defuse();
512 self.ptr.store(core::ptr::null_mut(), Ordering::Release);
513 return None;
514 }
515 }
516 }
517 Err(_) => {
518 // ── Loser ───────────────────────────────────────────────
519 // Spin ONLY while the state is INITIALIZING. This is the
520 // anti-livelock rule: a `!= READY` spin would
521 // deadlock if the winner rolled back to null after OOM
522 // (READY never comes). Falling out on any non-INITIALIZING
523 // observation lets us return READY (winner published) or
524 // loop back to the top and re-race (winner rolled back).
525 loop {
526 let p = self.ptr.load(Ordering::Acquire);
527 let a = p.addr();
528 if a == SENTINEL_INITIALIZING {
529 // Still initialising — keep spinning.
530 spin_hint();
531 continue;
532 }
533 if a != 0 {
534 // READY: the winner published a real pointer.
535 // SAFETY: `a != 0` (checked above) and `a !=
536 // SENTINEL_INITIALIZING` (the `if` above this one
537 // already returned/continued on that value), so
538 // `p` is neither null nor the sentinel — a real
539 // published pointer.
540 return Some(unsafe { NonNull::new_unchecked(p) });
541 }
542 // null: the winner rolled back after OOM. Break out of
543 // the spin and re-race the CAS from the top — do NOT
544 // keep waiting for a READY that will never be published.
545 break;
546 }
547 // Fall through to the outer loop: re-race.
548 }
549 }
550 }
551 }
552
553 /// Test-probe introspection: `true` iff the cell is currently `READY`
554 /// (holds a real, non-null, non-sentinel pointer). Says nothing about
555 /// the published *value* itself (that is [`OncePtrCell::get`]'s
556 /// contract).
557 ///
558 /// This is functionally identical to `get().is_some()` — same single
559 /// `Acquire` load, same predicate, no capability `get` lacks — it does
560 /// **not** avoid racing a concurrent init any differently than `get`
561 /// does (an earlier version of this doc claimed
562 /// otherwise). It exists as a named, self-documenting boolean
563 /// introspection primitive: a caller writing `assert!(cell.dbg_is_ready())`
564 /// reads as "assert the cell materialised" without an
565 /// `.is_some()`/`.is_none()` match at the call site. The `sefer-alloc`
566 /// allocator this crate was extracted from relies on exactly that: its
567 /// own `Registry::dbg_chunk_is_materialised` forwards here to assert
568 /// chunk-materialisation state in its regression tests.
569 ///
570 /// # Stability
571 ///
572 /// This is a deliberate, STABLE part of the public API — a
573 /// `dbg_`-prefixed test-probe surface, not a hidden implementation
574 /// detail. It carries the crate's normal semver guarantee like any
575 /// other public item; a `#[doc(hidden)]` posture was rejected precisely
576 /// because it would advertise this function to downstream consumers'
577 /// tests (see [`OncePtrCell::dbg_rollback_reenterable`]'s own doc) while
578 /// hiding it from the rustdoc those consumers would need to discover it
579 /// — see the crate README's "Test-probe API stability" section for the
580 /// full rationale.
581 #[inline]
582 #[must_use]
583 pub fn dbg_is_ready(&self) -> bool {
584 Self::is_ready(self.ptr.load(Ordering::Acquire))
585 }
586
587 /// Test-only anti-livelock rollback probe. Drives THIS cell through the
588 /// exact `null -> sentinel -> rollback -> re-CAS` sequence the internal
589 /// OOM-bailout runs, and proves the postcondition the whole design rests on:
590 /// after a rollback, a fresh `CAS(null -> sentinel)` MUST succeed (the
591 /// sentinel was genuinely cleared, so no future winner or spinning loser is
592 /// wedged).
593 ///
594 /// Returns [`RollbackProbe::Proven`] if the rollback provably cleared the
595 /// sentinel (the postcondition CAS re-won the cell; it is restored to
596 /// `UNINIT` before returning). [`RollbackProbe::NotApplicable`] covers
597 /// TWO distinct "could not test" cases, deliberately conflated because
598 /// neither is evidence rollback is broken: (a) the cell was not observed
599 /// `UNINIT` on the entry CAS (already `READY`, or another thread owned it
600 /// at that instant), or (b) the postcondition CAS in step 3 failed
601 /// because a real `get_or_try_init` caller raced in and re-won the cell
602 /// during the probe's own rollback-then-reCAS window — in that case the
603 /// probe leaves the cell alone (does NOT touch the new owner's state).
604 ///
605 /// **There is deliberately no "rollback is broken" variant.** This probe
606 /// cannot distinguish that from "someone else legitimately owns the cell
607 /// now" by construction — both look identical from here, the
608 /// postcondition CAS simply fails either way — so the return type
609 /// encodes exactly the two answers it can actually give, and no third
610 /// one it could never produce.
611 ///
612 /// Exists so a consumer's test can drive the rollback on a REAL, LIVE cell
613 /// (e.g. a process-global registry chunk) — proving the shipped code path,
614 /// not a copy — without a process-terminating OOM. The whole probe is a
615 /// bounded, single-threaded sequence of atomic ops; callers MUST pick a
616 /// cell no other thread is concurrently initialising. The entry CAS is
617 /// only a POINT-IN-TIME check, not mutual exclusion across the whole
618 /// probe: if the cell is not observed `UNINIT` at that instant, the probe
619 /// returns [`RollbackProbe::NotApplicable`] and touches nothing, but a
620 /// concurrent
621 /// [`OncePtrCell::get_or_try_init`] racing in AFTER the entry CAS (during
622 /// the probe's own rollback-then-reCAS window) is not excluded by it — the
623 /// probe's final restore step accounts for that by only touching the cell
624 /// when its own postcondition CAS actually re-won ownership (see the
625 /// step-by-step comments in the body).
626 ///
627 /// # Stability
628 ///
629 /// This is a deliberate, STABLE part of the public API, not
630 /// `#[doc(hidden)]`. This function is explicitly written to be called
631 /// FROM a downstream consumer's own test suite ("a consumer's test can
632 /// drive the rollback on a REAL, LIVE cell" above) — a `#[doc(hidden)]`
633 /// posture would have advertised it to those consumers while hiding it
634 /// from the rustdoc they would need to find it in the first place, an
635 /// unresolvable contradiction the crate's rust-intel audit caught. See
636 /// the crate README's "Test-probe API stability" section for the full
637 /// rationale and the rejected feature-flag alternative.
638 #[must_use]
639 pub fn dbg_rollback_reenterable(&self) -> RollbackProbe {
640 // Step 1: only proceed if the cell is UNINIT (null). If it is already
641 // READY or contended, do not touch it.
642 if self
643 .ptr
644 .compare_exchange(
645 core::ptr::null_mut(),
646 Self::sentinel(),
647 Ordering::Acquire,
648 Ordering::Relaxed,
649 )
650 .is_err()
651 {
652 return RollbackProbe::NotApplicable;
653 }
654
655 // Step 2: run the EXACT rollback the internal OOM-bailout runs (sentinel
656 // -> null, Release).
657 self.ptr.store(core::ptr::null_mut(), Ordering::Release);
658
659 // Step 3: prove the postcondition — a fresh CAS(null -> sentinel) must
660 // now succeed.
661 let postcondition_holds = self
662 .ptr
663 .compare_exchange(
664 core::ptr::null_mut(),
665 Self::sentinel(),
666 Ordering::Acquire,
667 Ordering::Relaxed,
668 )
669 .is_ok();
670
671 // Step 4: restore to null, exactly as observed on entry — but ONLY if
672 // step 3's CAS actually re-won ownership of the cell (postcondition
673 // held). If it failed, the cell is not ours any more: a real
674 // `get_or_try_init` caller raced in during the window between step 2's
675 // rollback and step 3's CAS, won the CAS itself, and may already be
676 // running (or have finished) the caller's init closure. Storing null
677 // unconditionally here would clobber that other owner's sentinel (or
678 // its published pointer) out from under it — the exact clobber this
679 // probe must not cause. When we did not re-win, leave the cell alone
680 // and report "not applicable" rather than a false rollback failure: a
681 // concurrent owner racing in is not evidence that rollback itself is
682 // broken.
683 if !postcondition_holds {
684 return RollbackProbe::NotApplicable;
685 }
686 self.ptr.store(core::ptr::null_mut(), Ordering::Release);
687
688 RollbackProbe::Proven
689 }
690}
691
692impl<T> Default for OncePtrCell<T> {
693 fn default() -> Self {
694 Self::new()
695 }
696}
697
698impl<T> core::fmt::Debug for OncePtrCell<T> {
699 /// Diagnostic-only classification of the cell's current state — never
700 /// dereferences the pointee, so no `T: Debug` bound is needed (`T` never
701 /// appears in the output). `Relaxed` is enough here: unlike `get`, this
702 /// never hands the pointer back to the caller to dereference, so there is
703 /// no happens-before edge to establish. Like any concurrent type's
704 /// `Debug` impl (`OnceLock`'s included), the state printed can be stale
705 /// the instant after this call returns.
706 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
707 let p = self.ptr.load(Ordering::Relaxed);
708 f.write_str("OncePtrCell(")?;
709 match p.addr() {
710 0 => f.write_str("Uninit")?,
711 SENTINEL_INITIALIZING => f.write_str("Initializing")?,
712 _ => write!(f, "Ready({p:p})")?,
713 }
714 f.write_str(")")
715 }
716}