use std::io;
use std::path::Path;
use rucc_diag::{Diagnostic, Severity, SourceBytes, SourceMap};
use rucc_pp::{Context, Predef, Preprocessor, PrintOptions};
use rucc_session::{FileSystem, Options, Session};
#[derive(Debug, Clone, Copy, Default)]
pub struct OsFileSystem;
impl OsFileSystem {
#[must_use]
pub fn new() -> OsFileSystem {
OsFileSystem
}
}
impl FileSystem for OsFileSystem {
fn read(&self, path: &Path) -> io::Result<SourceBytes> {
crate::map::read(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Preprocessed {
pub text: String,
pub messages: Vec<String>,
pub errors: u32,
}
impl Preprocessed {
#[must_use]
pub fn failed(&self) -> bool {
self.errors > 0
}
}
#[must_use]
pub fn preprocess(opts: &Options, name: &str, fs: &dyn FileSystem) -> Preprocessed {
let mut sess = Session::new(opts.clone());
let bytes = match fs.read(Path::new(name)) {
Ok(bytes) => bytes,
Err(e) => return failure(format!("{name}: {e}")),
};
let Ok(file) = sess.sources.add_shared(name, bytes, None) else {
return failure(format!("{name}: the source map has no room left for this file"));
};
let mut pp = Preprocessor::new();
let predef = Predef::for_options(opts);
let mut cx = Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
return failure(format!("{name}: the source map has no room left for the built in macros"));
}
let tokens = pp.run(file, &mut cx);
let text = if opts.dumps.macros {
rucc_pp::dump_macros(pp.macros(), &sess.interner)
} else {
rucc_pp::print(
file,
&tokens,
&sess.sources,
&sess.interner,
PrintOptions { line_markers: opts.line_markers },
)
};
let mut messages = Vec::new();
let mut errors = 0;
for diag in pp.take_diagnostics() {
let fatal = diag.severity.is_fatal()
|| (diag.severity == Severity::Warning && opts.warnings_are_errors);
if fatal {
errors += 1;
}
messages.push(render(&diag, &sess.sources, opts.warnings_are_errors));
}
Preprocessed { text, messages, errors }
}
fn failure(message: String) -> Preprocessed {
Preprocessed {
text: String::new(),
messages: vec![format!("rucc: error: {message}")],
errors: 1,
}
}
pub(crate) fn render(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
let mut out = String::new();
let mut chain = sources.include_stack(diag.span.lo);
chain.reverse();
for (at, from) in chain.iter().enumerate() {
let lead = if at == 0 { "In file included from" } else { " from" };
out.push_str(&format!("{lead} {}:\n", sources.render_position(from.lo)));
}
out.push_str(&line(diag, sources, warnings_are_errors));
for child in &diag.children {
out.push('\n');
out.push_str(&line(child, sources, false));
}
out
}
fn line(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
let severity = if diag.severity == Severity::Warning && warnings_are_errors {
"error"
} else {
diag.severity.as_str()
};
let position = if diag.span.is_dummy() {
"rucc".to_owned()
} else {
sources.render_position(diag.span.lo)
};
match diag.code {
Some(code) => format!("{position}: {severity}: {} [{code}]", diag.message),
None => format!("{position}: {severity}: {}", diag.message),
}
}
#[cfg(test)]
mod tests {
use rucc_session::MemoryFileSystem;
use rucc_target::Triple;
use super::*;
fn options() -> Options {
Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap())
}
fn at(dir: &str, name: &str) -> String {
Path::new(dir).join(name).display().to_string()
}
fn run(opts: &Options, files: &[(&str, &str)]) -> Preprocessed {
let mut fs = MemoryFileSystem::new();
for (path, text) in files {
fs.insert(*path, (*text).to_owned().into_bytes());
}
preprocess(opts, files[0].0, &fs)
}
#[test]
fn a_file_that_is_not_there_says_so_and_produces_nothing() {
let fs = MemoryFileSystem::new();
let result = preprocess(&options(), "/nope.c", &fs);
assert!(result.failed());
assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
assert!(result.text.is_empty());
}
#[test]
fn the_output_is_the_expanded_text_with_a_line_marker_on_top() {
let result = run(&options(), &[("/main.c", "#define N 2\nint a[N];\n")]);
assert_eq!(result.messages, Vec::<String>::new());
assert_eq!(result.text, "# 1 \"/main.c\"\n\nint a[2];\n");
}
#[test]
fn the_predefined_macros_are_there_without_being_asked_for() {
let result = run(&options(), &[("/main.c", "__SIZEOF_LONG__ __x86_64__\n")]);
assert_eq!(result.text, "# 1 \"/main.c\"\n8 1\n");
}
#[test]
fn dash_d_and_dash_u_reach_the_macro_table() {
let mut opts = options();
opts.defines.push("FOO=41+1".to_owned());
opts.defines.push("BAR".to_owned());
opts.undefines.push("__x86_64__".to_owned());
let result = run(&opts, &[("/main.c", "FOO BAR\n#ifdef __x86_64__\ngone\n#endif\n")]);
assert_eq!(result.text, "# 1 \"/main.c\"\n41 +1 1\n");
}
#[test]
fn dash_i_is_where_an_angled_include_looks() {
let mut opts = options();
opts.search.push_bracket("/inc");
let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
let result = run(&opts, &files);
let one = at("/inc", "one.h").replace('\\', "\\\\");
let expected = format!(
"# 1 \"/main.c\"\n# 1 \"{one}\" 1\nint in_it;\n# 2 \"/main.c\" 2\nint after;\n"
);
assert_eq!(result.text, expected);
}
#[test]
fn dash_p_leaves_the_markers_out() {
let mut opts = options();
opts.line_markers = false;
opts.search.push_bracket("/inc");
let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
assert_eq!(run(&opts, &files).text, "int in_it;\nint after;\n");
}
#[test]
fn a_diagnostic_says_where_it_is_and_carries_its_code() {
let result = run(&options(), &[("/main.c", "#error no\n")]);
assert_eq!(result.errors, 1);
assert!(
result.messages[0].starts_with("/main.c:1:8: error: no ["),
"{:?}",
result.messages
);
}
#[test]
fn a_diagnostic_in_a_header_prints_the_chain_that_reached_it() {
let mut opts = options();
opts.search.push_bracket("/inc");
let files = [
("/main.c", "#include <one.h>\n"),
("/inc/one.h", "#include <two.h>\n"),
("/inc/two.h", "#error deep\n"),
];
let result = run(&opts, &files);
let text = result.messages.join("\n");
assert!(text.starts_with("In file included from /main.c:1:1:\n"), "{text}");
assert!(
text.contains(&format!(" from {}:1:1:\n", at("/inc", "one.h"))),
"{text}"
);
assert!(text.contains(&format!("{}:1:8: error: deep", at("/inc", "two.h"))), "{text}");
}
#[test]
fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
let source = "#warning careful\n";
let plain = run(&options(), &[("/main.c", source)]);
assert_eq!(plain.errors, 0);
assert!(plain.messages[0].contains("warning: careful"), "{:?}", plain.messages);
let mut opts = options();
opts.warnings_are_errors = true;
let strict = run(&opts, &[("/main.c", source)]);
assert_eq!(strict.errors, 1);
assert!(strict.messages[0].contains("error: careful"), "{:?}", strict.messages);
}
#[test]
fn dash_dm_prints_the_macros_the_file_left_behind_and_not_the_file() {
let source = "#define KEPT 1\n#define GONE 2\n#undef GONE\n#ifdef NEVER\n#define \
HIDDEN 3\n#endif\nint x;\n";
let result = run(&options(), &[("/main.c", source)]);
assert_eq!(result.errors, 0);
assert!(result.text.contains("int x;"), "the output is the file without -dM");
assert!(!result.text.contains("#define"), "a directive line is not part of the output");
let mut opts = options();
opts.dumps.macros = true;
let dumped = run(&opts, &[("/main.c", source)]);
assert!(dumped.text.contains("#define KEPT 1\n"), "{}", dumped.text);
assert!(!dumped.text.contains("int x;"), "-dM replaces the output rather than adding");
assert!(!dumped.text.contains("GONE"), "{}", dumped.text);
assert!(!dumped.text.contains("HIDDEN"), "{}", dumped.text);
assert!(dumped.text.contains("#define __x86_64__ 1\n"), "{}", dumped.text);
}
#[test]
fn the_dialect_reaches_the_predefined_set() {
let mut opts = options();
opts.std = rucc_session::Std::C99;
opts.gnu_extensions = false;
let result = run(&opts, &[("/main.c", "__STDC_VERSION__ __STRICT_ANSI__\n")]);
assert_eq!(result.text, "# 1 \"/main.c\"\n199901L 1\n");
}
}