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