use alloc::boxed::Box;
use alloc::vec::Vec;
use crate::error::PassError;
use crate::pass::Pass;
use crate::report::Report;
pub struct PassManager<T> {
passes: Vec<Box<dyn Pass<T>>>,
}
impl<T> PassManager<T> {
#[must_use]
pub fn new() -> Self {
Self { passes: Vec::new() }
}
pub fn add(&mut self, pass: impl Pass<T> + 'static) -> &mut Self {
self.passes.push(Box::new(pass));
self
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.passes.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.passes.is_empty()
}
pub fn run(&mut self, unit: &mut T) -> Result<Report, PassError> {
let mut report = Report::with_capacity(self.passes.len());
let mut changed = false;
for pass in &mut self.passes {
let outcome = pass.run(unit).map_err(|e| e.in_pass(pass.name()))?;
changed |= outcome.changed();
report.record(pass.name(), outcome);
}
report.finalize(1, !changed);
Ok(report)
}
pub fn run_to_fixpoint(&mut self, unit: &mut T, max_iters: usize) -> Result<Report, PassError> {
let mut report = Report::with_capacity(self.passes.len());
let mut iterations = 0;
let mut converged = false;
while iterations < max_iters {
iterations += 1;
let mut changed = false;
for pass in &mut self.passes {
let outcome = pass.run(unit).map_err(|e| e.in_pass(pass.name()))?;
changed |= outcome.changed();
report.record(pass.name(), outcome);
}
if !changed {
converged = true;
break;
}
}
report.finalize(iterations, converged);
Ok(report)
}
}
impl<T> Default for PassManager<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::pass::Outcome;
use alloc::vec;
use alloc::vec::Vec;
struct Tag(&'static str);
impl Pass<Vec<&'static str>> for Tag {
fn name(&self) -> &'static str {
self.0
}
fn run(&mut self, unit: &mut Vec<&'static str>) -> Result<Outcome, PassError> {
unit.push(self.0);
Ok(Outcome::Changed)
}
}
struct Decrement;
impl Pass<i64> for Decrement {
fn name(&self) -> &'static str {
"decrement"
}
fn run(&mut self, unit: &mut i64) -> Result<Outcome, PassError> {
if *unit <= 0 {
return Ok(Outcome::Unchanged);
}
*unit -= 1;
Ok(Outcome::Changed)
}
}
struct Boom;
impl Pass<i64> for Boom {
fn name(&self) -> &'static str {
"boom"
}
fn run(&mut self, _unit: &mut i64) -> Result<Outcome, PassError> {
Err(PassError::new("intentional"))
}
}
struct Flip;
impl Pass<i64> for Flip {
fn name(&self) -> &'static str {
"flip"
}
fn run(&mut self, unit: &mut i64) -> Result<Outcome, PassError> {
*unit = 1 - *unit;
Ok(Outcome::Changed)
}
}
#[test]
fn test_new_is_empty() {
let pm = PassManager::<i64>::new();
assert!(pm.is_empty());
assert_eq!(pm.len(), 0);
}
#[test]
fn test_default_matches_new() {
let pm: PassManager<i64> = PassManager::default();
assert!(pm.is_empty());
}
#[test]
fn test_add_counts_passes() {
let mut pm = PassManager::new();
pm.add(Decrement).add(Decrement);
assert_eq!(pm.len(), 2);
}
#[test]
fn test_run_empty_pipeline_is_converged_noop() {
let mut pm = PassManager::<i64>::new();
let mut unit = 7;
let report = pm.run(&mut unit).unwrap();
assert_eq!(unit, 7);
assert!(report.runs().is_empty());
assert_eq!(report.iterations(), 1);
assert!(report.converged());
}
#[test]
fn test_run_preserves_registration_order() {
let mut pm = PassManager::new();
pm.add(Tag("a")).add(Tag("b")).add(Tag("c"));
let mut unit: Vec<&'static str> = vec![];
let report = pm.run(&mut unit).unwrap();
assert_eq!(unit, vec!["a", "b", "c"]);
let names: Vec<_> = report.runs().iter().map(|r| r.name()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn test_run_single_sweep_converged_when_unchanged() {
let mut pm = PassManager::new();
pm.add(Decrement);
let mut unit = 0; let report = pm.run(&mut unit).unwrap();
assert!(report.converged());
assert_eq!(report.changes(), 0);
}
#[test]
fn test_run_single_sweep_not_converged_when_changed() {
let mut pm = PassManager::new();
pm.add(Decrement);
let mut unit = 5;
let report = pm.run(&mut unit).unwrap();
assert_eq!(unit, 4);
assert!(!report.converged());
assert_eq!(report.changes(), 1);
}
#[test]
fn test_run_to_fixpoint_settles() {
let mut pm = PassManager::new();
pm.add(Decrement);
let mut unit = 4;
let report = pm.run_to_fixpoint(&mut unit, 100).unwrap();
assert_eq!(unit, 0);
assert!(report.converged());
assert_eq!(report.iterations(), 5);
assert_eq!(report.changes(), 4);
assert_eq!(report.runs().len(), 5);
}
#[test]
fn test_run_to_fixpoint_respects_bound_when_oscillating() {
let mut pm = PassManager::new();
pm.add(Flip);
let mut unit = 0;
let report = pm.run_to_fixpoint(&mut unit, 10).unwrap();
assert_eq!(report.iterations(), 10);
assert!(!report.converged());
assert_eq!(report.runs().len(), 10);
}
#[test]
fn test_run_to_fixpoint_zero_iters_does_nothing() {
let mut pm = PassManager::new();
pm.add(Decrement);
let mut unit = 9;
let report = pm.run_to_fixpoint(&mut unit, 0).unwrap();
assert_eq!(unit, 9);
assert_eq!(report.iterations(), 0);
assert!(!report.converged());
assert!(report.runs().is_empty());
}
#[test]
fn test_run_halts_at_failing_pass_and_names_it() {
let mut pm = PassManager::new();
pm.add(Decrement).add(Boom).add(Decrement);
let mut unit = 5;
let err = pm.run(&mut unit).unwrap_err();
assert_eq!(err.pass(), "boom");
assert_eq!(err.message(), "intentional");
assert_eq!(unit, 4);
}
#[test]
fn test_run_to_fixpoint_propagates_failure() {
let mut pm = PassManager::new();
pm.add(Boom);
let mut unit = 0;
let err = pm.run_to_fixpoint(&mut unit, 5).unwrap_err();
assert_eq!(err.pass(), "boom");
}
}