1#![doc(html_root_url = "https://docs.rs/rucc-driver/0.4.1")]
28
29pub mod compile;
30pub mod library;
31pub mod link;
32mod map;
33pub mod phase;
34pub mod preprocess;
35pub mod schedule;
36
37use std::fmt::Write as _;
38use std::io::Write as _;
39use std::path::PathBuf;
40
41use rucc_codegen::coverage::{self, Fired};
42use rucc_session::{Dumps, EmitKind, Options, Session, Std, runtime};
43use rucc_target::Triple;
44
45use crate::link::LinkOptions;
46
47pub use crate::compile::{Artifact, Compiled, compile, compile_ir};
48pub use crate::phase::{Input, InputKind, Job, LinkJob, Output, Phase, Plan};
49pub use crate::preprocess::{OsFileSystem, Preprocessed, preprocess};
50pub use crate::schedule::Jobs;
51
52pub const VERSION: &str = env!("CARGO_PKG_VERSION");
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum Action {
58 Help,
60 Version,
62 PrintConfig(Box<Options>),
64 PrintPipeline(Box<Options>),
66 PrintPlan {
68 opts: Box<Options>,
70 plan: Box<Plan>,
72 link: Box<LinkOptions>,
74 },
75 Compile {
77 opts: Box<Options>,
79 plan: Box<Plan>,
81 link: Box<LinkOptions>,
83 jobs: Jobs,
85 verbose: bool,
87 },
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct CliError {
93 pub message: String,
96}
97
98impl std::fmt::Display for CliError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(&self.message)
101 }
102}
103
104impl std::error::Error for CliError {}
105
106fn err(message: impl Into<String>) -> CliError {
107 CliError { message: message.into() }
108}
109
110pub const USAGE: &str = "\
115rucc, an optimizing C compiler
116
117usage: rucc [options] file...
118
119options:
120 -c compile and assemble, do not link
121 -S compile only, emit assembly
122 -E preprocess only
123 -o <file> write output to <file>, or to standard output for -
124 -D <name>[=<value>], -U <name> define a macro, or undefine one after every -D
125 -I <dir> add <dir> to the include search path
126 -iquote -isystem -idirafter <dir> the other chains, -nostdinc drops ours
127 --sysroot=<dir> look for the library's headers under <dir>, -isysroot too
128 -P, -dM with -E: leave out the markers, or dump the macros
129 -std=<dialect> c89 through c23, and the gnu spellings
130 -fgnuc-version=<v> the GCC release to claim, default 7.0.0
131 -x <lang> treat later inputs as <lang>, or none to stop
132 -O<level> optimize: 0, 1, 2, 3, s, z
133 -f<pass> -fno-<pass> -fpass-fuel=<pass>=<n> -fdump-ir=<what> the optimizer's own flags
134 -g, -fno-omit-frame-pointer, -mno-red-zone debug info, keep a frame pointer, no red zone
135 -l<name>, -L <dir>, -B <dir> link a library, where to look for one, where our own tools are
136 -static -shared -pie -no-pie -nostdlib -nostartfiles -nodefaultlibs -rdynamic -s how to link
137 -Wl,<arg>, -Xlinker <arg>, -fuse-ld=<name> hand an argument to the linker, or pick one
138 -Werror -pedantic warnings are errors, diagnose what the standard forbids
139 -j[n] compile n translation units at once, default all
140 -v, -### print each phase as it runs, or without running any
141 --target=<triple> generate code for <triple>
142 --emit=<kind> exe, obj, asm, preprocessed, tast, ir, mir-final
143 --print-config, --print-pipeline print the configuration or the pipeline, and exit
144 --version print the version and exit
145 -h, --help print this message and exit
146
147See spec/04-driver-and-cli.md for the full flag reference.
148";
149
150fn joined_or_next(
154 arg: &str,
155 at: usize,
156 args: &[String],
157 i: &mut usize,
158) -> Result<String, CliError> {
159 if arg.len() > at {
160 return Ok(arg[at..].to_owned());
161 }
162 let next = args.get(*i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
163 *i += 1;
164 Ok(next.clone())
165}
166
167pub fn parse_args(args: &[String]) -> Result<Action, CliError> {
174 let host = Triple::host()
175 .ok_or_else(|| err("this host is not a supported target and no --target was given"))?;
176 let mut opts = Options::new(host);
177 let mut inputs: Vec<Input> = Vec::new();
178 let mut print_config = false;
179 let mut print_pipeline = false;
180 let mut print_plan = false;
181 let mut verbose = false;
182 let mut jobs = Jobs::default();
183 let mut nostdinc = false;
184 let mut sysroot: Option<PathBuf> = None;
185 let mut output = None;
186 let mut link = LinkOptions::default();
187 let mut forced: Option<InputKind> = None;
190
191 let mut i = 0;
192 while i < args.len() {
193 let arg = args[i].as_str();
194 i += 1;
195 match arg {
196 "-h" | "--help" => return Ok(Action::Help),
197 "--version" => return Ok(Action::Version),
198 "--print-config" => print_config = true,
199 "--print-pipeline" => print_pipeline = true,
200 "-###" => print_plan = true,
201 "-v" => verbose = true,
202 "-c" => opts.emit = EmitKind::Object,
203 "-S" => opts.emit = EmitKind::Asm,
204 "-E" => opts.emit = EmitKind::Preprocessed,
205 "-g" => opts.debug_info = true,
206 "-Werror" => opts.warnings_are_errors = true,
207 "-P" => opts.line_markers = false,
208 "-ansi" => {
209 opts.std = Std::C89;
210 opts.gnu_extensions = false;
211 }
212 "-pedantic" | "-Wpedantic" => opts.pedantic = true,
215 "-ffreestanding" => opts.hosted = false,
216 "-fhosted" => opts.hosted = true,
217 "-fno-omit-frame-pointer" => opts.frame_pointer = true,
220 "-fomit-frame-pointer" => opts.frame_pointer = false,
221 "-mno-red-zone" => opts.red_zone = false,
222 "-mred-zone" => opts.red_zone = true,
223 "-nostdinc" => nostdinc = true,
227 "-o" => {
228 output = Some(args.get(i).ok_or_else(|| err("-o requires an argument"))?.clone());
229 i += 1;
230 }
231 "-isysroot" => {
238 let dir = args.get(i).ok_or_else(|| err("-isysroot requires an argument"))?;
239 i += 1;
240 sysroot = Some(PathBuf::from(dir));
241 }
242 "-iquote" | "-isystem" | "-idirafter" => {
243 let dir = args.get(i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
244 i += 1;
245 match arg {
246 "-iquote" => opts.search.push_quote(dir.clone()),
247 "-isystem" => opts.search.push_system(dir.clone()),
248 _ => opts.search.push_after(dir.clone()),
249 }
250 }
251 "-x" => {
252 let lang = args.get(i).ok_or_else(|| err("-x requires an argument"))?;
253 i += 1;
254 forced = if lang == "none" {
255 None
256 } else {
257 Some(InputKind::from_x_arg(lang).map_err(|e| err(format!("{e}")))?)
258 };
259 }
260 _ if arg.starts_with("-D") => {
268 let value = joined_or_next(arg, 2, args, &mut i)?;
269 opts.defines.push(value);
270 }
271 _ if arg.starts_with("-U") => {
272 let value = joined_or_next(arg, 2, args, &mut i)?;
273 opts.undefines.push(value);
274 }
275 _ if arg.starts_with("-I") => {
276 let dir = joined_or_next(arg, 2, args, &mut i)?;
277 opts.search.push_bracket(dir);
278 }
279 _ if arg.starts_with("-std=") => {
280 let name = &arg["-std=".len()..];
281 let (std, gnu) = Std::from_flag(name)
282 .ok_or_else(|| err(format!("unknown dialect `{name}`, see --help")))?;
283 opts.std = std;
284 opts.gnu_extensions = gnu;
285 }
286 _ if Dumps::is_family(arg) => {
295 opts.dumps.add(&arg[2..]);
296 }
297 _ if arg.starts_with("-fgnuc-version=") => {
298 let v = &arg["-fgnuc-version=".len()..];
299 opts.gnuc = v.parse().map_err(err)?;
300 }
301 "-fnested-functions" => {
306 return Err(err(
307 "nested functions are not supported: a call to one goes through a trampoline \
308 written on the stack, which no target that enforces an unexecutable stack \
309 allows",
310 ));
311 }
312 "-fno-nested-functions" => {}
313 "-static" => link.is_static = true,
317 "-shared" => link.shared = true,
318 "-pie" => link.pie = Some(true),
319 "-no-pie" | "-nopie" => link.pie = Some(false),
320 "-nostdlib" => link.no_stdlib = true,
321 "-nostartfiles" => link.no_startfiles = true,
322 "-nodefaultlibs" => link.no_defaultlibs = true,
323 "-fno-builtins-lib" => link.no_builtins_lib = true,
324 "-fbuiltins-lib" => link.no_builtins_lib = false,
325 "-rdynamic" | "-export-dynamic" => link.export_dynamic = true,
326 "-s" => link.strip = true,
327 "-Xlinker" => {
328 let next = args.get(i).ok_or_else(|| err("-Xlinker requires an argument"))?;
329 i += 1;
330 link.passthrough.push(next.clone());
331 }
332 _ if arg.starts_with("-Wl,") => {
333 link.passthrough.extend(arg["-Wl,".len()..].split(',').map(str::to_owned));
336 }
337 _ if arg.starts_with("-fuse-ld=") => {
338 link.use_ld = Some(arg["-fuse-ld=".len()..].to_owned());
339 }
340 _ if arg.starts_with("-l") && arg.len() > 2 => {
341 inputs.push(Input::library(&arg[2..]));
342 }
343 "-l" => {
344 let next = args.get(i).ok_or_else(|| err("-l requires an argument"))?;
345 i += 1;
346 inputs.push(Input::library(next));
347 }
348 _ if arg.starts_with("-L") => {
349 link.search.push(PathBuf::from(joined_or_next(arg, 2, args, &mut i)?));
350 }
351 _ if arg.starts_with("-B") => {
352 link.prefixes.push(PathBuf::from(joined_or_next(arg, 2, args, &mut i)?));
353 }
354 _ if arg.starts_with("-j") => {
355 jobs = Jobs::parse(&arg[2..]).map_err(err)?;
356 }
357 _ if arg.starts_with("--sysroot=") => {
358 sysroot = Some(PathBuf::from(&arg["--sysroot=".len()..]));
359 }
360 _ if arg.starts_with("--target=") => {
361 let t = &arg["--target=".len()..];
362 opts.target = t.parse().map_err(|e| err(format!("{e}")))?;
363 }
364 _ if arg.starts_with("--emit=") => {
365 let k = &arg["--emit=".len()..];
366 opts.emit = k
367 .parse()
368 .map_err(|()| err(format!("unknown --emit kind `{k}`, see --help")))?;
369 }
370 _ if arg.starts_with("-O") => {
371 opts.opt_level = arg[2..]
372 .parse()
373 .map_err(|()| err(format!("unknown optimization level `{arg}`")))?;
374 }
375 _ if arg.starts_with("-fpass-fuel=") => {
379 let (name, count) = arg["-fpass-fuel=".len()..]
380 .split_once('=')
381 .ok_or_else(|| err("-fpass-fuel= is spelled <pass>=<count>"))?;
382 if rucc_opt::pass::find(name).is_none() {
383 return Err(err(format!(
384 "`{name}` is not a pass this compiler has, see --print-pipeline"
385 )));
386 }
387 let count: u32 = count
388 .parse()
389 .map_err(|_| err(format!("`{count}` is not a number of transformations")))?;
390 opts.pass_fuel.push((name.to_owned(), count));
391 }
392 _ if arg.starts_with("-fdump-ir=") => {
393 let spec = &arg["-fdump-ir=".len()..];
396 rucc_opt::Dumps::default().add(spec).map_err(err)?;
397 opts.dump_ir.push(spec.to_owned());
398 }
399 _ if arg.strip_prefix("-fno-").is_some_and(|n| rucc_opt::pass::find(n).is_some()) => {
400 opts.passes.push((arg["-fno-".len()..].to_owned(), false));
401 }
402 _ if arg.strip_prefix("-f").is_some_and(|n| rucc_opt::pass::find(n).is_some()) => {
403 opts.passes.push((arg["-f".len()..].to_owned(), true));
404 }
405 "-Zverify-each" => opts.verify_each = true,
411 _ if arg.starts_with("-Zrule-coverage=") => {
412 let file = &arg["-Zrule-coverage=".len()..];
413 if file.is_empty() {
414 return Err(err("-Zrule-coverage= needs a file to write to"));
415 }
416 opts.rule_coverage = Some(file.to_owned());
417 }
418 _ if arg.starts_with("-Z") => {
419 return Err(err(format!(
420 "`{arg}` is not an unstable option this compiler has, see \
421 spec/04-driver-and-cli.md section 4.11 for the ones it does"
422 )));
423 }
424 _ if arg.starts_with('-') && arg.len() > 1 => {
425 return Err(err(format!("unknown option `{arg}`")));
430 }
431 _ => inputs.push(Input { path: arg.to_owned(), forced, library: false }),
432 }
433 }
434
435 link.sysroot = sysroot.clone();
442 if !nostdinc {
443 opts.search.push_system(runtime::DIR);
444 for dir in library::system_dirs(opts.target, sysroot.as_deref()) {
448 opts.search.push_system(dir);
449 }
450 }
451 opts.search.remove_duplicates();
455
456 if print_config {
459 return Ok(Action::PrintConfig(Box::new(opts)));
460 }
461 if print_pipeline {
462 return Ok(Action::PrintPipeline(Box::new(opts)));
463 }
464 let plan = Plan::new(&opts, &inputs, output.as_deref()).map_err(|e| err(e.message))?;
465 if print_plan {
466 return Ok(Action::PrintPlan {
467 opts: Box::new(opts),
468 plan: Box::new(plan),
469 link: Box::new(link),
470 });
471 }
472 Ok(Action::Compile {
473 opts: Box::new(opts),
474 plan: Box::new(plan),
475 link: Box::new(link),
476 jobs,
477 verbose,
478 })
479}
480
481#[must_use]
487pub fn print_pipeline(opts: &Options) -> String {
488 let mut settings = rucc_opt::Options::for_level(opts.opt_level);
489 settings.toggles.clone_from(&opts.passes);
490 rucc_opt::pipeline::print(&settings)
491}
492
493#[must_use]
498pub fn print_config(opts: &Options) -> String {
499 let sess = Session::new(opts.clone());
500 let t = &sess.target;
501 let mut out = String::new();
502 let _ = writeln!(out, "version: {VERSION}");
503 let _ = writeln!(out, "target: {}", t.triple);
504 let _ = writeln!(out, "arch: {}", t.triple.arch.as_str());
505 let _ = writeln!(out, "os: {}", t.triple.os.as_str());
506 let _ = writeln!(out, "env: {}", t.triple.env.as_str());
507 let _ = writeln!(out, "object-format: {}", t.object_format.as_str());
508 let _ = writeln!(out, "pointer-width: {}", t.pointer_width);
509 let _ = writeln!(out, "long-width: {}", t.long_width);
510 let _ = writeln!(out, "long-double-width: {}", t.long_double_width);
511 let _ = writeln!(out, "endian: {}", if t.little_endian { "little" } else { "big" });
512 let _ = writeln!(out, "char-signed: {}", t.char_is_signed);
513 let _ = writeln!(out, "va-list: {}", t.va_list.as_str());
514 let regs: Vec<String> = t
517 .regs
518 .classes()
519 .map(|(class, info)| format!("{} {}", info.name, t.regs.len(class)))
520 .collect();
521 let _ = writeln!(
522 out,
523 "registers: {}",
524 if regs.is_empty() { "none".to_string() } else { regs.join(", ") }
525 );
526 let _ = writeln!(out, "opt-level: {}", sess.opts.opt_level);
527 let _ = writeln!(out, "emit: {}", sess.opts.emit.as_str());
528 let _ = writeln!(out, "debug-info: {}", sess.opts.debug_info);
529 let _ = writeln!(out, "frame-pointer: {}", sess.opts.frame_pointer);
530 let _ = writeln!(out, "red-zone: {}", sess.opts.red_zone);
531 for dir in sess.opts.search.dirs() {
534 let system = if dir.is_system { " (system)" } else { "" };
535 let _ = writeln!(out, "include: {}{system}", dir.path.display());
536 }
537 out
538}
539
540fn preprocess_all(opts: &Options, plan: &Plan) -> i32 {
546 let fs = OsFileSystem::new();
547 let mut stderr = std::io::stderr().lock();
548 let mut failed = false;
549 for job in &plan.jobs {
550 if !job.phases.first().is_some_and(|p| *p == Phase::Preprocess) {
551 continue;
554 }
555 let result = preprocess(opts, &job.input, &fs);
556 for message in &result.messages {
557 let _ = writeln!(stderr, "{message}");
558 }
559 if result.failed() {
560 failed = true;
561 continue;
562 }
563 if let Err(e) = write_out(&job.output, result.text.as_bytes()) {
564 let _ = writeln!(stderr, "rucc: error: {e}");
565 failed = true;
566 }
567 }
568 i32::from(failed)
569}
570
571fn compile_all(opts: &Options, plan: &Plan) -> i32 {
577 let fs = OsFileSystem::new();
578 let mut stderr = std::io::stderr().lock();
579 let mut failed = false;
580 let mut fired = Fired::new();
581 for job in &plan.jobs {
582 if !job.phases.contains(&Phase::Compile) {
583 continue;
584 }
585 let result = if job.kind == InputKind::Ir {
589 compile_ir(opts, &job.input, &fs)
590 } else {
591 compile(opts, &job.input, &fs)
592 };
593 fired.merge(&result.fired);
594 failed |= !write_dumps(&job.input, &result.dumps, &mut stderr);
595 for message in &result.messages {
596 let _ = writeln!(stderr, "{message}");
597 }
598 if result.failed() {
599 failed = true;
600 continue;
601 }
602 if let Err(e) = write_out(&job.output, result.artifact.bytes()) {
603 let _ = writeln!(stderr, "rucc: error: {e}");
604 failed = true;
605 }
606 }
607 failed |= !write_coverage(opts, &fired, &mut stderr);
608 i32::from(failed)
609}
610
611struct Scratch {
618 dir: PathBuf,
620}
621
622impl Scratch {
623 fn new() -> Result<Scratch, String> {
629 let dir = std::env::temp_dir().join(format!("rucc-{}", std::process::id()));
630 std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
631 Ok(Scratch { dir })
632 }
633}
634
635impl Drop for Scratch {
636 fn drop(&mut self) {
637 let _ = std::fs::remove_dir_all(&self.dir);
638 }
639}
640
641fn link_line(opts: &Options, link: &LinkOptions, job: &LinkJob) -> Result<String, link::Error> {
648 let linker = link::find(opts.target, link)?;
649 let args = link::line(opts.target, link, &job.inputs, &job.output)?;
650 Ok(link::render(&linker, &args))
651}
652
653fn link_all(opts: &Options, plan: &Plan, link: &LinkOptions, verbose: bool) -> i32 {
660 let Some(job) = &plan.link else {
661 let mut stderr = std::io::stderr().lock();
664 let _ = writeln!(stderr, "rucc: error: there is nothing to link");
665 return 1;
666 };
667 let linker = match link::find(opts.target, link) {
670 Ok(linker) => linker,
671 Err(why) => return complain(why),
672 };
673
674 let scratch = match Scratch::new() {
675 Ok(scratch) => scratch,
676 Err(why) => return complain(format!("could not make a place for the object files: {why}")),
677 };
678
679 let fs = OsFileSystem::new();
680 let mut failed = false;
681 let mut produced: Vec<String> = Vec::with_capacity(plan.jobs.len());
684 let mut fired = Fired::new();
685 {
686 let mut stderr = std::io::stderr().lock();
687 for (at, job) in plan.jobs.iter().enumerate() {
688 let out = match &job.output {
689 Output::Temporary(hint) => {
690 scratch.dir.join(format!("{at}-{hint}")).display().to_string()
693 }
694 Output::File(path) => path.clone(),
695 Output::Stdout => continue,
698 };
699 produced.push(out.clone());
700 if !job.phases.contains(&Phase::Compile) {
701 continue;
702 }
703 let result = if job.kind == InputKind::Ir {
704 compile_ir(opts, &job.input, &fs)
705 } else {
706 compile(opts, &job.input, &fs)
707 };
708 fired.merge(&result.fired);
709 failed |= !write_dumps(&job.input, &result.dumps, &mut stderr);
710 for message in &result.messages {
711 let _ = writeln!(stderr, "{message}");
712 }
713 if result.failed() {
714 failed = true;
715 continue;
716 }
717 if !matches!(result.artifact, Artifact::Object(_)) {
718 let _ = writeln!(
723 stderr,
724 "rucc: internal error: {}: no object file was produced for the link",
725 job.input
726 );
727 failed = true;
728 continue;
729 }
730 if let Err(e) = std::fs::write(&out, result.artifact.bytes()) {
731 let _ = writeln!(stderr, "rucc: error: {out}: {e}");
732 failed = true;
733 }
734 }
735 failed |= !write_coverage(opts, &fired, &mut stderr);
736 }
737 if failed {
738 return 1;
742 }
743
744 let mut outputs = produced.into_iter();
748 let mut items = Vec::with_capacity(job.inputs.len());
749 for item in &job.inputs {
750 match item {
751 link::Item::Library(name) => items.push(link::Item::Library(name.clone())),
752 link::Item::File(_) => match outputs.next() {
753 Some(path) => items.push(link::Item::File(path)),
754 None => return complain("the plan asks the linker for a file nothing produced"),
755 },
756 }
757 }
758
759 let args = match link::line(opts.target, link, &items, &job.output) {
760 Ok(args) => args,
761 Err(why) => return complain(why),
762 };
763 if verbose {
764 let mut stderr = std::io::stderr().lock();
765 let _ = writeln!(stderr, "{}", link::render(&linker, &args));
766 }
767 match link::run(&linker, &args) {
768 Ok(()) => 0,
769 Err(link::Error::Refused { .. }) => 1,
772 Err(why) => complain(why),
773 }
774}
775
776fn complain(why: impl std::fmt::Display) -> i32 {
778 let mut stderr = std::io::stderr().lock();
779 let _ = writeln!(stderr, "rucc: error: {why}");
780 1
781}
782
783fn write_coverage(opts: &Options, fired: &Fired, stderr: &mut impl std::io::Write) -> bool {
792 let Some(path) = &opts.rule_coverage else { return true };
793 let Some(table) = coverage::table(opts.target.arch) else {
794 let _ = writeln!(
795 stderr,
796 "rucc: error: there are no lowering rules for {} yet, so there is no coverage of them \
797 to report",
798 opts.target
799 );
800 return false;
801 };
802 match std::fs::write(path, fired.listing(table)) {
803 Ok(()) => true,
804 Err(e) => {
805 let _ = writeln!(stderr, "rucc: error: {path}: {e}");
806 false
807 }
808 }
809}
810
811fn write_dumps(input: &str, dumps: &[rucc_opt::Dump], stderr: &mut impl std::io::Write) -> bool {
822 let stem = std::path::Path::new(input)
823 .file_name()
824 .map_or_else(|| input.to_owned(), |name| name.to_string_lossy().into_owned());
825 let mut ok = true;
826 for dump in dumps {
827 let path = format!("{stem}.{}.ir", dump.name);
828 if let Err(e) = std::fs::write(&path, &dump.text) {
829 let _ = writeln!(stderr, "rucc: error: {path}: {e}");
830 ok = false;
831 }
832 }
833 ok
834}
835
836fn write_out(output: &Output, bytes: &[u8]) -> Result<(), String> {
843 match output {
844 Output::Stdout => {
845 let mut stdout = std::io::stdout().lock();
846 stdout.write_all(bytes).map_err(|e| format!("writing to standard output: {e}"))
847 }
848 Output::File(path) | Output::Temporary(path) => {
849 std::fs::write(path, bytes).map_err(|e| format!("{path}: {e}"))
850 }
851 }
852}
853
854pub fn run(args: &[String]) -> i32 {
859 match parse_args(args) {
860 Ok(Action::Help) => {
861 print!("{USAGE}");
862 0
863 }
864 Ok(Action::Version) => {
865 println!("rucc {VERSION}");
866 0
867 }
868 Ok(Action::PrintConfig(opts)) => {
869 print!("{}", print_config(&opts));
870 0
871 }
872 Ok(Action::PrintPipeline(opts)) => {
873 print!("{}", print_pipeline(&opts));
874 0
875 }
876 Ok(Action::PrintPlan { opts, plan, link }) => {
877 print!("{}", plan.render());
878 if let Some(job) = &plan.link {
882 match link_line(&opts, &link, job) {
883 Ok(line) => println!("{line}"),
884 Err(why) => {
885 let mut stderr = std::io::stderr().lock();
886 let _ = writeln!(stderr, "rucc: error: {why}");
887 return 1;
888 }
889 }
890 }
891 0
892 }
893 Ok(Action::Compile { opts, plan, link, jobs, verbose }) => {
894 {
895 let mut stderr = std::io::stderr().lock();
896 if verbose {
897 let _ = write!(stderr, "{}", plan.render());
898 let _ = writeln!(stderr, "workers: {}", jobs.count());
899 }
900 }
901 if opts.emit == EmitKind::Preprocessed {
902 return preprocess_all(&opts, &plan);
903 }
904 if opts.emit != EmitKind::Executable {
905 return compile_all(&opts, &plan);
906 }
907 link_all(&opts, &plan, &link, verbose)
908 }
909 Err(e) => {
910 let mut stderr = std::io::stderr().lock();
911 let _ = writeln!(stderr, "rucc: error: {e}");
912 let _ = writeln!(stderr, "rucc: note: run `rucc --help` for usage");
913 1
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use rucc_session::{GnucVersion, OptLevel};
921
922 use super::*;
923
924 fn args(s: &[&str]) -> Vec<String> {
925 s.iter().map(|x| (*x).to_owned()).collect()
926 }
927
928 #[test]
929 fn help_and_version_win_over_everything_else() {
930 assert_eq!(parse_args(&args(&["-c", "--help", "x.c"])).unwrap(), Action::Help);
931 assert_eq!(parse_args(&args(&["--version"])).unwrap(), Action::Version);
932 }
933
934 fn compile(s: &[&str]) -> (Box<Options>, Box<Plan>) {
935 match parse_args(&args(s)).expect("expected a compilation") {
936 Action::Compile { opts, plan, .. } => (opts, plan),
937 other => panic!("expected a compilation, got {other:?}"),
938 }
939 }
940
941 fn linking(s: &[&str]) -> (Box<LinkOptions>, Box<Plan>) {
942 match parse_args(&args(s)).expect("expected a compilation") {
943 Action::Compile { link, plan, .. } => (link, plan),
944 other => panic!("expected a compilation, got {other:?}"),
945 }
946 }
947
948 #[test]
949 fn collects_inputs_and_flags() {
950 let (opts, plan) = compile(&["-c", "-O2", "-g", "a.c", "b.c"]);
951 let paths: Vec<&str> = plan.jobs.iter().map(|j| j.input.as_str()).collect();
952 assert_eq!(paths, vec!["a.c", "b.c"]);
953 assert_eq!(opts.opt_level, OptLevel::O2);
954 assert_eq!(opts.emit, EmitKind::Object);
955 assert!(opts.debug_info);
956 }
957
958 #[test]
961 fn an_unstable_option_is_taken_and_one_that_does_not_exist_is_refused() {
962 let (opts, _) = compile(&["-c", "-Zrule-coverage=/tmp/rules.cov", "a.c"]);
963 assert_eq!(opts.rule_coverage.as_deref(), Some("/tmp/rules.cov"));
964
965 let (plain, _) = compile(&["-c", "a.c"]);
966 assert_eq!(plain.rule_coverage, None, "nothing is measured unless it was asked for");
967
968 assert!(parse_args(&args(&["-Zrule-coverage=", "a.c"])).is_err(), "a file with no name");
969 let unknown = parse_args(&args(&["-Zwhat", "a.c"])).expect_err("there is no such option");
970 assert!(unknown.message.contains("4.11"), "{}", unknown.message);
971 }
972
973 #[test]
974 fn a_bare_dash_o_means_o1_the_way_gcc_reads_it() {
975 let (opts, _) = compile(&["-O", "a.c"]);
976 assert_eq!(opts.opt_level, OptLevel::O1);
977 }
978
979 #[test]
980 fn dash_x_applies_to_later_inputs_only_and_none_stops_it() {
981 let (_, plan) = compile(&["a.o", "-x", "c", "b.txt", "-x", "none", "c.o"]);
982 assert_eq!(plan.jobs[0].kind, InputKind::LinkerInput);
983 assert_eq!(plan.jobs[1].kind, InputKind::C);
984 assert_eq!(plan.jobs[2].kind, InputKind::LinkerInput);
985 }
986
987 #[test]
988 fn dash_j_reaches_the_scheduler_and_defaults_to_the_machine() {
989 let (_, _, jobs) = match parse_args(&args(&["-j4", "a.c"])).unwrap() {
990 Action::Compile { opts, plan, jobs, .. } => (opts, plan, jobs),
991 other => panic!("expected a compilation, got {other:?}"),
992 };
993 assert_eq!(jobs.count(), 4);
994
995 let default = match parse_args(&args(&["a.c"])).unwrap() {
996 Action::Compile { jobs, .. } => jobs,
997 other => panic!("expected a compilation, got {other:?}"),
998 };
999 assert_eq!(default, Jobs::available());
1000 assert!(parse_args(&args(&["-j0", "a.c"])).is_err());
1001 }
1002
1003 #[test]
1004 fn triple_hash_prints_the_plan_and_runs_nothing() {
1005 let a = parse_args(&args(&["-###", "-c", "a.c"])).unwrap();
1006 let Action::PrintPlan { plan, .. } = a else { panic!("expected a plan dump") };
1007 assert!(plan.render().contains("a.c: preprocess, compile, assemble -> a.o"));
1008 }
1009
1010 #[test]
1011 fn dash_x_names_what_it_accepts_when_it_does_not_know_a_language() {
1012 let e = parse_args(&args(&["-x", "fortran", "a.c"])).unwrap_err();
1013 assert!(e.message.contains("assembler-with-cpp"), "{}", e.message);
1014 }
1015
1016 #[test]
1017 fn an_unknown_flag_is_an_error_rather_than_a_shrug() {
1018 let e = parse_args(&args(&["-fno-such-thing", "a.c"])).unwrap_err();
1019 assert!(e.message.contains("unknown option"), "{}", e.message);
1020 }
1021
1022 #[test]
1023 fn asking_for_nested_functions_is_told_why_it_is_not_coming() {
1024 let e = parse_args(&args(&["-fnested-functions", "a.c"])).unwrap_err();
1025 assert!(e.message.contains("trampoline"), "{}", e.message);
1026 assert!(parse_args(&args(&["-fno-nested-functions", "a.c"])).is_ok());
1027 }
1028
1029 #[test]
1030 fn an_unsupported_target_names_itself() {
1031 let e = parse_args(&args(&["--target=sparc64-linux-gnu", "a.c"])).unwrap_err();
1032 assert!(e.message.contains("sparc64"), "{}", e.message);
1033 }
1034
1035 #[test]
1036 fn no_inputs_is_an_error_but_print_config_needs_none() {
1037 assert!(parse_args(&args(&[])).is_err());
1038 assert!(matches!(parse_args(&args(&["--print-config"])), Ok(Action::PrintConfig(_))));
1039 }
1040
1041 #[test]
1042 fn print_config_reports_the_target_it_was_given_not_the_host() {
1043 let a = parse_args(&args(&["--print-config", "--target=riscv64-linux-musl"])).unwrap();
1044 let Action::PrintConfig(opts) = a else { panic!("expected a configuration dump") };
1045 let text = print_config(&opts);
1046 assert!(text.contains("target: riscv64-unknown-linux-musl"), "{text}");
1047 assert!(text.contains("char-signed: false"), "{text}");
1048 assert!(text.contains("object-format: elf"), "{text}");
1049 assert!(text.contains("va-list: void-pointer"), "{text}");
1050 assert!(text.contains("registers: none"), "{text}");
1053 }
1054
1055 #[test]
1056 fn print_config_has_one_key_per_line_and_a_fixed_order() {
1057 let opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
1058 let text = print_config(&opts);
1059 let keys: Vec<&str> =
1060 text.lines().map(|l| l.split(':').next().unwrap_or_default()).collect();
1061 assert_eq!(keys[0], "version");
1062 assert_eq!(keys[1], "target");
1063 assert_eq!(keys.len(), 18);
1064 assert!(text.ends_with('\n'));
1065 }
1066
1067 #[test]
1068 fn print_pipeline_answers_with_the_passes_the_level_asked_for() {
1069 let a = parse_args(&args(&["--print-pipeline", "-O2"])).unwrap();
1070 let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
1071 let text = print_pipeline(&opts);
1072 assert!(text.starts_with("level: -O2\n"), "{text}");
1073 assert!(text.contains("fold"), "{text}");
1074
1075 let a = parse_args(&args(&["--print-pipeline"])).unwrap();
1076 let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
1077 assert!(print_pipeline(&opts).contains("no passes"), "{}", print_pipeline(&opts));
1079 }
1080
1081 #[test]
1082 fn print_pipeline_takes_the_toggles_into_account() {
1083 let a = parse_args(&args(&["--print-pipeline", "-O2", "-fno-fold"])).unwrap();
1084 let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
1085 let text = print_pipeline(&opts);
1086 assert!(!text.contains("fold"), "{text}");
1089 assert!(text.contains("dce"), "{text}");
1090
1091 let mut off = vec!["--print-pipeline".to_owned(), "-O2".to_owned()];
1095 off.extend(rucc_opt::PASSES.iter().map(|p| format!("-fno-{}", p.name())));
1096 let spelled: Vec<&str> = off.iter().map(String::as_str).collect();
1097 let a = parse_args(&args(&spelled)).unwrap();
1098 let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
1099 assert!(print_pipeline(&opts).contains("no passes"), "{}", print_pipeline(&opts));
1100 }
1101
1102 #[test]
1105 fn a_pass_is_named_by_dash_f_and_unnamed_by_dash_f_no() {
1106 let (opts, _) = compile(&["-c", "-O0", "-ffold", "-fno-fold", "-ffold", "a.c"]);
1107 assert_eq!(
1108 opts.passes,
1109 [("fold".to_owned(), true), ("fold".to_owned(), false), ("fold".to_owned(), true)]
1110 );
1111
1112 let e = parse_args(&args(&["-fno-such-pass", "a.c"])).unwrap_err();
1113 assert!(e.message.contains("unknown option"), "{}", e.message);
1114 }
1115
1116 #[test]
1117 fn pass_fuel_names_a_pass_and_a_count_and_refuses_anything_else() {
1118 let (opts, _) = compile(&["-c", "-O2", "-fpass-fuel=fold=3", "a.c"]);
1119 assert_eq!(opts.pass_fuel, [("fold".to_owned(), 3)]);
1120
1121 let e = parse_args(&args(&["-fpass-fuel=fold", "a.c"])).unwrap_err();
1122 assert!(e.message.contains("<pass>=<count>"), "{}", e.message);
1123 let e = parse_args(&args(&["-fpass-fuel=nosuch=3", "a.c"])).unwrap_err();
1124 assert!(e.message.contains("--print-pipeline"), "{}", e.message);
1125 let e = parse_args(&args(&["-fpass-fuel=fold=lots", "a.c"])).unwrap_err();
1126 assert!(e.message.contains("not a number"), "{}", e.message);
1127 }
1128
1129 #[test]
1133 fn a_dump_is_checked_when_it_is_asked_for_rather_than_when_it_is_taken() {
1134 let (opts, _) = compile(&["-c", "-O2", "-fdump-ir=all", "-fdump-ir=after-fold", "a.c"]);
1135 assert_eq!(opts.dump_ir, ["all", "after-fold"]);
1136
1137 let e = parse_args(&args(&["-fdump-ir=after-nosuch", "a.c"])).unwrap_err();
1138 assert!(e.message.contains("nosuch"), "{}", e.message);
1139 assert!(parse_args(&args(&["-fdump-ir=sideways-fold", "a.c"])).is_err());
1140 }
1141
1142 #[test]
1143 fn verify_each_is_unstable_and_off_unless_it_was_asked_for() {
1144 let (opts, _) = compile(&["-c", "-Zverify-each", "a.c"]);
1145 assert!(opts.verify_each);
1146 assert!(!USAGE.contains("verify-each"), "an unstable option stays out of the usage text");
1147 }
1148
1149 #[test]
1150 fn dash_o_needs_an_argument() {
1151 let e = parse_args(&args(&["a.c", "-o"])).unwrap_err();
1152 assert_eq!(e.message, "-o requires an argument");
1153 }
1154
1155 #[test]
1156 fn dash_d_and_dash_u_are_read_joined_or_separated_and_keep_their_order() {
1157 let (opts, _) = compile(&["-DFOO=1", "-D", "BAR", "-UBAZ", "-U", "QUX", "a.c"]);
1158 assert_eq!(opts.defines, ["FOO=1", "BAR"]);
1159 assert_eq!(opts.undefines, ["BAZ", "QUX"]);
1160 }
1161
1162 #[test]
1163 fn the_include_flags_land_on_the_chain_each_one_names() {
1164 let (opts, _) = compile(&[
1167 "-Ii",
1168 "-iquote",
1169 "q",
1170 "-isystem",
1171 "sys",
1172 "-idirafter",
1173 "after",
1174 "--sysroot=/nowhere-at-all",
1175 "a.c",
1176 ]);
1177 let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
1178 assert_eq!(dirs, ["q", "i", "sys", runtime::DIR, "after"]);
1181 assert!(!opts.search.dirs()[1].is_system);
1182 assert!(opts.search.dirs()[2].is_system);
1183 }
1184
1185 #[test]
1186 fn the_librarys_headers_come_after_the_compilers_own_and_go_away_with_them() {
1187 let (opts, _) = compile(&["a.c"]);
1191 let dirs = opts.search.dirs();
1192 let ours = dirs.iter().position(|d| d.path.to_str() == Some(runtime::DIR));
1193 assert_eq!(ours, Some(0), "{dirs:?}");
1194 assert!(dirs[1..].iter().all(|d| d.is_system), "{dirs:?}");
1195 let (bare, _) = compile(&["-nostdinc", "a.c"]);
1196 assert!(bare.search.dirs().is_empty(), "{:?}", bare.search.dirs());
1197 }
1198
1199 #[test]
1200 fn a_sysroot_moves_the_librarys_directories_and_nothing_else() {
1201 let (opts, _) = compile(&["-isystem", "sys", "--sysroot=/nowhere-at-all", "a.c"]);
1202 let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
1203 assert_eq!(dirs, ["sys", runtime::DIR]);
1204 }
1205
1206 #[test]
1207 fn nostdinc_takes_the_compilers_own_headers_off_the_path() {
1208 let (opts, _) = compile(&["-Ii", "-nostdinc", "a.c"]);
1209 let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
1210 assert_eq!(dirs, ["i"]);
1211 }
1212
1213 #[test]
1214 fn the_dialect_flags_set_the_language_and_the_extensions_separately() {
1215 let (opts, _) = compile(&["-std=gnu11", "a.c"]);
1216 assert_eq!(opts.std, Std::C11);
1217 assert!(opts.gnu_extensions);
1218
1219 let (opts, _) = compile(&["-std=iso9899:1999", "a.c"]);
1220 assert_eq!(opts.std, Std::C99);
1221 assert!(!opts.gnu_extensions);
1222
1223 let (opts, _) = compile(&["-ansi", "a.c"]);
1224 assert_eq!(opts.std, Std::C89);
1225 assert!(!opts.gnu_extensions);
1226
1227 let e = parse_args(&args(&["-std=c94jr", "a.c"])).unwrap_err();
1228 assert!(e.message.contains("unknown dialect"), "{}", e.message);
1229 }
1230
1231 #[test]
1232 fn the_dump_letters_are_a_family_and_everything_else_beginning_with_d_is_not() {
1233 let (opts, _) = compile(&["-dM", "a.c"]);
1234 assert!(opts.dumps.macros);
1235
1236 let (opts, _) = compile(&["-dDM", "a.c"]);
1239 assert!(opts.dumps.macros);
1240 let (opts, _) = compile(&["-dD", "a.c"]);
1241 assert!(!opts.dumps.macros);
1242
1243 let (opts, _) = compile(&["a.c"]);
1244 assert!(!opts.dumps.any());
1245
1246 let e = parse_args(&args(&["-dumpversion", "a.c"])).unwrap_err();
1249 assert!(e.message.contains("unknown option"), "{}", e.message);
1250 }
1251
1252 #[test]
1253 fn the_gcc_version_claimed_is_a_flag_and_the_short_spellings_are_the_ones_people_write() {
1254 let (opts, _) = compile(&["a.c"]);
1255 assert_eq!(
1256 opts.gnuc,
1257 GnucVersion { major: 7, minor: 0, patch: 0 },
1258 "the lowest claim a modern glibc gives its own declarations to"
1259 );
1260
1261 let (opts, _) = compile(&["-fgnuc-version=15.1.0", "a.c"]);
1262 assert_eq!(opts.gnuc, GnucVersion { major: 15, minor: 1, patch: 0 });
1263
1264 let (opts, _) = compile(&["-fgnuc-version=15", "a.c"]);
1267 assert_eq!(opts.gnuc, GnucVersion { major: 15, minor: 0, patch: 0 });
1268
1269 let (opts, _) = compile(&["-fgnuc-version=13.2", "a.c"]);
1270 assert_eq!(opts.gnuc, GnucVersion { major: 13, minor: 2, patch: 0 });
1271
1272 let e = parse_args(&args(&["-fgnuc-version=15.x", "a.c"])).unwrap_err();
1273 assert!(e.message.contains("minor that is not a number"), "{}", e.message);
1274
1275 let e = parse_args(&args(&["-fgnuc-version=1.2.3.4", "a.c"])).unwrap_err();
1276 assert!(e.message.contains("more than three"), "{}", e.message);
1277 }
1278
1279 #[test]
1280 fn pedantic_has_two_spellings_and_is_not_the_same_knob_as_the_dialect() {
1281 let (opts, _) = compile(&["-std=c17", "-pedantic", "a.c"]);
1282 assert!(opts.pedantic);
1283 assert_eq!(opts.std, Std::C17);
1284
1285 let (opts, _) = compile(&["-Wpedantic", "a.c"]);
1288 assert!(opts.pedantic);
1289
1290 let (opts, _) = compile(&["-std=c17", "a.c"]);
1291 assert!(!opts.pedantic, "a dialect on its own does not diagnose an extension");
1292 }
1293
1294 #[test]
1295 fn dash_p_and_dash_ffreestanding_reach_the_options() {
1296 let (opts, _) = compile(&["-E", "-P", "-ffreestanding", "a.c"]);
1297 assert!(!opts.line_markers);
1298 assert!(!opts.hosted);
1299 assert_eq!(opts.emit, EmitKind::Preprocessed);
1300 }
1301
1302 #[test]
1305 fn the_two_frame_flags_are_read_in_both_directions() {
1306 let (opts, _) = compile(&["-c", "a.c"]);
1307 assert!(!opts.frame_pointer, "gcc omits it above -O0 and so does this");
1308 assert!(opts.red_zone, "the psABI has one and nothing said not to use it");
1309
1310 let (opts, _) = compile(&["-c", "-fno-omit-frame-pointer", "-mno-red-zone", "a.c"]);
1311 assert!(opts.frame_pointer);
1312 assert!(!opts.red_zone);
1313
1314 let (opts, _) = compile(&[
1315 "-c",
1316 "-fno-omit-frame-pointer",
1317 "-fomit-frame-pointer",
1318 "-mno-red-zone",
1319 "-mred-zone",
1320 "a.c",
1321 ]);
1322 assert!(!opts.frame_pointer, "the last one wins, as it does in gcc");
1323 assert!(opts.red_zone);
1324 }
1325
1326 #[test]
1327 fn the_link_flags_are_collected_apart_from_the_compilation() {
1328 let (link, _) = linking(&[
1329 "-static",
1330 "-nostartfiles",
1331 "-rdynamic",
1332 "-s",
1333 "-fuse-ld=mold",
1334 "-L/opt/lib",
1335 "-B",
1336 "/opt/tools",
1337 "a.c",
1338 ]);
1339 assert!(link.is_static);
1340 assert!(link.no_startfiles);
1341 assert!(link.export_dynamic);
1342 assert!(link.strip);
1343 assert_eq!(link.use_ld.as_deref(), Some("mold"));
1344 assert_eq!(link.search, vec![PathBuf::from("/opt/lib")]);
1345 assert_eq!(link.prefixes, vec![PathBuf::from("/opt/tools")]);
1346 }
1347
1348 #[test]
1349 fn a_comma_in_dash_wl_separates_two_arguments() {
1350 let (link, _) = linking(&["-Wl,-rpath,/opt/lib", "-Xlinker", "--as-needed", "a.c"]);
1351 assert_eq!(link.passthrough, vec!["-rpath", "/opt/lib", "--as-needed"]);
1352 }
1353
1354 #[test]
1355 fn a_library_keeps_its_place_between_the_objects() {
1356 let (_, plan) = linking(&["--target=x86_64-unknown-linux-gnu", "a.c", "-lm", "b.c"]);
1361 let link = plan.link.expect("expected a link step");
1362 assert_eq!(
1363 link.inputs,
1364 vec![
1365 link::Item::File("a.o".into()),
1366 link::Item::Library("m".into()),
1367 link::Item::File("b.o".into()),
1368 ]
1369 );
1370 assert_eq!(plan.jobs.len(), 2);
1372 }
1373
1374 #[test]
1375 fn a_library_on_a_dash_c_line_is_a_note_rather_than_an_error() {
1376 let (_, plan) = linking(&["-c", "-lm", "a.c"]);
1377 assert!(plan.link.is_none());
1378 assert!(plan.notes.iter().any(|n| n.contains("-lm")), "{:?}", plan.notes);
1379 }
1380
1381 #[test]
1382 fn the_sysroot_reaches_the_linker_as_well_as_the_headers() {
1383 let (link, _) = linking(&["--sysroot=/opt/root", "a.c"]);
1384 assert_eq!(link.sysroot, Some(PathBuf::from("/opt/root")));
1385 }
1386
1387 #[test]
1388 fn usage_fits_on_a_screen() {
1389 assert!(USAGE.lines().count() < 34, "usage text has grown past one screen");
1392 }
1393}