Skip to main content

almost_enough/
boxed.rs

1//! Boxed dynamic dispatch for Stop.
2//!
3//! This module provides [`BoxedStop`], a heap-allocated wrapper that enables
4//! dynamic dispatch without monomorphization bloat.
5//!
6//! # When to Use
7//!
8//! **Prefer [`StopToken`](crate::StopToken)** which is `Clone` (via `Arc`).
9//! `BoxedStop` is retained for cases where unique ownership is required.
10//!
11//! Generic functions like `fn process(stop: impl Stop)` are monomorphized
12//! for each concrete type, increasing binary size. `BoxedStop` provides a
13//! single concrete type for dynamic dispatch:
14//!
15//! ```rust
16//! use almost_enough::{BoxedStop, Stop};
17//!
18//! // Single implementation - no monomorphization bloat
19//! fn process_boxed(stop: BoxedStop) {
20//!     // ...
21//! }
22//! ```
23//!
24//! # Alternatives
25//!
26//! For borrowed dynamic dispatch with zero allocation, use `&dyn Stop`:
27//!
28//! ```rust
29//! use almost_enough::{StopSource, Stop};
30//!
31//! fn process(stop: &dyn Stop) {
32//!     if stop.should_stop() {
33//!         return;
34//!     }
35//!     // ...
36//! }
37//!
38//! let source = StopSource::new();
39//! process(&source);
40//! ```
41
42use alloc::boxed::Box;
43
44use crate::{Stop, StopReason};
45
46/// A heap-allocated [`Stop`] implementation.
47///
48/// **Prefer [`StopToken`](crate::StopToken)** which is `Clone` (via `Arc`) and
49/// supports indirection collapsing. `BoxedStop` is retained for cases where
50/// unique ownership is required.
51///
52/// No-op stops (like `Unstoppable`) are optimized away at construction —
53/// `check()` short-circuits without any vtable dispatch.
54///
55/// # Example
56///
57/// ```rust
58/// use almost_enough::{BoxedStop, StopSource, Stopper, Unstoppable, Stop};
59///
60/// fn process(stop: BoxedStop) {
61///     for i in 0..1000 {
62///         if i % 100 == 0 && stop.should_stop() {
63///             return;
64///         }
65///         // process...
66///     }
67/// }
68///
69/// // Works with any Stop implementation
70/// process(BoxedStop::new(Unstoppable));
71/// process(BoxedStop::new(StopSource::new()));
72/// process(BoxedStop::new(Stopper::new()));
73/// ```
74pub struct BoxedStop(Option<Box<dyn Stop + Send + Sync>>);
75
76impl BoxedStop {
77    /// Create a new boxed stop from any [`Stop`] implementation.
78    ///
79    /// No-op stops (where `may_stop()` returns false) are not allocated —
80    /// `check()` will short-circuit to `Ok(())`.
81    #[inline]
82    pub fn new<T: Stop + 'static>(stop: T) -> Self {
83        if !stop.may_stop() {
84            return Self(None);
85        }
86        Self(Some(Box::new(stop)))
87    }
88}
89
90impl Stop for BoxedStop {
91    #[inline]
92    fn check(&self) -> Result<(), StopReason> {
93        match &self.0 {
94            Some(inner) => inner.check(),
95            None => Ok(()),
96        }
97    }
98
99    #[inline]
100    fn should_stop(&self) -> bool {
101        match &self.0 {
102            Some(inner) => inner.should_stop(),
103            None => false,
104        }
105    }
106
107    #[inline]
108    fn may_stop(&self) -> bool {
109        self.0.is_some()
110    }
111}
112
113impl core::fmt::Debug for BoxedStop {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        f.debug_tuple("BoxedStop").finish()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::{StopSource, Stopper, Unstoppable};
123
124    #[test]
125    fn boxed_stop_from_unstoppable() {
126        let stop = BoxedStop::new(Unstoppable);
127        assert!(!stop.should_stop());
128        assert!(stop.check().is_ok());
129        assert!(!stop.may_stop());
130    }
131
132    #[test]
133    fn boxed_stop_from_stopper() {
134        let stopper = Stopper::new();
135        let stop = BoxedStop::new(stopper.clone());
136
137        assert!(!stop.should_stop());
138
139        stopper.cancel();
140
141        assert!(stop.should_stop());
142        assert_eq!(stop.check(), Err(StopReason::Cancelled));
143    }
144
145    #[test]
146    fn boxed_stop_is_send_sync() {
147        fn assert_send_sync<T: Send + Sync>() {}
148        assert_send_sync::<BoxedStop>();
149    }
150
151    #[test]
152    fn boxed_stop_debug() {
153        let stop = BoxedStop::new(Unstoppable);
154        let debug = alloc::format!("{:?}", stop);
155        assert!(debug.contains("BoxedStop"));
156    }
157
158    #[test]
159    fn boxed_stop_avoids_monomorphization() {
160        fn process(stop: BoxedStop) -> bool {
161            stop.should_stop()
162        }
163
164        assert!(!process(BoxedStop::new(Unstoppable)));
165        assert!(!process(BoxedStop::new(StopSource::new())));
166        assert!(!process(BoxedStop::new(Stopper::new())));
167    }
168
169    #[test]
170    fn may_stop_delegates_through_boxed() {
171        assert!(!BoxedStop::new(Unstoppable).may_stop());
172        assert!(BoxedStop::new(Stopper::new()).may_stop());
173    }
174
175    #[test]
176    fn unstoppable_no_allocation() {
177        // Unstoppable wraps to None — no heap allocation
178        let stop = BoxedStop::new(Unstoppable);
179        assert!(!stop.may_stop());
180        assert!(stop.check().is_ok());
181    }
182}