use rucc_base::Interner;
use rucc_diag::{FileId, SourceMap};
use rucc_lex::{PpTokenKind, TokenFlags};
use crate::include::{quoted, spelling};
use crate::token::Tok;
const MAX_BLANKS: u32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrintOptions {
pub line_markers: bool,
}
impl PrintOptions {
pub fn new() -> PrintOptions {
PrintOptions { line_markers: true }
}
}
impl Default for PrintOptions {
fn default() -> PrintOptions {
PrintOptions::new()
}
}
pub fn print(
main: FileId,
tokens: &[Tok],
sources: &SourceMap,
interner: &Interner,
opts: PrintOptions,
) -> String {
let mut printer = Printer {
out: String::new(),
opts,
sources,
interner,
file: main,
line: 1,
printed: false,
stack: vec![main],
};
printer.start();
let mut previous: Option<Tok> = None;
for &tok in tokens {
printer.token(tok, previous);
previous = Some(tok);
}
printer.finish()
}
struct Printer<'a> {
out: String,
opts: PrintOptions,
sources: &'a SourceMap,
interner: &'a Interner,
file: FileId,
line: u32,
printed: bool,
stack: Vec<FileId>,
}
impl Printer<'_> {
fn start(&mut self) {
if self.opts.line_markers {
self.out.push_str(&format!("# 1 {}\n", quoted(&self.sources.file(self.file).name)));
}
}
fn token(&mut self, tok: Tok, previous: Option<Tok>) {
let at = tok.report_span().lo;
if let Some(loc) = self.sources.lookup(at) {
self.move_to(loc.file, loc.line, loc.column);
}
let text = spelling(tok, self.interner);
if self.space_before(tok, text, previous) {
self.out.push(' ');
}
self.out.push_str(text);
self.printed = true;
}
fn space_before(&self, tok: Tok, text: &str, previous: Option<Tok>) -> bool {
if tok.flags.has(TokenFlags::LEADING_SPACE) {
return true;
}
match previous {
Some(prev) if self.printed => {
avoid_paste(prev, spelling(prev, self.interner), tok, text)
}
_ => false,
}
}
fn move_to(&mut self, file: FileId, line: u32, column: u32) {
if file == self.file && line == self.line && self.printed {
return;
}
self.end_line();
if file != self.file {
self.marker(file, line);
} else if line > self.line && line - self.line <= MAX_BLANKS {
if self.opts.line_markers {
for _ in self.line..line {
self.out.push('\n');
}
}
self.line = line;
} else if line != self.line {
self.jump(file, line);
}
self.indent(column);
}
fn end_line(&mut self) {
if self.printed {
self.out.push('\n');
self.line += 1;
self.printed = false;
}
}
fn marker(&mut self, file: FileId, line: u32) {
let flag = match self.stack.iter().position(|&f| f == file) {
Some(at) => {
self.stack.truncate(at + 1);
2
}
None => {
self.stack.push(file);
1
}
};
if self.opts.line_markers {
let name = quoted(&self.sources.file(file).name);
self.out.push_str(&format!("# {line} {name} {flag}\n"));
}
self.file = file;
self.line = line;
}
fn jump(&mut self, file: FileId, line: u32) {
if self.opts.line_markers {
let name = quoted(&self.sources.file(file).name);
self.out.push_str(&format!("# {line} {name}\n"));
}
self.line = line;
}
fn indent(&mut self, column: u32) {
if self.printed {
return;
}
for _ in 2..column {
self.out.push(' ');
}
}
fn finish(mut self) -> String {
if self.printed {
self.out.push('\n');
}
self.out
}
}
fn avoid_paste(prev: Tok, prev_text: &str, next: Tok, next_text: &str) -> bool {
let Some(first) = next_text.chars().next() else {
return false;
};
let word = matches!(prev.kind, PpTokenKind::Ident | PpTokenKind::Number | PpTokenKind::Other);
if word {
let joins = matches!(
next.kind,
PpTokenKind::Ident
| PpTokenKind::Number
| PpTokenKind::CharConst
| PpTokenKind::StringLit
);
if joins {
return true;
}
if prev.kind == PpTokenKind::Number {
return matches!(first, '.' | '+' | '-');
}
return false;
}
if first == '=' {
return matches!(
prev_text,
"=" | "!" | "<" | ">" | "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>"
);
}
match prev_text {
">" => first == '>',
"<" => matches!(first, '<' | '%' | ':'),
"+" => first == '+',
"-" => matches!(first, '-' | '>'),
"/" => matches!(first, '/' | '*'),
"%" => matches!(first, ':' | '%' | '>'),
"&" => first == '&',
"|" => first == '|',
":" => matches!(first, ':' | '>'),
"." => first == '.' || next.kind == PpTokenKind::Number,
"#" => matches!(first, '#' | '%'),
_ => false,
}
}
#[cfg(test)]
mod tests {
use rucc_diag::SourceMap;
use rucc_session::{MemoryFileSystem, SearchPath};
use super::*;
use crate::directive::Preprocessor;
use crate::include::Context;
struct Run {
interner: Interner,
sources: SourceMap,
fs: MemoryFileSystem,
search: SearchPath,
pp: Preprocessor,
}
impl Run {
fn new() -> Run {
Run {
interner: Interner::new(),
sources: SourceMap::new(),
fs: MemoryFileSystem::new(),
search: SearchPath::new(),
pp: Preprocessor::new(),
}
}
fn file(&mut self, path: &str, contents: &str) {
self.fs.insert(path, contents.as_bytes().to_vec());
}
fn go(&mut self, src: &str) -> String {
self.print(src, PrintOptions::new())
}
fn print(&mut self, src: &str, opts: PrintOptions) -> String {
let main =
self.sources.add("/main.c", src.as_bytes().to_vec()).expect("the map has room");
let out = {
let mut cx =
Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
self.pp.run(main, &mut cx)
};
assert!(self.pp.diagnostics().is_empty(), "{:?}", self.pp.diagnostics());
print(main, &out, &self.sources, &self.interner, opts)
}
}
#[test]
fn the_first_line_says_which_file_this_is() {
let mut run = Run::new();
assert_eq!(run.go("int x;\n"), "# 1 \"/main.c\"\nint x;\n");
}
#[test]
fn a_line_the_preprocessor_ate_comes_back_as_a_blank_one() {
let mut run = Run::new();
assert_eq!(run.go("#define N 1\nint x;\n"), "# 1 \"/main.c\"\n\nint x;\n");
}
#[test]
fn a_long_gap_is_a_marker_rather_than_a_page_of_blank_lines() {
let mut run = Run::new();
let src = format!("a;{}b;\n", "\n".repeat(20));
let text = run.go(&src);
assert!(text.contains("# 21 \"/main.c\"\nb;\n"), "{text}");
assert!(!text.contains("\n\n\n"), "a gap that big is a marker, not blank lines: {text}");
}
#[test]
fn entering_and_leaving_a_header_are_both_marked() {
let mut run = Run::new();
run.file("/one.h", "int from_the_header;\n");
let text = run.go("#include \"one.h\"\nint after;\n");
assert_eq!(
text,
"# 1 \"/main.c\"\n\
# 1 \"/one.h\" 1\n\
int from_the_header;\n\
# 2 \"/main.c\" 2\n\
int after;\n"
);
}
#[test]
fn dash_p_prints_the_tokens_and_nothing_else() {
let mut run = Run::new();
run.file("/one.h", "int from_the_header;\n");
let src = "#include \"one.h\"\n\n\n\nint after;\n";
let text = run.print(src, PrintOptions { line_markers: false });
assert_eq!(text, "int from_the_header;\nint after;\n");
}
#[test]
fn indentation_survives() {
let mut run = Run::new();
assert_eq!(run.go(" int x;\n"), "# 1 \"/main.c\"\n int x;\n");
}
#[test]
fn a_space_goes_in_where_the_tokens_would_otherwise_paste() {
let mut run = Run::new();
let src = "#define P +\n#define M -\nP+x;\nM-x;\n";
assert_eq!(run.go(src), "# 1 \"/main.c\"\n\n\n+ +x;\n- -x;\n");
}
#[test]
fn a_name_and_a_number_do_not_run_together() {
let mut run = Run::new();
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");
}
#[test]
fn a_slash_and_a_star_do_not_open_a_comment() {
let mut run = Run::new();
assert_eq!(run.go("#define D /\nD*p;\n"), "# 1 \"/main.c\"\n\n/ *p;\n");
}
#[test]
fn a_run_of_spaces_is_one_space_and_the_indent_is_the_real_one() {
let mut run = Run::new();
assert_eq!(run.go(" int x = a+b;\n"), "# 1 \"/main.c\"\n int x = a+b;\n");
}
#[test]
fn a_macro_that_spans_lines_leaves_the_output_where_the_call_was() {
let mut run = Run::new();
let text = run.go("#define ADD(a, b) a + b\nADD(1,\n 2)\nlast;\n");
assert_eq!(text, "# 1 \"/main.c\"\n\n1 + 2\n\nlast;\n");
}
#[test]
fn a_macro_that_expands_to_nothing_leaves_its_space_behind() {
let mut run = Run::new();
let text = run.print("#define E\nint a E;\n", PrintOptions { line_markers: false });
assert_eq!(text, "int a ;\n");
}
#[test]
fn the_space_is_only_left_where_there_was_one() {
let mut run = Run::new();
let text = run.print("#define E\na1(E);\n", PrintOptions { line_markers: false });
assert_eq!(text, "a1();\n");
}
#[test]
fn a_space_owed_by_one_empty_macro_is_not_paid_twice() {
let mut run = Run::new();
let text = run.print("#define E\nd1 E E E d2;\n", PrintOptions { line_markers: false });
assert_eq!(text, "d1 d2;\n");
}
#[test]
fn the_space_crosses_out_of_the_expansion_that_owed_it() {
let mut run = Run::new();
let text = run
.print("#define E\n#define J(x) x E\np6 J(4);\n", PrintOptions { line_markers: false });
assert_eq!(text, "p6 4 ;\n");
}
#[test]
fn a_function_like_macro_with_an_empty_body_leaves_a_space_too() {
let mut run = Run::new();
let text = run
.print("#define F(x)\nint d(int F(9), int);\n", PrintOptions { line_markers: false });
assert_eq!(text, "int d(int , int);\n");
}
}