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    ///
130    /// The paste test is asked only where the two tokens did not arrive together. Two tokens
131    /// the user wrote next to each other read back as themselves by construction, because they
132    /// came out of the lexer that way, so `[52-2*sizeof(x)]` in a header prints as it was
133    /// written. It is a macro that can put two tokens next to each other that were never next
134    /// to each other, and that is where the question is worth asking. GCC arrives at the same
135    /// place from the other end: it inserts padding around each expansion and consults
136    /// `cpp_avoid_paste` only where one sits.
137    ///
138    /// "Arrived together" is the trace rather than the outermost invocation, because the
139    /// outermost is the same for every token of a nest and the boundaries inside it are real.
140    /// lz4 writes `#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2)` over a `LZ4_MEMORY_USAGE` of 14,
141    /// and the `14` and the `-` are two steps apart, so gcc prints `(14 -2)` and so does this.
142    fn space_before(&self, tok: Tok, text: &str, previous: Option<Tok>) -> bool {
143        if tok.flags.has(TokenFlags::LEADING_SPACE) {
144            return true;
145        }
146        match previous {
147            Some(prev) if self.printed && prev.trace != tok.trace => {
148                avoid_paste(prev, spelling(prev, self.interner), tok, text)
149            }
150            _ => false,
151        }
152    }
153
154    /// Moves the output to a file and a line, printing whatever that takes.
155    fn move_to(&mut self, file: FileId, line: u32, column: u32) {
156        if file == self.file && line == self.line && self.printed {
157            return;
158        }
159        self.end_line();
160        if file != self.file {
161            self.marker(file, line);
162        } else if line > self.line && line - self.line <= MAX_BLANKS {
163            // Close enough to walk to. Under `-P` the walk is skipped and the lines simply
164            // follow each other, which is what makes `-P` output compact.
165            if self.opts.line_markers {
166                for _ in self.line..line {
167                    self.out.push('\n');
168                }
169            }
170            self.line = line;
171        } else if line != self.line {
172            // Too far to walk, or backwards, which happens when a macro invocation spans lines
173            // and the tokens after it are reported at the line it started on.
174            self.jump(file, line);
175        }
176        self.indent(column);
177    }
178
179    /// Ends the current output line, if anything is on it.
180    fn end_line(&mut self) {
181        if self.printed {
182            self.out.push('\n');
183            self.line += 1;
184            self.printed = false;
185        }
186    }
187
188    /// A marker that says the output has changed file.
189    fn marker(&mut self, file: FileId, line: u32) {
190        // Entering or returning is decided by whether the file is already on the stack. A file
191        // that is not is one the output has not been in, which is an entry however it was
192        // reached.
193        let flag = match self.stack.iter().position(|&f| f == file) {
194            Some(at) => {
195                self.stack.truncate(at + 1);
196                2
197            }
198            None => {
199                self.stack.push(file);
200                1
201            }
202        };
203        if self.opts.line_markers {
204            let name = quoted(&self.sources.file(file).name);
205            self.out.push_str(&format!("# {line} {name} {flag}\n"));
206        }
207        self.file = file;
208        self.line = line;
209    }
210
211    /// A marker that says the output has moved within the same file.
212    fn jump(&mut self, file: FileId, line: u32) {
213        if self.opts.line_markers {
214            let name = quoted(&self.sources.file(file).name);
215            self.out.push_str(&format!("# {line} {name}\n"));
216        }
217        self.line = line;
218    }
219
220    /// Indents the first token of a line to the column it was written at.
221    ///
222    /// One space short of the column, because the token's own leading space flag supplies the
223    /// last one. GCC does exactly this, and the reason to copy it rather than to print the
224    /// tokens flush left is that indentation is most of what makes preprocessed output
225    /// readable when something has gone wrong in it.
226    fn indent(&mut self, column: u32) {
227        if self.printed {
228            return;
229        }
230        for _ in 2..column {
231            self.out.push(' ');
232        }
233    }
234
235    /// The finished text, which always ends in a newline.
236    fn finish(mut self) -> String {
237        if self.printed {
238            self.out.push('\n');
239        }
240        self.out
241    }
242}
243
244/// Whether writing these two tokens next to each other would change what they say.
245///
246/// This is GCC's `cpp_avoid_paste` with the same answers, written over spellings rather than
247/// over token codes. The word case is deliberately wider than GCC's: an identifier followed by
248/// a number gets a space here, because `x` and `1` written together are the single identifier
249/// `x1`, and output that does not read back as itself is not output.
250fn avoid_paste(prev: Tok, prev_text: &str, next: Tok, next_text: &str) -> bool {
251    let Some(first) = next_text.chars().next() else {
252        return false;
253    };
254    // Anything that ends in a word character followed by anything that starts as one. This
255    // covers name and name, name and number, number and number, and the prefixed forms of a
256    // character constant and a string literal, which are a name followed by a quote.
257    let word = matches!(prev.kind, PpTokenKind::Ident | PpTokenKind::Number | PpTokenKind::Other);
258    if word {
259        let joins = matches!(
260            next.kind,
261            PpTokenKind::Ident
262                | PpTokenKind::Number
263                | PpTokenKind::CharConst
264                | PpTokenKind::StringLit
265        );
266        if joins {
267            return true;
268        }
269        // A pp-number swallows a following sign after an exponent, and a `.` either side of
270        // one is part of the number rather than a separate token.
271        if prev.kind == PpTokenKind::Number {
272            return matches!(first, '.' | '+' | '-');
273        }
274        return false;
275    }
276
277    // An `=` glues onto every operator that has a compound assignment form, and onto the
278    // comparisons, which is most of them, so it is asked first.
279    if first == '=' {
280        return matches!(
281            prev_text,
282            "=" | "!" | "<" | ">" | "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>"
283        );
284    }
285    match prev_text {
286        ">" => first == '>',
287        "<" => matches!(first, '<' | '%' | ':'),
288        "+" => first == '+',
289        "-" => matches!(first, '-' | '>'),
290        // Not an operator that pastes: `/` and `*` written together open a comment, and `//`
291        // swallows the rest of the line.
292        "/" => matches!(first, '/' | '*'),
293        "%" => matches!(first, ':' | '%' | '>'),
294        "&" => first == '&',
295        "|" => first == '|',
296        ":" => matches!(first, ':' | '>'),
297        "." => first == '.' || next.kind == PpTokenKind::Number,
298        "#" => matches!(first, '#' | '%'),
299        _ => false,
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use rucc_diag::SourceMap;
306    use rucc_session::{MemoryFileSystem, SearchPath};
307
308    use super::*;
309    use crate::directive::Preprocessor;
310    use crate::include::Context;
311
312    /// A translation unit through phase 4 and back out as text.
313    struct Run {
314        interner: Interner,
315        sources: SourceMap,
316        fs: MemoryFileSystem,
317        search: SearchPath,
318        pp: Preprocessor,
319    }
320
321    impl Run {
322        fn new() -> Run {
323            Run {
324                interner: Interner::new(),
325                sources: SourceMap::new(),
326                fs: MemoryFileSystem::new(),
327                search: SearchPath::new(),
328                pp: Preprocessor::new(),
329            }
330        }
331
332        fn file(&mut self, path: &str, contents: &str) {
333            self.fs.insert(path, contents.as_bytes().to_vec());
334        }
335
336        fn go(&mut self, src: &str) -> String {
337            self.print(src, PrintOptions::new())
338        }
339
340        fn print(&mut self, src: &str, opts: PrintOptions) -> String {
341            let main =
342                self.sources.add("/main.c", src.as_bytes().to_vec()).expect("the map has room");
343            let out = {
344                let mut cx =
345                    Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
346                self.pp.run(main, &mut cx)
347            };
348            assert!(self.pp.diagnostics().is_empty(), "{:?}", self.pp.diagnostics());
349            print(main, &out, &self.sources, &self.interner, opts)
350        }
351    }
352
353    #[test]
354    fn the_first_line_says_which_file_this_is() {
355        let mut run = Run::new();
356        assert_eq!(run.go("int x;\n"), "# 1 \"/main.c\"\nint x;\n");
357    }
358
359    #[test]
360    fn a_line_the_preprocessor_ate_comes_back_as_a_blank_one() {
361        let mut run = Run::new();
362        // The definition produced no tokens, so line 2 is blank and `x` is still on line 3.
363        // Keeping it there is what lets a diagnostic from a later phase name the right line.
364        assert_eq!(run.go("#define N 1\nint x;\n"), "# 1 \"/main.c\"\n\nint x;\n");
365    }
366
367    #[test]
368    fn a_long_gap_is_a_marker_rather_than_a_page_of_blank_lines() {
369        let mut run = Run::new();
370        let src = format!("a;{}b;\n", "\n".repeat(20));
371        let text = run.go(&src);
372        assert!(text.contains("# 21 \"/main.c\"\nb;\n"), "{text}");
373        assert!(!text.contains("\n\n\n"), "a gap that big is a marker, not blank lines: {text}");
374    }
375
376    #[test]
377    fn entering_and_leaving_a_header_are_both_marked() {
378        let mut run = Run::new();
379        run.file("/one.h", "int from_the_header;\n");
380        let text = run.go("#include \"one.h\"\nint after;\n");
381        assert_eq!(
382            text,
383            "# 1 \"/main.c\"\n\
384             # 1 \"/one.h\" 1\n\
385             int from_the_header;\n\
386             # 2 \"/main.c\" 2\n\
387             int after;\n"
388        );
389    }
390
391    #[test]
392    fn dash_p_prints_the_tokens_and_nothing_else() {
393        let mut run = Run::new();
394        run.file("/one.h", "int from_the_header;\n");
395        let src = "#include \"one.h\"\n\n\n\nint after;\n";
396        let text = run.print(src, PrintOptions { line_markers: false });
397        assert_eq!(text, "int from_the_header;\nint after;\n");
398    }
399
400    #[test]
401    fn indentation_survives() {
402        let mut run = Run::new();
403        assert_eq!(run.go("    int x;\n"), "# 1 \"/main.c\"\n    int x;\n");
404    }
405
406    #[test]
407    fn a_space_goes_in_where_the_tokens_would_otherwise_paste() {
408        let mut run = Run::new();
409        // `+ +` rather than `++`, and `- -` rather than `--`, because those are different
410        // operators and the output has to say what the input said.
411        let src = "#define P +\n#define M -\nP+x;\nM-x;\n";
412        assert_eq!(run.go(src), "# 1 \"/main.c\"\n\n\n+ +x;\n- -x;\n");
413    }
414
415    #[test]
416    fn a_name_and_a_number_do_not_run_together() {
417        let mut run = Run::new();
418        // `x1` would read back as one identifier, so the space is not optional.
419        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");
420    }
421
422    /// The paste test is for tokens a macro put next to each other. Two the user wrote next to
423    /// each other came out of the lexer that way and read back as themselves, so nothing is
424    /// inserted between them: the kernel's `sound/asound.h` writes an array bound as
425    /// `[52-2*sizeof(x)]` and GCC prints it back unchanged.
426    #[test]
427    fn a_paste_is_only_avoided_where_a_macro_put_the_tokens_together() {
428        let mut run = Run::new();
429        assert_eq!(
430            run.go("char a[52-2*sizeof(int)];\n"),
431            "# 1 \"/main.c\"\nchar a[52-2*sizeof(int)];\n"
432        );
433
434        // The number comes out of `N` and the sign does not, so they did not arrive together
435        // and `52-2` would read back as a different pp-number than the two tokens it is.
436        let mut run = Run::new();
437        assert_eq!(run.go("#define N 52\nN-2;\n"), "# 1 \"/main.c\"\n\n52 -2;\n");
438
439        // Both out of the same expansion, so the body's own spacing is what is printed.
440        let mut run = Run::new();
441        assert_eq!(run.go("#define S 41+1\nS;\n"), "# 1 \"/main.c\"\n\n41+1;\n");
442
443        // A nest, which is lz4's `#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2)` cut down. The two
444        // tokens share an outermost invocation and are still a step apart, and gcc prints the
445        // space, so the question is asked of the trace rather than of the outermost.
446        let mut run = Run::new();
447        assert_eq!(
448            run.go("#define A 14\n#define B (A-2)\nint t[1 << B];\n"),
449            "# 1 \"/main.c\"\n\n\nint t[1 << (14 -2)];\n"
450        );
451    }
452
453    #[test]
454    fn a_slash_and_a_star_do_not_open_a_comment() {
455        let mut run = Run::new();
456        assert_eq!(run.go("#define D /\nD*p;\n"), "# 1 \"/main.c\"\n\n/ *p;\n");
457    }
458
459    #[test]
460    fn a_run_of_spaces_is_one_space_and_the_indent_is_the_real_one() {
461        let mut run = Run::new();
462        // GCC collapses whitespace between tokens to one space and rebuilds the indentation
463        // from the column, so a line that was indented by two still is.
464        assert_eq!(run.go("  int   x = a+b;\n"), "# 1 \"/main.c\"\n  int x = a+b;\n");
465    }
466
467    #[test]
468    fn a_macro_that_spans_lines_leaves_the_output_where_the_call_was() {
469        let mut run = Run::new();
470        let text = run.go("#define ADD(a, b) a + b\nADD(1,\n    2)\nlast;\n");
471        assert_eq!(text, "# 1 \"/main.c\"\n\n1 + 2\n\nlast;\n");
472    }
473
474    #[test]
475    fn a_macro_that_expands_to_nothing_leaves_its_space_behind() {
476        let mut run = Run::new();
477        // GCC and clang both print `int a ;` here, and the space is not decoration. The glibc
478        // headers hang `__THROW` and its relatives off the end of several hundred prototypes
479        // per file, and on a dialect where those expand to nothing this one space is the whole
480        // difference between agreeing with the reference compiler and not.
481        let text = run.print("#define E\nint a E;\n", PrintOptions { line_markers: false });
482        assert_eq!(text, "int a ;\n");
483    }
484
485    #[test]
486    fn the_space_is_only_left_where_there_was_one() {
487        let mut run = Run::new();
488        // No space before the macro means no space after it. `a1(E);` is `a1();` and not
489        // `a1( );`, which is the case that stops this rule from turning into "always insert".
490        let text = run.print("#define E\na1(E);\n", PrintOptions { line_markers: false });
491        assert_eq!(text, "a1();\n");
492    }
493
494    #[test]
495    fn a_space_owed_by_one_empty_macro_is_not_paid_twice() {
496        let mut run = Run::new();
497        // Three vanishing macros in a row owe one space between them, not three. The debt is
498        // handed along until a token that survives takes it.
499        let text = run.print("#define E\nd1 E E E d2;\n", PrintOptions { line_markers: false });
500        assert_eq!(text, "d1 d2;\n");
501    }
502
503    #[test]
504    fn the_space_crosses_out_of_the_expansion_that_owed_it() {
505        let mut run = Run::new();
506        // `J(4)` expands to `4 E`, and the `E` vanishes at the end of the replacement list. The
507        // token that takes the space is the `;` from the source, which the expansion never saw.
508        let text = run
509            .print("#define E\n#define J(x) x E\np6 J(4);\n", PrintOptions { line_markers: false });
510        assert_eq!(text, "p6 4 ;\n");
511    }
512
513    #[test]
514    fn a_function_like_macro_with_an_empty_body_leaves_a_space_too() {
515        let mut run = Run::new();
516        // The rule is about the invocation vanishing, not about which kind of macro it was.
517        let text = run
518            .print("#define F(x)\nint d(int F(9), int);\n", PrintOptions { line_markers: false });
519        assert_eq!(text, "int d(int , int);\n");
520    }
521}