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}