Skip to main content

almost_enough/
stop_token.rs

1//! Arc-based cloneable dynamic dispatch for Stop.
2//!
3//! This module provides [`StopToken`], a shared-ownership wrapper that enables
4//! dynamic dispatch without monomorphization bloat, with cheap `Clone`.
5//!
6//! # StopToken vs BoxedStop
7//!
8//! | | `StopToken` | `BoxedStop` |
9//! |---|-----------|-------------|
10//! | Clone | Yes (Arc increment) | No |
11//! | Storage | `Arc<dyn Stop>` | `Box<dyn Stop>` |
12//! | Send to threads | Clone and move | Must wrap in Arc yourself |
13//! | Use case | Default choice | When Clone is unwanted |
14//!
15//! # Example
16//!
17//! ```rust
18//! use almost_enough::{StopToken, Stopper, Unstoppable, Stop};
19//!
20//! let stopper = Stopper::new();
21//! let stop = StopToken::new(stopper.clone());
22//! let stop2 = stop.clone(); // Arc increment, no allocation
23//!
24//! stopper.cancel();
25//! assert!(stop.should_stop());
26//! assert!(stop2.should_stop());
27//! ```
28
29use alloc::sync::Arc;
30use core::any::{Any, TypeId};
31
32use crate::{Stop, StopReason};
33
34/// A shared-ownership [`Stop`] implementation with cheap `Clone`.
35///
36/// Wraps any `Stop` in an `Arc` for shared ownership across threads.
37/// Cloning is an atomic increment — no heap allocation.
38///
39/// # Indirection Collapsing
40///
41/// `StopToken::new()` detects when you pass another `StopToken` and unwraps
42/// it instead of double-wrapping. No-op stops (`Unstoppable`) are stored
43/// as `None` — `check()` short-circuits without any vtable dispatch.
44///
45/// # Example
46///
47/// ```rust
48/// use almost_enough::{StopToken, Stopper, Stop, StopReason};
49///
50/// let stopper = Stopper::new();
51/// let stop = StopToken::new(stopper.clone());
52/// let stop2 = stop.clone(); // cheap Arc clone
53///
54/// stopper.cancel();
55/// assert!(stop.should_stop());
56/// assert!(stop2.should_stop()); // both see cancellation
57/// ```
58pub struct StopToken {
59    /// `None` when the wrapped type's `may_stop()` returned false at
60    /// construction (e.g., `Unstoppable`). `check()` and `should_stop()`
61    /// short-circuit to no-op without any vtable dispatch.
62    inner: Option<Arc<dyn Stop + Send + Sync>>,
63}
64
65impl StopToken {
66    /// Create a new `StopToken` from any [`Stop`] implementation.
67    ///
68    /// If `stop` is already a `StopToken`, it is unwrapped instead of
69    /// double-wrapping (no extra indirection).
70    #[inline]
71    pub fn new<T: Stop + 'static>(stop: T) -> Self {
72        // Fast path: no-op stops skip all wrapping (no Arc, no TypeId checks)
73        if !stop.may_stop() {
74            return Self { inner: None };
75        }
76        // Collapse StopToken nesting: reuse its inner Option<Arc>
77        if TypeId::of::<T>() == TypeId::of::<StopToken>() {
78            let any_ref: &dyn Any = &stop;
79            let inner = any_ref.downcast_ref::<StopToken>().unwrap();
80            let result = inner.clone();
81            drop(stop);
82            return result;
83        }
84        // Flatten Stopper: reuse its inner Arc (one hop, not two)
85        if TypeId::of::<T>() == TypeId::of::<crate::Stopper>() {
86            let any_ref: &dyn Any = &stop;
87            let stopper = any_ref.downcast_ref::<crate::Stopper>().unwrap();
88            let result = Self {
89                inner: Some(stopper.inner.clone() as Arc<dyn Stop + Send + Sync>),
90            };
91            drop(stop);
92            return result;
93        }
94        // Flatten SyncStopper: same
95        if TypeId::of::<T>() == TypeId::of::<crate::SyncStopper>() {
96            let any_ref: &dyn Any = &stop;
97            let stopper = any_ref.downcast_ref::<crate::SyncStopper>().unwrap();
98            let result = Self {
99                inner: Some(stopper.inner.clone() as Arc<dyn Stop + Send + Sync>),
100            };
101            drop(stop);
102            return result;
103        }
104        Self {
105            inner: Some(Arc::new(stop)),
106        }
107    }
108
109    /// Create a `StopToken` from an existing `Arc<T>` without re-wrapping.
110    ///
111    /// This is zero-cost — just widens the pointer. Use this when you
112    /// already have an `Arc`-wrapped stop type.
113    ///
114    /// If `T` is `StopToken`, the inner Arc is extracted to avoid double
115    /// indirection (`Arc<Arc<dyn Stop>>`).
116    ///
117    /// ```rust
118    /// use almost_enough::{StopToken, Stopper, Stop};
119    /// # #[cfg(feature = "std")]
120    /// # fn main() {
121    /// use std::sync::Arc;
122    ///
123    /// let stopper = Arc::new(Stopper::new());
124    /// let stop = StopToken::from_arc(stopper); // pointer widening, no allocation
125    /// assert!(!stop.should_stop());
126    /// # }
127    /// # #[cfg(not(feature = "std"))]
128    /// # fn main() {}
129    /// ```
130    #[inline]
131    pub fn from_arc<T: Stop + 'static>(arc: Arc<T>) -> Self {
132        if !arc.may_stop() {
133            return Self { inner: None };
134        }
135        // Collapse Arc<StopToken> → reuse inner Arc
136        if TypeId::of::<T>() == TypeId::of::<StopToken>() {
137            let any_ref: &dyn Any = &*arc;
138            let inner = any_ref.downcast_ref::<StopToken>().unwrap();
139            return inner.clone();
140        }
141        Self {
142            inner: Some(arc as Arc<dyn Stop + Send + Sync>),
143        }
144    }
145}
146
147impl Clone for StopToken {
148    #[inline]
149    fn clone(&self) -> Self {
150        Self {
151            inner: self.inner.clone(),
152        }
153    }
154}
155
156impl Stop for StopToken {
157    #[inline(always)]
158    fn check(&self) -> Result<(), StopReason> {
159        match &self.inner {
160            Some(inner) => inner.check(),
161            None => Ok(()),
162        }
163    }
164
165    #[inline(always)]
166    fn should_stop(&self) -> bool {
167        match &self.inner {
168            Some(inner) => inner.should_stop(),
169            None => false,
170        }
171    }
172
173    #[inline(always)]
174    fn may_stop(&self) -> bool {
175        self.inner.is_some()
176    }
177}
178
179/// Zero-cost conversion: reuses the Stopper's existing `Arc` via pointer widening.
180/// No double-wrapping — the resulting `StopToken` shares the same heap allocation.
181impl From<crate::Stopper> for StopToken {
182    #[inline]
183    fn from(stopper: crate::Stopper) -> Self {
184        Self {
185            inner: Some(stopper.inner as Arc<dyn Stop + Send + Sync>),
186        }
187    }
188}
189
190/// Zero-cost conversion: reuses the SyncStopper's existing `Arc` via pointer widening.
191impl From<crate::SyncStopper> for StopToken {
192    #[inline]
193    fn from(stopper: crate::SyncStopper) -> Self {
194        Self {
195            inner: Some(stopper.inner as Arc<dyn Stop + Send + Sync>),
196        }
197    }
198}
199
200// Option<Arc<dyn Stop>> gets null-pointer optimization — same size as Arc<dyn Stop>.
201// StopToken is two pointers (data + vtable), no larger than a bare Arc<dyn>.
202const _: () = assert!(
203    core::mem::size_of::<StopToken>() == core::mem::size_of::<Arc<dyn Stop + Send + Sync>>()
204);
205
206impl core::fmt::Debug for StopToken {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208        f.debug_tuple("StopToken").finish()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::{FnStop, StopSource, Stopper, Unstoppable};
216
217    #[test]
218    fn from_unstoppable() {
219        let stop = StopToken::new(Unstoppable);
220        assert!(!stop.should_stop());
221        assert!(stop.check().is_ok());
222    }
223
224    #[test]
225    fn from_stopper() {
226        let stopper = Stopper::new();
227        let stop = StopToken::new(stopper.clone());
228
229        assert!(!stop.should_stop());
230
231        stopper.cancel();
232
233        assert!(stop.should_stop());
234        assert_eq!(stop.check(), Err(StopReason::Cancelled));
235    }
236
237    #[test]
238    fn clone_is_cheap() {
239        let stopper = Stopper::new();
240        let stop = StopToken::new(stopper.clone());
241        let stop2 = stop.clone();
242
243        stopper.cancel();
244
245        // Both clones see the cancellation (shared state)
246        assert!(stop.should_stop());
247        assert!(stop2.should_stop());
248    }
249
250    #[cfg(feature = "std")]
251    #[test]
252    fn clone_send_to_thread() {
253        let stopper = Stopper::new();
254        let stop = StopToken::new(stopper.clone());
255
256        let handle = std::thread::spawn({
257            let stop = stop.clone();
258            move || stop.should_stop()
259        });
260
261        stopper.cancel();
262        // Thread may or may not see cancellation depending on timing
263        let _ = handle.join().unwrap();
264    }
265
266    #[test]
267    fn is_send_sync() {
268        fn assert_send_sync<T: Send + Sync>() {}
269        assert_send_sync::<StopToken>();
270    }
271
272    #[test]
273    fn debug_format() {
274        let stop = StopToken::new(Unstoppable);
275        let debug = alloc::format!("{:?}", stop);
276        assert!(debug.contains("StopToken"));
277    }
278
279    #[test]
280    fn may_stop_delegates() {
281        assert!(!StopToken::new(Unstoppable).may_stop());
282        assert!(StopToken::new(Stopper::new()).may_stop());
283    }
284
285    #[test]
286    fn unstoppable_is_none_internally() {
287        let stop = StopToken::new(Unstoppable);
288        assert!(!stop.may_stop());
289        assert!(stop.check().is_ok());
290    }
291
292    #[test]
293    fn collapses_nested_dyn_stop() {
294        let inner = StopToken::new(Unstoppable);
295        let outer = StopToken::new(inner);
296        assert!(!outer.may_stop());
297    }
298
299    #[test]
300    fn collapses_nested_stopper() {
301        let stopper = Stopper::new();
302        let inner = StopToken::new(stopper.clone());
303        let outer = StopToken::new(inner.clone());
304
305        // Both share the same Arc chain
306        stopper.cancel();
307        assert!(outer.should_stop());
308        assert!(inner.should_stop());
309    }
310
311    #[test]
312    fn from_arc() {
313        let stopper = Arc::new(Stopper::new());
314        let cancel_handle = stopper.clone();
315        let stop = StopToken::from_arc(stopper);
316
317        assert!(!stop.should_stop());
318        cancel_handle.cancel();
319        assert!(stop.should_stop());
320    }
321
322    #[test]
323    fn from_non_clone_fn_stop() {
324        // FnStop with non-Clone closure — StopToken doesn't need Clone on T
325        let flag = Arc::new(core::sync::atomic::AtomicBool::new(false));
326        let flag2 = flag.clone();
327        let stop = StopToken::new(FnStop::new(move || {
328            flag2.load(core::sync::atomic::Ordering::Relaxed)
329        }));
330
331        assert!(!stop.should_stop());
332
333        // Clone the StopToken (shares the Arc, not the closure)
334        let stop2 = stop.clone();
335        flag.store(true, core::sync::atomic::Ordering::Relaxed);
336
337        assert!(stop.should_stop());
338        assert!(stop2.should_stop());
339    }
340
341    #[test]
342    fn avoids_monomorphization() {
343        fn process(stop: StopToken) -> bool {
344            stop.should_stop()
345        }
346
347        assert!(!process(StopToken::new(Unstoppable)));
348        assert!(!process(StopToken::new(StopSource::new())));
349        assert!(!process(StopToken::new(Stopper::new())));
350    }
351
352    #[test]
353    fn hot_loop_pattern() {
354        let stop = StopToken::new(Unstoppable);
355        for _ in 0..1000 {
356            assert!(stop.check().is_ok()); // None path, no dispatch
357        }
358    }
359
360    #[test]
361    fn from_stopper_zero_cost() {
362        let stopper = Stopper::new();
363        let cancel = stopper.clone();
364        let stop: StopToken = stopper.into(); // zero-cost: reuses Arc
365
366        assert!(!stop.should_stop());
367        cancel.cancel();
368        assert!(stop.should_stop()); // same Arc, same AtomicBool
369    }
370
371    #[test]
372    fn from_sync_stopper_zero_cost() {
373        let stopper = crate::SyncStopper::new();
374        let cancel = stopper.clone();
375        let stop: StopToken = stopper.into();
376
377        assert!(!stop.should_stop());
378        cancel.cancel();
379        assert!(stop.should_stop());
380    }
381
382    #[test]
383    fn new_stopper_flattens() {
384        // StopToken::new(Stopper) should reuse the Stopper's Arc,
385        // not double-wrap in Arc<Stopper{Arc<AtomicBool>}>
386        let stopper = Stopper::new();
387        let cancel = stopper.clone();
388        let stop = StopToken::new(stopper);
389
390        cancel.cancel();
391        assert!(stop.should_stop());
392    }
393
394    #[test]
395    fn new_sync_stopper_flattens() {
396        let stopper = crate::SyncStopper::new();
397        let cancel = stopper.clone();
398        let stop = StopToken::new(stopper);
399
400        cancel.cancel();
401        assert!(stop.should_stop());
402    }
403
404    #[test]
405    fn from_arc_collapses_dynstop() {
406        // from_arc(Arc<StopToken>) should reuse inner, not double-wrap
407        let inner = StopToken::new(Stopper::new());
408        let arc = alloc::sync::Arc::new(inner);
409        let stop = StopToken::from_arc(arc);
410        assert!(!stop.should_stop());
411    }
412
413    #[test]
414    fn stopper_inner_debug() {
415        let stop = Stopper::new();
416        let debug = alloc::format!("{:?}", stop);
417        assert!(debug.contains("cancelled"));
418    }
419
420    #[test]
421    fn sync_stopper_inner_debug() {
422        let stop = crate::SyncStopper::new();
423        let debug = alloc::format!("{:?}", stop);
424        assert!(debug.contains("cancelled"));
425    }
426
427    #[test]
428    fn from_stopper_clone_shares_state() {
429        let stopper = Stopper::new();
430        let stop: StopToken = stopper.clone().into();
431        let stop2 = stop.clone(); // Arc clone of the flattened inner
432
433        stopper.cancel();
434        // All three (stopper, stop, stop2) share the same AtomicBool
435        assert!(stop.should_stop());
436        assert!(stop2.should_stop());
437    }
438}