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}