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;
13
14use rucc_diag::{Diagnostic, Severity, SourceBytes, SourceMap};
15use rucc_pp::{Context, 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
40/// What preprocessing one file produced.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct Preprocessed {
43    /// The text to write, empty when the file could not be read.
44    pub text: String,
45    /// The diagnostics, already rendered, one per line, in the order they were reported.
46    pub messages: Vec<String>,
47    /// How many of them were errors.
48    pub errors: u32,
49}
50
51impl Preprocessed {
52    /// Whether anything went wrong badly enough that the output should not be used.
53    #[must_use]
54    pub fn failed(&self) -> bool {
55        self.errors > 0
56    }
57}
58
59/// Preprocesses one file and renders the result.
60///
61/// `name` is the path as the user wrote it, which is the name the output and every diagnostic
62/// about the file use. It is not canonicalised, because a message naming a path nobody typed
63/// is a message that is harder to act on.
64#[must_use]
65pub fn preprocess(opts: &Options, name: &str, fs: &dyn FileSystem) -> Preprocessed {
66    let mut sess = Session::new(opts.clone());
67    let bytes = match fs.read(Path::new(name)) {
68        Ok(bytes) => bytes,
69        Err(e) => return failure(format!("{name}: {e}")),
70    };
71    let Ok(file) = sess.sources.add_shared(name, bytes, None) else {
72        return failure(format!("{name}: the source map has no room left for this file"));
73    };
74
75    let mut pp = Preprocessor::new();
76    let predef = Predef::for_options(opts);
77    let mut cx = Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
78    if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
79        return failure(format!("{name}: the source map has no room left for the built in macros"));
80    }
81    let tokens = pp.run(file, &mut cx);
82    // `-dM` replaces the output rather than adding to it. The run still happens, and it has
83    // to: the table at the end is the one the file left behind, so a `#define` inside an
84    // `#ifdef` that was false is correctly absent.
85    let text = if opts.dumps.macros {
86        rucc_pp::dump_macros(pp.macros(), &sess.interner)
87    } else {
88        rucc_pp::print(
89            file,
90            &tokens,
91            &sess.sources,
92            &sess.interner,
93            PrintOptions { line_markers: opts.line_markers },
94        )
95    };
96
97    let mut messages = Vec::new();
98    let mut errors = 0;
99    for diag in pp.take_diagnostics() {
100        let fatal = diag.severity.is_fatal()
101            || (diag.severity == Severity::Warning && opts.warnings_are_errors);
102        if fatal {
103            errors += 1;
104        }
105        messages.push(render(&diag, &sess.sources, opts.warnings_are_errors));
106    }
107    Preprocessed { text, messages, errors }
108}
109
110/// A result that is nothing but one message, for the failures that happen before there is
111/// anything to preprocess.
112fn failure(message: String) -> Preprocessed {
113    Preprocessed {
114        text: String::new(),
115        messages: vec![format!("rucc: error: {message}")],
116        errors: 1,
117    }
118}
119
120/// One diagnostic as the lines it prints.
121///
122/// GCC's shape: the position, the severity, the message, then the code, then any notes
123/// underneath. The chain of includes that reached the file comes first, because a diagnostic
124/// in a header three levels down is unactionable without the path that got there.
125pub(crate) fn render(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
126    let mut out = String::new();
127    let mut chain = sources.include_stack(diag.span.lo);
128    chain.reverse();
129    for (at, from) in chain.iter().enumerate() {
130        let lead = if at == 0 { "In file included from" } else { "                 from" };
131        out.push_str(&format!("{lead} {}:\n", sources.render_position(from.lo)));
132    }
133    out.push_str(&line(diag, sources, warnings_are_errors));
134    for child in &diag.children {
135        out.push('\n');
136        out.push_str(&line(child, sources, false));
137    }
138    out
139}
140
141/// The one line a diagnostic or one of its notes prints.
142fn line(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
143    let severity = if diag.severity == Severity::Warning && warnings_are_errors {
144        // Not a relabelling for its own sake. A build that turned warnings into errors and
145        // then reads "warning" next to a failed compilation has to go looking for why it
146        // failed, and the answer is right here.
147        "error"
148    } else {
149        diag.severity.as_str()
150    };
151    let position = if diag.span.is_dummy() {
152        "rucc".to_owned()
153    } else {
154        sources.render_position(diag.span.lo)
155    };
156    match diag.code {
157        Some(code) => format!("{position}: {severity}: {} [{code}]", diag.message),
158        None => format!("{position}: {severity}: {}", diag.message),
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use rucc_session::MemoryFileSystem;
165    use rucc_target::Triple;
166
167    use super::*;
168
169    fn options() -> Options {
170        Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap())
171    }
172
173    /// The path an include search produces for `name` under `dir`.
174    ///
175    /// The search joins the two with the platform's separator, so an expectation with a slash
176    /// written into it is an expectation about Unix rather than about the preprocessor, and it
177    /// fails on Windows for a reason that has nothing to do with what the test is checking.
178    fn at(dir: &str, name: &str) -> String {
179        Path::new(dir).join(name).display().to_string()
180    }
181
182    fn run(opts: &Options, files: &[(&str, &str)]) -> Preprocessed {
183        let mut fs = MemoryFileSystem::new();
184        for (path, text) in files {
185            fs.insert(*path, (*text).to_owned().into_bytes());
186        }
187        preprocess(opts, files[0].0, &fs)
188    }
189
190    #[test]
191    fn a_file_that_is_not_there_says_so_and_produces_nothing() {
192        let fs = MemoryFileSystem::new();
193        let result = preprocess(&options(), "/nope.c", &fs);
194        assert!(result.failed());
195        assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
196        assert!(result.text.is_empty());
197    }
198
199    #[test]
200    fn the_output_is_the_expanded_text_with_a_line_marker_on_top() {
201        let result = run(&options(), &[("/main.c", "#define N 2\nint a[N];\n")]);
202        assert_eq!(result.messages, Vec::<String>::new());
203        assert_eq!(result.text, "# 1 \"/main.c\"\n\nint a[2];\n");
204    }
205
206    #[test]
207    fn the_predefined_macros_are_there_without_being_asked_for() {
208        let result = run(&options(), &[("/main.c", "__SIZEOF_LONG__ __x86_64__\n")]);
209        assert_eq!(result.text, "# 1 \"/main.c\"\n8 1\n");
210    }
211
212    #[test]
213    fn dash_d_and_dash_u_reach_the_macro_table() {
214        let mut opts = options();
215        opts.defines.push("FOO=41+1".to_owned());
216        opts.defines.push("BAR".to_owned());
217        opts.undefines.push("__x86_64__".to_owned());
218        let result = run(&opts, &[("/main.c", "FOO BAR\n#ifdef __x86_64__\ngone\n#endif\n")]);
219        // `41 +1` rather than `41+1`: GCC puts a space there because a preprocessing number
220        // absorbs a sign after an `e`, and this output has to read back as itself.
221        assert_eq!(result.text, "# 1 \"/main.c\"\n41 +1 1\n");
222    }
223
224    #[test]
225    fn dash_i_is_where_an_angled_include_looks() {
226        let mut opts = options();
227        opts.search.push_bracket("/inc");
228        let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
229        let result = run(&opts, &files);
230        // A line marker's file name is a string literal, so a separator that is a backslash
231        // comes out escaped, which is what GCC does and what a reader of the output has to be
232        // able to parse back.
233        let one = at("/inc", "one.h").replace('\\', "\\\\");
234        let expected = format!(
235            "# 1 \"/main.c\"\n# 1 \"{one}\" 1\nint in_it;\n# 2 \"/main.c\" 2\nint after;\n"
236        );
237        assert_eq!(result.text, expected);
238    }
239
240    #[test]
241    fn dash_p_leaves_the_markers_out() {
242        let mut opts = options();
243        opts.line_markers = false;
244        opts.search.push_bracket("/inc");
245        let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
246        assert_eq!(run(&opts, &files).text, "int in_it;\nint after;\n");
247    }
248
249    #[test]
250    fn a_diagnostic_says_where_it_is_and_carries_its_code() {
251        let result = run(&options(), &[("/main.c", "#error no\n")]);
252        assert_eq!(result.errors, 1);
253        assert!(
254            result.messages[0].starts_with("/main.c:1:8: error: no ["),
255            "{:?}",
256            result.messages
257        );
258    }
259
260    #[test]
261    fn a_diagnostic_in_a_header_prints_the_chain_that_reached_it() {
262        let mut opts = options();
263        opts.search.push_bracket("/inc");
264        let files = [
265            ("/main.c", "#include <one.h>\n"),
266            ("/inc/one.h", "#include <two.h>\n"),
267            ("/inc/two.h", "#error deep\n"),
268        ];
269        let result = run(&opts, &files);
270        let text = result.messages.join("\n");
271        assert!(text.starts_with("In file included from /main.c:1:1:\n"), "{text}");
272        assert!(
273            text.contains(&format!("                 from {}:1:1:\n", at("/inc", "one.h"))),
274            "{text}"
275        );
276        assert!(text.contains(&format!("{}:1:8: error: deep", at("/inc", "two.h"))), "{text}");
277    }
278
279    #[test]
280    fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
281        let source = "#warning careful\n";
282        let plain = run(&options(), &[("/main.c", source)]);
283        assert_eq!(plain.errors, 0);
284        assert!(plain.messages[0].contains("warning: careful"), "{:?}", plain.messages);
285
286        let mut opts = options();
287        opts.warnings_are_errors = true;
288        let strict = run(&opts, &[("/main.c", source)]);
289        assert_eq!(strict.errors, 1);
290        assert!(strict.messages[0].contains("error: careful"), "{:?}", strict.messages);
291    }
292
293    #[test]
294    fn dash_dm_prints_the_macros_the_file_left_behind_and_not_the_file() {
295        let source = "#define KEPT 1\n#define GONE 2\n#undef GONE\n#ifdef NEVER\n#define \
296                      HIDDEN 3\n#endif\nint x;\n";
297        let result = run(&options(), &[("/main.c", source)]);
298        assert_eq!(result.errors, 0);
299        assert!(result.text.contains("int x;"), "the output is the file without -dM");
300        assert!(!result.text.contains("#define"), "a directive line is not part of the output");
301
302        let mut opts = options();
303        opts.dumps.macros = true;
304        let dumped = run(&opts, &[("/main.c", source)]);
305        assert!(dumped.text.contains("#define KEPT 1\n"), "{}", dumped.text);
306        assert!(!dumped.text.contains("int x;"), "-dM replaces the output rather than adding");
307        // Undefined is gone, and a define the conditional skipped was never made. The dump is
308        // the table at the end of the run and not a list of the lines that were written.
309        assert!(!dumped.text.contains("GONE"), "{}", dumped.text);
310        assert!(!dumped.text.contains("HIDDEN"), "{}", dumped.text);
311        // The predefined set is in there too, because it is defined the same way everything
312        // else is, which is the whole reason this output can be diffed against GCC's.
313        assert!(dumped.text.contains("#define __x86_64__ 1\n"), "{}", dumped.text);
314    }
315
316    #[test]
317    fn the_dialect_reaches_the_predefined_set() {
318        let mut opts = options();
319        opts.std = rucc_session::Std::C99;
320        opts.gnu_extensions = false;
321        let result = run(&opts, &[("/main.c", "__STDC_VERSION__ __STRICT_ANSI__\n")]);
322        assert_eq!(result.text, "# 1 \"/main.c\"\n199901L 1\n");
323    }
324}