Skip to main content

primase_core/
summary.rs

1//! Summaries and decisions: what a conversion in parts knows of the whole of what it writes.
2//!
3//! A decision over the whole of what is written (a map's floor, a light cap per cell that crosses parts, a texture
4//! budget) cannot be made from one part. A conversion in parts ([`crate::convert::convert_parts_summarized`]) goes over
5//! the parts twice: first each part's summary passes make small typed records of what the part holds (a
6//! [`Summary`]: the lowest points of its placed parts, its texture weights), which are merged over every part; the
7//! decision passes then make the decisions from the merged summaries, once, as records of their own; and each part,
8//! loaded again, gets the summaries and decisions before its passes run, so a pass replays a decision instead of
9//! making it from what one part holds.
10//!
11//! A summary's merge is a property of the data, not of the pass that made it, and it must not depend on the order the
12//! parts arrive in or how they are grouped: the merge is commutative and associative, a contract its crate states and
13//! tests (shuffled orders, groupings, overlapping inputs). The conversion keeps every part's summaries until it merges
14//! them (metadata, each summary stating the bytes of the data it holds, [`Summary::retained_bytes`]: logical-data
15//! accounting, not a memory bound; the conversion reports their total, [`crate::convert::ConvertReport::summary_bytes`]), merges them in arrival order,
16//! and, when its pass manager runs the pass checks (debug builds and the tests by default, or asked for:
17//! `PassManager::with_pass_checks`), also in the reverse order, refusing a summary whose two merges differ: a
18//! forward/reverse consistency check, opt-in in release builds, which catches many order dependencies but proves none
19//! absent (a merge that keeps only its first two
20//! arrivals can agree on A, B, A forward and backward and still differ on A, A, B). The crate that defines a summary
21//! or a decision registers it ([`SummaryRegistry`]), as it registers its passes.
22
23use std::collections::BTreeMap;
24
25use crate::World;
26use crate::world::Resource;
27
28/// A record a pass makes from one part, merged with the other parts' into the whole's. The merge must be order-free
29/// (commutative and associative); equality is what the conversion compares the two merge orders by.
30pub trait Summary: Resource + PartialEq {
31    /// Fold `other`, another part's, into this one; an error for summaries that cannot be merged (made for other
32    /// settings).
33    fn merge(&mut self, other: Self) -> Result<(), String>;
34    /// The bytes of the data it holds, counted logically (its entries and payloads): accounting of what a conversion
35    /// keeps of it, not a memory bound (tree nodes, capacities and the merge's temporary copies are not counted). A
36    /// conversion's observed peak memory is measured separately.
37    fn retained_bytes(&self) -> usize;
38}
39
40/// What the registry does with one kind of record, by type, through plain functions.
41#[derive(Clone, Copy)]
42struct Kind {
43    /// Merge `from`'s record (if any) into `into`'s, or move it there when `into` has none.
44    merge: fn(into: &mut World, from: &World) -> Result<(), String>,
45    /// Whether two worlds hold equal records (both none counts as equal).
46    equal: fn(a: &World, b: &World) -> bool,
47    /// Copy `from`'s record (if any) into `to`.
48    copy: fn(from: &World, to: &mut World),
49    /// The bytes `w`'s record holds (0 without one; decisions are not counted).
50    bytes: fn(w: &World) -> usize,
51    /// A summary (merged over parts), or a decision (made once, handed on).
52    summary: bool,
53}
54
55impl std::fmt::Debug for Kind {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.write_str(if self.summary { "summary" } else { "decision" })
58    }
59}
60
61fn merge_typed<S: Summary>(into: &mut World, from: &World) -> Result<(), String> {
62    let Some(s) = from.resource::<S>().cloned() else {
63        return Ok(());
64    };
65    match into.resource_mut::<S>() {
66        Some(m) => m.merge(s),
67        None => {
68            into.insert_resource(s);
69            Ok(())
70        }
71    }
72}
73
74fn bytes_typed<S: Summary>(w: &World) -> usize {
75    w.resource::<S>().map_or(0, Summary::retained_bytes)
76}
77
78fn no_bytes(_: &World) -> usize {
79    0
80}
81
82fn equal_typed<S: Summary>(a: &World, b: &World) -> bool {
83    a.resource::<S>() == b.resource::<S>()
84}
85
86fn copy_typed<R: Resource>(from: &World, to: &mut World) {
87    if let Some(r) = from.resource::<R>() {
88        to.insert_resource(r.clone());
89    }
90}
91
92fn never_merged<R: Resource>(_: &mut World, _: &World) -> Result<(), String> {
93    Err(format!("{} is a decision, made once: it is not merged", R::NAME))
94}
95
96/// Decisions are not compared: they are made once.
97fn never_compared(_: &World, _: &World) -> bool {
98    true
99}
100
101/// The summaries and decisions a conversion in parts carries, by resource name.
102#[derive(Debug, Default)]
103pub struct SummaryRegistry {
104    kinds: BTreeMap<&'static str, Kind>,
105}
106
107impl SummaryRegistry {
108    pub fn new() -> Self {
109        SummaryRegistry::default()
110    }
111
112    /// Register a summary: merged over every part, then handed to every part.
113    pub fn summary<S: Summary>(&mut self) {
114        self.kinds.insert(
115            S::NAME,
116            Kind {
117                merge: merge_typed::<S>,
118                equal: equal_typed::<S>,
119                copy: copy_typed::<S>,
120                bytes: bytes_typed::<S>,
121                summary: true,
122            },
123        );
124    }
125
126    /// Register a decision: made once from the merged summaries, then handed to every part.
127    pub fn decision<D: Resource>(&mut self) {
128        self.kinds.insert(
129            D::NAME,
130            Kind {
131                merge: never_merged::<D>,
132                equal: never_compared,
133                copy: copy_typed::<D>,
134                bytes: no_bytes,
135                summary: false,
136            },
137        );
138    }
139
140    /// The summaries' names.
141    pub fn summaries(&self) -> impl Iterator<Item = &'static str> + '_ {
142        self.kinds.iter().filter(|(_, k)| k.summary).map(|(n, _)| *n)
143    }
144
145    /// The decisions' names.
146    pub fn decisions(&self) -> impl Iterator<Item = &'static str> + '_ {
147        self.kinds.iter().filter(|(_, k)| !k.summary).map(|(n, _)| *n)
148    }
149
150    /// The bytes `w`'s summaries hold, as they state them.
151    pub fn retained(&self, w: &World) -> usize {
152        self.kinds.values().map(|k| (k.bytes)(w)).sum()
153    }
154
155    /// A world holding only `part`'s summaries (a copy of each), to keep and merge.
156    pub fn take_summaries(&self, part: &World) -> World {
157        let mut out = World::new();
158        for k in self.kinds.values().filter(|k| k.summary) {
159            (k.copy)(part, &mut out);
160        }
161        out
162    }
163
164    /// The parts' summaries (each world as [`SummaryRegistry::take_summaries`] made it) merged in the order given, and
165    /// in the reverse order: an error naming a summary whose two merges differ (its merge depends on the order the
166    /// parts arrive in) or that cannot be merged. A consistency check, not a proof of order-freedom (see the module).
167    pub fn merge_order_free(&self, parts: &[World]) -> Result<World, String> {
168        let forward = self.merge_in(&mut parts.iter())?;
169        let backward = self.merge_in(&mut parts.iter().rev())?;
170        for (name, k) in self.kinds.iter().filter(|(_, k)| k.summary) {
171            if !(k.equal)(&forward, &backward) {
172                return Err(format!(
173                    "{name}: its merge depends on the order the parts arrive in (the merge of {} parts differs \
174                     forward and backward)",
175                    parts.len()
176                ));
177            }
178        }
179        Ok(forward)
180    }
181
182    /// The parts' summaries merged once, in the order given: [`SummaryRegistry::merge_order_free`] without its check
183    /// of the reverse order, for a conversion that doesn't run its self-checks (`PassManager::pass_checks`).
184    pub fn merge(&self, parts: &[World]) -> Result<World, String> {
185        self.merge_in(&mut parts.iter())
186    }
187
188    fn merge_in(&self, order: &mut dyn Iterator<Item = &World>) -> Result<World, String> {
189        let mut out = World::new();
190        for p in order {
191            for (name, k) in self.kinds.iter().filter(|(_, k)| k.summary) {
192                (k.merge)(&mut out, p).map_err(|e| format!("{name}: {e}"))?;
193            }
194        }
195        Ok(out)
196    }
197
198    /// Copy `from`'s summaries and decisions into `to` (a part about to run its passes). They stay in the part: the
199    /// writer's target judges them as any other resource.
200    pub fn hand_on(&self, from: &World, to: &mut World) {
201        for k in self.kinds.values() {
202            (k.copy)(from, to);
203        }
204    }
205
206    /// A world holding the summaries `w` made itself: those it holds that `handed` (what it was handed, as
207    /// [`SummaryRegistry::hand_on`] gave them) does not, so merging them counts nothing twice.
208    pub fn take_summaries_made(&self, w: &World, handed: &World) -> World {
209        let given: std::collections::BTreeSet<&str> = handed.resource_names().into_iter().collect();
210        let mut out = World::new();
211        for (name, k) in self.kinds.iter().filter(|(_, k)| k.summary) {
212            if !given.contains(name) {
213                (k.copy)(w, &mut out);
214            }
215        }
216        out
217    }
218
219    /// Copy `from`'s decisions (not its summaries) into `to`.
220    pub fn carry_decisions(&self, from: &World, to: &mut World) {
221        for k in self.kinds.values().filter(|k| !k.summary) {
222            (k.copy)(from, to);
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    /// The lowest of the values seen: order-free.
232    #[derive(Clone, Debug, PartialEq)]
233    struct Lowest(f64);
234    impl Resource for Lowest {
235        const NAME: &'static str = "toy.lowest";
236    }
237    impl Summary for Lowest {
238        fn merge(&mut self, other: Self) -> Result<(), String> {
239            self.0 = self.0.min(other.0);
240            Ok(())
241        }
242        fn retained_bytes(&self) -> usize {
243            8
244        }
245    }
246
247    /// The values in the order they were merged: not order-free.
248    #[derive(Clone, Debug, PartialEq)]
249    struct Arrivals(Vec<u32>);
250    impl Resource for Arrivals {
251        const NAME: &'static str = "toy.arrivals";
252    }
253    impl Summary for Arrivals {
254        fn merge(&mut self, other: Self) -> Result<(), String> {
255            self.0.extend(other.0);
256            Ok(())
257        }
258        fn retained_bytes(&self) -> usize {
259            4 * self.0.len()
260        }
261    }
262
263    #[derive(Clone, Debug, PartialEq)]
264    struct Floor(f64);
265    impl Resource for Floor {
266        const NAME: &'static str = "toy.floor";
267    }
268
269    fn part(lowest: f64, arrival: Option<u32>) -> World {
270        let mut w = World::new();
271        w.insert_resource(Lowest(lowest));
272        if let Some(a) = arrival {
273            w.insert_resource(Arrivals(vec![a]));
274        }
275        w
276    }
277
278    #[test]
279    fn an_order_free_summary_merges_and_an_order_bound_one_is_refused() {
280        let mut r = SummaryRegistry::new();
281        r.summary::<Lowest>();
282        r.decision::<Floor>();
283        let parts: Vec<World> = [3.0, -2.0, 7.0]
284            .into_iter()
285            .map(|l| r.take_summaries(&part(l, Some(1))))
286            .collect();
287        // a summary not registered is not taken
288        assert!(parts[0].resource::<Arrivals>().is_none());
289        let merged = r.merge_order_free(&parts).unwrap();
290        assert_eq!(merged.resource::<Lowest>(), Some(&Lowest(-2.0)));
291        // the decisions and summaries are handed on
292        let mut decided = merged;
293        decided.insert_resource(Floor(-2.0));
294        let mut p = World::new();
295        r.hand_on(&decided, &mut p);
296        assert_eq!(
297            (p.resource::<Lowest>(), p.resource::<Floor>()),
298            (Some(&Lowest(-2.0)), Some(&Floor(-2.0)))
299        );
300
301        r.summary::<Arrivals>();
302        let parts: Vec<World> = [1, 2, 3]
303            .into_iter()
304            .map(|a| r.take_summaries(&part(0.0, Some(a))))
305            .collect();
306        let e = r.merge_order_free(&parts).unwrap_err();
307        assert!(
308            e.starts_with("toy.arrivals: its merge depends on the order"),
309            "{e}"
310        );
311        // one part, or parts without it: nothing to differ
312        assert!(r.merge_order_free(&parts[..1]).is_ok());
313        let without: Vec<World> = (0..2).map(|_| r.take_summaries(&part(1.0, None))).collect();
314        assert!(r.merge_order_free(&without).is_ok());
315    }
316
317    #[test]
318    fn a_decision_is_not_merged() {
319        let mut r = SummaryRegistry::new();
320        r.decision::<Floor>();
321        let mut into = World::new();
322        let mut from = World::new();
323        from.insert_resource(Floor(1.0));
324        let k = r.kinds[Floor::NAME];
325        assert!((k.merge)(&mut into, &from).unwrap_err().contains("a decision"));
326        assert_eq!(r.decisions().collect::<Vec<_>>(), [Floor::NAME]);
327        assert_eq!(r.summaries().count(), 0);
328    }
329}