Skip to main content

rucc_driver/
lib.rs

1//! The driver: command line parsing, the phase graph, job scheduling and the linker
2//! invocation.
3//!
4//! Design: `spec/04-driver-and-cli.md`. Layer rank 12, see `spec/18-package-layout.md`.
5//!
6//! This is the only crate that is allowed to know the process exists. It reads the command
7//! line, touches the file system, spawns the linker and writes to the terminal, and it hands
8//! everything below it a [`Session`]. The binary crate is a `main` that calls
9//! [`run`] and nothing else, so that the whole driver is reachable from a test.
10//!
11//! # Status
12//!
13//! `--help`, `--version` and `--print-config` are real, which is the `M0` exit criterion in
14//! `spec/17-milestones.md`. The phase graph is real and `-###` prints it, and the scheduler
15//! that will run it is real and tested. Nothing runs the phases yet, and asking it to says
16//! so.
17//!
18//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
19//! explicitly unstable and will change without a major version bump.
20
21#![doc(html_root_url = "https://docs.rs/rucc-driver/0.1.0")]
22
23pub mod phase;
24pub mod schedule;
25
26use std::fmt::Write as _;
27use std::io::Write as _;
28
29use rucc_session::{EmitKind, Options, Session};
30use rucc_target::Triple;
31
32pub use crate::phase::{Input, InputKind, Job, LinkJob, Output, Phase, Plan};
33pub use crate::schedule::Jobs;
34
35/// The compiler's version, taken from the workspace manifest.
36pub const VERSION: &str = env!("CARGO_PKG_VERSION");
37
38/// What the command line asked for.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Action {
41    /// Print usage and exit successfully.
42    Help,
43    /// Print the version and exit successfully.
44    Version,
45    /// Print the resolved configuration and exit successfully.
46    PrintConfig(Box<Options>),
47    /// Print the phase plan and exit successfully, which is what `-###` asks for.
48    PrintPlan(Box<Plan>),
49    /// Compile the given inputs.
50    Compile {
51        /// The resolved options.
52        opts: Box<Options>,
53        /// What to do to each input, and in what order.
54        plan: Box<Plan>,
55        /// How many translation units to compile at once.
56        jobs: Jobs,
57        /// Whether `-v` asked for the plan to be printed while it runs.
58        verbose: bool,
59    },
60}
61
62/// Why a command line was rejected.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct CliError {
65    /// The message, lowercase and without a trailing period, in the same shape as any other
66    /// diagnostic.
67    pub message: String,
68}
69
70impl std::fmt::Display for CliError {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(&self.message)
73    }
74}
75
76impl std::error::Error for CliError {}
77
78fn err(message: impl Into<String>) -> CliError {
79    CliError { message: message.into() }
80}
81
82/// Usage text.
83///
84/// Deliberately short. `spec/04-driver-and-cli.md` puts the full flag reference in the
85/// manual page, because a `--help` nobody can read in one screen is a `--help` nobody reads.
86pub const USAGE: &str = "\
87rucc, an optimizing C compiler
88
89usage: rucc [options] file...
90
91options:
92  -c                     compile and assemble, do not link
93  -S                     compile only, emit assembly
94  -E                     preprocess only
95  -o <file>              write output to <file>
96  -x <lang>              treat later inputs as <lang>, or none to stop
97  -O<level>              optimize: 0, 1, 2, 3, s, z
98  -g                     emit debug information
99  -Werror                treat warnings as errors
100  -j[n]                  compile n translation units at once, default all
101  -v                     print each phase as it runs
102  -###                   print the phases without running any of them
103  --target=<triple>      generate code for <triple>
104  --emit=<kind>          exe, obj, asm, preprocessed, tast, ir, mir-final
105  --print-config         print the resolved configuration and exit
106  --version              print the version and exit
107  -h, --help             print this message and exit
108
109See spec/04-driver-and-cli.md for the full flag reference.
110";
111
112/// Parses a command line, without the program name.
113///
114/// # Errors
115///
116/// Returns the message to print when the arguments do not name a compilation this compiler
117/// can attempt.
118pub fn parse_args(args: &[String]) -> Result<Action, CliError> {
119    let host = Triple::host()
120        .ok_or_else(|| err("this host is not a supported target and no --target was given"))?;
121    let mut opts = Options::new(host);
122    let mut inputs: Vec<Input> = Vec::new();
123    let mut print_config = false;
124    let mut print_plan = false;
125    let mut verbose = false;
126    let mut jobs = Jobs::default();
127    let mut output = None;
128    // `-x` applies to inputs that come after it and stays in effect until the next one, which
129    // is why it is tracked across the loop rather than attached to a single argument.
130    let mut forced: Option<InputKind> = None;
131
132    let mut i = 0;
133    while i < args.len() {
134        let arg = args[i].as_str();
135        i += 1;
136        match arg {
137            "-h" | "--help" => return Ok(Action::Help),
138            "--version" => return Ok(Action::Version),
139            "--print-config" => print_config = true,
140            "-###" => print_plan = true,
141            "-v" => verbose = true,
142            "-c" => opts.emit = EmitKind::Object,
143            "-S" => opts.emit = EmitKind::Asm,
144            "-E" => opts.emit = EmitKind::Preprocessed,
145            "-g" => opts.debug_info = true,
146            "-Werror" => opts.warnings_are_errors = true,
147            "-o" => {
148                output = Some(args.get(i).ok_or_else(|| err("-o requires an argument"))?.clone());
149                i += 1;
150            }
151            "-x" => {
152                let lang = args.get(i).ok_or_else(|| err("-x requires an argument"))?;
153                i += 1;
154                forced = if lang == "none" {
155                    None
156                } else {
157                    Some(InputKind::from_x_arg(lang).map_err(|e| err(format!("{e}")))?)
158                };
159            }
160            // Not a GCC flag. spec/03-architecture.md section 3.5 compiles several
161            // translation units in one process rather than making the build system fork, and
162            // section 3.8's determinism check compares `-j1` against `-j16`, so the knob has
163            // to exist and has to be spelled the way `make` spells it.
164            _ if arg.starts_with("-j") => {
165                jobs = Jobs::parse(&arg[2..]).map_err(err)?;
166            }
167            _ if arg.starts_with("--target=") => {
168                let t = &arg["--target=".len()..];
169                opts.target = t.parse().map_err(|e| err(format!("{e}")))?;
170            }
171            _ if arg.starts_with("--emit=") => {
172                let k = &arg["--emit=".len()..];
173                opts.emit = k
174                    .parse()
175                    .map_err(|()| err(format!("unknown --emit kind `{k}`, see --help")))?;
176            }
177            _ if arg.starts_with("-O") => {
178                opts.opt_level = arg[2..]
179                    .parse()
180                    .map_err(|()| err(format!("unknown optimization level `{arg}`")))?;
181            }
182            _ if arg.starts_with('-') && arg.len() > 1 => {
183                // Silently ignoring an unknown flag is how a build ends up not doing what
184                // its author asked. spec/13-gnu-compat.md section 13.4 makes this an error
185                // for the flags that change code generation, and the safe default until the
186                // flag table is populated is to reject everything we do not know.
187                return Err(err(format!("unknown option `{arg}`")));
188            }
189            _ => inputs.push(Input { path: arg.to_owned(), forced }),
190        }
191    }
192
193    // The target has to be resolved before the configuration is printed, so this check comes
194    // after the loop rather than at the point `--print-config` was seen.
195    if print_config {
196        return Ok(Action::PrintConfig(Box::new(opts)));
197    }
198    let plan = Plan::new(&opts, &inputs, output.as_deref()).map_err(|e| err(e.message))?;
199    if print_plan {
200        return Ok(Action::PrintPlan(Box::new(plan)));
201    }
202    Ok(Action::Compile { opts: Box::new(opts), plan: Box::new(plan), jobs, verbose })
203}
204
205/// Renders the resolved configuration.
206///
207/// One `key: value` per line, sorted by nothing in particular but fixed in order, because
208/// this output is diffed across hosts in CI and a reordering would read as a change.
209#[must_use]
210pub fn print_config(opts: &Options) -> String {
211    let sess = Session::new(opts.clone());
212    let t = &sess.target;
213    let mut out = String::new();
214    let _ = writeln!(out, "version: {VERSION}");
215    let _ = writeln!(out, "target: {}", t.triple);
216    let _ = writeln!(out, "arch: {}", t.triple.arch.as_str());
217    let _ = writeln!(out, "os: {}", t.triple.os.as_str());
218    let _ = writeln!(out, "env: {}", t.triple.env.as_str());
219    let _ = writeln!(out, "object-format: {}", t.object_format.as_str());
220    let _ = writeln!(out, "pointer-width: {}", t.pointer_width);
221    let _ = writeln!(out, "long-width: {}", t.long_width);
222    let _ = writeln!(out, "long-double-width: {}", t.long_double_width);
223    let _ = writeln!(out, "endian: {}", if t.little_endian { "little" } else { "big" });
224    let _ = writeln!(out, "char-signed: {}", t.char_is_signed);
225    let _ = writeln!(out, "opt-level: {}", sess.opts.opt_level);
226    let _ = writeln!(out, "emit: {}", sess.opts.emit.as_str());
227    let _ = writeln!(out, "debug-info: {}", sess.opts.debug_info);
228    out
229}
230
231/// Runs the driver and returns the process exit code.
232///
233/// `args` excludes the program name. Output goes to `stdout` and errors to `stderr`, which
234/// is the one place in the compiler that is true.
235pub fn run(args: &[String]) -> i32 {
236    match parse_args(args) {
237        Ok(Action::Help) => {
238            print!("{USAGE}");
239            0
240        }
241        Ok(Action::Version) => {
242            println!("rucc {VERSION}");
243            0
244        }
245        Ok(Action::PrintConfig(opts)) => {
246            print!("{}", print_config(&opts));
247            0
248        }
249        Ok(Action::PrintPlan(plan)) => {
250            print!("{}", plan.render());
251            0
252        }
253        Ok(Action::Compile { plan, jobs, verbose, .. }) => {
254            let mut stderr = std::io::stderr().lock();
255            if verbose {
256                let _ = write!(stderr, "{}", plan.render());
257                let _ = writeln!(stderr, "workers: {}", jobs.count());
258            }
259            // M0 in spec/17-milestones.md is the skeleton and nothing more. The plan above is
260            // real and can be inspected with `-###`; running it is M1 through M3. Saying so
261            // is better than a panic, and better than pretending to have produced an object.
262            let _ = writeln!(
263                stderr,
264                "rucc: error: running the phases is not implemented yet; \
265                 use -### to see the plan, and see spec/17-milestones.md for when it runs"
266            );
267            1
268        }
269        Err(e) => {
270            let mut stderr = std::io::stderr().lock();
271            let _ = writeln!(stderr, "rucc: error: {e}");
272            let _ = writeln!(stderr, "rucc: note: run `rucc --help` for usage");
273            1
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use rucc_session::OptLevel;
281
282    use super::*;
283
284    fn args(s: &[&str]) -> Vec<String> {
285        s.iter().map(|x| (*x).to_owned()).collect()
286    }
287
288    #[test]
289    fn help_and_version_win_over_everything_else() {
290        assert_eq!(parse_args(&args(&["-c", "--help", "x.c"])).unwrap(), Action::Help);
291        assert_eq!(parse_args(&args(&["--version"])).unwrap(), Action::Version);
292    }
293
294    fn compile(s: &[&str]) -> (Box<Options>, Box<Plan>) {
295        match parse_args(&args(s)).expect("expected a compilation") {
296            Action::Compile { opts, plan, .. } => (opts, plan),
297            other => panic!("expected a compilation, got {other:?}"),
298        }
299    }
300
301    #[test]
302    fn collects_inputs_and_flags() {
303        let (opts, plan) = compile(&["-c", "-O2", "-g", "a.c", "b.c"]);
304        let paths: Vec<&str> = plan.jobs.iter().map(|j| j.input.as_str()).collect();
305        assert_eq!(paths, vec!["a.c", "b.c"]);
306        assert_eq!(opts.opt_level, OptLevel::O2);
307        assert_eq!(opts.emit, EmitKind::Object);
308        assert!(opts.debug_info);
309    }
310
311    #[test]
312    fn a_bare_dash_o_means_o1_the_way_gcc_reads_it() {
313        let (opts, _) = compile(&["-O", "a.c"]);
314        assert_eq!(opts.opt_level, OptLevel::O1);
315    }
316
317    #[test]
318    fn dash_x_applies_to_later_inputs_only_and_none_stops_it() {
319        let (_, plan) = compile(&["a.o", "-x", "c", "b.txt", "-x", "none", "c.o"]);
320        assert_eq!(plan.jobs[0].kind, InputKind::LinkerInput);
321        assert_eq!(plan.jobs[1].kind, InputKind::C);
322        assert_eq!(plan.jobs[2].kind, InputKind::LinkerInput);
323    }
324
325    #[test]
326    fn dash_j_reaches_the_scheduler_and_defaults_to_the_machine() {
327        let (_, _, jobs) = match parse_args(&args(&["-j4", "a.c"])).unwrap() {
328            Action::Compile { opts, plan, jobs, .. } => (opts, plan, jobs),
329            other => panic!("expected a compilation, got {other:?}"),
330        };
331        assert_eq!(jobs.count(), 4);
332
333        let default = match parse_args(&args(&["a.c"])).unwrap() {
334            Action::Compile { jobs, .. } => jobs,
335            other => panic!("expected a compilation, got {other:?}"),
336        };
337        assert_eq!(default, Jobs::available());
338        assert!(parse_args(&args(&["-j0", "a.c"])).is_err());
339    }
340
341    #[test]
342    fn triple_hash_prints_the_plan_and_runs_nothing() {
343        let a = parse_args(&args(&["-###", "-c", "a.c"])).unwrap();
344        let Action::PrintPlan(plan) = a else { panic!("expected a plan dump") };
345        assert!(plan.render().contains("a.c: preprocess, compile, assemble -> a.o"));
346    }
347
348    #[test]
349    fn dash_x_names_what_it_accepts_when_it_does_not_know_a_language() {
350        let e = parse_args(&args(&["-x", "fortran", "a.c"])).unwrap_err();
351        assert!(e.message.contains("assembler-with-cpp"), "{}", e.message);
352    }
353
354    #[test]
355    fn an_unknown_flag_is_an_error_rather_than_a_shrug() {
356        let e = parse_args(&args(&["-fno-such-thing", "a.c"])).unwrap_err();
357        assert!(e.message.contains("unknown option"), "{}", e.message);
358    }
359
360    #[test]
361    fn an_unsupported_target_names_itself() {
362        let e = parse_args(&args(&["--target=sparc64-linux-gnu", "a.c"])).unwrap_err();
363        assert!(e.message.contains("sparc64"), "{}", e.message);
364    }
365
366    #[test]
367    fn no_inputs_is_an_error_but_print_config_needs_none() {
368        assert!(parse_args(&args(&[])).is_err());
369        assert!(matches!(parse_args(&args(&["--print-config"])), Ok(Action::PrintConfig(_))));
370    }
371
372    #[test]
373    fn print_config_reports_the_target_it_was_given_not_the_host() {
374        let a = parse_args(&args(&["--print-config", "--target=riscv64-linux-musl"])).unwrap();
375        let Action::PrintConfig(opts) = a else { panic!("expected a configuration dump") };
376        let text = print_config(&opts);
377        assert!(text.contains("target: riscv64-unknown-linux-musl"), "{text}");
378        assert!(text.contains("char-signed: false"), "{text}");
379        assert!(text.contains("object-format: elf"), "{text}");
380    }
381
382    #[test]
383    fn print_config_has_one_key_per_line_and_a_fixed_order() {
384        let opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
385        let text = print_config(&opts);
386        let keys: Vec<&str> =
387            text.lines().map(|l| l.split(':').next().unwrap_or_default()).collect();
388        assert_eq!(keys[0], "version");
389        assert_eq!(keys[1], "target");
390        assert_eq!(keys.len(), 14);
391        assert!(text.ends_with('\n'));
392    }
393
394    #[test]
395    fn dash_o_needs_an_argument() {
396        let e = parse_args(&args(&["a.c", "-o"])).unwrap_err();
397        assert_eq!(e.message, "-o requires an argument");
398    }
399
400    #[test]
401    fn usage_fits_on_a_screen() {
402        // Not a style preference. A help text that scrolls is one nobody reads, and this is
403        // the cheapest way to keep it honest as flags accumulate.
404        assert!(USAGE.lines().count() < 30, "usage text has grown past one screen");
405    }
406}