use core::fmt;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Budget {
pub node_visits: u64,
pub candidates: u64,
pub match_steps: u64,
pub rewrites: u64,
pub new_nodes: u64,
pub fact_work: u64,
pub pass_work: u64,
pub mba_calls: u64,
pub eqsat_nodes: u64,
pub eqsat_work: u64,
}
impl Budget {
pub const UNLIMITED: Budget = Budget {
node_visits: u64::MAX,
candidates: u64::MAX,
match_steps: u64::MAX,
rewrites: u64::MAX,
new_nodes: u64::MAX,
fact_work: u64::MAX,
pass_work: u64::MAX,
mba_calls: u64::MAX,
eqsat_nodes: u64::MAX,
eqsat_work: u64::MAX,
};
pub const ZERO: Budget = Budget {
node_visits: 0,
candidates: 0,
match_steps: 0,
rewrites: 0,
new_nodes: 0,
fact_work: 0,
pass_work: 0,
mba_calls: 0,
eqsat_nodes: 0,
eqsat_work: 0,
};
pub fn min(&self, o: &Budget) -> Budget {
self.zip(o, u64::min)
}
fn zip(&self, o: &Budget, f: impl Fn(u64, u64) -> u64) -> Budget {
Budget {
node_visits: f(self.node_visits, o.node_visits),
candidates: f(self.candidates, o.candidates),
match_steps: f(self.match_steps, o.match_steps),
rewrites: f(self.rewrites, o.rewrites),
new_nodes: f(self.new_nodes, o.new_nodes),
fact_work: f(self.fact_work, o.fact_work),
pass_work: f(self.pass_work, o.pass_work),
mba_calls: f(self.mba_calls, o.mba_calls),
eqsat_nodes: f(self.eqsat_nodes, o.eqsat_nodes),
eqsat_work: f(self.eqsat_work, o.eqsat_work),
}
}
pub(crate) fn saturating_sub(&self, o: &Budget) -> Budget {
self.zip(o, u64::saturating_sub)
}
pub(crate) fn saturating_add(&self, o: &Budget) -> Budget {
self.zip(o, u64::saturating_add)
}
}
impl Default for Budget {
fn default() -> Self {
Budget {
node_visits: 1 << 22,
candidates: 1 << 22,
match_steps: 1 << 26,
rewrites: 1 << 22,
new_nodes: 1 << 22,
fact_work: 1 << 26,
pass_work: 1 << 26,
mba_calls: 1 << 12,
eqsat_nodes: 1 << 22,
eqsat_work: 1 << 26,
}
}
}
setters!(Budget {
with_node_visits: node_visits: u64,
with_candidates: candidates: u64,
with_match_steps: match_steps: u64,
with_rewrites: rewrites: u64,
with_new_nodes: new_nodes: u64,
with_fact_work: fact_work: u64,
with_pass_work: pass_work: u64,
with_mba_calls: mba_calls: u64,
with_eqsat_nodes: eqsat_nodes: u64,
with_eqsat_work: eqsat_work: u64,
});
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Exhausted {
NodeVisits,
Candidates,
MatchSteps,
Rewrites,
NewNodes,
FactWork,
PassWork,
MbaCalls,
EqsatNodes,
EqsatWork,
Deadline,
ArenaCapacity,
}
impl fmt::Display for Exhausted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Exhausted::NodeVisits => "node visits",
Exhausted::Candidates => "candidates",
Exhausted::MatchSteps => "matcher steps",
Exhausted::Rewrites => "rewrites",
Exhausted::NewNodes => "new nodes",
Exhausted::FactWork => "fact work",
Exhausted::PassWork => "pass work",
Exhausted::MbaCalls => "MBA solver calls",
Exhausted::EqsatNodes => "e-nodes",
Exhausted::EqsatWork => "equality-saturation work",
Exhausted::Deadline => "deadline",
Exhausted::ArenaCapacity => "arena capacity",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Allowance {
remaining: Budget,
spent: Budget,
}
impl Allowance {
pub fn new(budget: Budget) -> Self {
Allowance {
remaining: budget,
spent: Budget::ZERO,
}
}
pub fn remaining(&self) -> Budget {
self.remaining
}
pub fn spent(&self) -> Budget {
self.spent
}
pub(crate) fn spend(&mut self, b: &Budget) {
self.remaining = self.remaining.saturating_sub(b);
self.spent = self.spent.saturating_add(b);
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Admission {
pub max_root_height: u32,
pub max_root_tree_size: u32,
pub max_root_dag_size: Option<u32>,
}
impl Default for Admission {
fn default() -> Self {
Admission {
max_root_height: u32::MAX,
max_root_tree_size: u32::MAX,
max_root_dag_size: None,
}
}
}
setters!(Admission {
with_max_root_height: max_root_height: u32,
with_max_root_tree_size: max_root_tree_size: u32,
with_max_root_dag_size: max_root_dag_size ? u32,
});
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Cap {
Height,
TreeSize,
DagSize,
}
pub trait Clock {
fn now_ticks(&self) -> u64;
}
#[derive(Copy, Clone)]
#[non_exhaustive]
pub struct Deadline<'a> {
pub clock: &'a dyn Clock,
pub at: u64,
pub check_every: u32,
}
impl<'a> Deadline<'a> {
pub fn new(clock: &'a dyn Clock, at: u64) -> Self {
Deadline {
clock,
at,
check_every: 64,
}
}
}
setters!(Deadline<'a> {
with_check_every: check_every: u32,
});
impl fmt::Debug for Deadline<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Deadline")
.field("at", &self.at)
.field("check_every", &self.check_every)
.finish_non_exhaustive()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(not(all(feature = "mba", feature = "eqsat")), allow(dead_code))] pub(crate) enum Counter {
NodeVisits,
Candidates,
Rewrites,
PassWork,
MbaCalls,
EqsatNodes,
EqsatWork,
}
impl Counter {
fn of(self, b: &Budget) -> u64 {
match self {
Counter::NodeVisits => b.node_visits,
Counter::Candidates => b.candidates,
Counter::Rewrites => b.rewrites,
Counter::PassWork => b.pass_work,
Counter::MbaCalls => b.mba_calls,
Counter::EqsatNodes => b.eqsat_nodes,
Counter::EqsatWork => b.eqsat_work,
}
}
fn slot(self, b: &mut Budget) -> &mut u64 {
match self {
Counter::NodeVisits => &mut b.node_visits,
Counter::Candidates => &mut b.candidates,
Counter::Rewrites => &mut b.rewrites,
Counter::PassWork => &mut b.pass_work,
Counter::MbaCalls => &mut b.mba_calls,
Counter::EqsatNodes => &mut b.eqsat_nodes,
Counter::EqsatWork => &mut b.eqsat_work,
}
}
fn exhausted(self) -> Exhausted {
match self {
Counter::NodeVisits => Exhausted::NodeVisits,
Counter::Candidates => Exhausted::Candidates,
Counter::Rewrites => Exhausted::Rewrites,
Counter::PassWork => Exhausted::PassWork,
Counter::MbaCalls => Exhausted::MbaCalls,
Counter::EqsatNodes => Exhausted::EqsatNodes,
Counter::EqsatWork => Exhausted::EqsatWork,
}
}
}
pub(crate) struct Meter<'a> {
pub(crate) limit: Budget,
pub(crate) spent: Budget,
deadline: Option<Deadline<'a>>,
until_check: u32,
expired: bool,
}
impl<'a> Meter<'a> {
pub(crate) fn new(limit: Budget, deadline: Option<Deadline<'a>>) -> Self {
Meter {
limit,
spent: Budget::ZERO,
deadline,
until_check: 0,
expired: false,
}
}
fn over(&self) -> Option<Exhausted> {
let (s, l) = (&self.spent, &self.limit);
if s.node_visits > l.node_visits {
Some(Exhausted::NodeVisits)
} else if s.candidates > l.candidates {
Some(Exhausted::Candidates)
} else if s.match_steps > l.match_steps {
Some(Exhausted::MatchSteps)
} else if s.rewrites > l.rewrites {
Some(Exhausted::Rewrites)
} else if s.new_nodes > l.new_nodes {
Some(Exhausted::NewNodes)
} else if s.fact_work > l.fact_work {
Some(Exhausted::FactWork)
} else if s.pass_work > l.pass_work {
Some(Exhausted::PassWork)
} else if s.mba_calls > l.mba_calls {
Some(Exhausted::MbaCalls)
} else if s.eqsat_nodes > l.eqsat_nodes {
Some(Exhausted::EqsatNodes)
} else if s.eqsat_work > l.eqsat_work {
Some(Exhausted::EqsatWork)
} else {
None
}
}
fn tick(&mut self) -> Result<(), Exhausted> {
if self.expired {
return Err(Exhausted::Deadline);
}
if let Some(d) = self.deadline {
if self.until_check == 0 {
self.until_check = d.check_every.max(1);
if d.clock.now_ticks() >= d.at {
self.expired = true;
return Err(Exhausted::Deadline);
}
}
self.until_check -= 1;
}
Ok(())
}
#[cfg_attr(not(feature = "eqsat"), allow(dead_code))]
pub(crate) fn deadline_now(&mut self) -> Result<(), Exhausted> {
if self.expired {
return Err(Exhausted::Deadline);
}
if let Some(d) = self.deadline
&& d.clock.now_ticks() >= d.at
{
self.expired = true;
return Err(Exhausted::Deadline);
}
Ok(())
}
#[inline]
pub(crate) fn charge(&mut self, c: Counter, n: u64) -> Result<(), Exhausted> {
if self.deadline.is_some() || self.expired {
self.tick()?;
}
debug_assert_eq!(self.over(), None, "a counter was recorded past its limit");
let spent = c.slot(&mut self.spent);
let after = spent.saturating_add(n);
if after > c.of(&self.limit) {
return Err(c.exhausted());
}
*spent = after;
Ok(())
}
pub(crate) fn visit(&mut self) -> Result<(), Exhausted> {
self.charge(Counter::NodeVisits, 1)
}
pub(crate) fn check(&self) -> Result<(), Exhausted> {
if self.expired {
return Err(Exhausted::Deadline);
}
match self.over() {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn left(&self) -> Budget {
self.limit.saturating_sub(&self.spent)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Ticks(core::cell::Cell<u64>);
impl Clock for Ticks {
fn now_ticks(&self) -> u64 {
let t = self.0.get();
self.0.set(t + 1);
t
}
}
#[test]
fn a_charge_the_limit_cannot_cover_is_refused_and_not_charged() {
let mut limit = Budget::UNLIMITED;
limit.pass_work = 10;
let mut m = Meter::new(limit, None);
assert_eq!(m.charge(Counter::PassWork, 7), Ok(()));
assert_eq!(m.charge(Counter::PassWork, 4), Err(Exhausted::PassWork));
assert_eq!(m.spent.pass_work, 7);
assert_eq!(m.charge(Counter::PassWork, 3), Ok(()));
assert_eq!(m.charge(Counter::PassWork, 1), Err(Exhausted::PassWork));
assert_eq!(m.spent.pass_work, 10);
let mut limit = Budget::UNLIMITED;
limit.eqsat_nodes = 0;
let mut m = Meter::new(limit, None);
assert_eq!(m.charge(Counter::EqsatNodes, 1), Err(Exhausted::EqsatNodes));
assert_eq!(m.spent, Budget::ZERO);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "a counter was recorded past its limit")]
fn a_counter_past_its_limit_is_caught_in_debug_builds() {
let mut limit = Budget::UNLIMITED;
limit.fact_work = 5;
let mut m = Meter::new(limit, None);
m.spent.fact_work = 10;
let _ = m.charge(Counter::MbaCalls, 1);
}
#[test]
fn every_charge_reads_the_deadline_when_due_and_the_stop_latches() {
let clock = Ticks(core::cell::Cell::new(0));
let d = Deadline {
clock: &clock,
at: 3,
check_every: 2,
};
let mut m = Meter::new(Budget::UNLIMITED, Some(d));
for _ in 0..6 {
assert_eq!(m.charge(Counter::EqsatWork, 1), Ok(()));
}
assert_eq!(m.charge(Counter::EqsatWork, 1), Err(Exhausted::Deadline));
assert_eq!(m.check(), Err(Exhausted::Deadline));
assert_eq!(m.visit(), Err(Exhausted::Deadline));
assert_eq!(clock.0.get(), 4);
}
}