1use std::fmt;
7use std::iter::FusedIterator;
8
9use henad_core::provenance::{BuildInfo, ModelSource};
10
11use super::ModelEntry;
12use crate::gpu::{GpuContext, GpuNeeds};
13
14#[derive(Debug, Clone)]
16pub struct ModelSet {
17 build: BuildInfo,
19 entries: Vec<ModelEntry>,
20}
21
22impl ModelSet {
23 pub fn new(build: BuildInfo) -> Self {
25 Self {
26 build,
27 entries: Vec::new(),
28 }
29 }
30
31 pub fn insert(&mut self, entry: ModelEntry) -> Result<&mut Self, ModelSetError> {
43 check_id(entry.id())?;
44 self.check_free(&entry)?;
45 let entry = if entry.source().build().is_some() {
46 entry
47 } else {
48 let source = entry.source().clone().__with_build(self.build);
49 entry.with_source(source)
50 };
51 self.entries.push(entry);
52 Ok(self)
53 }
54
55 pub fn extend(&mut self, other: Self) -> Result<&mut Self, ModelSetError> {
62 for entry in &other.entries {
63 self.check_free(entry)?;
64 }
65 self.entries.extend(other.entries);
66 Ok(self)
67 }
68
69 pub fn get(&self, id: &str) -> Option<&ModelEntry> {
71 self.entries.iter().find(|entry| entry.id() == id)
72 }
73
74 pub fn lookup(&self, id: &str, gpu: Option<&GpuContext>) -> Result<&ModelEntry, ModelLookupError> {
81 if let Some(entry) = self.runnable(gpu).find(|entry| entry.id() == id) {
82 return Ok(entry);
83 }
84 let id = id.to_owned();
85 Err(if self.get(&id).is_some() {
86 ModelLookupError::NeedsGpu { id }
87 } else {
88 ModelLookupError::NotInSet { id }
89 })
90 }
91
92 pub fn runnable<'a>(&'a self, gpu: Option<&GpuContext>) -> impl Iterator<Item = &'a ModelEntry> + use<'a> {
94 let device = gpu.is_some();
95 self.entries
96 .iter()
97 .filter(move |entry| device || entry.gpu_needs().is_none())
98 }
99
100 pub fn iter(&self) -> ModelSetIter<'_> {
102 ModelSetIter(self.entries.iter())
103 }
104
105 pub fn len(&self) -> usize {
107 self.entries.len()
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.entries.is_empty()
113 }
114
115 pub fn gpu_needs(&self) -> GpuNeeds {
117 self.entries
118 .iter()
119 .filter_map(ModelEntry::gpu_needs)
120 .fold(GpuNeeds::default(), GpuNeeds::merge)
121 }
122
123 fn check_free(&self, entry: &ModelEntry) -> Result<(), ModelSetError> {
125 match self.get(entry.id()) {
126 Some(existing) => Err(ModelSetError::DuplicateId {
127 id: entry.id().to_owned(),
128 existing: Box::new(existing.source().clone()),
129 added: Box::new(entry.source().clone()),
130 }),
131 None => Ok(()),
132 }
133 }
134}
135
136impl<'a> IntoIterator for &'a ModelSet {
137 type Item = &'a ModelEntry;
138 type IntoIter = ModelSetIter<'a>;
139
140 fn into_iter(self) -> Self::IntoIter {
141 self.iter()
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct ModelSetIter<'a>(std::slice::Iter<'a, ModelEntry>);
148
149impl<'a> Iterator for ModelSetIter<'a> {
150 type Item = &'a ModelEntry;
151
152 fn next(&mut self) -> Option<Self::Item> {
153 self.0.next()
154 }
155
156 fn size_hint(&self) -> (usize, Option<usize>) {
157 self.0.size_hint()
158 }
159}
160
161impl ExactSizeIterator for ModelSetIter<'_> {}
162
163impl FusedIterator for ModelSetIter<'_> {}
164
165fn check_id(id: &str) -> Result<(), ModelSetError> {
167 let invalid = |reason| {
168 Err(ModelSetError::InvalidId {
169 id: id.to_owned(),
170 reason,
171 })
172 };
173 let mut chars = id.chars();
174 match chars.next() {
175 None => invalid("is empty"),
176 Some(first) if !first.is_ascii_lowercase() => invalid("does not start with a lowercase ASCII letter"),
177 Some(_)
178 if !chars
179 .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_') =>
180 {
181 invalid("holds a character other than a lowercase ASCII letter, a digit or an underscore")
182 }
183 Some(_) => Ok(()),
184 }
185}
186
187#[derive(Debug)]
189#[non_exhaustive]
190pub enum ModelSetError {
191 DuplicateId {
193 id: String,
195 existing: Box<ModelSource>,
197 added: Box<ModelSource>,
199 },
200 InvalidId {
202 id: String,
204 reason: &'static str,
206 },
207}
208
209impl fmt::Display for ModelSetError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::DuplicateId { id, existing, added } => write!(
213 f,
214 "the set already holds model '{id}' ({}), and cannot add it again ({})",
215 existing.type_path(),
216 added.type_path()
217 ),
218 Self::InvalidId { id, reason } => write!(
219 f,
220 "model id '{id}' {reason} (an id is a lowercase ASCII letter, then lowercase letters, digits and underscores)"
221 ),
222 }
223 }
224}
225
226impl std::error::Error for ModelSetError {}
227
228#[derive(Debug)]
230#[non_exhaustive]
231pub enum ModelLookupError {
232 NotInSet {
234 id: String,
236 },
237 NeedsGpu {
239 id: String,
241 },
242}
243
244impl fmt::Display for ModelLookupError {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::NotInSet { id } => write!(f, "this build does not include model '{id}'"),
248 Self::NeedsGpu { id } => write!(
249 f,
250 "model '{id}' needs a GPU with compute support, and this device has none"
251 ),
252 }
253 }
254}
255
256impl std::error::Error for ModelLookupError {}
257
258#[cfg(test)]
259mod tests {
260 use henad_core::authoring::model::grid_model::GridModel;
261 use henad_core::grid::Grid2D;
262 use henad_core::params::{ParamDescriptor, ParamValue};
263 use henad_core::provenance::BuildInfo;
264 use henad_core::topology::NeighborhoodKind;
265 use henad_core::view::{StatDescriptor, StatValue};
266
267 use super::{ModelLookupError, ModelSet, ModelSetError};
268 use crate::entry::{ModelEntry, register_grid_model};
269
270 macro_rules! still_model {
272 ($ty:ident, $id:literal) => {
273 struct $ty;
274
275 impl GridModel for $ty {
276 const NAME: &'static str = $id;
277 const ID: &'static str = $id;
278 const DESCRIPTION: &'static str = $id;
279 const PALETTE: &'static [[u8; 4]] = &[[0, 0, 0, 255]];
280 const NEIGHBORHOOD: NeighborhoodKind = NeighborhoodKind::Moore;
281 const STATS: &'static [StatDescriptor] = &[];
282 type Params = ();
283
284 fn param_descriptors() -> Vec<ParamDescriptor> {
285 Vec::new()
286 }
287
288 fn from_params(_params: &[ParamValue]) -> Self::Params {}
289
290 fn init(_grid: &mut Grid2D<u8>, _params: &[ParamValue], _rng: &mut u64) {}
291
292 fn step_cell(cell: u8, _neighbors: &[u8], _params: &Self::Params, _rng: &mut u64) -> u8 {
293 cell
294 }
295
296 fn stats(_grid: &Grid2D<u8>) -> Vec<StatValue> {
297 Vec::new()
298 }
299 }
300 };
301 }
302
303 still_model!(Still, "still");
304 still_model!(StillAgain, "still");
305 still_model!(Calm, "calm_2");
306 still_model!(Capital, "Still");
307 still_model!(Hyphen, "still-life");
308 still_model!(Digit, "2still");
309 still_model!(Empty, "");
310
311 fn build(package: &'static str) -> BuildInfo {
312 BuildInfo::__from_env(package, "1.0.0", None, None, None, None, false)
313 }
314
315 fn ids(models: &ModelSet) -> Vec<&str> {
316 models.iter().map(ModelEntry::id).collect()
317 }
318
319 #[test]
320 fn a_model_set_refuses_a_duplicate_id() {
321 let mut models = ModelSet::new(build("host"));
322 models.insert(register_grid_model::<Still>()).expect("a fresh id");
323 let Err(error) = models.insert(register_grid_model::<StillAgain>()) else {
324 panic!("a second 'still' joined the set");
325 };
326 let ModelSetError::DuplicateId { id, existing, added } = &error else {
327 panic!("expected a duplicate id, got {error:?}");
328 };
329 assert_eq!(id, "still");
330 assert!(existing.type_path().ends_with("::Still"), "{error}");
331 assert!(added.type_path().ends_with("::StillAgain"), "{error}");
332 assert_eq!(ids(&models), ["still"]);
333
334 let mut other = ModelSet::new(build("library"));
335 other.insert(register_grid_model::<Calm>()).expect("a fresh id");
336 other.insert(register_grid_model::<StillAgain>()).expect("a fresh id");
337 assert!(matches!(models.extend(other), Err(ModelSetError::DuplicateId { .. })));
338 assert_eq!(ids(&models), ["still"], "a refused extend adds nothing");
339 }
340
341 #[test]
342 fn a_set_iterates_in_insertion_order_either_way() {
343 let mut models = ModelSet::new(build("host"));
344 models.insert(register_grid_model::<Still>()).expect("a fresh id");
345 models.insert(register_grid_model::<Calm>()).expect("a fresh id");
346 let mut through_iter = models.iter();
347 assert_eq!(through_iter.len(), 2);
348 assert_eq!(through_iter.next().map(ModelEntry::id), Some("still"));
349 assert_eq!(through_iter.len(), 1);
350 let through_loop: Vec<&str> = (&models).into_iter().map(ModelEntry::id).collect();
351 assert_eq!(through_loop, ["still", "calm_2"]);
352 }
353
354 #[test]
355 fn a_model_set_refuses_an_id_outside_the_grammar() {
356 let mut models = ModelSet::new(build("host"));
357 for entry in [
358 register_grid_model::<Capital>(),
359 register_grid_model::<Hyphen>(),
360 register_grid_model::<Digit>(),
361 register_grid_model::<Empty>(),
362 ] {
363 let id = entry.id().to_owned();
364 let Err(error) = models.insert(entry) else {
365 panic!("'{id}' joined the set");
366 };
367 assert!(matches!(error, ModelSetError::InvalidId { .. }), "{error:?}");
368 assert!(error.to_string().contains(&format!("'{id}'")), "{error}");
369 }
370 assert!(models.is_empty());
371 models
372 .insert(register_grid_model::<Calm>())
373 .expect("letters, digits and an underscore");
374 }
375
376 #[test]
377 fn a_set_keeps_the_source_of_an_entry_from_another_set() {
378 let mut library = ModelSet::new(build("library"));
379 library.insert(register_grid_model::<Still>()).expect("a fresh id");
380 let mut host = ModelSet::new(build("host"));
381 host.insert(register_grid_model::<Calm>()).expect("a fresh id");
382 host.insert(library.get("still").expect("still is in the library").clone())
383 .expect("a fresh id");
384 assert_eq!(host.get("still").map(|entry| entry.source().package()), Some("library"));
385 assert_eq!(host.get("calm_2").map(|entry| entry.source().package()), Some("host"));
386
387 let mut combined = ModelSet::new(build("other"));
388 combined.extend(host).expect("no shared id");
389 assert_eq!(ids(&combined), ["calm_2", "still"]);
390 assert_eq!(
391 combined.get("still").map(|entry| entry.source().package()),
392 Some("library")
393 );
394 assert_eq!(
395 combined.get("calm_2").map(|entry| entry.source().package()),
396 Some("host")
397 );
398 }
399
400 #[test]
401 fn a_cpu_model_needs_no_device() {
402 let mut models = ModelSet::new(build("host"));
403 models.insert(register_grid_model::<Still>()).expect("a fresh id");
404 assert_eq!(models.gpu_needs(), crate::gpu::GpuNeeds::default());
405 assert!(models.lookup("still", None).is_ok());
406 assert!(matches!(
407 models.lookup("calm_2", None),
408 Err(ModelLookupError::NotInSet { .. })
409 ));
410 }
411}