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