pub const MARK: &str = "/* rucc */";
pub const MACRO: &str = "__GLIBC_MINOR__";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Releases {
minors: Vec<u32>,
}
impl Releases {
pub fn new(minors: Vec<u32>) -> Result<Self, String> {
if minors.len() < 2 {
return Err("merging wants at least two releases, and one is a copy".to_owned());
}
for pair in minors.windows(2) {
if pair[0] >= pair[1] {
return Err(format!(
"the releases have to be ascending and distinct, and 2.{} comes after 2.{}",
pair[0], pair[1]
));
}
}
Ok(Self { minors })
}
pub fn count(&self) -> usize {
self.minors.len()
}
pub fn minors(&self) -> &[u32] {
&self.minors
}
pub fn spelled(&self, at: usize) -> String {
format!("2.{}", self.minors[at])
}
pub fn condition(&self, present: &[bool]) -> Option<String> {
assert_eq!(present.len(), self.minors.len(), "a presence set has one flag per release");
if present.iter().all(|&p| p) {
return None;
}
let mut runs: Vec<Vec<String>> = Vec::new();
let mut at = 0;
while at < present.len() {
if !present[at] {
at += 1;
continue;
}
let start = at;
while at + 1 < present.len() && present[at + 1] {
at += 1;
}
let mut atoms: Vec<String> = Vec::new();
if start > 0 {
atoms.push(format!("{MACRO} >= {}", self.minors[start]));
}
if at + 1 < present.len() {
atoms.push(format!("{MACRO} < {}", self.minors[at + 1]));
}
runs.push(atoms);
at += 1;
}
if runs.is_empty() {
return Some("0".to_owned());
}
let one = runs.len() == 1;
let terms: Vec<String> = runs
.into_iter()
.map(|atoms| match atoms.len() {
0 => "1".to_owned(),
1 => atoms.into_iter().next().unwrap_or_default(),
_ if one => atoms.join(" && "),
_ => format!("({})", atoms.join(" && ")),
})
.collect();
Some(terms.join(" || "))
}
}
pub fn directive(keyword: &str, condition: Option<&str>) -> String {
match condition {
Some(text) => format!("#{keyword} {text} {MARK}\n"),
None => format!("#{keyword} {MARK}\n"),
}
}
pub fn carries_mark(text: &str) -> bool {
text.contains(MARK)
}
pub fn evaluate(text: &str, minor: u32) -> Result<String, String> {
struct Frame {
outer: bool,
taken: bool,
active: bool,
}
let mut stack: Vec<Frame> = Vec::new();
let mut out = String::with_capacity(text.len());
for (n, line) in text.split_inclusive('\n').enumerate() {
let at = n + 1;
let active = stack.last().is_none_or(|f| f.active);
let Some(body) = ours(line) else {
if active {
out.push_str(line);
}
continue;
};
if let Some(condition) = body.strip_prefix("#if ") {
let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
stack.push(Frame { outer: active, taken: holds, active: active && holds });
} else if let Some(condition) = body.strip_prefix("#elif ") {
let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
let frame = stack.last_mut().ok_or(format!("{at}: #elif with no #if"))?;
frame.active = frame.outer && !frame.taken && holds;
frame.taken = frame.taken || holds;
} else if body == "#else" {
let frame = stack.last_mut().ok_or(format!("{at}: #else with no #if"))?;
frame.active = frame.outer && !frame.taken;
frame.taken = true;
} else if body == "#endif" {
stack.pop().ok_or(format!("{at}: #endif with no #if"))?;
} else {
return Err(format!("{at}: {body} is marked as ours and is not a directive"));
}
}
if stack.is_empty() {
Ok(out)
} else {
Err(format!("{} of our conditionals are still open at the end", stack.len()))
}
}
fn ours(line: &str) -> Option<&str> {
let body = line.trim_end();
let body = body.strip_suffix(MARK)?.trim_end();
body.starts_with('#').then_some(body)
}
fn holds(condition: &str, minor: u32) -> Result<bool, String> {
let mut any = false;
for term in condition.split("||") {
let term = term.trim();
let term = match term.strip_prefix('(') {
Some(rest) => {
rest.strip_suffix(')').ok_or(format!("unbalanced parentheses: {term}"))?
}
None => term,
};
let mut all = true;
for atom in term.split("&&") {
all &= atom_holds(atom.trim(), minor)?;
}
any |= all;
}
Ok(any)
}
fn atom_holds(atom: &str, minor: u32) -> Result<bool, String> {
match atom {
"1" => return Ok(true),
"0" => return Ok(false),
_ => {}
}
let rest = atom.strip_prefix(MACRO).ok_or(format!("not a condition of ours: {atom}"))?;
let rest = rest.trim_start();
if let Some(value) = rest.strip_prefix(">=") {
Ok(minor >= number(value)?)
} else if let Some(value) = rest.strip_prefix('<') {
Ok(minor < number(value)?)
} else {
Err(format!("not a comparison of ours: {atom}"))
}
}
fn number(text: &str) -> Result<u32, String> {
text.trim().parse().map_err(|_| format!("not a version: {text}"))
}
#[cfg(test)]
mod tests {
use super::*;
const EIGHT: [u32; 8] = [28, 31, 34, 35, 36, 39, 41, 44];
fn releases() -> Releases {
Releases::new(EIGHT.to_vec()).expect("ascending and distinct")
}
fn present(of: &[u32]) -> Vec<bool> {
EIGHT.iter().map(|m| of.contains(m)).collect()
}
fn holding(condition: &Option<String>) -> Vec<u32> {
EIGHT
.iter()
.copied()
.filter(|&m| match condition {
None => true,
Some(text) => holds(text, m).expect("our own grammar"),
})
.collect()
}
#[test]
fn every_subset_of_eight_releases_gets_a_condition_that_means_it() {
let all = releases();
for bits in 0u32..256 {
let chosen: Vec<u32> = EIGHT
.iter()
.enumerate()
.filter(|(n, _)| bits & (1 << n) != 0)
.map(|(_, &m)| m)
.collect();
let condition = all.condition(&present(&chosen));
assert_eq!(holding(&condition), chosen, "{condition:?}");
}
}
#[test]
fn the_whole_set_wants_no_conditional() {
assert_eq!(releases().condition(&[true; 8]), None);
}
#[test]
fn the_shapes_a_reviewer_reads() {
let all = releases();
assert_eq!(all.condition(&present(&[44])), Some("__GLIBC_MINOR__ >= 44".to_owned()));
assert_eq!(
all.condition(&present(&[39, 41, 44])),
Some("__GLIBC_MINOR__ >= 39".to_owned())
);
assert_eq!(all.condition(&present(&[28])), Some("__GLIBC_MINOR__ < 31".to_owned()));
assert_eq!(all.condition(&present(&[28, 31])), Some("__GLIBC_MINOR__ < 34".to_owned()));
assert_eq!(
all.condition(&present(&[34, 35])),
Some("__GLIBC_MINOR__ >= 34 && __GLIBC_MINOR__ < 36".to_owned())
);
assert_eq!(
all.condition(&present(&[28, 41, 44])),
Some("__GLIBC_MINOR__ < 31 || __GLIBC_MINOR__ >= 41".to_owned())
);
assert_eq!(
all.condition(&present(&[31, 34, 44])),
Some(
"(__GLIBC_MINOR__ >= 31 && __GLIBC_MINOR__ < 35) || __GLIBC_MINOR__ >= 44"
.to_owned()
)
);
}
#[test]
fn a_release_between_two_surveyed_ones_belongs_to_the_older() {
let all = releases();
let condition = all.condition(&present(&[28, 31])).expect("not everything");
for minor in [0, 17, 28, 30, 31, 33] {
assert!(holds(&condition, minor).expect("ours"), "2.{minor}");
}
for minor in [34, 35, 44, 99] {
assert!(!holds(&condition, minor).expect("ours"), "2.{minor}");
}
}
#[test]
fn a_run_of_two_releases_and_nothing_else_is_rejected_as_a_set_of_one() {
assert!(Releases::new(vec![28]).is_err());
assert!(Releases::new(vec![31, 28]).is_err());
assert!(Releases::new(vec![28, 28]).is_err());
assert!(Releases::new(vec![28, 31]).is_ok());
}
#[test]
fn a_conditional_is_read_back_the_way_it_was_written() {
let text = format!(
"common\n{}new\n{}old\n{}tail\n",
directive("if", Some("__GLIBC_MINOR__ >= 34")),
directive("else", None),
directive("endif", None),
);
assert_eq!(evaluate(&text, 34).expect("ours"), "common\nnew\ntail\n");
assert_eq!(evaluate(&text, 31).expect("ours"), "common\nold\ntail\n");
}
#[test]
fn an_elif_chain_takes_the_first_branch_that_holds_and_no_other() {
let text = format!(
"{}a\n{}b\n{}c\n{}",
directive("if", Some("__GLIBC_MINOR__ >= 41")),
directive("elif", Some("__GLIBC_MINOR__ >= 34")),
directive("else", None),
directive("endif", None),
);
assert_eq!(evaluate(&text, 44).expect("ours"), "a\n");
assert_eq!(evaluate(&text, 36).expect("ours"), "b\n");
assert_eq!(evaluate(&text, 28).expect("ours"), "c\n");
}
#[test]
fn the_files_own_conditionals_are_left_alone() {
let text = format!(
"#ifdef __USE_GNU\n{}int f (void);\n{}#endif\n",
directive("if", Some("__GLIBC_MINOR__ >= 34")),
directive("endif", None),
);
assert_eq!(evaluate(&text, 44).expect("ours"), "#ifdef __USE_GNU\nint f (void);\n#endif\n");
assert_eq!(evaluate(&text, 28).expect("ours"), "#ifdef __USE_GNU\n#endif\n");
}
#[test]
fn a_branch_inside_a_branch_that_is_not_taken_stays_shut() {
let text = format!(
"{}outer\n{}inner\n{}{}",
directive("if", Some("__GLIBC_MINOR__ >= 41")),
directive("if", Some("__GLIBC_MINOR__ >= 44")),
directive("endif", None),
directive("endif", None),
);
assert_eq!(evaluate(&text, 44).expect("ours"), "outer\ninner\n");
assert_eq!(evaluate(&text, 41).expect("ours"), "outer\n");
assert_eq!(evaluate(&text, 28).expect("ours"), "");
}
#[test]
fn an_unfinished_conditional_of_ours_is_an_error_and_not_a_guess() {
let text = directive("if", Some("__GLIBC_MINOR__ >= 34"));
assert!(evaluate(&text, 34).is_err());
assert!(evaluate(&directive("endif", None), 34).is_err());
assert!(evaluate(&directive("else", None), 34).is_err());
}
#[test]
fn a_condition_we_did_not_write_is_an_error() {
let text = format!("#if defined __USE_GNU {MARK}\n{}", directive("endif", None));
let why = evaluate(&text, 34).expect_err("not our grammar");
assert!(why.contains("not a condition of ours"), "{why}");
}
#[test]
fn the_marker_is_what_a_tree_is_refused_for_carrying() {
assert!(carries_mark(&directive("endif", None)));
assert!(!carries_mark("#endif /* features.h */\n"));
}
}