Skip to main content

rucc_driver/
compile.rs

1//! Running the front end over one file, from the bytes on disk to the typed tree.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.3, and the `M2` exit criterion in
4//! `spec/17-milestones.md` that says `--emit=tast` works.
5//!
6//! [`preprocess`](mod@crate::preprocess) stops after phase 4 because `-E` stops there. This
7//! carries on: phase 7, the parse, and the checking. It is one function rather than four composed
8//! ones because of what the four share. The tokens hold interned symbols, the untyped tree holds
9//! tokens, the typed tree holds the untyped tree's spans, and none of them owns the table it is
10//! reading, so one [`Session`] has to outlive all of them and there has to be one place that
11//! holds it.
12
13use std::path::Path;
14
15use rucc_diag::{Diagnostic, Severity};
16use rucc_lex::{Convert, Keywords, PpToken, convert};
17use rucc_sema::{Checker, Context as CheckContext};
18use rucc_session::{EmitKind, FileSystem, Options, Session};
19
20use crate::preprocess::render;
21
22/// What compiling one file produced.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Compiled {
25    /// The text to write, empty when there was nothing to write or the compilation failed.
26    pub text: String,
27    /// The diagnostics, already rendered, one per element, in the order they were reported.
28    pub messages: Vec<String>,
29    /// How many of them were errors.
30    pub errors: u32,
31}
32
33impl Compiled {
34    /// Whether anything went wrong badly enough that the output should not be used.
35    #[must_use]
36    pub fn failed(&self) -> bool {
37        self.errors > 0
38    }
39}
40
41/// Compiles one file as far as `opts.emit` asks for and renders the result.
42///
43/// `name` is the path as the user wrote it, which is the name every diagnostic about the file
44/// uses. Only [`EmitKind::Tast`] produces text today. Every later kind runs the same front end
45/// and gives back nothing, so that a file with a mistake in it is reported the same way
46/// whichever of them was asked for, rather than compiling silently until the part that is
47/// written notices.
48///
49/// The checking is skipped when the parse reported an error. The two poisoning rules mean a
50/// diagnosed expression produces no further complaints, but a declaration the parser had to skip
51/// past leaves no declaration behind at all, and every later use of that name would be reported
52/// as undeclared. One mistake is worth one message.
53#[must_use]
54pub fn compile(opts: &Options, name: &str, fs: &dyn FileSystem) -> Compiled {
55    let mut sess = Session::new(opts.clone());
56    // Before anything else interns a name. The keyword symbols have to be one unbroken run for
57    // a lookup to be a subtraction, and the preprocessor interns every identifier it reads, so
58    // building this after the expansion would mean building it after `char` had been seen.
59    let keywords = Keywords::new(&mut sess.interner, opts.std, opts.gnu_extensions);
60    let mut diagnostics: Vec<Diagnostic> = Vec::new();
61
62    let bytes = match fs.read(Path::new(name)) {
63        Ok(bytes) => bytes,
64        Err(e) => return failure(format!("{name}: {e}")),
65    };
66    let Ok(file) = sess.sources.add_shared(name, bytes, None) else {
67        return failure(format!("{name}: the source map has no room left for this file"));
68    };
69
70    // Phases 1 to 4. The expanded stream is turned into pp-tokens straight away, because the
71    // include context borrows the source map that rendering a diagnostic reads and the borrow
72    // has to end before anything is rendered.
73    let mut pp = rucc_pp::Preprocessor::new();
74    let predef = rucc_pp::Predef::for_options(opts);
75    let expanded: Vec<PpToken> = {
76        let mut cx = rucc_pp::Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
77        if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
78            return failure(format!("{name}: the source map has no room for the built in macros"));
79        }
80        pp.run(file, &mut cx).iter().map(|token| token.to_pp()).collect()
81    };
82    diagnostics.extend(pp.take_diagnostics());
83
84    // Phase 7, which is where a spelling becomes a keyword and a preprocessing number becomes
85    // a constant of a type.
86    let cx = Convert {
87        keywords: &keywords,
88        interner: &sess.interner,
89        target: &sess.target,
90        std: opts.std,
91        pedantic: opts.pedantic,
92    };
93    let (tokens, complaints) = convert(&expanded, &cx);
94    diagnostics.extend(complaints);
95
96    let parsed = rucc_parse::parse(
97        &tokens,
98        rucc_parse::Context {
99            interner: &sess.interner,
100            std: opts.std,
101            gnu: opts.gnu_extensions,
102            pedantic: opts.pedantic,
103            error_limit: opts.error_limit as usize,
104        },
105    );
106    let parse_failed = parsed.diagnostics.iter().any(|d| d.severity.is_fatal());
107    diagnostics.extend(parsed.diagnostics);
108
109    let mut text = String::new();
110    if !parse_failed {
111        let mut checker = Checker::new(
112            &parsed.ast,
113            CheckContext {
114                names: &sess.interner,
115                target: &sess.target,
116                std: opts.std,
117                gnu: opts.gnu_extensions,
118                pedantic: opts.pedantic,
119                error_limit: opts.error_limit as usize,
120            },
121        );
122        checker.check_unit();
123        let checked = checker.finish();
124        if !checked.failed() && opts.emit == EmitKind::Tast {
125            text = rucc_sema::print(&checked.tast, &checked.types, &sess.interner);
126        }
127        diagnostics.extend(checked.diagnostics);
128    }
129
130    let mut messages = Vec::with_capacity(diagnostics.len());
131    let mut errors = 0;
132    for diag in &diagnostics {
133        if diag.severity.is_fatal()
134            || (diag.severity == Severity::Warning && opts.warnings_are_errors)
135        {
136            errors += 1;
137        }
138        messages.push(render(diag, &sess.sources, opts.warnings_are_errors));
139    }
140    if errors > 0 {
141        // A tree built from a file that did not compile is not a tree anything should read.
142        text.clear();
143    }
144    Compiled { text, messages, errors }
145}
146
147/// A result that is nothing but one message, for the failures that happen before there is
148/// anything to compile.
149fn failure(message: String) -> Compiled {
150    Compiled { text: String::new(), messages: vec![format!("rucc: error: {message}")], errors: 1 }
151}
152
153#[cfg(test)]
154mod tests {
155    use rucc_session::{MemoryFileSystem, Std};
156    use rucc_target::Triple;
157
158    use super::*;
159
160    fn options() -> Options {
161        let mut opts = Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
162        opts.emit = EmitKind::Tast;
163        opts
164    }
165
166    fn run(opts: &Options, source: &str) -> Compiled {
167        let mut fs = MemoryFileSystem::new();
168        fs.insert("/main.c", source.to_owned().into_bytes());
169        compile(opts, "/main.c", &fs)
170    }
171
172    /// The typed tree of `source`, insisting that it compiled cleanly.
173    fn tast(source: &str) -> String {
174        let result = run(&options(), source);
175        assert_eq!(result.messages, Vec::<String>::new(), "expected this to compile:\n{source}");
176        result.text
177    }
178
179    #[test]
180    fn a_file_that_is_not_there_says_so_and_produces_nothing() {
181        let fs = MemoryFileSystem::new();
182        let result = compile(&options(), "/nope.c", &fs);
183        assert!(result.failed());
184        assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
185        assert!(result.text.is_empty());
186    }
187
188    #[test]
189    fn an_object_comes_out_with_its_type_its_linkage_and_how_much_of_a_definition_it_is() {
190        let text = tast("int x = 1;\n");
191        let expected = "\
192decl #0 x : int object external static defined
193  init
194    +0
195      const 1 : int
196";
197        assert_eq!(text, expected);
198    }
199
200    #[test]
201    fn the_macros_are_expanded_before_anything_is_parsed() {
202        // The whole pipeline in one line. The bound came out of a macro, so it was expanded,
203        // converted from a preprocessing number to a constant of a type, parsed as an
204        // expression, and folded to the number the array type carries.
205        let text = tast("#define N 2\nint a[N];\n");
206        assert!(text.starts_with("decl #0 a : int [2] object external static tentative"), "{text}");
207    }
208
209    #[test]
210    fn every_conversion_the_language_performs_is_a_node_in_the_output() {
211        // The point of a typed tree. The source has one operator and the output has the
212        // widening that operator asked for, spelled out, so that nothing downstream has to
213        // work out the conversion rules a second time.
214        let text = tast("long f(int a, long b) { return a + b; }\n");
215        assert!(text.contains("convert arithmetic"), "{text}");
216    }
217
218    #[test]
219    fn a_mistake_in_each_phase_reaches_the_caller_and_writes_no_tree() {
220        for source in [
221            "#error stop\n",
222            "int f(void) { return 1 + ; }\n",
223            "int f(void) { return undeclared; }\n",
224        ] {
225            let result = run(&options(), source);
226            assert!(result.failed(), "expected this to fail:\n{source}");
227            assert!(result.text.is_empty(), "a file that did not compile wrote a tree:\n{source}");
228        }
229    }
230
231    #[test]
232    fn one_undeclared_name_is_one_message_and_not_one_per_use() {
233        // The poisoning rule from `spec/06-lexer-and-parser.md` section 6.8, seen from the
234        // outside. Three uses of a name that was never declared, and the operators over them
235        // say nothing at all.
236        let result = run(&options(), "int f(void) { return nope + nope * nope; }\n");
237        assert_eq!(result.errors, 1, "{:?}", result.messages);
238    }
239
240    #[test]
241    fn a_declaration_the_parser_skipped_does_not_become_an_undeclared_name_as_well() {
242        // The reason the checking is skipped after a failed parse. The parser gave up on the
243        // first line and there is no `x` in the tree, so a checker run over it would report
244        // every use of `x` below as undeclared, which is a second message about one mistake.
245        let result = run(&options(), "int x = ;\nint f(void) { return x; }\n");
246        assert_eq!(result.errors, 1, "{:?}", result.messages);
247    }
248
249    #[test]
250    fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
251        let source = "int f(void) { char c = 300; return c; }\n";
252        let plain = run(&options(), source);
253        assert_eq!(plain.errors, 0, "{:?}", plain.messages);
254        assert_eq!(plain.messages.len(), 1, "expected a warning about the narrowed constant");
255        assert!(!plain.text.is_empty(), "a warning is not a reason to write nothing");
256
257        let mut opts = options();
258        opts.warnings_are_errors = true;
259        let strict = run(&opts, source);
260        assert!(strict.failed());
261        assert!(strict.text.is_empty(), "and under -Werror it is a reason to write nothing");
262        for message in &strict.messages {
263            assert!(!message.contains("warning:"), "{message}");
264        }
265    }
266
267    #[test]
268    fn the_dialect_reaches_the_keywords_and_the_checking() {
269        // `typeof` is C23's and GNU's, so the same source is a declaration under one dialect
270        // and a mistake under the other, which is the keyword table being built per dialect.
271        let source = "typeof(1) x;\n";
272        let mut opts = options();
273        opts.std = Std::C23;
274        opts.gnu_extensions = false;
275        assert!(!run(&opts, source).failed(), "{:?}", run(&opts, source).messages);
276
277        opts.std = Std::C17;
278        assert!(run(&opts, source).failed());
279    }
280
281    #[test]
282    fn asking_for_a_later_kind_runs_the_same_front_end_and_writes_nothing_yet() {
283        let mut opts = options();
284        opts.emit = EmitKind::Ir;
285        let result = run(&opts, "int x = 1;\n");
286        assert!(!result.failed(), "{:?}", result.messages);
287        assert!(result.text.is_empty());
288        // And it still finds what the checking finds, so `--emit=ir` on a broken file is not a
289        // silent success.
290        assert!(run(&opts, "int f(void) { return undeclared; }\n").failed());
291    }
292}