Skip to main content

rucc_opt/
optinfo.rs

1//! `-fopt-info`, which is the compiler saying what it did and what it nearly did.
2//!
3//! GCC's version is documented at `gcc/doc/invoke.texi:20403` and takes the keywords `optimized`,
4//! `missed`, `note` and `all`. Section 42.2 of `spec/optimizer/42-measurement.md` picks out
5//! `missed` as the one that earns the feature: it turns "this loop was not vectorized" from a
6//! mystery into a sentence, and a compiler that reports only its successes cannot be tuned by
7//! anybody outside it.
8//!
9//! What is printed here comes from the records the passes returned, so this module invents
10//! nothing and cannot drift from what the passes actually did. Everything a pass wants said has
11//! to be in its [`crate::Stats`], which is the same requirement that makes the pass manager
12//! believe it changed something.
13//!
14//! # The format, and the position that is not in it
15//!
16//! One line per pass per function per event:
17//!
18//! ```text
19//! a.c: f: optimized: integer instruction folded to a constant (3) [fold]
20//! ```
21//!
22//! The file, the function, the kind, the event, how many times, and the pass whose `-fno-` flag
23//! turns it off. GCC puts a line and a column after the file. This does not, because the IR does
24//! not carry source positions yet, and a zero there would be a position rather than an admission
25//! that there is not one. When the IR carries them this line grows a `:line:col` and the shape of
26//! everything else stays as it is.
27
28use std::fmt::Write as _;
29
30use rucc_base::Interner;
31
32use crate::pipeline::Report;
33use crate::stats::Kind;
34
35/// Which kinds of remark were asked for.
36///
37/// Empty is a real answer and means the flag was never given, which is why [`Wants::is_empty`]
38/// exists and why the driver checks it rather than carrying an `Option` around.
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub struct Wants {
41    /// Whether rewrites are printed.
42    optimized: bool,
43    /// Whether the sites a pass gave up on are printed.
44    missed: bool,
45    /// Whether everything else is printed.
46    note: bool,
47}
48
49impl Wants {
50    /// Nothing, which is what a compilation with no `-fopt-info` on it asks for.
51    #[must_use]
52    pub const fn none() -> Self {
53        Self { optimized: false, missed: false, note: false }
54    }
55
56    /// Every kind, which is what `-fopt-info-all` asks for.
57    #[must_use]
58    pub const fn all() -> Self {
59        Self { optimized: true, missed: true, note: true }
60    }
61
62    /// Adds what one `-fopt-info` argument asked for.
63    ///
64    /// The keywords are joined by hyphens, the way GCC joins them, so
65    /// `-fopt-info-missed-optimized` is two of them. A bare `-fopt-info` is spelled here as an
66    /// empty argument and means `optimized`, which is the default GCC documents.
67    ///
68    /// Two of these flags add up rather than the second replacing the first, because a person who
69    /// writes both wanted both.
70    ///
71    /// # Errors
72    ///
73    /// When a keyword is not one this compiler has. A misspelling that quietly printed nothing
74    /// would look exactly like a compilation where no pass had anything to say, and telling those
75    /// two apart is the whole reason somebody reached for this flag.
76    pub fn add(&mut self, spec: &str) -> Result<(), String> {
77        if spec.is_empty() {
78            self.optimized = true;
79            return Ok(());
80        }
81        for word in spec.split('-') {
82            match word {
83                "all" => *self = Self::all(),
84                "optimized" => self.optimized = true,
85                "missed" => self.missed = true,
86                "note" => self.note = true,
87                _ => {
88                    return Err(format!(
89                        "`{word}` is not a kind of remark this compiler makes, which are \
90                         `optimized`, `missed`, `note` and `all`"
91                    ));
92                }
93            }
94        }
95        Ok(())
96    }
97
98    /// Whether this kind is printed.
99    #[must_use]
100    pub const fn wants(self, kind: Kind) -> bool {
101        match kind {
102            Kind::Optimized => self.optimized,
103            Kind::Missed => self.missed,
104            Kind::Note => self.note,
105        }
106    }
107
108    /// Whether nothing at all was asked for.
109    #[must_use]
110    pub const fn is_empty(self) -> bool {
111        !self.optimized && !self.missed && !self.note
112    }
113}
114
115/// Renders the remarks a run produced, as the lines `-fopt-info` prints.
116///
117/// In the order the passes ran, then the order the module holds its functions, then the order
118/// each pass recorded its events. That is the order the work happened in, which is the order
119/// somebody reading down the output is reconstructing.
120///
121/// A function a pass had nothing to say about produces no lines. The record for it still exists,
122/// and `--print-pass-stats` is where a pass that fires on nothing becomes visible. Printing a
123/// line per silent pass per function here would bury the ones that spoke.
124#[must_use]
125pub fn render(file: &str, report: &Report, names: &Interner, wants: Wants) -> String {
126    let mut out = String::new();
127    if wants.is_empty() {
128        return out;
129    }
130    for remark in &report.remarks {
131        let func = names.resolve(remark.func);
132        for event in remark.stats.events() {
133            if !wants.wants(event.kind) {
134                continue;
135            }
136            let _ = writeln!(
137                out,
138                "{file}: {func}: {}: {} ({}) [{}]",
139                event.kind, event.what, event.count, remark.pass
140            );
141        }
142    }
143    out
144}
145
146/// Renders what `--print-pass-stats` prints: every pass that ran, and its totals.
147///
148/// Every pass, including the ones that said nothing, and that is the difference between this and
149/// [`render`]. A pass that fires zero times over a whole corpus is either dead code or a bug, and
150/// there is no way to find out which from output that omits it.
151#[must_use]
152pub fn totals(report: &Report) -> String {
153    let mut out = String::new();
154    let mut seen: Vec<&'static str> = Vec::new();
155    for remark in &report.remarks {
156        if !seen.contains(&remark.pass) {
157            seen.push(remark.pass);
158        }
159    }
160    for pass in seen {
161        let stats = report.totals(pass);
162        if stats.is_empty() {
163            let _ = writeln!(out, "{pass}: nothing");
164            continue;
165        }
166        for event in stats.events() {
167            let _ = writeln!(out, "{pass}: {}: {} ({})", event.kind, event.what, event.count);
168        }
169    }
170    out
171}
172
173#[cfg(test)]
174mod tests {
175    use rucc_base::Interner;
176
177    use super::{Wants, render, totals};
178    use crate::Stats;
179    use crate::pipeline::{Remark, Report};
180    use crate::stats::Kind;
181
182    /// A report with one talkative pass and one silent one.
183    fn report(names: &mut Interner) -> Report {
184        let f = names.intern("f");
185        let g = names.intern("g");
186        let mut loud = Stats::new();
187        loud.record(Kind::Optimized, "a rewrite", 3);
188        loud.missed("a site it gave up on");
189        loud.note("something an analysis found");
190        Report {
191            remarks: vec![
192                Remark { pass: "fold", func: f, stats: loud },
193                Remark { pass: "fold", func: g, stats: Stats::new() },
194                Remark { pass: "dce", func: f, stats: Stats::new() },
195                Remark { pass: "dce", func: g, stats: Stats::new() },
196            ],
197            ..Report::default()
198        }
199    }
200
201    #[test]
202    fn nothing_is_printed_when_nothing_was_asked_for() {
203        let mut names = Interner::new();
204        let report = report(&mut names);
205        assert_eq!(render("a.c", &report, &names, Wants::none()), "");
206    }
207
208    #[test]
209    fn a_bare_flag_asks_for_the_rewrites_and_nothing_else() {
210        let mut wants = Wants::none();
211        wants.add("").expect("the bare flag is always allowed");
212        assert!(wants.wants(Kind::Optimized));
213        assert!(!wants.wants(Kind::Missed));
214        assert!(!wants.wants(Kind::Note));
215    }
216
217    #[test]
218    fn keywords_are_joined_by_hyphens_and_two_flags_add_up() {
219        let mut wants = Wants::none();
220        wants.add("missed-note").expect("both of those exist");
221        assert!(!wants.wants(Kind::Optimized));
222        assert!(wants.wants(Kind::Missed));
223        assert!(wants.wants(Kind::Note));
224        wants.add("optimized").expect("that exists too");
225        assert_eq!(wants, Wants::all(), "the second flag replaced the first");
226    }
227
228    #[test]
229    fn a_keyword_that_does_not_exist_is_refused_rather_than_ignored() {
230        let mut wants = Wants::none();
231        let why = wants.add("vectorized").expect_err("no such kind");
232        assert!(why.contains("`optimized`"), "{why}");
233        let why = wants.add("missed-vectorized").expect_err("one bad word spoils the argument");
234        assert!(why.contains("vectorized"), "{why}");
235    }
236
237    #[test]
238    fn every_kind_asked_for_is_printed_with_its_count_its_function_and_its_pass() {
239        let mut names = Interner::new();
240        let report = report(&mut names);
241        let text = render("a.c", &report, &names, Wants::all());
242        assert_eq!(
243            text,
244            "a.c: f: optimized: a rewrite (3) [fold]\n\
245             a.c: f: missed: a site it gave up on (1) [fold]\n\
246             a.c: f: note: something an analysis found (1) [fold]\n"
247        );
248    }
249
250    #[test]
251    fn asking_for_the_misses_leaves_out_the_rewrites() {
252        let mut names = Interner::new();
253        let report = report(&mut names);
254        let mut wants = Wants::none();
255        wants.add("missed").expect("that exists");
256        let text = render("a.c", &report, &names, wants);
257        assert_eq!(text, "a.c: f: missed: a site it gave up on (1) [fold]\n");
258    }
259
260    #[test]
261    fn the_totals_name_every_pass_that_ran_including_the_ones_that_said_nothing() {
262        let mut names = Interner::new();
263        let report = report(&mut names);
264        let text = totals(&report);
265        // `dce` ran over both functions and had nothing to say, and that is the line worth
266        // having. A pass that fires on nothing is either dead code or a bug, and output that
267        // leaves it out cannot tell anybody which.
268        assert_eq!(
269            text,
270            "fold: optimized: a rewrite (3)\n\
271             fold: missed: a site it gave up on (1)\n\
272             fold: note: something an analysis found (1)\n\
273             dce: nothing\n"
274        );
275    }
276}