almost_enough/
source.rs

1//! Zero-allocation cancellation primitives.
2//!
3//! This module provides stack-based cancellation using borrowed references.
4//! Works in `no_std` environments without an allocator.
5//!
6//! # Overview
7//!
8//! - [`StopSource`] - A cancellation source that owns an `AtomicBool` on the stack
9//! - [`StopRef`] - A borrowed reference to check cancellation
10//!
11//! # Example
12//!
13//! ```rust
14//! use almost_enough::{StopSource, Stop};
15//!
16//! let source = StopSource::new();
17//! let stop = source.as_ref();
18//!
19//! assert!(!stop.should_stop());
20//!
21//! source.cancel();
22//! assert!(stop.should_stop());
23//! ```
24//!
25//! # When to Use
26//!
27//! Use `StopSource`/`StopRef` when:
28//! - You need zero-allocation cancellation
29//! - The source outlives all references (stack-based usage)
30//! - You're in a `no_std` environment
31//!
32//! Use [`Stopper`](crate::Stopper) when:
33//! - You need to share ownership (clone instead of borrow)
34//! - You want to pass stops across thread boundaries without lifetimes
35
36use core::sync::atomic::{AtomicBool, Ordering};
37
38use crate::{Stop, StopReason};
39
40/// A stack-based cancellation source.
41///
42/// This is a zero-allocation cancellation primitive. The source owns the
43/// atomic and can issue borrowed references via [`as_ref()`](Self::as_ref).
44///
45/// # Example
46///
47/// ```rust
48/// use almost_enough::{StopSource, Stop};
49///
50/// let source = StopSource::new();
51/// let stop = source.as_ref();
52///
53/// // Check in your operation
54/// assert!(!stop.should_stop());
55///
56/// // Cancel when needed
57/// source.cancel();
58/// assert!(stop.should_stop());
59/// ```
60///
61/// # Const Construction
62///
63/// `StopSource` can be created in const context:
64///
65/// ```rust
66/// use almost_enough::StopSource;
67///
68/// static GLOBAL_STOP: StopSource = StopSource::new();
69/// ```
70#[derive(Debug)]
71pub struct StopSource {
72    cancelled: AtomicBool,
73}
74
75impl StopSource {
76    /// Create a new cancellation source.
77    #[inline]
78    pub const fn new() -> Self {
79        Self {
80            cancelled: AtomicBool::new(false),
81        }
82    }
83
84    /// Create a source that is already cancelled.
85    ///
86    /// Useful for testing or when you want to signal immediate stop.
87    #[inline]
88    pub const fn cancelled() -> Self {
89        Self {
90            cancelled: AtomicBool::new(true),
91        }
92    }
93
94    /// Signal all references to stop.
95    ///
96    /// This is idempotent - calling it multiple times has no additional effect.
97    #[inline]
98    pub fn cancel(&self) {
99        self.cancelled.store(true, Ordering::Relaxed);
100    }
101
102    /// Check if this source has been cancelled.
103    #[inline]
104    pub fn is_cancelled(&self) -> bool {
105        self.cancelled.load(Ordering::Relaxed)
106    }
107
108    /// Get a borrowed reference to pass to operations.
109    ///
110    /// The reference borrows from this source, so it cannot outlive it.
111    /// For owned stops, use [`Stopper`](crate::Stopper).
112    #[inline]
113    pub fn as_ref(&self) -> StopRef<'_> {
114        StopRef {
115            cancelled: &self.cancelled,
116        }
117    }
118
119    /// Alias for [`as_ref()`](Self::as_ref) for migration from AtomicStop.
120    #[inline]
121    #[deprecated(since = "0.1.0", note = "use as_ref() instead")]
122    pub fn token(&self) -> StopRef<'_> {
123        self.as_ref()
124    }
125}
126
127impl Default for StopSource {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Stop for StopSource {
134    #[inline]
135    fn check(&self) -> Result<(), StopReason> {
136        if self.cancelled.load(Ordering::Relaxed) {
137            Err(StopReason::Cancelled)
138        } else {
139            Ok(())
140        }
141    }
142
143    #[inline]
144    fn should_stop(&self) -> bool {
145        self.cancelled.load(Ordering::Relaxed)
146    }
147}
148
149/// A borrowed reference to a [`StopSource`].
150///
151/// This is a lightweight reference that can only check for cancellation -
152/// it cannot trigger it. Use the source to cancel.
153///
154/// # Example
155///
156/// ```rust
157/// use almost_enough::{StopSource, Stop};
158///
159/// fn process(data: &[u8], stop: impl Stop) {
160///     for (i, chunk) in data.chunks(100).enumerate() {
161///         if i % 10 == 0 && stop.should_stop() {
162///             return;
163///         }
164///         // process chunk...
165///     }
166/// }
167///
168/// let source = StopSource::new();
169/// process(&[0u8; 1000], source.as_ref());
170/// ```
171///
172/// # Copy Semantics
173///
174/// `StopRef` is `Copy`, so you can freely copy it without cloning:
175///
176/// ```rust
177/// use almost_enough::{StopSource, Stop};
178///
179/// let source = StopSource::new();
180/// let r1 = source.as_ref();
181/// let r2 = r1;  // Copy
182/// let r3 = r1;  // Still valid
183///
184/// source.cancel();
185/// assert!(r1.should_stop());
186/// assert!(r2.should_stop());
187/// assert!(r3.should_stop());
188/// ```
189#[derive(Debug, Clone, Copy)]
190pub struct StopRef<'a> {
191    cancelled: &'a AtomicBool,
192}
193
194impl Stop for StopRef<'_> {
195    #[inline]
196    fn check(&self) -> Result<(), StopReason> {
197        if self.cancelled.load(Ordering::Relaxed) {
198            Err(StopReason::Cancelled)
199        } else {
200            Ok(())
201        }
202    }
203
204    #[inline]
205    fn should_stop(&self) -> bool {
206        self.cancelled.load(Ordering::Relaxed)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn stop_source_basic() {
216        let source = StopSource::new();
217        assert!(!source.is_cancelled());
218        assert!(!source.should_stop());
219        assert!(source.check().is_ok());
220
221        source.cancel();
222
223        assert!(source.is_cancelled());
224        assert!(source.should_stop());
225        assert_eq!(source.check(), Err(StopReason::Cancelled));
226    }
227
228    #[test]
229    fn stop_source_cancelled_constructor() {
230        let source = StopSource::cancelled();
231        assert!(source.is_cancelled());
232        assert!(source.should_stop());
233    }
234
235    #[test]
236    fn stop_ref_basic() {
237        let source = StopSource::new();
238        let stop = source.as_ref();
239
240        assert!(!stop.should_stop());
241        assert!(stop.check().is_ok());
242
243        source.cancel();
244
245        assert!(stop.should_stop());
246        assert_eq!(stop.check(), Err(StopReason::Cancelled));
247    }
248
249    #[test]
250    fn stop_ref_is_copy() {
251        let source = StopSource::new();
252        let r1 = source.as_ref();
253        let r2 = r1; // Copy
254        let _ = r1; // Still valid
255        let _ = r2;
256    }
257
258    #[test]
259    fn stop_source_is_default() {
260        let source: StopSource = Default::default();
261        assert!(!source.is_cancelled());
262    }
263
264    #[test]
265    fn stop_source_is_send_sync() {
266        fn assert_send_sync<T: Send + Sync>() {}
267        assert_send_sync::<StopSource>();
268        assert_send_sync::<StopRef<'_>>();
269    }
270
271    #[test]
272    fn cancel_is_idempotent() {
273        let source = StopSource::new();
274        source.cancel();
275        source.cancel();
276        source.cancel();
277        assert!(source.is_cancelled());
278    }
279
280    #[test]
281    fn const_construction() {
282        static SOURCE: StopSource = StopSource::new();
283        assert!(!SOURCE.is_cancelled());
284    }
285}