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}