1use std::io;
12use std::path::{Path, PathBuf};
13
14use rucc_diag::{Diagnostic, Severity, SourceBytes, SourceMap};
15use rucc_pp::{Context, Dependency, Predef, Preprocessor, PrintOptions};
16use rucc_session::{FileSystem, Options, Session};
17
18#[derive(Debug, Clone, Copy, Default)]
20pub struct OsFileSystem;
21
22impl OsFileSystem {
23 #[must_use]
25 pub fn new() -> OsFileSystem {
26 OsFileSystem
27 }
28}
29
30impl FileSystem for OsFileSystem {
31 fn read(&self, path: &Path) -> io::Result<SourceBytes> {
32 crate::map::read(path)
37 }
38
39 fn identity(&self, path: &Path) -> PathBuf {
40 std::fs::canonicalize(path).unwrap_or_else(|_| rucc_session::path_key(path))
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Preprocessed {
52 pub text: String,
54 pub messages: Vec<String>,
56 pub errors: u32,
58 pub deps: Vec<Dependency>,
64}
65
66impl Preprocessed {
67 #[must_use]
69 pub fn failed(&self) -> bool {
70 self.errors > 0
71 }
72}
73
74#[must_use]
80pub fn preprocess(opts: &Options, name: &str, fs: &dyn FileSystem) -> Preprocessed {
81 let mut sess = Session::new(opts.clone());
82 let bytes = match fs.read(Path::new(name)) {
83 Ok(bytes) => bytes,
84 Err(e) => return failure(format!("{name}: {e}")),
85 };
86 let Ok(file) = sess.sources.add_shared(crate::phase::source_name(name), bytes, None) else {
87 return failure(format!("{name}: the source map has no room left for this file"));
88 };
89
90 let mut pp = Preprocessor::with_prefix_map(opts.prefix_map.macros.clone());
91 let predef = Predef::for_options(opts);
92 let mut cx = Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
93 cx.lex = rucc_lex::Options::for_dialect(opts.std, opts.gnu_extensions);
94 cx.pedantic = opts.pedantic;
95 if pp.predefine(&sess.target, &predef, &mut cx).is_err() {
96 return failure(format!("{name}: the source map has no room left for the built in macros"));
97 }
98 let mut tokens = Vec::new();
99 if pp.preinclude(&opts.preincludes, &mut tokens, &mut cx).is_err() {
100 return failure(format!("{name}: the source map has no room left for the command line"));
101 }
102 tokens.append(&mut pp.run(file, &mut cx));
103 let text = if opts.dumps.macros {
107 rucc_pp::dump_macros(pp.macros(), &sess.interner)
108 } else {
109 rucc_pp::print(
110 file,
111 &tokens,
112 pp.line_directives(),
113 &sess.sources,
114 &sess.interner,
115 PrintOptions { line_markers: opts.line_markers },
116 )
117 };
118
119 let mut messages = Vec::new();
120 let mut errors = 0;
121 for diag in pp.take_diagnostics() {
122 if rucc_diag::dropped(&diag, &sess.sources, opts.warnings, opts.system_header_warnings) {
124 continue;
125 }
126 let fatal = diag.severity.is_fatal()
127 || (diag.severity == Severity::Warning && opts.warnings_are_errors);
128 if fatal {
129 errors += 1;
130 }
131 messages.push(render(&diag, &sess.sources, opts.warnings_are_errors));
132 }
133 Preprocessed { text, messages, errors, deps: pp.dependencies().to_vec() }
134}
135
136fn failure(message: String) -> Preprocessed {
139 Preprocessed {
140 text: String::new(),
141 messages: vec![format!("rucc: error: {message}")],
142 errors: 1,
143 deps: Vec::new(),
144 }
145}
146
147pub(crate) fn render(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
153 let mut out = String::new();
154 let mut chain = sources.include_stack(diag.span.lo);
155 chain.reverse();
156 for (at, from) in chain.iter().enumerate() {
157 let lead = if at == 0 { "In file included from" } else { " from" };
158 out.push_str(&format!("{lead} {}:\n", sources.render_position(from.lo)));
159 }
160 out.push_str(&line(diag, sources, warnings_are_errors));
161 for child in &diag.children {
162 out.push('\n');
163 out.push_str(&line(child, sources, false));
164 }
165 out
166}
167
168fn line(diag: &Diagnostic, sources: &SourceMap, warnings_are_errors: bool) -> String {
170 let severity = if diag.severity == Severity::Warning && warnings_are_errors {
171 "error"
175 } else {
176 diag.severity.as_str()
177 };
178 let position = if diag.span.is_dummy() {
179 "rucc".to_owned()
180 } else {
181 sources.render_position(diag.span.lo)
182 };
183 match diag.code {
184 Some(code) => format!("{position}: {severity}: {} [{code}]", diag.message),
185 None => format!("{position}: {severity}: {}", diag.message),
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use rucc_session::MemoryFileSystem;
192 use rucc_target::Triple;
193
194 use super::*;
195
196 fn options() -> Options {
197 Options::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap())
198 }
199
200 fn at(dir: &str, name: &str) -> String {
206 Path::new(dir).join(name).display().to_string()
207 }
208
209 fn run(opts: &Options, files: &[(&str, &str)]) -> Preprocessed {
210 let mut fs = MemoryFileSystem::new();
211 for (path, text) in files {
212 fs.insert(*path, (*text).to_owned().into_bytes());
213 }
214 preprocess(opts, files[0].0, &fs)
215 }
216
217 #[test]
218 fn a_file_that_is_not_there_says_so_and_produces_nothing() {
219 let fs = MemoryFileSystem::new();
220 let result = preprocess(&options(), "/nope.c", &fs);
221 assert!(result.failed());
222 assert!(result.messages[0].contains("/nope.c"), "{:?}", result.messages);
223 assert!(result.text.is_empty());
224 }
225
226 #[test]
227 fn a_warning_about_a_header_that_came_with_the_machine_is_not_printed() {
228 let mut opts = options();
233 opts.search.push_system("/usr/include");
234 opts.search.push_bracket("/project");
235 let files = [
236 ("/main.c", "#define N 1\n#include <sys.h>\n#include \"own.h\"\n"),
237 ("/usr/include/sys.h", "#define N 2\n#include <inner.h>\n"),
238 ("/usr/include/inner.h", "#define N 3\n"),
239 ("/project/own.h", "#define N 4\n"),
240 ];
241
242 let result = run(&opts, &files);
247 assert_eq!(result.messages.len(), 1, "{:?}", result.messages);
248 assert!(result.messages[0].contains("own.h"), "{:?}", result.messages);
249 assert_eq!(result.errors, 0);
250
251 opts.system_header_warnings = true;
253 let result = run(&opts, &files);
254 assert_eq!(result.messages.len(), 3, "{:?}", result.messages);
255
256 opts.warnings = false;
258 let result = run(&opts, &files);
259 assert_eq!(result.messages, Vec::<String>::new());
260 }
261
262 #[test]
263 fn the_output_is_the_expanded_text_with_a_line_marker_on_top() {
264 let result = run(&options(), &[("/main.c", "#define N 2\nint a[N];\n")]);
265 assert_eq!(result.messages, Vec::<String>::new());
266 assert_eq!(result.text, "# 1 \"/main.c\"\n\nint a[2];\n");
267 }
268
269 #[test]
270 fn the_predefined_macros_are_there_without_being_asked_for() {
271 let result = run(&options(), &[("/main.c", "__SIZEOF_LONG__ __x86_64__\n")]);
272 assert_eq!(result.text, "# 1 \"/main.c\"\n8 1\n");
273 }
274
275 #[test]
276 fn dash_d_and_dash_u_reach_the_macro_table() {
277 let mut opts = options();
278 opts.defines.push("FOO=41+1".to_owned());
279 opts.defines.push("BAR".to_owned());
280 opts.undefines.push("__x86_64__".to_owned());
281 let result = run(&opts, &[("/main.c", "FOO BAR\n#ifdef __x86_64__\ngone\n#endif\n")]);
282 assert_eq!(result.text, "# 1 \"/main.c\"\n41+1 1\n");
287 }
288
289 #[test]
290 fn dash_i_is_where_an_angled_include_looks() {
291 let mut opts = options();
292 opts.search.push_bracket("/inc");
293 let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
294 let result = run(&opts, &files);
295 let one = at("/inc", "one.h").replace('\\', "\\\\");
299 let expected = format!(
300 "# 1 \"/main.c\"\n# 1 \"{one}\" 1\nint in_it;\n# 2 \"/main.c\" 2\nint after;\n"
301 );
302 assert_eq!(result.text, expected);
303 }
304
305 #[test]
306 fn dash_p_leaves_the_markers_out() {
307 let mut opts = options();
308 opts.line_markers = false;
309 opts.search.push_bracket("/inc");
310 let files = [("/main.c", "#include <one.h>\nint after;\n"), ("/inc/one.h", "int in_it;\n")];
311 assert_eq!(run(&opts, &files).text, "int in_it;\nint after;\n");
312 }
313
314 #[test]
315 fn a_diagnostic_says_where_it_is_and_carries_its_code() {
316 let result = run(&options(), &[("/main.c", "#error no\n")]);
317 assert_eq!(result.errors, 1);
318 assert!(
319 result.messages[0].starts_with("/main.c:1:8: error: no ["),
320 "{:?}",
321 result.messages
322 );
323 }
324
325 #[test]
326 fn a_diagnostic_in_a_header_prints_the_chain_that_reached_it() {
327 let mut opts = options();
328 opts.search.push_bracket("/inc");
329 let files = [
330 ("/main.c", "#include <one.h>\n"),
331 ("/inc/one.h", "#include <two.h>\n"),
332 ("/inc/two.h", "#error deep\n"),
333 ];
334 let result = run(&opts, &files);
335 let text = result.messages.join("\n");
336 assert!(text.starts_with("In file included from /main.c:1:1:\n"), "{text}");
337 assert!(
338 text.contains(&format!(" from {}:1:1:\n", at("/inc", "one.h"))),
339 "{text}"
340 );
341 assert!(text.contains(&format!("{}:1:8: error: deep", at("/inc", "two.h"))), "{text}");
342 }
343
344 #[test]
345 fn werror_turns_a_warning_into_an_error_in_the_count_and_in_the_word() {
346 let source = "#warning careful\n";
347 let plain = run(&options(), &[("/main.c", source)]);
348 assert_eq!(plain.errors, 0);
349 assert!(plain.messages[0].contains("warning: careful"), "{:?}", plain.messages);
350
351 let mut opts = options();
352 opts.warnings_are_errors = true;
353 let strict = run(&opts, &[("/main.c", source)]);
354 assert_eq!(strict.errors, 1);
355 assert!(strict.messages[0].contains("error: careful"), "{:?}", strict.messages);
356 }
357
358 #[test]
359 fn dash_dm_prints_the_macros_the_file_left_behind_and_not_the_file() {
360 let source = "#define KEPT 1\n#define GONE 2\n#undef GONE\n#ifdef NEVER\n#define \
361 HIDDEN 3\n#endif\nint x;\n";
362 let result = run(&options(), &[("/main.c", source)]);
363 assert_eq!(result.errors, 0);
364 assert!(result.text.contains("int x;"), "the output is the file without -dM");
365 assert!(!result.text.contains("#define"), "a directive line is not part of the output");
366
367 let mut opts = options();
368 opts.dumps.macros = true;
369 let dumped = run(&opts, &[("/main.c", source)]);
370 assert!(dumped.text.contains("#define KEPT 1\n"), "{}", dumped.text);
371 assert!(!dumped.text.contains("int x;"), "-dM replaces the output rather than adding");
372 assert!(!dumped.text.contains("GONE"), "{}", dumped.text);
375 assert!(!dumped.text.contains("HIDDEN"), "{}", dumped.text);
376 assert!(dumped.text.contains("#define __x86_64__ 1\n"), "{}", dumped.text);
379 }
380
381 #[test]
382 fn the_dialect_reaches_the_predefined_set() {
383 let mut opts = options();
384 opts.std = rucc_session::Std::C99;
385 opts.gnu_extensions = false;
386 let result = run(&opts, &[("/main.c", "__STDC_VERSION__ __STRICT_ANSI__\n")]);
387 assert_eq!(result.text, "# 1 \"/main.c\"\n199901L 1\n");
388 }
389}