Skip to main content

pass_lang/
manager.rs

1//! The ordered pass pipeline and the loops that run it.
2
3use alloc::boxed::Box;
4use alloc::vec::Vec;
5
6use crate::error::PassError;
7use crate::pass::Pass;
8use crate::report::Report;
9
10/// An ordered pipeline of passes over a unit of type `T`.
11///
12/// A `PassManager` holds passes in the order they were registered and runs them
13/// over a unit. It is the scheduler, and scheduling is its only job: it never
14/// reads or rewrites the unit itself — it only calls the passes, in order, and
15/// records what they report. Registering a pass with [`add`](Self::add) is the
16/// plugin seam: any crate can contribute a transform by implementing
17/// [`Pass<T>`](crate::Pass) and adding it here.
18///
19/// Two ways to run a pipeline:
20///
21/// - [`run`](Self::run) makes a single sweep — every pass once, in order. This
22///   is the common case.
23/// - [`run_to_fixpoint`](Self::run_to_fixpoint) repeats the sweep until a full
24///   pass over the pipeline changes nothing, or an iteration bound is reached.
25///   Use it when passes feed one another and a transform can expose further work
26///   (constant folding exposing dead code, which exposes more folding).
27///
28/// The manager is single-threaded by design: a pipeline is an inherently ordered
29/// sequence of mutations, so it carries no atomic overhead. Dispatch to a pass is
30/// one indirect call amortised over the whole unit that pass rewrites.
31///
32/// # Examples
33///
34/// ```
35/// use pass_lang::{Outcome, Pass, PassError, PassManager};
36///
37/// struct Double;
38/// impl Pass<i64> for Double {
39///     fn name(&self) -> &'static str { "double" }
40///     fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
41///         *u *= 2;
42///         Ok(Outcome::Changed)
43///     }
44/// }
45///
46/// let mut pm = PassManager::new();
47/// pm.add(Double).add(Double);
48///
49/// let mut unit = 3;
50/// let report = pm.run(&mut unit).unwrap();
51///
52/// assert_eq!(unit, 12);            // 3 -> 6 -> 12
53/// assert_eq!(report.runs().len(), 2);
54/// ```
55pub struct PassManager<T> {
56    passes: Vec<Box<dyn Pass<T>>>,
57}
58
59impl<T> PassManager<T> {
60    /// Create an empty pipeline.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use pass_lang::PassManager;
66    ///
67    /// let pm = PassManager::<i64>::new();
68    /// assert!(pm.is_empty());
69    /// ```
70    #[must_use]
71    pub fn new() -> Self {
72        Self { passes: Vec::new() }
73    }
74
75    /// Register a pass at the end of the pipeline.
76    ///
77    /// Returns `&mut Self` so registrations can be chained. This is the plugin
78    /// seam: the pass must be `'static`, and it runs after every pass already
79    /// registered.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use pass_lang::{Outcome, Pass, PassError, PassManager};
85    ///
86    /// struct Noop(&'static str);
87    /// impl Pass<i64> for Noop {
88    ///     fn name(&self) -> &'static str { self.0 }
89    ///     fn run(&mut self, _: &mut i64) -> Result<Outcome, PassError> { Ok(Outcome::Unchanged) }
90    /// }
91    ///
92    /// let mut pm = PassManager::new();
93    /// pm.add(Noop("first")).add(Noop("second"));
94    /// assert_eq!(pm.len(), 2);
95    /// ```
96    pub fn add(&mut self, pass: impl Pass<T> + 'static) -> &mut Self {
97        self.passes.push(Box::new(pass));
98        self
99    }
100
101    /// The number of registered passes.
102    #[must_use]
103    #[inline]
104    pub fn len(&self) -> usize {
105        self.passes.len()
106    }
107
108    /// Whether the pipeline has no passes.
109    #[must_use]
110    #[inline]
111    pub fn is_empty(&self) -> bool {
112        self.passes.is_empty()
113    }
114
115    /// Run every pass once, in registration order.
116    ///
117    /// Each pass transforms `unit` in place; the returned [`Report`] lists every
118    /// pass that ran with the [`Outcome`](crate::Outcome) it reported. If a pass
119    /// returns a [`PassError`], the pipeline stops at that pass — later passes do
120    /// not run — and the error is returned with the failing pass's name stamped
121    /// in.
122    ///
123    /// The report's [`converged`](crate::Report::converged) is `true` when the
124    /// sweep changed nothing (the unit is already at a fixpoint for this
125    /// pipeline), and [`iterations`](crate::Report::iterations) is always `1`.
126    ///
127    /// # Errors
128    ///
129    /// Returns the [`PassError`] of the first pass that fails.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use pass_lang::{Outcome, Pass, PassError, PassManager};
135    ///
136    /// struct FailIfNegative;
137    /// impl Pass<i64> for FailIfNegative {
138    ///     fn name(&self) -> &'static str { "fail-if-negative" }
139    ///     fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
140    ///         if *u < 0 { return Err(PassError::new("unit went negative")); }
141    ///         Ok(Outcome::Unchanged)
142    ///     }
143    /// }
144    ///
145    /// let mut pm = PassManager::new();
146    /// pm.add(FailIfNegative);
147    ///
148    /// let mut unit = -1;
149    /// let err = pm.run(&mut unit).unwrap_err();
150    /// assert_eq!(err.pass(), "fail-if-negative");
151    /// assert_eq!(err.message(), "unit went negative");
152    /// ```
153    pub fn run(&mut self, unit: &mut T) -> Result<Report, PassError> {
154        let mut report = Report::with_capacity(self.passes.len());
155        let mut changed = false;
156        for pass in &mut self.passes {
157            let outcome = pass.run(unit).map_err(|e| e.in_pass(pass.name()))?;
158            changed |= outcome.changed();
159            report.record(pass.name(), outcome);
160        }
161        report.finalize(1, !changed);
162        Ok(report)
163    }
164
165    /// Repeat the pipeline until it settles, or until `max_iters` sweeps run.
166    ///
167    /// Each sweep runs every pass once, in order. After a sweep in which no pass
168    /// reported [`Changed`](crate::Outcome::Changed), the unit is at a fixpoint
169    /// and the loop stops with [`converged`](crate::Report::converged) `true`. If
170    /// `max_iters` sweeps run while the unit is still changing, the loop stops
171    /// with `converged` `false` — the bound guarantees termination even if a pass
172    /// oscillates. Passing `max_iters == 0` performs no sweeps.
173    ///
174    /// The [`Report`] accumulates every pass execution across every sweep, so its
175    /// [`runs`](crate::Report::runs) length is the sum over sweeps (a short final
176    /// sweep on error aside).
177    ///
178    /// # Errors
179    ///
180    /// Returns the [`PassError`] of the first pass that fails, on whichever sweep
181    /// it fails.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use pass_lang::{Outcome, Pass, PassError, PassManager};
187    ///
188    /// // Halve toward 1; idempotent once it reaches 1.
189    /// struct Halve;
190    /// impl Pass<i64> for Halve {
191    ///     fn name(&self) -> &'static str { "halve" }
192    ///     fn run(&mut self, u: &mut i64) -> Result<Outcome, PassError> {
193    ///         if *u <= 1 { return Ok(Outcome::Unchanged); }
194    ///         *u /= 2;
195    ///         Ok(Outcome::Changed)
196    ///     }
197    /// }
198    ///
199    /// let mut pm = PassManager::new();
200    /// pm.add(Halve);
201    ///
202    /// let mut unit = 16;
203    /// let report = pm.run_to_fixpoint(&mut unit, 32).unwrap();
204    /// assert_eq!(unit, 1);
205    /// assert!(report.converged());
206    /// ```
207    pub fn run_to_fixpoint(&mut self, unit: &mut T, max_iters: usize) -> Result<Report, PassError> {
208        let mut report = Report::with_capacity(self.passes.len());
209        let mut iterations = 0;
210        let mut converged = false;
211        while iterations < max_iters {
212            iterations += 1;
213            let mut changed = false;
214            for pass in &mut self.passes {
215                let outcome = pass.run(unit).map_err(|e| e.in_pass(pass.name()))?;
216                changed |= outcome.changed();
217                report.record(pass.name(), outcome);
218            }
219            if !changed {
220                converged = true;
221                break;
222            }
223        }
224        report.finalize(iterations, converged);
225        Ok(report)
226    }
227}
228
229impl<T> Default for PassManager<T> {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235#[cfg(test)]
236#[allow(clippy::expect_used, clippy::unwrap_used)]
237mod tests {
238    use super::*;
239    use crate::pass::Outcome;
240    use alloc::vec;
241    use alloc::vec::Vec;
242
243    /// Records the order in which passes ran, by pushing its tag onto the unit.
244    struct Tag(&'static str);
245    impl Pass<Vec<&'static str>> for Tag {
246        fn name(&self) -> &'static str {
247            self.0
248        }
249        fn run(&mut self, unit: &mut Vec<&'static str>) -> Result<Outcome, PassError> {
250            unit.push(self.0);
251            Ok(Outcome::Changed)
252        }
253    }
254
255    /// Subtracts one while positive; reports `Unchanged` at zero (idempotent).
256    struct Decrement;
257    impl Pass<i64> for Decrement {
258        fn name(&self) -> &'static str {
259            "decrement"
260        }
261        fn run(&mut self, unit: &mut i64) -> Result<Outcome, PassError> {
262            if *unit <= 0 {
263                return Ok(Outcome::Unchanged);
264            }
265            *unit -= 1;
266            Ok(Outcome::Changed)
267        }
268    }
269
270    /// Always fails.
271    struct Boom;
272    impl Pass<i64> for Boom {
273        fn name(&self) -> &'static str {
274            "boom"
275        }
276        fn run(&mut self, _unit: &mut i64) -> Result<Outcome, PassError> {
277            Err(PassError::new("intentional"))
278        }
279    }
280
281    /// Flips a value between 0 and 1 every run — never converges.
282    struct Flip;
283    impl Pass<i64> for Flip {
284        fn name(&self) -> &'static str {
285            "flip"
286        }
287        fn run(&mut self, unit: &mut i64) -> Result<Outcome, PassError> {
288            *unit = 1 - *unit;
289            Ok(Outcome::Changed)
290        }
291    }
292
293    #[test]
294    fn test_new_is_empty() {
295        let pm = PassManager::<i64>::new();
296        assert!(pm.is_empty());
297        assert_eq!(pm.len(), 0);
298    }
299
300    #[test]
301    fn test_default_matches_new() {
302        let pm: PassManager<i64> = PassManager::default();
303        assert!(pm.is_empty());
304    }
305
306    #[test]
307    fn test_add_counts_passes() {
308        let mut pm = PassManager::new();
309        pm.add(Decrement).add(Decrement);
310        assert_eq!(pm.len(), 2);
311    }
312
313    #[test]
314    fn test_run_empty_pipeline_is_converged_noop() {
315        let mut pm = PassManager::<i64>::new();
316        let mut unit = 7;
317        let report = pm.run(&mut unit).unwrap();
318        assert_eq!(unit, 7);
319        assert!(report.runs().is_empty());
320        assert_eq!(report.iterations(), 1);
321        assert!(report.converged());
322    }
323
324    #[test]
325    fn test_run_preserves_registration_order() {
326        let mut pm = PassManager::new();
327        pm.add(Tag("a")).add(Tag("b")).add(Tag("c"));
328        let mut unit: Vec<&'static str> = vec![];
329        let report = pm.run(&mut unit).unwrap();
330        assert_eq!(unit, vec!["a", "b", "c"]);
331        let names: Vec<_> = report.runs().iter().map(|r| r.name()).collect();
332        assert_eq!(names, vec!["a", "b", "c"]);
333    }
334
335    #[test]
336    fn test_run_single_sweep_converged_when_unchanged() {
337        let mut pm = PassManager::new();
338        pm.add(Decrement);
339        let mut unit = 0; // already at fixpoint
340        let report = pm.run(&mut unit).unwrap();
341        assert!(report.converged());
342        assert_eq!(report.changes(), 0);
343    }
344
345    #[test]
346    fn test_run_single_sweep_not_converged_when_changed() {
347        let mut pm = PassManager::new();
348        pm.add(Decrement);
349        let mut unit = 5;
350        let report = pm.run(&mut unit).unwrap();
351        assert_eq!(unit, 4);
352        assert!(!report.converged());
353        assert_eq!(report.changes(), 1);
354    }
355
356    #[test]
357    fn test_run_to_fixpoint_settles() {
358        let mut pm = PassManager::new();
359        pm.add(Decrement);
360        let mut unit = 4;
361        let report = pm.run_to_fixpoint(&mut unit, 100).unwrap();
362        assert_eq!(unit, 0);
363        assert!(report.converged());
364        // Four sweeps decrement (4->3->2->1->0), a fifth confirms no change.
365        assert_eq!(report.iterations(), 5);
366        assert_eq!(report.changes(), 4);
367        assert_eq!(report.runs().len(), 5);
368    }
369
370    #[test]
371    fn test_run_to_fixpoint_respects_bound_when_oscillating() {
372        let mut pm = PassManager::new();
373        pm.add(Flip);
374        let mut unit = 0;
375        let report = pm.run_to_fixpoint(&mut unit, 10).unwrap();
376        assert_eq!(report.iterations(), 10);
377        assert!(!report.converged());
378        assert_eq!(report.runs().len(), 10);
379    }
380
381    #[test]
382    fn test_run_to_fixpoint_zero_iters_does_nothing() {
383        let mut pm = PassManager::new();
384        pm.add(Decrement);
385        let mut unit = 9;
386        let report = pm.run_to_fixpoint(&mut unit, 0).unwrap();
387        assert_eq!(unit, 9);
388        assert_eq!(report.iterations(), 0);
389        assert!(!report.converged());
390        assert!(report.runs().is_empty());
391    }
392
393    #[test]
394    fn test_run_halts_at_failing_pass_and_names_it() {
395        let mut pm = PassManager::new();
396        pm.add(Decrement).add(Boom).add(Decrement);
397        let mut unit = 5;
398        let err = pm.run(&mut unit).unwrap_err();
399        assert_eq!(err.pass(), "boom");
400        assert_eq!(err.message(), "intentional");
401        // The first Decrement ran (5 -> 4); the pass after Boom did not.
402        assert_eq!(unit, 4);
403    }
404
405    #[test]
406    fn test_run_to_fixpoint_propagates_failure() {
407        let mut pm = PassManager::new();
408        pm.add(Boom);
409        let mut unit = 0;
410        let err = pm.run_to_fixpoint(&mut unit, 5).unwrap_err();
411        assert_eq!(err.pass(), "boom");
412    }
413}