Skip to main content

primase_core/
pass.rs

1//! The pass manager (DESIGN.md ยง4): passes that transform the world, the pipelines a program states, the targets
2//! writers declare, and the verifier that runs between every step.
3//!
4//! - A [`Pass`] has a stable name, text options, and declares the component and resource names it **requires**,
5//!   **provides**, **removes** and **modifies** (in place), plus what it approximates (a name is a component's or a
6//!   resource's, never both), and the run **settings** it reads. An input (required) is content a reader or an
7//!   earlier pass makes, and a world may simply not hold it: a pass that opts in is skipped where it finds none of its
8//!   inputs ([`Pass::may_lack_inputs`]). A setting ([`Setting`]) is how the program runs the pass, never made from
9//!   the content: its thread count (`core.threads`). The program states its settings once, on the manager
10//!   ([`PassManager::with_settings`], [`Settings`]), which states them in every world it runs a pipeline on, a
11//!   conversion's parts included, as operational resources no writer writes. A pass that runs without a setting it
12//!   declares is refused like one without an input ([`Error::Missing`]), but a setting never decides whether a pass is
13//!   skipped, and the run report states each setting a step read with its value (`threads 16`,
14//!   [`StepReport::settings`]). A value that changes what a pass makes is neither: it is the pass's option.
15//!   After a pass runs, the names that actually appeared and disappeared must be among those it declared. A pass that
16//!   declares it **despawns entities** (leaves placements out, discards forms) may also make a name disappear that
17//!   only despawned entities held: no surviving entity lost it. A pass that **removes by log** may make a component name disappear that it recorded
18//!   as removed ([`PassLog::removed`]: what a hook dropped, known only when it runs).
19//! - A [`Pipeline`] is the program's: it states every pass, in order, with its options (`load-meshes,
20//!   lower-pbr{normals=rebuild}`, [`PassRegistry::parse`]), and runs as written. The manager never chooses, adds or
21//!   reorders a pass.
22//! - A [`Target`] (a writer's declaration) gives each component name a [`Legality`]: legal, dynamic (a predicate on
23//!   the entity's value), covered (written through another component) or illegal. A name it does not list is not
24//!   written: [`Mode::Partial`] keeps it and reports it, [`Mode::Full`] refuses it. A target never carries a name it
25//!   does not write (CONTRIBUTING.md, law 4).
26//! - [`PassManager::convert`] runs a stated pipeline and checks the world it leaves against a target: a name the
27//!   target still refuses is an error that says whether any pass of the pipeline removes or rewrites it.
28//! - The verifier ([`Component::verify`] on every component, [`crate::Resource::verify`] on every resource, plus the
29//!   manager's world [`Validator`]s) runs on a conversion's reader output and before its writer in every build, and
30//!   after every pass only with the pass checks ([`PassManager::with_pass_checks`]): on in debug builds, off by
31//!   default in release builds for speed (a program opts in); its errors name the step and the entity.
32//! - An [`Instrument`] sees the world before and after every pass and when one fails (`print-world-after`, see
33//!   [`crate::print`]). [`PassManager::run`] and [`PassManager::convert`] are [`PassManager::run_instrumented`] and
34//!   [`PassManager::convert_instrumented`] with an instrument that does nothing.
35//!
36//! Everything is deterministic: passes and names are walked in sorted order.
37
38use 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
51/// A pass's text options (`name{key=value,...}`), sorted by key.
52pub type Options = BTreeMap<String, String>;
53
54/// Counts a pass logs while it runs: what it changed, what it approximated and what it left out, and the entities it
55/// placed.
56#[derive(Clone, Debug, Default, PartialEq, Eq)]
57pub struct PassLog {
58    pub counts: BTreeMap<String, u64>,
59    pub approximations: BTreeMap<String, u64>,
60    /// Of [`PassLog::approximations`], the entities each reason was recorded on ([`PassLog::approximate_on`]): the
61    /// material or mesh the approximation concerns, by reason. An approximation recorded with [`PassLog::approximate`]
62    /// names none. The ids are the world's the pass ran on; the manager refuses one that is not alive after the pass.
63    pub approximated_on: BTreeMap<String, BTreeSet<Entity>>,
64    /// What the pass left out because what it makes has no form for it, by reason (`collision triangles on a surface
65    /// of another kind, left out: PHYS_CASTER`). A conversion reports each as an unsupported notice.
66    pub omitted: BTreeMap<String, u64>,
67    /// Component names the pass removed from some entity without declaring them in [`Pass::removes`] (a component a
68    /// hook dropped, as [`World::apply_linear`] reports): accepted as removals only for a pass that opts in with
69    /// [`Pass::removes_by_log`].
70    pub removed: BTreeSet<String>,
71    /// Entities the pass made that stand on their own: content nothing need reference (a marker it places). A
72    /// conversion keeps them with the writer's roots ([`crate::convert`]); any other entity a pass makes is kept only
73    /// while something kept references it. The manager refuses an entity here that the pass did not make.
74    pub placed: BTreeSet<Entity>,
75    /// Of [`PassLog::placed`], those made from one source entity, with it ([`PassLog::place_from`]).
76    pub placed_from: BTreeMap<Entity, Entity>,
77    /// What the pass left out on purpose, by measure and reason (`placements in an interior left out`); a conversion
78    /// reports each as a selection notice ([`crate::ledger`]).
79    pub drops: BTreeMap<(Measure, String), u64>,
80    /// Display-only structured explanations that the typed selections/exclusions, approximations and counts cannot
81    /// express; prefer those typed entries first. A program may show these values generically, but must never parse
82    /// their fields or branch on their contents. A value needed for a decision belongs in a typed core field.
83    /// Repeated entries under one label retain their order when logs join. JSON reporters omit this field when empty.
84    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    /// Append a display-only explanation (see [`PassLog::details`]), without replacing an earlier explanation.
93    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    /// Record that the pass removed component `name` from some entity (see [`PassLog::removed`]).
98    pub fn record_removed(&mut self, name: &str) {
99        self.removed.insert(name.to_owned());
100    }
101
102    /// Record `n` items lowered with an approximation (`"layer blend baked at 1024 px"`).
103    pub fn approximate(&mut self, what: &str, n: u64) {
104        *self.approximations.entry(what.to_owned()).or_default() += n;
105    }
106
107    /// Record `n` items lowered with an approximation, on the entity `e` they concern (a material whose base albedo
108    /// could not be read): [`PassLog::approximate`] and the entity kept with the reason ([`PassLog::approximated_on`]).
109    /// The count stays the number of items, not of entities: one call per item, or `n` items of one entity.
110    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    /// Record `n` items left out because what the pass makes has no form for them (see [`PassLog::omitted`]).
116    pub fn omit(&mut self, what: &str, n: u64) {
117        *self.omitted.entry(what.to_owned()).or_default() += n;
118    }
119
120    /// Record `n` of `measure` left out on purpose, and why (see [`PassLog::drops`]; an entity despawned whole:
121    /// [`crate::ledger::despawn`]).
122    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    /// The drops it recorded in `measure`, over every reason.
129    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    /// Record that the pass made `e` as content of its own (see [`PassLog::placed`]): content of the whole world, as a
138    /// spawn a selection places. Content made from one source entity is [`PassLog::place_from`].
139    pub fn place(&mut self, e: Entity) {
140        self.placed.insert(e);
141    }
142
143    /// Record that the pass made `e` as content of its own from `source` (a marker for a placement): a conversion in
144    /// parts keeps it only in the part that owns `source`, so content made from a neighbour loaded as context is
145    /// written once, by its owner.
146    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    /// Take in the log of work done on the pass's behalf (a function returning its own [`PassLog`]): every count,
152    /// approximation and omission added to this log's, its recorded removals and its placed entities (with their
153    /// sources) and drops, so nothing it reported is lost on the way to the pass's report.
154    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/// The run settings a program states for every pipeline its manager runs ([`PassManager::with_settings`]): how the
191/// passes run, never what they make.
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub struct Settings {
194    /// The threads a pass may use (`core.threads`, [`crate::parallel::THREADS`]).
195    pub threads: primase_foundations::parallel::Threads,
196}
197
198/// A run setting a pass reads ([`Pass::settings`]): a resource the program states for the run, by name, with how a
199/// run report states its value ([`RunSetting::text`]). Made by [`Setting::of`].
200#[derive(Clone, Copy, Debug)]
201pub struct Setting {
202    /// The resource's name (`core.threads`).
203    pub name: &'static str,
204    text: fn(&World) -> Option<String>,
205}
206
207impl Setting {
208    /// The setting `T`.
209    pub const fn of<T: RunSetting>() -> Self {
210        Setting {
211            name: T::NAME,
212            text: setting_text::<T>,
213        }
214    }
215
216    /// Its value in `w` as a report states it (`threads 16`); none where `w` does not state it.
217    pub fn text(&self, w: &World) -> Option<String> {
218        (self.text)(w)
219    }
220}
221
222/// A resource a program states as a run setting ([`Setting`]): how it runs the passes, never what they make.
223pub trait RunSetting: Resource {
224    /// The value as a run report states it (`threads 16`).
225    fn text(&self) -> String;
226}
227
228fn setting_text<T: RunSetting>(w: &World) -> Option<String> {
229    w.resource::<T>().map(RunSetting::text)
230}
231
232/// What entities leaving made a pass's world drop to stay whole ([`crate::Dropped`]), counted in the pass's log.
233fn 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
263/// The entities the world itself took component `name` from to stay whole ([`crate::Dropped`]).
264fn 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/// A setting read from a world that does not state it ([`Setting`]): a pass that reads one without declaring it in
276/// [`Pass::settings`], or a caller outside the pass manager. The program states its settings for the run
277/// ([`PassManager::with_settings`]), and no default stands in for one.
278#[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    /// The setting's resource name.
282    pub name: &'static str,
283}
284
285/// A transformation of the world. Declarations are component names (`core.mesh`, a destination's `material`).
286pub trait Pass: Send + Sync {
287    /// Stable name, used in pipelines and reports (`load-meshes`).
288    fn name(&self) -> &'static str;
289    /// The options this instance was made with (printed into the pipeline).
290    fn options(&self) -> Options {
291        Options::new()
292    }
293    /// Whether a run may skip it in a world (or a part) that holds none of its required names: a lowering whose input
294    /// the world may simply not have (a part of only a light has no materials, a read region no lights) opts in;
295    /// the step is recorded as skipped ([`StepReport::skipped`]). Required names are prerequisites, so this is false
296    /// unless the pass says otherwise: an absent name may be a failed load or a wrong order, and a pass that
297    /// summarizes, decides or plans never opts in.
298    fn may_lack_inputs(&self) -> bool {
299        false
300    }
301    /// Names that must be present before it runs.
302    fn requires(&self) -> &[&'static str] {
303        &[]
304    }
305    /// The run settings it reads, which the program states for the run on its manager (its thread count,
306    /// [`crate::parallel::THREADS`], [`PassManager::with_settings`]): checked with [`Pass::requires`] before it runs and
307    /// refused where absent, never counted in whether it is skipped ([`Pass::may_lack_inputs`]), and stated in its
308    /// step's report.
309    fn settings(&self) -> &[Setting] {
310        &[]
311    }
312    /// Names it may add.
313    fn provides(&self) -> &[&'static str] {
314        &[]
315    }
316    /// Names it may take away (what it lowers).
317    fn removes(&self) -> &[&'static str] {
318        &[]
319    }
320    /// Names whose values it edits in place, and resources it writes (replaced, also by removing and inserting again,
321    /// or borrowed with [`World::resource_mut`]). The pass manager checks the resources; an in-place edit of a
322    /// component's value on an entity that keeps it (`get_mut`, or [`World::insert`] over the value) is declared here
323    /// and not checked: seeing it would take a controlled editing interface, later work. What the gap costs: a pass that
324    /// edits a value in place without declaring it here is never named, in any build, and its edit can make what the
325    /// declarations say about that name wrong ([`Pipeline::unfed`], [`Pipeline::check`]); one that leaves an invalid value there is named at the pass in a build with the
326    /// pass checks, and in a release build without them only by the verifier that runs before the writer, which names the
327    /// entity but not the pass. Adding a name to an entity, taking one away and despawning are checked entity by entity
328    /// against [`Pass::provides`], [`Pass::removes`] and [`Pass::despawns_entities`].
329    fn modifies(&self) -> &[&'static str] {
330        &[]
331    }
332    /// What it approximates, in words (empty when it is exact).
333    fn approximates(&self) -> &'static str {
334        ""
335    }
336    /// Whether it despawns entities. If so, a component name it does not declare in [`Pass::removes`] may disappear
337    /// when every entity that held the name before the pass was despawned by it (a dropped placement's
338    /// `core.name`, a discarded form's dialect data); a name still held by a surviving entity before the pass must
339    /// be declared as ever. Added names are checked as for every pass.
340    fn despawns_entities(&self) -> bool {
341        false
342    }
343    /// Whether the components it removes are not all known in advance (a hook decides, as
344    /// [`crate::Component::apply_linear`] does). If so, a component name that disappears is accepted when the pass
345    /// recorded it in [`PassLog::removed`] as well as when it is declared in [`Pass::removes`]; a name that disappears
346    /// unrecorded and undeclared is an error as ever, and a resource it removes must be declared as for any pass.
347    fn removes_by_log(&self) -> bool {
348        false
349    }
350    /// Names whose values it brings to their final form (a classifier: every surface classified). A later pass in a
351    /// pipeline that provides one of them adds values that never went through this pass: unless a pass after it settles
352    /// the name again, [`PassManager::run`] refuses such a pipeline before it runs ([`Error::SettledThenChanged`]).
353    /// Modifying settled values later is allowed. A modifier must not add
354    /// values without declaring them in [`Pass::provides`]; the target's value rules check the result.
355    fn settles(&self) -> &[&'static str] {
356        &[]
357    }
358    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String>;
359}
360
361/// The passes a manager knows, by name: each by its declaration ([`PassSpec`]).
362#[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    /// Register a pass by its declaration: the registry checks every pipeline's options against it before the pass is
379    /// made ([`PassSpec::check`]), makes it from the examples to read its declarations ([`PassSpec::example`]; a pass
380    /// with a required option is never run so: a pipeline that leaves the option out fails to parse), and says what it
381    /// takes ([`PassRegistry::describe`]). Panics when the name is taken, the declaration is wrong ([`PassSpec`]: a key
382    /// twice, a group of one, a choice with no values, an optional option that says nothing of its absence), the pass
383    /// is not made from its examples or is not the pass named, or what it prints back ([`Pass::options`]) holds an undeclared key, leaves out a required option or a
384    /// group, or does not make a pass that prints the same.
385    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    /// What pass `name` takes, from its declaration ([`PassSpec::describe`]); `None` for a pass not registered.
417    pub fn describe(&self, name: &str) -> Option<String> {
418        self.specs.get(name).map(|spec| spec.describe())
419    }
420
421    /// Whether `name` was registered with required options ([`PassSpec::requires_options`]): made with its example only
422    /// to read its declarations, never to run.
423    pub fn requires_options(&self, name: &str) -> bool {
424        self.specs.get(name).is_some_and(|spec| spec.requires_options())
425    }
426
427    /// Every registered pass's name, in order.
428    pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
429        self.specs.keys().copied()
430    }
431
432    /// The pass `name` made with its declaration's example options (none, or a required-option pass's example).
433    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    /// A pipeline from its text form: `pass, pass{key=value,key=value}, ...` (whitespace around items ignored).
453    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    /// Describe a preparation without discarding its unfed texts. `runnable` is the declaration-based
462    /// projection, while `requested` retains every text for an explicitly optional run
463    /// ([`PassManager::with_missing_inputs`]). Invalid texts remain requested and runnable so parsing
464    /// still reports their error; they are never silently treated as absent source content.
465    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            // A value may hold commas (`rect=x0,y0,x1,y1`, as `Display` writes it): an item without `=` continues the
522            // previous option's value.
523            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/// Passes in the order they run.
557#[derive(Default)]
558pub struct Pipeline {
559    pub passes: Vec<Box<dyn Pass>>,
560}
561
562/// A preparation's requested texts and the declaration-based projection on the source's names.
563/// Consumers retain `unfed` in their report instead of losing the reason a text was not fed.
564#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
565pub struct TextPreparation {
566    /// Every caller-requested text, in order, including ones whose inputs were absent.
567    pub requested: Vec<String>,
568    /// The texts whose declarations were fed by the initial names or preceding runnable texts.
569    pub runnable: Vec<String>,
570    /// Unfed texts and their absent names; no component or resource is synthesized for them.
571    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
580/// The text form, which [`PassRegistry::parse`] reads back.
581impl Pipeline {
582    /// The checks [`PassManager::run`] makes from the passes' declarations before it runs anything: today, that no
583    /// pass provides a name an earlier pass settles ([`Pass::settles`]) unless a later one settles it again. It needs
584    /// no world, so a program's tests run it on every pipeline it composes (data-free, unlike a run).
585    pub fn check(&self) -> Result<()> {
586        check_settled(&self.passes)
587    }
588
589    /// The passes a run would refuse for a required name absent, in a world holding the component and resource names
590    /// `present` before it runs, and the names held after the passes that can run: each pass in order, with what the
591    /// passes before it provide added. Settings ([`Pass::settings`]) are not inputs: a pass is never unfed for one, and a
592    /// run without one refuses the pass rather than leaving it out. That is the declarations' answer, not a run's: a pass may add less than it
593    /// provides and take away less than it removes, so every name provided is taken as added and every name removed as
594    /// kept. A pass that may lack its inputs and finds none of them is skipped, as a run skips it, and provides nothing;
595    /// one that finds some but not all is refused, as a run refuses it ([`Pass::may_lack_inputs`]). A refused pass
596    /// provides nothing: the passes after it are checked as if it were left out, where a run would stop at it.
597    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/// What [`Pipeline::unfed`] finds of a pipeline in a world.
621#[derive(Clone, Debug, PartialEq, Eq)]
622pub struct Unfed {
623    /// Each pass a run would refuse for a required name absent: its text and the names it lacks, in pipeline order;
624    /// empty when every pass can run or is skipped.
625    pub passes: Vec<(String, Vec<&'static str>)>,
626    /// The names held after the passes that can run: those present before, with what each such pass provides. The
627    /// world a following pipeline is checked against.
628    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
643/// Whether the pipeline text form carries `value` as an option's value: [`PassRegistry::parse`] reads back exactly
644/// what [`pass_text`] writes. It does not for a value with a brace, with a comma-separated part after the first that
645/// is blank, holds `=` (it would start another option) or has whitespace at its ends, or with whitespace at the
646/// value's own ends. A pass whose options take text checks its values with this, so its text form reads back as it.
647pub 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
655/// A pass with its options as pipeline text (`bake{size=512}`), as [`PassRegistry::parse`] reads it; what
656/// `core.provenance` records for the passes that derive an entity.
657pub fn pass_text(p: &dyn Pass) -> String {
658    stated(p.name(), &p.options())
659}
660
661/// `value` as text (`read-fromsoft-area{area=m10_00_00_00,margin=30}`), in a pass's form; [`parse_text_form`] reads it
662/// back.
663pub fn text_form<T: TextForm>(value: &T) -> String {
664    stated(T::NAME, &value.options())
665}
666
667/// `name` with `options` as pipeline text: the name alone where there are none.
668fn 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
676/// The `T` that `text` states: one item, named [`TextForm::NAME`], whose options the declaration lets through
677/// ([`TextForm::OPTIONS`]; refused as a pass's would be) and `T` takes.
678pub 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
695/// The checks [`PassRegistry::add`] makes of a pass, made of `T`: panics where the declaration is wrong (a key twice,
696/// a group of one, a choice with no values, an optional option that says nothing of its absence), `T` is not made from
697/// the declaration's examples, or what it prints is refused by the declaration or does not make a `T` that prints the
698/// same.
699pub 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
724/// A predicate deciding whether one entity's component is legal for a target (e.g. a texture format it writes).
725pub type LegalityCheck = Arc<dyn Fn(&World, Entity) -> bool + Send + Sync>;
726
727/// What a target does with one component name. A name it gives no rule is not written: kept and reported in
728/// [`Mode::Partial`], refused in [`Mode::Full`]. A writer's target never carries a name through untouched: it writes its
729/// format's meaning and reports the rest (CONTRIBUTING.md, law 4).
730#[derive(Clone)]
731pub enum Legality {
732    /// The writer writes it.
733    Legal,
734    /// Legal for the entities the predicate accepts; the others must be lowered.
735    Dynamic(LegalityCheck),
736    /// Written through another component: legal on the entities the check accepts (a `core.light` with the
737    /// destination's `light` it was raised to); on the rest as an unlisted name (kept and reported as not written in
738    /// [`Mode::Partial`], illegal in [`Mode::Full`]). See [`Target::covered`].
739    Covered(fn(&World, Entity) -> bool),
740    /// Must be lowered before writing.
741    Illegal,
742}
743
744/// The kind of a target's rule for a name, without its checks ([`Target::rules`]).
745#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
746pub enum Rule {
747    /// [`Legality::Legal`].
748    Legal,
749    /// [`Legality::Dynamic`]: legal on the entities a check accepts.
750    Dynamic,
751    /// [`Legality::Covered`]: written through another component on the entities a check accepts.
752    Covered,
753    /// [`Legality::Illegal`].
754    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/// How strict a conversion is about names the target does not list.
769#[derive(Clone, Copy, Debug, PartialEq, Eq)]
770pub enum Mode {
771    /// Only illegal (and failing dynamic) names must go; unlisted names stay, not written.
772    Partial,
773    /// Every name must be legal; unlisted names are illegal.
774    Full,
775}
776
777/// A value rule on a resource ([`Target::check_resource`]).
778#[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/// A writer's declaration of what it accepts.
791#[derive(Clone, Debug)]
792pub struct Target {
793    pub name: String,
794    rules: BTreeMap<String, Legality>,
795    /// The words naming a component's value rule ([`Target::check`]), by component name.
796    rule_words: BTreeMap<String, String>,
797    /// Value rules on resources ([`Target::check_resource`]), by resource name.
798    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    /// A name the writer writes through another component: legal on the entities `check` accepts, `what` naming the
829    /// cover (`"covered by <dialect>.light"`); the rest are reported as `<name> not <what>` ([`Legality::Covered`]).
830    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    /// The legal checks of a typed component: legal when `check` accepts the value.
836    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    /// A named value rule on a typed component: as [`Target::dynamic_typed`], with `what` naming the rule in errors
841    /// ("every placement regular").
842    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    /// A named value rule on a resource: the target refuses the world while the resource is there and `check` refuses
852    /// it (`"every surface classified"`). A pass of the stated pipeline that modifies or removes the resource fixes it.
853    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    /// The words naming the value rule on `name` (a component's [`Target::check`], a covered name's cover
869    /// ([`Target::covered`]) or a resource's [`Target::check_resource`]), if it has a named one.
870    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    /// The resources of `w` whose value rule refuses them, by name.
878    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    /// Every name the target states a rule for, with the rule's kind (its checks left out): what a coverage report
891    /// lists of the writer.
892    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    /// The resources it checks the values of ([`Target::check_resource`]), by name.
908    pub fn checked_resources(&self) -> Vec<&str> {
909        self.resource_rules.keys().map(String::as_str).collect()
910    }
911
912    /// Check a prepared world against the target without changing it: what the writer won't write (an illegal or an
913    /// unlisted name, and a covered name's uncovered entities: [`Prepared::unsupported`], entity counts by name; a
914    /// resource it neither lists nor rules on: [`Prepared::unsupported_resources`]; the conversion's own resources,
915    /// [`crate::convert::OPERATIONAL_RESOURCES`], aside), and values of names it writes that its value rule refuses
916    /// ([`Prepared::invalid`], by name with the rule's words) or resources it refuses ([`Prepared::invalid_resources`]).
917    /// A resource is listed like a component ([`Target::legal`], [`Target::illegal`]).
918    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    /// Entities whose `name` component the target does not accept as it is (all of them for an illegal name).
958    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/// What [`Target::check_prepared`] found.
973#[derive(Clone, Debug, Default, PartialEq, Eq)]
974pub struct Prepared {
975    /// Names the writer doesn't write, with how many entities hold them; a covered name ([`Target::covered`]) as
976    /// `<name> not <its cover>` (`core.light not covered by the destination's light`), for its uncovered entities.
977    pub unsupported: Vec<(String, u64)>,
978    /// Names it writes whose values its rule refuses: the name, the rule's words, the entities.
979    pub invalid: Vec<(String, String, Vec<Entity>)>,
980    /// Resources its rule refuses.
981    pub invalid_resources: Vec<String>,
982    /// Resources the writer doesn't write.
983    pub unsupported_resources: Vec<String>,
984}
985
986/// A world-wide rule (one name -> one payload, frame sanity); returns its failures.
987pub trait Validator: Send + Sync {
988    fn name(&self) -> &'static str;
989    fn check(&self, w: &World) -> Vec<(Option<Entity>, String)>;
990}
991
992/// One failure of the verifier.
993#[derive(Clone, Debug, PartialEq, Eq)]
994pub struct VerifyFailure {
995    /// The component name or validator that failed.
996    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    /// A [`TextForm`]'s options refused by its declaration or by it.
1026    #[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    /// A world that already states a run setting otherwise than the program does on its manager.
1031    #[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        /// The entities refused (none for a resource).
1057        entities: Vec<Entity>,
1058        /// The words naming the rule refused, empty when it has none.
1059        rule: String,
1060        /// The passes of the stated pipeline that remove or rewrite the name, as pipeline text; empty when nothing in
1061        /// it does (the pipeline is missing a lowering).
1062        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
1093/// A pipeline in which a pass provides a name an earlier pass settles ([`Pass::settles`]) and no pass after it settles
1094/// the name again.
1095fn 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/// One pass's run. Its equality is the determinism comparison: every field but [`StepReport::time`] (two runs that made
1114/// the same are equal however long they took).
1115#[derive(Clone, Debug)]
1116pub struct StepReport {
1117    /// The pass with its options, as in the pipeline text.
1118    pub pass: String,
1119    pub approximates: &'static str,
1120    pub log: PassLog,
1121    /// Component names that appeared and disappeared.
1122    pub added: Vec<String>,
1123    pub removed: Vec<String>,
1124    /// With conservation diagnostics reporting ([`Conservation::Report`]): each measure whose losses differ from what
1125    /// the pass declared. Empty otherwise.
1126    pub conservation: Vec<Gap>,
1127    /// `Some(why)` when the pass failed (its run, its declarations, conservation or the verifier refused it): the
1128    /// record keeps what its log had noted until then, so its omissions and approximations still reach the report;
1129    /// `added` and `removed` are then empty (not checked).
1130    pub failed: Option<String>,
1131    /// Required names absent when the step was skipped: a pass that opts in and finds none
1132    /// ([`Pass::may_lack_inputs`], [`SKIPPED`]), or the caller's explicit [`MissingInputs::Skip`]
1133    /// preparation policy ([`SKIPPED_UNFED`]). Empty when the pass ran.
1134    pub skipped: Vec<String>,
1135    /// How long the step took, by the wall clock (zero for a skipped step).
1136    pub time: StepTime,
1137    /// The run settings the pass read ([`Pass::settings`]), as the run stated them (`threads 16`), in its declared order;
1138    /// empty for a skipped or refused step. How the step was run, not what it made: a [`StepReport`]'s equality
1139    /// leaves them out, as it does the time.
1140    pub settings: Vec<String>,
1141}
1142
1143/// A step's wall-clock time, as the pass manager measured it: for a program's log and for profiling, not part of what
1144/// a conversion makes (it differs from run to run), so a [`StepReport`]'s equality leaves it out.
1145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1146pub struct StepTime {
1147    /// The pass's own [`Pass::run`].
1148    pub run: Duration,
1149    /// The manager's work around it: the requirements, the declarations and their effects, conservation diagnostics
1150    /// and the verifier ([`PassManager::with_pass_checks`]).
1151    pub checks: Duration,
1152}
1153
1154impl PartialEq for StepReport {
1155    /// Every field but the time and the settings.
1156    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    /// Every step's time added up.
1194    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
1202/// The count a pass skipped for holding none of its required names gets in its log ([`Pass::may_lack_inputs`]), in a
1203/// whole run or a part run, so a conversion's totals give the skips per pass.
1204pub const SKIPPED: &str = "runs holding none of its required names, skipped";
1205
1206/// The count for a preparation step omitted by an explicit [`MissingInputs::Skip`] policy.
1207pub const SKIPPED_UNFED: &str = "runs missing required names, skipped";
1208
1209/// How a caller's preparation handles a pass whose required components or resources are absent.
1210/// A strict conversion keeps [`MissingInputs::Refuse`]; a preparation spanning optional source
1211/// content may explicitly skip unfed passes, retaining each one in the typed run report.
1212#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1213pub enum MissingInputs {
1214    /// Refuse absent inputs, except for a pass's own [`Pass::may_lack_inputs`] declaration.
1215    #[default]
1216    Refuse,
1217    /// Skip an unfed step, recording precisely which required names are absent.
1218    Skip,
1219}
1220
1221/// What a conversion did. Its equality is the determinism comparison: every field, the steps compared without their
1222/// times ([`StepReport`]'s equality).
1223#[derive(Clone, Debug, Default, PartialEq, Eq)]
1224pub struct RunReport {
1225    /// The target a conversion checked the world against ([`PassManager::convert`]); empty for a bare run.
1226    pub target: String,
1227    /// The stated pipeline a conversion ran, as text; empty for a bare run.
1228    pub pipeline: String,
1229    pub steps: Vec<StepReport>,
1230    /// What the world dropped outside any pass ([`crate::Dropped`]: a retain, despawn or part made before the run or
1231    /// between its passes), by the pass that ran next: losses no pass's log holds, recorded where the run first sees
1232    /// them.
1233    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/// One pass of a pipeline run, as an [`Instrument`] sees it.
1278#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1279pub struct Step<'a> {
1280    /// Position in the pipeline, from 0.
1281    pub index: usize,
1282    /// The pass's stable name (`select-lod`).
1283    pub name: &'static str,
1284    /// The pass with its options, as in the pipeline text (`select-lod{form=default}`).
1285    pub text: &'a str,
1286}
1287
1288/// Hooks the manager calls around every pass of a run (DESIGN.md ยง4 instrumentation): before it runs, after it ran and
1289/// passed the verifier, and instead of `after_pass` when it fails (the world is then as the failure left it). An error
1290/// from `before_pass` or `after_pass` stops the run as [`Error::Instrument`]; one from `pass_failed` is dropped in
1291/// favour of the pass's own error. Instruments only look: they get the world by shared reference.
1292pub 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
1314/// The instrument of an uninstrumented run.
1315struct NoInstrument;
1316impl Instrument for NoInstrument {}
1317
1318/// Runs the pipelines a program states and verifies the world between steps.
1319#[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    /// State the program's run settings: before every pipeline it runs, plain or a conversion's part, the manager
1354    /// states them in the world, and a pass that declares a setting ([`Pass::settings`]) reads it there. A manager that
1355    /// states none runs only passes that read none (another is refused, [`Error::Missing`]); a world that already
1356    /// states a setting otherwise is refused ([`Error::SettingDisagrees`]).
1357    pub fn with_settings(mut self, settings: Settings) -> Self {
1358        self.settings = Some(settings);
1359        self
1360    }
1361
1362    /// The program's settings ([`PassManager::with_settings`]), stated in `w`: refused where `w` already states one
1363    /// otherwise.
1364    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    /// The pass checks, after every pass: its effects entity by entity against its declarations (what it adds to or
1383    /// takes from an entity, what it despawns, the resources it writes: [`Pass::provides`], [`Pass::removes`],
1384    /// [`Pass::modifies`]), then the verifier ([`Component::verify`], [`crate::Resource::verify`], the validators) on
1385    /// what it leaves. On in debug builds (the test suites); off by default in release builds, for speed, so a pass
1386    /// that leaves an invalid component is not named there unless the program opts in with `true`. Measured on a
1387    /// level of 16,000 placements (12 passes, release): the verifier 0.26 s and the declaration diff another
1388    /// 0.46 s, against 1.6 s of passes. What a pass's names do across the whole world is checked either way, and a
1389    /// conversion ([`PassManager::convert`]) verifies the reader's output and the world before its writer either way.
1390    /// The in-place edits of values on entities that keep the name are not in the declaration diff in any build
1391    /// ([`Pass::modifies`]); only the verifier sees their result, and only after the pass when the checks are on.
1392    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    /// State how to handle unfed preparation steps. Skipping never supplies default data or
1402    /// calls an unfed pass: its [`StepReport::skipped`] lists the absent names, and later steps
1403    /// inspect the world actually left by earlier steps. The default remains strict refusal.
1404    pub fn with_missing_inputs(mut self, policy: MissingInputs) -> Self {
1405        self.missing_inputs = policy;
1406        self
1407    }
1408
1409    /// Run the conservation diagnostics around every pass ([`crate::ledger`]): off by default.
1410    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    /// Every component's own checks and every world validator. `after` names the step for the error.
1424    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    /// Run a pipeline as written, verifying after every pass and checking each pass's declarations.
1461    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    /// [`PassManager::run`], calling `instrument` around every pass.
1466    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    /// [`PassManager::run`] on one part of a conversion ([`crate::convert`]), with the same rule as any run: a pass that
1477    /// opts in ([`Pass::may_lack_inputs`]), requires names and finds none of them has nothing to do there and is
1478    /// skipped, recorded (its step's [`StepReport::skipped`], a [`SKIPPED`] count in its log). Any other pass whose
1479    /// required names are absent fails, as does an opted-in pass that finds some of them but not all; a pass that
1480    /// requires nothing always runs. An explicit [`MissingInputs::Skip`] policy also skips
1481    /// other unfed steps, retaining their missing names in the report.
1482    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            // what a part owns before the pass: a source the pass consumes still classifies what it placed
1535            let owned_before = w
1536                .resource::<crate::convert::PartScope>()
1537                .map(|s| s.owned(w))
1538                .unwrap_or_default();
1539            // what the world dropped since the last pass ran is not this pass's: the run records it
1540            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                    // a part's declared output so far, for the passes after (PartScope::output)
1556                    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                    // the failed step's record keeps what its log noted, marked failed
1567                    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                    // the pass's error is the one that matters; a failing hook has nothing to add to it
1580                    let _ = instrument.pass_failed(&step, w, &e);
1581                    return Err(e);
1582                }
1583            }
1584        }
1585        Ok(())
1586    }
1587
1588    /// One pass: its requirements, the run, its declarations, the verifier. `log` is the caller's, so what the pass
1589    /// noted survives a failure; `run` is set to how long the pass's own run took (left zero if it never ran).
1590    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        // who held each name a despawning pass may make disappear without declaring it
1619        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        // who held each name, per entity: what the pass adds to or takes from an entity is checked against its
1629        // declarations, not only the names that appear or disappear across the whole world
1630        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        // what it placed it made (allocated during the pass: indices are never reused), and it is there, unless it was
1669        // an intermediate: made, placed, something else placed from it, then consumed in the same pass
1670        // (PartScope::declare still classifies what came from it); every source was made by someone
1671        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        // what it attributed an approximation to is alive: a pass that names a dead entity has a bug, not a notice
1690        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        // resources by the same declarations; despawning never takes one away, so no exemption
1739        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    /// The pass's effects entity by entity, against its declarations: a name it put on an entity that didn't hold it
1791    /// (other than by copying an entity that did, [`World::duplicate`] / [`World::copy_components`]) is in
1792    /// [`Pass::provides`]; a name it took from an entity that survives is in [`Pass::removes`] (or, for
1793    /// [`Pass::removes_by_log`], in its log); an entity it despawned while it held names needs
1794    /// [`Pass::despawns_entities`]; a resource it replaced or borrowed to write is in [`Pass::modifies`] or
1795    /// [`Pass::provides`]. Values a pass edits in place on entities that keep the name (`get_mut`) are not seen: that is
1796    /// declared in [`Pass::modifies`] and not checked (a controlled editing interface would be needed to see it).
1797    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        // added to entities
1826        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        // taken from entities that survive; entities despawned while they held names
1843        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            // the world's own drops, counted in the log ([`count_dropped`])
1854            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        // a resource taken away and held again afterwards was replaced: a write
1874        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    /// Run the stated `pipeline` and check the world it leaves against `target`: verify the reader's world, run the
1892    /// pipeline as written (verifying after every pass with the pass checks), then check that the target accepts every
1893    /// component and resource in `mode`, and verify once more before the writer. A name the target still refuses is
1894    /// [`Error::Illegal`], naming the passes of the pipeline that remove or rewrite it, or none: the manager never adds a
1895    /// pass to lower it.
1896    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    /// [`PassManager::convert`], calling `instrument` around every pass of the pipeline.
1907    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    /// The text form carries a value with commas between non-blank parts that hold no '=' (`-64,-64,64,64`), and not one
1959    /// whose part after a comma is blank (a trailing ','), holds '=', or has a brace.
1960    #[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    // toy components
1994    #[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    /// toy.file -> toy.mesh (needs nothing).
2027    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    /// toy.layered -> toy.pbr, needs the mesh (bakes per vertex), option `size`.
2058    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    /// Adds a name it does not declare.
2112    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    /// Sleeps for its milliseconds in its run, doing nothing else.
2163    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    /// A pipeline's unfed passes are those a run would refuse, each with the names it lacks, checked with what the
2175    /// passes before it provide; what it holds after counts only the passes that can run.
2176    #[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        // load provides the mesh bake needs; a removed name is taken as kept
2183        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        // no file: load is refused, and bake, after it, lacks the mesh load would have provided; neither adds a name
2191        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        // a pass that may lack its inputs is skipped when it finds none, providing nothing, and refused when it finds
2202        // some
2203        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    /// Each step's time is measured around its own run (a 30 ms run is at least 30 ms, the manager's checks apart);
2323    /// the report's time adds the steps up and its text shows each step's; a step a part skips takes none.
2324    #[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        // reports compare without their times (the determinism comparison); times compare as themselves
2381        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    /// A target's rules read back by kind, its checks left out; its checked resources too.
2390    #[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    /// A pass taking in a function's log keeps everything it reported: counts, approximations, omissions and drops
2401    /// add up, removals, placed entities and their sources join.
2402    #[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    /// A conversion runs the stated pipeline as written and checks what it leaves; the report names the target and the
2461    /// pipeline. Partial keeps a name the target does not list.
2462    #[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    /// What the target still refuses after the stated pipeline is an error that says nothing in the pipeline lowers it:
2488    /// the manager adds no pass.
2489    #[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        // Full refuses a name the target does not list
2494        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        // stating what it accepts makes it pass
2503        let accepting = gltf().legal("game.thing");
2504        m.convert(&mut world(), &pipeline, &accepting, Mode::Full)
2505            .unwrap();
2506        // a pipeline that leaves an illegal name
2507        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(); // File(1): legal as it is
2531        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)); // fails the check
2538            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        // a stated pass that lowers it
2546        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)); // loads into the unlucky mesh
2565        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    /// Despawns entities holding toy.layered; with `strip`, also takes toy.file off the entities it keeps.
2578    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    /// Takes toy.layered off every entity without declaring it; `opted` says it removes by log, `record` that it
2604    /// records the removal there.
2605    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                // recorded but still present elsewhere, or never present: fine
2623                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        // vanished unrecorded, or recorded by a pass that did not opt in: an error as ever
2640        assert_eq!(run(true, false).unwrap_err().to_string(), undeclared);
2641        assert_eq!(run(false, true).unwrap_err().to_string(), undeclared);
2642    }
2643
2644    /// Places what `which` says: a new entity, the world's first (not its own) or a new one it despawns again.
2645    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            // an intermediate: something placed from it, then it is consumed
2662            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        // a consumed intermediate with output placed from it passes; what came from it is placed
2687        assert_eq!(run("consumed"), Ok(2));
2688    }
2689
2690    /// Attributes an approximation to what `which` says: a live entity, or one it despawns again.
2691    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    /// The entities an absorbed log attributed approximations to join, and an unattributed approximation names none.
2727    #[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    /// One entity with toy.layered, toy.file and game.thing (culled), one with toy.mesh and toy.file (kept).
2748    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        // undeclared: a name gone with its entity is still an undeclared removal
2768        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        // declared: every holder despawned, so the names may go; the report keeps all of them
2774        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        // a surviving entity that lost a name is not covered by despawning
2779        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        // names it adds are checked as for any pass
2785        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    /// Inserts `toy.env` (`add`) or takes it away, declaring it or not.
2815    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        // inserted without declaring it: refused by name
2866        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        // declared: reported with the component names
2880        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        // replacing one that is there is a write: refused undeclared, accepted declared (no change of names)
2892        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        // taken away undeclared: refused, also by a pass that despawns entities (no entity held it)
2916        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        // a refused pass has still run: the world is as it left it
2930        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        // a pass may require a resource
2942        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    /// Makes odd meshes even in place.
2968    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        // legal values need nothing
3013        m.convert(&mut mesh_world(2), &Pipeline::default(), &even, Mode::Partial)
3014            .unwrap();
3015        // without the pass the failing value stays, refused
3016        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    /// Removes toy.layered only when made with `lowers=true`: its declarations follow its options.
3027    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    /// A stated pass is made with the options the pipeline gives it, and its declarations come from that instance.
3057    #[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        // load makes Mesh(3) from File(3); the target wants even meshes: evens, stated after load, fixes it
3126        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        // a produced value the check accepts needs nothing more
3146        let load = m.registry.parse("load").unwrap();
3147        m.convert(&mut w_with_file(2), &load, &t, Mode::Partial).unwrap();
3148        // a produced value the pipeline does not fix is refused after it
3149        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    /// A table of rows a classifier fills in: `None` is a row not classified yet.
3166    #[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    /// Classifies every row of toy.table (in place) and settles it.
3173    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    /// Adds an unclassified row to toy.table.
3199    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        // a table the rule accepts needs nothing
3246        m.convert(
3247            &mut table_world(vec![Some(1)]),
3248            &Pipeline::default(),
3249            &classified(),
3250            Mode::Partial,
3251        )
3252        .unwrap();
3253        // nothing classifies: refused, naming the rule
3254        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    /// A covered name is legal where its cover is: the uncovered entity is reported, by its cover's words and count,
3271    /// and kept in Partial; in Full it is illegal, the covered one still not.
3272    #[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        // every entity covered: nothing to report
3291        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        // `lazy` modifies toy.mesh but changes nothing: the check after the pipeline names the rule and the pass
3298        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        // add-row after classify would leave a row unclassified: refused before anything runs
3345        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        // the other way round is fine, and so is classifying again after adding
3359        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    /// The verifier runs after a pass with the pass checks (a world the pass left invalid is refused, naming the
3403    /// pass), not without them; they are on by default in debug builds only.
3404    #[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    /// Reads the thread count, a setting it declares: what it ran on, as a count.
3435    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    /// The program states its thread count once, on the manager: the manager states it in the world it runs, the
3466    /// pass reads it there, and the step's report says it (`threads 3`), outside the report's equality.
3467    #[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        // a world the manager already stated them in is run again as it is
3483        manager.run(&mut w, &pipeline, &mut RunReport::default()).unwrap();
3484    }
3485
3486    /// A pass that declares a setting the program states nowhere is refused before it runs, naming the pass and the
3487    /// setting; one that is skipped for want of its inputs needs none.
3488    #[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    /// A world that already states another thread count than the program's is a disagreement, refused naming both.
3508    #[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}