Skip to main content

primase_core/
convert.rs

1//! Convert: one reader, the program's passes, one writer, and the loop that drives them ([`convert_parts`],
2//! [`convert`]).
3//!
4//! The loop owns the streaming: it reads the world and runs the program's selection on it, lets a [`PartSource`] plan
5//! the parts from that world (geometry not loaded yet), then loads each part through the reader (its roots and the
6//! context its passes need), runs the program's passes on it once, checks the part against what the writer writes
7//! without changing it, and hands the part's own output to the writer read-only, one part in memory at a time. It
8//! adds no pass of its own: what the writer doesn't write is reported as a notice, and values it can't encode are an
9//! error. It does no I/O of its own: reading and writing are the reader's and the writer's, and what they report comes
10//! back in the [`ConvertReport`]: every part's pass reports whole ([`PartReport`]), and the notices the reader and
11//! writer give ([`Notice`]). A conversion that fails keeps what it had reported ([`ConvertError`]).
12//!
13//! How the content is divided is a choice: [`WholeWorld`] is one part; [`PlannedParts`] takes the parts a partition
14//! pass (as `partition-grid`) put in the selected world's [`PartitionPlan`]; a destination may plan its own.
15//!
16//! Every part states whether it is the whole of what is written ([`Part::whole`]): the source says so, as it alone
17//! knows how it divides the content, and the part's passes read it in [`PartScope::whole`]; an operation that needs
18//! the whole (a map's floor, its spawn, a budget over all of it) refuses a part that is not, unless the decision was
19//! made over every part: [`convert_parts_summarized`] goes over the parts twice, the first time for their summaries,
20//! merged and decided on once ([`crate::summary`]), the second time to write each part with those decisions. A world
21//! composed directly, outside this loop (no `core.part_scope` resource), is by definition the whole of what is
22//! written.
23use std::collections::{BTreeMap, BTreeSet};
24use std::time::{Duration, Instant};
25
26use crate::pass::{PassManager, Pipeline, RunReport, Target};
27use crate::summary::SummaryRegistry;
28use crate::{Entity, Resource, World};
29
30pub type Error = Box<dyn std::error::Error + Send + Sync>;
31pub type Result<T> = std::result::Result<T, Error>;
32
33/// Labelled lines for the log: what a reader read or loaded, what a writer wrote.
34pub type Report = Vec<(String, String)>;
35
36/// A source of worlds: the world to plan from, then the parts asked for, loaded.
37pub trait Reader {
38    /// Optional declaration of this reader's caller-stated loading scope. The declaration reuses
39    /// pass options and supplies no default for a required scope choice.
40    fn scope_spec(&self) -> Option<&'static crate::read_scope::ReadScopeSpec> {
41        None
42    }
43    /// Effective checked scope, including source alternatives left unloaded. A scoped reader
44    /// also retains it in [`crate::read_scope::ReadScopes`] and reports its selection notice.
45    fn read_scope(&self) -> Option<crate::read_scope::ReadScope> {
46        None
47    }
48    /// The world to plan from: placements, prototypes and their state; geometry and textures not loaded yet.
49    fn read(&mut self) -> Result<(World, Report)>;
50    /// The subset of `world` that `roots` reach, with everything a writer needs loaded (meshes, textures); entity ids
51    /// stay `world`'s, and what loading spawns is reached through the roots.
52    fn load(&mut self, world: &World, roots: &BTreeSet<Entity>) -> Result<(World, Report)>;
53    /// [`Reader::load`] of every entity of `world`, which it takes: the loaded world of a program that wants the whole
54    /// source. The default is that load, so `world` and the world it returns are alive together; a reader whose load
55    /// copies what it keeps overrides this to load `world` in place, and the source is held once.
56    fn load_all(&mut self, world: World) -> Result<(World, Report)> {
57        let roots: BTreeSet<Entity> = world.entities().collect();
58        self.load(&world, &roots)
59    }
60    /// The component namespaces it may put in a world (`core`, its own dialect): a reader reads, it lowers nothing
61    /// (no `pbr.*` unless its source format is PBR). [`check_emits`] holds it to that after reading, and
62    /// [`check_load_emits`] holds what each load adds to it.
63    fn emits(&self) -> &'static [&'static str];
64    /// A prototype's box (core frame) before its geometry is loaded, where the reader knows it.
65    fn prototype_box(&self, world: &World, prototype: Entity) -> Option<[[f64; 3]; 2]>;
66    /// What it noticed since it was last asked (source it can't read, what it tolerated), taken after reading and
67    /// after each load. None by default.
68    fn notices(&mut self) -> Vec<Notice> {
69        Vec::new()
70    }
71}
72
73/// Whether `name` (a component or resource name) is in one of the namespaces in `emits`.
74fn emitted(name: &str, emits: &[&str]) -> bool {
75    emits.contains(&name.split('.').next().unwrap_or(""))
76}
77
78/// An error naming the first component or resource of `w` outside the reader's namespaces (`when`: after what).
79pub fn check_emits(w: &World, emits: &[&str], when: &str) -> Result<()> {
80    if let Some(name) = w.component_names().into_iter().find(|n| !emitted(n, emits)) {
81        return Err(format!("{when}: the reader put {name} in the world (it emits {emits:?})").into());
82    }
83    if let Some(name) = w.resource_names().into_iter().find(|n| !emitted(n, emits)) {
84        return Err(
85            format!("{when}: the reader put the resource {name} in the world (it emits {emits:?})").into(),
86        );
87    }
88    Ok(())
89}
90
91/// An error naming the first component a load added outside the reader's namespaces: a component of the loaded `part`
92/// on an entity the `selected` world didn't have it on (an entity the load spawned, or one the selection hadn't given
93/// it). What the program's selection put there is the program's, not the reader's, so a selection that lowers is let
94/// through and a reader whose load adds the same name to other entities is not. It compares which entities carry each
95/// name, not values: a load that overwrites a component the selection already put on that entity is not seen.
96pub fn check_load_emits(selected: &World, part: &World, emits: &[&str]) -> Result<()> {
97    let before: BTreeSet<&str> = selected.resource_names().into_iter().collect();
98    if let Some(name) = part
99        .resource_names()
100        .into_iter()
101        .find(|n| !emitted(n, emits) && !before.contains(n))
102    {
103        return Err(format!(
104            "load: the reader put the resource {name} in the part, which the selection had not (it emits {emits:?})"
105        )
106        .into());
107    }
108    for name in part.component_names() {
109        if emitted(name, emits) {
110            continue;
111        }
112        let before: BTreeSet<Entity> = selected.entities_named(name).into_iter().collect();
113        if let Some(e) = part
114            .entities_named(name)
115            .into_iter()
116            .find(|e| !before.contains(e))
117        {
118            return Err(format!(
119                "load: the reader put {name} on {e:?}, which the selection had not (it emits {emits:?})"
120            )
121            .into());
122        }
123    }
124    Ok(())
125}
126
127/// What a part asks the reader to load: its roots, what it shares and its context. What it shares is another part's,
128/// so it cannot also be one of its roots (sharing would take away that root's ownership) or its context; the part
129/// source is refused, naming the entity.
130fn asked(
131    roots: &BTreeSet<Entity>,
132    shared: &BTreeSet<Entity>,
133    context: &BTreeSet<Entity>,
134) -> Result<BTreeSet<Entity>> {
135    for (set, what) in [(roots, "one of its roots"), (context, "its context")] {
136        if let Some(e) = shared.intersection(set).next() {
137            return Err(format!("part source: a part shares {e:?}, which is also {what}").into());
138        }
139    }
140    Ok(roots.iter().chain(shared).chain(context).copied().collect())
141}
142
143/// One part to load: the roots it outputs, and the context its passes need around them (neighbouring placements a
144/// collision or normal operation reads). The reader loads both; only the roots' closure, with what the passes placed
145/// for it, goes to the writer, so content a part only reads is written by the part that owns it. The passes see which
146/// is which in the part's [`PartScope`]; content they make from a context entity ([`crate::pass::PassLog::place_from`])
147/// is left to the part that owns that entity.
148#[derive(Clone, Debug, PartialEq)]
149pub struct Part {
150    pub roots: BTreeSet<Entity>,
151    /// Entities the part writes a share of and another part owns (a placement whose elements are divided among the
152    /// parts holding them): loaded and output with the roots, but content its passes make from them
153    /// ([`crate::pass::PassLog::place_from`]) is their owner's, left to it as content made from the context is, so it is
154    /// written once whatever the division.
155    pub shared: BTreeSet<Entity>,
156    pub context: BTreeSet<Entity>,
157    /// Whether this part is the whole of what is written: every selected entity, nothing in another part. Stated by
158    /// the source (no default): an operation that needs the whole refuses a part that is not.
159    pub whole: bool,
160    /// The ground area `[west, south, east, north]` (metres, half-open) its context covers: every placement and world
161    /// collision that reaches it is loaded (roots or context), so an operation reading the world around its own content
162    /// (a marker's search for ground) can tell whether the part holds all of it. `None`: the source states none (the
163    /// whole world, or a source that does not plan by area).
164    pub area: Option<[f64; 4]>,
165}
166
167/// How a conversion divides the selected world into parts.
168pub trait PartSource {
169    /// Plan from the selected world, its geometry not loaded yet: `boxes` gives a prototype's box (core frame) where
170    /// the reader knows it.
171    fn plan(&mut self, world: &World, boxes: &dyn Fn(Entity) -> Option<[[f64; 3]; 2]>) -> Result<()>;
172    /// The next part to load, `None` when every part is done.
173    fn next_part(&mut self) -> Result<Option<Part>>;
174    /// The part just asked for, as the writer got it: for a source that refines its plan from loaded geometry.
175    fn written(&mut self, _part: &World) -> Result<()> {
176        Ok(())
177    }
178    /// Give the same parts again, from the first, as planned ([`convert_parts_summarized`] goes over them twice). A
179    /// source that cannot (its parts depend on what was written) refuses.
180    fn restart(&mut self) -> Result<()> {
181        Err("this part source gives its parts once: it cannot be gone over twice".into())
182    }
183}
184
185/// One part: every entity of the selected world.
186#[derive(Debug, Default)]
187pub struct WholeWorld {
188    planned: BTreeSet<Entity>,
189    given: bool,
190}
191
192impl PartSource for WholeWorld {
193    fn plan(&mut self, world: &World, _boxes: &dyn Fn(Entity) -> Option<[[f64; 3]; 2]>) -> Result<()> {
194        self.planned = world.entities().collect();
195        self.given = false;
196        Ok(())
197    }
198    fn next_part(&mut self) -> Result<Option<Part>> {
199        if self.given {
200            return Ok(None);
201        }
202        self.given = true;
203        Ok(Some(Part {
204            roots: self.planned.clone(),
205            shared: BTreeSet::new(),
206            context: BTreeSet::new(),
207            whole: true,
208            area: None,
209        }))
210    }
211    fn restart(&mut self) -> Result<()> {
212        self.given = false;
213        Ok(())
214    }
215}
216
217/// `core.partition_plan`: how a pass divided the content into output parts, in order. Each group owns its roots
218/// (every root in one group) and names the context its passes need (roots of other groups, loaded but written by
219/// their owner).
220#[derive(Clone, Debug, Default, PartialEq)]
221pub struct PartitionPlan {
222    /// What made it (`partition-grid{cell=200}`).
223    pub by: String,
224    pub groups: Vec<PartitionGroup>,
225}
226
227/// One part of a [`PartitionPlan`].
228#[derive(Clone, Debug, Default, PartialEq)]
229pub struct PartitionGroup {
230    /// A name for reports (`cell 3,-2`).
231    pub key: String,
232    pub roots: BTreeSet<Entity>,
233    pub context: BTreeSet<Entity>,
234    /// The ground area `[west, south, east, north]` (metres, half-open) whose content the group's operations read:
235    /// what it owns and its halo. [`PlannedParts`] adds to the context every placement whose authored box reaches it,
236    /// and every world collision soup whose points' box does.
237    pub area: Option<[f64; 4]>,
238}
239
240/// `core.part_scope`: which entities of a part the conversion writes (its roots, and what they reach) and which it
241/// only loaded as context, for passes that make content per source entity. Put in the part's world before its passes
242/// and taken out before writing.
243#[derive(Clone, Debug, PartialEq)]
244pub struct PartScope {
245    pub roots: BTreeSet<Entity>,
246    /// The part's [`Part::shared`]: output with the roots, owned by another part.
247    pub shared: BTreeSet<Entity>,
248    pub context: BTreeSet<Entity>,
249    /// The part's [`Part::whole`]: whether it is the whole of what is written.
250    pub whole: bool,
251    /// The part's [`Part::area`]: the ground area its context covers, if its source states one.
252    pub area: Option<[f64; 4]>,
253    /// The first entity index the part's passes make ([`World::next_index`] as loaded): an entity below it was loaded
254    /// (its id is the planning world's, the same in every part); one at or above it was made in this part. Set by the
255    /// conversion; `None` in a scope composed by hand (every entity taken as loaded).
256    pub first_made: Option<u32>,
257    /// What the part's passes declared as its output so far ([`crate::pass::PassLog::place`]): `true`, the part's own
258    /// (placed with no source, or from an entity in the roots' closure when it was placed); `false`, placed from an
259    /// entity it only loaded as context or shares ([`crate::pass::PassLog::place_from`]: that part's). Classified when
260    /// declared, so a source a later pass removes does not change it. The pass manager adds each pass's declarations after it
261    /// runs ([`PartScope::declare`]); empty in a scope composed by hand.
262    pub placed: BTreeMap<Entity, bool>,
263}
264
265/// A part's output as conversion writes it ([`PartScope::output`]).
266#[derive(Clone, Debug, Default, PartialEq)]
267pub struct PartOutput {
268    /// What is written: the closure of the roots and of `placed`.
269    pub keep: BTreeSet<Entity>,
270    /// What its passes placed for it: content of its own nothing need reference (a marker), and content made from an
271    /// entity it owns.
272    pub placed: BTreeSet<Entity>,
273    /// What its passes placed from an entity it only loaded as context: the owner's, not written here.
274    pub from_context: BTreeSet<Entity>,
275}
276
277impl PartScope {
278    /// The part's output in `w`: the closure of its roots, of what it shares and of what its passes placed, but for
279    /// what they placed from an entity it does not own (its context's or a shared entity's: that part's). Context is
280    /// not output unless the output reaches it; what the passes made and did not place is not output unless it reaches
281    /// it. The one rule: the conversion writes it, and a pass that needs what a part owns (a summary of its content)
282    /// asks it.
283    pub fn output(&self, w: &World) -> PartOutput {
284        let mut out = PartOutput::default();
285        for (&e, &own) in self.placed.iter().filter(|(e, _)| w.is_alive(**e)) {
286            if own {
287                out.placed.insert(e);
288            } else {
289                out.from_context.insert(e);
290            }
291        }
292        out.keep = w.closure(self.roots.iter().chain(&self.shared).chain(&out.placed).copied());
293        out
294    }
295
296    /// What the part owns in `w` now: the closure of its roots and of what its passes declared as its own, but for
297    /// what it shares (another part's, also where something it owns reaches it).
298    pub fn owned(&self, w: &World) -> BTreeSet<Entity> {
299        let own = self.placed.iter().filter(|(e, own)| **own && w.is_alive(**e));
300        let mut owned = w.closure(self.roots.iter().copied().chain(own.map(|(e, _)| *e)));
301        owned.retain(|e| !self.shared.contains(e));
302        owned
303    }
304
305    /// A pass's declarations (`placed`, with their sources `placed_from`) added to `w`'s part scope, each classified
306    /// now: the part's own when placed with no source or from an entity the part owns, else its context's. The part
307    /// owns what [`PartScope::owned`] gives after the pass, what it gave before the pass (`owned_before`: a source
308    /// the pass consumed still counts), and this pass's own declarations, so a chain of them (placed from one placed
309    /// from a root) is the part's whatever their order. Nothing without a scope (a world outside conversion).
310    pub fn declare(
311        w: &mut World,
312        placed: &BTreeSet<Entity>,
313        placed_from: &BTreeMap<Entity, Entity>,
314        owned_before: &BTreeSet<Entity>,
315    ) {
316        let Some(scope) = w.resource::<PartScope>() else {
317            return;
318        };
319        if placed.is_empty() {
320            return;
321        }
322        let shared = scope.shared.clone();
323        let mut owned = scope.owned(w);
324        owned.extend(owned_before.difference(&shared));
325        let mut own: BTreeSet<Entity> = BTreeSet::new();
326        // to a fixpoint: one declared from another of this pass's own is the part's too
327        loop {
328            let found: Vec<Entity> = placed
329                .iter()
330                .filter(|e| !own.contains(e))
331                .filter(|e| placed_from.get(e).is_none_or(|s| owned.contains(s)))
332                .copied()
333                .collect();
334            if found.is_empty() {
335                break;
336            }
337            // the ids themselves too: one the pass made, placed from and then consumed is no longer in the world
338            // what it shares stays another part's, also where something declared here reaches it
339            owned.extend(found.iter().copied());
340            owned.extend(w.closure(found.iter().copied()).difference(&shared));
341            own.extend(found);
342        }
343        let classified: Vec<(Entity, bool)> = placed.iter().map(|e| (*e, own.contains(e))).collect();
344        if let Some(scope) = w.resource_mut::<PartScope>() {
345            scope.placed.extend(classified);
346        }
347    }
348}
349
350/// A part loaded for its passes ([`load_part`]): its world with its [`PartScope`] in it, and what loading it reported.
351#[derive(Debug)]
352pub struct LoadedPart {
353    pub world: World,
354    /// The reader's report on loading it.
355    pub load: Report,
356    /// Every entity as loaded, before anything was handed on to it: what its passes then make is told apart from it.
357    loaded: BTreeSet<Entity>,
358}
359
360/// A part loaded and prepared ([`prepare_part`]): what a writer would get, and the part's report.
361#[derive(Debug)]
362pub struct PreparedPart {
363    /// The part's output ([`PartScope::output`]): the closure of its roots and of what its passes placed for it.
364    pub world: World,
365    /// Its roots, context, load and passes' reports, and what it kept of what its passes made; `ordinal` is 0 and the
366    /// writer's checks (`unsupported`) are empty: a part prepared on its own has no place in a conversion's order and no
367    /// writer.
368    pub report: PartReport,
369}
370
371/// The part of the planning `world` that `part` asks for (its roots, what it shares and its context), loaded through
372/// `reader`, held to the reader's namespaces ([`check_load_emits`]) and verified (`when` names the step in an error).
373/// The reader's notices go to `notices`.
374fn load_checked(
375    reader: &mut dyn Reader,
376    world: &World,
377    part: &Part,
378    manager: &PassManager,
379    when: &str,
380    notices: &mut Vec<Notice>,
381) -> Result<(World, Report)> {
382    let asked = asked(&part.roots, &part.shared, &part.context)?;
383    let loaded = reader.load(world, &asked);
384    notices.extend(reader.notices());
385    let (loaded, report) = loaded?;
386    check_load_emits(world, &loaded, reader.emits())?;
387    // what was loaded is checked before any pass reads it
388    manager.verify(&loaded, when)?;
389    Ok((loaded, report))
390}
391
392/// `loaded` (a part as [`load_checked`] loaded it) with what `hand_on` gives it (a conversion's decisions) and then its
393/// [`PartScope`], which records where the part's own entities begin.
394fn scoped(mut loaded: World, part: Part, load: Report, hand_on: impl FnOnce(&mut World)) -> LoadedPart {
395    let entities: BTreeSet<Entity> = loaded.entities().collect();
396    hand_on(&mut loaded);
397    let first_made = Some(loaded.next_index());
398    let Part {
399        roots,
400        shared,
401        context,
402        whole,
403        area,
404    } = part;
405    loaded.insert_resource(PartScope {
406        roots,
407        shared,
408        context,
409        whole,
410        area,
411        first_made,
412        placed: BTreeMap::new(),
413    });
414    LoadedPart {
415        world: loaded,
416        load,
417        loaded: entities,
418    }
419}
420
421/// Load `part` of the planning `world` (what the program's selection left, geometry not loaded) through `reader`,
422/// ready for its passes: what conversion does with each part before running them, for a program that loads parts in
423/// an order of its own, as many times as it likes (a viewer keeping the parts around its camera). The reader's notices
424/// go to `notices`.
425pub fn load_part(
426    reader: &mut dyn Reader,
427    world: &World,
428    part: Part,
429    manager: &PassManager,
430    notices: &mut Vec<Notice>,
431) -> Result<LoadedPart> {
432    let (loaded, load) = load_checked(reader, world, &part, manager, "load", notices)?;
433    Ok(scoped(loaded, part, load, |_| {}))
434}
435
436/// Run the program's `passes` on a loaded part (their report in `explicit`) and keep only its output
437/// ([`PartScope::output`]): the closure of its roots and of what the passes placed for it. Content made from its context
438/// is left to the part that owns that context, and what nothing kept reaches (materials of dropped geometry, of a
439/// discarded level of detail) is dropped; both are counted in `kept` by their components, with the entities the passes
440/// placed. The scope is taken out of the part.
441pub fn prepare_loaded(
442    loaded: LoadedPart,
443    manager: &PassManager,
444    passes: &Pipeline,
445    explicit: &mut RunReport,
446    kept: &mut BTreeMap<String, u64>,
447) -> Result<World> {
448    let LoadedPart {
449        world: mut part,
450        loaded,
451        ..
452    } = loaded;
453    manager.run_part(&mut part, passes, explicit)?;
454    let scope = part
455        .remove_resource::<PartScope>()
456        .ok_or("the part's scope was taken out by its passes")?;
457    let PartOutput {
458        keep,
459        placed,
460        from_context,
461    } = scope.output(&part);
462    let names: Vec<String> = part.component_names().into_iter().map(str::to_owned).collect();
463    let kinds = |e: Entity| {
464        names
465            .iter()
466            .filter(|n| part.entity_has_named(e, n))
467            .map(String::as_str)
468            .collect::<Vec<_>>()
469            .join(" + ")
470    };
471    for e in part.entities().filter(|e| !loaded.contains(e)) {
472        let what = if placed.contains(&e) {
473            "entities the passes placed"
474        } else if from_context.contains(&e) {
475            "entities placed from context, left to the part that owns their source"
476        } else if keep.contains(&e) {
477            continue;
478        } else {
479            "made entities nothing keeps, dropped"
480        };
481        *kept.entry(format!("{what} ({})", kinds(e))).or_default() += 1;
482    }
483    part.retain(&keep);
484    manager.verify(&part, "the passes")?;
485    Ok(part)
486}
487
488/// [`load_part`] then [`prepare_loaded`]: `part` of the planning `world` loaded through `reader` and prepared by
489/// `passes`, as conversion prepares each part before handing it to its writer. The reader's notices go to `notices`.
490pub fn prepare_part(
491    reader: &mut dyn Reader,
492    world: &World,
493    part: Part,
494    manager: &PassManager,
495    passes: &Pipeline,
496    notices: &mut Vec<Notice>,
497) -> Result<PreparedPart> {
498    let mut report = PartReport {
499        roots: part.roots.clone(),
500        shared: part.shared.clone(),
501        context: part.context.clone(),
502        ..PartReport::default()
503    };
504    let loaded = load_part(reader, world, part, manager, notices)?;
505    report.load = loaded.load.clone();
506    let world = prepare_loaded(loaded, manager, passes, &mut report.explicit, &mut report.kept)?;
507    Ok(PreparedPart { world, report })
508}
509
510impl Resource for PartScope {
511    const NAME: &'static str = "core.part_scope";
512    fn references(&self, out: &mut Vec<Entity>) {
513        out.extend(
514            self.roots
515                .iter()
516                .chain(&self.shared)
517                .chain(&self.context)
518                .copied(),
519        );
520    }
521    fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
522        self.roots.retain(|&e| keep(e));
523        self.shared.retain(|&e| keep(e));
524        self.context.retain(|&e| keep(e));
525    }
526}
527
528/// The resources a conversion makes for its own work, and the run settings the pass manager states
529/// ([`crate::pass::PassManager::with_settings`]): never content a writer is to write.
530pub const OPERATIONAL_RESOURCES: &[&str] = &[
531    PartitionPlan::NAME,
532    PartScope::NAME,
533    <primase_foundations::parallel::Threads as Resource>::NAME,
534];
535
536impl Resource for PartitionPlan {
537    const NAME: &'static str = "core.partition_plan";
538    fn to_json(&self) -> std::result::Result<Option<serde_json::Value>, crate::JsonError> {
539        let groups: Vec<serde_json::Value> = self
540            .groups
541            .iter()
542            .map(|g| {
543                serde_json::json!({
544                    "key": g.key,
545                    "roots": g.roots.iter().map(|e| e.index()).collect::<Vec<_>>(),
546                    "context": g.context.iter().map(|e| e.index()).collect::<Vec<_>>(),
547                    "area": g.area,
548                })
549            })
550            .collect();
551        Ok(Some(serde_json::json!({ "by": self.by, "groups": groups })))
552    }
553    /// Each group's key once: reports and the conversion name a part by it.
554    fn verify(&self, _w: &World) -> std::result::Result<(), String> {
555        let mut keys = BTreeSet::new();
556        match self.groups.iter().find(|g| !keys.insert(g.key.as_str())) {
557            Some(g) => Err(format!("partition group key {:?} twice", g.key)),
558            None => Ok(()),
559        }
560    }
561    fn references(&self, out: &mut Vec<Entity>) {
562        for g in &self.groups {
563            out.extend(g.roots.iter().chain(&g.context).copied());
564        }
565    }
566    fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
567        for g in &mut self.groups {
568            g.roots.retain(|&e| keep(e));
569            g.context.retain(|&e| keep(e));
570        }
571    }
572}
573
574/// The parts of the selected world's [`PartitionPlan`], in its order. A world without one is an error: the program
575/// runs the partition pass in its selection. A group with an area also reads, as context, every other placement whose
576/// prototype's authored box (the reader's, before geometry is loaded) reaches that area, so a long object whose pivot
577/// stands far off is still in its neighbours' operations.
578#[derive(Debug, Default)]
579pub struct PlannedParts {
580    planned: Vec<PartitionGroup>,
581    groups: std::vec::IntoIter<PartitionGroup>,
582    /// Whether the plan has one group (a part is then the whole when it needs no context).
583    single: bool,
584}
585
586/// The ground box `[west, south, east, north]` of a placement's prototype box.
587fn ground_box(t: &crate::Transform, b: [[f64; 3]; 2]) -> [f64; 4] {
588    let mut out = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
589    for k in 0..8 {
590        let c = [b[k & 1][0], b[(k >> 1) & 1][1], b[(k >> 2) & 1][2]];
591        let p: [f64; 3] = std::array::from_fn(|i| {
592            (0..3).map(|j| f64::from(t.cols[j][i]) * c[j]).sum::<f64>() + f64::from(t.cols[3][i])
593        });
594        let (east, north) = (p[0], -p[2]);
595        out = [
596            out[0].min(east),
597            out[1].min(north),
598            out[2].max(east),
599            out[3].max(north),
600        ];
601    }
602    out
603}
604
605impl PartSource for PlannedParts {
606    fn plan(&mut self, world: &World, boxes: &dyn Fn(Entity) -> Option<[[f64; 3]; 2]>) -> Result<()> {
607        let plan = world
608            .resource::<PartitionPlan>()
609            .ok_or("no core.partition_plan in the selected world: run a partition pass in the selection")?;
610        let placed: Vec<(Entity, [f64; 4])> = world
611            .iter::<crate::Instance>()
612            .filter_map(|(e, i)| {
613                let t = world.get::<crate::Transform>(e)?;
614                Some((e, ground_box(t, boxes(i.prototype)?)))
615            })
616            .collect();
617        // the world's collision soups (no placed prototype's), by the ground box of their points
618        let prototypes: BTreeSet<Entity> = world
619            .iter::<crate::Instance>()
620            .map(|(_, i)| i.prototype)
621            .collect();
622        let soups: Vec<(Entity, [f64; 4])> = world
623            .iter::<crate::Collision>()
624            .filter(|(e, c)| !prototypes.contains(e) && !c.positions.is_empty())
625            .map(|(e, c)| {
626                let t = world
627                    .get::<crate::Transform>(e)
628                    .copied()
629                    .unwrap_or(crate::Transform::IDENTITY);
630                let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
631                for p in &c.positions {
632                    let q = t.apply(*p);
633                    let (east, north) = (f64::from(q[0]), -f64::from(q[2]));
634                    b = [b[0].min(east), b[1].min(north), b[2].max(east), b[3].max(north)];
635                }
636                (e, b)
637            })
638            .collect();
639        let mut groups = plan.groups.clone();
640        for g in &mut groups {
641            let Some(a) = g.area else { continue };
642            for &(e, b) in placed.iter().chain(&soups) {
643                if !g.roots.contains(&e) && b[0] < a[2] && b[2] >= a[0] && b[1] < a[3] && b[3] >= a[1] {
644                    g.context.insert(e);
645                }
646            }
647        }
648        self.single = groups.len() == 1;
649        self.planned = groups.clone();
650        self.groups = groups.into_iter();
651        Ok(())
652    }
653    fn restart(&mut self) -> Result<()> {
654        self.groups = self.planned.clone().into_iter();
655        Ok(())
656    }
657    fn next_part(&mut self) -> Result<Option<Part>> {
658        // the whole only when the plan has one group and it needs no context
659        let single = self.single;
660        Ok(self.groups.next().map(|g| Part {
661            whole: single && g.context.is_empty(),
662            roots: g.roots,
663            shared: BTreeSet::new(),
664            context: g.context,
665            area: g.area,
666        }))
667    }
668}
669
670/// A destination. It is handed each part, loaded and through the program's passes: the closure of its roots and of
671/// every entity a pass placed (content nothing references, such as a marker; [`crate::pass::PassLog::place`]).
672/// Other entities the passes made are in it only where something in it references them.
673pub trait Writer {
674    /// What it writes: every part is checked against it before it is written ([`Target::check_prepared`]), without
675    /// changing it. A name it doesn't write is reported; a value of a name it writes that its rule refuses is an error.
676    fn target(&self) -> Target;
677    fn write(&mut self, part: &World) -> Result<()>;
678    /// What it noticed since it was last asked (what it omitted because it doesn't support it), taken after each
679    /// part and before [`Writer::finish`]. None by default.
680    fn notices(&mut self) -> Vec<Notice> {
681        Vec::new()
682    }
683    /// Finish writing (indexes, manifests): its report, and what it noticed while finishing.
684    fn finish(self: Box<Self>) -> Result<Finished>;
685}
686
687/// What a writer's [`Writer::finish`] returns.
688#[derive(Clone, Debug, Default, PartialEq, Eq)]
689pub struct Finished {
690    pub report: Report,
691    /// What it noticed while finishing (an omission it can only tell once every part is in).
692    pub notices: Vec<Notice>,
693}
694
695impl From<Report> for Finished {
696    fn from(report: Report) -> Finished {
697        Finished {
698            report,
699            notices: Vec::new(),
700        }
701    }
702}
703
704/// What a [`Notice`] tells the program.
705#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
706pub enum NoticeKind {
707    /// Something in the world or source the stage doesn't support, left out.
708    Unsupported,
709    /// Something kept, but not exactly.
710    Approximation,
711    /// Something left out or chosen deliberately, by a selection or by what a part keeps.
712    Selection,
713    /// What a stage was doing when it failed.
714    FailureContext,
715    /// What a diagnostic the program turned on found (conservation: a pass's losses differ from what it declared).
716    Diagnostic,
717}
718
719/// The most examples a notice keeps; its count still counts every case.
720pub const NOTICE_EXAMPLES: usize = 8;
721
722/// One thing a conversion stage tells the program: what stage, what kind, why, how many, and a few examples (source
723/// names or ids) of what it concerns.
724#[derive(Clone, Debug, PartialEq, Eq)]
725pub struct Notice {
726    /// The reader, a pass (as in its pipeline), `convert` or the writer.
727    pub stage: String,
728    pub kind: NoticeKind,
729    pub reason: String,
730    pub count: u64,
731    /// At most [`NOTICE_EXAMPLES`].
732    pub examples: Vec<String>,
733    /// The entities an approximation was attributed to ([`crate::pass::PassLog::approximate_on`]), at most
734    /// [`NOTICE_EXAMPLES`]; the count still counts every case. Ids of the world the pass ran on: set only for the passes
735    /// of a whole-world run (the select, decided and finalized runs), empty for the runs of groups and parts, whose
736    /// worlds differ, and for every notice that is not an approximation.
737    pub entities: Vec<Entity>,
738}
739
740impl Notice {
741    pub fn new(stage: impl Into<String>, kind: NoticeKind, reason: impl Into<String>, count: u64) -> Notice {
742        Notice {
743            stage: stage.into(),
744            kind,
745            reason: reason.into(),
746            count,
747            examples: Vec::new(),
748            entities: Vec::new(),
749        }
750    }
751
752    /// Add an example, unless it has [`NOTICE_EXAMPLES`] already.
753    pub fn example(mut self, e: impl Into<String>) -> Notice {
754        if self.examples.len() < NOTICE_EXAMPLES {
755            self.examples.push(e.into());
756        }
757        self
758    }
759}
760
761impl std::fmt::Display for Notice {
762    /// `approximation lower-<dialect>-pbr: specular folded into roughness x 12 (part 0, part 3)`
763    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764        let kind = match self.kind {
765            NoticeKind::Unsupported => "unsupported",
766            NoticeKind::Approximation => "approximation",
767            NoticeKind::Selection => "selection",
768            NoticeKind::FailureContext => "failure",
769            NoticeKind::Diagnostic => "diagnostic",
770        };
771        write!(f, "{kind} {}: {} x {}", self.stage, self.reason, self.count)?;
772        if !self.examples.is_empty() {
773            write!(f, " ({})", self.examples.join(", "))?;
774        }
775        Ok(())
776    }
777}
778
779/// What one part of a conversion went through, in full.
780#[derive(Debug, Default)]
781pub struct PartReport {
782    /// Its place in the order the writer asked for the parts, from 0.
783    pub ordinal: usize,
784    /// The roots asked for. Entity ids are the planning world's; a pass's own entities are the part's.
785    pub roots: BTreeSet<Entity>,
786    /// What it shares with the part that owns it ([`Part::shared`]).
787    pub shared: BTreeSet<Entity>,
788    /// The context loaded with them, not written by this part.
789    pub context: BTreeSet<Entity>,
790    /// The reader's report on loading it.
791    pub load: Report,
792    /// The program's passes.
793    pub explicit: RunReport,
794    /// Names the writer doesn't write, with how many entities held them ([`Target::check_prepared`]).
795    pub unsupported: Vec<(String, u64)>,
796    /// Resources the writer doesn't write.
797    pub unsupported_resources: Vec<String>,
798    /// What the part kept and left out of what the passes made, by its components: `entities the passes placed (..)`,
799    /// `made entities nothing keeps, dropped (..)`.
800    pub kept: BTreeMap<String, u64>,
801}
802
803/// One group of a conversion with groups ([`Gathered`]): its passes' report, what the writer's target doesn't write of
804/// it, and its bulk payload as written.
805#[derive(Clone, Debug, Default)]
806pub struct GroupReport {
807    pub ordinal: usize,
808    pub run: RunReport,
809    pub unsupported: Vec<(String, u64)>,
810    pub unsupported_resources: Vec<String>,
811    pub written: BTreeMap<String, u64>,
812}
813
814/// What a conversion did.
815#[derive(Debug, Default)]
816pub struct ConvertReport {
817    /// The reader's report on reading.
818    pub read: Report,
819    /// The selection passes.
820    pub select: RunReport,
821    /// The reader's reports on loading, part by part.
822    pub loads: Report,
823    /// In a conversion with summaries ([`convert_parts_summarized`]): the reader's reports on loading each part for its
824    /// summary, the summary passes' report per part, and the decision passes' report.
825    pub summary_loads: Report,
826    pub summarized: Vec<RunReport>,
827    pub decided: RunReport,
828    /// In a conversion with groups ([`convert_parts_gathered`]): each group's report, in the order written, and the
829    /// finalizing decision passes' report.
830    pub groups: Vec<GroupReport>,
831    pub finalized: RunReport,
832    /// In a conversion with groups: the reader's reports on loading each part for the gather traversal, and the gather
833    /// passes' report per part.
834    pub gather_loads: Report,
835    pub gather_runs: Vec<RunReport>,
836    /// The bytes of every part's summaries kept until the merge (the most the first traversal holds of them), and of
837    /// the merged summaries handed to every part, as the summaries state them ([`crate::summary::Summary::retained_bytes`]):
838    /// logical-data accounting, not a measured or bounded memory peak.
839    pub summary_bytes: usize,
840    pub merged_summary_bytes: usize,
841    pub parts: usize,
842    /// The bulk payload of every part, in the order written.
843    pub part_sizes: Vec<PartSize>,
844    /// Every pass's counts over all parts, as `<pass>: <what>`, and what the parts kept of what the passes made
845    /// (`convert: <what>`). A sum of [`ConvertReport::part_reports`], kept for callers that print totals.
846    pub counts: BTreeMap<String, u64>,
847    /// Each part's reports, whole, in the order written.
848    pub part_reports: Vec<PartReport>,
849    /// What the reader and the writer noticed, in the order they reported it.
850    pub reported: Vec<Notice>,
851    /// The writer's report.
852    pub written: Report,
853    /// Each stage of the conversion in the order it ran, named as [`ConvertError::during`] names it (`read`, `select`,
854    /// `plan`, `part N: load`, `part N: passes`, `part N: target check`, `part N: write`, `finish`, and the summary,
855    /// gather and group stages), with its wall-clock time: for a program's log and for profiling, not part of what the
856    /// conversion makes. A failed conversion's partial report ends with the stage that failed.
857    pub stages: Vec<(String, Duration)>,
858}
859
860impl ConvertReport {
861    /// Every approximation, as `<pass>: <what>`, over the selection and every part, from the reports kept.
862    pub fn approximations(&self) -> BTreeMap<String, u64> {
863        let mut out = BTreeMap::new();
864        let runs = [&self.select, &self.decided, &self.finalized]
865            .into_iter()
866            .chain(self.groups.iter().map(|g| &g.run))
867            .chain(self.part_reports.iter().map(|p| &p.explicit));
868        for run in runs {
869            for step in &run.steps {
870                for (k, v) in &step.log.approximations {
871                    *out.entry(format!("{}: {k}", step.pass)).or_default() += v;
872                }
873            }
874        }
875        for (traversal, runs) in self.preparing_traversals() {
876            for step in runs.iter().flat_map(|r| &r.steps) {
877                for (k, v) in &step.log.approximations {
878                    *out.entry(format!("{} ({traversal}): {k}", step.pass))
879                        .or_default() += v;
880                }
881            }
882        }
883        out
884    }
885
886    /// The traversals that prepare what a later stage uses and write nothing: the summary traversal's and the gather
887    /// traversal's runs, part by part. Their notices are named by traversal, so a pass the write traversal runs again
888    /// is not summed with them.
889    fn preparing_traversals(&self) -> [(&'static str, &Vec<RunReport>); 2] {
890        [
891            ("summary traversal", &self.summarized),
892            ("gather traversal", &self.gather_runs),
893        ]
894    }
895
896    /// Every notice: the reader's and the writer's, then the passes' approximations, omissions and drops (what they
897    /// left out on purpose: a selection, `<measure>: <why>`), what the parts left out and what the writer doesn't write,
898    /// one notice per stage and reason over all parts, with the parts as examples.
899    pub fn notices(&self) -> Vec<Notice> {
900        let mut out = self.reported.clone();
901        let mut derived: BTreeMap<(String, NoticeKind, String), (u64, Vec<String>)> = BTreeMap::new();
902        // the entities approximations of the whole-world runs were attributed to, by the notice they join
903        let attributed: std::cell::RefCell<BTreeMap<(String, NoticeKind, String), BTreeSet<Entity>>> =
904            std::cell::RefCell::new(BTreeMap::new());
905        let mut add = |stage: &str, kind: NoticeKind, reason: &str, n: u64, part: Option<String>| {
906            let (count, examples) = derived
907                .entry((stage.to_owned(), kind, reason.to_owned()))
908                .or_default();
909            *count += n;
910            if let Some(at) = part
911                && examples.len() < NOTICE_EXAMPLES
912            {
913                examples.push(at);
914            }
915        };
916        let whole = [&self.select, &self.decided, &self.finalized]
917            .into_iter()
918            .map(|r| (r, None))
919            .chain(
920                self.groups
921                    .iter()
922                    .map(|g| (&g.run, Some(format!("group {}", g.ordinal)))),
923            );
924        for (run, at) in whole {
925            for step in &run.steps {
926                for (k, v) in &step.log.approximations {
927                    add(&step.pass, NoticeKind::Approximation, k, *v, at.clone());
928                    if at.is_none()
929                        && let Some(on) = step.log.approximated_on.get(k)
930                    {
931                        attributed
932                            .borrow_mut()
933                            .entry((step.pass.clone(), NoticeKind::Approximation, k.clone()))
934                            .or_default()
935                            .extend(on.iter().copied());
936                    }
937                }
938                for (k, v) in &step.log.omitted {
939                    add(&step.pass, NoticeKind::Unsupported, k, *v, at.clone());
940                }
941                for ((m, why), v) in &step.log.drops {
942                    add(
943                        &step.pass,
944                        NoticeKind::Selection,
945                        &format!("{m}: {why}"),
946                        *v,
947                        at.clone(),
948                    );
949                }
950                for g in &step.conservation {
951                    add(&step.pass, NoticeKind::Diagnostic, &g.to_string(), 1, at.clone());
952                }
953            }
954        }
955        for (traversal, runs) in self.preparing_traversals() {
956            for (i, run) in runs.iter().enumerate() {
957                for step in &run.steps {
958                    let stage = format!("{} ({traversal})", step.pass);
959                    let at = || Some(format!("part {i}"));
960                    for (k, v) in &step.log.approximations {
961                        add(&stage, NoticeKind::Approximation, k, *v, at());
962                    }
963                    for (k, v) in &step.log.omitted {
964                        add(&stage, NoticeKind::Unsupported, k, *v, at());
965                    }
966                    for ((m, why), v) in &step.log.drops {
967                        add(&stage, NoticeKind::Selection, &format!("{m}: {why}"), *v, at());
968                    }
969                    for g in &step.conservation {
970                        add(&stage, NoticeKind::Diagnostic, &g.to_string(), 1, at());
971                    }
972                }
973            }
974        }
975        for g in &self.groups {
976            for (name, n) in &g.unsupported {
977                add(
978                    "convert",
979                    NoticeKind::Unsupported,
980                    &format!("{name}: the writer doesn't write it"),
981                    *n,
982                    Some(format!("group {}", g.ordinal)),
983                );
984            }
985            for name in &g.unsupported_resources {
986                add(
987                    "convert",
988                    NoticeKind::Unsupported,
989                    &format!("resource {name}: the writer doesn't write it"),
990                    1,
991                    Some(format!("group {}", g.ordinal)),
992                );
993            }
994        }
995        for p in &self.part_reports {
996            for step in &p.explicit.steps {
997                for (k, v) in &step.log.approximations {
998                    add(
999                        &step.pass,
1000                        NoticeKind::Approximation,
1001                        k,
1002                        *v,
1003                        Some(format!("part {}", p.ordinal)),
1004                    );
1005                }
1006                for (k, v) in &step.log.omitted {
1007                    add(
1008                        &step.pass,
1009                        NoticeKind::Unsupported,
1010                        k,
1011                        *v,
1012                        Some(format!("part {}", p.ordinal)),
1013                    );
1014                }
1015                for ((m, why), v) in &step.log.drops {
1016                    add(
1017                        &step.pass,
1018                        NoticeKind::Selection,
1019                        &format!("{m}: {why}"),
1020                        *v,
1021                        Some(format!("part {}", p.ordinal)),
1022                    );
1023                }
1024                for g in &step.conservation {
1025                    add(
1026                        &step.pass,
1027                        NoticeKind::Diagnostic,
1028                        &g.to_string(),
1029                        1,
1030                        Some(format!("part {}", p.ordinal)),
1031                    );
1032                }
1033            }
1034            for (k, v) in &p.kept {
1035                if k.starts_with("made entities nothing keeps") {
1036                    add(
1037                        "convert",
1038                        NoticeKind::Selection,
1039                        k,
1040                        *v,
1041                        Some(format!("part {}", p.ordinal)),
1042                    );
1043                }
1044            }
1045            for (name, n) in &p.unsupported {
1046                add(
1047                    "convert",
1048                    NoticeKind::Unsupported,
1049                    &format!("{name}: the writer doesn't write it"),
1050                    *n,
1051                    Some(format!("part {}", p.ordinal)),
1052                );
1053            }
1054            for name in &p.unsupported_resources {
1055                add(
1056                    "convert",
1057                    NoticeKind::Unsupported,
1058                    &format!("resource {name}: the writer doesn't write it"),
1059                    1,
1060                    Some(format!("part {}", p.ordinal)),
1061                );
1062            }
1063        }
1064        out.extend(
1065            derived
1066                .into_iter()
1067                .map(|((stage, kind, reason), (count, examples))| Notice {
1068                    entities: attributed
1069                        .borrow()
1070                        .get(&(stage.clone(), kind, reason.clone()))
1071                        .map(|e| e.iter().copied().take(NOTICE_EXAMPLES).collect())
1072                        .unwrap_or_default(),
1073                    stage,
1074                    kind,
1075                    reason,
1076                    count,
1077                    examples,
1078                }),
1079        );
1080        out
1081    }
1082}
1083
1084/// A conversion that failed: the error, where, and everything reported before it: the parts written, the failing
1085/// part's report as far as it got (the passes that ran, the target check), and every notice the reader and writer
1086/// gave, those of the failing call included.
1087#[derive(Debug)]
1088pub struct ConvertError {
1089    pub error: Error,
1090    /// What was being done: `read`, `select`, `plan`, `part N: load`, `part N: passes`, `part N: write`, `finish`.
1091    pub during: String,
1092    pub partial: Box<ConvertReport>,
1093}
1094
1095impl std::fmt::Display for ConvertError {
1096    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1097        self.error.fmt(f)
1098    }
1099}
1100
1101impl std::error::Error for ConvertError {
1102    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1103        Some(self.error.as_ref())
1104    }
1105}
1106
1107/// The stage a conversion is in, and the ones it finished with their wall-clock times ([`ConvertReport::stages`]).
1108#[derive(Debug)]
1109struct Stages {
1110    during: String,
1111    started: Instant,
1112    done: Vec<(String, Duration)>,
1113}
1114
1115impl Stages {
1116    fn new() -> Stages {
1117        Stages {
1118            during: String::new(),
1119            started: Instant::now(),
1120            done: Vec::new(),
1121        }
1122    }
1123
1124    /// The stage before ends and `stage` begins.
1125    fn enter(&mut self, stage: impl Into<String>) {
1126        self.close();
1127        self.during = stage.into();
1128    }
1129
1130    fn close(&mut self) {
1131        if !self.during.is_empty() {
1132            self.done.push((self.during.clone(), self.started.elapsed()));
1133        }
1134        self.started = Instant::now();
1135    }
1136}
1137
1138/// A conversion's outcome from `run`: its report with the stages' times, or the error with where it happened and what
1139/// was reported until then.
1140fn finish(mut report: ConvertReport, mut stages: Stages, ran: Result<()>) -> Result<ConvertReport> {
1141    stages.close();
1142    report.stages = std::mem::take(&mut stages.done);
1143    match ran {
1144        Ok(()) => Ok(report),
1145        Err(error) => {
1146            let during = stages.during;
1147            report.reported.push(Notice::new(
1148                "convert",
1149                NoticeKind::FailureContext,
1150                format!("failed during {during}: {error}"),
1151                1,
1152            ));
1153            Err(Box::new(ConvertError {
1154                error,
1155                during,
1156                partial: Box::new(report),
1157            }))
1158        }
1159    }
1160}
1161
1162/// The bulk payload one part held ([`World::payload_bytes`]), by component name.
1163#[derive(Clone, Debug, Default, PartialEq, Eq)]
1164pub struct PartSize {
1165    /// The roots the writer asked for.
1166    pub roots: usize,
1167    /// As the reader loaded it.
1168    pub loaded: BTreeMap<String, u64>,
1169    /// After the passes, as the writer got it.
1170    pub written: BTreeMap<String, u64>,
1171}
1172
1173fn sizes(w: &World) -> BTreeMap<String, u64> {
1174    w.payload_bytes()
1175        .into_iter()
1176        .map(|(n, b)| (n.to_owned(), b))
1177        .collect()
1178}
1179
1180/// Adds each step's counts to `counts` under `<pass>: <what>`.
1181fn tally(counts: &mut BTreeMap<String, u64>, report: &RunReport) {
1182    for step in &report.steps {
1183        for (k, v) in &step.log.counts {
1184            *counts.entry(format!("{}: {k}", step.pass)).or_default() += v;
1185        }
1186    }
1187}
1188
1189/// Reads with `reader`, runs the program's `select` on the read world, lets `parts` plan, then loads each part, runs
1190/// the program's `passes` on it once, checks it against `writer`'s target and writes it (see the module
1191/// documentation). On failure the error is a [`ConvertError`] holding what was reported until then.
1192pub fn convert_parts(
1193    reader: &mut dyn Reader,
1194    parts: &mut dyn PartSource,
1195    writer: Box<dyn Writer>,
1196    manager: &PassManager,
1197    select: &Pipeline,
1198    passes: &Pipeline,
1199) -> Result<ConvertReport> {
1200    let mut report = ConvertReport::default();
1201    let mut stages = Stages::new();
1202    let ran = run(
1203        reader,
1204        parts,
1205        writer,
1206        manager,
1207        select,
1208        None,
1209        passes,
1210        &mut report,
1211        &mut stages,
1212    );
1213    finish(report, stages, ran)
1214}
1215
1216/// The program's summaries and decisions for a conversion in parts ([`convert_parts_summarized`]).
1217#[derive(Debug)]
1218pub struct Summarized<'a> {
1219    /// The summaries and decisions carried from the first traversal to the second.
1220    pub registry: &'a SummaryRegistry,
1221    /// Run on each part in the first traversal: the passes that make its summaries.
1222    pub summarize: &'a Pipeline,
1223    /// Run once on the merged summaries: the passes that make the decisions.
1224    pub decide: &'a Pipeline,
1225}
1226
1227/// [`convert_parts`] over the parts twice: first each part is loaded and its `summarized.summarize` passes make its
1228/// summaries, which are merged over every part (with the pass checks on, also in reverse, refused when the two
1229/// differ); the `summarized.decide` passes make the decisions from them, once; then each part is loaded again, given the merged
1230/// summaries and the decisions, and goes through `passes` and the writer as in [`convert_parts`]. The part source must
1231/// give the same parts again ([`PartSource::restart`]).
1232#[allow(clippy::too_many_arguments)]
1233pub fn convert_parts_summarized(
1234    reader: &mut dyn Reader,
1235    parts: &mut dyn PartSource,
1236    writer: Box<dyn Writer>,
1237    manager: &PassManager,
1238    select: &Pipeline,
1239    summarized: &Summarized,
1240    passes: &Pipeline,
1241) -> Result<ConvertReport> {
1242    let mut report = ConvertReport::default();
1243    let mut stages = Stages::new();
1244    let ran = run(
1245        reader,
1246        parts,
1247        writer,
1248        manager,
1249        select,
1250        Some((summarized, None)),
1251        passes,
1252        &mut report,
1253        &mut stages,
1254    );
1255    finish(report, stages, ran)
1256}
1257
1258/// Content the first traversal of a conversion in parts sets aside to be prepared and written by groups of its own
1259/// rather than by the parts it came from: what one part cannot finish alone (a grid cell several parts reach), held on
1260/// disk or elsewhere outside the parts, so what the conversion keeps in memory is one part or one group at a time.
1261pub trait Spool {
1262    /// Take a part's contributions out of `part` (after its summary passes ran) and keep them. What it takes are
1263    /// entities the summary passes declared as the part's output; what stays is the part's summary input.
1264    fn take(&mut self, part: &mut World) -> Result<()>;
1265    /// The next group, as a world of its own, in an order that does not depend on the order the parts came in; `None`
1266    /// once every group was given.
1267    fn next(&mut self) -> Result<Option<World>>;
1268}
1269
1270/// The groups of a conversion in parts ([`convert_parts_gathered`]): after the decisions, each part again through the
1271/// `gather` passes (given every summary and decision; nothing written), its contributions taken by `spool`; each group
1272/// the spool gives prepared by the `group` passes and written on its own; the `finalize` passes, run once on every
1273/// summary (the parts', the gather traversal's and the groups'), for the decisions that need the groups.
1274pub struct Gathered<'a> {
1275    pub spool: &'a mut dyn Spool,
1276    pub gather: &'a Pipeline,
1277    pub group: &'a Pipeline,
1278    pub finalize: &'a Pipeline,
1279}
1280
1281impl std::fmt::Debug for Gathered<'_> {
1282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1283        f.debug_struct("Gathered")
1284            .field("gather", self.gather)
1285            .field("group", self.group)
1286            .field("finalize", self.finalize)
1287            .finish_non_exhaustive()
1288    }
1289}
1290
1291/// [`convert_parts_summarized`] with groups ([`Gathered`]). After `summarized.decide`, a gather traversal: each part is
1292/// loaded again, handed the merged summaries and the decisions, run through `gathered.gather`, and the spool takes its
1293/// contributions (nothing is written). Then each group the spool gives is handed them too, prepared by
1294/// `gathered.group` (a whole run: the group is a whole of its own), checked against the writer's target and written.
1295/// The summaries the gather traversal and the groups made are merged with the parts' (as the parts' are: the order
1296/// checked with the pass checks on), the decisions kept, and `gathered.finalize` makes the decisions that need them, once. The write traversal
1297/// then runs as in [`convert_parts_summarized`], each part given every summary and decision. The part source gives the
1298/// same parts three times.
1299#[allow(clippy::too_many_arguments)]
1300pub fn convert_parts_gathered(
1301    reader: &mut dyn Reader,
1302    parts: &mut dyn PartSource,
1303    writer: Box<dyn Writer>,
1304    manager: &PassManager,
1305    select: &Pipeline,
1306    summarized: &Summarized,
1307    gathered: Gathered,
1308    passes: &Pipeline,
1309) -> Result<ConvertReport> {
1310    let mut report = ConvertReport::default();
1311    let mut stages = Stages::new();
1312    let mut gathered = gathered;
1313    let ran = run(
1314        reader,
1315        parts,
1316        writer,
1317        manager,
1318        select,
1319        Some((summarized, Some(&mut gathered))),
1320        passes,
1321        &mut report,
1322        &mut stages,
1323    );
1324    finish(report, stages, ran)
1325}
1326
1327/// A part's or a group's check against the writer's target: what the target doesn't write (reported), or an error
1328/// naming the first value its rules refuse.
1329/// What a target doesn't write of a part or group: the names (with how many held them), the resources.
1330type NotWritten = (Vec<(String, u64)>, Vec<String>);
1331
1332fn check_target(target: &Target, w: &World) -> Result<NotWritten> {
1333    let checked = target.check_prepared(w);
1334    if let Some((name, words, bad)) = checked.invalid.first() {
1335        return Err(format!(
1336            "{}: {} of {name} the writer can't write ({words}), the first {:?}",
1337            target.name,
1338            bad.len(),
1339            bad[0]
1340        )
1341        .into());
1342    }
1343    if let Some(name) = checked.invalid_resources.first() {
1344        return Err(format!(
1345            "{}: resource {name} refused ({})",
1346            target.name,
1347            target.rule_words(name).unwrap_or_default()
1348        )
1349        .into());
1350    }
1351    Ok((checked.unsupported, checked.unsupported_resources))
1352}
1353
1354/// [`convert_parts`] in one part: every entity of the selected world ([`WholeWorld`]).
1355pub fn convert(
1356    reader: &mut dyn Reader,
1357    writer: Box<dyn Writer>,
1358    manager: &PassManager,
1359    select: &Pipeline,
1360    passes: &Pipeline,
1361) -> Result<ConvertReport> {
1362    convert_parts(
1363        reader,
1364        &mut WholeWorld::default(),
1365        writer,
1366        manager,
1367        select,
1368        passes,
1369    )
1370}
1371
1372#[allow(clippy::too_many_arguments)]
1373fn run(
1374    reader: &mut dyn Reader,
1375    parts: &mut dyn PartSource,
1376    mut writer: Box<dyn Writer>,
1377    manager: &PassManager,
1378    select: &Pipeline,
1379    summarized: Option<(&Summarized, Option<&mut Gathered>)>,
1380    passes: &Pipeline,
1381    report: &mut ConvertReport,
1382    stages: &mut Stages,
1383) -> Result<()> {
1384    stages.enter("read");
1385    let read = reader.read();
1386    report.reported.extend(reader.notices());
1387    let (mut world, read) = read?;
1388    report.read = read;
1389    check_emits(&world, reader.emits(), "read")?;
1390    // what was read is checked before anything uses it: the selection, the plan, an empty pipeline too
1391    manager.verify(&world, "read")?;
1392    stages.enter("select");
1393    manager.run(&mut world, select, &mut report.select)?;
1394    let target = writer.target();
1395    stages.enter("plan");
1396    parts.plan(&world, &|p| reader.prototype_box(&world, p))?;
1397    // the first traversal: each part's summaries, kept (their bytes counted) and merged; the decisions made
1398    // once from them
1399    let decided = match summarized {
1400        None => None,
1401        Some((s, gathered)) => {
1402            let mut kept = Vec::new();
1403            while let Some(Part {
1404                roots,
1405                shared,
1406                context,
1407                whole,
1408                area,
1409            }) = parts.next_part()?
1410            {
1411                let ordinal = kept.len();
1412                stages.enter(format!("summary {ordinal}: load"));
1413                let part = Part {
1414                    roots,
1415                    shared,
1416                    context,
1417                    whole,
1418                    area,
1419                };
1420                let when = format!("summary {ordinal}: load");
1421                let (loaded, load) =
1422                    load_checked(reader, &world, &part, manager, &when, &mut report.reported)?;
1423                report.summary_loads.extend(load.iter().cloned());
1424                stages.enter(format!("summary {ordinal}: passes"));
1425                let mut part = scoped(loaded, part, load, |_| {}).world;
1426                let mut r = RunReport::default();
1427                let ran = manager.run_part(&mut part, s.summarize, &mut r);
1428                tally(&mut report.counts, &r);
1429                report.summarized.push(r);
1430                ran?;
1431                let summaries = s.registry.take_summaries(&part);
1432                report.summary_bytes += s.registry.retained(&summaries);
1433                kept.push(summaries);
1434            }
1435            stages.enter("decide");
1436            let mut merged = merge(s.registry, manager, &kept)?;
1437            drop(kept);
1438            report.merged_summary_bytes = s.registry.retained(&merged);
1439            // a merge makes the decisions' input: checked before the first decision pass reads it
1440            manager.verify(&merged, "the merged summaries")?;
1441            manager.run(&mut merged, s.decide, &mut report.decided)?;
1442            tally(&mut report.counts, &report.decided.clone());
1443            if let Some(g) = gathered {
1444                parts.restart()?;
1445                merged = gather(
1446                    g,
1447                    s.registry,
1448                    &merged,
1449                    reader,
1450                    parts,
1451                    &world,
1452                    manager,
1453                    &target,
1454                    &mut writer,
1455                    report,
1456                    stages,
1457                )?;
1458            }
1459            parts.restart()?;
1460            Some((s.registry, merged))
1461        }
1462    };
1463    while let Some(Part {
1464        roots,
1465        shared,
1466        context,
1467        whole,
1468        area,
1469    }) = parts.next_part()?
1470    {
1471        let ordinal = report.part_reports.len();
1472        stages.enter(format!("part {ordinal}: load"));
1473        let part = Part {
1474            roots,
1475            shared,
1476            context,
1477            whole,
1478            area,
1479        };
1480        let (loaded, load) = load_checked(reader, &world, &part, manager, "load", &mut report.reported)?;
1481        report.loads.extend(load.iter().cloned());
1482        let mut size = PartSize {
1483            roots: part.roots.len(),
1484            loaded: sizes(&loaded),
1485            ..PartSize::default()
1486        };
1487        stages.enter(format!("part {ordinal}: passes"));
1488        // the part's report is published before its passes run, so a failure keeps what ran
1489        report.part_reports.push(PartReport {
1490            ordinal,
1491            roots: part.roots.clone(),
1492            shared: part.shared.clone(),
1493            context: part.context.clone(),
1494            load: load.clone(),
1495            ..PartReport::default()
1496        });
1497        let part_report = report.part_reports.last_mut().expect("pushed");
1498        let loaded = scoped(loaded, part, load, |part| {
1499            if let Some((registry, decided)) = &decided {
1500                registry.hand_on(decided, part);
1501            }
1502        });
1503        let prepared = prepare_loaded(
1504            loaded,
1505            manager,
1506            passes,
1507            &mut part_report.explicit,
1508            &mut part_report.kept,
1509        );
1510        for (k, v) in &part_report.kept {
1511            *report.counts.entry(format!("convert: {k}")).or_default() += v;
1512        }
1513        let part = prepared?;
1514        stages.enter(format!("part {ordinal}: target check"));
1515        (part_report.unsupported, part_report.unsupported_resources) = check_target(&target, &part)?;
1516        let explicit = part_report.explicit.clone();
1517        tally(&mut report.counts, &explicit);
1518        size.written = sizes(&part);
1519        report.part_sizes.push(size);
1520        stages.enter(format!("part {ordinal}: write"));
1521        let written = writer.write(&part);
1522        report.reported.extend(writer.notices());
1523        written?;
1524        parts.written(&part)?;
1525        report.parts += 1;
1526    }
1527    stages.enter("finish");
1528    report.reported.extend(writer.notices());
1529    let finished = writer.finish()?;
1530    report.reported.extend(finished.notices);
1531    report.written = finished.report;
1532    Ok(())
1533}
1534
1535/// The gather traversal and the groups of a conversion ([`Gathered`]), after the decisions (`decided`): see
1536/// [`convert_parts_gathered`]. What the write traversal gets: every summary merged, the decisions, the finalizing ones.
1537#[allow(clippy::too_many_arguments)]
1538fn gather(
1539    g: &mut Gathered,
1540    registry: &SummaryRegistry,
1541    decided: &World,
1542    reader: &mut dyn Reader,
1543    parts: &mut dyn PartSource,
1544    world: &World,
1545    manager: &PassManager,
1546    target: &Target,
1547    writer: &mut Box<dyn Writer>,
1548    report: &mut ConvertReport,
1549    stages: &mut Stages,
1550) -> Result<World> {
1551    let mut kept = vec![registry.take_summaries(decided)];
1552    let mut handed = World::new();
1553    registry.hand_on(decided, &mut handed);
1554    // the gather traversal: each part again, given every summary and decision, prepared as far as its contributions
1555    // need, which the spool takes; nothing is written
1556    while let Some(Part {
1557        roots,
1558        shared,
1559        context,
1560        whole,
1561        area,
1562    }) = parts.next_part()?
1563    {
1564        let ordinal = report.gather_runs.len();
1565        stages.enter(format!("gather {ordinal}: load"));
1566        let part = Part {
1567            roots,
1568            shared,
1569            context,
1570            whole,
1571            area,
1572        };
1573        let when = format!("gather {ordinal}: load");
1574        let (loaded, load) = load_checked(reader, world, &part, manager, &when, &mut report.reported)?;
1575        report.gather_loads.extend(load.iter().cloned());
1576        let mut part = scoped(loaded, part, load, |part| registry.hand_on(decided, part)).world;
1577        stages.enter(format!("gather {ordinal}: passes"));
1578        let mut r = RunReport::default();
1579        let ran = manager.run_part(&mut part, g.gather, &mut r);
1580        tally(&mut report.counts, &r);
1581        report.gather_runs.push(r);
1582        ran?;
1583        stages.enter(format!("gather {ordinal}: spool"));
1584        g.spool.take(&mut part)?;
1585        kept.push(registry.take_summaries_made(&part, &handed));
1586    }
1587    while let Some(mut group) = g.spool.next()? {
1588        let ordinal = report.groups.len();
1589        stages.enter(format!("group {ordinal}: load"));
1590        manager.verify(&group, &format!("group {ordinal}"))?;
1591        registry.hand_on(decided, &mut group);
1592        stages.enter(format!("group {ordinal}: passes"));
1593        report.groups.push(GroupReport {
1594            ordinal,
1595            ..GroupReport::default()
1596        });
1597        let group_report = report.groups.last_mut().expect("pushed");
1598        manager.run(&mut group, g.group, &mut group_report.run)?;
1599        tally(&mut report.counts, &report.groups[ordinal].run.clone());
1600        kept.push(registry.take_summaries_made(&group, &handed));
1601        stages.enter(format!("group {ordinal}: target check"));
1602        manager.verify(&group, "the group's passes")?;
1603        let (unsupported, unsupported_resources) = check_target(target, &group)?;
1604        let group_report = &mut report.groups[ordinal];
1605        group_report.unsupported = unsupported;
1606        group_report.unsupported_resources = unsupported_resources;
1607        group_report.written = sizes(&group);
1608        stages.enter(format!("group {ordinal}: write"));
1609        let written = writer.write(&group);
1610        report.reported.extend(writer.notices());
1611        written?;
1612    }
1613    stages.enter("finalize");
1614    let mut merged = merge(registry, manager, &kept)?;
1615    drop(kept);
1616    registry.carry_decisions(decided, &mut merged);
1617    report.merged_summary_bytes = registry.retained(&merged);
1618    manager.verify(&merged, "the merged summaries and groups")?;
1619    manager.run(&mut merged, g.finalize, &mut report.finalized)?;
1620    tally(&mut report.counts, &report.finalized.clone());
1621    Ok(merged)
1622}
1623
1624/// Kept summaries merged: in either order with the order checked when the manager runs its self-checks
1625/// ([`PassManager::pass_checks`]), once otherwise.
1626fn merge(registry: &SummaryRegistry, manager: &PassManager, kept: &[World]) -> Result<World> {
1627    Ok(if manager.pass_checks() {
1628        registry.merge_order_free(kept)?
1629    } else {
1630        registry.merge(kept)?
1631    })
1632}