Skip to main content

cu29_runtime/
app.rs

1use crate::curuntime::KeyFrame;
2use core::fmt;
3use core::marker::PhantomData;
4use cu29_traits::CopperListTuple;
5use cu29_traits::{CuError, CuResult};
6use cu29_unifiedlog::{SectionStorage, UnifiedLogWrite};
7
8#[cfg(feature = "std")]
9use crate::copperlist::CopperList;
10#[cfg(not(feature = "std"))]
11use alloc::vec::Vec;
12#[cfg(feature = "std")]
13use cu29_clock::RobotClockMock;
14#[cfg(feature = "std")]
15use std::vec::Vec;
16
17#[cfg(not(feature = "std"))]
18mod imp {
19    pub use alloc::boxed::Box;
20    pub use alloc::string::String;
21}
22
23#[cfg(feature = "std")]
24mod imp {
25    pub use crate::config::CuConfig;
26    pub use crate::simulation::SimOverride;
27    pub use cu29_clock::RobotClock;
28    pub use cu29_unifiedlog::memmap::MmapSectionStorage;
29    pub use std::sync::{Arc, Mutex};
30}
31
32use imp::*;
33
34/// Convenience trait for CuApplication when it is just a std App
35#[cfg(feature = "std")]
36pub trait CuStdApplication:
37    CuApplication<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite>
38{
39}
40
41#[cfg(feature = "std")]
42impl<T> CuStdApplication for T where
43    T: CuApplication<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite>
44{
45}
46
47/// Compile-time subsystem identity embedded in generated Copper applications.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Subsystem {
50    id: Option<&'static str>,
51    code: u16,
52}
53
54impl Subsystem {
55    #[inline]
56    pub const fn new(id: Option<&'static str>, code: u16) -> Self {
57        Self { id, code }
58    }
59
60    #[inline]
61    pub const fn id(self) -> Option<&'static str> {
62        self.id
63    }
64
65    #[inline]
66    pub const fn code(self) -> u16 {
67        self.code
68    }
69}
70
71/// Compile-time subsystem identity embedded in generated Copper applications.
72pub trait CuSubsystemMetadata {
73    /// Multi-Copper subsystem identity for this generated application.
74    fn subsystem() -> Subsystem;
75}
76
77/// A trait that defines the structure and behavior of a CuApplication.
78///
79/// CuApplication is the normal, running on robot version of an application and its runtime.
80///
81/// The `CuApplication` trait outlines the necessary functions required for managing an application lifecycle,
82/// including configuration management, initialization, task execution, and runtime control. It is meant to be
83/// implemented by types that represent specific applications, providing them with unified control and execution features.
84///
85/// This is the more generic version that allows you to specify a custom unified logger.
86pub trait CuApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> {
87    /// Returns the original configuration as a string, typically loaded from a RON file.
88    /// This configuration represents the default settings for the application before any overrides.
89    fn get_original_config() -> String;
90
91    /// Starts all tasks managed by the application/runtime.
92    ///
93    /// # Returns
94    /// * `Ok(())` - If all tasks are started successfully.
95    /// * `Err(CuResult)` - If an error occurs while attempting to start one
96    ///   or more tasks.
97    #[deprecated(
98        since = "1.2.0",
99        note = "use the typed transition `start()` on the handle returned by `build()`"
100    )]
101    fn start_all_tasks(&mut self) -> CuResult<()>;
102
103    /// Executes a single iteration of copper-generated runtime (generating and logging one copperlist)
104    ///
105    /// # Returns
106    ///
107    /// * `CuResult<()>` - Returns `Ok(())` if the iteration completes successfully, or an error
108    ///   wrapped in `CuResult` if something goes wrong during execution.
109    ///
110    #[deprecated(
111        since = "1.2.0",
112        note = "use `run_one_iteration()` on the Running handle from `build()?.start()?`"
113    )]
114    fn run_one_iteration(&mut self) -> CuResult<()>;
115
116    /// Runs indefinitely looping over run_one_iteration
117    ///
118    /// # Returns
119    ///
120    /// Returns a `CuResult<()>`, which indicates the success or failure of the
121    /// operation.
122    /// - On success, the result is `Ok(())`.
123    /// - On failure, an appropriate error wrapped in `CuResult` is returned.
124    #[deprecated(
125        since = "1.2.0",
126        note = "use the typed transition `run_until_shutdown()` on the handle returned by `build()`"
127    )]
128    fn run(&mut self) -> CuResult<()>;
129
130    /// Stops all tasks managed by the application/runtime.
131    ///
132    /// # Returns
133    ///
134    /// Returns a `CuResult<()>`, which indicates the success or failure of the
135    /// operation.
136    /// - On success, the result is `Ok(())`.
137    /// - On failure, an appropriate error wrapped in `CuResult` is returned.
138    ///
139    #[deprecated(
140        since = "1.2.0",
141        note = "use the typed transition `stop()` on the Running handle"
142    )]
143    fn stop_all_tasks(&mut self) -> CuResult<()>;
144
145    /// Restore all tasks from the given frozen state
146    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()>;
147}
148
149/// A trait that defines the structure and behavior of a simulation-enabled CuApplication.
150///
151/// CuSimApplication is the simulation version of an application and its runtime, allowing
152/// overriding of steps with simulated behavior.
153///
154/// The `CuSimApplication` trait outlines the necessary functions required for managing an application lifecycle
155/// in simulation mode, including configuration management, initialization, task execution, and runtime control.
156#[cfg(feature = "std")]
157pub trait CuSimApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> {
158    /// The type representing a simulation step that can be overridden
159    type Step<'z>;
160
161    /// Returns the original configuration as a string, typically loaded from a RON file.
162    /// This configuration represents the default settings for the application before any overrides.
163    fn get_original_config() -> String;
164
165    /// Returns the mission id this generated application is bound to, when applicable.
166    fn mission_id() -> Option<&'static str> {
167        None
168    }
169
170    /// Starts all tasks managed by the application/runtime in simulation mode.
171    ///
172    /// # Arguments
173    /// * `sim_callback` - A mutable function reference that allows overriding individual simulation steps.
174    ///
175    /// # Returns
176    /// * `Ok(())` - If all tasks are started successfully.
177    /// * `Err(CuResult)` - If an error occurs while attempting to start one
178    ///   or more tasks.
179    #[deprecated(
180        since = "1.2.0",
181        note = "use the typed transition `start()` on the handle returned by `build()`"
182    )]
183    fn start_all_tasks(
184        &mut self,
185        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
186    ) -> CuResult<()>;
187
188    /// Executes a single iteration of copper-generated runtime in simulation mode.
189    ///
190    /// # Arguments
191    /// * `sim_callback` - A mutable function reference that allows overriding individual simulation steps.
192    ///
193    /// # Returns
194    ///
195    /// * `CuResult<()>` - Returns `Ok(())` if the iteration completes successfully, or an error
196    ///   wrapped in `CuResult` if something goes wrong during execution.
197    #[deprecated(
198        since = "1.2.0",
199        note = "use `run_one_iteration()` on the Running handle from `build()?.start()?`"
200    )]
201    fn run_one_iteration(
202        &mut self,
203        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
204    ) -> CuResult<()>;
205
206    /// Runs indefinitely looping over run_one_iteration in simulation mode
207    ///
208    /// # Arguments
209    /// * `sim_callback` - A mutable function reference that allows overriding individual simulation steps.
210    ///
211    /// # Returns
212    ///
213    /// Returns a `CuResult<()>`, which indicates the success or failure of the
214    /// operation.
215    /// - On success, the result is `Ok(())`.
216    /// - On failure, an appropriate error wrapped in `CuResult` is returned.
217    #[deprecated(
218        since = "1.2.0",
219        note = "use the typed transition `run_until_shutdown()` on the handle returned by `build()`"
220    )]
221    fn run(
222        &mut self,
223        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
224    ) -> CuResult<()>;
225
226    /// Stops all tasks managed by the application/runtime in simulation mode.
227    ///
228    /// # Arguments
229    /// * `sim_callback` - A mutable function reference that allows overriding individual simulation steps.
230    ///
231    /// # Returns
232    ///
233    /// Returns a `CuResult<()>`, which indicates the success or failure of the
234    /// operation.
235    /// - On success, the result is `Ok(())`.
236    /// - On failure, an appropriate error wrapped in `CuResult` is returned.
237    #[deprecated(
238        since = "1.2.0",
239        note = "use the typed transition `stop()` on the Running handle"
240    )]
241    fn stop_all_tasks(
242        &mut self,
243        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
244    ) -> CuResult<()>;
245
246    /// Restore all tasks from the given frozen state
247    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()>;
248}
249
250/// Optional introspection hook exposing the latest runtime-generated CopperList snapshot.
251///
252/// This is remote-debug-only: debugger conveniences must not add unconditional
253/// runtime-path overhead to normal Copper builds. Non-`remote-debug` builds
254/// should implement this as a cheap `None`.
255pub trait CurrentRuntimeCopperList<P: CopperListTuple> {
256    fn current_runtime_copperlist_bytes(&self) -> Option<&[u8]>;
257
258    fn set_current_runtime_copperlist_bytes(&mut self, snapshot: Option<Vec<u8>>) {
259        let _ = snapshot;
260    }
261}
262
263/// Simulation-enabled applications that can replay a recorded CopperList verbatim.
264///
265/// This is the exact-output replay primitive used by deterministic re-sim flows:
266/// task outputs and bridge receives are overridden from the recorded CopperList,
267/// bridge sends are skipped, and an optional recorded keyframe can be injected
268/// verbatim when the current CL is expected to capture one.
269#[cfg(feature = "std")]
270pub trait CuRecordedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
271    CuSimApplication<S, L>
272{
273    /// The generated recorded CopperList payload set for this application.
274    type RecordedDataSet: CopperListTuple;
275
276    /// Replay one recorded CopperList exactly as logged.
277    ///
278    /// Generated implementations validate continuity, drain prior asynchronous
279    /// output, and align the allocator to the recorded id before executing the
280    /// captured-output callback. This is an offline operation and may wait for
281    /// logging workers; it is not a real-time task-path API. A matching keyframe
282    /// permits a jump across missing history and preserves its recorded timing.
283    /// Call [`CuSimApplication::restore_keyframe`] separately when restoring task
284    /// state at that boundary.
285    fn replay_recorded_copperlist(
286        &mut self,
287        clock_mock: &RobotClockMock,
288        copperlist: &CopperList<Self::RecordedDataSet>,
289        keyframe: Option<&KeyFrame>,
290    ) -> CuResult<()>;
291}
292
293/// Simulation-enabled applications that can be instantiated for distributed replay.
294///
295/// This extends exact-output replay with the one extra capability the
296/// distributed engine needs: build a replayable app for a specific
297/// deployment `instance_id` while keeping app construction type-safe.
298#[cfg(feature = "std")]
299pub trait CuDistributedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
300    CuRecordedReplayApplication<S, L> + CuSubsystemMetadata
301{
302    /// Build this app for deterministic distributed replay.
303    fn build_distributed_replay(
304        clock: RobotClock,
305        unified_logger: Arc<Mutex<L>>,
306        instance_id: u32,
307        config_override: Option<CuConfig>,
308    ) -> CuResult<Self>
309    where
310        Self: Sized;
311}
312
313/// Typestate marker: the application is built but its tasks have not been started yet.
314pub struct Initialized;
315
316/// Typestate marker: tasks are started and iterations can be executed.
317pub struct Running;
318
319/// Typestate marker: tasks are stopped. The application can be started again,
320/// which is useful to chain missions within the same process.
321pub struct Stopped;
322
323/// Typestate marker: a lifecycle transition failed and the application may be
324/// partially started or partially stopped. The only way forward is a
325/// best-effort cleanup through `stop_all_tasks`.
326pub struct Faulted;
327
328mod sealed {
329    /// Seals the lifecycle state traits so external code cannot add new states
330    /// and bypass the transitions enforced by [`CuAppLifecycle`](super::CuAppLifecycle).
331    pub trait Sealed {}
332    impl Sealed for super::Initialized {}
333    impl Sealed for super::Running {}
334    impl Sealed for super::Stopped {}
335    impl Sealed for super::Faulted {}
336}
337
338/// Lifecycle states from which the application tasks can be started (`Initialized`, `Stopped`).
339#[diagnostic::on_unimplemented(
340    message = "the application cannot be started from the `{Self}` lifecycle state",
341    label = "`start_all_tasks` and `run` require an `Initialized` or `Stopped` application",
342    note = "a `Running` application is already started and a `Faulted` application must first be cleaned up with `stop_all_tasks`"
343)]
344pub trait Startable: sealed::Sealed {}
345impl Startable for Initialized {}
346impl Startable for Stopped {}
347
348/// Lifecycle states from which the application tasks can be stopped (`Running`, `Faulted`).
349#[diagnostic::on_unimplemented(
350    message = "the application cannot be stopped from the `{Self}` lifecycle state",
351    label = "`stop_all_tasks` requires a `Running` or `Faulted` application",
352    note = "start the application first with `start_all_tasks`"
353)]
354pub trait Stoppable: sealed::Sealed {}
355impl Stoppable for Running {}
356impl Stoppable for Faulted {}
357
358/// Compile-time enforced lifecycle for a [`CuApplication`].
359///
360/// Wrapping an application moves lifecycle mistakes (double start, iterating
361/// before start, mixing `start_all_tasks` with `run`...) from runtime surprises
362/// to compile errors: each state is a distinct type exposing only the
363/// transitions that are legal from it.
364///
365/// ```text
366///                      start
367///   Initialized ------------------> Running <---+
368///        |                            |  |      | run_one_iteration
369///        | run_until_shutdown         |  +------+
370///        |                            | stop
371///        +--------> Stopped <---------+
372///                    |   ^
373///                    |   | stop (cleanup)
374///        (restart)   |  Faulted <--- any failed transition
375///                    +---> start / run_until_shutdown again
376/// ```
377///
378/// Transitions consume the wrapper and return it typed with the new state, so
379/// a stale pre-transition handle cannot be reused. When a transition fails,
380/// the application is handed back inside [`LifecycleError`], typed [`Faulted`],
381/// so cleanup is still possible and nothing is lost.
382///
383/// This is the default construction path: the generated application builder
384/// hands one out from `build_app()`. The raw [`CuApplication`] methods remain
385/// available (deprecated on the generated application) for framework-level
386/// harnesses such as replay engines; [`into_inner`](CuAppLifecycle::into_inner)
387/// drops back to that level when needed.
388///
389/// The application is boxed internally: generated applications embed their
390/// copperlist pools and can be megabytes, so the consuming transitions move a
391/// pointer instead of the application itself (which in debug builds would
392/// blow through a test thread's stack).
393pub struct CuAppLifecycle<S, L, A, State = Initialized> {
394    app: Box<A>,
395    _lifecycle: PhantomData<(S, L, State)>,
396}
397
398impl<S, L, A, State> fmt::Debug for CuAppLifecycle<S, L, A, State> {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        f.debug_struct("CuAppLifecycle")
401            .field("state", &core::any::type_name::<State>())
402            .finish_non_exhaustive()
403    }
404}
405
406/// Convenience alias for applications using the default std unified logger,
407/// mirroring [`CuStdApplication`].
408#[cfg(feature = "std")]
409pub type CuStdAppLifecycle<A, State = Initialized> =
410    CuAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
411
412/// Result of a lifecycle transition: on success the application typed with its
413/// new state, on failure [`LifecycleError`] carrying the application typed [`Faulted`].
414pub type TransitionResult<S, L, A, Next> =
415    Result<CuAppLifecycle<S, L, A, Next>, LifecycleError<S, L, A>>;
416
417/// A failed lifecycle transition.
418///
419/// Because transitions consume the application by value, the failure path hands
420/// it back typed as [`Faulted`]: `app` only exposes `stop_all_tasks` for
421/// best-effort cleanup.
422pub struct LifecycleError<S, L, A> {
423    /// The error reported by the underlying application.
424    pub error: CuError,
425    /// The application, in the `Faulted` state. Call `stop_all_tasks` on it to clean up.
426    pub app: CuAppLifecycle<S, L, A, Faulted>,
427}
428
429impl<S, L, A> fmt::Debug for LifecycleError<S, L, A> {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        f.debug_struct("LifecycleError")
432            .field("error", &self.error)
433            .finish_non_exhaustive()
434    }
435}
436
437impl<S, L, A> fmt::Display for LifecycleError<S, L, A> {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        write!(f, "lifecycle transition failed: {}", self.error)
440    }
441}
442
443impl<S, L, A> core::error::Error for LifecycleError<S, L, A> {
444    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
445        Some(&self.error)
446    }
447}
448
449/// Allows `?` in functions returning `CuResult` when the faulted application
450/// does not need to be recovered.
451impl<S, L, A> From<LifecycleError<S, L, A>> for CuError {
452    fn from(value: LifecycleError<S, L, A>) -> Self {
453        value.error
454    }
455}
456
457impl<S, L, A, State> CuAppLifecycle<S, L, A, State> {
458    /// Rewraps the application under a new typestate. Private: state changes
459    /// only happen through the lifecycle transitions.
460    fn into_state<Next>(self) -> CuAppLifecycle<S, L, A, Next> {
461        CuAppLifecycle {
462            app: self.app,
463            _lifecycle: PhantomData,
464        }
465    }
466
467    /// Read-only access to the wrapped application.
468    pub fn inner(&self) -> &A {
469        &self.app
470    }
471
472    /// Consumes the wrapper and returns the application, dropping the
473    /// compile-time lifecycle tracking.
474    pub fn into_inner(self) -> A {
475        *self.app
476    }
477}
478
479impl<S, L, A> CuAppLifecycle<S, L, A, Initialized>
480where
481    S: SectionStorage,
482    L: UnifiedLogWrite<S> + 'static,
483    A: CuApplication<S, L>,
484{
485    /// Wraps a freshly built application in the `Initialized` state.
486    pub fn new(app: A) -> Self {
487        CuAppLifecycle {
488            app: Box::new(app),
489            _lifecycle: PhantomData,
490        }
491    }
492
493    /// Mutable access to the wrapped application, only before it is started.
494    ///
495    /// This exists for pre-start configuration (attaching monitors,
496    /// inspecting the runtime...). Once started, only the read-only
497    /// [`inner`](CuAppLifecycle::inner) view remains available so the
498    /// lifecycle transitions cannot be bypassed mid-flight.
499    pub fn inner_mut(&mut self) -> &mut A {
500        &mut self.app
501    }
502}
503
504impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
505where
506    S: SectionStorage,
507    L: UnifiedLogWrite<S> + 'static,
508    A: CuApplication<S, L>,
509    State: Startable,
510{
511    /// Starts all tasks, transitioning to `Running`.
512    ///
513    /// On failure the application may be partially started; it is returned
514    /// typed `Faulted` inside the error so it can be cleaned up.
515    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
516    pub fn start(mut self) -> TransitionResult<S, L, A, Running> {
517        match self.app.start_all_tasks() {
518            Ok(()) => Ok(self.into_state()),
519            Err(error) => Err(LifecycleError {
520                error,
521                app: self.into_state(),
522            }),
523        }
524    }
525
526    /// Runs the full lifecycle (start, iterate until shutdown, stop),
527    /// transitioning to `Stopped` on success.
528    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
529    pub fn run_until_shutdown(mut self) -> TransitionResult<S, L, A, Stopped> {
530        match self.app.run() {
531            Ok(()) => Ok(self.into_state()),
532            Err(error) => Err(LifecycleError {
533                error,
534                app: self.into_state(),
535            }),
536        }
537    }
538}
539
540impl<S, L, A> CuAppLifecycle<S, L, A, Running>
541where
542    S: SectionStorage,
543    L: UnifiedLogWrite<S> + 'static,
544    A: CuApplication<S, L>,
545{
546    /// Executes one iteration of the runtime. An iteration error does not
547    /// change the lifecycle state: the caller decides whether to keep
548    /// iterating or stop.
549    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
550    pub fn run_one_iteration(&mut self) -> CuResult<()> {
551        self.app.run_one_iteration()
552    }
553}
554
555impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
556where
557    S: SectionStorage,
558    L: UnifiedLogWrite<S> + 'static,
559    A: CuApplication<S, L>,
560    State: Stoppable,
561{
562    /// Stops all tasks, transitioning to `Stopped`. From `Faulted` this is a
563    /// best-effort cleanup. On failure the application is handed back typed
564    /// `Faulted` again.
565    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
566    pub fn stop(mut self) -> TransitionResult<S, L, A, Stopped> {
567        match self.app.stop_all_tasks() {
568            Ok(()) => Ok(self.into_state()),
569            Err(error) => Err(LifecycleError {
570                error,
571                app: self.into_state(),
572            }),
573        }
574    }
575}
576
577/// Read access to the wrapped application in every lifecycle state, so
578/// conveniences like `app.clock()` keep working without ceremony.
579impl<S, L, A, State> core::ops::Deref for CuAppLifecycle<S, L, A, State> {
580    type Target = A;
581
582    fn deref(&self) -> &A {
583        &self.app
584    }
585}
586
587/// Mutable access only before the application is started: after a typed
588/// start, raw mutable access could bypass the lifecycle guarantees.
589impl<S, L, A> core::ops::DerefMut for CuAppLifecycle<S, L, A, Initialized> {
590    fn deref_mut(&mut self) -> &mut A {
591        &mut self.app
592    }
593}
594
595/// Legacy escape hatch: the raw (deprecated) lifecycle API remains callable
596/// on what `build()` returns, so pre-typestate code compiles unchanged and
597/// only picks up deprecation warnings. Raw calls do not advance the
598/// typestate; mixing them with typed transitions is outside the typestate
599/// guarantees.
600#[allow(deprecated)]
601impl<S, L, A> CuApplication<S, L> for CuAppLifecycle<S, L, A, Initialized>
602where
603    S: SectionStorage,
604    L: UnifiedLogWrite<S> + 'static,
605    A: CuApplication<S, L>,
606{
607    fn get_original_config() -> String {
608        A::get_original_config()
609    }
610
611    fn start_all_tasks(&mut self) -> CuResult<()> {
612        self.app.start_all_tasks()
613    }
614
615    fn run_one_iteration(&mut self) -> CuResult<()> {
616        self.app.run_one_iteration()
617    }
618
619    fn run(&mut self) -> CuResult<()> {
620        self.app.run()
621    }
622
623    fn stop_all_tasks(&mut self) -> CuResult<()> {
624        self.app.stop_all_tasks()
625    }
626
627    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
628        self.app.restore_keyframe(freezer)
629    }
630}
631
632/// Compile-time enforced lifecycle for a [`CuSimApplication`].
633///
634/// Simulation counterpart of [`CuAppLifecycle`]: same states and transitions,
635/// with each transition threading the `sim_callback` the simulation runtime
636/// requires. Replay primitives (`replay_recorded_copperlist`,
637/// `restore_keyframe`) are framework-level and stay on the raw traits.
638#[cfg(feature = "std")]
639pub struct CuSimAppLifecycle<S, L, A, State = Initialized> {
640    app: Box<A>,
641    _lifecycle: PhantomData<(S, L, State)>,
642}
643
644#[cfg(feature = "std")]
645impl<S, L, A, State> fmt::Debug for CuSimAppLifecycle<S, L, A, State> {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        f.debug_struct("CuSimAppLifecycle")
648            .field("state", &core::any::type_name::<State>())
649            .finish_non_exhaustive()
650    }
651}
652
653/// Convenience alias for simulation applications using the default std
654/// unified logger, mirroring [`CuStdAppLifecycle`].
655#[cfg(feature = "std")]
656pub type CuStdSimAppLifecycle<A, State = Initialized> =
657    CuSimAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
658
659/// Result of a simulation lifecycle transition: on success the application
660/// typed with its new state, on failure [`SimLifecycleError`] carrying the
661/// application typed [`Faulted`].
662#[cfg(feature = "std")]
663pub type SimTransitionResult<S, L, A, Next> =
664    Result<CuSimAppLifecycle<S, L, A, Next>, SimLifecycleError<S, L, A>>;
665
666/// A failed simulation lifecycle transition, mirroring [`LifecycleError`].
667#[cfg(feature = "std")]
668pub struct SimLifecycleError<S, L, A> {
669    /// The error reported by the underlying application.
670    pub error: CuError,
671    /// The application, in the `Faulted` state. Call `stop_all_tasks` on it to clean up.
672    pub app: CuSimAppLifecycle<S, L, A, Faulted>,
673}
674
675#[cfg(feature = "std")]
676impl<S, L, A> fmt::Debug for SimLifecycleError<S, L, A> {
677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678        f.debug_struct("SimLifecycleError")
679            .field("error", &self.error)
680            .finish_non_exhaustive()
681    }
682}
683
684#[cfg(feature = "std")]
685impl<S, L, A> fmt::Display for SimLifecycleError<S, L, A> {
686    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687        write!(f, "lifecycle transition failed: {}", self.error)
688    }
689}
690
691#[cfg(feature = "std")]
692impl<S, L, A> core::error::Error for SimLifecycleError<S, L, A> {
693    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
694        Some(&self.error)
695    }
696}
697
698/// Allows `?` in functions returning `CuResult` when the faulted application
699/// does not need to be recovered.
700#[cfg(feature = "std")]
701impl<S, L, A> From<SimLifecycleError<S, L, A>> for CuError {
702    fn from(value: SimLifecycleError<S, L, A>) -> Self {
703        value.error
704    }
705}
706
707#[cfg(feature = "std")]
708impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State> {
709    /// Rewraps the application under a new typestate. Private: state changes
710    /// only happen through the lifecycle transitions.
711    fn into_state<Next>(self) -> CuSimAppLifecycle<S, L, A, Next> {
712        CuSimAppLifecycle {
713            app: self.app,
714            _lifecycle: PhantomData,
715        }
716    }
717
718    /// Read-only access to the wrapped application.
719    pub fn inner(&self) -> &A {
720        &self.app
721    }
722
723    /// Consumes the wrapper and returns the application, dropping the
724    /// compile-time lifecycle tracking.
725    pub fn into_inner(self) -> A {
726        *self.app
727    }
728}
729
730#[cfg(feature = "std")]
731impl<S, L, A> CuSimAppLifecycle<S, L, A, Initialized>
732where
733    S: SectionStorage,
734    L: UnifiedLogWrite<S> + 'static,
735    A: CuSimApplication<S, L>,
736{
737    /// Wraps a freshly built simulation application in the `Initialized` state.
738    pub fn new(app: A) -> Self {
739        CuSimAppLifecycle {
740            app: Box::new(app),
741            _lifecycle: PhantomData,
742        }
743    }
744}
745
746#[cfg(feature = "std")]
747impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
748where
749    S: SectionStorage,
750    L: UnifiedLogWrite<S> + 'static,
751    A: CuSimApplication<S, L>,
752    State: Startable,
753{
754    /// Starts all tasks, transitioning to `Running`.
755    ///
756    /// On failure the application may be partially started; it is returned
757    /// typed `Faulted` inside the error so it can be cleaned up.
758    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
759    pub fn start(
760        mut self,
761        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
762    ) -> SimTransitionResult<S, L, A, Running> {
763        match self.app.start_all_tasks(sim_callback) {
764            Ok(()) => Ok(self.into_state()),
765            Err(error) => Err(SimLifecycleError {
766                error,
767                app: self.into_state(),
768            }),
769        }
770    }
771
772    /// Runs the full lifecycle (start, iterate until shutdown, stop),
773    /// transitioning to `Stopped` on success.
774    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
775    pub fn run_until_shutdown(
776        mut self,
777        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
778    ) -> SimTransitionResult<S, L, A, Stopped> {
779        match self.app.run(sim_callback) {
780            Ok(()) => Ok(self.into_state()),
781            Err(error) => Err(SimLifecycleError {
782                error,
783                app: self.into_state(),
784            }),
785        }
786    }
787}
788
789#[cfg(feature = "std")]
790impl<S, L, A> CuSimAppLifecycle<S, L, A, Running>
791where
792    S: SectionStorage,
793    L: UnifiedLogWrite<S> + 'static,
794    A: CuSimApplication<S, L>,
795{
796    /// Executes one iteration of the runtime. An iteration error does not
797    /// change the lifecycle state: the caller decides whether to keep
798    /// iterating or stop.
799    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
800    pub fn run_one_iteration(
801        &mut self,
802        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
803    ) -> CuResult<()> {
804        self.app.run_one_iteration(sim_callback)
805    }
806}
807
808#[cfg(feature = "std")]
809impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
810where
811    S: SectionStorage,
812    L: UnifiedLogWrite<S> + 'static,
813    A: CuSimApplication<S, L>,
814    State: Stoppable,
815{
816    /// Stops all tasks, transitioning to `Stopped`. From `Faulted` this is a
817    /// best-effort cleanup. On failure the application is handed back typed
818    /// `Faulted` again.
819    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
820    pub fn stop(
821        mut self,
822        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
823    ) -> SimTransitionResult<S, L, A, Stopped> {
824        match self.app.stop_all_tasks(sim_callback) {
825            Ok(()) => Ok(self.into_state()),
826            Err(error) => Err(SimLifecycleError {
827                error,
828                app: self.into_state(),
829            }),
830        }
831    }
832}
833
834/// Read access to the wrapped application in every lifecycle state.
835#[cfg(feature = "std")]
836impl<S, L, A, State> core::ops::Deref for CuSimAppLifecycle<S, L, A, State> {
837    type Target = A;
838
839    fn deref(&self) -> &A {
840        &self.app
841    }
842}
843
844/// Mutable access only before the application is started, mirroring
845/// [`CuAppLifecycle`].
846#[cfg(feature = "std")]
847impl<S, L, A> core::ops::DerefMut for CuSimAppLifecycle<S, L, A, Initialized> {
848    fn deref_mut(&mut self) -> &mut A {
849        &mut self.app
850    }
851}
852
853/// Legacy escape hatch mirroring the [`CuApplication`] impl on
854/// [`CuAppLifecycle`]: pre-typestate simulation code compiles unchanged
855/// against what `build()` returns and only picks up deprecation warnings.
856#[cfg(feature = "std")]
857#[allow(deprecated)]
858impl<S, L, A> CuSimApplication<S, L> for CuSimAppLifecycle<S, L, A, Initialized>
859where
860    S: SectionStorage,
861    L: UnifiedLogWrite<S> + 'static,
862    A: CuSimApplication<S, L>,
863{
864    type Step<'z> = <A as CuSimApplication<S, L>>::Step<'z>;
865
866    fn get_original_config() -> String {
867        A::get_original_config()
868    }
869
870    fn mission_id() -> Option<&'static str> {
871        A::mission_id()
872    }
873
874    fn start_all_tasks(
875        &mut self,
876        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
877    ) -> CuResult<()> {
878        self.app.start_all_tasks(sim_callback)
879    }
880
881    fn run_one_iteration(
882        &mut self,
883        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
884    ) -> CuResult<()> {
885        self.app.run_one_iteration(sim_callback)
886    }
887
888    fn run(
889        &mut self,
890        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
891    ) -> CuResult<()> {
892        self.app.run(sim_callback)
893    }
894
895    fn stop_all_tasks(
896        &mut self,
897        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
898    ) -> CuResult<()> {
899        self.app.stop_all_tasks(sim_callback)
900    }
901
902    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
903        self.app.restore_keyframe(freezer)
904    }
905}
906
907#[cfg(all(test, feature = "std"))]
908mod lifecycle_tests {
909    use super::*;
910
911    #[derive(Default)]
912    struct MockApp {
913        started: u32,
914        stopped: u32,
915        iterations: u32,
916        runs: u32,
917        fail_start: bool,
918        fail_stop: bool,
919    }
920
921    #[allow(deprecated)] // mocks implement the deprecated raw trait methods
922    impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for MockApp {
923        fn get_original_config() -> String {
924            String::new()
925        }
926
927        fn start_all_tasks(&mut self) -> CuResult<()> {
928            if self.fail_start {
929                return Err("mock start failure".into());
930            }
931            self.started += 1;
932            Ok(())
933        }
934
935        fn run_one_iteration(&mut self) -> CuResult<()> {
936            self.iterations += 1;
937            Ok(())
938        }
939
940        fn run(&mut self) -> CuResult<()> {
941            if self.fail_start {
942                return Err("mock run failure".into());
943            }
944            self.runs += 1;
945            Ok(())
946        }
947
948        fn stop_all_tasks(&mut self) -> CuResult<()> {
949            if self.fail_stop {
950                return Err("mock stop failure".into());
951            }
952            self.stopped += 1;
953            Ok(())
954        }
955
956        fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
957            Ok(())
958        }
959    }
960
961    type Lifecycle = CuStdAppLifecycle<MockApp>;
962
963    #[test]
964    fn full_cycle_with_restart() {
965        let app = Lifecycle::new(MockApp::default());
966        let mut running = app.start().unwrap();
967        running.run_one_iteration().unwrap();
968        running.run_one_iteration().unwrap();
969        let stopped = running.stop().unwrap();
970
971        // Restarting a stopped application is legal (mission chaining).
972        let running = stopped.start().unwrap();
973        let stopped = running.stop().unwrap();
974
975        let mock = stopped.into_inner();
976        assert_eq!(mock.started, 2);
977        assert_eq!(mock.iterations, 2);
978        assert_eq!(mock.stopped, 2);
979    }
980
981    #[test]
982    fn run_transitions_to_stopped_and_can_run_again() {
983        let app = Lifecycle::new(MockApp::default());
984        let stopped = app.run_until_shutdown().unwrap();
985        let stopped = stopped.run_until_shutdown().unwrap();
986        assert_eq!(stopped.inner().runs, 2);
987    }
988
989    #[test]
990    fn failed_start_hands_back_a_faulted_app_for_cleanup() {
991        let app = Lifecycle::new(MockApp {
992            fail_start: true,
993            ..Default::default()
994        });
995        let err = app.start().unwrap_err();
996        assert!(err.error.to_string().contains("mock start failure"));
997
998        // The only legal transition from Faulted is the cleanup.
999        let stopped = err.app.stop().unwrap();
1000        let mock = stopped.into_inner();
1001        assert_eq!(mock.started, 0);
1002        assert_eq!(mock.stopped, 1);
1003    }
1004
1005    #[test]
1006    fn failed_stop_hands_back_a_faulted_app() {
1007        let app = Lifecycle::new(MockApp {
1008            fail_stop: true,
1009            ..Default::default()
1010        });
1011        let running = app.start().unwrap();
1012        let err = running.stop().unwrap_err();
1013        assert!(err.error.to_string().contains("mock stop failure"));
1014    }
1015
1016    #[test]
1017    fn lifecycle_error_propagates_with_question_mark() {
1018        fn drive() -> CuResult<()> {
1019            let app = Lifecycle::new(MockApp {
1020                fail_start: true,
1021                ..Default::default()
1022            });
1023            let _running = app.start()?;
1024            Ok(())
1025        }
1026        let error = drive().unwrap_err();
1027        assert!(error.to_string().contains("mock start failure"));
1028    }
1029
1030    #[derive(Default)]
1031    struct MockSimApp {
1032        started: u32,
1033        stopped: u32,
1034        iterations: u32,
1035        fail_start: bool,
1036    }
1037
1038    struct MockStep;
1039
1040    #[allow(deprecated)] // mocks implement the deprecated raw trait methods
1041    impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for MockSimApp {
1042        type Step<'z> = MockStep;
1043
1044        fn get_original_config() -> String {
1045            String::new()
1046        }
1047
1048        fn start_all_tasks(
1049            &mut self,
1050            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1051        ) -> CuResult<()> {
1052            if self.fail_start {
1053                return Err("mock sim start failure".into());
1054            }
1055            let _ = sim_callback(MockStep);
1056            self.started += 1;
1057            Ok(())
1058        }
1059
1060        fn run_one_iteration(
1061            &mut self,
1062            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1063        ) -> CuResult<()> {
1064            let _ = sim_callback(MockStep);
1065            self.iterations += 1;
1066            Ok(())
1067        }
1068
1069        fn run(
1070            &mut self,
1071            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1072        ) -> CuResult<()> {
1073            let _ = sim_callback(MockStep);
1074            Ok(())
1075        }
1076
1077        fn stop_all_tasks(
1078            &mut self,
1079            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1080        ) -> CuResult<()> {
1081            let _ = sim_callback(MockStep);
1082            self.stopped += 1;
1083            Ok(())
1084        }
1085
1086        fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
1087            Ok(())
1088        }
1089    }
1090
1091    type SimLifecycle = CuStdSimAppLifecycle<MockSimApp>;
1092
1093    #[test]
1094    fn sim_full_cycle_threads_the_callback() {
1095        let mut callback_calls = 0u32;
1096        let mut cb = |_step: MockStep| -> SimOverride {
1097            callback_calls += 1;
1098            SimOverride::ExecuteByRuntime
1099        };
1100
1101        let app = SimLifecycle::new(MockSimApp::default());
1102        let mut running = app.start(&mut cb).unwrap();
1103        running.run_one_iteration(&mut cb).unwrap();
1104        let stopped = running.stop(&mut cb).unwrap();
1105
1106        let mock = stopped.into_inner();
1107        assert_eq!(mock.started, 1);
1108        assert_eq!(mock.iterations, 1);
1109        assert_eq!(mock.stopped, 1);
1110        assert_eq!(callback_calls, 3);
1111    }
1112
1113    #[test]
1114    fn sim_failed_start_hands_back_a_faulted_app_for_cleanup() {
1115        let mut cb = |_step: MockStep| -> SimOverride { SimOverride::ExecuteByRuntime };
1116        let app = SimLifecycle::new(MockSimApp {
1117            fail_start: true,
1118            ..Default::default()
1119        });
1120        let err = app.start(&mut cb).unwrap_err();
1121        assert!(err.error.to_string().contains("mock sim start failure"));
1122        let stopped = err.app.stop(&mut cb).unwrap();
1123        assert_eq!(stopped.inner().stopped, 1);
1124    }
1125}