Skip to main content

how_far/
stages.rs

1//! Best-effort observation of sequential work; errors belong to the work.
2use crate::{AsStopReason, Child, Complete, Execution, Outcome, PhaseSpec, PlanError, Pulse};
3use alloc::{collections::VecDeque, sync::Arc};
4
5/// Sequential work returning the library's ordinary `Result<T, E>`.
6///
7/// Start with [`Stages::define`]: bind its reservations to variables, use
8/// [`Stages::begin`] to obtain each active handle, and call your operation
9/// with `&stage`. For repeated work over a runtime-sized list, [`Stages::new`]
10/// and [`Stages::begin_next`] provide the same handle and result handoff.
11///
12/// Implement [`AsStopReason`] for your error to recognize cancellation.
13/// `From<StopReason>` supports `?` at checks; no planning-error conversion is
14/// required. For explicitly fallible planning use [`crate::TryStages`].
15///
16/// Pass the final result to `complete` (or `ResultExt::finish_phase`) to resolve
17/// untouched stages. Use [`Stages::begin`] for direct function calls and hand
18/// each result to [`crate::ResultExt::finish_phase`] before applying `?`.
19/// Drop alone records abandonment. Recovery belongs within the operation that
20/// owns a stage, using separate child owners for individual attempts.
21#[must_use = "run stages and hand over the final result; Drop records abandonment"]
22pub struct Stages<'a> {
23    parent: &'a dyn Pulse,
24    pending: VecDeque<(Child<'a>, Option<StageKey>)>,
25    state: State,
26}
27enum State {
28    Ready,
29    Running,
30    Stopped,
31}
32
33impl<'a> Stages<'a> {
34    /// Declare a runtime-sized sequence for [`Stages::begin_next`] or [`Stages::run`].
35    ///
36    /// For a fixed set of distinct operations, [`Stages::define`] also gives
37    /// you reservations to bind to variables. Rejected observations retain
38    /// cancellation through fallback children and cannot fail the work.
39    #[track_caller]
40    pub fn new(parent: &'a dyn Pulse, parts: &[PhaseSpec<'_>]) -> Self {
41        let pending = parent
42            .plan(Execution::Sequence, parts)
43            .into_iter()
44            .map(|child| (child, None))
45            .collect();
46        Self {
47            parent,
48            pending,
49            state: State::Ready,
50        }
51    }
52
53    /// Declare a sequence and bind its reservations to variables.
54    ///
55    /// Destructure the returned array, then pass each key to [`Stages::begin`]
56    /// in declaration order. Keys belong to this particular plan, even when
57    /// another plan has identical specifications. Moving the plan is allowed.
58    /// Dropping a key does not abandon its pending stage: the plan retains
59    /// completion authority until the stage begins or the plan completes.
60    ///
61    /// Labels are for observers only; beginning a stage never compares strings.
62    /// One shared identity allocation belongs to each nonempty definition.
63    #[track_caller]
64    pub fn define<const N: usize>(
65        parent: &'a dyn Pulse,
66        parts: [PhaseSpec<'_>; N],
67    ) -> (Self, [StageKey; N]) {
68        let mut stages = Self::new(parent, &parts);
69        // Arc keeps identity stable across moves and prevents address reuse
70        // while a stale key survives. No global counter or mutable state.
71        let identity = (N != 0).then(|| Arc::new(()));
72        let keys = core::array::from_fn(|index| {
73            let key = StageKey {
74                identity: Arc::clone(identity.as_ref().expect("nonempty definition")),
75                index,
76            };
77            stages.pending[index].1 = Some(key.clone());
78            key
79        });
80        (stages, keys)
81    }
82
83    /// Begin the next stage using its reservation from [`Stages::define`].
84    ///
85    /// Hand the operation's result to [`crate::ResultExt::finish_phase`]
86    /// **before** applying `?`. Failed or cancelled handoffs immediately mark
87    /// remaining stages [`Outcome::NotRun`]. Complete the plan with its final
88    /// result to resolve untouched optional work.
89    ///
90    /// A key from another plan, out of order, or already used records
91    /// [`PlanError::StageMismatch`] and returns a cancellation-only stage
92    /// without consuming the next declaration. After failure or exhaustion,
93    /// work also retains cancellation and the protocol issue is recorded.
94    ///
95    /// Only one active stage can borrow the plan. Join workers before handing
96    /// over that stage's result. Dropping it unfinished records abandonment
97    /// and prevents the remaining sequence from earning progress.
98    #[track_caller]
99    pub fn begin(&mut self, key: StageKey) -> Stage<'_, 'a> {
100        self.begin_stage(Some(&key))
101    }
102
103    /// Begin the next stage in a runtime-sized sequence.
104    ///
105    /// Use with [`Stages::new`] when a loop repeats the same operation over a
106    /// slice. The handle and result handoff are the same as [`Stages::begin`],
107    /// but no key is checked. Exhaustion records [`PlanError::NoMoreStages`]
108    /// and returns a cancellation-only stage.
109    #[track_caller]
110    pub fn begin_next(&mut self) -> Stage<'_, 'a> {
111        self.begin_stage(None)
112    }
113
114    #[track_caller]
115    fn begin_stage(&mut self, key: Option<&StageKey>) -> Stage<'_, 'a> {
116        let child = self.begin_child(key);
117        Stage {
118            stages: Some(self),
119            child,
120        }
121    }
122
123    /// Run the next stage, preserving the original result and error identity.
124    /// After a stage error, later declared stages become NotRun. Calling again
125    /// is diagnosed and runs against a cancellation-only view of the parent.
126    #[track_caller]
127    pub fn run<T, E>(&mut self, work: impl FnOnce(&dyn Pulse) -> Result<T, E>) -> Result<T, E>
128    where
129        E: AsStopReason,
130    {
131        let child = self.begin_child(None);
132        let result = work(&child);
133        self.complete_result(child, result)
134    }
135
136    /// Run with an explicit classifier for foreign error wrappers or a local
137    /// classification policy, returning the work's original Result unchanged.
138    #[track_caller]
139    pub fn run_classified<T, E>(
140        &mut self,
141        is_stop: impl FnOnce(&E) -> bool,
142        work: impl FnOnce(&dyn Pulse) -> Result<T, E>,
143    ) -> Result<T, E> {
144        let child = self.begin_child(None);
145        let result = work(&child);
146        self.end(child, Outcome::from_result(&result, is_stop));
147        result
148    }
149
150    // Keep bookkeeping out of the per-closure monomorphization.
151    fn complete_result<T, E>(&mut self, child: Child<'_>, result: Result<T, E>) -> Result<T, E>
152    where
153        E: AsStopReason,
154    {
155        let outcome = Outcome::from_result(&result, |error| error.as_stop_reason().is_some());
156        self.end(child, outcome);
157        result
158    }
159
160    #[track_caller]
161    fn begin_child(&mut self, key: Option<&StageKey>) -> Child<'a> {
162        let child = self.next(key);
163        if !matches!(self.state, State::Stopped) {
164            self.state = State::Running;
165        }
166        if let Err(error) = child.start() {
167            self.parent.record_issue(error);
168        }
169        child
170    }
171
172    fn end(&mut self, child: Child<'_>, outcome: Outcome) {
173        child.complete_as(outcome);
174        if matches!(
175            outcome,
176            Outcome::Failed | Outcome::Cancelled | Outcome::Abandoned | Outcome::NotRun
177        ) {
178            self.state = State::Stopped;
179            self.resolve_remaining(Outcome::NotRun);
180        } else if matches!(self.state, State::Running) {
181            self.state = State::Ready;
182        }
183    }
184
185    /// Explicitly mark the next phase unnecessary, making that decision visible now.
186    #[track_caller]
187    pub fn skip(&mut self) {
188        self.next(None).complete_as(Outcome::Skipped);
189    }
190
191    #[track_caller]
192    fn next(&mut self, key: Option<&StageKey>) -> Child<'a> {
193        if matches!(self.state, State::Running) {
194            // A prior closure panicked, and the caller caught it.
195            self.state = State::Stopped;
196            self.resolve_remaining(Outcome::NotRun);
197        }
198        if matches!(self.state, State::Stopped) {
199            self.parent.record_issue(PlanError::Finished);
200        } else if let Some((_, expected)) = self.pending.front() {
201            if key.is_some_and(|key| {
202                !expected
203                    .as_ref()
204                    .is_some_and(|expected| expected.matches(key))
205            }) {
206                self.parent.record_issue(PlanError::StageMismatch);
207            } else {
208                return self.pending.pop_front().expect("checked pending stage").0;
209            }
210        } else {
211            self.parent.record_issue(PlanError::NoMoreStages);
212        }
213        Child::untracked(self.parent)
214    }
215
216    fn resolve_remaining(&mut self, outcome: Outcome) {
217        while let Some((child, _)) = self.pending.pop_front() {
218            child.complete_inferred(outcome);
219        }
220    }
221}
222impl Complete for Stages<'_> {
223    fn complete_as(mut self, outcome: Outcome) {
224        self.resolve_remaining(match outcome {
225            Outcome::Succeeded | Outcome::Skipped => Outcome::Skipped,
226            Outcome::Abandoned => Outcome::Abandoned,
227            _ => Outcome::NotRun,
228        });
229    }
230}
231
232/// A reservation identifying one stage in one [`Stages::define`] plan.
233///
234/// Pass it to [`Stages::begin`]. It carries no progress or completion rights;
235/// dropping it leaves the stage pending. A clone identifies the same stage,
236/// not another execution: attempting to begin it twice is diagnosed.
237#[derive(Clone, Debug)]
238pub struct StageKey {
239    identity: Arc<()>,
240    index: usize,
241}
242impl StageKey {
243    fn matches(&self, other: &Self) -> bool {
244        self.index == other.index && Arc::ptr_eq(&self.identity, &other.identity)
245    }
246}
247
248/// One active sequential stage, borrowing its plan until result handoff.
249///
250/// Pass `&stage` as the operation's [`Pulse`], then use
251/// [`crate::ResultExt::finish_phase`] or [`Complete::complete`] before `?`.
252/// Completion consumes the handle and updates its plan. Dropping it without
253/// completion records [`Outcome::Abandoned`] and resolves pending work as
254/// [`Outcome::NotRun`]. Shared views have no completion rights.
255///
256/// A second stage cannot begin while the first is live:
257///
258/// ```compile_fail,E0499
259/// use how_far::{prelude::*, NoPulse, PhaseSpec, Stages, Total};
260/// let (mut stages, [a, b]) = Stages::define(&NoPulse, [
261///     PhaseSpec::new("a", 1, Total::Unknown),
262///     PhaseSpec::new("b", 1, Total::Unknown),
263/// ]);
264/// let first = stages.begin(a);
265/// let second = stages.begin(b);
266/// first.complete(Ok::<_, how_far::StopReason>(())).unwrap();
267/// ```
268#[must_use = "hand over the stage result before ?; dropping it records Abandoned"]
269pub struct Stage<'s, 'p> {
270    stages: Option<&'s mut Stages<'p>>,
271    child: Child<'p>,
272}
273impl Stage<'_, '_> {
274    /// Start independently owned branches within this stage.
275    ///
276    /// This declares [`Execution::ForkJoin`] and starts each branch; it does
277    /// not schedule work. Move the handles into scoped threads, Rayon work,
278    /// or borrowed async futures. Each branch can use different data and
279    /// return a different type, or declare a nested [`Stages`] sequence.
280    ///
281    /// Hand each result to its branch, join **all** workers, then hand the
282    /// combined result to this stage. A failed branch does not automatically
283    /// cancel its siblings. Every returned branch is active: dropping one
284    /// unfinished records [`Outcome::Abandoned`], not optional skipped work.
285    /// Rejected plans retain cancellation in every branch and record an issue.
286    ///
287    /// The parent cannot be completed while a branch still borrows it:
288    ///
289    /// ```compile_fail,E0505
290    /// use how_far::{prelude::*, NoPulse, PhaseSpec, Stages, Total, Outcome};
291    /// let (mut stages, [work]) = Stages::define(&NoPulse, [
292    ///     PhaseSpec::new("work", 1, Total::Unknown),
293    /// ]);
294    /// let work = stages.begin(work);
295    /// let [branch] = work.fork([PhaseSpec::new("branch", 1, Total::Unknown)]);
296    /// work.complete_as(Outcome::Succeeded);
297    /// branch.complete_as(Outcome::Succeeded);
298    /// ```
299    #[track_caller]
300    pub fn fork<const N: usize>(&self, parts: [PhaseSpec<'_>; N]) -> [Child<'_>; N] {
301        let mut children = self.fork_children(&parts).into_iter();
302        // Best-effort planning guarantees exactly one handle per declaration,
303        // including when a sink violates its split contract.
304        core::array::from_fn(|_| children.next().expect("one child per declaration"))
305    }
306
307    #[track_caller]
308    fn fork_children(&self, parts: &[PhaseSpec<'_>]) -> alloc::vec::Vec<Child<'_>> {
309        let children = self.child.plan(Execution::ForkJoin, parts);
310        for child in &children {
311            if let Err(error) = child.start() {
312                self.child.record_issue(error);
313            }
314        }
315        children
316    }
317
318    fn close(&mut self, outcome: Outcome) {
319        if let Some(stages) = self.stages.take() {
320            let child = core::mem::replace(&mut self.child, Child::inert());
321            stages.end(child, outcome);
322        }
323    }
324}
325impl core::fmt::Debug for Stage<'_, '_> {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        f.debug_struct("Stage")
328            .field("child", &self.child)
329            .finish_non_exhaustive()
330    }
331}
332impl Complete for Stage<'_, '_> {
333    fn complete_as(mut self, outcome: Outcome) {
334        self.close(outcome);
335    }
336}
337impl Drop for Stage<'_, '_> {
338    fn drop(&mut self) {
339        self.close(Outcome::Abandoned);
340    }
341}
342impl crate::Stop for Stage<'_, '_> {
343    #[inline]
344    #[track_caller]
345    fn check(&self) -> Result<(), crate::StopReason> {
346        self.child.check()
347    }
348    #[inline]
349    fn may_stop(&self) -> bool {
350        self.child.may_stop()
351    }
352}
353impl crate::Report for Stage<'_, '_> {
354    #[inline]
355    #[track_caller]
356    fn advance(&self, completed: u64) {
357        self.child.advance(completed);
358    }
359    #[inline]
360    fn may_report(&self) -> bool {
361        self.child.may_report()
362    }
363}
364impl Pulse for Stage<'_, '_> {
365    #[track_caller]
366    fn record_issue(&self, error: PlanError) {
367        self.child.record_issue(error);
368    }
369    fn split(
370        &self,
371        execution: Execution,
372        parts: &[PhaseSpec<'_>],
373    ) -> Result<alloc::vec::Vec<Child<'_>>, PlanError> {
374        self.child.split(execution, parts)
375    }
376    fn start(&self) -> Result<(), PlanError> {
377        self.child.start()
378    }
379    fn set_total(&self, total: crate::Total) -> Result<(), PlanError> {
380        self.child.set_total(total)
381    }
382    fn share(&self) -> Result<crate::SharedPulse, PlanError> {
383        self.child.share()
384    }
385}