Skip to main content

rucc_session/
lib.rs

1//! The `Session`: the options, the interner and the diagnostic sink that every stage of a
2//! single compilation is handed.
3//!
4//! Design: `spec/03-architecture.md` and `spec/04-driver-and-cli.md`. Layer rank 3, see
5//! `spec/18-package-layout.md`.
6//!
7//! Everything below the driver reaches the outside world through this type and not through
8//! `std::fs`, `std::env` or `println!`. That is the whole reason the compiler can be used as
9//! a library and tested without spawning a process, and it is enforced by the layer rule
10//! rather than by discipline.
11//!
12//! # Status
13//!
14//! Options, optimisation levels, emit kinds and diagnostic counting are real. The parallel
15//! job model and the file system abstraction land with the rest of `M0` and `M1`.
16//!
17//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
18//! explicitly unstable and will change without a major version bump.
19
20#![doc(html_root_url = "https://docs.rs/rucc-session/0.1.0")]
21
22use std::fmt;
23use std::str::FromStr;
24
25use rucc_base::Interner;
26use rucc_diag::{Diagnostic, Severity};
27use rucc_target::{TargetInfo, Triple};
28
29/// An optimisation level.
30///
31/// `spec/16-performance.md` section 16.4 gives each level a throughput budget and a code
32/// quality budget, and the levels exist to make that tradeoff explicit rather than to be a
33/// dial. There is no `-O4`, because a level nobody can state the contract for is a level
34/// nobody can test.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
36pub enum OptLevel {
37    /// `-O0`. Compile as fast as possible and keep every variable inspectable.
38    #[default]
39    O0,
40    /// `-O1`. The cheap wins, at roughly the cost of `-O0`.
41    O1,
42    /// `-O2`. The full pipeline. This is the level the code quality claim is about.
43    O2,
44    /// `-O3`. `-O2` plus the transformations that trade size for speed.
45    O3,
46    /// `-Os`. Optimise for size, at roughly `-O2` compile time.
47    Os,
48    /// `-Oz`. Optimise for size, aggressively.
49    Oz,
50}
51
52impl OptLevel {
53    /// The flag that selects this level.
54    pub const fn as_flag(self) -> &'static str {
55        match self {
56            OptLevel::O0 => "-O0",
57            OptLevel::O1 => "-O1",
58            OptLevel::O2 => "-O2",
59            OptLevel::O3 => "-O3",
60            OptLevel::Os => "-Os",
61            OptLevel::Oz => "-Oz",
62        }
63    }
64
65    /// Whether this level optimises for size rather than speed.
66    pub const fn is_size(self) -> bool {
67        matches!(self, OptLevel::Os | OptLevel::Oz)
68    }
69
70    /// Whether the middle end runs at all.
71    pub const fn runs_optimizer(self) -> bool {
72        !matches!(self, OptLevel::O0)
73    }
74}
75
76impl fmt::Display for OptLevel {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.as_flag())
79    }
80}
81
82impl FromStr for OptLevel {
83    type Err = ();
84
85    /// Parses the part after `-O`, so `""` is `-O` which GCC treats as `-O1`.
86    fn from_str(s: &str) -> Result<Self, ()> {
87        Ok(match s {
88            "0" => OptLevel::O0,
89            "" | "1" => OptLevel::O1,
90            "2" => OptLevel::O2,
91            // GCC accepts `-O4` and above and treats them as `-O3`. Build systems in the
92            // wild do pass them, so matching that is cheaper than being right.
93            "3" | "4" | "5" | "6" | "7" | "8" | "9" => OptLevel::O3,
94            "s" => OptLevel::Os,
95            "z" => OptLevel::Oz,
96            _ => return Err(()),
97        })
98    }
99}
100
101/// What the compiler should produce.
102///
103/// The intermediate forms are not a debugging convenience bolted on later. Every one of them
104/// is a documented textual form that round-trips, which is what makes the per-stage testing
105/// in `spec/15-testing.md` section 15.2 possible.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
107// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
108// match that needs to change, in this workspace and in anyone else's code. That is
109// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
110// target is a data change: the compiler tells you every place the data is read.
111pub enum EmitKind {
112    /// A linked executable. The default.
113    #[default]
114    Executable,
115    /// An object file, `-c`.
116    Object,
117    /// Assembly text, `-S`.
118    Asm,
119    /// Preprocessed source, `-E`.
120    Preprocessed,
121    /// The typed AST, `--emit=tast`.
122    Tast,
123    /// The IR, `--emit=ir`.
124    Ir,
125    /// The machine IR after register allocation, `--emit=mir-final`.
126    MirFinal,
127}
128
129impl EmitKind {
130    /// The name used by `--emit=` and by `--print-config`.
131    pub const fn as_str(self) -> &'static str {
132        match self {
133            EmitKind::Executable => "exe",
134            EmitKind::Object => "obj",
135            EmitKind::Asm => "asm",
136            EmitKind::Preprocessed => "preprocessed",
137            EmitKind::Tast => "tast",
138            EmitKind::Ir => "ir",
139            EmitKind::MirFinal => "mir-final",
140        }
141    }
142}
143
144impl FromStr for EmitKind {
145    type Err = ();
146
147    fn from_str(s: &str) -> Result<Self, ()> {
148        Ok(match s {
149            "exe" => EmitKind::Executable,
150            "obj" => EmitKind::Object,
151            "asm" => EmitKind::Asm,
152            "preprocessed" => EmitKind::Preprocessed,
153            "tast" => EmitKind::Tast,
154            "ir" => EmitKind::Ir,
155            "mir-final" => EmitKind::MirFinal,
156            _ => return Err(()),
157        })
158    }
159}
160
161/// Everything a compilation was asked to do.
162///
163/// Options are a plain value with no interior mutability, so a caller can build one, clone
164/// it, tweak one field and run a second compilation, which is exactly what the differential
165/// testing in `spec/15-testing.md` needs.
166#[derive(Debug, Clone, PartialEq, Eq)]
167#[non_exhaustive]
168pub struct Options {
169    /// The target to generate code for.
170    pub target: Triple,
171    /// The optimisation level.
172    pub opt_level: OptLevel,
173    /// What to produce.
174    pub emit: EmitKind,
175    /// Whether to emit debug information.
176    pub debug_info: bool,
177    /// Whether warnings are errors.
178    pub warnings_are_errors: bool,
179    /// How many diagnostics to print before giving up. Past a certain point the output is
180    /// noise from a single earlier mistake, and GCC's default of no limit is not a kindness.
181    pub error_limit: u32,
182}
183
184impl Options {
185    /// Default options for `target`.
186    pub fn new(target: Triple) -> Self {
187        Self {
188            target,
189            opt_level: OptLevel::default(),
190            emit: EmitKind::default(),
191            debug_info: false,
192            warnings_are_errors: false,
193            error_limit: 20,
194        }
195    }
196}
197
198/// One compilation.
199///
200/// Holds the options, the string interner and the diagnostics raised so far. Passing a
201/// `&mut Session` is how a stage reports a problem, and the return value of a stage says
202/// what it produced, never whether it succeeded: that question is answered by
203/// [`Session::has_errors`].
204#[derive(Debug)]
205pub struct Session {
206    /// What this compilation was asked to do.
207    pub opts: Options,
208    /// Everything known about the target.
209    pub target: TargetInfo,
210    /// The one interner for the compilation.
211    pub interner: Interner,
212    diagnostics: Vec<Diagnostic>,
213    error_count: u32,
214    warning_count: u32,
215}
216
217impl Session {
218    /// A session for `opts`.
219    pub fn new(opts: Options) -> Self {
220        let target = TargetInfo::new(opts.target);
221        Self {
222            opts,
223            target,
224            interner: Interner::with_capacity(1024),
225            diagnostics: Vec::new(),
226            error_count: 0,
227            warning_count: 0,
228        }
229    }
230
231    /// Records a diagnostic.
232    ///
233    /// Under `-Werror` a warning is promoted here, once, rather than at every site that
234    /// raises one.
235    pub fn emit(&mut self, mut diag: Diagnostic) {
236        if self.opts.warnings_are_errors && diag.severity == Severity::Warning {
237            diag.severity = Severity::Error;
238        }
239        match diag.severity {
240            Severity::Error | Severity::Ice => self.error_count += 1,
241            Severity::Warning => self.warning_count += 1,
242            Severity::Note | Severity::Help => {}
243        }
244        self.diagnostics.push(diag);
245    }
246
247    /// Everything raised so far, in the order it was raised.
248    pub fn diagnostics(&self) -> &[Diagnostic] {
249        &self.diagnostics
250    }
251
252    /// Whether anything fatal has been raised.
253    pub fn has_errors(&self) -> bool {
254        self.error_count > 0
255    }
256
257    /// How many errors have been raised.
258    pub fn error_count(&self) -> u32 {
259        self.error_count
260    }
261
262    /// How many warnings have been raised.
263    pub fn warning_count(&self) -> u32 {
264        self.warning_count
265    }
266
267    /// Whether the error limit has been reached and the caller should stop.
268    pub fn error_limit_reached(&self) -> bool {
269        self.opts.error_limit != 0 && self.error_count >= self.opts.error_limit
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn session() -> Session {
278        Session::new(Options::new("x86_64-unknown-linux-gnu".parse().unwrap()))
279    }
280
281    #[test]
282    fn optimisation_levels_parse_the_way_gcc_spells_them() {
283        assert_eq!("".parse::<OptLevel>().unwrap(), OptLevel::O1);
284        assert_eq!("0".parse::<OptLevel>().unwrap(), OptLevel::O0);
285        assert_eq!("2".parse::<OptLevel>().unwrap(), OptLevel::O2);
286        assert_eq!("9".parse::<OptLevel>().unwrap(), OptLevel::O3);
287        assert_eq!("s".parse::<OptLevel>().unwrap(), OptLevel::Os);
288        assert!("q".parse::<OptLevel>().is_err());
289    }
290
291    #[test]
292    fn only_o0_skips_the_optimizer() {
293        assert!(!OptLevel::O0.runs_optimizer());
294        assert!(OptLevel::O1.runs_optimizer());
295        assert!(OptLevel::Oz.runs_optimizer());
296    }
297
298    #[test]
299    fn emit_kinds_round_trip_through_their_names() {
300        for k in [
301            EmitKind::Executable,
302            EmitKind::Object,
303            EmitKind::Asm,
304            EmitKind::Preprocessed,
305            EmitKind::Tast,
306            EmitKind::Ir,
307            EmitKind::MirFinal,
308        ] {
309            assert_eq!(k.as_str().parse::<EmitKind>().unwrap(), k);
310        }
311    }
312
313    #[test]
314    fn errors_are_counted_and_warnings_are_not() {
315        let mut s = session();
316        s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
317        s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
318        assert_eq!(s.error_count(), 1);
319        assert_eq!(s.warning_count(), 1);
320        assert!(s.has_errors());
321        assert_eq!(s.diagnostics().len(), 2);
322    }
323
324    #[test]
325    fn werror_promotes_once_at_the_sink() {
326        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
327        opts.warnings_are_errors = true;
328        let mut s = Session::new(opts);
329        s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
330        assert_eq!(s.error_count(), 1);
331        assert_eq!(s.warning_count(), 0);
332        assert_eq!(s.diagnostics()[0].severity, Severity::Error);
333    }
334
335    #[test]
336    fn the_error_limit_can_be_switched_off() {
337        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
338        opts.error_limit = 0;
339        let mut s = Session::new(opts);
340        for _ in 0..100 {
341            s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
342        }
343        assert!(!s.error_limit_reached());
344    }
345
346    #[test]
347    fn the_session_carries_the_resolved_target() {
348        let s = session();
349        assert_eq!(s.target.pointer_width, 64);
350        assert!(s.target.char_is_signed);
351    }
352}