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.2.17")]
23
24mod fs;
25pub mod runtime;
26
27pub use crate::fs::{Dir, FileSystem, Found, IncludeForm, MemoryFileSystem, SearchPath};
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 the version Clang claimed for over a decade, which is the one value every
244/// real header set is known to cope with from a compiler that is not GCC. It is deliberately
245/// low. glibc gates most of what it hands a caller on `__GNUC_PREREQ`, so the claim decides
246/// which half of `sys/cdefs.h` we get, and claiming a version whose promises we have not kept
247/// means being handed syntax we cannot parse.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
249pub struct GnucVersion {
250 /// `__GNUC__`.
251 pub major: u32,
252 /// `__GNUC_MINOR__`.
253 pub minor: u32,
254 /// `__GNUC_PATCHLEVEL__`.
255 pub patch: u32,
256}
257
258impl Default for GnucVersion {
259 fn default() -> GnucVersion {
260 GnucVersion { major: 4, minor: 2, patch: 1 }
261 }
262}
263
264impl FromStr for GnucVersion {
265 type Err = String;
266
267 /// Reads `-fgnuc-version=`, which is `15`, `15.1` or `15.1.0`.
268 ///
269 /// The short forms are not a convenience, they are what people write. A missing component
270 /// is zero, the same way GCC treats a release with no patchlevel.
271 fn from_str(text: &str) -> Result<GnucVersion, String> {
272 let mut parts = text.split('.');
273 let mut next = |what: &str| -> Result<u32, String> {
274 match parts.next() {
275 None => Ok(0),
276 Some(field) => {
277 field.parse().map_err(|_| format!("`{text}` has a {what} that is not a number"))
278 }
279 }
280 };
281 let major = next("major")?;
282 let minor = next("minor")?;
283 let patch = next("patchlevel")?;
284 if parts.next().is_some() {
285 return Err(format!("`{text}` has more than three components"));
286 }
287 Ok(GnucVersion { major, minor, patch })
288 }
289}
290
291/// What the `-d` family asks to be dumped alongside, or instead of, the preprocessed output.
292///
293/// Design: `spec/04-driver-and-cli.md` section 4.4.
294///
295/// GCC spells these as letters packed into one flag, so `-dDI` is two of them, and a letter it
296/// does not know is ignored rather than rejected. That last part is deliberate on GCC's side
297/// and worth copying: the family is a debugging aid and a build that passes `-dumpbase` should
298/// not die on the `-d`.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
300pub struct Dumps {
301 /// `-dM`. Print the macros that are defined at the end, and nothing else.
302 pub macros: bool,
303}
304
305impl Dumps {
306 /// The letters GCC's preprocessor takes after `-d`.
307 ///
308 /// `M` is the macros, `D` is the macros in place, `N` is their names only, `I` is the
309 /// `#include` lines and `U` is the macros as they are used. Only `M` does anything so far.
310 const LETTERS: &'static str = "MDNIU";
311
312 /// Whether `arg` is a flag from this family rather than something else beginning with
313 /// `-d`.
314 ///
315 /// The check is here rather than in the driver so that the set of letters and the set of
316 /// flags accepted cannot drift apart. It matters because `-dumpversion` also begins with
317 /// `-d`, and a family that swallowed every such flag would turn a flag we have not written
318 /// into a dump of nothing.
319 #[must_use]
320 pub fn is_family(arg: &str) -> bool {
321 match arg.strip_prefix("-d") {
322 Some("") | None => false,
323 Some(letters) => letters.chars().all(|c| Dumps::LETTERS.contains(c)),
324 }
325 }
326
327 /// Reads the letters after `-d`, ignoring the ones we do not implement yet.
328 pub fn add(&mut self, letters: &str) {
329 for letter in letters.chars() {
330 if letter == 'M' {
331 self.macros = true;
332 }
333 }
334 }
335
336 /// Whether anything at all was asked for.
337 #[must_use]
338 pub const fn any(self) -> bool {
339 self.macros
340 }
341}
342
343/// Everything a compilation was asked to do.
344///
345/// Options are a plain value with no interior mutability, so a caller can build one, clone
346/// it, tweak one field and run a second compilation, which is exactly what the differential
347/// testing in `spec/15-testing.md` needs.
348#[derive(Debug, Clone, PartialEq, Eq)]
349#[non_exhaustive]
350pub struct Options {
351 /// The target to generate code for.
352 pub target: Triple,
353 /// The optimisation level.
354 pub opt_level: OptLevel,
355 /// What to produce.
356 pub emit: EmitKind,
357 /// Whether to emit debug information.
358 pub debug_info: bool,
359 /// Whether warnings are errors.
360 pub warnings_are_errors: bool,
361 /// How many diagnostics to print before giving up. Past a certain point the output is
362 /// noise from a single earlier mistake, and GCC's default of no limit is not a kindness.
363 pub error_limit: u32,
364 /// The dialect, from `-std=`.
365 pub std: Std,
366 /// Whether the GNU extensions are on, which is `-std=gnu23` rather than `-std=c23`.
367 pub gnu_extensions: bool,
368 /// Whether `-pedantic` was given, which is what turns a use of an extension from silence
369 /// into a diagnostic. It is not the same knob as the dialect: `-std=c17 -pedantic` warns
370 /// about a construct that `-std=c17` alone accepts without a word.
371 pub pedantic: bool,
372 /// The GCC release claimed, from `-fgnuc-version=`.
373 pub gnuc: GnucVersion,
374 /// Whether there is a standard library, which is `-ffreestanding` turned around.
375 pub hosted: bool,
376 /// `-D` in command line order. `FOO` means `FOO=1`, as GCC has it.
377 pub defines: Vec<String>,
378 /// `-U` in command line order, applied after the defines because `-U` wins.
379 pub undefines: Vec<String>,
380 /// Where a header is looked for.
381 pub search: SearchPath,
382 /// Whether `-E` writes line markers, which `-P` turns off.
383 pub line_markers: bool,
384 /// What the `-d` family asks for.
385 pub dumps: Dumps,
386}
387
388impl Options {
389 /// Default options for `target`.
390 pub fn new(target: Triple) -> Self {
391 Self {
392 target,
393 opt_level: OptLevel::default(),
394 emit: EmitKind::default(),
395 debug_info: false,
396 warnings_are_errors: false,
397 error_limit: 20,
398 std: Std::default(),
399 gnu_extensions: true,
400 pedantic: false,
401 gnuc: GnucVersion::default(),
402 hosted: true,
403 defines: Vec::new(),
404 undefines: Vec::new(),
405 search: SearchPath::new(),
406 line_markers: true,
407 dumps: Dumps::default(),
408 }
409 }
410}
411
412/// One compilation.
413///
414/// Holds the options, the string interner and the diagnostics raised so far. Passing a
415/// `&mut Session` is how a stage reports a problem, and the return value of a stage says
416/// what it produced, never whether it succeeded: that question is answered by
417/// [`Session::has_errors`].
418#[derive(Debug)]
419pub struct Session {
420 /// What this compilation was asked to do.
421 pub opts: Options,
422 /// Everything known about the target.
423 pub target: TargetInfo,
424 /// The one interner for the compilation.
425 pub interner: Interner,
426 /// Every file read during the compilation, and the flat coordinate space their spans
427 /// live in.
428 ///
429 /// This is on the session rather than passed around separately because a span is only
430 /// meaningful against the map that issued it, and one map per compilation is the rule
431 /// that makes that true by construction.
432 pub sources: SourceMap,
433 diagnostics: Vec<Diagnostic>,
434 error_count: u32,
435 warning_count: u32,
436}
437
438impl Session {
439 /// A session for `opts`.
440 pub fn new(opts: Options) -> Self {
441 let target = TargetInfo::new(opts.target);
442 Self {
443 opts,
444 target,
445 interner: Interner::with_capacity(1024),
446 sources: SourceMap::new(),
447 diagnostics: Vec::new(),
448 error_count: 0,
449 warning_count: 0,
450 }
451 }
452
453 /// Records a diagnostic.
454 ///
455 /// Under `-Werror` a warning is promoted here, once, rather than at every site that
456 /// raises one.
457 pub fn emit(&mut self, mut diag: Diagnostic) {
458 if self.opts.warnings_are_errors && diag.severity == Severity::Warning {
459 diag.severity = Severity::Error;
460 }
461 match diag.severity {
462 Severity::Error | Severity::Ice => self.error_count += 1,
463 Severity::Warning => self.warning_count += 1,
464 Severity::Note | Severity::Help => {}
465 }
466 self.diagnostics.push(diag);
467 }
468
469 /// Everything raised so far, in the order it was raised.
470 pub fn diagnostics(&self) -> &[Diagnostic] {
471 &self.diagnostics
472 }
473
474 /// Whether anything fatal has been raised.
475 pub fn has_errors(&self) -> bool {
476 self.error_count > 0
477 }
478
479 /// How many errors have been raised.
480 pub fn error_count(&self) -> u32 {
481 self.error_count
482 }
483
484 /// How many warnings have been raised.
485 pub fn warning_count(&self) -> u32 {
486 self.warning_count
487 }
488
489 /// Whether the error limit has been reached and the caller should stop.
490 pub fn error_limit_reached(&self) -> bool {
491 self.opts.error_limit != 0 && self.error_count >= self.opts.error_limit
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 fn session() -> Session {
500 Session::new(Options::new("x86_64-unknown-linux-gnu".parse().unwrap()))
501 }
502
503 #[test]
504 fn a_version_claim_reads_the_way_gcc_prints_one() {
505 // `gcc -dumpfullversion` gives all three, `gcc -dumpversion` gives one, and both are
506 // things a script pastes straight into a flag.
507 let all = |v: &str| v.parse::<GnucVersion>().unwrap();
508 assert_eq!(all("15.1.0"), GnucVersion { major: 15, minor: 1, patch: 0 });
509 assert_eq!(all("15"), GnucVersion { major: 15, minor: 0, patch: 0 });
510 assert_eq!(all("4.2"), GnucVersion { major: 4, minor: 2, patch: 0 });
511 assert!("".parse::<GnucVersion>().is_err());
512 assert!("15.".parse::<GnucVersion>().is_err(), "a trailing dot is a typo, not a zero");
513 assert!("1.2.3.4".parse::<GnucVersion>().is_err());
514 }
515
516 #[test]
517 fn optimisation_levels_parse_the_way_gcc_spells_them() {
518 assert_eq!("".parse::<OptLevel>().unwrap(), OptLevel::O1);
519 assert_eq!("0".parse::<OptLevel>().unwrap(), OptLevel::O0);
520 assert_eq!("2".parse::<OptLevel>().unwrap(), OptLevel::O2);
521 assert_eq!("9".parse::<OptLevel>().unwrap(), OptLevel::O3);
522 assert_eq!("s".parse::<OptLevel>().unwrap(), OptLevel::Os);
523 assert!("q".parse::<OptLevel>().is_err());
524 }
525
526 #[test]
527 fn only_o0_skips_the_optimizer() {
528 assert!(!OptLevel::O0.runs_optimizer());
529 assert!(OptLevel::O1.runs_optimizer());
530 assert!(OptLevel::Oz.runs_optimizer());
531 }
532
533 #[test]
534 fn emit_kinds_round_trip_through_their_names() {
535 for k in [
536 EmitKind::Executable,
537 EmitKind::Object,
538 EmitKind::Asm,
539 EmitKind::Preprocessed,
540 EmitKind::Tast,
541 EmitKind::Ir,
542 EmitKind::MirFinal,
543 ] {
544 assert_eq!(k.as_str().parse::<EmitKind>().unwrap(), k);
545 }
546 }
547
548 #[test]
549 fn errors_are_counted_and_warnings_are_not() {
550 let mut s = session();
551 s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
552 s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
553 assert_eq!(s.error_count(), 1);
554 assert_eq!(s.warning_count(), 1);
555 assert!(s.has_errors());
556 assert_eq!(s.diagnostics().len(), 2);
557 }
558
559 #[test]
560 fn werror_promotes_once_at_the_sink() {
561 let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
562 opts.warnings_are_errors = true;
563 let mut s = Session::new(opts);
564 s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
565 assert_eq!(s.error_count(), 1);
566 assert_eq!(s.warning_count(), 0);
567 assert_eq!(s.diagnostics()[0].severity, Severity::Error);
568 }
569
570 #[test]
571 fn the_error_limit_can_be_switched_off() {
572 let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
573 opts.error_limit = 0;
574 let mut s = Session::new(opts);
575 for _ in 0..100 {
576 s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
577 }
578 assert!(!s.error_limit_reached());
579 }
580
581 #[test]
582 fn the_session_carries_the_source_map_spans_are_resolved_against() {
583 let mut s = session();
584 let file = s.sources.add("a.c", b"int x;\n".to_vec()).unwrap();
585 let start = s.sources.file(file).start;
586 assert_eq!(s.sources.render_position(start + 4), "a.c:1:5");
587 }
588
589 #[test]
590 fn the_session_carries_the_resolved_target() {
591 let s = session();
592 assert_eq!(s.target.pointer_width, 64);
593 assert!(s.target.char_is_signed);
594 }
595}