Skip to main content

rucc_headers/
cond.rs

1//! The conditionals the merge writes, and reading them back.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.3. The macro is `__GLIBC_MINOR__` because
4//! that is glibc's own spelling and the one `__GLIBC_PREREQ` reads, so every autoconf probe ever
5//! written agrees with it. The tree does not define it; the compiler does, from the tuple's
6//! `env_version`, which is `rucc_sysroot::bundled_glibc_minor`.
7//!
8//! # Why the oldest branch has no lower bound
9//!
10//! Because a release older than the oldest one surveyed is served by defining the macro lower, and
11//! a floor of `__GLIBC_MINOR__ >= 28` on the oldest branch would serve it nothing at all. A tree
12//! that hands 2.17 the declarations of 2.28 is too permissive, which is the direction section 8.3
13//! accepts and document 09 section 9.5 describes; a tree that hands it an empty file is broken. So
14//! the branches cover every value of the macro, the oldest surveyed release's text is what anything
15//! below it gets, and the one place that says a version was never surveyed is the guard that
16//! replaces glibc's own definition of the macro.
17//!
18//! # Why every directive carries a marker
19//!
20//! Because the merge has to be able to read its own output back, to check that what it wrote
21//! reproduces each release, and a header full of real `#if` and `#endif` lines gives it no way to
22//! tell which ones are its own. Counting nesting is not enough: the files being merged are the ones
23//! with eight levels of conditionals in them. So every directive this module writes ends in
24//! [`MARK`], nothing else in the tree is allowed to contain that text, and reading the output back
25//! is then a matter of looking at the end of the line.
26
27/// What every directive the merge writes ends with.
28///
29/// A comment, so it changes nothing about what the preprocessor does, and glibc's own headers end
30/// their `#endif` lines with comments too, so it reads like the text around it.
31pub const MARK: &str = "/* rucc */";
32
33/// The macro the conditionals are written against.
34pub const MACRO: &str = "__GLIBC_MINOR__";
35
36/// The releases a tree is merged from, in ascending order.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Releases {
39    minors: Vec<u32>,
40}
41
42impl Releases {
43    /// The releases, which have to be ascending, distinct and at least two.
44    ///
45    /// At least two because merging one release is copying it, and a tool that accepts that
46    /// silently is a tool somebody will run by mistake and ship the result of.
47    pub fn new(minors: Vec<u32>) -> Result<Self, String> {
48        if minors.len() < 2 {
49            return Err("merging wants at least two releases, and one is a copy".to_owned());
50        }
51        for pair in minors.windows(2) {
52            if pair[0] >= pair[1] {
53                return Err(format!(
54                    "the releases have to be ascending and distinct, and 2.{} comes after 2.{}",
55                    pair[0], pair[1]
56                ));
57            }
58        }
59        Ok(Self { minors })
60    }
61
62    /// How many releases there are. Never none, and never one.
63    pub fn count(&self) -> usize {
64        self.minors.len()
65    }
66
67    /// The releases themselves.
68    pub fn minors(&self) -> &[u32] {
69        &self.minors
70    }
71
72    /// How the release at `at` is spelled in a message.
73    pub fn spelled(&self, at: usize) -> String {
74        format!("2.{}", self.minors[at])
75    }
76
77    /// The condition that holds for exactly the releases marked in `present`, or `None` when that
78    /// is all of them and no conditional is wanted.
79    ///
80    /// A release between two surveyed ones belongs to the older of the two, because that is what
81    /// the bounds say and because the alternative is claiming to know what a release we did not
82    /// look at contains.
83    ///
84    /// # Panics
85    ///
86    /// When `present` is not one flag per release, which is a mistake in the caller.
87    pub fn condition(&self, present: &[bool]) -> Option<String> {
88        assert_eq!(present.len(), self.minors.len(), "a presence set has one flag per release");
89        if present.iter().all(|&p| p) {
90            return None;
91        }
92        // One run of consecutive releases at a time, each becoming a term of its own.
93        let mut runs: Vec<Vec<String>> = Vec::new();
94        let mut at = 0;
95        while at < present.len() {
96            if !present[at] {
97                at += 1;
98                continue;
99            }
100            let start = at;
101            while at + 1 < present.len() && present[at + 1] {
102                at += 1;
103            }
104            let mut atoms: Vec<String> = Vec::new();
105            // No lower bound on a run that starts at the oldest release, for the reason in the
106            // module documentation.
107            if start > 0 {
108                atoms.push(format!("{MACRO} >= {}", self.minors[start]));
109            }
110            if at + 1 < present.len() {
111                atoms.push(format!("{MACRO} < {}", self.minors[at + 1]));
112            }
113            runs.push(atoms);
114            at += 1;
115        }
116        if runs.is_empty() {
117            // Nothing is present, which the merge never asks for, and `0` is the honest answer.
118            return Some("0".to_owned());
119        }
120        // Parentheses only where there is something to group, because a condition a reviewer has
121        // to count brackets in is a condition a reviewer skips.
122        let one = runs.len() == 1;
123        let terms: Vec<String> = runs
124            .into_iter()
125            .map(|atoms| match atoms.len() {
126                0 => "1".to_owned(),
127                1 => atoms.into_iter().next().unwrap_or_default(),
128                _ if one => atoms.join(" && "),
129                _ => format!("({})", atoms.join(" && ")),
130            })
131            .collect();
132        Some(terms.join(" || "))
133    }
134}
135
136/// One directive line, marked as ours, with its newline on it.
137pub fn directive(keyword: &str, condition: Option<&str>) -> String {
138    match condition {
139        Some(text) => format!("#{keyword} {text} {MARK}\n"),
140        None => format!("#{keyword} {MARK}\n"),
141    }
142}
143
144/// Whether a release's own text would be mistaken for the merge's own directives.
145pub fn carries_mark(text: &str) -> bool {
146    text.contains(MARK)
147}
148
149/// What the preprocessor would leave of a merged file, for one value of the macro.
150///
151/// This is how the merge checks itself: the text it is about to write, read back the way a compiler
152/// would read it, has to be the release it came from. Only the merge's own directives are
153/// evaluated. Everything else is text, including the header's own conditionals, because what this
154/// has to reproduce is the file as a release shipped it and not what a compilation of it means.
155pub fn evaluate(text: &str, minor: u32) -> Result<String, String> {
156    struct Frame {
157        outer: bool,
158        taken: bool,
159        active: bool,
160    }
161    let mut stack: Vec<Frame> = Vec::new();
162    let mut out = String::with_capacity(text.len());
163    for (n, line) in text.split_inclusive('\n').enumerate() {
164        let at = n + 1;
165        let active = stack.last().is_none_or(|f| f.active);
166        let Some(body) = ours(line) else {
167            if active {
168                out.push_str(line);
169            }
170            continue;
171        };
172        if let Some(condition) = body.strip_prefix("#if ") {
173            let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
174            stack.push(Frame { outer: active, taken: holds, active: active && holds });
175        } else if let Some(condition) = body.strip_prefix("#elif ") {
176            let holds = holds(condition.trim(), minor).map_err(|why| format!("{at}: {why}"))?;
177            let frame = stack.last_mut().ok_or(format!("{at}: #elif with no #if"))?;
178            frame.active = frame.outer && !frame.taken && holds;
179            frame.taken = frame.taken || holds;
180        } else if body == "#else" {
181            let frame = stack.last_mut().ok_or(format!("{at}: #else with no #if"))?;
182            frame.active = frame.outer && !frame.taken;
183            frame.taken = true;
184        } else if body == "#endif" {
185            stack.pop().ok_or(format!("{at}: #endif with no #if"))?;
186        } else {
187            return Err(format!("{at}: {body} is marked as ours and is not a directive"));
188        }
189    }
190    if stack.is_empty() {
191        Ok(out)
192    } else {
193        Err(format!("{} of our conditionals are still open at the end", stack.len()))
194    }
195}
196
197/// The directive on this line, if the line is one of ours.
198fn ours(line: &str) -> Option<&str> {
199    let body = line.trim_end();
200    let body = body.strip_suffix(MARK)?.trim_end();
201    body.starts_with('#').then_some(body)
202}
203
204/// Whether one of our conditions holds, which is the only grammar this has to read.
205fn holds(condition: &str, minor: u32) -> Result<bool, String> {
206    let mut any = false;
207    for term in condition.split("||") {
208        let term = term.trim();
209        let term = match term.strip_prefix('(') {
210            Some(rest) => {
211                rest.strip_suffix(')').ok_or(format!("unbalanced parentheses: {term}"))?
212            }
213            None => term,
214        };
215        let mut all = true;
216        for atom in term.split("&&") {
217            all &= atom_holds(atom.trim(), minor)?;
218        }
219        any |= all;
220    }
221    Ok(any)
222}
223
224/// Whether one comparison holds. `1` and `0` are the two conditions that name no version.
225fn atom_holds(atom: &str, minor: u32) -> Result<bool, String> {
226    match atom {
227        "1" => return Ok(true),
228        "0" => return Ok(false),
229        _ => {}
230    }
231    let rest = atom.strip_prefix(MACRO).ok_or(format!("not a condition of ours: {atom}"))?;
232    let rest = rest.trim_start();
233    if let Some(value) = rest.strip_prefix(">=") {
234        Ok(minor >= number(value)?)
235    } else if let Some(value) = rest.strip_prefix('<') {
236        Ok(minor < number(value)?)
237    } else {
238        Err(format!("not a comparison of ours: {atom}"))
239    }
240}
241
242/// The version on the right of a comparison.
243fn number(text: &str) -> Result<u32, String> {
244    text.trim().parse().map_err(|_| format!("not a version: {text}"))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    const EIGHT: [u32; 8] = [28, 31, 34, 35, 36, 39, 41, 44];
252
253    fn releases() -> Releases {
254        Releases::new(EIGHT.to_vec()).expect("ascending and distinct")
255    }
256
257    fn present(of: &[u32]) -> Vec<bool> {
258        EIGHT.iter().map(|m| of.contains(m)).collect()
259    }
260
261    /// What a condition means is which releases it holds for, so that is what the tests check,
262    /// rather than the text, with one exception below that is about the text.
263    fn holding(condition: &Option<String>) -> Vec<u32> {
264        EIGHT
265            .iter()
266            .copied()
267            .filter(|&m| match condition {
268                None => true,
269                Some(text) => holds(text, m).expect("our own grammar"),
270            })
271            .collect()
272    }
273
274    #[test]
275    fn every_subset_of_eight_releases_gets_a_condition_that_means_it() {
276        let all = releases();
277        for bits in 0u32..256 {
278            let chosen: Vec<u32> = EIGHT
279                .iter()
280                .enumerate()
281                .filter(|(n, _)| bits & (1 << n) != 0)
282                .map(|(_, &m)| m)
283                .collect();
284            let condition = all.condition(&present(&chosen));
285            assert_eq!(holding(&condition), chosen, "{condition:?}");
286        }
287    }
288
289    #[test]
290    fn the_whole_set_wants_no_conditional() {
291        assert_eq!(releases().condition(&[true; 8]), None);
292    }
293
294    #[test]
295    fn the_shapes_a_reviewer_reads() {
296        let all = releases();
297        assert_eq!(all.condition(&present(&[44])), Some("__GLIBC_MINOR__ >= 44".to_owned()));
298        assert_eq!(
299            all.condition(&present(&[39, 41, 44])),
300            Some("__GLIBC_MINOR__ >= 39".to_owned())
301        );
302        assert_eq!(all.condition(&present(&[28])), Some("__GLIBC_MINOR__ < 31".to_owned()));
303        assert_eq!(all.condition(&present(&[28, 31])), Some("__GLIBC_MINOR__ < 34".to_owned()));
304        assert_eq!(
305            all.condition(&present(&[34, 35])),
306            Some("__GLIBC_MINOR__ >= 34 && __GLIBC_MINOR__ < 36".to_owned())
307        );
308        assert_eq!(
309            all.condition(&present(&[28, 41, 44])),
310            Some("__GLIBC_MINOR__ < 31 || __GLIBC_MINOR__ >= 41".to_owned())
311        );
312        assert_eq!(
313            all.condition(&present(&[31, 34, 44])),
314            Some(
315                "(__GLIBC_MINOR__ >= 31 && __GLIBC_MINOR__ < 35) || __GLIBC_MINOR__ >= 44"
316                    .to_owned()
317            )
318        );
319    }
320
321    /// A release nobody surveyed belongs to the newest surveyed release that is not newer than it,
322    /// and anything older than the oldest one surveyed gets that one.
323    #[test]
324    fn a_release_between_two_surveyed_ones_belongs_to_the_older() {
325        let all = releases();
326        let condition = all.condition(&present(&[28, 31])).expect("not everything");
327        for minor in [0, 17, 28, 30, 31, 33] {
328            assert!(holds(&condition, minor).expect("ours"), "2.{minor}");
329        }
330        for minor in [34, 35, 44, 99] {
331            assert!(!holds(&condition, minor).expect("ours"), "2.{minor}");
332        }
333    }
334
335    #[test]
336    fn a_run_of_two_releases_and_nothing_else_is_rejected_as_a_set_of_one() {
337        assert!(Releases::new(vec![28]).is_err());
338        assert!(Releases::new(vec![31, 28]).is_err());
339        assert!(Releases::new(vec![28, 28]).is_err());
340        assert!(Releases::new(vec![28, 31]).is_ok());
341    }
342
343    #[test]
344    fn a_conditional_is_read_back_the_way_it_was_written() {
345        let text = format!(
346            "common\n{}new\n{}old\n{}tail\n",
347            directive("if", Some("__GLIBC_MINOR__ >= 34")),
348            directive("else", None),
349            directive("endif", None),
350        );
351        assert_eq!(evaluate(&text, 34).expect("ours"), "common\nnew\ntail\n");
352        assert_eq!(evaluate(&text, 31).expect("ours"), "common\nold\ntail\n");
353    }
354
355    #[test]
356    fn an_elif_chain_takes_the_first_branch_that_holds_and_no_other() {
357        let text = format!(
358            "{}a\n{}b\n{}c\n{}",
359            directive("if", Some("__GLIBC_MINOR__ >= 41")),
360            directive("elif", Some("__GLIBC_MINOR__ >= 34")),
361            directive("else", None),
362            directive("endif", None),
363        );
364        assert_eq!(evaluate(&text, 44).expect("ours"), "a\n");
365        assert_eq!(evaluate(&text, 36).expect("ours"), "b\n");
366        assert_eq!(evaluate(&text, 28).expect("ours"), "c\n");
367    }
368
369    /// The header's own conditionals are text, and the marker is what keeps them apart from ours.
370    #[test]
371    fn the_files_own_conditionals_are_left_alone() {
372        let text = format!(
373            "#ifdef __USE_GNU\n{}int f (void);\n{}#endif\n",
374            directive("if", Some("__GLIBC_MINOR__ >= 34")),
375            directive("endif", None),
376        );
377        assert_eq!(evaluate(&text, 44).expect("ours"), "#ifdef __USE_GNU\nint f (void);\n#endif\n");
378        assert_eq!(evaluate(&text, 28).expect("ours"), "#ifdef __USE_GNU\n#endif\n");
379    }
380
381    #[test]
382    fn a_branch_inside_a_branch_that_is_not_taken_stays_shut() {
383        let text = format!(
384            "{}outer\n{}inner\n{}{}",
385            directive("if", Some("__GLIBC_MINOR__ >= 41")),
386            directive("if", Some("__GLIBC_MINOR__ >= 44")),
387            directive("endif", None),
388            directive("endif", None),
389        );
390        assert_eq!(evaluate(&text, 44).expect("ours"), "outer\ninner\n");
391        assert_eq!(evaluate(&text, 41).expect("ours"), "outer\n");
392        assert_eq!(evaluate(&text, 28).expect("ours"), "");
393    }
394
395    #[test]
396    fn an_unfinished_conditional_of_ours_is_an_error_and_not_a_guess() {
397        let text = directive("if", Some("__GLIBC_MINOR__ >= 34"));
398        assert!(evaluate(&text, 34).is_err());
399        assert!(evaluate(&directive("endif", None), 34).is_err());
400        assert!(evaluate(&directive("else", None), 34).is_err());
401    }
402
403    #[test]
404    fn a_condition_we_did_not_write_is_an_error() {
405        let text = format!("#if defined __USE_GNU {MARK}\n{}", directive("endif", None));
406        let why = evaluate(&text, 34).expect_err("not our grammar");
407        assert!(why.contains("not a condition of ours"), "{why}");
408    }
409
410    #[test]
411    fn the_marker_is_what_a_tree_is_refused_for_carrying() {
412        assert!(carries_mark(&directive("endif", None)));
413        assert!(!carries_mark("#endif /* features.h */\n"));
414    }
415}