1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! The crate's error type.
use pragma_core::graph::GraphError;
use pragma_core::jobs::JobError;
use crate::Entity;
/// An error of an ECS operation.
///
/// New variants are added as the API grows (`#[non_exhaustive]`), so match with a `_` arm.
///
/// ```
/// use pragma_ecs::{EcsError, World};
///
/// let mut world = World::new();
/// let entity = world.spawn()?;
/// world.despawn(entity)?;
///
/// match world.insert(entity, 1.0_f32) {
/// Err(EcsError::NoSuchEntity { entity: gone, .. }) => assert_eq!(gone, entity),
/// other => panic!("unexpected: {other:?}"),
/// }
/// # Ok::<(), EcsError>(())
/// ```
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EcsError {
/// An allocation whose size the input sets failed (`try_reserve`). The world is unchanged.
#[error("out of memory while growing storage to {entities} entities")]
#[non_exhaustive]
OutOfMemory {
/// The entity count (or table row count) the storage tried to grow to; the world's entity
/// count for a query's cache or a schedule's lending slots, 0 when no world is involved
/// (building a query's access, adding a system).
entities: usize,
},
/// The entity was despawned (its id is stale) or was never spawned by this world.
#[error("entity {entity} does not exist (despawned or never spawned)")]
#[non_exhaustive]
NoSuchEntity {
/// The id that was passed in.
entity: Entity,
},
/// The world has used up its entity index space (`u32::MAX` slots).
#[error("entity index space exhausted")]
CapacityExhausted,
/// A query writes a component type that it also reads or writes elsewhere, e.g.
/// `(&A, &mut A)`: the two references would alias (DECISIONS #14).
///
/// ```
/// use pragma_ecs::{EcsError, Query};
///
/// struct Health(u32);
///
/// let err = Query::<(&Health, &mut Health)>::new().err();
/// assert!(matches!(
/// err,
/// Some(EcsError::ConflictingAccess { component, .. }) if component.ends_with("Health")
/// ));
/// ```
#[error(
"conflicting access to component `{component}`: it is written and also accessed elsewhere"
)]
#[non_exhaustive]
ConflictingAccess {
/// The type name of the component (`std::any::type_name`).
component: &'static str,
},
/// A [`Bundle`](crate::Bundle) holds the same component type twice, e.g. `(A, A)`: an
/// entity has at most one component of each type (DECISIONS #35). Nothing was changed.
///
/// ```
/// use pragma_ecs::{EcsError, World};
///
/// struct Speed(f32);
///
/// let mut world = World::new();
/// let err = world.spawn_bundle((Speed(1.0), Speed(2.0))).err();
/// assert!(matches!(
/// err,
/// Some(EcsError::DuplicateComponent { component, .. }) if component.ends_with("Speed")
/// ));
/// assert!(world.is_empty());
/// ```
#[error("a bundle holds the component type `{component}` more than once")]
#[non_exhaustive]
DuplicateComponent {
/// The type name of the component (`std::any::type_name`).
component: &'static str,
},
/// A system uses a resource type twice and at least one use writes it, e.g.
/// `(Res<Score>, ResMut<Score>)`: the two borrows would alias (DECISIONS #17).
///
/// ```
/// use pragma_ecs::{EcsError, Res, ResMut, Schedule};
///
/// struct Score(u32);
///
/// fn cheat(_: Res<Score>, _: ResMut<Score>) {}
///
/// let err = Schedule::builder().add_system("cheat", cheat).err();
/// assert!(matches!(
/// err,
/// Some(EcsError::ConflictingResourceAccess { resource, .. }) if resource.ends_with("Score")
/// ));
/// ```
#[error(
"conflicting access to resource `{resource}`: it is written and also accessed elsewhere"
)]
#[non_exhaustive]
ConflictingResourceAccess {
/// The type name of the resource (`std::any::type_name`).
resource: &'static str,
},
/// A pool task failed: the closure of a parallel query panicked (the panic was caught) or
/// the pool cancelled the task. Rows of the other chunks were still processed.
#[error("parallel query iteration failed: {source}")]
#[non_exhaustive]
Job {
/// The error reported by the `pragma_core` thread pool.
#[from]
source: JobError,
},
/// Two systems use the same data, at least one of them writes it, and neither is ordered
/// before the other (DECISIONS #19). Returned by
/// [`ScheduleBuilder::build`](crate::ScheduleBuilder::build).
///
/// ```
/// use pragma_ecs::{EcsError, Schedule, View};
///
/// struct Position(f32);
///
/// fn movement(_: View<&mut Position>) {}
/// fn render(_: View<&Position>) {}
///
/// let mut builder = Schedule::builder();
/// builder.add_system("movement", movement)?.add_system("render", render)?;
/// match builder.build() {
/// Err(EcsError::AmbiguousOrder { first, second, data, .. }) => {
/// assert_eq!((first.as_str(), second.as_str()), ("movement", "render"));
/// assert!(data.ends_with("Position"));
/// }
/// other => panic!("unexpected {other:?}"),
/// }
/// # Ok::<(), EcsError>(())
/// ```
#[error(
"systems `{first}` and `{second}` conflict on `{data}` without an explicit order \
(add `after`/`before`, or build an `ordered()` schedule)"
)]
#[non_exhaustive]
AmbiguousOrder {
/// The system registered first.
first: String,
/// The system registered second.
second: String,
/// The type name of the component or resource both use.
data: String,
},
/// A system takes a resource the world does not have. Checked before any system of the
/// frame runs, so the frame did not start.
///
/// ```
/// use pragma_core::prelude::{PoolConfig, ThreadPool};
/// use pragma_ecs::{EcsError, Res, Schedule, World};
///
/// struct Gravity(f32);
///
/// fn fall(_: Res<Gravity>) {}
///
/// let mut builder = Schedule::builder();
/// builder.add_system("fall", fall)?;
/// let mut schedule = builder.build()?;
/// let pool = ThreadPool::new(PoolConfig::default().with_workers(1))?;
/// let err = schedule.run(&mut World::new(), &pool).err();
/// assert!(matches!(
/// err,
/// Some(EcsError::MissingResource { system, resource, .. })
/// if system == "fall" && resource.ends_with("Gravity")
/// ));
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[error("system `{system}` needs resource `{resource}`, which the world does not have")]
#[non_exhaustive]
MissingResource {
/// The first system that takes the resource.
system: String,
/// The type name of the resource.
resource: String,
},
/// A system returned an error (see [`SystemOutput`](crate::SystemOutput)). Independent
/// systems still ran; the systems ordered after it did not run in this frame.
#[error("system `{system}` failed: {message}")]
#[non_exhaustive]
SystemFailed {
/// The system's name.
system: String,
/// The message of the error the system returned (its `Display`).
message: String,
/// The error itself, if it was an `EcsError`.
source: Option<Box<EcsError>>,
},
/// A system panicked; the panic was caught. Independent systems still ran; the systems
/// ordered after it did not run in this frame.
#[error("system `{system}` panicked: {message}")]
#[non_exhaustive]
SystemPanicked {
/// The system's name.
system: String,
/// The panic message, if it was a string.
message: String,
},
/// Applying the commands or events a system deferred to the sync point failed, e.g. out of
/// memory while spawning (DECISIONS #25). The commands applied before stay applied; the
/// failing one and everything deferred after it, by this and later systems, were discarded.
/// A command on an entity that is gone is not an error: it does nothing.
///
/// ```
/// use pragma_core::prelude::ThreadPool;
/// use pragma_ecs::{EcsError, Schedule, World};
///
/// fn frame(schedule: &mut Schedule, world: &mut World, pool: &ThreadPool) -> Result<(), EcsError> {
/// match schedule.run(world, pool) {
/// Err(EcsError::CommandFailed { system, source, .. }) => {
/// // The world is consistent: report, free memory, run the next frame.
/// eprintln!("commands of `{system}` were not applied: {source}");
/// Ok(())
/// }
/// other => other,
/// }
/// }
/// # let _ = frame;
/// ```
#[error("applying the commands of system `{system}` failed: {source}")]
#[non_exhaustive]
CommandFailed {
/// The system whose commands or events could not be applied.
system: String,
/// Why: [`EcsError::OutOfMemory`] or [`EcsError::CapacityExhausted`].
source: Box<EcsError>,
},
/// Any other error of the `pragma_core` frame graph a schedule is built on: a dependency
/// cycle, an `after`/`before` naming an unknown system, a name used twice, a schedule too
/// large (DECISIONS #21).
///
/// ```
/// use pragma_ecs::{EcsError, Schedule};
///
/// fn tick() {}
///
/// let mut builder = Schedule::builder();
/// builder.add_system_with("tick", tick, |c| c.after("input"))?;
/// assert!(matches!(builder.build(), Err(EcsError::Schedule { .. })));
/// # Ok::<(), EcsError>(())
/// ```
#[error("schedule error: {source}")]
#[non_exhaustive]
Schedule {
/// The error reported by the frame graph.
source: GraphError,
},
}
impl EcsError {
/// Wraps a frame-graph error that has no variant of its own.
pub(crate) const fn graph(source: GraphError) -> Self {
Self::Schedule { source }
}
}