bevy_ecs/schedule/
error.rs

1use alloc::{format, string::String, vec::Vec};
2use core::fmt::Write as _;
3
4use thiserror::Error;
5
6use crate::{
7    component::Components,
8    schedule::{
9        graph::{
10            DagCrossDependencyError, DagOverlappingGroupError, DagRedundancyError,
11            DiGraphToposortError, GraphNodeId,
12        },
13        AmbiguousSystemConflictsWarning, ConflictingSystems, NodeId, ScheduleGraph, SystemKey,
14        SystemSetKey, SystemTypeSetAmbiguityError,
15    },
16    world::World,
17};
18
19/// Category of errors encountered during [`Schedule::initialize`](crate::schedule::Schedule::initialize).
20#[non_exhaustive]
21#[derive(Error, Debug)]
22pub enum ScheduleBuildError {
23    /// Tried to topologically sort the hierarchy of system sets.
24    #[error("Failed to topologically sort the hierarchy of system sets: {0}")]
25    HierarchySort(DiGraphToposortError<NodeId>),
26    /// Tried to topologically sort the dependency graph.
27    #[error("Failed to topologically sort the dependency graph: {0}")]
28    DependencySort(DiGraphToposortError<NodeId>),
29    /// Tried to topologically sort the flattened dependency graph.
30    #[error("Failed to topologically sort the flattened dependency graph: {0}")]
31    FlatDependencySort(DiGraphToposortError<SystemKey>),
32    /// Tried to order a system (set) relative to a system set it belongs to.
33    #[error("`{:?}` and `{:?}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.", .0.0, .0.1)]
34    CrossDependency(#[from] DagCrossDependencyError<NodeId>),
35    /// Tried to order system sets that share systems.
36    #[error("`{:?}` and `{:?}` have a `before`-`after` relationship (which may be transitive) but share systems.", .0.0, .0.1)]
37    SetsHaveOrderButIntersect(#[from] DagOverlappingGroupError<SystemSetKey>),
38    /// Tried to order a system (set) relative to all instances of some system function.
39    #[error(transparent)]
40    SystemTypeSetAmbiguity(#[from] SystemTypeSetAmbiguityError),
41    /// Tried to run a schedule before all of its systems have been initialized.
42    #[error("Tried to run a schedule before all of its systems have been initialized.")]
43    Uninitialized,
44    /// A warning that was elevated to an error.
45    #[error(transparent)]
46    Elevated(#[from] ScheduleBuildWarning),
47}
48
49/// Category of warnings encountered during [`Schedule::initialize`](crate::schedule::Schedule::initialize).
50#[non_exhaustive]
51#[derive(Error, Debug)]
52pub enum ScheduleBuildWarning {
53    /// The hierarchy of system sets contains redundant edges.
54    ///
55    /// This warning is **enabled** by default, but can be disabled by setting
56    /// [`ScheduleBuildSettings::hierarchy_detection`] to [`LogLevel::Ignore`]
57    /// or upgraded to a [`ScheduleBuildError`] by setting it to [`LogLevel::Error`].
58    ///
59    /// [`ScheduleBuildSettings::hierarchy_detection`]: crate::schedule::ScheduleBuildSettings::hierarchy_detection
60    /// [`LogLevel::Ignore`]: crate::schedule::LogLevel::Ignore
61    /// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
62    #[error("The hierarchy of system sets contains redundant edges: {0:?}")]
63    HierarchyRedundancy(#[from] DagRedundancyError<NodeId>),
64    /// Systems with conflicting access have indeterminate run order.
65    ///
66    /// This warning is **disabled** by default, but can be enabled by setting
67    /// [`ScheduleBuildSettings::ambiguity_detection`] to [`LogLevel::Warn`]
68    /// or upgraded to a [`ScheduleBuildError`] by setting it to [`LogLevel::Error`].
69    ///
70    /// [`ScheduleBuildSettings::ambiguity_detection`]: crate::schedule::ScheduleBuildSettings::ambiguity_detection
71    /// [`LogLevel::Warn`]: crate::schedule::LogLevel::Warn
72    /// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
73    #[error(transparent)]
74    Ambiguity(#[from] AmbiguousSystemConflictsWarning),
75}
76
77impl ScheduleBuildError {
78    /// Renders the error as a human-readable string with node identifiers
79    /// replaced with their names.
80    ///
81    /// The given `graph` and `world` are used to resolve the names of the nodes
82    /// and components involved in the error. The same `graph` and `world`
83    /// should be used as those used to [`initialize`] the [`Schedule`]. Failure
84    /// to do so will result in incorrect or incomplete error messages.
85    ///
86    /// [`initialize`]: crate::schedule::Schedule::initialize
87    /// [`Schedule`]: crate::schedule::Schedule
88    pub fn to_string(&self, graph: &ScheduleGraph, world: &World) -> String {
89        match self {
90            ScheduleBuildError::HierarchySort(DiGraphToposortError::Loop(node_id)) => {
91                Self::hierarchy_loop_to_string(node_id, graph)
92            }
93            ScheduleBuildError::HierarchySort(DiGraphToposortError::Cycle(cycles)) => {
94                Self::hierarchy_cycle_to_string(cycles, graph)
95            }
96            ScheduleBuildError::DependencySort(DiGraphToposortError::Loop(node_id)) => {
97                Self::dependency_loop_to_string(node_id, graph)
98            }
99            ScheduleBuildError::DependencySort(DiGraphToposortError::Cycle(cycles)) => {
100                Self::dependency_cycle_to_string(cycles, graph)
101            }
102            ScheduleBuildError::FlatDependencySort(DiGraphToposortError::Loop(node_id)) => {
103                Self::dependency_loop_to_string(&NodeId::System(*node_id), graph)
104            }
105            ScheduleBuildError::FlatDependencySort(DiGraphToposortError::Cycle(cycles)) => {
106                Self::dependency_cycle_to_string(cycles, graph)
107            }
108            ScheduleBuildError::CrossDependency(error) => {
109                Self::cross_dependency_to_string(error, graph)
110            }
111            ScheduleBuildError::SetsHaveOrderButIntersect(DagOverlappingGroupError(a, b)) => {
112                Self::sets_have_order_but_intersect_to_string(a, b, graph)
113            }
114            ScheduleBuildError::SystemTypeSetAmbiguity(SystemTypeSetAmbiguityError(set)) => {
115                Self::system_type_set_ambiguity_to_string(set, graph)
116            }
117            ScheduleBuildError::Uninitialized => Self::uninitialized_to_string(),
118            ScheduleBuildError::Elevated(e) => e.to_string(graph, world),
119        }
120    }
121
122    fn hierarchy_loop_to_string(node_id: &NodeId, graph: &ScheduleGraph) -> String {
123        format!(
124            "{} `{}` contains itself",
125            node_id.kind(),
126            graph.get_node_name(node_id)
127        )
128    }
129
130    fn hierarchy_cycle_to_string(cycles: &[Vec<NodeId>], graph: &ScheduleGraph) -> String {
131        let mut message = format!("schedule has {} in_set cycle(s):\n", cycles.len());
132        for (i, cycle) in cycles.iter().enumerate() {
133            let mut names = cycle.iter().map(|id| (id.kind(), graph.get_node_name(id)));
134            let (first_kind, first_name) = names.next().unwrap();
135            writeln!(
136                message,
137                "cycle {}: {first_kind} `{first_name}` contains itself",
138                i + 1,
139            )
140            .unwrap();
141            writeln!(message, "{first_kind} `{first_name}`").unwrap();
142            for (kind, name) in names.chain(core::iter::once((first_kind, first_name))) {
143                writeln!(message, " ... which contains {kind} `{name}`").unwrap();
144            }
145            writeln!(message).unwrap();
146        }
147        message
148    }
149
150    fn hierarchy_redundancy_to_string(
151        transitive_edges: &[(NodeId, NodeId)],
152        graph: &ScheduleGraph,
153    ) -> String {
154        let mut message = String::from("hierarchy contains redundant edge(s)");
155        for (parent, child) in transitive_edges {
156            writeln!(
157                message,
158                " -- {} `{}` cannot be child of {} `{}`, longer path exists",
159                child.kind(),
160                graph.get_node_name(child),
161                parent.kind(),
162                graph.get_node_name(parent),
163            )
164            .unwrap();
165        }
166        message
167    }
168
169    fn dependency_loop_to_string(node_id: &NodeId, graph: &ScheduleGraph) -> String {
170        format!(
171            "{} `{}` has been told to run before itself",
172            node_id.kind(),
173            graph.get_node_name(node_id)
174        )
175    }
176
177    fn dependency_cycle_to_string<N: GraphNodeId + Into<NodeId>>(
178        cycles: &[Vec<N>],
179        graph: &ScheduleGraph,
180    ) -> String {
181        let mut message = format!("schedule has {} before/after cycle(s):\n", cycles.len());
182        for (i, cycle) in cycles.iter().enumerate() {
183            let mut names = cycle
184                .iter()
185                .map(|&id| (id.kind(), graph.get_node_name(&id.into())));
186            let (first_kind, first_name) = names.next().unwrap();
187            writeln!(
188                message,
189                "cycle {}: {first_kind} `{first_name}` must run before itself",
190                i + 1,
191            )
192            .unwrap();
193            writeln!(message, "{first_kind} `{first_name}`").unwrap();
194            for (kind, name) in names.chain(core::iter::once((first_kind, first_name))) {
195                writeln!(message, " ... which must run before {kind} `{name}`").unwrap();
196            }
197            writeln!(message).unwrap();
198        }
199        message
200    }
201
202    fn cross_dependency_to_string(
203        error: &DagCrossDependencyError<NodeId>,
204        graph: &ScheduleGraph,
205    ) -> String {
206        let DagCrossDependencyError(a, b) = error;
207        format!(
208            "{} `{}` and {} `{}` have both `in_set` and `before`-`after` relationships (these might be transitive). \
209            This combination is unsolvable as a system cannot run before or after a set it belongs to.",
210            a.kind(),
211            graph.get_node_name(a),
212            b.kind(),
213            graph.get_node_name(b)
214        )
215    }
216
217    fn sets_have_order_but_intersect_to_string(
218        a: &SystemSetKey,
219        b: &SystemSetKey,
220        graph: &ScheduleGraph,
221    ) -> String {
222        format!(
223            "`{}` and `{}` have a `before`-`after` relationship (which may be transitive) but share systems.",
224            graph.get_node_name(&NodeId::Set(*a)),
225            graph.get_node_name(&NodeId::Set(*b)),
226        )
227    }
228
229    fn system_type_set_ambiguity_to_string(set: &SystemSetKey, graph: &ScheduleGraph) -> String {
230        let name = graph.get_node_name(&NodeId::Set(*set));
231        format!(
232            "Tried to order against `{name}` in a schedule that has more than one `{name}` instance. `{name}` is a \
233            `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction."
234        )
235    }
236
237    pub(crate) fn ambiguity_to_string(
238        ambiguities: &ConflictingSystems,
239        graph: &ScheduleGraph,
240        components: &Components,
241    ) -> String {
242        let n_ambiguities = ambiguities.len();
243        let mut message = format!(
244            "{n_ambiguities} pairs of systems with conflicting data access have indeterminate execution order. \
245            Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n",
246        );
247        let ambiguities = ambiguities.to_string(graph, components);
248        for (name_a, name_b, conflicts) in ambiguities {
249            writeln!(message, " -- {name_a} and {name_b}").unwrap();
250
251            if !conflicts.is_empty() {
252                writeln!(message, "    conflict on: {conflicts:?}").unwrap();
253            } else {
254                // one or both systems must be exclusive
255                let world = core::any::type_name::<World>();
256                writeln!(message, "    conflict on: {world}").unwrap();
257            }
258        }
259        message
260    }
261
262    fn uninitialized_to_string() -> String {
263        String::from("tried to run a schedule before all of its systems have been initialized")
264    }
265}
266
267impl ScheduleBuildWarning {
268    /// Renders the warning as a human-readable string with node identifiers
269    /// replaced with their names.
270    pub fn to_string(&self, graph: &ScheduleGraph, world: &World) -> String {
271        match self {
272            ScheduleBuildWarning::HierarchyRedundancy(DagRedundancyError(transitive_edges)) => {
273                ScheduleBuildError::hierarchy_redundancy_to_string(transitive_edges, graph)
274            }
275            ScheduleBuildWarning::Ambiguity(AmbiguousSystemConflictsWarning(ambiguities)) => {
276                ScheduleBuildError::ambiguity_to_string(ambiguities, graph, world.components())
277            }
278        }
279    }
280}
281
282/// Error returned from some `Schedule` methods
283#[derive(Error, Debug)]
284pub enum ScheduleError {
285    /// Operation cannot be completed because the schedule has changed and `Schedule::initialize` needs to be called
286    #[error("Operation cannot be completed because the schedule has changed and `Schedule::initialize` needs to be called")]
287    Uninitialized,
288    /// Method could not find set
289    #[error("Set not found")]
290    SetNotFound,
291    /// Schedule not found
292    #[error("Schedule not found.")]
293    ScheduleNotFound,
294    /// Error initializing schedule
295    #[error("{0}")]
296    ScheduleBuildError(ScheduleBuildError),
297}
298
299impl From<ScheduleBuildError> for ScheduleError {
300    fn from(value: ScheduleBuildError) -> Self {
301        Self::ScheduleBuildError(value)
302    }
303}