1use std::collections::{BTreeMap, BTreeSet};
39use std::fmt;
40use std::sync::Arc;
41use std::time::{Duration, Instant};
42
43pub mod memo;
44pub mod stages;
45
46pub use crate::ledger::{Conservation, Gap, Measure};
47pub use crate::pass_options::{Choice, Given, OptionSpec, PassSpec, TextForm};
48use crate::world::{Component, Entity, Resource, World};
49pub use stages::Stages;
50
51pub type Options = BTreeMap<String, String>;
53
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
57pub struct PassLog {
58 pub counts: BTreeMap<String, u64>,
59 pub approximations: BTreeMap<String, u64>,
60 pub approximated_on: BTreeMap<String, BTreeSet<Entity>>,
64 pub omitted: BTreeMap<String, u64>,
67 pub removed: BTreeSet<String>,
71 pub placed: BTreeSet<Entity>,
75 pub placed_from: BTreeMap<Entity, Entity>,
77 pub drops: BTreeMap<(Measure, String), u64>,
80 pub details: BTreeMap<String, Vec<serde_json::Value>>,
85}
86
87impl PassLog {
88 pub fn count(&mut self, what: &str, n: u64) {
89 *self.counts.entry(what.to_owned()).or_default() += n;
90 }
91
92 pub fn detail(&mut self, label: &str, value: serde_json::Value) {
94 self.details.entry(label.to_owned()).or_default().push(value);
95 }
96
97 pub fn record_removed(&mut self, name: &str) {
99 self.removed.insert(name.to_owned());
100 }
101
102 pub fn approximate(&mut self, what: &str, n: u64) {
104 *self.approximations.entry(what.to_owned()).or_default() += n;
105 }
106
107 pub fn approximate_on(&mut self, e: Entity, what: &str, n: u64) {
111 self.approximate(what, n);
112 self.approximated_on.entry(what.to_owned()).or_default().insert(e);
113 }
114
115 pub fn omit(&mut self, what: &str, n: u64) {
117 *self.omitted.entry(what.to_owned()).or_default() += n;
118 }
119
120 pub fn dropped(&mut self, measure: Measure, why: &str, n: u64) {
123 if n > 0 {
124 *self.drops.entry((measure, why.to_owned())).or_default() += n;
125 }
126 }
127
128 pub fn declared(&self, measure: Measure) -> u64 {
130 self.drops
131 .iter()
132 .filter(|((m, _), _)| *m == measure)
133 .map(|(_, n)| n)
134 .sum()
135 }
136
137 pub fn place(&mut self, e: Entity) {
140 self.placed.insert(e);
141 }
142
143 pub fn place_from(&mut self, e: Entity, source: Entity) {
147 self.placed.insert(e);
148 self.placed_from.insert(e, source);
149 }
150
151 pub fn absorb(&mut self, other: PassLog) {
155 let PassLog {
156 counts,
157 approximations,
158 approximated_on,
159 omitted,
160 removed,
161 placed,
162 placed_from,
163 drops,
164 details,
165 } = other;
166 for ((m, why), n) in drops {
167 self.dropped(m, &why, n);
168 }
169 for (k, n) in counts {
170 self.count(&k, n);
171 }
172 for (k, n) in approximations {
173 self.approximate(&k, n);
174 }
175 for (k, entities) in approximated_on {
176 self.approximated_on.entry(k).or_default().extend(entities);
177 }
178 for (k, n) in omitted {
179 self.omit(&k, n);
180 }
181 self.removed.extend(removed);
182 self.placed.extend(placed);
183 self.placed_from.extend(placed_from);
184 for (label, entries) in details {
185 self.details.entry(label).or_default().extend(entries);
186 }
187 }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub struct Settings {
194 pub threads: primase_foundations::parallel::Threads,
196}
197
198#[derive(Clone, Copy, Debug)]
201pub struct Setting {
202 pub name: &'static str,
204 text: fn(&World) -> Option<String>,
205}
206
207impl Setting {
208 pub const fn of<T: RunSetting>() -> Self {
210 Setting {
211 name: T::NAME,
212 text: setting_text::<T>,
213 }
214 }
215
216 pub fn text(&self, w: &World) -> Option<String> {
218 (self.text)(w)
219 }
220}
221
222pub trait RunSetting: Resource {
224 fn text(&self) -> String;
226}
227
228fn setting_text<T: RunSetting>(w: &World) -> Option<String> {
229 w.resource::<T>().map(RunSetting::text)
230}
231
232fn count_dropped(log: &mut PassLog, d: &crate::Dropped) {
234 let n = |x: usize| u64::try_from(x).unwrap_or(u64::MAX);
235 for (what, count) in [
236 (
237 "render graph steps dropped: they drew with an entity that left the world",
238 n(d.graph.steps.len()),
239 ),
240 (
241 "render graph lighting pipelines dropped: they selected a dropped step",
242 u64::from(d.graph.lighting),
243 ),
244 (
245 "render graph pictures dropped: a dropped step wrote them",
246 u64::from(d.graph.picture),
247 ),
248 (
249 "step bindings dropped: only dropped render graph steps made their draws",
250 n(d.step_bindings.len()),
251 ),
252 (
253 "step draws dropped: only dropped render graph steps made their draws",
254 n(d.step_draws.len()),
255 ),
256 ] {
257 if count > 0 {
258 log.count(what, count);
259 }
260 }
261}
262
263fn dropped_from(d: &crate::Dropped, name: &str) -> BTreeSet<Entity> {
265 let list = if name == <crate::StepBindings as crate::Component>::NAME {
266 &d.step_bindings
267 } else if name == <crate::StepDraws as crate::Component>::NAME {
268 &d.step_draws
269 } else {
270 return BTreeSet::new();
271 };
272 list.iter().map(|(e, _)| *e).collect()
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
279#[error("`{name}`, which the world does not have: the program states it for the run")]
280pub struct Unstated {
281 pub name: &'static str,
283}
284
285pub trait Pass: Send + Sync {
287 fn name(&self) -> &'static str;
289 fn options(&self) -> Options {
291 Options::new()
292 }
293 fn may_lack_inputs(&self) -> bool {
299 false
300 }
301 fn requires(&self) -> &[&'static str] {
303 &[]
304 }
305 fn settings(&self) -> &[Setting] {
310 &[]
311 }
312 fn provides(&self) -> &[&'static str] {
314 &[]
315 }
316 fn removes(&self) -> &[&'static str] {
318 &[]
319 }
320 fn modifies(&self) -> &[&'static str] {
330 &[]
331 }
332 fn approximates(&self) -> &'static str {
334 ""
335 }
336 fn despawns_entities(&self) -> bool {
341 false
342 }
343 fn removes_by_log(&self) -> bool {
348 false
349 }
350 fn settles(&self) -> &[&'static str] {
356 &[]
357 }
358 fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String>;
359}
360
361#[derive(Default)]
363pub struct PassRegistry {
364 specs: BTreeMap<&'static str, &'static PassSpec>,
365}
366
367impl fmt::Debug for PassRegistry {
368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369 f.debug_list().entries(self.specs.keys()).finish()
370 }
371}
372
373impl PassRegistry {
374 pub fn new() -> Self {
375 Self::default()
376 }
377
378 pub fn add(&mut self, spec: &'static PassSpec) {
386 let name = spec.name;
387 spec.validate();
388 let example = spec.example();
389 let made = spec
390 .instance(&example)
391 .unwrap_or_else(|e| panic!("pass {name} with its example options {example:?}: {e}"));
392 assert_eq!(
393 made.name(),
394 name,
395 "factory registered as {name} makes {}",
396 made.name()
397 );
398 let printed = made.options();
399 if let Err(e) = spec.check(&printed) {
400 panic!("pass {name} prints {printed:?}, which its declaration refuses: {e}");
401 }
402 let again = spec
403 .instance(&printed)
404 .unwrap_or_else(|e| panic!("pass {name}: what it prints, {printed:?}, does not make it: {e}"));
405 assert_eq!(
406 again.options(),
407 printed,
408 "pass {name}: made from what it prints, it prints otherwise"
409 );
410 assert!(
411 self.specs.insert(name, spec).is_none(),
412 "pass {name} registered twice"
413 );
414 }
415
416 pub fn describe(&self, name: &str) -> Option<String> {
418 self.specs.get(name).map(|spec| spec.describe())
419 }
420
421 pub fn requires_options(&self, name: &str) -> bool {
424 self.specs.get(name).is_some_and(|spec| spec.requires_options())
425 }
426
427 pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
429 self.specs.keys().copied()
430 }
431
432 pub fn make_registered(&self, name: &str) -> Result<Box<dyn Pass>, Error> {
434 let spec = self
435 .specs
436 .get(name)
437 .ok_or_else(|| Error::UnknownPass(name.to_owned()))?;
438 self.make(name, &spec.example())
439 }
440
441 pub fn make(&self, name: &str, options: &Options) -> Result<Box<dyn Pass>, Error> {
442 let spec = self
443 .specs
444 .get(name)
445 .ok_or_else(|| Error::UnknownPass(name.to_owned()))?;
446 spec.instance(options).map_err(|message| Error::Options {
447 pass: name.to_owned(),
448 message,
449 })
450 }
451
452 pub fn parse(&self, text: &str) -> Result<Pipeline, Error> {
454 let mut passes = Vec::new();
455 for (name, options) in parse_pipeline(text)? {
456 passes.push(self.make(&name, &options)?);
457 }
458 Ok(Pipeline { passes })
459 }
460
461 pub fn prepare_texts(&self, present: &BTreeSet<String>, texts: Vec<String>) -> TextPreparation {
466 let mut held = present.clone();
467 let mut runnable = Vec::new();
468 let mut unfed = Vec::new();
469 for text in &texts {
470 let Ok(pipeline) = self.parse(text) else {
471 runnable.push(text.clone());
472 continue;
473 };
474 let inputs = pipeline.unfed(&held);
475 if inputs.passes.is_empty() {
476 held = inputs.held;
477 runnable.push(text.clone());
478 } else {
479 let mut missing = Vec::new();
480 for name in inputs.passes.iter().flat_map(|(_, names)| names) {
481 if !missing.iter().any(|held| held == name) {
482 missing.push((*name).to_owned());
483 }
484 }
485 unfed.push((text.clone(), missing));
486 }
487 }
488 TextPreparation {
489 requested: texts,
490 runnable,
491 unfed,
492 }
493 }
494}
495
496fn is_word(s: &str) -> bool {
497 !s.is_empty()
498 && s.bytes()
499 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
500}
501
502pub(crate) fn parse_pipeline(text: &str) -> Result<Vec<(String, Options)>, Error> {
503 let bad = |m: String| Error::Parse(format!("{m} in pipeline {text:?}"));
504 let mut out = Vec::new();
505 let mut rest = text.trim();
506 if rest.is_empty() {
507 return Ok(out);
508 }
509 loop {
510 let end = rest.find([',', '{']).unwrap_or(rest.len());
511 let name = rest[..end].trim();
512 if !is_word(name) {
513 return Err(bad(format!("{name:?} is not a pass name")));
514 }
515 rest = &rest[end..];
516 let mut options = Options::new();
517 if let Some(r) = rest.strip_prefix('{') {
518 let close = r
519 .find('}')
520 .ok_or_else(|| bad(format!("{name}: no closing brace")))?;
521 let mut last: Option<String> = None;
524 for item in r[..close].split(',').filter(|s| !s.trim().is_empty()) {
525 let Some((k, v)) = item.split_once('=') else {
526 let value = last
527 .as_ref()
528 .and_then(|k| options.get_mut(k))
529 .ok_or_else(|| bad(format!("{name}: option {item:?} is not key=value")))?;
530 value.push(',');
531 value.push_str(item.trim());
532 continue;
533 };
534 let (k, v) = (k.trim(), v.trim());
535 if !is_word(k) || v.contains('{') {
536 return Err(bad(format!("{name}: bad option {item:?}")));
537 }
538 if options.insert(k.to_owned(), v.to_owned()).is_some() {
539 return Err(bad(format!("{name}: option {k} twice")));
540 }
541 last = Some(k.to_owned());
542 }
543 rest = r[close + 1..].trim_start();
544 }
545 out.push((name.to_owned(), options));
546 if rest.is_empty() {
547 return Ok(out);
548 }
549 rest = rest
550 .strip_prefix(',')
551 .ok_or_else(|| bad(format!("unexpected {rest:?}")))?
552 .trim_start();
553 }
554}
555
556#[derive(Default)]
558pub struct Pipeline {
559 pub passes: Vec<Box<dyn Pass>>,
560}
561
562#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
565pub struct TextPreparation {
566 pub requested: Vec<String>,
568 pub runnable: Vec<String>,
570 pub unfed: Vec<(String, Vec<String>)>,
572}
573
574impl fmt::Debug for Pipeline {
575 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576 write!(f, "Pipeline({self})")
577 }
578}
579
580impl Pipeline {
582 pub fn check(&self) -> Result<()> {
586 check_settled(&self.passes)
587 }
588
589 pub fn unfed(&self, present: &BTreeSet<String>) -> Unfed {
598 let mut held = present.clone();
599 let mut passes = Vec::new();
600 for p in &self.passes {
601 let missing: Vec<&'static str> = p
602 .requires()
603 .iter()
604 .copied()
605 .filter(|&r| !held.contains(r))
606 .collect();
607 if p.may_lack_inputs() && !p.requires().is_empty() && missing.len() == p.requires().len() {
608 continue;
609 }
610 if missing.is_empty() {
611 held.extend(p.provides().iter().map(|&n| n.to_owned()));
612 } else {
613 passes.push((pass_text(p.as_ref()), missing));
614 }
615 }
616 Unfed { passes, held }
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
622pub struct Unfed {
623 pub passes: Vec<(String, Vec<&'static str>)>,
626 pub held: BTreeSet<String>,
629}
630
631impl fmt::Display for Pipeline {
632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 for (i, p) in self.passes.iter().enumerate() {
634 if i > 0 {
635 f.write_str(", ")?;
636 }
637 f.write_str(&pass_text(p.as_ref()))?;
638 }
639 Ok(())
640 }
641}
642
643pub fn carries(value: &str) -> bool {
648 parse_pipeline(&format!("p{{k={value}}}"))
649 .ok()
650 .and_then(|mut passes| passes.pop())
651 .and_then(|(_, options)| options.get("k").cloned())
652 .is_some_and(|v| v == value)
653}
654
655pub fn pass_text(p: &dyn Pass) -> String {
658 stated(p.name(), &p.options())
659}
660
661pub fn text_form<T: TextForm>(value: &T) -> String {
664 stated(T::NAME, &value.options())
665}
666
667fn stated(name: &str, options: &Options) -> String {
669 if options.is_empty() {
670 return name.to_owned();
671 }
672 let items: Vec<String> = options.iter().map(|(k, v)| format!("{k}={v}")).collect();
673 format!("{name}{{{}}}", items.join(","))
674}
675
676pub fn parse_text_form<T: TextForm>(text: &str) -> Result<T, Error> {
679 let mut items = parse_pipeline(text)?;
680 let (name, options) = match (items.pop(), items.is_empty()) {
681 (Some(item), true) => item,
682 _ => return Err(Error::Parse(format!("{text:?} is not one {}", T::NAME))),
683 };
684 if name != T::NAME {
685 return Err(Error::Parse(format!("{name} is not {}", T::NAME)));
686 }
687 let refused = |message| Error::Stated {
688 name: T::NAME,
689 message,
690 };
691 crate::pass_options::check_options(T::NAME, T::OPTIONS, &options).map_err(refused)?;
692 T::make(&Given::new(T::NAME, T::OPTIONS, &options)).map_err(refused)
693}
694
695pub fn validate_text_form<T: TextForm>() {
700 let name = T::NAME;
701 assert!(is_word(name), "{name:?} is not a name a text can start with");
702 crate::pass_options::validate_options(name, T::OPTIONS);
703 let example = PassSpec {
704 name,
705 options: T::OPTIONS,
706 make: |_| unreachable!("the example is only read"),
707 }
708 .example();
709 let made = T::make(&Given::new(name, T::OPTIONS, &example))
710 .unwrap_or_else(|e| panic!("{name} with its example options {example:?}: {e}"));
711 let printed = made.options();
712 if let Err(e) = crate::pass_options::check_options(name, T::OPTIONS, &printed) {
713 panic!("{name} prints {printed:?}, which its declaration refuses: {e}");
714 }
715 let again = T::make(&Given::new(name, T::OPTIONS, &printed))
716 .unwrap_or_else(|e| panic!("{name}: what it prints, {printed:?}, does not make it: {e}"));
717 assert_eq!(
718 again.options(),
719 printed,
720 "{name}: made from what it prints, it prints otherwise"
721 );
722}
723
724pub type LegalityCheck = Arc<dyn Fn(&World, Entity) -> bool + Send + Sync>;
726
727#[derive(Clone)]
731pub enum Legality {
732 Legal,
734 Dynamic(LegalityCheck),
736 Covered(fn(&World, Entity) -> bool),
740 Illegal,
742}
743
744#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
746pub enum Rule {
747 Legal,
749 Dynamic,
751 Covered,
753 Illegal,
755}
756
757impl fmt::Debug for Legality {
758 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759 f.write_str(match self {
760 Legality::Legal => "Legal",
761 Legality::Dynamic(_) => "Dynamic",
762 Legality::Covered(_) => "Covered",
763 Legality::Illegal => "Illegal",
764 })
765 }
766}
767
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
770pub enum Mode {
771 Partial,
773 Full,
775}
776
777#[derive(Clone)]
779struct ResourceRule {
780 what: String,
781 check: Arc<dyn Fn(&World) -> bool + Send + Sync>,
782}
783
784impl fmt::Debug for ResourceRule {
785 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786 write!(f, "ResourceRule({:?})", self.what)
787 }
788}
789
790#[derive(Clone, Debug)]
792pub struct Target {
793 pub name: String,
794 rules: BTreeMap<String, Legality>,
795 rule_words: BTreeMap<String, String>,
797 resource_rules: BTreeMap<String, ResourceRule>,
799}
800
801impl Target {
802 pub fn new(name: impl Into<String>) -> Self {
803 Target {
804 name: name.into(),
805 rules: BTreeMap::new(),
806 rule_words: BTreeMap::new(),
807 resource_rules: BTreeMap::new(),
808 }
809 }
810
811 pub fn rule(mut self, name: &str, legality: Legality) -> Self {
812 self.rules.insert(name.to_owned(), legality);
813 self
814 }
815
816 pub fn legal(self, name: &str) -> Self {
817 self.rule(name, Legality::Legal)
818 }
819
820 pub fn illegal(self, name: &str) -> Self {
821 self.rule(name, Legality::Illegal)
822 }
823
824 pub fn dynamic(self, name: &str, check: impl Fn(&World, Entity) -> bool + Send + Sync + 'static) -> Self {
825 self.rule(name, Legality::Dynamic(Arc::new(check)))
826 }
827
828 pub fn covered(mut self, name: &str, what: &str, check: fn(&World, Entity) -> bool) -> Self {
831 self.rule_words.insert(name.to_owned(), what.to_owned());
832 self.rule(name, Legality::Covered(check))
833 }
834
835 pub fn dynamic_typed<T: Component>(self, check: impl Fn(&T) -> bool + Send + Sync + 'static) -> Self {
837 self.dynamic(T::NAME, move |w, e| w.get::<T>(e).is_some_and(&check))
838 }
839
840 pub fn check<T: Component>(
843 mut self,
844 what: &str,
845 check: impl Fn(&T) -> bool + Send + Sync + 'static,
846 ) -> Self {
847 self.rule_words.insert(T::NAME.to_owned(), what.to_owned());
848 self.dynamic_typed::<T>(check)
849 }
850
851 pub fn check_resource<R: Resource>(
854 mut self,
855 what: &str,
856 check: impl Fn(&R) -> bool + Send + Sync + 'static,
857 ) -> Self {
858 self.resource_rules.insert(
859 R::NAME.to_owned(),
860 ResourceRule {
861 what: what.to_owned(),
862 check: Arc::new(move |w: &World| w.resource::<R>().is_none_or(&check)),
863 },
864 );
865 self
866 }
867
868 pub fn rule_words(&self, name: &str) -> Option<&str> {
871 self.rule_words
872 .get(name)
873 .or_else(|| self.resource_rules.get(name).map(|r| &r.what))
874 .map(String::as_str)
875 }
876
877 pub fn refused_resources(&self, w: &World) -> Vec<&str> {
879 self.resource_rules
880 .iter()
881 .filter(|(_, r)| !(r.check)(w))
882 .map(|(n, _)| n.as_str())
883 .collect()
884 }
885
886 pub fn legality(&self, name: &str) -> Option<&Legality> {
887 self.rules.get(name)
888 }
889
890 pub fn rules(&self) -> Vec<(&str, Rule)> {
893 self.rules
894 .iter()
895 .map(|(name, legality)| {
896 let rule = match legality {
897 Legality::Legal => Rule::Legal,
898 Legality::Dynamic(_) => Rule::Dynamic,
899 Legality::Covered(_) => Rule::Covered,
900 Legality::Illegal => Rule::Illegal,
901 };
902 (name.as_str(), rule)
903 })
904 .collect()
905 }
906
907 pub fn checked_resources(&self) -> Vec<&str> {
909 self.resource_rules.keys().map(String::as_str).collect()
910 }
911
912 pub fn check_prepared(&self, w: &World) -> Prepared {
919 let mut out = Prepared::default();
920 for name in w.component_names() {
921 let all = w.entities_named(name);
922 match self.rules.get(name) {
923 Some(Legality::Legal) => {}
924 Some(Legality::Dynamic(f)) => {
925 let bad: Vec<Entity> = all.into_iter().filter(|&e| !f(w, e)).collect();
926 if !bad.is_empty() {
927 let words = self.rule_words(name).unwrap_or_default().to_owned();
928 out.invalid.push((name.to_owned(), words, bad));
929 }
930 }
931 Some(Legality::Covered(f)) => {
932 let uncovered = all.iter().filter(|&&e| !f(w, e)).count();
933 if uncovered > 0 {
934 let words = self.rule_words(name).unwrap_or_default();
935 out.unsupported
936 .push((format!("{name} not {words}"), uncovered as u64));
937 }
938 }
939 Some(Legality::Illegal) | None => out.unsupported.push((name.to_owned(), all.len() as u64)),
940 }
941 }
942 out.invalid_resources = self.refused_resources(w).into_iter().map(str::to_owned).collect();
943 for name in w.resource_names() {
944 let written = crate::convert::OPERATIONAL_RESOURCES.contains(&name)
945 || self.resource_rules.contains_key(name)
946 || self
947 .rules
948 .get(name)
949 .is_some_and(|rule| !matches!(rule, Legality::Illegal));
950 if !written {
951 out.unsupported_resources.push(name.to_owned());
952 }
953 }
954 out
955 }
956
957 pub fn illegal_entities(&self, w: &World, name: &str, mode: Mode) -> Vec<Entity> {
959 let all = w.entities_named(name);
960 match self.rules.get(name) {
961 Some(Legality::Legal) => Vec::new(),
962 Some(Legality::Illegal) => all,
963 Some(Legality::Dynamic(f)) => all.into_iter().filter(|&e| !f(w, e)).collect(),
964 Some(Legality::Covered(_)) if mode == Mode::Partial => Vec::new(),
965 Some(Legality::Covered(f)) => all.into_iter().filter(|&e| !f(w, e)).collect(),
966 None if mode == Mode::Partial => Vec::new(),
967 None => all,
968 }
969 }
970}
971
972#[derive(Clone, Debug, Default, PartialEq, Eq)]
974pub struct Prepared {
975 pub unsupported: Vec<(String, u64)>,
978 pub invalid: Vec<(String, String, Vec<Entity>)>,
980 pub invalid_resources: Vec<String>,
982 pub unsupported_resources: Vec<String>,
984}
985
986pub trait Validator: Send + Sync {
988 fn name(&self) -> &'static str;
989 fn check(&self, w: &World) -> Vec<(Option<Entity>, String)>;
990}
991
992#[derive(Clone, Debug, PartialEq, Eq)]
994pub struct VerifyFailure {
995 pub check: String,
997 pub entity: Option<Entity>,
998 pub message: String,
999}
1000
1001impl fmt::Display for VerifyFailure {
1002 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1003 match self.entity {
1004 Some(e) => write!(f, "{} on {e:?}: {}", self.check, self.message),
1005 None => write!(f, "{}: {}", self.check, self.message),
1006 }
1007 }
1008}
1009
1010#[derive(Debug, thiserror::Error)]
1011pub enum Error {
1012 #[error("{0}")]
1013 Parse(String),
1014 #[error("pass {pass} left out {dropped} {measure} but declared {declared} (PassLog::dropped)")]
1015 Conservation {
1016 pass: String,
1017 measure: Measure,
1018 dropped: u64,
1019 declared: u64,
1020 },
1021 #[error("no pass named {0:?}")]
1022 UnknownPass(String),
1023 #[error("pass {pass}: {message}")]
1024 Options { pass: String, message: String },
1025 #[error("{name}: {message}")]
1027 Stated { name: &'static str, message: String },
1028 #[error("pass {pass} requires `{name}`, which the world does not have")]
1029 Missing { pass: String, name: String },
1030 #[error("the world states `{name}` as {world}, the program as {program}")]
1032 SettingDisagrees {
1033 name: &'static str,
1034 world: String,
1035 program: String,
1036 },
1037 #[error("pass {pass} failed: {message}")]
1038 Pass { pass: String, message: String },
1039 #[error("pass {pass} {what} `{names:?}` without declaring it")]
1040 Undeclared {
1041 pass: String,
1042 what: &'static str,
1043 names: Vec<String>,
1044 },
1045 #[error("verify after {after}: {}", failures.iter().take(10).map(|f| f.to_string()).collect::<Vec<_>>().join("; "))]
1046 Verify {
1047 after: String,
1048 failures: Vec<VerifyFailure>,
1049 },
1050 #[error("instrument at {step}: {message}")]
1051 Instrument { step: String, message: String },
1052 #[error("{}", illegal_text(.target, .name, .entities, .rule, .lowered_by))]
1053 Illegal {
1054 target: String,
1055 name: String,
1056 entities: Vec<Entity>,
1058 rule: String,
1060 lowered_by: Vec<String>,
1063 },
1064 #[error("{changed_by} provides `{name}` after {settled_by} settled it: its values would not be settled")]
1065 SettledThenChanged {
1066 settled_by: String,
1067 changed_by: String,
1068 name: String,
1069 },
1070}
1071
1072pub type Result<T, E = Error> = std::result::Result<T, E>;
1073
1074fn illegal_text(target: &str, name: &str, entities: &[Entity], rule: &str, lowered_by: &[String]) -> String {
1075 let rule = if rule.is_empty() {
1076 String::new()
1077 } else {
1078 format!(" ({rule})")
1079 };
1080 let on = if entities.is_empty() {
1081 String::new()
1082 } else {
1083 format!(" on {entities:?}")
1084 };
1085 let why = if lowered_by.is_empty() {
1086 "nothing in the stated pipeline removes or rewrites it".to_owned()
1087 } else {
1088 format!("{} left it", lowered_by.join(", "))
1089 };
1090 format!("`{target}` does not accept `{name}`{rule}{on} after the pipeline: {why}")
1091}
1092
1093fn check_settled(passes: &[Box<dyn Pass>]) -> Result<()> {
1096 for (i, a) in passes.iter().enumerate() {
1097 for &n in a.settles() {
1098 for (j, b) in passes.iter().enumerate().skip(i + 1) {
1099 let adds = b.provides().contains(&n) && !b.settles().contains(&n);
1100 if adds && !passes[j + 1..].iter().any(|c| c.settles().contains(&n)) {
1101 return Err(Error::SettledThenChanged {
1102 settled_by: pass_text(a.as_ref()),
1103 changed_by: pass_text(b.as_ref()),
1104 name: n.to_owned(),
1105 });
1106 }
1107 }
1108 }
1109 }
1110 Ok(())
1111}
1112
1113#[derive(Clone, Debug)]
1116pub struct StepReport {
1117 pub pass: String,
1119 pub approximates: &'static str,
1120 pub log: PassLog,
1121 pub added: Vec<String>,
1123 pub removed: Vec<String>,
1124 pub conservation: Vec<Gap>,
1127 pub failed: Option<String>,
1131 pub skipped: Vec<String>,
1135 pub time: StepTime,
1137 pub settings: Vec<String>,
1141}
1142
1143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1146pub struct StepTime {
1147 pub run: Duration,
1149 pub checks: Duration,
1152}
1153
1154impl PartialEq for StepReport {
1155 fn eq(&self, other: &StepReport) -> bool {
1157 let StepReport {
1158 pass,
1159 approximates,
1160 log,
1161 added,
1162 removed,
1163 conservation,
1164 failed,
1165 skipped,
1166 time: _,
1167 settings: _,
1168 } = self;
1169 (
1170 pass,
1171 approximates,
1172 log,
1173 added,
1174 removed,
1175 conservation,
1176 failed,
1177 skipped,
1178 ) == (
1179 &other.pass,
1180 &other.approximates,
1181 &other.log,
1182 &other.added,
1183 &other.removed,
1184 &other.conservation,
1185 &other.failed,
1186 &other.skipped,
1187 )
1188 }
1189}
1190impl Eq for StepReport {}
1191
1192impl RunReport {
1193 pub fn time(&self) -> StepTime {
1195 self.steps.iter().fold(StepTime::default(), |t, s| StepTime {
1196 run: t.run + s.time.run,
1197 checks: t.checks + s.time.checks,
1198 })
1199 }
1200}
1201
1202pub const SKIPPED: &str = "runs holding none of its required names, skipped";
1205
1206pub const SKIPPED_UNFED: &str = "runs missing required names, skipped";
1208
1209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1213pub enum MissingInputs {
1214 #[default]
1216 Refuse,
1217 Skip,
1219}
1220
1221#[derive(Clone, Debug, Default, PartialEq, Eq)]
1224pub struct RunReport {
1225 pub target: String,
1227 pub pipeline: String,
1229 pub steps: Vec<StepReport>,
1230 pub dropped_outside: Vec<(String, crate::Dropped)>,
1234}
1235
1236impl fmt::Display for RunReport {
1237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1238 writeln!(f, "target {}: pipeline [{}]", self.target, self.pipeline)?;
1239 for s in &self.steps {
1240 write!(f, " {}: +{:?} -{:?}", s.pass, s.added, s.removed)?;
1241 for (k, n) in &s.log.counts {
1242 write!(f, " {k}={n}")?;
1243 }
1244 for (k, n) in &s.log.approximations {
1245 write!(f, " ~{k}={n}")?;
1246 }
1247 for (k, n) in &s.log.omitted {
1248 write!(f, " -{k}={n}")?;
1249 }
1250 for ((m, why), n) in &s.log.drops {
1251 write!(f, " -{m}:{why}={n}")?;
1252 }
1253 for g in &s.conservation {
1254 write!(f, " !{g}")?;
1255 }
1256 if s.skipped.is_empty() {
1257 write!(
1258 f,
1259 " ({:.3} s, checks {:.3} s",
1260 s.time.run.as_secs_f64(),
1261 s.time.checks.as_secs_f64()
1262 )?;
1263 for setting in &s.settings {
1264 write!(f, "; {setting}")?;
1265 }
1266 write!(f, ")")?;
1267 }
1268 writeln!(f)?;
1269 }
1270 for (next, dropped) in &self.dropped_outside {
1271 writeln!(f, " outside any pass, before {next}: dropped {dropped}")?;
1272 }
1273 Ok(())
1274 }
1275}
1276
1277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1279pub struct Step<'a> {
1280 pub index: usize,
1282 pub name: &'static str,
1284 pub text: &'a str,
1286}
1287
1288pub trait Instrument {
1293 fn before_pass(&mut self, _step: &Step<'_>, _w: &World) -> std::result::Result<(), String> {
1294 Ok(())
1295 }
1296 fn after_pass(
1297 &mut self,
1298 _step: &Step<'_>,
1299 _w: &World,
1300 _report: &StepReport,
1301 ) -> std::result::Result<(), String> {
1302 Ok(())
1303 }
1304 fn pass_failed(
1305 &mut self,
1306 _step: &Step<'_>,
1307 _w: &World,
1308 _error: &Error,
1309 ) -> std::result::Result<(), String> {
1310 Ok(())
1311 }
1312}
1313
1314struct NoInstrument;
1316impl Instrument for NoInstrument {}
1317
1318#[derive(Default)]
1320pub struct PassManager {
1321 pub registry: PassRegistry,
1322 validators: Vec<Box<dyn Validator>>,
1323 conservation: Conservation,
1324 pass_checks: bool,
1325 missing_inputs: MissingInputs,
1326 settings: Option<Settings>,
1327}
1328
1329impl fmt::Debug for PassManager {
1330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1331 f.debug_struct("PassManager")
1332 .field("registry", &self.registry)
1333 .field(
1334 "validators",
1335 &self.validators.iter().map(|v| v.name()).collect::<Vec<_>>(),
1336 )
1337 .finish()
1338 }
1339}
1340
1341impl PassManager {
1342 pub fn new(registry: PassRegistry) -> Self {
1343 PassManager {
1344 registry,
1345 validators: Vec::new(),
1346 conservation: Conservation::Off,
1347 pass_checks: cfg!(debug_assertions),
1348 missing_inputs: MissingInputs::Refuse,
1349 settings: None,
1350 }
1351 }
1352
1353 pub fn with_settings(mut self, settings: Settings) -> Self {
1358 self.settings = Some(settings);
1359 self
1360 }
1361
1362 fn state_settings(&self, w: &mut World) -> Result<()> {
1365 let Some(Settings { threads }) = self.settings else {
1366 return Ok(());
1367 };
1368 match w.resource::<primase_foundations::parallel::Threads>() {
1369 Some(&held) if held != threads => Err(Error::SettingDisagrees {
1370 name: <primase_foundations::parallel::Threads as Resource>::NAME,
1371 world: RunSetting::text(&held),
1372 program: RunSetting::text(&threads),
1373 }),
1374 Some(_) => Ok(()),
1375 None => {
1376 w.insert_resource(threads);
1377 Ok(())
1378 }
1379 }
1380 }
1381
1382 pub fn with_pass_checks(mut self, on: bool) -> Self {
1393 self.pass_checks = on;
1394 self
1395 }
1396
1397 pub fn pass_checks(&self) -> bool {
1398 self.pass_checks
1399 }
1400
1401 pub fn with_missing_inputs(mut self, policy: MissingInputs) -> Self {
1405 self.missing_inputs = policy;
1406 self
1407 }
1408
1409 pub fn with_conservation(mut self, c: Conservation) -> Self {
1411 self.conservation = c;
1412 self
1413 }
1414
1415 pub fn conservation(&self) -> Conservation {
1416 self.conservation
1417 }
1418
1419 pub fn add_validator(&mut self, v: Box<dyn Validator>) {
1420 self.validators.push(v);
1421 }
1422
1423 pub fn verify(&self, w: &World, after: &str) -> Result<()> {
1425 let mut failures: Vec<VerifyFailure> = w
1426 .verify_components()
1427 .into_iter()
1428 .map(|(c, e, message)| VerifyFailure {
1429 check: c.to_owned(),
1430 entity: Some(e),
1431 message,
1432 })
1433 .collect();
1434 failures.extend(
1435 w.verify_resources()
1436 .into_iter()
1437 .map(|(r, message)| VerifyFailure {
1438 check: r.to_owned(),
1439 entity: None,
1440 message,
1441 }),
1442 );
1443 for v in &self.validators {
1444 failures.extend(v.check(w).into_iter().map(|(entity, message)| VerifyFailure {
1445 check: v.name().to_owned(),
1446 entity,
1447 message,
1448 }));
1449 }
1450 if failures.is_empty() {
1451 Ok(())
1452 } else {
1453 Err(Error::Verify {
1454 after: after.to_owned(),
1455 failures,
1456 })
1457 }
1458 }
1459
1460 pub fn run(&self, w: &mut World, pipeline: &Pipeline, report: &mut RunReport) -> Result<()> {
1462 self.run_instrumented(w, pipeline, report, &mut NoInstrument)
1463 }
1464
1465 pub fn run_instrumented(
1467 &self,
1468 w: &mut World,
1469 pipeline: &Pipeline,
1470 report: &mut RunReport,
1471 instrument: &mut dyn Instrument,
1472 ) -> Result<()> {
1473 self.run_with(w, pipeline, report, instrument)
1474 }
1475
1476 pub fn run_part(&self, w: &mut World, pipeline: &Pipeline, report: &mut RunReport) -> Result<()> {
1483 self.run_with(w, pipeline, report, &mut NoInstrument)
1484 }
1485
1486 fn run_with(
1487 &self,
1488 w: &mut World,
1489 pipeline: &Pipeline,
1490 report: &mut RunReport,
1491 instrument: &mut dyn Instrument,
1492 ) -> Result<()> {
1493 pipeline.check()?;
1494 self.state_settings(w)?;
1495 for (index, p) in pipeline.passes.iter().enumerate() {
1496 let text = pass_text(p.as_ref());
1497 let missing: Vec<String> = p
1498 .requires()
1499 .iter()
1500 .filter(|r| !w.has_component_named(r) && !w.resource_names().contains(r))
1501 .map(|r| (*r).to_owned())
1502 .collect();
1503 let declared_skip =
1504 p.may_lack_inputs() && !p.requires().is_empty() && missing.len() == p.requires().len();
1505 if declared_skip || (self.missing_inputs == MissingInputs::Skip && !missing.is_empty()) {
1506 let mut log = PassLog::default();
1507 log.count(if declared_skip { SKIPPED } else { SKIPPED_UNFED }, 1);
1508 report.steps.push(StepReport {
1509 pass: text,
1510 approximates: p.approximates(),
1511 log,
1512 added: Vec::new(),
1513 removed: Vec::new(),
1514 conservation: Vec::new(),
1515 failed: None,
1516 skipped: missing,
1517 time: StepTime::default(),
1518 settings: Vec::new(),
1519 });
1520 continue;
1521 }
1522 let step = Step {
1523 index,
1524 name: p.name(),
1525 text: &text,
1526 };
1527 let hook = |r: std::result::Result<(), String>| {
1528 r.map_err(|message| Error::Instrument {
1529 step: text.clone(),
1530 message,
1531 })
1532 };
1533 hook(instrument.before_pass(&step, w))?;
1534 let owned_before = w
1536 .resource::<crate::convert::PartScope>()
1537 .map(|s| s.owned(w))
1538 .unwrap_or_default();
1539 let outside = w.take_dropped();
1541 if !outside.is_empty() {
1542 report.dropped_outside.push((text.clone(), outside));
1543 }
1544 let mut log = PassLog::default();
1545 let started = Instant::now();
1546 let mut run = Duration::ZERO;
1547 let ran = self.run_pass(w, p.as_ref(), &text, &mut log, &mut run);
1548 let time = StepTime {
1549 run,
1550 checks: started.elapsed().saturating_sub(run),
1551 };
1552 match ran {
1553 Ok(mut done) => {
1554 done.time = time;
1555 crate::convert::PartScope::declare(
1557 w,
1558 &done.log.placed,
1559 &done.log.placed_from,
1560 &owned_before,
1561 );
1562 hook(instrument.after_pass(&step, w, &done))?;
1563 report.steps.push(done);
1564 }
1565 Err(e) => {
1566 report.steps.push(StepReport {
1568 pass: text.clone(),
1569 approximates: p.approximates(),
1570 log,
1571 added: Vec::new(),
1572 removed: Vec::new(),
1573 conservation: Vec::new(),
1574 failed: Some(e.to_string()),
1575 skipped: Vec::new(),
1576 time,
1577 settings: Vec::new(),
1578 });
1579 let _ = instrument.pass_failed(&step, w, &e);
1581 return Err(e);
1582 }
1583 }
1584 }
1585 Ok(())
1586 }
1587
1588 fn run_pass(
1591 &self,
1592 w: &mut World,
1593 p: &dyn Pass,
1594 text: &str,
1595 log: &mut PassLog,
1596 run: &mut Duration,
1597 ) -> Result<StepReport> {
1598 for &r in p.requires() {
1599 if !w.has_component_named(r) && !w.resource_names().contains(&r) {
1600 return Err(Error::Missing {
1601 pass: text.to_owned(),
1602 name: r.to_owned(),
1603 });
1604 }
1605 }
1606 let settings = p
1607 .settings()
1608 .iter()
1609 .map(|s| {
1610 s.text(w).ok_or_else(|| Error::Missing {
1611 pass: text.to_owned(),
1612 name: s.name.to_owned(),
1613 })
1614 })
1615 .collect::<Result<Vec<_>>>()?;
1616 let before: BTreeSet<String> = w.component_names().into_iter().map(str::to_owned).collect();
1617 let resources_before: BTreeSet<String> = w.resource_names().into_iter().map(str::to_owned).collect();
1618 let holders: BTreeMap<&str, Vec<Entity>> = if p.despawns_entities() {
1620 before
1621 .iter()
1622 .filter(|n| !p.removes().contains(&n.as_str()))
1623 .map(|n| (n.as_str(), w.entities_named(n)))
1624 .collect()
1625 } else {
1626 BTreeMap::new()
1627 };
1628 let held_before: BTreeMap<&str, BTreeSet<Entity>> = if self.pass_checks {
1631 before
1632 .iter()
1633 .map(|n| (n.as_str(), w.entities_named(n).into_iter().collect()))
1634 .collect()
1635 } else {
1636 BTreeMap::new()
1637 };
1638 if self.pass_checks {
1639 w.start_effects();
1640 }
1641 let first_made = w.next_index();
1642 let ledger = (self.conservation != Conservation::Off).then(|| {
1643 w.start_rebuild_log();
1644 crate::ledger::Before::take(w)
1645 });
1646 let started = Instant::now();
1647 let ran = p.run(w, log);
1648 *run = started.elapsed();
1649 let rebuilds = w.take_rebuild_log();
1650 let mut effects = w.take_effects();
1651 effects.dropped = w.take_dropped();
1652 ran.map_err(|message| Error::Pass {
1653 pass: text.to_owned(),
1654 message,
1655 })?;
1656 count_dropped(log, &effects.dropped);
1657 let conservation = ledger.map_or_else(Vec::new, |b| b.gaps(w, &rebuilds, log));
1658 if self.conservation == Conservation::Enforce
1659 && let Some(g) = conservation.first()
1660 {
1661 return Err(Error::Conservation {
1662 pass: text.to_owned(),
1663 measure: g.measure,
1664 dropped: g.dropped,
1665 declared: g.declared,
1666 });
1667 }
1668 let made = first_made..w.next_index();
1672 let consumed = |e: &Entity| log.placed_from.values().any(|s| s == e);
1673 if let Some(e) = log
1674 .placed
1675 .iter()
1676 .find(|e| !made.contains(&e.index()) || (!w.is_alive(**e) && !consumed(e)))
1677 {
1678 return Err(Error::Pass {
1679 pass: text.to_owned(),
1680 message: if e.index() < first_made {
1681 format!("placed {e:?}, which it did not make")
1682 } else if e.index() >= made.end {
1683 format!("placed {e:?}, which was never made")
1684 } else {
1685 format!("placed {e:?}, which is not alive after it, and nothing it placed came from it")
1686 },
1687 });
1688 }
1689 if let Some((what, e)) = log
1691 .approximated_on
1692 .iter()
1693 .flat_map(|(what, entities)| entities.iter().map(move |e| (what, e)))
1694 .find(|(_, e)| !w.is_alive(**e))
1695 {
1696 return Err(Error::Pass {
1697 pass: text.to_owned(),
1698 message: format!("approximation {what:?} attributed to {e:?}, which is not alive after it"),
1699 });
1700 }
1701 if let Some((e, s)) = log.placed_from.iter().find(|(_, s)| s.index() >= made.end) {
1702 return Err(Error::Pass {
1703 pass: text.to_owned(),
1704 message: format!("placed {e:?} from {s:?}, which was never made"),
1705 });
1706 }
1707 let after: BTreeSet<String> = w.component_names().into_iter().map(str::to_owned).collect();
1708 let resources_after: BTreeSet<String> = w.resource_names().into_iter().map(str::to_owned).collect();
1709 let mut added: Vec<String> = after.difference(&before).cloned().collect();
1710 let mut removed: Vec<String> = before.difference(&after).cloned().collect();
1711 let despawned_with = |n: &str| {
1712 holders
1713 .get(n)
1714 .is_some_and(|held| held.iter().all(|&e| !w.is_alive(e)))
1715 };
1716 for (what, names, declared, removal) in [
1717 ("added", &added, p.provides(), false),
1718 ("removed", &removed, p.removes(), true),
1719 ] {
1720 let undeclared: Vec<String> = names
1721 .iter()
1722 .filter(|n| {
1723 !declared.contains(&n.as_str())
1724 && !(removal && despawned_with(n))
1725 && !(removal && !dropped_from(&effects.dropped, n).is_empty())
1726 && !(removal && p.removes_by_log() && log.removed.contains(n.as_str()))
1727 })
1728 .cloned()
1729 .collect();
1730 if !undeclared.is_empty() {
1731 return Err(Error::Undeclared {
1732 pass: text.to_owned(),
1733 what,
1734 names: undeclared,
1735 });
1736 }
1737 }
1738 for (what, names, declared) in [
1740 (
1741 "added",
1742 resources_after.difference(&resources_before),
1743 p.provides(),
1744 ),
1745 (
1746 "removed",
1747 resources_before.difference(&resources_after),
1748 p.removes(),
1749 ),
1750 ] {
1751 let names: Vec<String> = names.cloned().collect();
1752 let undeclared: Vec<String> = names
1753 .iter()
1754 .filter(|n| !declared.contains(&n.as_str()))
1755 .cloned()
1756 .collect();
1757 if !undeclared.is_empty() {
1758 return Err(Error::Undeclared {
1759 pass: text.to_owned(),
1760 what,
1761 names: undeclared,
1762 });
1763 }
1764 if what == "added" {
1765 added.extend(names);
1766 } else {
1767 removed.extend(names);
1768 }
1769 }
1770 added.sort();
1771 removed.sort();
1772 if self.pass_checks {
1773 self.check_entity_effects(w, p, text, log, &held_before, &effects)?;
1774 self.verify(w, text)?;
1775 }
1776 Ok(StepReport {
1777 pass: text.to_owned(),
1778 approximates: p.approximates(),
1779 log: std::mem::take(log),
1780 added,
1781 removed,
1782 conservation,
1783 failed: None,
1784 skipped: Vec::new(),
1785 time: StepTime::default(),
1786 settings,
1787 })
1788 }
1789
1790 fn check_entity_effects(
1798 &self,
1799 w: &World,
1800 p: &dyn Pass,
1801 text: &str,
1802 log: &PassLog,
1803 held_before: &BTreeMap<&str, BTreeSet<Entity>>,
1804 effects: &crate::world::Effects,
1805 ) -> Result<()> {
1806 let undeclared = |what: &'static str, names: Vec<String>| -> Result<()> {
1807 if names.is_empty() {
1808 Ok(())
1809 } else {
1810 Err(Error::Undeclared {
1811 pass: text.to_owned(),
1812 what,
1813 names,
1814 })
1815 }
1816 };
1817 let some = |n: &str, es: &[Entity]| format!("{n} ({} entities, the first {:?})", es.len(), es[0]);
1818 let copied = |e: Entity, name: &str| {
1819 effects.copies.iter().any(|&(from, to)| {
1820 to == e
1821 && (held_before.get(name).is_some_and(|h| h.contains(&from))
1822 || w.entity_has_named(from, name))
1823 })
1824 };
1825 let mut added = Vec::new();
1827 for name in w.component_names() {
1828 if p.provides().contains(&name) {
1829 continue;
1830 }
1831 let before = held_before.get(name);
1832 let new: Vec<Entity> = w
1833 .entities_named(name)
1834 .into_iter()
1835 .filter(|e| !before.is_some_and(|b| b.contains(e)) && !copied(*e, name))
1836 .collect();
1837 if !new.is_empty() {
1838 added.push(some(name, &new));
1839 }
1840 }
1841 undeclared("added to entities", added)?;
1842 let (mut taken, mut despawned) = (Vec::new(), BTreeSet::new());
1844 for (&name, before) in held_before {
1845 let declared = p.removes().contains(&name) || (p.removes_by_log() && log.removed.contains(name));
1846 let gone: Vec<Entity> = before
1847 .iter()
1848 .copied()
1849 .filter(|&e| !w.entity_has_named(e, name))
1850 .collect();
1851 let (mut alive, dead): (Vec<Entity>, Vec<Entity>) =
1852 gone.into_iter().partition(|&e| w.is_alive(e));
1853 let world_dropped = dropped_from(&effects.dropped, name);
1855 alive.retain(|e| !world_dropped.contains(e));
1856 if !declared && !alive.is_empty() {
1857 taken.push(some(name, &alive));
1858 }
1859 if !p.despawns_entities() {
1860 despawned.extend(dead);
1861 }
1862 }
1863 undeclared("taken from entities", taken)?;
1864 if let Some(first) = despawned.first() {
1865 return Err(Error::Pass {
1866 pass: text.to_owned(),
1867 message: format!(
1868 "despawned {} entities (the first {first:?}) without declaring it (Pass::despawns_entities)",
1869 despawned.len()
1870 ),
1871 });
1872 }
1873 let names = w.resource_names();
1875 let replaced = effects
1876 .resources_removed
1877 .iter()
1878 .filter(|r| names.iter().any(|n| n == *r));
1879 let written: Vec<String> = effects
1880 .resources_written
1881 .iter()
1882 .chain(replaced)
1883 .collect::<BTreeSet<_>>()
1884 .into_iter()
1885 .filter(|r| !p.modifies().contains(&r.as_str()) && !p.provides().contains(&r.as_str()))
1886 .cloned()
1887 .collect();
1888 undeclared("written (resources)", written)
1889 }
1890
1891 pub fn convert(
1897 &self,
1898 w: &mut World,
1899 pipeline: &Pipeline,
1900 target: &Target,
1901 mode: Mode,
1902 ) -> Result<RunReport> {
1903 self.convert_instrumented(w, pipeline, target, mode, &mut NoInstrument)
1904 }
1905
1906 pub fn convert_instrumented(
1908 &self,
1909 w: &mut World,
1910 pipeline: &Pipeline,
1911 target: &Target,
1912 mode: Mode,
1913 instrument: &mut dyn Instrument,
1914 ) -> Result<RunReport> {
1915 self.verify(w, "the reader")?;
1916 let mut report = RunReport {
1917 target: target.name.clone(),
1918 pipeline: pipeline.to_string(),
1919 ..RunReport::default()
1920 };
1921 self.run_instrumented(w, pipeline, &mut report, instrument)?;
1922 let refused = w
1923 .component_names()
1924 .into_iter()
1925 .map(|name| (name.to_owned(), target.illegal_entities(w, name, mode)))
1926 .find(|(_, bad)| !bad.is_empty())
1927 .or_else(|| {
1928 target
1929 .refused_resources(w)
1930 .first()
1931 .map(|n| ((*n).to_owned(), Vec::new()))
1932 });
1933 if let Some((name, bad)) = refused {
1934 let lowered_by = pipeline
1935 .passes
1936 .iter()
1937 .filter(|p| p.removes().contains(&name.as_str()) || p.modifies().contains(&name.as_str()))
1938 .map(|p| pass_text(p.as_ref()))
1939 .collect();
1940 return Err(Error::Illegal {
1941 target: target.name.clone(),
1942 rule: target.rule_words(&name).unwrap_or_default().to_owned(),
1943 name,
1944 entities: bad.into_iter().take(10).collect(),
1945 lowered_by,
1946 });
1947 }
1948 self.verify(w, &format!("the pipeline, before {}", target.name))?;
1949 Ok(report)
1950 }
1951}
1952
1953#[cfg(test)]
1954mod tests {
1955 use super::*;
1956 use crate::world::{Opaque, OpaqueData};
1957
1958 #[test]
1961 fn what_the_text_form_carries() {
1962 for carried in ["", "512", "-64,-64,64,64", "3:2,4:1", "a:x;b:y", "k=1:v"] {
1963 assert!(carries(carried), "{carried:?}");
1964 }
1965 for lost in ["a:x,", "a:1,;b=c:2", "a:1,b=2", "x{y}", " a", "a ", "a, b"] {
1966 assert!(!carries(lost), "{lost:?}");
1967 }
1968 }
1969
1970 #[test]
1971 fn option_values_with_commas_parse_as_display_writes_them() {
1972 let parsed = parse_pipeline("a{rect=-64,-64,64,64,depth=3:2,4:1},b{by=1,2,3},c").unwrap();
1973 let opts = |pairs: &[(&str, &str)]| -> Options {
1974 pairs
1975 .iter()
1976 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
1977 .collect()
1978 };
1979 assert_eq!(
1980 parsed,
1981 vec![
1982 (
1983 "a".to_owned(),
1984 opts(&[("rect", "-64,-64,64,64"), ("depth", "3:2,4:1")])
1985 ),
1986 ("b".to_owned(), opts(&[("by", "1,2,3")])),
1987 ("c".to_owned(), Options::new()),
1988 ]
1989 );
1990 assert!(parse_pipeline("a{-64,x=1}").is_err());
1991 }
1992
1993 #[derive(Clone, Debug, PartialEq)]
1995 struct File(u32);
1996 impl Component for File {
1997 const NAME: &'static str = "toy.file";
1998 const GEOMETRY: crate::Geometry = crate::Geometry::None;
1999 }
2000 #[derive(Clone, Debug, PartialEq)]
2001 struct Mesh(u32);
2002 impl Component for Mesh {
2003 const NAME: &'static str = "toy.mesh";
2004 const GEOMETRY: crate::Geometry = crate::Geometry::None;
2005 fn verify(&self, _e: Entity, _w: &World) -> std::result::Result<(), String> {
2006 if self.0 == 13 {
2007 Err("unlucky mesh".into())
2008 } else {
2009 Ok(())
2010 }
2011 }
2012 }
2013 #[derive(Clone, Debug, PartialEq)]
2014 struct Layered(u32);
2015 impl Component for Layered {
2016 const NAME: &'static str = "toy.layered";
2017 const GEOMETRY: crate::Geometry = crate::Geometry::None;
2018 }
2019 #[derive(Clone, Debug, PartialEq)]
2020 struct Pbr(u32);
2021 impl Component for Pbr {
2022 const NAME: &'static str = "toy.pbr";
2023 const GEOMETRY: crate::Geometry = crate::Geometry::None;
2024 }
2025
2026 struct Load;
2028 impl Pass for Load {
2029 fn name(&self) -> &'static str {
2030 "load"
2031 }
2032 fn provides(&self) -> &[&'static str] {
2033 &["toy.mesh"]
2034 }
2035 fn removes(&self) -> &[&'static str] {
2036 &["toy.file"]
2037 }
2038 fn requires(&self) -> &[&'static str] {
2039 &["toy.file"]
2040 }
2041 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2042 let files: Vec<(Entity, u32)> = w.iter::<File>().map(|(e, f)| (e, f.0)).collect();
2043 for (e, n) in files {
2044 w.remove::<File>(e);
2045 w.insert(e, Mesh(n));
2046 log.count("loaded", 1);
2047 }
2048 Ok(())
2049 }
2050 }
2051 const LOAD: PassSpec = PassSpec {
2052 name: "load",
2053 options: &[],
2054 make: |_| Ok(Box::new(Load)),
2055 };
2056
2057 struct Bake {
2059 size: u32,
2060 }
2061 impl Pass for Bake {
2062 fn name(&self) -> &'static str {
2063 "bake"
2064 }
2065 fn options(&self) -> Options {
2066 if self.size == 1024 {
2067 Options::new()
2068 } else {
2069 Options::from([("size".to_owned(), self.size.to_string())])
2070 }
2071 }
2072 fn requires(&self) -> &[&'static str] {
2073 &["toy.mesh", "toy.layered"]
2074 }
2075 fn provides(&self) -> &[&'static str] {
2076 &["toy.pbr"]
2077 }
2078 fn removes(&self) -> &[&'static str] {
2079 &["toy.layered"]
2080 }
2081 fn approximates(&self) -> &'static str {
2082 "layer blend baked to one texture"
2083 }
2084 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2085 let ls: Vec<(Entity, u32)> = w.iter::<Layered>().map(|(e, l)| (e, l.0)).collect();
2086 for (e, n) in ls {
2087 w.remove::<Layered>(e);
2088 w.insert(e, Pbr(n));
2089 log.approximate("baked", 1);
2090 }
2091 Ok(())
2092 }
2093 }
2094 const BAKE: PassSpec = PassSpec {
2095 name: "bake",
2096 options: &[OptionSpec::optional(
2097 "size",
2098 "512",
2099 "texels: the baked texture's size",
2100 "1024",
2101 )],
2102 make: |g| {
2103 let size = match g.optional("size") {
2104 Some(v) => v.parse().map_err(|_| format!("size {v:?} is not a number"))?,
2105 None => 1024,
2106 };
2107 Ok(Box::new(Bake { size }))
2108 },
2109 };
2110
2111 struct Sneaky;
2113 impl Pass for Sneaky {
2114 fn name(&self) -> &'static str {
2115 "sneaky"
2116 }
2117 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2118 let e = w.spawn();
2119 w.insert(e, Pbr(1));
2120 Ok(())
2121 }
2122 }
2123 const SNEAKY: PassSpec = PassSpec {
2124 name: "sneaky",
2125 options: &[],
2126 make: |_| Ok(Box::new(Sneaky)),
2127 };
2128
2129 fn registry() -> PassRegistry {
2130 let mut r = PassRegistry::new();
2131 r.add(&LOAD);
2132 r.add(&BAKE);
2133 r.add(&SNEAKY);
2134 r
2135 }
2136
2137 fn world() -> World {
2138 let mut w = World::new();
2139 let a = w.spawn();
2140 w.insert(a, File(1));
2141 w.insert(a, Layered(5));
2142 w.insert_opaque(
2143 a,
2144 "game.thing",
2145 Opaque {
2146 version: 1,
2147 data: OpaqueData::Bytes(vec![]),
2148 },
2149 )
2150 .unwrap();
2151 w
2152 }
2153
2154 fn gltf() -> Target {
2155 Target::new("gltf")
2156 .legal("toy.mesh")
2157 .legal("toy.pbr")
2158 .illegal("toy.file")
2159 .illegal("toy.layered")
2160 }
2161
2162 struct Slow(u64);
2164 impl Pass for Slow {
2165 fn name(&self) -> &'static str {
2166 "slow"
2167 }
2168 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2169 std::thread::sleep(Duration::from_millis(self.0));
2170 Ok(())
2171 }
2172 }
2173
2174 #[test]
2177 fn unfed_passes_are_named_with_what_they_lack_and_the_names_held_after() {
2178 let names = |n: &[&str]| n.iter().map(|s| (*s).to_owned()).collect::<BTreeSet<String>>();
2179 let load_then_bake = || Pipeline {
2180 passes: vec![Box::new(Load), Box::new(Bake { size: 1024 })],
2181 };
2182 assert_eq!(
2184 load_then_bake().unfed(&names(&["toy.file", "toy.layered"])),
2185 Unfed {
2186 passes: Vec::new(),
2187 held: names(&["toy.file", "toy.layered", "toy.mesh", "toy.pbr"]),
2188 }
2189 );
2190 assert_eq!(
2192 load_then_bake().unfed(&names(&["toy.layered"])),
2193 Unfed {
2194 passes: vec![
2195 ("load".to_owned(), vec!["toy.file"]),
2196 ("bake".to_owned(), vec!["toy.mesh"])
2197 ],
2198 held: names(&["toy.layered"]),
2199 }
2200 );
2201 struct Either;
2204 impl Pass for Either {
2205 fn name(&self) -> &'static str {
2206 "either"
2207 }
2208 fn requires(&self) -> &[&'static str] {
2209 &["toy.mesh", "toy.pbr"]
2210 }
2211 fn provides(&self) -> &[&'static str] {
2212 &["toy.lit"]
2213 }
2214 fn may_lack_inputs(&self) -> bool {
2215 true
2216 }
2217 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2218 Ok(())
2219 }
2220 }
2221 let either = || Pipeline {
2222 passes: vec![Box::new(Either)],
2223 };
2224 assert_eq!(
2225 either().unfed(&names(&[])),
2226 Unfed {
2227 passes: Vec::new(),
2228 held: names(&[]),
2229 }
2230 );
2231 assert_eq!(
2232 either().unfed(&names(&["toy.mesh"])).passes,
2233 [("either".to_owned(), vec!["toy.pbr"])]
2234 );
2235 assert_eq!(
2236 either().unfed(&names(&["toy.mesh", "toy.pbr"])),
2237 Unfed {
2238 passes: Vec::new(),
2239 held: names(&["toy.lit", "toy.mesh", "toy.pbr"]),
2240 }
2241 );
2242 }
2243
2244 #[test]
2245 fn optional_preparation_reports_missing_inputs_and_uses_actual_preceding_outputs() {
2246 struct NeedsBoth;
2247 impl Pass for NeedsBoth {
2248 fn name(&self) -> &'static str {
2249 "needs-both"
2250 }
2251 fn requires(&self) -> &[&'static str] {
2252 &["toy.mesh", "toy.pbr"]
2253 }
2254 fn run(&self, _: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2255 log.count("ran with both inputs", 1);
2256 Ok(())
2257 }
2258 }
2259 let pipeline = || Pipeline {
2260 passes: vec![
2261 Box::new(NeedsBoth),
2262 Box::new(Load),
2263 Box::new(NeedsBoth),
2264 Box::new(Bake { size: 1024 }),
2265 Box::new(NeedsBoth),
2266 ],
2267 };
2268 let mut input = world();
2269 let mut report = RunReport::default();
2270 PassManager::new(PassRegistry::new())
2271 .with_missing_inputs(MissingInputs::Skip)
2272 .run(&mut input, &pipeline(), &mut report)
2273 .unwrap();
2274 assert_eq!(report.steps.len(), 5);
2275 assert_eq!(report.steps[0].skipped, ["toy.mesh", "toy.pbr"]);
2276 assert_eq!(report.steps[2].skipped, ["toy.pbr"]);
2277 assert_eq!(report.steps[2].log.counts[SKIPPED_UNFED], 1);
2278 assert!(report.steps[0].added.is_empty() && report.steps[2].added.is_empty());
2279 assert!(report.steps[4].skipped.is_empty());
2280 assert_eq!(report.steps[4].log.counts["ran with both inputs"], 1);
2281 assert!(input.has_component_named("toy.pbr"));
2282 let mut strict_report = RunReport::default();
2283 assert!(matches!(
2284 PassManager::new(PassRegistry::new()).run(&mut world(), &pipeline(), &mut strict_report),
2285 Err(Error::Missing { .. })
2286 ));
2287 assert!(strict_report.steps[0].skipped.is_empty());
2288 }
2289
2290 #[test]
2291 fn optional_preparation_does_not_hide_a_fed_pass_failure() {
2292 struct Fails;
2293 impl Pass for Fails {
2294 fn name(&self) -> &'static str {
2295 "fails"
2296 }
2297 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2298 Err("source data is malformed".into())
2299 }
2300 }
2301 let mut report = RunReport::default();
2302 let result = PassManager::new(PassRegistry::new())
2303 .with_missing_inputs(MissingInputs::Skip)
2304 .run(
2305 &mut world(),
2306 &Pipeline {
2307 passes: vec![Box::new(Fails)],
2308 },
2309 &mut report,
2310 );
2311 assert!(result.is_err());
2312 assert!(
2313 report.steps[0]
2314 .failed
2315 .as_ref()
2316 .unwrap()
2317 .contains("source data is malformed")
2318 );
2319 assert!(report.steps[0].skipped.is_empty());
2320 }
2321
2322 #[test]
2325 fn steps_are_timed_around_their_runs() {
2326 let pipeline = Pipeline {
2327 passes: vec![Box::new(Slow(30)), Box::new(Load)],
2328 };
2329 let mut report = RunReport::default();
2330 PassManager::new(PassRegistry::new())
2331 .with_pass_checks(true)
2332 .run(&mut world(), &pipeline, &mut report)
2333 .unwrap();
2334 let [slow, load] = [&report.steps[0].time, &report.steps[1].time];
2335 assert!(slow.run >= Duration::from_millis(30), "{slow:?}");
2336 assert!(load.run < slow.run, "{load:?} {slow:?}");
2337 assert_eq!(report.time().run, slow.run + load.run);
2338 assert_eq!(report.time().checks, slow.checks + load.checks);
2339 let text = report.to_string();
2340 let line = text
2341 .lines()
2342 .find(|l| l.trim_start().starts_with("slow:"))
2343 .unwrap();
2344 assert!(
2345 line.ends_with(&format!(
2346 "({:.3} s, checks {:.3} s)",
2347 slow.run.as_secs_f64(),
2348 slow.checks.as_secs_f64()
2349 )),
2350 "{line}"
2351 );
2352
2353 struct Skippable;
2354 impl Pass for Skippable {
2355 fn name(&self) -> &'static str {
2356 "skippable"
2357 }
2358 fn requires(&self) -> &[&'static str] {
2359 &["toy.pbr"]
2360 }
2361 fn may_lack_inputs(&self) -> bool {
2362 true
2363 }
2364 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2365 Ok(())
2366 }
2367 }
2368 let mut report = RunReport::default();
2369 PassManager::new(PassRegistry::new())
2370 .run_part(
2371 &mut world(),
2372 &Pipeline {
2373 passes: vec![Box::new(Skippable)],
2374 },
2375 &mut report,
2376 )
2377 .unwrap();
2378 assert_eq!(report.steps[0].time, StepTime::default());
2379 assert!(!report.to_string().contains(" s, checks "));
2380 let mut later = report.clone();
2382 later.steps[0].time.run = Duration::from_secs(1);
2383 assert_eq!(later, report);
2384 assert_ne!(later.steps[0].time, report.steps[0].time);
2385 later.steps[0].pass = "other".into();
2386 assert_ne!(later, report);
2387 }
2388
2389 #[test]
2391 fn a_targets_rules_read_back() {
2392 let t = Target::new("t").legal("toy.pbr").illegal("toy.layered");
2393 assert_eq!(
2394 t.rules(),
2395 [("toy.layered", Rule::Illegal), ("toy.pbr", Rule::Legal)]
2396 );
2397 assert!(t.checked_resources().is_empty());
2398 }
2399
2400 #[test]
2403 fn absorb_keeps_every_part_of_the_log() {
2404 let mut w = World::new();
2405 let (a, b, source) = (w.spawn(), w.spawn(), w.spawn());
2406 let mut log = PassLog::default();
2407 log.count("kept", 1);
2408 log.approximate("rounded", 2);
2409 log.place(a);
2410 log.detail("explanation", serde_json::json!({"input": 1, "why": "first"}));
2411 let mut other = PassLog::default();
2412 other.count("kept", 3);
2413 other.count("made", 1);
2414 other.approximate("rounded", 1);
2415 other.omit("no form", 4);
2416 other.record_removed("toy.hat");
2417 other.place_from(b, source);
2418 other.dropped(Measure::Placements, "left out", 2);
2419 other.detail("explanation", serde_json::json!({"input": 2, "why": "second"}));
2420 other.detail("additional", serde_json::json!(["nested", {"result": true}]));
2421 log.dropped(Measure::Placements, "left out", 1);
2422 log.absorb(other);
2423 assert_eq!(
2424 log.counts,
2425 BTreeMap::from([("kept".to_owned(), 4), ("made".to_owned(), 1)])
2426 );
2427 assert_eq!(log.approximations, BTreeMap::from([("rounded".to_owned(), 3)]));
2428 assert_eq!(log.omitted, BTreeMap::from([("no form".to_owned(), 4)]));
2429 assert_eq!(log.removed, BTreeSet::from(["toy.hat".to_owned()]));
2430 assert_eq!(log.placed, BTreeSet::from([a, b]));
2431 assert_eq!(log.placed_from, BTreeMap::from([(b, source)]));
2432 assert_eq!(log.declared(Measure::Placements), 3);
2433 assert_eq!(
2434 log.details["explanation"],
2435 [
2436 serde_json::json!({"input": 1, "why": "first"}),
2437 serde_json::json!({"input": 2, "why": "second"}),
2438 ]
2439 );
2440 assert_eq!(
2441 log.details["additional"],
2442 [serde_json::json!(["nested", {"result": true}])]
2443 );
2444 }
2445
2446 #[test]
2447 fn pipeline_text_round_trips() {
2448 let r = registry();
2449 let p = r.parse(" load , bake{ size = 512 } ,bake").unwrap();
2450 assert_eq!(p.to_string(), "load, bake{size=512}, bake");
2451 assert_eq!(r.parse(&p.to_string()).unwrap().to_string(), p.to_string());
2452 assert_eq!(r.parse("").unwrap().passes.len(), 0);
2453 assert!(matches!(r.parse("load,,bake"), Err(Error::Parse(_))));
2454 assert!(matches!(r.parse("bake{size=512"), Err(Error::Parse(_))));
2455 assert!(matches!(r.parse("nope"), Err(Error::UnknownPass(n)) if n == "nope"));
2456 assert!(matches!(r.parse("bake{size=big}"), Err(Error::Options { .. })));
2457 assert!(matches!(r.parse("bake{colour=red}"), Err(Error::Options { .. })));
2458 }
2459
2460 #[test]
2463 fn a_conversion_runs_the_stated_pipeline() {
2464 let m = PassManager::new(registry());
2465 let mut w = world();
2466 let pipeline = m.registry.parse("load, bake").unwrap();
2467 let report = m.convert(&mut w, &pipeline, &gltf(), Mode::Partial).unwrap();
2468 assert_eq!(report.target, "gltf");
2469 assert_eq!(report.pipeline, "load, bake");
2470 assert_eq!(
2471 report.steps[1].log.approximations,
2472 BTreeMap::from([("baked".to_owned(), 1)])
2473 );
2474 assert_eq!(report.steps[1].approximates, "layer blend baked to one texture");
2475 assert_eq!(
2476 w.component_names(),
2477 ["game.thing", "toy.mesh", "toy.pbr"],
2478 "partial keeps game.thing"
2479 );
2480 assert!(
2481 report
2482 .to_string()
2483 .starts_with("target gltf: pipeline [load, bake]\n")
2484 );
2485 }
2486
2487 #[test]
2490 fn a_refused_name_says_the_stated_pipeline_does_not_lower_it() {
2491 let m = PassManager::new(registry());
2492 let pipeline = m.registry.parse("load, bake").unwrap();
2493 let err = m
2495 .convert(&mut world(), &pipeline, &gltf(), Mode::Full)
2496 .unwrap_err();
2497 assert_eq!(
2498 err.to_string(),
2499 "`gltf` does not accept `game.thing` on [e0] after the pipeline: nothing in the stated pipeline removes or \
2500 rewrites it"
2501 );
2502 let accepting = gltf().legal("game.thing");
2504 m.convert(&mut world(), &pipeline, &accepting, Mode::Full)
2505 .unwrap();
2506 let mut w = world();
2508 let err = m
2509 .convert(&mut w, &m.registry.parse("load").unwrap(), &gltf(), Mode::Partial)
2510 .unwrap_err();
2511 assert!(
2512 matches!(&err, Error::Illegal { name, lowered_by, .. } if name == "toy.layered" && lowered_by.is_empty()),
2513 "{err}"
2514 );
2515 assert!(
2516 w.has_component_named("toy.layered"),
2517 "nothing was added to lower it"
2518 );
2519 }
2520
2521 #[test]
2522 fn dynamic_legality_refuses_only_failing_values() {
2523 let m = PassManager::new(registry());
2524 let t = Target::new("skate")
2525 .legal("toy.mesh")
2526 .legal("toy.layered")
2527 .legal("game.thing")
2528 .dynamic_typed::<File>(|f| f.0 < 10);
2529 let none = Pipeline::default();
2530 let mut w = world(); let report = m.convert(&mut w, &none, &t, Mode::Full).unwrap();
2532 assert_eq!(report.pipeline, "");
2533 assert!(w.has_component_named("toy.file"));
2534 let failing = || {
2535 let mut w = world();
2536 let e = w.entities().next().unwrap();
2537 w.insert(e, File(99)); w
2539 };
2540 let err = m.convert(&mut failing(), &none, &t, Mode::Full).unwrap_err();
2541 assert!(
2542 matches!(&err, Error::Illegal { name, .. } if name == "toy.file"),
2543 "{err}"
2544 );
2545 let load = m.registry.parse("load").unwrap();
2547 let report = m.convert(&mut failing(), &load, &t, Mode::Full).unwrap();
2548 assert_eq!(report.pipeline, "load");
2549 }
2550
2551 #[test]
2552 fn undeclared_changes_and_verifier_name_the_pass() {
2553 let m = PassManager::new(registry()).with_pass_checks(true);
2554 let mut w = world();
2555 let p = m.registry.parse("sneaky").unwrap();
2556 let err = m.run(&mut w, &p, &mut RunReport::default()).unwrap_err();
2557 assert_eq!(
2558 err.to_string(),
2559 "pass sneaky added `[\"toy.pbr\"]` without declaring it"
2560 );
2561
2562 let mut w = world();
2563 let e = w.entities().next().unwrap();
2564 w.insert(e, File(13)); let p = m.registry.parse("load").unwrap();
2566 let err = m.run(&mut w, &p, &mut RunReport::default()).unwrap_err();
2567 assert_eq!(err.to_string(), "verify after load: toy.mesh on e0: unlucky mesh");
2568
2569 let p = m.registry.parse("bake").unwrap();
2570 let err = m.run(&mut world(), &p, &mut RunReport::default()).unwrap_err();
2571 assert_eq!(
2572 err.to_string(),
2573 "pass bake requires `toy.mesh`, which the world does not have"
2574 );
2575 }
2576
2577 struct Cull {
2579 opted: bool,
2580 strip: bool,
2581 }
2582 impl Pass for Cull {
2583 fn name(&self) -> &'static str {
2584 "cull"
2585 }
2586 fn despawns_entities(&self) -> bool {
2587 self.opted
2588 }
2589 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2590 for e in w.iter::<Layered>().map(|(e, _)| e).collect::<Vec<_>>() {
2591 w.despawn(e);
2592 log.count("culled", 1);
2593 }
2594 if self.strip {
2595 for e in w.iter::<File>().map(|(e, _)| e).collect::<Vec<_>>() {
2596 w.remove::<File>(e);
2597 }
2598 }
2599 Ok(())
2600 }
2601 }
2602
2603 struct LogStrip {
2606 opted: bool,
2607 record: bool,
2608 }
2609 impl Pass for LogStrip {
2610 fn name(&self) -> &'static str {
2611 "log-strip"
2612 }
2613 fn removes_by_log(&self) -> bool {
2614 self.opted
2615 }
2616 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2617 for e in w.iter::<Layered>().map(|(e, _)| e).collect::<Vec<_>>() {
2618 w.remove::<Layered>(e);
2619 }
2620 if self.record {
2621 log.record_removed("toy.layered");
2622 log.record_removed("toy.file");
2624 }
2625 Ok(())
2626 }
2627 }
2628
2629 #[test]
2630 fn a_pass_removing_by_log_may_take_the_names_it_recorded() {
2631 let run = |opted, record| {
2632 let pipeline = Pipeline {
2633 passes: vec![Box::new(LogStrip { opted, record })],
2634 };
2635 PassManager::new(PassRegistry::new()).run(&mut world(), &pipeline, &mut RunReport::default())
2636 };
2637 let undeclared = "pass log-strip removed `[\"toy.layered\"]` without declaring it";
2638 assert!(run(true, true).is_ok());
2639 assert_eq!(run(true, false).unwrap_err().to_string(), undeclared);
2641 assert_eq!(run(false, true).unwrap_err().to_string(), undeclared);
2642 }
2643
2644 struct Placer {
2646 which: &'static str,
2647 }
2648 impl Pass for Placer {
2649 fn name(&self) -> &'static str {
2650 "placer"
2651 }
2652 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2653 let e = match self.which {
2654 "old" => w.entities().next().expect("an entity"),
2655 _ => w.spawn(),
2656 };
2657 if self.which == "gone" {
2658 w.despawn(e);
2659 }
2660 log.place(e);
2661 if self.which == "consumed" {
2663 let derived = w.spawn();
2664 log.place_from(derived, e);
2665 w.despawn(e);
2666 }
2667 Ok(())
2668 }
2669 }
2670
2671 #[test]
2672 fn a_pass_places_only_what_it_made_and_what_is_alive() {
2673 let run = |which| {
2674 let pipeline = Pipeline {
2675 passes: vec![Box::new(Placer { which })],
2676 };
2677 let mut report = RunReport::default();
2678 PassManager::new(PassRegistry::new())
2679 .run(&mut world(), &pipeline, &mut report)
2680 .map(|()| report.steps[0].log.placed.len())
2681 .map_err(|e| e.to_string())
2682 };
2683 assert_eq!(run("new"), Ok(1));
2684 assert!(run("old").unwrap_err().contains("which it did not make"));
2685 assert!(run("gone").unwrap_err().contains("which is not alive after it"));
2686 assert_eq!(run("consumed"), Ok(2));
2688 }
2689
2690 struct Attributer {
2692 which: &'static str,
2693 }
2694 impl Pass for Attributer {
2695 fn name(&self) -> &'static str {
2696 "attributer"
2697 }
2698 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
2699 let e = w.spawn();
2700 if self.which == "gone" {
2701 w.despawn(e);
2702 }
2703 log.approximate_on(e, "base albedo could not be read", 1);
2704 Ok(())
2705 }
2706 }
2707
2708 #[test]
2709 fn an_approximation_is_attributed_to_a_live_entity() {
2710 let run = |which| {
2711 let pipeline = Pipeline {
2712 passes: vec![Box::new(Attributer { which })],
2713 };
2714 let mut report = RunReport::default();
2715 PassManager::new(PassRegistry::new())
2716 .run(&mut world(), &pipeline, &mut report)
2717 .map(|()| report.steps[0].log.clone())
2718 .map_err(|e| e.to_string())
2719 };
2720 let log = run("live").expect("a live entity is accepted");
2721 assert_eq!(log.approximations["base albedo could not be read"], 1);
2722 assert_eq!(log.approximated_on["base albedo could not be read"].len(), 1);
2723 assert!(run("gone").unwrap_err().contains("attributed to"));
2724 }
2725
2726 #[test]
2728 fn absorb_keeps_the_entities_an_approximation_was_attributed_to() {
2729 let mut w = World::new();
2730 let (a, b) = (w.spawn(), w.spawn());
2731 let mut log = PassLog::default();
2732 log.approximate_on(a, "unread", 1);
2733 log.approximate("rounded", 2);
2734 let mut other = PassLog::default();
2735 other.approximate_on(b, "unread", 3);
2736 log.absorb(other);
2737 assert_eq!(
2738 log.approximations,
2739 BTreeMap::from([("rounded".to_owned(), 2), ("unread".to_owned(), 4)])
2740 );
2741 assert_eq!(
2742 log.approximated_on,
2743 BTreeMap::from([("unread".to_owned(), BTreeSet::from([a, b]))])
2744 );
2745 }
2746
2747 fn cull_world() -> World {
2749 let mut w = world();
2750 let kept = w.spawn();
2751 w.insert(kept, Mesh(2));
2752 w.insert(kept, File(2));
2753 w
2754 }
2755
2756 fn cull(opted: bool, strip: bool, w: &mut World) -> Result<RunReport> {
2757 let mut report = RunReport::default();
2758 let pipeline = Pipeline {
2759 passes: vec![Box::new(Cull { opted, strip })],
2760 };
2761 PassManager::new(PassRegistry::new()).run(w, &pipeline, &mut report)?;
2762 Ok(report)
2763 }
2764
2765 #[test]
2766 fn a_despawning_pass_may_take_names_only_despawned_entities_held() {
2767 let err = cull(false, false, &mut cull_world()).unwrap_err();
2769 assert_eq!(
2770 err.to_string(),
2771 "pass cull removed `[\"game.thing\", \"toy.layered\"]` without declaring it"
2772 );
2773 let mut w = cull_world();
2775 let report = cull(true, false, &mut w).unwrap();
2776 assert_eq!(report.steps[0].removed, ["game.thing", "toy.layered"]);
2777 assert_eq!(w.component_names(), ["toy.file", "toy.mesh"]);
2778 let err = cull(true, true, &mut cull_world()).unwrap_err();
2780 assert_eq!(
2781 err.to_string(),
2782 "pass cull removed `[\"toy.file\"]` without declaring it"
2783 );
2784 struct Adds;
2786 impl Pass for Adds {
2787 fn name(&self) -> &'static str {
2788 "adds"
2789 }
2790 fn despawns_entities(&self) -> bool {
2791 true
2792 }
2793 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2794 let e = w.spawn();
2795 w.insert(e, Pbr(1));
2796 Ok(())
2797 }
2798 }
2799 let pipeline = Pipeline {
2800 passes: vec![Box::new(Adds)],
2801 };
2802 let err = PassManager::new(PassRegistry::new())
2803 .run(&mut world(), &pipeline, &mut RunReport::default())
2804 .unwrap_err();
2805 assert!(matches!(err, Error::Undeclared { what: "added", .. }), "{err}");
2806 }
2807
2808 #[derive(Clone, Debug)]
2809 struct Env;
2810 impl crate::world::Resource for Env {
2811 const NAME: &'static str = "toy.env";
2812 }
2813
2814 struct EnvPass {
2816 add: bool,
2817 declared: bool,
2818 despawns: bool,
2819 }
2820 impl Pass for EnvPass {
2821 fn name(&self) -> &'static str {
2822 "env"
2823 }
2824 fn provides(&self) -> &[&'static str] {
2825 if self.add && self.declared {
2826 &["toy.env"]
2827 } else {
2828 &[]
2829 }
2830 }
2831 fn removes(&self) -> &[&'static str] {
2832 if !self.add && self.declared {
2833 &["toy.env"]
2834 } else {
2835 &[]
2836 }
2837 }
2838 fn despawns_entities(&self) -> bool {
2839 self.despawns
2840 }
2841 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2842 if self.add {
2843 w.insert_resource(Env);
2844 } else {
2845 w.remove_resource::<Env>();
2846 }
2847 Ok(())
2848 }
2849 }
2850
2851 fn env_run(pass: EnvPass, w: &mut World) -> Result<RunReport> {
2852 let mut report = RunReport::default();
2853 PassManager::new(PassRegistry::new()).with_pass_checks(true).run(
2854 w,
2855 &Pipeline {
2856 passes: vec![Box::new(pass)],
2857 },
2858 &mut report,
2859 )?;
2860 Ok(report)
2861 }
2862
2863 #[test]
2864 fn resources_are_checked_by_the_same_declarations() {
2865 let err = env_run(
2867 EnvPass {
2868 add: true,
2869 declared: false,
2870 despawns: false,
2871 },
2872 &mut world(),
2873 )
2874 .unwrap_err();
2875 assert_eq!(
2876 err.to_string(),
2877 "pass env added `[\"toy.env\"]` without declaring it"
2878 );
2879 let mut w = world();
2881 let report = env_run(
2882 EnvPass {
2883 add: true,
2884 declared: true,
2885 despawns: false,
2886 },
2887 &mut w,
2888 )
2889 .unwrap();
2890 assert_eq!(report.steps[0].added, ["toy.env"]);
2891 let err = env_run(
2893 EnvPass {
2894 add: true,
2895 declared: false,
2896 despawns: false,
2897 },
2898 &mut w,
2899 )
2900 .unwrap_err();
2901 assert_eq!(
2902 err.to_string(),
2903 "pass env written (resources) `[\"toy.env\"]` without declaring it"
2904 );
2905 let report = env_run(
2906 EnvPass {
2907 add: true,
2908 declared: true,
2909 despawns: false,
2910 },
2911 &mut w,
2912 )
2913 .unwrap();
2914 assert!(report.steps[0].added.is_empty());
2915 let err = env_run(
2917 EnvPass {
2918 add: false,
2919 declared: false,
2920 despawns: true,
2921 },
2922 &mut w,
2923 )
2924 .unwrap_err();
2925 assert_eq!(
2926 err.to_string(),
2927 "pass env removed `[\"toy.env\"]` without declaring it"
2928 );
2929 w.insert_resource(Env);
2931 let report = env_run(
2932 EnvPass {
2933 add: false,
2934 declared: true,
2935 despawns: false,
2936 },
2937 &mut w,
2938 )
2939 .unwrap();
2940 assert_eq!(report.steps[0].removed, ["toy.env"]);
2941 struct NeedsEnv;
2943 impl Pass for NeedsEnv {
2944 fn name(&self) -> &'static str {
2945 "needs-env"
2946 }
2947 fn requires(&self) -> &[&'static str] {
2948 &["toy.env"]
2949 }
2950 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2951 Ok(())
2952 }
2953 }
2954 let needs = || Pipeline {
2955 passes: vec![Box::new(NeedsEnv)],
2956 };
2957 let m = PassManager::new(PassRegistry::new());
2958 assert!(matches!(
2959 m.run(&mut world(), &needs(), &mut RunReport::default()),
2960 Err(Error::Missing { .. })
2961 ));
2962 let mut w = world();
2963 w.insert_resource(Env);
2964 m.run(&mut w, &needs(), &mut RunReport::default()).unwrap();
2965 }
2966
2967 struct Evens;
2969 impl Pass for Evens {
2970 fn name(&self) -> &'static str {
2971 "evens"
2972 }
2973 fn requires(&self) -> &[&'static str] {
2974 &["toy.mesh"]
2975 }
2976 fn modifies(&self) -> &[&'static str] {
2977 &["toy.mesh"]
2978 }
2979 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
2980 if let Some(mut c) = w.column_mut::<Mesh>() {
2981 for (_, m) in c.iter_mut() {
2982 m.0 += m.0 % 2;
2983 }
2984 }
2985 Ok(())
2986 }
2987 }
2988 const EVENS: PassSpec = PassSpec {
2989 name: "evens",
2990 options: &[],
2991 make: |_| Ok(Box::new(Evens)),
2992 };
2993
2994 fn mesh_world(n: u32) -> World {
2995 let mut w = World::new();
2996 let e = w.spawn();
2997 w.insert(e, Mesh(n));
2998 w
2999 }
3000
3001 #[test]
3002 fn a_stated_pass_that_rewrites_in_place_fixes_failing_values() {
3003 let even = Target::new("even").dynamic_typed::<Mesh>(|m| m.0 % 2 == 0);
3004 let mut r = PassRegistry::new();
3005 r.add(&EVENS);
3006 let m = PassManager::new(r);
3007 let evens = m.registry.parse("evens").unwrap();
3008 let mut w = mesh_world(3);
3009 let report = m.convert(&mut w, &evens, &even, Mode::Partial).unwrap();
3010 assert_eq!(report.pipeline, "evens");
3011 assert_eq!(w.iter::<Mesh>().map(|(_, m)| m.0).collect::<Vec<_>>(), [4]);
3012 m.convert(&mut mesh_world(2), &Pipeline::default(), &even, Mode::Partial)
3014 .unwrap();
3015 let err = m
3017 .convert(&mut mesh_world(3), &Pipeline::default(), &even, Mode::Partial)
3018 .unwrap_err();
3019 assert_eq!(
3020 err.to_string(),
3021 "`even` does not accept `toy.mesh` on [e0] after the pipeline: nothing in the stated pipeline removes or \
3022 rewrites it"
3023 );
3024 }
3025
3026 struct Optional {
3028 lowers: bool,
3029 }
3030 impl Pass for Optional {
3031 fn name(&self) -> &'static str {
3032 "optional"
3033 }
3034 fn options(&self) -> Options {
3035 if self.lowers {
3036 Options::from([("lowers".to_owned(), "true".to_owned())])
3037 } else {
3038 Options::new()
3039 }
3040 }
3041 fn removes(&self) -> &[&'static str] {
3042 if self.lowers { &["toy.layered"] } else { &[] }
3043 }
3044 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
3045 if !self.lowers {
3046 return Ok(());
3047 }
3048 let es: Vec<Entity> = w.iter::<Layered>().map(|(e, _)| e).collect();
3049 for e in es {
3050 w.remove::<Layered>(e);
3051 }
3052 Ok(())
3053 }
3054 }
3055
3056 #[test]
3058 fn a_stated_pass_declares_by_its_options() {
3059 let mut r = PassRegistry::new();
3060 const OPTIONAL: PassSpec = PassSpec {
3061 name: "optional",
3062 options: &[OptionSpec::optional(
3063 "lowers",
3064 "true",
3065 "true: it removes toy.layered",
3066 "it removes nothing",
3067 )],
3068 make: |g| {
3069 Ok(Box::new(Optional {
3070 lowers: g.optional("lowers").is_some_and(|v| v == "true"),
3071 }))
3072 },
3073 };
3074 r.add(&OPTIONAL);
3075 let m = PassManager::new(r);
3076 let t = Target::new("t").illegal("toy.layered");
3077 let mut w = World::new();
3078 let e = w.spawn();
3079 w.insert(e, Layered(1));
3080 let err = m
3081 .convert(
3082 &mut w.clone(),
3083 &m.registry.parse("optional").unwrap(),
3084 &t,
3085 Mode::Full,
3086 )
3087 .unwrap_err();
3088 assert!(
3089 matches!(&err, Error::Illegal { lowered_by, .. } if lowered_by.is_empty()),
3090 "{err}"
3091 );
3092 let report = m
3093 .convert(
3094 &mut w,
3095 &m.registry.parse("optional{lowers=true}").unwrap(),
3096 &t,
3097 Mode::Full,
3098 )
3099 .unwrap();
3100 assert_eq!(report.pipeline, "optional{lowers=true}");
3101 }
3102
3103 struct Decl {
3104 name: &'static str,
3105 requires: &'static [&'static str],
3106 modifies: &'static [&'static str],
3107 }
3108 impl Pass for Decl {
3109 fn name(&self) -> &'static str {
3110 self.name
3111 }
3112 fn requires(&self) -> &[&'static str] {
3113 self.requires
3114 }
3115 fn modifies(&self) -> &[&'static str] {
3116 self.modifies
3117 }
3118 fn run(&self, _: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
3119 Ok(())
3120 }
3121 }
3122
3123 #[test]
3124 fn values_a_stated_pass_produces_are_checked_after_the_pipeline() {
3125 let mut r = PassRegistry::new();
3127 r.add(&LOAD);
3128 r.add(&EVENS);
3129 let m = PassManager::new(r);
3130 let t = Target::new("even")
3131 .illegal("toy.file")
3132 .dynamic_typed::<Mesh>(|m| m.0 % 2 == 0);
3133 let mut w = w_with_file(3);
3134 let e = w.entities().next().unwrap();
3135 let report = m
3136 .convert(
3137 &mut w,
3138 &m.registry.parse("load, evens").unwrap(),
3139 &t,
3140 Mode::Partial,
3141 )
3142 .unwrap();
3143 assert_eq!(report.pipeline, "load, evens");
3144 assert_eq!(w.get::<Mesh>(e), Some(&Mesh(4)));
3145 let load = m.registry.parse("load").unwrap();
3147 m.convert(&mut w_with_file(2), &load, &t, Mode::Partial).unwrap();
3148 let err = m
3150 .convert(&mut w_with_file(5), &load, &t, Mode::Partial)
3151 .unwrap_err();
3152 assert!(
3153 matches!(&err, Error::Illegal { name, lowered_by, .. } if name == "toy.mesh" && lowered_by.is_empty()),
3154 "{err}"
3155 );
3156 }
3157
3158 fn w_with_file(n: u32) -> World {
3159 let mut w = World::new();
3160 let e = w.spawn();
3161 w.insert(e, File(n));
3162 w
3163 }
3164
3165 #[derive(Clone, Debug, PartialEq)]
3167 struct Table(Vec<Option<u8>>);
3168 impl crate::world::Resource for Table {
3169 const NAME: &'static str = "toy.table";
3170 }
3171
3172 struct Classify;
3174 impl Pass for Classify {
3175 fn name(&self) -> &'static str {
3176 "classify"
3177 }
3178 fn requires(&self) -> &[&'static str] {
3179 &["toy.table"]
3180 }
3181 fn modifies(&self) -> &[&'static str] {
3182 &["toy.table"]
3183 }
3184 fn settles(&self) -> &[&'static str] {
3185 &["toy.table"]
3186 }
3187 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
3188 if let Some(t) = w.resource_mut::<Table>() {
3189 for row in t.0.iter_mut().filter(|r| r.is_none()) {
3190 *row = Some(0);
3191 log.count("rows classified", 1);
3192 }
3193 }
3194 Ok(())
3195 }
3196 }
3197
3198 struct AddRow;
3200 impl Pass for AddRow {
3201 fn name(&self) -> &'static str {
3202 "add-row"
3203 }
3204 fn provides(&self) -> &[&'static str] {
3205 &["toy.table"]
3206 }
3207 fn run(&self, w: &mut World, _: &mut PassLog) -> std::result::Result<(), String> {
3208 match w.resource_mut::<Table>() {
3209 Some(t) => t.0.push(None),
3210 None => {
3211 w.insert_resource(Table(vec![None]));
3212 }
3213 }
3214 Ok(())
3215 }
3216 }
3217
3218 fn classified() -> Target {
3219 Target::new("t").check_resource::<Table>("every row classified", |t| t.0.iter().all(Option::is_some))
3220 }
3221
3222 fn table_world(rows: Vec<Option<u8>>) -> World {
3223 let mut w = World::new();
3224 w.insert_resource(Table(rows));
3225 w
3226 }
3227
3228 #[test]
3229 fn a_resource_value_rule_is_checked_after_the_pipeline() {
3230 let mut r = PassRegistry::new();
3231 const CLASSIFY: PassSpec = PassSpec {
3232 name: "classify",
3233 options: &[],
3234 make: |_| Ok(Box::new(Classify)),
3235 };
3236 r.add(&CLASSIFY);
3237 let m = PassManager::new(r);
3238 let mut w = table_world(vec![Some(3), None]);
3239 let classify = m.registry.parse("classify").unwrap();
3240 let report = m
3241 .convert(&mut w, &classify, &classified(), Mode::Partial)
3242 .unwrap();
3243 assert_eq!(report.pipeline, "classify");
3244 assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(3), Some(0)])));
3245 m.convert(
3247 &mut table_world(vec![Some(1)]),
3248 &Pipeline::default(),
3249 &classified(),
3250 Mode::Partial,
3251 )
3252 .unwrap();
3253 let err = m
3255 .convert(
3256 &mut table_world(vec![None]),
3257 &Pipeline::default(),
3258 &classified(),
3259 Mode::Partial,
3260 )
3261 .unwrap_err();
3262 assert_eq!(
3263 err.to_string(),
3264 "`t` does not accept `toy.table` (every row classified) after the pipeline: nothing in the stated pipeline \
3265 removes or rewrites it"
3266 );
3267 assert_eq!(classified().rule_words("toy.table"), Some("every row classified"));
3268 }
3269
3270 #[test]
3273 fn a_covered_name_reports_only_its_uncovered_entities() {
3274 let mut w = World::new();
3275 let covered = w.spawn();
3276 w.insert(covered, File(1));
3277 w.insert(covered, Pbr(1));
3278 let bare = w.spawn();
3279 w.insert(bare, File(2));
3280 let t = Target::new("cover")
3281 .legal(Pbr::NAME)
3282 .covered(File::NAME, "covered by toy.pbr", |w, e| w.has::<Pbr>(e));
3283 assert_eq!(
3284 t.check_prepared(&w).unsupported,
3285 vec![("toy.file not covered by toy.pbr".to_owned(), 1)]
3286 );
3287 assert_eq!(t.rule_words(File::NAME), Some("covered by toy.pbr"));
3288 assert!(t.illegal_entities(&w, File::NAME, Mode::Partial).is_empty());
3289 assert_eq!(t.illegal_entities(&w, File::NAME, Mode::Full), vec![bare]);
3290 w.insert(bare, Pbr(2));
3292 assert!(t.check_prepared(&w).unsupported.is_empty());
3293 }
3294
3295 #[test]
3296 fn a_refused_value_names_its_rule_and_the_passes_that_left_it() {
3297 let mut r = PassRegistry::new();
3299 const LAZY: PassSpec = PassSpec {
3300 name: "lazy",
3301 options: &[],
3302 make: |_| {
3303 Ok(Box::new(Decl {
3304 name: "lazy",
3305 requires: &["toy.mesh"],
3306 modifies: &["toy.mesh"],
3307 }))
3308 },
3309 };
3310 r.add(&LAZY);
3311 let m = PassManager::new(r);
3312 let t = Target::new("even").check::<Mesh>("even meshes", |m| m.0 % 2 == 0);
3313 let err = m
3314 .convert(
3315 &mut mesh_world(3),
3316 &m.registry.parse("lazy").unwrap(),
3317 &t,
3318 Mode::Partial,
3319 )
3320 .unwrap_err();
3321 assert_eq!(
3322 err.to_string(),
3323 "`even` does not accept `toy.mesh` (even meshes) on [e0] after the pipeline: lazy left it"
3324 );
3325 assert!(matches!(err, Error::Illegal { ref rule, .. } if rule == "even meshes"));
3326 }
3327
3328 #[test]
3329 fn a_pass_that_provides_a_settled_name_after_it_refuses_the_pipeline() {
3330 let mut r = PassRegistry::new();
3331 const CLASSIFY: PassSpec = PassSpec {
3332 name: "classify",
3333 options: &[],
3334 make: |_| Ok(Box::new(Classify)),
3335 };
3336 r.add(&CLASSIFY);
3337 const ADD_ROW: PassSpec = PassSpec {
3338 name: "add-row",
3339 options: &[],
3340 make: |_| Ok(Box::new(AddRow)),
3341 };
3342 r.add(&ADD_ROW);
3343 let m = PassManager::new(r);
3344 let mut w = table_world(vec![None]);
3346 let err = m
3347 .run(
3348 &mut w,
3349 &m.registry.parse("classify, add-row").unwrap(),
3350 &mut RunReport::default(),
3351 )
3352 .unwrap_err();
3353 assert_eq!(
3354 err.to_string(),
3355 "add-row provides `toy.table` after classify settled it: its values would not be settled"
3356 );
3357 assert_eq!(w.resource::<Table>(), Some(&Table(vec![None])), "nothing ran");
3358 let mut w = table_world(vec![None]);
3360 m.run(
3361 &mut w,
3362 &m.registry.parse("add-row, classify").unwrap(),
3363 &mut RunReport::default(),
3364 )
3365 .unwrap();
3366 assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(0), Some(0)])));
3367 let mut w = table_world(vec![None]);
3368 m.run(
3369 &mut w,
3370 &m.registry.parse("classify, add-row, classify").unwrap(),
3371 &mut RunReport::default(),
3372 )
3373 .unwrap();
3374 assert_eq!(w.resource::<Table>(), Some(&Table(vec![Some(0), Some(0)])));
3375 }
3376
3377 #[test]
3378 fn validators_run_with_the_verifier() {
3379 struct NoPbr;
3380 impl Validator for NoPbr {
3381 fn name(&self) -> &'static str {
3382 "no-pbr"
3383 }
3384 fn check(&self, w: &World) -> Vec<(Option<Entity>, String)> {
3385 w.iter::<Pbr>()
3386 .map(|(e, _)| (Some(e), "pbr is banned here".to_owned()))
3387 .collect()
3388 }
3389 }
3390 let mut m = PassManager::new(registry()).with_pass_checks(true);
3391 m.add_validator(Box::new(NoPbr));
3392 let pipeline = m.registry.parse("load, bake").unwrap();
3393 let err = m
3394 .convert(&mut world(), &pipeline, &gltf(), Mode::Partial)
3395 .unwrap_err();
3396 assert_eq!(
3397 err.to_string(),
3398 "verify after bake: no-pbr on e0: pbr is banned here"
3399 );
3400 }
3401
3402 #[test]
3405 fn the_verifier_runs_after_passes_with_the_pass_checks() {
3406 struct NoPbr;
3407 impl Validator for NoPbr {
3408 fn name(&self) -> &'static str {
3409 "no-pbr"
3410 }
3411 fn check(&self, w: &World) -> Vec<(Option<Entity>, String)> {
3412 w.iter::<Pbr>()
3413 .map(|(e, _)| (Some(e), "pbr is banned here".to_owned()))
3414 .collect()
3415 }
3416 }
3417 assert_eq!(PassManager::new(registry()).pass_checks(), cfg!(debug_assertions));
3418 for checks in [true, false] {
3419 let mut m = PassManager::new(registry()).with_pass_checks(checks);
3420 m.add_validator(Box::new(NoPbr));
3421 let pipeline = m.registry.parse("load, bake").unwrap();
3422 let ran = m.run(&mut world(), &pipeline, &mut RunReport::default());
3423 if checks {
3424 assert_eq!(
3425 ran.unwrap_err().to_string(),
3426 "verify after bake: no-pbr on e0: pbr is banned here"
3427 );
3428 } else {
3429 assert!(ran.is_ok());
3430 }
3431 }
3432 }
3433
3434 struct Threaded;
3436 impl Pass for Threaded {
3437 fn name(&self) -> &'static str {
3438 "threaded"
3439 }
3440 fn may_lack_inputs(&self) -> bool {
3441 true
3442 }
3443 fn requires(&self) -> &[&'static str] {
3444 &["toy.layered"]
3445 }
3446 fn settings(&self) -> &[Setting] {
3447 &[crate::parallel::THREADS]
3448 }
3449 fn approximates(&self) -> &'static str {
3450 ""
3451 }
3452 fn run(&self, w: &mut World, log: &mut PassLog) -> std::result::Result<(), String> {
3453 let threads = crate::parallel::threads(w).map_err(|e| e.to_string())?;
3454 log.count("threads", threads.get() as u64);
3455 Ok(())
3456 }
3457 }
3458
3459 fn threads(n: usize) -> Settings {
3460 Settings {
3461 threads: primase_foundations::parallel::Threads::new(n).unwrap(),
3462 }
3463 }
3464
3465 #[test]
3468 fn the_manager_states_the_programs_settings_and_the_report_says_them() {
3469 let manager = PassManager::new(PassRegistry::new()).with_settings(threads(3));
3470 let mut w = world();
3471 let mut report = RunReport::default();
3472 let pipeline = Pipeline {
3473 passes: vec![Box::new(Threaded)],
3474 };
3475 manager.run(&mut w, &pipeline, &mut report).unwrap();
3476 assert_eq!(report.steps[0].log.counts["threads"], 3);
3477 assert_eq!(report.steps[0].settings, ["threads 3"]);
3478 assert!(report.to_string().contains("; threads 3)"), "{report}");
3479 let mut other = report.clone();
3480 other.steps[0].settings.clear();
3481 assert_eq!(report, other, "how a step ran is not what it made");
3482 manager.run(&mut w, &pipeline, &mut RunReport::default()).unwrap();
3484 }
3485
3486 #[test]
3489 fn a_setting_the_program_does_not_state_refuses_the_pass_but_never_decides_a_skip() {
3490 let manager = PassManager::new(PassRegistry::new());
3491 let pipeline = Pipeline {
3492 passes: vec![Box::new(Threaded)],
3493 };
3494 let refused = manager
3495 .run(&mut world(), &pipeline, &mut RunReport::default())
3496 .unwrap_err();
3497 assert!(
3498 matches!(&refused, Error::Missing { pass, name } if pass == "threaded" && name == "core.threads"),
3499 "{refused}"
3500 );
3501 let mut report = RunReport::default();
3502 manager.run(&mut World::new(), &pipeline, &mut report).unwrap();
3503 assert_eq!(report.steps[0].skipped, ["toy.layered"]);
3504 assert!(report.steps[0].settings.is_empty());
3505 }
3506
3507 #[test]
3509 fn a_world_stating_another_count_than_the_program_is_refused() {
3510 let manager = PassManager::new(PassRegistry::new()).with_settings(threads(3));
3511 let mut w = world();
3512 w.insert_resource(primase_foundations::parallel::Threads::new(2).unwrap());
3513 let pipeline = Pipeline {
3514 passes: vec![Box::new(Threaded)],
3515 };
3516 let refused = manager
3517 .run(&mut w, &pipeline, &mut RunReport::default())
3518 .unwrap_err();
3519 assert_eq!(
3520 refused.to_string(),
3521 "the world states `core.threads` as threads 2, the program as threads 3"
3522 );
3523 }
3524}