Skip to main content

behavior_actors/lifecycle/
child_shutdown.rs

1//! Shutdown phases derived from committed direct-child creation results.
2
3use core::marker::PhantomData;
4
5use behavior::{
6    Actions, Address, Behavior, BirthMode, ChildChoice, ChildHead, ChildOccurrenceProduct,
7    ChildOccurrenceShape, ChildOccurrences, ChildTail, CreationId, CreationKind, EventLayer,
8    InterpreterRequests, Never, ResolveChildOccurrence, SendEffects, SendLayer, Step,
9};
10
11use super::shutdown_coordinator::heterogeneous;
12use super::{
13    HeterogeneousShutdownCoordinator, HeterogeneousShutdownPlan, NoShutdownTargets,
14    ReportShutdownPlan, ShutdownChoice, ShutdownPlanError, ShutdownTargetAt,
15};
16use crate::{CreationResolved, ObserveCreation};
17use behavior::Protocol;
18
19/// Creation state expected by the shutdown-plan composition when one
20/// authoritative creation fact arrives.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ChildCreationExpectation {
23    /// The event named no child position in the declared birth product.
24    UnknownPosition,
25    /// No creation for this declared child has been staged yet.
26    NotRequested,
27    /// This exact staged creation is awaiting its committed result.
28    Awaiting {
29        creation: CreationId,
30        kind: CreationKind,
31    },
32    /// A successful result has already established this child route.
33    Established { creation: CreationId },
34    /// Every required route was established and the plan was already reported.
35    PlanReported,
36}
37
38/// Failure while turning committed child creations into shutdown phases.
39#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum ChildShutdownPlanError<E, A: Address> {
41    /// The application rejected its own transition.
42    #[error("application behavior rejected the transition")]
43    Behavior(#[source] E),
44    /// More than one creation was emitted for a role that denotes one child.
45    #[error("more than one configured creation was emitted for child position {position}")]
46    DuplicateCreation { position: usize },
47    /// Initialization did not stage the child declared by this role.
48    #[error("configured child position {position} was not created during initialization")]
49    MissingCreation { position: usize },
50    /// A creation result did not match the request observed at this position.
51    #[error("creation result did not match child position {position}")]
52    UnexpectedCreationResult {
53        position: usize,
54        expected: ChildCreationExpectation,
55        observed: CreationResolved<A>,
56    },
57    /// The interpreter rejected one required child creation.
58    #[error("required child creation was rejected")]
59    CreationRejected {
60        position: usize,
61        observed: CreationResolved<A>,
62    },
63    /// Committed child routes did not form a valid shutdown plan.
64    #[error(transparent)]
65    InvalidPlan(ShutdownPlanError<CreationId>),
66    /// A plan declaration was evaluated before every required creation had
67    /// committed. This variant is unreachable through the public fold and is
68    /// retained to keep the plan-building operation total.
69    #[error("required child position {position} has not committed")]
70    ChildNotEstablished { position: usize },
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::{Activate as _, ShutdownCoordinatorError, StopOnShutdown};
77    use behavior::{
78        BehaviorActed, BehaviorBase, Births, ChildOccurrence, ChildRole, Children, CreateChild,
79        CreationSequence, Creations, DeclaredChildOccurrence, Here, Inside, MailAddr, NoBirths,
80        NoSends, User,
81    };
82
83    struct Store;
84    struct Gateway;
85
86    macro_rules! child {
87        ($child:ty) => {
88            impl Protocol for $child {
89                type Addr = MailAddr;
90                type Msg = Never;
91            }
92
93            impl Behavior for $child {
94                type Protocol = Self;
95                type Event = User<MailAddr, Never>;
96                type Sends = NoSends;
97                type Ph = Never;
98                type Error = Never;
99                type Birth = NoBirths;
100
101                fn transition(
102                    &mut self,
103                    _: behavior::ActiveTurn,
104                    event: Self::Event,
105                ) -> BehaviorActed<Self> {
106                    match event.message {}
107                }
108            }
109        };
110    }
111
112    child!(Store);
113    child!(Gateway);
114
115    type StoreChild = StopOnShutdown<Store>;
116    type GatewayChild = StopOnShutdown<Gateway>;
117    type ChildrenNode = ChildChoice<GatewayChild, ChildChoice<StoreChild, Never>>;
118
119    struct StoreRole;
120    struct GatewayRole;
121
122    #[derive(Clone, Copy)]
123    struct ApplicationChildren {
124        store: CreationId,
125        gateway: CreationId,
126        alternate_store: CreationId,
127    }
128
129    impl ApplicationChildren {
130        fn issue() -> Self {
131            let mut creations = CreationSequence::new();
132            let store = creations.issue().expect("the store creation ID exists");
133            let gateway = creations.issue().expect("the gateway creation ID exists");
134            let alternate_store = creations
135                .issue()
136                .expect("the alternate store creation ID exists");
137            Self {
138                store,
139                gateway,
140                alternate_store,
141            }
142        }
143    }
144
145    #[derive(Clone, Copy)]
146    enum InitialChildren {
147        Complete {
148            store: CreationId,
149            gateway: CreationId,
150        },
151        MissingStore {
152            gateway: CreationId,
153        },
154        DuplicateStore {
155            store: CreationId,
156            alternate_store: CreationId,
157            gateway: CreationId,
158        },
159    }
160
161    struct Application(InitialChildren);
162
163    impl Application {
164        const fn complete(children: ApplicationChildren) -> Self {
165            Self(InitialChildren::Complete {
166                store: children.store,
167                gateway: children.gateway,
168            })
169        }
170
171        const fn missing_store(children: ApplicationChildren) -> Self {
172            Self(InitialChildren::MissingStore {
173                gateway: children.gateway,
174            })
175        }
176
177        const fn duplicate_store(children: ApplicationChildren) -> Self {
178            Self(InitialChildren::DuplicateStore {
179                store: children.store,
180                alternate_store: children.alternate_store,
181                gateway: children.gateway,
182            })
183        }
184    }
185    impl Protocol for Application {
186        type Addr = MailAddr;
187        type Msg = Never;
188    }
189
190    impl BehaviorBase for Application {
191        type Base = Self;
192
193        fn base(&self) -> &Self::Base {
194            self
195        }
196    }
197
198    impl ChildRole<Application> for StoreRole {
199        type Child = StoreChild;
200        type Position = ChildTail<ChildHead>;
201    }
202
203    impl ChildOccurrence<Application> for StoreRole {
204        type Resolution = DeclaredChildOccurrence;
205    }
206
207    impl ChildRole<Application> for GatewayRole {
208        type Child = GatewayChild;
209        type Position = ChildHead;
210    }
211
212    impl ChildOccurrence<Application> for GatewayRole {
213        type Resolution = DeclaredChildOccurrence;
214    }
215
216    impl Behavior for Application {
217        type Protocol = Self;
218        type Event = User<MailAddr, Never>;
219        type Sends = NoSends;
220        type Ph = Never;
221        type Error = Never;
222        type Birth = Births<ChildrenNode>;
223
224        fn init(&mut self, _: behavior::InitializationTurn) -> BehaviorActed<Self> {
225            let creates = match self.0 {
226                InitialChildren::Complete { store, gateway } => Children::<MailAddr>::new()
227                    .child(store, StopOnShutdown::new(Store))
228                    .child(gateway, StopOnShutdown::new(Gateway))
229                    .into_creates(),
230                InitialChildren::MissingStore { gateway } => Creations::one(CreateChild::birth(
231                    gateway,
232                    ChildChoice::Head(StopOnShutdown::new(Gateway)),
233                )),
234                InitialChildren::DuplicateStore {
235                    store,
236                    alternate_store,
237                    gateway,
238                } => {
239                    let store_child =
240                        || ChildChoice::Tail(ChildChoice::Head(StopOnShutdown::new(Store)));
241                    Creations::one(CreateChild::birth(store, store_child()))
242                        .and(CreateChild::birth(alternate_store, store_child()))
243                        .and(CreateChild::birth(
244                            gateway,
245                            ChildChoice::Head(StopOnShutdown::new(Gateway)),
246                        ))
247                }
248            };
249            Ok(Actions::create(creates))
250        }
251
252        fn transition(
253            &mut self,
254            _: behavior::ActiveTurn,
255            event: Self::Event,
256        ) -> BehaviorActed<Self> {
257            match event.message {}
258        }
259    }
260
261    #[test]
262    fn rejected_creation_remains_a_typed_planning_failure() {
263        let children = ApplicationChildren::issue();
264        let mut active = shutdown_after_children(Application::complete(children))
265            .shutdown_phase(StoreRole)
266            .shutdown_phase(GatewayRole)
267            .finish()
268            .initialize()
269            .unwrap()
270            .behavior;
271        let rejected = CreationResolved::rejected(
272            children.store,
273            CreationKind::Birth,
274            behavior::CreationRejection::EnvironmentFailed,
275        );
276        let result = active.on_path::<_, Inside<Inside<Here>>>(rejected);
277        let Err(error) = result else {
278            panic!("rejected creation unexpectedly produced actions");
279        };
280        assert!(matches!(
281            error,
282            ShutdownCoordinatorError::Behavior(ChildShutdownPlanError::CreationRejected {
283                position: 1,
284                observed,
285            }) if observed == rejected
286        ));
287    }
288
289    #[test]
290    fn configured_children_are_total_and_unique_before_any_observation_is_emitted() {
291        let children = ApplicationChildren::issue();
292        let missing = shutdown_after_children(Application::missing_store(children))
293            .shutdown_phase(StoreRole)
294            .shutdown_phase(GatewayRole)
295            .finish()
296            .initialize();
297        assert!(matches!(
298            missing,
299            Err(ShutdownCoordinatorError::Behavior(
300                ChildShutdownPlanError::MissingCreation { position: 1 }
301            ))
302        ));
303
304        let duplicate = shutdown_after_children(Application::duplicate_store(children))
305            .shutdown_phase(StoreRole)
306            .shutdown_phase(GatewayRole)
307            .finish()
308            .initialize();
309        assert!(matches!(
310            duplicate,
311            Err(ShutdownCoordinatorError::Behavior(
312                ChildShutdownPlanError::DuplicateCreation { position: 1 }
313            ))
314        ));
315    }
316
317    #[test]
318    fn mismatched_and_stale_creation_resolutions_are_returned_complete() {
319        let children = ApplicationChildren::issue();
320        let mut mismatched = shutdown_after_children(Application::complete(children))
321            .shutdown_phase(StoreRole)
322            .shutdown_phase(GatewayRole)
323            .finish()
324            .initialize()
325            .unwrap()
326            .behavior;
327        let observed =
328            CreationResolved::replacement(children.alternate_store, children.store, MailAddr(191));
329        let Err(ShutdownCoordinatorError::Behavior(
330            ChildShutdownPlanError::UnexpectedCreationResult {
331                position,
332                expected,
333                observed: returned,
334            },
335        )) = mismatched.on_path::<_, Inside<Inside<Here>>>(observed)
336        else {
337            panic!("mismatched fact was not returned through the typed failure");
338        };
339        assert_eq!(position, 1);
340        assert_eq!(
341            expected,
342            ChildCreationExpectation::Awaiting {
343                creation: children.store,
344                kind: CreationKind::Birth,
345            }
346        );
347        assert_eq!(returned, observed);
348
349        let mut stale = shutdown_after_children(Application::complete(children))
350            .shutdown_phase(StoreRole)
351            .shutdown_phase(GatewayRole)
352            .finish()
353            .initialize()
354            .unwrap()
355            .behavior;
356        assert!(
357            stale
358                .on_path::<_, Inside<Inside<Here>>>(CreationResolved::birth(
359                    children.store,
360                    MailAddr(101),
361                ))
362                .is_ok()
363        );
364        assert!(
365            stale
366                .on_path::<_, Inside<Here>>(CreationResolved::birth(
367                    children.gateway,
368                    MailAddr(102),
369                ))
370                .is_ok()
371        );
372        let observed = CreationResolved::birth(children.gateway, MailAddr(202));
373        let Err(ShutdownCoordinatorError::Behavior(
374            ChildShutdownPlanError::UnexpectedCreationResult {
375                position,
376                expected,
377                observed: returned,
378            },
379        )) = stale.on_path::<_, Inside<Here>>(observed)
380        else {
381            panic!("stale fact was silently discarded");
382        };
383        assert_eq!(position, 0);
384        assert_eq!(expected, ChildCreationExpectation::PlanReported);
385        assert_eq!(returned, observed);
386    }
387
388    #[test]
389    fn creation_resolution_requires_matching_id_and_kind_independently() {
390        let children = ApplicationChildren::issue();
391        let wrong_kind =
392            CreationResolved::replacement(children.store, children.gateway, MailAddr(191));
393        let wrong_id = CreationResolved::birth(children.alternate_store, MailAddr(192));
394
395        for observed in [wrong_kind, wrong_id] {
396            let mut active = shutdown_after_children(Application::complete(children))
397                .shutdown_phase(StoreRole)
398                .shutdown_phase(GatewayRole)
399                .finish()
400                .initialize()
401                .unwrap()
402                .behavior;
403            let Err(ShutdownCoordinatorError::Behavior(
404                ChildShutdownPlanError::UnexpectedCreationResult {
405                    position,
406                    expected,
407                    observed: returned,
408                },
409            )) = active.on_path::<_, Inside<Inside<Here>>>(observed)
410            else {
411                panic!("a mismatched creation component must return the report");
412            };
413            assert_eq!(position, 1);
414            assert_eq!(
415                expected,
416                ChildCreationExpectation::Awaiting {
417                    creation: children.store,
418                    kind: CreationKind::Birth,
419                }
420            );
421            assert_eq!(returned, observed);
422
423            let Ok(accepted) = active.on_path::<_, Inside<Inside<Here>>>(CreationResolved::birth(
424                children.store,
425                MailAddr(193),
426            )) else {
427                panic!("the exact birth remains admissible after rejection");
428            };
429            assert!(accepted.creates.is_empty());
430            assert!(matches!(accepted.become_, Step::Continue));
431        }
432    }
433}
434
435/// Start declaring shutdown phases for every direct child role of `application`.
436///
437/// Each call to [`ChildShutdownPhases::shutdown_phase`] consumes one declared
438/// role from the available child product. [`ChildShutdownPhases::finish`] is
439/// therefore available only after every direct child appears exactly once.
440#[must_use]
441pub fn shutdown_after_children<B>(
442    application: B,
443) -> ChildShutdownPhases<B, AvailableFor<B>, NoPhases>
444where
445    B: Behavior,
446    <B::Birth as BirthMode>::Child: ChildOccurrenceProduct<AvailableChildren>,
447{
448    ChildShutdownPhases {
449        application,
450        available: PhantomData,
451        phases: PhantomData,
452    }
453}
454
455/// Inferred builder for statically complete child shutdown phases.
456pub struct ChildShutdownPhases<B, Available, Phases> {
457    application: B,
458    available: PhantomData<fn() -> Available>,
459    phases: PhantomData<fn() -> Phases>,
460}
461
462/// Generic consumer operation for entering the one inferred shutdown-phase builder.
463///
464/// This operation is the associated-output form of [`shutdown_after_children`].
465/// It exists so framework code can carry the returned builder without naming
466/// its closed initial availability proof; it does not introduce another
467/// planner or phase representation.
468pub trait BeginShutdownPhases: Behavior + Sized {
469    type Output;
470
471    #[must_use]
472    fn begin_shutdown_phases(self) -> Self::Output;
473}
474
475impl<B> BeginShutdownPhases for B
476where
477    B: Behavior,
478    <B::Birth as BirthMode>::Child: ChildOccurrenceProduct<AvailableChildren>,
479{
480    type Output = ChildShutdownPhases<B, AvailableFor<B>, NoPhases>;
481
482    fn begin_shutdown_phases(self) -> Self::Output {
483        shutdown_after_children(self)
484    }
485}
486
487/// Generic consumer operation for appending one statically valid shutdown phase.
488///
489/// Framework code can carry [`Output`](Self::Output) without naming the
490/// builder's private availability proof. Implementations do not plan or store
491/// phases independently; they delegate to [`ChildShutdownPhases::shutdown_phase`].
492pub trait DeclareShutdownPhase<Role>: Sized {
493    type Output;
494
495    #[must_use]
496    fn shutdown_phase(self, role: Role) -> Self::Output;
497}
498
499/// Generic consumer operation for finishing a complete shutdown declaration.
500///
501/// This trait is implemented only after every declared direct child has been
502/// assigned exactly once. Its associated output preserves the complete
503/// heterogeneous coordinator type without asking a framework to reproduce the
504/// builder's typestate vocabulary.
505pub trait FinishShutdownPhases: Sized {
506    type Output;
507
508    #[must_use]
509    fn finish(self) -> Self::Output;
510}
511
512#[allow(
513    private_bounds,
514    reason = "the public inferred builder hides its closed type-level phase proof"
515)]
516impl<B, Available, Phases> ChildShutdownPhases<B, Available, Phases>
517where
518    B: Behavior,
519{
520    /// Append one child as the next shutdown phase.
521    ///
522    /// Foreign roles, roles declared for another application, wrongly typed
523    /// roles, and duplicate roles have no applicable implementation.
524    ///
525    /// A duplicate role is rejected independently:
526    ///
527    /// ```compile_fail,E0277
528    /// struct Worker;
529    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never)]
530    /// impl Worker {
531    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
532    ///         match message {}
533    ///     }
534    /// }
535    /// type ManagedWorker = behavior_actors::StopOnShutdown<Worker>;
536    /// struct Application;
537    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never, births = {
538    ///     store: ManagedWorker,
539    ///     gateway: ManagedWorker,
540    /// }, creation_settlements = retain_for_retirement)]
541    /// impl Application {
542    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
543    ///         match message {}
544    ///     }
545    /// }
546    /// let _ = behavior_actors::shutdown_after_children(Application)
547    ///     .shutdown_phase(ApplicationChild::Store)
548    ///     .shutdown_phase(ApplicationChild::Store);
549    /// ```
550    ///
551    /// A role that belongs to no child declaration is rejected independently:
552    ///
553    /// ```compile_fail,E0277
554    /// struct Worker;
555    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never)]
556    /// impl Worker {
557    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
558    ///         match message {}
559    ///     }
560    /// }
561    /// type ManagedWorker = behavior_actors::StopOnShutdown<Worker>;
562    /// struct Application;
563    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never, births = {
564    ///     store: ManagedWorker,
565    ///     gateway: ManagedWorker,
566    /// }, creation_settlements = retain_for_retirement)]
567    /// impl Application {
568    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
569    ///         match message {}
570    ///     }
571    /// }
572    /// struct ForeignRole;
573    /// let _ = behavior_actors::shutdown_after_children(Application).shutdown_phase(ForeignRole);
574    /// ```
575    ///
576    /// A route value cannot stand in for its declared role:
577    ///
578    /// ```compile_fail,E0277
579    /// struct Worker;
580    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never)]
581    /// impl Worker {
582    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
583    ///         match message {}
584    ///     }
585    /// }
586    /// type ManagedWorker = behavior_actors::StopOnShutdown<Worker>;
587    /// struct Application;
588    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never, births = {
589    ///     store: ManagedWorker,
590    ///     gateway: ManagedWorker,
591    /// }, creation_settlements = retain_for_retirement)]
592    /// impl Application {
593    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
594    ///         match message {}
595    ///     }
596    /// }
597    /// let routes = ApplicationChildrenRoutes::new(1, 2);
598    /// let _ = behavior_actors::shutdown_after_children(Application).shutdown_phase(routes.store);
599    /// ```
600    #[must_use]
601    pub fn shutdown_phase<Role>(
602        self,
603        _: Role,
604    ) -> ChildShutdownPhases<
605        B,
606        <Available as AssignAt<<B as ResolveChildOccurrence<Role>>::Position>>::Assigned,
607        Phase<Role, Phases>,
608    >
609    where
610        B: ResolveChildOccurrence<Role>,
611        Available: AssignAt<<B as ResolveChildOccurrence<Role>>::Position>,
612    {
613        ChildShutdownPhases {
614            application: self.application,
615            available: PhantomData,
616            phases: PhantomData,
617        }
618    }
619}
620
621#[allow(
622    private_bounds,
623    reason = "the public consumer operation preserves the builder's closed availability proof"
624)]
625impl<B, Available, Phases, Role> DeclareShutdownPhase<Role>
626    for ChildShutdownPhases<B, Available, Phases>
627where
628    B: Behavior + ResolveChildOccurrence<Role>,
629    Available: AssignAt<<B as ResolveChildOccurrence<Role>>::Position>,
630{
631    type Output = ChildShutdownPhases<
632        B,
633        <Available as AssignAt<<B as ResolveChildOccurrence<Role>>::Position>>::Assigned,
634        Phase<Role, Phases>,
635    >;
636
637    fn shutdown_phase(self, role: Role) -> Self::Output {
638        ChildShutdownPhases::shutdown_phase(self, role)
639    }
640}
641
642#[allow(
643    private_bounds,
644    reason = "the public inferred builder hides its closed plan-construction proof"
645)]
646impl<B, Available, Phases> ChildShutdownPhases<B, Available, Phases>
647where
648    B: Behavior,
649    B::Birth: BirthMode,
650    Available: AllAssigned,
651    <B::Birth as BirthMode>::Child: ChildOccurrenceProduct<ShutdownTargets<B>>,
652    Phases: BuildShutdownPlan<B, TargetsFor<B>>,
653    TargetsFor<B>: super::shutdown_coordinator::heterogeneous::Selection<Addr = behavior::BehaviorAddr<B>>
654        + Copy,
655    <behavior::BehaviorAddr<B> as Address>::Nonce: Copy + Eq,
656{
657    /// Complete the application after proving that every child role occurs in
658    /// exactly one declared phase.
659    ///
660    /// The final actor's source-indexed event ingress carries the plan through
661    /// arbitrary outer layers without exposing a structural path.
662    ///
663    /// ```compile_fail,E0599
664    /// struct Worker;
665    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never)]
666    /// impl Worker {
667    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
668    ///         match message {}
669    ///     }
670    /// }
671    /// type ManagedWorker = behavior_actors::StopOnShutdown<Worker>;
672    /// struct Application;
673    /// #[behavior::behavior(addr = behavior::MailAddr, message = behavior::Never, births = {
674    ///     store: ManagedWorker,
675    ///     gateway: ManagedWorker,
676    /// }, creation_settlements = retain_for_retirement)]
677    /// impl Application {
678    ///     fn receive(&mut self, _: behavior::MailAddr, message: behavior::Never) -> behavior::BehaviorActed<Self> {
679    ///         match message {}
680    ///     }
681    /// }
682    /// // `gateway` remains unassigned, so `finish` does not exist here.
683    /// let incomplete = behavior_actors::shutdown_after_children(Application)
684    ///     .shutdown_phase(ApplicationChild::Store)
685    ///     .finish();
686    /// ```
687    #[must_use]
688    pub fn finish(
689        self,
690    ) -> HeterogeneousShutdownCoordinator<ChildShutdownPlan<B, Phases>, TargetsFor<B>>
691    where
692        ChildShutdownPlan<B, Phases>: Behavior<Protocol = B::Protocol>,
693    {
694        HeterogeneousShutdownCoordinator::awaiting_plan(ChildShutdownPlan::new(self.application))
695    }
696}
697
698#[allow(
699    private_bounds,
700    reason = "the public consumer operation preserves the builder's closed finishing proof"
701)]
702impl<B, Available, Phases> FinishShutdownPhases for ChildShutdownPhases<B, Available, Phases>
703where
704    B: Behavior,
705    B::Birth: BirthMode,
706    Available: AllAssigned,
707    <B::Birth as BirthMode>::Child: ChildOccurrenceProduct<ShutdownTargets<B>>,
708    Phases: BuildShutdownPlan<B, TargetsFor<B>>,
709    TargetsFor<B>: super::shutdown_coordinator::heterogeneous::Selection<Addr = behavior::BehaviorAddr<B>>
710        + Copy,
711    <behavior::BehaviorAddr<B> as Address>::Nonce: Copy + Eq,
712    ChildShutdownPlan<B, Phases>: Behavior<Protocol = B::Protocol>,
713{
714    type Output = HeterogeneousShutdownCoordinator<ChildShutdownPlan<B, Phases>, TargetsFor<B>>;
715
716    fn finish(self) -> Self::Output {
717        ChildShutdownPhases::finish(self)
718    }
719}
720
721/// Type-level marker for the absence of declared phases.
722pub struct NoPhases;
723
724/// One declared phase appended after `Earlier` phases.
725pub struct Phase<Role, Earlier>(PhantomData<fn() -> (Role, Earlier)>);
726
727/// Shape of the child product consumed by phase declarations.
728pub struct AvailableChildren;
729
730/// One not-yet-declared direct child.
731pub struct Unassigned<Child, Tail>(PhantomData<fn() -> (Child, Tail)>);
732
733/// One direct child already assigned to a phase.
734pub struct Assigned<Child, Tail>(PhantomData<fn() -> (Child, Tail)>);
735
736/// End of the direct-child declaration product.
737pub struct NoChildren;
738
739impl ChildOccurrenceShape for AvailableChildren {
740    type Empty = NoChildren;
741    type Member<Occurrence, Child: Behavior, Tail> = Unassigned<Child, Tail>;
742}
743
744/// Statically consume the child at one structural position.
745pub trait AssignAt<Position> {
746    type Assigned;
747}
748
749impl<Child, Tail> AssignAt<ChildHead> for Unassigned<Child, Tail> {
750    type Assigned = Assigned<Child, Tail>;
751}
752
753impl<Child, Tail, Position> AssignAt<ChildTail<Position>> for Unassigned<Child, Tail>
754where
755    Tail: AssignAt<Position>,
756{
757    type Assigned = Unassigned<Child, <Tail as AssignAt<Position>>::Assigned>;
758}
759
760impl<Child, Tail, Position> AssignAt<ChildTail<Position>> for Assigned<Child, Tail>
761where
762    Tail: AssignAt<Position>,
763{
764    type Assigned = Assigned<Child, <Tail as AssignAt<Position>>::Assigned>;
765}
766
767/// Proof that no direct child remains unassigned.
768pub trait AllAssigned {}
769
770impl AllAssigned for NoChildren {}
771
772impl<Child, Tail: AllAssigned> AllAssigned for Assigned<Child, Tail> {}
773
774/// Shape of the coordinator's heterogeneous target sum.
775pub struct ShutdownTargets<B: Behavior>(PhantomData<fn() -> B>);
776
777impl<B: Behavior> ChildOccurrenceShape for ShutdownTargets<B> {
778    type Empty = NoShutdownTargets<behavior::BehaviorAddr<B>>;
779    type Member<Occurrence, Child: Behavior, Tail> = ShutdownChoice<Child, Tail>;
780}
781
782/// Complete shutdown target sum derived from an application's direct births.
783pub type TargetsFor<B> =
784    ChildOccurrences<<<B as Behavior>::Birth as BirthMode>::Child, ShutdownTargets<B>>;
785
786/// Complete role-availability product derived from direct births.
787pub type AvailableFor<B> =
788    ChildOccurrences<<<B as Behavior>::Birth as BirthMode>::Child, AvailableChildren>;
789
790trait PositionNumber {
791    const INDEX: usize;
792}
793
794impl PositionNumber for ChildHead {
795    const INDEX: usize = 0;
796}
797
798impl<Position: PositionNumber> PositionNumber for ChildTail<Position> {
799    const INDEX: usize = Position::INDEX + 1;
800}
801
802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
803enum ChildStatus {
804    NotRequested,
805    Awaiting {
806        creation: CreationId,
807        kind: CreationKind,
808    },
809    Established {
810        creation: CreationId,
811    },
812}
813
814impl ChildStatus {
815    fn expectation(self) -> ChildCreationExpectation {
816        match self {
817            Self::NotRequested => ChildCreationExpectation::NotRequested,
818            Self::Awaiting { creation, kind } => {
819                ChildCreationExpectation::Awaiting { creation, kind }
820            }
821            Self::Established { creation } => ChildCreationExpectation::Established { creation },
822        }
823    }
824}
825
826enum Planning {
827    Collecting(Vec<ChildStatus>),
828    Reported,
829}
830
831enum PlannedEvent<E, A: Address> {
832    Application(E),
833    Creation {
834        position: usize,
835        result: CreationResolved<A>,
836    },
837}
838
839/// Internal application composition that observes child creations and reports
840/// one existing heterogeneous shutdown plan to its outer coordinator.
841pub struct ChildShutdownPlan<B: Behavior, Phases> {
842    application: B,
843    planning: Planning,
844    phases: PhantomData<fn() -> Phases>,
845}
846
847impl<B: Behavior, Phases> ChildShutdownPlan<B, Phases> {
848    fn new(application: B) -> Self {
849        Self {
850            application,
851            planning: Planning::Collecting(Vec::new()),
852            phases: PhantomData,
853        }
854    }
855}
856
857impl<B, Phases> behavior::BehaviorBase for ChildShutdownPlan<B, Phases>
858where
859    B: Behavior + behavior::BehaviorBase,
860{
861    type Base = B::Base;
862
863    fn base(&self) -> &Self::Base {
864        self.application.base()
865    }
866}
867
868impl<B, Phases> crate::StashStatus for ChildShutdownPlan<B, Phases>
869where
870    B: Behavior + crate::StashStatus,
871{
872    fn stashed_messages(&self) -> usize {
873        self.application.stashed_messages()
874    }
875}
876
877type Plan<Targets> = HeterogeneousShutdownPlan<Targets>;
878type ReportPlan<Targets> = ReportShutdownPlan<Plan<Targets>>;
879type BaseSends<B, Targets> =
880    SendLayer<InterpreterRequests<ReportPlan<Targets>>, <B as Behavior>::Sends>;
881
882struct PlanningChildren;
883struct PlannedEnd;
884struct PlannedChild<Position, Child, Tail>(PhantomData<fn() -> (Position, Child, Tail)>);
885
886impl ChildOccurrenceShape for PlanningChildren {
887    type Empty = PlannedEnd;
888    type Member<Occurrence, Child: Behavior, Tail> = PlannedChild<Occurrence, Child, Tail>;
889}
890
891trait PlanChildren<Node, A: Address, Event, Sends> {
892    type Events;
893    type Sends: SendEffects;
894
895    const COUNT: usize;
896
897    fn empty_sends(inner: Sends) -> Self::Sends;
898
899    fn observe(
900        child: &Node,
901        creation: CreationId,
902        kind: CreationKind,
903        sends: &mut Self::Sends,
904        requested: &mut Vec<(usize, CreationId, CreationKind)>,
905    );
906
907    fn read(event: Self::Events) -> PlannedEvent<Event, A>;
908}
909
910impl<A, Event, Sends> PlanChildren<Never, A, Event, Sends> for PlannedEnd
911where
912    A: Address,
913    Sends: SendEffects,
914{
915    type Events = EventLayer<Never, Event>;
916    type Sends = Sends;
917    const COUNT: usize = 0;
918
919    fn empty_sends(inner: Sends) -> Self::Sends {
920        inner
921    }
922
923    fn observe(
924        child: &Never,
925        _: CreationId,
926        _: CreationKind,
927        _: &mut Self::Sends,
928        _: &mut Vec<(usize, CreationId, CreationKind)>,
929    ) {
930        match *child {}
931    }
932
933    fn read(event: Self::Events) -> PlannedEvent<Event, A> {
934        match event {
935            EventLayer::Owned(never) => match never {},
936            EventLayer::Inner(event) => PlannedEvent::Application(event),
937        }
938    }
939}
940
941impl<A, Position, Event, Sends, Child> PlanChildren<Child, A, Event, Sends>
942    for PlannedChild<Position, Child, PlannedEnd>
943where
944    A: Address,
945    Position: PositionNumber,
946    Sends: SendEffects,
947    Child: Behavior,
948    Child::Protocol: Protocol<Addr = A>,
949{
950    type Events = EventLayer<CreationResolved<A>, EventLayer<Never, Event>>;
951    type Sends = SendLayer<InterpreterRequests<ObserveCreation<Child::Protocol, Position>>, Sends>;
952    const COUNT: usize = 1;
953
954    fn empty_sends(inner: Sends) -> Self::Sends {
955        SendLayer::new(InterpreterRequests::empty(), inner)
956    }
957
958    fn observe(
959        _: &Child,
960        creation: CreationId,
961        kind: CreationKind,
962        sends: &mut Self::Sends,
963        requested: &mut Vec<(usize, CreationId, CreationKind)>,
964    ) {
965        sends.owned.send(ObserveCreation::new(creation));
966        requested.push((Position::INDEX, creation, kind));
967    }
968
969    fn read(event: Self::Events) -> PlannedEvent<Event, A> {
970        match event {
971            EventLayer::Owned(result) => PlannedEvent::Creation {
972                position: Position::INDEX,
973                result,
974            },
975            EventLayer::Inner(EventLayer::Owned(never)) => match never {},
976            EventLayer::Inner(EventLayer::Inner(event)) => PlannedEvent::Application(event),
977        }
978    }
979}
980
981impl<A, Position, Event, Sends, Head, Tail, TailPlan>
982    PlanChildren<ChildChoice<Head, Tail>, A, Event, Sends>
983    for PlannedChild<Position, Head, TailPlan>
984where
985    A: Address,
986    Position: PositionNumber,
987    Sends: SendEffects,
988    Head: Behavior,
989    Head::Protocol: Protocol<Addr = A>,
990    TailPlan: PlanChildren<Tail, A, Event, Sends>,
991{
992    type Events = EventLayer<CreationResolved<A>, TailPlan::Events>;
993    type Sends =
994        SendLayer<InterpreterRequests<ObserveCreation<Head::Protocol, Position>>, TailPlan::Sends>;
995
996    const COUNT: usize = 1 + TailPlan::COUNT;
997
998    fn empty_sends(inner: Sends) -> Self::Sends {
999        SendLayer::new(InterpreterRequests::empty(), TailPlan::empty_sends(inner))
1000    }
1001
1002    fn observe(
1003        child: &ChildChoice<Head, Tail>,
1004        creation: CreationId,
1005        kind: CreationKind,
1006        sends: &mut Self::Sends,
1007        requested: &mut Vec<(usize, CreationId, CreationKind)>,
1008    ) {
1009        match child {
1010            ChildChoice::Head(_) => {
1011                sends.owned.send(ObserveCreation::new(creation));
1012                requested.push((Position::INDEX, creation, kind));
1013            }
1014            ChildChoice::Tail(tail) => {
1015                TailPlan::observe(tail, creation, kind, &mut sends.inner, requested);
1016            }
1017        }
1018    }
1019
1020    fn read(event: Self::Events) -> PlannedEvent<Event, A> {
1021        match event {
1022            EventLayer::Owned(result) => PlannedEvent::Creation {
1023                position: Position::INDEX,
1024                result,
1025            },
1026            EventLayer::Inner(event) => TailPlan::read(event),
1027        }
1028    }
1029}
1030
1031trait BuildShutdownPlan<B: Behavior, Targets> {
1032    fn build(
1033        children: &[ChildStatus],
1034    ) -> Result<Vec<Vec<Targets>>, ChildShutdownPlanError<B::Error, behavior::BehaviorAddr<B>>>;
1035}
1036
1037impl<B: Behavior, Targets> BuildShutdownPlan<B, Targets> for NoPhases {
1038    fn build(
1039        _: &[ChildStatus],
1040    ) -> Result<Vec<Vec<Targets>>, ChildShutdownPlanError<B::Error, behavior::BehaviorAddr<B>>>
1041    {
1042        Ok(Vec::new())
1043    }
1044}
1045
1046impl<B, Targets, Role, Earlier> BuildShutdownPlan<B, Targets> for Phase<Role, Earlier>
1047where
1048    B: Behavior + ResolveChildOccurrence<Role>,
1049    behavior::BehaviorAddr<B>: Address,
1050    <B as ResolveChildOccurrence<Role>>::Position: PositionNumber,
1051    <<B as ResolveChildOccurrence<Role>>::Child as Behavior>::Protocol:
1052        Protocol<Addr = behavior::BehaviorAddr<B>>,
1053    Targets: ShutdownTargetAt<
1054            <B as ResolveChildOccurrence<Role>>::Child,
1055            <B as ResolveChildOccurrence<Role>>::Position,
1056        >,
1057    Earlier: BuildShutdownPlan<B, Targets>,
1058{
1059    fn build(
1060        children: &[ChildStatus],
1061    ) -> Result<Vec<Vec<Targets>>, ChildShutdownPlanError<B::Error, behavior::BehaviorAddr<B>>>
1062    {
1063        let mut phases = Earlier::build(children)?;
1064        let position = <<B as ResolveChildOccurrence<Role>>::Position as PositionNumber>::INDEX;
1065        let Some(ChildStatus::Established { creation }) = children.get(position) else {
1066            return Err(ChildShutdownPlanError::ChildNotEstablished { position });
1067        };
1068        phases.push(vec![Targets::shutdown_target_at(*creation)]);
1069        Ok(phases)
1070    }
1071}
1072
1073impl<B, Phases, A, Ph, Sends, Br, Node, Shape, Targets, Events, Planned> Behavior
1074    for ChildShutdownPlan<B, Phases>
1075where
1076    A: Address,
1077    A::Nonce: Copy + Eq,
1078    Sends: SendEffects,
1079    Br: BirthMode<Child = Node>,
1080    B: Behavior<Ph = Ph, Sends = Sends, Birth = Br>,
1081    B::Protocol: Protocol<Addr = A>,
1082    Node: ChildOccurrenceProduct<ShutdownTargets<B>, Product = Targets>
1083        + ChildOccurrenceProduct<PlanningChildren, Product = Shape>,
1084    Shape: PlanChildren<Node, A, B::Event, BaseSends<B, Targets>, Events = Events, Sends = Planned>,
1085    Targets: heterogeneous::Selection<Addr = A> + Copy,
1086    Phases: BuildShutdownPlan<B, Targets>,
1087    Events: behavior::UserEvent<Addr = A, Message = <B::Protocol as Protocol>::Msg>,
1088    Planned: SendEffects + behavior::SendsFor<Events>,
1089{
1090    type Protocol = B::Protocol;
1091    type Event = Events;
1092    type Sends = Planned;
1093    type Ph = Ph;
1094    type Error = ChildShutdownPlanError<B::Error, A>;
1095    type Birth = Br;
1096
1097    fn init(&mut self, _: behavior::InitializationTurn) -> behavior::BehaviorActed<Self> {
1098        let actions = behavior::initialize(&mut self.application)
1099            .map_err(ChildShutdownPlanError::Behavior)?;
1100        self.wrap_initialization(actions)
1101    }
1102
1103    fn transition(
1104        &mut self,
1105        _: behavior::ActiveTurn,
1106        event: Self::Event,
1107    ) -> behavior::BehaviorActed<Self> {
1108        match Shape::read(event) {
1109            PlannedEvent::Application(event) => {
1110                let actions = behavior::delegate_transition(&mut self.application, event)
1111                    .map_err(ChildShutdownPlanError::Behavior)?;
1112                Ok(self.wrap_transition(actions))
1113            }
1114            PlannedEvent::Creation { position, result } => self.creation_resolved(position, result),
1115        }
1116    }
1117}
1118
1119#[allow(
1120    private_bounds,
1121    reason = "the public compiler representation hides its closed planning fold"
1122)]
1123impl<B, Phases, A, Ph, Sends, Br, Node, Shape, Targets, Events, Planned>
1124    ChildShutdownPlan<B, Phases>
1125where
1126    A: Address,
1127    A::Nonce: Copy + Eq,
1128    Sends: SendEffects,
1129    Br: BirthMode<Child = Node>,
1130    B: Behavior<Ph = Ph, Sends = Sends, Birth = Br>,
1131    B::Protocol: Protocol<Addr = A>,
1132    Node: ChildOccurrenceProduct<ShutdownTargets<B>, Product = Targets>
1133        + ChildOccurrenceProduct<PlanningChildren, Product = Shape>,
1134    Shape: PlanChildren<Node, A, B::Event, BaseSends<B, Targets>, Events = Events, Sends = Planned>,
1135    Targets: heterogeneous::Selection<Addr = A> + Copy,
1136    Phases: BuildShutdownPlan<B, Targets>,
1137    Events: behavior::UserEvent<Addr = A, Message = <B::Protocol as Protocol>::Msg>,
1138    Planned: SendEffects + behavior::SendsFor<Events>,
1139{
1140    fn wrap_initialization(
1141        &mut self,
1142        actions: Actions<A, Ph, Sends, Br>,
1143    ) -> Result<Actions<A, Ph, Planned, Br>, ChildShutdownPlanError<B::Error, A>> {
1144        let Actions {
1145            sends,
1146            creates,
1147            become_,
1148        } = actions;
1149        let reports = if Shape::COUNT == 0 {
1150            let phases = Phases::build(&[])?;
1151            let plan = HeterogeneousShutdownPlan::new(phases)
1152                .map_err(ChildShutdownPlanError::InvalidPlan)?;
1153            self.planning = Planning::Reported;
1154            InterpreterRequests::one(crate::ReportShutdownPlan::new(plan))
1155        } else {
1156            InterpreterRequests::empty()
1157        };
1158        let base = SendLayer::new(reports, sends);
1159        let mut planned = Shape::empty_sends(base);
1160        let mut requested = Vec::new();
1161        for creation in &creates {
1162            Shape::observe(
1163                creation.child(),
1164                creation.id(),
1165                creation.kind(),
1166                &mut planned,
1167                &mut requested,
1168            );
1169        }
1170        if Shape::COUNT != 0 {
1171            let mut next = vec![ChildStatus::NotRequested; Shape::COUNT];
1172            for (position, creation, kind) in requested {
1173                let Some(state) = next.get_mut(position) else {
1174                    return Err(ChildShutdownPlanError::DuplicateCreation { position });
1175                };
1176                if !matches!(state, ChildStatus::NotRequested) {
1177                    return Err(ChildShutdownPlanError::DuplicateCreation { position });
1178                }
1179                *state = ChildStatus::Awaiting { creation, kind };
1180            }
1181            if let Some(position) = next
1182                .iter()
1183                .position(|child| matches!(child, ChildStatus::NotRequested))
1184            {
1185                return Err(ChildShutdownPlanError::MissingCreation { position });
1186            }
1187            self.planning = Planning::Collecting(next);
1188        }
1189        Ok(Actions::new(planned, creates, become_))
1190    }
1191
1192    fn wrap_transition(&self, actions: Actions<A, Ph, Sends, Br>) -> Actions<A, Ph, Planned, Br> {
1193        let Actions {
1194            sends,
1195            creates,
1196            become_,
1197        } = actions;
1198        let base = SendLayer::new(InterpreterRequests::empty(), sends);
1199        Actions::new(Shape::empty_sends(base), creates, become_)
1200    }
1201
1202    fn creation_resolved(
1203        &mut self,
1204        position: usize,
1205        result: CreationResolved<A>,
1206    ) -> Result<Actions<A, Ph, Planned, Br>, ChildShutdownPlanError<B::Error, A>> {
1207        let Planning::Collecting(children) = &self.planning else {
1208            return Err(ChildShutdownPlanError::UnexpectedCreationResult {
1209                position,
1210                expected: ChildCreationExpectation::PlanReported,
1211                observed: result,
1212            });
1213        };
1214        let Some(state) = children.get(position).copied() else {
1215            return Err(ChildShutdownPlanError::UnexpectedCreationResult {
1216                position,
1217                expected: ChildCreationExpectation::UnknownPosition,
1218                observed: result,
1219            });
1220        };
1221        let ChildStatus::Awaiting { creation, kind } = state else {
1222            return Err(ChildShutdownPlanError::UnexpectedCreationResult {
1223                position,
1224                expected: state.expectation(),
1225                observed: result,
1226            });
1227        };
1228        if result.creation != creation || result.kind != kind {
1229            return Err(ChildShutdownPlanError::UnexpectedCreationResult {
1230                position,
1231                expected: ChildCreationExpectation::Awaiting { creation, kind },
1232                observed: result,
1233            });
1234        }
1235        let established = match result.result {
1236            Ok(_) => ChildStatus::Established { creation },
1237            Err(_) => {
1238                return Err(ChildShutdownPlanError::CreationRejected {
1239                    position,
1240                    observed: result,
1241                });
1242            }
1243        };
1244        let mut next = children.clone();
1245        next[position] = established;
1246        if !next
1247            .iter()
1248            .all(|child| matches!(child, ChildStatus::Established { .. }))
1249        {
1250            self.planning = Planning::Collecting(next);
1251            return Ok(Actions::cont());
1252        }
1253        let phases = Phases::build(&next)?;
1254        let plan =
1255            HeterogeneousShutdownPlan::new(phases).map_err(ChildShutdownPlanError::InvalidPlan)?;
1256        let base = SendLayer::new(
1257            InterpreterRequests::one(crate::ReportShutdownPlan::new(plan)),
1258            B::Sends::empty(),
1259        );
1260        let sends = Shape::empty_sends(base);
1261        self.planning = Planning::Reported;
1262        Ok(Actions::new(
1263            sends,
1264            behavior::Creations::empty(),
1265            Step::Continue,
1266        ))
1267    }
1268}