use std::path::Path;
use rucc_diag::{Diagnostic, Severity};
use rucc_lex::{Convert, Keywords, PpToken, convert};
use rucc_sema::{Checker, Context as CheckContext};
use rucc_session::{EmitKind, FileSystem, Options, Session};
use crate::preprocess::render;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Compiled {
pub text: String,
pub messages: Vec<String>,
pub errors: u32,
}
impl Compiled {
#[must_use]
pub fn failed(&self) -> bool {
self.errors > 0
}
}
#[must_use]
pub fn compile(opts: &Options, name: &str, fs: &dyn FileSystem) -> Compiled {
let mut sess = Session::new(opts.clone());
let keywords = Keywords::new(&mut sess.interner, opts.std, opts.gnu_extensions);
let mut diagnostics: Vec<Diagnostic> = Vec::new();
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 = rucc_pp::Preprocessor::new();
let predef = rucc_pp::Predef::for_options(opts);
let expanded: Vec<PpToken> = {
let mut cx = rucc_pp::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 for the built in macros"));
}
pp.run(file, &mut cx).iter().map(|token| token.to_pp()).collect()
};
diagnostics.extend(pp.take_diagnostics());
let cx = Convert {
keywords: &keywords,
interner: &sess.interner,
target: &sess.target,
std: opts.std,
pedantic: opts.pedantic,
};
let (tokens, complaints) = convert(&expanded, &cx);
diagnostics.extend(complaints);
let parsed = rucc_parse::parse(
&tokens,
rucc_parse::Context {
interner: &sess.interner,
std: opts.std,
gnu: opts.gnu_extensions,
pedantic: opts.pedantic,
error_limit: opts.error_limit as usize,
},
);
let parse_failed = parsed.diagnostics.iter().any(|d| d.severity.is_fatal());
diagnostics.extend(parsed.diagnostics);
let mut text = String::new();
if !parse_failed {
let mut checker = Checker::new(
&parsed.ast,
CheckContext {
names: &sess.interner,
target: &sess.target,
std: opts.std,
gnu: opts.gnu_extensions,
pedantic: opts.pedantic,
error_limit: opts.error_limit as usize,
},
);
checker.check_unit();
let checked = checker.finish();
if !checked.failed() && opts.emit == EmitKind::Tast {
text = rucc_sema::print(&checked.tast, &checked.types, &sess.interner);
}
diagnostics.extend(checked.diagnostics);
}
let mut messages = Vec::with_capacity(diagnostics.len());
let mut errors = 0;
for diag in &diagnostics {
if diag.severity.is_fatal()
|| (diag.severity == Severity::Warning && opts.warnings_are_errors)
{
errors += 1;
}
messages.push(render(diag, &sess.sources, opts.warnings_are_errors));
}
if errors > 0 {
text.clear();
}
Compiled { text, messages, errors }
}
fn failure(message: String) -> Compiled {
Compiled { text: String::new(), messages: vec![format!("rucc: error: {message}")], errors: 1 }
}
#[cfg(test)]
mod tests {
use rucc_session::{MemoryFileSystem, Std};
use rucc_target::Triple;
use super::*;
fn options() -> Options {
let mut opts = Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
opts.emit = EmitKind::Tast;
opts
}
fn run(opts: &Options, source: &str) -> Compiled {
let mut fs = MemoryFileSystem::new();
fs.insert("/main.c", source.to_owned().into_bytes());
compile(opts, "/main.c", &fs)
}
fn tast(source: &str) -> String {
let result = run(&options(), source);
assert_eq!(result.messages, Vec::<String>::new(), "expected this to compile:\n{source}");
result.text
}
#[test]
fn a_file_that_is_not_there_says_so_and_produces_nothing() {
let fs = MemoryFileSystem::new();
let result = compile(&options(), "/nope.c", &fs);
assert!(result.failed());
assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
assert!(result.text.is_empty());
}
#[test]
fn an_object_comes_out_with_its_type_its_linkage_and_how_much_of_a_definition_it_is() {
let text = tast("int x = 1;\n");
let expected = "\
decl #0 x : int object external static defined
init
+0
const 1 : int
";
assert_eq!(text, expected);
}
#[test]
fn the_macros_are_expanded_before_anything_is_parsed() {
let text = tast("#define N 2\nint a[N];\n");
assert!(text.starts_with("decl #0 a : int [2] object external static tentative"), "{text}");
}
#[test]
fn every_conversion_the_language_performs_is_a_node_in_the_output() {
let text = tast("long f(int a, long b) { return a + b; }\n");
assert!(text.contains("convert arithmetic"), "{text}");
}
#[test]
fn a_mistake_in_each_phase_reaches_the_caller_and_writes_no_tree() {
for source in [
"#error stop\n",
"int f(void) { return 1 + ; }\n",
"int f(void) { return undeclared; }\n",
] {
let result = run(&options(), source);
assert!(result.failed(), "expected this to fail:\n{source}");
assert!(result.text.is_empty(), "a file that did not compile wrote a tree:\n{source}");
}
}
#[test]
fn one_undeclared_name_is_one_message_and_not_one_per_use() {
let result = run(&options(), "int f(void) { return nope + nope * nope; }\n");
assert_eq!(result.errors, 1, "{:?}", result.messages);
}
#[test]
fn a_declaration_the_parser_skipped_does_not_become_an_undeclared_name_as_well() {
let result = run(&options(), "int x = ;\nint f(void) { return x; }\n");
assert_eq!(result.errors, 1, "{:?}", result.messages);
}
#[test]
fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
let source = "int f(void) { char c = 300; return c; }\n";
let plain = run(&options(), source);
assert_eq!(plain.errors, 0, "{:?}", plain.messages);
assert_eq!(plain.messages.len(), 1, "expected a warning about the narrowed constant");
assert!(!plain.text.is_empty(), "a warning is not a reason to write nothing");
let mut opts = options();
opts.warnings_are_errors = true;
let strict = run(&opts, source);
assert!(strict.failed());
assert!(strict.text.is_empty(), "and under -Werror it is a reason to write nothing");
for message in &strict.messages {
assert!(!message.contains("warning:"), "{message}");
}
}
#[test]
fn the_dialect_reaches_the_keywords_and_the_checking() {
let source = "typeof(1) x;\n";
let mut opts = options();
opts.std = Std::C23;
opts.gnu_extensions = false;
assert!(!run(&opts, source).failed(), "{:?}", run(&opts, source).messages);
opts.std = Std::C17;
assert!(run(&opts, source).failed());
}
#[test]
fn asking_for_a_later_kind_runs_the_same_front_end_and_writes_nothing_yet() {
let mut opts = options();
opts.emit = EmitKind::Ir;
let result = run(&opts, "int x = 1;\n");
assert!(!result.failed(), "{:?}", result.messages);
assert!(result.text.is_empty());
assert!(run(&opts, "int f(void) { return undeclared; }\n").failed());
}
}