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, SaveTemps};
14use rucc_target::{Env, Os};
15
16use crate::link::Item;
17
18/// A step in the compilation of one input.
19///
20/// The order of the variants is the order of the pipeline, and the derived `Ord` is relied on
21/// when a mode flag truncates a sequence. `Compile` covers parsing through code generation,
22/// which is one phase from the driver's point of view because nothing between them can be
23/// stopped at from the command line.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum Phase {
26    /// Translation phases 1 to 4, producing preprocessed source.
27    Preprocess,
28    /// Parse, check, optimize and generate code, producing assembly.
29    Compile,
30    /// Assemble, producing an object file.
31    Assemble,
32    /// Collect the objects into a static library.
33    ///
34    /// In front of the link rather than after it because the derived `Ord` is what truncates a
35    /// sequence, and every phase an archive needs is a phase a link needs too. No input's sequence
36    /// has this in it: an archive is one step for the whole command line, the way a link is, and
37    /// what each input contributes to it is an object.
38    Archive,
39    /// Link the objects into an executable or a shared library.
40    Link,
41}
42
43impl Phase {
44    /// The name used in `-###` output and in diagnostics.
45    #[must_use]
46    pub fn as_str(self) -> &'static str {
47        match self {
48            Phase::Preprocess => "preprocess",
49            Phase::Compile => "compile",
50            Phase::Assemble => "assemble",
51            Phase::Archive => "archive",
52            Phase::Link => "link",
53        }
54    }
55}
56
57impl std::fmt::Display for Phase {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.write_str(self.as_str())
60    }
61}
62
63/// What an input file is, which decides where in the pipeline it enters.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum InputKind {
66    /// C source. Extension `.c`, or `-x c`.
67    C,
68    /// A header compiled on its own. Extension `.h` with `-x c-header`, or `-x c-header`.
69    CHeader,
70    /// Already preprocessed C. Extension `.i`, or `-x cpp-output`.
71    PreprocessedC,
72    /// The IR this compiler prints. Extension `.ir`, or `-x ir`.
73    ///
74    /// Not a GCC input kind, because GCC has no textual IR. It is here because the IR's
75    /// printer and its parser are a pair, and a pair is only known to agree if something reads
76    /// back what was written: `rucc --emit=ir a.c -o a.ir` and then `rucc --emit=ir a.ir` are
77    /// two files a byte comparison has an opinion about, over whatever code is at hand rather
78    /// than over the modules a test happens to build.
79    Ir,
80    /// Assembly. Extension `.s`, or `-x assembler`.
81    Assembler,
82    /// Assembly that still needs the preprocessor. Extension `.S` or `.sx`, or
83    /// `-x assembler-with-cpp`.
84    AssemblerWithCpp,
85    /// An object file, an archive or a shared library. Anything the linker takes directly.
86    LinkerInput,
87}
88
89impl InputKind {
90    /// The name `-x` uses for this kind, where one exists.
91    #[must_use]
92    pub fn as_str(self) -> &'static str {
93        match self {
94            InputKind::C => "c",
95            InputKind::CHeader => "c-header",
96            InputKind::PreprocessedC => "cpp-output",
97            InputKind::Ir => "ir",
98            InputKind::Assembler => "assembler",
99            InputKind::AssemblerWithCpp => "assembler-with-cpp",
100            InputKind::LinkerInput => "linker-input",
101        }
102    }
103
104    /// Parses the argument of `-x`.
105    ///
106    /// # Errors
107    ///
108    /// Returns the offending name when it is not one we accept. C++ gets its own message,
109    /// because "unknown language c++" reads like an oversight and it is a decision.
110    pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
111        match name {
112            "c" => Ok(InputKind::C),
113            "c-header" => Ok(InputKind::CHeader),
114            "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
115            "ir" => Ok(InputKind::Ir),
116            "assembler" => Ok(InputKind::Assembler),
117            "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
118            "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
119                Err(XError::Unsupported(name.to_owned()))
120            }
121            _ => Err(XError::Unknown(name.to_owned())),
122        }
123    }
124
125    /// Classifies an input by its extension, the way `spec/04-driver-and-cli.md` section 4.2
126    /// tabulates it.
127    ///
128    /// An unrecognized extension is a linker input, which is GCC's behavior and is what makes
129    /// `rucc foo.o bar.builtin-suffix` work. The exception is a C++ extension, which is a
130    /// hard error rather than a confusing link failure later.
131    ///
132    /// # Errors
133    ///
134    /// Returns the extension when it names a language that is permanently out of scope.
135    pub fn from_path(path: &str) -> Result<InputKind, XError> {
136        let ext = extension(path);
137        match ext {
138            // Matched case-sensitively on purpose: `.S` and `.s` are different languages and
139            // conflating them is a real bug on case-insensitive file systems that GCC also
140            // has. The comment is here so the next person does not "fix" it.
141            "c" => Ok(InputKind::C),
142            "i" => Ok(InputKind::PreprocessedC),
143            "ir" => Ok(InputKind::Ir),
144            "h" => Ok(InputKind::CHeader),
145            "s" => Ok(InputKind::Assembler),
146            "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
147            "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
148                Err(XError::Unsupported(ext.to_owned()))
149            }
150            _ => Ok(InputKind::LinkerInput),
151        }
152    }
153
154    /// The full phase sequence for this kind, before any mode flag truncates it.
155    fn full_sequence(self) -> &'static [Phase] {
156        use Phase::{Assemble, Compile, Link, Preprocess};
157        match self {
158            InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
159            InputKind::PreprocessedC | InputKind::Ir => &[Compile, Assemble, Link],
160            // Note the gap: assembly with a preprocessor skips `Compile` entirely. This is why
161            // the sequence is a list rather than a range over the enum.
162            InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
163            InputKind::Assembler => &[Assemble, Link],
164            InputKind::LinkerInput => &[Link],
165        }
166    }
167}
168
169/// Why an input or an `-x` argument was rejected.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum XError {
172    /// A language we do not know at all.
173    Unknown(String),
174    /// A language we know and will not implement.
175    Unsupported(String),
176}
177
178impl std::fmt::Display for XError {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self {
181            XError::Unknown(name) => {
182                write!(
183                    f,
184                    "unknown language `{name}`; \
185                     accepted: c, c-header, cpp-output, ir, assembler, assembler-with-cpp, none"
186                )
187            }
188            XError::Unsupported(name) => {
189                write!(
190                    f,
191                    "`{name}` is not C, and this compiler is only ever going to compile C; \
192                     see the not-in-scope list in spec/00-README.md"
193                )
194            }
195        }
196    }
197}
198
199impl std::error::Error for XError {}
200
201/// What one entry of the input list was written as.
202///
203/// Three things share the list because all three are positional and the position is what they mean.
204/// An archive is searched for what is undefined at the moment the linker reaches it, so a library
205/// named before the object that needs it contributes nothing. A great many of the linker's own
206/// options are a bracket around the files after them, so an option moved away from what it brackets
207/// says nothing at all. Keeping a list of files, a list of libraries and a list of linker words
208/// apart would lose the one fact each of them depends on.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum Role {
211    /// A path, which is a file this command line may have something to do to.
212    File,
213    /// `-l<name>`, which the linker resolves against its search path.
214    Library,
215    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
216    Linker,
217}
218
219/// One input file, with the `-x` setting that was in effect where it appeared.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct Input {
222    /// The path as it was written on the command line, the name of a `-l` library, or one word
223    /// handed straight to the linker.
224    pub path: String,
225    /// The language forced by an earlier `-x`, if any. `-x none` clears it.
226    pub forced: Option<InputKind>,
227    /// Which of the three this is, and so what its position means.
228    pub role: Role,
229}
230
231impl Input {
232    /// An input with no `-x` in effect.
233    #[must_use]
234    pub fn new(path: impl Into<String>) -> Input {
235        Input { path: path.into(), forced: None, role: Role::File }
236    }
237
238    /// `-l<name>`, which is an input to the link and to nothing else.
239    #[must_use]
240    pub fn library(name: impl Into<String>) -> Input {
241        Input { path: name.into(), forced: None, role: Role::Library }
242    }
243
244    /// One word of `-Wl,` or `-Xlinker`, which is read by the linker and by nothing here.
245    #[must_use]
246    pub fn linker(arg: impl Into<String>) -> Input {
247        Input { path: arg.into(), forced: None, role: Role::Linker }
248    }
249
250    /// How this input is named in a message about it.
251    #[must_use]
252    pub fn named(&self) -> String {
253        match self.role {
254            Role::File => self.path.clone(),
255            Role::Library => format!("-l{}", self.path),
256            Role::Linker => format!("-Wl,{}", self.path),
257        }
258    }
259
260    /// What this input is, taking `-x` into account.
261    ///
262    /// # Errors
263    ///
264    /// Returns the extension when it names a language that is out of scope.
265    pub fn kind(&self) -> Result<InputKind, XError> {
266        if self.role != Role::File {
267            return Ok(InputKind::LinkerInput);
268        }
269        match self.forced {
270            Some(k) => Ok(k),
271            None => InputKind::from_path(&self.path),
272        }
273    }
274}
275
276/// Where the result of a job goes.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum Output {
279    /// Standard output, which is where `-E` writes when there is no `-o`.
280    Stdout,
281    /// A path the user can see and named, or that we derived from the input name.
282    File(String),
283    /// A file the link step consumes and nothing else ever sees. The name is a hint for
284    /// `-###` output; the real path is chosen in a temporary directory at execution time.
285    Temporary(String),
286}
287
288impl Output {
289    fn render(&self) -> String {
290        match self {
291            Output::Stdout => "-".to_owned(),
292            Output::File(p) => p.clone(),
293            Output::Temporary(p) => format!("{p} (temporary)"),
294        }
295    }
296
297    /// The path the link step reads, for an output that feeds it.
298    fn as_link_input(&self) -> Option<&str> {
299        match self {
300            Output::File(p) | Output::Temporary(p) => Some(p),
301            Output::Stdout => None,
302        }
303    }
304}
305
306/// Everything that has to happen to one input file.
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct Job {
309    /// The input path as written.
310    pub input: String,
311    /// What we decided it is.
312    pub kind: InputKind,
313    /// The phases to run, in order. Empty when the input goes straight to the linker.
314    pub phases: Vec<Phase>,
315    /// Where the last phase writes.
316    pub output: Output,
317    /// What the files `-save-temps` keeps are called, without the suffix that says which one it
318    /// is, or `None` when there is nothing to keep.
319    ///
320    /// Nothing to keep is the usual case: the flag was not given, or it was and this job has no
321    /// step whose result the compilation would have thrown away. `-E -save-temps` is the second
322    /// of those, since the preprocessed text is the output and is already being written.
323    pub aux_base: Option<String>,
324    /// Where `-fstack-usage` writes this job's report, or `None` when it was not asked for or the
325    /// job does not compile C.
326    ///
327    /// gcc's auxiliary base name with `.su` on the end, the same name `-save-temps` builds its
328    /// files from: `-c a.c -o out/x.o` writes `out/x.su`, a bare `-c sub/a.c` writes `a.su` in the
329    /// working directory, and `a.c` linked into `out/prog` writes `out/prog-a.su`. The rest of the
330    /// rules are on `aux_base` in this file.
331    pub stack_usage: Option<String>,
332}
333
334impl Job {
335    /// Where the preprocessed text goes when `-save-temps` asked for it to be kept.
336    ///
337    /// `None` when the flag was not given, when the input arrives preprocessed already and there
338    /// is no phase 4 to keep the result of, or when the text is this job's own output and is
339    /// being written anyway.
340    #[must_use]
341    pub fn saved_text(&self) -> Option<String> {
342        let base = self.aux_base.as_ref()?;
343        self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
344    }
345
346    /// Where the assembly goes when `-save-temps` asked for it to be kept.
347    ///
348    /// `None` for the same reasons, the last of them being `-S`: the assembly is the output
349    /// there, and a copy of it under a second name is a file nobody asked for.
350    #[must_use]
351    pub fn saved_asm(&self) -> Option<String> {
352        let base = self.aux_base.as_ref()?;
353        let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
354        (past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
355    }
356}
357
358/// The link step, when there is one.
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct LinkJob {
361    /// Objects and libraries, in command line order, because link order is semantic.
362    pub inputs: Vec<Item>,
363    /// The executable.
364    pub output: String,
365}
366
367/// The archive step, when there is one.
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct ArchiveJob {
370    /// What the members are called inside the archive, in command line order.
371    ///
372    /// Command line order is the order they are written in, which is the stated order
373    /// `spec/cross-compile/13-distribution.md` section 13.6 asks for: the caller chose it and the
374    /// same command line produces the same archive. The names are the object file names, because
375    /// that is what `ar t` over anybody else's archive shows.
376    pub members: Vec<String>,
377    /// The archive.
378    pub output: String,
379}
380
381/// The whole plan for one invocation.
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Plan {
384    /// One per input, in command line order.
385    pub jobs: Vec<Job>,
386    /// The link step, or `None` when a mode flag stopped short of it.
387    pub link: Option<LinkJob>,
388    /// The archive step, which is there only under `--emit=archive` and is never there beside a
389    /// link: one command line produces one of the two.
390    pub archive: Option<ArchiveJob>,
391    /// Things worth saying under `-v` that are not errors, such as an object file passed on a
392    /// command line that is not linking.
393    pub notes: Vec<String>,
394    /// The argument of `-o` as it was written, if it was given.
395    ///
396    /// Kept alongside the paths it produced because the `-M` family needs the name rather than
397    /// the path: a make rule whose target is the object the build asked for is one the build
398    /// can read back, and a rule naming a temporary directory is one nothing will ever match.
399    pub output: Option<String>,
400}
401
402/// Why a command line could not be turned into a plan.
403#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct PlanError {
405    /// Lowercase, no trailing period, the same shape as every other diagnostic.
406    pub message: String,
407}
408
409impl std::fmt::Display for PlanError {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        f.write_str(&self.message)
412    }
413}
414
415impl std::error::Error for PlanError {}
416
417fn plan_err(message: impl Into<String>) -> PlanError {
418    PlanError { message: message.into() }
419}
420
421/// The last phase that runs, given what the user asked to be emitted.
422///
423/// `--emit=tast` and the other intermediate dumps stop where `-S` stops, because they are
424/// produced inside the compile phase and there is nothing after them to run.
425#[must_use]
426pub fn last_phase(emit: EmitKind) -> Phase {
427    match emit {
428        EmitKind::Preprocessed => Phase::Preprocess,
429        EmitKind::Asm
430        | EmitKind::Tast
431        | EmitKind::Ir
432        | EmitKind::MirFinal
433        | EmitKind::SafetySummary
434        | EmitKind::TypeGranules
435        | EmitKind::SyntaxOnly => Phase::Compile,
436        EmitKind::Object => Phase::Assemble,
437        EmitKind::Archive => Phase::Archive,
438        EmitKind::Executable => Phase::Link,
439    }
440}
441
442/// What an input written on the command line is when it means standard input rather than a file.
443pub const STDIN: &str = "-";
444
445/// What a file reads as in a message, in a line marker and in `__FILE__`.
446///
447/// Standard input has no name, so it gets the one gcc gives it. A build that pipes a file in and
448/// then greps the output for a name is reading gcc's name for it, and `-` is not that name.
449#[must_use]
450pub fn source_name(path: &str) -> &str {
451    if path == STDIN { "<stdin>" } else { path }
452}
453
454/// The extension of a path, without the dot, or the empty string when there is none.
455fn extension(path: &str) -> &str {
456    let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
457    match name.rfind('.') {
458        // A leading dot is a hidden file, not an extension, and `.` and `..` are not inputs.
459        Some(0) | None => "",
460        Some(i) => &name[i + 1..],
461    }
462}
463
464/// The last component of a path, which is the whole of it when there is no directory in it.
465fn file_part(path: &str) -> &str {
466    path.rsplit(['/', '\\']).next().unwrap_or(path)
467}
468
469/// The path with its extension taken off and its directory left on.
470///
471/// This is what a name derived from `-o` is built on, since `-o out/a.o` puts the files that go
472/// beside the object in `out` and not in the working directory.
473fn without_extension(path: &str) -> &str {
474    let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
475    match path[start..].rfind('.') {
476        // A leading dot is a hidden file, not an extension, and `.` and `..` are not inputs.
477        Some(0) | None => path,
478        Some(i) => &path[..start + i],
479    }
480}
481
482/// The path without its extension, keeping any directory part off, because GCC writes the
483/// output into the current directory rather than next to the source.
484fn stem(path: &str) -> &str {
485    file_part(without_extension(path))
486}
487
488/// The name the files `-save-temps` and `-fstack-usage` keep beside an output are built from,
489/// without the suffix that says which one it is.
490///
491/// GCC calls this the auxiliary base name, and it is the name of the file the compilation
492/// produces with the extension taken off: `-c a.c -o out/a.o` keeps `out/a.i`, `out/a.s` and
493/// `out/a.su`. A command line that links has one output for however many inputs, so the input's
494/// own name goes on the end and `a.c` under `-o out/prog` becomes `out/prog-a`. The output keeps
495/// its extension there, since it is not the name of an object, and only an `.exe` comes off, so
496/// `-o lib.so` gives `lib.so-a`. A link with no `-o` and one input has nothing to tell apart and
497/// uses the input's name alone, and with more than one it uses `a-`, from `a.out`.
498/// `-save-temps=cwd` is the same name with the directory taken off, which is the only thing the
499/// two spellings disagree about.
500///
501/// `-dumpdir` replaces everything in front of the input's name, or of the output's in a compile
502/// that names one, and is a directory when it ends in a slash and the start of a name when it
503/// does not. `-dumpbase` replaces the name, still with the input's on the end when linking, and
504/// keeps the directory of the output or of `-dumpdir` unless it has one of its own.
505/// `-dumpbase-ext` is taken off the end of `-dumpbase`. Every case here was measured against gcc
506/// 16.
507///
508/// `collecting` is that one output, which is the link and the archive both, and `alone` is
509/// whether the command line has a single input. `output` is `None` for an `-o -`, which names no
510/// file.
511fn aux_base(
512    opts: &Options,
513    input: &str,
514    output: Option<&str>,
515    collecting: bool,
516    alone: bool,
517) -> String {
518    let own = stem(input);
519    let named = if let Some(base) = opts.dump_base.as_deref() {
520        let ext = opts.dump_base_ext.as_deref().filter(|ext| !ext.is_empty());
521        let stripped = ext.and_then(|ext| base.strip_suffix(ext)).filter(|b| !b.is_empty());
522        let base = stripped.unwrap_or(base);
523        let base = if collecting { format!("{base}-{own}") } else { base.to_owned() };
524        if base.contains(['/', '\\']) {
525            base
526        } else {
527            let dir = opts.dump_dir.as_deref().unwrap_or_else(|| output.map_or("", directory));
528            format!("{dir}{base}")
529        }
530    } else if let Some(dir) = &opts.dump_dir {
531        let name = match output {
532            Some(o) if !collecting => file_part(without_extension(o)),
533            _ => own,
534        };
535        format!("{dir}{name}")
536    } else if collecting {
537        match output {
538            Some(o) => format!("{}-{own}", o.strip_suffix(".exe").unwrap_or(o)),
539            None if alone => own.to_owned(),
540            None => format!("{}-{own}", stem(default_exe(opts))),
541        }
542    } else {
543        output.map_or(own, without_extension).to_owned()
544    };
545    if opts.save_temps == SaveTemps::Cwd { file_part(&named).to_owned() } else { named }
546}
547
548/// The directory part of a path with the slash that ends it, or nothing when there is none.
549fn directory(path: &str) -> &str {
550    path.rfind(['/', '\\']).map_or("", |i| &path[..=i])
551}
552
553/// The suffix a phase's output carries, for this target.
554fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
555    match phase {
556        Phase::Preprocess => "i",
557        // The compile phase is where every intermediate dump comes out, and each of them is a
558        // different language, so each gets a name of its own. `rucc --emit=tast a.c` writing
559        // `a.s` would be a file that neither an assembler nor a reader could make sense of.
560        Phase::Compile => match opts.emit {
561            EmitKind::Tast => "tast",
562            EmitKind::Ir => "ir",
563            EmitKind::MirFinal => "mir",
564            // Two extensions rather than one, because the content is JSON and a tool that reads
565            // JSON should be able to tell by looking, and because `a.json` next to `a.c` says
566            // nothing about which of a build's several JSON files it is.
567            EmitKind::SafetySummary => "safety.json",
568            // Two extensions for the same reason, and text rather than JSON because this one
569            // is read by a person once and not by a build every time.
570            EmitKind::TypeGranules => "granules.txt",
571            _ => "s",
572        },
573        // MSVC-targeted builds expect `.obj`, and build systems written for that target look
574        // for it by name.
575        Phase::Assemble => {
576            if opts.target.os == Os::Windows {
577                "obj"
578            } else {
579                "o"
580            }
581        }
582        // Neither of the last two is a suffix anything derives: both write one file for the whole
583        // command line and both take its name from `-o`, and the archive requires one.
584        Phase::Archive | Phase::Link => "",
585    }
586}
587
588/// The default name of the linked output, which is GCC's `a.out` everywhere but Windows.
589fn default_exe(opts: &Options) -> &'static str {
590    if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
591}
592
593/// What a link writes when `-o` named `output`, which on a MinGW target is the name with `.exe`
594/// on it when its last component has no extension at all.
595///
596/// That is MinGW gcc's rule, `convert_filename` in its driver, and clang's for the same target, so
597/// `-o foo` writes `foo.exe` and `-o foo.bin` and `-o foo.` are left alone. It applies to every
598/// link, `-shared` and `-r` included, which gcc does as well. Standard output and the bit bucket
599/// are not files to name. Both slashes end a directory, which is what they do where MinGW gcc runs.
600fn exe_name(opts: &Options, output: &str) -> String {
601    let mingw = opts.target.os == Os::Windows && opts.target.env == Env::Gnu;
602    let last = &output[directory(output).len()..];
603    if !mingw || last.contains('.') || output == STDIN || output == "/dev/null" {
604        return output.to_owned();
605    }
606    format!("{output}.exe")
607}
608
609impl Plan {
610    /// Builds the plan for one invocation.
611    ///
612    /// `output` is the argument of `-o`, if it was given.
613    ///
614    /// # Errors
615    ///
616    /// Returns a message when the inputs and the mode flags do not describe a compilation:
617    /// an out of scope language, `-o` naming one file for several outputs, or nothing to do.
618    pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
619        if inputs.is_empty() {
620            return Err(plan_err("no input files"));
621        }
622        let last = last_phase(opts.emit);
623        let linking = last == Phase::Link;
624        let archiving = last == Phase::Archive;
625        // An archive has no default name. `a.out` is a convention old enough that a build which
626        // gets one knows what happened, and a `libwhatever.a` this made up would be a file the
627        // build then fails to find under the name it asked for.
628        if archiving && output.is_none() {
629            return Err(plan_err("an archive has no default name, so `--emit=archive` needs `-o`"));
630        }
631        // One output for however many inputs, which both of the collecting steps are. What it
632        // decides below is that no input names the output and that the objects are temporary.
633        let collecting = linking || archiving;
634
635        let mut kinds = Vec::with_capacity(inputs.len());
636        for input in inputs {
637            // Standard input has no extension to read a language out of, so either `-x` said what
638            // it is or `-E` did, and gcc refuses the rest rather than guessing. The refusal is
639            // worth keeping: `rucc -` on a terminal with nothing else on the line would otherwise
640            // sit there waiting for a program to be typed at it.
641            if input.role == Role::File && input.path == STDIN && input.forced.is_none() {
642                if opts.emit != EmitKind::Preprocessed {
643                    return Err(plan_err("-E or -x required when input is from standard input"));
644                }
645                kinds.push(InputKind::C);
646                continue;
647            }
648            kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
649        }
650
651        // How many inputs actually write an output of their own. A `.o` on a `-c` line
652        // produces nothing, and neither does a `.s` on an `-E` line, so neither may count
653        // toward the `-o` check below. When linking there is exactly one output and it is the
654        // executable, so nothing counts.
655        let producing = if collecting {
656            0
657        } else {
658            kinds
659                .iter()
660                .filter(|k| **k != InputKind::LinkerInput)
661                .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
662                .count()
663        };
664        if output.is_some() && !collecting && producing > 1 {
665            return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
666        }
667
668        // Whether there is one input, which decides whether the files kept beside a link's output
669        // need the input's name on them to tell them apart. A library or a word for the linker is
670        // not an input here and an object is, which is how gcc counts.
671        let alone = inputs.iter().filter(|input| input.role == Role::File).count() == 1;
672
673        let mut notes = Vec::new();
674        let mut jobs = Vec::with_capacity(inputs.len());
675        let mut link_inputs = Vec::new();
676        let mut members = Vec::new();
677
678        for (input, kind) in inputs.iter().zip(kinds) {
679            // A word the user handed the linker is not a file and nothing here does anything to it,
680            // so it is neither a job nor something an archive could hold. It is in this list rather
681            // than beside the other link flags so that it reaches the linker among the files it was
682            // written among, which is the note on [`Role`]. On a command line that does not link it
683            // is dropped without a word, which is what GCC does with one.
684            if input.role == Role::Linker {
685                if linking {
686                    link_inputs.push(Item::Linker(input.path.clone()));
687                }
688                continue;
689            }
690
691            // An object, an archive or a shared library has nothing done to it. It reaches the
692            // linker under the name it was written with, and its name is not derived from
693            // anything, which is why this case is separate rather than falling out of the
694            // sequence below. Deriving it would rewrite `libm.a` into `libm.o`.
695            if kind == InputKind::LinkerInput {
696                // An object, an archive or a library on an archive command line is refused rather
697                // than noted and dropped. The symbol index is why: it needs the names each member
698                // defines, and this compiler knows those for a file it compiled and not for one it
699                // was handed, since nothing here reads an object file back. Carrying on and
700                // leaving the file out would be an archive that is quietly missing half of what
701                // was asked for, which a link finds out about much later.
702                if archiving {
703                    let named = input.named();
704                    return Err(plan_err(format!(
705                        "{named}: an archive is written from the objects this command line \
706                         compiles, and the symbol index in it needs the names each member \
707                         defines, which this compiler knows for a file it compiled and not for \
708                         one it was handed"
709                    )));
710                }
711                if linking {
712                    link_inputs.push(if input.role == Role::Library {
713                        Item::Library(input.path.clone())
714                    } else {
715                        Item::File(input.path.clone())
716                    });
717                } else {
718                    // GCC warns and carries on here, and configure scripts rely on that, so
719                    // this is a note rather than an error.
720                    notes.push(format!(
721                        "{}: linker input unused because linking was not requested",
722                        input.named()
723                    ));
724                }
725                // A library is not a file this compilation does anything to, so it gets no job.
726                // One would print a line under `-###` saying nothing happens to it, next to the
727                // note above already saying so.
728                if input.role == Role::Library {
729                    continue;
730                }
731                jobs.push(Job {
732                    input: input.path.clone(),
733                    kind,
734                    phases: Vec::new(),
735                    output: Output::File(input.path.clone()),
736                    aux_base: None,
737                    stack_usage: None,
738                });
739                continue;
740            }
741
742            let phases: Vec<Phase> =
743                kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
744            // `rucc -E a.s` lands here: assembly enters at `Assemble`, which is past where
745            // `-E` stops, so there is no phase left to run. GCC carries on rather than
746            // failing, and so do we.
747            let Some(&final_phase) = phases.last() else {
748                notes.push(format!(
749                    "{}: input unused because it enters the pipeline after the last phase \
750                     the mode flags asked for",
751                    input.path
752                ));
753                jobs.push(Job {
754                    input: input.path.clone(),
755                    kind,
756                    phases,
757                    output: Output::File(input.path.clone()),
758                    aux_base: None,
759                    stack_usage: None,
760                });
761                continue;
762            };
763            let named = if producing == 1 { output } else { None };
764            // A job that stops at the preprocessed text has nothing to keep, since that text is
765            // what it writes. Everything past it does: the text and, once there is a back end
766            // step after it, the assembly.
767            let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
768                .then(|| aux_base(opts, &input.path, output, collecting, alone));
769            let out = if final_phase == Phase::Link || archiving {
770                // The job stops at the object, and the step below takes it from here. Under
771                // `-save-temps` the object is one of the files being kept, so it is written where
772                // the person can see it rather than in a directory that goes away.
773                let ext = suffix_for(Phase::Assemble, opts);
774                match &aux {
775                    Some(base) => Output::File(format!("{base}.{ext}")),
776                    None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
777                }
778            } else if opts.emit == EmitKind::SyntaxOnly {
779                // Nothing is produced, so nothing is written, and an `-o` is ignored the way gcc
780                // ignores it. Standard output takes the empty artifact without leaving a file.
781                Output::Stdout
782            } else if let Some(o) = named {
783                // `-o -` is standard output rather than a file of that name, which is what gcc
784                // does for everything it compiles, the object file included. Its link step is
785                // the exception and writes a file called `-`, because the name goes to the
786                // linker and the linker takes it literally.
787                if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
788            } else if final_phase == Phase::Preprocess {
789                // `-E` writes to standard output unless it was given a name, which is the one
790                // place where the default is not a file.
791                Output::Stdout
792            } else {
793                Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
794            };
795            // An input whose output has the name it has itself would be read and then written
796            // over, and what it held would be gone. GCC compares the two names the way they
797            // were written and so does this, which catches `rucc --emit=ir a.ir` and leaves
798            // the same file reached by two different paths to the file system. `/dev/null` is
799            // left out, as gcc leaves out its bit bucket: nothing is lost by writing to it, and
800            // kbuild's cc-option asks whether a flag is taken by compiling `-xc /dev/null` with
801            // `-o /dev/null`, so refusing it refused every flag busybox probed for.
802            if let Output::File(path) = &out {
803                if *path == input.path && path != "/dev/null" {
804                    return Err(plan_err(format!(
805                        "input file `{}` is the same as the output file",
806                        input.path
807                    )));
808                }
809            }
810
811            if linking {
812                if let Some(p) = out.as_link_input() {
813                    link_inputs.push(Item::File(p.to_owned()));
814                }
815            }
816            if archiving {
817                // The name inside the archive rather than the path the object is written to, which
818                // under `-save-temps` is a path with a directory on it and otherwise is a
819                // temporary. What `ar t` shows is a file name, so that is what goes in.
820                members.push(format!(
821                    "{}.{}",
822                    stem(&input.path),
823                    suffix_for(Phase::Assemble, opts)
824                ));
825            }
826            // Only for a job that compiles C, since the numbers are the frames the back end laid
827            // out. An input of assembly has no compile phase and an input of IR is only ever
828            // printed back, so neither has a frame to report, and gcc writes nothing under `-E`.
829            // `-fsyntax-only` gets a file with nothing in it, which is what gcc gives it, named
830            // the way a link's would be since nothing on the command line said to stop at an
831            // object.
832            let syntax = opts.emit == EmitKind::SyntaxOnly;
833            let backed = syntax
834                || matches!(
835                    opts.emit,
836                    EmitKind::Asm | EmitKind::Object | EmitKind::Archive | EmitKind::Executable
837                );
838            let stack_usage = (opts.stack_usage
839                && backed
840                && kind != InputKind::Ir
841                && phases.contains(&Phase::Compile))
842            .then(|| {
843                let named = output.filter(|o| *o != "-");
844                format!("{}.su", aux_base(opts, &input.path, named, collecting || syntax, alone))
845            });
846            jobs.push(Job {
847                input: input.path.clone(),
848                kind,
849                phases,
850                output: out,
851                aux_base: aux,
852                stack_usage,
853            });
854        }
855
856        let link = linking.then(|| LinkJob {
857            inputs: link_inputs,
858            output: output.map_or_else(|| default_exe(opts).to_owned(), |o| exe_name(opts, o)),
859        });
860        let archive = archiving.then(|| ArchiveJob {
861            members,
862            // Checked at the top, so there is an `-o` here. Saying so is cheaper than an unwrap
863            // that would have to be explained.
864            output: output.unwrap_or_default().to_owned(),
865        });
866
867        Ok(Plan { jobs, link, archive, notes, output: output.map(str::to_owned) })
868    }
869
870    /// Renders the plan the way `-###` prints it.
871    ///
872    /// One line per job, then the link line. This is meant to be read next to `gcc -###`
873    /// output when a build behaves differently under the two compilers, so it says what will
874    /// happen rather than how it is represented.
875    #[must_use]
876    pub fn render(&self) -> String {
877        let mut out = String::new();
878        for note in &self.notes {
879            let _ = writeln!(out, "note: {note}");
880        }
881        for job in &self.jobs {
882            // A linker input has no phases of its own. It shows up in the link line below, or
883            // in a note above when there is no link line, and repeating it here would suggest
884            // something happens to it.
885            if job.phases.is_empty() {
886                continue;
887            }
888            let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
889            let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
890            // The files `-save-temps` keeps, which are as much a part of what will happen as the
891            // output is and are the only reason the flag was passed.
892            let kept: Vec<String> =
893                [job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
894            if !kept.is_empty() {
895                let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
896            }
897        }
898        if let Some(link) = &self.link {
899            let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
900            let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
901        }
902        if let Some(archive) = &self.archive {
903            let _ = writeln!(out, "archive: {} -> {}", archive.members.join(" "), archive.output);
904        }
905        out
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    use rucc_session::Options;
912
913    use super::*;
914
915    fn opts(triple: &str) -> Options {
916        Options::new(triple.parse().expect("test triple"))
917    }
918
919    fn linux() -> Options {
920        opts("x86_64-unknown-linux-gnu")
921    }
922
923    fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
924        let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
925        Plan::new(o, &inputs, output).expect("expected a plan")
926    }
927
928    #[test]
929    fn extensions_map_to_the_table_in_the_spec() {
930        assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
931        assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
932        assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
933        assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
934        assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
935        assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
936        assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
937        assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
938        assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
939        assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
940    }
941
942    #[test]
943    fn standard_input_is_c_under_dash_e_and_wants_telling_otherwise() {
944        // `$(CPP) $(CFLAGS) -` is how a generated header gets preprocessed in a Makefile, and
945        // there is no extension on `-` to read a language out of. Under `-E` gcc takes it as C
946        // and otherwise it refuses, which is the difference between a build that works and one
947        // that sits waiting for a program to be typed at a terminal.
948        let mut o = linux();
949        o.emit = EmitKind::Preprocessed;
950        let plan = plan(&o, &["-"], None);
951        assert_eq!(plan.jobs.len(), 1);
952        assert_eq!(plan.jobs[0].kind, InputKind::C);
953        assert_eq!(plan.jobs[0].phases, [Phase::Preprocess]);
954        assert_eq!(plan.jobs[0].output, Output::Stdout);
955
956        let o = linux();
957        let error = Plan::new(&o, &[Input::new("-")], None).expect_err("expected this refused");
958        assert_eq!(error.message, "-E or -x required when input is from standard input");
959
960        // `-x` is the other way of saying it, and then the whole pipeline runs and the object
961        // is named after the dash, which is the name gcc derives too.
962        let mut o = linux();
963        o.emit = EmitKind::Object;
964        let inputs = [Input { path: "-".to_owned(), forced: Some(InputKind::C), role: Role::File }];
965        let plan = Plan::new(&o, &inputs, None).expect("expected a plan");
966        assert_eq!(plan.jobs[0].kind, InputKind::C);
967        assert_eq!(plan.jobs[0].output, Output::File("-.o".to_owned()));
968    }
969
970    #[test]
971    fn standard_input_is_named_the_way_gcc_names_it_and_a_file_is_named_after_itself() {
972        assert_eq!(source_name("-"), "<stdin>");
973        assert_eq!(source_name("a.c"), "a.c");
974        assert_eq!(source_name("sub/-"), "sub/-");
975    }
976
977    #[test]
978    fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
979        // It is the compiler's own output coming back in, so the phases in front of the walk
980        // have already happened to it and the ones after it are the ones still to run.
981        assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
982        assert_eq!(InputKind::Ir.as_str(), "ir");
983        assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
984        assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
985    }
986
987    #[test]
988    fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
989        // `rucc --emit=ir a.ir` would read the file and then write the result over it, and
990        // what it held would be gone.
991        let mut o = linux();
992        o.emit = EmitKind::Ir;
993        let inputs = [Input::new("a.ir")];
994        let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
995        assert!(error.message.contains("is the same as the output file"), "{error}");
996        // Naming it something else is fine, and so is the same name reached through `-o`
997        // being refused for the same reason.
998        assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
999        assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
1000        // The bit bucket is not a file anything is kept in, which is how kbuild asks whether a
1001        // flag is taken.
1002        let null = Input { forced: Some(InputKind::C), ..Input::new("/dev/null") };
1003        assert!(Plan::new(&linux(), &[null], Some("/dev/null")).is_ok());
1004    }
1005
1006    #[test]
1007    fn capital_s_and_small_s_are_different_languages() {
1008        // On a case-insensitive file system it is tempting to fold these together. They are
1009        // not the same: one runs the preprocessor and one does not.
1010        let hi = InputKind::from_path("a.S").unwrap();
1011        let lo = InputKind::from_path("a.s").unwrap();
1012        assert_ne!(hi, lo);
1013        assert!(hi.full_sequence().contains(&Phase::Preprocess));
1014        assert!(!lo.full_sequence().contains(&Phase::Preprocess));
1015    }
1016
1017    #[test]
1018    fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
1019        let e = InputKind::from_path("a.cpp").unwrap_err();
1020        assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
1021        let e = InputKind::from_x_arg("c++").unwrap_err();
1022        assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
1023    }
1024
1025    #[test]
1026    fn a_file_with_no_extension_goes_to_the_linker() {
1027        assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
1028        assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
1029    }
1030
1031    #[test]
1032    fn the_default_line_compiles_and_links_to_a_out() {
1033        let p = plan(&linux(), &["a.c"], None);
1034        assert_eq!(
1035            p.jobs[0].phases,
1036            vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
1037        );
1038        assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
1039        let link = p.link.expect("expected a link step");
1040        assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
1041        assert_eq!(link.output, "a.out");
1042    }
1043
1044    #[test]
1045    fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
1046        let mut o = linux();
1047        o.emit = EmitKind::Object;
1048        let p = plan(&o, &["src/a.c", "src/b.c"], None);
1049        assert!(p.link.is_none());
1050        assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
1051        assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
1052        // Next to the source is what people expect and it is not what GCC does. The object
1053        // lands in the current directory.
1054        assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
1055    }
1056
1057    #[test]
1058    fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
1059        let mut o = linux();
1060        o.emit = EmitKind::Preprocessed;
1061        assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
1062        assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
1063    }
1064
1065    #[test]
1066    fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
1067        let mut o = linux();
1068        o.emit = EmitKind::Preprocessed;
1069        assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
1070        o.emit = EmitKind::Object;
1071        assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
1072        // The linker is handed the name and makes a file of it, which is gcc's behaviour and
1073        // is the one place the dash is not standard output.
1074        let p = plan(&linux(), &["a.c"], Some("-"));
1075        assert_eq!(p.link.expect("a link step").output, "-");
1076    }
1077
1078    #[test]
1079    fn dash_s_produces_assembly_named_after_the_source() {
1080        let mut o = linux();
1081        o.emit = EmitKind::Asm;
1082        let p = plan(&o, &["dir/a.c"], None);
1083        assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
1084        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
1085    }
1086
1087    #[test]
1088    fn an_already_preprocessed_file_skips_the_preprocessor() {
1089        let p = plan(&linux(), &["a.i"], None);
1090        assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
1091    }
1092
1093    #[test]
1094    fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
1095        let p = plan(&linux(), &["a.S"], None);
1096        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
1097        assert!(!p.jobs[0].phases.contains(&Phase::Compile));
1098    }
1099
1100    #[test]
1101    fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
1102        // Link order is semantic. A plan that reorders it is a plan that produces a different
1103        // program, and the failure would be a missing symbol nobody could explain.
1104        let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
1105        let link = p.link.expect("expected a link step");
1106        assert_eq!(
1107            link.inputs,
1108            vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
1109        );
1110    }
1111
1112    #[test]
1113    fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
1114        // Configure scripts do this. Erroring here fails builds that work under GCC.
1115        let mut o = linux();
1116        o.emit = EmitKind::Object;
1117        let p = plan(&o, &["a.c", "b.o"], None);
1118        assert!(p.jobs[1].phases.is_empty());
1119        assert_eq!(p.notes.len(), 1);
1120        assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
1121    }
1122
1123    #[test]
1124    fn dash_o_with_several_compilations_is_rejected() {
1125        let mut o = linux();
1126        o.emit = EmitKind::Object;
1127        let inputs = [Input::new("a.c"), Input::new("b.c")];
1128        let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
1129        assert!(e.message.contains("multiple inputs"), "{}", e.message);
1130    }
1131
1132    #[test]
1133    fn dash_o_with_one_compilation_and_some_objects_is_fine() {
1134        // `rucc -c -o out.o a.c b.o` has exactly one thing to write, so the check above must
1135        // not count the object.
1136        let mut o = linux();
1137        o.emit = EmitKind::Object;
1138        let inputs = [Input::new("a.c"), Input::new("b.o")];
1139        let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
1140        assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
1141    }
1142
1143    #[test]
1144    fn dash_x_overrides_the_extension() {
1145        let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), role: Role::File }];
1146        let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
1147        assert_eq!(p.jobs[0].kind, InputKind::C);
1148        assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
1149    }
1150
1151    #[test]
1152    fn windows_gets_obj_and_a_exe() {
1153        let o = opts("x86_64-pc-windows-msvc");
1154        let p = plan(&o, &["a.c"], None);
1155        assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
1156        assert_eq!(p.link.expect("expected a link step").output, "a.exe");
1157    }
1158
1159    #[test]
1160    fn a_mingw_link_puts_exe_on_a_name_with_no_extension() {
1161        // What x86_64-w64-mingw32-gcc does with each of these, measured, and clang does the same.
1162        // tamnd/rucc#2152.
1163        let exe = |triple: &str, output: &str| {
1164            plan(&opts(triple), &["a.c"], Some(output)).link.expect("expected a link step").output
1165        };
1166        let mingw = "x86_64-windows-gnu";
1167        assert_eq!(exe(mingw, "foo"), "foo.exe");
1168        assert_eq!(exe(mingw, "sub.d/foo"), "sub.d/foo.exe", "only the last component counts");
1169        assert_eq!(exe(mingw, r"sub.d\foo"), r"sub.d\foo.exe");
1170        assert_eq!(exe(mingw, "foo.exe"), "foo.exe");
1171        assert_eq!(exe(mingw, "a.b"), "a.b");
1172        assert_eq!(exe(mingw, "foo."), "foo.", "any dot at all is an extension to gcc");
1173        assert_eq!(exe(mingw, "-"), "-");
1174        assert_eq!(exe(mingw, "/dev/null"), "/dev/null");
1175        // Neither MSVC's linker nor anything but Windows does this.
1176        assert_eq!(exe("x86_64-pc-windows-msvc", "foo"), "foo");
1177        assert_eq!(exe("x86_64-unknown-linux-gnu", "foo"), "foo");
1178        // Nothing is linked, so the name is the object's or the assembly's as written.
1179        for emit in [EmitKind::Object, EmitKind::Asm] {
1180            let mut o = opts(mingw);
1181            o.emit = emit;
1182            assert_eq!(plan(&o, &["a.c"], Some("foo")).jobs[0].output, Output::File("foo".into()));
1183        }
1184    }
1185
1186    /// The archive is the link's shape rather than `-c`'s: one file out of however many inputs.
1187    #[test]
1188    fn an_archive_is_one_file_however_many_inputs_there_are() {
1189        let mut o = linux();
1190        o.emit = EmitKind::Archive;
1191        let p = plan(&o, &["a.c", "sub/b.c"], Some("out/libx.a"));
1192        assert_eq!(p.jobs.len(), 2);
1193        for job in &p.jobs {
1194            // Each input stops at its object, which is where `-c` stops, and the object is a file
1195            // nobody asked for and nobody sees: what was asked for is the archive.
1196            assert_eq!(job.phases.last(), Some(&Phase::Assemble), "{}", job.input);
1197            assert!(matches!(job.output, Output::Temporary(_)), "{:?}", job.output);
1198        }
1199        let archive = p.archive.expect("an archive step");
1200        assert_eq!(archive.members, ["a.o", "b.o"]);
1201        assert_eq!(archive.output, "out/libx.a");
1202        assert!(p.link.is_none(), "one command line produces one of the two and not both");
1203    }
1204
1205    #[test]
1206    fn a_member_is_called_what_an_object_is_called_on_this_target() {
1207        let mut o = opts("x86_64-pc-windows-msvc");
1208        o.emit = EmitKind::Archive;
1209        let p = plan(&o, &["a.c"], Some("x.lib"));
1210        assert_eq!(p.archive.expect("an archive step").members, ["a.obj"]);
1211    }
1212
1213    /// `a.out` is a convention old enough that a build which gets one knows what happened. A
1214    /// `libsomething.a` invented here would be a file the build then fails to find.
1215    #[test]
1216    fn an_archive_has_no_default_name() {
1217        let mut o = linux();
1218        o.emit = EmitKind::Archive;
1219        let inputs = [Input::new("a.c")];
1220        let error = Plan::new(&o, &inputs, None).expect_err("no name for the archive");
1221        assert!(error.message.contains("needs `-o`"), "{error}");
1222    }
1223
1224    /// An `ar` would take these. This cannot, and says so rather than writing an archive with
1225    /// half of what was asked for in it.
1226    #[test]
1227    fn something_this_compilation_did_not_produce_cannot_go_into_an_archive() {
1228        let mut o = linux();
1229        o.emit = EmitKind::Archive;
1230        for handed in [Input::new("b.o"), Input::library("m")] {
1231            let inputs = [Input::new("a.c"), handed];
1232            let error = Plan::new(&o, &inputs, Some("libx.a")).expect_err("not ours to index");
1233            assert!(error.message.contains("names each member"), "{error}");
1234        }
1235    }
1236
1237    #[test]
1238    fn the_plan_says_what_goes_into_the_archive() {
1239        let mut o = linux();
1240        o.emit = EmitKind::Archive;
1241        let text = plan(&o, &["a.c", "b.c"], Some("libx.a")).render();
1242        assert!(text.contains("archive: a.o b.o -> libx.a"), "{text}");
1243        assert!(!text.contains("link:"), "{text}");
1244    }
1245
1246    #[test]
1247    fn the_intermediate_dumps_stop_where_dash_s_stops() {
1248        for emit in [
1249            EmitKind::Tast,
1250            EmitKind::Ir,
1251            EmitKind::MirFinal,
1252            EmitKind::SafetySummary,
1253            EmitKind::TypeGranules,
1254        ] {
1255            assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
1256        }
1257    }
1258
1259    #[test]
1260    fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
1261        // They all come out of the compile phase and none of them is assembly, so writing any
1262        // of them to `a.s` would leave a file that neither an assembler nor a reader can use.
1263        for (emit, name) in [
1264            (EmitKind::Asm, "a.s"),
1265            (EmitKind::Tast, "a.tast"),
1266            (EmitKind::Ir, "a.ir"),
1267            (EmitKind::MirFinal, "a.mir"),
1268            (EmitKind::SafetySummary, "a.safety.json"),
1269            (EmitKind::TypeGranules, "a.granules.txt"),
1270        ] {
1271            let mut o = linux();
1272            o.emit = emit;
1273            assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
1274        }
1275    }
1276
1277    #[test]
1278    fn syntax_only_writes_no_file_even_when_given_a_name() {
1279        let mut o = linux();
1280        o.emit = EmitKind::SyntaxOnly;
1281        assert_eq!(last_phase(o.emit), Phase::Compile);
1282        let p = plan(&o, &["a.c", "b.c"], None);
1283        assert!(p.jobs.iter().all(|job| job.output == Output::Stdout), "{:?}", p.jobs);
1284        assert_eq!(plan(&o, &["a.c"], Some("a.o")).jobs[0].output, Output::Stdout);
1285        assert!(!plan(&o, &["a.c"], None).render().contains("link:"));
1286    }
1287
1288    #[test]
1289    fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
1290        // `rucc -E a.s` has nothing to preprocess. GCC carries on, and a configure script
1291        // that probes with a mixed input list depends on that.
1292        let mut o = linux();
1293        o.emit = EmitKind::Preprocessed;
1294        let p = plan(&o, &["a.c", "b.s"], None);
1295        assert!(p.jobs[1].phases.is_empty());
1296        assert_eq!(p.notes.len(), 1);
1297        assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
1298        // And it must not count against `-o`, because only one file is being written.
1299        let inputs = [Input::new("a.c"), Input::new("b.s")];
1300        assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
1301    }
1302
1303    #[test]
1304    fn no_inputs_is_an_error() {
1305        assert!(Plan::new(&linux(), &[], None).is_err());
1306    }
1307
1308    /// The plan for `paths` under `-save-temps` in the spelling `kind`.
1309    fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
1310        let mut o = linux();
1311        o.emit = emit;
1312        o.save_temps = kind;
1313        plan(&o, paths, output)
1314    }
1315
1316    /// What one job of that plan keeps, in the order the files are produced.
1317    fn kept(plan: &Plan, at: usize) -> Vec<String> {
1318        [plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
1319    }
1320
1321    #[test]
1322    fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
1323        // gcc 16's bare `-save-temps` is `-save-temps=obj`, whatever its manual says, so
1324        // `-o out/t.o` puts them in `out` and not in the working directory. Both spellings are
1325        // here because the whole of the difference between them is the directory.
1326        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
1327        assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
1328        let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
1329        assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1330    }
1331
1332    #[test]
1333    fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
1334        // `-o out/x.o` keeps `x.i` and not `t.i`, and an output with no extension on it keeps
1335        // the whole of the name it was given.
1336        let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
1337        assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
1338        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
1339        assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
1340    }
1341
1342    #[test]
1343    fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
1344        // The object of `sub/u.c` is `u.o` in the working directory, so what is kept beside it
1345        // is in the working directory as well, under both spellings.
1346        for kind in [SaveTemps::Object, SaveTemps::Cwd] {
1347            let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
1348            assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
1349            assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
1350        }
1351    }
1352
1353    #[test]
1354    fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
1355        // One output for however many inputs, so the input's own name goes on the end and two
1356        // files that would otherwise both be `prog.i` are two files.
1357        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
1358        assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1359        assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
1360        // With no `-o` the executable is `a.out`, and the `a` of it is what the files are named
1361        // from, which is where `a-t.i` comes from on a command line nobody wrote an `a` on. With
1362        // one input there is nothing to tell apart and gcc uses the input's name alone.
1363        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "u.c"], None);
1364        assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
1365        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
1366        assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1367        // The executable's name is not an object's, so it keeps its extension, all but `.exe`.
1368        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("o/p.so"));
1369        assert_eq!(kept(&p, 0), vec!["o/p.so-t.i", "o/p.so-t.s"]);
1370        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("o/p.exe"));
1371        assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1372    }
1373
1374    #[test]
1375    fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
1376        // Without the flag it goes in a directory that is gone by the end of the run, and that
1377        // is the one thing `-save-temps` cannot leave true: the object is one of the files it
1378        // was asked to keep.
1379        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
1380        assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
1381        let plain = plan(&linux(), &["t.c"], Some("out/prog"));
1382        assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
1383    }
1384
1385    #[test]
1386    fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
1387        // `-E` writes the preprocessed text, so there is nothing left over to keep, and `-S`
1388        // writes the assembly and keeps only the text that came before it.
1389        let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
1390        assert_eq!(p.jobs[0].aux_base, None);
1391        assert_eq!(kept(&p, 0), Vec::<String>::new());
1392        let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
1393        assert_eq!(kept(&p, 0), vec!["t.i"]);
1394    }
1395
1396    #[test]
1397    fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
1398        // There is no phase 4 to keep the result of, and the file the compilation read is the
1399        // one that would have been written, which is already on the disk under its own name.
1400        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1401        assert_eq!(kept(&p, 0), vec!["t.s"]);
1402        // And an input the linker takes directly goes through no step at all.
1403        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
1404        assert_eq!(p.jobs[0].aux_base, None);
1405    }
1406
1407    #[test]
1408    fn nothing_is_kept_when_the_flag_was_not_given() {
1409        let p = plan(&linux(), &["t.c"], None);
1410        assert_eq!(p.jobs[0].aux_base, None);
1411        assert_eq!(kept(&p, 0), Vec::<String>::new());
1412    }
1413
1414    #[test]
1415    fn the_rendering_says_what_will_happen() {
1416        let p = plan(&linux(), &["a.c", "b.o"], None);
1417        let text = p.render();
1418        assert!(
1419            text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
1420            "{text}"
1421        );
1422        assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
1423        // The object has nothing done to it, so it appears once, in the link line.
1424        assert_eq!(text.matches("b.o").count(), 1, "{text}");
1425    }
1426
1427    #[test]
1428    fn the_rendering_names_the_files_that_will_be_kept() {
1429        // `-###` is what will happen, and under `-save-temps` two more files being written is
1430        // part of that. It is also the only way to see the names without running a compilation.
1431        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c", "b.c"], None);
1432        let text = p.render();
1433        assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
1434        assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
1435    }
1436}