Skip to main content

rucc_pp/
print.rs

1//! Printing the token stream back out, which is what `-E` writes.
2//!
3//! Design: `spec/05-preprocessor.md` section 5.6.
4//!
5//! Two rules decide everything here, and they pull against each other. The output has to be
6//! usable as input, so two tokens that would lex as one token when written next to each other
7//! get a space between them. And the output has to be diffable against GCC's, because that
8//! diff is the fastest way to find a preprocessor bug, so the line structure, the indentation
9//! and the line markers all follow GCC rather than being tidied up.
10//!
11//! The line marker format is GCC's: `# 42 "file.h" 1` where the number after the name is 1 for
12//! entering a file, 2 for returning to one, 3 for a system header and 4 for a header whose
13//! contents are implicitly `extern "C"`. A gap of up to eight lines is printed as blank lines
14//! rather than as a marker, which is what GCC does and what keeps the output readable.
15
16use rucc_base::Interner;
17use rucc_diag::{FileId, SourceMap};
18use rucc_lex::{PpTokenKind, TokenFlags};
19
20use crate::include::{quoted, spelling};
21use crate::token::Tok;
22
23/// How many blank lines are worth printing before a line marker is cheaper.
24///
25/// GCC's number. It is not tuned for anything, but matching it is the difference between an
26/// empty diff and a diff on every header boundary.
27const MAX_BLANKS: u32 = 8;
28
29/// What `-E` was asked for.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct PrintOptions {
32    /// Whether to write line markers, which `-P` turns off.
33    ///
34    /// With them off the blank line padding goes too, because the point of `-P` is output for
35    /// something other than a compiler to read.
36    pub line_markers: bool,
37}
38
39impl PrintOptions {
40    /// The default, which is what plain `-E` asks for.
41    pub fn new() -> PrintOptions {
42        PrintOptions { line_markers: true }
43    }
44}
45
46impl Default for PrintOptions {
47    fn default() -> PrintOptions {
48        PrintOptions::new()
49    }
50}
51
52/// Renders `tokens` the way `-E` prints them.
53///
54/// `main` is the file named on the command line, which is what the first line marker says even
55/// when the first token comes from a header.
56pub fn print(
57    main: FileId,
58    tokens: &[Tok],
59    sources: &SourceMap,
60    interner: &Interner,
61    opts: PrintOptions,
62) -> String {
63    let mut printer = Printer {
64        out: String::new(),
65        opts,
66        sources,
67        interner,
68        file: main,
69        line: 1,
70        printed: false,
71        stack: vec![main],
72    };
73    printer.start();
74    let mut previous: Option<Tok> = None;
75    for &tok in tokens {
76        printer.token(tok, previous);
77        previous = Some(tok);
78    }
79    printer.finish()
80}
81
82/// The state of the output: which file and line it is standing on.
83struct Printer<'a> {
84    out: String,
85    opts: PrintOptions,
86    sources: &'a SourceMap,
87    interner: &'a Interner,
88    /// The file the output is currently in.
89    file: FileId,
90    /// The line of that file the current output line stands for.
91    line: u32,
92    /// Whether anything has been written on the current output line.
93    printed: bool,
94    /// The include stack as the output has walked it, which is what decides whether a marker
95    /// says entering or returning. It is the output's own stack rather than the
96    /// preprocessor's, because by the time this runs the preprocessor's is long gone.
97    stack: Vec<FileId>,
98}
99
100impl Printer<'_> {
101    /// The marker that says which file the output starts in.
102    fn start(&mut self) {
103        if self.opts.line_markers {
104            self.out.push_str(&format!("# 1 {}\n", quoted(&self.sources.file(self.file).name)));
105        }
106    }
107
108    /// Writes one token, with whatever whitespace has to come before it.
109    fn token(&mut self, tok: Tok, previous: Option<Tok>) {
110        let at = tok.report_span().lo;
111        // A token the preprocessor made up rather than read has no position to move to, so it
112        // stays on whatever line the output is already on. `_Pragma` produces these.
113        if let Some(loc) = self.sources.lookup(at) {
114            self.move_to(loc.file, loc.line, loc.column);
115        }
116        let text = spelling(tok, self.interner);
117        if self.space_before(tok, text, previous) {
118            self.out.push(' ');
119        }
120        self.out.push_str(text);
121        self.printed = true;
122    }
123
124    /// Whether a space goes between the previous token and this one.
125    ///
126    /// A run of spaces in the input is one space here, which is what GCC does. The indentation
127    /// of a line is the exception and it is rebuilt from the column instead, so the space this
128    /// returns for the first token of a line is the last of the ones `indent` wrote.
129    fn space_before(&self, tok: Tok, text: &str, previous: Option<Tok>) -> bool {
130        if tok.flags.has(TokenFlags::LEADING_SPACE) {
131            return true;
132        }
133        match previous {
134            Some(prev) if self.printed => {
135                avoid_paste(prev, spelling(prev, self.interner), tok, text)
136            }
137            _ => false,
138        }
139    }
140
141    /// Moves the output to a file and a line, printing whatever that takes.
142    fn move_to(&mut self, file: FileId, line: u32, column: u32) {
143        if file == self.file && line == self.line && self.printed {
144            return;
145        }
146        self.end_line();
147        if file != self.file {
148            self.marker(file, line);
149        } else if line > self.line && line - self.line <= MAX_BLANKS {
150            // Close enough to walk to. Under `-P` the walk is skipped and the lines simply
151            // follow each other, which is what makes `-P` output compact.
152            if self.opts.line_markers {
153                for _ in self.line..line {
154                    self.out.push('\n');
155                }
156            }
157            self.line = line;
158        } else if line != self.line {
159            // Too far to walk, or backwards, which happens when a macro invocation spans lines
160            // and the tokens after it are reported at the line it started on.
161            self.jump(file, line);
162        }
163        self.indent(column);
164    }
165
166    /// Ends the current output line, if anything is on it.
167    fn end_line(&mut self) {
168        if self.printed {
169            self.out.push('\n');
170            self.line += 1;
171            self.printed = false;
172        }
173    }
174
175    /// A marker that says the output has changed file.
176    fn marker(&mut self, file: FileId, line: u32) {
177        // Entering or returning is decided by whether the file is already on the stack. A file
178        // that is not is one the output has not been in, which is an entry however it was
179        // reached.
180        let flag = match self.stack.iter().position(|&f| f == file) {
181            Some(at) => {
182                self.stack.truncate(at + 1);
183                2
184            }
185            None => {
186                self.stack.push(file);
187                1
188            }
189        };
190        if self.opts.line_markers {
191            let name = quoted(&self.sources.file(file).name);
192            self.out.push_str(&format!("# {line} {name} {flag}\n"));
193        }
194        self.file = file;
195        self.line = line;
196    }
197
198    /// A marker that says the output has moved within the same file.
199    fn jump(&mut self, file: FileId, line: u32) {
200        if self.opts.line_markers {
201            let name = quoted(&self.sources.file(file).name);
202            self.out.push_str(&format!("# {line} {name}\n"));
203        }
204        self.line = line;
205    }
206
207    /// Indents the first token of a line to the column it was written at.
208    ///
209    /// One space short of the column, because the token's own leading space flag supplies the
210    /// last one. GCC does exactly this, and the reason to copy it rather than to print the
211    /// tokens flush left is that indentation is most of what makes preprocessed output
212    /// readable when something has gone wrong in it.
213    fn indent(&mut self, column: u32) {
214        if self.printed {
215            return;
216        }
217        for _ in 2..column {
218            self.out.push(' ');
219        }
220    }
221
222    /// The finished text, which always ends in a newline.
223    fn finish(mut self) -> String {
224        if self.printed {
225            self.out.push('\n');
226        }
227        self.out
228    }
229}
230
231/// Whether writing these two tokens next to each other would change what they say.
232///
233/// This is GCC's `cpp_avoid_paste` with the same answers, written over spellings rather than
234/// over token codes. The word case is deliberately wider than GCC's: an identifier followed by
235/// a number gets a space here, because `x` and `1` written together are the single identifier
236/// `x1`, and output that does not read back as itself is not output.
237fn avoid_paste(prev: Tok, prev_text: &str, next: Tok, next_text: &str) -> bool {
238    let Some(first) = next_text.chars().next() else {
239        return false;
240    };
241    // Anything that ends in a word character followed by anything that starts as one. This
242    // covers name and name, name and number, number and number, and the prefixed forms of a
243    // character constant and a string literal, which are a name followed by a quote.
244    let word = matches!(prev.kind, PpTokenKind::Ident | PpTokenKind::Number | PpTokenKind::Other);
245    if word {
246        let joins = matches!(
247            next.kind,
248            PpTokenKind::Ident
249                | PpTokenKind::Number
250                | PpTokenKind::CharConst
251                | PpTokenKind::StringLit
252        );
253        if joins {
254            return true;
255        }
256        // A pp-number swallows a following sign after an exponent, and a `.` either side of
257        // one is part of the number rather than a separate token.
258        if prev.kind == PpTokenKind::Number {
259            return matches!(first, '.' | '+' | '-');
260        }
261        return false;
262    }
263
264    // An `=` glues onto every operator that has a compound assignment form, and onto the
265    // comparisons, which is most of them, so it is asked first.
266    if first == '=' {
267        return matches!(
268            prev_text,
269            "=" | "!" | "<" | ">" | "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>"
270        );
271    }
272    match prev_text {
273        ">" => first == '>',
274        "<" => matches!(first, '<' | '%' | ':'),
275        "+" => first == '+',
276        "-" => matches!(first, '-' | '>'),
277        // Not an operator that pastes: `/` and `*` written together open a comment, and `//`
278        // swallows the rest of the line.
279        "/" => matches!(first, '/' | '*'),
280        "%" => matches!(first, ':' | '%' | '>'),
281        "&" => first == '&',
282        "|" => first == '|',
283        ":" => matches!(first, ':' | '>'),
284        "." => first == '.' || next.kind == PpTokenKind::Number,
285        "#" => matches!(first, '#' | '%'),
286        _ => false,
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use rucc_diag::SourceMap;
293    use rucc_session::{MemoryFileSystem, SearchPath};
294
295    use super::*;
296    use crate::directive::Preprocessor;
297    use crate::include::Context;
298
299    /// A translation unit through phase 4 and back out as text.
300    struct Run {
301        interner: Interner,
302        sources: SourceMap,
303        fs: MemoryFileSystem,
304        search: SearchPath,
305        pp: Preprocessor,
306    }
307
308    impl Run {
309        fn new() -> Run {
310            Run {
311                interner: Interner::new(),
312                sources: SourceMap::new(),
313                fs: MemoryFileSystem::new(),
314                search: SearchPath::new(),
315                pp: Preprocessor::new(),
316            }
317        }
318
319        fn file(&mut self, path: &str, contents: &str) {
320            self.fs.insert(path, contents.as_bytes().to_vec());
321        }
322
323        fn go(&mut self, src: &str) -> String {
324            self.print(src, PrintOptions::new())
325        }
326
327        fn print(&mut self, src: &str, opts: PrintOptions) -> String {
328            let main =
329                self.sources.add("/main.c", src.as_bytes().to_vec()).expect("the map has room");
330            let out = {
331                let mut cx =
332                    Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
333                self.pp.run(main, &mut cx)
334            };
335            assert!(self.pp.diagnostics().is_empty(), "{:?}", self.pp.diagnostics());
336            print(main, &out, &self.sources, &self.interner, opts)
337        }
338    }
339
340    #[test]
341    fn the_first_line_says_which_file_this_is() {
342        let mut run = Run::new();
343        assert_eq!(run.go("int x;\n"), "# 1 \"/main.c\"\nint x;\n");
344    }
345
346    #[test]
347    fn a_line_the_preprocessor_ate_comes_back_as_a_blank_one() {
348        let mut run = Run::new();
349        // The definition produced no tokens, so line 2 is blank and `x` is still on line 3.
350        // Keeping it there is what lets a diagnostic from a later phase name the right line.
351        assert_eq!(run.go("#define N 1\nint x;\n"), "# 1 \"/main.c\"\n\nint x;\n");
352    }
353
354    #[test]
355    fn a_long_gap_is_a_marker_rather_than_a_page_of_blank_lines() {
356        let mut run = Run::new();
357        let src = format!("a;{}b;\n", "\n".repeat(20));
358        let text = run.go(&src);
359        assert!(text.contains("# 21 \"/main.c\"\nb;\n"), "{text}");
360        assert!(!text.contains("\n\n\n"), "a gap that big is a marker, not blank lines: {text}");
361    }
362
363    #[test]
364    fn entering_and_leaving_a_header_are_both_marked() {
365        let mut run = Run::new();
366        run.file("/one.h", "int from_the_header;\n");
367        let text = run.go("#include \"one.h\"\nint after;\n");
368        assert_eq!(
369            text,
370            "# 1 \"/main.c\"\n\
371             # 1 \"/one.h\" 1\n\
372             int from_the_header;\n\
373             # 2 \"/main.c\" 2\n\
374             int after;\n"
375        );
376    }
377
378    #[test]
379    fn dash_p_prints_the_tokens_and_nothing_else() {
380        let mut run = Run::new();
381        run.file("/one.h", "int from_the_header;\n");
382        let src = "#include \"one.h\"\n\n\n\nint after;\n";
383        let text = run.print(src, PrintOptions { line_markers: false });
384        assert_eq!(text, "int from_the_header;\nint after;\n");
385    }
386
387    #[test]
388    fn indentation_survives() {
389        let mut run = Run::new();
390        assert_eq!(run.go("    int x;\n"), "# 1 \"/main.c\"\n    int x;\n");
391    }
392
393    #[test]
394    fn a_space_goes_in_where_the_tokens_would_otherwise_paste() {
395        let mut run = Run::new();
396        // `+ +` rather than `++`, and `- -` rather than `--`, because those are different
397        // operators and the output has to say what the input said.
398        let src = "#define P +\n#define M -\nP+x;\nM-x;\n";
399        assert_eq!(run.go(src), "# 1 \"/main.c\"\n\n\n+ +x;\n- -x;\n");
400    }
401
402    #[test]
403    fn a_name_and_a_number_do_not_run_together() {
404        let mut run = Run::new();
405        // `x1` would read back as one identifier, so the space is not optional.
406        assert_eq!(run.go("#define J(a,b) a b\nJ(x,1)J(2,y)\n"), "# 1 \"/main.c\"\n\nx 1 2 y\n");
407    }
408
409    #[test]
410    fn a_slash_and_a_star_do_not_open_a_comment() {
411        let mut run = Run::new();
412        assert_eq!(run.go("#define D /\nD*p;\n"), "# 1 \"/main.c\"\n\n/ *p;\n");
413    }
414
415    #[test]
416    fn a_run_of_spaces_is_one_space_and_the_indent_is_the_real_one() {
417        let mut run = Run::new();
418        // GCC collapses whitespace between tokens to one space and rebuilds the indentation
419        // from the column, so a line that was indented by two still is.
420        assert_eq!(run.go("  int   x = a+b;\n"), "# 1 \"/main.c\"\n  int x = a+b;\n");
421    }
422
423    #[test]
424    fn a_macro_that_spans_lines_leaves_the_output_where_the_call_was() {
425        let mut run = Run::new();
426        let text = run.go("#define ADD(a, b) a + b\nADD(1,\n    2)\nlast;\n");
427        assert_eq!(text, "# 1 \"/main.c\"\n\n1 + 2\n\nlast;\n");
428    }
429
430    #[test]
431    fn a_macro_that_expands_to_nothing_leaves_its_space_behind() {
432        let mut run = Run::new();
433        // GCC and clang both print `int a ;` here, and the space is not decoration. The glibc
434        // headers hang `__THROW` and its relatives off the end of several hundred prototypes
435        // per file, and on a dialect where those expand to nothing this one space is the whole
436        // difference between agreeing with the reference compiler and not.
437        let text = run.print("#define E\nint a E;\n", PrintOptions { line_markers: false });
438        assert_eq!(text, "int a ;\n");
439    }
440
441    #[test]
442    fn the_space_is_only_left_where_there_was_one() {
443        let mut run = Run::new();
444        // No space before the macro means no space after it. `a1(E);` is `a1();` and not
445        // `a1( );`, which is the case that stops this rule from turning into "always insert".
446        let text = run.print("#define E\na1(E);\n", PrintOptions { line_markers: false });
447        assert_eq!(text, "a1();\n");
448    }
449
450    #[test]
451    fn a_space_owed_by_one_empty_macro_is_not_paid_twice() {
452        let mut run = Run::new();
453        // Three vanishing macros in a row owe one space between them, not three. The debt is
454        // handed along until a token that survives takes it.
455        let text = run.print("#define E\nd1 E E E d2;\n", PrintOptions { line_markers: false });
456        assert_eq!(text, "d1 d2;\n");
457    }
458
459    #[test]
460    fn the_space_crosses_out_of_the_expansion_that_owed_it() {
461        let mut run = Run::new();
462        // `J(4)` expands to `4 E`, and the `E` vanishes at the end of the replacement list. The
463        // token that takes the space is the `;` from the source, which the expansion never saw.
464        let text = run
465            .print("#define E\n#define J(x) x E\np6 J(4);\n", PrintOptions { line_markers: false });
466        assert_eq!(text, "p6 4 ;\n");
467    }
468
469    #[test]
470    fn a_function_like_macro_with_an_empty_body_leaves_a_space_too() {
471        let mut run = Run::new();
472        // The rule is about the invocation vanishing, not about which kind of macro it was.
473        let text = run
474            .print("#define F(x)\nint d(int F(9), int);\n", PrintOptions { line_markers: false });
475        assert_eq!(text, "int d(int , int);\n");
476    }
477}