Skip to main content

moirai/schedule/
error.rs

1//! Schedule build-time and runtime control errors (`no_std` with optional `std` display).
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use crate::operation::StageOperation;
7use crate::schedule::system::FlushMode;
8use crate::world::WorldError;
9
10/// Schedule construction failure before first execution.
11#[non_exhaustive]
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum BuildError {
14    /// World still has unflushed deferred commands.
15    PendingCommands,
16    /// World run guard is active; build requires an idle world.
17    WorldRunning,
18    /// Change-tick exhaustion poisoned the world.
19    WorldMutationPoisoned,
20    /// Attached execution lease does not match this schedule.
21    LeaseMismatch,
22    /// Another compiled schedule already holds a live world lease.
23    LiveLeaseAlreadyAttached,
24    /// Referenced stage label was never registered.
25    UnknownStage { label: String },
26    /// Ordering edge names a system that was never added.
27    UnknownSystem { label: String },
28    /// System or ordering edge names an unregistered set.
29    UnknownSystemSet { label: String },
30    /// Same system-set label registered twice.
31    DuplicateSystemSet { label: String },
32    /// Two systems share the same label.
33    DuplicateSystemLabel { label: String },
34    /// Build-time initializer for a system returned an error.
35    SystemInitialization { system: String, detail: String },
36    /// Ordering edge would cross Update/Render operations.
37    CrossOperationEdge { from: String, to: String },
38    /// System ordering edge spans different stages.
39    CrossStageSystemEdge { from: String, to: String },
40    /// System declared a resource lock for a type not present in the world.
41    MissingRequiredResource { name: String },
42    /// System event role references an unregistered event or lifecycle channel.
43    UnregisteredEventRole { system: String, event: String },
44    /// Frame-retained event operation does not match the system's stage operation.
45    EventOperationMismatch {
46        system: String,
47        event: String,
48        event_operation: StageOperation,
49        system_operation: StageOperation,
50    },
51    /// Internal frame event consumer has no emitting producer.
52    MissingEventProducer { system: String, event: String },
53    /// Producer is not ordered before the consumer for the same frame event.
54    UnreachableEventProducer {
55        producer: String,
56        consumer: String,
57        event: String,
58    },
59    /// System depends on itself in the ordering graph.
60    SelfEdge { label: String },
61    /// Stage-local ordering graph contains a cycle.
62    Cycle { path: Vec<String> },
63    /// FixedUpdate systems exist but no fixed timestep was configured.
64    FixedUpdateWithoutConfig,
65    /// Fixed timestep configured without a FixedUpdate stage.
66    FixedConfigWithoutFixedUpdate,
67    /// Re-adding a stage label with a different operation.
68    StageOperationMismatch { label: String },
69    /// Flush mode is invalid for the stage's operation.
70    InvalidStageFlushMode { label: String, mode: FlushMode },
71    /// Flush mode is invalid for the system's stage operation.
72    InvalidSystemFlushMode { label: String, mode: FlushMode },
73    /// Underlying world construction or attachment failed.
74    WorldBuild(WorldError),
75}
76
77impl From<WorldError> for BuildError {
78    fn from(value: WorldError) -> Self {
79        Self::WorldBuild(value)
80    }
81}
82
83/// Runtime schedule control failure.
84#[non_exhaustive]
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum ScheduleError {
87    /// Handle was issued by a different schedule instance.
88    OwnerMismatch,
89    /// Handle index or generation no longer matches the compiled schedule.
90    StaleHandle,
91    /// Update plan lists the same stage more than once.
92    DuplicateStageInPlan,
93    /// Update plan includes a Render-only stage.
94    NonUpdateStageInPlan,
95    /// Startup is implicit on first update and cannot be planned explicitly.
96    StartupStageInPlan,
97    /// No compiled system matches the label.
98    SystemNotFound { label: String },
99}
100
101#[cfg(feature = "std")]
102impl core::fmt::Display for BuildError {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        match self {
105            Self::PendingCommands => f.write_str("world has pending commands"),
106            Self::WorldRunning => f.write_str("world is running"),
107            Self::WorldMutationPoisoned => f.write_str("world mutation is poisoned"),
108            Self::LeaseMismatch => f.write_str("world and schedule execution lease mismatch"),
109            Self::LiveLeaseAlreadyAttached => {
110                f.write_str("world already has a live compiled schedule lease")
111            }
112            Self::UnknownStage { label } => write!(f, "unknown stage '{label}'"),
113            Self::UnknownSystem { label } => write!(f, "unknown system '{label}'"),
114            Self::UnknownSystemSet { label } => write!(f, "unknown system set '{label}'"),
115            Self::DuplicateSystemSet { label } => write!(f, "duplicate system set '{label}'"),
116            Self::DuplicateSystemLabel { label } => write!(f, "duplicate system label '{label}'"),
117            Self::SystemInitialization { system, detail } => {
118                write!(f, "system '{system}' initialization failed: {detail}")
119            }
120            Self::CrossStageSystemEdge { from, to } => {
121                write!(f, "cross-stage system edge: {from} -> {to}")
122            }
123            Self::MissingRequiredResource { name } => {
124                write!(f, "missing required resource '{name}'")
125            }
126            Self::UnregisteredEventRole { system, event } => {
127                write!(f, "system '{system}' declares unregistered event role '{event}'")
128            }
129            Self::EventOperationMismatch {
130                system,
131                event,
132                event_operation,
133                system_operation,
134            } => write!(
135                f,
136                "system '{system}' runs in {system_operation:?} but event '{event}' belongs to {event_operation:?}"
137            ),
138            Self::MissingEventProducer { system, event } => {
139                write!(f, "event consumer '{system}' has no producer for '{event}'")
140            }
141            Self::UnreachableEventProducer {
142                producer,
143                consumer,
144                event,
145            } => write!(
146                f,
147                "event producer '{producer}' is not ordered before consumer '{consumer}' for '{event}'"
148            ),
149            Self::CrossOperationEdge { from, to } => {
150                write!(f, "ordering edge crosses operations: {from} -> {to}")
151            }
152            Self::SelfEdge { label } => write!(f, "system cannot depend on itself: {label}"),
153            Self::Cycle { path } => write!(f, "schedule cycle: {}", path.join(" -> ")),
154            Self::FixedUpdateWithoutConfig => {
155                f.write_str("FixedUpdate systems require fixed configuration")
156            }
157            Self::FixedConfigWithoutFixedUpdate => {
158                f.write_str("fixed configuration requires a FixedUpdate stage")
159            }
160            Self::StageOperationMismatch { label } => {
161                write!(f, "stage operation mismatch for '{label}'")
162            }
163            Self::InvalidStageFlushMode { label, mode } => {
164                write!(f, "invalid {mode:?} flush mode for stage '{label}'")
165            }
166            Self::InvalidSystemFlushMode { label, mode } => {
167                write!(f, "invalid {mode:?} flush mode for system '{label}'")
168            }
169            Self::WorldBuild(error) => write!(f, "world build failed: {error}"),
170        }
171    }
172}
173
174#[cfg(feature = "std")]
175impl core::fmt::Display for ScheduleError {
176    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
177        match self {
178            Self::OwnerMismatch => f.write_str("schedule handle belongs to a different schedule"),
179            Self::StaleHandle => f.write_str("stale schedule handle"),
180            Self::DuplicateStageInPlan => f.write_str("update plan contains a duplicate stage"),
181            Self::NonUpdateStageInPlan => f.write_str("update plan contains a non-update stage"),
182            Self::StartupStageInPlan => f.write_str("update plan cannot select Startup"),
183            Self::SystemNotFound { label } => write!(f, "system not found: {label}"),
184        }
185    }
186}
187
188#[cfg(feature = "std")]
189impl std::error::Error for BuildError {}
190
191#[cfg(feature = "std")]
192impl std::error::Error for ScheduleError {}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::world::WorldError;
198    #[cfg(feature = "std")]
199    use alloc::string::ToString;
200
201    #[test]
202    fn world_error_converts_into_build_error() {
203        let error: BuildError = WorldError::NestedRun.into();
204        assert!(matches!(
205            error,
206            BuildError::WorldBuild(WorldError::NestedRun)
207        ));
208    }
209
210    #[cfg(feature = "std")]
211    #[test]
212    fn build_error_display_covers_detailed_variants() {
213        let errors = [
214            BuildError::SystemInitialization {
215                system: "init".into(),
216                detail: "failed".into(),
217            },
218            BuildError::UnregisteredEventRole {
219                system: "reader".into(),
220                event: "event".into(),
221            },
222            BuildError::EventOperationMismatch {
223                system: "reader".into(),
224                event: "event".into(),
225                event_operation: crate::schedule::StageOperation::Render,
226                system_operation: crate::schedule::StageOperation::Update,
227            },
228            BuildError::MissingEventProducer {
229                system: "reader".into(),
230                event: "event".into(),
231            },
232            BuildError::UnreachableEventProducer {
233                producer: "writer".into(),
234                consumer: "reader".into(),
235                event: "event".into(),
236            },
237            BuildError::InvalidStageFlushMode {
238                label: "Render".into(),
239                mode: crate::schedule::FlushMode::Stage,
240            },
241            BuildError::InvalidSystemFlushMode {
242                label: "render".into(),
243                mode: crate::schedule::FlushMode::AfterSystem,
244            },
245        ];
246
247        for error in errors {
248            assert!(!error.to_string().is_empty());
249        }
250    }
251}