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#[non_exhaustive]
21#[derive(Error, Debug)]
22pub enum ScheduleBuildError {
23 #[error("Failed to topologically sort the hierarchy of system sets: {0}")]
25 HierarchySort(DiGraphToposortError<NodeId>),
26 #[error("Failed to topologically sort the dependency graph: {0}")]
28 DependencySort(DiGraphToposortError<NodeId>),
29 #[error("Failed to topologically sort the flattened dependency graph: {0}")]
31 FlatDependencySort(DiGraphToposortError<SystemKey>),
32 #[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 #[error("`{:?}` and `{:?}` have a `before`-`after` relationship (which may be transitive) but share systems.", .0.0, .0.1)]
37 SetsHaveOrderButIntersect(#[from] DagOverlappingGroupError<SystemSetKey>),
38 #[error(transparent)]
40 SystemTypeSetAmbiguity(#[from] SystemTypeSetAmbiguityError),
41 #[error("Tried to run a schedule before all of its systems have been initialized.")]
43 Uninitialized,
44 #[error(transparent)]
46 Elevated(#[from] ScheduleBuildWarning),
47}
48
49#[non_exhaustive]
51#[derive(Error, Debug)]
52pub enum ScheduleBuildWarning {
53 #[error("The hierarchy of system sets contains redundant edges: {0:?}")]
63 HierarchyRedundancy(#[from] DagRedundancyError<NodeId>),
64 #[error(transparent)]
74 Ambiguity(#[from] AmbiguousSystemConflictsWarning),
75}
76
77impl ScheduleBuildError {
78 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 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 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#[derive(Error, Debug)]
284pub enum ScheduleError {
285 #[error("Operation cannot be completed because the schedule has changed and `Schedule::initialize` needs to be called")]
287 Uninitialized,
288 #[error("Set not found")]
290 SetNotFound,
291 #[error("Schedule not found.")]
293 ScheduleNotFound,
294 #[error("{0}")]
296 ScheduleBuildError(ScheduleBuildError),
297}
298
299impl From<ScheduleBuildError> for ScheduleError {
300 fn from(value: ScheduleBuildError) -> Self {
301 Self::ScheduleBuildError(value)
302 }
303}