use indexmap::{IndexMap, IndexSet};
use instant::{Duration, Instant};
use log::*;
use crate::{Analysis, EGraph, Id, Language, RecExpr, Rewrite, SearchMatches};
pub struct Runner<L: Language, N: Analysis<L>, IterData = ()> {
pub egraph: EGraph<L, N>,
pub iterations: Vec<Iteration<IterData>>,
pub roots: Vec<Id>,
pub stop_reason: Option<StopReason>,
iter_limit: usize,
node_limit: usize,
time_limit: Duration,
start_time: Option<Instant>,
scheduler: Box<dyn RewriteScheduler<L, N>>,
}
impl<L, N> Default for Runner<L, N, ()>
where
L: Language,
N: Analysis<L> + Default,
{
fn default() -> Self {
Runner::new(N::default())
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-1", derive(serde::Serialize))]
pub enum StopReason {
Saturated,
IterationLimit(usize),
NodeLimit(usize),
TimeLimit(f64),
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-1", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Iteration<IterData> {
pub egraph_nodes: usize,
pub egraph_classes: usize,
pub applied: IndexMap<String, usize>,
pub search_time: f64,
pub apply_time: f64,
pub rebuild_time: f64,
pub total_time: f64,
pub data: IterData,
pub n_rebuilds: usize,
pub stop_reason: Option<StopReason>,
}
type RunnerResult<T> = std::result::Result<T, StopReason>;
impl<L, N, IterData> Runner<L, N, IterData>
where
L: Language,
N: Analysis<L>,
IterData: IterationData<L, N>,
{
pub fn new(analysis: N) -> Self {
Self {
iter_limit: 30,
node_limit: 10_000,
time_limit: Duration::from_secs(5),
egraph: EGraph::new(analysis),
roots: vec![],
iterations: vec![],
stop_reason: None,
start_time: None,
scheduler: Box::new(BackoffScheduler::default()),
}
}
pub fn with_iter_limit(self, iter_limit: usize) -> Self {
Self { iter_limit, ..self }
}
pub fn with_node_limit(self, node_limit: usize) -> Self {
Self { node_limit, ..self }
}
pub fn with_time_limit(self, time_limit: Duration) -> Self {
Self { time_limit, ..self }
}
pub fn with_scheduler(self, scheduler: impl RewriteScheduler<L, N> + 'static) -> Self {
let scheduler = Box::new(scheduler);
Self { scheduler, ..self }
}
pub fn with_expr(mut self, expr: &RecExpr<L>) -> Self {
let id = self.egraph.add_expr(expr);
self.roots.push(id);
self
}
pub fn with_egraph(self, egraph: EGraph<L, N>) -> Self {
Self { egraph, ..self }
}
pub fn run(mut self, rules: &[Rewrite<L, N>]) -> Self {
check_rules(rules);
self.egraph.rebuild();
loop {
if let Err(stop_reason) = self.run_one(rules) {
info!("Stopping: {:?}", stop_reason);
self.stop_reason = Some(stop_reason);
self.iterations.push(Iteration {
stop_reason: self.stop_reason.clone(),
egraph_nodes: self.egraph.total_number_of_nodes(),
egraph_classes: self.egraph.number_of_classes(),
data: IterData::make(&self),
applied: Default::default(),
search_time: Default::default(),
apply_time: Default::default(),
rebuild_time: Default::default(),
total_time: Default::default(),
n_rebuilds: Default::default(),
});
break;
}
}
self
}
#[rustfmt::skip]
pub fn print_report(&self) {
let search_time: f64 = self.iterations.iter().map(|i| i.search_time).sum();
let apply_time: f64 = self.iterations.iter().map(|i| i.apply_time).sum();
let rebuild_time: f64 = self.iterations.iter().map(|i| i.rebuild_time).sum();
let total_time: f64 = self.iterations.iter().map(|i| i.total_time).sum();
let iters = self.iterations.len();
let rebuilds: usize = self.iterations.iter().map(|i| i.n_rebuilds).sum();
let eg = &self.egraph;
println!("Runner report");
println!("=============");
println!(" Stop reason: {:?}", self.stop_reason.as_ref().unwrap());
println!(" Iterations: {}", iters);
println!(" Egraph size: {} nodes, {} classes, {} memo", eg.total_number_of_nodes(), eg.number_of_classes(), eg.total_size());
println!(" Rebuilds: {}, {:.2} per iter", rebuilds, (rebuilds as f64) / (iters as f64));
println!(" Total time: {}", total_time);
println!(" Search: ({:.2}) {}", search_time / total_time, search_time);
println!(" Apply: ({:.2}) {}", apply_time / total_time, apply_time);
println!(" Rebuild: ({:.2}) {}", rebuild_time / total_time, rebuild_time);
}
fn run_one(&mut self, rules: &[Rewrite<L, N>]) -> RunnerResult<()> {
assert!(self.stop_reason.is_none());
info!("\nIteration {}", self.iterations.len());
self.try_start();
self.check_limits()?;
let i = self.iterations.len();
let egraph_nodes = self.egraph.total_size();
let egraph_classes = self.egraph.number_of_classes();
trace!("EGraph {:?}", self.egraph.dump());
let start_time = Instant::now();
let mut matches = Vec::new();
for rule in rules {
let ms = self.scheduler.search_rewrite(i, &self.egraph, rule);
matches.push(ms);
if self.check_limits().is_err() {
matches.clear();
break;
}
}
let search_time = start_time.elapsed().as_secs_f64();
info!("Search time: {}", search_time);
let apply_time = Instant::now();
let mut applied = IndexMap::new();
for (rw, ms) in rules.iter().zip(matches) {
let total_matches: usize = ms.iter().map(|m| m.substs.len()).sum();
if total_matches == 0 {
continue;
}
debug!("Applying {} {} times", rw.name(), total_matches);
let actually_matched = self.scheduler.apply_rewrite(i, &mut self.egraph, rw, ms);
if actually_matched > 0 {
if let Some(count) = applied.get_mut(rw.name()) {
*count += actually_matched;
} else {
applied.insert(rw.name().to_owned(), actually_matched);
}
debug!("Applied {} {} times", rw.name(), actually_matched);
}
if self.check_limits().is_err() {
break;
}
}
let apply_time = apply_time.elapsed().as_secs_f64();
info!("Apply time: {}", apply_time);
let rebuild_time = Instant::now();
let n_rebuilds = self.egraph.rebuild();
let rebuild_time = rebuild_time.elapsed().as_secs_f64();
info!("Rebuild time: {}", rebuild_time);
info!(
"Size: n={}, e={}",
self.egraph.total_size(),
self.egraph.number_of_classes()
);
let saturated = applied.is_empty() && self.scheduler.can_stop(i);
self.iterations.push(Iteration {
applied,
egraph_nodes,
egraph_classes,
search_time,
apply_time,
rebuild_time,
n_rebuilds,
data: IterData::make(&self),
total_time: start_time.elapsed().as_secs_f64(),
stop_reason: None,
});
if saturated {
Err(StopReason::Saturated)
} else {
Ok(())
}
}
fn try_start(&mut self) {
self.start_time.get_or_insert_with(Instant::now);
}
fn check_limits(&self) -> RunnerResult<()> {
let elapsed = self.start_time.unwrap().elapsed();
if elapsed > self.time_limit {
return Err(StopReason::TimeLimit(elapsed.as_secs_f64()));
}
let size = self.egraph.total_size();
if size > self.node_limit {
return Err(StopReason::NodeLimit(size));
}
if self.iterations.len() >= self.iter_limit {
return Err(StopReason::IterationLimit(self.iterations.len()));
}
Ok(())
}
}
fn check_rules<L, N>(rules: &[Rewrite<L, N>]) {
let mut name_counts = IndexMap::new();
for rw in rules {
*name_counts.entry(rw.name()).or_default() += 1
}
name_counts.retain(|_, count: &mut usize| *count > 1);
if !name_counts.is_empty() {
eprintln!("WARNING: Duplicated rule names may affect rule reporting and scheduling.");
log::warn!("Duplicated rule names may affect rule reporting and scheduling.");
for (name, &count) in name_counts.iter() {
assert!(count > 1);
eprintln!("Rule '{}' appears {} times", name, count);
log::warn!("Rule '{}' appears {} times", name, count);
}
}
}
#[allow(unused_variables)]
pub trait RewriteScheduler<L, N>
where
L: Language,
N: Analysis<L>,
{
fn can_stop(&mut self, iteration: usize) -> bool {
true
}
fn search_rewrite(
&mut self,
iteration: usize,
egraph: &EGraph<L, N>,
rewrite: &Rewrite<L, N>,
) -> Vec<SearchMatches> {
rewrite.search(egraph)
}
fn apply_rewrite(
&mut self,
iteration: usize,
egraph: &mut EGraph<L, N>,
rewrite: &Rewrite<L, N>,
matches: Vec<SearchMatches>,
) -> usize {
rewrite.apply(egraph, &matches).len()
}
}
pub struct SimpleScheduler;
impl<L, N> RewriteScheduler<L, N> for SimpleScheduler
where
L: Language,
N: Analysis<L>,
{
}
pub struct BackoffScheduler {
initial_match_limit: usize,
ban_length: usize,
stats: IndexMap<String, RuleStats>,
dont_ban: IndexSet<String>,
}
struct RuleStats {
times_applied: usize,
banned_until: usize,
times_banned: usize,
}
impl BackoffScheduler {
pub fn with_initial_match_limit(self, initial_match_limit: usize) -> Self {
Self {
initial_match_limit,
..self
}
}
pub fn with_ban_length(self, ban_length: usize) -> Self {
Self { ban_length, ..self }
}
pub fn do_not_ban(mut self, name: impl Into<String>) -> Self {
self.dont_ban.insert(name.into());
self
}
}
impl Default for BackoffScheduler {
fn default() -> Self {
Self {
dont_ban: Default::default(),
stats: Default::default(),
initial_match_limit: 1_000,
ban_length: 5,
}
}
}
impl<L, N> RewriteScheduler<L, N> for BackoffScheduler
where
L: Language,
N: Analysis<L>,
{
fn can_stop(&mut self, iteration: usize) -> bool {
let n_stats = self.stats.len();
assert!(n_stats > 0);
let mut banned: Vec<_> = self
.stats
.iter_mut()
.filter(|(_, s)| s.banned_until > iteration)
.collect();
if banned.is_empty() {
true
} else {
let min_ban = banned
.iter()
.map(|(_, s)| s.banned_until)
.min()
.expect("banned cannot be empty here");
assert!(min_ban >= iteration);
let delta = min_ban - iteration;
let mut unbanned = vec![];
for (name, s) in &mut banned {
s.banned_until -= delta;
if s.banned_until == iteration {
unbanned.push(name.as_str());
}
}
assert!(!unbanned.is_empty());
info!(
"Banned {}/{}, fast-forwarded by {} to unban {}",
banned.len(),
n_stats,
delta,
unbanned.join(", "),
);
false
}
}
fn search_rewrite(
&mut self,
iteration: usize,
egraph: &EGraph<L, N>,
rewrite: &Rewrite<L, N>,
) -> Vec<SearchMatches> {
if let Some(limit) = self.stats.get_mut(rewrite.name()) {
if iteration < limit.banned_until {
debug!(
"Skipping {} ({}-{}), banned until {}...",
rewrite.name(),
limit.times_applied,
limit.times_banned,
limit.banned_until,
);
return vec![];
}
let matches = rewrite.search(egraph);
let total_len: usize = matches.iter().map(|m| m.substs.len()).sum();
let threshold = self.initial_match_limit << limit.times_banned;
if total_len > threshold {
let ban_length = self.ban_length << limit.times_banned;
limit.times_banned += 1;
limit.banned_until = iteration + ban_length;
info!(
"Banning {} ({}-{}) for {} iters: {} < {}",
rewrite.name(),
limit.times_applied,
limit.times_banned,
ban_length,
threshold,
total_len,
);
vec![]
} else {
limit.times_applied += 1;
matches
}
} else {
if !self.dont_ban.contains(rewrite.name()) {
self.stats.insert(
rewrite.name().into(),
RuleStats {
times_applied: 0,
banned_until: 0,
times_banned: 0,
},
);
}
rewrite.search(egraph)
}
}
}
pub trait IterationData<L, N>: Sized
where
L: Language,
N: Analysis<L>,
{
fn make(runner: &Runner<L, N, Self>) -> Self;
}
impl<L, N> IterationData<L, N> for ()
where
L: Language,
N: Analysis<L>,
{
fn make(_: &Runner<L, N, Self>) -> Self {}
}