Skip to main content

rucc_driver/
phase.rs

1//! The phase graph: what has to happen to each input file, in what order, and where the
2//! result goes.
3//!
4//! Design: `spec/04-driver-and-cli.md` section 4.2.
5//!
6//! The plan is computed before anything runs and is a plain data structure with no side
7//! effects, which is what makes `-###` possible and what makes this testable without a file
8//! system. Nothing in here reads a file or spawns a process. Executing the plan is M3, when
9//! there is something for the phases to do.
10
11use std::fmt::Write as _;
12
13use rucc_session::{EmitKind, Options};
14use rucc_target::Os;
15
16/// A step in the compilation of one input.
17///
18/// The order of the variants is the order of the pipeline, and the derived `Ord` is relied on
19/// when a mode flag truncates a sequence. `Compile` covers parsing through code generation,
20/// which is one phase from the driver's point of view because nothing between them can be
21/// stopped at from the command line.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum Phase {
24    /// Translation phases 1 to 4, producing preprocessed source.
25    Preprocess,
26    /// Parse, check, optimize and generate code, producing assembly.
27    Compile,
28    /// Assemble, producing an object file.
29    Assemble,
30    /// Link the objects into an executable or a shared library.
31    Link,
32}
33
34impl Phase {
35    /// The name used in `-###` output and in diagnostics.
36    #[must_use]
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Phase::Preprocess => "preprocess",
40            Phase::Compile => "compile",
41            Phase::Assemble => "assemble",
42            Phase::Link => "link",
43        }
44    }
45}
46
47impl std::fmt::Display for Phase {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.write_str(self.as_str())
50    }
51}
52
53/// What an input file is, which decides where in the pipeline it enters.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum InputKind {
56    /// C source. Extension `.c`, or `-x c`.
57    C,
58    /// A header compiled on its own. Extension `.h` with `-x c-header`, or `-x c-header`.
59    CHeader,
60    /// Already preprocessed C. Extension `.i`, or `-x cpp-output`.
61    PreprocessedC,
62    /// Assembly. Extension `.s`, or `-x assembler`.
63    Assembler,
64    /// Assembly that still needs the preprocessor. Extension `.S` or `.sx`, or
65    /// `-x assembler-with-cpp`.
66    AssemblerWithCpp,
67    /// An object file, an archive or a shared library. Anything the linker takes directly.
68    LinkerInput,
69}
70
71impl InputKind {
72    /// The name `-x` uses for this kind, where one exists.
73    #[must_use]
74    pub fn as_str(self) -> &'static str {
75        match self {
76            InputKind::C => "c",
77            InputKind::CHeader => "c-header",
78            InputKind::PreprocessedC => "cpp-output",
79            InputKind::Assembler => "assembler",
80            InputKind::AssemblerWithCpp => "assembler-with-cpp",
81            InputKind::LinkerInput => "linker-input",
82        }
83    }
84
85    /// Parses the argument of `-x`.
86    ///
87    /// # Errors
88    ///
89    /// Returns the offending name when it is not one we accept. C++ gets its own message,
90    /// because "unknown language c++" reads like an oversight and it is a decision.
91    pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
92        match name {
93            "c" => Ok(InputKind::C),
94            "c-header" => Ok(InputKind::CHeader),
95            "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
96            "assembler" => Ok(InputKind::Assembler),
97            "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
98            "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
99                Err(XError::Unsupported(name.to_owned()))
100            }
101            _ => Err(XError::Unknown(name.to_owned())),
102        }
103    }
104
105    /// Classifies an input by its extension, the way `spec/04-driver-and-cli.md` section 4.2
106    /// tabulates it.
107    ///
108    /// An unrecognized extension is a linker input, which is GCC's behavior and is what makes
109    /// `rucc foo.o bar.builtin-suffix` work. The exception is a C++ extension, which is a
110    /// hard error rather than a confusing link failure later.
111    ///
112    /// # Errors
113    ///
114    /// Returns the extension when it names a language that is permanently out of scope.
115    pub fn from_path(path: &str) -> Result<InputKind, XError> {
116        let ext = extension(path);
117        match ext {
118            // Matched case-sensitively on purpose: `.S` and `.s` are different languages and
119            // conflating them is a real bug on case-insensitive file systems that GCC also
120            // has. The comment is here so the next person does not "fix" it.
121            "c" => Ok(InputKind::C),
122            "i" => Ok(InputKind::PreprocessedC),
123            "h" => Ok(InputKind::CHeader),
124            "s" => Ok(InputKind::Assembler),
125            "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
126            "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
127                Err(XError::Unsupported(ext.to_owned()))
128            }
129            _ => Ok(InputKind::LinkerInput),
130        }
131    }
132
133    /// The full phase sequence for this kind, before any mode flag truncates it.
134    fn full_sequence(self) -> &'static [Phase] {
135        use Phase::{Assemble, Compile, Link, Preprocess};
136        match self {
137            InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
138            InputKind::PreprocessedC => &[Compile, Assemble, Link],
139            // Note the gap: assembly with a preprocessor skips `Compile` entirely. This is why
140            // the sequence is a list rather than a range over the enum.
141            InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
142            InputKind::Assembler => &[Assemble, Link],
143            InputKind::LinkerInput => &[Link],
144        }
145    }
146}
147
148/// Why an input or an `-x` argument was rejected.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum XError {
151    /// A language we do not know at all.
152    Unknown(String),
153    /// A language we know and will not implement.
154    Unsupported(String),
155}
156
157impl std::fmt::Display for XError {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            XError::Unknown(name) => {
161                write!(
162                    f,
163                    "unknown language `{name}`; \
164                     accepted: c, c-header, cpp-output, assembler, assembler-with-cpp, none"
165                )
166            }
167            XError::Unsupported(name) => {
168                write!(
169                    f,
170                    "`{name}` is not C, and this compiler is only ever going to compile C; \
171                     see the not-in-scope list in spec/00-README.md"
172                )
173            }
174        }
175    }
176}
177
178impl std::error::Error for XError {}
179
180/// One input file, with the `-x` setting that was in effect where it appeared.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Input {
183    /// The path as it was written on the command line.
184    pub path: String,
185    /// The language forced by an earlier `-x`, if any. `-x none` clears it.
186    pub forced: Option<InputKind>,
187}
188
189impl Input {
190    /// An input with no `-x` in effect.
191    #[must_use]
192    pub fn new(path: impl Into<String>) -> Input {
193        Input { path: path.into(), forced: None }
194    }
195
196    /// What this input is, taking `-x` into account.
197    ///
198    /// # Errors
199    ///
200    /// Returns the extension when it names a language that is out of scope.
201    pub fn kind(&self) -> Result<InputKind, XError> {
202        match self.forced {
203            Some(k) => Ok(k),
204            None => InputKind::from_path(&self.path),
205        }
206    }
207}
208
209/// Where the result of a job goes.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum Output {
212    /// Standard output, which is where `-E` writes when there is no `-o`.
213    Stdout,
214    /// A path the user can see and named, or that we derived from the input name.
215    File(String),
216    /// A file the link step consumes and nothing else ever sees. The name is a hint for
217    /// `-###` output; the real path is chosen in a temporary directory at execution time.
218    Temporary(String),
219}
220
221impl Output {
222    fn render(&self) -> String {
223        match self {
224            Output::Stdout => "-".to_owned(),
225            Output::File(p) => p.clone(),
226            Output::Temporary(p) => format!("{p} (temporary)"),
227        }
228    }
229
230    /// The path the link step reads, for an output that feeds it.
231    fn as_link_input(&self) -> Option<&str> {
232        match self {
233            Output::File(p) | Output::Temporary(p) => Some(p),
234            Output::Stdout => None,
235        }
236    }
237}
238
239/// Everything that has to happen to one input file.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct Job {
242    /// The input path as written.
243    pub input: String,
244    /// What we decided it is.
245    pub kind: InputKind,
246    /// The phases to run, in order. Empty when the input goes straight to the linker.
247    pub phases: Vec<Phase>,
248    /// Where the last phase writes.
249    pub output: Output,
250}
251
252/// The link step, when there is one.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct LinkJob {
255    /// Objects and libraries, in command line order, because link order is semantic.
256    pub inputs: Vec<String>,
257    /// The executable.
258    pub output: String,
259}
260
261/// The whole plan for one invocation.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct Plan {
264    /// One per input, in command line order.
265    pub jobs: Vec<Job>,
266    /// The link step, or `None` when a mode flag stopped short of it.
267    pub link: Option<LinkJob>,
268    /// Things worth saying under `-v` that are not errors, such as an object file passed on a
269    /// command line that is not linking.
270    pub notes: Vec<String>,
271}
272
273/// Why a command line could not be turned into a plan.
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct PlanError {
276    /// Lowercase, no trailing period, the same shape as every other diagnostic.
277    pub message: String,
278}
279
280impl std::fmt::Display for PlanError {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        f.write_str(&self.message)
283    }
284}
285
286impl std::error::Error for PlanError {}
287
288fn plan_err(message: impl Into<String>) -> PlanError {
289    PlanError { message: message.into() }
290}
291
292/// The last phase that runs, given what the user asked to be emitted.
293///
294/// `--emit=tast` and the other intermediate dumps stop where `-S` stops, because they are
295/// produced inside the compile phase and there is nothing after them to run.
296#[must_use]
297pub fn last_phase(emit: EmitKind) -> Phase {
298    match emit {
299        EmitKind::Preprocessed => Phase::Preprocess,
300        EmitKind::Asm | EmitKind::Tast | EmitKind::Ir | EmitKind::MirFinal => Phase::Compile,
301        EmitKind::Object => Phase::Assemble,
302        EmitKind::Executable => Phase::Link,
303    }
304}
305
306/// The extension of a path, without the dot, or the empty string when there is none.
307fn extension(path: &str) -> &str {
308    let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
309    match name.rfind('.') {
310        // A leading dot is a hidden file, not an extension, and `.` and `..` are not inputs.
311        Some(0) | None => "",
312        Some(i) => &name[i + 1..],
313    }
314}
315
316/// The path without its extension, keeping any directory part off, because GCC writes the
317/// output into the current directory rather than next to the source.
318fn stem(path: &str) -> &str {
319    let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
320    match name.rfind('.') {
321        Some(0) | None => name,
322        Some(i) => &name[..i],
323    }
324}
325
326/// The suffix a phase's output carries, for this target.
327fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
328    match phase {
329        Phase::Preprocess => "i",
330        Phase::Compile => "s",
331        // MSVC-targeted builds expect `.obj`, and build systems written for that target look
332        // for it by name.
333        Phase::Assemble => {
334            if opts.target.os == Os::Windows {
335                "obj"
336            } else {
337                "o"
338            }
339        }
340        Phase::Link => "",
341    }
342}
343
344/// The default name of the linked output, which is GCC's `a.out` everywhere but Windows.
345fn default_exe(opts: &Options) -> &'static str {
346    if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
347}
348
349impl Plan {
350    /// Builds the plan for one invocation.
351    ///
352    /// `output` is the argument of `-o`, if it was given.
353    ///
354    /// # Errors
355    ///
356    /// Returns a message when the inputs and the mode flags do not describe a compilation:
357    /// an out of scope language, `-o` naming one file for several outputs, or nothing to do.
358    pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
359        if inputs.is_empty() {
360            return Err(plan_err("no input files"));
361        }
362        let last = last_phase(opts.emit);
363        let linking = last == Phase::Link;
364
365        let mut kinds = Vec::with_capacity(inputs.len());
366        for input in inputs {
367            kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
368        }
369
370        // How many inputs actually write an output of their own. A `.o` on a `-c` line
371        // produces nothing, and neither does a `.s` on an `-E` line, so neither may count
372        // toward the `-o` check below. When linking there is exactly one output and it is the
373        // executable, so nothing counts.
374        let producing = if linking {
375            0
376        } else {
377            kinds
378                .iter()
379                .filter(|k| **k != InputKind::LinkerInput)
380                .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
381                .count()
382        };
383        if output.is_some() && !linking && producing > 1 {
384            return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
385        }
386
387        let mut notes = Vec::new();
388        let mut jobs = Vec::with_capacity(inputs.len());
389        let mut link_inputs = Vec::new();
390
391        for (input, kind) in inputs.iter().zip(kinds) {
392            // An object, an archive or a shared library has nothing done to it. It reaches the
393            // linker under the name it was written with, and its name is not derived from
394            // anything, which is why this case is separate rather than falling out of the
395            // sequence below. Deriving it would rewrite `libm.a` into `libm.o`.
396            if kind == InputKind::LinkerInput {
397                if linking {
398                    link_inputs.push(input.path.clone());
399                } else {
400                    // GCC warns and carries on here, and configure scripts rely on that, so
401                    // this is a note rather than an error.
402                    notes.push(format!(
403                        "{}: linker input unused because linking was not requested",
404                        input.path
405                    ));
406                }
407                jobs.push(Job {
408                    input: input.path.clone(),
409                    kind,
410                    phases: Vec::new(),
411                    output: Output::File(input.path.clone()),
412                });
413                continue;
414            }
415
416            let phases: Vec<Phase> =
417                kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
418            // `rucc -E a.s` lands here: assembly enters at `Assemble`, which is past where
419            // `-E` stops, so there is no phase left to run. GCC carries on rather than
420            // failing, and so do we.
421            let Some(&final_phase) = phases.last() else {
422                notes.push(format!(
423                    "{}: input unused because it enters the pipeline after the last phase \
424                     the mode flags asked for",
425                    input.path
426                ));
427                jobs.push(Job {
428                    input: input.path.clone(),
429                    kind,
430                    phases,
431                    output: Output::File(input.path.clone()),
432                });
433                continue;
434            };
435            let named = if producing == 1 { output } else { None };
436            let out = if final_phase == Phase::Link {
437                // The job stops at the object, and the link step below takes it from here.
438                let ext = suffix_for(Phase::Assemble, opts);
439                Output::Temporary(format!("{}.{ext}", stem(&input.path)))
440            } else if let Some(o) = named {
441                Output::File(o.to_owned())
442            } else if final_phase == Phase::Preprocess {
443                // `-E` writes to standard output unless it was given a name, which is the one
444                // place where the default is not a file.
445                Output::Stdout
446            } else {
447                Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
448            };
449
450            if linking {
451                if let Some(p) = out.as_link_input() {
452                    link_inputs.push(p.to_owned());
453                }
454            }
455            jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
456        }
457
458        let link = linking.then(|| LinkJob {
459            inputs: link_inputs,
460            output: output.unwrap_or(default_exe(opts)).to_owned(),
461        });
462
463        Ok(Plan { jobs, link, notes })
464    }
465
466    /// Renders the plan the way `-###` prints it.
467    ///
468    /// One line per job, then the link line. This is meant to be read next to `gcc -###`
469    /// output when a build behaves differently under the two compilers, so it says what will
470    /// happen rather than how it is represented.
471    #[must_use]
472    pub fn render(&self) -> String {
473        let mut out = String::new();
474        for note in &self.notes {
475            let _ = writeln!(out, "note: {note}");
476        }
477        for job in &self.jobs {
478            // A linker input has no phases of its own. It shows up in the link line below, or
479            // in a note above when there is no link line, and repeating it here would suggest
480            // something happens to it.
481            if job.phases.is_empty() {
482                continue;
483            }
484            let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
485            let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
486        }
487        if let Some(link) = &self.link {
488            let _ = writeln!(out, "link: {} -> {}", link.inputs.join(" "), link.output);
489        }
490        out
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use rucc_session::Options;
497
498    use super::*;
499
500    fn opts(triple: &str) -> Options {
501        Options::new(triple.parse().expect("test triple"))
502    }
503
504    fn linux() -> Options {
505        opts("x86_64-unknown-linux-gnu")
506    }
507
508    fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
509        let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
510        Plan::new(o, &inputs, output).expect("expected a plan")
511    }
512
513    #[test]
514    fn extensions_map_to_the_table_in_the_spec() {
515        assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
516        assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
517        assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
518        assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
519        assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
520        assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
521        assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
522        assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
523        assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
524    }
525
526    #[test]
527    fn capital_s_and_small_s_are_different_languages() {
528        // On a case-insensitive file system it is tempting to fold these together. They are
529        // not the same: one runs the preprocessor and one does not.
530        let hi = InputKind::from_path("a.S").unwrap();
531        let lo = InputKind::from_path("a.s").unwrap();
532        assert_ne!(hi, lo);
533        assert!(hi.full_sequence().contains(&Phase::Preprocess));
534        assert!(!lo.full_sequence().contains(&Phase::Preprocess));
535    }
536
537    #[test]
538    fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
539        let e = InputKind::from_path("a.cpp").unwrap_err();
540        assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
541        let e = InputKind::from_x_arg("c++").unwrap_err();
542        assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
543    }
544
545    #[test]
546    fn a_file_with_no_extension_goes_to_the_linker() {
547        assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
548        assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
549    }
550
551    #[test]
552    fn the_default_line_compiles_and_links_to_a_out() {
553        let p = plan(&linux(), &["a.c"], None);
554        assert_eq!(
555            p.jobs[0].phases,
556            vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
557        );
558        assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
559        let link = p.link.expect("expected a link step");
560        assert_eq!(link.inputs, vec!["a.o"]);
561        assert_eq!(link.output, "a.out");
562    }
563
564    #[test]
565    fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
566        let mut o = linux();
567        o.emit = EmitKind::Object;
568        let p = plan(&o, &["src/a.c", "src/b.c"], None);
569        assert!(p.link.is_none());
570        assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
571        assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
572        // Next to the source is what people expect and it is not what GCC does. The object
573        // lands in the current directory.
574        assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
575    }
576
577    #[test]
578    fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
579        let mut o = linux();
580        o.emit = EmitKind::Preprocessed;
581        assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
582        assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
583    }
584
585    #[test]
586    fn dash_s_produces_assembly_named_after_the_source() {
587        let mut o = linux();
588        o.emit = EmitKind::Asm;
589        let p = plan(&o, &["dir/a.c"], None);
590        assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
591        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
592    }
593
594    #[test]
595    fn an_already_preprocessed_file_skips_the_preprocessor() {
596        let p = plan(&linux(), &["a.i"], None);
597        assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
598    }
599
600    #[test]
601    fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
602        let p = plan(&linux(), &["a.S"], None);
603        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
604        assert!(!p.jobs[0].phases.contains(&Phase::Compile));
605    }
606
607    #[test]
608    fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
609        // Link order is semantic. A plan that reorders it is a plan that produces a different
610        // program, and the failure would be a missing symbol nobody could explain.
611        let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
612        let link = p.link.expect("expected a link step");
613        assert_eq!(link.inputs, vec!["a.o", "b.o", "libm.a"]);
614    }
615
616    #[test]
617    fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
618        // Configure scripts do this. Erroring here fails builds that work under GCC.
619        let mut o = linux();
620        o.emit = EmitKind::Object;
621        let p = plan(&o, &["a.c", "b.o"], None);
622        assert!(p.jobs[1].phases.is_empty());
623        assert_eq!(p.notes.len(), 1);
624        assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
625    }
626
627    #[test]
628    fn dash_o_with_several_compilations_is_rejected() {
629        let mut o = linux();
630        o.emit = EmitKind::Object;
631        let inputs = [Input::new("a.c"), Input::new("b.c")];
632        let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
633        assert!(e.message.contains("multiple inputs"), "{}", e.message);
634    }
635
636    #[test]
637    fn dash_o_with_one_compilation_and_some_objects_is_fine() {
638        // `rucc -c -o out.o a.c b.o` has exactly one thing to write, so the check above must
639        // not count the object.
640        let mut o = linux();
641        o.emit = EmitKind::Object;
642        let inputs = [Input::new("a.c"), Input::new("b.o")];
643        let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
644        assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
645    }
646
647    #[test]
648    fn dash_x_overrides_the_extension() {
649        let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C) }];
650        let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
651        assert_eq!(p.jobs[0].kind, InputKind::C);
652        assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
653    }
654
655    #[test]
656    fn windows_gets_obj_and_a_exe() {
657        let o = opts("x86_64-pc-windows-msvc");
658        let p = plan(&o, &["a.c"], None);
659        assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
660        assert_eq!(p.link.expect("expected a link step").output, "a.exe");
661    }
662
663    #[test]
664    fn the_intermediate_dumps_stop_where_dash_s_stops() {
665        for emit in [EmitKind::Tast, EmitKind::Ir, EmitKind::MirFinal] {
666            assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
667        }
668    }
669
670    #[test]
671    fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
672        // `rucc -E a.s` has nothing to preprocess. GCC carries on, and a configure script
673        // that probes with a mixed input list depends on that.
674        let mut o = linux();
675        o.emit = EmitKind::Preprocessed;
676        let p = plan(&o, &["a.c", "b.s"], None);
677        assert!(p.jobs[1].phases.is_empty());
678        assert_eq!(p.notes.len(), 1);
679        assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
680        // And it must not count against `-o`, because only one file is being written.
681        let inputs = [Input::new("a.c"), Input::new("b.s")];
682        assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
683    }
684
685    #[test]
686    fn no_inputs_is_an_error() {
687        assert!(Plan::new(&linux(), &[], None).is_err());
688    }
689
690    #[test]
691    fn the_rendering_says_what_will_happen() {
692        let p = plan(&linux(), &["a.c", "b.o"], None);
693        let text = p.render();
694        assert!(
695            text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
696            "{text}"
697        );
698        assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
699        // The object has nothing done to it, so it appears once, in the link line.
700        assert_eq!(text.matches("b.o").count(), 1, "{text}");
701    }
702}