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