use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Rule {
StatsAll,
Presize,
DirectAddressing,
ValidityFree,
FilterOrder,
TopNSeed,
NarrowArithmetic,
JoinElimination,
MemoryReservation,
GraphSections,
}
impl Rule {
pub const ALL: [Self; 10] = [
Self::StatsAll,
Self::Presize,
Self::DirectAddressing,
Self::ValidityFree,
Self::FilterOrder,
Self::TopNSeed,
Self::NarrowArithmetic,
Self::JoinElimination,
Self::MemoryReservation,
Self::GraphSections,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::StatsAll => "stats.all",
Self::Presize => "stats.presize",
Self::DirectAddressing => "stats.direct_addressing",
Self::ValidityFree => "stats.validity_free",
Self::FilterOrder => "stats.filter_order",
Self::TopNSeed => "stats.top_n_seed",
Self::NarrowArithmetic => "stats.narrow_arithmetic",
Self::JoinElimination => "stats.join_elimination",
Self::MemoryReservation => "stats.memory_reservation",
Self::GraphSections => "graph.sections",
}
}
#[must_use]
pub const fn master(self) -> Option<Self> {
match self {
Self::StatsAll | Self::GraphSections => None,
_ => Some(Self::StatsAll),
}
}
#[must_use]
pub const fn starts_on(self) -> bool {
!matches!(self, Self::GraphSections)
}
#[must_use]
pub fn from_name(key: &str) -> Option<Self> {
let name = canonical(key);
Self::ALL.into_iter().find(|rule| rule.name() == name)
}
}
#[must_use]
pub fn looks_like_rule(key: &str) -> bool {
let name = canonical(key);
name.starts_with("stats.") || name.starts_with("graph.") || Rule::from_name(&name).is_some()
}
#[must_use]
pub fn rule_names() -> String {
Rule::ALL.map(Rule::name).join(", ")
}
fn canonical(key: &str) -> String {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"statistics" => return Rule::StatsAll.name().to_string(),
"graph_sections" => return Rule::GraphSections.name().to_string(),
_ => {}
}
if lower.contains('.') {
return lower;
}
match lower.split_once('_') {
Some((head, rest)) => format!("{head}.{rest}"),
None => lower,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rules(u16);
impl Default for Rules {
fn default() -> Self {
Self::new()
}
}
impl Rules {
#[must_use]
pub const fn new() -> Self {
let mut bits = 0;
let mut index = 0;
while index < Rule::ALL.len() {
let rule = Rule::ALL[index];
if rule.starts_on() {
bits |= bit(rule);
}
index += 1;
}
Self(bits)
}
#[must_use]
pub fn enabled(self, rule: Rule) -> bool {
match rule.master() {
Some(master) if !self.is_set(master) => false,
_ => self.is_set(rule),
}
}
#[must_use]
pub fn is_set(self, rule: Rule) -> bool {
self.0 & bit(rule) != 0
}
pub fn set(&mut self, rule: Rule, enabled: bool) {
if enabled {
self.0 |= bit(rule);
} else {
self.0 &= !bit(rule);
}
}
pub fn set_named(&mut self, key: &str, enabled: bool) -> Result<()> {
let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
self.set(rule, enabled);
Ok(())
}
pub fn reset_named(&mut self, key: &str) -> Result<()> {
let Some(rule) = Rule::from_name(key) else { return Err(no_such_rule(key)) };
self.set(rule, rule.starts_on());
Ok(())
}
#[must_use]
pub fn named(self, key: &str) -> Option<bool> {
Rule::from_name(key).map(|rule| self.is_set(rule))
}
pub fn states(self) -> impl Iterator<Item = (&'static str, bool)> {
Rule::ALL.into_iter().map(move |rule| (rule.name(), self.is_set(rule)))
}
pub fn changed(self) -> impl Iterator<Item = (&'static str, bool)> {
let fresh = Self::new();
Rule::ALL
.into_iter()
.filter(move |&rule| self.is_set(rule) != fresh.is_set(rule))
.map(move |rule| (rule.name(), self.is_set(rule)))
}
}
const fn bit(rule: Rule) -> u16 {
1 << (rule as u16)
}
fn no_such_rule(key: &str) -> Error {
Error::catalog(format!("no rule called {key}, the rules are {}", rule_names()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_statistics_rule_starts_on_and_the_graph_sections_start_off() {
let rules = Rules::new();
for rule in Rule::ALL {
if rule == Rule::GraphSections {
assert!(!rules.enabled(rule), "the graph sections should start off");
} else {
assert!(rules.enabled(rule), "{} should start on", rule.name());
}
}
assert_eq!(rules.changed().count(), 0);
}
#[test]
fn turning_the_graph_sections_on_is_a_change_worth_reporting() {
let mut rules = Rules::new();
rules.set(Rule::GraphSections, true);
assert!(rules.enabled(Rule::GraphSections));
assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("graph.sections", true)]);
}
#[test]
fn the_master_turns_off_the_rules_under_it() {
let mut rules = Rules::new();
rules.set(Rule::StatsAll, false);
assert!(!rules.enabled(Rule::Presize));
assert!(!rules.enabled(Rule::NarrowArithmetic));
rules.set(Rule::GraphSections, true);
assert!(rules.enabled(Rule::GraphSections));
assert!(rules.is_set(Rule::Presize));
}
#[test]
fn one_rule_goes_off_without_taking_the_others_with_it() {
let mut rules = Rules::new();
rules.set(Rule::FilterOrder, false);
assert!(!rules.enabled(Rule::FilterOrder));
assert!(rules.enabled(Rule::Presize));
assert!(rules.enabled(Rule::StatsAll));
assert_eq!(rules.changed().collect::<Vec<_>>(), vec![("stats.filter_order", false)]);
}
#[test]
fn the_spellings_all_reach_the_same_rule() {
for spelling in ["stats.all", "stats_all", "statistics", "STATISTICS", "Stats.All"] {
assert_eq!(Rule::from_name(spelling), Some(Rule::StatsAll), "{spelling}");
}
for spelling in ["graph.sections", "graph_sections", "GRAPH.SECTIONS"] {
assert_eq!(Rule::from_name(spelling), Some(Rule::GraphSections), "{spelling}");
}
for spelling in ["stats.top_n_seed", "stats_top_n_seed"] {
assert_eq!(Rule::from_name(spelling), Some(Rule::TopNSeed), "{spelling}");
}
}
#[test]
fn a_name_nobody_has_is_not_a_rule() {
assert_eq!(Rule::from_name("memory_limit"), None);
assert_eq!(Rule::from_name("stats.presise"), None);
assert!(!looks_like_rule("memory_limit"));
assert!(!looks_like_rule("threads"));
assert!(looks_like_rule("stats.presise"));
assert!(looks_like_rule("graph_adjacency"));
}
#[test]
fn setting_by_name_says_what_the_names_are() {
let mut rules = Rules::new();
rules.set_named("stats_presize", false).expect("a rule by its underscore spelling");
assert!(!rules.enabled(Rule::Presize));
assert_eq!(rules.named("stats.presize"), Some(false));
let refused = rules.set_named("stats.presise", false).expect_err("no such rule");
assert!(refused.to_string().contains("stats.presize"), "{refused}");
}
#[test]
fn every_rule_has_its_own_bit() {
let mut seen = Vec::new();
for rule in Rule::ALL {
assert!(!seen.contains(&bit(rule)), "{} shares a bit", rule.name());
seen.push(bit(rule));
}
}
#[test]
fn a_report_lists_every_rule() {
let states = Rules::new().states().collect::<Vec<_>>();
assert_eq!(states.len(), Rule::ALL.len());
assert_eq!(states[0], ("stats.all", true));
}
}