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::directive::LineDirective;
21use crate::include::{quoted, spelling};
22use crate::token::Tok;
23
24/// How many blank lines are worth printing before a line marker is cheaper.
25///
26/// GCC's number. It is not tuned for anything, but matching it is the difference between an
27/// empty diff and a diff on every header boundary.
28const MAX_BLANKS: u32 = 8;
29
30/// What `-E` was asked for.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct PrintOptions {
33 /// Whether to write line markers, which `-P` turns off.
34 ///
35 /// With them off the blank line padding goes too, because the point of `-P` is output for
36 /// something other than a compiler to read.
37 pub line_markers: bool,
38}
39
40impl PrintOptions {
41 /// The default, which is what plain `-E` asks for.
42 pub fn new() -> PrintOptions {
43 PrintOptions { line_markers: true }
44 }
45}
46
47impl Default for PrintOptions {
48 fn default() -> PrintOptions {
49 PrintOptions::new()
50 }
51}
52
53/// Renders `tokens` the way `-E` prints them.
54///
55/// `main` is the file named on the command line, which is what the first line marker says even
56/// when the first token comes from a header. `lines` is the `#line` directives the run read,
57/// in the order it read them, because each one is a marker in the output at the point it was
58/// written rather than at the point its effect is first visible.
59pub fn print(
60 main: FileId,
61 tokens: &[Tok],
62 lines: &[LineDirective],
63 sources: &SourceMap,
64 interner: &Interner,
65 opts: PrintOptions,
66) -> String {
67 let mut printer = Printer {
68 out: String::new(),
69 opts,
70 sources,
71 interner,
72 file: main,
73 name: sources.file(main).name.clone(),
74 line: 1,
75 printed: false,
76 stack: vec![main],
77 lines,
78 next: 0,
79 };
80 printer.start();
81 let mut previous: Option<Tok> = None;
82 for (at, &tok) in tokens.iter().enumerate() {
83 printer.line_directives(at);
84 printer.token(tok, previous);
85 previous = Some(tok);
86 }
87 printer.line_directives(tokens.len());
88 printer.finish()
89}
90
91/// The state of the output: which file and line it is standing on.
92struct Printer<'a> {
93 out: String,
94 opts: PrintOptions,
95 sources: &'a SourceMap,
96 interner: &'a Interner,
97 /// The file the output is currently in.
98 file: FileId,
99 /// The name that file is going under, which a `#line` can change without the output
100 /// leaving the file. It is held rather than looked up because it is what the next marker
101 /// is compared against, and the comparison is per token.
102 name: String,
103 /// The line of that file the current output line stands for, presented rather than real,
104 /// since a marker is what tells the next compiler along where it is.
105 line: u32,
106 /// Whether anything has been written on the current output line.
107 printed: bool,
108 /// The include stack as the output has walked it, which is what decides whether a marker
109 /// says entering or returning. It is the output's own stack rather than the
110 /// preprocessor's, because by the time this runs the preprocessor's is long gone.
111 stack: Vec<FileId>,
112 /// The `#line` directives, in the order they were read.
113 lines: &'a [LineDirective],
114 /// How many of them have been written out.
115 next: usize,
116}
117
118impl Printer<'_> {
119 /// Writes the marker for every `#line` that was read before the token at `at`.
120 ///
121 /// GCC prints one of these per directive, where the directive was written, and so does
122 /// this. Letting the effect show up on its own instead would put the same information in
123 /// the output in a different place: `#line 5` followed by three blank lines and a
124 /// statement comes out as a marker and three blank lines here, and as eight blank lines
125 /// if the printer only ever reacts to the line a token claims to be on.
126 fn line_directives(&mut self, at: usize) {
127 let (lines, sources) = (self.lines, self.sources);
128 while let Some(directive) = lines.get(self.next).filter(|d| d.at <= at) {
129 self.next += 1;
130 let Some(loc) = sources.presumed_after(directive.span.lo) else { continue };
131 self.end_line();
132 self.jump(loc.name, loc.line);
133 }
134 }
135
136 /// The marker that says which file the output starts in.
137 fn start(&mut self) {
138 if self.opts.line_markers {
139 self.out.push_str(&format!("# 1 {}\n", quoted(&self.name)));
140 }
141 }
142
143 /// Writes one token, with whatever whitespace has to come before it.
144 fn token(&mut self, tok: Tok, previous: Option<Tok>) {
145 let at = tok.report_span().lo;
146 // A token the preprocessor made up rather than read has no position to move to, so it
147 // stays on whatever line the output is already on. `_Pragma` produces these.
148 // Which file it is in is the real one, since that is what the include stack is kept
149 // in, and where it says it is is the presented one, since that is what a marker says.
150 // `sources` is copied out of `self` so that the borrow of the name outlives the call
151 // that needs `self` mutably. It is a shared reference the printer does not own.
152 let sources = self.sources;
153 if let Some(file) = sources.lookup_file(at) {
154 if let Some(loc) = sources.presumed(at) {
155 self.move_to(file, loc.name, loc.line, loc.column);
156 }
157 }
158 let text = spelling(tok, self.interner);
159 if self.space_before(tok, text, previous) {
160 self.out.push(' ');
161 }
162 self.out.push_str(text);
163 self.printed = true;
164 }
165
166 /// Whether a space goes between the previous token and this one.
167 ///
168 /// A run of spaces in the input is one space here, which is what GCC does. The indentation
169 /// of a line is the exception and it is rebuilt from the column instead, so the space this
170 /// returns for the first token of a line is the last of the ones `indent` wrote.
171 ///
172 /// The paste test is asked only where the two tokens did not arrive together. Two tokens
173 /// the user wrote next to each other read back as themselves by construction, because they
174 /// came out of the lexer that way, so `[52-2*sizeof(x)]` in a header prints as it was
175 /// written. It is a macro that can put two tokens next to each other that were never next
176 /// to each other, and that is where the question is worth asking. GCC arrives at the same
177 /// place from the other end: it inserts padding around each expansion and consults
178 /// `cpp_avoid_paste` only where one sits.
179 ///
180 /// "Arrived together" is the trace rather than the outermost invocation, because the
181 /// outermost is the same for every token of a nest and the boundaries inside it are real.
182 /// lz4 writes `#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2)` over a `LZ4_MEMORY_USAGE` of 14,
183 /// and the `14` and the `-` are two steps apart, so gcc prints `(14 -2)` and so does this.
184 fn space_before(&self, tok: Tok, text: &str, previous: Option<Tok>) -> bool {
185 if tok.flags.has(TokenFlags::LEADING_SPACE) {
186 return true;
187 }
188 match previous {
189 Some(prev) if self.printed && prev.trace != tok.trace => {
190 avoid_paste(prev, spelling(prev, self.interner), tok, text)
191 }
192 _ => false,
193 }
194 }
195
196 /// Moves the output to a file and a line, printing whatever that takes.
197 fn move_to(&mut self, file: FileId, name: &str, line: u32, column: u32) {
198 if file == self.file && line == self.line && self.printed && name == self.name {
199 return;
200 }
201 self.end_line();
202 if file != self.file {
203 self.marker(file, name, line);
204 } else if name != self.name {
205 // Same file, different name, which is a `#line` that renamed it. GCC prints that
206 // as a plain marker with no flag on it, the same as a jump within a file, because
207 // as far as the output is concerned that is what it is.
208 self.jump(name, line);
209 } else if line > self.line && line - self.line <= MAX_BLANKS {
210 // Close enough to walk to. Under `-P` the walk is skipped and the lines simply
211 // follow each other, which is what makes `-P` output compact.
212 if self.opts.line_markers {
213 for _ in self.line..line {
214 self.out.push('\n');
215 }
216 }
217 self.line = line;
218 } else if line != self.line {
219 // Too far to walk, or backwards, which happens when a macro invocation spans lines
220 // and the tokens after it are reported at the line it started on.
221 self.jump(name, line);
222 }
223 self.indent(column);
224 }
225
226 /// Ends the current output line, if anything is on it.
227 fn end_line(&mut self) {
228 if self.printed {
229 self.out.push('\n');
230 self.line += 1;
231 self.printed = false;
232 }
233 }
234
235 /// A marker that says the output has changed file.
236 fn marker(&mut self, file: FileId, name: &str, line: u32) {
237 // Entering or returning is decided by whether the file is already on the stack. A file
238 // that is not is one the output has not been in, which is an entry however it was
239 // reached.
240 let flag = match self.stack.iter().position(|&f| f == file) {
241 Some(at) => {
242 self.stack.truncate(at + 1);
243 2
244 }
245 None => {
246 self.stack.push(file);
247 1
248 }
249 };
250 if self.opts.line_markers {
251 self.out.push_str(&format!("# {line} {} {flag}\n", quoted(name)));
252 }
253 self.file = file;
254 self.set_name(name);
255 self.line = line;
256 }
257
258 /// A marker that says the output has moved within the same file.
259 fn jump(&mut self, name: &str, line: u32) {
260 if self.opts.line_markers {
261 self.out.push_str(&format!("# {line} {}\n", quoted(name)));
262 }
263 self.set_name(name);
264 self.line = line;
265 }
266
267 /// Records the name the output is now going under, without allocating when it has not
268 /// changed, which is every token of every file that has no `#line` in it.
269 fn set_name(&mut self, name: &str) {
270 if self.name != name {
271 self.name.clear();
272 self.name.push_str(name);
273 }
274 }
275
276 /// Indents the first token of a line to the column it was written at.
277 ///
278 /// One space short of the column, because the token's own leading space flag supplies the
279 /// last one. GCC does exactly this, and the reason to copy it rather than to print the
280 /// tokens flush left is that indentation is most of what makes preprocessed output
281 /// readable when something has gone wrong in it.
282 fn indent(&mut self, column: u32) {
283 if self.printed {
284 return;
285 }
286 for _ in 2..column {
287 self.out.push(' ');
288 }
289 }
290
291 /// The finished text, which always ends in a newline.
292 fn finish(mut self) -> String {
293 if self.printed {
294 self.out.push('\n');
295 }
296 self.out
297 }
298}
299
300/// Whether writing these two tokens next to each other would change what they say.
301///
302/// This is GCC's `cpp_avoid_paste` with the same answers, written over spellings rather than
303/// over token codes. The word case is deliberately wider than GCC's: an identifier followed by
304/// a number gets a space here, because `x` and `1` written together are the single identifier
305/// `x1`, and output that does not read back as itself is not output.
306fn avoid_paste(prev: Tok, prev_text: &str, next: Tok, next_text: &str) -> bool {
307 let Some(first) = next_text.chars().next() else {
308 return false;
309 };
310 // Anything that ends in a word character followed by anything that starts as one. This
311 // covers name and name, name and number, number and number, and the prefixed forms of a
312 // character constant and a string literal, which are a name followed by a quote.
313 let word = matches!(prev.kind, PpTokenKind::Ident | PpTokenKind::Number | PpTokenKind::Other);
314 if word {
315 let joins = matches!(
316 next.kind,
317 PpTokenKind::Ident
318 | PpTokenKind::Number
319 | PpTokenKind::CharConst
320 | PpTokenKind::StringLit
321 );
322 if joins {
323 return true;
324 }
325 // A pp-number swallows a following sign after an exponent, and a `.` either side of
326 // one is part of the number rather than a separate token.
327 if prev.kind == PpTokenKind::Number {
328 return matches!(first, '.' | '+' | '-');
329 }
330 return false;
331 }
332
333 // An `=` glues onto every operator that has a compound assignment form, and onto the
334 // comparisons, which is most of them, so it is asked first.
335 if first == '=' {
336 return matches!(
337 prev_text,
338 "=" | "!" | "<" | ">" | "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>"
339 );
340 }
341 match prev_text {
342 ">" => first == '>',
343 "<" => matches!(first, '<' | '%' | ':'),
344 "+" => first == '+',
345 "-" => matches!(first, '-' | '>'),
346 // Not an operator that pastes: `/` and `*` written together open a comment, and `//`
347 // swallows the rest of the line.
348 "/" => matches!(first, '/' | '*'),
349 "%" => matches!(first, ':' | '%' | '>'),
350 "&" => first == '&',
351 "|" => first == '|',
352 ":" => matches!(first, ':' | '>'),
353 "." => first == '.' || next.kind == PpTokenKind::Number,
354 "#" => matches!(first, '#' | '%'),
355 _ => false,
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use rucc_diag::SourceMap;
362 use rucc_session::{MemoryFileSystem, SearchPath};
363
364 use super::*;
365 use crate::directive::Preprocessor;
366 use crate::include::Context;
367
368 /// A translation unit through phase 4 and back out as text.
369 struct Run {
370 interner: Interner,
371 sources: SourceMap,
372 fs: MemoryFileSystem,
373 search: SearchPath,
374 pp: Preprocessor,
375 }
376
377 impl Run {
378 fn new() -> Run {
379 Run {
380 interner: Interner::new(),
381 sources: SourceMap::new(),
382 fs: MemoryFileSystem::new(),
383 search: SearchPath::new(),
384 pp: Preprocessor::new(),
385 }
386 }
387
388 fn file(&mut self, path: &str, contents: &str) {
389 self.fs.insert(path, contents.as_bytes().to_vec());
390 }
391
392 fn go(&mut self, src: &str) -> String {
393 self.print(src, PrintOptions::new())
394 }
395
396 fn print(&mut self, src: &str, opts: PrintOptions) -> String {
397 let main =
398 self.sources.add("/main.c", src.as_bytes().to_vec()).expect("the map has room");
399 let out = {
400 let mut cx =
401 Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
402 self.pp.run(main, &mut cx)
403 };
404 assert!(self.pp.diagnostics().is_empty(), "{:?}", self.pp.diagnostics());
405 print(main, &out, self.pp.line_directives(), &self.sources, &self.interner, opts)
406 }
407 }
408
409 #[test]
410 fn the_first_line_says_which_file_this_is() {
411 let mut run = Run::new();
412 assert_eq!(run.go("int x;\n"), "# 1 \"/main.c\"\nint x;\n");
413 }
414
415 #[test]
416 fn a_line_the_preprocessor_ate_comes_back_as_a_blank_one() {
417 let mut run = Run::new();
418 // The definition produced no tokens, so line 2 is blank and `x` is still on line 3.
419 // Keeping it there is what lets a diagnostic from a later phase name the right line.
420 assert_eq!(run.go("#define N 1\nint x;\n"), "# 1 \"/main.c\"\n\nint x;\n");
421 }
422
423 #[test]
424 fn a_long_gap_is_a_marker_rather_than_a_page_of_blank_lines() {
425 let mut run = Run::new();
426 let src = format!("a;{}b;\n", "\n".repeat(20));
427 let text = run.go(&src);
428 assert!(text.contains("# 21 \"/main.c\"\nb;\n"), "{text}");
429 assert!(!text.contains("\n\n\n"), "a gap that big is a marker, not blank lines: {text}");
430 }
431
432 #[test]
433 fn entering_and_leaving_a_header_are_both_marked() {
434 let mut run = Run::new();
435 run.file("/one.h", "int from_the_header;\n");
436 let text = run.go("#include \"one.h\"\nint after;\n");
437 assert_eq!(
438 text,
439 "# 1 \"/main.c\"\n\
440 # 1 \"/one.h\" 1\n\
441 int from_the_header;\n\
442 # 2 \"/main.c\" 2\n\
443 int after;\n"
444 );
445 }
446
447 #[test]
448 fn dash_p_prints_the_tokens_and_nothing_else() {
449 let mut run = Run::new();
450 run.file("/one.h", "int from_the_header;\n");
451 let src = "#include \"one.h\"\n\n\n\nint after;\n";
452 let text = run.print(src, PrintOptions { line_markers: false });
453 assert_eq!(text, "int from_the_header;\nint after;\n");
454 }
455
456 #[test]
457 fn indentation_survives() {
458 let mut run = Run::new();
459 assert_eq!(run.go(" int x;\n"), "# 1 \"/main.c\"\n int x;\n");
460 }
461
462 #[test]
463 fn a_space_goes_in_where_the_tokens_would_otherwise_paste() {
464 let mut run = Run::new();
465 // `+ +` rather than `++`, and `- -` rather than `--`, because those are different
466 // operators and the output has to say what the input said.
467 let src = "#define P +\n#define M -\nP+x;\nM-x;\n";
468 assert_eq!(run.go(src), "# 1 \"/main.c\"\n\n\n+ +x;\n- -x;\n");
469 }
470
471 #[test]
472 fn a_name_and_a_number_do_not_run_together() {
473 let mut run = Run::new();
474 // `x1` would read back as one identifier, so the space is not optional.
475 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");
476 }
477
478 /// The paste test is for tokens a macro put next to each other. Two the user wrote next to
479 /// each other came out of the lexer that way and read back as themselves, so nothing is
480 /// inserted between them: the kernel's `sound/asound.h` writes an array bound as
481 /// `[52-2*sizeof(x)]` and GCC prints it back unchanged.
482 #[test]
483 fn a_paste_is_only_avoided_where_a_macro_put_the_tokens_together() {
484 let mut run = Run::new();
485 assert_eq!(
486 run.go("char a[52-2*sizeof(int)];\n"),
487 "# 1 \"/main.c\"\nchar a[52-2*sizeof(int)];\n"
488 );
489
490 // The number comes out of `N` and the sign does not, so they did not arrive together
491 // and `52-2` would read back as a different pp-number than the two tokens it is.
492 let mut run = Run::new();
493 assert_eq!(run.go("#define N 52\nN-2;\n"), "# 1 \"/main.c\"\n\n52 -2;\n");
494
495 // Both out of the same expansion, so the body's own spacing is what is printed.
496 let mut run = Run::new();
497 assert_eq!(run.go("#define S 41+1\nS;\n"), "# 1 \"/main.c\"\n\n41+1;\n");
498
499 // A nest, which is lz4's `#define LZ4_HASHLOG (LZ4_MEMORY_USAGE-2)` cut down. The two
500 // tokens share an outermost invocation and are still a step apart, and gcc prints the
501 // space, so the question is asked of the trace rather than of the outermost.
502 let mut run = Run::new();
503 assert_eq!(
504 run.go("#define A 14\n#define B (A-2)\nint t[1 << B];\n"),
505 "# 1 \"/main.c\"\n\n\nint t[1 << (14 -2)];\n"
506 );
507 }
508
509 #[test]
510 fn a_slash_and_a_star_do_not_open_a_comment() {
511 let mut run = Run::new();
512 assert_eq!(run.go("#define D /\nD*p;\n"), "# 1 \"/main.c\"\n\n/ *p;\n");
513 }
514
515 #[test]
516 fn a_run_of_spaces_is_one_space_and_the_indent_is_the_real_one() {
517 let mut run = Run::new();
518 // GCC collapses whitespace between tokens to one space and rebuilds the indentation
519 // from the column, so a line that was indented by two still is.
520 assert_eq!(run.go(" int x = a+b;\n"), "# 1 \"/main.c\"\n int x = a+b;\n");
521 }
522
523 #[test]
524 fn a_macro_that_spans_lines_leaves_the_output_where_the_call_was() {
525 let mut run = Run::new();
526 let text = run.go("#define ADD(a, b) a + b\nADD(1,\n 2)\nlast;\n");
527 assert_eq!(text, "# 1 \"/main.c\"\n\n1 + 2\n\nlast;\n");
528 }
529
530 #[test]
531 fn a_macro_that_expands_to_nothing_leaves_its_space_behind() {
532 let mut run = Run::new();
533 // GCC and clang both print `int a ;` here, and the space is not decoration. The glibc
534 // headers hang `__THROW` and its relatives off the end of several hundred prototypes
535 // per file, and on a dialect where those expand to nothing this one space is the whole
536 // difference between agreeing with the reference compiler and not.
537 let text = run.print("#define E\nint a E;\n", PrintOptions { line_markers: false });
538 assert_eq!(text, "int a ;\n");
539 }
540
541 #[test]
542 fn the_space_is_only_left_where_there_was_one() {
543 let mut run = Run::new();
544 // No space before the macro means no space after it. `a1(E);` is `a1();` and not
545 // `a1( );`, which is the case that stops this rule from turning into "always insert".
546 let text = run.print("#define E\na1(E);\n", PrintOptions { line_markers: false });
547 assert_eq!(text, "a1();\n");
548 }
549
550 #[test]
551 fn a_space_owed_by_one_empty_macro_is_not_paid_twice() {
552 let mut run = Run::new();
553 // Three vanishing macros in a row owe one space between them, not three. The debt is
554 // handed along until a token that survives takes it.
555 let text = run.print("#define E\nd1 E E E d2;\n", PrintOptions { line_markers: false });
556 assert_eq!(text, "d1 d2;\n");
557 }
558
559 #[test]
560 fn the_space_crosses_out_of_the_expansion_that_owed_it() {
561 let mut run = Run::new();
562 // `J(4)` expands to `4 E`, and the `E` vanishes at the end of the replacement list. The
563 // token that takes the space is the `;` from the source, which the expansion never saw.
564 let text = run
565 .print("#define E\n#define J(x) x E\np6 J(4);\n", PrintOptions { line_markers: false });
566 assert_eq!(text, "p6 4 ;\n");
567 }
568
569 #[test]
570 fn a_function_like_macro_with_an_empty_body_leaves_a_space_too() {
571 let mut run = Run::new();
572 // The rule is about the invocation vanishing, not about which kind of macro it was.
573 let text = run
574 .print("#define F(x)\nint d(int F(9), int);\n", PrintOptions { line_markers: false });
575 assert_eq!(text, "int d(int , int);\n");
576 }
577
578 #[test]
579 fn a_line_directive_is_a_marker_where_it_was_written() {
580 let mut run = Run::new();
581 // GCC writes the marker at the directive and then walks the three blank lines from
582 // there. Reacting to the line the statement claims to be on instead would put the same
583 // information in the output as eight blank lines and no marker.
584 let text = run.go("#line 5\n\n\n\nint a;\n");
585 assert_eq!(text, "# 1 \"/main.c\"\n# 5 \"/main.c\"\n\n\n\nint a;\n");
586 }
587
588 #[test]
589 fn two_directives_in_a_row_are_two_markers() {
590 let mut run = Run::new();
591 assert_eq!(
592 run.go("#line 5\n#line 9\nint a;\n"),
593 "# 1 \"/main.c\"\n# 5 \"/main.c\"\n# 9 \"/main.c\"\nint a;\n"
594 );
595 }
596
597 #[test]
598 fn a_directive_with_nothing_after_it_still_writes_its_marker() {
599 let mut run = Run::new();
600 assert_eq!(run.go("int a;\n#line 5\n"), "# 1 \"/main.c\"\nint a;\n# 5 \"/main.c\"\n");
601 }
602
603 #[test]
604 fn the_marker_goes_where_the_directive_is_and_not_where_its_bytes_are() {
605 let mut run = Run::new();
606 // The header is added to the source map after the file that includes it, so its bytes
607 // come after every byte of this file, including the ones after the `#include`. A
608 // printer that ordered the markers by position would write the rename after the
609 // header rather than before it.
610 run.file("/one.h", "int in_header;\n");
611 let text = run.go("#line 900 \"outer\"\n#include \"one.h\"\nint after;\n");
612 assert_eq!(
613 text,
614 "# 1 \"/main.c\"\n\
615 # 900 \"outer\"\n\
616 # 1 \"/one.h\" 1\n\
617 int in_header;\n\
618 # 901 \"outer\" 2\n\
619 int after;\n"
620 );
621 }
622
623 #[test]
624 fn dash_p_drops_the_markers_a_directive_makes_like_every_other_one() {
625 let mut run = Run::new();
626 let text = run.print("#line 900 \"outer\"\nint a;\n", PrintOptions { line_markers: false });
627 assert_eq!(text, "int a;\n");
628 }
629}