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    fn replay_recorded_copperlist(
278        &mut self,
279        clock_mock: &RobotClockMock,
280        copperlist: &CopperList<Self::RecordedDataSet>,
281        keyframe: Option<&KeyFrame>,
282    ) -> CuResult<()>;
283}
284
285/// Simulation-enabled applications that can be instantiated for distributed replay.
286///
287/// This extends exact-output replay with the one extra capability the
288/// distributed engine needs: build a replayable app for a specific
289/// deployment `instance_id` while keeping app construction type-safe.
290#[cfg(feature = "std")]
291pub trait CuDistributedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
292    CuRecordedReplayApplication<S, L> + CuSubsystemMetadata
293{
294    /// Build this app for deterministic distributed replay.
295    fn build_distributed_replay(
296        clock: RobotClock,
297        unified_logger: Arc<Mutex<L>>,
298        instance_id: u32,
299        config_override: Option<CuConfig>,
300    ) -> CuResult<Self>
301    where
302        Self: Sized;
303}
304
305/// Typestate marker: the application is built but its tasks have not been started yet.
306pub struct Initialized;
307
308/// Typestate marker: tasks are started and iterations can be executed.
309pub struct Running;
310
311/// Typestate marker: tasks are stopped. The application can be started again,
312/// which is useful to chain missions within the same process.
313pub struct Stopped;
314
315/// Typestate marker: a lifecycle transition failed and the application may be
316/// partially started or partially stopped. The only way forward is a
317/// best-effort cleanup through `stop_all_tasks`.
318pub struct Faulted;
319
320mod sealed {
321    /// Seals the lifecycle state traits so external code cannot add new states
322    /// and bypass the transitions enforced by [`CuAppLifecycle`](super::CuAppLifecycle).
323    pub trait Sealed {}
324    impl Sealed for super::Initialized {}
325    impl Sealed for super::Running {}
326    impl Sealed for super::Stopped {}
327    impl Sealed for super::Faulted {}
328}
329
330/// Lifecycle states from which the application tasks can be started (`Initialized`, `Stopped`).
331#[diagnostic::on_unimplemented(
332    message = "the application cannot be started from the `{Self}` lifecycle state",
333    label = "`start_all_tasks` and `run` require an `Initialized` or `Stopped` application",
334    note = "a `Running` application is already started and a `Faulted` application must first be cleaned up with `stop_all_tasks`"
335)]
336pub trait Startable: sealed::Sealed {}
337impl Startable for Initialized {}
338impl Startable for Stopped {}
339
340/// Lifecycle states from which the application tasks can be stopped (`Running`, `Faulted`).
341#[diagnostic::on_unimplemented(
342    message = "the application cannot be stopped from the `{Self}` lifecycle state",
343    label = "`stop_all_tasks` requires a `Running` or `Faulted` application",
344    note = "start the application first with `start_all_tasks`"
345)]
346pub trait Stoppable: sealed::Sealed {}
347impl Stoppable for Running {}
348impl Stoppable for Faulted {}
349
350/// Compile-time enforced lifecycle for a [`CuApplication`].
351///
352/// Wrapping an application moves lifecycle mistakes (double start, iterating
353/// before start, mixing `start_all_tasks` with `run`...) from runtime surprises
354/// to compile errors: each state is a distinct type exposing only the
355/// transitions that are legal from it.
356///
357/// ```text
358///                      start
359///   Initialized ------------------> Running <---+
360///        |                            |  |      | run_one_iteration
361///        | run_until_shutdown         |  +------+
362///        |                            | stop
363///        +--------> Stopped <---------+
364///                    |   ^
365///                    |   | stop (cleanup)
366///        (restart)   |  Faulted <--- any failed transition
367///                    +---> start / run_until_shutdown again
368/// ```
369///
370/// Transitions consume the wrapper and return it typed with the new state, so
371/// a stale pre-transition handle cannot be reused. When a transition fails,
372/// the application is handed back inside [`LifecycleError`], typed [`Faulted`],
373/// so cleanup is still possible and nothing is lost.
374///
375/// This is the default construction path: the generated application builder
376/// hands one out from `build_app()`. The raw [`CuApplication`] methods remain
377/// available (deprecated on the generated application) for framework-level
378/// harnesses such as replay engines; [`into_inner`](CuAppLifecycle::into_inner)
379/// drops back to that level when needed.
380///
381/// The application is boxed internally: generated applications embed their
382/// copperlist pools and can be megabytes, so the consuming transitions move a
383/// pointer instead of the application itself (which in debug builds would
384/// blow through a test thread's stack).
385pub struct CuAppLifecycle<S, L, A, State = Initialized> {
386    app: Box<A>,
387    _lifecycle: PhantomData<(S, L, State)>,
388}
389
390impl<S, L, A, State> fmt::Debug for CuAppLifecycle<S, L, A, State> {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        f.debug_struct("CuAppLifecycle")
393            .field("state", &core::any::type_name::<State>())
394            .finish_non_exhaustive()
395    }
396}
397
398/// Convenience alias for applications using the default std unified logger,
399/// mirroring [`CuStdApplication`].
400#[cfg(feature = "std")]
401pub type CuStdAppLifecycle<A, State = Initialized> =
402    CuAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
403
404/// Result of a lifecycle transition: on success the application typed with its
405/// new state, on failure [`LifecycleError`] carrying the application typed [`Faulted`].
406pub type TransitionResult<S, L, A, Next> =
407    Result<CuAppLifecycle<S, L, A, Next>, LifecycleError<S, L, A>>;
408
409/// A failed lifecycle transition.
410///
411/// Because transitions consume the application by value, the failure path hands
412/// it back typed as [`Faulted`]: `app` only exposes `stop_all_tasks` for
413/// best-effort cleanup.
414pub struct LifecycleError<S, L, A> {
415    /// The error reported by the underlying application.
416    pub error: CuError,
417    /// The application, in the `Faulted` state. Call `stop_all_tasks` on it to clean up.
418    pub app: CuAppLifecycle<S, L, A, Faulted>,
419}
420
421impl<S, L, A> fmt::Debug for LifecycleError<S, L, A> {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        f.debug_struct("LifecycleError")
424            .field("error", &self.error)
425            .finish_non_exhaustive()
426    }
427}
428
429impl<S, L, A> fmt::Display for LifecycleError<S, L, A> {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        write!(f, "lifecycle transition failed: {}", self.error)
432    }
433}
434
435impl<S, L, A> core::error::Error for LifecycleError<S, L, A> {
436    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
437        Some(&self.error)
438    }
439}
440
441/// Allows `?` in functions returning `CuResult` when the faulted application
442/// does not need to be recovered.
443impl<S, L, A> From<LifecycleError<S, L, A>> for CuError {
444    fn from(value: LifecycleError<S, L, A>) -> Self {
445        value.error
446    }
447}
448
449impl<S, L, A, State> CuAppLifecycle<S, L, A, State> {
450    /// Rewraps the application under a new typestate. Private: state changes
451    /// only happen through the lifecycle transitions.
452    fn into_state<Next>(self) -> CuAppLifecycle<S, L, A, Next> {
453        CuAppLifecycle {
454            app: self.app,
455            _lifecycle: PhantomData,
456        }
457    }
458
459    /// Read-only access to the wrapped application.
460    pub fn inner(&self) -> &A {
461        &self.app
462    }
463
464    /// Consumes the wrapper and returns the application, dropping the
465    /// compile-time lifecycle tracking.
466    pub fn into_inner(self) -> A {
467        *self.app
468    }
469}
470
471impl<S, L, A> CuAppLifecycle<S, L, A, Initialized>
472where
473    S: SectionStorage,
474    L: UnifiedLogWrite<S> + 'static,
475    A: CuApplication<S, L>,
476{
477    /// Wraps a freshly built application in the `Initialized` state.
478    pub fn new(app: A) -> Self {
479        CuAppLifecycle {
480            app: Box::new(app),
481            _lifecycle: PhantomData,
482        }
483    }
484
485    /// Mutable access to the wrapped application, only before it is started.
486    ///
487    /// This exists for pre-start configuration (attaching monitors,
488    /// inspecting the runtime...). Once started, only the read-only
489    /// [`inner`](CuAppLifecycle::inner) view remains available so the
490    /// lifecycle transitions cannot be bypassed mid-flight.
491    pub fn inner_mut(&mut self) -> &mut A {
492        &mut self.app
493    }
494}
495
496impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
497where
498    S: SectionStorage,
499    L: UnifiedLogWrite<S> + 'static,
500    A: CuApplication<S, L>,
501    State: Startable,
502{
503    /// Starts all tasks, transitioning to `Running`.
504    ///
505    /// On failure the application may be partially started; it is returned
506    /// typed `Faulted` inside the error so it can be cleaned up.
507    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
508    pub fn start(mut self) -> TransitionResult<S, L, A, Running> {
509        match self.app.start_all_tasks() {
510            Ok(()) => Ok(self.into_state()),
511            Err(error) => Err(LifecycleError {
512                error,
513                app: self.into_state(),
514            }),
515        }
516    }
517
518    /// Runs the full lifecycle (start, iterate until shutdown, stop),
519    /// transitioning to `Stopped` on success.
520    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
521    pub fn run_until_shutdown(mut self) -> TransitionResult<S, L, A, Stopped> {
522        match self.app.run() {
523            Ok(()) => Ok(self.into_state()),
524            Err(error) => Err(LifecycleError {
525                error,
526                app: self.into_state(),
527            }),
528        }
529    }
530}
531
532impl<S, L, A> CuAppLifecycle<S, L, A, Running>
533where
534    S: SectionStorage,
535    L: UnifiedLogWrite<S> + 'static,
536    A: CuApplication<S, L>,
537{
538    /// Executes one iteration of the runtime. An iteration error does not
539    /// change the lifecycle state: the caller decides whether to keep
540    /// iterating or stop.
541    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
542    pub fn run_one_iteration(&mut self) -> CuResult<()> {
543        self.app.run_one_iteration()
544    }
545}
546
547impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
548where
549    S: SectionStorage,
550    L: UnifiedLogWrite<S> + 'static,
551    A: CuApplication<S, L>,
552    State: Stoppable,
553{
554    /// Stops all tasks, transitioning to `Stopped`. From `Faulted` this is a
555    /// best-effort cleanup. On failure the application is handed back typed
556    /// `Faulted` again.
557    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
558    pub fn stop(mut self) -> TransitionResult<S, L, A, Stopped> {
559        match self.app.stop_all_tasks() {
560            Ok(()) => Ok(self.into_state()),
561            Err(error) => Err(LifecycleError {
562                error,
563                app: self.into_state(),
564            }),
565        }
566    }
567}
568
569/// Read access to the wrapped application in every lifecycle state, so
570/// conveniences like `app.clock()` keep working without ceremony.
571impl<S, L, A, State> core::ops::Deref for CuAppLifecycle<S, L, A, State> {
572    type Target = A;
573
574    fn deref(&self) -> &A {
575        &self.app
576    }
577}
578
579/// Mutable access only before the application is started: after a typed
580/// start, raw mutable access could bypass the lifecycle guarantees.
581impl<S, L, A> core::ops::DerefMut for CuAppLifecycle<S, L, A, Initialized> {
582    fn deref_mut(&mut self) -> &mut A {
583        &mut self.app
584    }
585}
586
587/// Legacy escape hatch: the raw (deprecated) lifecycle API remains callable
588/// on what `build()` returns, so pre-typestate code compiles unchanged and
589/// only picks up deprecation warnings. Raw calls do not advance the
590/// typestate; mixing them with typed transitions is outside the typestate
591/// guarantees.
592#[allow(deprecated)]
593impl<S, L, A> CuApplication<S, L> for CuAppLifecycle<S, L, A, Initialized>
594where
595    S: SectionStorage,
596    L: UnifiedLogWrite<S> + 'static,
597    A: CuApplication<S, L>,
598{
599    fn get_original_config() -> String {
600        A::get_original_config()
601    }
602
603    fn start_all_tasks(&mut self) -> CuResult<()> {
604        self.app.start_all_tasks()
605    }
606
607    fn run_one_iteration(&mut self) -> CuResult<()> {
608        self.app.run_one_iteration()
609    }
610
611    fn run(&mut self) -> CuResult<()> {
612        self.app.run()
613    }
614
615    fn stop_all_tasks(&mut self) -> CuResult<()> {
616        self.app.stop_all_tasks()
617    }
618
619    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
620        self.app.restore_keyframe(freezer)
621    }
622}
623
624/// Compile-time enforced lifecycle for a [`CuSimApplication`].
625///
626/// Simulation counterpart of [`CuAppLifecycle`]: same states and transitions,
627/// with each transition threading the `sim_callback` the simulation runtime
628/// requires. Replay primitives (`replay_recorded_copperlist`,
629/// `restore_keyframe`) are framework-level and stay on the raw traits.
630#[cfg(feature = "std")]
631pub struct CuSimAppLifecycle<S, L, A, State = Initialized> {
632    app: Box<A>,
633    _lifecycle: PhantomData<(S, L, State)>,
634}
635
636#[cfg(feature = "std")]
637impl<S, L, A, State> fmt::Debug for CuSimAppLifecycle<S, L, A, State> {
638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639        f.debug_struct("CuSimAppLifecycle")
640            .field("state", &core::any::type_name::<State>())
641            .finish_non_exhaustive()
642    }
643}
644
645/// Convenience alias for simulation applications using the default std
646/// unified logger, mirroring [`CuStdAppLifecycle`].
647#[cfg(feature = "std")]
648pub type CuStdSimAppLifecycle<A, State = Initialized> =
649    CuSimAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
650
651/// Result of a simulation lifecycle transition: on success the application
652/// typed with its new state, on failure [`SimLifecycleError`] carrying the
653/// application typed [`Faulted`].
654#[cfg(feature = "std")]
655pub type SimTransitionResult<S, L, A, Next> =
656    Result<CuSimAppLifecycle<S, L, A, Next>, SimLifecycleError<S, L, A>>;
657
658/// A failed simulation lifecycle transition, mirroring [`LifecycleError`].
659#[cfg(feature = "std")]
660pub struct SimLifecycleError<S, L, A> {
661    /// The error reported by the underlying application.
662    pub error: CuError,
663    /// The application, in the `Faulted` state. Call `stop_all_tasks` on it to clean up.
664    pub app: CuSimAppLifecycle<S, L, A, Faulted>,
665}
666
667#[cfg(feature = "std")]
668impl<S, L, A> fmt::Debug for SimLifecycleError<S, L, A> {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        f.debug_struct("SimLifecycleError")
671            .field("error", &self.error)
672            .finish_non_exhaustive()
673    }
674}
675
676#[cfg(feature = "std")]
677impl<S, L, A> fmt::Display for SimLifecycleError<S, L, A> {
678    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679        write!(f, "lifecycle transition failed: {}", self.error)
680    }
681}
682
683#[cfg(feature = "std")]
684impl<S, L, A> core::error::Error for SimLifecycleError<S, L, A> {
685    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
686        Some(&self.error)
687    }
688}
689
690/// Allows `?` in functions returning `CuResult` when the faulted application
691/// does not need to be recovered.
692#[cfg(feature = "std")]
693impl<S, L, A> From<SimLifecycleError<S, L, A>> for CuError {
694    fn from(value: SimLifecycleError<S, L, A>) -> Self {
695        value.error
696    }
697}
698
699#[cfg(feature = "std")]
700impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State> {
701    /// Rewraps the application under a new typestate. Private: state changes
702    /// only happen through the lifecycle transitions.
703    fn into_state<Next>(self) -> CuSimAppLifecycle<S, L, A, Next> {
704        CuSimAppLifecycle {
705            app: self.app,
706            _lifecycle: PhantomData,
707        }
708    }
709
710    /// Read-only access to the wrapped application.
711    pub fn inner(&self) -> &A {
712        &self.app
713    }
714
715    /// Consumes the wrapper and returns the application, dropping the
716    /// compile-time lifecycle tracking.
717    pub fn into_inner(self) -> A {
718        *self.app
719    }
720}
721
722#[cfg(feature = "std")]
723impl<S, L, A> CuSimAppLifecycle<S, L, A, Initialized>
724where
725    S: SectionStorage,
726    L: UnifiedLogWrite<S> + 'static,
727    A: CuSimApplication<S, L>,
728{
729    /// Wraps a freshly built simulation application in the `Initialized` state.
730    pub fn new(app: A) -> Self {
731        CuSimAppLifecycle {
732            app: Box::new(app),
733            _lifecycle: PhantomData,
734        }
735    }
736}
737
738#[cfg(feature = "std")]
739impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
740where
741    S: SectionStorage,
742    L: UnifiedLogWrite<S> + 'static,
743    A: CuSimApplication<S, L>,
744    State: Startable,
745{
746    /// Starts all tasks, transitioning to `Running`.
747    ///
748    /// On failure the application may be partially started; it is returned
749    /// typed `Faulted` inside the error so it can be cleaned up.
750    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
751    pub fn start(
752        mut self,
753        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
754    ) -> SimTransitionResult<S, L, A, Running> {
755        match self.app.start_all_tasks(sim_callback) {
756            Ok(()) => Ok(self.into_state()),
757            Err(error) => Err(SimLifecycleError {
758                error,
759                app: self.into_state(),
760            }),
761        }
762    }
763
764    /// Runs the full lifecycle (start, iterate until shutdown, stop),
765    /// transitioning to `Stopped` on success.
766    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
767    pub fn run_until_shutdown(
768        mut self,
769        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
770    ) -> SimTransitionResult<S, L, A, Stopped> {
771        match self.app.run(sim_callback) {
772            Ok(()) => Ok(self.into_state()),
773            Err(error) => Err(SimLifecycleError {
774                error,
775                app: self.into_state(),
776            }),
777        }
778    }
779}
780
781#[cfg(feature = "std")]
782impl<S, L, A> CuSimAppLifecycle<S, L, A, Running>
783where
784    S: SectionStorage,
785    L: UnifiedLogWrite<S> + 'static,
786    A: CuSimApplication<S, L>,
787{
788    /// Executes one iteration of the runtime. An iteration error does not
789    /// change the lifecycle state: the caller decides whether to keep
790    /// iterating or stop.
791    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
792    pub fn run_one_iteration(
793        &mut self,
794        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
795    ) -> CuResult<()> {
796        self.app.run_one_iteration(sim_callback)
797    }
798}
799
800#[cfg(feature = "std")]
801impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
802where
803    S: SectionStorage,
804    L: UnifiedLogWrite<S> + 'static,
805    A: CuSimApplication<S, L>,
806    State: Stoppable,
807{
808    /// Stops all tasks, transitioning to `Stopped`. From `Faulted` this is a
809    /// best-effort cleanup. On failure the application is handed back typed
810    /// `Faulted` again.
811    #[allow(deprecated)] // forwards to the raw trait, deprecated for direct use only
812    pub fn stop(
813        mut self,
814        sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
815    ) -> SimTransitionResult<S, L, A, Stopped> {
816        match self.app.stop_all_tasks(sim_callback) {
817            Ok(()) => Ok(self.into_state()),
818            Err(error) => Err(SimLifecycleError {
819                error,
820                app: self.into_state(),
821            }),
822        }
823    }
824}
825
826/// Read access to the wrapped application in every lifecycle state.
827#[cfg(feature = "std")]
828impl<S, L, A, State> core::ops::Deref for CuSimAppLifecycle<S, L, A, State> {
829    type Target = A;
830
831    fn deref(&self) -> &A {
832        &self.app
833    }
834}
835
836/// Mutable access only before the application is started, mirroring
837/// [`CuAppLifecycle`].
838#[cfg(feature = "std")]
839impl<S, L, A> core::ops::DerefMut for CuSimAppLifecycle<S, L, A, Initialized> {
840    fn deref_mut(&mut self) -> &mut A {
841        &mut self.app
842    }
843}
844
845/// Legacy escape hatch mirroring the [`CuApplication`] impl on
846/// [`CuAppLifecycle`]: pre-typestate simulation code compiles unchanged
847/// against what `build()` returns and only picks up deprecation warnings.
848#[cfg(feature = "std")]
849#[allow(deprecated)]
850impl<S, L, A> CuSimApplication<S, L> for CuSimAppLifecycle<S, L, A, Initialized>
851where
852    S: SectionStorage,
853    L: UnifiedLogWrite<S> + 'static,
854    A: CuSimApplication<S, L>,
855{
856    type Step<'z> = <A as CuSimApplication<S, L>>::Step<'z>;
857
858    fn get_original_config() -> String {
859        A::get_original_config()
860    }
861
862    fn mission_id() -> Option<&'static str> {
863        A::mission_id()
864    }
865
866    fn start_all_tasks(
867        &mut self,
868        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
869    ) -> CuResult<()> {
870        self.app.start_all_tasks(sim_callback)
871    }
872
873    fn run_one_iteration(
874        &mut self,
875        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
876    ) -> CuResult<()> {
877        self.app.run_one_iteration(sim_callback)
878    }
879
880    fn run(
881        &mut self,
882        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
883    ) -> CuResult<()> {
884        self.app.run(sim_callback)
885    }
886
887    fn stop_all_tasks(
888        &mut self,
889        sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
890    ) -> CuResult<()> {
891        self.app.stop_all_tasks(sim_callback)
892    }
893
894    fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
895        self.app.restore_keyframe(freezer)
896    }
897}
898
899#[cfg(all(test, feature = "std"))]
900mod lifecycle_tests {
901    use super::*;
902
903    #[derive(Default)]
904    struct MockApp {
905        started: u32,
906        stopped: u32,
907        iterations: u32,
908        runs: u32,
909        fail_start: bool,
910        fail_stop: bool,
911    }
912
913    #[allow(deprecated)] // mocks implement the deprecated raw trait methods
914    impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for MockApp {
915        fn get_original_config() -> String {
916            String::new()
917        }
918
919        fn start_all_tasks(&mut self) -> CuResult<()> {
920            if self.fail_start {
921                return Err("mock start failure".into());
922            }
923            self.started += 1;
924            Ok(())
925        }
926
927        fn run_one_iteration(&mut self) -> CuResult<()> {
928            self.iterations += 1;
929            Ok(())
930        }
931
932        fn run(&mut self) -> CuResult<()> {
933            if self.fail_start {
934                return Err("mock run failure".into());
935            }
936            self.runs += 1;
937            Ok(())
938        }
939
940        fn stop_all_tasks(&mut self) -> CuResult<()> {
941            if self.fail_stop {
942                return Err("mock stop failure".into());
943            }
944            self.stopped += 1;
945            Ok(())
946        }
947
948        fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
949            Ok(())
950        }
951    }
952
953    type Lifecycle = CuStdAppLifecycle<MockApp>;
954
955    #[test]
956    fn full_cycle_with_restart() {
957        let app = Lifecycle::new(MockApp::default());
958        let mut running = app.start().unwrap();
959        running.run_one_iteration().unwrap();
960        running.run_one_iteration().unwrap();
961        let stopped = running.stop().unwrap();
962
963        // Restarting a stopped application is legal (mission chaining).
964        let running = stopped.start().unwrap();
965        let stopped = running.stop().unwrap();
966
967        let mock = stopped.into_inner();
968        assert_eq!(mock.started, 2);
969        assert_eq!(mock.iterations, 2);
970        assert_eq!(mock.stopped, 2);
971    }
972
973    #[test]
974    fn run_transitions_to_stopped_and_can_run_again() {
975        let app = Lifecycle::new(MockApp::default());
976        let stopped = app.run_until_shutdown().unwrap();
977        let stopped = stopped.run_until_shutdown().unwrap();
978        assert_eq!(stopped.inner().runs, 2);
979    }
980
981    #[test]
982    fn failed_start_hands_back_a_faulted_app_for_cleanup() {
983        let app = Lifecycle::new(MockApp {
984            fail_start: true,
985            ..Default::default()
986        });
987        let err = app.start().unwrap_err();
988        assert!(err.error.to_string().contains("mock start failure"));
989
990        // The only legal transition from Faulted is the cleanup.
991        let stopped = err.app.stop().unwrap();
992        let mock = stopped.into_inner();
993        assert_eq!(mock.started, 0);
994        assert_eq!(mock.stopped, 1);
995    }
996
997    #[test]
998    fn failed_stop_hands_back_a_faulted_app() {
999        let app = Lifecycle::new(MockApp {
1000            fail_stop: true,
1001            ..Default::default()
1002        });
1003        let running = app.start().unwrap();
1004        let err = running.stop().unwrap_err();
1005        assert!(err.error.to_string().contains("mock stop failure"));
1006    }
1007
1008    #[test]
1009    fn lifecycle_error_propagates_with_question_mark() {
1010        fn drive() -> CuResult<()> {
1011            let app = Lifecycle::new(MockApp {
1012                fail_start: true,
1013                ..Default::default()
1014            });
1015            let _running = app.start()?;
1016            Ok(())
1017        }
1018        let error = drive().unwrap_err();
1019        assert!(error.to_string().contains("mock start failure"));
1020    }
1021
1022    #[derive(Default)]
1023    struct MockSimApp {
1024        started: u32,
1025        stopped: u32,
1026        iterations: u32,
1027        fail_start: bool,
1028    }
1029
1030    struct MockStep;
1031
1032    #[allow(deprecated)] // mocks implement the deprecated raw trait methods
1033    impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for MockSimApp {
1034        type Step<'z> = MockStep;
1035
1036        fn get_original_config() -> String {
1037            String::new()
1038        }
1039
1040        fn start_all_tasks(
1041            &mut self,
1042            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1043        ) -> CuResult<()> {
1044            if self.fail_start {
1045                return Err("mock sim start failure".into());
1046            }
1047            let _ = sim_callback(MockStep);
1048            self.started += 1;
1049            Ok(())
1050        }
1051
1052        fn run_one_iteration(
1053            &mut self,
1054            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1055        ) -> CuResult<()> {
1056            let _ = sim_callback(MockStep);
1057            self.iterations += 1;
1058            Ok(())
1059        }
1060
1061        fn run(
1062            &mut self,
1063            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1064        ) -> CuResult<()> {
1065            let _ = sim_callback(MockStep);
1066            Ok(())
1067        }
1068
1069        fn stop_all_tasks(
1070            &mut self,
1071            sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1072        ) -> CuResult<()> {
1073            let _ = sim_callback(MockStep);
1074            self.stopped += 1;
1075            Ok(())
1076        }
1077
1078        fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
1079            Ok(())
1080        }
1081    }
1082
1083    type SimLifecycle = CuStdSimAppLifecycle<MockSimApp>;
1084
1085    #[test]
1086    fn sim_full_cycle_threads_the_callback() {
1087        let mut callback_calls = 0u32;
1088        let mut cb = |_step: MockStep| -> SimOverride {
1089            callback_calls += 1;
1090            SimOverride::ExecuteByRuntime
1091        };
1092
1093        let app = SimLifecycle::new(MockSimApp::default());
1094        let mut running = app.start(&mut cb).unwrap();
1095        running.run_one_iteration(&mut cb).unwrap();
1096        let stopped = running.stop(&mut cb).unwrap();
1097
1098        let mock = stopped.into_inner();
1099        assert_eq!(mock.started, 1);
1100        assert_eq!(mock.iterations, 1);
1101        assert_eq!(mock.stopped, 1);
1102        assert_eq!(callback_calls, 3);
1103    }
1104
1105    #[test]
1106    fn sim_failed_start_hands_back_a_faulted_app_for_cleanup() {
1107        let mut cb = |_step: MockStep| -> SimOverride { SimOverride::ExecuteByRuntime };
1108        let app = SimLifecycle::new(MockSimApp {
1109            fail_start: true,
1110            ..Default::default()
1111        });
1112        let err = app.start(&mut cb).unwrap_err();
1113        assert!(err.error.to_string().contains("mock sim start failure"));
1114        let stopped = err.app.stop(&mut cb).unwrap();
1115        assert_eq!(stopped.inner().stopped, 1);
1116    }
1117}