use crate::pass;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Gates {
rules: Vec<Rule>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Rule {
pass: String,
on: bool,
scope: Scope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Scope {
Everything,
These(Vec<Pick>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Pick {
Id(u32),
Span(u32, u32),
Name(String),
}
impl Pick {
fn covers(&self, id: u32, name: &str) -> bool {
match self {
Pick::Id(want) => *want == id,
Pick::Span(low, high) => (*low..=*high).contains(&id),
Pick::Name(want) => want == name,
}
}
}
impl Scope {
fn covers(&self, id: u32, name: &str) -> bool {
match self {
Scope::Everything => true,
Scope::These(picks) => picks.iter().any(|pick| pick.covers(id, name)),
}
}
}
impl Gates {
pub fn add(&mut self, on: bool, spec: &str) -> Result<(), String> {
let (name, list) = match spec.split_once('=') {
Some((name, list)) => (name, Some(list)),
None => (spec, None),
};
if pass::find(name).is_none() {
return Err(format!("`{name}` is not a pass this compiler has, see --print-pipeline"));
}
let scope = match list {
None => Scope::Everything,
Some(list) => Scope::These(picks(list)?),
};
self.rules.push(Rule { pass: name.to_owned(), on, scope });
Ok(())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
#[must_use]
pub fn allows(&self, pass: &str, default: bool, id: u32, name: &str) -> bool {
let mut answer = default;
for rule in &self.rules {
if rule.pass == pass && rule.scope.covers(id, name) {
answer = rule.on;
}
}
answer
}
#[must_use]
pub fn enabled(&self) -> Vec<&str> {
let mut out: Vec<&str> = Vec::new();
for rule in self.rules.iter().filter(|rule| rule.on) {
let name = rule.pass.as_str();
if !out.contains(&name) {
out.push(name);
}
}
out
}
#[must_use]
pub fn note(&self, pass: &str) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
for rule in self.rules.iter().filter(|rule| rule.pass == pass) {
let word = if rule.on { "on" } else { "off" };
parts.push(match &rule.scope {
Scope::Everything => word.to_owned(),
Scope::These(picks) => format!("{word} for {}", render(picks)),
});
}
match parts.is_empty() {
true => None,
false => Some(parts.join(", ")),
}
}
}
fn picks(list: &str) -> Result<Vec<Pick>, String> {
if list.is_empty() {
return Err("the list of functions after the `=` is empty".to_owned());
}
let mut out = Vec::new();
for item in list.split(',') {
out.push(pick(item)?);
}
Ok(out)
}
fn pick(item: &str) -> Result<Pick, String> {
if item.is_empty() {
return Err("there is an empty item in the list of functions".to_owned());
}
if !item.starts_with(|c: char| c.is_ascii_digit()) {
return Ok(Pick::Name(item.to_owned()));
}
let Some((low, high)) = item.split_once('-') else {
return Ok(Pick::Id(number(item)?));
};
let (low, high) = (number(low)?, number(high)?);
if low > high {
return Err(format!("the range `{item}` ends before it starts"));
}
Ok(Pick::Span(low, high))
}
fn number(text: &str) -> Result<u32, String> {
text.parse().map_err(|_| format!("`{text}` is not the number of a function"))
}
fn render(picks: &[Pick]) -> String {
let parts: Vec<String> = picks
.iter()
.map(|pick| match pick {
Pick::Id(id) => id.to_string(),
Pick::Span(low, high) => format!("{low}-{high}"),
Pick::Name(name) => name.clone(),
})
.collect();
parts.join(",")
}
#[cfg(test)]
mod tests {
use super::Gates;
fn gates(flags: &[(bool, &str)]) -> Gates {
let mut gates = Gates::default();
for (on, spec) in flags {
gates.add(*on, spec).expect("the test asked for a gate this compiler refuses");
}
gates
}
#[test]
fn nothing_asked_for_means_the_level_decides() {
let gates = Gates::default();
assert!(gates.is_empty());
assert!(gates.allows("fold", true, 0, "main"));
assert!(!gates.allows("fold", false, 0, "main"));
assert_eq!(gates.note("fold"), None);
}
#[test]
fn disabling_a_pass_with_no_range_takes_it_away_from_every_function() {
let gates = gates(&[(false, "fold")]);
assert!(!gates.allows("fold", true, 0, "main"));
assert!(!gates.allows("fold", true, 7, "other"));
assert!(gates.allows("dce", true, 0, "main"), "one pass named is not every pass named");
}
#[test]
fn a_range_leaves_every_function_it_does_not_name_alone() {
let gates = gates(&[(false, "fold=1-3")]);
assert!(gates.allows("fold", true, 0, "a"));
assert!(!gates.allows("fold", true, 1, "b"));
assert!(!gates.allows("fold", true, 3, "d"));
assert!(gates.allows("fold", true, 4, "e"));
}
#[test]
fn a_function_can_be_named_as_well_as_numbered() {
let gates = gates(&[(false, "dce=parse_line,9")]);
assert!(!gates.allows("dce", true, 0, "parse_line"));
assert!(!gates.allows("dce", true, 9, "whatever"));
assert!(gates.allows("dce", true, 0, "main"));
}
#[test]
fn the_last_rule_that_covers_a_function_is_the_one_that_decides() {
let gates = gates(&[(false, "fold"), (true, "fold=2")]);
assert!(!gates.allows("fold", true, 1, "a"));
assert!(gates.allows("fold", true, 2, "b"), "the second rule covers this one");
}
#[test]
fn enabling_a_pass_reaches_one_the_level_did_not_choose() {
let gates = gates(&[(true, "narrow=2")]);
assert!(!gates.allows("narrow", false, 1, "a"));
assert!(gates.allows("narrow", false, 2, "b"));
assert_eq!(gates.enabled(), ["narrow"]);
}
#[test]
fn a_pass_enabled_twice_is_named_once() {
let gates = gates(&[(true, "narrow=2"), (false, "fold"), (true, "narrow=5")]);
assert_eq!(gates.enabled(), ["narrow"]);
}
#[test]
fn the_listing_says_what_was_asked_for() {
let gates = gates(&[(false, "fold"), (true, "fold=2-4,main")]);
assert_eq!(gates.note("fold").as_deref(), Some("off, on for 2-4,main"));
assert_eq!(gates.note("dce"), None);
}
#[test]
fn a_pass_this_compiler_does_not_have_is_refused_rather_than_ignored() {
let mut gates = Gates::default();
let why = gates.add(false, "nosuch").expect_err("a pass that does not exist was accepted");
assert!(why.contains("not a pass"), "{why}");
assert!(gates.is_empty());
}
#[test]
fn a_range_list_that_says_nothing_is_refused() {
let mut gates = Gates::default();
assert!(gates.add(false, "fold=").is_err());
assert!(gates.add(false, "fold=1,,3").is_err());
}
#[test]
fn a_range_that_runs_backwards_is_refused() {
let mut gates = Gates::default();
let why = gates.add(false, "fold=9-2").expect_err("a backwards range was accepted");
assert!(why.contains("ends before it starts"), "{why}");
}
#[test]
fn a_number_that_is_not_one_is_refused() {
let mut gates = Gates::default();
assert!(gates.add(false, "fold=1x").is_err());
assert!(gates.add(false, "fold=1-x").is_err());
}
}