Skip to main content

rucc_driver/
preprocess.rs

1//! Running phase 4 and writing what came out, which is what `-E` asks for.
2//!
3//! Design: `spec/04-driver-and-cli.md` sections 4.3 and 4.4, and `spec/05-preprocessor.md`.
4//!
5//! This is the first phase the driver actually runs, so it is also where the file system
6//! implementation lives. Everything below the driver reads through the [`FileSystem`] trait,
7//! and [`OsFileSystem`] is the one implementation of it that talks to the disk. Keeping it
8//! here rather than in `rucc-session` is what keeps the layer rule true: a preprocessor test
9//! is a map from path to bytes and cannot accidentally read the machine it runs on.
10
11use std::io;
12use std::path::{Path, PathBuf};
13
14use rucc_diag::{Diagnostic, Severity, SourceBytes, SourceMap};
15use rucc_pp::{Context, Dependency, Predef, Preprocessor, PrintOptions};
16use rucc_session::{FileSystem, Options, Session};
17
18/// The file system the compiler reads through when it is a compiler rather than a library.
19#[derive(Debug, Clone, Copy, Default)]
20pub struct OsFileSystem;
21
22impl OsFileSystem {
23    /// The one value of this type.
24    #[must_use]
25    pub fn new() -> OsFileSystem {
26        OsFileSystem
27    }
28}
29
30impl FileSystem for OsFileSystem {
31    fn read(&self, path: &Path) -> io::Result<SourceBytes> {
32        // Bytes rather than a string. A source file that is not valid UTF-8 is a file this
33        // compiler still has to have an opinion about, and phase 1 is where that opinion
34        // belongs, not here. Whether the bytes are a mapping or a buffer is `map`'s decision
35        // and is invisible from here.
36        crate::map::read(path)
37    }
38
39    fn identity(&self, path: &Path) -> PathBuf {
40        // `canonicalize` is the portable spelling of what GCC does with the device and inode
41        // pair: it resolves the relative part, the `..` and the symlinks, so that every route
42        // to one header gives one answer. It fails only if the file is not there, and this is
43        // asked about files that have just been read, so the fallback is for a file that was
44        // deleted between the two calls and it does not matter what it says.
45        std::fs::canonicalize(path).unwrap_or_else(|_| rucc_session::path_key(path))
46    }
47}
48
49/// What preprocessing one file produced.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Preprocessed {
52    /// The text to write, empty when the file could not be read.
53    pub text: String,
54    /// The diagnostics, already rendered, one per line, in the order they were reported.
55    pub messages: Vec<String>,
56    /// How many of them were errors.
57    pub errors: u32,
58    /// Every file an `#include` found, for the `-M` family.
59    ///
60    /// Collected whatever the command line asked for, because the cost of it is one path per
61    /// header and a field that is only filled in under a flag is a field that is wrong the first
62    /// time somebody reads it under a different one.
63    pub deps: Vec<Dependency>,
64}
65
66impl Preprocessed {
67    /// Whether anything went wrong badly enough that the output should not be used.
68    #[must_use]
69    pub fn failed(&self) -> bool {
70        self.errors > 0
71    }
72}
73
74/// Preprocesses one file and renders the result.
75///
76/// `name` is the path as the user wrote it, which is the name the output and every diagnostic
77/// about the file use. It is not canonicalised, because a message naming a path nobody typed
78/// is a message that is harder to act on.
79#[must_use]
80pub fn preprocess(opts: &Options, name: &str, fs: &dyn FileSystem) -> Preprocessed {
81    let mut sess = Session::new(opts.clone());
82    let bytes = match fs.read(Path::new(name)) {
83        Ok(bytes) => bytes,
84        Err(e) => return failure(format!("{name}: {e}")),
85    };
86    let Ok(file) = sess.sources.add_shared(crate::phase::source_name(name), bytes, None) else {
87        return failure(format!("{name}: the source map has no room left for this file"));
88    };
89
90    let mut pp = Preprocessor::with_prefix_map(opts.prefix_map.macros.clone());
91    let predef = Predef::for_options(opts);
92    let mut cx = Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
93    cx.lex = rucc_lex::Options::for_dialect(opts.std, opts.gnu_extensions);
94    cx.pedantic = opts.pedantic;
95    if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
96        return failure(format!("{name}: the source map has no room left for the built in macros"));
97    }
98    let mut tokens = Vec::new();
99    if pp.preinclude(&opts.preincludes, &mut tokens, &mut cx).is_err() {
100        return failure(format!("{name}: the source map has no room left for the command line"));
101    }
102    tokens.append(&mut pp.run(file, &mut cx));
103    // `-dM` replaces the output rather than adding to it. The run still happens, and it has
104    // to: the table at the end is the one the file left behind, so a `#define` inside an
105    // `#ifdef` that was false is correctly absent.
106    let text = if opts.dumps.macros {
107        rucc_pp::dump_macros(pp.macros(), &sess.interner)
108    } else {
109        rucc_pp::print(
110            file,
111            &tokens,
112            pp.line_directives(),
113            &sess.sources,
114            &sess.interner,
115            PrintOptions { line_markers: opts.line_markers },
116        )
117    };
118
119    let mut messages = Vec::new();
120    let mut errors = 0;
121    for diag in pp.take_diagnostics() {
122        // `-w` and `-Wsystem-headers`, for the reason they are read here in the compiler proper.
123        if rucc_diag::dropped(&diag, &sess.sources, opts.warnings, opts.system_header_warnings) {
124            continue;
125        }
126        let fatal = diag.severity.is_fatal()
127            || (diag.severity == Severity::Warning && opts.warnings_are_errors);
128        if fatal {
129            errors += 1;
130        }
131        messages.push(render(&diag, &sess.sources, opts.warnings_are_errors));
132    }
133    Preprocessed { text, messages, errors, deps: pp.dependencies().to_vec() }
134}
135
136/// A result that is nothing but one message, for the failures that happen before there is
137/// anything to preprocess.
138fn failure(message: String) -> Preprocessed {
139    Preprocessed {
140        text: String::new(),
141        messages: vec![format!("rucc: error: {message}")],
142        errors: 1,
143        deps: Vec::new(),
144    }
145}
146
147/// One diagnostic as the lines it prints.
148///
149/// GCC's shape: the position, the severity, the message, then the code, then any notes
150/// underneath. The chain of includes that reached the file comes first, because a diagnostic
151/// in a header three levels down is unactionable without the path that got there.
152pub(crate) fn render(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
153    let mut out = String::new();
154    let mut chain = sources.include_stack(diag.span.lo);
155    chain.reverse();
156    for (at, from) in chain.iter().enumerate() {
157        let lead = if at == 0 { "In file included from" } else { "                 from" };
158        out.push_str(&format!("{lead} {}:\n", sources.render_position(from.lo)));
159    }
160    out.push_str(&line(diag, sources, warnings_are_errors));
161    for child in &diag.children {
162        out.push('\n');
163        out.push_str(&line(child, sources, false));
164    }
165    out
166}
167
168/// The one line a diagnostic or one of its notes prints.
169fn line(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
170    let severity = if diag.severity == Severity::Warning && warnings_are_errors {
171        // Not a relabelling for its own sake. A build that turned warnings into errors and
172        // then reads "warning" next to a failed compilation has to go looking for why it
173        // failed, and the answer is right here.
174        "error"
175    } else {
176        diag.severity.as_str()
177    };
178    let position = if diag.span.is_dummy() {
179        "rucc".to_owned()
180    } else {
181        sources.render_position(diag.span.lo)
182    };
183    match diag.code {
184        Some(code) => format!("{position}: {severity}: {} [{code}]", diag.message),
185        None => format!("{position}: {severity}: {}", diag.message),
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use rucc_session::MemoryFileSystem;
192    use rucc_target::Triple;
193
194    use super::*;
195
196    fn options() -> Options {
197        Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap())
198    }
199
200    /// The path an include search produces for `name` under `dir`.
201    ///
202    /// The search joins the two with the platform's separator, so an expectation with a slash
203    /// written into it is an expectation about Unix rather than about the preprocessor, and it
204    /// fails on Windows for a reason that has nothing to do with what the test is checking.
205    fn at(dir: &str, name: &str) -> String {
206        Path::new(dir).join(name).display().to_string()
207    }
208
209    fn run(opts: &Options, files: &[(&str, &str)]) -> Preprocessed {
210        let mut fs = MemoryFileSystem::new();
211        for (path, text) in files {
212            fs.insert(*path, (*text).to_owned().into_bytes());
213        }
214        preprocess(opts, files[0].0, &fs)
215    }
216
217    #[test]
218    fn a_file_that_is_not_there_says_so_and_produces_nothing() {
219        let fs = MemoryFileSystem::new();
220        let result = preprocess(&options(), "/nope.c", &fs);
221        assert!(result.failed());
222        assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
223        assert!(result.text.is_empty());
224    }
225
226    #[test]
227    fn a_warning_about_a_header_that_came_with_the_machine_is_not_printed() {
228        // micropython is what found this. It defines `_DIRENT_HAVE_D_TYPE` in its own
229        // configuration header and glibc defines it again in `bits/dirent.h`, gcc says nothing
230        // because the second one is in a system header, and the build runs with warnings as
231        // errors, so without this rule it stops on a line nobody in the project wrote.
232        let mut opts = options();
233        opts.search.push_system("/usr/include");
234        opts.search.push_bracket("/project");
235        let files = [
236            ("/main.c", "#define N 1\n#include <sys.h>\n#include \"own.h\"\n"),
237            ("/usr/include/sys.h", "#define N 2\n#include <inner.h>\n"),
238            ("/usr/include/inner.h", "#define N 3\n"),
239            ("/project/own.h", "#define N 4\n"),
240        ];
241
242        // One message, about the project's own header. The system one and the header it pulled in
243        // are both quiet, and the second of those is the propagation: `inner.h` was found in the
244        // same directory, but a header a system header includes is a system header whichever
245        // directory answers for it.
246        let result = run(&opts, &files);
247        assert_eq!(result.messages.len(), 1, "{:?}", result.messages);
248        assert!(result.messages[0].contains("own.h"), "{:?}", result.messages);
249        assert_eq!(result.errors, 0);
250
251        // `-Wsystem-headers` is how somebody porting a header hears all of it.
252        opts.system_header_warnings = true;
253        let result = run(&opts, &files);
254        assert_eq!(result.messages.len(), 3, "{:?}", result.messages);
255
256        // `-w` beats it, because a warning that was never raised cannot be asked for back.
257        opts.warnings = false;
258        let result = run(&opts, &files);
259        assert_eq!(result.messages, Vec::<String>::new());
260    }
261
262    #[test]
263    fn the_output_is_the_expanded_text_with_a_line_marker_on_top() {
264        let result = run(&options(), &[("/main.c", "#define N 2\nint a[N];\n")]);
265        assert_eq!(result.messages, Vec::<String>::new());
266        assert_eq!(result.text, "# 1 \"/main.c\"\n\nint a[2];\n");
267    }
268
269    #[test]
270    fn the_predefined_macros_are_there_without_being_asked_for() {
271        let result = run(&options(), &[("/main.c", "__SIZEOF_LONG__ __x86_64__\n")]);
272        assert_eq!(result.text, "# 1 \"/main.c\"\n8 1\n");
273    }
274
275    #[test]
276    fn dash_d_and_dash_u_reach_the_macro_table() {
277        let mut opts = options();
278        opts.defines.push("FOO=41+1".to_owned());
279        opts.defines.push("BAR".to_owned());
280        opts.undefines.push("__x86_64__".to_owned());
281        let result = run(&opts, &[("/main.c", "FOO BAR\n#ifdef __x86_64__\ngone\n#endif\n")]);
282        // `41+1` with no space in it, which is what it was written as on the command line and
283        // what GCC prints. The three tokens all came out of the one expansion of `FOO`, and a
284        // paste is only worth avoiding where a macro put two tokens together that the person
285        // did not write together.
286        assert_eq!(result.text, "# 1 \"/main.c\"\n41+1 1\n");
287    }
288
289    #[test]
290    fn dash_i_is_where_an_angled_include_looks() {
291        let mut opts = options();
292        opts.search.push_bracket("/inc");
293        let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
294        let result = run(&opts, &files);
295        // A line marker's file name is a string literal, so a separator that is a backslash
296        // comes out escaped, which is what GCC does and what a reader of the output has to be
297        // able to parse back.
298        let one = at("/inc", "one.h").replace('\\', "\\\\");
299        let expected = format!(
300            "# 1 \"/main.c\"\n# 1 \"{one}\" 1\nint in_it;\n# 2 \"/main.c\" 2\nint after;\n"
301        );
302        assert_eq!(result.text, expected);
303    }
304
305    #[test]
306    fn dash_p_leaves_the_markers_out() {
307        let mut opts = options();
308        opts.line_markers = false;
309        opts.search.push_bracket("/inc");
310        let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
311        assert_eq!(run(&opts, &files).text, "int in_it;\nint after;\n");
312    }
313
314    #[test]
315    fn a_diagnostic_says_where_it_is_and_carries_its_code() {
316        let result = run(&options(), &[("/main.c", "#error no\n")]);
317        assert_eq!(result.errors, 1);
318        assert!(
319            result.messages[0].starts_with("/main.c:1:8: error: no ["),
320            "{:?}",
321            result.messages
322        );
323    }
324
325    #[test]
326    fn a_diagnostic_in_a_header_prints_the_chain_that_reached_it() {
327        let mut opts = options();
328        opts.search.push_bracket("/inc");
329        let files = [
330            ("/main.c", "#include <one.h>\n"),
331            ("/inc/one.h", "#include <two.h>\n"),
332            ("/inc/two.h", "#error deep\n"),
333        ];
334        let result = run(&opts, &files);
335        let text = result.messages.join("\n");
336        assert!(text.starts_with("In file included from /main.c:1:1:\n"), "{text}");
337        assert!(
338            text.contains(&format!("                 from {}:1:1:\n", at("/inc", "one.h"))),
339            "{text}"
340        );
341        assert!(text.contains(&format!("{}:1:8: error: deep", at("/inc", "two.h"))), "{text}");
342    }
343
344    #[test]
345    fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
346        let source = "#warning careful\n";
347        let plain = run(&options(), &[("/main.c", source)]);
348        assert_eq!(plain.errors, 0);
349        assert!(plain.messages[0].contains("warning: careful"), "{:?}", plain.messages);
350
351        let mut opts = options();
352        opts.warnings_are_errors = true;
353        let strict = run(&opts, &[("/main.c", source)]);
354        assert_eq!(strict.errors, 1);
355        assert!(strict.messages[0].contains("error: careful"), "{:?}", strict.messages);
356    }
357
358    #[test]
359    fn dash_dm_prints_the_macros_the_file_left_behind_and_not_the_file() {
360        let source = "#define KEPT 1\n#define GONE 2\n#undef GONE\n#ifdef NEVER\n#define \
361                      HIDDEN 3\n#endif\nint x;\n";
362        let result = run(&options(), &[("/main.c", source)]);
363        assert_eq!(result.errors, 0);
364        assert!(result.text.contains("int x;"), "the output is the file without -dM");
365        assert!(!result.text.contains("#define"), "a directive line is not part of the output");
366
367        let mut opts = options();
368        opts.dumps.macros = true;
369        let dumped = run(&opts, &[("/main.c", source)]);
370        assert!(dumped.text.contains("#define KEPT 1\n"), "{}", dumped.text);
371        assert!(!dumped.text.contains("int x;"), "-dM replaces the output rather than adding");
372        // Undefined is gone, and a define the conditional skipped was never made. The dump is
373        // the table at the end of the run and not a list of the lines that were written.
374        assert!(!dumped.text.contains("GONE"), "{}", dumped.text);
375        assert!(!dumped.text.contains("HIDDEN"), "{}", dumped.text);
376        // The predefined set is in there too, because it is defined the same way everything
377        // else is, which is the whole reason this output can be diffed against GCC's.
378        assert!(dumped.text.contains("#define __x86_64__ 1\n"), "{}", dumped.text);
379    }
380
381    #[test]
382    fn the_dialect_reaches_the_predefined_set() {
383        let mut opts = options();
384        opts.std = rucc_session::Std::C99;
385        opts.gnu_extensions = false;
386        let result = run(&opts, &[("/main.c", "__STDC_VERSION__ __STRICT_ANSI__\n")]);
387        assert_eq!(result.text, "# 1 \"/main.c\"\n199901L 1\n");
388    }
389}