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#[non_exhaustive]
14#[derive(Error, Debug)]
15pub enum ScheduleBuildError {
16 #[error("System set `{0:?}` contains itself.")]
18 HierarchyLoop(NodeId),
19 #[error("The hierarchy of system sets contains a cycle: {0:?}")]
21 HierarchyCycle(Vec<Vec<NodeId>>),
22 #[error("`{0:?}` has been told to run before itself.")]
24 DependencyLoop(NodeId),
25 #[error("The dependency graph contains a cycle: {0:?}")]
27 DependencyCycle(Vec<Vec<NodeId>>),
28 #[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 #[error("`{0:?}` and `{1:?}` have a `before`-`after` relationship (which may be transitive) but share systems.")]
33 SetsHaveOrderButIntersect(SystemSetKey, SystemSetKey),
34 #[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 #[error("Tried to run a schedule before all of its systems have been initialized.")]
39 Uninitialized,
40 #[error(transparent)]
42 Elevated(#[from] ScheduleBuildWarning),
43}
44
45#[non_exhaustive]
47#[derive(Error, Debug)]
48pub enum ScheduleBuildWarning {
49 #[error("The hierarchy of system sets contains redundant edges: {0:?}")]
59 HierarchyRedundancy(Vec<(NodeId, NodeId)>),
60 #[error("Systems with conflicting access have indeterminate run order: {0:?}")]
70 Ambiguity(Vec<(SystemKey, SystemKey, Vec<ComponentId>)>),
71}
72
73impl ScheduleBuildError {
74 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 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 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}