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