Skip to main content

rucc_opt/
gate.rs

1//! Which functions a pass is allowed to run on.
2//!
3//! Section 41.6 of `spec/optimizer/41-correctness.md` asks for `-fdisable-<pass>[=<range>]` and
4//! `-fenable-<pass>[=<range>]` from the point at which there is more than one pass, and gives the
5//! reason. A wrong-code bug is two questions, which pass did it and which function did it happen
6//! in, and with these two flags they are two independent bisections a script can run without a
7//! debugger and without reading a diff of two assembly listings. `-fpass-fuel` narrows the first
8//! answer further, to one rewrite inside the guilty pass, so the three of them together take a
9//! report of the form "this program is wrong at `-O2`" down to a line of the optimizer.
10//!
11//! A rule applies only to the functions it names, and the last rule that names a function is the
12//! one that decides for it. Everything the rules do not name keeps the answer the optimization
13//! level already gave, which is what makes `-fenable-<pass>=3` mean "also run it there" rather
14//! than "run it only there". GCC's `override_gate_status` works the same way and the flags are
15//! useless if they do not, because a bisection that changes two things at once has bisected
16//! nothing.
17
18use crate::pass;
19
20/// The rules `-fdisable-<pass>` and `-fenable-<pass>` left behind, in the order they were given.
21///
22/// Empty by default, and an empty set of gates answers yes to everything, so the cost of the
23/// feature on a compilation nobody is debugging is one test of a `Vec` for emptiness per pass.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct Gates {
26    rules: Vec<Rule>,
27}
28
29/// One `-fdisable-` or `-fenable-`, remembered as what it said rather than as its effect, because
30/// the effect depends on the function being asked about.
31#[derive(Debug, Clone, PartialEq, Eq)]
32struct Rule {
33    /// The pass it names, which is checked against the pass list when the rule is added.
34    pass: String,
35    /// Whether it turns the pass on or off for what it covers.
36    on: bool,
37    /// Which functions it covers.
38    scope: Scope,
39}
40
41/// What a rule was written against.
42#[derive(Debug, Clone, PartialEq, Eq)]
43enum Scope {
44    /// The flag had no `=`, so it covers every function in the module.
45    Everything,
46    /// The flag had a range list, so it covers what the list picks out.
47    These(Vec<Pick>),
48}
49
50/// One item of a range list.
51#[derive(Debug, Clone, PartialEq, Eq)]
52enum Pick {
53    /// One function, by the position it has in the module, counting from zero.
54    Id(u32),
55    /// Every function whose position is between these two, both ends included.
56    Span(u32, u32),
57    /// One function, by the name it has in the source.
58    Name(String),
59}
60
61impl Pick {
62    /// Whether this item picks out that function.
63    fn covers(&self, id: u32, name: &str) -> bool {
64        match self {
65            Pick::Id(want) => *want == id,
66            Pick::Span(low, high) => (*low..=*high).contains(&id),
67            Pick::Name(want) => want == name,
68        }
69    }
70}
71
72impl Scope {
73    /// Whether this scope covers that function.
74    fn covers(&self, id: u32, name: &str) -> bool {
75        match self {
76            Scope::Everything => true,
77            Scope::These(picks) => picks.iter().any(|pick| pick.covers(id, name)),
78        }
79    }
80}
81
82impl Gates {
83    /// Adds one `-fdisable-<pass>[=<range>]` or `-fenable-<pass>[=<range>]`, with `on` saying
84    /// which of the two it was and `spec` being everything after the second hyphen.
85    ///
86    /// # Errors
87    ///
88    /// When the pass is not one this compiler has, when the range list is empty, when an item of
89    /// it is empty, or when a span runs backwards. A misspelled pass name is the error worth
90    /// catching here: it would otherwise look exactly like a pass that is not guilty, and the
91    /// bisection would carry on past the one thing it was looking for.
92    pub fn add(&mut self, on: bool, spec: &str) -> Result<(), String> {
93        let (name, list) = match spec.split_once('=') {
94            Some((name, list)) => (name, Some(list)),
95            None => (spec, None),
96        };
97        if pass::find(name).is_none() {
98            return Err(format!("`{name}` is not a pass this compiler has, see --print-pipeline"));
99        }
100        let scope = match list {
101            None => Scope::Everything,
102            Some(list) => Scope::These(picks(list)?),
103        };
104        self.rules.push(Rule { pass: name.to_owned(), on, scope });
105        Ok(())
106    }
107
108    /// Whether anything was asked for at all.
109    #[must_use]
110    pub fn is_empty(&self) -> bool {
111        self.rules.is_empty()
112    }
113
114    /// Whether that pass runs over that function, where `default` is what the optimization level
115    /// already decided about the pass.
116    ///
117    /// The function is identified both ways at once because both spellings are useful and neither
118    /// is available in both places. A script bisecting a file it has never read counts functions
119    /// and gives numbers. A person who has just read `-fopt-info` gives the name it printed.
120    #[must_use]
121    pub fn allows(&self, pass: &str, default: bool, id: u32, name: &str) -> bool {
122        let mut answer = default;
123        for rule in &self.rules {
124            if rule.pass == pass && rule.scope.covers(id, name) {
125                answer = rule.on;
126            }
127        }
128        answer
129    }
130
131    /// Every pass some rule turns on, in the order the rules were given, without repeats.
132    ///
133    /// A pass named by `-fenable-` that the level did not choose has to join the pipeline, or the
134    /// flag would be a way of asking for something and being given nothing. That is also how the
135    /// flag reaches a pass at `-O0`, which is where a bisection would rather start.
136    #[must_use]
137    pub fn enabled(&self) -> Vec<&str> {
138        let mut out: Vec<&str> = Vec::new();
139        for rule in self.rules.iter().filter(|rule| rule.on) {
140            let name = rule.pass.as_str();
141            if !out.contains(&name) {
142                out.push(name);
143            }
144        }
145        out
146    }
147
148    /// What `--print-pipeline` says after a pass a rule mentions, or nothing when no rule does.
149    ///
150    /// The listing is the answer to why a program came out the way it did, and a pass that is in
151    /// the list and did not run on the function being asked about is exactly the kind of thing
152    /// that answer has to include.
153    #[must_use]
154    pub fn note(&self, pass: &str) -> Option<String> {
155        let mut parts: Vec<String> = Vec::new();
156        for rule in self.rules.iter().filter(|rule| rule.pass == pass) {
157            let word = if rule.on { "on" } else { "off" };
158            parts.push(match &rule.scope {
159                Scope::Everything => word.to_owned(),
160                Scope::These(picks) => format!("{word} for {}", render(picks)),
161            });
162        }
163        match parts.is_empty() {
164            true => None,
165            false => Some(parts.join(", ")),
166        }
167    }
168}
169
170/// The items of a range list, in the order they were written.
171fn picks(list: &str) -> Result<Vec<Pick>, String> {
172    if list.is_empty() {
173        return Err("the list of functions after the `=` is empty".to_owned());
174    }
175    let mut out = Vec::new();
176    for item in list.split(',') {
177        out.push(pick(item)?);
178    }
179    Ok(out)
180}
181
182/// One item of a range list.
183///
184/// An item that starts with a digit is a number or a span of them, and anything else is a name,
185/// which is unambiguous because no identifier in C starts with a digit.
186fn pick(item: &str) -> Result<Pick, String> {
187    if item.is_empty() {
188        return Err("there is an empty item in the list of functions".to_owned());
189    }
190    if !item.starts_with(|c: char| c.is_ascii_digit()) {
191        return Ok(Pick::Name(item.to_owned()));
192    }
193    let Some((low, high)) = item.split_once('-') else {
194        return Ok(Pick::Id(number(item)?));
195    };
196    let (low, high) = (number(low)?, number(high)?);
197    if low > high {
198        return Err(format!("the range `{item}` ends before it starts"));
199    }
200    Ok(Pick::Span(low, high))
201}
202
203/// One function number.
204fn number(text: &str) -> Result<u32, String> {
205    text.parse().map_err(|_| format!("`{text}` is not the number of a function"))
206}
207
208/// A range list written back out, for the pipeline listing.
209fn render(picks: &[Pick]) -> String {
210    let parts: Vec<String> = picks
211        .iter()
212        .map(|pick| match pick {
213            Pick::Id(id) => id.to_string(),
214            Pick::Span(low, high) => format!("{low}-{high}"),
215            Pick::Name(name) => name.clone(),
216        })
217        .collect();
218    parts.join(",")
219}
220
221#[cfg(test)]
222mod tests {
223    use super::Gates;
224
225    /// Gates that say what these flags said, or the reason they could not be added.
226    fn gates(flags: &[(bool, &str)]) -> Gates {
227        let mut gates = Gates::default();
228        for (on, spec) in flags {
229            gates.add(*on, spec).expect("the test asked for a gate this compiler refuses");
230        }
231        gates
232    }
233
234    #[test]
235    fn nothing_asked_for_means_the_level_decides() {
236        let gates = Gates::default();
237        assert!(gates.is_empty());
238        assert!(gates.allows("fold", true, 0, "main"));
239        assert!(!gates.allows("fold", false, 0, "main"));
240        assert_eq!(gates.note("fold"), None);
241    }
242
243    #[test]
244    fn disabling_a_pass_with_no_range_takes_it_away_from_every_function() {
245        let gates = gates(&[(false, "fold")]);
246        assert!(!gates.allows("fold", true, 0, "main"));
247        assert!(!gates.allows("fold", true, 7, "other"));
248        assert!(gates.allows("dce", true, 0, "main"), "one pass named is not every pass named");
249    }
250
251    #[test]
252    fn a_range_leaves_every_function_it_does_not_name_alone() {
253        let gates = gates(&[(false, "fold=1-3")]);
254        assert!(gates.allows("fold", true, 0, "a"));
255        assert!(!gates.allows("fold", true, 1, "b"));
256        assert!(!gates.allows("fold", true, 3, "d"));
257        assert!(gates.allows("fold", true, 4, "e"));
258    }
259
260    #[test]
261    fn a_function_can_be_named_as_well_as_numbered() {
262        let gates = gates(&[(false, "dce=parse_line,9")]);
263        assert!(!gates.allows("dce", true, 0, "parse_line"));
264        assert!(!gates.allows("dce", true, 9, "whatever"));
265        assert!(gates.allows("dce", true, 0, "main"));
266    }
267
268    #[test]
269    fn the_last_rule_that_covers_a_function_is_the_one_that_decides() {
270        let gates = gates(&[(false, "fold"), (true, "fold=2")]);
271        assert!(!gates.allows("fold", true, 1, "a"));
272        assert!(gates.allows("fold", true, 2, "b"), "the second rule covers this one");
273    }
274
275    #[test]
276    fn enabling_a_pass_reaches_one_the_level_did_not_choose() {
277        let gates = gates(&[(true, "narrow=2")]);
278        assert!(!gates.allows("narrow", false, 1, "a"));
279        assert!(gates.allows("narrow", false, 2, "b"));
280        assert_eq!(gates.enabled(), ["narrow"]);
281    }
282
283    #[test]
284    fn a_pass_enabled_twice_is_named_once() {
285        let gates = gates(&[(true, "narrow=2"), (false, "fold"), (true, "narrow=5")]);
286        assert_eq!(gates.enabled(), ["narrow"]);
287    }
288
289    #[test]
290    fn the_listing_says_what_was_asked_for() {
291        let gates = gates(&[(false, "fold"), (true, "fold=2-4,main")]);
292        assert_eq!(gates.note("fold").as_deref(), Some("off, on for 2-4,main"));
293        assert_eq!(gates.note("dce"), None);
294    }
295
296    #[test]
297    fn a_pass_this_compiler_does_not_have_is_refused_rather_than_ignored() {
298        let mut gates = Gates::default();
299        let why = gates.add(false, "nosuch").expect_err("a pass that does not exist was accepted");
300        assert!(why.contains("not a pass"), "{why}");
301        assert!(gates.is_empty());
302    }
303
304    #[test]
305    fn a_range_list_that_says_nothing_is_refused() {
306        let mut gates = Gates::default();
307        assert!(gates.add(false, "fold=").is_err());
308        assert!(gates.add(false, "fold=1,,3").is_err());
309    }
310
311    #[test]
312    fn a_range_that_runs_backwards_is_refused() {
313        let mut gates = Gates::default();
314        let why = gates.add(false, "fold=9-2").expect_err("a backwards range was accepted");
315        assert!(why.contains("ends before it starts"), "{why}");
316    }
317
318    #[test]
319    fn a_number_that_is_not_one_is_refused() {
320        let mut gates = Gates::default();
321        assert!(gates.add(false, "fold=1x").is_err());
322        assert!(gates.add(false, "fold=1-x").is_err());
323    }
324}