|
| 1 | +use alloc::{format, string::String, vec::Vec}; |
| 2 | +use core::fmt::Write as _; |
| 3 | + |
| 4 | +use thiserror::Error; |
| 5 | + |
| 6 | +use 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)] |
| 15 | +pub 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)] |
| 48 | +pub 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 | + |
| 73 | +impl 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 | + |
| 248 | +impl 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 | +} |
0 commit comments