pass_lang/report.rs
1//! The record a [`PassManager`](crate::PassManager) run produces.
2
3use alloc::vec::Vec;
4
5use crate::pass::Outcome;
6
7/// One entry in a [`Report`]: a pass that ran and what it did.
8///
9/// Entries appear in execution order. When a pipeline is run to a fixpoint, a
10/// pass that runs on three sweeps contributes three entries, one per sweep.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize))]
13pub struct PassRun {
14 name: &'static str,
15 outcome: Outcome,
16}
17
18impl PassRun {
19 /// The [`name`](crate::Pass::name) of the pass that ran.
20 ///
21 /// # Examples
22 ///
23 /// ```
24 /// use pass_lang::{Outcome, Pass, PassError, PassManager};
25 ///
26 /// struct Touch;
27 /// impl Pass<i64> for Touch {
28 /// fn name(&self) -> &'static str { "touch" }
29 /// fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
30 /// *u += 1;
31 /// Ok(Outcome::Changed)
32 /// }
33 /// }
34 ///
35 /// let mut pm = PassManager::new();
36 /// pm.add(Touch);
37 /// let mut unit = 0;
38 /// let report = pm.run(&mut unit).unwrap();
39 /// assert_eq!(report.runs()[0].name(), "touch");
40 /// ```
41 #[must_use]
42 #[inline]
43 pub fn name(&self) -> &'static str {
44 self.name
45 }
46
47 /// What the pass reported on that run.
48 #[must_use]
49 #[inline]
50 pub fn outcome(&self) -> Outcome {
51 self.outcome
52 }
53}
54
55/// A record of one [`PassManager`](crate::PassManager) run.
56///
57/// A `Report` is the observability hook: it lists every pass that ran, in order,
58/// with the [`Outcome`] each reported, and summarizes the run — how many sweeps
59/// it took, whether it reached a fixpoint, and how many passes changed the unit.
60/// It is produced by [`PassManager::run`](crate::PassManager::run) and
61/// [`PassManager::run_to_fixpoint`](crate::PassManager::run_to_fixpoint); you
62/// read it, you do not build it.
63///
64/// # Examples
65///
66/// ```
67/// use pass_lang::{Outcome, Pass, PassError, PassManager};
68///
69/// struct Halve;
70/// impl Pass<i64> for Halve {
71/// fn name(&self) -> &'static str { "halve" }
72/// fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
73/// if *u <= 1 { return Ok(Outcome::Unchanged); }
74/// *u /= 2;
75/// Ok(Outcome::Changed)
76/// }
77/// }
78///
79/// let mut pm = PassManager::new();
80/// pm.add(Halve);
81///
82/// let mut unit = 8;
83/// let report = pm.run_to_fixpoint(&mut unit, 16).unwrap();
84///
85/// assert_eq!(unit, 1); // 8 -> 4 -> 2 -> 1
86/// assert!(report.converged()); // a final sweep changed nothing
87/// assert_eq!(report.iterations(), 4); // three halving sweeps + one confirming sweep
88/// assert_eq!(report.changes(), 3); // three sweeps reported Changed
89/// ```
90#[derive(Debug, Clone, PartialEq, Eq, Default)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize))]
92pub struct Report {
93 runs: Vec<PassRun>,
94 iterations: usize,
95 converged: bool,
96}
97
98impl Report {
99 /// Start a report sized for at least `capacity` pass executions, so a single
100 /// sweep records without reallocating its run list.
101 pub(crate) fn with_capacity(capacity: usize) -> Self {
102 Self {
103 runs: Vec::with_capacity(capacity),
104 iterations: 0,
105 converged: false,
106 }
107 }
108
109 /// Append the outcome of one pass execution.
110 #[inline]
111 pub(crate) fn record(&mut self, name: &'static str, outcome: Outcome) {
112 self.runs.push(PassRun { name, outcome });
113 }
114
115 /// Stamp the aggregate fields once the run is complete.
116 pub(crate) fn finalize(&mut self, iterations: usize, converged: bool) {
117 self.iterations = iterations;
118 self.converged = converged;
119 }
120
121 /// Every pass execution, in the order it happened.
122 ///
123 /// # Examples
124 ///
125 /// ```
126 /// use pass_lang::{Outcome, Pass, PassError, PassManager};
127 ///
128 /// struct A;
129 /// impl Pass<i64> for A {
130 /// fn name(&self) -> &'static str { "a" }
131 /// fn run(&mut self, _: &mut i64) -> Result<Outcome, PassError> { Ok(Outcome::Unchanged) }
132 /// }
133 /// struct B;
134 /// impl Pass<i64> for B {
135 /// fn name(&self) -> &'static str { "b" }
136 /// fn run(&mut self, _: &mut i64) -> Result<Outcome, PassError> { Ok(Outcome::Unchanged) }
137 /// }
138 ///
139 /// let mut pm = PassManager::new();
140 /// pm.add(A).add(B);
141 /// let mut unit = 0;
142 /// let report = pm.run(&mut unit).unwrap();
143 ///
144 /// let names: Vec<_> = report.runs().iter().map(|r| r.name()).collect();
145 /// assert_eq!(names, ["a", "b"]);
146 /// ```
147 #[must_use]
148 #[inline]
149 pub fn runs(&self) -> &[PassRun] {
150 &self.runs
151 }
152
153 /// How many pass executions reported [`Outcome::Changed`].
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// use pass_lang::{Outcome, Pass, PassError, PassManager};
159 ///
160 /// struct Inc;
161 /// impl Pass<i64> for Inc {
162 /// fn name(&self) -> &'static str { "inc" }
163 /// fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
164 /// *u += 1;
165 /// Ok(Outcome::Changed)
166 /// }
167 /// }
168 ///
169 /// let mut pm = PassManager::new();
170 /// pm.add(Inc);
171 /// let mut unit = 0;
172 /// assert_eq!(pm.run(&mut unit).unwrap().changes(), 1);
173 /// ```
174 #[must_use]
175 pub fn changes(&self) -> usize {
176 self.runs.iter().filter(|r| r.outcome.changed()).count()
177 }
178
179 /// The number of full sweeps over the pipeline.
180 ///
181 /// [`PassManager::run`](crate::PassManager::run) always reports `1`.
182 /// [`PassManager::run_to_fixpoint`](crate::PassManager::run_to_fixpoint)
183 /// reports how many sweeps it actually performed.
184 #[must_use]
185 #[inline]
186 pub fn iterations(&self) -> usize {
187 self.iterations
188 }
189
190 /// Whether the final sweep made no change — the pipeline reached a fixpoint.
191 ///
192 /// For [`run`](crate::PassManager::run) this is `true` when the single sweep
193 /// changed nothing. For
194 /// [`run_to_fixpoint`](crate::PassManager::run_to_fixpoint) it is `true` when
195 /// a sweep settled before the iteration bound was hit, and `false` when the
196 /// bound was reached with the unit still changing.
197 #[must_use]
198 #[inline]
199 pub fn converged(&self) -> bool {
200 self.converged
201 }
202}
203
204#[cfg(test)]
205#[allow(clippy::expect_used, clippy::unwrap_used)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_empty_report_defaults() {
211 let report = Report::with_capacity(0);
212 assert!(report.runs().is_empty());
213 assert_eq!(report.changes(), 0);
214 assert_eq!(report.iterations(), 0);
215 assert!(!report.converged());
216 }
217
218 #[test]
219 fn test_record_appends_in_order() {
220 let mut report = Report::with_capacity(0);
221 report.record("a", Outcome::Changed);
222 report.record("b", Outcome::Unchanged);
223 let names: alloc::vec::Vec<_> = report.runs().iter().map(PassRun::name).collect();
224 assert_eq!(names, ["a", "b"]);
225 }
226
227 #[test]
228 fn test_changes_counts_only_changed() {
229 let mut report = Report::with_capacity(0);
230 report.record("a", Outcome::Changed);
231 report.record("b", Outcome::Unchanged);
232 report.record("c", Outcome::Changed);
233 assert_eq!(report.changes(), 2);
234 }
235
236 #[test]
237 fn test_finalize_sets_aggregate() {
238 let mut report = Report::with_capacity(0);
239 report.finalize(3, true);
240 assert_eq!(report.iterations(), 3);
241 assert!(report.converged());
242 }
243
244 #[test]
245 fn test_pass_run_accessors() {
246 let mut report = Report::with_capacity(0);
247 report.record("only", Outcome::Changed);
248 let run = report.runs()[0];
249 assert_eq!(run.name(), "only");
250 assert_eq!(run.outcome(), Outcome::Changed);
251 }
252}