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, diagnostic counting, the source map every span
15//! is resolved against, the file system the compiler reads through and the include search
16//! path are real. The parallel job model is still a placeholder.
17//!
18//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
19//! explicitly unstable and will change without a major version bump.
20
21#![doc(html_root_url = "https://docs.rs/rucc-session/0.2.8")]
22
23mod fs;
24
25pub use crate::fs::{Dir, FileSystem, Found, IncludeForm, MemoryFileSystem, SearchPath};
26
27use std::fmt;
28use std::str::FromStr;
29
30use rucc_base::Interner;
31use rucc_diag::{Diagnostic, Severity, SourceMap};
32use rucc_target::{TargetInfo, Triple};
33
34/// An optimisation level.
35///
36/// `spec/16-performance.md` section 16.4 gives each level a throughput budget and a code
37/// quality budget, and the levels exist to make that tradeoff explicit rather than to be a
38/// dial. There is no `-O4`, because a level nobody can state the contract for is a level
39/// nobody can test.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
41pub enum OptLevel {
42    /// `-O0`. Compile as fast as possible and keep every variable inspectable.
43    #[default]
44    O0,
45    /// `-O1`. The cheap wins, at roughly the cost of `-O0`.
46    O1,
47    /// `-O2`. The full pipeline. This is the level the code quality claim is about.
48    O2,
49    /// `-O3`. `-O2` plus the transformations that trade size for speed.
50    O3,
51    /// `-Os`. Optimise for size, at roughly `-O2` compile time.
52    Os,
53    /// `-Oz`. Optimise for size, aggressively.
54    Oz,
55}
56
57impl OptLevel {
58    /// The flag that selects this level.
59    pub const fn as_flag(self) -> &'static str {
60        match self {
61            OptLevel::O0 => "-O0",
62            OptLevel::O1 => "-O1",
63            OptLevel::O2 => "-O2",
64            OptLevel::O3 => "-O3",
65            OptLevel::Os => "-Os",
66            OptLevel::Oz => "-Oz",
67        }
68    }
69
70    /// Whether this level optimises for size rather than speed.
71    pub const fn is_size(self) -> bool {
72        matches!(self, OptLevel::Os | OptLevel::Oz)
73    }
74
75    /// Whether the middle end runs at all.
76    pub const fn runs_optimizer(self) -> bool {
77        !matches!(self, OptLevel::O0)
78    }
79}
80
81impl fmt::Display for OptLevel {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(self.as_flag())
84    }
85}
86
87impl FromStr for OptLevel {
88    type Err = ();
89
90    /// Parses the part after `-O`, so `""` is `-O` which GCC treats as `-O1`.
91    fn from_str(s: &str) -> Result<Self, ()> {
92        Ok(match s {
93            "0" => OptLevel::O0,
94            "" | "1" => OptLevel::O1,
95            "2" => OptLevel::O2,
96            // GCC accepts `-O4` and above and treats them as `-O3`. Build systems in the
97            // wild do pass them, so matching that is cheaper than being right.
98            "3" | "4" | "5" | "6" | "7" | "8" | "9" => OptLevel::O3,
99            "s" => OptLevel::Os,
100            "z" => OptLevel::Oz,
101            _ => return Err(()),
102        })
103    }
104}
105
106/// What the compiler should produce.
107///
108/// The intermediate forms are not a debugging convenience bolted on later. Every one of them
109/// is a documented textual form that round-trips, which is what makes the per-stage testing
110/// in `spec/15-testing.md` section 15.2 possible.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
112// Deliberately not `#[non_exhaustive]`. Adding a variant here has to break every
113// match that needs to change, in this workspace and in anyone else's code. That is
114// the property `spec/10-backend.md` section 10.8 is claiming when it says adding a
115// target is a data change: the compiler tells you every place the data is read.
116pub enum EmitKind {
117    /// A linked executable. The default.
118    #[default]
119    Executable,
120    /// An object file, `-c`.
121    Object,
122    /// Assembly text, `-S`.
123    Asm,
124    /// Preprocessed source, `-E`.
125    Preprocessed,
126    /// The typed AST, `--emit=tast`.
127    Tast,
128    /// The IR, `--emit=ir`.
129    Ir,
130    /// The machine IR after register allocation, `--emit=mir-final`.
131    MirFinal,
132}
133
134impl EmitKind {
135    /// The name used by `--emit=` and by `--print-config`.
136    pub const fn as_str(self) -> &'static str {
137        match self {
138            EmitKind::Executable => "exe",
139            EmitKind::Object => "obj",
140            EmitKind::Asm => "asm",
141            EmitKind::Preprocessed => "preprocessed",
142            EmitKind::Tast => "tast",
143            EmitKind::Ir => "ir",
144            EmitKind::MirFinal => "mir-final",
145        }
146    }
147}
148
149impl FromStr for EmitKind {
150    type Err = ();
151
152    fn from_str(s: &str) -> Result<Self, ()> {
153        Ok(match s {
154            "exe" => EmitKind::Executable,
155            "obj" => EmitKind::Object,
156            "asm" => EmitKind::Asm,
157            "preprocessed" => EmitKind::Preprocessed,
158            "tast" => EmitKind::Tast,
159            "ir" => EmitKind::Ir,
160            "mir-final" => EmitKind::MirFinal,
161            _ => return Err(()),
162        })
163    }
164}
165
166/// Which C the source is written in.
167///
168/// The GNU variants are the same language with `__STRICT_ANSI__` left undefined, so the
169/// dialect and the extension question are two fields rather than ten variants.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
171pub enum Std {
172    /// `-std=c89`, and `-ansi`.
173    C89,
174    /// `-std=c99`.
175    C99,
176    /// `-std=c11`.
177    C11,
178    /// `-std=c17`, which is C11 with the defect reports applied.
179    C17,
180    /// `-std=c23`. The default, matching current GCC.
181    #[default]
182    C23,
183}
184
185impl Std {
186    /// What `__STDC_VERSION__` says, which C89 does not define at all.
187    pub const fn stdc_version(self) -> Option<&'static str> {
188        match self {
189            Std::C89 => None,
190            Std::C99 => Some("199901L"),
191            Std::C11 => Some("201112L"),
192            Std::C17 => Some("201710L"),
193            Std::C23 => Some("202311L"),
194        }
195    }
196
197    /// The name in `-std=`.
198    pub const fn as_str(self) -> &'static str {
199        match self {
200            Std::C89 => "c89",
201            Std::C99 => "c99",
202            Std::C11 => "c11",
203            Std::C17 => "c17",
204            Std::C23 => "c23",
205        }
206    }
207
208    /// Whether this dialect has `_Atomic`, `_Thread_local` and the rest of C11.
209    pub const fn has_c11(self) -> bool {
210        matches!(self, Std::C11 | Std::C17 | Std::C23)
211    }
212
213    /// Reads a `-std=` argument, and says whether the GNU extensions came with it.
214    ///
215    /// Every alias GCC takes is here, including the `iso9899` spellings and the year based
216    /// ones, because a build system that passes `-std=iso9899:1999` is passing what its
217    /// author tested against and rejecting it helps nobody. An unknown dialect is `None`
218    /// rather than a guess, since guessing means compiling a different language than the one
219    /// asked for.
220    #[must_use]
221    pub fn from_flag(name: &str) -> Option<(Std, bool)> {
222        let gnu = name.starts_with("gnu");
223        let std = match name {
224            "c89" | "c90" | "gnu89" | "gnu90" | "iso9899:1990" | "iso9899:199409" => Std::C89,
225            "c99" | "c9x" | "gnu99" | "gnu9x" | "iso9899:1999" | "iso9899:199x" => Std::C99,
226            "c11" | "c1x" | "gnu11" | "gnu1x" | "iso9899:2011" => Std::C11,
227            "c17" | "c18" | "gnu17" | "gnu18" | "iso9899:2017" | "iso9899:2018" => Std::C17,
228            "c23" | "c2x" | "gnu23" | "gnu2x" => Std::C23,
229            _ => return None,
230        };
231        Some((std, gnu))
232    }
233}
234
235/// The GCC release the compiler claims to be, as `__GNUC__`, `__GNUC_MINOR__` and
236/// `__GNUC_PATCHLEVEL__`.
237///
238/// Design: `spec/04-driver-and-cli.md` section 4.5, which makes this a knob rather than a
239/// constant and says to start conservative and raise it as the matrix in `rucc-gnu` fills in.
240///
241/// The default is the version Clang claimed for over a decade, which is the one value every
242/// real header set is known to cope with from a compiler that is not GCC. It is deliberately
243/// low. glibc gates most of what it hands a caller on `__GNUC_PREREQ`, so the claim decides
244/// which half of `sys/cdefs.h` we get, and claiming a version whose promises we have not kept
245/// means being handed syntax we cannot parse.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
247pub struct GnucVersion {
248    /// `__GNUC__`.
249    pub major: u32,
250    /// `__GNUC_MINOR__`.
251    pub minor: u32,
252    /// `__GNUC_PATCHLEVEL__`.
253    pub patch: u32,
254}
255
256impl Default for GnucVersion {
257    fn default() -> GnucVersion {
258        GnucVersion { major: 4, minor: 2, patch: 1 }
259    }
260}
261
262impl FromStr for GnucVersion {
263    type Err = String;
264
265    /// Reads `-fgnuc-version=`, which is `15`, `15.1` or `15.1.0`.
266    ///
267    /// The short forms are not a convenience, they are what people write. A missing component
268    /// is zero, the same way GCC treats a release with no patchlevel.
269    fn from_str(text: &str) -> Result<GnucVersion, String> {
270        let mut parts = text.split('.');
271        let mut next = |what: &str| -> Result<u32, String> {
272            match parts.next() {
273                None => Ok(0),
274                Some(field) => {
275                    field.parse().map_err(|_| format!("`{text}` has a {what} that is not a number"))
276                }
277            }
278        };
279        let major = next("major")?;
280        let minor = next("minor")?;
281        let patch = next("patchlevel")?;
282        if parts.next().is_some() {
283            return Err(format!("`{text}` has more than three components"));
284        }
285        Ok(GnucVersion { major, minor, patch })
286    }
287}
288
289/// What the `-d` family asks to be dumped alongside, or instead of, the preprocessed output.
290///
291/// Design: `spec/04-driver-and-cli.md` section 4.4.
292///
293/// GCC spells these as letters packed into one flag, so `-dDI` is two of them, and a letter it
294/// does not know is ignored rather than rejected. That last part is deliberate on GCC's side
295/// and worth copying: the family is a debugging aid and a build that passes `-dumpbase` should
296/// not die on the `-d`.
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
298pub struct Dumps {
299    /// `-dM`. Print the macros that are defined at the end, and nothing else.
300    pub macros: bool,
301}
302
303impl Dumps {
304    /// The letters GCC's preprocessor takes after `-d`.
305    ///
306    /// `M` is the macros, `D` is the macros in place, `N` is their names only, `I` is the
307    /// `#include` lines and `U` is the macros as they are used. Only `M` does anything so far.
308    const LETTERS: &'static str = "MDNIU";
309
310    /// Whether `arg` is a flag from this family rather than something else beginning with
311    /// `-d`.
312    ///
313    /// The check is here rather than in the driver so that the set of letters and the set of
314    /// flags accepted cannot drift apart. It matters because `-dumpversion` also begins with
315    /// `-d`, and a family that swallowed every such flag would turn a flag we have not written
316    /// into a dump of nothing.
317    #[must_use]
318    pub fn is_family(arg: &str) -> bool {
319        match arg.strip_prefix("-d") {
320            Some("") | None => false,
321            Some(letters) => letters.chars().all(|c| Dumps::LETTERS.contains(c)),
322        }
323    }
324
325    /// Reads the letters after `-d`, ignoring the ones we do not implement yet.
326    pub fn add(&mut self, letters: &str) {
327        for letter in letters.chars() {
328            if letter == 'M' {
329                self.macros = true;
330            }
331        }
332    }
333
334    /// Whether anything at all was asked for.
335    #[must_use]
336    pub const fn any(self) -> bool {
337        self.macros
338    }
339}
340
341/// Everything a compilation was asked to do.
342///
343/// Options are a plain value with no interior mutability, so a caller can build one, clone
344/// it, tweak one field and run a second compilation, which is exactly what the differential
345/// testing in `spec/15-testing.md` needs.
346#[derive(Debug, Clone, PartialEq, Eq)]
347#[non_exhaustive]
348pub struct Options {
349    /// The target to generate code for.
350    pub target: Triple,
351    /// The optimisation level.
352    pub opt_level: OptLevel,
353    /// What to produce.
354    pub emit: EmitKind,
355    /// Whether to emit debug information.
356    pub debug_info: bool,
357    /// Whether warnings are errors.
358    pub warnings_are_errors: bool,
359    /// How many diagnostics to print before giving up. Past a certain point the output is
360    /// noise from a single earlier mistake, and GCC's default of no limit is not a kindness.
361    pub error_limit: u32,
362    /// The dialect, from `-std=`.
363    pub std: Std,
364    /// Whether the GNU extensions are on, which is `-std=gnu23` rather than `-std=c23`.
365    pub gnu_extensions: bool,
366    /// The GCC release claimed, from `-fgnuc-version=`.
367    pub gnuc: GnucVersion,
368    /// Whether there is a standard library, which is `-ffreestanding` turned around.
369    pub hosted: bool,
370    /// `-D` in command line order. `FOO` means `FOO=1`, as GCC has it.
371    pub defines: Vec<String>,
372    /// `-U` in command line order, applied after the defines because `-U` wins.
373    pub undefines: Vec<String>,
374    /// Where a header is looked for.
375    pub search: SearchPath,
376    /// Whether `-E` writes line markers, which `-P` turns off.
377    pub line_markers: bool,
378    /// What the `-d` family asks for.
379    pub dumps: Dumps,
380}
381
382impl Options {
383    /// Default options for `target`.
384    pub fn new(target: Triple) -> Self {
385        Self {
386            target,
387            opt_level: OptLevel::default(),
388            emit: EmitKind::default(),
389            debug_info: false,
390            warnings_are_errors: false,
391            error_limit: 20,
392            std: Std::default(),
393            gnu_extensions: true,
394            gnuc: GnucVersion::default(),
395            hosted: true,
396            defines: Vec::new(),
397            undefines: Vec::new(),
398            search: SearchPath::new(),
399            line_markers: true,
400            dumps: Dumps::default(),
401        }
402    }
403}
404
405/// One compilation.
406///
407/// Holds the options, the string interner and the diagnostics raised so far. Passing a
408/// `&mut Session` is how a stage reports a problem, and the return value of a stage says
409/// what it produced, never whether it succeeded: that question is answered by
410/// [`Session::has_errors`].
411#[derive(Debug)]
412pub struct Session {
413    /// What this compilation was asked to do.
414    pub opts: Options,
415    /// Everything known about the target.
416    pub target: TargetInfo,
417    /// The one interner for the compilation.
418    pub interner: Interner,
419    /// Every file read during the compilation, and the flat coordinate space their spans
420    /// live in.
421    ///
422    /// This is on the session rather than passed around separately because a span is only
423    /// meaningful against the map that issued it, and one map per compilation is the rule
424    /// that makes that true by construction.
425    pub sources: SourceMap,
426    diagnostics: Vec<Diagnostic>,
427    error_count: u32,
428    warning_count: u32,
429}
430
431impl Session {
432    /// A session for `opts`.
433    pub fn new(opts: Options) -> Self {
434        let target = TargetInfo::new(opts.target);
435        Self {
436            opts,
437            target,
438            interner: Interner::with_capacity(1024),
439            sources: SourceMap::new(),
440            diagnostics: Vec::new(),
441            error_count: 0,
442            warning_count: 0,
443        }
444    }
445
446    /// Records a diagnostic.
447    ///
448    /// Under `-Werror` a warning is promoted here, once, rather than at every site that
449    /// raises one.
450    pub fn emit(&mut self, mut diag: Diagnostic) {
451        if self.opts.warnings_are_errors && diag.severity == Severity::Warning {
452            diag.severity = Severity::Error;
453        }
454        match diag.severity {
455            Severity::Error | Severity::Ice => self.error_count += 1,
456            Severity::Warning => self.warning_count += 1,
457            Severity::Note | Severity::Help => {}
458        }
459        self.diagnostics.push(diag);
460    }
461
462    /// Everything raised so far, in the order it was raised.
463    pub fn diagnostics(&self) -> &[Diagnostic] {
464        &self.diagnostics
465    }
466
467    /// Whether anything fatal has been raised.
468    pub fn has_errors(&self) -> bool {
469        self.error_count > 0
470    }
471
472    /// How many errors have been raised.
473    pub fn error_count(&self) -> u32 {
474        self.error_count
475    }
476
477    /// How many warnings have been raised.
478    pub fn warning_count(&self) -> u32 {
479        self.warning_count
480    }
481
482    /// Whether the error limit has been reached and the caller should stop.
483    pub fn error_limit_reached(&self) -> bool {
484        self.opts.error_limit != 0 && self.error_count >= self.opts.error_limit
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn session() -> Session {
493        Session::new(Options::new("x86_64-unknown-linux-gnu".parse().unwrap()))
494    }
495
496    #[test]
497    fn a_version_claim_reads_the_way_gcc_prints_one() {
498        // `gcc -dumpfullversion` gives all three, `gcc -dumpversion` gives one, and both are
499        // things a script pastes straight into a flag.
500        let all = |v: &str| v.parse::<GnucVersion>().unwrap();
501        assert_eq!(all("15.1.0"), GnucVersion { major: 15, minor: 1, patch: 0 });
502        assert_eq!(all("15"), GnucVersion { major: 15, minor: 0, patch: 0 });
503        assert_eq!(all("4.2"), GnucVersion { major: 4, minor: 2, patch: 0 });
504        assert!("".parse::<GnucVersion>().is_err());
505        assert!("15.".parse::<GnucVersion>().is_err(), "a trailing dot is a typo, not a zero");
506        assert!("1.2.3.4".parse::<GnucVersion>().is_err());
507    }
508
509    #[test]
510    fn optimisation_levels_parse_the_way_gcc_spells_them() {
511        assert_eq!("".parse::<OptLevel>().unwrap(), OptLevel::O1);
512        assert_eq!("0".parse::<OptLevel>().unwrap(), OptLevel::O0);
513        assert_eq!("2".parse::<OptLevel>().unwrap(), OptLevel::O2);
514        assert_eq!("9".parse::<OptLevel>().unwrap(), OptLevel::O3);
515        assert_eq!("s".parse::<OptLevel>().unwrap(), OptLevel::Os);
516        assert!("q".parse::<OptLevel>().is_err());
517    }
518
519    #[test]
520    fn only_o0_skips_the_optimizer() {
521        assert!(!OptLevel::O0.runs_optimizer());
522        assert!(OptLevel::O1.runs_optimizer());
523        assert!(OptLevel::Oz.runs_optimizer());
524    }
525
526    #[test]
527    fn emit_kinds_round_trip_through_their_names() {
528        for k in [
529            EmitKind::Executable,
530            EmitKind::Object,
531            EmitKind::Asm,
532            EmitKind::Preprocessed,
533            EmitKind::Tast,
534            EmitKind::Ir,
535            EmitKind::MirFinal,
536        ] {
537            assert_eq!(k.as_str().parse::<EmitKind>().unwrap(), k);
538        }
539    }
540
541    #[test]
542    fn errors_are_counted_and_warnings_are_not() {
543        let mut s = session();
544        s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
545        s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
546        assert_eq!(s.error_count(), 1);
547        assert_eq!(s.warning_count(), 1);
548        assert!(s.has_errors());
549        assert_eq!(s.diagnostics().len(), 2);
550    }
551
552    #[test]
553    fn werror_promotes_once_at_the_sink() {
554        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
555        opts.warnings_are_errors = true;
556        let mut s = Session::new(opts);
557        s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
558        assert_eq!(s.error_count(), 1);
559        assert_eq!(s.warning_count(), 0);
560        assert_eq!(s.diagnostics()[0].severity, Severity::Error);
561    }
562
563    #[test]
564    fn the_error_limit_can_be_switched_off() {
565        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
566        opts.error_limit = 0;
567        let mut s = Session::new(opts);
568        for _ in 0..100 {
569            s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
570        }
571        assert!(!s.error_limit_reached());
572    }
573
574    #[test]
575    fn the_session_carries_the_source_map_spans_are_resolved_against() {
576        let mut s = session();
577        let file = s.sources.add("a.c", b"int x;\n".to_vec()).unwrap();
578        let start = s.sources.file(file).start;
579        assert_eq!(s.sources.render_position(start + 4), "a.c:1:5");
580    }
581
582    #[test]
583    fn the_session_carries_the_resolved_target() {
584        let s = session();
585        assert_eq!(s.target.pointer_width, 64);
586        assert!(s.target.char_is_signed);
587    }
588}