Skip to main content

how_far/
pulse.rs

1//! Phase planning and the combined cancellation/progress interface.
2
3use crate::Report;
4use alloc::{boxed::Box, sync::Arc, vec::Vec};
5use core::{fmt, num::NonZeroUsize};
6use enough::{Stop, StopReason};
7
8/// The denominator for completed work in one phase.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum Total {
12    /// A known count. Counting past it is recorded as an overrun.
13    Exact(u64),
14    /// A revisable estimate. Reaching it does not finish the phase.
15    Estimated(u64),
16    /// No useful denominator yet.
17    Unknown,
18}
19
20/// How a phase's work is scheduled. Declaring it schedules nothing.
21///
22/// On a leaf, it describes the leaf's own units: `WorkPool` means several
23/// workers share one count. On a branch, it describes how the children run.
24/// When a phase splits, the execution passed to [`Pulse::split`] replaces the
25/// one in its [`PhaseSpec`].
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Execution {
29    /// No scheduling relationship is declared.
30    #[default]
31    Unspecified,
32    /// One after another, in declared order.
33    Sequence,
34    /// Concurrently; all join before the parent finishes.
35    ForkJoin,
36    /// Shared by a pool of at most `max_parallelism` workers.
37    /// Build it with [`Execution::work_pool`].
38    #[non_exhaustive]
39    WorkPool {
40        /// The pool's concurrency ceiling, not a count of dedicated cores.
41        max_parallelism: NonZeroUsize,
42    },
43}
44
45impl Execution {
46    /// Work shared by a pool of at most `max_parallelism` workers, such as
47    /// `std::thread::available_parallelism()` or a Rayon pool's thread count.
48    pub const fn work_pool(max_parallelism: NonZeroUsize) -> Self {
49        Self::WorkPool { max_parallelism }
50    }
51}
52
53/// How a phase ended. Reaching a count total is not an outcome.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum Outcome {
57    /// All of the phase's work completed.
58    Succeeded,
59    /// The planned work turned out to be unnecessary.
60    Skipped,
61    /// Work stopped cooperatively, after a cancellation request or timeout.
62    Cancelled,
63    /// Work failed for a reason other than a stop request.
64    Failed,
65    /// The phase's owner went away without publishing an outcome.
66    Abandoned,
67    /// Prevented from running by an earlier error; earns no completed share.
68    NotRun,
69}
70
71impl Outcome {
72    /// The outcome of an operation's result: `Succeeded` for `Ok`, `Cancelled`
73    /// for an error `is_stop` accepts, and `Failed` for any other error.
74    ///
75    /// ```
76    /// use how_far::{Outcome, StopReason};
77    ///
78    /// let stopped: Result<(), StopReason> = Err(StopReason::Cancelled);
79    /// assert_eq!(Outcome::from_result(&stopped, |_| true), Outcome::Cancelled);
80    /// ```
81    pub fn from_result<T, E>(result: &Result<T, E>, is_stop: impl FnOnce(&E) -> bool) -> Self {
82        match result {
83            Ok(_) => Self::Succeeded,
84            Err(error) if is_stop(error) => Self::Cancelled,
85            Err(_) => Self::Failed,
86        }
87    }
88}
89
90/// A phase plan that cannot be applied, or a lifecycle step out of order.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum PlanError {
94    /// A split needs at least one part, and every weight must be positive.
95    EmptyOrZeroWeight,
96    /// The weights' sum or the job's phase identifiers overflowed.
97    Overflow,
98    /// The phase already counted work or planned children. Plan once, first.
99    AlreadyInUse,
100    /// The phase has children; only a leaf has its own total.
101    NotALeaf,
102    /// This pulse cannot plan children.
103    Unsupported,
104    /// This borrowed implementation cannot provide an owned pulse.
105    NotShareable,
106    /// The phase already has an outcome.
107    Finished,
108    /// Every child must finish before its parent.
109    UnfinishedChildren,
110    /// Every declared stage has already run.
111    NoMoreStages,
112    /// A stage reservation does not match the next declaration in this sequence.
113    StageMismatch,
114}
115
116impl fmt::Display for PlanError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(match self {
119            Self::EmptyOrZeroWeight => "a split needs at least one part with a positive weight",
120            Self::Overflow => "phase weights or identifiers overflowed",
121            Self::AlreadyInUse => "the phase already counted work or planned children",
122            Self::NotALeaf => "only a leaf phase has its own total",
123            Self::Unsupported => "this pulse cannot plan children",
124            Self::NotShareable => "this pulse cannot provide an owned view",
125            Self::Finished => "the phase already has an outcome",
126            Self::UnfinishedChildren => "every child must finish before its parent",
127            Self::NoMoreStages => "every declared stage has already run",
128            Self::StageMismatch => "stage reservation does not match the next declaration",
129        })
130    }
131}
132
133impl core::error::Error for PlanError {}
134
135/// One child in a split: a name, a weight relative to its siblings, a total,
136/// and optionally a unit name and an execution model.
137///
138/// Weights are relative, so `[35, 30, 35]` gives the middle child exactly 30%
139/// of its parent however many workers it later uses. Build it with
140/// [`PhaseSpec::new`]; the fields stay readable, and new planning options can
141/// be added without breaking existing code.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct PhaseSpec<'a> {
145    /// Human-readable phase name.
146    pub name: &'a str,
147    /// Positive weight relative to the siblings.
148    pub weight: u64,
149    /// Denominator for this phase's completed units.
150    pub total: Total,
151    /// Name of the counted unit, such as `rows` or `superblocks`.
152    pub units: &'a str,
153    /// How this phase's work is scheduled.
154    pub execution: Execution,
155}
156
157impl<'a> PhaseSpec<'a> {
158    /// Describe one weighted child that counts `items`.
159    pub const fn new(name: &'a str, weight: u64, total: Total) -> Self {
160        Self {
161            name,
162            weight,
163            total,
164            units: "items",
165            execution: Execution::Unspecified,
166        }
167    }
168
169    /// Name the counted unit.
170    pub const fn units(mut self, units: &'a str) -> Self {
171        self.units = units;
172        self
173    }
174
175    /// Declare how this phase's work is scheduled.
176    pub const fn execution(mut self, execution: Execution) -> Self {
177        self.execution = execution;
178        self
179    }
180
181    /// Check a split's parts the way every [`Pulse::split`] must, and return
182    /// the sum of their weights.
183    ///
184    /// Fails with [`PlanError::EmptyOrZeroWeight`] for no parts or a zero
185    /// weight, and [`PlanError::Overflow`] if the weights' sum overflows. Call
186    /// it first in a `split` implementation, so every pulse rejects the same
187    /// plans with the same errors.
188    pub fn validate_split(parts: &[PhaseSpec<'_>]) -> Result<u64, PlanError> {
189        // Plain loops keep this crate's compiled code small.
190        if parts.is_empty() {
191            return Err(PlanError::EmptyOrZeroWeight);
192        }
193        let mut sum = 0_u64;
194        for part in parts {
195            if part.weight == 0 {
196                return Err(PlanError::EmptyOrZeroWeight);
197            }
198            sum = sum.checked_add(part.weight).ok_or(PlanError::Overflow)?;
199        }
200        Ok(sum)
201    }
202}
203
204/// Cancellation, completed work, and nested phase planning, in one value.
205///
206/// A library accepts `&dyn Pulse` and uses it three ways:
207///
208/// - `check()` asks whether to stop. It is the cheapest call, unless the
209///   caller made it run code of its own, as a checkpoint
210///   callback does; check once per row, block or tile, not per byte.
211/// - `advance(n)` counts finished units; [`ProgressExt::step`](crate::ProgressExt::step)
212///   counts and then checks.
213/// - [`split`](Self::split) declares weighted children. The library owns the
214///   returned [`Child`] handles and finishes each one. It never finishes the
215///   pulse it was given; that pulse belongs to its caller.
216///
217/// Splitting and counting are exclusive: a phase either counts its own units
218/// (a leaf) or delegates to children (a branch). Plan before the first report.
219///
220/// Wrappers that add measurement or forwarding implement this trait, so it is
221/// open. Methods added in later compatible releases will have default bodies.
222///
223/// # Implementing
224///
225/// A tracker or checkpoint callback adapter from `how-far-along` usually
226/// suffices. Implementations must keep these rules:
227///
228/// - `check` and `advance` may run on many threads at once, and `advance` must
229///   not block. `may_stop` and `may_report` return `false` only when that can
230///   never change.
231/// - `split` starts with [`PhaseSpec::validate_split`]. It fails with
232///   [`PlanError::AlreadyInUse`] if this phase already counted work or split,
233///   and otherwise returns one [`Child::new`] per part, in order, each
234///   checking and counting like any pulse. Children from [`Child::inert`]
235///   cannot be stopped or observed, so they suit only parts nobody watches.
236/// - [`share`](Self::share) returns an owned view that checks the same stop,
237///   counts into the same phase and plans its children, or fails with
238///   [`PlanError::NotShareable`]; it never silently drops cancellation.
239/// - [`ChildPulse::finish`] fails with [`PlanError::UnfinishedChildren`] while
240///   a descendant is unfinished. Completed failed attempts may belong to a
241///   successful parent. A dropped unfinished child records `Abandoned`.
242pub trait Pulse: Stop + Report {
243    /// Record a reporting-protocol problem for optional diagnostics. This never
244    /// changes cancellation or the operation's return value. Ordinary sinks may
245    /// ignore it; diagnostic wrappers retain bounded evidence.
246    #[track_caller]
247    fn record_issue(&self, _error: PlanError) {}
248    /// Split this phase into weighted children, in declared order.
249    ///
250    /// Returns exactly one [`Child`] per part. The caller owns them: pass
251    /// `&child` to code that does the child's work, then finish the child once
252    /// that work has joined. Fails if the parts are invalid, if this phase
253    /// already counted work or split, or if the pulse cannot plan children.
254    fn split(
255        &self,
256        execution: Execution,
257        parts: &[PhaseSpec<'_>],
258    ) -> Result<Vec<Child<'_>>, PlanError>;
259
260    /// Mark this phase running, without counting or ending planning.
261    /// Implementations without lifecycle observations may ignore this.
262    fn start(&self) -> Result<(), PlanError> {
263        Ok(())
264    }
265
266    /// Revise a leaf's denominator without changing completed units or weights.
267    /// Unsupported implementations return an error instead of losing the update.
268    fn set_total(&self, _total: Total) -> Result<(), PlanError> {
269        Err(PlanError::Unsupported)
270    }
271
272    /// Share all pulse capabilities with owned work, but never completion rights.
273    ///
274    /// The [`SharedPulse`] is `'static`, cloneable and thread-safe, and works
275    /// wherever a `Stop` or `Report` is expected: give it to a codec context that
276    /// stores `impl Stop + 'static`, a spawned thread, or an async task. Join
277    /// such work before finishing the original owner. Unsupported ownership is
278    /// explicit; this method never silently replaces cancellation with a no-op.
279    fn share(&self) -> Result<SharedPulse, PlanError> {
280        Err(PlanError::NotShareable)
281    }
282}
283
284// A reference, box or `Arc` of a pulse is a pulse, as for `Stop` and `Report`,
285// so generic and owned code can hold any pulse. Checkpoints recognize only
286// `&NoPulse` itself, so `&&NoPulse` works but pays for its calls.
287
288/// Forward every `Pulse` method through a pointer-like wrapper.
289macro_rules! forward_pulse {
290    ($($wrapper:ty),*) => {$(
291        impl<P: Pulse + ?Sized> Pulse for $wrapper {
292            #[track_caller]
293            fn record_issue(&self, error: PlanError) {
294                (**self).record_issue(error);
295            }
296            #[inline]
297            fn split(
298                &self,
299                execution: Execution,
300                parts: &[PhaseSpec<'_>],
301            ) -> Result<Vec<Child<'_>>, PlanError> {
302                (**self).split(execution, parts)
303            }
304            fn start(&self) -> Result<(), PlanError> {
305                (**self).start()
306            }
307            fn set_total(&self, total: Total) -> Result<(), PlanError> {
308                (**self).set_total(total)
309            }
310            fn share(&self) -> Result<SharedPulse, PlanError> {
311                (**self).share()
312            }
313        }
314    )*};
315}
316forward_pulse!(&P, &mut P, Box<P>, Arc<P>);
317
318/// What a pulse's children add: publishing a terminal outcome.
319///
320/// Implement this for the type your [`Pulse::split`] returns, and wrap each
321/// child with [`Child::new`]. Library code never calls it directly; it calls
322/// [`Child::finish`], which consumes the handle so a child is finished at most
323/// once.
324pub trait ChildPulse: Pulse {
325    /// Resolve an untouched phase from its enclosing result. The default uses
326    /// ordinary completion; trackers can retain the inference as evidence.
327    fn complete_inferred(self: Box<Self>, outcome: Outcome) {
328        self.complete_as(outcome);
329    }
330    /// Complete best-effort observation. Trackers may infer untouched descendants
331    /// from this result; the default preserves older implementors' finish logic.
332    fn complete_as(self: Box<Self>, outcome: Outcome) {
333        let _ = self.finish(outcome);
334    }
335    /// Publish this child's terminal outcome.
336    fn finish(self: Box<Self>, outcome: Outcome) -> Result<(), PlanError>;
337}
338
339/// A planned child phase, owned by the code that called [`Pulse::split`].
340///
341/// A `Child` is a [`Pulse`]: pass `&child` wherever `&dyn Pulse` is expected,
342/// or call `check`, `step` and `split` on it directly. Finish it once, after
343/// its work has joined. A tracker records `Outcome::Abandoned` for a child that
344/// is dropped unfinished, including on unwinding. It is two words wide.
345/// Completion consumes the owner, so it cannot be repeated:
346///
347/// ```compile_fail,E0382
348/// use how_far::{Child, Outcome};
349/// let child = Child::inert();
350/// child.finish(Outcome::Succeeded).unwrap();
351/// child.finish(Outcome::Succeeded).unwrap();
352/// ```
353#[must_use = "finish the child after its work joins; dropping a tracked child abandons it"]
354pub struct Child<'a> {
355    /// `None` for a child of [`NoPulse`], which needs no allocation.
356    pulse: Option<Box<dyn ChildPulse + 'a>>,
357}
358
359impl<'a> Child<'a> {
360    /// Resolve an unused phase from its enclosing result, retaining inference evidence.
361    pub fn complete_inferred(self, outcome: Outcome) {
362        if let Some(pulse) = self.pulse {
363            pulse.complete_inferred(outcome);
364        }
365    }
366    /// A cancellation/checkpoint-preserving view without progress accounting.
367    pub(crate) fn untracked(parent: &'a dyn Pulse) -> Self {
368        Self::new(Untracked { inner: parent })
369    }
370
371    /// Plan nested observations without introducing a new work error.
372    #[track_caller]
373    pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
374        self.pulse().plan(execution, parts)
375    }
376    /// Wrap one child returned from a [`Pulse::split`] implementation.
377    pub fn new(pulse: impl ChildPulse + 'a) -> Self {
378        Self {
379            pulse: Some(Box::new(pulse)),
380        }
381    }
382
383    /// A child like those [`NoPulse`] hands out: it never stops, discards
384    /// reports, and finishing it does nothing. Its work cannot be stopped
385    /// through it and nobody sees its progress, so return it from
386    /// [`Pulse::split`] only for parts that nobody needs to observe or stop.
387    pub const fn inert() -> Self {
388        Self { pulse: None }
389    }
390
391    /// Publish this child's outcome. Its parent can finish only after every
392    /// child has finished.
393    ///
394    /// Fails without recording `outcome` if the child's own children are still
395    /// running; the consumed child is then dropped, so a tracker records it as
396    /// abandoned. Finished children's outcomes need not match: a successful
397    /// child may retain a failed attempt it recovered from.
398    pub fn finish(self, outcome: Outcome) -> Result<(), PlanError> {
399        match self.pulse {
400            Some(pulse) => pulse.finish(outcome),
401            None => Ok(()),
402        }
403    }
404
405    /// The child's own pulse, without the forwarding a `Child` adds when it is
406    /// itself used as a `&dyn Pulse`: the static [`NoPulse`] for its children.
407    #[inline]
408    pub(crate) fn pulse(&self) -> &(dyn Pulse + 'a) {
409        match &self.pulse {
410            Some(pulse) => &**pulse,
411            None => &NoPulse,
412        }
413    }
414}
415
416impl fmt::Debug for Child<'_> {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        f.debug_struct("Child").finish_non_exhaustive()
419    }
420}
421
422impl crate::Complete for Child<'_> {
423    fn complete_as(self, outcome: Outcome) {
424        if let Some(pulse) = self.pulse {
425            pulse.complete_as(outcome);
426        }
427    }
428}
429
430impl dyn Pulse + '_ {
431    /// Best-effort planning. If the sink rejects the plan, returns one untracked
432    /// child per part, all retaining this pulse's stop/checkpoint policy.
433    #[track_caller]
434    pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
435        match self.split(execution, parts) {
436            Ok(children) if children.len() == parts.len() => children,
437            Ok(_) => {
438                self.record_issue(PlanError::Unsupported);
439                untracked_children(self, parts.len())
440            }
441            Err(error) => {
442                self.record_issue(error);
443                untracked_children(self, parts.len())
444            }
445        }
446    }
447}
448
449/// `count` unobserved children that all check `parent`'s stop policy.
450pub(crate) fn untracked_children(parent: &dyn Pulse, count: usize) -> Vec<Child<'_>> {
451    let mut children = Vec::with_capacity(count);
452    for _ in 0..count {
453        children.push(Child::untracked(parent));
454    }
455    children
456}
457
458struct Untracked<P> {
459    inner: P,
460}
461impl<P: Pulse> Stop for Untracked<P> {
462    #[track_caller]
463    fn check(&self) -> Result<(), StopReason> {
464        self.inner.check()
465    }
466    fn may_stop(&self) -> bool {
467        self.inner.may_stop()
468    }
469}
470impl<P: Pulse> Report for Untracked<P> {
471    fn advance(&self, _: u64) {}
472    fn may_report(&self) -> bool {
473        false
474    }
475}
476impl<P: Pulse> Pulse for Untracked<P> {
477    fn split(&self, _: Execution, parts: &[PhaseSpec<'_>]) -> Result<Vec<Child<'_>>, PlanError> {
478        PhaseSpec::validate_split(parts)?;
479        Ok(untracked_children(&self.inner, parts.len()))
480    }
481    /// Nobody observes an untracked phase, as with `NoPulse`.
482    fn set_total(&self, _: Total) -> Result<(), PlanError> {
483        Ok(())
484    }
485    fn share(&self) -> Result<SharedPulse, PlanError> {
486        Ok(SharedPulse::new(Untracked {
487            inner: self.inner.share()?,
488        }))
489    }
490    #[track_caller]
491    fn record_issue(&self, error: PlanError) {
492        self.inner.record_issue(error);
493    }
494}
495impl<P: Pulse> ChildPulse for Untracked<P> {
496    fn finish(self: Box<Self>, _: Outcome) -> Result<(), PlanError> {
497        Ok(())
498    }
499}
500
501impl Stop for Child<'_> {
502    #[inline]
503    #[track_caller]
504    fn check(&self) -> Result<(), StopReason> {
505        match &self.pulse {
506            Some(pulse) => pulse.check(),
507            None => Ok(()),
508        }
509    }
510    #[inline]
511    fn may_stop(&self) -> bool {
512        matches!(&self.pulse, Some(pulse) if pulse.may_stop())
513    }
514}
515
516impl Report for Child<'_> {
517    #[inline]
518    #[track_caller]
519    fn advance(&self, completed: u64) {
520        if let Some(pulse) = &self.pulse {
521            pulse.advance(completed);
522        }
523    }
524    #[inline]
525    fn may_report(&self) -> bool {
526        matches!(&self.pulse, Some(pulse) if pulse.may_report())
527    }
528}
529
530impl Pulse for Child<'_> {
531    #[track_caller]
532    fn record_issue(&self, error: PlanError) {
533        self.pulse().record_issue(error);
534    }
535    fn split(
536        &self,
537        execution: Execution,
538        parts: &[PhaseSpec<'_>],
539    ) -> Result<Vec<Child<'_>>, PlanError> {
540        self.pulse().split(execution, parts)
541    }
542    fn start(&self) -> Result<(), PlanError> {
543        self.pulse().start()
544    }
545    fn set_total(&self, total: Total) -> Result<(), PlanError> {
546        self.pulse().set_total(total)
547    }
548    fn share(&self) -> Result<SharedPulse, PlanError> {
549        self.pulse().share()
550    }
551}
552
553/// The type of [`NoPulse`]. It has exactly one value, that static: nothing
554/// outside this crate can build, copy or move one.
555pub struct Inert {
556    /// One byte, so that the static has an address of its own.
557    _unique: u8,
558}
559
560/// Never stops and discards reports, including in every nested phase.
561///
562/// Pass `&NoPulse`. [`step`](crate::ProgressExt::step),
563/// [`live`](crate::ProgressExt::live) and [`Paced`](crate::Paced) recognize it
564/// by its address, so a loop that steps on every iteration runs with no
565/// checkpoint code when nobody listens: the one comparison moves out of the
566/// loop. Where it cannot, it is a comparison and a branch. A bare `check()` or
567/// `advance()` through a `&dyn Pulse` still makes one call, to a function that
568/// returns at once. A pulse that wraps it, such as `&&NoPulse` or a
569/// `Box<&NoPulse>`, works but is not recognized, and pays for both calls.
570///
571/// Its children and stages are free the same way and need no allocation. It
572/// still validates plans, so a library's planning mistakes surface even when
573/// nobody observes it.
574#[allow(non_upper_case_globals)]
575pub static NoPulse: Inert = Inert { _unique: 0 };
576
577impl fmt::Debug for Inert {
578    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579        f.write_str("NoPulse")
580    }
581}
582
583impl Stop for Inert {
584    #[inline(always)]
585    fn check(&self) -> Result<(), StopReason> {
586        Ok(())
587    }
588    #[inline(always)]
589    fn may_stop(&self) -> bool {
590        false
591    }
592}
593
594impl Report for Inert {
595    #[inline(always)]
596    fn advance(&self, _: u64) {}
597    #[inline(always)]
598    fn may_report(&self) -> bool {
599        false
600    }
601}
602
603impl Pulse for Inert {
604    fn split(&self, _: Execution, parts: &[PhaseSpec<'_>]) -> Result<Vec<Child<'_>>, PlanError> {
605        PhaseSpec::validate_split(parts)?;
606        let mut children = Vec::with_capacity(parts.len());
607        for _ in parts {
608            children.push(Child::inert());
609        }
610        Ok(children)
611    }
612    fn set_total(&self, _: Total) -> Result<(), PlanError> {
613        Ok(())
614    }
615    fn share(&self) -> Result<SharedPulse, PlanError> {
616        Ok(SharedPulse::default())
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623    use crate::ProgressExt;
624    use alloc::format;
625
626    #[test]
627    fn no_pulse_children_are_the_static_and_need_no_allocation() {
628        let parts = [
629            PhaseSpec::new("a", 1, Total::Exact(1)),
630            PhaseSpec::new("b", 1, Total::Exact(1)),
631        ];
632        let children = NoPulse.split(Execution::Sequence, &parts).unwrap();
633        for child in children {
634            assert!(child.pulse.is_none(), "no boxed pulse");
635            assert!(core::ptr::addr_eq(child.pulse(), &NoPulse));
636            assert!(!child.may_stop() && !child.may_report());
637            assert!(child.live().is_none());
638            assert_eq!(child.step(5), Ok(()));
639            assert_eq!(
640                format!("{:?}", child.paced(1)),
641                "Paced { live: false, pending: 0, every: 1 }"
642            );
643            child.finish(Outcome::Succeeded).unwrap();
644        }
645        assert_eq!(
646            NoPulse.split(Execution::Sequence, &[]).err(),
647            Some(PlanError::EmptyOrZeroWeight)
648        );
649    }
650}
651
652/// An owned, cloneable pulse that retains planning and cancellation capabilities.
653/// The original phase owner alone may finish it. Requires pointer atomics.
654///
655/// A shared view has no completion operation:
656///
657/// ```compile_fail,E0599
658/// use how_far::{Outcome, SharedPulse};
659/// let shared = SharedPulse::default();
660/// shared.finish(Outcome::Succeeded).unwrap();
661/// ```
662#[derive(Clone, Default)]
663pub struct SharedPulse(Option<Arc<dyn Pulse>>);
664impl SharedPulse {
665    /// Erase an owned implementation. The implementation must not own the
666    /// phase's completion token: dropping clones must not finish the phase.
667    pub fn new(pulse: impl Pulse + 'static) -> Self {
668        Self(Some(Arc::new(pulse)))
669    }
670    /// Borrow the underlying pulse, preserving the canonical no-op fast path.
671    pub fn as_pulse(&self) -> &dyn Pulse {
672        match &self.0 {
673            Some(p) => &**p,
674            None => &NoPulse,
675        }
676    }
677
678    /// Best-effort planning, as for a borrowed pulse; see `<dyn Pulse>::plan`.
679    #[track_caller]
680    pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
681        self.as_pulse().plan(execution, parts)
682    }
683}
684impl fmt::Debug for SharedPulse {
685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686        f.debug_struct("SharedPulse")
687            .field("live", &self.0.is_some())
688            .finish()
689    }
690}
691impl Stop for SharedPulse {
692    #[track_caller]
693    fn check(&self) -> Result<(), StopReason> {
694        self.as_pulse().check()
695    }
696    fn may_stop(&self) -> bool {
697        self.as_pulse().may_stop()
698    }
699}
700impl Report for SharedPulse {
701    #[track_caller]
702    fn advance(&self, n: u64) {
703        self.as_pulse().advance(n)
704    }
705    fn may_report(&self) -> bool {
706        self.as_pulse().may_report()
707    }
708}
709impl Pulse for SharedPulse {
710    #[track_caller]
711    fn record_issue(&self, error: PlanError) {
712        self.as_pulse().record_issue(error);
713    }
714    fn split(
715        &self,
716        execution: Execution,
717        parts: &[PhaseSpec<'_>],
718    ) -> Result<Vec<Child<'_>>, PlanError> {
719        self.as_pulse().split(execution, parts)
720    }
721    fn start(&self) -> Result<(), PlanError> {
722        self.as_pulse().start()
723    }
724    fn set_total(&self, total: Total) -> Result<(), PlanError> {
725        self.as_pulse().set_total(total)
726    }
727    fn share(&self) -> Result<SharedPulse, PlanError> {
728        Ok(self.clone())
729    }
730}